Skip to content

Commit 730fa51

Browse files
committed
docs: changes for scripts.
1 parent 0aca43b commit 730fa51

3 files changed

Lines changed: 304 additions & 0 deletions

File tree

.github/workflows/docs-qa.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Docs QA
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'docs/**'
7+
- '.github/workflows/docs-qa.yml'
8+
push:
9+
branches: [main]
10+
paths:
11+
- 'docs/**'
12+
13+
jobs:
14+
check-code-examples:
15+
name: Verify Code Examples
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install Zen C Compiler
21+
run: |
22+
git clone --depth 1 https://github.com/zenc-lang/zenc.git /tmp/zenc-build
23+
cd /tmp/zenc-build
24+
sudo apt-get update && sudo apt-get install -y libtcc-dev
25+
make -j$(nproc)
26+
sudo make install
27+
cd -
28+
29+
- name: Run Code Example Checker
30+
run: |
31+
python3 docs/scripts/check_doc_examples.py docs/reference/*.md docs/std/*.md
32+
33+
check-translations:
34+
name: Translation Coverage
35+
runs-on: ubuntu-latest
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Check Translation Gaps
40+
run: |
41+
python3 docs/scripts/check_translations.py docs/reference docs/std

scripts/check_doc_examples.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Extract Zen C code examples from markdown files and compile them.
4+
Reports any compilation errors without blocking deployment.
5+
6+
Usage:
7+
python3 check_doc_examples.py docs/reference/*.md docs/std/*.md
8+
"""
9+
10+
import os
11+
import re
12+
import subprocess
13+
import sys
14+
import tempfile
15+
import glob
16+
17+
ZC_BINARY = "./zc" # Path to the Zen C compiler
18+
19+
def find_zc():
20+
"""Find the zc binary in common locations."""
21+
candidates = [
22+
"./zc",
23+
"../zc",
24+
"/usr/local/bin/zc",
25+
os.path.expanduser("~/zc"),
26+
]
27+
for c in candidates:
28+
if os.path.isfile(c) and os.access(c, os.X_OK):
29+
return os.path.abspath(c)
30+
return None
31+
32+
def extract_code_blocks(filepath):
33+
"""Extract Zen C code blocks from a markdown file.
34+
Yields (lineno, code) tuples for each ```zc block found.
35+
"""
36+
zc_blocks = []
37+
with open(filepath, 'r', encoding='utf-8') as f:
38+
lines = f.readlines()
39+
40+
in_block = False
41+
block_start = 0
42+
code_lines = []
43+
44+
for i, line in enumerate(lines):
45+
stripped = line.strip()
46+
if stripped.startswith('```') and not in_block:
47+
lang = stripped[3:].strip()
48+
if lang in ('zc', 'zenc', ''):
49+
# Check if next lines look like Zen C
50+
in_block = True
51+
block_start = i + 2 # 1-indexed, 1-based
52+
code_lines = []
53+
elif stripped.startswith('```') and in_block:
54+
code = ''.join(code_lines)
55+
if code.strip():
56+
yield (block_start, code.strip())
57+
in_block = False
58+
code_lines = []
59+
elif in_block:
60+
code_lines.append(line)
61+
62+
def compile_code(code, file_hint="example"):
63+
"""Try to compile a Zen C code snippet.
64+
Returns (success, output) tuple.
65+
"""
66+
zc = find_zc()
67+
if not zc:
68+
return (False, "zc compiler not found")
69+
70+
# Wrap in a test if it looks like a snippet (no test/fn at top level)
71+
# Simple heuristic: if it doesn't have 'fn ' or 'test ' at top level, wrap it
72+
lines = code.strip().split('\n')
73+
has_fn = any(l.strip().startswith('fn ') for l in lines)
74+
has_test = any(l.strip().startswith('test ') for l in lines)
75+
has_import = any(l.strip().startswith('import ') for l in lines)
76+
77+
if not has_fn and not has_test and not has_import:
78+
# It's probably a loose expression or statement — wrap in a test
79+
code = f'test "doc_example" {{\n {code.strip()}\n}}'
80+
81+
with tempfile.TemporaryDirectory() as tmpdir:
82+
tmpfile = os.path.join(tmpdir, "example.zc")
83+
with open(tmpfile, 'w') as f:
84+
f.write(code)
85+
86+
outfile = os.path.join(tmpdir, "example")
87+
88+
try:
89+
result = subprocess.run(
90+
[zc, 'build', tmpfile, '-o', outfile],
91+
capture_output=True, text=False, timeout=30
92+
)
93+
stderr = result.stderr.decode('utf-8', errors='replace')
94+
if result.returncode != 0:
95+
if "error:" in stderr:
96+
return (False, stderr[:500])
97+
return (True, "")
98+
return (True, "")
99+
except subprocess.TimeoutExpired:
100+
return (False, "Compilation timed out (30s)")
101+
except FileNotFoundError:
102+
return (False, f"Compiler not found: {zc}")
103+
104+
def main():
105+
files = sys.argv[1:] if len(sys.argv) > 1 else []
106+
if not files:
107+
# Default: scan all reference and std docs
108+
script_dir = os.path.dirname(os.path.abspath(__file__))
109+
docs_dir = os.path.join(script_dir, '..')
110+
files = (glob.glob(os.path.join(docs_dir, 'reference/*.md')) +
111+
glob.glob(os.path.join(docs_dir, 'std/*.md')))
112+
# Filter to only English originals (skip translations)
113+
files = [f for f in files if not re.search(r'\.(de|es|it|pt|ru|zh-cn|zh-tw)\.md$', f)]
114+
115+
zc = find_zc()
116+
if not zc:
117+
print("::warning::zc compiler not found — skipping code verification")
118+
sys.exit(0)
119+
120+
total = 0
121+
failed = 0
122+
skipped = 0
123+
124+
for filepath in sorted(set(files)):
125+
if not os.path.isfile(filepath):
126+
continue
127+
for lineno, code in extract_code_blocks(filepath):
128+
total += 1
129+
# Skip very long blocks (likely full programs with imports)
130+
if len(code) > 2000:
131+
skipped += 1
132+
continue
133+
134+
success, output = compile_code(code, filepath)
135+
if not success:
136+
relpath = os.path.relpath(filepath)
137+
print(f"::error file={relpath},line={lineno}::Code example failed to compile")
138+
print(f" Code: {code[:100].strip()!r}...")
139+
print(f" Error: {output.strip()}")
140+
failed += 1
141+
142+
print(f"\nChecked {total} code blocks: {failed} failed, {skipped} skipped (too long)")
143+
if failed > 0:
144+
sys.exit(1)
145+
146+
if __name__ == "__main__":
147+
main()

scripts/check_translations.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Check translation coverage across all languages.
4+
Reports missing files and aims for consistency.
5+
6+
Usage:
7+
python3 scripts/check_translations.py docs/reference docs/std
8+
"""
9+
10+
import os
11+
import sys
12+
import re
13+
import glob
14+
from collections import defaultdict
15+
16+
LANGUAGES = ['en', 'de', 'es', 'it', 'pt', 'ru', 'zh-cn', 'zh-tw']
17+
LANG_NAMES = {
18+
'en': 'English', 'de': 'German', 'es': 'Spanish', 'it': 'Italian',
19+
'pt': 'Portuguese', 'ru': 'Russian', 'zh-cn': 'Chinese (Simplified)',
20+
'zh-tw': 'Chinese (Traditional)',
21+
}
22+
23+
def get_lang(filename):
24+
"""Extract language suffix from a filename."""
25+
base = os.path.basename(filename)
26+
for lang in LANGUAGES:
27+
if lang == 'en':
28+
# English files have no language suffix
29+
if not re.search(r'\.(de|es|it|pt|ru|zh-cn|zh-tw)\.', base):
30+
return 'en'
31+
else:
32+
if f'.{lang}.' in base:
33+
return lang
34+
return None
35+
36+
def analyze_directory(dirpath):
37+
"""Analyze a directory of translated documentation."""
38+
files = glob.glob(os.path.join(dirpath, '*.md'))
39+
40+
# Group by base name
41+
groups = defaultdict(dict)
42+
for f in files:
43+
base = os.path.basename(f)
44+
lang = get_lang(base)
45+
if lang:
46+
# Get the base name without language suffix
47+
if lang == 'en':
48+
stem = base
49+
else:
50+
stem = base.replace(f'.{lang}.md', '.md')
51+
groups[stem][lang] = f
52+
53+
return groups
54+
55+
def report_directory(dirpath, name):
56+
"""Report translation status for a directory."""
57+
groups = analyze_directory(dirpath)
58+
59+
total = len(groups)
60+
complete = 0
61+
missing_any = 0
62+
63+
for stem, langs in sorted(groups.items()):
64+
missing = [l for l in LANGUAGES if l not in langs]
65+
if not missing:
66+
complete += 1
67+
else:
68+
missing_any += 1
69+
if len(missing) <= 3: # Only report if few missing
70+
pass # Report below
71+
72+
print(f"\n## {name} ({total} documents)")
73+
print(f"| Status | Count |")
74+
print(f"|--------|-------|")
75+
print(f"| Fully translated | {complete} |")
76+
print(f"| Missing translations | {missing_any} |")
77+
print()
78+
79+
# Report specific gaps
80+
for stem, langs in sorted(groups.items()):
81+
missing = [l for l in LANGUAGES if l not in langs]
82+
if missing:
83+
english_file = langs.get('en', stem)
84+
print(f"❌ **{stem}**: missing {', '.join(LANG_NAMES[l] for l in missing)}")
85+
86+
# Report files that exist but weren't in the original English set
87+
en_files = {stem for stem, langs in groups.items() if 'en' in langs}
88+
non_en_only = {stem for stem, langs in groups.items() if 'en' not in langs}
89+
if non_en_only:
90+
for stem in sorted(non_en_only):
91+
langs_present = ', '.join(LANG_NAMES[l] for l in groups[stem])
92+
print(f"⚠️ **{stem}**: exists in {langs_present} but NOT in English")
93+
94+
def main():
95+
dirs = sys.argv[1:] if len(sys.argv) > 1 else []
96+
if not dirs:
97+
script_dir = os.path.dirname(os.path.abspath(__file__))
98+
docs_dir = os.path.join(script_dir, '..')
99+
dirs = [
100+
os.path.join(docs_dir, 'reference'),
101+
os.path.join(docs_dir, 'std'),
102+
]
103+
104+
print("# Translation Coverage Report\n")
105+
overall_complete = 0
106+
overall_total = 0
107+
108+
for d in dirs:
109+
if os.path.isdir(d):
110+
name = os.path.basename(d)
111+
report_directory(d, name)
112+
113+
print("\n---\n*Report generated by scripts/check_translations.py*")
114+
115+
if __name__ == "__main__":
116+
main()

0 commit comments

Comments
 (0)