@dooc-dev/dooc 0.0.1 → 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/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/viewer/plugins.js
CHANGED
|
@@ -32,7 +32,7 @@ const readInput = async (req) => {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
/** Local viewer only: the cookie identifies a browser session, never a document-supplied user. */
|
|
35
|
-
export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
35
|
+
export const createPluginHandler = (plugins, presentation = { mocks: {} }, credentials) => {
|
|
36
36
|
let config = validateViewerPlugins({ plugins, ...presentation });
|
|
37
37
|
let mocks = new Map(Object.entries(config.mocks));
|
|
38
38
|
let origins = new Map([...mocks].map(([id, mock]) => [mock.origin, { id, ...mock }]));
|
|
@@ -43,7 +43,7 @@ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
|
43
43
|
const mock = origins.get(new URL(request.url).origin);
|
|
44
44
|
return mock ? mock.handle(request) : fetch(input, init);
|
|
45
45
|
};
|
|
46
|
-
const runtime = createPluginRuntime(config.plugins, transport);
|
|
46
|
+
const runtime = createPluginRuntime(config.plugins, transport, credentials);
|
|
47
47
|
const cookie = `dooc-plugins-${randomBytes(8).toString("hex")}`;
|
|
48
48
|
let revision = 0;
|
|
49
49
|
const streams = new Set();
|
|
@@ -84,6 +84,24 @@ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
|
84
84
|
});
|
|
85
85
|
},
|
|
86
86
|
};
|
|
87
|
+
const tickets = new Map();
|
|
88
|
+
const loginLink = (plugin, origin) => {
|
|
89
|
+
if (!config.plugins.some((item) => item.id === plugin))
|
|
90
|
+
throw new Error("Plugin is not configured");
|
|
91
|
+
for (const [key, value] of tickets)
|
|
92
|
+
if (value.expires <= Date.now())
|
|
93
|
+
tickets.delete(key);
|
|
94
|
+
const ticket = randomBytes(32).toString("hex");
|
|
95
|
+
tickets.set(ticket, { plugin, origin, expires: Date.now() + 10 * 60 * 1000 });
|
|
96
|
+
return `${origin}/api/plugins/login/${plugin}?ticket=${ticket}`;
|
|
97
|
+
};
|
|
98
|
+
const authorizationUrl = (session, plugin, host) => {
|
|
99
|
+
const authorization = new URL(runtime.connect(session, plugin, `http://${host}/api/plugins/callback/${plugin}`));
|
|
100
|
+
const mock = origins.get(authorization.origin);
|
|
101
|
+
return mock
|
|
102
|
+
? `http://${host}/api/plugins/mock/${mock.id}${authorization.pathname}${authorization.search}`
|
|
103
|
+
: authorization.href;
|
|
104
|
+
};
|
|
87
105
|
const handle = async (req, res) => {
|
|
88
106
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
89
107
|
if (url.pathname !== "/api/plugins" && !url.pathname.startsWith("/api/plugins/"))
|
|
@@ -150,11 +168,42 @@ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
|
150
168
|
.map((part) => part.trim())
|
|
151
169
|
.find((part) => part.startsWith(`${cookie}=`))
|
|
152
170
|
?.slice(cookie.length + 1) ?? "";
|
|
171
|
+
const createSession = () => {
|
|
172
|
+
session = runtime.createSession();
|
|
173
|
+
res.setHeader("set-cookie", `${cookie}=${session}; HttpOnly; SameSite=Lax; Path=/api/plugins; Max-Age=28800`);
|
|
174
|
+
};
|
|
175
|
+
const login = /^\/api\/plugins\/login\/([a-z][a-z0-9-]{0,63})$/.exec(url.pathname);
|
|
176
|
+
if (req.method === "GET" && login?.[1]) {
|
|
177
|
+
const key = url.searchParams.get("ticket") ?? "";
|
|
178
|
+
const ticket = tickets.get(key);
|
|
179
|
+
if (!ticket ||
|
|
180
|
+
ticket.expires <= Date.now() ||
|
|
181
|
+
ticket.plugin !== login[1] ||
|
|
182
|
+
ticket.origin !== `http://${host}`)
|
|
183
|
+
throw new PluginError(400, "Invalid or expired login link");
|
|
184
|
+
tickets.delete(key);
|
|
185
|
+
if (!runtime.hasSession(session))
|
|
186
|
+
createSession();
|
|
187
|
+
res.writeHead(303, { location: authorizationUrl(session, ticket.plugin, host) });
|
|
188
|
+
res.end();
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
153
191
|
const callback = /^\/api\/plugins\/callback\/([a-z][a-z0-9-]{0,63})$/.exec(url.pathname);
|
|
154
192
|
if (req.method === "GET" && callback?.[1]) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
193
|
+
let cancelled = false;
|
|
194
|
+
try {
|
|
195
|
+
await runtime.callback(session, callback[1], `http://${host}${url.pathname}`, url.searchParams);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
if (!isPluginError(error) ||
|
|
199
|
+
error.message !== "OAuth authorization was not completed")
|
|
200
|
+
throw error;
|
|
201
|
+
cancelled = true;
|
|
202
|
+
}
|
|
203
|
+
res.writeHead(cancelled ? 400 : 200, {
|
|
204
|
+
"content-type": "text/html; charset=utf-8",
|
|
205
|
+
});
|
|
206
|
+
res.end(`<!doctype html><html lang="en"><meta charset="utf-8"><title>${cancelled ? "Sign-in not completed" : "Plugin connected"}</title><p>${cancelled ? "Sign-in was not completed. No account was connected." : "Connected."} You can close this tab and return to dooc.</p></html>`);
|
|
158
207
|
return true;
|
|
159
208
|
}
|
|
160
209
|
if (url.pathname !== "/api/plugins")
|
|
@@ -165,26 +214,27 @@ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
|
165
214
|
const input = await readInput(req);
|
|
166
215
|
if (input.action === "resolve") {
|
|
167
216
|
const plugin = runtime.resolve(input.url);
|
|
217
|
+
if (plugin && credentials && !runtime.hasSession(session))
|
|
218
|
+
createSession();
|
|
168
219
|
send(200, {
|
|
169
220
|
plugin,
|
|
221
|
+
...(plugin && runtime.hasSession(session) && runtime.status(session, plugin)
|
|
222
|
+
? { connected: true }
|
|
223
|
+
: {}),
|
|
170
224
|
...(plugin && mocks.get(plugin)?.autoConnect ? { autoConnect: true } : {}),
|
|
171
225
|
});
|
|
172
226
|
return true;
|
|
173
227
|
}
|
|
174
228
|
if (typeof input.plugin !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(input.plugin))
|
|
175
229
|
throw new PluginError(400, "Invalid plugin ID");
|
|
230
|
+
if (!runtime.hasSession(session) &&
|
|
231
|
+
(credentials || input.action === "connect" || input.action === "status"))
|
|
232
|
+
createSession();
|
|
176
233
|
if (input.action === "connect") {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
const authorization = new URL(runtime.connect(session, input.plugin, `http://${host}/api/plugins/callback/${input.plugin}`));
|
|
182
|
-
const mock = origins.get(authorization.origin);
|
|
183
|
-
send(200, {
|
|
184
|
-
url: mock
|
|
185
|
-
? `http://${host}/api/plugins/mock/${mock.id}${authorization.pathname}${authorization.search}`
|
|
186
|
-
: authorization.href,
|
|
187
|
-
});
|
|
234
|
+
send(200, { url: authorizationUrl(session, input.plugin, host) });
|
|
235
|
+
}
|
|
236
|
+
else if (input.action === "status") {
|
|
237
|
+
send(200, { user: runtime.status(session, input.plugin) ?? null });
|
|
188
238
|
}
|
|
189
239
|
else if (input.action === "disconnect") {
|
|
190
240
|
if (runtime.hasSession(session))
|
|
@@ -210,5 +260,8 @@ export const createPluginHandler = (plugins, presentation = { mocks: {} }) => {
|
|
|
210
260
|
}
|
|
211
261
|
return true;
|
|
212
262
|
};
|
|
213
|
-
return Object.assign(handle, registry
|
|
263
|
+
return Object.assign(handle, registry, {
|
|
264
|
+
loginLink,
|
|
265
|
+
configuration: () => config,
|
|
266
|
+
});
|
|
214
267
|
};
|
package/dist/viewer/serve.d.ts
CHANGED
|
@@ -203,4 +203,8 @@ export interface BundleCache {
|
|
|
203
203
|
export declare const createBundleCache: () => BundleCache;
|
|
204
204
|
/** Client bundle (src/<entry> as an IIFE). Sourcemap for the server, minified for publish. */
|
|
205
205
|
export declare const bundleClient: (entry: string, publish?: boolean) => Promise<string>;
|
|
206
|
-
export declare const startServer: (mode: ServeMode, port?: number, pluginModule?: string
|
|
206
|
+
export declare const startServer: (mode: ServeMode, port?: number, pluginModule?: string, startup?: {
|
|
207
|
+
file?: string;
|
|
208
|
+
data?: string;
|
|
209
|
+
login?: string;
|
|
210
|
+
}) => Promise<ViewerRuntime>;
|
package/dist/viewer/serve.js
CHANGED
|
@@ -30,8 +30,10 @@ import { readManifest, writeManifest } from "../compiler/manifest.js";
|
|
|
30
30
|
import { assembleDoc, createDocOps, isOpError } from "../compiler/ops.js";
|
|
31
31
|
import { walkBlocks } from "../core/blocks.js";
|
|
32
32
|
import { docIdOfFile } from "../core/content-metadata.js";
|
|
33
|
+
import { MAX_EXTENSION_BYTES } from "../core/extension-package.js";
|
|
33
34
|
import { figureFrameHtml, mountFigureFrames } from "../core/figure-frame.js";
|
|
34
35
|
import { blocksHtml, docHtml, escapeHtml, termsHtml } from "../core/html.js";
|
|
36
|
+
import { PLUGINS_ICON } from "../core/icons.js";
|
|
35
37
|
import { DEFAULT_LANG, isLangTag, resolveLanguage, } from "../core/language.js";
|
|
36
38
|
import { defaultPreviewSettings, parsePreviewSettings, } from "../core/previewers.js";
|
|
37
39
|
import { ancestorsOf, childrenOf, docPath, resolveDocPath } from "../core/routing.js";
|
|
@@ -44,10 +46,12 @@ import { mountThemeVariants } from "../core/theme-variants.js";
|
|
|
44
46
|
import { mountToc } from "../core/toc-ui.js";
|
|
45
47
|
import { mountVideos } from "../core/video-ui.js";
|
|
46
48
|
import { parseWorkspaceSource, workspaceLabel } from "../core/workspace.js";
|
|
49
|
+
import { createLocalPluginStorage } from "../plugins/local-storage.js";
|
|
47
50
|
import { readViewerAsset, prebuilt as usePrebuiltAssets } from "./assets.js";
|
|
48
51
|
import { mountBundleNetwork } from "./bundle-network.js";
|
|
49
52
|
import { codeFileResponse } from "./code-file.js";
|
|
50
53
|
import { isLocalToolRequest, runCodeTool, validateCodeTools, validateTextfileCode, } from "./editor-tools.js";
|
|
54
|
+
import { createExtensionManager, EXTENSIONS_DIR } from "./extensions.js";
|
|
51
55
|
import { fileReferences } from "./file-references.js";
|
|
52
56
|
import { mountFontPickers } from "./font-picker.js";
|
|
53
57
|
import { createFontSettings } from "./font-settings.js";
|
|
@@ -399,6 +403,7 @@ const DOCS_ICON = icon('<path d="M10 5.2c-1.2-1-2.9-1.5-4.7-1.5-.7 0-1.3.6-1.3 1
|
|
|
399
403
|
const CHEVRON_ICON = icon('<path d="M8 5.5 12.5 10 8 14.5"/>', 14);
|
|
400
404
|
const GLOBE_ICON = icon('<circle cx="10" cy="10" r="7"/><ellipse cx="10" cy="10" rx="3" ry="7"/><path d="M3 10h14"/>');
|
|
401
405
|
const BOX_ICON = icon('<path d="M3.5 6.5 10 3l6.5 3.5v7L10 17l-6.5-3.5v-7Z"/><path d="M3.5 6.5 10 10l6.5-3.5M10 10v7"/>');
|
|
406
|
+
const STATICS_ICON = icon('<path d="M3 3v14h14M7 13V9m4 4V5m4 8V7"/>');
|
|
402
407
|
const GIT_ICON = icon('<path d="M5.5 6.5v7M14.5 6.5v1a4 4 0 0 1-4 4h-5"/><circle cx="5.5" cy="4.5" r="2"/><circle cx="5.5" cy="15.5" r="2"/><circle cx="14.5" cy="4.5" r="2"/>');
|
|
403
408
|
const STORAGE_ICON = icon('<path d="m3 12 2-8h10l2 8"/><rect x="3" y="12" width="14" height="5" rx="1"/><path d="M13 14.5h1"/>');
|
|
404
409
|
const workspaceName = (site) => site.workspace?.protocol === "storage" ? workspaceLabel(site.workspace) : "Files";
|
|
@@ -644,11 +649,10 @@ img.doc-icon { flex: none; width: 18px; height: 18px; object-fit: contain; }
|
|
|
644
649
|
.nav-row > a.cur .doc-icon-frame > img, .nav-doc-row:has(> a.cur) > .nav-doc-icon .doc-icon-frame > img { visibility: hidden; }
|
|
645
650
|
.nav-row > a.cur .doc-icon-frame::after, .nav-doc-row:has(> a.cur) > .nav-doc-icon .doc-icon-frame::after {
|
|
646
651
|
content: ""; position: absolute; inset: 0; background: currentColor; mask: var(--doc-icon) center / contain no-repeat; }
|
|
647
|
-
.nav-count { margin-left: auto; padding-left: 8px; color: var(--c-fg-faint); font-size: 11px; }
|
|
648
652
|
body.sidebar-closed #sidebar-content { display: block; overflow-y: auto; scrollbar-width: none; padding: 6px 0; }
|
|
649
653
|
body.sidebar-closed #nav-groups { display: block; }
|
|
650
654
|
body.sidebar-closed #nav-docs { scrollbar-width: none; }
|
|
651
|
-
body.sidebar-closed #nav-groups :is(.nav-label, .nav-
|
|
655
|
+
body.sidebar-closed #nav-groups :is(.nav-label, .nav-toc, .nav-tree-toggle, .nav-tree-spacer, .nav-placeholder, .new, .nav-delete, .files-upload-status, .files-drop-hint, .extensions-drop-hint, input, .nav-icon-clear),
|
|
652
656
|
body.sidebar-closed #nav-status { display: none; }
|
|
653
657
|
body.sidebar-closed #nav-groups .nav-tree { display: block; padding: 0; margin: 0; }
|
|
654
658
|
body.sidebar-closed #nav-groups .nav-row { position: relative; padding: 0; gap: 0; min-height: 36px; }
|
|
@@ -679,25 +683,19 @@ body.sidebar-closed :is(#tools-resize, .workspace-location) { display: none; }
|
|
|
679
683
|
.nav-resources .nav-row > .nav-link { height: 100%; padding: 8px 10px; gap: 8px; }
|
|
680
684
|
.nav-resources .nav-row { cursor: default; }
|
|
681
685
|
.nav-resources .nav-link::before { display: none; }
|
|
682
|
-
.
|
|
683
|
-
.files-upload {
|
|
684
|
-
|
|
685
|
-
.files-upload
|
|
686
|
-
.files-
|
|
687
|
-
.files-upload > span { display: none; font: 10px var(--doc-mono); }
|
|
688
|
-
.files-upload[data-state] > svg { display: none; }
|
|
689
|
-
.files-upload[data-state] > span { display: block; }
|
|
690
|
-
.files-upload[data-state="error"] { color: var(--c-danger); }
|
|
691
|
-
.files-upload[data-state="done"] { color: var(--doc-ok); }
|
|
692
|
-
.files-drop-hint { position: absolute; inset: 0; display: none; align-items: center;
|
|
686
|
+
.files-upload-status { flex: none; margin-right: 10px; color: var(--c-fg-muted); font: 10px var(--doc-mono); }
|
|
687
|
+
.files-upload-status:empty { display: none; }
|
|
688
|
+
.files-upload-status[data-state="error"] { color: var(--c-danger); }
|
|
689
|
+
.files-upload-status[data-state="done"] { color: var(--doc-ok); }
|
|
690
|
+
.files-drop-hint, .extensions-drop-hint { position: absolute; inset: 0; display: none; align-items: center;
|
|
693
691
|
justify-content: center; gap: 6px; padding: 6px; pointer-events: none; text-align: center; }
|
|
694
692
|
.files-drop-hint > svg { width: 28px; height: 28px; }
|
|
695
693
|
.files-drop-hint ul { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 8px;
|
|
696
694
|
margin: 0; padding: 0; list-style: none; font: 11px var(--doc-mono); color: var(--c-fg-muted); }
|
|
697
695
|
@container (max-height: 140px) { .files-drop-hint ul { display: none; } }
|
|
698
696
|
body.files-dragging #nav-files { border-color: var(--c-accent); }
|
|
699
|
-
body.files-dragging #nav-files > .nav-item { opacity: .15; }
|
|
700
|
-
body.files-dragging .files-drop-hint { display: flex; }
|
|
697
|
+
body.files-dragging :is(#nav-files, #nav-extensions) > .nav-item { opacity: .15; }
|
|
698
|
+
body.files-dragging :is(.files-drop-hint, .extensions-drop-hint) { display: flex; }
|
|
701
699
|
body.files-over #nav-files { background: color-mix(in srgb, var(--c-accent) 10%, var(--c-bg1)); }
|
|
702
700
|
#nav-status { min-height: 16px; margin: 6px 16px 0; color: var(--c-fg-faint); font-size: 11px;
|
|
703
701
|
line-height: 1.35; }
|
|
@@ -738,21 +736,10 @@ article { max-width: 860px; margin: 0 auto; font-family: var(--viewer-font, syst
|
|
|
738
736
|
[data-plugin-controls] button:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: 3px; }
|
|
739
737
|
[data-plugin-controls] button:disabled { opacity: .55; cursor: wait; }
|
|
740
738
|
.plugin-content { padding: 12px; overflow: auto; }
|
|
741
|
-
.plugin-content:has(> iframe) { padding: 0; }
|
|
742
739
|
.plugin-content:empty { display: none; }
|
|
743
|
-
.plugin .plugin-card { margin: 0; max-width: none; }
|
|
744
|
-
.plugin-card strong { font-size: 14px; font-weight: 600; }
|
|
745
|
-
.plugin-card p { margin: 6px 0 0; font-size: 13px; white-space: pre-wrap; line-height: 1.5; overflow-wrap: anywhere; }
|
|
746
|
-
.plugin-content > ul { display: grid; gap: 4px; padding-left: 20px; margin: 0; line-height: 1.5; }
|
|
747
|
-
.plugin-content > ul li::marker { color: var(--doc-accent); }
|
|
748
|
-
.plugin-content > table { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 1.6; }
|
|
749
740
|
.plugin-content :is(th, td) { padding: 6px 8px; text-align: start; border-bottom: 1px solid var(--doc-border); }
|
|
750
741
|
.plugin-content th { background: var(--doc-bg-2); color: var(--doc-fg-2); font-size: 12px; font-weight: 600; }
|
|
751
742
|
.plugin-content tr:last-child td { border-bottom: 0; }
|
|
752
|
-
.plugin-content > pre { margin: 0; max-height: 320px; padding: 10px; }
|
|
753
|
-
.plugin-content > iframe { display: block; color-scheme: inherit; }
|
|
754
|
-
[data-plugin-result][data-resized] > iframe { height: 100% !important; }
|
|
755
|
-
[data-plugin-result][data-resized] > pre { max-height: none; }
|
|
756
743
|
.plugin-resize { display: none; height: 12px; align-items: center; justify-content: center;
|
|
757
744
|
cursor: row-resize; touch-action: none; user-select: none; }
|
|
758
745
|
.plugin[data-ready="true"]:not(.collapsed) > .plugin-resize { display: flex; }
|
|
@@ -762,63 +749,60 @@ article { max-width: 860px; margin: 0 auto; font-family: var(--viewer-font, syst
|
|
|
762
749
|
body:has(.plugin-resize.drag), body:has(.plugin-resize.drag) * { cursor: row-resize !important; user-select: none !important; }
|
|
763
750
|
body:has(.plugin-resize.drag) iframe { pointer-events: none; }
|
|
764
751
|
|
|
765
|
-
.plugin
|
|
766
|
-
.plugin
|
|
767
|
-
.plugin
|
|
768
|
-
.plugin[data-
|
|
769
|
-
.plugin[data-
|
|
770
|
-
.plugin[data-
|
|
771
|
-
.plugin
|
|
752
|
+
.plugin { container-type: inline-size; background: var(--doc-bg); color: var(--doc-fg); }
|
|
753
|
+
.plugin .plugin-content { padding: 12px 14px; }
|
|
754
|
+
.plugin .plugin-header { padding: 0 16px; }
|
|
755
|
+
.plugin[data-ready="true"] .plugin-info { display: block; flex-basis: auto; }
|
|
756
|
+
.plugin[data-ready="true"] .plugin-info h2 { display: none; }
|
|
757
|
+
.plugin[data-ready="true"] .plugin-header:has([data-plugin-status]:not(:empty)) { display: flex; padding-top: 12px; }
|
|
758
|
+
.plugin [data-plugin-status] { margin: 0; }
|
|
772
759
|
.plugin[data-operation-error="true"] [data-plugin-status] { color: var(--doc-danger); }
|
|
773
|
-
.plugin-heading { order: -1; display: flex; align-items: center; gap: 8px; min-width: 0; max-width: calc(100% - 156px); font: 11px
|
|
760
|
+
.plugin-heading { order: -1; display: flex; align-items: center; gap: 8px; min-width: 0; max-width: calc(100% - 156px); font: inherit; font-size: 11px; line-height: 20px; }
|
|
774
761
|
.plugin-heading::before { content: ""; border-top: 2px solid var(--doc-border); width: 8px; flex: none; }
|
|
775
762
|
.plugin-heading a { display: flex; align-items: center; gap: 6px; color: var(--doc-fg-2); text-decoration: none; flex: none; }
|
|
776
763
|
.plugin-heading img { display: block; width: 16px; height: 16px; }
|
|
777
764
|
.plugin-heading-context { color: var(--doc-fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
778
765
|
.plugin-heading button { font: inherit; color: var(--doc-fg-2); background: transparent; border: 0; padding: 0; cursor: pointer; white-space: nowrap; }
|
|
779
|
-
.plugin-
|
|
780
|
-
.plugin-action-menu > summary { list-style: none; font-size: 20px; line-height: 16px; }
|
|
781
|
-
.plugin-action-menu > summary::-webkit-details-marker { display: none; }
|
|
782
|
-
.plugin-action-menu > div { position: absolute; z-index: 20; right: 0; top: 26px; width: min(340px, calc(100vw - 64px)); max-height: 65vh; overflow: auto; padding: 8px; background: var(--doc-bg); color: var(--doc-fg); border: 1px solid var(--doc-border-2); border-radius: 8px; box-shadow: 0 8px 24px var(--c-shadow); }
|
|
783
|
-
.plugin-action-menu > div > div > button { width: 100%; text-align: start; margin-bottom: 3px; }
|
|
784
|
-
.plugin-blocks { font: 13px/1.65 var(--doc-mono); overflow-wrap: anywhere; }
|
|
766
|
+
.plugin-blocks { font: inherit; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
785
767
|
.plugin-blocks > :first-child, .plugin-block-columns > div > :first-child { margin-top: 0; }
|
|
786
|
-
.plugin-blocks .plugin-block-heading { margin: 0 0
|
|
787
|
-
.plugin-block-text { margin:
|
|
768
|
+
.plugin-blocks .plugin-block-heading { margin: 0 0 8px; padding: 0; border: 0; font: inherit; font-size: 17px; font-weight: 600; line-height: 1.4; color: var(--doc-fg); }
|
|
769
|
+
.plugin-block-text { margin: 8px 0; white-space: pre-wrap; }
|
|
788
770
|
.plugin-block-text:has(p, pre, ul, ol) { white-space: normal; }
|
|
789
|
-
.plugin-blocks p { margin:
|
|
771
|
+
.plugin-blocks p { margin: 4px 0; white-space: pre-wrap; }
|
|
772
|
+
.plugin-blocks :is(ul, ol) { margin: 6px 0; padding-left: 22px; }
|
|
773
|
+
.plugin-blocks > :last-child { margin-bottom: 0; }
|
|
790
774
|
.plugin-blocks strong { font-weight: 600; }
|
|
791
775
|
.plugin-blocks a { color: var(--doc-accent); text-underline-offset: 3px; }
|
|
792
776
|
.plugin-blocks code { font-family: var(--doc-mono); }
|
|
793
|
-
.plugin-block-context { display: flex; align-items: center; flex-wrap: wrap; gap:
|
|
794
|
-
.plugin-block-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap:
|
|
777
|
+
.plugin-block-context { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 10px; margin: 6px 0; color: var(--doc-fg-3); font-size: 11px; }
|
|
778
|
+
.plugin-block-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 6px 16px; margin: 8px 0; }
|
|
795
779
|
.plugin-block-fields > div { min-width: 0; }
|
|
796
|
-
.plugin-block-fields dt { color: var(--doc-fg-3); font-size:
|
|
797
|
-
.plugin-block-fields dd { margin: 0; min-height:
|
|
798
|
-
.plugin-block-columns { display: grid; grid-template-columns: minmax(0, 1fr)
|
|
780
|
+
.plugin-block-fields dt { color: var(--doc-fg-3); font-size: 10px; margin-bottom: 1px; }
|
|
781
|
+
.plugin-block-fields dd { margin: 0; min-height: 20px; font-size: 12px; }
|
|
782
|
+
.plugin-block-columns { display: grid; grid-template-columns: minmax(0, 1fr) 180px; gap: 16px; }
|
|
799
783
|
.plugin-block-columns > div, .plugin-block-columns aside { min-width: 0; }
|
|
800
|
-
.plugin-block-columns aside { border-left: 1px solid var(--doc-border); padding-left:
|
|
784
|
+
.plugin-block-columns aside { border-left: 1px solid var(--doc-border); padding-left: 12px; }
|
|
801
785
|
.plugin-block-columns aside .plugin-block-fields { grid-template-columns: 1fr; margin-top: 0; }
|
|
802
|
-
.plugin-block-section { border-top: 1px solid var(--doc-border); padding-top:
|
|
786
|
+
.plugin-block-section { border-top: 1px solid var(--doc-border); padding-top: 6px; margin: 10px 0 0; min-width: 0; }
|
|
803
787
|
.plugin-block-section > summary { cursor: pointer; font-size: 12px; font-weight: 600; color: var(--doc-fg-2); padding: 4px 0; }
|
|
804
788
|
.plugin-block-note { color: var(--doc-warn); font-size: 11px; border-left: 2px solid currentColor; padding-left: 10px; }
|
|
805
789
|
.plugin-block-table { overflow: auto; width: 100%; margin: 8px 0; }
|
|
806
790
|
.plugin-block-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
807
|
-
.plugin-block-table :is(th, td) { padding: 8px
|
|
791
|
+
.plugin-block-table :is(th, td) { padding: 5px 8px; min-width: 72px; max-width: 320px; vertical-align: top; }
|
|
808
792
|
.plugin-blocks .plugin-block-code { font: 12px/1.65 var(--doc-mono); padding: 12px; margin: 10px 0; border: 1px solid var(--doc-border); border-radius: 6px; max-height: 400px; overflow: auto; white-space: pre-wrap; }
|
|
809
793
|
.plugin-block-diff code > span { display: inline-block; min-width: 100%; }
|
|
810
794
|
.plugin-block-diff [data-tone="success"] { color: var(--doc-ok); background: color-mix(in srgb, var(--doc-ok) 8%, transparent); }
|
|
811
795
|
.plugin-block-diff [data-tone="danger"] { color: var(--doc-danger); background: color-mix(in srgb, var(--doc-danger) 8%, transparent); }
|
|
812
|
-
.plugin-block-message { padding:
|
|
796
|
+
.plugin-block-message { padding: 8px 0; border-bottom: 1px solid var(--doc-border); }
|
|
813
797
|
.plugin-block-message:last-child { border-bottom: 0; }
|
|
814
798
|
.plugin-message-byline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
815
799
|
.plugin-message-byline > span:not(.plugin-avatar, .plugin-message-actions) { color: var(--doc-fg-3); font-size: 10px; }
|
|
816
|
-
.plugin-avatar { display: grid; place-items: center; width:
|
|
800
|
+
.plugin-avatar { display: grid; place-items: center; width: 24px; height: 24px; flex: none; border-radius: 7px; color: var(--doc-accent); background: color-mix(in srgb, var(--doc-accent) 12%, var(--doc-bg)); font-size: 10px; }
|
|
817
801
|
.plugin-task { display: flex; align-items: baseline; gap: 8px; list-style: none; }
|
|
818
802
|
.plugin-message-actions svg { display: block; width: 14px; height: 14px; }
|
|
819
803
|
.plugin-message-actions .plugin-block-button { padding: 4px; border: 0; color: var(--doc-fg-3); }
|
|
820
804
|
.plugin-message-actions { margin-left: auto; display: flex; gap: 4px; }
|
|
821
|
-
.plugin-message-body, .plugin-reactions { margin:
|
|
805
|
+
.plugin-message-body, .plugin-reactions { margin: 4px 0 0 32px; }
|
|
822
806
|
.plugin-reactions { display: flex; flex-wrap: wrap; gap: 5px; font-size: 11px; }
|
|
823
807
|
.plugin-reactions button { font: inherit; padding: 2px 8px; border: 1px solid var(--doc-border); border-radius: 12px; background: transparent; color: var(--doc-fg-2); cursor: pointer; }
|
|
824
808
|
.plugin-reactions [aria-pressed="true"] { border-color: var(--doc-accent); color: var(--doc-accent); background: color-mix(in srgb, var(--doc-accent) 8%, transparent); }
|
|
@@ -832,19 +816,18 @@ body:has(.plugin-resize.drag) iframe { pointer-events: none; }
|
|
|
832
816
|
.plugin-copy svg { width: 14px; height: 14px; }
|
|
833
817
|
.plugin-block-progress { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--doc-fg-3); margin: 12px 0; }
|
|
834
818
|
.plugin-block-progress progress { flex: 1; width: 0; height: 6px; accent-color: var(--doc-accent); }
|
|
835
|
-
.plugin-block-actions { display: flex; flex-wrap: wrap; gap: 6px; margin:
|
|
819
|
+
.plugin-block-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
|
|
836
820
|
.plugin-block-actions:has([data-block-composer]) { display: block; }
|
|
837
821
|
.plugin-block-button { font: inherit; font-size: 12px; padding: 5px 10px; border: 1px solid var(--doc-border); border-radius: 5px; color: var(--doc-fg); background: transparent; cursor: pointer; }
|
|
838
|
-
.plugin-block-button:hover
|
|
822
|
+
.plugin-block-button:hover { background: var(--doc-bg-2); }
|
|
839
823
|
.plugin-block-button:disabled { opacity: .5; cursor: wait; }
|
|
824
|
+
.plugin-action[aria-pressed="true"] { color: var(--doc-accent); background: var(--doc-bg-2); }
|
|
840
825
|
.plugin-danger { color: var(--doc-danger); }
|
|
841
|
-
.plugin-edit-
|
|
842
|
-
.plugin-
|
|
843
|
-
.plugin-editor { display: grid; gap:
|
|
844
|
-
.plugin-editor
|
|
845
|
-
.plugin-editor
|
|
846
|
-
.plugin-editor textarea { resize: vertical; min-height: 96px; max-height: 60vh; }
|
|
847
|
-
.plugin-editor-controls { display: flex; justify-content: flex-end; gap: 6px; flex-wrap: wrap; }
|
|
826
|
+
.plugin [data-plugin-edit-trigger]:focus-visible, .plugin :is(button, summary, input, select, textarea):focus-visible, .plugin-block-table:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: 2px; }
|
|
827
|
+
.plugin-editor { display: grid; gap: 6px; margin: 0; padding: 0; border: 0; font: inherit; font-size: 12px; line-height: 1.5; color: var(--doc-fg); background: var(--doc-bg); min-width: 0; }
|
|
828
|
+
.plugin-editor label { display: grid; gap: 0; font-size: 0; }
|
|
829
|
+
.plugin-editor :is(input, textarea, select) { box-sizing: border-box; width: 100%; min-width: 0; max-width: 100%; padding: 3px 6px; border: 1px solid var(--doc-border); border-radius: 4px; color: var(--doc-fg); background: var(--doc-bg); font: inherit; font-size: 12px; line-height: 1.5; }
|
|
830
|
+
.plugin-editor textarea { resize: vertical; min-height: 54px; max-height: 40vh; }
|
|
848
831
|
.plugin-editor .plugin-submit { background: var(--doc-accent); color: var(--doc-accent-fg); border-color: transparent; }
|
|
849
832
|
.plugin-editor-notice { color: var(--doc-danger); font-size: 11px; }
|
|
850
833
|
.plugin-editor-notice:empty { display: none; }
|
|
@@ -852,9 +835,9 @@ body:has(.plugin-resize.drag) iframe { pointer-events: none; }
|
|
|
852
835
|
@container (max-width: 600px) {
|
|
853
836
|
.plugin-block-columns { grid-template-columns: minmax(0, 1fr); gap: 14px; }
|
|
854
837
|
.plugin-block-columns aside { border-left: 0; border-top: 1px solid var(--doc-border); padding: 14px 0 0; }
|
|
855
|
-
.plugin-block-
|
|
838
|
+
.plugin-block-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
856
839
|
.plugin-heading-context { display: none; }
|
|
857
|
-
.plugin-blocks .plugin-block-heading { font-size:
|
|
840
|
+
.plugin-blocks .plugin-block-heading { font-size: 16px; }
|
|
858
841
|
.plugin-message-body, .plugin-reactions { margin-left: 0; }
|
|
859
842
|
}
|
|
860
843
|
|
|
@@ -1075,6 +1058,40 @@ details summary { cursor: pointer; opacity: .8; }
|
|
|
1075
1058
|
.terms .term-description::before { content: ""; position: absolute; left: 0; right: 0; top: -9px; height: 9px; }
|
|
1076
1059
|
.terms .term-name:hover .term-description,
|
|
1077
1060
|
.terms .term-name:focus .term-description { display: block; }
|
|
1061
|
+
|
|
1062
|
+
.extensions-header { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin: 12px 0 6px; }
|
|
1063
|
+
.extensions-header h1 { margin: 0 auto 0 0; font-size: 18px; }
|
|
1064
|
+
.extensions-description, .extension-info small { color: var(--c-fg-muted); font-size: 12px; }
|
|
1065
|
+
.extensions-description { margin: 0 0 12px; }
|
|
1066
|
+
.extensions-notice { color: var(--c-danger); white-space: pre-wrap; font-size: 12px; }
|
|
1067
|
+
.extensions-notice:empty { display: none; }
|
|
1068
|
+
.extension-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 0; border-top: 1px solid var(--c-line); }
|
|
1069
|
+
.extension-info { flex: 1; min-width: 180px; overflow-wrap: anywhere; }
|
|
1070
|
+
.extension-info strong { font-size: 13px; font-weight: 500; }
|
|
1071
|
+
.extension-version { margin-left: 8px; color: var(--c-fg-muted); font-size: 11px; }
|
|
1072
|
+
.extension-info small { display: block; margin-top: 3px; }
|
|
1073
|
+
.extension-accounts { width: 100%; display: grid; gap: 4px; font-size: 12px; color: var(--c-fg-muted); }
|
|
1074
|
+
.extension-accounts .extension-actions { align-items: center; }
|
|
1075
|
+
.extension-actions { display: flex; gap: 6px; }
|
|
1076
|
+
.extension-button, .extension-editor select { padding: 4px 8px; border: 1px solid var(--c-line); border-radius: 4px; color: var(--c-fg); background: var(--c-bg1); font: inherit; font-size: 12px; cursor: pointer; }
|
|
1077
|
+
.extension-button:hover { background: var(--c-raised); }
|
|
1078
|
+
.extension-button:disabled { opacity: .5; cursor: default; }
|
|
1079
|
+
.extension-editor { border-top: 1px solid var(--c-line); margin-top: 12px; padding-top: 4px; }
|
|
1080
|
+
.extension-editor .extensions-header { font-size: 12px; }
|
|
1081
|
+
.extension-editor select { min-width: 0; max-width: 100%; margin-left: auto; }
|
|
1082
|
+
.extension-editor textarea { display: block; box-sizing: border-box; width: 100%; min-height: 320px; margin: 8px 0; padding: 10px; border: 1px solid var(--c-line); border-radius: 4px; background: var(--c-bg1); color: var(--c-fg); font: 12px/1.6 var(--font-mono); tab-size: 2; resize: vertical; }
|
|
1083
|
+
.extension-icon { position: relative; flex: none; display: grid; place-items: center; width: 40px; height: 40px; color: var(--c-fg-muted); }
|
|
1084
|
+
.extension-icon > :is(svg, img) { width: 32px; height: 32px; object-fit: contain; }
|
|
1085
|
+
.extension-icon > img { position: absolute; inset: 4px; }
|
|
1086
|
+
.extension-icon:has(img) > svg { visibility: hidden; }
|
|
1087
|
+
.extensions-dropzone { position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 28px 16px; margin: 16px 0; border: 1px dashed var(--c-fg-faint); border-radius: 8px; color: var(--c-fg-muted); font-size: 12px; cursor: pointer; }
|
|
1088
|
+
.extensions-dropzone strong { color: var(--c-fg); font-size: 14px; font-weight: 500; }
|
|
1089
|
+
.extensions-dropzone input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
|
1090
|
+
.extensions-dropzone:focus-within { outline: 2px solid var(--c-accent); outline-offset: 2px; }
|
|
1091
|
+
.extensions-dropzone:has(input:disabled) { opacity: .5; }
|
|
1092
|
+
body.extensions-over #nav-extensions, body.extensions-over .extensions-dropzone { outline: 1px solid var(--c-accent); outline-offset: -1px; background: var(--c-raised); }
|
|
1093
|
+
|
|
1094
|
+
|
|
1078
1095
|
.resource-library { width: 100%; max-width: none; }
|
|
1079
1096
|
.git-page { padding-top: 8px; font: 12px/1.55 var(--font-sans); }
|
|
1080
1097
|
.git-controls { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
@@ -1572,7 +1589,6 @@ const navFilesHtml = (site, currentRes) => {
|
|
|
1572
1589
|
const statics = currentRes?.startsWith("_statics") ||
|
|
1573
1590
|
["dictionary", "code", "urls"].includes(currentRes ?? "");
|
|
1574
1591
|
const folderIcon = icon('<path d="M2 5h6l2 2h8v10H2Z"/>');
|
|
1575
|
-
const upload = `<button class="files-upload" type="button" title="Upload files" data-i18n-title="Upload files" aria-label="Upload files" data-i18n-aria-label="Upload files">${UPLOAD_ICON}<span></span></button><input id="files-upload-input" type="file" multiple hidden>`;
|
|
1576
1592
|
const hint = `<div class="files-drop-hint" aria-hidden="true">${UPLOAD_ICON}<span data-i18n="Upload files">Upload files</span></div>`;
|
|
1577
1593
|
const location = source?.protocol === "git"
|
|
1578
1594
|
? `<div class="workspace-location" data-workspace-protocol="git" title="${escapeHtml(`${source.repo ?? "repo"}[${source.branch}]`)}">${GIT_ICON}<span class="workspace-address"><span class="git-repo-name">${escapeHtml(source.repo ?? "repo")}</span><span class="git-branch-caption">[${escapeHtml(source.branch)}]</span></span></div>`
|
|
@@ -1580,7 +1596,8 @@ const navFilesHtml = (site, currentRes) => {
|
|
|
1580
1596
|
const git = source?.protocol === "git"
|
|
1581
1597
|
? `<div class="nav-resources" id="nav-git"><div class="nav-item" data-node-id="_git"><div class="nav-row"><a class="nav-link${currentRes === "_git" ? " cur" : ""}" href="/_git" title="Git controls" data-i18n-title="Git controls">${GIT_ICON}<span class="nav-label">Git</span></a></div></div></div>`
|
|
1582
1598
|
: "";
|
|
1583
|
-
|
|
1599
|
+
const extensions = `<div class="nav-resources" id="nav-extensions" data-scope="${createHash("sha256").update(root).digest("hex")}"><div class="nav-item" data-node-id="_extensions"><div class="nav-row"><a class="nav-link${currentRes === "_extensions" ? " cur" : ""}" href="/_extensions" title="Plugins" data-i18n-title="Plugins">${PLUGINS_ICON}<span class="nav-label" data-i18n="Plugins">Plugins</span></a></div></div><div class="extensions-drop-hint" aria-hidden="true">${PLUGINS_ICON}<span data-i18n="Add plugins">Add plugins</span></div></div>`;
|
|
1600
|
+
return `<section id="nav-tools" aria-label="Workspace tools" data-i18n-aria-label="Workspace tools"><div id="tools-resize" role="separator" tabindex="0" aria-orientation="horizontal" aria-controls="nav-tools" aria-label="Resize workspace tools" data-i18n-aria-label="Resize workspace tools"></div>${location}<div class="nav-tool-cards"><div class="nav-resources" id="nav-files"><div class="nav-item" data-node-id="_files"><div class="nav-row"><a class="nav-link${currentRes !== undefined && currentRes !== "_git" && currentRes !== "_extensions" && !statics ? " cur" : ""}" href="/_files" title="Files" data-i18n-title="Files">${folderIcon}<span class="nav-label" data-i18n="Files">Files</span></a><span class="files-upload-status" role="status"></span></div></div>${hint}</div><div class="nav-resources" id="nav-statics"><div class="nav-item" data-node-id="_statics"><div class="nav-row"><a class="nav-link${statics ? " cur" : ""}" href="/_statics" title="Statics" data-i18n-title="Statics">${STATICS_ICON}<span class="nav-label" data-i18n="Statics">Statics</span></a></div></div></div>${git}${extensions}</div></section>`;
|
|
1584
1601
|
};
|
|
1585
1602
|
const formatSize = (bytes) => bytes < 1024
|
|
1586
1603
|
? `${bytes} B`
|
|
@@ -1674,6 +1691,11 @@ export const textfilePreviewErrors = (site, file, source) => {
|
|
|
1674
1691
|
return errors;
|
|
1675
1692
|
};
|
|
1676
1693
|
export const resourcesMain = (site, rest) => {
|
|
1694
|
+
if (rest === "_extensions")
|
|
1695
|
+
return {
|
|
1696
|
+
title: "Plugins",
|
|
1697
|
+
main: '<article class="res-page resource-library"><div id="extensions-manager"></div></article>',
|
|
1698
|
+
};
|
|
1677
1699
|
if (rest === "_git")
|
|
1678
1700
|
return site.workspace?.protocol === "git"
|
|
1679
1701
|
? {
|
|
@@ -1932,7 +1954,7 @@ ${mode === "edit"
|
|
|
1932
1954
|
if (window.__docsRefreshNav) return window.__docsRefreshNav();
|
|
1933
1955
|
const cur = document.querySelector("#nav-groups .nav-row a.cur");
|
|
1934
1956
|
const id = (cur && cur.closest(".nav-item").dataset.docId) || "";
|
|
1935
|
-
const rp = location.pathname.startsWith("/_statics") ? location.pathname.slice(1) :
|
|
1957
|
+
const rp = location.pathname.startsWith("/_statics") ? location.pathname.slice(1) : ["/_git", "/_extensions"].includes(location.pathname) ? location.pathname.slice(1) : location.pathname.startsWith("/_files") ? location.pathname.slice(8).replace(/^\\/|\\/$/g, "") : null;
|
|
1936
1958
|
const res = await localeFetch("/api/nav?current=" + encodeURIComponent(id) + (rp == null ? "" : "&res=" + encodeURIComponent(rp)));
|
|
1937
1959
|
if (res.ok) document.getElementById("nav-groups").innerHTML = (await res.json()).html;
|
|
1938
1960
|
if (window.__docsNavRestore) window.__docsNavRestore();
|
|
@@ -2391,7 +2413,8 @@ export const pageDataResponse = (site, path) => {
|
|
|
2391
2413
|
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
2392
2414
|
return { status: 400, body: { error: "route must be /<docId>" } };
|
|
2393
2415
|
}
|
|
2394
|
-
if (path === "/
|
|
2416
|
+
if (path === "/_extensions" ||
|
|
2417
|
+
path === "/_git" ||
|
|
2395
2418
|
path === "/_statics" ||
|
|
2396
2419
|
path.startsWith("/_statics/") ||
|
|
2397
2420
|
path === "/_files" ||
|
|
@@ -2399,8 +2422,8 @@ export const pageDataResponse = (site, path) => {
|
|
|
2399
2422
|
try {
|
|
2400
2423
|
const resource = resourcesMain(site, path.startsWith("/_statics")
|
|
2401
2424
|
? path.slice(1)
|
|
2402
|
-
:
|
|
2403
|
-
?
|
|
2425
|
+
: ["/_git", "/_extensions"].includes(path)
|
|
2426
|
+
? path.slice(1)
|
|
2404
2427
|
: path.slice("/_files".length).replace(/^\/|\/$/g, ""));
|
|
2405
2428
|
return resource
|
|
2406
2429
|
? {
|
|
@@ -2734,14 +2757,18 @@ export const bundleClient = async (entry, publish = false) => {
|
|
|
2734
2757
|
});
|
|
2735
2758
|
return out.outputFiles?.[0]?.text ?? "";
|
|
2736
2759
|
};
|
|
2737
|
-
export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
2760
|
+
export const startServer = async (mode, port = 5179, pluginModule, startup) => {
|
|
2738
2761
|
// Language adapters are read once; the manifest is re-read by the compiler.
|
|
2739
2762
|
siteLanguages = await loadSiteLanguages(root);
|
|
2740
2763
|
const viewerPlugins = await loadViewerPlugins(pluginModule);
|
|
2741
2764
|
workspaceSource = viewerPlugins.workspace;
|
|
2742
2765
|
let gitOrigin = viewerPlugins.gitOrigin;
|
|
2743
2766
|
const handleGit = createGitHandler(root, () => gitOrigin);
|
|
2744
|
-
const
|
|
2767
|
+
const personal = createLocalPluginStorage();
|
|
2768
|
+
const handlePlugins = createPluginHandler(viewerPlugins.plugins, viewerPlugins, personal.credentials);
|
|
2769
|
+
const handleExtensions = createExtensionManager(root, handlePlugins, viewerPlugins, personal);
|
|
2770
|
+
let loginPlugins = startup?.login ? [startup.login] : [];
|
|
2771
|
+
process.once("exit", handleExtensions.dispose);
|
|
2745
2772
|
// Client bundles (bundleClient) = edit's editing UI and the schema graph interaction layer
|
|
2746
2773
|
// shared by preview/edit. React Flow's stylesheet isn't mixed into the bundle — the mount
|
|
2747
2774
|
// layer (flow-css.ts) injects it into a <style> at runtime, so the page needs no CSS delivery
|
|
@@ -2776,6 +2803,21 @@ export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
|
2776
2803
|
};
|
|
2777
2804
|
viewer.setAllowedDomains(viewerPlugins.allowedDomains ?? []);
|
|
2778
2805
|
await viewerPlugins.setup?.(viewer);
|
|
2806
|
+
const extensionErrors = await handleExtensions
|
|
2807
|
+
.restore()
|
|
2808
|
+
.catch((error) => [error instanceof Error ? error.message : String(error)]);
|
|
2809
|
+
for (const error of extensionErrors)
|
|
2810
|
+
console.error(`Plugin: ${error}`);
|
|
2811
|
+
if (startup?.file && startup.data) {
|
|
2812
|
+
const result = (await handleExtensions({
|
|
2813
|
+
action: "install",
|
|
2814
|
+
file: startup.file,
|
|
2815
|
+
data: startup.data,
|
|
2816
|
+
base: null,
|
|
2817
|
+
}));
|
|
2818
|
+
loginPlugins = result.items.find((item) => item.file === startup.file)?.plugins ?? [];
|
|
2819
|
+
console.log(`Installed ${startup.file} in ${join(root, EXTENSIONS_DIR)}`);
|
|
2820
|
+
}
|
|
2779
2821
|
// DOCS_HMR=0 = no hot reload (nothing is watched = changes need a manual reload).
|
|
2780
2822
|
if (process.env.DOCS_HMR !== "0") {
|
|
2781
2823
|
const watcher = watchProject(root, pkgRoot, (kind, files) => {
|
|
@@ -2851,6 +2893,32 @@ export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
|
2851
2893
|
return;
|
|
2852
2894
|
}
|
|
2853
2895
|
if (mode === "edit" && req.method === "POST") {
|
|
2896
|
+
if (path === "/api/extensions") {
|
|
2897
|
+
res.setHeader("cache-control", "no-store");
|
|
2898
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"]) ||
|
|
2899
|
+
![
|
|
2900
|
+
`localhost:${req.socket.localPort}`,
|
|
2901
|
+
`127.0.0.1:${req.socket.localPort}`,
|
|
2902
|
+
`[::1]:${req.socket.localPort}`,
|
|
2903
|
+
].includes(req.headers.host ?? "")) {
|
|
2904
|
+
sendJson(res, 403, {
|
|
2905
|
+
error: "Plugins require a same-origin local editor request",
|
|
2906
|
+
});
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
try {
|
|
2910
|
+
const input = await readJson(req, MAX_EXTENSION_BYTES * 6 + 4096);
|
|
2911
|
+
if (!input)
|
|
2912
|
+
throw new Error("Expected a plugin request");
|
|
2913
|
+
sendJson(res, 200, await handleExtensions(input));
|
|
2914
|
+
}
|
|
2915
|
+
catch (error) {
|
|
2916
|
+
sendJson(res, 409, {
|
|
2917
|
+
error: error instanceof Error ? error.message : "Plugin operation failed",
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
return;
|
|
2921
|
+
}
|
|
2854
2922
|
if (path === "/api/git") {
|
|
2855
2923
|
res.setHeader("cache-control", "no-store");
|
|
2856
2924
|
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"]) ||
|
|
@@ -3143,7 +3211,8 @@ export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
|
3143
3211
|
return;
|
|
3144
3212
|
}
|
|
3145
3213
|
if ((mode === "edit" &&
|
|
3146
|
-
(path === "/
|
|
3214
|
+
(path === "/_extensions" ||
|
|
3215
|
+
path === "/_git" ||
|
|
3147
3216
|
path === "/_statics" ||
|
|
3148
3217
|
path.startsWith("/_statics/") ||
|
|
3149
3218
|
path === "/_files" ||
|
|
@@ -3151,8 +3220,8 @@ export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
|
3151
3220
|
path === "/_files/terms") {
|
|
3152
3221
|
const rest = path.startsWith("/_statics")
|
|
3153
3222
|
? path.slice(1)
|
|
3154
|
-
:
|
|
3155
|
-
?
|
|
3223
|
+
: ["/_git", "/_extensions"].includes(path)
|
|
3224
|
+
? path.slice(1)
|
|
3156
3225
|
: requestUrl.pathname.slice("/_files".length).replace(/^\/|\/$/g, "");
|
|
3157
3226
|
const page = resourcesMain(site, rest);
|
|
3158
3227
|
if (!page) {
|
|
@@ -3216,6 +3285,16 @@ export const startServer = async (mode, port = 5179, pluginModule) => {
|
|
|
3216
3285
|
const addr = server.address();
|
|
3217
3286
|
const p = typeof addr === "object" && addr ? addr.port : port;
|
|
3218
3287
|
console.log(`dooc ${mode} (${root}): http://localhost:${p}/`);
|
|
3288
|
+
for (const plugin of loginPlugins) {
|
|
3289
|
+
try {
|
|
3290
|
+
console.log(`Log in to ${plugin}: ${handlePlugins.loginLink(plugin, `http://localhost:${p}`)}`);
|
|
3291
|
+
}
|
|
3292
|
+
catch (error) {
|
|
3293
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
if (loginPlugins.length)
|
|
3297
|
+
console.log("Keep this command running while signing in. Login links expire in 10 minutes.");
|
|
3219
3298
|
});
|
|
3220
3299
|
const listen = (p, left) => {
|
|
3221
3300
|
server.once("error", (err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dooc-dev/dooc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "View and edit source files with dooc.yaml configuration, and share them as source ZIPs or standalone HTML.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
"test:install": "node scripts/test-install.mjs",
|
|
66
66
|
"check": "biome check . && tsc --noEmit && pnpm run build && vitest run && pnpm run test:install && pnpm run test:plugin-package",
|
|
67
67
|
"fix": "biome check --write .",
|
|
68
|
-
"build:fonts": "node -e \"require('node:fs').cpSync('src/viewer/fonts', 'dist/viewer/fonts', {recursive:true})\""
|
|
68
|
+
"build:fonts": "node -e \"require('node:fs').cpSync('src/viewer/fonts', 'dist/viewer/fonts', {recursive:true})\"",
|
|
69
|
+
"build:sample-extension": "node --import tsx scripts/build-sample-extension.mjs"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@biomejs/biome": "2.4.15",
|