@laitszkin/apollo-toolkit 3.13.2 → 3.14.1

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 (154) hide show
  1. package/AGENTS.md +7 -7
  2. package/CHANGELOG.md +36 -0
  3. package/CLAUDE.md +8 -8
  4. package/analyse-app-logs/SKILL.md +3 -3
  5. package/bin/apollo-toolkit.ts +7 -0
  6. package/codex/codex-memory-manager/SKILL.md +2 -2
  7. package/codex/learn-skill-from-conversations/SKILL.md +3 -3
  8. package/dist/bin/apollo-toolkit.d.ts +2 -0
  9. package/dist/bin/apollo-toolkit.js +7 -0
  10. package/dist/lib/cli.d.ts +41 -0
  11. package/dist/lib/cli.js +655 -0
  12. package/dist/lib/installer.d.ts +59 -0
  13. package/dist/lib/installer.js +404 -0
  14. package/dist/lib/tool-runner.d.ts +19 -0
  15. package/dist/lib/tool-runner.js +536 -0
  16. package/dist/lib/tools/architecture.d.ts +2 -0
  17. package/dist/lib/tools/architecture.js +23 -0
  18. package/dist/lib/tools/create-specs.d.ts +2 -0
  19. package/dist/lib/tools/create-specs.js +175 -0
  20. package/dist/lib/tools/docs-to-voice.d.ts +2 -0
  21. package/dist/lib/tools/docs-to-voice.js +705 -0
  22. package/dist/lib/tools/enforce-video-aspect-ratio.d.ts +2 -0
  23. package/dist/lib/tools/enforce-video-aspect-ratio.js +312 -0
  24. package/dist/lib/tools/extract-conversations.d.ts +2 -0
  25. package/dist/lib/tools/extract-conversations.js +105 -0
  26. package/dist/lib/tools/extract-pdf-text.d.ts +2 -0
  27. package/dist/lib/tools/extract-pdf-text.js +92 -0
  28. package/dist/lib/tools/filter-logs.d.ts +2 -0
  29. package/dist/lib/tools/filter-logs.js +94 -0
  30. package/dist/lib/tools/find-github-issues.d.ts +2 -0
  31. package/dist/lib/tools/find-github-issues.js +176 -0
  32. package/dist/lib/tools/generate-storyboard-images.d.ts +2 -0
  33. package/dist/lib/tools/generate-storyboard-images.js +419 -0
  34. package/dist/lib/tools/log-cli-utils.d.ts +35 -0
  35. package/dist/lib/tools/log-cli-utils.js +233 -0
  36. package/dist/lib/tools/open-github-issue.d.ts +2 -0
  37. package/dist/lib/tools/open-github-issue.js +750 -0
  38. package/dist/lib/tools/read-github-issue.d.ts +2 -0
  39. package/dist/lib/tools/read-github-issue.js +134 -0
  40. package/dist/lib/tools/render-error-book.d.ts +2 -0
  41. package/dist/lib/tools/render-error-book.js +265 -0
  42. package/dist/lib/tools/render-katex.d.ts +2 -0
  43. package/dist/lib/tools/render-katex.js +294 -0
  44. package/dist/lib/tools/review-threads.d.ts +2 -0
  45. package/dist/lib/tools/review-threads.js +491 -0
  46. package/dist/lib/tools/search-logs.d.ts +2 -0
  47. package/dist/lib/tools/search-logs.js +164 -0
  48. package/dist/lib/tools/sync-memory-index.d.ts +2 -0
  49. package/dist/lib/tools/sync-memory-index.js +113 -0
  50. package/dist/lib/tools/validate-openai-agent-config.d.ts +2 -0
  51. package/dist/lib/tools/validate-openai-agent-config.js +190 -0
  52. package/dist/lib/tools/validate-skill-frontmatter.d.ts +2 -0
  53. package/dist/lib/tools/validate-skill-frontmatter.js +118 -0
  54. package/dist/lib/types.d.ts +82 -0
  55. package/dist/lib/types.js +2 -0
  56. package/dist/lib/updater.d.ts +34 -0
  57. package/dist/lib/updater.js +112 -0
  58. package/dist/lib/utils/format.d.ts +2 -0
  59. package/dist/lib/utils/format.js +6 -0
  60. package/dist/lib/utils/terminal.d.ts +12 -0
  61. package/dist/lib/utils/terminal.js +26 -0
  62. package/docs-to-voice/SKILL.md +0 -1
  63. package/generate-spec/SKILL.md +1 -1
  64. package/katex/SKILL.md +1 -2
  65. package/lib/cli.ts +780 -0
  66. package/lib/installer.ts +466 -0
  67. package/lib/tool-runner.ts +561 -0
  68. package/lib/tools/architecture.ts +20 -0
  69. package/lib/tools/create-specs.ts +204 -0
  70. package/lib/tools/docs-to-voice.ts +799 -0
  71. package/lib/tools/enforce-video-aspect-ratio.ts +368 -0
  72. package/lib/tools/extract-conversations.ts +114 -0
  73. package/lib/tools/extract-pdf-text.ts +99 -0
  74. package/lib/tools/filter-logs.ts +118 -0
  75. package/lib/tools/find-github-issues.ts +211 -0
  76. package/lib/tools/generate-storyboard-images.ts +455 -0
  77. package/lib/tools/log-cli-utils.ts +262 -0
  78. package/lib/tools/open-github-issue.ts +930 -0
  79. package/lib/tools/read-github-issue.ts +179 -0
  80. package/lib/tools/render-error-book.ts +300 -0
  81. package/lib/tools/render-katex.ts +325 -0
  82. package/lib/tools/review-threads.ts +590 -0
  83. package/lib/tools/search-logs.ts +200 -0
  84. package/lib/tools/sync-memory-index.ts +114 -0
  85. package/lib/tools/validate-openai-agent-config.ts +213 -0
  86. package/lib/tools/validate-skill-frontmatter.ts +124 -0
  87. package/lib/types.ts +90 -0
  88. package/lib/updater.ts +165 -0
  89. package/lib/utils/format.ts +7 -0
  90. package/lib/utils/terminal.ts +22 -0
  91. package/open-github-issue/SKILL.md +2 -2
  92. package/optimise-skill/SKILL.md +1 -1
  93. package/package.json +13 -4
  94. package/resources/project-architecture/assets/architecture.css +764 -0
  95. package/resources/project-architecture/assets/viewer.client.js +144 -0
  96. package/resources/project-architecture/index.html +42 -0
  97. package/review-spec-related-changes/SKILL.md +1 -1
  98. package/solve-issues-found-during-review/SKILL.md +2 -1
  99. package/tsconfig.json +28 -0
  100. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  101. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  102. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  103. package/analyse-app-logs/scripts/filter_logs_by_time.py +0 -64
  104. package/analyse-app-logs/scripts/log_cli_utils.py +0 -112
  105. package/analyse-app-logs/scripts/search_logs.py +0 -137
  106. package/analyse-app-logs/tests/test_filter_logs_by_time.py +0 -95
  107. package/analyse-app-logs/tests/test_search_logs.py +0 -100
  108. package/codex/codex-memory-manager/scripts/extract_recent_conversations.py +0 -369
  109. package/codex/codex-memory-manager/scripts/sync_memory_index.py +0 -130
  110. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +0 -177
  111. package/codex/codex-memory-manager/tests/test_memory_template.py +0 -37
  112. package/codex/codex-memory-manager/tests/test_sync_memory_index.py +0 -84
  113. package/codex/learn-skill-from-conversations/scripts/extract_recent_conversations.py +0 -369
  114. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +0 -177
  115. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  116. package/docs-to-voice/scripts/docs_to_voice.py +0 -1385
  117. package/docs-to-voice/scripts/docs_to_voice.sh +0 -11
  118. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +0 -210
  119. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +0 -115
  120. package/docs-to-voice/tests/test_docs_to_voice_settings.py +0 -43
  121. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +0 -51
  122. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +0 -57
  123. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  124. package/generate-spec/scripts/create-specs +0 -215
  125. package/generate-spec/tests/test_create_specs.py +0 -200
  126. package/init-project-html/scripts/architecture-bootstrap-render.js +0 -16
  127. package/init-project-html/scripts/architecture.js +0 -296
  128. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  129. package/katex/scripts/render_katex.py +0 -247
  130. package/katex/scripts/render_katex.sh +0 -11
  131. package/katex/tests/test_render_katex.py +0 -174
  132. package/learning-error-book/scripts/render_error_book_json_to_pdf.py +0 -590
  133. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +0 -134
  134. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  135. package/open-github-issue/scripts/open_github_issue.py +0 -705
  136. package/open-github-issue/tests/test_open_github_issue.py +0 -381
  137. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +0 -763
  138. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +0 -177
  139. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  140. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  141. package/read-github-issue/scripts/find_issues.py +0 -148
  142. package/read-github-issue/scripts/read_issue.py +0 -108
  143. package/read-github-issue/tests/test_find_issues.py +0 -127
  144. package/read-github-issue/tests/test_read_issue.py +0 -109
  145. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  146. package/resolve-review-comments/scripts/review_threads.py +0 -425
  147. package/resolve-review-comments/tests/test_review_threads.py +0 -74
  148. package/scripts/validate_openai_agent_config.py +0 -209
  149. package/scripts/validate_skill_frontmatter.py +0 -131
  150. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  151. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +0 -350
  152. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +0 -194
  153. package/weekly-financial-event-report/scripts/extract_pdf_text_pdfkit.swift +0 -99
  154. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +0 -64
@@ -1,296 +0,0 @@
1
- #!/usr/bin/env node
2
- // architecture.js — thin shim over lib/atlas/cli.js.
3
- //
4
- // Backward-compatible legacy entrypoint:
5
- // architecture.js # same as `open`
6
- // architecture.js open [--project <root>] [--no-open]
7
- // architecture.js diff [--project <root>] [--out <dir>] [--no-open]
8
- //
9
- // All new declarative verbs (feature add, submodule add, function add,
10
- // variable add, dataflow add|remove|reorder, error add, edge add, meta
11
- // set, actor add, render, validate, undo) are routed through
12
- // lib/atlas/cli.js, which owns layout, no-overlap, DOM, CSS, and pan/zoom.
13
-
14
- 'use strict';
15
-
16
- const fs = require('node:fs');
17
- const path = require('node:path');
18
- const { spawn, spawnSync } = require('node:child_process');
19
-
20
- const newCli = require('../lib/atlas/cli');
21
-
22
- const ATLAS_REL = path.join('resources', 'project-architecture', 'index.html');
23
- const RESOURCES_REL = path.join('resources', 'project-architecture');
24
- const PLANS_REL = path.join('docs', 'plans');
25
- const DIFF_DIRNAME = 'architecture_diff';
26
- const REMOVED_FILE = '_removed.txt';
27
- const ATLAS_DIRNAME = 'atlas';
28
- const DEFAULT_OUT_REL = path.join('.apollo-toolkit', 'architecture-diff');
29
-
30
- const LEGACY_VERBS = new Set(['open', 'diff']);
31
-
32
- const USAGE = newCli.buildArchitectureHelpPage();
33
-
34
- function parseArgs(argv) {
35
- const args = [...argv];
36
- const result = {
37
- subcommand: 'open',
38
- projectRoot: null,
39
- out: null,
40
- open: true,
41
- help: false,
42
- };
43
-
44
- if (args.length > 0 && !args[0].startsWith('-')) {
45
- const candidate = args[0];
46
- if (candidate === 'open' || candidate === 'diff') {
47
- result.subcommand = candidate;
48
- args.shift();
49
- }
50
- }
51
-
52
- while (args.length > 0) {
53
- const arg = args.shift();
54
- if (arg === '--help' || arg === '-h') {
55
- result.help = true;
56
- } else if (arg === '--project') {
57
- const value = args.shift();
58
- if (!value) throw new Error('Missing value for --project');
59
- result.projectRoot = path.resolve(value);
60
- } else if (arg === '--out') {
61
- const value = args.shift();
62
- if (!value) throw new Error('Missing value for --out');
63
- result.out = path.resolve(value);
64
- } else if (arg === '--no-open') {
65
- result.open = false;
66
- } else {
67
- throw new Error(`Unexpected argument: ${arg}`);
68
- }
69
- }
70
-
71
- return result;
72
- }
73
-
74
- function findProjectRoot(startDir) {
75
- let dir = path.resolve(startDir);
76
- while (true) {
77
- if (fs.existsSync(path.join(dir, ATLAS_REL))) return dir;
78
- if (fs.existsSync(path.join(dir, RESOURCES_REL, ATLAS_DIRNAME, 'atlas.index.yaml'))) return dir;
79
- const parent = path.dirname(dir);
80
- if (parent === dir) return null;
81
- dir = parent;
82
- }
83
- }
84
-
85
- function openInBrowser(filePath) {
86
- const platform = process.platform;
87
- let command;
88
- let args;
89
- if (platform === 'darwin') { command = 'open'; args = [filePath]; }
90
- else if (platform === 'win32') { command = 'cmd'; args = ['/c', 'start', '""', filePath]; }
91
- else { command = 'xdg-open'; args = [filePath]; }
92
- try {
93
- const child = spawn(command, args, { stdio: 'ignore', detached: true });
94
- child.on('error', () => {});
95
- child.unref();
96
- } catch (_e) { /* best effort */ }
97
- }
98
-
99
- function walkArchitectureDiffDirs(plansRoot) {
100
- const result = [];
101
- if (!fs.existsSync(plansRoot)) return result;
102
- function recurse(dir) {
103
- let entries;
104
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
105
- for (const entry of entries) {
106
- if (!entry.isDirectory()) continue;
107
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
108
- const full = path.join(dir, entry.name);
109
- if (entry.name === DIFF_DIRNAME) { result.push(full); continue; }
110
- recurse(full);
111
- }
112
- }
113
- recurse(plansRoot);
114
- return result;
115
- }
116
-
117
- function walkAfterStateHtml(diffDir) {
118
- const out = [];
119
- function recurse(dir, relParts) {
120
- let entries;
121
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return; }
122
- for (const entry of entries) {
123
- if (entry.name === 'assets') continue;
124
- if (entry.name === ATLAS_DIRNAME) continue;
125
- if (entry.name === REMOVED_FILE) continue;
126
- if (entry.name.startsWith('.')) continue;
127
- const full = path.join(dir, entry.name);
128
- const nextRel = [...relParts, entry.name];
129
- if (entry.isDirectory()) recurse(full, nextRel);
130
- else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
131
- out.push({ abs: full, rel: nextRel.join('/') });
132
- }
133
- }
134
- }
135
- recurse(diffDir, []);
136
- return out;
137
- }
138
-
139
- function readRemovedManifest(diffDir) {
140
- const manifestPath = path.join(diffDir, REMOVED_FILE);
141
- if (!fs.existsSync(manifestPath)) return [];
142
- return fs.readFileSync(manifestPath, 'utf8')
143
- .split(/\r?\n/)
144
- .map((line) => line.trim())
145
- .filter((line) => line && !line.startsWith('#'));
146
- }
147
-
148
- function collectChanges(projectRoot) {
149
- const resourcesRoot = path.join(projectRoot, RESOURCES_REL);
150
- const plansRoot = path.join(projectRoot, PLANS_REL);
151
- const diffDirs = walkArchitectureDiffDirs(plansRoot);
152
- const changes = [];
153
-
154
- for (const diffDir of diffDirs) {
155
- const specDir = path.dirname(diffDir);
156
- const specLabel = path.relative(projectRoot, specDir);
157
- for (const after of walkAfterStateHtml(diffDir)) {
158
- const beforeAbs = path.join(resourcesRoot, after.rel);
159
- const beforeExists = fs.existsSync(beforeAbs);
160
- changes.push({
161
- kind: beforeExists ? 'modified' : 'added',
162
- rel: after.rel,
163
- spec: specLabel,
164
- beforePath: beforeExists ? path.relative(projectRoot, beforeAbs) : null,
165
- afterPath: path.relative(projectRoot, after.abs),
166
- });
167
- }
168
- for (const removedRel of readRemovedManifest(diffDir)) {
169
- const beforeAbs = path.join(resourcesRoot, removedRel);
170
- if (!fs.existsSync(beforeAbs)) continue;
171
- changes.push({
172
- kind: 'removed',
173
- rel: removedRel,
174
- spec: specLabel,
175
- beforePath: path.relative(projectRoot, beforeAbs),
176
- afterPath: null,
177
- });
178
- }
179
- }
180
-
181
- changes.sort((a, b) => {
182
- if (a.spec !== b.spec) return a.spec.localeCompare(b.spec);
183
- if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
184
- return a.rel.localeCompare(b.rel);
185
- });
186
- return changes;
187
- }
188
-
189
- function toViewerRel(outDir, projectRoot, projectRelPath) {
190
- if (!projectRelPath) return null;
191
- const absolute = path.resolve(projectRoot, projectRelPath);
192
- const rel = path.relative(outDir, absolute);
193
- return rel.split(path.sep).join('/');
194
- }
195
-
196
- function renderViewer({ changes, projectRoot, outDir }) {
197
- return newCli.renderDiffViewer({ changes, projectRoot, outDir });
198
- }
199
-
200
- function runOpen(opts, io) {
201
- let projectRoot = opts.projectRoot
202
- ? path.resolve(opts.projectRoot)
203
- : findProjectRoot(process.cwd());
204
- if (!projectRoot) {
205
- projectRoot = process.cwd();
206
- }
207
- fs.mkdirSync(path.join(projectRoot, RESOURCES_REL), { recursive: true });
208
- const atlas = path.join(projectRoot, ATLAS_REL);
209
- if (!fs.existsSync(atlas)) {
210
- const bootstrap = path.join(__dirname, 'architecture-bootstrap-render.js');
211
- const result = spawnSync(process.execPath, [bootstrap, 'render', '--project', projectRoot, '--no-open'], {
212
- stdio: 'ignore',
213
- });
214
- if (result.status !== 0) {
215
- io.stderr.write(`Atlas not found and render failed: ${atlas}\n`);
216
- return 1;
217
- }
218
- }
219
- if (!fs.existsSync(atlas)) {
220
- io.stderr.write(`Atlas not found: ${atlas}\n`);
221
- return 1;
222
- }
223
- io.stdout.write(`${atlas}\n`);
224
- if (opts.open) openInBrowser(atlas);
225
- return 0;
226
- }
227
-
228
- function runDiff(opts, io) {
229
- let projectRoot = opts.projectRoot
230
- ? path.resolve(opts.projectRoot)
231
- : findProjectRoot(process.cwd());
232
- if (!projectRoot) {
233
- projectRoot = process.cwd();
234
- }
235
- const bootstrap = path.join(__dirname, 'architecture-bootstrap-render.js');
236
- const args = [bootstrap, 'diff', '--project', projectRoot];
237
- if (opts.out) args.push('--out', opts.out);
238
- if (!opts.open) args.push('--no-open');
239
- const result = spawnSync(process.execPath, args, { encoding: 'utf8' });
240
- if (result.stdout) io.stdout.write(result.stdout);
241
- if (result.stderr) io.stderr.write(result.stderr);
242
- return result.status || 0;
243
- }
244
-
245
- // main(argv, io) is sync and supports the legacy verbs `open` and
246
- // `diff` only. Tests rely on the sync return-code contract. All other
247
- // verbs go through dispatchAsync().
248
- function main(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
249
- let opts;
250
- try {
251
- opts = parseArgs(argv);
252
- } catch (error) {
253
- io.stderr.write(`${error.message}\n\n${USAGE}\n`);
254
- return 1;
255
- }
256
- if (opts.help) {
257
- const explicitLegacyVerb = argv.length > 0 && !argv[0].startsWith('-') && LEGACY_VERBS.has(argv[0]);
258
- const helpText = explicitLegacyVerb ? newCli.buildArchitectureHelpPage(argv[0]) : USAGE;
259
- io.stdout.write(`${helpText}\n`);
260
- return 0;
261
- }
262
- if (opts.subcommand === 'open') return runOpen(opts, io);
263
- if (opts.subcommand === 'diff') return runDiff(opts, io);
264
- io.stderr.write(`Unknown subcommand: ${opts.subcommand}\n\n${USAGE}\n`);
265
- return 1;
266
- }
267
-
268
- async function dispatchAsync(argv, io = { stdout: process.stdout, stderr: process.stderr }) {
269
- return newCli.dispatch(argv, io);
270
- }
271
-
272
- if (require.main === module) {
273
- const argv = process.argv.slice(2);
274
- const verb = argv[0];
275
- if (!verb || verb.startsWith('-') || LEGACY_VERBS.has(verb)) {
276
- process.exit(main(argv));
277
- } else {
278
- dispatchAsync(argv).then((code) => process.exit(code)).catch((err) => {
279
- process.stderr.write(`${err && err.stack ? err.stack : err}\n`);
280
- process.exit(1);
281
- });
282
- }
283
- }
284
-
285
- module.exports = {
286
- parseArgs,
287
- findProjectRoot,
288
- collectChanges,
289
- renderViewer,
290
- toViewerRel,
291
- walkArchitectureDiffDirs,
292
- walkAfterStateHtml,
293
- readRemovedManifest,
294
- main,
295
- dispatchAsync,
296
- };
@@ -1,247 +0,0 @@
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:]))
@@ -1,11 +0,0 @@
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" "$@"