starlight-cannoli-plugins 2.10.5 → 2.12.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.
package/README.md
CHANGED
|
@@ -144,6 +144,7 @@ dvisvgm --version
|
|
|
144
144
|
- `svgOutputDir` (required): Directory where compiled SVG files are written. Must be inside `public/` so Astro serves them as static assets.
|
|
145
145
|
- `removeOrphanedSvgs` (optional, default: `false`): When `true`, SVG files that are no longer referenced by any `tex compile` block are deleted automatically. In dev mode, stale SVGs are removed immediately when a block is edited. On build, any remaining orphans are swept at the end.
|
|
146
146
|
- `texInputDirs` (optional): Directories added to the TeX input search path (`TEXINPUTS`), allowing `\input{}` and `\include{}` to resolve files from your project. Use a trailing `/` to search only that directory, or `//` to search it recursively. Multiple directories are supported.
|
|
147
|
+
- `tempOutputDir` (optional): When set, a JPEG copy of each compiled diagram is written to this directory, mirroring the folder structure of `src/content/docs/`. Only blocks that carry a `blockid=<n>` meta tag produce a JPEG — blocks without it are ignored. The filename format is `<originating-file>--<blockid>--<hash>.jpg` (e.g. `tex-test.md--5--abc123.jpg`). JPEGs are deleted automatically when their block is removed, its `blockid` changes, or its content changes. SVG output is unaffected — the JPEG is complementary and intended for local inspection.
|
|
147
148
|
|
|
148
149
|
```ts
|
|
149
150
|
astroLatexCompile({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/plugins/astro-latex-compile/index.ts
|
|
2
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2
3
|
import { rm } from "fs/promises";
|
|
3
|
-
import { join as join2, resolve } from "path";
|
|
4
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
4
5
|
import { visit, SKIP } from "unist-util-visit";
|
|
5
6
|
import { MetaOptions } from "@expressive-code/core";
|
|
6
7
|
|
|
@@ -13,8 +14,9 @@ import {
|
|
|
13
14
|
rmSync,
|
|
14
15
|
mkdtempSync
|
|
15
16
|
} from "fs";
|
|
16
|
-
import { join } from "path";
|
|
17
|
+
import { basename, dirname, join, resolve } from "path";
|
|
17
18
|
import { tmpdir } from "os";
|
|
19
|
+
import sharp from "sharp";
|
|
18
20
|
|
|
19
21
|
// src/plugins/astro-latex-compile/error-parser.ts
|
|
20
22
|
function parseLatexError(latexOutput) {
|
|
@@ -167,7 +169,7 @@ ${formattedSource}
|
|
|
167
169
|
// src/plugins/utils/process-utils.ts
|
|
168
170
|
import { spawn } from "child_process";
|
|
169
171
|
function execProcess(command, args, options) {
|
|
170
|
-
return new Promise((
|
|
172
|
+
return new Promise((resolve3, reject) => {
|
|
171
173
|
let stdout = "";
|
|
172
174
|
let stderr = "";
|
|
173
175
|
const proc = spawn(command, args, { env: options?.env });
|
|
@@ -181,12 +183,78 @@ function execProcess(command, args, options) {
|
|
|
181
183
|
reject(err);
|
|
182
184
|
});
|
|
183
185
|
proc.on("close", (code) => {
|
|
184
|
-
|
|
186
|
+
resolve3({ status: code ?? 1, stdout, stderr });
|
|
185
187
|
});
|
|
186
188
|
});
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
// src/plugins/astro-latex-compile/utils.ts
|
|
192
|
+
var CONTENT_ROOT = "src/content/docs/";
|
|
193
|
+
function computeJpgPath(tempOutputDir, filePath, blockId, hash) {
|
|
194
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
195
|
+
const idx = normalized.indexOf(CONTENT_ROOT);
|
|
196
|
+
const relativePath = idx !== -1 ? normalized.slice(idx + CONTENT_ROOT.length) : basename(normalized);
|
|
197
|
+
const dir = dirname(relativePath);
|
|
198
|
+
const filename = basename(relativePath);
|
|
199
|
+
const jpgFilename = `${filename}--${blockId}--${hash}.jpg`;
|
|
200
|
+
const base = resolve(tempOutputDir);
|
|
201
|
+
return dir === "." ? join(base, jpgFilename) : join(base, dir, jpgFilename);
|
|
202
|
+
}
|
|
203
|
+
async function writeJpgFromSvg(svgPath, jpgPath) {
|
|
204
|
+
mkdirSync(dirname(jpgPath), { recursive: true });
|
|
205
|
+
await sharp(svgPath).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
|
|
206
|
+
}
|
|
207
|
+
function stripAnsi(text) {
|
|
208
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
209
|
+
}
|
|
210
|
+
function escapeXml(text) {
|
|
211
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
212
|
+
}
|
|
213
|
+
function wrapText(text, maxChars) {
|
|
214
|
+
const lines = [];
|
|
215
|
+
for (const raw of text.split("\n")) {
|
|
216
|
+
if (raw.length <= maxChars) {
|
|
217
|
+
lines.push(raw);
|
|
218
|
+
} else {
|
|
219
|
+
let remaining = raw;
|
|
220
|
+
while (remaining.length > maxChars) {
|
|
221
|
+
lines.push(remaining.slice(0, maxChars));
|
|
222
|
+
remaining = remaining.slice(maxChars);
|
|
223
|
+
}
|
|
224
|
+
if (remaining) lines.push(remaining);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
async function writeJpgError(jpgPath, header, errorText) {
|
|
230
|
+
const MAX_LINES = 40;
|
|
231
|
+
const clean = stripAnsi(errorText);
|
|
232
|
+
const sourceIdx = clean.indexOf("LaTeX source:");
|
|
233
|
+
const summary = (sourceIdx !== -1 ? clean.slice(0, sourceIdx) : clean).trimEnd();
|
|
234
|
+
let lines = wrapText(summary, 90);
|
|
235
|
+
if (lines.length > MAX_LINES) {
|
|
236
|
+
lines = lines.slice(0, MAX_LINES);
|
|
237
|
+
lines.push("...");
|
|
238
|
+
}
|
|
239
|
+
const fontSize = 13;
|
|
240
|
+
const lineHeight = 18;
|
|
241
|
+
const padding = 20;
|
|
242
|
+
const headerHeight = 55;
|
|
243
|
+
const width = 800;
|
|
244
|
+
const height = padding * 2 + headerHeight + lines.length * lineHeight;
|
|
245
|
+
const textLines = lines.map(
|
|
246
|
+
(line, i) => `<text x="${padding}" y="${padding + headerHeight + i * lineHeight}" font-family="monospace" font-size="${fontSize}" fill="#333333">${escapeXml(line)}</text>`
|
|
247
|
+
).join("\n ");
|
|
248
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
249
|
+
<rect width="${width}" height="${height}" fill="white"/>
|
|
250
|
+
<rect x="0" y="0" width="${width}" height="4" fill="#cc0000"/>
|
|
251
|
+
<text x="${padding}" y="${padding + 20}" font-family="monospace" font-size="16" font-weight="bold" fill="#cc0000">LaTeX Compilation Error</text>
|
|
252
|
+
<text x="${padding}" y="${padding + 40}" font-family="monospace" font-size="12" fill="#666666">${escapeXml(header)}</text>
|
|
253
|
+
${textLines}
|
|
254
|
+
</svg>`;
|
|
255
|
+
mkdirSync(dirname(jpgPath), { recursive: true });
|
|
256
|
+
await sharp(Buffer.from(svg)).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
|
|
257
|
+
}
|
|
190
258
|
function hashLatexCode(code) {
|
|
191
259
|
const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
|
|
192
260
|
return createHash("md5").update(normalized).digest("hex").slice(0, 16);
|
|
@@ -285,7 +353,7 @@ Error: ${errorOutput}`
|
|
|
285
353
|
|
|
286
354
|
// src/plugins/astro-latex-compile/index.ts
|
|
287
355
|
function remarkLatexCompile(options) {
|
|
288
|
-
const svgOutputDir =
|
|
356
|
+
const svgOutputDir = resolve2(options.svgOutputDir);
|
|
289
357
|
return async function transformer(tree, file) {
|
|
290
358
|
const nodes = [];
|
|
291
359
|
visit(tree, "code", (node, index, parent) => {
|
|
@@ -298,7 +366,10 @@ function remarkLatexCompile(options) {
|
|
|
298
366
|
const filePath = file.path || "unknown";
|
|
299
367
|
const results = await Promise.all(
|
|
300
368
|
nodes.map(async ({ node, index, parent }) => {
|
|
301
|
-
const
|
|
369
|
+
const lineNumberStr = node.position?.start.line ?? "?";
|
|
370
|
+
const blockId = new MetaOptions(node.meta ?? "").getInteger("blockid");
|
|
371
|
+
const contentHash = hashLatexCode(node.value);
|
|
372
|
+
const canWriteJpg = !!options.tempOutputDir && blockId !== void 0;
|
|
302
373
|
try {
|
|
303
374
|
const result = await compileLatexToSvg(
|
|
304
375
|
node.value,
|
|
@@ -307,20 +378,49 @@ function remarkLatexCompile(options) {
|
|
|
307
378
|
);
|
|
308
379
|
if (result.wasCompiled) {
|
|
309
380
|
console.log(
|
|
310
|
-
`[remark-latex-compile] ${filePath}:${
|
|
381
|
+
`[remark-latex-compile] ${filePath}:${lineNumberStr}: compiled ${result.hash}.svg`
|
|
311
382
|
);
|
|
312
383
|
}
|
|
313
384
|
options._referencedHashes?.add(result.hash);
|
|
314
|
-
|
|
385
|
+
const jpgPath = canWriteJpg ? computeJpgPath(options.tempOutputDir, filePath, blockId, contentHash) : null;
|
|
386
|
+
if (jpgPath && !existsSync2(jpgPath)) {
|
|
387
|
+
await writeJpgFromSvg(result.svgPath, jpgPath);
|
|
388
|
+
console.log(
|
|
389
|
+
`[remark-latex-compile] ${filePath}:${lineNumberStr}: wrote ${jpgPath}`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
index,
|
|
394
|
+
parent,
|
|
395
|
+
result,
|
|
396
|
+
error: null,
|
|
397
|
+
hash: result.hash,
|
|
398
|
+
jpgPath
|
|
399
|
+
};
|
|
315
400
|
} catch (err) {
|
|
316
401
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
317
402
|
const match = errorMsg.match(/\n\n([\s\S]+)/);
|
|
318
403
|
const details = match ? match[1] : errorMsg;
|
|
319
404
|
console.error(
|
|
320
|
-
`[remark-latex-compile] ${filePath}:${
|
|
405
|
+
`[remark-latex-compile] ${filePath}:${lineNumberStr}
|
|
321
406
|
${details}`
|
|
322
407
|
);
|
|
323
|
-
|
|
408
|
+
const jpgPath = canWriteJpg ? computeJpgPath(options.tempOutputDir, filePath, blockId, `${contentHash}--error`) : null;
|
|
409
|
+
if (jpgPath && !existsSync2(jpgPath)) {
|
|
410
|
+
await writeJpgError(
|
|
411
|
+
jpgPath,
|
|
412
|
+
`${filePath}:${lineNumberStr}`,
|
|
413
|
+
errorMsg
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
index,
|
|
418
|
+
parent,
|
|
419
|
+
result: null,
|
|
420
|
+
error: err,
|
|
421
|
+
hash: null,
|
|
422
|
+
jpgPath
|
|
423
|
+
};
|
|
324
424
|
}
|
|
325
425
|
})
|
|
326
426
|
);
|
|
@@ -342,6 +442,20 @@ ${details}`
|
|
|
342
442
|
);
|
|
343
443
|
options._fileHashMap.set(filePath, newHashes);
|
|
344
444
|
}
|
|
445
|
+
if (options.tempOutputDir && options._fileJpgPathMap) {
|
|
446
|
+
const newJpgPaths = new Set(
|
|
447
|
+
results.map((r) => r.jpgPath).filter(Boolean)
|
|
448
|
+
);
|
|
449
|
+
const oldJpgPaths = options._fileJpgPathMap.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
450
|
+
const staleJpgPaths = [...oldJpgPaths].filter((p) => !newJpgPaths.has(p));
|
|
451
|
+
for (const stalePath of staleJpgPaths) {
|
|
452
|
+
console.warn(
|
|
453
|
+
`[remark-latex-compile] Removing orphaned jpg: ${stalePath}`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
await Promise.all(staleJpgPaths.map((p) => rm(p, { force: true })));
|
|
457
|
+
options._fileJpgPathMap.set(filePath, newJpgPaths);
|
|
458
|
+
}
|
|
345
459
|
for (let i = results.length - 1; i >= 0; i--) {
|
|
346
460
|
const { index, parent, result } = results[i];
|
|
347
461
|
const { node } = nodes[i];
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "./chunk-3UMY7T6G.js";
|
|
10
10
|
import {
|
|
11
11
|
remarkLatexCompile
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-G2PJENXJ.js";
|
|
13
13
|
import {
|
|
14
14
|
astroNormalizePaths
|
|
15
15
|
} from "./chunk-TLOFSB33.js";
|
|
@@ -243,6 +243,7 @@ async function clearContentLayerCache(config) {
|
|
|
243
243
|
function astroLatexCompile(options) {
|
|
244
244
|
const referencedHashes = /* @__PURE__ */ new Set();
|
|
245
245
|
const fileHashMap = /* @__PURE__ */ new Map();
|
|
246
|
+
const fileJpgPathMap = /* @__PURE__ */ new Map();
|
|
246
247
|
return {
|
|
247
248
|
name: "astro-latex-compile",
|
|
248
249
|
hooks: {
|
|
@@ -255,7 +256,8 @@ function astroLatexCompile(options) {
|
|
|
255
256
|
const remarkOptions = {
|
|
256
257
|
...options,
|
|
257
258
|
_fileHashMap: options.removeOrphanedSvgs ? fileHashMap : void 0,
|
|
258
|
-
_referencedHashes: command === "build" && options.removeOrphanedSvgs ? referencedHashes : void 0
|
|
259
|
+
_referencedHashes: command === "build" && options.removeOrphanedSvgs ? referencedHashes : void 0,
|
|
260
|
+
_fileJpgPathMap: options.tempOutputDir ? fileJpgPathMap : void 0
|
|
259
261
|
};
|
|
260
262
|
updateConfig({
|
|
261
263
|
markdown: {
|
|
@@ -29,6 +29,15 @@ interface RemarkLatexCompileOptions {
|
|
|
29
29
|
* search (e.g. "src/latex/" or "src/latex//").
|
|
30
30
|
*/
|
|
31
31
|
texInputDirs?: string[];
|
|
32
|
+
/**
|
|
33
|
+
* When set, a JPEG copy of each compiled diagram is written here, mirroring
|
|
34
|
+
* the folder structure of `src/content/docs/`. Only blocks that carry a
|
|
35
|
+
* `blockid=<n>` meta tag produce a JPEG. Filename format:
|
|
36
|
+
* `<originating-file>--<blockid>--<hash>.jpg`.
|
|
37
|
+
* JPEGs are deleted automatically when their block is removed or its content
|
|
38
|
+
* changes. Intended for local inspection, not for publishing.
|
|
39
|
+
*/
|
|
40
|
+
tempOutputDir?: string;
|
|
32
41
|
/**
|
|
33
42
|
* @internal Populated by the Astro integration to track which hashes were
|
|
34
43
|
* referenced during a build, used for full orphan cleanup at build:done.
|
|
@@ -39,6 +48,11 @@ interface RemarkLatexCompileOptions {
|
|
|
39
48
|
* previous remark run. Used to delete stale SVGs when a block changes.
|
|
40
49
|
*/
|
|
41
50
|
_fileHashMap?: Map<string, Set<string>>;
|
|
51
|
+
/**
|
|
52
|
+
* @internal Maps each file path to the set of JPG paths it produced on the
|
|
53
|
+
* previous remark run. Used to delete stale JPGs when a block changes.
|
|
54
|
+
*/
|
|
55
|
+
_fileJpgPathMap?: Map<string, Set<string>>;
|
|
42
56
|
}
|
|
43
57
|
declare function remarkLatexCompile(options: RemarkLatexCompileOptions): (tree: Root, file: VFile) => Promise<void>;
|
|
44
58
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-cannoli-plugins",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.12.0",
|
|
5
5
|
"description": "Starlight plugins for automatic sidebar generation and link validation",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"glob": "^11.0.0",
|
|
75
75
|
"jsdom": "^29.0.2",
|
|
76
76
|
"minimatch": "^9.0.0",
|
|
77
|
+
"sharp": "^0.34.5",
|
|
77
78
|
"unist-util-visit": "^5.0.0",
|
|
78
79
|
"yaml": "^2.4.0"
|
|
79
80
|
},
|
|
@@ -123,7 +124,6 @@
|
|
|
123
124
|
"rehype-raw": "^7.0.0",
|
|
124
125
|
"remark-math": "^6.0.0",
|
|
125
126
|
"sass-embedded": "^1.97.3",
|
|
126
|
-
"sharp": "^0.34.2",
|
|
127
127
|
"tsup": "^8.3.0",
|
|
128
128
|
"tsx": "^4.21.0",
|
|
129
129
|
"typescript": "^5.9.3",
|