@dooc-dev/dooc 0.0.0-stage → 0.0.1
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 +26 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +244 -0
- package/dist/code.global.js +285 -0
- package/dist/compiler/bundle-urls.d.ts +3 -0
- package/dist/compiler/bundle-urls.js +294 -0
- package/dist/compiler/bundle.d.ts +2 -0
- package/dist/compiler/bundle.js +100 -0
- package/dist/compiler/check-prose.d.ts +11 -0
- package/dist/compiler/check-prose.js +138 -0
- package/dist/compiler/check-types.d.ts +44 -0
- package/dist/compiler/check-types.js +1 -0
- package/dist/compiler/check.d.ts +5 -0
- package/dist/compiler/check.js +450 -0
- package/dist/compiler/code-blocks.d.ts +17 -0
- package/dist/compiler/code-blocks.js +198 -0
- package/dist/compiler/code-languages.d.ts +10 -0
- package/dist/compiler/code-languages.js +109 -0
- package/dist/compiler/compile.d.ts +95 -0
- package/dist/compiler/compile.js +410 -0
- package/dist/compiler/config.d.ts +7 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/dialect.d.ts +24 -0
- package/dist/compiler/dialect.js +198 -0
- package/dist/compiler/drawio.d.ts +26 -0
- package/dist/compiler/drawio.js +176 -0
- package/dist/compiler/figures.d.ts +14 -0
- package/dist/compiler/figures.js +221 -0
- package/dist/compiler/file-cache.d.ts +14 -0
- package/dist/compiler/file-cache.js +61 -0
- package/dist/compiler/files.d.ts +29 -0
- package/dist/compiler/files.js +431 -0
- package/dist/compiler/fonts.d.ts +10 -0
- package/dist/compiler/fonts.js +54 -0
- package/dist/compiler/format.d.ts +1 -0
- package/dist/compiler/format.js +42 -0
- package/dist/compiler/from-mdast.d.ts +16 -0
- package/dist/compiler/from-mdast.js +142 -0
- package/dist/compiler/icons.d.ts +14 -0
- package/dist/compiler/icons.js +85 -0
- package/dist/compiler/language-codes.d.ts +7 -0
- package/dist/compiler/language-codes.js +17 -0
- package/dist/compiler/locales.d.ts +7 -0
- package/dist/compiler/locales.js +67 -0
- package/dist/compiler/manifest.d.ts +6 -0
- package/dist/compiler/manifest.js +30 -0
- package/dist/compiler/ops.d.ts +44 -0
- package/dist/compiler/ops.js +362 -0
- package/dist/compiler/public-file.d.ts +2 -0
- package/dist/compiler/public-file.js +93 -0
- package/dist/compiler/serialize.d.ts +3 -0
- package/dist/compiler/serialize.js +16 -0
- package/dist/compiler/terms.d.ts +9 -0
- package/dist/compiler/terms.js +44 -0
- package/dist/compiler/textfile-remote.d.ts +12 -0
- package/dist/compiler/textfile-remote.js +124 -0
- package/dist/compiler/textfile.d.ts +9 -0
- package/dist/compiler/textfile.js +113 -0
- package/dist/compiler/theme-variants.d.ts +4 -0
- package/dist/compiler/theme-variants.js +22 -0
- package/dist/compiler/zip.d.ts +6 -0
- package/dist/compiler/zip.js +48 -0
- package/dist/core/blocks.d.ts +99 -0
- package/dist/core/blocks.js +29 -0
- package/dist/core/code-file.d.ts +12 -0
- package/dist/core/code-file.js +68 -0
- package/dist/core/content-metadata.d.ts +48 -0
- package/dist/core/content-metadata.js +264 -0
- package/dist/core/content-types.d.ts +45 -0
- package/dist/core/content-types.js +1 -0
- package/dist/core/figure-frame.d.ts +12 -0
- package/dist/core/figure-frame.js +108 -0
- package/dist/core/file-embed.d.ts +13 -0
- package/dist/core/file-embed.js +43 -0
- package/dist/core/files.d.ts +15 -0
- package/dist/core/files.js +59 -0
- package/dist/core/flow-base-css.generated.d.ts +1 -0
- package/dist/core/flow-base-css.generated.js +4 -0
- package/dist/core/flow-css.d.ts +9 -0
- package/dist/core/flow-css.js +54 -0
- package/dist/core/graph-common.d.ts +32 -0
- package/dist/core/graph-common.js +113 -0
- package/dist/core/graph.d.ts +117 -0
- package/dist/core/graph.js +480 -0
- package/dist/core/html.d.ts +29 -0
- package/dist/core/html.js +444 -0
- package/dist/core/icons.d.ts +1 -0
- package/dist/core/icons.js +1 -0
- package/dist/core/inline.d.ts +27 -0
- package/dist/core/inline.js +42 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +44 -0
- package/dist/core/languages/ja.d.ts +2 -0
- package/dist/core/languages/ja.js +5 -0
- package/dist/core/markdown.d.ts +27 -0
- package/dist/core/markdown.js +154 -0
- package/dist/core/previewers.d.ts +37 -0
- package/dist/core/previewers.js +103 -0
- package/dist/core/routing.d.ts +20 -0
- package/dist/core/routing.js +64 -0
- package/dist/core/schema-comments.d.ts +14 -0
- package/dist/core/schema-comments.js +52 -0
- package/dist/core/schema-graph-flow.d.ts +9 -0
- package/dist/core/schema-graph-flow.js +379 -0
- package/dist/core/schema-graph-mount.d.ts +9 -0
- package/dist/core/schema-graph-mount.js +105 -0
- package/dist/core/schema-graph.d.ts +254 -0
- package/dist/core/schema-graph.js +981 -0
- package/dist/core/table-ui.d.ts +2 -0
- package/dist/core/table-ui.js +180 -0
- package/dist/core/taxonomy.d.ts +12 -0
- package/dist/core/taxonomy.js +49 -0
- package/dist/core/term-reference.d.ts +5 -0
- package/dist/core/term-reference.js +19 -0
- package/dist/core/terms-ui.d.ts +6 -0
- package/dist/core/terms-ui.js +22 -0
- package/dist/core/textfile.d.ts +10 -0
- package/dist/core/textfile.js +39 -0
- package/dist/core/theme-variants.d.ts +3 -0
- package/dist/core/theme-variants.js +37 -0
- package/dist/core/toc-ui.d.ts +2 -0
- package/dist/core/toc-ui.js +161 -0
- package/dist/core/video-ui.d.ts +2 -0
- package/dist/core/video-ui.js +253 -0
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +28 -0
- package/dist/edit.global.js +305 -0
- package/dist/flow.global.js +4959 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +467 -0
- package/dist/plugins/actions.d.ts +45 -0
- package/dist/plugins/actions.js +125 -0
- package/dist/plugins/blocks.d.ts +92 -0
- package/dist/plugins/blocks.js +99 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +164 -0
- package/dist/plugins/runtime.d.ts +24 -0
- package/dist/plugins/runtime.js +339 -0
- package/dist/plugins/types.d.ts +56 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +29 -0
- package/dist/plugins/view.js +60 -0
- package/dist/ui.global.js +77 -0
- package/dist/viewer/assets.d.ts +2 -0
- package/dist/viewer/assets.js +3 -0
- package/dist/viewer/build.d.ts +12 -0
- package/dist/viewer/build.js +55 -0
- package/dist/viewer/bundle-network.d.ts +2 -0
- package/dist/viewer/bundle-network.js +88 -0
- package/dist/viewer/code-file.d.ts +20 -0
- package/dist/viewer/code-file.js +42 -0
- package/dist/viewer/editor-tools.d.ts +38 -0
- package/dist/viewer/editor-tools.js +211 -0
- package/dist/viewer/file-references.d.ts +14 -0
- package/dist/viewer/file-references.js +38 -0
- package/dist/viewer/font-picker.d.ts +4 -0
- package/dist/viewer/font-picker.js +145 -0
- package/dist/viewer/font-settings.d.ts +11 -0
- package/dist/viewer/font-settings.js +50 -0
- package/dist/viewer/fonts/LICENSE.txt +119 -0
- package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
- package/dist/viewer/fonts.d.ts +2 -0
- package/dist/viewer/fonts.js +13 -0
- package/dist/viewer/git-diff.d.ts +3 -0
- package/dist/viewer/git-diff.js +33 -0
- package/dist/viewer/git-types.d.ts +73 -0
- package/dist/viewer/git-types.js +1 -0
- package/dist/viewer/git.d.ts +5 -0
- package/dist/viewer/git.js +337 -0
- package/dist/viewer/hmr.d.ts +53 -0
- package/dist/viewer/hmr.js +335 -0
- package/dist/viewer/i18n.d.ts +6 -0
- package/dist/viewer/i18n.js +65 -0
- package/dist/viewer/ja-lint.d.ts +3 -0
- package/dist/viewer/ja-lint.js +44 -0
- package/dist/viewer/language-picker.d.ts +2 -0
- package/dist/viewer/language-picker.js +233 -0
- package/dist/viewer/languages.d.ts +11 -0
- package/dist/viewer/languages.js +20 -0
- package/dist/viewer/locales/eng.json +294 -0
- package/dist/viewer/locales/jpn.json +294 -0
- package/dist/viewer/locales/zho.json +294 -0
- package/dist/viewer/nav-focus.d.ts +2 -0
- package/dist/viewer/nav-focus.js +64 -0
- package/dist/viewer/plugin-blocks.d.ts +3 -0
- package/dist/viewer/plugin-blocks.js +115 -0
- package/dist/viewer/plugin-config.d.ts +35 -0
- package/dist/viewer/plugin-config.js +57 -0
- package/dist/viewer/plugin-view.d.ts +3 -0
- package/dist/viewer/plugin-view.js +28 -0
- package/dist/viewer/plugins.d.ts +8 -0
- package/dist/viewer/plugins.js +214 -0
- package/dist/viewer/resize.d.ts +4 -0
- package/dist/viewer/resize.js +238 -0
- package/dist/viewer/resource-library.d.ts +29 -0
- package/dist/viewer/resource-library.js +104 -0
- package/dist/viewer/search.d.ts +16 -0
- package/dist/viewer/search.js +358 -0
- package/dist/viewer/serve.d.ts +206 -0
- package/dist/viewer/serve.js +3233 -0
- package/dist/viewer/site-icon.d.ts +8 -0
- package/dist/viewer/site-icon.js +56 -0
- package/dist/viewer/textfile.d.ts +9 -0
- package/dist/viewer/textfile.js +50 -0
- package/dist/viewer/theme-picker.d.ts +2 -0
- package/dist/viewer/theme-picker.js +135 -0
- package/dist/viewer/workspace.d.ts +4 -0
- package/dist/viewer/workspace.js +47 -0
- package/package.json +127 -5
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Write operations on the docs root (create / move / rename / delete docs and save body text;
|
|
2
|
+
// add / remove figures). Shared by the viewer's edit API, the CLI (doc / figure) and MCP.
|
|
3
|
+
// Failures are returned as { error, status } (status is HTTP-like) with no partial updates.
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { flattenTree, insertDoc, isDocId, moveDoc, removeDoc, renameDoc, } from "../core/content-metadata.js";
|
|
7
|
+
import { defaultFilePath, docIconFile, isDocumentFileName, parseFileLayout, } from "../core/files.js";
|
|
8
|
+
import { parseDocLink } from "../core/routing.js";
|
|
9
|
+
import { collectDocLinks, compileSite } from "./compile.js";
|
|
10
|
+
import { figureName, figureUploadName } from "./figures.js";
|
|
11
|
+
import { projectFiles, rewriteFileLinks } from "./files.js";
|
|
12
|
+
import { translationFile, translationFiles, translationLanguages } from "./locales.js";
|
|
13
|
+
import { docFile, readManifest, writeManifest } from "./manifest.js";
|
|
14
|
+
import { assembleDoc } from "./serialize.js";
|
|
15
|
+
import { textfilePath } from "./textfile.js";
|
|
16
|
+
export const isOpError = (r) => typeof r === "object" && r != null && "error" in r;
|
|
17
|
+
export { assembleDoc } from "./serialize.js";
|
|
18
|
+
export const createDocOps = (root) => {
|
|
19
|
+
const figuresDir = join(root, "figures");
|
|
20
|
+
const docPath = (id) => textfilePath(root, docFile(id, readManifest(root).files));
|
|
21
|
+
const iconPath = (id) => textfilePath(root, docIconFile(id, readManifest(root).files));
|
|
22
|
+
// Read before rewriting dooc.yaml. If it's broken, don't write (= don't clobber
|
|
23
|
+
// hand-written content).
|
|
24
|
+
const loadTree = () => {
|
|
25
|
+
const manifest = readManifest(root);
|
|
26
|
+
return manifest.errors.length > 0
|
|
27
|
+
? { error: manifest.errors.join(" / "), status: 409 }
|
|
28
|
+
: manifest;
|
|
29
|
+
};
|
|
30
|
+
const existingDoc = (id) => typeof id === "string" && isDocId(id) && existsSync(docPath(id))
|
|
31
|
+
? { id }
|
|
32
|
+
: { error: `doc "${String(id)}" not found`, status: 404 };
|
|
33
|
+
// Include extracted callout sources and unregistered Markdown files in reference checks.
|
|
34
|
+
const referenceFiles = () => [...projectFiles(root).map((file) => file.path), ...translationFiles(root)].filter((file) => /\.(md|callouth)$/.test(file));
|
|
35
|
+
const referencingFiles = (id, removed) => {
|
|
36
|
+
const target = docFile(id, readManifest(root).files);
|
|
37
|
+
return referenceFiles()
|
|
38
|
+
.filter((file) => !removed.includes(file) &&
|
|
39
|
+
collectDocLinks(readFileSync(textfilePath(root, file), "utf8")).some((url) => parseDocLink(url)?.file === target))
|
|
40
|
+
.map((file) => file.replace(/^content\//, ""));
|
|
41
|
+
};
|
|
42
|
+
/** Creates a document and appends it as the last child of parent. */
|
|
43
|
+
const add = (id, title, parent, filename) => {
|
|
44
|
+
if (!title) {
|
|
45
|
+
return { error: "title is required", status: 400 };
|
|
46
|
+
}
|
|
47
|
+
if (!isDocId(id)) {
|
|
48
|
+
return { error: "id must be lowercase alphanumerics and hyphens", status: 400 };
|
|
49
|
+
}
|
|
50
|
+
if (filename !== undefined && !isDocumentFileName(filename))
|
|
51
|
+
return { error: "Enter a file name ending in .md", status: 400 };
|
|
52
|
+
if (existsSync(docPath(id))) {
|
|
53
|
+
return { error: `id "${id}" is already in use`, status: 409 };
|
|
54
|
+
}
|
|
55
|
+
const loaded = loadTree();
|
|
56
|
+
if (isOpError(loaded)) {
|
|
57
|
+
return loaded;
|
|
58
|
+
}
|
|
59
|
+
const tree = insertDoc(loaded.tree, id, parent);
|
|
60
|
+
if (!tree) {
|
|
61
|
+
return { error: `duplicate id "${id}" or missing parent "${parent}"`, status: 409 };
|
|
62
|
+
}
|
|
63
|
+
if (filename !== undefined || loaded.files?.defaultLayout !== false) {
|
|
64
|
+
loaded.files ??= parseFileLayout({});
|
|
65
|
+
loaded.files.documents[id] = loaded.files.defaultLayout
|
|
66
|
+
? defaultFilePath(filename ?? `${id}.md`)
|
|
67
|
+
: `content/${filename}`;
|
|
68
|
+
}
|
|
69
|
+
const file = textfilePath(root, docFile(id, loaded.files));
|
|
70
|
+
if (existsSync(file))
|
|
71
|
+
return { error: "File already exists", status: 409 };
|
|
72
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
73
|
+
writeFileSync(file, assembleDoc({ title }, ""), { flag: "wx" });
|
|
74
|
+
writeManifest(root, tree, loaded);
|
|
75
|
+
return { id };
|
|
76
|
+
};
|
|
77
|
+
const addFile = (name, title, parent) => {
|
|
78
|
+
if (!isDocumentFileName(name))
|
|
79
|
+
return { error: "Enter a file name ending in .md", status: 400 };
|
|
80
|
+
const loaded = loadTree();
|
|
81
|
+
if (isOpError(loaded))
|
|
82
|
+
return loaded;
|
|
83
|
+
const ids = new Set(flattenTree(loaded.tree).map((doc) => doc.id));
|
|
84
|
+
const stem = name
|
|
85
|
+
.slice(0, -3)
|
|
86
|
+
.normalize("NFKD")
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
89
|
+
.replace(/^-+|-+$/g, "") || "doc";
|
|
90
|
+
let id = stem;
|
|
91
|
+
for (let index = 2; ids.has(id) || existsSync(docPath(id)); index++)
|
|
92
|
+
id = `${stem}-${index}`;
|
|
93
|
+
return add(id, title, parent, name);
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Reorders and reparents. Rewrites only dooc.yaml. Omitted index = after the last child
|
|
97
|
+
* of parent.
|
|
98
|
+
*/
|
|
99
|
+
const move = (id, parent, index) => {
|
|
100
|
+
const loaded = loadTree();
|
|
101
|
+
if (isOpError(loaded)) {
|
|
102
|
+
return loaded;
|
|
103
|
+
}
|
|
104
|
+
const at = index ??
|
|
105
|
+
flattenTree(loaded.tree).filter((n) => n.parent === parent && n.id !== id).length;
|
|
106
|
+
const tree = moveDoc(loaded.tree, id, parent, at);
|
|
107
|
+
if (!tree) {
|
|
108
|
+
return {
|
|
109
|
+
error: "cannot move to that position (under its own descendant / unknown doc / index out of range)",
|
|
110
|
+
status: 409,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
writeManifest(root, tree, loaded);
|
|
114
|
+
return { id };
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Renames content/<old>.md and updates the manifest id and the file links (exact match) in
|
|
118
|
+
* every doc's body text to follow.
|
|
119
|
+
*/
|
|
120
|
+
const rename = (oldId, newId) => {
|
|
121
|
+
const target = existingDoc(oldId);
|
|
122
|
+
if (isOpError(target)) {
|
|
123
|
+
return target;
|
|
124
|
+
}
|
|
125
|
+
if (newId === "" || newId === oldId) {
|
|
126
|
+
return { id: oldId, updatedRefs: 0 };
|
|
127
|
+
}
|
|
128
|
+
if (!isDocId(newId)) {
|
|
129
|
+
return { error: "id must be a slug (lowercase alphanumerics, hyphens)", status: 400 };
|
|
130
|
+
}
|
|
131
|
+
if (existsSync(docPath(newId))) {
|
|
132
|
+
return { error: `id "${newId}" is already in use`, status: 409 };
|
|
133
|
+
}
|
|
134
|
+
const loaded = loadTree();
|
|
135
|
+
if (isOpError(loaded)) {
|
|
136
|
+
return loaded;
|
|
137
|
+
}
|
|
138
|
+
const tree = renameDoc(loaded.tree, oldId, newId);
|
|
139
|
+
if (!tree) {
|
|
140
|
+
return { error: "doc is not in dooc.yaml", status: 404 };
|
|
141
|
+
}
|
|
142
|
+
const oldFile = docFile(oldId, loaded.files);
|
|
143
|
+
const mapped = loaded.files && Object.hasOwn(loaded.files.documents, oldId);
|
|
144
|
+
const newFile = mapped ? oldFile : docFile(newId, loaded.files);
|
|
145
|
+
const moves = new Map(oldFile === newFile ? [] : [[oldFile, newFile]]);
|
|
146
|
+
try {
|
|
147
|
+
for (const language of translationLanguages(root)) {
|
|
148
|
+
const from = translationFile(oldFile, language);
|
|
149
|
+
const to = translationFile(newFile, language);
|
|
150
|
+
if (from === to)
|
|
151
|
+
continue;
|
|
152
|
+
if (existsSync(textfilePath(root, to)))
|
|
153
|
+
return { error: `translation file "${to}" already exists`, status: 409 };
|
|
154
|
+
if (existsSync(textfilePath(root, from)))
|
|
155
|
+
moves.set(from, to);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return { error: String(error), status: 409 };
|
|
160
|
+
}
|
|
161
|
+
const writes = [];
|
|
162
|
+
let updatedRefs = 0;
|
|
163
|
+
// Validate every rewrite before renaming files or changing the manifest.
|
|
164
|
+
try {
|
|
165
|
+
for (const file of referenceFiles()) {
|
|
166
|
+
const raw = readFileSync(textfilePath(root, file), "utf8");
|
|
167
|
+
const body = rewriteFileLinks(raw, moves);
|
|
168
|
+
if (body !== raw) {
|
|
169
|
+
writes.push({ file: moves.get(file) ?? file, body });
|
|
170
|
+
updatedRefs++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
return { error: error instanceof Error ? error.message : String(error), status: 409 };
|
|
176
|
+
}
|
|
177
|
+
if (loaded.files && Object.hasOwn(loaded.files.documents, oldId)) {
|
|
178
|
+
loaded.files.documents[newId] = docFile(oldId, loaded.files);
|
|
179
|
+
delete loaded.files.documents[oldId];
|
|
180
|
+
}
|
|
181
|
+
else
|
|
182
|
+
renameSync(docPath(oldId), docPath(newId));
|
|
183
|
+
if (loaded.files?.icons && Object.hasOwn(loaded.files.icons, oldId)) {
|
|
184
|
+
loaded.files.icons[newId] = docIconFile(oldId, loaded.files);
|
|
185
|
+
delete loaded.files.icons[oldId];
|
|
186
|
+
}
|
|
187
|
+
else if (existsSync(iconPath(oldId)))
|
|
188
|
+
renameSync(iconPath(oldId), iconPath(newId));
|
|
189
|
+
for (const [from, to] of moves) {
|
|
190
|
+
if (from === oldFile)
|
|
191
|
+
continue;
|
|
192
|
+
const path = textfilePath(root, to);
|
|
193
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
194
|
+
renameSync(textfilePath(root, from), path);
|
|
195
|
+
}
|
|
196
|
+
writeManifest(root, tree, loaded);
|
|
197
|
+
for (const { file, body } of writes)
|
|
198
|
+
writeFileSync(textfilePath(root, file), body);
|
|
199
|
+
return { id: newId, updatedRefs };
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Removes the doc from the manifest and deletes the .md (and icon). Docs with children are
|
|
203
|
+
* not deleted. If other docs reference it via file links, refuses without force and returns
|
|
204
|
+
* referencedBy. parent = the deleted doc's parent.
|
|
205
|
+
*/
|
|
206
|
+
const remove = (id, force = false) => {
|
|
207
|
+
const target = existingDoc(id);
|
|
208
|
+
if (isOpError(target)) {
|
|
209
|
+
return target;
|
|
210
|
+
}
|
|
211
|
+
const loaded = loadTree();
|
|
212
|
+
if (isOpError(loaded)) {
|
|
213
|
+
return loaded;
|
|
214
|
+
}
|
|
215
|
+
const tree = removeDoc(loaded.tree, id);
|
|
216
|
+
if (!tree) {
|
|
217
|
+
return { error: "doc has children (move / delete them first)", status: 409 };
|
|
218
|
+
}
|
|
219
|
+
const file = docFile(id, loaded.files);
|
|
220
|
+
let removed;
|
|
221
|
+
let referencedBy;
|
|
222
|
+
try {
|
|
223
|
+
removed = [
|
|
224
|
+
file,
|
|
225
|
+
...translationLanguages(root).map((lang) => translationFile(file, lang)),
|
|
226
|
+
];
|
|
227
|
+
for (const file of removed)
|
|
228
|
+
textfilePath(root, file);
|
|
229
|
+
referencedBy = referencingFiles(id, removed);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
return { error: String(error), status: 409 };
|
|
233
|
+
}
|
|
234
|
+
if (referencedBy.length > 0 && !force) {
|
|
235
|
+
return {
|
|
236
|
+
error: `${referencedBy.length} doc(s) reference it via file links`,
|
|
237
|
+
status: 409,
|
|
238
|
+
referencedBy,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
const parent = flattenTree(loaded.tree).find((n) => n.id === id)?.parent;
|
|
242
|
+
const removedPath = docPath(id);
|
|
243
|
+
const removedIcon = iconPath(id);
|
|
244
|
+
if (loaded.files) {
|
|
245
|
+
delete loaded.files.documents[id];
|
|
246
|
+
if (loaded.files.icons)
|
|
247
|
+
delete loaded.files.icons[id];
|
|
248
|
+
loaded.files.order = loaded.files.order.filter((file) => join(root, file) !== removedPath);
|
|
249
|
+
}
|
|
250
|
+
writeManifest(root, tree, loaded);
|
|
251
|
+
unlinkSync(removedPath);
|
|
252
|
+
if (existsSync(removedIcon)) {
|
|
253
|
+
unlinkSync(removedIcon);
|
|
254
|
+
}
|
|
255
|
+
for (const file of removed.slice(1)) {
|
|
256
|
+
const path = textfilePath(root, file);
|
|
257
|
+
if (existsSync(path))
|
|
258
|
+
unlinkSync(path);
|
|
259
|
+
}
|
|
260
|
+
return { parent, danglingRefs: referencedBy.length };
|
|
261
|
+
};
|
|
262
|
+
/** Overwrites an existing doc's full raw text (including front-matter). */
|
|
263
|
+
const write = (id, raw) => {
|
|
264
|
+
const target = existingDoc(id);
|
|
265
|
+
if (isOpError(target)) {
|
|
266
|
+
return target;
|
|
267
|
+
}
|
|
268
|
+
writeFileSync(docPath(id), raw.endsWith("\n") ? raw : `${raw}\n`);
|
|
269
|
+
return { id };
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Writes a figure to figures/. file = the original file name (its extension decides the
|
|
273
|
+
* kind). A figure name is shared across extensions, so if the same stem already exists,
|
|
274
|
+
* -2, -3… is appended (if the same name with the same content exists, returns it without
|
|
275
|
+
* writing).
|
|
276
|
+
*/
|
|
277
|
+
const addFigure = (file, data) => {
|
|
278
|
+
const upload = figureUploadName(file);
|
|
279
|
+
if (!upload) {
|
|
280
|
+
return { error: "specify an svg / png / webp / jpeg / html file", status: 400 };
|
|
281
|
+
}
|
|
282
|
+
if (data.length === 0 ||
|
|
283
|
+
(upload.endsWith(".svg") && !data.toString("utf8").includes("<svg"))) {
|
|
284
|
+
return { error: `${upload} has invalid content`, status: 400 };
|
|
285
|
+
}
|
|
286
|
+
const manifest = loadTree();
|
|
287
|
+
if (isOpError(manifest))
|
|
288
|
+
return manifest;
|
|
289
|
+
if (manifest.files?.defaultLayout !== false) {
|
|
290
|
+
const initial = defaultFilePath(upload);
|
|
291
|
+
let path = initial;
|
|
292
|
+
const ext = upload.slice(figureName(upload).length);
|
|
293
|
+
for (let i = 2; existsSync(textfilePath(root, path)); i++) {
|
|
294
|
+
if (readFileSync(textfilePath(root, path)).equals(data))
|
|
295
|
+
return { file: path.split("/").pop(), path };
|
|
296
|
+
path = `${initial.slice(0, -ext.length)}-${i}${ext}`;
|
|
297
|
+
}
|
|
298
|
+
const target = textfilePath(root, path);
|
|
299
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
300
|
+
writeFileSync(target, data, { flag: "wx" });
|
|
301
|
+
return { file: path.split("/").pop(), path };
|
|
302
|
+
}
|
|
303
|
+
mkdirSync(figuresDir, { recursive: true });
|
|
304
|
+
const taken = new Map(readdirSync(figuresDir).map((f) => [figureName(f), f]));
|
|
305
|
+
const ext = upload.slice(figureName(upload).length);
|
|
306
|
+
let name = figureName(upload);
|
|
307
|
+
for (let i = 2; taken.has(name); i += 1) {
|
|
308
|
+
const existing = taken.get(name);
|
|
309
|
+
if (existing === `${name}${ext}` &&
|
|
310
|
+
readFileSync(join(figuresDir, existing)).equals(data)) {
|
|
311
|
+
return { file: existing };
|
|
312
|
+
}
|
|
313
|
+
name = `${figureName(upload)}-${i}`;
|
|
314
|
+
}
|
|
315
|
+
writeFileSync(join(figuresDir, `${name}${ext}`), data);
|
|
316
|
+
return { file: `${name}${ext}` };
|
|
317
|
+
};
|
|
318
|
+
/** Files in figures/ (figure name → file). */
|
|
319
|
+
const figureFiles = () => new Map(compileSite(root).figures.map((f) => [
|
|
320
|
+
f.name,
|
|
321
|
+
f.file.startsWith("figures/") ? f.file.slice(8) : f.file,
|
|
322
|
+
]));
|
|
323
|
+
/**
|
|
324
|
+
* Deletes a figure. If a doc references it via a Markdown link or embed, refuses without force and returns
|
|
325
|
+
* referencedBy.
|
|
326
|
+
*/
|
|
327
|
+
const removeFigure = (name, force = false) => {
|
|
328
|
+
const file = figureFiles().get(name);
|
|
329
|
+
if (!file) {
|
|
330
|
+
return { error: `figure "${name}" not found`, status: 404 };
|
|
331
|
+
}
|
|
332
|
+
const referencedBy = [
|
|
333
|
+
...new Set([undefined, ...translationLanguages(root)].flatMap((locale) => compileSite(root, { locale, defaultLang: "und" })
|
|
334
|
+
.docs.filter((d) => d.refs.figures.includes(name))
|
|
335
|
+
.map((d) => d.id))),
|
|
336
|
+
];
|
|
337
|
+
if (referencedBy.length > 0 && !force) {
|
|
338
|
+
return {
|
|
339
|
+
error: `${referencedBy.length} doc(s) reference it`,
|
|
340
|
+
status: 409,
|
|
341
|
+
referencedBy,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
unlinkSync(textfilePath(root, file.includes("/") ? file : `figures/${file}`));
|
|
345
|
+
return { file };
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
contentDir: join(root, "content"),
|
|
349
|
+
figuresDir,
|
|
350
|
+
loadTree,
|
|
351
|
+
referencingFiles,
|
|
352
|
+
figureFiles,
|
|
353
|
+
add,
|
|
354
|
+
addFile,
|
|
355
|
+
move,
|
|
356
|
+
rename,
|
|
357
|
+
remove,
|
|
358
|
+
write,
|
|
359
|
+
addFigure,
|
|
360
|
+
removeFigure,
|
|
361
|
+
};
|
|
362
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Self-contained: the synchronous compiler also runs this function in a bounded child process. */
|
|
2
|
+
export async function fetchPublicFile(source, contentType) {
|
|
3
|
+
const { request } = await import("node:https");
|
|
4
|
+
const { lookup } = await import("node:dns/promises");
|
|
5
|
+
const { BlockList, isIP } = await import("node:net");
|
|
6
|
+
const url = new URL(source);
|
|
7
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port)
|
|
8
|
+
throw new Error("BLOCKED");
|
|
9
|
+
const blocked = new BlockList();
|
|
10
|
+
for (const [address, prefix] of [
|
|
11
|
+
["0.0.0.0", 8],
|
|
12
|
+
["10.0.0.0", 8],
|
|
13
|
+
["100.64.0.0", 10],
|
|
14
|
+
["127.0.0.0", 8],
|
|
15
|
+
["169.254.0.0", 16],
|
|
16
|
+
["172.16.0.0", 12],
|
|
17
|
+
["192.0.0.0", 24],
|
|
18
|
+
["192.0.2.0", 24],
|
|
19
|
+
["192.88.99.0", 24],
|
|
20
|
+
["192.168.0.0", 16],
|
|
21
|
+
["198.18.0.0", 15],
|
|
22
|
+
["198.51.100.0", 24],
|
|
23
|
+
["203.0.113.0", 24],
|
|
24
|
+
["224.0.0.0", 3],
|
|
25
|
+
])
|
|
26
|
+
blocked.addSubnet(address, prefix, "ipv4");
|
|
27
|
+
const globalV6 = new BlockList();
|
|
28
|
+
globalV6.addSubnet("2000::", 3, "ipv6");
|
|
29
|
+
for (const [address, prefix] of [
|
|
30
|
+
["2001::", 23],
|
|
31
|
+
["2001:db8::", 32],
|
|
32
|
+
["2002::", 16],
|
|
33
|
+
["3fff::", 20],
|
|
34
|
+
])
|
|
35
|
+
blocked.addSubnet(address, prefix, "ipv6");
|
|
36
|
+
const publicAddress = (address) => isIP(address) === 4
|
|
37
|
+
? !blocked.check(address, "ipv4")
|
|
38
|
+
: isIP(address) === 6 &&
|
|
39
|
+
globalV6.check(address, "ipv6") &&
|
|
40
|
+
!blocked.check(address, "ipv6");
|
|
41
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
42
|
+
if (isIP(host) && !publicAddress(host))
|
|
43
|
+
throw new Error("BLOCKED");
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
// No redirects, proxy, cookies or inherited authorization; TLS still validates the original host.
|
|
46
|
+
const req = request(url, {
|
|
47
|
+
agent: false,
|
|
48
|
+
signal: AbortSignal.timeout(10000),
|
|
49
|
+
lookup(hostname, options, callback) {
|
|
50
|
+
void lookup(hostname, { all: true, verbatim: true }).then((addresses) => {
|
|
51
|
+
const first = addresses[0];
|
|
52
|
+
if (!first ||
|
|
53
|
+
!addresses.every(({ address }) => publicAddress(address))) {
|
|
54
|
+
callback(new Error("BLOCKED"), "", 0);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (options.all)
|
|
58
|
+
callback(null, addresses);
|
|
59
|
+
else
|
|
60
|
+
callback(null, first.address, first.family);
|
|
61
|
+
}, (error) => callback(error, "", 0));
|
|
62
|
+
},
|
|
63
|
+
}, (response) => {
|
|
64
|
+
if (response.statusCode === 404 || response.statusCode === 410) {
|
|
65
|
+
response.destroy();
|
|
66
|
+
reject(new Error("NOT_FOUND"));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (response.statusCode !== 200) {
|
|
70
|
+
response.destroy();
|
|
71
|
+
reject(new Error("UNAVAILABLE"));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
contentType?.(response.headers["content-type"] ?? "");
|
|
75
|
+
const chunks = [];
|
|
76
|
+
let size = 0;
|
|
77
|
+
response.on("data", (chunk) => {
|
|
78
|
+
size += chunk.length;
|
|
79
|
+
if (size > 2 * 1024 * 1024) {
|
|
80
|
+
response.destroy();
|
|
81
|
+
reject(new Error("UNAVAILABLE"));
|
|
82
|
+
}
|
|
83
|
+
else
|
|
84
|
+
chunks.push(chunk);
|
|
85
|
+
});
|
|
86
|
+
response.on("end", () => resolve(Buffer.concat(chunks)));
|
|
87
|
+
response.on("error", reject);
|
|
88
|
+
response.on("aborted", () => reject(new Error("UNAVAILABLE")));
|
|
89
|
+
});
|
|
90
|
+
req.on("error", reject);
|
|
91
|
+
req.end();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Serializes front-matter in hand-written style (double-quoted scalars / flow sequences). A
|
|
2
|
+
// generic YAML stringify isn't used because it changes quote/sequence style and pollutes git
|
|
3
|
+
// diffs against existing files. Key order is title / lang. The key set is
|
|
4
|
+
// taxonomy's closed set, and a JSON string is a valid YAML double-quoted scalar.
|
|
5
|
+
const serializeFrontmatter = (fm) => {
|
|
6
|
+
const lines = [`title: ${JSON.stringify(fm.title)}`];
|
|
7
|
+
if (fm.lang) {
|
|
8
|
+
lines.push(`lang: ${JSON.stringify(fm.lang)}`);
|
|
9
|
+
}
|
|
10
|
+
return `---\n${lines.join("\n")}\n---\n`;
|
|
11
|
+
};
|
|
12
|
+
/** Form frontmatter + body md → full file text (normalized to one trailing newline). */
|
|
13
|
+
export const assembleDoc = (fm, body) => {
|
|
14
|
+
const b = body.replace(/\n+$/, "");
|
|
15
|
+
return serializeFrontmatter(fm) + (b === "" ? "" : `\n${b}\n`);
|
|
16
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TermEntry } from "../core/content-types.js";
|
|
2
|
+
/** The internal identity is a file path, or a document position for an inline definition. */
|
|
3
|
+
export declare function parseTermSource(id: string, source: string): {
|
|
4
|
+
entries: TermEntry[];
|
|
5
|
+
errors: {
|
|
6
|
+
line: number;
|
|
7
|
+
message: string;
|
|
8
|
+
}[];
|
|
9
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseDocument } from "yaml";
|
|
2
|
+
/** The internal identity is a file path, or a document position for an inline definition. */
|
|
3
|
+
export function parseTermSource(id, source) {
|
|
4
|
+
const entries = [];
|
|
5
|
+
const errors = [];
|
|
6
|
+
const fail = (message) => {
|
|
7
|
+
errors.push({ line: 0, message });
|
|
8
|
+
};
|
|
9
|
+
let data;
|
|
10
|
+
try {
|
|
11
|
+
const doc = parseDocument(source);
|
|
12
|
+
if (doc.errors.length) {
|
|
13
|
+
for (const e of doc.errors)
|
|
14
|
+
errors.push({ line: (e.linePos?.[0]?.line ?? 1) - 1, message: e.message });
|
|
15
|
+
return { entries, errors };
|
|
16
|
+
}
|
|
17
|
+
data = doc.toJS({ maxAliasCount: 0 });
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
fail(`cannot read YAML: ${e instanceof Error ? e.message : String(e)}`);
|
|
21
|
+
return { entries, errors };
|
|
22
|
+
}
|
|
23
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
24
|
+
fail("expected a YAML object with name and definition");
|
|
25
|
+
return { entries, errors };
|
|
26
|
+
}
|
|
27
|
+
const row = data;
|
|
28
|
+
for (const key of Object.keys(row))
|
|
29
|
+
if (key !== "name" && key !== "definition" && key !== "description")
|
|
30
|
+
fail(`unknown field "${key}"`);
|
|
31
|
+
for (const key of ["name", "definition"])
|
|
32
|
+
if (typeof row[key] !== "string" || !row[key].trim())
|
|
33
|
+
fail(`${key} must be a non-empty string`);
|
|
34
|
+
if (row.description !== undefined && typeof row.description !== "string")
|
|
35
|
+
fail("description must be a string");
|
|
36
|
+
if (!errors.length)
|
|
37
|
+
entries.push({
|
|
38
|
+
id,
|
|
39
|
+
name: row.name,
|
|
40
|
+
definition: row.definition,
|
|
41
|
+
...(typeof row.description === "string" ? { description: row.description } : {}),
|
|
42
|
+
});
|
|
43
|
+
return { entries, errors };
|
|
44
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class TextfileUnavailable extends Error {
|
|
2
|
+
readonly reason: "missing" | "unreachable";
|
|
3
|
+
constructor(reason: "missing" | "unreachable");
|
|
4
|
+
}
|
|
5
|
+
export declare const remoteTextfileSource: (file: string) => {
|
|
6
|
+
url: string;
|
|
7
|
+
ref?: string;
|
|
8
|
+
path?: string;
|
|
9
|
+
};
|
|
10
|
+
/** The viewer fetches in the background; the compiler reuses the completed cache entry. */
|
|
11
|
+
export declare const readRemoteTextfileAsync: (file: string, domains: readonly string[], refresh?: boolean) => Promise<Buffer>;
|
|
12
|
+
export declare const readRemoteTextfile: (file: string, domains: readonly string[], refresh?: boolean) => Buffer;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { allowedPreviewUrl } from "../core/previewers.js";
|
|
3
|
+
import { fetchPublicFile } from "./public-file.js";
|
|
4
|
+
export class TextfileUnavailable extends Error {
|
|
5
|
+
reason;
|
|
6
|
+
constructor(reason) {
|
|
7
|
+
super(reason === "missing" ? "File not found" : "File could not be loaded");
|
|
8
|
+
this.reason = reason;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
// The compiler is synchronous; a bounded child fetch keeps its public API synchronous too.
|
|
13
|
+
const FETCH = `
|
|
14
|
+
const __name = (value) => value;
|
|
15
|
+
(${fetchPublicFile.toString()})(process.argv[1])
|
|
16
|
+
.then(data => process.stdout.write(data))
|
|
17
|
+
.catch(error => process.exit(error.message === "NOT_FOUND" ? 44 : error.message === "BLOCKED" ? 45 : 1));
|
|
18
|
+
`;
|
|
19
|
+
export const remoteTextfileSource = (file) => {
|
|
20
|
+
const url = new URL(file);
|
|
21
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password)
|
|
22
|
+
throw new Error("expected an HTTP(S) URL without embedded credentials");
|
|
23
|
+
if (url.pathname.endsWith(".git")) {
|
|
24
|
+
const match = /^#([^:]+):(.+)$/.exec(url.hash);
|
|
25
|
+
const ref = decodeURIComponent(match?.[1] ?? "");
|
|
26
|
+
const path = decodeURIComponent(match?.[2] ?? "");
|
|
27
|
+
if (!ref ||
|
|
28
|
+
ref.startsWith("-") ||
|
|
29
|
+
/[\s\0~^:?*[\\]/.test(ref) ||
|
|
30
|
+
ref.includes("..") ||
|
|
31
|
+
ref.includes("@{") ||
|
|
32
|
+
!path ||
|
|
33
|
+
path.startsWith("/") ||
|
|
34
|
+
/[\0\r\n\\]/.test(path) ||
|
|
35
|
+
path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
36
|
+
throw new Error("use https://host/repo.git#ref:path/to/file.ext");
|
|
37
|
+
url.hash = "";
|
|
38
|
+
return { url: url.href, ref, path };
|
|
39
|
+
}
|
|
40
|
+
// Accept the ordinary GitHub file URL as well as a raw content URL.
|
|
41
|
+
if (url.hostname === "github.com" && /^\/[^/]+\/[^/]+\/blob\//.test(url.pathname)) {
|
|
42
|
+
url.hostname = "raw.githubusercontent.com";
|
|
43
|
+
url.pathname = url.pathname.replace("/blob/", "/");
|
|
44
|
+
}
|
|
45
|
+
url.hash = "";
|
|
46
|
+
return { url: url.href };
|
|
47
|
+
};
|
|
48
|
+
// shortcut: cache remote reads for one minute; use explicit reload for immediate branch updates.
|
|
49
|
+
const cache = new Map();
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
/** The viewer fetches in the background; the compiler reuses the completed cache entry. */
|
|
52
|
+
export const readRemoteTextfileAsync = async (file, domains, refresh = false) => {
|
|
53
|
+
allowedPreviewUrl(file, domains);
|
|
54
|
+
const source = remoteTextfileSource(file);
|
|
55
|
+
if (source.ref)
|
|
56
|
+
throw new Error("Git repositories require a dedicated preview protocol; use a public HTTPS file URL");
|
|
57
|
+
allowedPreviewUrl(source.url, domains);
|
|
58
|
+
const cached = cache.get(file);
|
|
59
|
+
if (!refresh && cached && cached.expires > Date.now()) {
|
|
60
|
+
if (cached.result instanceof Error)
|
|
61
|
+
throw cached.result;
|
|
62
|
+
return cached.result;
|
|
63
|
+
}
|
|
64
|
+
const active = pending.get(file);
|
|
65
|
+
if (active)
|
|
66
|
+
return active;
|
|
67
|
+
const task = (async () => {
|
|
68
|
+
let result;
|
|
69
|
+
try {
|
|
70
|
+
result = await fetchPublicFile(source.url);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error.message === "BLOCKED")
|
|
74
|
+
throw new Error("External previews require a public network address");
|
|
75
|
+
result = new TextfileUnavailable(error.message === "NOT_FOUND" ? "missing" : "unreachable");
|
|
76
|
+
}
|
|
77
|
+
if (cache.size >= 64)
|
|
78
|
+
cache.clear();
|
|
79
|
+
cache.set(file, { result, expires: Date.now() + 60000 });
|
|
80
|
+
if (result instanceof Error)
|
|
81
|
+
throw result;
|
|
82
|
+
return result;
|
|
83
|
+
})();
|
|
84
|
+
pending.set(file, task);
|
|
85
|
+
try {
|
|
86
|
+
return await task;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
pending.delete(file);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
export const readRemoteTextfile = (file, domains, refresh = false) => {
|
|
93
|
+
allowedPreviewUrl(file, domains);
|
|
94
|
+
const source = remoteTextfileSource(file);
|
|
95
|
+
if (source.ref)
|
|
96
|
+
throw new Error("Git repositories require a dedicated preview protocol; use a public HTTPS file URL");
|
|
97
|
+
allowedPreviewUrl(source.url, domains);
|
|
98
|
+
const cached = cache.get(file);
|
|
99
|
+
if (!refresh && cached && cached.expires > Date.now()) {
|
|
100
|
+
if (cached.result instanceof Error)
|
|
101
|
+
throw cached.result;
|
|
102
|
+
return cached.result;
|
|
103
|
+
}
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = execFileSync(process.execPath, ["--input-type=module", "-e", FETCH, source.url], {
|
|
107
|
+
timeout: 12000,
|
|
108
|
+
maxBuffer: MAX_BYTES,
|
|
109
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
110
|
+
env: { PATH: process.env.PATH },
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.status === 45)
|
|
115
|
+
throw new Error("External previews require a public network address");
|
|
116
|
+
result = new TextfileUnavailable(error.status === 44 ? "missing" : "unreachable");
|
|
117
|
+
}
|
|
118
|
+
if (cache.size >= 64)
|
|
119
|
+
cache.clear();
|
|
120
|
+
cache.set(file, { result, expires: Date.now() + 60000 });
|
|
121
|
+
if (result instanceof Error)
|
|
122
|
+
throw result;
|
|
123
|
+
return result;
|
|
124
|
+
};
|