starlight-cannoli-plugins 2.12.0 → 2.13.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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseFrontmatter
3
- } from "./chunk-NCXV367P.js";
3
+ } from "./chunk-WFN7OFMJ.js";
4
4
 
5
5
  // src/plugins/starlight-index-only-sidebar/index.ts
6
6
  import { join as join2 } from "path";
@@ -22,7 +22,12 @@ function expressiveCodeEmphasis() {
22
22
  if (terms.length === 0) return;
23
23
  for (const line of context.codeBlock.getLines()) {
24
24
  for (const term of terms) {
25
- const regex = new RegExp(`\\b${escapeRegex(term)}\\b`, "g");
25
+ const left = /\w/.test(term[0]) ? "\\b" : "(?<!\\S)";
26
+ const right = /\w/.test(term[term.length - 1]) ? "\\b" : "(?!\\S)";
27
+ const regex = new RegExp(
28
+ `${left}${escapeRegex(term)}${right}`,
29
+ "g"
30
+ );
26
31
  for (const match of line.text.matchAll(regex)) {
27
32
  line.addAnnotation(
28
33
  new EmphasisAnnotation({
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  parseFrontmatter
3
- } from "./chunk-NCXV367P.js";
3
+ } from "./chunk-WFN7OFMJ.js";
4
4
 
5
5
  // src/plugins/astro-sync-docs-to-public/index.ts
6
6
  import { fileURLToPath } from "url";
@@ -1,7 +1,11 @@
1
+ import {
2
+ parseFrontmatter
3
+ } from "./chunk-C2VXRQOK.js";
4
+
1
5
  // src/plugins/astro-latex-compile/index.ts
2
- import { existsSync as existsSync2 } from "fs";
6
+ import { readFileSync as readFileSync2 } from "fs";
3
7
  import { rm } from "fs/promises";
4
- import { join as join2, resolve as resolve2 } from "path";
8
+ import { join as join2, relative as relative2, resolve as resolve2 } from "path";
5
9
  import { visit, SKIP } from "unist-util-visit";
6
10
  import { MetaOptions } from "@expressive-code/core";
7
11
 
@@ -10,11 +14,13 @@ import { createHash } from "crypto";
10
14
  import {
11
15
  existsSync,
12
16
  mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
13
19
  writeFileSync,
14
20
  rmSync,
15
21
  mkdtempSync
16
22
  } from "fs";
17
- import { basename, dirname, join, resolve } from "path";
23
+ import { basename, dirname, join, relative, resolve } from "path";
18
24
  import { tmpdir } from "os";
19
25
  import sharp from "sharp";
20
26
 
@@ -188,15 +194,70 @@ function execProcess(command, args, options) {
188
194
  });
189
195
  }
190
196
 
197
+ // src/plugins/utils/html-builder.ts
198
+ import { JSDOM } from "jsdom";
199
+ var { document } = new JSDOM("").window;
200
+ var HtmlSanitizer = {
201
+ tempElement: document.createElement("div"),
202
+ sanitize(htmlString) {
203
+ this.tempElement.textContent = htmlString;
204
+ return this.tempElement.innerHTML;
205
+ }
206
+ };
207
+ var HtmlString = class extends String {
208
+ element = null;
209
+ constructor(value) {
210
+ super(value);
211
+ }
212
+ asElement() {
213
+ if (this.element !== null) {
214
+ return this.element;
215
+ }
216
+ const temp = document.createElement("div");
217
+ temp.innerHTML = this.valueOf();
218
+ if (temp.childElementCount > 1) {
219
+ throw new Error("html template does not accept more than 1 element");
220
+ }
221
+ const child = temp.firstElementChild;
222
+ if (child === null) {
223
+ throw new Error("html template produced no elements");
224
+ }
225
+ this.element = child;
226
+ return this.element;
227
+ }
228
+ };
229
+ function html(literalValues, ...interpolatedValues) {
230
+ let result = "";
231
+ interpolatedValues.forEach((currentInterpolatedVal, idx) => {
232
+ const literalVal = literalValues[idx];
233
+ let interpolatedVal = "";
234
+ if (Array.isArray(currentInterpolatedVal)) {
235
+ interpolatedVal = currentInterpolatedVal.join("\n");
236
+ } else if (typeof currentInterpolatedVal !== "boolean") {
237
+ interpolatedVal = currentInterpolatedVal.toString();
238
+ }
239
+ const isSanitize = !literalVal.endsWith("$");
240
+ if (isSanitize) {
241
+ result += literalVal;
242
+ result += HtmlSanitizer.sanitize(interpolatedVal);
243
+ } else {
244
+ result += literalVal.slice(0, -1);
245
+ result += interpolatedVal;
246
+ }
247
+ });
248
+ result += literalValues.slice(-1);
249
+ return new HtmlString(result);
250
+ }
251
+
191
252
  // src/plugins/astro-latex-compile/utils.ts
192
253
  var CONTENT_ROOT = "src/content/docs/";
193
- function computeJpgPath(tempOutputDir, filePath, blockId, hash) {
254
+ function computeJpgPath(tempOutputDir, filePath, blockId) {
194
255
  const normalized = filePath.replace(/\\/g, "/");
195
256
  const idx = normalized.indexOf(CONTENT_ROOT);
196
257
  const relativePath = idx !== -1 ? normalized.slice(idx + CONTENT_ROOT.length) : basename(normalized);
197
258
  const dir = dirname(relativePath);
198
259
  const filename = basename(relativePath);
199
- const jpgFilename = `${filename}--${blockId}--${hash}.jpg`;
260
+ const jpgFilename = `${filename}--${blockId}.jpg`;
200
261
  const base = resolve(tempOutputDir);
201
262
  return dir === "." ? join(base, jpgFilename) : join(base, dir, jpgFilename);
202
263
  }
@@ -255,33 +316,107 @@ async function writeJpgError(jpgPath, header, errorText) {
255
316
  mkdirSync(dirname(jpgPath), { recursive: true });
256
317
  await sharp(Buffer.from(svg)).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
257
318
  }
258
- function hashLatexCode(code) {
319
+ function computeLineOffset(_latexCode) {
320
+ return 0;
321
+ }
322
+ function buildErrorHtml(header, errorMsg, latexCode) {
323
+ const clean = stripAnsi(errorMsg);
324
+ const sourceIdx = clean.indexOf("LaTeX source:");
325
+ const summary = (sourceIdx !== -1 ? clean.slice(0, sourceIdx) : clean).trimEnd();
326
+ const compiledLineNums = [];
327
+ const linePattern = /Error \(line (\d+)\)/g;
328
+ let m;
329
+ while ((m = linePattern.exec(summary)) !== null) {
330
+ compiledLineNums.push(parseInt(m[1], 10));
331
+ }
332
+ const offset = computeLineOffset(latexCode);
333
+ const errorLineSet = new Set(
334
+ compiledLineNums.map((n) => n - offset).filter((n) => n > 0)
335
+ );
336
+ const codeLines = latexCode.split("\n");
337
+ const lineWidth = String(codeLines.length).length;
338
+ const summaryLines = summary.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => html`<div>${line}</div>`);
339
+ const codeBlock = new HtmlString(
340
+ codeLines.map((line, i) => {
341
+ const lineNum = i + 1;
342
+ const isError = errorLineSet.has(lineNum);
343
+ const gutter = String(lineNum).padStart(lineWidth);
344
+ const bg = isError ? "background:var(--cannoli-error-low);" : "";
345
+ return html`<span style="${bg}display:block;padding:0 14px"
346
+ ><span style="color:var(--sl-color-gray-3);user-select:none"
347
+ >${gutter} │ </span
348
+ >${line}</span
349
+ >`.toString();
350
+ }).join("")
351
+ );
352
+ return html`<div
353
+ class="not-content"
354
+ style="border:1px solid var(--cannoli-error-low);border-radius:6px;overflow:hidden;margin:1.5em 0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:1.5;display:flex;flex-direction:column"
355
+ >
356
+ <div
357
+ style="background:var(--cannoli-error);color:var(--sl-color-black);padding:8px 14px;font-weight:700"
358
+ >
359
+ &#9888;&nbsp;LaTeX Compilation Error &mdash; ${header}
360
+ </div>
361
+ <div
362
+ style="background:var(--cannoli-error-low);color:var(--cannoli-error-high);padding:10px 14px;border-bottom:1px solid var(--cannoli-error)"
363
+ >
364
+ $${summaryLines}
365
+ </div>
366
+ <pre
367
+ style="margin:0;background:var(--sl-color-gray-6);color:var(--sl-color-text);overflow-x:auto"
368
+ ><code style="display:block;padding:8px 0;font-size:12px">$${codeBlock}</code></pre>
369
+ </div>`.toString();
370
+ }
371
+ function collectTexInputFiles(dir, recursive) {
372
+ const files = [];
373
+ try {
374
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
375
+ const full = join(dir, entry.name);
376
+ if (entry.isDirectory() && recursive) {
377
+ files.push(...collectTexInputFiles(full, recursive));
378
+ } else if (entry.isFile()) {
379
+ files.push(full);
380
+ }
381
+ }
382
+ } catch {
383
+ }
384
+ return files.sort();
385
+ }
386
+ function computeTexInputDirsSalt(texInputDirs) {
387
+ if (texInputDirs.length === 0) return "";
388
+ const hash = createHash("md5");
389
+ let hasAnyFile = false;
390
+ for (const dir of texInputDirs) {
391
+ const recursive = dir.endsWith("//");
392
+ const resolved = resolve(dir.endsWith("/") ? dir.slice(0, -1) : dir);
393
+ for (const filePath of collectTexInputFiles(resolved, recursive)) {
394
+ try {
395
+ hash.update(relative(process.cwd(), filePath));
396
+ hash.update(readFileSync(filePath));
397
+ hasAnyFile = true;
398
+ } catch {
399
+ }
400
+ }
401
+ }
402
+ return hasAnyFile ? hash.digest("hex").slice(0, 16) : "";
403
+ }
404
+ function hashLatexCode(code, salt = "") {
259
405
  const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
260
- return createHash("md5").update(normalized).digest("hex").slice(0, 16);
406
+ const h = createHash("md5").update(normalized);
407
+ if (salt) h.update(salt);
408
+ return h.digest("hex").slice(0, 16);
261
409
  }
262
410
  function buildLatexSource(latexCode) {
263
411
  if (latexCode.includes("\\documentclass") && latexCode.includes("\\begin{document}")) {
264
412
  return latexCode.trim();
265
413
  }
266
- const separatorRegex = /%[ \t]*===/;
267
- const parts = latexCode.split(separatorRegex);
268
- let preamble = "";
269
- let content = latexCode.trim();
270
- if (parts.length === 2) {
271
- preamble = parts[0].trim();
272
- content = parts[1].trim();
273
- }
274
- return [
275
- "\\documentclass[border=5pt]{standalone}",
276
- preamble,
277
- "\\begin{document}",
278
- "\\Large",
279
- content,
280
- "\\end{document}"
281
- ].join("\n");
414
+ throw new Error(
415
+ `[remark-latex-compile] Code block is not a complete LaTeX document. Blocks must contain both \\documentclass and \\begin{document}.`
416
+ );
282
417
  }
283
- async function compileLatexToSvg(latexCode, svgOutputDir, texInputDirs = []) {
284
- const hash = hashLatexCode(latexCode);
418
+ async function compileLatexToSvg(latexCode, svgOutputDir, texInputDirs = [], inputsSalt = "") {
419
+ const hash = hashLatexCode(latexCode, inputsSalt);
285
420
  const svgPath = join(svgOutputDir, `${hash}.svg`);
286
421
  if (existsSync(svgPath)) {
287
422
  return { hash, svgPath, wasCompiled: false };
@@ -352,6 +487,16 @@ Error: ${errorOutput}`
352
487
  }
353
488
 
354
489
  // src/plugins/astro-latex-compile/index.ts
490
+ function getFrontmatterOffset(absoluteFilePath) {
491
+ try {
492
+ const original = readFileSync2(absoluteFilePath, "utf-8");
493
+ const { rawFrontmatter } = parseFrontmatter(original);
494
+ if (!rawFrontmatter) return 0;
495
+ return rawFrontmatter.split("\n").length - 1 + 2;
496
+ } catch {
497
+ return 0;
498
+ }
499
+ }
355
500
  function remarkLatexCompile(options) {
356
501
  const svgOutputDir = resolve2(options.svgOutputDir);
357
502
  return async function transformer(tree, file) {
@@ -364,29 +509,31 @@ function remarkLatexCompile(options) {
364
509
  });
365
510
  if (nodes.length === 0) return;
366
511
  const filePath = file.path || "unknown";
512
+ const relFilePath = filePath !== "unknown" ? relative2(process.cwd(), filePath) : "unknown";
513
+ const frontmatterOffset = filePath !== "unknown" ? getFrontmatterOffset(filePath) : 0;
514
+ const inputsSalt = computeTexInputDirsSalt(options.texInputDirs ?? []);
367
515
  const results = await Promise.all(
368
516
  nodes.map(async ({ node, index, parent }) => {
369
- const lineNumberStr = node.position?.start.line ?? "?";
517
+ const lineNumberStr = node.position?.start.line !== void 0 ? String(node.position.start.line + frontmatterOffset) : "?";
370
518
  const blockId = new MetaOptions(node.meta ?? "").getInteger("blockid");
371
- const contentHash = hashLatexCode(node.value);
372
- const canWriteJpg = !!options.tempOutputDir && blockId !== void 0;
519
+ const jpgPath = options.tempOutputDir && blockId !== void 0 ? computeJpgPath(options.tempOutputDir, filePath, blockId) : null;
373
520
  try {
374
521
  const result = await compileLatexToSvg(
375
522
  node.value,
376
523
  svgOutputDir,
377
- options.texInputDirs ?? []
524
+ options.texInputDirs ?? [],
525
+ inputsSalt
378
526
  );
379
527
  if (result.wasCompiled) {
380
528
  console.log(
381
- `[remark-latex-compile] ${filePath}:${lineNumberStr}: compiled ${result.hash}.svg`
529
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}: compiled ${result.hash}.svg`
382
530
  );
383
531
  }
384
532
  options._referencedHashes?.add(result.hash);
385
- const jpgPath = canWriteJpg ? computeJpgPath(options.tempOutputDir, filePath, blockId, contentHash) : null;
386
- if (jpgPath && !existsSync2(jpgPath)) {
533
+ if (jpgPath) {
387
534
  await writeJpgFromSvg(result.svgPath, jpgPath);
388
535
  console.log(
389
- `[remark-latex-compile] ${filePath}:${lineNumberStr}: wrote ${jpgPath}`
536
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}: wrote ${jpgPath}`
390
537
  );
391
538
  }
392
539
  return {
@@ -402,14 +549,13 @@ function remarkLatexCompile(options) {
402
549
  const match = errorMsg.match(/\n\n([\s\S]+)/);
403
550
  const details = match ? match[1] : errorMsg;
404
551
  console.error(
405
- `[remark-latex-compile] ${filePath}:${lineNumberStr}
552
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}
406
553
  ${details}`
407
554
  );
408
- const jpgPath = canWriteJpg ? computeJpgPath(options.tempOutputDir, filePath, blockId, `${contentHash}--error`) : null;
409
- if (jpgPath && !existsSync2(jpgPath)) {
555
+ if (jpgPath) {
410
556
  await writeJpgError(
411
557
  jpgPath,
412
- `${filePath}:${lineNumberStr}`,
558
+ `${relFilePath}:${lineNumberStr}`,
413
559
  errorMsg
414
560
  );
415
561
  }
@@ -457,9 +603,22 @@ ${details}`
457
603
  options._fileJpgPathMap.set(filePath, newJpgPaths);
458
604
  }
459
605
  for (let i = results.length - 1; i >= 0; i--) {
460
- const { index, parent, result } = results[i];
606
+ const { index, parent, result, error } = results[i];
461
607
  const { node } = nodes[i];
462
- if (!result) continue;
608
+ if (!result) {
609
+ const lineNumberStr = node.position?.start.line !== void 0 ? String(node.position.start.line + frontmatterOffset) : "?";
610
+ const errorMsg = error instanceof Error ? error.message : String(error);
611
+ const errorNode = {
612
+ type: "html",
613
+ value: buildErrorHtml(
614
+ `${relFilePath}:${lineNumberStr}`,
615
+ errorMsg,
616
+ node.value
617
+ )
618
+ };
619
+ parent.children.splice(index, 1, errorNode);
620
+ continue;
621
+ }
463
622
  const metaOptions = new MetaOptions(node.meta ?? "");
464
623
  const customClasses = metaOptions.getString("class")?.split(/\s+/).filter(Boolean) ?? [];
465
624
  const altText = metaOptions.getString("alt") ?? "LaTeX diagram";
@@ -0,0 +1,28 @@
1
+ import {
2
+ parseFrontmatter
3
+ } from "./chunk-C2VXRQOK.js";
4
+
5
+ // src/plugins/utils/workspace-utils.ts
6
+ import * as fs from "fs";
7
+ function parseFrontmatter2(filePath) {
8
+ let content;
9
+ try {
10
+ content = fs.readFileSync(filePath, "utf-8");
11
+ } catch {
12
+ return {};
13
+ }
14
+ try {
15
+ const { frontmatter } = parseFrontmatter(content);
16
+ return frontmatter;
17
+ } catch (err) {
18
+ console.warn(
19
+ `[parseFrontmatter] Failed to parse frontmatter in ${filePath}:`,
20
+ err
21
+ );
22
+ return {};
23
+ }
24
+ }
25
+
26
+ export {
27
+ parseFrontmatter2 as parseFrontmatter
28
+ };
package/dist/index.js CHANGED
@@ -1,27 +1,29 @@
1
1
  import {
2
2
  expressiveCodeEmphasis
3
- } from "./chunk-TRGFYY3Y.js";
3
+ } from "./chunk-O2TZASB7.js";
4
4
  import {
5
5
  starlightIndexOnlySidebar
6
- } from "./chunk-3GUF4GRX.js";
6
+ } from "./chunk-JFHWCFHF.js";
7
7
  import {
8
8
  rehypeValidateLinks
9
9
  } from "./chunk-3UMY7T6G.js";
10
10
  import {
11
11
  remarkLatexCompile
12
- } from "./chunk-G2PJENXJ.js";
12
+ } from "./chunk-WFEFWQAK.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-K65RRBFX.js";
19
+ } from "./chunk-QPHWDN2I.js";
20
+ import {
21
+ parseFrontmatter
22
+ } from "./chunk-WFN7OFMJ.js";
20
23
  import {
21
- parseFrontmatter,
22
24
  remarkParse,
23
25
  unified
24
- } from "./chunk-NCXV367P.js";
26
+ } from "./chunk-C2VXRQOK.js";
25
27
  import {
26
28
  starlightDomPatches
27
29
  } from "./chunk-PPNIMJPX.js";
@@ -14,7 +14,7 @@ interface CompilationResult {
14
14
  * @returns Result object with hash, svgPath, and whether compilation occurred
15
15
  * @throws Error if compilation fails
16
16
  */
17
- declare function compileLatexToSvg(latexCode: string, svgOutputDir: string, texInputDirs?: string[]): Promise<CompilationResult>;
17
+ declare function compileLatexToSvg(latexCode: string, svgOutputDir: string, texInputDirs?: string[], inputsSalt?: string): Promise<CompilationResult>;
18
18
 
19
19
  interface RemarkLatexCompileOptions {
20
20
  /**
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  compileLatexToSvg,
3
3
  remarkLatexCompile
4
- } from "../chunk-G2PJENXJ.js";
4
+ } from "../chunk-WFEFWQAK.js";
5
+ import "../chunk-C2VXRQOK.js";
5
6
  import "../chunk-4VNS5WPM.js";
6
7
  export {
7
8
  compileLatexToSvg,
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  syncDocsToPublic
3
- } from "../chunk-K65RRBFX.js";
4
- import "../chunk-NCXV367P.js";
3
+ } from "../chunk-QPHWDN2I.js";
4
+ import "../chunk-WFN7OFMJ.js";
5
+ import "../chunk-C2VXRQOK.js";
5
6
  import "../chunk-4VNS5WPM.js";
6
7
  export {
7
8
  syncDocsToPublic
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  expressiveCodeEmphasis
3
- } from "../chunk-TRGFYY3Y.js";
3
+ } from "../chunk-O2TZASB7.js";
4
4
  import "../chunk-4VNS5WPM.js";
5
5
  export {
6
6
  expressiveCodeEmphasis
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  starlightIndexOnlySidebar
3
- } from "../chunk-3GUF4GRX.js";
4
- import "../chunk-NCXV367P.js";
3
+ } from "../chunk-JFHWCFHF.js";
4
+ import "../chunk-WFN7OFMJ.js";
5
+ import "../chunk-C2VXRQOK.js";
5
6
  import "../chunk-4VNS5WPM.js";
6
7
  export {
7
8
  starlightIndexOnlySidebar
@@ -2,6 +2,21 @@
2
2
 
3
3
  $DARK_MODE_ROTATE: 178deg;
4
4
 
5
+ /********** Custom Error Colors **********/
6
+ // Starlight's --sl-color-red-* sit at hsl(349°) which reads as pink/magenta.
7
+ // These replacements use true red (0°) for a more conventional error appearance.
8
+ :root {
9
+ --cannoli-error-low: hsl(0, 70%, 94%);
10
+ --cannoli-error: hsl(0, 72%, 42%);
11
+ --cannoli-error-high: hsl(0, 72%, 28%);
12
+ }
13
+
14
+ [data-theme="dark"] {
15
+ --cannoli-error-low: hsl(0, 55%, 16%);
16
+ --cannoli-error: hsl(0, 75%, 60%);
17
+ --cannoli-error-high: hsl(0, 80%, 82%);
18
+ }
19
+
5
20
  .sl-container:where(.astro-7nkwcw3z) {
6
21
  max-width: 50rem;
7
22
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "starlight-cannoli-plugins",
3
3
  "type": "module",
4
- "version": "2.12.0",
4
+ "version": "2.13.0",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",