starlight-cannoli-plugins 2.12.1 → 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,6 +1,11 @@
1
+ import {
2
+ parseFrontmatter
3
+ } from "./chunk-C2VXRQOK.js";
4
+
1
5
  // src/plugins/astro-latex-compile/index.ts
6
+ import { readFileSync as readFileSync2 } from "fs";
2
7
  import { rm } from "fs/promises";
3
- import { join as join2, resolve as resolve2 } from "path";
8
+ import { join as join2, relative as relative2, resolve as resolve2 } from "path";
4
9
  import { visit, SKIP } from "unist-util-visit";
5
10
  import { MetaOptions } from "@expressive-code/core";
6
11
 
@@ -9,11 +14,13 @@ import { createHash } from "crypto";
9
14
  import {
10
15
  existsSync,
11
16
  mkdirSync,
17
+ readdirSync,
18
+ readFileSync,
12
19
  writeFileSync,
13
20
  rmSync,
14
21
  mkdtempSync
15
22
  } from "fs";
16
- import { basename, dirname, join, resolve } from "path";
23
+ import { basename, dirname, join, relative, resolve } from "path";
17
24
  import { tmpdir } from "os";
18
25
  import sharp from "sharp";
19
26
 
@@ -187,6 +194,61 @@ function execProcess(command, args, options) {
187
194
  });
188
195
  }
189
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
+
190
252
  // src/plugins/astro-latex-compile/utils.ts
191
253
  var CONTENT_ROOT = "src/content/docs/";
192
254
  function computeJpgPath(tempOutputDir, filePath, blockId) {
@@ -254,33 +316,107 @@ async function writeJpgError(jpgPath, header, errorText) {
254
316
  mkdirSync(dirname(jpgPath), { recursive: true });
255
317
  await sharp(Buffer.from(svg)).flatten({ background: { r: 255, g: 255, b: 255 } }).jpeg({ quality: 90 }).toFile(jpgPath);
256
318
  }
257
- 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 = "") {
258
405
  const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
259
- 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);
260
409
  }
261
410
  function buildLatexSource(latexCode) {
262
411
  if (latexCode.includes("\\documentclass") && latexCode.includes("\\begin{document}")) {
263
412
  return latexCode.trim();
264
413
  }
265
- const separatorRegex = /%[ \t]*===/;
266
- const parts = latexCode.split(separatorRegex);
267
- let preamble = "";
268
- let content = latexCode.trim();
269
- if (parts.length === 2) {
270
- preamble = parts[0].trim();
271
- content = parts[1].trim();
272
- }
273
- return [
274
- "\\documentclass[border=5pt]{standalone}",
275
- preamble,
276
- "\\begin{document}",
277
- "\\Large",
278
- content,
279
- "\\end{document}"
280
- ].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
+ );
281
417
  }
282
- async function compileLatexToSvg(latexCode, svgOutputDir, texInputDirs = []) {
283
- const hash = hashLatexCode(latexCode);
418
+ async function compileLatexToSvg(latexCode, svgOutputDir, texInputDirs = [], inputsSalt = "") {
419
+ const hash = hashLatexCode(latexCode, inputsSalt);
284
420
  const svgPath = join(svgOutputDir, `${hash}.svg`);
285
421
  if (existsSync(svgPath)) {
286
422
  return { hash, svgPath, wasCompiled: false };
@@ -351,6 +487,16 @@ Error: ${errorOutput}`
351
487
  }
352
488
 
353
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
+ }
354
500
  function remarkLatexCompile(options) {
355
501
  const svgOutputDir = resolve2(options.svgOutputDir);
356
502
  return async function transformer(tree, file) {
@@ -363,27 +509,31 @@ function remarkLatexCompile(options) {
363
509
  });
364
510
  if (nodes.length === 0) return;
365
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 ?? []);
366
515
  const results = await Promise.all(
367
516
  nodes.map(async ({ node, index, parent }) => {
368
- const lineNumberStr = node.position?.start.line ?? "?";
517
+ const lineNumberStr = node.position?.start.line !== void 0 ? String(node.position.start.line + frontmatterOffset) : "?";
369
518
  const blockId = new MetaOptions(node.meta ?? "").getInteger("blockid");
370
519
  const jpgPath = options.tempOutputDir && blockId !== void 0 ? computeJpgPath(options.tempOutputDir, filePath, blockId) : null;
371
520
  try {
372
521
  const result = await compileLatexToSvg(
373
522
  node.value,
374
523
  svgOutputDir,
375
- options.texInputDirs ?? []
524
+ options.texInputDirs ?? [],
525
+ inputsSalt
376
526
  );
377
527
  if (result.wasCompiled) {
378
528
  console.log(
379
- `[remark-latex-compile] ${filePath}:${lineNumberStr}: compiled ${result.hash}.svg`
529
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}: compiled ${result.hash}.svg`
380
530
  );
381
531
  }
382
532
  options._referencedHashes?.add(result.hash);
383
533
  if (jpgPath) {
384
534
  await writeJpgFromSvg(result.svgPath, jpgPath);
385
535
  console.log(
386
- `[remark-latex-compile] ${filePath}:${lineNumberStr}: wrote ${jpgPath}`
536
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}: wrote ${jpgPath}`
387
537
  );
388
538
  }
389
539
  return {
@@ -399,13 +549,13 @@ function remarkLatexCompile(options) {
399
549
  const match = errorMsg.match(/\n\n([\s\S]+)/);
400
550
  const details = match ? match[1] : errorMsg;
401
551
  console.error(
402
- `[remark-latex-compile] ${filePath}:${lineNumberStr}
552
+ `[remark-latex-compile] ${relFilePath}:${lineNumberStr}
403
553
  ${details}`
404
554
  );
405
555
  if (jpgPath) {
406
556
  await writeJpgError(
407
557
  jpgPath,
408
- `${filePath}:${lineNumberStr}`,
558
+ `${relFilePath}:${lineNumberStr}`,
409
559
  errorMsg
410
560
  );
411
561
  }
@@ -453,9 +603,22 @@ ${details}`
453
603
  options._fileJpgPathMap.set(filePath, newJpgPaths);
454
604
  }
455
605
  for (let i = results.length - 1; i >= 0; i--) {
456
- const { index, parent, result } = results[i];
606
+ const { index, parent, result, error } = results[i];
457
607
  const { node } = nodes[i];
458
- 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
+ }
459
622
  const metaOptions = new MetaOptions(node.meta ?? "");
460
623
  const customClasses = metaOptions.getString("class")?.split(/\s+/).filter(Boolean) ?? [];
461
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-OLF3SRCP.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-OLF3SRCP.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.1",
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",