@dooc-dev/dooc 0.0.0-stage → 0.0.2
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 +311 -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/extension-package.d.ts +34 -0
- package/dist/core/extension-package.js +147 -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 +2 -0
- package/dist/core/icons.js +2 -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 +306 -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 +93 -0
- package/dist/plugins/blocks.js +100 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/local-storage.d.ts +18 -0
- package/dist/plugins/local-storage.js +115 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +170 -0
- package/dist/plugins/runtime.d.ts +26 -0
- package/dist/plugins/runtime.js +395 -0
- package/dist/plugins/types.d.ts +59 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +7 -0
- package/dist/plugins/view.js +21 -0
- package/dist/ui.global.js +78 -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/extension-pack.d.ts +2 -0
- package/dist/viewer/extension-pack.js +36 -0
- package/dist/viewer/extensions.d.ts +10 -0
- package/dist/viewer/extensions.js +302 -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 +337 -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 +317 -0
- package/dist/viewer/locales/jpn.json +317 -0
- package/dist/viewer/locales/zho.json +317 -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 +2 -0
- package/dist/viewer/plugin-view.js +6 -0
- package/dist/viewer/plugins.d.ts +12 -0
- package/dist/viewer/plugins.js +267 -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 +210 -0
- package/dist/viewer/serve.js +3312 -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 +128 -5
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { posix } from "node:path";
|
|
4
|
+
import { parseFragment, serialize } from "parse5";
|
|
5
|
+
import postcss from "postcss";
|
|
6
|
+
import valueParser from "postcss-value-parser";
|
|
7
|
+
import { embeddedImages, parseMdast } from "../core/markdown.js";
|
|
8
|
+
import { isRemoteTextfile } from "../core/textfile.js";
|
|
9
|
+
import { collectRefs } from "./dialect.js";
|
|
10
|
+
import { readManifest } from "./manifest.js";
|
|
11
|
+
import { fetchPublicFile } from "./public-file.js";
|
|
12
|
+
import { textfilePath } from "./textfile.js";
|
|
13
|
+
import { remoteTextfileSource } from "./textfile-remote.js";
|
|
14
|
+
const types = {
|
|
15
|
+
html: "text/html",
|
|
16
|
+
htm: "text/html",
|
|
17
|
+
css: "text/css",
|
|
18
|
+
svg: "image/svg+xml",
|
|
19
|
+
png: "image/png",
|
|
20
|
+
jpg: "image/jpeg",
|
|
21
|
+
jpeg: "image/jpeg",
|
|
22
|
+
gif: "image/gif",
|
|
23
|
+
webp: "image/webp",
|
|
24
|
+
mp4: "video/mp4",
|
|
25
|
+
webm: "video/webm",
|
|
26
|
+
woff: "font/woff",
|
|
27
|
+
woff2: "font/woff2",
|
|
28
|
+
ttf: "font/ttf",
|
|
29
|
+
otf: "font/otf",
|
|
30
|
+
json: "application/json",
|
|
31
|
+
};
|
|
32
|
+
const extension = (source) => posix.extname(new URL(source).pathname).slice(1).toLowerCase();
|
|
33
|
+
const decode = (data) => new TextDecoder("utf-8", { fatal: true }).decode(data);
|
|
34
|
+
/** Snapshot embedded URLs and their declarative assets without changing the source bundle. */
|
|
35
|
+
export const embedBundleUrls = async (root, entries) => {
|
|
36
|
+
const result = new Map(entries.map((entry) => [entry.name, { ...entry }]));
|
|
37
|
+
const figures = new Set(entries
|
|
38
|
+
.filter((entry) => /\.(md|callouth)$/.test(entry.name))
|
|
39
|
+
.flatMap((entry) => collectRefs(parseMdast(decode(entry.data))).figures));
|
|
40
|
+
const siteIcon = readManifest(root).icon;
|
|
41
|
+
if (siteIcon)
|
|
42
|
+
figures.add(siteIcon);
|
|
43
|
+
const cache = new Map();
|
|
44
|
+
const sources = new Map();
|
|
45
|
+
const pending = new Set();
|
|
46
|
+
let total = 0;
|
|
47
|
+
let downloads = 0;
|
|
48
|
+
const keyOf = (source, base) => {
|
|
49
|
+
const url = new URL(source, base);
|
|
50
|
+
if (url.protocol !== "https:" &&
|
|
51
|
+
!(url.protocol === "bundle:" && new URL(base).protocol === "bundle:"))
|
|
52
|
+
throw new Error(`Cannot bundle resource: ${url.href} (public HTTPS required)`);
|
|
53
|
+
if (url.username || url.password || url.port)
|
|
54
|
+
throw new Error(`Cannot bundle resource with credentials or a custom port: ${url.origin}`);
|
|
55
|
+
return url;
|
|
56
|
+
};
|
|
57
|
+
const resource = async (url, stylesheet = true) => {
|
|
58
|
+
url = new URL(url);
|
|
59
|
+
url.hash = "";
|
|
60
|
+
const key = url.href;
|
|
61
|
+
const cacheKey = `${stylesheet}:${key}`;
|
|
62
|
+
const cached = cache.get(cacheKey);
|
|
63
|
+
if (cached)
|
|
64
|
+
return cached;
|
|
65
|
+
if (pending.has(key))
|
|
66
|
+
throw new Error(`Circular embedded resource: ${key}`);
|
|
67
|
+
pending.add(key);
|
|
68
|
+
try {
|
|
69
|
+
let source = sources.get(key);
|
|
70
|
+
if (!source) {
|
|
71
|
+
if (url.protocol === "https:" && ++downloads > 256)
|
|
72
|
+
throw new Error("Bundle exceeds 256 remote assets");
|
|
73
|
+
let type = "";
|
|
74
|
+
const data = url.protocol === "bundle:"
|
|
75
|
+
? readFileSync(textfilePath(root, decodeURIComponent(url.pathname.slice(1))))
|
|
76
|
+
: await fetchPublicFile(url.href, (value) => {
|
|
77
|
+
type = value.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
78
|
+
});
|
|
79
|
+
if (url.protocol === "https:")
|
|
80
|
+
total += data.length;
|
|
81
|
+
if (total > 64 * 1024 * 1024)
|
|
82
|
+
throw new Error("Embedded assets exceed 64 MiB");
|
|
83
|
+
if (!type || type === "application/octet-stream")
|
|
84
|
+
type = types[extension(key)] ?? "text/plain";
|
|
85
|
+
source = { data, type };
|
|
86
|
+
sources.set(key, source);
|
|
87
|
+
}
|
|
88
|
+
const { data, type } = source;
|
|
89
|
+
let output = data;
|
|
90
|
+
if (type === "text/html" || type === "image/svg+xml")
|
|
91
|
+
output = Buffer.from(await markup(decode(data), key));
|
|
92
|
+
else if (type === "text/css" && stylesheet)
|
|
93
|
+
output = Buffer.from(await css(decode(data), key));
|
|
94
|
+
const asset = { data: output, type };
|
|
95
|
+
cache.set(cacheKey, asset);
|
|
96
|
+
return asset;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`${key}: ${error instanceof Error ? error.message : error}`);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
pending.delete(key);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const inline = async (source, base) => {
|
|
106
|
+
if (!source || source.startsWith("#") || source.startsWith("data:"))
|
|
107
|
+
return source;
|
|
108
|
+
const url = keyOf(source, base);
|
|
109
|
+
const asset = await resource(url);
|
|
110
|
+
return `data:${asset.type};base64,${asset.data.toString("base64")}${url.hash}`;
|
|
111
|
+
};
|
|
112
|
+
const css = async (source, base) => {
|
|
113
|
+
const tree = postcss.parse(source);
|
|
114
|
+
const values = [];
|
|
115
|
+
tree.walkDecls((node) => {
|
|
116
|
+
values.push({
|
|
117
|
+
value: node.value,
|
|
118
|
+
set: (value) => {
|
|
119
|
+
node.value = value;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
tree.walkAtRules((node) => {
|
|
124
|
+
values.push({
|
|
125
|
+
value: node.params,
|
|
126
|
+
set: (value) => {
|
|
127
|
+
node.params = value;
|
|
128
|
+
},
|
|
129
|
+
imports: node.name.toLowerCase() === "import",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
for (const item of values) {
|
|
133
|
+
const parsed = valueParser(item.value);
|
|
134
|
+
const urls = [];
|
|
135
|
+
parsed.walk((node) => {
|
|
136
|
+
if (node.type === "function" && node.value.toLowerCase() === "url") {
|
|
137
|
+
urls.push(node);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (node.type === "function" && /^(?:-webkit-)?image-set$/i.test(node.value))
|
|
141
|
+
urls.push(...node.nodes.filter((child) => child.type === "string"));
|
|
142
|
+
});
|
|
143
|
+
const first = parsed.nodes.find((node) => node.type !== "space" && node.type !== "comment");
|
|
144
|
+
if (item.imports && first?.type === "string")
|
|
145
|
+
urls.push(first);
|
|
146
|
+
for (const node of urls) {
|
|
147
|
+
const source = node.type === "function"
|
|
148
|
+
? valueParser.stringify(node.nodes).replace(/^(['"])(.*)\1$/, "$2")
|
|
149
|
+
: node.value;
|
|
150
|
+
if (source.includes("\\"))
|
|
151
|
+
throw new Error("Escaped CSS resource URLs are not supported in static bundles");
|
|
152
|
+
const next = await inline(source, base);
|
|
153
|
+
if (node.type === "function")
|
|
154
|
+
node.nodes = [
|
|
155
|
+
{
|
|
156
|
+
type: "string",
|
|
157
|
+
quote: '"',
|
|
158
|
+
value: next,
|
|
159
|
+
sourceIndex: 0,
|
|
160
|
+
sourceEndIndex: 0,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
else
|
|
164
|
+
node.value = next;
|
|
165
|
+
}
|
|
166
|
+
item.set(parsed.toString());
|
|
167
|
+
}
|
|
168
|
+
return tree.toString();
|
|
169
|
+
};
|
|
170
|
+
const srcset = async (source, base) => {
|
|
171
|
+
const candidates = [];
|
|
172
|
+
let rest = source;
|
|
173
|
+
while (rest.trim()) {
|
|
174
|
+
rest = rest.replace(/^[\s,]+/, "");
|
|
175
|
+
const token = /^\S+/.exec(rest)?.[0];
|
|
176
|
+
if (!token)
|
|
177
|
+
break;
|
|
178
|
+
rest = rest.slice(token.length);
|
|
179
|
+
const url = token.replace(/,+$/, "");
|
|
180
|
+
const descriptor = token.endsWith(",") ? "" : (/^[^,]*/.exec(rest)?.[0] ?? "");
|
|
181
|
+
if (!token.endsWith(","))
|
|
182
|
+
rest = rest.slice(descriptor.length).replace(/^,/, "");
|
|
183
|
+
candidates.push(`${await inline(url, base)}${descriptor}`);
|
|
184
|
+
}
|
|
185
|
+
return candidates.join(", ");
|
|
186
|
+
};
|
|
187
|
+
const markup = async (source, base) => {
|
|
188
|
+
const tree = parseFragment(source);
|
|
189
|
+
const walk = async (parent) => {
|
|
190
|
+
for (const node of [...parent.childNodes]) {
|
|
191
|
+
if (!("tagName" in node))
|
|
192
|
+
continue;
|
|
193
|
+
const attr = (name) => node.attrs.find((item) => item.name === name)?.value;
|
|
194
|
+
const href = attr("href");
|
|
195
|
+
if (node.tagName === "base" && href)
|
|
196
|
+
base = keyOf(href, base).href;
|
|
197
|
+
if (["script", "base"].includes(node.tagName) ||
|
|
198
|
+
(node.tagName === "meta" && attr("http-equiv"))) {
|
|
199
|
+
parent.childNodes.splice(parent.childNodes.indexOf(node), 1);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
node.attrs = node.attrs.filter((item) => !/^on/i.test(item.name) &&
|
|
203
|
+
![
|
|
204
|
+
"ping",
|
|
205
|
+
"integrity",
|
|
206
|
+
"crossorigin",
|
|
207
|
+
"nonce",
|
|
208
|
+
"formaction",
|
|
209
|
+
"action",
|
|
210
|
+
].includes(item.name));
|
|
211
|
+
if (node.tagName === "link" && !/^(stylesheet|icon)$/i.test(attr("rel") ?? "")) {
|
|
212
|
+
parent.childNodes.splice(parent.childNodes.indexOf(node), 1);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
for (const item of node.attrs) {
|
|
216
|
+
if (item.name === "style")
|
|
217
|
+
item.value = await css(item.value, base);
|
|
218
|
+
else if (item.name === "srcdoc")
|
|
219
|
+
item.value = await markup(item.value, base);
|
|
220
|
+
else if (item.name === "srcset" || item.name === "imagesrcset")
|
|
221
|
+
item.value = await srcset(item.value, base);
|
|
222
|
+
else if (["src", "poster", "background"].includes(item.name) ||
|
|
223
|
+
(item.name === "data" && node.tagName === "object") ||
|
|
224
|
+
(item.name === "href" && !["a", "area"].includes(node.tagName)))
|
|
225
|
+
item.value = await inline(item.value, base);
|
|
226
|
+
else if (item.name === "href" && !item.value.startsWith("#")) {
|
|
227
|
+
const url = new URL(item.value, base);
|
|
228
|
+
item.value = /^(https?:|mailto:)$/.test(url.protocol) ? url.href : "#";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (node.tagName === "style") {
|
|
232
|
+
for (const child of node.childNodes)
|
|
233
|
+
if ("value" in child)
|
|
234
|
+
child.value = await css(child.value, base);
|
|
235
|
+
}
|
|
236
|
+
if (node.tagName === "iframe")
|
|
237
|
+
node.attrs.push({ name: "sandbox", value: "" });
|
|
238
|
+
await walk("content" in node ? node.content : node);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
await walk(tree);
|
|
242
|
+
return serialize(tree);
|
|
243
|
+
};
|
|
244
|
+
for (const entry of entries) {
|
|
245
|
+
if (/\.(svg|html?)$/i.test(entry.name) &&
|
|
246
|
+
(figures.has(entry.name) ||
|
|
247
|
+
/^(?:figures|icons)\//.test(entry.name) ||
|
|
248
|
+
(entry.name.startsWith("themes/") &&
|
|
249
|
+
[...figures].some((file) => entry.name.endsWith(`/${file}`))))) {
|
|
250
|
+
const base = `bundle:///${entry.name.split("/").map(encodeURIComponent).join("/")}`;
|
|
251
|
+
const data = await resource(new URL(base));
|
|
252
|
+
result.set(entry.name, { name: entry.name, data: data.data });
|
|
253
|
+
}
|
|
254
|
+
if (!/\.(md|callouth)$/.test(entry.name))
|
|
255
|
+
continue;
|
|
256
|
+
let source = decode(entry.data);
|
|
257
|
+
const changes = [];
|
|
258
|
+
const images = embeddedImages(parseMdast(source));
|
|
259
|
+
for (const image of images) {
|
|
260
|
+
if (!isRemoteTextfile(image.url))
|
|
261
|
+
continue;
|
|
262
|
+
const remote = remoteTextfileSource(image.url);
|
|
263
|
+
if (remote.ref)
|
|
264
|
+
throw new Error("Git URL embeds require a public HTTPS file URL");
|
|
265
|
+
const url = keyOf(remote.url, image.url);
|
|
266
|
+
const asset = await resource(url, false);
|
|
267
|
+
const ext = Object.entries(types).find(([, type]) => type === asset.type)?.[0] ??
|
|
268
|
+
(extension(url.href) || "txt");
|
|
269
|
+
const name = `files/bundle/${createHash("sha256").update(url.href).digest("hex")}.${ext}`;
|
|
270
|
+
const data = asset.type === "text/html"
|
|
271
|
+
? Buffer.from(`<!--fig\ntitle: ${url.hostname}\n-->\n${decode(asset.data)}`)
|
|
272
|
+
: asset.data;
|
|
273
|
+
const existing = result.get(name);
|
|
274
|
+
if (existing && !existing.data.equals(data))
|
|
275
|
+
throw new Error(`Bundle resource path is already in use: ${name}`);
|
|
276
|
+
result.set(name, { name, data });
|
|
277
|
+
const hash = new URL(image.url).hash;
|
|
278
|
+
const range = /^#(?:L\d+(?:-L\d+)?|H)$/.test(hash) ? hash : "";
|
|
279
|
+
const start = image.position?.start.offset, end = image.position?.end.offset;
|
|
280
|
+
if (start === undefined || end === undefined)
|
|
281
|
+
throw new Error(`Cannot rewrite embed: ${image.url}`);
|
|
282
|
+
const alt = (image.alt ?? "").replace(/[\\[\]]/g, "\\$&");
|
|
283
|
+
changes.push({
|
|
284
|
+
start,
|
|
285
|
+
end,
|
|
286
|
+
value: `}"` : ""})`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
for (const change of changes.sort((a, b) => b.start - a.start))
|
|
290
|
+
source = source.slice(0, change.start) + change.value + source.slice(change.end);
|
|
291
|
+
result.set(entry.name, { name: entry.name, data: Buffer.from(source) });
|
|
292
|
+
}
|
|
293
|
+
return [...result.values()];
|
|
294
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// A bundle contains dooc.yaml and its managed source files, assets and local configuration.
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, posix } from "node:path";
|
|
4
|
+
import { MANIFEST_FILE } from "../core/content-metadata.js";
|
|
5
|
+
import { isRemoteTextfile } from "../core/textfile.js";
|
|
6
|
+
import { CODE_LANGUAGES_DIR } from "./code-languages.js";
|
|
7
|
+
import { CONFIG_FILE } from "./config.js";
|
|
8
|
+
import { collectRefs, parseDoc } from "./dialect.js";
|
|
9
|
+
import { projectFiles } from "./files.js";
|
|
10
|
+
import { PROJECT_FONTS_DIR } from "./fonts.js";
|
|
11
|
+
import { readManifest } from "./manifest.js";
|
|
12
|
+
import { readTextfile, textfilePath } from "./textfile.js";
|
|
13
|
+
const LAYOUT_DIRS = [
|
|
14
|
+
"content",
|
|
15
|
+
"figures",
|
|
16
|
+
"icons",
|
|
17
|
+
"locales",
|
|
18
|
+
"themes",
|
|
19
|
+
".dooc/editor-tools",
|
|
20
|
+
PROJECT_FONTS_DIR,
|
|
21
|
+
];
|
|
22
|
+
export const bundleEntries = (root) => {
|
|
23
|
+
const entries = [
|
|
24
|
+
...[CONFIG_FILE, MANIFEST_FILE]
|
|
25
|
+
.filter((name) => existsSync(join(root, name)))
|
|
26
|
+
.map((name) => ({ name, data: readFileSync(join(root, name)) })),
|
|
27
|
+
...(existsSync(join(root, CODE_LANGUAGES_DIR))
|
|
28
|
+
? readdirSync(join(root, CODE_LANGUAGES_DIR))
|
|
29
|
+
.filter((file) => !file.startsWith(".") &&
|
|
30
|
+
file.endsWith(".json") &&
|
|
31
|
+
statSync(join(root, CODE_LANGUAGES_DIR, file)).isFile())
|
|
32
|
+
.sort()
|
|
33
|
+
.map((file) => ({
|
|
34
|
+
name: `${CODE_LANGUAGES_DIR}/${file}`,
|
|
35
|
+
data: readFileSync(join(root, CODE_LANGUAGES_DIR, file)),
|
|
36
|
+
}))
|
|
37
|
+
: []),
|
|
38
|
+
...LAYOUT_DIRS.filter((dir) => existsSync(join(root, dir))).flatMap((dir) => readdirSync(join(root, dir), { recursive: true, encoding: "utf8" })
|
|
39
|
+
.filter((rel) => !rel
|
|
40
|
+
.replaceAll("\\", "/")
|
|
41
|
+
.split("/")
|
|
42
|
+
.some((seg) => seg.startsWith(".")))
|
|
43
|
+
.map((rel) => `${dir}/${rel.replaceAll("\\", "/")}`)
|
|
44
|
+
.filter((name) => statSync(join(root, name)).isFile())
|
|
45
|
+
.sort()
|
|
46
|
+
.map((name) => ({ name, data: readFileSync(textfilePath(root, name)) }))),
|
|
47
|
+
];
|
|
48
|
+
const known = new Set(entries.map((entry) => entry.name));
|
|
49
|
+
const layout = readManifest(root).files;
|
|
50
|
+
const mapped = new Set([...(layout?.order ?? []), ...Object.values(layout?.documents ?? {})]);
|
|
51
|
+
for (const file of projectFiles(root))
|
|
52
|
+
if ((file.path.startsWith("files/") || mapped.has(file.path)) && !known.has(file.path))
|
|
53
|
+
entries.push({ name: file.path, data: readFileSync(join(root, file.path)) });
|
|
54
|
+
const bodies = entries
|
|
55
|
+
.filter((entry) => /\.(md|callouth)$/.test(entry.name))
|
|
56
|
+
.map((entry) => entry.data.toString("utf8"));
|
|
57
|
+
const included = new Set(entries.map((entry) => entry.name));
|
|
58
|
+
const { icon } = readManifest(root);
|
|
59
|
+
if (icon && !included.has(posix.normalize(icon))) {
|
|
60
|
+
const name = posix.normalize(icon);
|
|
61
|
+
entries.push({ name, data: Buffer.from(readTextfile(root, icon), "utf8") });
|
|
62
|
+
included.add(name);
|
|
63
|
+
}
|
|
64
|
+
for (const body of bodies) {
|
|
65
|
+
const refs = collectRefs(parseDoc(body).root);
|
|
66
|
+
for (const file of refs.figures) {
|
|
67
|
+
if (included.has(file))
|
|
68
|
+
continue;
|
|
69
|
+
try {
|
|
70
|
+
entries.push({ name: file, data: readFileSync(textfilePath(root, file)) });
|
|
71
|
+
included.add(file);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error.code !== "ENOENT")
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const file of [...refs.textfiles, ...refs.terms]) {
|
|
79
|
+
if (isRemoteTextfile(file))
|
|
80
|
+
continue;
|
|
81
|
+
let text;
|
|
82
|
+
try {
|
|
83
|
+
text = readTextfile(root, file);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code === "ENOENT")
|
|
87
|
+
continue;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
const name = posix.normalize(file);
|
|
91
|
+
if (included.has(name))
|
|
92
|
+
continue;
|
|
93
|
+
entries.push({ name, data: Buffer.from(text, "utf8") });
|
|
94
|
+
included.add(name);
|
|
95
|
+
if (name.endsWith(".callouth"))
|
|
96
|
+
bodies.push(text);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return entries;
|
|
100
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import type { CheckDiagnostic } from "./check-types.js";
|
|
3
|
+
export interface ProseSegment {
|
|
4
|
+
text: string;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
}
|
|
8
|
+
/** Extract rendered prose, leaving code examples, URLs and front-matter syntax out. */
|
|
9
|
+
export declare const markdownProse: (root: Root, lineOffset?: number) => ProseSegment[];
|
|
10
|
+
export declare const assetProse: (file: string, raw: string) => ProseSegment[] | undefined;
|
|
11
|
+
export declare const checkProse: (segments: ProseSegment[], language: string) => Omit<CheckDiagnostic, "file" | "check">[];
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { francAll } from "franc";
|
|
2
|
+
import { visit } from "unist-util-visit";
|
|
3
|
+
import { parseMdast } from "../core/markdown.js";
|
|
4
|
+
import { parseContentSource } from "./code-blocks.js";
|
|
5
|
+
/** Extract rendered prose, leaving code examples, URLs and front-matter syntax out. */
|
|
6
|
+
export const markdownProse = (root, lineOffset = 0) => {
|
|
7
|
+
const segments = [];
|
|
8
|
+
visit(root, (node) => {
|
|
9
|
+
const line = (node.position?.start.line ?? 1) + lineOffset;
|
|
10
|
+
const column = node.position?.start.column ?? 1;
|
|
11
|
+
if (["paragraph", "heading", "tableCell"].includes(node.type)) {
|
|
12
|
+
const parts = [];
|
|
13
|
+
visit(node, (child, _index, parent) => {
|
|
14
|
+
if (child.type === "text") {
|
|
15
|
+
if (parent?.type === "link" && child.value === parent.url)
|
|
16
|
+
return;
|
|
17
|
+
parts.push(child.value);
|
|
18
|
+
}
|
|
19
|
+
if (child.type === "image" && child.alt)
|
|
20
|
+
parts.push(child.alt);
|
|
21
|
+
});
|
|
22
|
+
if (parts.length)
|
|
23
|
+
segments.push({ text: parts.join(" "), line, column });
|
|
24
|
+
}
|
|
25
|
+
if (node.type !== "code")
|
|
26
|
+
return;
|
|
27
|
+
if (node.lang === "callouth") {
|
|
28
|
+
if (node.meta)
|
|
29
|
+
segments.push({ text: node.meta, line, column });
|
|
30
|
+
segments.push(...markdownProse(parseMdast(node.value), line));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const parsed = parseContentSource(node.lang ?? "", node.value, "check");
|
|
34
|
+
if (parsed && node.meta)
|
|
35
|
+
segments.push({ text: node.meta, line, column });
|
|
36
|
+
for (const block of parsed?.blocks ?? []) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (block.type === "term")
|
|
39
|
+
parts.push(block.entry.name, block.entry.definition, block.entry.description ?? "");
|
|
40
|
+
if (block.type === "graph")
|
|
41
|
+
parts.push(...block.graph.nodes.map((n) => (n.label === n.id ? "" : n.label)), ...block.graph.edges.map((edge) => edge.label ?? ""), ...block.graph.regions.map((region) => region.label ?? ""));
|
|
42
|
+
if (block.type === "schemaGraph")
|
|
43
|
+
parts.push(...block.graph.groups.map((group) => group.label ?? ""), ...block.graph.tables.flatMap((table) => table.notes));
|
|
44
|
+
if (parts.length)
|
|
45
|
+
segments.push({ text: parts.join(" "), line: line + 1, column: 1 });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return segments;
|
|
49
|
+
};
|
|
50
|
+
export const assetProse = (file, raw) => {
|
|
51
|
+
if (/\.(md|callouth)$/.test(file))
|
|
52
|
+
return markdownProse(parseMdast(raw));
|
|
53
|
+
const language = /\.(termth|graphth|schemath)$/.exec(file)?.[1];
|
|
54
|
+
if (language)
|
|
55
|
+
return markdownProse(parseMdast(`\`\`\`${language}\n${raw}\n\`\`\``), -1);
|
|
56
|
+
if (/\.(txt|html|svg)$/.test(file)) {
|
|
57
|
+
// shortcut: markup text is heuristic, use an XML/HTML parser if exact element ranges are needed.
|
|
58
|
+
const text = file.endsWith(".txt")
|
|
59
|
+
? raw
|
|
60
|
+
: raw
|
|
61
|
+
.replace(/<!--[\s\S]*?-->/g, " ")
|
|
62
|
+
.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ")
|
|
63
|
+
.replace(/<[^>]*>/g, " ")
|
|
64
|
+
.replace(/&(?:#\d+|#x[\da-f]+|[a-z]+);/gi, " ");
|
|
65
|
+
return [{ text, line: 1, column: 1 }];
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
};
|
|
69
|
+
// Franc reports individual languages for several commonly configured macrolanguages.
|
|
70
|
+
const macroLanguages = {
|
|
71
|
+
cmn: "zho",
|
|
72
|
+
arb: "ara",
|
|
73
|
+
pes: "fas",
|
|
74
|
+
ekk: "est",
|
|
75
|
+
swh: "swa",
|
|
76
|
+
azj: "aze",
|
|
77
|
+
uzn: "uzb",
|
|
78
|
+
pbu: "pus",
|
|
79
|
+
npi: "nep",
|
|
80
|
+
khk: "mon",
|
|
81
|
+
lvs: "lav",
|
|
82
|
+
};
|
|
83
|
+
export const checkProse = (segments, language) => {
|
|
84
|
+
const findings = [];
|
|
85
|
+
let checked = 0;
|
|
86
|
+
const inspect = ({ text, line, column }) => {
|
|
87
|
+
const clean = text
|
|
88
|
+
.replace(/https?:\/\/\S+/g, " ")
|
|
89
|
+
.replace(/\s+/g, " ")
|
|
90
|
+
.trim();
|
|
91
|
+
const letters = clean.match(/\p{L}/gu)?.length ?? 0;
|
|
92
|
+
const minimum = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(clean)
|
|
93
|
+
? 10
|
|
94
|
+
: 40;
|
|
95
|
+
if (letters < minimum)
|
|
96
|
+
return;
|
|
97
|
+
const ranked = francAll(clean, { minLength: minimum });
|
|
98
|
+
const detected = ranked[0]?.[0];
|
|
99
|
+
if (!detected || detected === "und")
|
|
100
|
+
return;
|
|
101
|
+
checked++;
|
|
102
|
+
const matches = (code) => code === language || macroLanguages[code] === language;
|
|
103
|
+
if (matches(detected))
|
|
104
|
+
return;
|
|
105
|
+
// Distance scores are relative rankings, not probabilities; near ties are inconclusive.
|
|
106
|
+
if (ranked.some(([code, score]) => matches(code) && score >= 0.9))
|
|
107
|
+
return;
|
|
108
|
+
findings.push({
|
|
109
|
+
code: "LANGUAGE_SUSPECT",
|
|
110
|
+
severity: "warning",
|
|
111
|
+
message: `Prose appears to be ${macroLanguages[detected] ?? detected}; expected ${language}. Language detection is heuristic.`,
|
|
112
|
+
line,
|
|
113
|
+
column,
|
|
114
|
+
language,
|
|
115
|
+
detectedLanguage: macroLanguages[detected] ?? detected,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const sentences = new Intl.Segmenter(undefined, { granularity: "sentence" });
|
|
119
|
+
// Franc samples at most 2048 characters; bounded chunks also check the end of long prose.
|
|
120
|
+
for (const segment of segments)
|
|
121
|
+
for (const sentence of sentences.segment(segment.text))
|
|
122
|
+
for (const text of sentence.segment.match(/[\s\S]{1,1024}/g) ?? [])
|
|
123
|
+
inspect({ ...segment, text });
|
|
124
|
+
if (!checked && segments.length)
|
|
125
|
+
inspect({
|
|
126
|
+
text: segments.map((s) => s.text).join(" "),
|
|
127
|
+
line: segments[0]?.line ?? 1,
|
|
128
|
+
column: 1,
|
|
129
|
+
});
|
|
130
|
+
if (!checked)
|
|
131
|
+
findings.push({
|
|
132
|
+
code: "LANGUAGE_UNDETERMINED",
|
|
133
|
+
severity: "info",
|
|
134
|
+
message: "Not enough identifiable prose to check the language.",
|
|
135
|
+
language,
|
|
136
|
+
});
|
|
137
|
+
return findings;
|
|
138
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const CHECK_KINDS: readonly ["render", "languages", "themes"];
|
|
2
|
+
export type CheckKind = (typeof CHECK_KINDS)[number];
|
|
3
|
+
export interface CheckOptions {
|
|
4
|
+
/** Run only these checks; omitted means all checks. Manifest validity is always checked. */
|
|
5
|
+
only?: readonly CheckKind[];
|
|
6
|
+
/** Permit identical SVG files across themes, including symlinks and byte copies. */
|
|
7
|
+
allowSharedSvg?: boolean;
|
|
8
|
+
warningsAsErrors?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type CheckCode = "INVOCATION_INVALID" | "CHECK_FAILED" | "MANIFEST_INVALID" | "RENDER_INVALID" | "FILE_MISSING" | "FILE_UNREADABLE" | "DOCUMENT_INVALID" | "ASSET_INVALID" | "LANGUAGE_INVALID" | "LANGUAGE_UNDECLARED" | "TRANSLATION_MISSING" | "TRANSLATION_ORPHAN" | "TRANSLATION_IDENTICAL" | "LANGUAGE_MISMATCH" | "LANGUAGE_SUSPECT" | "LANGUAGE_UNDETERMINED" | "LANGUAGE_UNCHECKED" | "THEME_INVALID" | "THEME_UNDECLARED" | "THEME_VARIANT_MISSING" | "THEME_VARIANT_ORPHAN" | "THEME_CONTENT_IDENTICAL" | "REMOTE_UNCHECKED";
|
|
11
|
+
export interface CheckDiagnostic {
|
|
12
|
+
check: CheckKind | "config";
|
|
13
|
+
code: CheckCode;
|
|
14
|
+
severity: "error" | "warning" | "info";
|
|
15
|
+
message: string;
|
|
16
|
+
/** Path relative to report.root; lines and columns are one-based. */
|
|
17
|
+
file: string;
|
|
18
|
+
line?: number;
|
|
19
|
+
column?: number;
|
|
20
|
+
documentId?: string;
|
|
21
|
+
sourceFile?: string;
|
|
22
|
+
relatedFile?: string;
|
|
23
|
+
/** Expected language, normalized to ISO 639-3. */
|
|
24
|
+
language?: string;
|
|
25
|
+
detectedLanguage?: string;
|
|
26
|
+
theme?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CheckReport {
|
|
29
|
+
schemaVersion: 1;
|
|
30
|
+
root: string;
|
|
31
|
+
/** Checks actually requested, so a partial report cannot be mistaken for a full audit. */
|
|
32
|
+
checks: CheckKind[];
|
|
33
|
+
languages: string[];
|
|
34
|
+
themes: string[];
|
|
35
|
+
/** Sorted deterministically by file, line, code, language and theme. */
|
|
36
|
+
diagnostics: CheckDiagnostic[];
|
|
37
|
+
summary: {
|
|
38
|
+
errors: number;
|
|
39
|
+
warnings: number;
|
|
40
|
+
info: number;
|
|
41
|
+
};
|
|
42
|
+
/** 0 = passed, 1 = findings failed policy, 2 = invocation or execution failed. */
|
|
43
|
+
exitCode: 0 | 1 | 2;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const CHECK_KINDS = ["render", "languages", "themes"];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CheckOptions, type CheckReport } from "./check-types.js";
|
|
2
|
+
export type { CheckCode, CheckDiagnostic, CheckKind, CheckOptions, CheckReport, } from "./check-types.js";
|
|
3
|
+
export { CHECK_KINDS } from "./check-types.js";
|
|
4
|
+
/** Read-only, offline consistency audit. Content errors are returned, never written or repaired. */
|
|
5
|
+
export declare const checkSite: (root: string, options?: CheckOptions) => CheckReport;
|