starlight-cannoli-plugins 1.1.0 → 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-IRJEKSXQ.js → chunk-T4UKGKU6.js} +201 -161
- package/dist/cli/cannoli-latex-cleanup.d.ts +1 -0
- package/dist/cli/cannoli-latex-cleanup.js +125 -0
- package/dist/{index-DapkTQmZ.d.ts → index-B9CyKYB4.d.ts} +3 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/remark-latex-compile.d.ts +1 -1
- package/dist/plugins/remark-latex-compile.js +1 -1
- package/dist/plugins/starlight-latex-compile.d.ts +1 -1
- package/dist/plugins/starlight-latex-compile.js +1 -1
- package/dist/static/.static-files-go-here +0 -0
- package/dist/styles/_starlight.scss +4 -0
- package/package.json +5 -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
|
|
@@ -3,13 +3,13 @@ import { resolve } from "path";
|
|
|
3
3
|
|
|
4
4
|
// src/plugins/remark-latex-compile/compile.ts
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
-
import {
|
|
6
|
+
import { spawnSync } from "child_process";
|
|
7
7
|
import {
|
|
8
8
|
existsSync,
|
|
9
9
|
mkdirSync,
|
|
10
|
-
copyFileSync,
|
|
11
10
|
writeFileSync,
|
|
12
|
-
rmSync
|
|
11
|
+
rmSync,
|
|
12
|
+
mkdtempSync
|
|
13
13
|
} from "fs";
|
|
14
14
|
import { join } from "path";
|
|
15
15
|
import { tmpdir } from "os";
|
|
@@ -17,116 +17,151 @@ import { tmpdir } from "os";
|
|
|
17
17
|
// src/plugins/remark-latex-compile/error-parser.ts
|
|
18
18
|
function parseLatexError(latexOutput) {
|
|
19
19
|
const lines = latexOutput.split("\n");
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
let
|
|
20
|
+
const errors = [];
|
|
21
|
+
const seenMessages = /* @__PURE__ */ new Set();
|
|
22
|
+
let hasFatal = false;
|
|
23
23
|
for (let i = 0; i < lines.length; i++) {
|
|
24
24
|
const line = lines[i];
|
|
25
|
-
if (line.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
37
40
|
}
|
|
38
|
-
errorLines.push(lines[j]);
|
|
39
41
|
break;
|
|
40
42
|
}
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
errors.push({
|
|
45
|
+
message,
|
|
46
|
+
line: lineNum,
|
|
47
|
+
context: context.slice(0, 3),
|
|
48
|
+
severity: "error"
|
|
49
|
+
});
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (contextLine.startsWith("l.")) {
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
for (let j = i; j < Math.min(i + 10, lines.length); j++) {
|
|
61
|
-
const lineMatch = lines[j].match(/^l\.(\d+)/);
|
|
62
|
-
if (lineMatch) {
|
|
63
|
-
errorLineNum = parseInt(lineMatch[1], 10);
|
|
64
|
-
break;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
break;
|
|
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
|
+
});
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
65
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
for (let j = Math.max(0, i - 5); j < i; j++) {
|
|
76
|
-
if (lines[j].startsWith("!")) {
|
|
77
|
-
mainError = lines[j].substring(1).trim();
|
|
78
|
-
errorLines.push(lines[j]);
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (!mainError) {
|
|
83
|
-
mainError = line.trim();
|
|
84
|
-
}
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
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;
|
|
87
70
|
}
|
|
88
71
|
}
|
|
89
|
-
if (
|
|
72
|
+
if (errors.length === 0 && latexOutput.length > 0) {
|
|
90
73
|
for (const line of lines) {
|
|
91
74
|
if (line.includes("error") || line.includes("Error") || line.includes("Misplaced") || line.includes("Missing")) {
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
84
|
break;
|
|
95
85
|
}
|
|
96
86
|
}
|
|
97
87
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 };
|
|
104
96
|
}
|
|
105
|
-
function formatLatexError(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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}`;
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
+
`;
|
|
115
128
|
}
|
|
116
129
|
}
|
|
130
|
+
for (const warn of errorsByType["warning"] || []) {
|
|
131
|
+
output += `${YELLOW}Warning${RESET}: ${warn.message}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
117
134
|
return output;
|
|
118
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
|
+
}
|
|
119
154
|
function createCompilationErrorMessage(latexSource, rawError) {
|
|
120
155
|
const parsed = parseLatexError(rawError);
|
|
121
156
|
const formatted = formatLatexError(parsed);
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
157
|
+
const formattedSource = formatLatexSourceWithLineNumbers(
|
|
158
|
+
latexSource,
|
|
159
|
+
parsed.errors
|
|
160
|
+
);
|
|
161
|
+
return `${formatted}
|
|
128
162
|
LaTeX source:
|
|
129
|
-
${
|
|
163
|
+
${formattedSource}
|
|
164
|
+
`;
|
|
130
165
|
}
|
|
131
166
|
|
|
132
167
|
// src/plugins/remark-latex-compile/compile.ts
|
|
@@ -135,21 +170,20 @@ function hashLatexCode(code) {
|
|
|
135
170
|
return createHash("md5").update(normalized).digest("hex").slice(0, 16);
|
|
136
171
|
}
|
|
137
172
|
function buildLatexSource(latexCode) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
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
|
+
}
|
|
141
181
|
return [
|
|
142
|
-
"\\documentclass[border=
|
|
143
|
-
|
|
144
|
-
"\\usepackage{xcolor}",
|
|
145
|
-
"\\pagecolor{white}",
|
|
182
|
+
"\\documentclass[border=4pt]{standalone}",
|
|
183
|
+
preamble,
|
|
146
184
|
"\\begin{document}",
|
|
147
185
|
"\\Large",
|
|
148
|
-
|
|
149
|
-
"\\fboxrule=0.5pt\\relax",
|
|
150
|
-
"\\fbox{",
|
|
151
|
-
codeWithoutPackages,
|
|
152
|
-
"}",
|
|
186
|
+
content,
|
|
153
187
|
"\\end{document}"
|
|
154
188
|
].join("\n");
|
|
155
189
|
}
|
|
@@ -160,43 +194,40 @@ function compileLatexToSvg(latexCode, svgOutputDir) {
|
|
|
160
194
|
return { hash, svgPath, wasCompiled: false };
|
|
161
195
|
}
|
|
162
196
|
mkdirSync(svgOutputDir, { recursive: true });
|
|
163
|
-
const workDir = join(tmpdir(),
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
const dviFile = join(workDir, `${hash}.dvi`);
|
|
167
|
-
const svgTempFile = join(workDir, `${hash}.svg`);
|
|
197
|
+
const workDir = mkdtempSync(join(tmpdir(), "latex-compile-"));
|
|
198
|
+
const texFile = join(workDir, "diagram.tex");
|
|
199
|
+
const pdfFile = join(workDir, "diagram.pdf");
|
|
168
200
|
const latexSource = buildLatexSource(latexCode);
|
|
169
201
|
try {
|
|
170
202
|
writeFileSync(texFile, latexSource, "utf-8");
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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() || "";
|
|
178
211
|
const userMessage = createCompilationErrorMessage(
|
|
179
212
|
latexSource,
|
|
180
213
|
errorOutput
|
|
181
214
|
);
|
|
182
215
|
throw new Error(userMessage);
|
|
183
216
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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() || "";
|
|
191
226
|
throw new Error(
|
|
192
|
-
`[remark-latex-compile]
|
|
193
|
-
Error: ${
|
|
194
|
-
Stderr: ${dvisvgmErr.stderr ?? ""}Stdout: ${dvisvgmErr.stdout ?? ""}`
|
|
227
|
+
`[remark-latex-compile] PDF to SVG conversion failed (hash: ${hash}).
|
|
228
|
+
Error: ${errorOutput}`
|
|
195
229
|
);
|
|
196
230
|
}
|
|
197
|
-
copyFileSync(svgTempFile, svgPath);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
throw err;
|
|
200
231
|
} finally {
|
|
201
232
|
try {
|
|
202
233
|
rmSync(workDir, { recursive: true, force: true });
|
|
@@ -207,37 +238,53 @@ Stderr: ${dvisvgmErr.stderr ?? ""}Stdout: ${dvisvgmErr.stdout ?? ""}`
|
|
|
207
238
|
}
|
|
208
239
|
|
|
209
240
|
// src/plugins/remark-latex-compile/index.ts
|
|
210
|
-
function traverseTree(node, svgOutputDir, depth = 0) {
|
|
241
|
+
function traverseTree(node, svgOutputDir, filePath, depth = 0) {
|
|
211
242
|
if (!node) return;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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")) {
|
|
216
248
|
try {
|
|
217
|
-
const result = compileLatexToSvg(child.value, svgOutputDir);
|
|
218
|
-
|
|
249
|
+
const result = compileLatexToSvg(String(child.value), svgOutputDir);
|
|
250
|
+
children[i] = {
|
|
219
251
|
type: "paragraph",
|
|
220
252
|
children: [
|
|
221
253
|
{
|
|
222
254
|
type: "image",
|
|
223
255
|
url: `/static/tex-svgs/${result.hash}.svg`,
|
|
224
|
-
alt: "LaTeX diagram"
|
|
256
|
+
alt: "LaTeX diagram",
|
|
257
|
+
data: {
|
|
258
|
+
hProperties: {
|
|
259
|
+
className: ["tex-compiled"]
|
|
260
|
+
}
|
|
261
|
+
}
|
|
225
262
|
}
|
|
226
263
|
]
|
|
227
264
|
};
|
|
228
265
|
} catch (err) {
|
|
229
|
-
|
|
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
|
+
}
|
|
230
275
|
}
|
|
231
276
|
} else {
|
|
232
|
-
traverseTree(child, svgOutputDir, depth + 1);
|
|
277
|
+
traverseTree(child, svgOutputDir, filePath, depth + 1);
|
|
233
278
|
}
|
|
234
279
|
}
|
|
235
280
|
}
|
|
236
281
|
}
|
|
237
282
|
function remarkLatexCompile(options) {
|
|
238
|
-
const svgOutputDir =
|
|
239
|
-
return (tree) => {
|
|
240
|
-
|
|
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);
|
|
241
288
|
};
|
|
242
289
|
}
|
|
243
290
|
|
|
@@ -250,7 +297,7 @@ function hashLatexCode2(code) {
|
|
|
250
297
|
return createHash2("md5").update(normalized).digest("hex").slice(0, 16);
|
|
251
298
|
}
|
|
252
299
|
function createAstroLatexIntegration(options) {
|
|
253
|
-
const svgOutputDir =
|
|
300
|
+
const svgOutputDir = resolve2(options.svgOutputDir);
|
|
254
301
|
const contentDir = options?.contentDir ? resolve2(options.contentDir) : resolve2("src/content/docs");
|
|
255
302
|
return {
|
|
256
303
|
name: "astro-latex-compile",
|
|
@@ -259,14 +306,7 @@ function createAstroLatexIntegration(options) {
|
|
|
259
306
|
console.log(
|
|
260
307
|
"[astro-latex-compile] Build start, scanning for tex/latex compile blocks"
|
|
261
308
|
);
|
|
262
|
-
|
|
263
|
-
await scanAndCompileLatex(contentDir, svgOutputDir);
|
|
264
|
-
} catch (err) {
|
|
265
|
-
console.error(
|
|
266
|
-
"[astro-latex-compile] Error during LaTeX compilation:",
|
|
267
|
-
err
|
|
268
|
-
);
|
|
269
|
-
}
|
|
309
|
+
await scanAndCompileLatex(contentDir, svgOutputDir);
|
|
270
310
|
},
|
|
271
311
|
"astro:build:done": async ({ dir }) => {
|
|
272
312
|
console.log(
|
|
@@ -315,10 +355,10 @@ async function processMarkdownFile(filePath, svgOutputDir) {
|
|
|
315
355
|
`[astro-latex-compile] ${filePath}:${lineNumber}: ${status} ${result.hash}.svg`
|
|
316
356
|
);
|
|
317
357
|
} catch (err) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
358
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
359
|
+
error.message = `${filePath}:${lineNumber}
|
|
360
|
+
${error.message}`;
|
|
361
|
+
throw error;
|
|
322
362
|
}
|
|
323
363
|
}
|
|
324
364
|
}
|
|
@@ -331,7 +371,7 @@ async function updateHtmlReferences(buildDir, contentDir, svgOutputDir) {
|
|
|
331
371
|
await scanMarkdownForHashes(fullPath, latexHashes);
|
|
332
372
|
}
|
|
333
373
|
}
|
|
334
|
-
await updateHtmlDirWithHashes(buildDir, latexHashes);
|
|
374
|
+
await updateHtmlDirWithHashes(buildDir, latexHashes, svgOutputDir);
|
|
335
375
|
}
|
|
336
376
|
async function scanMarkdownForHashes(dir, hashes) {
|
|
337
377
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
@@ -351,26 +391,26 @@ async function scanMarkdownForHashes(dir, hashes) {
|
|
|
351
391
|
}
|
|
352
392
|
}
|
|
353
393
|
}
|
|
354
|
-
async function updateHtmlDirWithHashes(dir, hashes) {
|
|
394
|
+
async function updateHtmlDirWithHashes(dir, hashes, svgOutputDir) {
|
|
355
395
|
let hashIndex = 0;
|
|
356
396
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
357
397
|
for (const entry of entries) {
|
|
358
398
|
const fullPath = join2(dir, entry.name);
|
|
359
399
|
if (entry.isDirectory()) {
|
|
360
|
-
await updateHtmlDirWithHashes(fullPath, hashes);
|
|
400
|
+
await updateHtmlDirWithHashes(fullPath, hashes, svgOutputDir);
|
|
361
401
|
} else if (entry.isFile() && entry.name.endsWith(".html")) {
|
|
362
402
|
let content = await readFile(fullPath, "utf-8");
|
|
363
403
|
let modified = false;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
}
|
|
371
|
-
return arguments[0];
|
|
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"`;
|
|
372
411
|
}
|
|
373
|
-
|
|
412
|
+
return match;
|
|
413
|
+
});
|
|
374
414
|
if (modified) {
|
|
375
415
|
await writeFile(fullPath, content, "utf-8");
|
|
376
416
|
console.log(`[astro-latex-compile] Updated ${fullPath}`);
|
|
@@ -392,7 +432,7 @@ function starlightLatexCompile(options) {
|
|
|
392
432
|
const existingPlugins = (Array.isArray(config.markdown?.remarkPlugins) ? config.markdown.remarkPlugins : []).filter((p) => p !== void 0 && p !== null);
|
|
393
433
|
updateConfig({
|
|
394
434
|
markdown: {
|
|
395
|
-
remarkPlugins: [...existingPlugins, [remarkLatexCompile, options
|
|
435
|
+
remarkPlugins: [...existingPlugins, [remarkLatexCompile, options]]
|
|
396
436
|
}
|
|
397
437
|
});
|
|
398
438
|
}
|
|
@@ -400,7 +440,7 @@ function starlightLatexCompile(options) {
|
|
|
400
440
|
});
|
|
401
441
|
hook.addIntegration(
|
|
402
442
|
createAstroLatexIntegration({
|
|
403
|
-
svgOutputDir: options
|
|
443
|
+
svgOutputDir: options.svgOutputDir
|
|
404
444
|
})
|
|
405
445
|
);
|
|
406
446
|
}
|
|
@@ -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();
|
|
@@ -23,7 +23,7 @@ declare function compileLatexToSvg(latexCode: string, svgOutputDir: string): Com
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
type StarlightLatexCompileOptions = RemarkLatexCompileOptions;
|
|
26
|
-
declare function starlightLatexCompile(options
|
|
26
|
+
declare function starlightLatexCompile(options: StarlightLatexCompileOptions): {
|
|
27
27
|
name: string;
|
|
28
28
|
hooks: {
|
|
29
29
|
"config:setup": (hook: HookParameters<"config:setup">) => void;
|
|
@@ -33,10 +33,9 @@ declare function starlightLatexCompile(options?: StarlightLatexCompileOptions):
|
|
|
33
33
|
interface RemarkLatexCompileOptions {
|
|
34
34
|
/**
|
|
35
35
|
* Directory where SVG files should be written.
|
|
36
|
-
* @default "public/static/tex-svgs"
|
|
37
36
|
*/
|
|
38
|
-
svgOutputDir
|
|
37
|
+
svgOutputDir: string;
|
|
39
38
|
}
|
|
40
|
-
declare function remarkLatexCompile(options
|
|
39
|
+
declare function remarkLatexCompile(options: RemarkLatexCompileOptions): (tree: Record<string, unknown>, file: unknown) => void;
|
|
41
40
|
|
|
42
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,7 +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-
|
|
4
|
+
export { r as remarkLatexCompile, s as starlightLatexCompile } from './index-B9CyKYB4.js';
|
|
5
5
|
import '@astrojs/starlight/types';
|
|
6
6
|
import 'hast';
|
|
7
7
|
import 'vfile';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { R as RemarkLatexCompileOptions, c as compileLatexToSvg, r as default, s as starlightLatexCompile } from '../index-
|
|
1
|
+
export { R as RemarkLatexCompileOptions, c as compileLatexToSvg, r as default, s as starlightLatexCompile } from '../index-B9CyKYB4.js';
|
|
2
2
|
import '@astrojs/starlight/types';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import '@astrojs/starlight/types';
|
|
2
|
-
export { S as StarlightLatexCompileOptions, s as default, s as starlightLatexCompile } from '../index-
|
|
2
|
+
export { S as StarlightLatexCompileOptions, s as default, s as starlightLatexCompile } from '../index-B9CyKYB4.js';
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-cannoli-plugins",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
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",
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
"clean:empty-dirs": "find src/content/docs -type d -empty -delete"
|
|
64
67
|
},
|
|
65
68
|
"dependencies": {
|
|
66
|
-
"eslint-cannoli-plugins": "^1.0.
|
|
69
|
+
"eslint-cannoli-plugins": "^1.0.13",
|
|
67
70
|
"glob": "^13.0.6",
|
|
68
71
|
"minimatch": "^10.2.4",
|
|
69
72
|
"unist-util-visit": "^5.0.0",
|