Skip to content

Commit 540ec7a

Browse files
committed
perf: Rework extractor and fn signature parser
The extractor needed some fine tuning to only pick up top level docstrings instead of (possibly wrong) locally scoped docstrings outside the reach of library users. The function signature parser should now be a tad bit more efficient. It performs no operations on the whole array that gets passed beyond those strictly required by however as many elements (lines) are part of the function signature. Prior to this, there were a bunch of `enumerate` and `join` calls that wouldn't exactly be efficient for, say, arrays containing thousands of lines for some of the source code.
1 parent 0459295 commit 540ec7a

3 files changed

Lines changed: 138 additions & 124 deletions

File tree

docs/genhtml.py

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def generate_output_from_typst(code, output_path, format="svg", cetz_path=DEFAUL
3737
if result.returncode != 0:
3838
print(f"Error generating {format.upper()}: {result.stderr}")
3939
return False
40-
40+
4141
return True
4242
except Exception as e:
4343
print(f"Exception generating {format.upper()}: {e}")
@@ -58,7 +58,7 @@ def convert_typst_to_html_content(text):
5858
"""
5959
tmp.write(typst_content)
6060
tmp.flush()
61-
61+
6262
with tempfile.NamedTemporaryFile(suffix='.html', delete=True) as html_tmp:
6363
result = subprocess.run(
6464
["typst", "compile", tmp.name, html_tmp.name,
@@ -69,19 +69,19 @@ def convert_typst_to_html_content(text):
6969
if result.returncode != 0:
7070
print(f"Error generating HTML: {result.stderr}")
7171
return None
72-
72+
7373
# Read the generated HTML and extract body content
7474
with open(html_tmp.name, 'r') as f:
7575
html_content = f.read()
76-
76+
7777
# Extract content between <body> tags
7878
import re
7979
body_match = re.search(r'<body[^>]*>(.*?)</body>', html_content, re.DOTALL)
8080
if body_match:
8181
return body_match.group(1).strip()
8282
else:
8383
return html_content
84-
84+
8585
except Exception as e:
8686
print(f"Exception converting Typst to HTML: {e}")
8787
return None
@@ -91,14 +91,14 @@ def html_to_mdx(html_content):
9191
"""Convert HTML content to MDX-compatible format."""
9292
if not html_content:
9393
return ""
94-
94+
9595
# Clean up the HTML for MDX compatibility:
9696
# - Remove style attributes
9797
# - Convert self closing to closing
9898
html_content = re.sub(r'\s+style="[^"]*"', '', html_content)
9999
html_content = re.sub(r'<br>', '<br />', html_content)
100100
html_content = re.sub(r'<hr>', '<hr />', html_content)
101-
101+
102102
return html_content.strip()
103103

104104

@@ -125,10 +125,10 @@ def generate_mdx_file(func_data, output_path, cetz_path=DEFAULT_CETZ_VERSION):
125125
comment = func_data.get("comment", {})
126126
signature = func_data.get("signature", {})
127127
func_name = signature.get("name", "unknown") if signature else "unknown"
128-
128+
129129
# Start MDX content
130130
mdx_lines = []
131-
131+
132132
# Build parameters object for Function component
133133
parameters = {}
134134
arguments = comment.get("arguments", [])
@@ -142,24 +142,24 @@ def generate_mdx_file(func_data, output_path, cetz_path=DEFAULT_CETZ_VERSION):
142142
default_value = default_value[2:]
143143
param_info["default"] = default_value
144144
parameters[name] = param_info
145-
145+
146146
# Function component
147147
import json
148148
parameters_json = json.dumps(parameters)
149149
mdx_lines.append(f'<Function name="{func_name}" parameters={{{parameters_json}}}/>'.replace('null', 'undefined'))
150-
150+
151151
# Convert main text through Typst -> HTML -> MDX
152152
main_text = comment.get("text", "")
153153
if main_text:
154154
# Remove example blocks from main text for separate processing
155155
text_without_examples = re.sub(r'```(?:typc?\s+)?(?:example|example-vertical)\s*\n.*?```', '', main_text, flags=re.DOTALL)
156-
156+
157157
html_content = convert_typst_to_html_content(text_without_examples)
158158
if html_content:
159159
mdx_content = html_to_mdx(html_content)
160160
mdx_lines.append(mdx_content)
161161
mdx_lines.append('')
162-
162+
163163
# Add example blocks
164164
examples = extract_example_blocks(main_text)
165165
if examples:
@@ -168,15 +168,15 @@ def generate_mdx_file(func_data, output_path, cetz_path=DEFAULT_CETZ_VERSION):
168168
mdx_lines.append(example_code)
169169
mdx_lines.append('```')
170170
mdx_lines.append('')
171-
171+
172172
# Add parameter documentation
173173
if arguments:
174174
for arg in arguments:
175175
name = arg.get("name", "")
176176
types = format_types(arg.get("types", []))
177177
default_value = arg.get("default-value", "")
178178
text = arg.get("text", "")
179-
179+
180180
if name:
181181
param_attrs = [f'name="{name}"']
182182
if types:
@@ -185,19 +185,19 @@ def generate_mdx_file(func_data, output_path, cetz_path=DEFAULT_CETZ_VERSION):
185185
if default_value.startswith("= "):
186186
default_value = default_value[2:]
187187
param_attrs.append(f'default_value="{escape_html_attr(default_value)}"')
188-
188+
189189
mdx_lines.append(f'<Parameter {" ".join(param_attrs)}>')
190-
190+
191191
if text:
192192
# Convert parameter description through Typst -> HTML -> MDX
193193
html_content = convert_typst_to_html_content(text)
194194
if html_content:
195195
mdx_content = html_to_mdx(html_content)
196196
mdx_lines.append(mdx_content)
197-
197+
198198
mdx_lines.append('</Parameter>')
199199
mdx_lines.append('')
200-
200+
201201
try:
202202
with open(output_path, 'w') as f:
203203
f.write('\n'.join(mdx_lines))
@@ -211,33 +211,33 @@ def extract_example_blocks(text):
211211
"""Extract all example code blocks from text with proper indentation."""
212212
if not text:
213213
return []
214-
214+
215215
# Find all ```example or ```example-vertical blocks
216216
pattern = r'```(?:typc?\s+)?(?:example|example-vertical)\s*\n(.*?)```'
217217
matches = re.findall(pattern, text, re.DOTALL)
218-
218+
219219
cleaned_blocks = []
220220
for match in matches:
221221
lines = match.split('\n')
222222
if not lines:
223223
continue
224-
224+
225225
# Remove leading/trailing empty lines
226226
while lines and not lines[0].strip():
227227
lines.pop(0)
228228
while lines and not lines[-1].strip():
229229
lines.pop()
230-
230+
231231
if not lines:
232232
continue
233-
233+
234234
# Find the minimum indentation (excluding empty lines)
235235
min_indent = float('inf')
236236
for line in lines:
237237
if line.strip(): # Skip empty lines
238238
indent = len(line) - len(line.lstrip())
239239
min_indent = min(min_indent, indent)
240-
240+
241241
# Remove the common indentation from all lines
242242
if min_indent != float('inf') and min_indent > 0:
243243
dedented_lines = []
@@ -249,7 +249,7 @@ def extract_example_blocks(text):
249249
cleaned_blocks.append('\n'.join(dedented_lines))
250250
else:
251251
cleaned_blocks.append('\n'.join(lines))
252-
252+
253253
return cleaned_blocks
254254

255255

@@ -259,7 +259,7 @@ def main():
259259
description='Generate MDX documentation from CeTZ docs with optional SVG examples'
260260
)
261261
parser.add_argument(
262-
'json_file',
262+
'json_file',
263263
nargs='?',
264264
help='Path to docs.json file (reads from stdin if not provided)'
265265
)
@@ -281,16 +281,16 @@ def main():
281281
const='svg_output',
282282
help='Generate SVG files from examples. Optional: specify output directory (default: svg_output)'
283283
)
284-
284+
285285
args = parser.parse_args()
286-
286+
287287
# Load JSON data
288288
if args.json_file:
289289
json_file = Path(args.json_file)
290290
if not json_file.exists():
291291
print(f"Error: {json_file} not found.")
292292
sys.exit(1)
293-
293+
294294
with open(json_file, 'r') as f:
295295
data = json.load(f)
296296
else:
@@ -300,80 +300,80 @@ def main():
300300
except:
301301
print("Error: Could not read from stdin.")
302302
sys.exit(1)
303-
303+
304304
mdx_dir = Path(args.output)
305305
mdx_dir.mkdir(exist_ok=True)
306306
svg_dir = None
307307
if args.svg:
308308
svg_dir = Path(args.svg)
309309
svg_dir.mkdir(exist_ok=True)
310-
310+
311311
if isinstance(data, list) and len(data) > 0:
312312
data = data[0]
313-
313+
314314
# Process each file in the JSON
315315
for file_path, functions in data.items():
316316
print(f"\nProcessing {file_path}")
317-
317+
318318
# Create directory structure matching source
319319
# e.g., "src/draw/shapes.typ" -> "draw/shapes/"
320320
path_parts = file_path.replace("src/", "").replace(".typ", "").split("/")
321-
321+
322322
current_mdx_dir = mdx_dir
323323
for part in path_parts:
324324
current_mdx_dir = current_mdx_dir / part
325325
current_mdx_dir.mkdir(exist_ok=True)
326-
326+
327327
file_base = file_path.replace("src/", "").replace("/", "_").replace(".typ", "")
328-
328+
329329
# Track functions for combined file generation
330330
function_names = []
331331
for func_data in functions:
332332
signature = func_data.get("signature", {})
333333
func_name = signature.get("name", "unknown") if signature else "unknown"
334334
comment = func_data.get("comment", {})
335335
text = comment.get("text", "")
336-
336+
337337
if not func_name.startswith("_"):
338338
function_names.append(func_name)
339-
339+
340340
# Generate MDX files
341341
mdx_filename = f"{func_name}.mdx"
342342
mdx_path = current_mdx_dir / mdx_filename
343-
343+
344344
if generate_mdx_file(func_data, mdx_path, args.cetz):
345345
print(f"[ OK] Generated MDX: {'/'.join(path_parts)}/{mdx_filename}")
346346
else:
347347
print(f"[ERROR] Failed to generate MDX: {'/'.join(path_parts)}/{mdx_filename}")
348-
348+
349349
# Generate SVGs
350350
if svg_dir:
351351
examples = extract_example_blocks(text)
352-
352+
353353
if examples:
354354
for i, example_code in enumerate(examples):
355355
svg_filename = f"{file_base}_{func_name}_{i}.svg"
356356
svg_path = svg_dir / svg_filename
357-
357+
358358
if generate_output_from_typst(example_code, svg_path, "svg", args.cetz):
359359
print(f"[ OK] Generated SVG: {svg_filename} ({i+1}/{len(examples)})")
360360
else:
361361
print(f"[ERROR] Failed to generate SVG: {svg_filename}")
362-
362+
363363
# Generate combined MDX file
364364
if function_names:
365365
#combined_filename = f"{path_parts[-1]}-combined.mdx"
366366
combined_filename = f"-combined.mdx"
367367
combined_path = current_mdx_dir / combined_filename
368-
368+
369369
combined_imports = []
370370
combined_lines = []
371371
for func_name in function_names:
372372
import_name = func_name.replace("-", "").upper()
373373
combined_imports.append(f'import {import_name} from "./{func_name}.mdx"')
374374
combined_lines.append(f'## {func_name}')
375375
combined_lines.append(f'<{import_name}/>\n')
376-
376+
377377
try:
378378
with open(combined_path, 'w') as f:
379379
f.write('\n'.join(combined_imports) + '\n\n' + '\n'.join(combined_lines) + '\n')

docs/typlodocus/extractor.typ

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@
1414

1515
let i = 0
1616
while i < lines.len() {
17-
let line = lines.at(i).trim()
17+
let line = lines.at(i).trim(at: end)
1818

1919
if line.starts-with("///") {
2020
in-comment = true
21-
current-comment += line.slice(3) + "\n"
21+
current-comment += line.slice(3).trim() + "\n"
2222
} else if in-comment {
23-
if line.starts-with(regex("\#?let\s+")) {
23+
if line.starts-with(regex(`#let\s+`.text)) {
2424
let function = parser.parse-function-signature(lines.slice(i))
2525
comments.push((
2626
comment: parser.parse-docstring(current-comment),

0 commit comments

Comments
 (0)