@dooc-dev/dooc 0.0.1 → 0.0.3
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/dist/cli.js +69 -2
- package/dist/core/extension-package.d.ts +34 -0
- package/dist/core/extension-package.js +147 -0
- package/dist/core/icons.d.ts +1 -0
- package/dist/core/icons.js +1 -0
- package/dist/edit.global.js +86 -85
- package/dist/plugins/blocks.d.ts +6 -5
- package/dist/plugins/blocks.js +3 -2
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/local-storage.d.ts +18 -0
- package/dist/plugins/local-storage.js +115 -0
- package/dist/plugins/oauth.js +6 -0
- package/dist/plugins/runtime.d.ts +3 -1
- package/dist/plugins/runtime.js +69 -13
- package/dist/plugins/types.d.ts +3 -0
- package/dist/plugins/view.d.ts +1 -23
- package/dist/plugins/view.js +2 -41
- package/dist/ui.global.js +34 -33
- 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/hmr.js +2 -0
- package/dist/viewer/locales/eng.json +25 -2
- package/dist/viewer/locales/jpn.json +25 -2
- package/dist/viewer/locales/zho.json +25 -2
- package/dist/viewer/plugin-view.d.ts +0 -1
- package/dist/viewer/plugin-view.js +1 -23
- package/dist/viewer/plugins.d.ts +5 -1
- package/dist/viewer/plugins.js +70 -17
- package/dist/viewer/serve.d.ts +5 -1
- package/dist/viewer/serve.js +155 -76
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// CLI entry shared by npx and mise. Root defaults to cwd; relative paths resolve against cwd.
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
import { prebuilt } from "./viewer/assets.js";
|
|
7
7
|
process.env.NODE_ENV = prebuilt ? "production" : "development";
|
|
@@ -14,6 +14,9 @@ const USAGE = `usage: dooc <command>
|
|
|
14
14
|
build zip [root] [out-dir] build bundle.zip (default <root>/dist)
|
|
15
15
|
--url_preview embed|network URL handling for either format (default: embed)
|
|
16
16
|
check [root] [options] JSON consistency report (check --help for options)
|
|
17
|
+
plugin pack <built-dir> [zip] package bundled JS and assets as a plugin ZIP
|
|
18
|
+
plugin add <zip> [root] [port] install a local plugin and print login links
|
|
19
|
+
plugin login <id> [root] [port] open a local login flow for an installed plugin
|
|
17
20
|
font add <file> [root] register a font in the project (.dooc/fonts/)
|
|
18
21
|
font list [root] list registered project fonts
|
|
19
22
|
font remove <name> [root] remove a registered project font
|
|
@@ -34,6 +37,70 @@ const writeOut = (out, data) => {
|
|
|
34
37
|
};
|
|
35
38
|
const [command, ...args] = process.argv.slice(2);
|
|
36
39
|
switch (command) {
|
|
40
|
+
case "plugin": {
|
|
41
|
+
try {
|
|
42
|
+
const { positionals, values } = parseArgs({
|
|
43
|
+
args,
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
options: { help: { type: "boolean", short: "h" } },
|
|
46
|
+
});
|
|
47
|
+
const [action, value, rootArg, portArg] = positionals;
|
|
48
|
+
if (values.help) {
|
|
49
|
+
console.log(`usage: dooc plugin pack <built-dir> [output.zip]
|
|
50
|
+
dooc plugin add <zip> [root] [port]
|
|
51
|
+
dooc plugin login <plugin-id> [root] [port]
|
|
52
|
+
|
|
53
|
+
ZIP root: package.json with name, version, type: module and main pointing to bundled .js/.mjs.
|
|
54
|
+
Optional icon: path to a bundled SVG asset up to 64 KiB.
|
|
55
|
+
Include relative assets (SVG, JSON, etc.); dependencies must already be bundled. Limit: 16 MiB / 256 files.
|
|
56
|
+
Default output: <built-dir>.zip. Installed ZIPs: <root>/.dooc/plugins/.
|
|
57
|
+
Personal logins and plugin approvals: ~/.dooc/plugins/ (override with DOOC_USER_DIR).
|
|
58
|
+
Add/login starts a local editor and prints browser login links. Keep it running while signing in.
|
|
59
|
+
Tokens stay on this computer and never enter exported documents.`);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (action === "pack" && value && positionals.length <= 3) {
|
|
63
|
+
const { packExtension } = await import("./viewer/extension-pack.js");
|
|
64
|
+
const { extensionFile } = await import("./core/extension-package.js");
|
|
65
|
+
const output = resolve(rootArg ?? `${resolve(value)}.zip`);
|
|
66
|
+
extensionFile(basename(output));
|
|
67
|
+
writeOut(output, await packExtension(resolve(value), output));
|
|
68
|
+
console.log(output);
|
|
69
|
+
}
|
|
70
|
+
else if ((action === "add" || action === "login") &&
|
|
71
|
+
value &&
|
|
72
|
+
positionals.length <= 4) {
|
|
73
|
+
const { extensionFile, MAX_EXTENSION_BYTES } = await import("./core/extension-package.js");
|
|
74
|
+
const port = Number(portArg ?? 5179);
|
|
75
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535)
|
|
76
|
+
throw new Error("Choose a local port between 1024 and 65535");
|
|
77
|
+
process.env.DOCS_ROOT = docsRoot(rootArg);
|
|
78
|
+
process.env.DOCS_HMR = "1";
|
|
79
|
+
const { startServer } = await import("./viewer/serve.js");
|
|
80
|
+
if (action === "add") {
|
|
81
|
+
const file = extensionFile(basename(value));
|
|
82
|
+
const data = readFileSync(resolve(value));
|
|
83
|
+
if (data.length > MAX_EXTENSION_BYTES)
|
|
84
|
+
throw new Error("Choose a ZIP smaller than 16 MiB");
|
|
85
|
+
await startServer("edit", port, undefined, {
|
|
86
|
+
file,
|
|
87
|
+
data: data.toString("base64"),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(value))
|
|
92
|
+
throw new Error("Invalid plugin ID");
|
|
93
|
+
await startServer("edit", port, undefined, { login: value });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
throw new Error("usage: dooc plugin pack <built-dir> [zip] | add <zip> [root] [port] | login <id> [root] [port]");
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
37
104
|
case "font": {
|
|
38
105
|
const { addProjectFont, listProjectFonts, removeProjectFont } = await import("./compiler/fonts.js");
|
|
39
106
|
try {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export declare const MAX_EXTENSION_BYTES: number;
|
|
2
|
+
export declare const MAX_EXTENSION_FILES = 256;
|
|
3
|
+
export interface ExtensionFile {
|
|
4
|
+
name: string;
|
|
5
|
+
data: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
export interface ExtensionPackage {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
main: string;
|
|
11
|
+
icon?: {
|
|
12
|
+
light: string;
|
|
13
|
+
dark?: string;
|
|
14
|
+
};
|
|
15
|
+
files: ExtensionFile[];
|
|
16
|
+
}
|
|
17
|
+
export interface ExtensionSummary {
|
|
18
|
+
file: string;
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
hash: string;
|
|
22
|
+
size: number;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
plugins: string[];
|
|
25
|
+
icon?: ExtensionPackage["icon"];
|
|
26
|
+
error?: string;
|
|
27
|
+
readonly?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare const extensionFile: (value: unknown) => string;
|
|
30
|
+
export declare const extensionPath: (value: string) => boolean;
|
|
31
|
+
export declare const extensionCrc: (data: Uint8Array) => number;
|
|
32
|
+
export declare const extensionManifest: (files: ExtensionFile[]) => ExtensionPackage;
|
|
33
|
+
/** Read bounded, single-disk ZIPs without extracting paths supplied by the archive. */
|
|
34
|
+
export declare const readExtensionZip: (bytes: Uint8Array, inflate: (data: Uint8Array, size: number) => Promise<Uint8Array>) => Promise<ExtensionPackage>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export const MAX_EXTENSION_BYTES = 16 * 1024 * 1024;
|
|
2
|
+
export const MAX_EXTENSION_FILES = 256;
|
|
3
|
+
export const extensionFile = (value) => {
|
|
4
|
+
if (typeof value !== "string" || !/^[\w][\w. -]{0,119}\.zip$/.test(value))
|
|
5
|
+
throw new Error("Choose a plugin ZIP file");
|
|
6
|
+
return value;
|
|
7
|
+
};
|
|
8
|
+
export const extensionPath = (value) => !!value &&
|
|
9
|
+
value.length <= 240 &&
|
|
10
|
+
!/[\\:]/.test(value) &&
|
|
11
|
+
[...value].every((char) => char.charCodeAt(0) >= 32 && char.charCodeAt(0) !== 127) &&
|
|
12
|
+
value
|
|
13
|
+
.split("/")
|
|
14
|
+
.every((part) => !!part &&
|
|
15
|
+
part !== "." &&
|
|
16
|
+
part !== ".." &&
|
|
17
|
+
!part.endsWith(".") &&
|
|
18
|
+
!part.endsWith(" "));
|
|
19
|
+
const table = new Uint32Array(256).map((_, value) => {
|
|
20
|
+
let crc = value;
|
|
21
|
+
for (let bit = 0; bit < 8; bit++)
|
|
22
|
+
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
|
|
23
|
+
return crc >>> 0;
|
|
24
|
+
});
|
|
25
|
+
export const extensionCrc = (data) => {
|
|
26
|
+
let crc = 0xffffffff;
|
|
27
|
+
for (const byte of data)
|
|
28
|
+
crc = (crc >>> 8) ^ (table[(crc ^ byte) & 255] ?? 0);
|
|
29
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
30
|
+
};
|
|
31
|
+
export const extensionManifest = (files) => {
|
|
32
|
+
const entry = files.find((file) => file.name === "package.json");
|
|
33
|
+
if (!entry || entry.data.length > 65536)
|
|
34
|
+
throw new Error("The ZIP needs package.json at its root");
|
|
35
|
+
const manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(entry.data));
|
|
36
|
+
if (!manifest ||
|
|
37
|
+
typeof manifest.name !== "string" ||
|
|
38
|
+
!/^(?:@[\w.-]+\/)?[\w.-]{1,100}$/.test(manifest.name) ||
|
|
39
|
+
typeof manifest.version !== "string" ||
|
|
40
|
+
!/^\d+\.\d+\.\d+(?:-[\w.-]+)?(?:\+[\w.-]+)?$/.test(manifest.version) ||
|
|
41
|
+
manifest.type !== "module" ||
|
|
42
|
+
typeof manifest.main !== "string" ||
|
|
43
|
+
!extensionPath(manifest.main) ||
|
|
44
|
+
!/\.m?js$/.test(manifest.main) ||
|
|
45
|
+
!files.some((file) => file.name === manifest.main))
|
|
46
|
+
throw new Error("package.json needs name, version, type: module and a bundled JavaScript main");
|
|
47
|
+
if (manifest.dependencies && Object.keys(manifest.dependencies).length)
|
|
48
|
+
throw new Error("Bundle dependencies into the JavaScript entry before packaging");
|
|
49
|
+
let icon;
|
|
50
|
+
if (manifest.icon !== undefined) {
|
|
51
|
+
const asset = files.find((file) => file.name === manifest.icon);
|
|
52
|
+
if (typeof manifest.icon !== "string" ||
|
|
53
|
+
!extensionPath(manifest.icon) ||
|
|
54
|
+
!manifest.icon.endsWith(".svg") ||
|
|
55
|
+
!asset ||
|
|
56
|
+
!asset.data.length ||
|
|
57
|
+
asset.data.length > 65536)
|
|
58
|
+
throw new Error("package.json icon must reference an SVG asset up to 64 KiB");
|
|
59
|
+
icon = {
|
|
60
|
+
light: `data:image/svg+xml;base64,${btoa(Array.from(asset.data, (byte) => String.fromCharCode(byte)).join(""))}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { name: manifest.name, version: manifest.version, main: manifest.main, files, icon };
|
|
64
|
+
};
|
|
65
|
+
/** Read bounded, single-disk ZIPs without extracting paths supplied by the archive. */
|
|
66
|
+
export const readExtensionZip = async (bytes, inflate) => {
|
|
67
|
+
const invalid = () => new Error("Invalid or unsupported plugin ZIP");
|
|
68
|
+
if (bytes.length < 22 || bytes.length > MAX_EXTENSION_BYTES)
|
|
69
|
+
throw invalid();
|
|
70
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
71
|
+
let end = bytes.length - 22;
|
|
72
|
+
while (end >= Math.max(0, bytes.length - 65557) &&
|
|
73
|
+
(view.getUint32(end, true) !== 0x06054b50 ||
|
|
74
|
+
end + 22 + view.getUint16(end + 20, true) !== bytes.length))
|
|
75
|
+
end--;
|
|
76
|
+
if (end < Math.max(0, bytes.length - 65557) ||
|
|
77
|
+
view.getUint16(end + 4, true) ||
|
|
78
|
+
view.getUint16(end + 6, true))
|
|
79
|
+
throw invalid();
|
|
80
|
+
const count = view.getUint16(end + 10, true);
|
|
81
|
+
const directory = view.getUint32(end + 16, true);
|
|
82
|
+
if (!count ||
|
|
83
|
+
count > MAX_EXTENSION_FILES ||
|
|
84
|
+
view.getUint16(end + 8, true) !== count ||
|
|
85
|
+
directory + view.getUint32(end + 12, true) !== end)
|
|
86
|
+
throw invalid();
|
|
87
|
+
const files = [];
|
|
88
|
+
const names = new Set();
|
|
89
|
+
const ranges = [];
|
|
90
|
+
let at = directory, total = 0;
|
|
91
|
+
for (let index = 0; index < count; index++) {
|
|
92
|
+
if (at + 46 > end || view.getUint32(at, true) !== 0x02014b50)
|
|
93
|
+
throw invalid();
|
|
94
|
+
const flags = view.getUint16(at + 8, true), method = view.getUint16(at + 10, true);
|
|
95
|
+
const compressed = view.getUint32(at + 20, true), size = view.getUint32(at + 24, true);
|
|
96
|
+
const length = view.getUint16(at + 28, true), extra = view.getUint16(at + 30, true), comment = view.getUint16(at + 32, true);
|
|
97
|
+
const offset = view.getUint32(at + 42, true), type = (view.getUint32(at + 38, true) >>> 16) & 0xf000;
|
|
98
|
+
total += size;
|
|
99
|
+
if (flags & ~0x080e ||
|
|
100
|
+
![0, 8].includes(method) ||
|
|
101
|
+
(method === 0 && size !== compressed) ||
|
|
102
|
+
total > MAX_EXTENSION_BYTES ||
|
|
103
|
+
at + 46 + length + extra + comment > end ||
|
|
104
|
+
offset + 30 > directory ||
|
|
105
|
+
view.getUint16(at + 34, true) ||
|
|
106
|
+
![0, 0x8000, 0x4000].includes(type))
|
|
107
|
+
throw invalid();
|
|
108
|
+
const name = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(at + 46, at + 46 + length));
|
|
109
|
+
const folder = name.endsWith("/");
|
|
110
|
+
const path = folder ? name.slice(0, -1) : name;
|
|
111
|
+
const key = path.normalize("NFC").toLowerCase();
|
|
112
|
+
if (!extensionPath(path) ||
|
|
113
|
+
names.has(key) ||
|
|
114
|
+
(folder && size !== 0) ||
|
|
115
|
+
(!folder && type === 0x4000))
|
|
116
|
+
throw invalid();
|
|
117
|
+
names.add(key);
|
|
118
|
+
if (view.getUint32(offset, true) !== 0x04034b50 ||
|
|
119
|
+
view.getUint16(offset + 6, true) !== flags ||
|
|
120
|
+
view.getUint16(offset + 8, true) !== method)
|
|
121
|
+
throw invalid();
|
|
122
|
+
const localName = view.getUint16(offset + 26, true), localExtra = view.getUint16(offset + 28, true);
|
|
123
|
+
const start = offset + 30 + localName + localExtra;
|
|
124
|
+
if (start + compressed > directory ||
|
|
125
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(offset + 30, offset + 30 + localName)) !== name)
|
|
126
|
+
throw invalid();
|
|
127
|
+
if (ranges.some(([from, to]) => offset < to && start + compressed > from))
|
|
128
|
+
throw invalid();
|
|
129
|
+
ranges.push([offset, start + compressed]);
|
|
130
|
+
const input = bytes.subarray(start, start + compressed);
|
|
131
|
+
const data = method === 0 ? input.slice() : await inflate(input, size);
|
|
132
|
+
if (data.length !== size || extensionCrc(data) !== view.getUint32(at + 16, true))
|
|
133
|
+
throw invalid();
|
|
134
|
+
if (!folder)
|
|
135
|
+
files.push({ name, data });
|
|
136
|
+
at += 46 + length + extra + comment;
|
|
137
|
+
}
|
|
138
|
+
if (at !== end)
|
|
139
|
+
throw invalid();
|
|
140
|
+
for (const file of files)
|
|
141
|
+
if (files.some((other) => file.name
|
|
142
|
+
.normalize("NFC")
|
|
143
|
+
.toLowerCase()
|
|
144
|
+
.startsWith(`${other.name.normalize("NFC").toLowerCase()}/`)))
|
|
145
|
+
throw invalid();
|
|
146
|
+
return extensionManifest(files);
|
|
147
|
+
};
|
package/dist/core/icons.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export declare const CHECK_ICON = "<svg class=\"ic-ok\" viewBox=\"0 0 20 20\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m4 10 4 4 8-8\"/></svg>";
|
|
2
|
+
export declare const PLUGINS_ICON = "<svg class=\"nav-icon\" viewBox=\"0 0 20 20\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m10 2 4 2v5l-4 2-4-2V4Zm-4 2 4 2 4-2m-4 2v5M6 9l-4 2v5l4 2 4-2v-5m-8 0 4 2 4-2m-4 2v5m8-9 4 2v5l-4 2-4-2m0-5 4 2 4-2m-4 2v5\"/></svg>";
|
package/dist/core/icons.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export const CHECK_ICON = '<svg class="ic-ok" viewBox="0 0 20 20" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 10 4 4 8-8"/></svg>';
|
|
2
|
+
export const PLUGINS_ICON = '<svg class="nav-icon" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m10 2 4 2v5l-4 2-4-2V4Zm-4 2 4 2 4-2m-4 2v5M6 9l-4 2v5l4 2 4-2v-5m-8 0 4 2 4-2m-4 2v5m8-9 4 2v5l-4 2-4-2m0-5 4 2 4-2m-4 2v5"/></svg>';
|