@laitszkin/apollo-toolkit 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/AGENTS.md +3 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +4 -1
  4. package/archive-specs/SKILL.md +4 -0
  5. package/commit-and-push/SKILL.md +3 -0
  6. package/develop-new-features/README.md +1 -0
  7. package/develop-new-features/SKILL.md +13 -1
  8. package/develop-new-features/agents/openai.yaml +1 -1
  9. package/document-vision-reader/LICENSE +21 -0
  10. package/document-vision-reader/README.md +66 -0
  11. package/document-vision-reader/SKILL.md +151 -0
  12. package/document-vision-reader/agents/openai.yaml +4 -0
  13. package/document-vision-reader/references/legibility-checklist.md +13 -0
  14. package/document-vision-reader/references/rendering-guide.md +37 -0
  15. package/enhance-existing-features/README.md +1 -0
  16. package/enhance-existing-features/SKILL.md +15 -2
  17. package/enhance-existing-features/agents/openai.yaml +1 -1
  18. package/exam-pdf-workflow/LICENSE +21 -0
  19. package/exam-pdf-workflow/README.md +38 -0
  20. package/exam-pdf-workflow/SKILL.md +106 -0
  21. package/exam-pdf-workflow/agents/openai.yaml +4 -0
  22. package/generate-spec/SKILL.md +5 -0
  23. package/katex/SKILL.md +92 -0
  24. package/katex/agents/openai.yaml +4 -0
  25. package/katex/references/insertion-patterns.md +54 -0
  26. package/katex/references/official-docs.md +35 -0
  27. package/katex/scripts/render_katex.py +247 -0
  28. package/katex/scripts/render_katex.sh +11 -0
  29. package/learning-error-book/SKILL.md +46 -31
  30. package/learning-error-book/agents/openai.yaml +2 -2
  31. package/learning-error-book/assets/long_question_reference_template.json +57 -0
  32. package/learning-error-book/assets/mc_question_reference_template.json +49 -0
  33. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +590 -0
  34. package/package.json +1 -1
  35. package/learning-error-book/assets/error_book_template.md +0 -66
  36. package/learning-error-book/scripts/render_markdown_to_pdf.py +0 -367
@@ -0,0 +1,106 @@
1
+ ---
2
+ name: exam-pdf-workflow
3
+ description: Create, typeset, and grade exam-style PDF deliverables for study workflows. Use when the user asks to read lecture or exam PDFs, generate mock exams, produce matching solution books, render math-heavy content with KaTeX, or grade completed answer-book PDFs into a scored graded PDF.
4
+ ---
5
+
6
+ # Exam PDF Workflow
7
+
8
+ ## Dependencies
9
+
10
+ - Required: `pdf` for reading source PDFs, rendering deliverables, and final PDF QA.
11
+ - Conditional: `document-vision-reader` when handwritten answers, scanned pages, or layout-sensitive grading require visual inspection instead of raw text extraction.
12
+ - Conditional: `katex` when the output contains mathematical notation that should be rendered instead of left as raw TeX/plain text.
13
+ - Optional: none.
14
+ - Fallback: If `pdf` is unavailable, stop and report the blocked dependency instead of improvising a text-only workflow.
15
+
16
+ ## Standards
17
+
18
+ - Evidence: Read the actual source PDFs and answer-book pages first, and verify any referenced file path against the filesystem before acting.
19
+ - Execution: Decide the task mode first, extract the needed source content, produce the full deliverable set for that mode, then inspect rendered PDFs before finishing.
20
+ - Quality: Treat solution books, math rendering, layout cleanup, and graded-PDF output as default completion criteria when the task implies them; do not leave raw TeX, ambiguous scoring, or unverified PDF layout behind.
21
+ - Output: Save only the requested final PDFs in the target folder and report the exact output paths plus grading or coverage notes.
22
+
23
+ ## Overview
24
+
25
+ Use this skill for study-material workflows where PDFs are both the input and the final deliverable. The skill covers source reading, mock-exam authoring, solution generation, math typesetting, handwritten-answer grading, and graded-PDF production.
26
+
27
+ ## Workflow
28
+
29
+ ### 1) Confirm the real input/output paths
30
+
31
+ - Verify every user-mentioned PDF path on disk before reading it.
32
+ - If the named file is missing but a clearly matching nearby file exists, switch to the real file and state the exact path used.
33
+ - Confirm the target output folder before rendering; create it only when the task requires writing deliverables.
34
+
35
+ ### 2) Choose the task mode
36
+
37
+ Pick one primary mode, and combine modes when the user asks for a package:
38
+
39
+ - `source-summary`: read lecture slides, past papers, or error books and extract study notes.
40
+ - `mock-exam`: create a new exam paper modeled on source material or user weaknesses.
41
+ - `solution-book`: produce worked solutions for an exam paper.
42
+ - `grading`: mark a completed answer book and write scores back into a graded PDF.
43
+ - `typesetting-refresh`: improve layout, rebuild formulas with KaTeX, or clean up an existing PDF package.
44
+
45
+ ### 3) Read source materials first
46
+
47
+ - Use `pdf` to extract the source exam, lecture slides, marking scheme, error book, or answer book.
48
+ - When the source is handwritten, scanned, or visually complex, use `document-vision-reader` so judgments come from the rendered page rather than noisy OCR alone.
49
+ - Base the output on the actual source artifacts; do not invent syllabus scope, notation, or marking rules when the source does not support them.
50
+
51
+ ### 4) Produce the complete deliverable set for the chosen mode
52
+
53
+ #### `mock-exam`
54
+
55
+ - Build the paper around the user's weak topics or the patterns visible in the source materials.
56
+ - Match the source paper's structure, tone, and sectioning closely enough to feel familiar without copying it verbatim.
57
+ - Unless the user explicitly says otherwise, also produce a matching `solution-book` as part of the same task.
58
+
59
+ #### `solution-book`
60
+
61
+ - Give full working, not answer-only stubs.
62
+ - Keep notation and variable names aligned with the paired exam paper.
63
+ - Use concise marking-friendly steps so the solution can also act as the grading reference.
64
+
65
+ #### `grading`
66
+
67
+ - Establish or derive the answer key before assigning scores.
68
+ - Judge each question from the rendered student pages, not from OCR fragments alone when handwriting matters.
69
+ - Write per-question scores and the total score into a new graded PDF rather than only reporting marks in chat.
70
+ - If any page is unreadable or ambiguous, say so explicitly and grade conservatively with a short note.
71
+
72
+ #### `typesetting-refresh`
73
+
74
+ - Improve spacing, pagination, headings, and section breaks so the PDF reads like a clean exam handout.
75
+ - Rebuild all mathematical expressions through `katex` when formulas appear.
76
+ - Do not leave raw TeX, ASCII approximations, or partially rendered formulas in the final PDF.
77
+
78
+ ### 5) Render math correctly
79
+
80
+ - When the output includes formulas, invoke `katex` and render the expressions before PDF export.
81
+ - Use display math for multi-step derivations and dense formulas; use inline math only for short expressions.
82
+ - After rendering, visually verify representative pages so symbols, superscripts, fractions, and brackets display correctly.
83
+
84
+ ### 6) Run PDF visual QA before finishing
85
+
86
+ - Open the rendered PDFs and inspect representative pages.
87
+ - Always check:
88
+ - the first page
89
+ - one page with dense mathematics
90
+ - one page with longer prose or worked solutions
91
+ - any page with grading annotations when in `grading` mode
92
+ - Confirm that layout is readable, formulas are fully rendered, page breaks are sensible, and annotations are visible.
93
+ - Revise and re-render if math, spacing, or score overlays are unclear.
94
+
95
+ ## Default completion rules
96
+
97
+ - If the user asks for a mock exam, default to delivering both the exam PDF and its solution PDF.
98
+ - If the user asks to improve exam formatting and the paper contains formulas, default to KaTeX-rendered math.
99
+ - If the user asks for grading, default to a new graded PDF with visible marks plus a chat summary of per-question scores.
100
+ - If the task is read-only, do not modify files; provide notes only.
101
+
102
+ ## Output rules
103
+
104
+ - Keep filenames explicit and stable, for example `... Mock Test.pdf`, `... Solution.pdf`, or `..._graded.pdf`.
105
+ - Store outputs in the user-requested folder when one is given; otherwise reuse the source document's nearby study-material folder.
106
+ - Report the exact final PDF paths and any unresolved ambiguity, such as unreadable handwriting or missing marking guidance.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Exam PDF Workflow"
3
+ short_description: "Create, typeset, and grade exam PDFs with math rendering."
4
+ default_prompt: "Use $exam-pdf-workflow to read exam PDFs, generate mock exams and worked solutions, render math with KaTeX, or grade completed answer-book PDFs into a graded PDF."
@@ -30,6 +30,9 @@ Own the shared planning-doc lifecycle for feature work so other skills can reuse
30
30
  - Confirm the target workspace root, feature name, and a kebab-case `change_name`.
31
31
  - Review only the code, configuration, and external docs needed to understand the requested behavior.
32
32
  - Record the references that should appear in `spec.md`.
33
+ - Inspect existing `docs/plans/` directories before deciding whether to edit an existing plan set.
34
+ - Reuse an existing plan set only when it clearly matches the same requested issue/change scope.
35
+ - If the requested work is adjacent to, but not actually covered by, an existing plan set, create a new directory instead of overwriting the neighboring one.
33
36
 
34
37
  ### 2) Generate the planning files
35
38
 
@@ -73,6 +76,7 @@ Own the shared planning-doc lifecycle for feature work so other skills can reuse
73
76
  - Review whether `spec.md`, `tasks.md`, and `checklist.md` must be updated.
74
77
  - After any clarification-driven update, obtain explicit approval on the updated spec set again.
75
78
  - Do not modify implementation code before approval.
79
+ - When clarification reveals the work is a different issue or materially different scope than the current plan set, stop editing that plan set and generate a new one with a distinct `change_name`.
76
80
 
77
81
  ### 7) Backfill after implementation and testing
78
82
 
@@ -87,6 +91,7 @@ Own the shared planning-doc lifecycle for feature work so other skills can reuse
87
91
  - Prefer realistic coverage over boilerplate: add or remove checklist items based on actual risk.
88
92
  - Use kebab-case for `change_name`; avoid spaces and special characters.
89
93
  - Path rule: `scripts/...` and `references/...` in this file always mean paths under the current skill folder, not the target project root.
94
+ - Never overwrite a nearby issue's plan set just because the technical area overlaps; shared modules are not sufficient evidence that the scope is the same.
90
95
 
91
96
  ## References
92
97
 
package/katex/SKILL.md ADDED
@@ -0,0 +1,92 @@
1
+ ---
2
+ name: katex
3
+ description: Render and embed math formulas with KaTeX using official documentation-backed patterns. Use when an agent needs inline or display math in HTML, Markdown, MDX, or other text-based outputs, or when it should generate insertion-ready KaTeX snippets from TeX.
4
+ ---
5
+
6
+ # KaTeX
7
+
8
+ ## Dependencies
9
+
10
+ - Required: none.
11
+ - Conditional: none.
12
+ - Optional: none.
13
+ - Fallback: If `node`, `npx`, or network package download is unavailable, keep the source TeX unchanged and explain that KaTeX rendering could not be generated locally.
14
+
15
+ ## Standards
16
+
17
+ - Evidence: Follow the official KaTeX docs in `references/official-docs.md` before choosing render mode, options, or insertion strategy.
18
+ - Execution: Decide between pre-rendering and client-side auto-render first, then use `scripts/render_katex.py` for deterministic output whenever a static rendered snippet is enough.
19
+ - Quality: Default to `htmlAndMathml`, keep `trust` disabled unless the content source is explicitly trusted, and include the KaTeX stylesheet whenever the output will be displayed in HTML.
20
+ - Output: Return insertion-ready content plus the exact CSS or runtime requirements still needed by the target file.
21
+
22
+ ## Goal
23
+
24
+ Use KaTeX safely and consistently so mathematical formulas can be inserted into documents without the agent re-deriving the rendering workflow each time.
25
+
26
+ ## Workflow
27
+
28
+ ### 1) Confirm the target surface
29
+
30
+ - Identify the destination file type before rendering: static HTML, Markdown/MDX, generated docs, or an existing HTML page that already contains TeX delimiters.
31
+ - Decide whether the destination can keep raw HTML. If it cannot, keep the TeX source and document the runtime rendering requirement instead of forcing a broken snippet.
32
+
33
+ ### 2) Pick the right rendering strategy
34
+
35
+ - Use **pre-rendering** when the agent needs a stable snippet to paste into HTML, Markdown, MDX, templates, or generated artifacts.
36
+ - Use **auto-render** only when the document should keep TeX delimiters in source and render in the browser at runtime.
37
+ - Prefer pre-rendering for one-off formulas and exported documents. Prefer auto-render when the file intentionally stores author-friendly TeX like `$...$` and `$$...$$`.
38
+
39
+ ### 3) Render static output with the bundled script
40
+
41
+ Run the bundled renderer for pre-rendered output:
42
+
43
+ ```bash
44
+ python3 katex/scripts/render_katex.py \
45
+ --tex '\int_0^1 x^2 \\, dx = \\frac{1}{3}' \
46
+ --display-mode \
47
+ --output-format html-fragment
48
+ ```
49
+
50
+ Useful output modes:
51
+
52
+ - `html-fragment`: paste into HTML, MDX, JSX, or Markdown engines that allow raw HTML.
53
+ - `html-page`: generate a standalone previewable HTML file with the KaTeX stylesheet link included.
54
+ - `markdown-inline`: emit an inline-friendly HTML fragment for Markdown/MDX.
55
+ - `markdown-block`: emit a block-friendly HTML fragment for Markdown/MDX.
56
+ - `json`: emit machine-friendly JSON for downstream automation or templating.
57
+
58
+ Useful options:
59
+
60
+ - `--display-mode` for centered display math; omit for inline math.
61
+ - `--katex-format htmlAndMathml` by default for accessibility; switch only when the destination needs HTML-only or MathML-only output.
62
+ - `--macro` / `--macro-file` for reusable custom macros.
63
+ - `--strict`, `--trust`, `--max-size`, `--max-expand`, `--error-color`, and `--no-throw-on-error` when the task needs explicit control from the official options reference.
64
+
65
+ ### 4) Insert output according to file type
66
+
67
+ - **HTML / template files**: paste `html-fragment` output and ensure the page loads the KaTeX stylesheet.
68
+ - **Standalone HTML exports**: use `html-page` when the user wants a ready-to-open preview file.
69
+ - **Markdown / MDX**: use `markdown-inline` or `markdown-block` only if the target renderer preserves raw HTML; otherwise keep source TeX and document the runtime render requirement.
70
+ - **Existing browser pages with raw delimiters**: keep the original TeX and wire in the official auto-render assets instead of pasting a pre-rendered snippet.
71
+
72
+ See `references/insertion-patterns.md` for concrete insertion guidance.
73
+
74
+ ### 5) Apply the official runtime requirements
75
+
76
+ - KaTeX-rendered HTML needs the KaTeX CSS to display correctly.
77
+ - Official docs recommend using the HTML5 doctype so browser quirks mode does not distort layout.
78
+ - For browser auto-render, load `katex.min.js`, `auto-render.min.js`, the stylesheet, then call `renderMathInElement(...)` after the DOM is ready.
79
+
80
+ ### 6) Keep safety and maintainability defaults
81
+
82
+ - Keep `trust` off unless the input source is trusted and the task explicitly needs trusted commands.
83
+ - On untrusted content, keep size and expansion limits bounded instead of relaxing them blindly.
84
+ - Reuse one macro definition source across the whole document when many formulas share the same commands.
85
+ - When the rendered output is only for preview and the final destination has its own KaTeX pipeline, prefer editing the TeX source instead of freezing rendered HTML too early.
86
+
87
+ ## References
88
+
89
+ - `references/official-docs.md`: condensed notes from the official KaTeX docs and direct source links.
90
+ - `references/insertion-patterns.md`: insertion patterns for HTML, Markdown/MDX, and auto-rendered pages.
91
+ - `scripts/render_katex.py`: deterministic wrapper around the official KaTeX CLI for insertion-ready output.
92
+ - `scripts/render_katex.sh`: shell wrapper for environments that prefer an executable script entrypoint.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "KaTeX"
3
+ short_description: "Render and embed math formulas with KaTeX using insertion-ready snippets."
4
+ default_prompt: "Use $katex to turn TeX math into insertion-ready KaTeX output, choose between pre-rendering and auto-render based on the target file, and use the bundled renderer script when a static snippet or preview file is needed."
@@ -0,0 +1,54 @@
1
+ # KaTeX Insertion Patterns
2
+
3
+ ## HTML or template files
4
+
5
+ - Use:
6
+
7
+ ```bash
8
+ python3 katex/scripts/render_katex.py \
9
+ --tex 'E = mc^2' \
10
+ --output-format html-fragment
11
+ ```
12
+
13
+ - Paste the output where the formula should appear.
14
+ - Ensure the destination page loads a KaTeX stylesheet such as the official CDN link.
15
+
16
+ ## Standalone preview page
17
+
18
+ - Use:
19
+
20
+ ```bash
21
+ python3 katex/scripts/render_katex.py \
22
+ --tex '\\sum_{i=1}^{n} i = \\frac{n(n+1)}{2}' \
23
+ --display-mode \
24
+ --output-format html-page \
25
+ --output-file /absolute/path/preview.html
26
+ ```
27
+
28
+ - Open the generated file in a browser to visually confirm spacing and line breaking.
29
+
30
+ ## Markdown or MDX that preserves raw HTML
31
+
32
+ - Use `markdown-inline` for inline formulas and `markdown-block` for display formulas.
33
+ - The result is still HTML. It works only when the downstream renderer keeps raw HTML intact.
34
+ - If the renderer sanitizes or escapes HTML, keep the source TeX instead of pasting the rendered fragment.
35
+
36
+ ## Existing HTML page that stores raw TeX delimiters
37
+
38
+ - Keep the source TeX in the document.
39
+ - Load the official assets:
40
+ - `katex.min.css`
41
+ - `katex.min.js`
42
+ - `contrib/auto-render.min.js`
43
+ - Call `renderMathInElement(container, options)` after the DOM is ready.
44
+ - Restrict the container instead of rendering the whole page when only one section contains math.
45
+
46
+ ## Shared macros across a document
47
+
48
+ - Put macros in a JSON file and pass `--macro-file`.
49
+ - Reuse the same macro definitions for every formula generated into the same document.
50
+
51
+ ## Error-handling defaults
52
+
53
+ - Keep `throwOnError` enabled unless the task explicitly prefers graceful fallback rendering.
54
+ - When previewing user-supplied formulas that may be invalid, use `--no-throw-on-error` and keep the original TeX nearby for debugging.
@@ -0,0 +1,35 @@
1
+ # Official KaTeX Docs Notes
2
+
3
+ These notes condense the official KaTeX documentation that was checked on 2026-03-21. Re-open the linked pages when exact behavior matters.
4
+
5
+ ## Core pages
6
+
7
+ - [API](https://katex.org/docs/api.html)
8
+ - Browser rendering uses `katex.render`.
9
+ - String generation uses `katex.renderToString`.
10
+ - The docs recommend reusing the same `macros` object when multiple formulas should share macro definitions.
11
+ - [Options](https://katex.org/docs/options.html)
12
+ - `displayMode`, `output`, `throwOnError`, `errorColor`, `macros`, `minRuleThickness`, `colorIsTextColor`, `strict`, `trust`, `maxSize`, and `maxExpand` are the main rendering controls.
13
+ - `htmlAndMathml` is the default output mode and is the accessibility-friendly default.
14
+ - [Node.js](https://katex.org/docs/node)
15
+ - Include the KaTeX stylesheet when rendering HTML generated by `renderToString`.
16
+ - Use the HTML5 doctype to avoid browser quirks mode.
17
+ - Browser-side `katex.render` and server-side `renderToString` can share the same options.
18
+ - [CLI](https://katex.org/docs/cli)
19
+ - The official CLI accepts TeX from stdin or `--input`.
20
+ - Useful flags include `--display-mode`, `--format`, `--macro`, `--macro-file`, `--error-color`, `--no-throw-on-error`, `--leqno`, `--fleqn`, `--min-rule-thickness`, `--color-is-text-color`, `--strict`, `--trust`, `--max-size`, and `--max-expand`.
21
+ - [Auto-render extension](https://katex.org/docs/autorender.html)
22
+ - Load `auto-render.min.js` after `katex.min.js`.
23
+ - Call `renderMathInElement(document.body)` or scope it to a narrower container.
24
+ - The docs show default delimiters for block math and right-associative delimiter ordering.
25
+ - Ignored tags include `script`, `noscript`, `style`, `textarea`, `pre`, `code`, and `option`.
26
+ - [Supported Functions](https://katex.org/docs/supported)
27
+ - Use this page to confirm whether a command is supported before promising a render.
28
+
29
+ ## Practical guidance extracted from the official docs
30
+
31
+ 1. Prefer pre-rendered output when the agent is generating a static artifact.
32
+ 2. Prefer auto-render only when the source document should remain human-editable TeX with delimiters.
33
+ 3. Keep `trust` disabled by default.
34
+ 4. Include the stylesheet for every HTML destination.
35
+ 5. Keep one shared macro map across formulas when macro definitions are expected to persist.
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ """Render TeX formulas with the official KaTeX CLI and wrap the output for reuse."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import pathlib
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ from typing import Iterable
14
+
15
+
16
+ DEFAULT_CSS_HREF = "https://cdn.jsdelivr.net/npm/katex@0.16.25/dist/katex.min.css"
17
+
18
+
19
+ class KatexRenderError(Exception):
20
+ """User-facing error for rendering failures."""
21
+
22
+
23
+ def parse_args(argv: list[str]) -> argparse.Namespace:
24
+ parser = argparse.ArgumentParser(
25
+ prog="render_katex.py",
26
+ description="Render TeX with KaTeX and emit insertion-ready output.",
27
+ )
28
+
29
+ input_group = parser.add_mutually_exclusive_group(required=True)
30
+ input_group.add_argument("--tex", help="Raw TeX expression without delimiters.")
31
+ input_group.add_argument("--input-file", help="Path to a UTF-8 text file containing raw TeX.")
32
+
33
+ parser.add_argument(
34
+ "--output-format",
35
+ choices=("html-fragment", "html-page", "markdown-inline", "markdown-block", "json"),
36
+ default="html-fragment",
37
+ help="How to wrap the rendered KaTeX output.",
38
+ )
39
+ parser.add_argument(
40
+ "--katex-format",
41
+ choices=("html", "mathml", "htmlAndMathml"),
42
+ default="htmlAndMathml",
43
+ help="KaTeX internal output format.",
44
+ )
45
+ parser.add_argument("--display-mode", action="store_true", help="Render in display mode.")
46
+ parser.add_argument("--output-file", help="Write the wrapped output to a file.")
47
+ parser.add_argument("--css-href", default=DEFAULT_CSS_HREF, help="Stylesheet href for html-page/json output.")
48
+ parser.add_argument("--title", default="KaTeX Render", help="Document title for html-page output.")
49
+ parser.add_argument("--lang", default="en", help="HTML lang attribute for html-page output.")
50
+
51
+ parser.add_argument("--macro", action="append", default=[], help="Macro definition in NAME:VALUE form.")
52
+ parser.add_argument("--macro-file", help="Path to a JSON file mapping macro names to expansion strings.")
53
+ parser.add_argument("--error-color", help="Hex color or CSS color name for parse errors.")
54
+ parser.add_argument("--strict", help="KaTeX strict mode setting.")
55
+ parser.add_argument("--trust", help="KaTeX trust mode setting.")
56
+ parser.add_argument("--max-size", type=float, help="Maximum user-specified size in em.")
57
+ parser.add_argument("--max-expand", type=int, help="Maximum macro expansion count.")
58
+ parser.add_argument("--min-rule-thickness", type=float, help="Minimum rule thickness in em.")
59
+ parser.add_argument("--leqno", action="store_true", help="Render display equations with left equation numbers.")
60
+ parser.add_argument("--fleqn", action="store_true", help="Render display equations flush left.")
61
+ parser.add_argument(
62
+ "--color-is-text-color",
63
+ action="store_true",
64
+ help="Interpret \\\\color like legacy text color behavior.",
65
+ )
66
+ parser.add_argument(
67
+ "--no-throw-on-error",
68
+ action="store_true",
69
+ help="Render invalid input with colored source text instead of failing.",
70
+ )
71
+
72
+ return parser.parse_args(argv)
73
+
74
+
75
+ def normalize_path(raw_path: str) -> pathlib.Path:
76
+ path = pathlib.Path(raw_path).expanduser()
77
+ if not path.is_absolute():
78
+ path = pathlib.Path.cwd() / path
79
+ return path.resolve()
80
+
81
+
82
+ def load_tex(args: argparse.Namespace) -> str:
83
+ if args.input_file:
84
+ path = normalize_path(args.input_file)
85
+ if not path.is_file():
86
+ raise KatexRenderError(f"Input file not found: {path}")
87
+ return path.read_text(encoding="utf-8").strip()
88
+ return (args.tex or "").strip()
89
+
90
+
91
+ def load_macro_pairs(values: Iterable[str]) -> list[tuple[str, str]]:
92
+ pairs: list[tuple[str, str]] = []
93
+ for raw_value in values:
94
+ if ":" not in raw_value:
95
+ raise KatexRenderError(f"Invalid --macro value '{raw_value}'. Use NAME:VALUE.")
96
+ name, expansion = raw_value.split(":", 1)
97
+ name = name.strip()
98
+ expansion = expansion.strip()
99
+ if not name or not expansion:
100
+ raise KatexRenderError(f"Invalid --macro value '{raw_value}'. Use NAME:VALUE.")
101
+ pairs.append((name, expansion))
102
+ return pairs
103
+
104
+
105
+ def run_katex_cli(tex: str, args: argparse.Namespace) -> str:
106
+ command = [
107
+ "npx",
108
+ "--yes",
109
+ "--package",
110
+ "katex",
111
+ "katex",
112
+ "--format",
113
+ args.katex_format,
114
+ ]
115
+
116
+ if args.display_mode:
117
+ command.append("--display-mode")
118
+ if args.leqno:
119
+ command.append("--leqno")
120
+ if args.fleqn:
121
+ command.append("--fleqn")
122
+ if args.color_is_text_color:
123
+ command.append("--color-is-text-color")
124
+ if args.no_throw_on_error:
125
+ command.append("--no-throw-on-error")
126
+
127
+ if args.error_color:
128
+ command.extend(["--error-color", args.error_color])
129
+ if args.strict:
130
+ command.extend(["--strict", args.strict])
131
+ if args.trust:
132
+ command.extend(["--trust", args.trust])
133
+ if args.max_size is not None:
134
+ command.extend(["--max-size", str(args.max_size)])
135
+ if args.max_expand is not None:
136
+ command.extend(["--max-expand", str(args.max_expand)])
137
+ if args.min_rule_thickness is not None:
138
+ command.extend(["--min-rule-thickness", str(args.min_rule_thickness)])
139
+
140
+ for name, expansion in load_macro_pairs(args.macro):
141
+ command.extend(["--macro", f"{name}:{expansion}"])
142
+
143
+ if args.macro_file:
144
+ macro_file = normalize_path(args.macro_file)
145
+ if not macro_file.is_file():
146
+ raise KatexRenderError(f"Macro file not found: {macro_file}")
147
+ command.extend(["--macro-file", str(macro_file)])
148
+
149
+ with tempfile.NamedTemporaryFile("w", suffix=".tex", encoding="utf-8", delete=False) as handle:
150
+ handle.write(tex)
151
+ handle.write("\n")
152
+ temp_path = pathlib.Path(handle.name)
153
+
154
+ try:
155
+ command.extend(["--input", str(temp_path)])
156
+ result = subprocess.run(
157
+ command,
158
+ check=False,
159
+ capture_output=True,
160
+ text=True,
161
+ encoding="utf-8",
162
+ )
163
+ finally:
164
+ temp_path.unlink(missing_ok=True)
165
+
166
+ if result.returncode != 0:
167
+ stderr = result.stderr.strip() or "KaTeX CLI failed."
168
+ raise KatexRenderError(stderr)
169
+
170
+ return result.stdout.strip()
171
+
172
+
173
+ def build_html_page(rendered_html: str, args: argparse.Namespace) -> str:
174
+ css_link = ""
175
+ if args.css_href.strip():
176
+ css_link = f' <link rel="stylesheet" href="{args.css_href.strip()}">\n'
177
+
178
+ return (
179
+ "<!DOCTYPE html>\n"
180
+ f'<html lang="{args.lang}">\n'
181
+ "<head>\n"
182
+ ' <meta charset="utf-8">\n'
183
+ f" <title>{args.title}</title>\n"
184
+ f"{css_link}"
185
+ "</head>\n"
186
+ "<body>\n"
187
+ f"{rendered_html}\n"
188
+ "</body>\n"
189
+ "</html>\n"
190
+ )
191
+
192
+
193
+ def wrap_output(rendered_html: str, tex: str, args: argparse.Namespace) -> str:
194
+ if args.output_format == "html-fragment":
195
+ return f"{rendered_html}\n"
196
+ if args.output_format == "html-page":
197
+ return build_html_page(rendered_html, args)
198
+ if args.output_format == "markdown-inline":
199
+ return f"{rendered_html}\n"
200
+ if args.output_format == "markdown-block":
201
+ return f"\n{rendered_html}\n"
202
+ if args.output_format == "json":
203
+ payload = {
204
+ "tex": tex,
205
+ "displayMode": args.display_mode,
206
+ "katexFormat": args.katex_format,
207
+ "cssHref": args.css_href,
208
+ "content": rendered_html,
209
+ }
210
+ return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
211
+ raise KatexRenderError(f"Unsupported output format: {args.output_format}")
212
+
213
+
214
+ def write_output(content: str, output_file: str | None) -> None:
215
+ if not output_file:
216
+ sys.stdout.write(content)
217
+ return
218
+
219
+ path = normalize_path(output_file)
220
+ path.parent.mkdir(parents=True, exist_ok=True)
221
+ path.write_text(content, encoding="utf-8")
222
+ sys.stdout.write(str(path) + "\n")
223
+
224
+
225
+ def main(argv: list[str]) -> int:
226
+ try:
227
+ args = parse_args(argv)
228
+ tex = load_tex(args)
229
+ if not tex:
230
+ raise KatexRenderError("Input TeX is empty.")
231
+ rendered_html = run_katex_cli(tex, args)
232
+ wrapped = wrap_output(rendered_html, tex, args)
233
+ write_output(wrapped, args.output_file)
234
+ return 0
235
+ except KatexRenderError as exc:
236
+ print(f"[ERROR] {exc}", file=sys.stderr)
237
+ return 1
238
+ except FileNotFoundError as exc:
239
+ missing = exc.filename or "required executable"
240
+ if os.path.basename(missing) in {"npx", "node"}:
241
+ print("[ERROR] node and npx are required to render KaTeX.", file=sys.stderr)
242
+ return 1
243
+ raise
244
+
245
+
246
+ if __name__ == "__main__":
247
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+
6
+ if ! command -v python3 >/dev/null 2>&1; then
7
+ echo "[ERROR] python3 is required." >&2
8
+ exit 1
9
+ fi
10
+
11
+ exec python3 "$script_dir/render_katex.py" "$@"