starlight-cannoli-plugins 1.2.18 → 2.0.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 +25 -39
- package/dist/chunk-3OL3VUEB.js +143 -0
- package/dist/{chunk-KQM5EAEK.js → chunk-47X5MKFJ.js} +52 -27
- package/dist/chunk-AZPHBHBE.js +71 -0
- package/dist/{chunk-HRUZQZ5L.js → chunk-FQLJMU2J.js} +15 -13
- package/dist/chunk-S4YMC25A.js +149 -0
- package/dist/cli/cannoli-latex-cleanup.js +1 -1
- package/dist/index.d.ts +17 -11
- package/dist/index.js +64 -67
- package/dist/plugins/astro-normalize-paths.d.ts +12 -0
- package/dist/plugins/astro-normalize-paths.js +9 -0
- package/dist/plugins/rehype-validate-links.js +1 -1
- package/dist/plugins/remark-latex-compile.d.ts +41 -4
- package/dist/plugins/remark-latex-compile.js +4 -6
- package/dist/plugins/starlight-sync-docs-to-public.d.ts +6 -9
- package/dist/plugins/starlight-sync-docs-to-public.js +5 -3
- package/package.json +1 -5
- package/dist/chunk-DEXMXUQL.js +0 -229
- package/dist/chunk-SBGY6FD3.js +0 -191
- package/dist/index-Ce1VCMrW.d.ts +0 -45
- package/dist/plugins/starlight-latex-compile.d.ts +0 -4
- package/dist/plugins/starlight-latex-compile.js +0 -10
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ export default defineConfig({
|
|
|
48
48
|
});
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### LaTeX Compile
|
|
52
52
|
|
|
53
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
54
|
|
|
@@ -58,7 +58,7 @@ Automatically compiles fenced `tex compile` and `latex compile` code blocks to S
|
|
|
58
58
|
- Caches compiled SVGs by content hash (no recompilation if unchanged)
|
|
59
59
|
- Comprehensive error reporting with line numbers and formatted LaTeX source
|
|
60
60
|
- Supports custom preamble via `% ===` separator in code blocks
|
|
61
|
-
- Works
|
|
61
|
+
- Works with Starlight and plain Astro projects
|
|
62
62
|
- Requires `svgOutputDir` configuration (no defaults)
|
|
63
63
|
|
|
64
64
|
**System Requirements:**
|
|
@@ -75,24 +75,26 @@ pdflatex --version
|
|
|
75
75
|
dvisvgm --version
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
**Options:**
|
|
79
|
+
|
|
80
|
+
- `svgOutputDir` (required): Directory where compiled SVG files are written. Must be inside `public/` so Astro serves them as static assets.
|
|
81
|
+
- `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.
|
|
82
|
+
|
|
78
83
|
**Usage:**
|
|
79
84
|
|
|
80
85
|
```ts
|
|
81
86
|
// astro.config.mjs
|
|
82
87
|
import { defineConfig } from "astro/config";
|
|
83
88
|
import starlight from "@astrojs/starlight";
|
|
84
|
-
import {
|
|
89
|
+
import { astroLatexCompile } from "cannoli-starlight-plugins";
|
|
85
90
|
|
|
86
91
|
export default defineConfig({
|
|
87
92
|
integrations: [
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
starlightLatexCompile({
|
|
92
|
-
svgOutputDir: "public/static/tex-svgs",
|
|
93
|
-
}),
|
|
94
|
-
],
|
|
93
|
+
astroLatexCompile({
|
|
94
|
+
svgOutputDir: "public/static/tex-svgs",
|
|
95
|
+
removeOrphanedSvgs: true, // optional
|
|
95
96
|
}),
|
|
97
|
+
starlight({ title: "My Docs" }),
|
|
96
98
|
],
|
|
97
99
|
});
|
|
98
100
|
```
|
|
@@ -174,16 +176,9 @@ Add custom CSS classes the `tex compile` code block to have them applied to the
|
|
|
174
176
|
|
|
175
177
|
The img element will have classes: `tex-compiled bg-white rounded-1` (note: the `tex-compiled` class is always included by default).
|
|
176
178
|
|
|
177
|
-
### Remark LaTeX Compile
|
|
178
|
-
|
|
179
|
-
The underlying remark plugin that powers `starlightLatexCompile`. Use this directly in Astro projects that don't use Starlight.
|
|
180
|
-
|
|
181
|
-
**System Requirements:**
|
|
179
|
+
### Remark LaTeX Compile (low-level)
|
|
182
180
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
- **`pdflatex`** — LaTeX compiler that produces PDF output
|
|
186
|
-
- **`dvisvgm`** — Converts PDF to SVG format
|
|
181
|
+
The underlying remark plugin used by `astroLatexCompile`. Use this directly if you need to wire the transformer into a custom pipeline — most users should use `astroLatexCompile` instead.
|
|
187
182
|
|
|
188
183
|
**Usage:**
|
|
189
184
|
|
|
@@ -195,18 +190,13 @@ import { remarkLatexCompile } from "cannoli-starlight-plugins/remark-latex-compi
|
|
|
195
190
|
export default defineConfig({
|
|
196
191
|
markdown: {
|
|
197
192
|
remarkPlugins: [
|
|
198
|
-
[
|
|
199
|
-
remarkLatexCompile,
|
|
200
|
-
{
|
|
201
|
-
svgOutputDir: "public/static/tex-svgs",
|
|
202
|
-
},
|
|
203
|
-
],
|
|
193
|
+
[remarkLatexCompile, { svgOutputDir: "public/static/tex-svgs" }],
|
|
204
194
|
],
|
|
205
195
|
},
|
|
206
196
|
});
|
|
207
197
|
```
|
|
208
198
|
|
|
209
|
-
|
|
199
|
+
Note: when used directly (without `astroLatexCompile`), the Starlight content layer cache is not cleared automatically, so SVGs may not recompile on repeat builds in Starlight projects.
|
|
210
200
|
|
|
211
201
|
### Rehype Validate Links
|
|
212
202
|
|
|
@@ -288,7 +278,7 @@ export default defineConfig({
|
|
|
288
278
|
});
|
|
289
279
|
```
|
|
290
280
|
|
|
291
|
-
###
|
|
281
|
+
### Sync Docs to Public
|
|
292
282
|
|
|
293
283
|
Syncs `src/content/docs/` to `public/` so local files (e.g., PDFs, images) referenced in markdown are served by the dev server and included in builds. In dev mode, it watches for file changes and re-syncs automatically — no restart needed.
|
|
294
284
|
|
|
@@ -305,18 +295,12 @@ Syncs `src/content/docs/` to `public/` so local files (e.g., PDFs, images) refer
|
|
|
305
295
|
// astro.config.mjs
|
|
306
296
|
import { defineConfig } from "astro/config";
|
|
307
297
|
import starlight from "@astrojs/starlight";
|
|
308
|
-
import {
|
|
298
|
+
import { syncDocsToPublic } from "cannoli-starlight-plugins";
|
|
309
299
|
|
|
310
300
|
export default defineConfig({
|
|
311
301
|
integrations: [
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
plugins: [
|
|
315
|
-
starlightSyncDocsToPublic({
|
|
316
|
-
preserveDirs: ["static"],
|
|
317
|
-
}),
|
|
318
|
-
],
|
|
319
|
-
}),
|
|
302
|
+
syncDocsToPublic({ preserveDirs: ["static"] }),
|
|
303
|
+
starlight({ title: "My Docs" }),
|
|
320
304
|
],
|
|
321
305
|
});
|
|
322
306
|
```
|
|
@@ -327,7 +311,7 @@ export default defineConfig({
|
|
|
327
311
|
- `ignorePatterns` (optional): Glob patterns for files to exclude from syncing. Patterns are matched against paths relative to `src/content/docs/`.
|
|
328
312
|
|
|
329
313
|
```ts
|
|
330
|
-
|
|
314
|
+
syncDocsToPublic({
|
|
331
315
|
preserveDirs: ["static"],
|
|
332
316
|
ignorePatterns: ["**/*.txt", "**/drafts/**"],
|
|
333
317
|
});
|
|
@@ -337,7 +321,9 @@ starlightSyncDocsToPublic({
|
|
|
337
321
|
|
|
338
322
|
### cannoli-latex-cleanup
|
|
339
323
|
|
|
340
|
-
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.
|
|
324
|
+
A manual 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.
|
|
325
|
+
|
|
326
|
+
> **Note:** If you use `removeOrphanedSvgs: true` in your `astroLatexCompile` config, this CLI is generally not needed — orphaned SVGs are cleaned up automatically during both dev and build.
|
|
341
327
|
|
|
342
328
|
**Usage:**
|
|
343
329
|
|
|
@@ -361,7 +347,7 @@ npx cannoli-latex-cleanup --svg-dir public/static/tex-svgs --docs-dir ./src/cont
|
|
|
361
347
|
|
|
362
348
|
**Options:**
|
|
363
349
|
|
|
364
|
-
- `--svg-dir` (required): Path to the SVG output directory configured in `
|
|
350
|
+
- `--svg-dir` (required): Path to the SVG output directory configured in `astroLatexCompile`
|
|
365
351
|
- `--docs-dir` (optional, default: `src/content/docs`): Path to markdown source directory
|
|
366
352
|
- `--check`: List orphaned SVGs without deleting
|
|
367
353
|
- `--delete`: Delete orphaned SVGs
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseFrontmatter
|
|
3
|
+
} from "./chunk-3ATSZG6H.js";
|
|
4
|
+
|
|
5
|
+
// src/plugins/starlight-sync-docs-to-public.ts
|
|
6
|
+
import { cp, mkdir, readdir, readFile, writeFile, rm, stat } from "fs/promises";
|
|
7
|
+
import { resolve, relative } from "path";
|
|
8
|
+
import { minimatch } from "minimatch";
|
|
9
|
+
var DEFAULT_SRC_DIR = "src/content/docs";
|
|
10
|
+
var DEFAULT_PUBLIC_DIR = "public";
|
|
11
|
+
async function fullSync(srcDir, publicDir, preserveDirs, ignorePatterns) {
|
|
12
|
+
await mkdir(publicDir, { recursive: true });
|
|
13
|
+
const items = await readdir(publicDir, { withFileTypes: true });
|
|
14
|
+
await Promise.all(
|
|
15
|
+
items.filter((item) => item.isDirectory() && !preserveDirs.includes(item.name)).map((item) => rm(resolve(publicDir, item.name), { recursive: true, force: true }))
|
|
16
|
+
);
|
|
17
|
+
await copyWithRetry(srcDir, publicDir, ignorePatterns);
|
|
18
|
+
console.log(
|
|
19
|
+
`[starlight-sync-docs-to-public] Synced ${DEFAULT_SRC_DIR}/ \u2192 ${DEFAULT_PUBLIC_DIR}/ (full sync)`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
async function incrementalSync(changedFilePath, srcDir, publicDir, ignorePatterns) {
|
|
23
|
+
const rel = relative(srcDir, changedFilePath);
|
|
24
|
+
if (ignorePatterns.some((pattern) => minimatch(rel, pattern, { dot: true }))) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (changedFilePath.endsWith(".md") || changedFilePath.endsWith(".mdx")) {
|
|
28
|
+
try {
|
|
29
|
+
const frontmatter = parseFrontmatter(changedFilePath);
|
|
30
|
+
if (frontmatter.draft === true) return;
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const destPath = resolve(publicDir, rel);
|
|
36
|
+
let srcStat;
|
|
37
|
+
try {
|
|
38
|
+
srcStat = await stat(changedFilePath);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err?.code === "ENOENT") {
|
|
41
|
+
await rm(destPath, { recursive: true, force: true });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
if (srcStat.isDirectory()) {
|
|
47
|
+
await mkdir(destPath, { recursive: true });
|
|
48
|
+
const files = await readdir(changedFilePath);
|
|
49
|
+
await Promise.all(
|
|
50
|
+
files.map(
|
|
51
|
+
(file) => incrementalSync(resolve(changedFilePath, file), srcDir, publicDir, ignorePatterns)
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
await mkdir(resolve(destPath, ".."), { recursive: true });
|
|
56
|
+
const content = await readFile(changedFilePath);
|
|
57
|
+
await writeFile(destPath, content);
|
|
58
|
+
}
|
|
59
|
+
console.log(`[starlight-sync-docs-to-public] Synced ${rel}`);
|
|
60
|
+
}
|
|
61
|
+
async function copyWithRetry(srcDir, publicDir, ignorePatterns) {
|
|
62
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
await cp(srcDir, publicDir, {
|
|
65
|
+
recursive: true,
|
|
66
|
+
force: true,
|
|
67
|
+
filter: (src) => {
|
|
68
|
+
const rel = relative(srcDir, src);
|
|
69
|
+
if (rel === "") return true;
|
|
70
|
+
if (ignorePatterns.some((pattern) => minimatch(rel, pattern, { dot: true }))) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (src.endsWith(".md") || src.endsWith(".mdx")) {
|
|
74
|
+
try {
|
|
75
|
+
const frontmatter = parseFrontmatter(src);
|
|
76
|
+
if (frontmatter.draft === true) return false;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const code = err?.code;
|
|
87
|
+
if (code !== "EEXIST" && code !== "ENOENT") throw err;
|
|
88
|
+
if (attempt < 2) await new Promise((r) => setTimeout(r, 10 * (attempt + 1)));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function syncDocsToPublic(options) {
|
|
93
|
+
const srcDir = resolve(DEFAULT_SRC_DIR);
|
|
94
|
+
const publicDir = resolve(DEFAULT_PUBLIC_DIR);
|
|
95
|
+
const { preserveDirs, ignorePatterns = [] } = options;
|
|
96
|
+
return {
|
|
97
|
+
name: "astro-sync-docs-to-public",
|
|
98
|
+
hooks: {
|
|
99
|
+
"astro:build:start": async () => {
|
|
100
|
+
await fullSync(srcDir, publicDir, preserveDirs, ignorePatterns);
|
|
101
|
+
},
|
|
102
|
+
"astro:server:setup": ({ server }) => {
|
|
103
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
104
|
+
let isSyncing = false;
|
|
105
|
+
let debounceTimer = null;
|
|
106
|
+
const performSync = async () => {
|
|
107
|
+
if (isSyncing) return;
|
|
108
|
+
isSyncing = true;
|
|
109
|
+
try {
|
|
110
|
+
while (pendingPaths.size > 0) {
|
|
111
|
+
const paths = [...pendingPaths];
|
|
112
|
+
pendingPaths.clear();
|
|
113
|
+
await Promise.all(
|
|
114
|
+
paths.map(
|
|
115
|
+
(p) => incrementalSync(p, srcDir, publicDir, ignorePatterns).catch((err) => {
|
|
116
|
+
console.error(`[starlight-sync-docs-to-public] Error syncing ${p}:`, err);
|
|
117
|
+
})
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
isSyncing = false;
|
|
123
|
+
if (pendingPaths.size > 0) void performSync();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
fullSync(srcDir, publicDir, preserveDirs, ignorePatterns).catch(console.error);
|
|
127
|
+
server.watcher.add(srcDir);
|
|
128
|
+
server.watcher.on("all", (_event, filePath) => {
|
|
129
|
+
if (!filePath.startsWith(srcDir)) return;
|
|
130
|
+
pendingPaths.add(filePath);
|
|
131
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
132
|
+
debounceTimer = setTimeout(performSync, 100);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
var starlightSyncDocsToPublic = syncDocsToPublic;
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
syncDocsToPublic,
|
|
142
|
+
starlightSyncDocsToPublic
|
|
143
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/plugins/remark-latex-compile/
|
|
1
|
+
// src/plugins/remark-latex-compile/utils.ts
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
4
|
import {
|
|
5
5
|
existsSync,
|
|
6
6
|
mkdirSync,
|
|
@@ -161,7 +161,7 @@ ${formattedSource}
|
|
|
161
161
|
`;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// src/plugins/remark-latex-compile/
|
|
164
|
+
// src/plugins/remark-latex-compile/utils.ts
|
|
165
165
|
var LATEX_BLOCK_REGEX = /```(?:tex|latex)\s+compile[^\r\n]*\r?\n([\s\S]*?)\r?\n```/g;
|
|
166
166
|
function hashLatexCode(code) {
|
|
167
167
|
const normalized = code.split("\n").map((line) => line.trim()).filter((line) => !line.startsWith("%")).filter(Boolean).join("\n").trim();
|
|
@@ -188,7 +188,26 @@ function buildLatexSource(latexCode) {
|
|
|
188
188
|
"\\end{document}"
|
|
189
189
|
].join("\n");
|
|
190
190
|
}
|
|
191
|
-
function
|
|
191
|
+
function execProcess(command, args) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
let stdout = "";
|
|
194
|
+
let stderr = "";
|
|
195
|
+
const proc = spawn(command, args);
|
|
196
|
+
proc.stdout.on("data", (data) => {
|
|
197
|
+
stdout += data.toString();
|
|
198
|
+
});
|
|
199
|
+
proc.stderr.on("data", (data) => {
|
|
200
|
+
stderr += data.toString();
|
|
201
|
+
});
|
|
202
|
+
proc.on("error", (err) => {
|
|
203
|
+
reject(err);
|
|
204
|
+
});
|
|
205
|
+
proc.on("close", (code) => {
|
|
206
|
+
resolve({ status: code ?? 1, stdout, stderr });
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async function compileLatexToSvg(latexCode, svgOutputDir) {
|
|
192
211
|
const hash = hashLatexCode(latexCode);
|
|
193
212
|
const svgPath = join(svgOutputDir, `${hash}.svg`);
|
|
194
213
|
if (existsSync(svgPath)) {
|
|
@@ -201,41 +220,47 @@ function compileLatexToSvg(latexCode, svgOutputDir) {
|
|
|
201
220
|
const latexSource = buildLatexSource(latexCode);
|
|
202
221
|
try {
|
|
203
222
|
writeFileSync(texFile, latexSource, "utf-8");
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
"
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
let result;
|
|
224
|
+
try {
|
|
225
|
+
result = await execProcess("pdflatex", [
|
|
226
|
+
"-interaction=nonstopmode",
|
|
227
|
+
"-output-directory",
|
|
228
|
+
workDir,
|
|
229
|
+
texFile
|
|
230
|
+
]);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const code = err.code;
|
|
212
233
|
throw new Error(
|
|
213
|
-
`[remark-latex-compile] pdflatex not found on PATH (${code})
|
|
234
|
+
`[remark-latex-compile] pdflatex not found on PATH (${code}).`,
|
|
235
|
+
{ cause: err }
|
|
214
236
|
);
|
|
215
237
|
}
|
|
216
|
-
if (
|
|
217
|
-
const errorOutput =
|
|
238
|
+
if (result.status !== 0) {
|
|
239
|
+
const errorOutput = result.stderr || result.stdout || "";
|
|
218
240
|
const userMessage = createCompilationErrorMessage(
|
|
219
241
|
latexSource,
|
|
220
242
|
errorOutput
|
|
221
243
|
);
|
|
222
244
|
throw new Error(userMessage);
|
|
223
245
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
246
|
+
let svgResult;
|
|
247
|
+
try {
|
|
248
|
+
svgResult = await execProcess("dvisvgm", [
|
|
249
|
+
"--pdf",
|
|
250
|
+
"--bbox=dvi",
|
|
251
|
+
pdfFile,
|
|
252
|
+
"-o",
|
|
253
|
+
svgPath
|
|
254
|
+
]);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
const code = err.code;
|
|
233
257
|
throw new Error(
|
|
234
|
-
`[remark-latex-compile] dvisvgm not found on PATH (${code})
|
|
258
|
+
`[remark-latex-compile] dvisvgm not found on PATH (${code}).`,
|
|
259
|
+
{ cause: err }
|
|
235
260
|
);
|
|
236
261
|
}
|
|
237
|
-
if (
|
|
238
|
-
const errorOutput =
|
|
262
|
+
if (svgResult.status !== 0) {
|
|
263
|
+
const errorOutput = svgResult.stderr || svgResult.stdout || "";
|
|
239
264
|
throw new Error(
|
|
240
265
|
`[remark-latex-compile] PDF to SVG conversion failed (hash: ${hash}).
|
|
241
266
|
Error: ${errorOutput}`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// src/plugins/astro-normalize-paths.ts
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { readFile, writeFile } from "fs/promises";
|
|
4
|
+
import { glob } from "glob";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
function astroNormalizePaths() {
|
|
7
|
+
return {
|
|
8
|
+
name: "astro-normalize-paths",
|
|
9
|
+
hooks: {
|
|
10
|
+
"astro:build:done": async ({ dir }) => {
|
|
11
|
+
const htmlFiles = await glob(`${dir.pathname}/**/*.html`);
|
|
12
|
+
await Promise.all(
|
|
13
|
+
htmlFiles.map(async (htmlFile) => {
|
|
14
|
+
let content = await readFile(htmlFile, "utf-8");
|
|
15
|
+
const originalContent = content;
|
|
16
|
+
const imgRegex = /<img([^>]*?)src=["']([^"']+)["']/g;
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = imgRegex.exec(content)) !== null) {
|
|
19
|
+
const attrs = match[1];
|
|
20
|
+
const src = match[2];
|
|
21
|
+
const normalized = normalizeAssetPath(src, htmlFile, dir.pathname);
|
|
22
|
+
if (normalized && src !== normalized) {
|
|
23
|
+
console.log(`[astro-normalize-paths] Img path resolution:`);
|
|
24
|
+
console.log(` Original: ${src}`);
|
|
25
|
+
console.log(` HTML file: ${htmlFile}`);
|
|
26
|
+
console.log(` Normalized: ${normalized}`);
|
|
27
|
+
content = content.replace(`<img${attrs}src="${src}"`, `<img${attrs}src="${normalized}"`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const anchorRegex = /<a([^>]*?)href=["']([^"']+)["']/g;
|
|
31
|
+
while ((match = anchorRegex.exec(content)) !== null) {
|
|
32
|
+
const attrs = match[1];
|
|
33
|
+
const href = match[2];
|
|
34
|
+
const normalized = normalizeAssetPath(href, htmlFile, dir.pathname);
|
|
35
|
+
if (normalized && href !== normalized) {
|
|
36
|
+
content = content.replace(`<a${attrs}href="${href}"`, `<a${attrs}href="${normalized}"`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (content !== originalContent) {
|
|
40
|
+
await writeFile(htmlFile, content, "utf-8");
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function normalizeAssetPath(path, htmlFile, siteRootPath) {
|
|
49
|
+
if (path.startsWith("http") || path.startsWith("data:") || path.startsWith("/")) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const htmlDir = dirname(htmlFile);
|
|
53
|
+
const resolvedPath = resolve(htmlDir, path);
|
|
54
|
+
const siteRoot = resolve(siteRootPath);
|
|
55
|
+
let absolutePath = resolvedPath.slice(siteRoot.length).replace(/\\/g, "/");
|
|
56
|
+
if (!existsSync(resolvedPath)) {
|
|
57
|
+
const parentDir = dirname(htmlDir);
|
|
58
|
+
const alternativePath = resolve(parentDir, path);
|
|
59
|
+
if (existsSync(alternativePath)) {
|
|
60
|
+
absolutePath = alternativePath.slice(siteRoot.length).replace(/\\/g, "/");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const finalPath = "/" + absolutePath.replace(/^\/+/, "");
|
|
64
|
+
return finalPath;
|
|
65
|
+
}
|
|
66
|
+
var astro_normalize_paths_default = astroNormalizePaths;
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
astroNormalizePaths,
|
|
70
|
+
astro_normalize_paths_default
|
|
71
|
+
};
|
|
@@ -72,24 +72,17 @@ function validateLink(link) {
|
|
|
72
72
|
if (project_absolute_href.includes(".{md,mdx}")) {
|
|
73
73
|
const matches = globSync(project_absolute_href);
|
|
74
74
|
if (matches.length === 0) {
|
|
75
|
-
|
|
76
|
-
`Link validation error: No matching file found for: ${link.original_href} (pattern: ${project_absolute_href})`
|
|
77
|
-
);
|
|
75
|
+
return `No matching file found for: ${link.original_href} (pattern: ${project_absolute_href})`;
|
|
78
76
|
}
|
|
79
77
|
if (matches.length > 1) {
|
|
80
|
-
|
|
81
|
-
`Link validation error: Multiple matching files found: ${matches.join(
|
|
82
|
-
", "
|
|
83
|
-
)} (from link: ${link.original_href})`
|
|
84
|
-
);
|
|
78
|
+
return `Multiple matching files found: ${matches.join(", ")} (from link: ${link.original_href})`;
|
|
85
79
|
}
|
|
86
80
|
} else {
|
|
87
81
|
if (!existsSync(project_absolute_href)) {
|
|
88
|
-
|
|
89
|
-
`Link validation error: File not found: ${project_absolute_href} (from link: ${link.original_href})`
|
|
90
|
-
);
|
|
82
|
+
return `File not found: ${project_absolute_href} (from link: ${link.original_href})`;
|
|
91
83
|
}
|
|
92
84
|
}
|
|
85
|
+
return null;
|
|
93
86
|
}
|
|
94
87
|
function rehypeValidateLinks(options) {
|
|
95
88
|
return (tree, file) => {
|
|
@@ -100,6 +93,7 @@ function rehypeValidateLinks(options) {
|
|
|
100
93
|
);
|
|
101
94
|
return;
|
|
102
95
|
}
|
|
96
|
+
const errors = [];
|
|
103
97
|
visit(tree, "element", (node) => {
|
|
104
98
|
if (node.tagName !== "a") return;
|
|
105
99
|
const href = node.properties?.href;
|
|
@@ -108,9 +102,17 @@ function rehypeValidateLinks(options) {
|
|
|
108
102
|
if (!link) return;
|
|
109
103
|
if (link.skipValidation) return;
|
|
110
104
|
if (node.properties?.["data-no-link-check"] !== void 0) return;
|
|
111
|
-
if (matchesSkipPattern(link.site_absolute_href, options?.skipPatterns))
|
|
112
|
-
|
|
105
|
+
if (matchesSkipPattern(link.site_absolute_href, options?.skipPatterns))
|
|
106
|
+
return;
|
|
107
|
+
const error = validateLink(link);
|
|
108
|
+
if (error) errors.push(error);
|
|
113
109
|
});
|
|
110
|
+
if (errors.length > 0) {
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Link validation errors in ${filePath}:
|
|
113
|
+
` + errors.map((e) => ` - ${e}`).join("\n")
|
|
114
|
+
);
|
|
115
|
+
}
|
|
114
116
|
};
|
|
115
117
|
}
|
|
116
118
|
var rehype_validate_links_default = rehypeValidateLinks;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
compileLatexToSvg,
|
|
3
|
+
hashLatexCode
|
|
4
|
+
} from "./chunk-47X5MKFJ.js";
|
|
5
|
+
|
|
6
|
+
// src/plugins/remark-latex-compile/index.ts
|
|
7
|
+
import { rm } from "fs/promises";
|
|
8
|
+
import { join, resolve } from "path";
|
|
9
|
+
import { visit as visit2, SKIP } from "unist-util-visit";
|
|
10
|
+
|
|
11
|
+
// src/plugins/remark-latex-compile/rehype-converter.ts
|
|
12
|
+
import { visit } from "unist-util-visit";
|
|
13
|
+
function rehypeLatexCompile() {
|
|
14
|
+
return (tree, _file) => {
|
|
15
|
+
visit(tree, "element", (node, index, parent) => {
|
|
16
|
+
if (node.tagName !== "pre") return;
|
|
17
|
+
const codeChild = node.children?.[0];
|
|
18
|
+
if (!codeChild || codeChild.tagName !== "code") return;
|
|
19
|
+
const classes = Array.isArray(codeChild.properties?.className) ? codeChild.properties.className : [];
|
|
20
|
+
if (!classes.includes("language-tex") && !classes.includes("language-latex")) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const codeContent = codeChild.children?.map(
|
|
24
|
+
(child) => typeof child === "string" ? child : child.value || ""
|
|
25
|
+
).join("").trim();
|
|
26
|
+
if (!codeContent) return;
|
|
27
|
+
const dataAttribute = codeChild.properties?.["data-meta"];
|
|
28
|
+
const isCompileBlock = dataAttribute && dataAttribute.includes("compile") || codeContent.includes("compile");
|
|
29
|
+
if (!isCompileBlock) return;
|
|
30
|
+
try {
|
|
31
|
+
const hash = hashLatexCode(codeContent);
|
|
32
|
+
const svgPath = `/static/tex-svgs/${hash}.svg`;
|
|
33
|
+
const imgElement = {
|
|
34
|
+
type: "element",
|
|
35
|
+
tagName: "img",
|
|
36
|
+
properties: {
|
|
37
|
+
src: svgPath,
|
|
38
|
+
alt: "LaTeX diagram",
|
|
39
|
+
className: ["tex-compiled"]
|
|
40
|
+
},
|
|
41
|
+
children: []
|
|
42
|
+
};
|
|
43
|
+
const paragraphElement = {
|
|
44
|
+
type: "element",
|
|
45
|
+
tagName: "p",
|
|
46
|
+
properties: {},
|
|
47
|
+
children: [imgElement]
|
|
48
|
+
};
|
|
49
|
+
if (parent && typeof index === "number") {
|
|
50
|
+
parent.children[index] = paragraphElement;
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(
|
|
54
|
+
`[rehype-latex-compile] Error processing code block:`,
|
|
55
|
+
err
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/plugins/remark-latex-compile/index.ts
|
|
63
|
+
function extractClassesFromMeta(meta) {
|
|
64
|
+
const classMatch = meta.match(/class="([^"]+)"/);
|
|
65
|
+
if (classMatch?.[1]) {
|
|
66
|
+
return classMatch[1].split(/\s+/).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
function remarkLatexCompile(options) {
|
|
71
|
+
const svgOutputDir = resolve(options.svgOutputDir);
|
|
72
|
+
return async function transformer(tree, file) {
|
|
73
|
+
const nodes = [];
|
|
74
|
+
visit2(tree, "code", (node, index, parent) => {
|
|
75
|
+
if ((node.lang === "tex" || node.lang === "latex") && node.meta?.includes("compile") && parent && index !== void 0) {
|
|
76
|
+
nodes.push({ node, index, parent });
|
|
77
|
+
}
|
|
78
|
+
return SKIP;
|
|
79
|
+
});
|
|
80
|
+
if (nodes.length === 0) return;
|
|
81
|
+
const filePath = file.path || "unknown";
|
|
82
|
+
const results = await Promise.all(
|
|
83
|
+
nodes.map(async ({ node, index, parent }) => {
|
|
84
|
+
const lineNumber = node.position?.start.line ?? "?";
|
|
85
|
+
try {
|
|
86
|
+
const result = await compileLatexToSvg(node.value, svgOutputDir);
|
|
87
|
+
if (result.wasCompiled) {
|
|
88
|
+
console.log(
|
|
89
|
+
`[remark-latex-compile] ${filePath}:${lineNumber}: compiled ${result.hash}.svg`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
options._referencedHashes?.add(result.hash);
|
|
93
|
+
return { index, parent, result, error: null, hash: result.hash };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
const match = errorMsg.match(/\n\n([\s\S]+)/);
|
|
97
|
+
const details = match ? match[1] : errorMsg;
|
|
98
|
+
console.error(
|
|
99
|
+
`[remark-latex-compile] ${filePath}:${lineNumber}
|
|
100
|
+
${details}`
|
|
101
|
+
);
|
|
102
|
+
return { index, parent, result: null, error: err, hash: null };
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
if (options._fileHashMap) {
|
|
107
|
+
const newHashes = new Set(
|
|
108
|
+
results.map((r) => r.hash).filter(Boolean)
|
|
109
|
+
);
|
|
110
|
+
const oldHashes = options._fileHashMap.get(filePath) ?? /* @__PURE__ */ new Set();
|
|
111
|
+
const staleHashes = [...oldHashes].filter((h) => !newHashes.has(h));
|
|
112
|
+
for (const staleHash of staleHashes) {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[remark-latex-compile] Removing orphaned svg: ${staleHash}.svg`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
await Promise.all(
|
|
118
|
+
staleHashes.map(
|
|
119
|
+
(h) => rm(join(svgOutputDir, `${h}.svg`), { force: true })
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
options._fileHashMap.set(filePath, newHashes);
|
|
123
|
+
}
|
|
124
|
+
for (let i = results.length - 1; i >= 0; i--) {
|
|
125
|
+
const { index, parent, result } = results[i];
|
|
126
|
+
const { node } = nodes[i];
|
|
127
|
+
if (!result) continue;
|
|
128
|
+
const customClasses = extractClassesFromMeta(node.meta ?? "");
|
|
129
|
+
const imageNode = {
|
|
130
|
+
type: "image",
|
|
131
|
+
url: `/static/tex-svgs/${result.hash}.svg`,
|
|
132
|
+
alt: "LaTeX diagram",
|
|
133
|
+
data: {
|
|
134
|
+
hProperties: { className: ["tex-compiled", ...customClasses] }
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const paragraph = {
|
|
138
|
+
type: "paragraph",
|
|
139
|
+
children: [imageNode]
|
|
140
|
+
};
|
|
141
|
+
parent.children.splice(index, 1, paragraph);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
rehypeLatexCompile,
|
|
148
|
+
remarkLatexCompile
|
|
149
|
+
};
|