starlight-cannoli-plugins 1.2.8 → 1.2.9

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.
@@ -0,0 +1,255 @@
1
+ // src/plugins/remark-latex-compile/compile.ts
2
+ import { createHash } from "crypto";
3
+ import { spawnSync } from "child_process";
4
+ import {
5
+ existsSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ rmSync,
9
+ mkdtempSync
10
+ } from "fs";
11
+ import { join } from "path";
12
+ import { tmpdir } from "os";
13
+
14
+ // src/plugins/remark-latex-compile/error-parser.ts
15
+ function parseLatexError(latexOutput) {
16
+ const lines = latexOutput.split("\n");
17
+ const errors = [];
18
+ const seenMessages = /* @__PURE__ */ new Set();
19
+ let hasFatal = false;
20
+ for (let i = 0; i < lines.length; i++) {
21
+ const line = lines[i];
22
+ if (line.startsWith("!")) {
23
+ const message = line.substring(1).trim();
24
+ if (seenMessages.has(message)) continue;
25
+ seenMessages.add(message);
26
+ const context = [];
27
+ let lineNum;
28
+ for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
29
+ const contextLine = lines[j];
30
+ if (contextLine.trim()) {
31
+ context.push(contextLine);
32
+ }
33
+ if (contextLine.startsWith("l.")) {
34
+ const lineMatch = contextLine.match(/^l\.(\d+)/);
35
+ if (lineMatch) {
36
+ lineNum = parseInt(lineMatch[1], 10);
37
+ }
38
+ break;
39
+ }
40
+ }
41
+ errors.push({
42
+ message,
43
+ line: lineNum,
44
+ context: context.slice(0, 3),
45
+ severity: "error"
46
+ });
47
+ }
48
+ }
49
+ for (let i = 0; i < lines.length; i++) {
50
+ const line = lines[i];
51
+ if (line.includes("Overfull") || line.includes("Underfull")) {
52
+ const msg = line.trim();
53
+ if (!seenMessages.has(msg)) {
54
+ seenMessages.add(msg);
55
+ errors.push({
56
+ message: msg,
57
+ context: [],
58
+ severity: "warning"
59
+ });
60
+ }
61
+ }
62
+ }
63
+ for (const line of lines) {
64
+ if (line.toLowerCase().includes("emergency stop") || line.toLowerCase().includes("fatal error") || line.toLowerCase().includes("not found")) {
65
+ hasFatal = true;
66
+ break;
67
+ }
68
+ }
69
+ if (errors.length === 0 && latexOutput.length > 0) {
70
+ for (const line of lines) {
71
+ if (line.includes("error") || line.includes("Error") || line.includes("Misplaced") || line.includes("Missing")) {
72
+ const msg = line.trim();
73
+ if (!seenMessages.has(msg)) {
74
+ seenMessages.add(msg);
75
+ errors.push({
76
+ message: msg,
77
+ context: [],
78
+ severity: "error"
79
+ });
80
+ }
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ if (errors.length === 0) {
86
+ errors.push({
87
+ message: "Unknown LaTeX compilation error",
88
+ context: [],
89
+ severity: "error"
90
+ });
91
+ }
92
+ return { errors, hasFatal };
93
+ }
94
+ function formatLatexError(parsed) {
95
+ const RED = "\x1B[31m";
96
+ const YELLOW = "\x1B[33m";
97
+ const RESET = "\x1B[0m";
98
+ const errorCount = parsed.errors.filter((e) => e.severity === "error").length;
99
+ const warningCount = parsed.errors.filter(
100
+ (e) => e.severity === "warning"
101
+ ).length;
102
+ let output = `${RED}[remark-latex-compile] LaTeX compilation failed${RESET}
103
+ `;
104
+ output += `${RED}${errorCount} error${errorCount !== 1 ? "s" : ""}${RESET}`;
105
+ if (warningCount > 0) {
106
+ output += `, ${YELLOW}${warningCount} warning${warningCount !== 1 ? "s" : ""}${RESET}`;
107
+ }
108
+ output += "\n\n";
109
+ const errorsByType = parsed.errors.reduce(
110
+ (acc, e) => {
111
+ if (!acc[e.severity]) acc[e.severity] = [];
112
+ acc[e.severity].push(e);
113
+ return acc;
114
+ },
115
+ {}
116
+ );
117
+ for (const err of errorsByType["error"] || []) {
118
+ output += `${RED}Error${RESET}`;
119
+ if (err.line) output += ` (line ${err.line})`;
120
+ output += `: ${err.message}
121
+ `;
122
+ if (err.context.length > 0) {
123
+ output += ` Context: ${err.context[0]}
124
+ `;
125
+ }
126
+ }
127
+ for (const warn of errorsByType["warning"] || []) {
128
+ output += `${YELLOW}Warning${RESET}: ${warn.message}
129
+ `;
130
+ }
131
+ return output;
132
+ }
133
+ function formatLatexSourceWithLineNumbers(latexSource, errors) {
134
+ const RED = "\x1B[31m";
135
+ const RESET = "\x1B[0m";
136
+ const lines = latexSource.split("\n");
137
+ const maxLineNum = lines.length;
138
+ const lineNumWidth = String(maxLineNum).length;
139
+ const errorLineNumbers = new Set(errors.map((e) => e.line).filter(Boolean));
140
+ const formattedLines = lines.map((line, index) => {
141
+ const lineNum = index + 1;
142
+ const lineNumStr = String(lineNum);
143
+ const padding = lineNumStr.length < lineNumWidth ? " " : "";
144
+ if (errorLineNumbers.has(lineNum)) {
145
+ return `${padding}${RED}[${lineNumStr}]:${RESET} ${line}`;
146
+ }
147
+ return `${padding}[${lineNumStr}]: ${line}`;
148
+ }).join("\n");
149
+ return formattedLines;
150
+ }
151
+ function createCompilationErrorMessage(latexSource, rawError) {
152
+ const parsed = parseLatexError(rawError);
153
+ const formatted = formatLatexError(parsed);
154
+ const formattedSource = formatLatexSourceWithLineNumbers(
155
+ latexSource,
156
+ parsed.errors
157
+ );
158
+ return `${formatted}
159
+ LaTeX source:
160
+ ${formattedSource}
161
+ `;
162
+ }
163
+
164
+ // src/plugins/remark-latex-compile/compile.ts
165
+ function hashLatexCode(code) {
166
+ const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
167
+ return createHash("md5").update(normalized).digest("hex").slice(0, 16);
168
+ }
169
+ function buildLatexSource(latexCode) {
170
+ if (latexCode.includes("\\documentclass") && latexCode.includes("\\begin{document}")) {
171
+ return latexCode.trim();
172
+ }
173
+ const separatorRegex = /%[ \t]*===/;
174
+ const parts = latexCode.split(separatorRegex);
175
+ let preamble = "";
176
+ let content = latexCode.trim();
177
+ if (parts.length === 2) {
178
+ preamble = parts[0].trim();
179
+ content = parts[1].trim();
180
+ }
181
+ return [
182
+ "\\documentclass[border=5pt]{standalone}",
183
+ preamble,
184
+ "\\begin{document}",
185
+ "\\Large",
186
+ content,
187
+ "\\end{document}"
188
+ ].join("\n");
189
+ }
190
+ function compileLatexToSvg(latexCode, svgOutputDir) {
191
+ const hash = hashLatexCode(latexCode);
192
+ const svgPath = join(svgOutputDir, `${hash}.svg`);
193
+ if (existsSync(svgPath)) {
194
+ return { hash, svgPath, wasCompiled: false };
195
+ }
196
+ mkdirSync(svgOutputDir, { recursive: true });
197
+ const workDir = mkdtempSync(join(tmpdir(), "latex-compile-"));
198
+ const texFile = join(workDir, "diagram.tex");
199
+ const pdfFile = join(workDir, "diagram.pdf");
200
+ const latexSource = buildLatexSource(latexCode);
201
+ try {
202
+ writeFileSync(texFile, latexSource, "utf-8");
203
+ const latexResult = spawnSync("pdflatex", [
204
+ "-interaction=nonstopmode",
205
+ "-output-directory",
206
+ workDir,
207
+ texFile
208
+ ]);
209
+ if (latexResult.error) {
210
+ const code = latexResult.error.code;
211
+ throw new Error(
212
+ `[remark-latex-compile] pdflatex not found on PATH (${code}).`
213
+ );
214
+ }
215
+ if (latexResult.status !== 0) {
216
+ const errorOutput = latexResult.stderr?.toString() || latexResult.stdout?.toString() || "";
217
+ const userMessage = createCompilationErrorMessage(
218
+ latexSource,
219
+ errorOutput
220
+ );
221
+ throw new Error(userMessage);
222
+ }
223
+ const dvisvgmResult = spawnSync("dvisvgm", [
224
+ "--pdf",
225
+ "--bbox=dvi",
226
+ pdfFile,
227
+ "-o",
228
+ svgPath
229
+ ]);
230
+ if (dvisvgmResult.error) {
231
+ const code = dvisvgmResult.error.code;
232
+ throw new Error(
233
+ `[remark-latex-compile] dvisvgm not found on PATH (${code}).`
234
+ );
235
+ }
236
+ if (dvisvgmResult.status !== 0) {
237
+ const errorOutput = dvisvgmResult.stderr?.toString() || dvisvgmResult.stdout?.toString() || "";
238
+ throw new Error(
239
+ `[remark-latex-compile] PDF to SVG conversion failed (hash: ${hash}).
240
+ Error: ${errorOutput}`
241
+ );
242
+ }
243
+ } finally {
244
+ try {
245
+ rmSync(workDir, { recursive: true, force: true });
246
+ } catch {
247
+ }
248
+ }
249
+ return { hash, svgPath, wasCompiled: true };
250
+ }
251
+
252
+ export {
253
+ hashLatexCode,
254
+ compileLatexToSvg
255
+ };
@@ -0,0 +1,233 @@
1
+ import {
2
+ compileLatexToSvg
3
+ } from "./chunk-QZUB4DOG.js";
4
+
5
+ // src/plugins/remark-latex-compile/index.ts
6
+ import { resolve } from "path";
7
+ function extractClassesFromMeta(meta) {
8
+ const classMatch = meta.match(/class="([^"]+)"/);
9
+ if (classMatch && classMatch[1]) {
10
+ return classMatch[1].split(/\s+/).filter(Boolean);
11
+ }
12
+ return [];
13
+ }
14
+ function traverseTree(node, svgOutputDir, filePath, depth = 0) {
15
+ if (!node) return;
16
+ const children = node.children;
17
+ if (Array.isArray(children)) {
18
+ for (let i = 0; i < children.length; i++) {
19
+ const child = children[i];
20
+ if (child.type === "code" && (child.lang === "tex" || child.lang === "latex") && String(child.meta || "").includes("compile")) {
21
+ try {
22
+ const result = compileLatexToSvg(String(child.value), svgOutputDir);
23
+ const customClasses = extractClassesFromMeta(
24
+ String(child.meta || "")
25
+ );
26
+ const allClasses = ["tex-compiled", ...customClasses];
27
+ children[i] = {
28
+ type: "paragraph",
29
+ children: [
30
+ {
31
+ type: "image",
32
+ url: `/static/tex-svgs/${result.hash}.svg`,
33
+ alt: "LaTeX diagram",
34
+ data: {
35
+ hProperties: {
36
+ className: allClasses
37
+ }
38
+ }
39
+ }
40
+ ]
41
+ };
42
+ } catch (err) {
43
+ if (process.env.NODE_ENV !== "production") {
44
+ const position = child.position;
45
+ const lineNumber = position?.start?.line || "?";
46
+ const errorMsg = err instanceof Error ? err.message : String(err);
47
+ const match = errorMsg.match(/\n\n([\s\S]+)/);
48
+ const details = match ? match[1] : errorMsg;
49
+ console.error(`${filePath}:${lineNumber}
50
+ ${details}`);
51
+ }
52
+ }
53
+ } else {
54
+ traverseTree(child, svgOutputDir, filePath, depth + 1);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ function remarkLatexCompile(options) {
60
+ const svgOutputDir = resolve(options.svgOutputDir);
61
+ return (tree, file) => {
62
+ const fileObj = file;
63
+ const filePath = String(fileObj?.path || fileObj?.filename || "unknown");
64
+ traverseTree(tree, svgOutputDir, filePath, 0);
65
+ };
66
+ }
67
+
68
+ // src/plugins/remark-latex-compile/astro-integration.ts
69
+ import { readdir, readFile, writeFile } from "fs/promises";
70
+ import { resolve as resolve2, join, extname } from "path";
71
+ import { createHash } from "crypto";
72
+ function hashLatexCode(code) {
73
+ const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
74
+ return createHash("md5").update(normalized).digest("hex").slice(0, 16);
75
+ }
76
+ function createAstroLatexIntegration(options) {
77
+ const svgOutputDir = resolve2(options.svgOutputDir);
78
+ const contentDir = options?.contentDir ? resolve2(options.contentDir) : resolve2("src/content/docs");
79
+ return {
80
+ name: "astro-latex-compile",
81
+ hooks: {
82
+ "astro:build:start": async () => {
83
+ console.log(
84
+ "[astro-latex-compile] Build start, scanning for tex/latex compile blocks"
85
+ );
86
+ await scanAndCompileLatex(contentDir, svgOutputDir);
87
+ },
88
+ "astro:build:done": async ({ dir }) => {
89
+ console.log(
90
+ "[astro-latex-compile] Build done, updating HTML references"
91
+ );
92
+ try {
93
+ await updateHtmlReferences(dir.pathname, contentDir, svgOutputDir);
94
+ } catch (err) {
95
+ console.error(
96
+ "[astro-latex-compile] Error updating HTML references:",
97
+ err
98
+ );
99
+ }
100
+ }
101
+ }
102
+ };
103
+ }
104
+ async function scanAndCompileLatex(dir, svgOutputDir) {
105
+ const entries = await readdir(dir, { withFileTypes: true });
106
+ for (const entry of entries) {
107
+ const fullPath = join(dir, entry.name);
108
+ if (entry.isDirectory()) {
109
+ await scanAndCompileLatex(fullPath, svgOutputDir);
110
+ } else if (entry.isFile()) {
111
+ const ext = extname(entry.name);
112
+ if (ext === ".md" || ext === ".mdx") {
113
+ await processMarkdownFile(fullPath, svgOutputDir);
114
+ }
115
+ }
116
+ }
117
+ }
118
+ function getLineNumber(content, position) {
119
+ return content.substring(0, position).split("\n").length;
120
+ }
121
+ async function processMarkdownFile(filePath, svgOutputDir) {
122
+ const content = await readFile(filePath, "utf-8");
123
+ const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
124
+ const matches = content.matchAll(latexBlockRegex);
125
+ for (const match of matches) {
126
+ const latexCode = match[1];
127
+ const lineNumber = getLineNumber(content, match.index || 0);
128
+ try {
129
+ const result = compileLatexToSvg(latexCode, svgOutputDir);
130
+ const status = result.wasCompiled ? "compiled" : "used cached";
131
+ console.log(
132
+ `[astro-latex-compile] ${filePath}:${lineNumber}: ${status} ${result.hash}.svg`
133
+ );
134
+ } catch (err) {
135
+ const error = err instanceof Error ? err : new Error(String(err));
136
+ error.message = `${filePath}:${lineNumber}
137
+ ${error.message}`;
138
+ throw error;
139
+ }
140
+ }
141
+ }
142
+ async function updateHtmlReferences(buildDir, contentDir, svgOutputDir) {
143
+ const latexHashes = [];
144
+ const entries = await readdir(contentDir, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const fullPath = join(contentDir, entry.name);
147
+ if (entry.isDirectory()) {
148
+ await scanMarkdownForHashes(fullPath, latexHashes);
149
+ }
150
+ }
151
+ await updateHtmlDirWithHashes(buildDir, latexHashes, svgOutputDir);
152
+ }
153
+ async function scanMarkdownForHashes(dir, hashes) {
154
+ const entries = await readdir(dir, { withFileTypes: true });
155
+ for (const entry of entries) {
156
+ const fullPath = join(dir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ await scanMarkdownForHashes(fullPath, hashes);
159
+ } else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))) {
160
+ const content = await readFile(fullPath, "utf-8");
161
+ const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
162
+ const matches = content.matchAll(latexBlockRegex);
163
+ for (const match of matches) {
164
+ const latexCode = match[1];
165
+ const hash = hashLatexCode(latexCode);
166
+ hashes.push(hash);
167
+ }
168
+ }
169
+ }
170
+ }
171
+ async function updateHtmlDirWithHashes(dir, hashes, svgOutputDir) {
172
+ let hashIndex = 0;
173
+ const entries = await readdir(dir, { withFileTypes: true });
174
+ for (const entry of entries) {
175
+ const fullPath = join(dir, entry.name);
176
+ if (entry.isDirectory()) {
177
+ await updateHtmlDirWithHashes(fullPath, hashes, svgOutputDir);
178
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
179
+ let content = await readFile(fullPath, "utf-8");
180
+ let modified = false;
181
+ const pathSegments = svgOutputDir.split("/").slice(-2).join("/");
182
+ const htmlPath = `/${pathSegments}`;
183
+ const svgRegex = new RegExp(`src="${htmlPath}/[a-f0-9]+\\.svg"`, "g");
184
+ content = content.replace(svgRegex, (match) => {
185
+ if (hashIndex < hashes.length) {
186
+ modified = true;
187
+ return `src="${htmlPath}/${hashes[hashIndex++]}.svg"`;
188
+ }
189
+ return match;
190
+ });
191
+ if (modified) {
192
+ await writeFile(fullPath, content, "utf-8");
193
+ console.log(`[astro-latex-compile] Updated ${fullPath}`);
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ // src/plugins/remark-latex-compile/starlight-plugin.ts
200
+ function starlightLatexCompile(options) {
201
+ return {
202
+ name: "starlight-latex-compile",
203
+ hooks: {
204
+ "config:setup": (hook) => {
205
+ hook.addIntegration({
206
+ name: "latex-compile-remark-integration",
207
+ hooks: {
208
+ "astro:config:setup": ({ updateConfig, config }) => {
209
+ const existingPlugins = (Array.isArray(config.markdown?.remarkPlugins) ? config.markdown.remarkPlugins : []).filter((p) => p !== void 0 && p !== null);
210
+ updateConfig({
211
+ markdown: {
212
+ remarkPlugins: [...existingPlugins, [remarkLatexCompile, options]]
213
+ }
214
+ });
215
+ }
216
+ }
217
+ });
218
+ hook.addIntegration(
219
+ createAstroLatexIntegration({
220
+ svgOutputDir: options.svgOutputDir
221
+ })
222
+ );
223
+ }
224
+ }
225
+ };
226
+ }
227
+ var starlight_plugin_default = starlightLatexCompile;
228
+
229
+ export {
230
+ starlightLatexCompile,
231
+ starlight_plugin_default,
232
+ remarkLatexCompile
233
+ };
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ hashLatexCode
4
+ } from "../chunk-QZUB4DOG.js";
5
+ import "../chunk-QGM4M3NI.js";
2
6
 
3
7
  // scripts/cli/cannoli-latex-cleanup.ts
4
8
  import { readdir, readFile, unlink } from "fs/promises";
5
9
  import { resolve, join } from "path";
6
- import { createHash } from "crypto";
7
10
  var args = process.argv.slice(2);
8
11
  var svgDir = null;
9
12
  var docsDir = "src/content/docs";
@@ -30,10 +33,6 @@ if (!checkMode && !deleteMode) {
30
33
  }
31
34
  var svgDirPath = resolve(svgDir);
32
35
  var docsDirPath = resolve(docsDir);
33
- function hashLatexCode(code) {
34
- const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
35
- return createHash("md5").update(normalized).digest("hex").slice(0, 16);
36
- }
37
36
  async function scanMarkdownForHashes(dir, hashes) {
38
37
  try {
39
38
  const entries = await readdir(dir, { withFileTypes: true });
package/dist/index.js CHANGED
@@ -7,11 +7,12 @@ import {
7
7
  import {
8
8
  remarkLatexCompile,
9
9
  starlightLatexCompile
10
- } from "./chunk-7NVLVO6Z.js";
10
+ } from "./chunk-UMYD2SJL.js";
11
11
  import {
12
12
  starlightSyncDocsToPublic
13
13
  } from "./chunk-GE24XGG7.js";
14
14
  import "./chunk-3ATSZG6H.js";
15
+ import "./chunk-QZUB4DOG.js";
15
16
  import "./chunk-QGM4M3NI.js";
16
17
 
17
18
  // src/plugins/astro-normalize-paths.ts
@@ -1,8 +1,10 @@
1
1
  import {
2
- compileLatexToSvg,
3
2
  remarkLatexCompile,
4
3
  starlightLatexCompile
5
- } from "../chunk-7NVLVO6Z.js";
4
+ } from "../chunk-UMYD2SJL.js";
5
+ import {
6
+ compileLatexToSvg
7
+ } from "../chunk-QZUB4DOG.js";
6
8
  import "../chunk-QGM4M3NI.js";
7
9
  export {
8
10
  compileLatexToSvg,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  starlightLatexCompile,
3
3
  starlight_plugin_default
4
- } from "../chunk-7NVLVO6Z.js";
4
+ } from "../chunk-UMYD2SJL.js";
5
+ import "../chunk-QZUB4DOG.js";
5
6
  import "../chunk-QGM4M3NI.js";
6
7
  export {
7
8
  starlight_plugin_default as default,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "1.2.8",
4
+ "version": "1.2.9",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",
@@ -1,483 +0,0 @@
1
- // src/plugins/remark-latex-compile/index.ts
2
- import { resolve } from "path";
3
-
4
- // src/plugins/remark-latex-compile/compile.ts
5
- import { createHash } from "crypto";
6
- import { spawnSync } from "child_process";
7
- import {
8
- existsSync,
9
- mkdirSync,
10
- writeFileSync,
11
- rmSync,
12
- mkdtempSync
13
- } from "fs";
14
- import { join } from "path";
15
- import { tmpdir } from "os";
16
-
17
- // src/plugins/remark-latex-compile/error-parser.ts
18
- function parseLatexError(latexOutput) {
19
- const lines = latexOutput.split("\n");
20
- const errors = [];
21
- const seenMessages = /* @__PURE__ */ new Set();
22
- let hasFatal = false;
23
- for (let i = 0; i < lines.length; i++) {
24
- const line = lines[i];
25
- if (line.startsWith("!")) {
26
- const message = line.substring(1).trim();
27
- if (seenMessages.has(message)) continue;
28
- seenMessages.add(message);
29
- const context = [];
30
- let lineNum;
31
- for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
32
- const contextLine = lines[j];
33
- if (contextLine.trim()) {
34
- context.push(contextLine);
35
- }
36
- if (contextLine.startsWith("l.")) {
37
- const lineMatch = contextLine.match(/^l\.(\d+)/);
38
- if (lineMatch) {
39
- lineNum = parseInt(lineMatch[1], 10);
40
- }
41
- break;
42
- }
43
- }
44
- errors.push({
45
- message,
46
- line: lineNum,
47
- context: context.slice(0, 3),
48
- severity: "error"
49
- });
50
- }
51
- }
52
- for (let i = 0; i < lines.length; i++) {
53
- const line = lines[i];
54
- if (line.includes("Overfull") || line.includes("Underfull")) {
55
- const msg = line.trim();
56
- if (!seenMessages.has(msg)) {
57
- seenMessages.add(msg);
58
- errors.push({
59
- message: msg,
60
- context: [],
61
- severity: "warning"
62
- });
63
- }
64
- }
65
- }
66
- for (const line of lines) {
67
- if (line.toLowerCase().includes("emergency stop") || line.toLowerCase().includes("fatal error") || line.toLowerCase().includes("not found")) {
68
- hasFatal = true;
69
- break;
70
- }
71
- }
72
- if (errors.length === 0 && latexOutput.length > 0) {
73
- for (const line of lines) {
74
- if (line.includes("error") || line.includes("Error") || line.includes("Misplaced") || line.includes("Missing")) {
75
- const msg = line.trim();
76
- if (!seenMessages.has(msg)) {
77
- seenMessages.add(msg);
78
- errors.push({
79
- message: msg,
80
- context: [],
81
- severity: "error"
82
- });
83
- }
84
- break;
85
- }
86
- }
87
- }
88
- if (errors.length === 0) {
89
- errors.push({
90
- message: "Unknown LaTeX compilation error",
91
- context: [],
92
- severity: "error"
93
- });
94
- }
95
- return { errors, hasFatal };
96
- }
97
- function formatLatexError(parsed) {
98
- const RED = "\x1B[31m";
99
- const YELLOW = "\x1B[33m";
100
- const RESET = "\x1B[0m";
101
- const errorCount = parsed.errors.filter((e) => e.severity === "error").length;
102
- const warningCount = parsed.errors.filter(
103
- (e) => e.severity === "warning"
104
- ).length;
105
- let output = `${RED}[remark-latex-compile] LaTeX compilation failed${RESET}
106
- `;
107
- output += `${RED}${errorCount} error${errorCount !== 1 ? "s" : ""}${RESET}`;
108
- if (warningCount > 0) {
109
- output += `, ${YELLOW}${warningCount} warning${warningCount !== 1 ? "s" : ""}${RESET}`;
110
- }
111
- output += "\n\n";
112
- const errorsByType = parsed.errors.reduce(
113
- (acc, e) => {
114
- if (!acc[e.severity]) acc[e.severity] = [];
115
- acc[e.severity].push(e);
116
- return acc;
117
- },
118
- {}
119
- );
120
- for (const err of errorsByType["error"] || []) {
121
- output += `${RED}Error${RESET}`;
122
- if (err.line) output += ` (line ${err.line})`;
123
- output += `: ${err.message}
124
- `;
125
- if (err.context.length > 0) {
126
- output += ` Context: ${err.context[0]}
127
- `;
128
- }
129
- }
130
- for (const warn of errorsByType["warning"] || []) {
131
- output += `${YELLOW}Warning${RESET}: ${warn.message}
132
- `;
133
- }
134
- return output;
135
- }
136
- function formatLatexSourceWithLineNumbers(latexSource, errors) {
137
- const RED = "\x1B[31m";
138
- const RESET = "\x1B[0m";
139
- const lines = latexSource.split("\n");
140
- const maxLineNum = lines.length;
141
- const lineNumWidth = String(maxLineNum).length;
142
- const errorLineNumbers = new Set(errors.map((e) => e.line).filter(Boolean));
143
- const formattedLines = lines.map((line, index) => {
144
- const lineNum = index + 1;
145
- const lineNumStr = String(lineNum);
146
- const padding = lineNumStr.length < lineNumWidth ? " " : "";
147
- if (errorLineNumbers.has(lineNum)) {
148
- return `${padding}${RED}[${lineNumStr}]:${RESET} ${line}`;
149
- }
150
- return `${padding}[${lineNumStr}]: ${line}`;
151
- }).join("\n");
152
- return formattedLines;
153
- }
154
- function createCompilationErrorMessage(latexSource, rawError) {
155
- const parsed = parseLatexError(rawError);
156
- const formatted = formatLatexError(parsed);
157
- const formattedSource = formatLatexSourceWithLineNumbers(
158
- latexSource,
159
- parsed.errors
160
- );
161
- return `${formatted}
162
- LaTeX source:
163
- ${formattedSource}
164
- `;
165
- }
166
-
167
- // src/plugins/remark-latex-compile/compile.ts
168
- function hashLatexCode(code) {
169
- const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
170
- return createHash("md5").update(normalized).digest("hex").slice(0, 16);
171
- }
172
- function buildLatexSource(latexCode) {
173
- if (latexCode.includes("\\documentclass") && latexCode.includes("\\begin{document}")) {
174
- return latexCode.trim();
175
- }
176
- const separatorRegex = /%[ \t]*===/;
177
- const parts = latexCode.split(separatorRegex);
178
- let preamble = "";
179
- let content = latexCode.trim();
180
- if (parts.length === 2) {
181
- preamble = parts[0].trim();
182
- content = parts[1].trim();
183
- }
184
- return [
185
- "\\documentclass[border=5pt]{standalone}",
186
- preamble,
187
- "\\begin{document}",
188
- "\\Large",
189
- content,
190
- "\\end{document}"
191
- ].join("\n");
192
- }
193
- function compileLatexToSvg(latexCode, svgOutputDir) {
194
- const hash = hashLatexCode(latexCode);
195
- const svgPath = join(svgOutputDir, `${hash}.svg`);
196
- if (existsSync(svgPath)) {
197
- return { hash, svgPath, wasCompiled: false };
198
- }
199
- mkdirSync(svgOutputDir, { recursive: true });
200
- const workDir = mkdtempSync(join(tmpdir(), "latex-compile-"));
201
- const texFile = join(workDir, "diagram.tex");
202
- const pdfFile = join(workDir, "diagram.pdf");
203
- const latexSource = buildLatexSource(latexCode);
204
- try {
205
- writeFileSync(texFile, latexSource, "utf-8");
206
- const latexResult = spawnSync("pdflatex", [
207
- "-interaction=nonstopmode",
208
- "-output-directory",
209
- workDir,
210
- texFile
211
- ]);
212
- if (latexResult.error) {
213
- const code = latexResult.error.code;
214
- throw new Error(
215
- `[remark-latex-compile] pdflatex not found on PATH (${code}).`
216
- );
217
- }
218
- if (latexResult.status !== 0) {
219
- const errorOutput = latexResult.stderr?.toString() || latexResult.stdout?.toString() || "";
220
- const userMessage = createCompilationErrorMessage(
221
- latexSource,
222
- errorOutput
223
- );
224
- throw new Error(userMessage);
225
- }
226
- const dvisvgmResult = spawnSync("dvisvgm", [
227
- "--pdf",
228
- "--bbox=dvi",
229
- pdfFile,
230
- "-o",
231
- svgPath
232
- ]);
233
- if (dvisvgmResult.error) {
234
- const code = dvisvgmResult.error.code;
235
- throw new Error(
236
- `[remark-latex-compile] dvisvgm not found on PATH (${code}).`
237
- );
238
- }
239
- if (dvisvgmResult.status !== 0) {
240
- const errorOutput = dvisvgmResult.stderr?.toString() || dvisvgmResult.stdout?.toString() || "";
241
- throw new Error(
242
- `[remark-latex-compile] PDF to SVG conversion failed (hash: ${hash}).
243
- Error: ${errorOutput}`
244
- );
245
- }
246
- } finally {
247
- try {
248
- rmSync(workDir, { recursive: true, force: true });
249
- } catch {
250
- }
251
- }
252
- return { hash, svgPath, wasCompiled: true };
253
- }
254
-
255
- // src/plugins/remark-latex-compile/index.ts
256
- function extractClassesFromMeta(meta) {
257
- const classMatch = meta.match(/class="([^"]+)"/);
258
- if (classMatch && classMatch[1]) {
259
- return classMatch[1].split(/\s+/).filter(Boolean);
260
- }
261
- return [];
262
- }
263
- function traverseTree(node, svgOutputDir, filePath, depth = 0) {
264
- if (!node) return;
265
- const children = node.children;
266
- if (Array.isArray(children)) {
267
- for (let i = 0; i < children.length; i++) {
268
- const child = children[i];
269
- if (child.type === "code" && (child.lang === "tex" || child.lang === "latex") && String(child.meta || "").includes("compile")) {
270
- try {
271
- const result = compileLatexToSvg(String(child.value), svgOutputDir);
272
- const customClasses = extractClassesFromMeta(
273
- String(child.meta || "")
274
- );
275
- const allClasses = ["tex-compiled", ...customClasses];
276
- children[i] = {
277
- type: "paragraph",
278
- children: [
279
- {
280
- type: "image",
281
- url: `/static/tex-svgs/${result.hash}.svg`,
282
- alt: "LaTeX diagram",
283
- data: {
284
- hProperties: {
285
- className: allClasses
286
- }
287
- }
288
- }
289
- ]
290
- };
291
- } catch (err) {
292
- if (process.env.NODE_ENV !== "production") {
293
- const position = child.position;
294
- const lineNumber = position?.start?.line || "?";
295
- const errorMsg = err instanceof Error ? err.message : String(err);
296
- const match = errorMsg.match(/\n\n([\s\S]+)/);
297
- const details = match ? match[1] : errorMsg;
298
- console.error(`${filePath}:${lineNumber}
299
- ${details}`);
300
- }
301
- }
302
- } else {
303
- traverseTree(child, svgOutputDir, filePath, depth + 1);
304
- }
305
- }
306
- }
307
- }
308
- function remarkLatexCompile(options) {
309
- const svgOutputDir = resolve(options.svgOutputDir);
310
- return (tree, file) => {
311
- const fileObj = file;
312
- const filePath = String(fileObj?.path || fileObj?.filename || "unknown");
313
- traverseTree(tree, svgOutputDir, filePath, 0);
314
- };
315
- }
316
-
317
- // src/plugins/remark-latex-compile/astro-integration.ts
318
- import { readdir, readFile, writeFile } from "fs/promises";
319
- import { resolve as resolve2, join as join2, extname } from "path";
320
- import { createHash as createHash2 } from "crypto";
321
- function hashLatexCode2(code) {
322
- const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
323
- return createHash2("md5").update(normalized).digest("hex").slice(0, 16);
324
- }
325
- function createAstroLatexIntegration(options) {
326
- const svgOutputDir = resolve2(options.svgOutputDir);
327
- const contentDir = options?.contentDir ? resolve2(options.contentDir) : resolve2("src/content/docs");
328
- return {
329
- name: "astro-latex-compile",
330
- hooks: {
331
- "astro:build:start": async () => {
332
- console.log(
333
- "[astro-latex-compile] Build start, scanning for tex/latex compile blocks"
334
- );
335
- await scanAndCompileLatex(contentDir, svgOutputDir);
336
- },
337
- "astro:build:done": async ({ dir }) => {
338
- console.log(
339
- "[astro-latex-compile] Build done, updating HTML references"
340
- );
341
- try {
342
- await updateHtmlReferences(dir.pathname, contentDir, svgOutputDir);
343
- } catch (err) {
344
- console.error(
345
- "[astro-latex-compile] Error updating HTML references:",
346
- err
347
- );
348
- }
349
- }
350
- }
351
- };
352
- }
353
- async function scanAndCompileLatex(dir, svgOutputDir) {
354
- const entries = await readdir(dir, { withFileTypes: true });
355
- for (const entry of entries) {
356
- const fullPath = join2(dir, entry.name);
357
- if (entry.isDirectory()) {
358
- await scanAndCompileLatex(fullPath, svgOutputDir);
359
- } else if (entry.isFile()) {
360
- const ext = extname(entry.name);
361
- if (ext === ".md" || ext === ".mdx") {
362
- await processMarkdownFile(fullPath, svgOutputDir);
363
- }
364
- }
365
- }
366
- }
367
- function getLineNumber(content, position) {
368
- return content.substring(0, position).split("\n").length;
369
- }
370
- async function processMarkdownFile(filePath, svgOutputDir) {
371
- const content = await readFile(filePath, "utf-8");
372
- const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
373
- const matches = content.matchAll(latexBlockRegex);
374
- for (const match of matches) {
375
- const latexCode = match[1];
376
- const lineNumber = getLineNumber(content, match.index || 0);
377
- try {
378
- const result = compileLatexToSvg(latexCode, svgOutputDir);
379
- const status = result.wasCompiled ? "compiled" : "used cached";
380
- console.log(
381
- `[astro-latex-compile] ${filePath}:${lineNumber}: ${status} ${result.hash}.svg`
382
- );
383
- } catch (err) {
384
- const error = err instanceof Error ? err : new Error(String(err));
385
- error.message = `${filePath}:${lineNumber}
386
- ${error.message}`;
387
- throw error;
388
- }
389
- }
390
- }
391
- async function updateHtmlReferences(buildDir, contentDir, svgOutputDir) {
392
- const latexHashes = [];
393
- const entries = await readdir(contentDir, { withFileTypes: true });
394
- for (const entry of entries) {
395
- const fullPath = join2(contentDir, entry.name);
396
- if (entry.isDirectory()) {
397
- await scanMarkdownForHashes(fullPath, latexHashes);
398
- }
399
- }
400
- await updateHtmlDirWithHashes(buildDir, latexHashes, svgOutputDir);
401
- }
402
- async function scanMarkdownForHashes(dir, hashes) {
403
- const entries = await readdir(dir, { withFileTypes: true });
404
- for (const entry of entries) {
405
- const fullPath = join2(dir, entry.name);
406
- if (entry.isDirectory()) {
407
- await scanMarkdownForHashes(fullPath, hashes);
408
- } else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))) {
409
- const content = await readFile(fullPath, "utf-8");
410
- const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
411
- const matches = content.matchAll(latexBlockRegex);
412
- for (const match of matches) {
413
- const latexCode = match[1];
414
- const hash = hashLatexCode2(latexCode);
415
- hashes.push(hash);
416
- }
417
- }
418
- }
419
- }
420
- async function updateHtmlDirWithHashes(dir, hashes, svgOutputDir) {
421
- let hashIndex = 0;
422
- const entries = await readdir(dir, { withFileTypes: true });
423
- for (const entry of entries) {
424
- const fullPath = join2(dir, entry.name);
425
- if (entry.isDirectory()) {
426
- await updateHtmlDirWithHashes(fullPath, hashes, svgOutputDir);
427
- } else if (entry.isFile() && entry.name.endsWith(".html")) {
428
- let content = await readFile(fullPath, "utf-8");
429
- let modified = false;
430
- const pathSegments = svgOutputDir.split("/").slice(-2).join("/");
431
- const htmlPath = `/${pathSegments}`;
432
- const svgRegex = new RegExp(`src="${htmlPath}/[a-f0-9]+\\.svg"`, "g");
433
- content = content.replace(svgRegex, (match) => {
434
- if (hashIndex < hashes.length) {
435
- modified = true;
436
- return `src="${htmlPath}/${hashes[hashIndex++]}.svg"`;
437
- }
438
- return match;
439
- });
440
- if (modified) {
441
- await writeFile(fullPath, content, "utf-8");
442
- console.log(`[astro-latex-compile] Updated ${fullPath}`);
443
- }
444
- }
445
- }
446
- }
447
-
448
- // src/plugins/remark-latex-compile/starlight-plugin.ts
449
- function starlightLatexCompile(options) {
450
- return {
451
- name: "starlight-latex-compile",
452
- hooks: {
453
- "config:setup": (hook) => {
454
- hook.addIntegration({
455
- name: "latex-compile-remark-integration",
456
- hooks: {
457
- "astro:config:setup": ({ updateConfig, config }) => {
458
- const existingPlugins = (Array.isArray(config.markdown?.remarkPlugins) ? config.markdown.remarkPlugins : []).filter((p) => p !== void 0 && p !== null);
459
- updateConfig({
460
- markdown: {
461
- remarkPlugins: [...existingPlugins, [remarkLatexCompile, options]]
462
- }
463
- });
464
- }
465
- }
466
- });
467
- hook.addIntegration(
468
- createAstroLatexIntegration({
469
- svgOutputDir: options.svgOutputDir
470
- })
471
- );
472
- }
473
- }
474
- };
475
- }
476
- var starlight_plugin_default = starlightLatexCompile;
477
-
478
- export {
479
- compileLatexToSvg,
480
- starlightLatexCompile,
481
- starlight_plugin_default,
482
- remarkLatexCompile
483
- };