starlight-cannoli-plugins 2.11.0 → 2.12.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.
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({
@@ -1,5 +1,4 @@
1
1
  // src/plugins/astro-latex-compile/index.ts
2
- import { existsSync as existsSync2 } from "fs";
3
2
  import { rm } from "fs/promises";
4
3
  import { join as join2, resolve as resolve2 } from "path";
5
4
  import { visit, SKIP } from "unist-util-visit";
@@ -190,13 +189,13 @@ function execProcess(command, args, options) {
190
189
 
191
190
  // src/plugins/astro-latex-compile/utils.ts
192
191
  var CONTENT_ROOT = "src/content/docs/";
193
- function computeJpgPath(tempOutputDir, filePath, lineNumber) {
192
+ function computeJpgPath(tempOutputDir, filePath, blockId) {
194
193
  const normalized = filePath.replace(/\\/g, "/");
195
194
  const idx = normalized.indexOf(CONTENT_ROOT);
196
195
  const relativePath = idx !== -1 ? normalized.slice(idx + CONTENT_ROOT.length) : basename(normalized);
197
196
  const dir = dirname(relativePath);
198
197
  const filename = basename(relativePath);
199
- const jpgFilename = `${filename}--${lineNumber}.jpg`;
198
+ const jpgFilename = `${filename}--${blockId}.jpg`;
200
199
  const base = resolve(tempOutputDir);
201
200
  return dir === "." ? join(base, jpgFilename) : join(base, dir, jpgFilename);
202
201
  }
@@ -204,6 +203,57 @@ async function writeJpgFromSvg(svgPath, jpgPath) {
204
203
  mkdirSync(dirname(jpgPath), { recursive: true });
205
204
  await sharp(svgPath).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
206
205
  }
206
+ function stripAnsi(text) {
207
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
208
+ }
209
+ function escapeXml(text) {
210
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
211
+ }
212
+ function wrapText(text, maxChars) {
213
+ const lines = [];
214
+ for (const raw of text.split("\n")) {
215
+ if (raw.length <= maxChars) {
216
+ lines.push(raw);
217
+ } else {
218
+ let remaining = raw;
219
+ while (remaining.length > maxChars) {
220
+ lines.push(remaining.slice(0, maxChars));
221
+ remaining = remaining.slice(maxChars);
222
+ }
223
+ if (remaining) lines.push(remaining);
224
+ }
225
+ }
226
+ return lines;
227
+ }
228
+ async function writeJpgError(jpgPath, header, errorText) {
229
+ const MAX_LINES = 40;
230
+ const clean = stripAnsi(errorText);
231
+ const sourceIdx = clean.indexOf("LaTeX source:");
232
+ const summary = (sourceIdx !== -1 ? clean.slice(0, sourceIdx) : clean).trimEnd();
233
+ let lines = wrapText(summary, 90);
234
+ if (lines.length > MAX_LINES) {
235
+ lines = lines.slice(0, MAX_LINES);
236
+ lines.push("...");
237
+ }
238
+ const fontSize = 13;
239
+ const lineHeight = 18;
240
+ const padding = 20;
241
+ const headerHeight = 55;
242
+ const width = 800;
243
+ const height = padding * 2 + headerHeight + lines.length * lineHeight;
244
+ const textLines = lines.map(
245
+ (line, i) => `<text x="${padding}" y="${padding + headerHeight + i * lineHeight}" font-family="monospace" font-size="${fontSize}" fill="#333333">${escapeXml(line)}</text>`
246
+ ).join("\n ");
247
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
248
+ <rect width="${width}" height="${height}" fill="white"/>
249
+ <rect x="0" y="0" width="${width}" height="4" fill="#cc0000"/>
250
+ <text x="${padding}" y="${padding + 20}" font-family="monospace" font-size="16" font-weight="bold" fill="#cc0000">LaTeX Compilation Error</text>
251
+ <text x="${padding}" y="${padding + 40}" font-family="monospace" font-size="12" fill="#666666">${escapeXml(header)}</text>
252
+ ${textLines}
253
+ </svg>`;
254
+ mkdirSync(dirname(jpgPath), { recursive: true });
255
+ await sharp(Buffer.from(svg)).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
256
+ }
207
257
  function hashLatexCode(code) {
208
258
  const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
209
259
  return createHash("md5").update(normalized).digest("hex").slice(0, 16);
@@ -315,9 +365,9 @@ function remarkLatexCompile(options) {
315
365
  const filePath = file.path || "unknown";
316
366
  const results = await Promise.all(
317
367
  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;
368
+ const lineNumberStr = node.position?.start.line ?? "?";
369
+ const blockId = new MetaOptions(node.meta ?? "").getInteger("blockid");
370
+ const jpgPath = options.tempOutputDir && blockId !== void 0 ? computeJpgPath(options.tempOutputDir, filePath, blockId) : null;
321
371
  try {
322
372
  const result = await compileLatexToSvg(
323
373
  node.value,
@@ -330,7 +380,7 @@ function remarkLatexCompile(options) {
330
380
  );
331
381
  }
332
382
  options._referencedHashes?.add(result.hash);
333
- if (jpgPath && !existsSync2(jpgPath)) {
383
+ if (jpgPath) {
334
384
  await writeJpgFromSvg(result.svgPath, jpgPath);
335
385
  console.log(
336
386
  `[remark-latex-compile] ${filePath}:${lineNumberStr}: wrote ${jpgPath}`
@@ -353,7 +403,11 @@ function remarkLatexCompile(options) {
353
403
  ${details}`
354
404
  );
355
405
  if (jpgPath) {
356
- await rm(jpgPath, { force: true });
406
+ await writeJpgError(
407
+ jpgPath,
408
+ `${filePath}:${lineNumberStr}`,
409
+ errorMsg
410
+ );
357
411
  }
358
412
  return {
359
413
  index,
@@ -361,7 +415,7 @@ ${details}`
361
415
  result: null,
362
416
  error: err,
363
417
  hash: null,
364
- jpgPath: null
418
+ jpgPath
365
419
  };
366
420
  }
367
421
  })
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-OLF3SRCP.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-OLF3SRCP.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.1",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",