starlight-cannoli-plugins 1.0.16 → 1.2.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 +160 -11
- package/dist/chunk-T4UKGKU6.js +457 -0
- package/dist/cli/cannoli-latex-cleanup.d.ts +1 -0
- package/dist/cli/cannoli-latex-cleanup.js +125 -0
- package/dist/index-B9CyKYB4.d.ts +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -1
- package/dist/plugins/remark-latex-compile.d.ts +2 -0
- package/dist/plugins/remark-latex-compile.js +10 -0
- package/dist/plugins/starlight-latex-compile.d.ts +2 -0
- package/dist/plugins/starlight-latex-compile.js +8 -0
- package/dist/styles/_starlight.scss +4 -0
- package/package.json +13 -2
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ A collection of powerful plugins for [Astro Starlight](https://starlight.astro.b
|
|
|
9
9
|
Automatically generates a nested Starlight sidebar by recursively scanning directories for `index.md`/`index.mdx` files. Only directories with index files appear in the sidebar, creating a clean, minimal navigation structure.
|
|
10
10
|
|
|
11
11
|
**Features:**
|
|
12
|
+
|
|
12
13
|
- Recursively scans directories for `index.md` or `index.mdx` files
|
|
13
14
|
- Creates sidebar entries only for pages with index files
|
|
14
15
|
- Respects frontmatter: `draft: true` and `sidebar.hidden: true` hide entries
|
|
@@ -38,7 +39,7 @@ export default defineConfig({
|
|
|
38
39
|
plugins: [
|
|
39
40
|
starlightIndexOnlySidebar({
|
|
40
41
|
directories: ["guides", "api", "tutorials"],
|
|
41
|
-
maxDepthNesting: 2,
|
|
42
|
+
maxDepthNesting: 2, // optional
|
|
42
43
|
dirnameDeterminesLabels: false, // optional
|
|
43
44
|
}),
|
|
44
45
|
],
|
|
@@ -47,11 +48,123 @@ export default defineConfig({
|
|
|
47
48
|
});
|
|
48
49
|
```
|
|
49
50
|
|
|
51
|
+
### Starlight LaTeX Compile
|
|
52
|
+
|
|
53
|
+
Automatically compiles fenced `tex compile` and `latex compile` code blocks to SVG diagrams during the build process. Uses `pdflatex` and `dvisvgm` for high-quality, cached SVG output.
|
|
54
|
+
|
|
55
|
+
**Features:**
|
|
56
|
+
|
|
57
|
+
- Compiles LaTeX/TikZ code blocks to SVG automatically
|
|
58
|
+
- Caches compiled SVGs by content hash (no recompilation if unchanged)
|
|
59
|
+
- Comprehensive error reporting with line numbers and formatted LaTeX source
|
|
60
|
+
- Supports custom preamble via `%---` separator in code blocks
|
|
61
|
+
- Works seamlessly with Starlight's content pipeline
|
|
62
|
+
- Requires `svgOutputDir` configuration (no defaults)
|
|
63
|
+
|
|
64
|
+
**System Requirements:**
|
|
65
|
+
|
|
66
|
+
This plugin requires the following CLI tools to be installed and available on your system:
|
|
67
|
+
|
|
68
|
+
- **`pdflatex`** — LaTeX compiler that produces PDF output
|
|
69
|
+
- **`dvisvgm`** — Converts PDF to SVG format
|
|
70
|
+
|
|
71
|
+
Verify installation by running:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pdflatex --version
|
|
75
|
+
dvisvgm --version
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Usage:**
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// astro.config.mjs
|
|
82
|
+
import { defineConfig } from "astro/config";
|
|
83
|
+
import starlight from "@astrojs/starlight";
|
|
84
|
+
import { starlightLatexCompile } from "cannoli-starlight-plugins";
|
|
85
|
+
|
|
86
|
+
export default defineConfig({
|
|
87
|
+
integrations: [
|
|
88
|
+
starlight({
|
|
89
|
+
title: "My Docs",
|
|
90
|
+
plugins: [
|
|
91
|
+
starlightLatexCompile({
|
|
92
|
+
svgOutputDir: "public/static/tex-svgs",
|
|
93
|
+
}),
|
|
94
|
+
],
|
|
95
|
+
}),
|
|
96
|
+
],
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Markdown Syntax:**
|
|
101
|
+
|
|
102
|
+
````markdown
|
|
103
|
+
```tex compile
|
|
104
|
+
\begin{tikzpicture}
|
|
105
|
+
\node (A) at (0,0) {A};
|
|
106
|
+
\node (B) at (2,0) {B};
|
|
107
|
+
\draw (A) -- (B);
|
|
108
|
+
\end{tikzpicture}
|
|
109
|
+
```
|
|
110
|
+
````
|
|
111
|
+
|
|
112
|
+
**Custom Preamble:**
|
|
113
|
+
|
|
114
|
+
Use `%---` to separate custom preamble from diagram content:
|
|
115
|
+
|
|
116
|
+
````markdown
|
|
117
|
+
```tex compile
|
|
118
|
+
\usepackage{tikz-3dplot}
|
|
119
|
+
|
|
120
|
+
%---
|
|
121
|
+
|
|
122
|
+
\begin{tikzpicture}
|
|
123
|
+
% diagram code here
|
|
124
|
+
\end{tikzpicture}
|
|
125
|
+
```
|
|
126
|
+
````
|
|
127
|
+
|
|
128
|
+
### Remark LaTeX Compile
|
|
129
|
+
|
|
130
|
+
The underlying remark plugin that powers `starlightLatexCompile`. Use this directly in Astro projects that don't use Starlight.
|
|
131
|
+
|
|
132
|
+
**System Requirements:**
|
|
133
|
+
|
|
134
|
+
Same as `starlightLatexCompile`:
|
|
135
|
+
|
|
136
|
+
- **`pdflatex`** — LaTeX compiler that produces PDF output
|
|
137
|
+
- **`dvisvgm`** — Converts PDF to SVG format
|
|
138
|
+
|
|
139
|
+
**Usage:**
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// astro.config.mjs
|
|
143
|
+
import { defineConfig } from "astro/config";
|
|
144
|
+
import { remarkLatexCompile } from "cannoli-starlight-plugins/remark-latex-compile";
|
|
145
|
+
|
|
146
|
+
export default defineConfig({
|
|
147
|
+
markdown: {
|
|
148
|
+
remarkPlugins: [
|
|
149
|
+
[
|
|
150
|
+
remarkLatexCompile,
|
|
151
|
+
{
|
|
152
|
+
svgOutputDir: "public/static/tex-svgs",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The plugin works identically to `starlightLatexCompile` but is configured directly in the Astro markdown pipeline.
|
|
161
|
+
|
|
50
162
|
### Rehype Validate Links
|
|
51
163
|
|
|
52
164
|
A rehype plugin that validates all internal links in your Markdown/MDX files at build time. Links without matching files will cause the build to fail.
|
|
53
165
|
|
|
54
166
|
**Features:**
|
|
167
|
+
|
|
55
168
|
- Validates `<a href>` and `<img src>` attributes
|
|
56
169
|
- Supports relative paths (`../other`) and absolute paths (`/some/page`)
|
|
57
170
|
- Auto-expands extensionless links to match `.md` or `.mdx` files
|
|
@@ -68,9 +181,7 @@ import { rehypeValidateLinks } from "cannoli-starlight-plugins";
|
|
|
68
181
|
|
|
69
182
|
export default defineConfig({
|
|
70
183
|
markdown: {
|
|
71
|
-
rehypePlugins: [
|
|
72
|
-
rehypeValidateLinks,
|
|
73
|
-
],
|
|
184
|
+
rehypePlugins: [rehypeValidateLinks],
|
|
74
185
|
},
|
|
75
186
|
});
|
|
76
187
|
```
|
|
@@ -100,7 +211,9 @@ Prepend a `?` to the link href to skip validation:
|
|
|
100
211
|
Use the `data-no-link-check` attribute on anchor tags:
|
|
101
212
|
|
|
102
213
|
```mdx
|
|
103
|
-
<a href="csci-320-331-obrenic/grade-calculator" data-no-link-check>
|
|
214
|
+
<a href="csci-320-331-obrenic/grade-calculator" data-no-link-check>
|
|
215
|
+
Grade Calculator
|
|
216
|
+
</a>
|
|
104
217
|
```
|
|
105
218
|
|
|
106
219
|
**3. Global Skip Patterns** (Configuration-based)
|
|
@@ -112,17 +225,53 @@ Use the `skipPatterns` option to exclude links matching glob patterns:
|
|
|
112
225
|
export default defineConfig({
|
|
113
226
|
markdown: {
|
|
114
227
|
rehypePlugins: [
|
|
115
|
-
[
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
228
|
+
[
|
|
229
|
+
rehypeValidateLinks,
|
|
230
|
+
{
|
|
231
|
+
skipPatterns: [
|
|
232
|
+
"/csci-320-331-obrenic/grade-calculator", // exact match
|
|
233
|
+
"**/draft-*", // glob pattern
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
],
|
|
121
237
|
],
|
|
122
238
|
},
|
|
123
239
|
});
|
|
124
240
|
```
|
|
125
241
|
|
|
242
|
+
## CLI Utilities
|
|
243
|
+
|
|
244
|
+
### cannoli-latex-cleanup
|
|
245
|
+
|
|
246
|
+
A cleanup utility for the LaTeX compile plugin. Scans your markdown source files for all `tex compile` code blocks, hashes them, and identifies orphaned SVG files in the output directory that are no longer referenced by any code block.
|
|
247
|
+
|
|
248
|
+
**Usage:**
|
|
249
|
+
|
|
250
|
+
Check for orphaned SVGs without deleting:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npx cannoli-latex-cleanup --svg-dir public/static/tex-svgs --check
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Delete orphaned SVGs:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npx cannoli-latex-cleanup --svg-dir public/static/tex-svgs --delete
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
With custom docs directory (defaults to `src/content/docs`):
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
npx cannoli-latex-cleanup --svg-dir public/static/tex-svgs --docs-dir ./src/content/docs --delete
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Options:**
|
|
269
|
+
|
|
270
|
+
- `--svg-dir` (required): Path to the SVG output directory configured in `starlightLatexCompile`
|
|
271
|
+
- `--docs-dir` (optional, default: `src/content/docs`): Path to markdown source directory
|
|
272
|
+
- `--check`: List orphaned SVGs without deleting
|
|
273
|
+
- `--delete`: Delete orphaned SVGs
|
|
274
|
+
|
|
126
275
|
## Installation
|
|
127
276
|
|
|
128
277
|
```bash
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// src/plugins/remark-latex-compile/index.ts
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
// src/plugins/remark-latex-compile/compile.ts
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
mkdtempSync
|
|
13
|
+
} from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { tmpdir } from "os";
|
|
16
|
+
|
|
17
|
+
// src/plugins/remark-latex-compile/error-parser.ts
|
|
18
|
+
function parseLatexError(latexOutput) {
|
|
19
|
+
const lines = latexOutput.split("\n");
|
|
20
|
+
const errors = [];
|
|
21
|
+
const seenMessages = /* @__PURE__ */ new Set();
|
|
22
|
+
let hasFatal = false;
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
if (line.startsWith("!")) {
|
|
26
|
+
const message = line.substring(1).trim();
|
|
27
|
+
if (seenMessages.has(message)) continue;
|
|
28
|
+
seenMessages.add(message);
|
|
29
|
+
const context = [];
|
|
30
|
+
let lineNum;
|
|
31
|
+
for (let j = i + 1; j < Math.min(i + 5, lines.length); j++) {
|
|
32
|
+
const contextLine = lines[j];
|
|
33
|
+
if (contextLine.trim()) {
|
|
34
|
+
context.push(contextLine);
|
|
35
|
+
}
|
|
36
|
+
if (contextLine.startsWith("l.")) {
|
|
37
|
+
const lineMatch = contextLine.match(/^l\.(\d+)/);
|
|
38
|
+
if (lineMatch) {
|
|
39
|
+
lineNum = parseInt(lineMatch[1], 10);
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
errors.push({
|
|
45
|
+
message,
|
|
46
|
+
line: lineNum,
|
|
47
|
+
context: context.slice(0, 3),
|
|
48
|
+
severity: "error"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
if (line.includes("Overfull") || line.includes("Underfull")) {
|
|
55
|
+
const msg = line.trim();
|
|
56
|
+
if (!seenMessages.has(msg)) {
|
|
57
|
+
seenMessages.add(msg);
|
|
58
|
+
errors.push({
|
|
59
|
+
message: msg,
|
|
60
|
+
context: [],
|
|
61
|
+
severity: "warning"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
if (line.toLowerCase().includes("emergency stop") || line.toLowerCase().includes("fatal error") || line.toLowerCase().includes("not found")) {
|
|
68
|
+
hasFatal = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (errors.length === 0 && latexOutput.length > 0) {
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
if (line.includes("error") || line.includes("Error") || line.includes("Misplaced") || line.includes("Missing")) {
|
|
75
|
+
const msg = line.trim();
|
|
76
|
+
if (!seenMessages.has(msg)) {
|
|
77
|
+
seenMessages.add(msg);
|
|
78
|
+
errors.push({
|
|
79
|
+
message: msg,
|
|
80
|
+
context: [],
|
|
81
|
+
severity: "error"
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (errors.length === 0) {
|
|
89
|
+
errors.push({
|
|
90
|
+
message: "Unknown LaTeX compilation error",
|
|
91
|
+
context: [],
|
|
92
|
+
severity: "error"
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { errors, hasFatal };
|
|
96
|
+
}
|
|
97
|
+
function formatLatexError(parsed) {
|
|
98
|
+
const RED = "\x1B[31m";
|
|
99
|
+
const YELLOW = "\x1B[33m";
|
|
100
|
+
const RESET = "\x1B[0m";
|
|
101
|
+
const errorCount = parsed.errors.filter((e) => e.severity === "error").length;
|
|
102
|
+
const warningCount = parsed.errors.filter(
|
|
103
|
+
(e) => e.severity === "warning"
|
|
104
|
+
).length;
|
|
105
|
+
let output = `${RED}[remark-latex-compile] LaTeX compilation failed${RESET}
|
|
106
|
+
`;
|
|
107
|
+
output += `${RED}${errorCount} error${errorCount !== 1 ? "s" : ""}${RESET}`;
|
|
108
|
+
if (warningCount > 0) {
|
|
109
|
+
output += `, ${YELLOW}${warningCount} warning${warningCount !== 1 ? "s" : ""}${RESET}`;
|
|
110
|
+
}
|
|
111
|
+
output += "\n\n";
|
|
112
|
+
const errorsByType = parsed.errors.reduce(
|
|
113
|
+
(acc, e) => {
|
|
114
|
+
if (!acc[e.severity]) acc[e.severity] = [];
|
|
115
|
+
acc[e.severity].push(e);
|
|
116
|
+
return acc;
|
|
117
|
+
},
|
|
118
|
+
{}
|
|
119
|
+
);
|
|
120
|
+
for (const err of errorsByType["error"] || []) {
|
|
121
|
+
output += `${RED}Error${RESET}`;
|
|
122
|
+
if (err.line) output += ` (line ${err.line})`;
|
|
123
|
+
output += `: ${err.message}
|
|
124
|
+
`;
|
|
125
|
+
if (err.context.length > 0) {
|
|
126
|
+
output += ` Context: ${err.context[0]}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const warn of errorsByType["warning"] || []) {
|
|
131
|
+
output += `${YELLOW}Warning${RESET}: ${warn.message}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
return output;
|
|
135
|
+
}
|
|
136
|
+
function formatLatexSourceWithLineNumbers(latexSource, errors) {
|
|
137
|
+
const RED = "\x1B[31m";
|
|
138
|
+
const RESET = "\x1B[0m";
|
|
139
|
+
const lines = latexSource.split("\n");
|
|
140
|
+
const maxLineNum = lines.length;
|
|
141
|
+
const lineNumWidth = String(maxLineNum).length;
|
|
142
|
+
const errorLineNumbers = new Set(errors.map((e) => e.line).filter(Boolean));
|
|
143
|
+
const formattedLines = lines.map((line, index) => {
|
|
144
|
+
const lineNum = index + 1;
|
|
145
|
+
const lineNumStr = String(lineNum);
|
|
146
|
+
const padding = lineNumStr.length < lineNumWidth ? " " : "";
|
|
147
|
+
if (errorLineNumbers.has(lineNum)) {
|
|
148
|
+
return `${padding}${RED}[${lineNumStr}]:${RESET} ${line}`;
|
|
149
|
+
}
|
|
150
|
+
return `${padding}[${lineNumStr}]: ${line}`;
|
|
151
|
+
}).join("\n");
|
|
152
|
+
return formattedLines;
|
|
153
|
+
}
|
|
154
|
+
function createCompilationErrorMessage(latexSource, rawError) {
|
|
155
|
+
const parsed = parseLatexError(rawError);
|
|
156
|
+
const formatted = formatLatexError(parsed);
|
|
157
|
+
const formattedSource = formatLatexSourceWithLineNumbers(
|
|
158
|
+
latexSource,
|
|
159
|
+
parsed.errors
|
|
160
|
+
);
|
|
161
|
+
return `${formatted}
|
|
162
|
+
LaTeX source:
|
|
163
|
+
${formattedSource}
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/plugins/remark-latex-compile/compile.ts
|
|
168
|
+
function hashLatexCode(code) {
|
|
169
|
+
const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
|
|
170
|
+
return createHash("md5").update(normalized).digest("hex").slice(0, 16);
|
|
171
|
+
}
|
|
172
|
+
function buildLatexSource(latexCode) {
|
|
173
|
+
const separatorRegex = /%[ \t]*---/;
|
|
174
|
+
const parts = latexCode.split(separatorRegex);
|
|
175
|
+
let preamble = "";
|
|
176
|
+
let content = latexCode.trim();
|
|
177
|
+
if (parts.length === 2) {
|
|
178
|
+
preamble = parts[0].trim();
|
|
179
|
+
content = parts[1].trim();
|
|
180
|
+
}
|
|
181
|
+
return [
|
|
182
|
+
"\\documentclass[border=4pt]{standalone}",
|
|
183
|
+
preamble,
|
|
184
|
+
"\\begin{document}",
|
|
185
|
+
"\\Large",
|
|
186
|
+
content,
|
|
187
|
+
"\\end{document}"
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
function compileLatexToSvg(latexCode, svgOutputDir) {
|
|
191
|
+
const hash = hashLatexCode(latexCode);
|
|
192
|
+
const svgPath = join(svgOutputDir, `${hash}.svg`);
|
|
193
|
+
if (existsSync(svgPath)) {
|
|
194
|
+
return { hash, svgPath, wasCompiled: false };
|
|
195
|
+
}
|
|
196
|
+
mkdirSync(svgOutputDir, { recursive: true });
|
|
197
|
+
const workDir = mkdtempSync(join(tmpdir(), "latex-compile-"));
|
|
198
|
+
const texFile = join(workDir, "diagram.tex");
|
|
199
|
+
const pdfFile = join(workDir, "diagram.pdf");
|
|
200
|
+
const latexSource = buildLatexSource(latexCode);
|
|
201
|
+
try {
|
|
202
|
+
writeFileSync(texFile, latexSource, "utf-8");
|
|
203
|
+
const latexResult = spawnSync("pdflatex", [
|
|
204
|
+
"-interaction=nonstopmode",
|
|
205
|
+
"-output-directory",
|
|
206
|
+
workDir,
|
|
207
|
+
texFile
|
|
208
|
+
]);
|
|
209
|
+
if (latexResult.status !== 0) {
|
|
210
|
+
const errorOutput = latexResult.stderr?.toString() || latexResult.stdout?.toString() || "";
|
|
211
|
+
const userMessage = createCompilationErrorMessage(
|
|
212
|
+
latexSource,
|
|
213
|
+
errorOutput
|
|
214
|
+
);
|
|
215
|
+
throw new Error(userMessage);
|
|
216
|
+
}
|
|
217
|
+
const dvisvgmResult = spawnSync("dvisvgm", [
|
|
218
|
+
"--pdf",
|
|
219
|
+
"--bbox=dvi",
|
|
220
|
+
pdfFile,
|
|
221
|
+
"-o",
|
|
222
|
+
svgPath
|
|
223
|
+
]);
|
|
224
|
+
if (dvisvgmResult.status !== 0) {
|
|
225
|
+
const errorOutput = dvisvgmResult.stderr?.toString() || dvisvgmResult.stdout?.toString() || "";
|
|
226
|
+
throw new Error(
|
|
227
|
+
`[remark-latex-compile] PDF to SVG conversion failed (hash: ${hash}).
|
|
228
|
+
Error: ${errorOutput}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
} finally {
|
|
232
|
+
try {
|
|
233
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { hash, svgPath, wasCompiled: true };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/plugins/remark-latex-compile/index.ts
|
|
241
|
+
function traverseTree(node, svgOutputDir, filePath, depth = 0) {
|
|
242
|
+
if (!node) return;
|
|
243
|
+
const children = node.children;
|
|
244
|
+
if (Array.isArray(children)) {
|
|
245
|
+
for (let i = 0; i < children.length; i++) {
|
|
246
|
+
const child = children[i];
|
|
247
|
+
if (child.type === "code" && (child.lang === "tex" || child.lang === "latex") && String(child.meta || "").includes("compile")) {
|
|
248
|
+
try {
|
|
249
|
+
const result = compileLatexToSvg(String(child.value), svgOutputDir);
|
|
250
|
+
children[i] = {
|
|
251
|
+
type: "paragraph",
|
|
252
|
+
children: [
|
|
253
|
+
{
|
|
254
|
+
type: "image",
|
|
255
|
+
url: `/static/tex-svgs/${result.hash}.svg`,
|
|
256
|
+
alt: "LaTeX diagram",
|
|
257
|
+
data: {
|
|
258
|
+
hProperties: {
|
|
259
|
+
className: ["tex-compiled"]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (process.env.NODE_ENV !== "production") {
|
|
267
|
+
const position = child.position;
|
|
268
|
+
const lineNumber = position?.start?.line || "?";
|
|
269
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
270
|
+
const match = errorMsg.match(/\n\n([\s\S]+)/);
|
|
271
|
+
const details = match ? match[1] : errorMsg;
|
|
272
|
+
console.error(`${filePath}:${lineNumber}
|
|
273
|
+
${details}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
traverseTree(child, svgOutputDir, filePath, depth + 1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function remarkLatexCompile(options) {
|
|
283
|
+
const svgOutputDir = resolve(options.svgOutputDir);
|
|
284
|
+
return (tree, file) => {
|
|
285
|
+
const fileObj = file;
|
|
286
|
+
const filePath = String(fileObj?.path || fileObj?.filename || "unknown");
|
|
287
|
+
traverseTree(tree, svgOutputDir, filePath, 0);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/plugins/remark-latex-compile/astro-integration.ts
|
|
292
|
+
import { readdir, readFile, writeFile } from "fs/promises";
|
|
293
|
+
import { resolve as resolve2, join as join2, extname } from "path";
|
|
294
|
+
import { createHash as createHash2 } from "crypto";
|
|
295
|
+
function hashLatexCode2(code) {
|
|
296
|
+
const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
|
|
297
|
+
return createHash2("md5").update(normalized).digest("hex").slice(0, 16);
|
|
298
|
+
}
|
|
299
|
+
function createAstroLatexIntegration(options) {
|
|
300
|
+
const svgOutputDir = resolve2(options.svgOutputDir);
|
|
301
|
+
const contentDir = options?.contentDir ? resolve2(options.contentDir) : resolve2("src/content/docs");
|
|
302
|
+
return {
|
|
303
|
+
name: "astro-latex-compile",
|
|
304
|
+
hooks: {
|
|
305
|
+
"astro:build:start": async () => {
|
|
306
|
+
console.log(
|
|
307
|
+
"[astro-latex-compile] Build start, scanning for tex/latex compile blocks"
|
|
308
|
+
);
|
|
309
|
+
await scanAndCompileLatex(contentDir, svgOutputDir);
|
|
310
|
+
},
|
|
311
|
+
"astro:build:done": async ({ dir }) => {
|
|
312
|
+
console.log(
|
|
313
|
+
"[astro-latex-compile] Build done, updating HTML references"
|
|
314
|
+
);
|
|
315
|
+
try {
|
|
316
|
+
await updateHtmlReferences(dir.pathname, contentDir, svgOutputDir);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(
|
|
319
|
+
"[astro-latex-compile] Error updating HTML references:",
|
|
320
|
+
err
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function scanAndCompileLatex(dir, svgOutputDir) {
|
|
328
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
const fullPath = join2(dir, entry.name);
|
|
331
|
+
if (entry.isDirectory()) {
|
|
332
|
+
await scanAndCompileLatex(fullPath, svgOutputDir);
|
|
333
|
+
} else if (entry.isFile()) {
|
|
334
|
+
const ext = extname(entry.name);
|
|
335
|
+
if (ext === ".md" || ext === ".mdx") {
|
|
336
|
+
await processMarkdownFile(fullPath, svgOutputDir);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function getLineNumber(content, position) {
|
|
342
|
+
return content.substring(0, position).split("\n").length;
|
|
343
|
+
}
|
|
344
|
+
async function processMarkdownFile(filePath, svgOutputDir) {
|
|
345
|
+
const content = await readFile(filePath, "utf-8");
|
|
346
|
+
const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
|
|
347
|
+
const matches = content.matchAll(latexBlockRegex);
|
|
348
|
+
for (const match of matches) {
|
|
349
|
+
const latexCode = match[1];
|
|
350
|
+
const lineNumber = getLineNumber(content, match.index || 0);
|
|
351
|
+
try {
|
|
352
|
+
const result = compileLatexToSvg(latexCode, svgOutputDir);
|
|
353
|
+
const status = result.wasCompiled ? "compiled" : "used cached";
|
|
354
|
+
console.log(
|
|
355
|
+
`[astro-latex-compile] ${filePath}:${lineNumber}: ${status} ${result.hash}.svg`
|
|
356
|
+
);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
359
|
+
error.message = `${filePath}:${lineNumber}
|
|
360
|
+
${error.message}`;
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function updateHtmlReferences(buildDir, contentDir, svgOutputDir) {
|
|
366
|
+
const latexHashes = [];
|
|
367
|
+
const entries = await readdir(contentDir, { withFileTypes: true });
|
|
368
|
+
for (const entry of entries) {
|
|
369
|
+
const fullPath = join2(contentDir, entry.name);
|
|
370
|
+
if (entry.isDirectory()) {
|
|
371
|
+
await scanMarkdownForHashes(fullPath, latexHashes);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
await updateHtmlDirWithHashes(buildDir, latexHashes, svgOutputDir);
|
|
375
|
+
}
|
|
376
|
+
async function scanMarkdownForHashes(dir, hashes) {
|
|
377
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
378
|
+
for (const entry of entries) {
|
|
379
|
+
const fullPath = join2(dir, entry.name);
|
|
380
|
+
if (entry.isDirectory()) {
|
|
381
|
+
await scanMarkdownForHashes(fullPath, hashes);
|
|
382
|
+
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))) {
|
|
383
|
+
const content = await readFile(fullPath, "utf-8");
|
|
384
|
+
const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
|
|
385
|
+
const matches = content.matchAll(latexBlockRegex);
|
|
386
|
+
for (const match of matches) {
|
|
387
|
+
const latexCode = match[1];
|
|
388
|
+
const hash = hashLatexCode2(latexCode);
|
|
389
|
+
hashes.push(hash);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function updateHtmlDirWithHashes(dir, hashes, svgOutputDir) {
|
|
395
|
+
let hashIndex = 0;
|
|
396
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const fullPath = join2(dir, entry.name);
|
|
399
|
+
if (entry.isDirectory()) {
|
|
400
|
+
await updateHtmlDirWithHashes(fullPath, hashes, svgOutputDir);
|
|
401
|
+
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
402
|
+
let content = await readFile(fullPath, "utf-8");
|
|
403
|
+
let modified = false;
|
|
404
|
+
const pathSegments = svgOutputDir.split("/").slice(-2).join("/");
|
|
405
|
+
const htmlPath = `/${pathSegments}`;
|
|
406
|
+
const svgRegex = new RegExp(`src="${htmlPath}/[a-f0-9]+\\.svg"`, "g");
|
|
407
|
+
content = content.replace(svgRegex, (match) => {
|
|
408
|
+
if (hashIndex < hashes.length) {
|
|
409
|
+
modified = true;
|
|
410
|
+
return `src="${htmlPath}/${hashes[hashIndex++]}.svg"`;
|
|
411
|
+
}
|
|
412
|
+
return match;
|
|
413
|
+
});
|
|
414
|
+
if (modified) {
|
|
415
|
+
await writeFile(fullPath, content, "utf-8");
|
|
416
|
+
console.log(`[astro-latex-compile] Updated ${fullPath}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/plugins/remark-latex-compile/starlight-plugin.ts
|
|
423
|
+
function starlightLatexCompile(options) {
|
|
424
|
+
return {
|
|
425
|
+
name: "starlight-latex-compile",
|
|
426
|
+
hooks: {
|
|
427
|
+
"config:setup": (hook) => {
|
|
428
|
+
hook.addIntegration({
|
|
429
|
+
name: "latex-compile-remark-integration",
|
|
430
|
+
hooks: {
|
|
431
|
+
"astro:config:setup": ({ updateConfig, config }) => {
|
|
432
|
+
const existingPlugins = (Array.isArray(config.markdown?.remarkPlugins) ? config.markdown.remarkPlugins : []).filter((p) => p !== void 0 && p !== null);
|
|
433
|
+
updateConfig({
|
|
434
|
+
markdown: {
|
|
435
|
+
remarkPlugins: [...existingPlugins, [remarkLatexCompile, options]]
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
hook.addIntegration(
|
|
442
|
+
createAstroLatexIntegration({
|
|
443
|
+
svgOutputDir: options.svgOutputDir
|
|
444
|
+
})
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
var starlight_plugin_default = starlightLatexCompile;
|
|
451
|
+
|
|
452
|
+
export {
|
|
453
|
+
compileLatexToSvg,
|
|
454
|
+
starlightLatexCompile,
|
|
455
|
+
starlight_plugin_default,
|
|
456
|
+
remarkLatexCompile
|
|
457
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// scripts/cli/cannoli-latex-cleanup.ts
|
|
4
|
+
import { readdir, readFile, unlink } from "fs/promises";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
|
+
import { createHash } from "crypto";
|
|
7
|
+
var args = process.argv.slice(2);
|
|
8
|
+
var svgDir = null;
|
|
9
|
+
var docsDir = "src/content/docs";
|
|
10
|
+
var checkMode = false;
|
|
11
|
+
var deleteMode = false;
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
if (args[i] === "--svg-dir" && i + 1 < args.length) {
|
|
14
|
+
svgDir = args[++i];
|
|
15
|
+
} else if (args[i] === "--docs-dir" && i + 1 < args.length) {
|
|
16
|
+
docsDir = args[++i];
|
|
17
|
+
} else if (args[i] === "--check") {
|
|
18
|
+
checkMode = true;
|
|
19
|
+
} else if (args[i] === "--delete") {
|
|
20
|
+
deleteMode = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (!svgDir) {
|
|
24
|
+
console.error("Error: --svg-dir is required");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (!checkMode && !deleteMode) {
|
|
28
|
+
console.error("Error: either --check or --delete must be specified");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
var svgDirPath = resolve(svgDir);
|
|
32
|
+
var docsDirPath = resolve(docsDir);
|
|
33
|
+
function hashLatexCode(code) {
|
|
34
|
+
const normalized = code.split("\n").map((line) => line.trim()).join("\n").trim();
|
|
35
|
+
return createHash("md5").update(normalized).digest("hex").slice(0, 16);
|
|
36
|
+
}
|
|
37
|
+
async function scanMarkdownForHashes(dir, hashes) {
|
|
38
|
+
try {
|
|
39
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const fullPath = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
await scanMarkdownForHashes(fullPath, hashes);
|
|
44
|
+
} else if (entry.isFile() && (entry.name.endsWith(".md") || entry.name.endsWith(".mdx"))) {
|
|
45
|
+
const content = await readFile(fullPath, "utf-8");
|
|
46
|
+
const latexBlockRegex = /```(?:tex|latex)\s+compile\n([\s\S]*?)\n```/g;
|
|
47
|
+
const matches = content.matchAll(latexBlockRegex);
|
|
48
|
+
for (const match of matches) {
|
|
49
|
+
const latexCode = match[1];
|
|
50
|
+
const hash = hashLatexCode(latexCode);
|
|
51
|
+
hashes.add(hash);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(`Error scanning directory ${dir}:`, err);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function findOrphanedSvgs(svgPath, usedHashes) {
|
|
61
|
+
const orphaned = [];
|
|
62
|
+
try {
|
|
63
|
+
const entries = await readdir(svgPath, { withFileTypes: true });
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (entry.isFile() && entry.name.endsWith(".svg")) {
|
|
66
|
+
const hash = entry.name.replace(".svg", "");
|
|
67
|
+
if (!usedHashes.has(hash)) {
|
|
68
|
+
orphaned.push(join(svgPath, entry.name));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.code === "ENOENT") {
|
|
74
|
+
console.warn(`SVG directory does not exist: ${svgPath}`);
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
return orphaned;
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
try {
|
|
83
|
+
console.log(`Scanning markdown files in ${docsDirPath}...`);
|
|
84
|
+
const usedHashes = /* @__PURE__ */ new Set();
|
|
85
|
+
await scanMarkdownForHashes(docsDirPath, usedHashes);
|
|
86
|
+
console.log(`Found ${usedHashes.size} unique tex compile blocks
|
|
87
|
+
`);
|
|
88
|
+
console.log(`Scanning SVG directory ${svgDirPath}...`);
|
|
89
|
+
const orphanedSvgs = await findOrphanedSvgs(svgDirPath, usedHashes);
|
|
90
|
+
if (orphanedSvgs.length === 0) {
|
|
91
|
+
console.log("No orphaned SVGs found \u2713");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
console.log(`
|
|
95
|
+
Found ${orphanedSvgs.length} orphaned SVG(s):`);
|
|
96
|
+
orphanedSvgs.forEach((svg) => {
|
|
97
|
+
const filename = svg.split("/").pop();
|
|
98
|
+
console.log(` - ${filename}`);
|
|
99
|
+
});
|
|
100
|
+
if (checkMode) {
|
|
101
|
+
console.log("\n(Use --delete to remove these files)");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
if (deleteMode) {
|
|
105
|
+
console.log("\nDeleting orphaned SVGs...");
|
|
106
|
+
let deleted = 0;
|
|
107
|
+
for (const svg of orphanedSvgs) {
|
|
108
|
+
try {
|
|
109
|
+
await unlink(svg);
|
|
110
|
+
deleted++;
|
|
111
|
+
console.log(` \u2713 Deleted ${svg.split("/").pop()}`);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(` \u2717 Failed to delete ${svg}:`, err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log(`
|
|
117
|
+
Deleted ${deleted}/${orphanedSvgs.length} files`);
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error("Error:", err);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HookParameters } from '@astrojs/starlight/types';
|
|
2
|
+
|
|
3
|
+
interface CompilationResult {
|
|
4
|
+
hash: string;
|
|
5
|
+
svgPath: string;
|
|
6
|
+
wasCompiled: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compile LaTeX code to SVG.
|
|
10
|
+
*
|
|
11
|
+
* @param latexCode - The LaTeX code to compile (e.g., TikZ, pgfplots, etc.)
|
|
12
|
+
* @param svgOutputDir - The directory where SVG files should be written
|
|
13
|
+
* @returns Result object with hash, svgPath, and whether compilation occurred
|
|
14
|
+
* @throws Error if compilation fails
|
|
15
|
+
*/
|
|
16
|
+
declare function compileLatexToSvg(latexCode: string, svgOutputDir: string): CompilationResult;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Starlight plugin wrapper for remark-latex-compile.
|
|
20
|
+
*
|
|
21
|
+
* This plugin hooks into Starlight's config:setup to inject the remark-latex-compile
|
|
22
|
+
* plugin and the build-time Astro integration for scanning markdown files.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type StarlightLatexCompileOptions = RemarkLatexCompileOptions;
|
|
26
|
+
declare function starlightLatexCompile(options: StarlightLatexCompileOptions): {
|
|
27
|
+
name: string;
|
|
28
|
+
hooks: {
|
|
29
|
+
"config:setup": (hook: HookParameters<"config:setup">) => void;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface RemarkLatexCompileOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Directory where SVG files should be written.
|
|
36
|
+
*/
|
|
37
|
+
svgOutputDir: string;
|
|
38
|
+
}
|
|
39
|
+
declare function remarkLatexCompile(options: RemarkLatexCompileOptions): (tree: Record<string, unknown>, file: unknown) => void;
|
|
40
|
+
|
|
41
|
+
export { type RemarkLatexCompileOptions as R, type StarlightLatexCompileOptions as S, compileLatexToSvg as c, remarkLatexCompile as r, starlightLatexCompile as s };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { starlightIndexOnlySidebar } from './plugins/starlight-index-only-sidebar.js';
|
|
2
2
|
export { default as rehypeValidateLinks } from './plugins/rehype-validate-links.js';
|
|
3
3
|
import { AstroIntegration } from 'astro';
|
|
4
|
+
export { r as remarkLatexCompile, s as starlightLatexCompile } from './index-B9CyKYB4.js';
|
|
4
5
|
import '@astrojs/starlight/types';
|
|
5
6
|
import 'hast';
|
|
6
7
|
import 'vfile';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
rehypeValidateLinks
|
|
6
6
|
} from "./chunk-HRUZQZ5L.js";
|
|
7
|
+
import {
|
|
8
|
+
remarkLatexCompile,
|
|
9
|
+
starlightLatexCompile
|
|
10
|
+
} from "./chunk-T4UKGKU6.js";
|
|
7
11
|
|
|
8
12
|
// src/plugins/astro-normalize-paths.ts
|
|
9
13
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -74,5 +78,7 @@ function normalizeAssetPath(path, htmlFile, siteRootPath) {
|
|
|
74
78
|
export {
|
|
75
79
|
astroNormalizePaths,
|
|
76
80
|
rehypeValidateLinks,
|
|
77
|
-
|
|
81
|
+
remarkLatexCompile,
|
|
82
|
+
starlightIndexOnlySidebar,
|
|
83
|
+
starlightLatexCompile
|
|
78
84
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-cannoli-plugins",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"description": "Starlight plugins for automatic sidebar generation and link validation",
|
|
6
6
|
"license": "ISC",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"cannoli-latex-cleanup": "./dist/cli/cannoli-latex-cleanup.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": {
|
|
11
14
|
"import": "./dist/index.js",
|
|
@@ -23,6 +26,14 @@
|
|
|
23
26
|
"import": "./dist/plugins/astro-normalize-paths.js",
|
|
24
27
|
"types": "./dist/plugins/astro-normalize-paths.d.ts"
|
|
25
28
|
},
|
|
29
|
+
"./remark-latex-compile": {
|
|
30
|
+
"import": "./dist/plugins/remark-latex-compile.js",
|
|
31
|
+
"types": "./dist/plugins/remark-latex-compile.d.ts"
|
|
32
|
+
},
|
|
33
|
+
"./starlight-latex-compile": {
|
|
34
|
+
"import": "./dist/plugins/starlight-latex-compile.js",
|
|
35
|
+
"types": "./dist/plugins/starlight-latex-compile.d.ts"
|
|
36
|
+
},
|
|
26
37
|
"./styles": "./dist/styles/",
|
|
27
38
|
"./styles/*": "./dist/styles/*"
|
|
28
39
|
},
|
|
@@ -55,7 +66,7 @@
|
|
|
55
66
|
"clean:empty-dirs": "find src/content/docs -type d -empty -delete"
|
|
56
67
|
},
|
|
57
68
|
"dependencies": {
|
|
58
|
-
"eslint-cannoli-plugins": "^1.0.
|
|
69
|
+
"eslint-cannoli-plugins": "^1.0.13",
|
|
59
70
|
"glob": "^13.0.6",
|
|
60
71
|
"minimatch": "^10.2.4",
|
|
61
72
|
"unist-util-visit": "^5.0.0",
|