@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,302 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { inflateRawSync } from "node:zlib";
|
|
7
|
+
import { textfilePath } from "../compiler/textfile.js";
|
|
8
|
+
import { zip } from "../compiler/zip.js";
|
|
9
|
+
import { extensionFile, extensionManifest, MAX_EXTENSION_BYTES, readExtensionZip, } from "../core/extension-package.js";
|
|
10
|
+
import { validateViewerPlugins, } from "./plugin-config.js";
|
|
11
|
+
export const EXTENSIONS_DIR = ".dooc/plugins";
|
|
12
|
+
const hash = (source) => createHash("sha256").update(source).digest("hex");
|
|
13
|
+
const unpack = (data) => readExtensionZip(data, async (bytes, size) => inflateRawSync(bytes, { maxOutputLength: Math.max(1, size) }));
|
|
14
|
+
/** ZIPs found on disk stay inactive until the browser approves their exact content hash. */
|
|
15
|
+
export const createExtensionManager = (root, registry, initial, personal) => {
|
|
16
|
+
const active = new Map();
|
|
17
|
+
const scope = createHash("sha256").update(realpathSync(root)).digest("hex");
|
|
18
|
+
const approvalFile = `extensions-${scope}.json`;
|
|
19
|
+
const approval = () => {
|
|
20
|
+
const value = personal?.read(approvalFile);
|
|
21
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: {};
|
|
24
|
+
};
|
|
25
|
+
const remember = (file, revision) => {
|
|
26
|
+
if (!personal)
|
|
27
|
+
return;
|
|
28
|
+
const approved = approval();
|
|
29
|
+
if (revision)
|
|
30
|
+
approved[file] = revision;
|
|
31
|
+
else
|
|
32
|
+
delete approved[file];
|
|
33
|
+
personal.write(approvalFile, approved);
|
|
34
|
+
};
|
|
35
|
+
let queued = Promise.resolve();
|
|
36
|
+
const directory = () => {
|
|
37
|
+
for (const name of [".dooc", EXTENSIONS_DIR]) {
|
|
38
|
+
const entry = lstatSync(join(root, name), { throwIfNoEntry: false });
|
|
39
|
+
if (entry && (!entry.isDirectory() || entry.isSymbolicLink()))
|
|
40
|
+
throw new Error("Plugin folders must be local directories");
|
|
41
|
+
}
|
|
42
|
+
return textfilePath(root, EXTENSIONS_DIR);
|
|
43
|
+
};
|
|
44
|
+
const read = (name) => {
|
|
45
|
+
const path = join(directory(), name);
|
|
46
|
+
const entry = lstatSync(path, { throwIfNoEntry: false });
|
|
47
|
+
if (!entry)
|
|
48
|
+
return null;
|
|
49
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_EXTENSION_BYTES)
|
|
50
|
+
throw new Error("Expected a plugin ZIP smaller than 16 MiB");
|
|
51
|
+
return readFileSync(path);
|
|
52
|
+
};
|
|
53
|
+
const disable = (name) => {
|
|
54
|
+
const value = active.get(name);
|
|
55
|
+
if (!value)
|
|
56
|
+
return;
|
|
57
|
+
for (const plugin of value.config.plugins)
|
|
58
|
+
if (!registry.configuration || registry.configuration().plugins.includes(plugin))
|
|
59
|
+
registry.unregister(plugin.id);
|
|
60
|
+
rmSync(value.directory, { recursive: true, force: true });
|
|
61
|
+
active.delete(name);
|
|
62
|
+
};
|
|
63
|
+
const list = async () => {
|
|
64
|
+
const path = directory();
|
|
65
|
+
const entries = lstatSync(path, { throwIfNoEntry: false })
|
|
66
|
+
? readdirSync(path, { withFileTypes: true })
|
|
67
|
+
: [];
|
|
68
|
+
const items = [];
|
|
69
|
+
for (const entry of entries.filter((entry) => entry.isFile() && entry.name.endsWith(".zip"))) {
|
|
70
|
+
let file;
|
|
71
|
+
try {
|
|
72
|
+
file = extensionFile(entry.name);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const data = read(file);
|
|
78
|
+
if (!data)
|
|
79
|
+
continue;
|
|
80
|
+
const revision = hash(data);
|
|
81
|
+
const loaded = active.get(file);
|
|
82
|
+
if (loaded &&
|
|
83
|
+
(loaded.hash !== revision ||
|
|
84
|
+
(registry.configuration &&
|
|
85
|
+
loaded.config.plugins.some((plugin) => !registry.configuration?.().plugins.includes(plugin)))))
|
|
86
|
+
disable(file);
|
|
87
|
+
try {
|
|
88
|
+
const pkg = await unpack(data);
|
|
89
|
+
items.push({
|
|
90
|
+
file,
|
|
91
|
+
name: pkg.name,
|
|
92
|
+
icon: pkg.icon ??
|
|
93
|
+
active.get(file)?.config.plugins.find((plugin) => plugin.icon)?.icon,
|
|
94
|
+
version: pkg.version,
|
|
95
|
+
hash: revision,
|
|
96
|
+
size: data.length,
|
|
97
|
+
enabled: active.has(file),
|
|
98
|
+
plugins: active.get(file)?.config.plugins.map((plugin) => plugin.id) ?? [],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
disable(file);
|
|
103
|
+
items.push({
|
|
104
|
+
file,
|
|
105
|
+
name: file,
|
|
106
|
+
version: "",
|
|
107
|
+
hash: revision,
|
|
108
|
+
size: data.length,
|
|
109
|
+
enabled: false,
|
|
110
|
+
plugins: [],
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const name of active.keys())
|
|
116
|
+
if (!items.some((item) => item.file === name))
|
|
117
|
+
disable(name);
|
|
118
|
+
const owned = new Set([...active.values()].flatMap((value) => value.config.plugins));
|
|
119
|
+
for (const plugin of (registry.configuration?.() ?? initial).plugins) {
|
|
120
|
+
if (owned.has(plugin))
|
|
121
|
+
continue;
|
|
122
|
+
items.push({
|
|
123
|
+
file: `viewer/${plugin.id}`,
|
|
124
|
+
name: plugin.name ?? plugin.id,
|
|
125
|
+
icon: plugin.icon,
|
|
126
|
+
version: "",
|
|
127
|
+
hash: "",
|
|
128
|
+
size: 0,
|
|
129
|
+
enabled: true,
|
|
130
|
+
plugins: [plugin.id],
|
|
131
|
+
readonly: true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return { scope, items: items.sort((a, b) => a.name.localeCompare(b.name)) };
|
|
135
|
+
};
|
|
136
|
+
const activate = async (name, data, save, base) => {
|
|
137
|
+
const revision = hash(data);
|
|
138
|
+
if (!save && active.get(name)?.hash === revision)
|
|
139
|
+
return;
|
|
140
|
+
const pkg = await unpack(data);
|
|
141
|
+
const folder = mkdtempSync(join(tmpdir(), "dooc-extension-"));
|
|
142
|
+
let timer;
|
|
143
|
+
try {
|
|
144
|
+
for (const file of pkg.files) {
|
|
145
|
+
const path = join(folder, file.name);
|
|
146
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
147
|
+
writeFileSync(path, file.data, { flag: "wx" });
|
|
148
|
+
}
|
|
149
|
+
const module = await Promise.race([
|
|
150
|
+
import(pathToFileURL(join(folder, pkg.main)).href),
|
|
151
|
+
new Promise((_, reject) => {
|
|
152
|
+
timer = setTimeout(() => reject(new Error("Plugin initialization timed out")), 10000);
|
|
153
|
+
}),
|
|
154
|
+
]);
|
|
155
|
+
const config = validateViewerPlugins(module.default);
|
|
156
|
+
if (!config.plugins.length ||
|
|
157
|
+
config.setup ||
|
|
158
|
+
config.gitOrigin ||
|
|
159
|
+
config.workspace ||
|
|
160
|
+
config.allowedDomains)
|
|
161
|
+
throw new Error("Export a viewer configuration with plugins and optional mocks");
|
|
162
|
+
const live = registry.configuration?.();
|
|
163
|
+
const owned = active.get(name)?.config.plugins ?? [];
|
|
164
|
+
for (const plugin of config.plugins) {
|
|
165
|
+
const existing = live?.plugins.find((item) => item.id === plugin.id);
|
|
166
|
+
if (existing && !owned.includes(existing))
|
|
167
|
+
throw new Error(`Plugin ID is already registered: ${plugin.id}`);
|
|
168
|
+
}
|
|
169
|
+
if (Object.keys(config.mocks).some((id) => !config.plugins.some((plugin) => plugin.id === id)))
|
|
170
|
+
throw new Error("Mocks must belong to plugins in this package");
|
|
171
|
+
const other = live
|
|
172
|
+
? [
|
|
173
|
+
{
|
|
174
|
+
plugins: live.plugins.filter((plugin) => !owned.includes(plugin)),
|
|
175
|
+
mocks: Object.fromEntries(Object.entries(live.mocks).filter(([id]) => !owned.some((plugin) => plugin.id === id))),
|
|
176
|
+
},
|
|
177
|
+
]
|
|
178
|
+
: [
|
|
179
|
+
initial,
|
|
180
|
+
...[...active]
|
|
181
|
+
.filter(([key]) => key !== name)
|
|
182
|
+
.map(([, value]) => value.config),
|
|
183
|
+
];
|
|
184
|
+
validateViewerPlugins({
|
|
185
|
+
plugins: [...other.flatMap((config) => config.plugins), ...config.plugins],
|
|
186
|
+
mocks: Object.assign({}, ...other.map((config) => config.mocks), config.mocks),
|
|
187
|
+
});
|
|
188
|
+
if (save) {
|
|
189
|
+
const current = read(name);
|
|
190
|
+
if ((current ? hash(current) : null) !== base)
|
|
191
|
+
throw new Error("Plugin changed. Reopen it before saving.");
|
|
192
|
+
const target = join(directory(), name);
|
|
193
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
194
|
+
const temporary = join(dirname(target), `.upload-${Date.now()}.tmp`);
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(temporary, data, { flag: "wx" });
|
|
197
|
+
renameSync(temporary, target);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
rmSync(temporary, { force: true });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
disable(name);
|
|
204
|
+
for (const plugin of config.plugins)
|
|
205
|
+
registry.register(plugin, { mock: config.mocks[plugin.id] });
|
|
206
|
+
active.set(name, { hash: revision, config, directory: folder });
|
|
207
|
+
remember(name, revision);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
if (active.get(name)?.directory !== folder)
|
|
212
|
+
rmSync(folder, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
const perform = async (input) => {
|
|
216
|
+
if (input.action === "list")
|
|
217
|
+
return list();
|
|
218
|
+
const name = extensionFile(input.file);
|
|
219
|
+
const current = read(name);
|
|
220
|
+
if (input.action === "read") {
|
|
221
|
+
if (!current)
|
|
222
|
+
throw new Error("Plugin not found");
|
|
223
|
+
const pkg = await unpack(current);
|
|
224
|
+
return {
|
|
225
|
+
file: name,
|
|
226
|
+
hash: hash(current),
|
|
227
|
+
...pkg,
|
|
228
|
+
files: pkg.files.map((file) => ({
|
|
229
|
+
name: file.name,
|
|
230
|
+
data: Buffer.from(file.data).toString("base64"),
|
|
231
|
+
})),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (input.action === "install") {
|
|
235
|
+
if (typeof input.data !== "string" ||
|
|
236
|
+
input.data.length > Math.ceil((MAX_EXTENSION_BYTES * 4) / 3) ||
|
|
237
|
+
input.data.length % 4 !== 0 ||
|
|
238
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(input.data))
|
|
239
|
+
throw new Error("Expected a ZIP smaller than 16 MiB");
|
|
240
|
+
if ((current ? hash(current) : null) !== input.base)
|
|
241
|
+
throw new Error("Plugin changed. Reopen it before saving.");
|
|
242
|
+
await activate(name, Buffer.from(input.data, "base64"), true, input.base);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
if (!current || hash(current) !== input.hash)
|
|
246
|
+
throw new Error("Plugin changed. Reopen it before continuing.");
|
|
247
|
+
if (input.action === "enable")
|
|
248
|
+
await activate(name, current, false, input.hash);
|
|
249
|
+
else if (input.action === "disable") {
|
|
250
|
+
disable(name);
|
|
251
|
+
remember(name);
|
|
252
|
+
}
|
|
253
|
+
else if (input.action === "delete") {
|
|
254
|
+
unlinkSync(join(directory(), name));
|
|
255
|
+
disable(name);
|
|
256
|
+
remember(name);
|
|
257
|
+
}
|
|
258
|
+
else if (input.action === "edit") {
|
|
259
|
+
if (typeof input.source !== "string" ||
|
|
260
|
+
Buffer.byteLength(input.source) > MAX_EXTENSION_BYTES ||
|
|
261
|
+
input.source.includes("\0"))
|
|
262
|
+
throw new Error("Expected UTF-8 text smaller than 16 MiB");
|
|
263
|
+
const pkg = await unpack(current);
|
|
264
|
+
const file = pkg.files.find((file) => file.name === input.path);
|
|
265
|
+
if (!file)
|
|
266
|
+
throw new Error("Plugin file not found");
|
|
267
|
+
file.data = Buffer.from(input.source);
|
|
268
|
+
extensionManifest(pkg.files);
|
|
269
|
+
const data = zip(pkg.files.map((file) => ({ name: file.name, data: Buffer.from(file.data) })));
|
|
270
|
+
await activate(name, data, true, input.hash);
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
throw new Error("Unknown plugin action");
|
|
274
|
+
}
|
|
275
|
+
return list();
|
|
276
|
+
};
|
|
277
|
+
const request = (input) => {
|
|
278
|
+
const result = queued.then(() => perform(input));
|
|
279
|
+
queued = result.catch(() => { });
|
|
280
|
+
return result;
|
|
281
|
+
};
|
|
282
|
+
return Object.assign(request, {
|
|
283
|
+
async restore() {
|
|
284
|
+
const approved = approval();
|
|
285
|
+
const errors = [];
|
|
286
|
+
for (const item of (await list()).items)
|
|
287
|
+
if (!item.readonly && !item.error && approved[item.file] === item.hash) {
|
|
288
|
+
try {
|
|
289
|
+
await request({ action: "enable", file: item.file, hash: item.hash });
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
errors.push(`${item.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return errors;
|
|
296
|
+
},
|
|
297
|
+
dispose: () => {
|
|
298
|
+
for (const name of active.keys())
|
|
299
|
+
disable(name);
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PreviewSettings } from "../core/previewers.js";
|
|
2
|
+
export interface FileReference {
|
|
3
|
+
line: number;
|
|
4
|
+
file: string;
|
|
5
|
+
kind: "textfile" | "figure" | "html" | "term";
|
|
6
|
+
}
|
|
7
|
+
/** Use the compiler's Markdown grammar so examples, escapes and URL encodings agree. */
|
|
8
|
+
export declare const fileReferences: (body: string, settings?: PreviewSettings) => FileReference[];
|
|
9
|
+
export interface FilePreview {
|
|
10
|
+
state: "pending" | "loading" | "valid" | "error";
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
/** An empty list means that this file compiled successfully. */
|
|
14
|
+
export type FilePreviewResults = Record<string, string[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
import { parseFileEmbed } from "../core/file-embed.js";
|
|
3
|
+
import { contentLinks, embeddedImages, parseMdast } from "../core/markdown.js";
|
|
4
|
+
import { parseTermReference } from "../core/term-reference.js";
|
|
5
|
+
/** Use the compiler's Markdown grammar so examples, escapes and URL encodings agree. */
|
|
6
|
+
export const fileReferences = (body, settings) => {
|
|
7
|
+
const root = parseMdast(body);
|
|
8
|
+
const images = new Set(embeddedImages(root));
|
|
9
|
+
const references = [];
|
|
10
|
+
for (const node of contentLinks(root)) {
|
|
11
|
+
try {
|
|
12
|
+
const file = parseTermReference(node.url);
|
|
13
|
+
if (file)
|
|
14
|
+
references.push({ line: node.position?.start.line ?? 1, file, kind: "term" });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* The compiler reports invalid paths. */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
visit(root, (node) => {
|
|
21
|
+
if (node.type === "image" && images.has(node)) {
|
|
22
|
+
try {
|
|
23
|
+
const { file, kind } = parseFileEmbed(node.url, settings);
|
|
24
|
+
if (kind === "external")
|
|
25
|
+
return;
|
|
26
|
+
references.push({
|
|
27
|
+
line: node.position?.start.line ?? 1,
|
|
28
|
+
file: file.replace(/^(\.\/)+/, ""),
|
|
29
|
+
kind,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// The compiler reports invalid URLs; no editor should open for them.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return references.sort((a, b) => a.line - b.line);
|
|
38
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectFont } from "../compiler/fonts.js";
|
|
2
|
+
import type { createFontSettings } from "./font-settings.js";
|
|
3
|
+
/** Self-contained because published viewers embed this function directly. */
|
|
4
|
+
export declare const mountFontPickers: (createSettings: typeof createFontSettings, registered: readonly ProjectFont[]) => Promise<void>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Self-contained because published viewers embed this function directly. */
|
|
2
|
+
export const mountFontPickers = async (createSettings, registered) => {
|
|
3
|
+
const settings = createSettings(registered);
|
|
4
|
+
const root = document.documentElement;
|
|
5
|
+
const t = (key) => window.__docsTranslate?.(key) ??
|
|
6
|
+
key;
|
|
7
|
+
const popup = document.createElement("div");
|
|
8
|
+
popup.className = "theme-picker font-picker";
|
|
9
|
+
popup.id = "font-picker";
|
|
10
|
+
popup.popover = "auto";
|
|
11
|
+
popup.setAttribute("role", "menu");
|
|
12
|
+
document.body.append(popup);
|
|
13
|
+
let trigger;
|
|
14
|
+
let busy = true;
|
|
15
|
+
let message = "Loading fonts";
|
|
16
|
+
const target = (button) => button.dataset.fontPicker === "editor" ? "editor" : "viewer";
|
|
17
|
+
const choices = () => new Map([
|
|
18
|
+
["", t("Default font")],
|
|
19
|
+
...registered
|
|
20
|
+
.filter((font) => settings.fonts.has(font.family))
|
|
21
|
+
.map((font) => [font.family, font.name]),
|
|
22
|
+
]);
|
|
23
|
+
const render = () => {
|
|
24
|
+
if (!trigger)
|
|
25
|
+
return;
|
|
26
|
+
popup.replaceChildren();
|
|
27
|
+
popup.setAttribute("aria-label", t(target(trigger) === "editor" ? "Editor font" : "Viewer font"));
|
|
28
|
+
for (const [family, name] of choices()) {
|
|
29
|
+
const option = document.createElement("button");
|
|
30
|
+
option.type = "button";
|
|
31
|
+
option.dataset.fontValue = family;
|
|
32
|
+
option.setAttribute("role", "menuitemradio");
|
|
33
|
+
option.setAttribute("aria-checked", String(family === settings.selected[target(trigger)]));
|
|
34
|
+
option.textContent = option.title = name;
|
|
35
|
+
popup.append(option);
|
|
36
|
+
}
|
|
37
|
+
if (message) {
|
|
38
|
+
const status = document.createElement("div");
|
|
39
|
+
status.className = "font-status";
|
|
40
|
+
status.setAttribute("role", "status");
|
|
41
|
+
status.textContent = t(message);
|
|
42
|
+
popup.append(status);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const sync = () => {
|
|
46
|
+
const names = choices();
|
|
47
|
+
for (const button of document.querySelectorAll("[data-font-picker]")) {
|
|
48
|
+
const kind = target(button);
|
|
49
|
+
button.disabled = busy;
|
|
50
|
+
button.title = `${t(kind === "editor" ? "Editor font" : "Viewer font")}: ${names.get(settings.selected[kind]) ?? t("Default font")}`;
|
|
51
|
+
button.setAttribute("aria-label", button.title);
|
|
52
|
+
button.setAttribute("aria-controls", popup.id);
|
|
53
|
+
button.setAttribute("popovertarget", popup.id);
|
|
54
|
+
}
|
|
55
|
+
if (popup.matches(":popover-open"))
|
|
56
|
+
render();
|
|
57
|
+
};
|
|
58
|
+
document.addEventListener("click", (event) => {
|
|
59
|
+
const button = event.target instanceof Element
|
|
60
|
+
? event.target.closest("[data-font-picker]")
|
|
61
|
+
: null;
|
|
62
|
+
if (!button || busy)
|
|
63
|
+
return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
if (trigger === button && popup.matches(":popover-open")) {
|
|
66
|
+
popup.hidePopover();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
70
|
+
if (popup.matches(":popover-open"))
|
|
71
|
+
popup.hidePopover();
|
|
72
|
+
trigger = button;
|
|
73
|
+
popup.dataset.theme =
|
|
74
|
+
(target(button) === "editor"
|
|
75
|
+
? root.dataset.editorTheme
|
|
76
|
+
: (button.closest(".preview-pane")?.dataset.theme ??
|
|
77
|
+
root.dataset.theme)) ?? "auto";
|
|
78
|
+
render();
|
|
79
|
+
const rect = button.getBoundingClientRect();
|
|
80
|
+
popup.style.left = `${Math.max(8, Math.min(rect.left, innerWidth - 248))}px`;
|
|
81
|
+
popup.style.top = `${rect.bottom + 6}px`;
|
|
82
|
+
popup.style.maxHeight = `${Math.max(60, innerHeight - rect.bottom - 14)}px`;
|
|
83
|
+
popup.showPopover();
|
|
84
|
+
button.setAttribute("aria-expanded", "true");
|
|
85
|
+
popup.querySelector('[aria-checked="true"]')?.focus();
|
|
86
|
+
});
|
|
87
|
+
popup.addEventListener("click", (event) => {
|
|
88
|
+
const option = event.target instanceof Element
|
|
89
|
+
? event.target.closest("[data-font-value]")
|
|
90
|
+
: null;
|
|
91
|
+
if (!option || !trigger)
|
|
92
|
+
return;
|
|
93
|
+
message = "";
|
|
94
|
+
try {
|
|
95
|
+
settings.select(target(trigger), option.dataset.fontValue ?? "");
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
message = error instanceof Error ? error.message : "Invalid font file";
|
|
99
|
+
}
|
|
100
|
+
if (!message) {
|
|
101
|
+
popup.hidePopover();
|
|
102
|
+
trigger.focus({ preventScroll: true });
|
|
103
|
+
}
|
|
104
|
+
sync();
|
|
105
|
+
if (message)
|
|
106
|
+
popup.querySelector('[aria-checked="true"]')?.focus();
|
|
107
|
+
});
|
|
108
|
+
popup.addEventListener("keydown", (event) => {
|
|
109
|
+
const options = [...popup.querySelectorAll("button")];
|
|
110
|
+
const index = options.indexOf(document.activeElement);
|
|
111
|
+
if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const next = event.key === "Home"
|
|
114
|
+
? 0
|
|
115
|
+
: event.key === "End"
|
|
116
|
+
? options.length - 1
|
|
117
|
+
: (index + (event.key === "ArrowDown" ? 1 : options.length - 1)) %
|
|
118
|
+
options.length;
|
|
119
|
+
options[next]?.focus();
|
|
120
|
+
}
|
|
121
|
+
if (event.key === "Escape")
|
|
122
|
+
trigger?.focus({ preventScroll: true });
|
|
123
|
+
});
|
|
124
|
+
popup.addEventListener("toggle", () => {
|
|
125
|
+
if (!popup.matches(":popover-open"))
|
|
126
|
+
trigger?.setAttribute("aria-expanded", "false");
|
|
127
|
+
});
|
|
128
|
+
new MutationObserver((records) => {
|
|
129
|
+
if (records.some((record) => [...record.addedNodes].some((node) => node instanceof Element &&
|
|
130
|
+
(node.matches("[data-font-picker]") ||
|
|
131
|
+
node.querySelector("[data-font-picker]")))))
|
|
132
|
+
sync();
|
|
133
|
+
}).observe(document.body, { childList: true, subtree: true });
|
|
134
|
+
document.addEventListener("docs:ui-language", sync);
|
|
135
|
+
sync();
|
|
136
|
+
try {
|
|
137
|
+
await settings.ready;
|
|
138
|
+
message = "";
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
message = error instanceof Error ? error.message : "Invalid font file";
|
|
142
|
+
}
|
|
143
|
+
busy = false;
|
|
144
|
+
sync();
|
|
145
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ProjectFont } from "../compiler/fonts.js";
|
|
2
|
+
/** Self-contained because published viewers embed this function directly. */
|
|
3
|
+
export declare const createFontSettings: (registered: readonly ProjectFont[]) => {
|
|
4
|
+
fonts: Map<string, FontFace>;
|
|
5
|
+
selected: {
|
|
6
|
+
editor: string;
|
|
7
|
+
viewer: string;
|
|
8
|
+
};
|
|
9
|
+
ready: Promise<void>;
|
|
10
|
+
select: (target: "editor" | "viewer", family: string) => void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** Self-contained because published viewers embed this function directly. */
|
|
2
|
+
export const createFontSettings = (registered) => {
|
|
3
|
+
const fonts = new Map();
|
|
4
|
+
const selected = { editor: "", viewer: "" };
|
|
5
|
+
const key = (target) => `docs-${target === "viewer" ? "preview" : target}:font`;
|
|
6
|
+
for (const target of ["editor", "viewer"]) {
|
|
7
|
+
try {
|
|
8
|
+
selected[target] = localStorage.getItem(key(target)) ?? "";
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
/* Defaults still work when storage is unavailable. */
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const apply = () => {
|
|
15
|
+
for (const target of ["editor", "viewer"]) {
|
|
16
|
+
const face = fonts.get(selected[target]);
|
|
17
|
+
if (face)
|
|
18
|
+
document.documentElement.style.setProperty(`--${target}-font`, `"${face.family}", "UDEV Gothic", monospace`);
|
|
19
|
+
else
|
|
20
|
+
document.documentElement.style.removeProperty(`--${target}-font`);
|
|
21
|
+
}
|
|
22
|
+
document.dispatchEvent(new Event("docs:font-change"));
|
|
23
|
+
};
|
|
24
|
+
const select = (target, family) => {
|
|
25
|
+
if (family && !fonts.has(family))
|
|
26
|
+
throw new Error("Invalid font file");
|
|
27
|
+
selected[target] = family;
|
|
28
|
+
apply();
|
|
29
|
+
try {
|
|
30
|
+
localStorage.setItem(key(target), family);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error("Font choice applies only to this tab");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const ready = (async () => {
|
|
37
|
+
const results = await Promise.allSettled(registered.map(async ({ family, source }) => {
|
|
38
|
+
const face = await new FontFace(family, source).load();
|
|
39
|
+
document.fonts.add(face);
|
|
40
|
+
fonts.set(family, face);
|
|
41
|
+
}));
|
|
42
|
+
for (const target of ["editor", "viewer"])
|
|
43
|
+
if (!fonts.has(selected[target]))
|
|
44
|
+
selected[target] = "";
|
|
45
|
+
apply();
|
|
46
|
+
if (results.some((result) => result.status === "rejected"))
|
|
47
|
+
throw new Error("Some registered fonts could not be loaded");
|
|
48
|
+
})();
|
|
49
|
+
return { fonts, selected, ready, select };
|
|
50
|
+
};
|