starlight-cannoli-plugins 2.17.2 → 2.19.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
  - `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.
145
145
  - `svgClassname` (optional): Default CSS class(es) applied to every compiled SVG `<img>` element. Individual blocks can override this with a `class` meta tag value. The `tex-compiled` class is always present and cannot be overridden.
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. When any file in these directories changes, all cached SVGs are invalidated and recompiled.
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
+ - `debug` (optional): When set, a JPEG copy of each compiled diagram is written to `debug.outputDir` as a flat file list (no mirrored folder structure). Only blocks that carry a `blockid=<n>` meta tag produce a JPEG — blocks without it are ignored. The filename format is `<originating-file>--<blockid>-<8-char-dir-checksum>.jpg` (e.g. `tex-test--5-a1b2c3d4.jpg`), where the checksum is an MD5 hash of the originating file's directory, namespacing files so that same-named files from different directories don't collide. JPEGs are deleted automatically when their block is removed, its `blockid` changes, or its content changes, and the entire `debug.outputDir` is cleared on dev server startup. Every JPEG (re)write is also appended to `debug.logFile`, which rotates automatically once it grows too large (rolling backups `<logFile>-1.log`, `<logFile>-2.log`, etc.). SVG output is unaffected — the JPEG/log output is complementary and intended for local inspection.
148
148
 
149
149
  ```ts
150
150
  astroLatexCompile({
@@ -81,6 +81,22 @@ var pageSrcStyles = css`
81
81
  background: var(--sl-color-bg-nav);
82
82
  }
83
83
 
84
+ @keyframes page-src-glow {
85
+ 0% {
86
+ box-shadow: 0 0 0 0 transparent;
87
+ }
88
+ 30% {
89
+ box-shadow: 0 0 12px 4px var(--sl-color-accent);
90
+ }
91
+ 100% {
92
+ box-shadow: 0 0 0 0 transparent;
93
+ }
94
+ }
95
+
96
+ .page-src-action-bar__btn--glow {
97
+ animation: page-src-glow 1s ease-out;
98
+ }
99
+
84
100
  .page-src-action-bar__btn--loading {
85
101
  cursor: default;
86
102
  pointer-events: none;
@@ -9,18 +9,67 @@ import { join as join2, relative as relative2, resolve as resolve2 } from "path"
9
9
  import { visit, SKIP } from "unist-util-visit";
10
10
  import { MetaOptions } from "@expressive-code/core";
11
11
 
12
+ // src/plugins/utils/rotating-file-logger.ts
13
+ import { existsSync, mkdirSync, renameSync, rmSync, statSync } from "fs";
14
+ import { appendFile } from "fs/promises";
15
+ import { dirname, extname } from "path";
16
+ function rotatedPath(filePath, index) {
17
+ const ext = extname(filePath);
18
+ const base = filePath.slice(0, filePath.length - ext.length);
19
+ return `${base}-${index}${ext}`;
20
+ }
21
+ function formatTimestamp(date) {
22
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
23
+ const datePart = `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
24
+ const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())},${pad(date.getMilliseconds(), 3)}`;
25
+ return `${datePart} ${timePart}`;
26
+ }
27
+ function rotate(options) {
28
+ const { filePath, backupCount } = options;
29
+ const oldestPath = rotatedPath(filePath, backupCount);
30
+ if (existsSync(oldestPath)) {
31
+ rmSync(oldestPath);
32
+ }
33
+ for (let index = backupCount - 1; index >= 1; index--) {
34
+ const source = rotatedPath(filePath, index);
35
+ if (existsSync(source)) {
36
+ renameSync(source, rotatedPath(filePath, index + 1));
37
+ }
38
+ }
39
+ if (backupCount >= 1) {
40
+ renameSync(filePath, rotatedPath(filePath, 1));
41
+ } else {
42
+ rmSync(filePath);
43
+ }
44
+ }
45
+ var RotatingFileLogger = class {
46
+ options;
47
+ constructor(options) {
48
+ this.options = options;
49
+ mkdirSync(dirname(options.filePath), { recursive: true });
50
+ }
51
+ async write(message) {
52
+ const { filePath, maxSizeBytes, identifier } = this.options;
53
+ const line = `[${identifier}] ${formatTimestamp(/* @__PURE__ */ new Date())} ${message}${message.endsWith("\n") ? "" : "\n"}`;
54
+ if (existsSync(filePath) && statSync(filePath).size >= maxSizeBytes) {
55
+ rotate(this.options);
56
+ }
57
+ await appendFile(filePath, line);
58
+ }
59
+ };
60
+
12
61
  // src/plugins/astro-latex-compile/utils.ts
13
62
  import { createHash } from "crypto";
14
63
  import {
15
- existsSync,
16
- mkdirSync,
64
+ existsSync as existsSync2,
65
+ mkdirSync as mkdirSync2,
17
66
  readdirSync,
18
67
  readFileSync,
19
68
  writeFileSync,
20
- rmSync,
69
+ rmSync as rmSync2,
21
70
  mkdtempSync
22
71
  } from "fs";
23
- import { basename, dirname, join, relative, resolve } from "path";
72
+ import { basename, dirname as dirname2, extname as extname2, join, relative, resolve } from "path";
24
73
  import { tmpdir } from "os";
25
74
  import sharp from "sharp";
26
75
 
@@ -251,18 +300,18 @@ function html(literalValues, ...interpolatedValues) {
251
300
 
252
301
  // src/plugins/astro-latex-compile/utils.ts
253
302
  var CONTENT_ROOT = "src/content/docs/";
254
- function computeJpgPath(tempOutputDir, filePath, blockId) {
303
+ function computeJpgPath(outputDir, filePath, blockId) {
255
304
  const normalized = filePath.replace(/\\/g, "/");
256
305
  const idx = normalized.indexOf(CONTENT_ROOT);
257
306
  const relativePath = idx !== -1 ? normalized.slice(idx + CONTENT_ROOT.length) : basename(normalized);
258
- const dir = dirname(relativePath);
259
- const filename = basename(relativePath);
260
- const jpgFilename = `${filename}--${blockId}.jpg`;
261
- const base = resolve(tempOutputDir);
262
- return dir === "." ? join(base, jpgFilename) : join(base, dir, jpgFilename);
307
+ const dir = dirname2(relativePath);
308
+ const filename = basename(relativePath, extname2(relativePath));
309
+ const dirChecksum = createHash("md5").update(dir).digest("hex").slice(0, 8);
310
+ const jpgFilename = `${filename}--${blockId}-${dirChecksum}.jpg`;
311
+ return join(resolve(outputDir), jpgFilename);
263
312
  }
264
313
  async function writeJpgFromSvg(svgPath, jpgPath) {
265
- mkdirSync(dirname(jpgPath), { recursive: true });
314
+ mkdirSync2(dirname2(jpgPath), { recursive: true });
266
315
  await sharp(svgPath, { density: 300 }).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
267
316
  }
268
317
  function stripAnsi(text) {
@@ -313,7 +362,7 @@ async function writeJpgError(jpgPath, header, errorText) {
313
362
  <text x="${padding}" y="${padding + 40}" font-family="monospace" font-size="12" fill="#666666">${escapeXml(header)}</text>
314
363
  ${textLines}
315
364
  </svg>`;
316
- mkdirSync(dirname(jpgPath), { recursive: true });
365
+ mkdirSync2(dirname2(jpgPath), { recursive: true });
317
366
  await sharp(Buffer.from(svg)).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
318
367
  }
319
368
  function computeLineOffset(_latexCode) {
@@ -418,10 +467,10 @@ function buildLatexSource(latexCode) {
418
467
  async function compileLatexToSvg(latexCode, svgOutputDir, texInputDirs = [], inputsSalt = "") {
419
468
  const hash = hashLatexCode(latexCode, inputsSalt);
420
469
  const svgPath = join(svgOutputDir, `${hash}.svg`);
421
- if (existsSync(svgPath)) {
470
+ if (existsSync2(svgPath)) {
422
471
  return { hash, svgPath, wasCompiled: false };
423
472
  }
424
- mkdirSync(svgOutputDir, { recursive: true });
473
+ mkdirSync2(svgOutputDir, { recursive: true });
425
474
  const workDir = mkdtempSync(join(tmpdir(), "latex-compile-"));
426
475
  const texFile = join(workDir, "diagram.tex");
427
476
  const pdfFile = join(workDir, "diagram.pdf");
@@ -479,7 +528,7 @@ Error: ${errorOutput}`
479
528
  }
480
529
  } finally {
481
530
  try {
482
- rmSync(workDir, { recursive: true, force: true });
531
+ rmSync2(workDir, { recursive: true, force: true });
483
532
  } catch {
484
533
  }
485
534
  }
@@ -487,6 +536,8 @@ Error: ${errorOutput}`
487
536
  }
488
537
 
489
538
  // src/plugins/astro-latex-compile/index.ts
539
+ var DEBUG_LOG_MAX_SIZE_BYTES = 5 * 1024 * 1024;
540
+ var DEBUG_LOG_BACKUP_COUNT = 3;
490
541
  function getFrontmatterOffset(absoluteFilePath) {
491
542
  try {
492
543
  const original = readFileSync2(absoluteFilePath, "utf-8");
@@ -499,6 +550,12 @@ function getFrontmatterOffset(absoluteFilePath) {
499
550
  }
500
551
  function latexCompileTransformer(options) {
501
552
  const svgOutputDir = resolve2(options.svgOutputDir);
553
+ const debugLogger = options.debug ? new RotatingFileLogger({
554
+ identifier: "astro-latex-compile",
555
+ filePath: options.debug.logFile,
556
+ maxSizeBytes: DEBUG_LOG_MAX_SIZE_BYTES,
557
+ backupCount: DEBUG_LOG_BACKUP_COUNT
558
+ }) : null;
502
559
  return async function transformer(tree, file) {
503
560
  const nodes = [];
504
561
  visit(tree, "code", (node, index, parent) => {
@@ -516,7 +573,7 @@ function latexCompileTransformer(options) {
516
573
  nodes.map(async ({ node, index, parent }) => {
517
574
  const lineNumberStr = node.position?.start.line !== void 0 ? String(node.position.start.line + frontmatterOffset) : "?";
518
575
  const blockId = new MetaOptions(node.meta ?? "").getInteger("blockid");
519
- const jpgPath = options.tempOutputDir && blockId !== void 0 ? computeJpgPath(options.tempOutputDir, filePath, blockId) : null;
576
+ const jpgPath = options.debug && blockId !== void 0 ? computeJpgPath(options.debug.outputDir, filePath, blockId) : null;
520
577
  try {
521
578
  const result = await compileLatexToSvg(
522
579
  node.value,
@@ -532,8 +589,8 @@ function latexCompileTransformer(options) {
532
589
  options._referencedHashes?.add(result.hash);
533
590
  if (jpgPath) {
534
591
  await writeJpgFromSvg(result.svgPath, jpgPath);
535
- console.log(
536
- `[astro-latex-compile] ${relFilePath}:${lineNumberStr}: wrote ${jpgPath}`
592
+ await debugLogger?.write(
593
+ `${relFilePath}:${lineNumberStr}: wrote ${jpgPath}`
537
594
  );
538
595
  }
539
596
  return {
@@ -588,7 +645,7 @@ ${details}`
588
645
  );
589
646
  options._fileHashMap.set(filePath, newHashes);
590
647
  }
591
- if (options.tempOutputDir && options._fileJpgPathMap) {
648
+ if (options.debug && options._fileJpgPathMap) {
592
649
  const newJpgPaths = new Set(
593
650
  results.map((r) => r.jpgPath).filter(Boolean)
594
651
  );
package/dist/index.js CHANGED
@@ -9,14 +9,14 @@ import {
9
9
  } from "./chunk-3UMY7T6G.js";
10
10
  import {
11
11
  latexCompileTransformer
12
- } from "./chunk-FDEHNCBK.js";
12
+ } from "./chunk-GVS36MVT.js";
13
13
  import {
14
14
  astroNormalizePaths
15
15
  } from "./chunk-TLOFSB33.js";
16
16
  import "./chunk-LJWSZSPB.js";
17
17
  import {
18
18
  syncDocsToPublic
19
- } from "./chunk-BM7LMGAH.js";
19
+ } from "./chunk-EKPDDOOG.js";
20
20
  import {
21
21
  parseFrontmatter
22
22
  } from "./chunk-EEPQVOZC.js";
@@ -311,12 +311,24 @@ function astroLatexCompile(options) {
311
311
  if (command === "build") {
312
312
  await clearContentLayerCache(config);
313
313
  }
314
+ if (command === "dev" && options.debug) {
315
+ const debugOutputDir = resolve2(options.debug.outputDir);
316
+ let entries;
317
+ try {
318
+ entries = await readdir(debugOutputDir);
319
+ } catch {
320
+ entries = [];
321
+ }
322
+ await Promise.all(
323
+ entries.filter((f) => f.endsWith(".jpg")).map((f) => rm(join3(debugOutputDir, f), { force: true }))
324
+ );
325
+ }
314
326
  const existingPlugins = Array.isArray(config.markdown?.remarkPlugins) ? config.markdown.remarkPlugins.filter(Boolean) : [];
315
327
  const pluginOptions = {
316
328
  ...options,
317
329
  _fileHashMap: options.removeOrphanedSvgs ? fileHashMap : void 0,
318
330
  _referencedHashes: command === "build" && options.removeOrphanedSvgs ? referencedHashes : void 0,
319
- _fileJpgPathMap: options.tempOutputDir ? fileJpgPathMap : void 0
331
+ _fileJpgPathMap: options.debug ? fileJpgPathMap : void 0
320
332
  };
321
333
  updateConfig({
322
334
  markdown: {
@@ -36,14 +36,20 @@ interface LatexCompilePluginOptions {
36
36
  */
37
37
  texInputDirs?: string[];
38
38
  /**
39
- * When set, a JPEG copy of each compiled diagram is written here, mirroring
40
- * the folder structure of `src/content/docs/`. Only blocks that carry a
41
- * `blockid=<n>` meta tag produce a JPEG. Filename format:
42
- * `<originating-file>--<blockid>--<hash>.jpg`.
39
+ * When set, a JPEG copy of each compiled diagram is written to a flat
40
+ * directory. Only blocks that carry a `blockid=<n>` meta tag produce a
41
+ * JPEG. Filename format: `<originating-file>-<8-char-dir-checksum>--<blockid>.jpg`,
42
+ * where the checksum namespaces files originating from different
43
+ * directories to avoid collisions between files with the same name.
43
44
  * JPEGs are deleted automatically when their block is removed or its content
44
45
  * changes. Intended for local inspection, not for publishing.
45
46
  */
46
- tempOutputDir?: string;
47
+ debug?: {
48
+ /** Directory where debug JPEGs are written, as a flat file list. */
49
+ outputDir: string;
50
+ /** Path to a log file that records every JPEG (re)write. Rotates by size. */
51
+ logFile: string;
52
+ };
47
53
  /**
48
54
  * @internal Populated by the Astro integration to track which hashes were
49
55
  * referenced during a build, used for full orphan cleanup at build:done.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  compileLatexToSvg,
3
3
  latexCompileTransformer
4
- } from "../chunk-FDEHNCBK.js";
4
+ } from "../chunk-GVS36MVT.js";
5
5
  import "../chunk-7D2KLZGE.js";
6
6
  import "../chunk-4VNS5WPM.js";
7
7
  export {
@@ -126,6 +126,12 @@ function createActionBar(options) {
126
126
  });
127
127
  bar.appendChild(menuBtn);
128
128
  bar.appendChild(menu);
129
+ menuBtn.classList.add("page-src-action-bar__btn--glow");
130
+ menuBtn.addEventListener(
131
+ "animationend",
132
+ () => menuBtn.classList.remove("page-src-action-bar__btn--glow"),
133
+ { once: true }
134
+ );
129
135
  return bar;
130
136
  }
131
137
  function initPageActionBar(options) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  syncDocsToPublic
3
- } from "../chunk-BM7LMGAH.js";
3
+ } from "../chunk-EKPDDOOG.js";
4
4
  import "../chunk-EEPQVOZC.js";
5
5
  import "../chunk-7D2KLZGE.js";
6
6
  import "../chunk-4VNS5WPM.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "2.17.2",
4
+ "version": "2.19.0",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",