starlight-cannoli-plugins 2.11.0 → 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,7 +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/`. The filename format is `<originating-file>--<line-number>.jpg` (e.g. `tex-test.md--11.jpg`). Useful for previewing or post-processing diagrams outside the browser. JPEGs are deleted automatically when their corresponding block fails compilation or is removed. SVG output is unaffected — the JPEG is complementary.
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.
148
148
 
149
149
  ```ts
150
150
  astroLatexCompile({
@@ -190,13 +190,13 @@ function execProcess(command, args, options) {
190
190
 
191
191
  // src/plugins/astro-latex-compile/utils.ts
192
192
  var CONTENT_ROOT = "src/content/docs/";
193
- function computeJpgPath(tempOutputDir, filePath, lineNumber) {
193
+ function computeJpgPath(tempOutputDir, filePath, blockId, hash) {
194
194
  const normalized = filePath.replace(/\\/g, "/");
195
195
  const idx = normalized.indexOf(CONTENT_ROOT);
196
196
  const relativePath = idx !== -1 ? normalized.slice(idx + CONTENT_ROOT.length) : basename(normalized);
197
197
  const dir = dirname(relativePath);
198
198
  const filename = basename(relativePath);
199
- const jpgFilename = `${filename}--${lineNumber}.jpg`;
199
+ const jpgFilename = `${filename}--${blockId}--${hash}.jpg`;
200
200
  const base = resolve(tempOutputDir);
201
201
  return dir === "." ? join(base, jpgFilename) : join(base, dir, jpgFilename);
202
202
  }
@@ -204,6 +204,57 @@ async function writeJpgFromSvg(svgPath, jpgPath) {
204
204
  mkdirSync(dirname(jpgPath), { recursive: true });
205
205
  await sharp(svgPath).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
206
206
  }
207
+ function stripAnsi(text) {
208
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
209
+ }
210
+ function escapeXml(text) {
211
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
+ }
207
258
  function hashLatexCode(code) {
208
259
  const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
209
260
  return createHash("md5").update(normalized).digest("hex").slice(0, 16);
@@ -315,9 +366,10 @@ function remarkLatexCompile(options) {
315
366
  const filePath = file.path || "unknown";
316
367
  const results = await Promise.all(
317
368
  nodes.map(async ({ node, index, parent }) => {
318
- const lineNumber = node.position?.start.line;
319
- const lineNumberStr = lineNumber ?? "?";
320
- const jpgPath = options.tempOutputDir && lineNumber !== void 0 ? computeJpgPath(options.tempOutputDir, filePath, lineNumber) : null;
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;
321
373
  try {
322
374
  const result = await compileLatexToSvg(
323
375
  node.value,
@@ -330,6 +382,7 @@ function remarkLatexCompile(options) {
330
382
  );
331
383
  }
332
384
  options._referencedHashes?.add(result.hash);
385
+ const jpgPath = canWriteJpg ? computeJpgPath(options.tempOutputDir, filePath, blockId, contentHash) : null;
333
386
  if (jpgPath && !existsSync2(jpgPath)) {
334
387
  await writeJpgFromSvg(result.svgPath, jpgPath);
335
388
  console.log(
@@ -352,8 +405,13 @@ function remarkLatexCompile(options) {
352
405
  `[remark-latex-compile] ${filePath}:${lineNumberStr}
353
406
  ${details}`
354
407
  );
355
- if (jpgPath) {
356
- await rm(jpgPath, { force: true });
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
+ );
357
415
  }
358
416
  return {
359
417
  index,
@@ -361,7 +419,7 @@ ${details}`
361
419
  result: null,
362
420
  error: err,
363
421
  hash: null,
364
- jpgPath: null
422
+ jpgPath
365
423
  };
366
424
  }
367
425
  })
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-JIURY757.js";
12
+ } from "./chunk-G2PJENXJ.js";
13
13
  import {
14
14
  astroNormalizePaths
15
15
  } from "./chunk-TLOFSB33.js";
@@ -31,11 +31,11 @@ interface RemarkLatexCompileOptions {
31
31
  texInputDirs?: string[];
32
32
  /**
33
33
  * When set, a JPEG copy of each compiled diagram is written here, mirroring
34
- * the folder structure of `src/content/docs/`. Filename format:
35
- * `<originating-file>--<line-number>.jpg`.
36
- * JPEGs are deleted automatically when their corresponding block fails
37
- * compilation or is removed. The purpose of this directory is not for publishing these to the public,
38
- * but to provide an easier way to locally inspect the generated diagrams.
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
39
  */
40
40
  tempOutputDir?: string;
41
41
  /**
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  compileLatexToSvg,
3
3
  remarkLatexCompile
4
- } from "../chunk-JIURY757.js";
4
+ } from "../chunk-G2PJENXJ.js";
5
5
  import "../chunk-4VNS5WPM.js";
6
6
  export {
7
7
  compileLatexToSvg,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "2.11.0",
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",