starlight-cannoli-plugins 2.17.1 → 2.18.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({
@@ -364,11 +364,15 @@ export default defineConfig({
364
364
 
365
365
  - `preserveDirs` (required): Names of child directories inside `public/` to preserve during sync. These will not be deleted when re-syncing.
366
366
  - `ignorePatterns` (optional): Glob patterns for files to exclude from syncing. Patterns are matched against paths relative to `src/content/docs/`.
367
+ - `exposePageSrcButton` (optional, default: `false`): Injects a client-side script on every page that adds a `⋮` dropdown next to the page's `<h1>` with "Copy Page", "Copy raw URL", and "View raw" actions for the page's raw markdown source.
368
+ - `enableAskAiOption` (optional, default: `false`): Adds "Ask ChatGPT", "Ask Claude", and "Ask Gemini" entries to the same `⋮` dropdown, each opening the respective chat app in a new tab with a prompt pre-filled to read the page's raw markdown URL. Independent of `exposePageSrcButton` — enabling this on its own still injects the dropdown button, containing only the "Ask" entries.
367
369
 
368
370
  ```ts
369
371
  syncDocsToPublic({
370
372
  preserveDirs: ["static"],
371
373
  ignorePatterns: ["**/*.txt", "**/drafts/**"],
374
+ exposePageSrcButton: true,
375
+ enableAskAiOption: true,
372
376
  });
373
377
  ```
374
378
 
@@ -138,17 +138,18 @@ var DEFAULT_SRC_DIR = "src/content/docs";
138
138
  var DEFAULT_PUBLIC_DIR = "public";
139
139
  function validateOptions(options) {
140
140
  const mdProbePaths = ["test.md", "test.mdx", "foo/test.md", "foo/test.mdx"];
141
- if (!options.exposePageSrcButton) return;
141
+ const needsRawMd = options.exposePageSrcButton || options.enableAskAiOption;
142
+ if (!needsRawMd) return;
142
143
  const offendingMdPatterns = (options.ignorePatterns ?? []).filter(
143
144
  (pattern) => mdProbePaths.some((probe) => minimatch(probe, pattern, { dot: true }))
144
145
  );
145
146
  const offendingMtPatternsStr = offendingMdPatterns.map((p) => ` - "${p}"`).join("\n");
146
147
  if (offendingMdPatterns.length > 0) {
147
148
  throw new Error(
148
- `The 'exposePageSrcButton' option is enabled but the following 'ignorePatterns' values conflict with it:
149
+ `The 'exposePageSrcButton'/'enableAskAiOption' option is enabled but the following 'ignorePatterns' values conflict with it:
149
150
  ${offendingMtPatternsStr}
150
151
 
151
- Since these ignore patterns match md/mdx files, the page source button would break. Either disable 'exposePageSrcButton' or remove these patterns.`
152
+ Since these ignore patterns match md/mdx files, the page action button would break. Either disable these options or remove these patterns.`
152
153
  );
153
154
  }
154
155
  }
@@ -256,7 +257,8 @@ function syncDocsToPublic(options) {
256
257
  const {
257
258
  preserveDirs: rawPreserveDirs,
258
259
  ignorePatterns = [],
259
- exposePageSrcButton = false
260
+ exposePageSrcButton = false,
261
+ enableAskAiOption = false
260
262
  } = options;
261
263
  const preserveDirs = rawPreserveDirs.map((d) => d.replace(/\/+$/, ""));
262
264
  return {
@@ -264,7 +266,7 @@ function syncDocsToPublic(options) {
264
266
  hooks: {
265
267
  "astro:config:setup": ({ injectScript }) => {
266
268
  validateOptions(options);
267
- if (exposePageSrcButton) {
269
+ if (exposePageSrcButton || enableAskAiOption) {
268
270
  injectScript(
269
271
  "page",
270
272
  `
@@ -288,8 +290,16 @@ function syncDocsToPublic(options) {
288
290
  import.meta.url
289
291
  );
290
292
  }
291
- const scriptPath = fileURLToPath(pageScriptUrl);
292
- injectScript("page", `import ${JSON.stringify(scriptPath)};`);
293
+ const scriptPath = JSON.stringify(fileURLToPath(pageScriptUrl));
294
+ const actionBarOptions = JSON.stringify({
295
+ exposePageSrcButton,
296
+ enableAskAiOption
297
+ });
298
+ injectScript(
299
+ "page",
300
+ `import { initPageActionBar } from ${scriptPath};
301
+ initPageActionBar(${actionBarOptions});`
302
+ );
293
303
  }
294
304
  },
295
305
  "astro:build:start": async () => {
@@ -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-4ZJKEEDA.js";
19
+ } from "./chunk-BM7LMGAH.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 {
@@ -1,2 +1,7 @@
1
+ interface ActionBarOptions {
2
+ exposePageSrcButton: boolean;
3
+ enableAskAiOption: boolean;
4
+ }
5
+ declare function initPageActionBar(options: ActionBarOptions): void;
1
6
 
2
- export { }
7
+ export { initPageActionBar };
@@ -1,3 +1,5 @@
1
+ import "../../chunk-4VNS5WPM.js";
2
+
1
3
  // src/plugins/astro-sync-docs-to-public/page-script.ts
2
4
  function showBanner(message, variant) {
3
5
  const banner = document.createElement("div");
@@ -28,7 +30,21 @@ async function getRawMdUrl() {
28
30
  }
29
31
  return new URL("index.md", base).toString();
30
32
  }
31
- function createActionBar() {
33
+ var LLM_ASK_TARGETS = [
34
+ {
35
+ label: "Ask ChatGPT",
36
+ buildUrl: (prompt) => `https://chatgpt.com/?${new URLSearchParams({ q: prompt })}`
37
+ },
38
+ {
39
+ label: "Ask Claude",
40
+ buildUrl: (prompt) => `https://claude.ai/new?${new URLSearchParams({ q: prompt })}`
41
+ },
42
+ {
43
+ label: "Ask Gemini",
44
+ buildUrl: (prompt) => `https://gemini.google.com/app?${new URLSearchParams({ q: prompt })}`
45
+ }
46
+ ];
47
+ function createActionBar(options) {
32
48
  const bar = document.createElement("div");
33
49
  bar.className = "page-src-action-bar";
34
50
  const menuBtn = document.createElement("button");
@@ -39,29 +55,48 @@ function createActionBar() {
39
55
  }
40
56
  const menu = document.createElement("div");
41
57
  menu.className = "page-src-action-bar__menu";
42
- const menuItems = [
43
- {
44
- label: "Copy Page",
45
- action: () => {
46
- setLoading(true);
47
- getRawMdUrl().then((url) => fetch(url)).then((resp) => resp.text()).then((text) => navigator.clipboard.writeText(text)).then(() => showBanner("Page source copied to clipboard!", "success")).catch(() => showBanner("Failed to copy page source.", "error")).finally(() => setLoading(false));
48
- }
49
- },
50
- {
51
- label: "Copy raw URL",
52
- action: () => {
53
- setLoading(true);
54
- getRawMdUrl().then((url) => navigator.clipboard.writeText(url)).then(() => showBanner("Raw URL copied to clipboard!", "success")).catch(() => showBanner("Failed to copy raw URL.", "error")).finally(() => setLoading(false));
55
- }
56
- },
57
- {
58
- label: "View raw",
59
- action: () => {
60
- setLoading(true);
61
- getRawMdUrl().then((url) => window.open(url, "_blank")).catch(() => showBanner("Failed to open raw source.", "error")).finally(() => setLoading(false));
58
+ const menuItems = [];
59
+ if (options.exposePageSrcButton) {
60
+ menuItems.push(
61
+ {
62
+ label: "Copy Page",
63
+ action: () => {
64
+ setLoading(true);
65
+ getRawMdUrl().then((url) => fetch(url)).then((resp) => resp.text()).then((text) => navigator.clipboard.writeText(text)).then(
66
+ () => showBanner("Page source copied to clipboard!", "success")
67
+ ).catch(() => showBanner("Failed to copy page source.", "error")).finally(() => setLoading(false));
68
+ }
69
+ },
70
+ {
71
+ label: "Copy raw URL",
72
+ action: () => {
73
+ setLoading(true);
74
+ getRawMdUrl().then((url) => navigator.clipboard.writeText(url)).then(() => showBanner("Raw URL copied to clipboard!", "success")).catch(() => showBanner("Failed to copy raw URL.", "error")).finally(() => setLoading(false));
75
+ }
76
+ },
77
+ {
78
+ label: "View raw",
79
+ action: () => {
80
+ setLoading(true);
81
+ getRawMdUrl().then((url) => window.open(url, "_blank")).catch(() => showBanner("Failed to open raw source.", "error")).finally(() => setLoading(false));
82
+ }
62
83
  }
84
+ );
85
+ }
86
+ if (options.enableAskAiOption) {
87
+ for (const { label, buildUrl } of LLM_ASK_TARGETS) {
88
+ menuItems.push({
89
+ label,
90
+ action: () => {
91
+ setLoading(true);
92
+ getRawMdUrl().then((url) => {
93
+ const prompt = `Read from ${url} so I can ask questions about it.`;
94
+ window.open(buildUrl(prompt), "_blank");
95
+ }).catch(() => showBanner(`Failed to open ${label}.`, "error")).finally(() => setLoading(false));
96
+ }
97
+ });
63
98
  }
64
- ];
99
+ }
65
100
  for (const item of menuItems) {
66
101
  const btn = document.createElement("button");
67
102
  btn.className = "page-src-action-bar__menu-item";
@@ -93,13 +128,15 @@ function createActionBar() {
93
128
  bar.appendChild(menu);
94
129
  return bar;
95
130
  }
96
- function main() {
131
+ function initPageActionBar(options) {
97
132
  const h1 = document.querySelector(".sl-container > h1");
98
133
  if (h1 === null) return;
99
134
  const wrapper = document.createElement("div");
100
135
  wrapper.className = "page-src-h1-wrapper";
101
136
  h1.replaceWith(wrapper);
102
137
  wrapper.appendChild(h1);
103
- wrapper.appendChild(createActionBar());
138
+ wrapper.appendChild(createActionBar(options));
104
139
  }
105
- main();
140
+ export {
141
+ initPageActionBar
142
+ };
@@ -17,6 +17,15 @@ interface SyncDocsToPublicOptions {
17
17
  * dev and build modes.
18
18
  */
19
19
  exposePageSrcButton?: boolean;
20
+ /**
21
+ * When true, adds "Ask ChatGPT", "Ask Claude", and "Ask Gemini" entries to
22
+ * the page action dropdown menu, each opening the respective chat app with
23
+ * a prompt pre-filled to read the current page's raw markdown URL.
24
+ *
25
+ * Independent of `exposePageSrcButton` — enabling this on its own still
26
+ * injects the dropdown button, containing only the "Ask" entries.
27
+ */
28
+ enableAskAiOption?: boolean;
20
29
  }
21
30
  declare function syncDocsToPublic(options: SyncDocsToPublicOptions): AstroIntegration;
22
31
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  syncDocsToPublic
3
- } from "../chunk-4ZJKEEDA.js";
3
+ } from "../chunk-BM7LMGAH.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.1",
4
+ "version": "2.18.0",
5
5
  "description": "Starlight plugins for automatic sidebar generation and link validation",
6
6
  "license": "ISC",
7
7
  "main": "./dist/index.js",