@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,3312 @@
|
|
|
1
|
+
import { filesSnapshot, updateFiles, uploadFile } from "../compiler/files.js";
|
|
2
|
+
import { loadProjectFonts } from "../compiler/fonts.js";
|
|
3
|
+
import { textfilePath, writeTextfile } from "../compiler/textfile.js";
|
|
4
|
+
import { defaultFilePath, docIconFile, MAX_FILE_UPLOAD_BYTES, parseFileLayout, docFile as sourceDocFile, suggestDocFileName, } from "../core/files.js";
|
|
5
|
+
// The local server itself (shared by preview / edit). Files are re-read on every request, so a
|
|
6
|
+
// change is always reflected on the next request. HMR (hmr.ts) triggers that "next request"
|
|
7
|
+
// automatically: content changes are pushed over SSE and preview swaps just the main body, while
|
|
8
|
+
// src/*.ts changes cause a tsx watch restart → reconnect → reload (= no manual reload or restart
|
|
9
|
+
// needed). Rendering goes through the same "mdast → Block AST → HTML" pipeline as the build.
|
|
10
|
+
// Dialect validation errors are shown at the top of the page.
|
|
11
|
+
// preview = read-only view
|
|
12
|
+
// edit = left = live render of the body / right = front-matter form + insert palette +
|
|
13
|
+
// CodeMirror (`dooc edit`). The save API rewrites content/<id>.md directly,
|
|
14
|
+
// and hierarchy changes (create / move / rename / delete) rewrite
|
|
15
|
+
// dooc.yaml.
|
|
16
|
+
// The client (src/viewer/edit-client.ts) is bundled in memory with esbuild on
|
|
17
|
+
// first request (cached) and served.
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
+
import { createServer } from "node:http";
|
|
21
|
+
import { dirname, join, resolve } from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { parseContentSource } from "../compiler/code-blocks.js";
|
|
24
|
+
import { compileDraft, createCompiler, } from "../compiler/compile.js";
|
|
25
|
+
import { figureName } from "../compiler/figures.js";
|
|
26
|
+
import { ICONS_DIR, squareSvg } from "../compiler/icons.js";
|
|
27
|
+
import { contentLanguageCode, contentLanguages, isContentLanguage, webLanguage, } from "../compiler/language-codes.js";
|
|
28
|
+
import { setDocTranslation, siteContentLanguages, translationFile, translationFiles, } from "../compiler/locales.js";
|
|
29
|
+
import { readManifest, writeManifest } from "../compiler/manifest.js";
|
|
30
|
+
import { assembleDoc, createDocOps, isOpError } from "../compiler/ops.js";
|
|
31
|
+
import { walkBlocks } from "../core/blocks.js";
|
|
32
|
+
import { docIdOfFile } from "../core/content-metadata.js";
|
|
33
|
+
import { MAX_EXTENSION_BYTES } from "../core/extension-package.js";
|
|
34
|
+
import { figureFrameHtml, mountFigureFrames } from "../core/figure-frame.js";
|
|
35
|
+
import { blocksHtml, docHtml, escapeHtml, termsHtml } from "../core/html.js";
|
|
36
|
+
import { PLUGINS_ICON } from "../core/icons.js";
|
|
37
|
+
import { DEFAULT_LANG, isLangTag, resolveLanguage, } from "../core/language.js";
|
|
38
|
+
import { defaultPreviewSettings, parsePreviewSettings, } from "../core/previewers.js";
|
|
39
|
+
import { ancestorsOf, childrenOf, docPath, resolveDocPath } from "../core/routing.js";
|
|
40
|
+
import { mountTables } from "../core/table-ui.js";
|
|
41
|
+
import { CODE_LANGS, docDisplayName } from "../core/taxonomy.js";
|
|
42
|
+
import { isTermFile, termAnchor, termFileUrl } from "../core/term-reference.js";
|
|
43
|
+
import { mountTerms } from "../core/terms-ui.js";
|
|
44
|
+
import { isRemoteTextfile, textfileName } from "../core/textfile.js";
|
|
45
|
+
import { mountThemeVariants } from "../core/theme-variants.js";
|
|
46
|
+
import { mountToc } from "../core/toc-ui.js";
|
|
47
|
+
import { mountVideos } from "../core/video-ui.js";
|
|
48
|
+
import { parseWorkspaceSource, workspaceLabel } from "../core/workspace.js";
|
|
49
|
+
import { createLocalPluginStorage } from "../plugins/local-storage.js";
|
|
50
|
+
import { readViewerAsset, prebuilt as usePrebuiltAssets } from "./assets.js";
|
|
51
|
+
import { mountBundleNetwork } from "./bundle-network.js";
|
|
52
|
+
import { codeFileResponse } from "./code-file.js";
|
|
53
|
+
import { isLocalToolRequest, runCodeTool, validateCodeTools, validateTextfileCode, } from "./editor-tools.js";
|
|
54
|
+
import { createExtensionManager, EXTENSIONS_DIR } from "./extensions.js";
|
|
55
|
+
import { fileReferences } from "./file-references.js";
|
|
56
|
+
import { mountFontPickers } from "./font-picker.js";
|
|
57
|
+
import { createFontSettings } from "./font-settings.js";
|
|
58
|
+
import { fontCss, readFont } from "./fonts.js";
|
|
59
|
+
import { createGitHandler, validateGitOrigin } from "./git.js";
|
|
60
|
+
import { gitBaseBody, gitBaseText } from "./git-diff.js";
|
|
61
|
+
import { createHmrHub, HMR_PATH, hmrClientScript, watchProject } from "./hmr.js";
|
|
62
|
+
import { translate } from "./i18n.js";
|
|
63
|
+
import { DEFAULT_SITE_LANGUAGES, loadSiteLanguages } from "./languages.js";
|
|
64
|
+
import { focusNavTree } from "./nav-focus.js";
|
|
65
|
+
import { loadViewerPlugins } from "./plugin-config.js";
|
|
66
|
+
import { createPluginHandler } from "./plugins.js";
|
|
67
|
+
import { mountPanelResize, mountToolsResize } from "./resize.js";
|
|
68
|
+
import { resourceIndex, resourceLibraryHtml, resourceTabs } from "./resource-library.js";
|
|
69
|
+
import { mountSearch, SEARCH_LABELS, searchIndex, searchSections } from "./search.js";
|
|
70
|
+
import { siteIconResponse } from "./site-icon.js";
|
|
71
|
+
import { previewTextfiles, textfileResponse } from "./textfile.js";
|
|
72
|
+
import { mountThemePickers } from "./theme-picker.js";
|
|
73
|
+
import { readWorkspaceSource } from "./workspace.js";
|
|
74
|
+
// pkgRoot = the viewer itself (src/ for the client bundles). docsRoot = the docs folder that
|
|
75
|
+
// follows the layout convention (content/ figures/).
|
|
76
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
77
|
+
const root = resolve(process.env.DOCS_ROOT ?? process.cwd());
|
|
78
|
+
const CONTENT_PREFIX = "content/";
|
|
79
|
+
const toEntry = (doc, source) => ({
|
|
80
|
+
id: doc.id,
|
|
81
|
+
...(doc.parent == null ? {} : { parent: doc.parent }),
|
|
82
|
+
file: `${doc.id}.md`,
|
|
83
|
+
sourceFile: doc.file,
|
|
84
|
+
frontmatter: source.frontmatter,
|
|
85
|
+
lang: doc.lang,
|
|
86
|
+
body: doc.body,
|
|
87
|
+
blocks: doc.blocks,
|
|
88
|
+
toc: doc.toc,
|
|
89
|
+
blockLines: source.lines,
|
|
90
|
+
raw: source.raw,
|
|
91
|
+
errors: source.errors,
|
|
92
|
+
...(doc.icon == null ? {} : { icon: doc.icon }),
|
|
93
|
+
figureRefs: doc.refs.figures,
|
|
94
|
+
termRefs: doc.refs.terms,
|
|
95
|
+
terms: source.terms,
|
|
96
|
+
});
|
|
97
|
+
// Recompile source files on every request. The compiler's cache
|
|
98
|
+
// skips re-parsing files unchanged on disk, so it stays cheap even when called on every keystroke
|
|
99
|
+
// (POST /api/render). Replaced in startServer once docs.config.mjs (language adapters)
|
|
100
|
+
// has been loaded.
|
|
101
|
+
let siteLanguages = DEFAULT_SITE_LANGUAGES;
|
|
102
|
+
let compiler = createCompiler(root);
|
|
103
|
+
const ops = createDocOps(root);
|
|
104
|
+
/** Compile result → viewer Site (shared by the server's per-request path and one-off publish). */
|
|
105
|
+
export const siteFrom = ({ title, icon, docs, terms, figures, sources, siteErrors, defaultLang, previews, contentLanguages, themes, }, languages = DEFAULT_SITE_LANGUAGES.languages) => {
|
|
106
|
+
return {
|
|
107
|
+
previews,
|
|
108
|
+
...(contentLanguages ? { contentLanguages } : {}),
|
|
109
|
+
...(themes ? { themes } : {}),
|
|
110
|
+
...(title === undefined ? {} : { title }),
|
|
111
|
+
...(icon === undefined ? {} : { icon }),
|
|
112
|
+
entries: docs.flatMap((doc) => {
|
|
113
|
+
const source = sources.get(doc.file);
|
|
114
|
+
return source ? [toEntry(doc, source)] : [];
|
|
115
|
+
}),
|
|
116
|
+
ctx: {
|
|
117
|
+
docLinks: new Map(docs.map((doc) => [doc.file, docPath(docs, doc.id)])),
|
|
118
|
+
figures: new Map(figures.map((f) => [f.name, f])),
|
|
119
|
+
terms,
|
|
120
|
+
termLinks: new Map([
|
|
121
|
+
...terms.map((term) => [
|
|
122
|
+
term.id,
|
|
123
|
+
`/_files/terms#${encodeURIComponent(termAnchor(term.id))}`,
|
|
124
|
+
]),
|
|
125
|
+
...docs.flatMap((doc) => doc.blocks.flatMap((block) => block.type === "term"
|
|
126
|
+
? [
|
|
127
|
+
[
|
|
128
|
+
block.entry.id,
|
|
129
|
+
`/${doc.id}#${encodeURIComponent(termAnchor(block.entry.id))}`,
|
|
130
|
+
],
|
|
131
|
+
]
|
|
132
|
+
: [])),
|
|
133
|
+
]),
|
|
134
|
+
},
|
|
135
|
+
errors: siteErrors,
|
|
136
|
+
languages,
|
|
137
|
+
defaultLang,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
const localeCompilers = new Map();
|
|
141
|
+
let workspaceSource;
|
|
142
|
+
let applicationPreviews = defaultPreviewSettings();
|
|
143
|
+
let watchTextfiles;
|
|
144
|
+
const watchEntryTextfiles = (entries) => {
|
|
145
|
+
watchTextfiles?.(entries.flatMap((entry) => [
|
|
146
|
+
entry.sourceFile ?? CONTENT_PREFIX + entry.file,
|
|
147
|
+
...fileReferences(entry.body).map((reference) => reference.file),
|
|
148
|
+
]));
|
|
149
|
+
};
|
|
150
|
+
const loadSite = (locale) => {
|
|
151
|
+
const previews = applicationPreviews;
|
|
152
|
+
const manifest = readManifest(root);
|
|
153
|
+
const source = contentLanguageCode(manifest.defaultLang ?? DEFAULT_LANG);
|
|
154
|
+
locale ??= source;
|
|
155
|
+
let selected = compiler;
|
|
156
|
+
if (locale !== source) {
|
|
157
|
+
selected = localeCompilers.get(locale) ?? createCompiler(root, { locale, previews });
|
|
158
|
+
localeCompilers.set(locale, selected);
|
|
159
|
+
}
|
|
160
|
+
const site = siteFrom(selected.compile(), siteLanguages.languages);
|
|
161
|
+
const sourceLanguage = contentLanguageCode(site.defaultLang);
|
|
162
|
+
for (const entry of site.entries) {
|
|
163
|
+
entry.translatedLanguages = [sourceLanguage];
|
|
164
|
+
entry.iconFile = docIconFile(entry.id, manifest.files);
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const translated = new Set(translationFiles(root));
|
|
168
|
+
for (const language of siteContentLanguages(root, manifest).filter((language) => language !== sourceLanguage)) {
|
|
169
|
+
for (const entry of site.entries) {
|
|
170
|
+
if (translated.has(translationFile(sourceDocFile(entry.id, manifest.files), language)))
|
|
171
|
+
entry.translatedLanguages?.push(language);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
site.errors.push(String(error));
|
|
177
|
+
}
|
|
178
|
+
watchEntryTextfiles(site.entries);
|
|
179
|
+
const siteIconFile = manifest.icon;
|
|
180
|
+
if (siteIconFile)
|
|
181
|
+
watchTextfiles?.([siteIconFile]);
|
|
182
|
+
return {
|
|
183
|
+
...site,
|
|
184
|
+
fonts: loadProjectFonts(root),
|
|
185
|
+
workspace: workspaceSource ?? readWorkspaceSource(root),
|
|
186
|
+
filePaths: manifest.errors.length
|
|
187
|
+
? undefined
|
|
188
|
+
: filesSnapshot(root).files.map((file) => file.path),
|
|
189
|
+
codeLanguages: siteLanguages.codeLanguages,
|
|
190
|
+
codeGrammars: siteLanguages.codeGrammars,
|
|
191
|
+
contentLanguage: locale,
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
const draftContext = (site) => ({
|
|
195
|
+
previews: site.previews,
|
|
196
|
+
root,
|
|
197
|
+
docs: site.entries.map((e) => ({
|
|
198
|
+
id: e.id,
|
|
199
|
+
...(e.parent == null ? {} : { parent: e.parent }),
|
|
200
|
+
file: e.sourceFile ?? CONTENT_PREFIX + e.file,
|
|
201
|
+
toc: e.toc,
|
|
202
|
+
})),
|
|
203
|
+
figures: site.ctx.figures,
|
|
204
|
+
defaultLang: site.defaultLang,
|
|
205
|
+
});
|
|
206
|
+
// fallbackTitle stands in for the h1 when frontmatter is missing. Each doc renders with its own
|
|
207
|
+
// language's adapter (term ordering) and `<article lang>`.
|
|
208
|
+
const docLinks = (site, html) => html.replace(/href="\/([a-z0-9][a-z0-9-]*)(?=["?#])/g, (whole, id) => site.entries.some((entry) => entry.id === id)
|
|
209
|
+
? `href="${docPath(site.entries, id)}`
|
|
210
|
+
: whole);
|
|
211
|
+
const renderEntry = (site, entry, fallbackTitle) => docLinks(site, docHtml(entry.frontmatter ?? { title: fallbackTitle }, entry.blocks, [...site.errors, ...entry.errors], {
|
|
212
|
+
...site.ctx,
|
|
213
|
+
terms: entry.terms ?? site.ctx.terms,
|
|
214
|
+
lang: webLanguage(entry.lang),
|
|
215
|
+
tocTitle: translate(entry.lang, "Contents"),
|
|
216
|
+
sectionUrl: `${docPath(site.entries, entry.id)}?lang=${encodeURIComponent(site.contentLanguage ?? entry.lang)}`,
|
|
217
|
+
language: resolveLanguage(entry.lang, site.languages),
|
|
218
|
+
}, entry.toc));
|
|
219
|
+
/** Compiles one source (unsaved or on disk; file = `<id>.md`) in the site context into an Entry. */
|
|
220
|
+
const draftEntry = (site, file, raw, textfiles) => {
|
|
221
|
+
const known = site.entries.find((entry) => entry.file === file);
|
|
222
|
+
const { doc, source } = compileDraft({ ...draftContext(site), defaultLang: known?.lang ?? site.defaultLang, textfiles }, known?.sourceFile ?? CONTENT_PREFIX + file, raw);
|
|
223
|
+
const entry = toEntry(doc, source);
|
|
224
|
+
watchEntryTextfiles([entry]);
|
|
225
|
+
return entry;
|
|
226
|
+
};
|
|
227
|
+
export const filePreviewResults = (entry, site) => {
|
|
228
|
+
const results = new Map();
|
|
229
|
+
const bodyOffset = entry.raw.slice(0, entry.raw.length - entry.body.length).split("\n").length - 1;
|
|
230
|
+
for (const { line, file, kind } of fileReferences(entry.body, site.previews)) {
|
|
231
|
+
const errors = results.get(file) ?? [];
|
|
232
|
+
errors.push(...entry.errors.filter((error) => error.startsWith(`L${bodyOffset + line}:`)));
|
|
233
|
+
if (kind === "term") {
|
|
234
|
+
errors.push(...entry.errors.filter((error) => error.startsWith(`${file}:`)));
|
|
235
|
+
}
|
|
236
|
+
else if (kind === "textfile") {
|
|
237
|
+
errors.push(...entry.errors.filter((error) => error.startsWith(`${file}:`)));
|
|
238
|
+
for (const block of walkBlocks(entry.blocks)) {
|
|
239
|
+
if (block.type !== "code" || block.filename?.replace(/^(\.\/)+/, "") !== file)
|
|
240
|
+
continue;
|
|
241
|
+
if (block.unavailable)
|
|
242
|
+
errors.push(block.unavailable === "missing"
|
|
243
|
+
? "File not found"
|
|
244
|
+
: "File could not be loaded");
|
|
245
|
+
errors.push(...entry.errors.filter((error) => error.startsWith(`${block.filename}${block.range ?? ""}:`)));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (site.ctx.figures.get(figureName(file))?.file !== file)
|
|
250
|
+
errors.push("File not found");
|
|
251
|
+
errors.push(...site.errors.filter((error) => error.startsWith(`${file}:`)));
|
|
252
|
+
}
|
|
253
|
+
results.set(file, [...new Set(errors)]);
|
|
254
|
+
}
|
|
255
|
+
return Object.fromEntries(results);
|
|
256
|
+
};
|
|
257
|
+
// Render the raw being edited (unsaved draft) to body html — shared by the save / render APIs.
|
|
258
|
+
const entryGitBase = (entry, site) => site.workspace?.protocol === "git"
|
|
259
|
+
? gitBaseBody(root, entry.sourceFile ?? CONTENT_PREFIX + entry.file)
|
|
260
|
+
: null;
|
|
261
|
+
const renderDraft = (site, file, raw, textfiles) => {
|
|
262
|
+
const entry = draftEntry(site, file, raw, textfiles);
|
|
263
|
+
return {
|
|
264
|
+
gitBase: entryGitBase(entry, site),
|
|
265
|
+
sourceFile: entry.sourceFile,
|
|
266
|
+
html: renderEntry(site, entry, ""),
|
|
267
|
+
errors: [...site.errors, ...entry.errors],
|
|
268
|
+
toc: entry.toc,
|
|
269
|
+
blockLines: entry.blockLines,
|
|
270
|
+
filePreviews: filePreviewResults(entry, site),
|
|
271
|
+
previews: site.previews,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
// Render result cache — opening the same doc repeatedly (page GET / nav switch) doesn't re-render.
|
|
275
|
+
// The compiler's cache returns the same blocks reference while disk is unchanged, so check that,
|
|
276
|
+
// plus that the id list (affects document file link resolution) / terms / figures / errors haven't
|
|
277
|
+
// changed. Rebuild if any of them did.
|
|
278
|
+
const renderSignature = (site, entry) => JSON.stringify([site.ctx.terms, site.errors, entry.errors, entry.frontmatter, entry.lang]) +
|
|
279
|
+
site.entries
|
|
280
|
+
.map((e) => [e.sourceFile ?? CONTENT_PREFIX + e.file, docPath(site.entries, e.id)])
|
|
281
|
+
.sort()
|
|
282
|
+
.join("\n");
|
|
283
|
+
const entryHtmlCache = new Map();
|
|
284
|
+
/** Renders only the requested doc, not every doc in site.entries. */
|
|
285
|
+
export const renderEntryHtml = (site, entry) => {
|
|
286
|
+
const sig = renderSignature(site, entry);
|
|
287
|
+
const cached = entryHtmlCache.get(entry.file);
|
|
288
|
+
if (cached &&
|
|
289
|
+
cached.blocks === entry.blocks &&
|
|
290
|
+
cached.sig === sig &&
|
|
291
|
+
cached.figures === site.ctx.figures) {
|
|
292
|
+
return cached.value;
|
|
293
|
+
}
|
|
294
|
+
const value = {
|
|
295
|
+
html: renderEntry(site, entry, entry.id),
|
|
296
|
+
errors: [...site.errors, ...entry.errors],
|
|
297
|
+
};
|
|
298
|
+
entryHtmlCache.set(entry.file, {
|
|
299
|
+
blocks: entry.blocks,
|
|
300
|
+
sig,
|
|
301
|
+
figures: site.ctx.figures,
|
|
302
|
+
value,
|
|
303
|
+
});
|
|
304
|
+
return value;
|
|
305
|
+
};
|
|
306
|
+
/** Compiles + renders one source (its current content on disk) (implements `/api/doc`). */
|
|
307
|
+
export const renderDocFile = (site, file, raw) => {
|
|
308
|
+
const entry = draftEntry(site, file, raw);
|
|
309
|
+
return {
|
|
310
|
+
gitBase: entryGitBase(entry, site),
|
|
311
|
+
sourceFile: entry.sourceFile,
|
|
312
|
+
raw,
|
|
313
|
+
frontmatter: entry.frontmatter,
|
|
314
|
+
body: entry.body,
|
|
315
|
+
html: renderEntry(site, entry, ""),
|
|
316
|
+
errors: [...site.errors, ...entry.errors],
|
|
317
|
+
toc: entry.toc,
|
|
318
|
+
blockLines: entry.blockLines,
|
|
319
|
+
filePreviews: filePreviewResults(entry, site),
|
|
320
|
+
previews: site.previews,
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
/** Response for `GET /api/doc?file=<id>.md` (status + body). mode === "edit" only. */
|
|
324
|
+
export const docQueryResponse = (site, file) => {
|
|
325
|
+
const target = resolveContentFile(file);
|
|
326
|
+
if (!target) {
|
|
327
|
+
return { status: 400, body: { error: "file must be <id>.md" } };
|
|
328
|
+
}
|
|
329
|
+
if (!existsSync(target.abs)) {
|
|
330
|
+
return { status: 404, body: { error: `${target.name} not found` } };
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
status: 200,
|
|
334
|
+
body: renderDocFile(site, target.name, site.entries.find((entry) => entry.file === target.name)?.raw ??
|
|
335
|
+
readFileSync(target.abs, "utf8")),
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
export { assembleDoc };
|
|
339
|
+
const asString = (v) => (typeof v === "string" ? v : "");
|
|
340
|
+
/** Client JSON → Frontmatter (keeps supported fields and drops empty optional values). */
|
|
341
|
+
export const sanitizeFrontmatter = (v) => {
|
|
342
|
+
const o = (typeof v === "object" && v != null ? v : {});
|
|
343
|
+
const lang = asString(o.lang).trim();
|
|
344
|
+
return { title: asString(o.title).trim(), ...(lang ? { lang } : {}) };
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Compares the base the editor started from (the disk source the client last read) with the
|
|
348
|
+
* current disk source to tell whether it was rewritten externally (direct edit / git etc.)
|
|
349
|
+
* without the editor knowing.
|
|
350
|
+
* A client that sends no base (no baseRaw) can't be judged, so false (= overwrite).
|
|
351
|
+
*/
|
|
352
|
+
export const isExternallyModified = (baseRaw, currentRaw) => typeof baseRaw === "string" && baseRaw !== currentRaw;
|
|
353
|
+
/**
|
|
354
|
+
* Validates the write target path — rejects anything other than `<id>.md` (directly under
|
|
355
|
+
* content/), including traversal.
|
|
356
|
+
*/
|
|
357
|
+
export const resolveContentFile = (rel) => {
|
|
358
|
+
const id = typeof rel === "string" ? docIdOfFile(rel) : undefined;
|
|
359
|
+
return id == null
|
|
360
|
+
? undefined
|
|
361
|
+
: {
|
|
362
|
+
id,
|
|
363
|
+
name: `${id}.md`,
|
|
364
|
+
abs: textfilePath(root, sourceDocFile(id, readManifest(root).files)),
|
|
365
|
+
};
|
|
366
|
+
};
|
|
367
|
+
// Default palette — resolves the figures' var(--doc-*) and color-mix() following the theme. The
|
|
368
|
+
// theme has 3 states: default = follow the OS (prefers-color-scheme), explicitly overridden via
|
|
369
|
+
// <html data-theme="light|dark">.
|
|
370
|
+
// Maps the viewer's --c-* palette onto the body's --doc-* tokens. Unmapped tokens use defaults.
|
|
371
|
+
const DARK_C = `
|
|
372
|
+
color-scheme: dark;
|
|
373
|
+
--c-bg0:#101316; --c-bg1:#16191d; --c-surface:#1b1f24; --c-raised:#22272d;
|
|
374
|
+
--c-line:#2a3037; --c-line-soft:#22272c;
|
|
375
|
+
--c-fg:#f5f6f7; --c-fg-muted:#b4bac0; --c-fg-faint:#8a939b; --fw-body:400;
|
|
376
|
+
--c-accent:#e65100; --c-ok:#4f9e6a; --c-danger:#c65b52; --c-warn:#c99a3a; --c-info:#6b8fbf;
|
|
377
|
+
--c-shadow:rgba(0,0,0,.4);
|
|
378
|
+
--doc-fg-4:#525a66; --doc-accent-2:#4a8bff; --doc-accent-fg:#ffffff;
|
|
379
|
+
--doc-ok-2:#56d364; --doc-warn-2:#ffc14d; --doc-danger-2:#ff7a7a; --doc-info-2:#79b0ff;`;
|
|
380
|
+
const LIGHT_C = `
|
|
381
|
+
color-scheme: light;
|
|
382
|
+
--c-bg0:#ffffff; --c-bg1:#f5f7f9; --c-surface:#fbfcfd; --c-raised:#e9edf2;
|
|
383
|
+
--c-line:#d5dae0; --c-line-soft:#e4e8ec;
|
|
384
|
+
--c-fg:#1a1e23; --c-fg-muted:#5a636c; --c-fg-faint:#7b848e; --fw-body:300;
|
|
385
|
+
--c-accent:#c24800; --c-ok:#2f7d4f; --c-danger:#b23c31; --c-warn:#8a6516; --c-info:#3a6ea5;
|
|
386
|
+
--c-shadow:rgba(15,23,32,.14);
|
|
387
|
+
--doc-fg-4:#9099a8; --doc-accent-2:#0277bd; --doc-accent-fg:#ffffff;
|
|
388
|
+
--doc-ok-2:#2ea043; --doc-warn-2:#f59f00; --doc-danger-2:#f04848; --doc-info-2:#4a8bff;`;
|
|
389
|
+
const DOC_FROM_C = `
|
|
390
|
+
--doc-bg:var(--c-bg0); --doc-bg-2:var(--c-bg1); --doc-bg-3:var(--c-surface);
|
|
391
|
+
--doc-panel:var(--c-surface); --doc-panel-2:var(--c-raised); --doc-rail:var(--c-bg1);
|
|
392
|
+
--doc-border:var(--c-line-soft); --doc-border-2:var(--c-line);
|
|
393
|
+
--doc-fg:var(--c-fg); --doc-fg-2:var(--c-fg-muted); --doc-fg-3:var(--c-fg-faint);
|
|
394
|
+
--doc-accent:var(--c-accent); --doc-ok:var(--c-ok); --doc-warn:var(--c-warn);
|
|
395
|
+
--doc-danger:var(--c-danger); --doc-info:var(--c-info);`;
|
|
396
|
+
// Default for code and editing; browser display settings can override each independently.
|
|
397
|
+
const MONO_FONT = `"UDEV Gothic", monospace`;
|
|
398
|
+
const SANS_FONT = `"IBM Plex Sans JP", -apple-system, "Segoe UI", "Noto Sans JP", sans-serif`;
|
|
399
|
+
// Viewer icons (20x20 stroke glyphs).
|
|
400
|
+
const icon = (paths, size = 18) => `<svg class="nav-icon" viewBox="0 0 20 20" width="${size}" height="${size}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
401
|
+
const SIDEBAR_ICON = icon('<rect x="3" y="4" width="14" height="12" rx="1.5"/><path d="M8 4v12"/>');
|
|
402
|
+
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.3v9c0 .7.6 1.3 1.3 1.3 1.8 0 3.5.5 4.7 1.5"/><path d="M10 5.2c1.2-1 2.9-1.5 4.7-1.5.7 0 1.3.6 1.3 1.3v9c0 .7-.6 1.3-1.3 1.3-1.8 0-3.5.5-4.7 1.5Z"/><path d="M10 5.2v11.6"/>');
|
|
403
|
+
const CHEVRON_ICON = icon('<path d="M8 5.5 12.5 10 8 14.5"/>', 14);
|
|
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"/>');
|
|
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"/>');
|
|
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"/>');
|
|
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"/>');
|
|
409
|
+
const workspaceName = (site) => site.workspace?.protocol === "storage" ? workspaceLabel(site.workspace) : "Files";
|
|
410
|
+
const workspaceText = (site) => `<span${site.workspace?.protocol === "storage" ? "" : ' data-i18n="Files"'}>${escapeHtml(workspaceName(site))}</span>`;
|
|
411
|
+
const workspaceIcon = (site) => {
|
|
412
|
+
if (site.workspace?.protocol === "git")
|
|
413
|
+
return icon('<path d="M2 5h6l2 2h8v10H2Z"/>');
|
|
414
|
+
return site.workspace?.icon
|
|
415
|
+
? svgImg(site.workspace.icon, "nav-icon workspace-icon")
|
|
416
|
+
: STORAGE_ICON;
|
|
417
|
+
};
|
|
418
|
+
const UPLOAD_ICON = icon('<path d="M11.5 3H6a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6.5L11.5 3Z"/><path d="M11.5 3v3.5H15M10 14V9M7.8 11.2 10 9l2.2 2.2"/>', 24);
|
|
419
|
+
const svgDataUri = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
420
|
+
/**
|
|
421
|
+
* Full svg → <img> (data URI; inline svgs would let the ids / styles of multiple icons collide,
|
|
422
|
+
* so each is confined to an img).
|
|
423
|
+
*/
|
|
424
|
+
const svgImg = (svg, cls) => {
|
|
425
|
+
const uri = svgDataUri(svg);
|
|
426
|
+
const img = `<img class="${cls}" alt="" src="${uri}">`;
|
|
427
|
+
return cls.includes("doc-icon")
|
|
428
|
+
? `<span class="doc-icon-frame" style="--doc-icon:url('${uri}')">${img}</span>`
|
|
429
|
+
: img;
|
|
430
|
+
};
|
|
431
|
+
const GEAR_ICON = icon('<path d="M4 6h7M15 6h1M4 14h1M9 14h7"/><circle cx="13" cy="6" r="2"/><circle cx="7" cy="14" r="2"/>', 16);
|
|
432
|
+
const DELETE_ICON = icon('<path d="M4.5 6h11M8 6V4.5h4V6M6 6l.7 9.5h6.6L14 6M8.7 9v4M11.3 9v4"/>', 15);
|
|
433
|
+
const PLUS_ICON = icon('<path d="M10 4.5v11M4.5 10h11"/>', 15);
|
|
434
|
+
const THEME_ICONS = {
|
|
435
|
+
"": icon('<circle cx="10" cy="10" r="6.5"/><path d="M10 3.5a6.5 6.5 0 0 0 0 13Z" fill="currentColor" stroke="none"/>'),
|
|
436
|
+
light: icon('<circle cx="10" cy="10" r="3.4"/><path d="M10 2.6v1.8M10 15.6v1.8M2.6 10h1.8M15.6 10h1.8M4.8 4.8l1.3 1.3M13.9 13.9l1.3 1.3M15.2 4.8l-1.3 1.3M6.1 13.9l-1.3 1.3"/>'),
|
|
437
|
+
dark: icon('<path d="M15.5 12.4A6.2 6.2 0 0 1 7.6 4.5a6.5 6.5 0 1 0 7.9 7.9Z"/>'),
|
|
438
|
+
};
|
|
439
|
+
const fontButton = (target) => {
|
|
440
|
+
const label = target === "editor" ? "Editor font" : "Viewer font";
|
|
441
|
+
return `<button class="pbtn font-control" type="button" data-font-picker="${target}" aria-haspopup="menu" aria-expanded="false" aria-label="${label}" title="${label}" disabled>${icon('<path d="M2.5 15 7 4l4.5 11M4 11h6M13 10.5c0-2.5 5-2.5 5 0V15M18 12h-2.5c-3.5 0-3.5 4 0 3L18 13.5"/>')}</button>`;
|
|
442
|
+
};
|
|
443
|
+
const CSS = `
|
|
444
|
+
:root { ${DARK_C} ${DOC_FROM_C} --doc-mono: ${MONO_FONT}; --font-sans: ${SANS_FONT}; }
|
|
445
|
+
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) { ${LIGHT_C} } }
|
|
446
|
+
:root[data-theme="light"] { ${LIGHT_C} }
|
|
447
|
+
body.edit :is(#sidebar, #panel, .resources-pane), .preview-pane[data-theme], .theme-picker[data-theme] { ${DARK_C} ${DOC_FROM_C} }
|
|
448
|
+
@media (prefers-color-scheme: light) {
|
|
449
|
+
:root:not([data-editor-theme="dark"]) body.edit :is(#sidebar, #panel, .resources-pane),
|
|
450
|
+
:is(.preview-pane, .theme-picker)[data-theme="auto"] { ${LIGHT_C} }
|
|
451
|
+
}
|
|
452
|
+
:root[data-editor-theme="light"] body.edit :is(#sidebar, #panel, .resources-pane),
|
|
453
|
+
:is(.preview-pane, .theme-picker)[data-theme="light"] { ${LIGHT_C} }
|
|
454
|
+
.preview-pane { background: var(--c-bg0); color: var(--c-fg); }
|
|
455
|
+
.theme-picker { position: fixed; margin: 0; padding: 5px; width: 148px; border: 1px solid var(--c-line);
|
|
456
|
+
border-radius: 6px; background: var(--c-bg1); color: var(--c-fg); box-shadow: 0 8px 24px var(--c-shadow); }
|
|
457
|
+
.theme-picker button { display: flex; align-items: center; gap: 9px; width: 100%; padding: 7px 8px;
|
|
458
|
+
border: 0; border-radius: 4px; background: transparent; color: inherit; font: inherit; font-size: 12px; text-align: left; cursor: pointer; }
|
|
459
|
+
.theme-picker button:hover, .theme-picker button:focus-visible { background: var(--c-raised); }
|
|
460
|
+
.theme-picker [aria-checked="true"]::after { content: "✓"; margin-left: auto; color: var(--c-accent); }
|
|
461
|
+
.font-control:focus:not(:focus-visible), .font-picker button:focus:not(:focus-visible) { outline: none; }
|
|
462
|
+
.font-control:focus-visible, .font-picker button:focus-visible { outline: 1px solid var(--c-accent); outline-offset: 1px; }
|
|
463
|
+
.font-control:disabled { opacity: .5; cursor: wait; }
|
|
464
|
+
.font-picker { width: min(240px, calc(100vw - 16px)); overflow-y: auto; }
|
|
465
|
+
.font-picker button { overflow-wrap: anywhere; }
|
|
466
|
+
.font-picker [aria-checked="true"]::after { flex: none; }
|
|
467
|
+
.font-status { padding: 8px; font-size: 11px; color: var(--c-danger); }
|
|
468
|
+
main { --doc-mono: var(--viewer-font, ${MONO_FONT}); }
|
|
469
|
+
.resources-pane { --doc-mono: var(--editor-font, ${MONO_FONT}); background: var(--c-bg1); color: var(--c-fg); font-family: var(--doc-mono); }
|
|
470
|
+
.page-actions, .pane-actions { position: sticky; display: flex; align-items: center; justify-content: flex-end;
|
|
471
|
+
gap: 2px; height: 36px; padding: 0 24px; border-bottom: 1px solid var(--c-line); background: var(--c-bg0); z-index: 5; }
|
|
472
|
+
.page-actions { top: 0; margin: -20px -24px 20px; }
|
|
473
|
+
.pbtn { flex: none; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px;
|
|
474
|
+
padding: 0; color: var(--c-fg-faint); background: none; border: none; border-radius: 5px; cursor: pointer;
|
|
475
|
+
transition: color 120ms ease, background 120ms ease; }
|
|
476
|
+
.pbtn:hover, .pbtn:focus-visible, #gear.open > .pbtn { color: var(--c-fg); background: var(--c-raised); }
|
|
477
|
+
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--c-fg-muted) 42%, transparent) transparent; }
|
|
478
|
+
::-webkit-scrollbar { width: 7px; height: 7px; }
|
|
479
|
+
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background: transparent; }
|
|
480
|
+
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--c-fg-muted) 42%, transparent); border: 2px solid transparent; border-radius: 999px; background-clip: padding-box; }
|
|
481
|
+
::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--c-fg-muted) 68%, transparent); }
|
|
482
|
+
@supports selector(::-webkit-scrollbar) { * { scrollbar-width: auto; scrollbar-color: auto; } }
|
|
483
|
+
input[type="checkbox"] { appearance: none; position: relative; display: inline-block; flex: none; width: 15px; height: 15px; margin: 0; vertical-align: middle; border: 1px solid var(--c-fg-muted); border-radius: 4px; background: transparent; cursor: pointer; }
|
|
484
|
+
input[type="checkbox"]:checked { border-color: var(--c-accent); }
|
|
485
|
+
input[type="checkbox"]:checked::after { content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid var(--c-accent); border-width: 0 2px 2px 0; transform: rotate(45deg); }
|
|
486
|
+
input[type="checkbox"]:indeterminate::after { content: ""; position: absolute; left: 3px; top: 6px; width: 7px; border-top: 2px solid var(--c-accent); }
|
|
487
|
+
input[type="checkbox"]:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 3px; }
|
|
488
|
+
input[type="checkbox"]:disabled { opacity: .5; cursor: default; }
|
|
489
|
+
@media (forced-colors: active) { input[type="checkbox"] { appearance: auto; } input[type="checkbox"]::after { display: none; } }
|
|
490
|
+
body { margin: 0; display: flex; min-height: 100vh; background: var(--c-bg0); color: var(--c-fg);
|
|
491
|
+
font-family: var(--font-sans); font-weight: var(--fw-body); font-size: 13px; line-height: 1.55;
|
|
492
|
+
overflow-x: hidden; }
|
|
493
|
+
button { font-family: inherit; font-size: inherit; font-weight: var(--fw-body); }
|
|
494
|
+
:focus-visible { outline: 1px solid var(--c-accent); outline-offset: 1px; }
|
|
495
|
+
|
|
496
|
+
#doc-search { position: fixed; top: 12px; left: 0; right: 0; z-index: 20;
|
|
497
|
+
width: min(600px, calc(100vw - 24px)); min-height: 56px; max-height: min(420px, calc(100dvh - 24px)); margin: 0 auto;
|
|
498
|
+
padding: 0 10px 10px; color: var(--c-fg); background: color-mix(in srgb, var(--c-bg1) 82%, transparent);
|
|
499
|
+
border: 1px solid color-mix(in srgb, var(--c-fg-muted) 55%, transparent);
|
|
500
|
+
backdrop-filter: blur(16px); border-radius: 8px; box-shadow: 0 8px 24px #0003; overflow: hidden; }
|
|
501
|
+
#doc-search[open] { display: flex; flex-direction: column; }
|
|
502
|
+
#doc-search :focus-visible { outline-color: var(--c-fg-muted); }
|
|
503
|
+
.search-head { display: flex; flex: none; align-items: center; gap: 10px; padding: 8px;
|
|
504
|
+
color: var(--c-fg-muted); }
|
|
505
|
+
.search-head svg { flex: none; }
|
|
506
|
+
#doc-search.search-filled .search-head svg { display: none; }
|
|
507
|
+
#doc-search-input { width: 100%; min-width: 0; padding: 4px 0; font: inherit; font-size: 15px;
|
|
508
|
+
color: var(--c-fg); background: none; border: 0; outline: none; }
|
|
509
|
+
#doc-search-input::-webkit-search-cancel-button { appearance: none; }
|
|
510
|
+
.search-status { padding: 8px 16px; color: var(--c-fg-muted); font-size: 12px; }
|
|
511
|
+
.search-status:empty { display: none; }
|
|
512
|
+
.search-count { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden;
|
|
513
|
+
clip-path: inset(50%); white-space: nowrap; }
|
|
514
|
+
#doc-search-results { overflow-y: auto; min-height: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
515
|
+
#doc-search-results a { display: flex; flex: none; flex-direction: column; gap: 6px;
|
|
516
|
+
border: 1px solid var(--c-line); border-radius: 5px; color: var(--c-fg); text-decoration: none;
|
|
517
|
+
background: color-mix(in srgb, var(--c-bg0) 50%, transparent); overflow-wrap: anywhere; }
|
|
518
|
+
#doc-search-results a:hover, #doc-search-results a[aria-selected="true"] { border-color: var(--c-fg-muted); }
|
|
519
|
+
.search-meta { display: flex; align-items: center; justify-content: space-between; gap: 12px;
|
|
520
|
+
padding: 5px 10px; border-bottom: 1px solid var(--c-line); color: var(--c-fg-muted); }
|
|
521
|
+
.search-path { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
522
|
+
#doc-search-results strong { margin: 2px 10px 0; font-weight: 500; }
|
|
523
|
+
#doc-search-results span { margin: 0 10px 10px; color: var(--c-fg-muted); }
|
|
524
|
+
.search-grip { position: absolute; top: 8px; bottom: 18px; width: 10px; cursor: grab; touch-action: none; }
|
|
525
|
+
.search-grip-left { left: 0; }
|
|
526
|
+
.search-grip-right { right: 0; }
|
|
527
|
+
.search-grip:active { cursor: grabbing; }
|
|
528
|
+
.search-grip::after { content: ""; position: absolute; top: 35%; bottom: 35%; left: 4px;
|
|
529
|
+
width: 2px; border-radius: 2px; background: var(--c-line); }
|
|
530
|
+
.search-grip:hover::after, .search-grip:focus-visible::after { background: var(--c-fg-muted); }
|
|
531
|
+
.search-resize { position: absolute; bottom: 0; right: 0; width: 18px; height: 18px;
|
|
532
|
+
cursor: nwse-resize; touch-action: none; }
|
|
533
|
+
.search-resize::after { content: ""; position: absolute; right: 4px; bottom: 4px; width: 7px; height: 7px;
|
|
534
|
+
border-right: 1px solid var(--c-fg-muted); border-bottom: 1px solid var(--c-fg-muted); }
|
|
535
|
+
|
|
536
|
+
/* ── sidebar ── */
|
|
537
|
+
#sidebar { width: var(--sidebar-width, 260px); height: 100vh; position: sticky; top: 0; flex: none;
|
|
538
|
+
display: flex; flex-direction: column; background: var(--c-bg1);
|
|
539
|
+
border-right: 1px solid var(--c-line); overflow: hidden; }
|
|
540
|
+
body.sidebar-closed #sidebar { width: 52px; }
|
|
541
|
+
body.sidebar-closed #sidebar-resize { display: none; }
|
|
542
|
+
.panel-resize { position: absolute; top: 0; bottom: 0; width: 8px; cursor: col-resize; z-index: 6;
|
|
543
|
+
touch-action: none; transition: background 120ms ease; }
|
|
544
|
+
#sidebar-resize { right: 0; }
|
|
545
|
+
#resize { left: 0; }
|
|
546
|
+
.panel-resize:hover, .panel-resize:focus-visible, .panel-resize.drag {
|
|
547
|
+
background: color-mix(in srgb, var(--c-accent) 60%, transparent); }
|
|
548
|
+
body.resizing, body.resizing * { cursor: col-resize !important; user-select: none !important; }
|
|
549
|
+
body.sidebar-closed .site-title { display: none; }
|
|
550
|
+
#sidebar-head { position: relative; flex: none; display: flex; align-items: center; min-width: 0; min-height: 36px;
|
|
551
|
+
border-bottom: 1px solid var(--c-line); }
|
|
552
|
+
#sidebar-toggle { flex: none; display: flex; align-items: center; padding: 8px 16px;
|
|
553
|
+
background: transparent; border: none; color: var(--c-fg-muted); cursor: pointer; }
|
|
554
|
+
#sidebar-toggle:hover { color: var(--c-fg); }
|
|
555
|
+
#site-icon-slot { position: relative; flex: none; width: 24px; height: 24px; margin: 0 8px 0 16px; color: var(--c-fg-muted); }
|
|
556
|
+
#site-icon, #site-icon-slot > .nav-icon { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
557
|
+
#site-icon[hidden], #site-icon-slot:has(#site-icon:not([hidden])) > .nav-icon { display: none; }
|
|
558
|
+
body.sidebar-closed #site-icon-slot { margin: 0 auto; }
|
|
559
|
+
body.sidebar-closed #sidebar-toggle { position: absolute; inset: 0; width: 100%; justify-content: center; padding: 0; }
|
|
560
|
+
body.sidebar-closed #sidebar-toggle svg { visibility: hidden; }
|
|
561
|
+
.site-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
562
|
+
padding-right: 12px; color: var(--c-fg); white-space: nowrap; }
|
|
563
|
+
#sidebar-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
|
|
564
|
+
scrollbar-gutter: stable;
|
|
565
|
+
padding: 10px 0 8px; }
|
|
566
|
+
.nav-link { position: relative; display: flex; align-items: center; gap: 14px; padding: 10px 16px;
|
|
567
|
+
color: var(--c-fg-muted); text-decoration: none; background: transparent; border: none;
|
|
568
|
+
font: inherit; text-align: left; cursor: pointer; min-width: 0; }
|
|
569
|
+
.nav-link::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
|
|
570
|
+
background: transparent; transition: background 120ms ease-out; }
|
|
571
|
+
.nav-link:hover { color: var(--c-fg); background: var(--c-raised); }
|
|
572
|
+
.nav-link:hover::before { background: var(--c-line); }
|
|
573
|
+
.nav-link.cur { color: var(--c-fg); }
|
|
574
|
+
.nav-link.cur::before { background: var(--c-accent); }
|
|
575
|
+
.nav-icon { flex: none; }
|
|
576
|
+
.workspace-icon { width: 18px; height: 18px; object-fit: contain; }
|
|
577
|
+
.nav-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
578
|
+
/* The doc tree. Top-level docs are .nav-link with an icon,
|
|
579
|
+
descendants are .nav-tree. --nav-depth is set on the children list (top level's children = 2),
|
|
580
|
+
--nav-pad = where that level's label starts, --nav-line = x of that level's guide line. Each
|
|
581
|
+
item draws a vertical line (the vertical of ├), and only the last item stops it at the row's
|
|
582
|
+
middle to form └. The path down to the current doc is accented.
|
|
583
|
+
The data has no root / child distinction (any doc can have children) — only the look changes
|
|
584
|
+
per level. */
|
|
585
|
+
.nav-row { position: relative; display: flex; align-items: center; min-width: 0; }
|
|
586
|
+
.nav-row > .nav-link { flex: 1; min-width: 0; padding: 9px 16px; }
|
|
587
|
+
.nav-row > .nav-link.cur { background: var(--c-raised); }
|
|
588
|
+
.nav-item:not(.open) > .nav-list { display: none; }
|
|
589
|
+
.nav-tree-toggle { flex: none; display: flex; align-items: center; justify-content: center;
|
|
590
|
+
width: 28px; height: 28px; margin-right: 6px; padding: 0; background: transparent; border: none;
|
|
591
|
+
border-radius: 4px; color: var(--c-fg-faint); cursor: pointer; }
|
|
592
|
+
.nav-tree-toggle:hover { color: var(--c-fg); background: var(--c-raised); }
|
|
593
|
+
.nav-tree-toggle svg { transition: transform 120ms ease-out; }
|
|
594
|
+
.nav-item.open > .nav-row > .nav-tree-toggle svg { transform: rotate(90deg); }
|
|
595
|
+
.nav-tree { --nav-pad: calc(48px + (var(--nav-depth, 2) - 2) * 29px);
|
|
596
|
+
--nav-line: calc(var(--nav-pad) - 23px); margin: 0; padding-bottom: 4px; }
|
|
597
|
+
.nav-tree:empty { padding-bottom: 0; }
|
|
598
|
+
.nav-tree > .nav-item { position: relative; }
|
|
599
|
+
.nav-tree > .nav-item:not(:last-child)::before, .nav-tree > .nav-item > .nav-row::before,
|
|
600
|
+
.nav-tree > .nav-item:last-child > .nav-row::after, .nav-tree > .nav-item:has(.cur) > .nav-row::after {
|
|
601
|
+
content: ""; position: absolute; z-index: 1; pointer-events: none; left: var(--nav-line);
|
|
602
|
+
background: var(--c-line); }
|
|
603
|
+
/* vertical of ├ / └ */
|
|
604
|
+
.nav-tree > .nav-item:not(:last-child)::before { top: 0; bottom: 0; width: 1px; z-index: 2; }
|
|
605
|
+
.nav-tree > .nav-item:last-child > .nav-row::after, .nav-tree > .nav-item:has(.cur) > .nav-row::after {
|
|
606
|
+
top: 0; height: calc(50% + 1px); width: 1px; z-index: 3; }
|
|
607
|
+
/* ─ (up to 6px before the label) */
|
|
608
|
+
.nav-tree > .nav-item > .nav-row::before { top: 50%; width: 17px; height: 1px; }
|
|
609
|
+
.nav-tree > .nav-item:has(~ .nav-item .cur)::before, .nav-tree > .nav-item:has(.cur) > .nav-row::before,
|
|
610
|
+
.nav-tree > .nav-item:has(.cur) > .nav-row::after { background: var(--c-accent); }
|
|
611
|
+
.nav-ancestor { --nav-pad: 12px; --nav-line: var(--nav-rail, 21px); }
|
|
612
|
+
.nav-list:has(> .nav-ancestor) > .nav-item:not(.nav-ancestor) { height: 0; opacity: 0; overflow: hidden; visibility: hidden; }
|
|
613
|
+
#nav-groups { --nav-motion: 260ms cubic-bezier(.2, .8, .2, 1); }
|
|
614
|
+
#nav-groups :is(.nav-tree-link, .nav-doc-row) { transition: padding-left var(--nav-motion); }
|
|
615
|
+
#nav-groups :is(.nav-create, .nav-new) { transition: margin-left var(--nav-motion); }
|
|
616
|
+
#nav-groups .nav-item::before, #nav-groups .nav-row::before, #nav-groups .nav-row::after {
|
|
617
|
+
transition: left var(--nav-motion), width var(--nav-motion), height var(--nav-motion), top var(--nav-motion); }
|
|
618
|
+
/* A connector changing orientation must stay 1px thick instead of morphing into a rectangle. */
|
|
619
|
+
#nav-groups .nav-row::before { transition-property: left, top; }
|
|
620
|
+
@media (prefers-reduced-motion: reduce) { #nav-groups { --nav-motion: 0s; } }
|
|
621
|
+
.nav-ancestor > .nav-tree > .nav-item::before,
|
|
622
|
+
.nav-ancestor > .nav-tree > .nav-item > .nav-row::before,
|
|
623
|
+
.nav-ancestor > .nav-tree > .nav-item > .nav-row::after { display: none; }
|
|
624
|
+
/* One rail reaches the next ancestor, then stops at the selected row. */
|
|
625
|
+
.nav-ancestor > .nav-tree > .nav-item:has(~ .nav-item .cur)::before {
|
|
626
|
+
display: block; left: var(--nav-rail, 21px); background: var(--c-accent); }
|
|
627
|
+
.nav-root > .nav-ancestor.open > .nav-row::after,
|
|
628
|
+
.nav-ancestor > .nav-tree > .nav-ancestor.open > .nav-row::after {
|
|
629
|
+
content: ""; display: block; position: absolute; left: var(--nav-rail, 21px); top: calc(50% + var(--nav-icon-half, 10px)); bottom: 0;
|
|
630
|
+
width: 1px; height: auto; background: var(--c-accent); pointer-events: none; }
|
|
631
|
+
.nav-ancestor > .nav-tree > .nav-ancestor > .nav-row::before {
|
|
632
|
+
display: block; left: var(--nav-rail, 21px); top: 0; width: 1px; height: calc(50% - var(--nav-icon-half, 10px)); background: var(--c-accent); }
|
|
633
|
+
.nav-ancestor > .nav-tree > .nav-item:has(> .nav-row > a.cur) { --nav-line: var(--nav-rail, 21px); }
|
|
634
|
+
.nav-ancestor > .nav-tree > .nav-item:has(> .nav-row > a.cur) > .nav-row::before { display: block; width: calc(42px - var(--nav-rail, 21px)); }
|
|
635
|
+
.nav-ancestor > .nav-tree > .nav-item:has(> .nav-row > a.cur) > .nav-row::after { display: block; }
|
|
636
|
+
.nav-tree-link { position: relative; flex: 1; min-width: 0; display: flex; align-items: center;
|
|
637
|
+
padding: 6px 8px 6px var(--nav-pad); color: var(--c-fg-muted); font-size: 13px; text-decoration: none; }
|
|
638
|
+
.nav-tree-link:hover:not(.cur) { color: var(--c-fg); background: var(--c-raised); }
|
|
639
|
+
/* The accented guide line marks the current doc, so no left-edge vertical bar (the top-level
|
|
640
|
+
marker). */
|
|
641
|
+
.nav-tree-link.cur { color: var(--c-fg); background: var(--c-raised); }
|
|
642
|
+
/* Doc-specific icon (icons/<id>.svg, 1:1). Top level: in place of the book icon; descendants:
|
|
643
|
+
before the label */
|
|
644
|
+
img.doc-icon { flex: none; width: 18px; height: 18px; object-fit: contain; }
|
|
645
|
+
.doc-icon-frame { position: relative; display: inline-flex; flex: none; width: 18px; height: 18px; }
|
|
646
|
+
.doc-icon-frame > img { width: 100%; height: 100%; }
|
|
647
|
+
.nav-tree-link > .doc-icon-frame, .nav-tree-link > .nav-icon { width: 15px; height: 15px; margin-right: 7px; }
|
|
648
|
+
.nav-row > a.cur :is(.nav-icon, .doc-icon-frame), .nav-doc-row:has(> a.cur) > .nav-doc-icon { color: var(--c-accent); }
|
|
649
|
+
.nav-row > a.cur .doc-icon-frame > img, .nav-doc-row:has(> a.cur) > .nav-doc-icon .doc-icon-frame > img { visibility: hidden; }
|
|
650
|
+
.nav-row > a.cur .doc-icon-frame::after, .nav-doc-row:has(> a.cur) > .nav-doc-icon .doc-icon-frame::after {
|
|
651
|
+
content: ""; position: absolute; inset: 0; background: currentColor; mask: var(--doc-icon) center / contain no-repeat; }
|
|
652
|
+
body.sidebar-closed #sidebar-content { display: block; overflow-y: auto; scrollbar-width: none; padding: 6px 0; }
|
|
653
|
+
body.sidebar-closed #nav-groups { display: block; }
|
|
654
|
+
body.sidebar-closed #nav-docs { scrollbar-width: none; }
|
|
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),
|
|
656
|
+
body.sidebar-closed #nav-status { display: none; }
|
|
657
|
+
body.sidebar-closed #nav-groups .nav-tree { display: block; padding: 0; margin: 0; }
|
|
658
|
+
body.sidebar-closed #nav-groups .nav-row { position: relative; padding: 0; gap: 0; min-height: 36px; }
|
|
659
|
+
body.sidebar-closed #nav-groups .nav-row > a { width: 100%; height: 36px; padding: 0; justify-content: center; }
|
|
660
|
+
body.sidebar-closed #nav-groups .nav-doc-icon { position: absolute; left: 16px; z-index: 1; pointer-events: none; }
|
|
661
|
+
body.sidebar-closed #nav-groups .nav-row > a .nav-icon, body.sidebar-closed #nav-groups .nav-row > a .doc-icon-frame { margin: 0; width: 18px; height: 18px; }
|
|
662
|
+
body.sidebar-closed #nav-groups :is(.nav-item, .nav-tree, .nav-row, a)::before,
|
|
663
|
+
body.sidebar-closed #nav-groups :is(.nav-item, .nav-tree, .nav-row, a)::after { display: none; }
|
|
664
|
+
body.sidebar-closed :is(#nav-files, #nav-git) { border: 0; border-top: 1px solid var(--c-line); border-radius: 0; margin: 6px 0 0; padding: 6px 0 0; overflow: visible; }
|
|
665
|
+
body.edit #sidebar-content { display: flex; flex-direction: column; overflow: hidden; scrollbar-gutter: auto; }
|
|
666
|
+
body.edit #nav-groups { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
|
667
|
+
#nav-docs { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; }
|
|
668
|
+
#nav-tools { position: relative; flex: none; display: flex; flex-direction: column; min-height: 0; height: var(--tools-height, 220px); margin: 10px 8px 4px; padding: 12px 4px 4px; border: 1px solid var(--c-line); border-radius: 8px; background: var(--c-bg1); }
|
|
669
|
+
#tools-resize { position: absolute; top: -4px; left: 0; right: 0; height: 8px; z-index: 2; cursor: row-resize; touch-action: none; border-radius: 6px; }
|
|
670
|
+
#tools-resize:hover, #tools-resize:focus-visible, body.resizing-tools #tools-resize { background: color-mix(in srgb, var(--c-fg-muted) 20%, transparent); }
|
|
671
|
+
body.resizing-tools, body.resizing-tools * { cursor: row-resize !important; user-select: none !important; }
|
|
672
|
+
.nav-tool-cards { flex: 1; min-height: 0; overflow: auto; }
|
|
673
|
+
#nav-tools .nav-resources { height: 36px; margin: 0; border: 0; border-radius: 4px; }
|
|
674
|
+
.workspace-location { position: absolute; top: -1px; left: 10px; z-index: 3; transform: translateY(-50%); display: flex; align-items: center; gap: 4px; max-width: calc(100% - 32px); color: var(--c-fg-muted); font: 400 10px/16px var(--font-sans); cursor: default; }
|
|
675
|
+
.workspace-location > .nav-icon { flex: none; width: 14px; height: 14px; padding: 1px; background: var(--c-bg1); }
|
|
676
|
+
.workspace-address { min-width: 0; padding: 0 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: var(--c-bg1); }
|
|
677
|
+
body.sidebar-closed #nav-tools { height: auto; margin: 4px 0; padding: 0; border: 0; }
|
|
678
|
+
body.sidebar-closed :is(#tools-resize, .workspace-location) { display: none; }
|
|
679
|
+
.nav-resources { position: relative; container-type: size; flex: none; height: 42px; margin: 8px 8px 4px;
|
|
680
|
+
border: 1px solid var(--c-line); border-radius: 8px; overflow: hidden;
|
|
681
|
+
background: var(--c-bg1); font-family: var(--font-sans); }
|
|
682
|
+
.nav-resources > .nav-item, .nav-resources .nav-row { height: 100%; }
|
|
683
|
+
.nav-resources .nav-row > .nav-link { height: 100%; padding: 8px 10px; gap: 8px; }
|
|
684
|
+
.nav-resources .nav-row { cursor: default; }
|
|
685
|
+
.nav-resources .nav-link::before { display: none; }
|
|
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;
|
|
691
|
+
justify-content: center; gap: 6px; padding: 6px; pointer-events: none; text-align: center; }
|
|
692
|
+
.files-drop-hint > svg { width: 28px; height: 28px; }
|
|
693
|
+
.files-drop-hint ul { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px 8px;
|
|
694
|
+
margin: 0; padding: 0; list-style: none; font: 11px var(--doc-mono); color: var(--c-fg-muted); }
|
|
695
|
+
@container (max-height: 140px) { .files-drop-hint ul { display: none; } }
|
|
696
|
+
body.files-dragging #nav-files { border-color: var(--c-accent); }
|
|
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; }
|
|
699
|
+
body.files-over #nav-files { background: color-mix(in srgb, var(--c-accent) 10%, var(--c-bg1)); }
|
|
700
|
+
#nav-status { min-height: 16px; margin: 6px 16px 0; color: var(--c-fg-faint); font-size: 11px;
|
|
701
|
+
line-height: 1.35; }
|
|
702
|
+
#nav-status.err { color: var(--c-danger); }
|
|
703
|
+
|
|
704
|
+
/* ── main (document body) ── */
|
|
705
|
+
main { flex: 1; min-width: 0; padding: 20px 24px 50vh; }
|
|
706
|
+
article { max-width: 860px; margin: 0 auto; font-family: var(--viewer-font, system-ui, sans-serif); }
|
|
707
|
+
.external-embed { margin: 18px 0; overflow-wrap: anywhere; }
|
|
708
|
+
.plugin { position: relative; min-width: 0; border: 2px solid var(--doc-border); border-top: 0; }
|
|
709
|
+
.plugin-toolbar { position: absolute; top: 0; left: -2px; right: -2px; transform: translateY(-50%);
|
|
710
|
+
display: flex; align-items: center; gap: 4px; }
|
|
711
|
+
.plugin-toolbar::before, .plugin-toolbar::after { content: ""; border-top: 2px solid var(--doc-border); }
|
|
712
|
+
.plugin-toolbar::before { flex: 1; min-width: 12px; }
|
|
713
|
+
.plugin-toolbar::after { flex: 0 0 8px; }
|
|
714
|
+
.plugin-action[data-state="copied"] { color: var(--doc-ok); }
|
|
715
|
+
.plugin-action .ic-ok, .plugin-action[data-state="copied"] svg:not(.ic-ok) { display: none; }
|
|
716
|
+
.plugin-action[data-state="copied"] .ic-ok { display: block; }
|
|
717
|
+
.plugin.collapsed [data-plugin-action="fold"] svg { transform: rotate(-90deg); }
|
|
718
|
+
.plugin.collapsed [data-plugin-result], .plugin.collapsed [data-plugin-controls],
|
|
719
|
+
.plugin.collapsed [data-plugin-status], .plugin[data-ready="true"]:not(.collapsed) .plugin-info,
|
|
720
|
+
.plugin[data-ready="true"]:not(.collapsed) .plugin-header:has([data-plugin-controls][hidden]) { display: none; }
|
|
721
|
+
.plugin:not(.collapsed) [data-plugin-collapsed],
|
|
722
|
+
.plugin.collapsed:has([data-plugin-collapsed]:not(:empty)) .plugin-header { display: none; }
|
|
723
|
+
.plugin-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
|
|
724
|
+
gap: 8px; padding: 12px; }
|
|
725
|
+
.plugin-info { flex: 1 1 240px; min-width: 0; }
|
|
726
|
+
.plugin-info h2 { margin: 0; padding: 0; border: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
727
|
+
[data-plugin-status] { margin: 6px 0 0; color: var(--doc-fg-3); font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
|
|
728
|
+
[data-plugin-status] a { color: var(--doc-accent); text-underline-offset: 3px; }
|
|
729
|
+
[data-plugin-controls] { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
730
|
+
[data-plugin-controls][hidden], [data-plugin-status]:empty { display: none; }
|
|
731
|
+
[data-plugin-controls] button { min-height: 26px; padding: 3px 8px; border: 1px solid var(--doc-border);
|
|
732
|
+
border-radius: 4px; background: var(--doc-bg); color: var(--doc-fg); font: inherit; font-size: 11px; cursor: pointer; }
|
|
733
|
+
[data-plugin-controls] button[data-plugin-action="read"] { background: var(--doc-accent); border-color: transparent; color: var(--doc-accent-fg); }
|
|
734
|
+
[data-plugin-controls] button[data-plugin-action="disconnect"] { background: transparent; border-color: transparent; color: var(--doc-fg-3); }
|
|
735
|
+
[data-plugin-controls] button:hover { box-shadow: inset 0 0 0 1px currentColor; }
|
|
736
|
+
[data-plugin-controls] button:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: 3px; }
|
|
737
|
+
[data-plugin-controls] button:disabled { opacity: .55; cursor: wait; }
|
|
738
|
+
.plugin-content { padding: 12px; overflow: auto; }
|
|
739
|
+
.plugin-content:empty { display: none; }
|
|
740
|
+
.plugin-content :is(th, td) { padding: 6px 8px; text-align: start; border-bottom: 1px solid var(--doc-border); }
|
|
741
|
+
.plugin-content th { background: var(--doc-bg-2); color: var(--doc-fg-2); font-size: 12px; font-weight: 600; }
|
|
742
|
+
.plugin-content tr:last-child td { border-bottom: 0; }
|
|
743
|
+
.plugin-resize { display: none; height: 12px; align-items: center; justify-content: center;
|
|
744
|
+
cursor: row-resize; touch-action: none; user-select: none; }
|
|
745
|
+
.plugin[data-ready="true"]:not(.collapsed) > .plugin-resize { display: flex; }
|
|
746
|
+
.plugin-resize::before { content: ""; width: 32px; height: 3px; border-radius: 2px; background: var(--doc-border); }
|
|
747
|
+
.plugin-resize:hover::before, .plugin-resize:focus-visible::before, .plugin-resize.drag::before { background: var(--doc-accent); }
|
|
748
|
+
.plugin-resize:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: -2px; }
|
|
749
|
+
body:has(.plugin-resize.drag), body:has(.plugin-resize.drag) * { cursor: row-resize !important; user-select: none !important; }
|
|
750
|
+
body:has(.plugin-resize.drag) iframe { pointer-events: none; }
|
|
751
|
+
|
|
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; }
|
|
759
|
+
.plugin[data-operation-error="true"] [data-plugin-status] { color: var(--doc-danger); }
|
|
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; }
|
|
761
|
+
.plugin-heading::before { content: ""; border-top: 2px solid var(--doc-border); width: 8px; flex: none; }
|
|
762
|
+
.plugin-heading a { display: flex; align-items: center; gap: 6px; color: var(--doc-fg-2); text-decoration: none; flex: none; }
|
|
763
|
+
.plugin-heading img { display: block; width: 16px; height: 16px; }
|
|
764
|
+
.plugin-heading-context { color: var(--doc-fg-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
765
|
+
.plugin-heading button { font: inherit; color: var(--doc-fg-2); background: transparent; border: 0; padding: 0; cursor: pointer; white-space: nowrap; }
|
|
766
|
+
.plugin-blocks { font: inherit; font-size: 13px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
767
|
+
.plugin-blocks > :first-child, .plugin-block-columns > div > :first-child { margin-top: 0; }
|
|
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; }
|
|
770
|
+
.plugin-block-text:has(p, pre, ul, ol) { white-space: normal; }
|
|
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; }
|
|
774
|
+
.plugin-blocks strong { font-weight: 600; }
|
|
775
|
+
.plugin-blocks a { color: var(--doc-accent); text-underline-offset: 3px; }
|
|
776
|
+
.plugin-blocks code { font-family: var(--doc-mono); }
|
|
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; }
|
|
779
|
+
.plugin-block-fields > div { min-width: 0; }
|
|
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; }
|
|
783
|
+
.plugin-block-columns > div, .plugin-block-columns aside { min-width: 0; }
|
|
784
|
+
.plugin-block-columns aside { border-left: 1px solid var(--doc-border); padding-left: 12px; }
|
|
785
|
+
.plugin-block-columns aside .plugin-block-fields { grid-template-columns: 1fr; margin-top: 0; }
|
|
786
|
+
.plugin-block-section { border-top: 1px solid var(--doc-border); padding-top: 6px; margin: 10px 0 0; min-width: 0; }
|
|
787
|
+
.plugin-block-section > summary { cursor: pointer; font-size: 12px; font-weight: 600; color: var(--doc-fg-2); padding: 4px 0; }
|
|
788
|
+
.plugin-block-note { color: var(--doc-warn); font-size: 11px; border-left: 2px solid currentColor; padding-left: 10px; }
|
|
789
|
+
.plugin-block-table { overflow: auto; width: 100%; margin: 8px 0; }
|
|
790
|
+
.plugin-block-table table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
791
|
+
.plugin-block-table :is(th, td) { padding: 5px 8px; min-width: 72px; max-width: 320px; vertical-align: top; }
|
|
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; }
|
|
793
|
+
.plugin-block-diff code > span { display: inline-block; min-width: 100%; }
|
|
794
|
+
.plugin-block-diff [data-tone="success"] { color: var(--doc-ok); background: color-mix(in srgb, var(--doc-ok) 8%, transparent); }
|
|
795
|
+
.plugin-block-diff [data-tone="danger"] { color: var(--doc-danger); background: color-mix(in srgb, var(--doc-danger) 8%, transparent); }
|
|
796
|
+
.plugin-block-message { padding: 8px 0; border-bottom: 1px solid var(--doc-border); }
|
|
797
|
+
.plugin-block-message:last-child { border-bottom: 0; }
|
|
798
|
+
.plugin-message-byline { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
799
|
+
.plugin-message-byline > span:not(.plugin-avatar, .plugin-message-actions) { color: var(--doc-fg-3); font-size: 10px; }
|
|
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; }
|
|
801
|
+
.plugin-task { display: flex; align-items: baseline; gap: 8px; list-style: none; }
|
|
802
|
+
.plugin-message-actions svg { display: block; width: 14px; height: 14px; }
|
|
803
|
+
.plugin-message-actions .plugin-block-button { padding: 4px; border: 0; color: var(--doc-fg-3); }
|
|
804
|
+
.plugin-message-actions { margin-left: auto; display: flex; gap: 4px; }
|
|
805
|
+
.plugin-message-body, .plugin-reactions { margin: 4px 0 0 32px; }
|
|
806
|
+
.plugin-reactions { display: flex; flex-wrap: wrap; gap: 5px; font-size: 11px; }
|
|
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; }
|
|
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); }
|
|
809
|
+
.plugin-badge { display: inline-flex; padding: 1px 7px; border-radius: 5px; font-size: 11px; color: var(--doc-fg-2); background: var(--doc-bg-2); }
|
|
810
|
+
.plugin-badge[data-tone="accent"] { color: var(--doc-accent); background: color-mix(in srgb, var(--doc-accent) 10%, var(--doc-bg)); }
|
|
811
|
+
.plugin-badge[data-tone="success"] { color: var(--doc-ok); background: color-mix(in srgb, var(--doc-ok) 10%, var(--doc-bg)); }
|
|
812
|
+
.plugin-badge[data-tone="warning"] { color: var(--doc-warn); background: color-mix(in srgb, var(--doc-warn) 10%, var(--doc-bg)); }
|
|
813
|
+
.plugin-badge[data-tone="danger"] { color: var(--doc-danger); background: color-mix(in srgb, var(--doc-danger) 10%, var(--doc-bg)); }
|
|
814
|
+
.plugin-copy-value { display: inline-flex; align-items: center; gap: 3px; max-width: 100%; }
|
|
815
|
+
.plugin-copy { width: 22px; height: 22px; padding: 3px; border: 0; background: transparent; color: var(--doc-fg-3); cursor: pointer; flex: none; }
|
|
816
|
+
.plugin-copy svg { width: 14px; height: 14px; }
|
|
817
|
+
.plugin-block-progress { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--doc-fg-3); margin: 12px 0; }
|
|
818
|
+
.plugin-block-progress progress { flex: 1; width: 0; height: 6px; accent-color: var(--doc-accent); }
|
|
819
|
+
.plugin-block-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0; }
|
|
820
|
+
.plugin-block-actions:has([data-block-composer]) { display: block; }
|
|
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; }
|
|
822
|
+
.plugin-block-button:hover { background: var(--doc-bg-2); }
|
|
823
|
+
.plugin-block-button:disabled { opacity: .5; cursor: wait; }
|
|
824
|
+
.plugin-action[aria-pressed="true"] { color: var(--doc-accent); background: var(--doc-bg-2); }
|
|
825
|
+
.plugin-danger { color: var(--doc-danger); }
|
|
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; }
|
|
831
|
+
.plugin-editor .plugin-submit { background: var(--doc-accent); color: var(--doc-accent-fg); border-color: transparent; }
|
|
832
|
+
.plugin-editor-notice { color: var(--doc-danger); font-size: 11px; }
|
|
833
|
+
.plugin-editor-notice:empty { display: none; }
|
|
834
|
+
.plugin [hidden] { display: none !important; }
|
|
835
|
+
@container (max-width: 600px) {
|
|
836
|
+
.plugin-block-columns { grid-template-columns: minmax(0, 1fr); gap: 14px; }
|
|
837
|
+
.plugin-block-columns aside { border-left: 0; border-top: 1px solid var(--doc-border); padding: 14px 0 0; }
|
|
838
|
+
.plugin-block-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
839
|
+
.plugin-heading-context { display: none; }
|
|
840
|
+
.plugin-blocks .plugin-block-heading { font-size: 16px; }
|
|
841
|
+
.plugin-message-body, .plugin-reactions { margin-left: 0; }
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
article, article * { font-weight: 400; }
|
|
845
|
+
h1, h2, h3 { font-weight: 400; }
|
|
846
|
+
article h1 { font-size: 26px; margin: 0 0 2px; }
|
|
847
|
+
article :is(h1, h2, h3, h4, h5, h6):has(> .section-copy) { position: relative; padding-right: 32px; }
|
|
848
|
+
.section-copy { position: absolute; right: 0; top: 0; display: flex; align-items: center; justify-content: center;
|
|
849
|
+
width: 28px; height: 28px; padding: 0; border: 0; border-radius: 4px; background: transparent;
|
|
850
|
+
color: var(--c-fg-faint); cursor: pointer; }
|
|
851
|
+
.section-copy:hover, .section-copy:focus-visible { background: var(--c-raised); color: var(--c-fg); }
|
|
852
|
+
.section-copy svg { flex: none; width: 16px; height: 16px; }
|
|
853
|
+
.section-copy .ic-ok, .section-copy.ok svg:not(.ic-ok), .section-copy.error svg { display: none; }
|
|
854
|
+
.section-copy.ok .ic-ok { display: block; color: var(--doc-ok); }
|
|
855
|
+
.section-copy.error::after { content: "!"; color: var(--c-danger); }
|
|
856
|
+
article h2 { font-size: 20px; margin-top: 24px; margin-bottom: 8px; border-bottom: 1px solid
|
|
857
|
+
color-mix(in srgb, currentColor 20%, transparent); padding-bottom: 3px; }
|
|
858
|
+
article h3 { font-size: 16px; margin-top: 16px; margin-bottom: 4px; }
|
|
859
|
+
h2[id], h3[id] { scroll-margin-top: 12px; }
|
|
860
|
+
p { margin: 8px 0; }
|
|
861
|
+
ul, ol { margin: 4px 0; line-height: 1.3; }
|
|
862
|
+
li { margin: 0; }
|
|
863
|
+
li > ul, li > ol { margin: 0; }
|
|
864
|
+
.fullname { opacity: .7; margin-bottom: 8px; }
|
|
865
|
+
code { background: color-mix(in srgb, currentColor 10%, transparent);
|
|
866
|
+
padding: 1px 5px; border-radius: 4px; font-size: .9em; font-family: var(--doc-mono); }
|
|
867
|
+
:not(pre) > code { white-space: break-spaces; }
|
|
868
|
+
pre { background: color-mix(in srgb, currentColor 6%, transparent);
|
|
869
|
+
padding: 10px 14px; overflow-x: auto; line-height: 1.5; font-family: var(--doc-mono); }
|
|
870
|
+
pre code { background: none; padding: 0; font-size: 13.5px; }
|
|
871
|
+
pre code.code-foldable { display: block; padding-left: 18px; }
|
|
872
|
+
.code-line { position: relative; }
|
|
873
|
+
.code-line[hidden] { display: none; }
|
|
874
|
+
.code-section-fold { position: absolute; left: -17px; top: 1px; display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 16px; padding: 0; border: 0; border-radius: 3px; background: transparent; color: var(--doc-fg-3); cursor: pointer; }
|
|
875
|
+
.code-section-fold:hover { color: var(--doc-accent); background: var(--doc-bg-2); }
|
|
876
|
+
.code-section-fold:focus-visible { outline: 2px solid var(--doc-accent); }
|
|
877
|
+
.code-section-fold[aria-expanded="false"] svg { transform: rotate(-90deg); }
|
|
878
|
+
.code-line-folded > .code-line-text::after { content: " …"; color: var(--doc-fg-3); }
|
|
879
|
+
.fig { margin: 10px 0; }
|
|
880
|
+
/* Only direct svg / img children (= figures) follow the width; icon svgs inside html fragments
|
|
881
|
+
are left alone */
|
|
882
|
+
.fig > svg, .fig > img, .fig > video { max-width: 100%; height: auto; display: block; }
|
|
883
|
+
.video-player { container-type: inline-size; width: 100%; min-width: 0; color: var(--doc-fg-2); }
|
|
884
|
+
.video-player > video { display: block; width: 100%; height: auto; border-radius: 8px; background: #000; }
|
|
885
|
+
.video-player > video:fullscreen { max-height: none; border-radius: 0; object-fit: contain; }
|
|
886
|
+
.video-controls { display: flex; align-items: center; gap: 8px; padding: 4px 0; }
|
|
887
|
+
.video-controls > button { display: grid; place-items: center; flex: none; width: 28px; height: 28px; padding: 0;
|
|
888
|
+
border: 0; border-radius: 5px; color: inherit; background: transparent; cursor: pointer; }
|
|
889
|
+
.video-controls > button:hover, .video-controls > button[aria-expanded="true"] { color: var(--doc-fg); background: var(--doc-bg-3); }
|
|
890
|
+
.video-controls > button:disabled { opacity: .4; cursor: default; }
|
|
891
|
+
.video-controls :is(button, input):focus-visible, .video-settings input:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: 2px; }
|
|
892
|
+
:is(.video-controls, .video-settings) input { appearance: none; height: 24px; padding: 0; margin: 0; border: 0; background: transparent; cursor: pointer; }
|
|
893
|
+
:is(.video-controls, .video-settings) input:disabled { opacity: .4; cursor: default; }
|
|
894
|
+
:is(.video-controls, .video-settings) input::-webkit-slider-runnable-track { height: 2px; border-radius: 1px;
|
|
895
|
+
background: linear-gradient(to right, var(--doc-accent) var(--video-progress, 0%), var(--doc-border-2) var(--video-progress, 0%)); }
|
|
896
|
+
:is(.video-controls, .video-settings) input::-webkit-slider-thumb { appearance: none; width: 8px; height: 8px; margin-top: -3px;
|
|
897
|
+
border: 0; border-radius: 50%; background: var(--doc-accent); }
|
|
898
|
+
:is(.video-controls, .video-settings) input::-moz-range-track { height: 2px; border-radius: 1px; background: var(--doc-border-2); }
|
|
899
|
+
:is(.video-controls, .video-settings) input::-moz-range-progress { height: 2px; border-radius: 1px; background: var(--doc-accent); }
|
|
900
|
+
:is(.video-controls, .video-settings) input::-moz-range-thumb { width: 8px; height: 8px; border: 0; border-radius: 50%; background: var(--doc-accent); }
|
|
901
|
+
.video-controls .video-seek { flex: 1; min-width: 24px; width: 0; }
|
|
902
|
+
.video-controls .video-volume { flex: 0 0 60px; width: 60px; }
|
|
903
|
+
.video-time { flex: none; font-size: 11px; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
904
|
+
.video-settings { position: fixed; inset: auto; box-sizing: border-box; width: min(240px, calc(100vw - 16px)); margin: 0;
|
|
905
|
+
padding: 12px; border: 1px solid var(--doc-border); border-radius: 8px; color: var(--doc-fg);
|
|
906
|
+
background: var(--doc-bg-2); box-shadow: 0 8px 24px var(--c-shadow); }
|
|
907
|
+
.video-settings label { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px 12px; font-size: 12px; }
|
|
908
|
+
.video-speed-value { color: var(--doc-fg-2); font-variant-numeric: tabular-nums; }
|
|
909
|
+
.video-settings .video-speed { grid-column: 1 / -1; width: 100%; }
|
|
910
|
+
.video-status { color: var(--doc-danger); font-size: 12px; margin: 6px 0; }
|
|
911
|
+
.video-player [hidden] { display: none; }
|
|
912
|
+
@container (max-width: 420px) { .video-controls .video-volume { display: none; } }
|
|
913
|
+
.fig-frame { display: block; max-width: 100%; border: 0; background: transparent; }
|
|
914
|
+
/* Code and file blocks share a border label; clicking it collapses the content. */
|
|
915
|
+
.codeblk { position: relative; margin: 12px 0; border: 2px solid var(--doc-border); border-top: 0; }
|
|
916
|
+
.codeblk > pre { margin: 0; background: transparent; padding: 8px 12px 6px; }
|
|
917
|
+
.codeblk-head { position: absolute; top: 0; left: -2px; right: -2px; transform: translateY(-50%);
|
|
918
|
+
display: flex; align-items: center; gap: 4px; cursor: pointer; }
|
|
919
|
+
.codeblk-head::before, .codeblk-head::after, .codeblk-lang::before { content: ""; border-top: 2px solid var(--doc-border); }
|
|
920
|
+
.codeblk-head::before { flex: 1; min-width: 12px; }
|
|
921
|
+
.codeblk-head::after { order: 2; flex: 0 0 8px; }
|
|
922
|
+
.codeblk-lang { order: -1; display: flex; align-items: center; gap: 4px;
|
|
923
|
+
font-family: var(--doc-mono); font-size: 11px; color: var(--doc-fg-2); }
|
|
924
|
+
.codeblk-lang::before { flex: 0 0 8px; }
|
|
925
|
+
.codeblk-filename { margin: 0; min-width: 0; max-width: 70%;
|
|
926
|
+
font-family: var(--doc-mono); font-size: 11px; color: var(--doc-fg-2);
|
|
927
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
928
|
+
.codefold svg { transition: transform 150ms ease; }
|
|
929
|
+
.codeblk.collapsed .codefold svg { transform: rotate(-90deg); }
|
|
930
|
+
@media (prefers-reduced-motion: reduce) { .codefold svg { transition: none; } }
|
|
931
|
+
.codeblk.collapsed > pre { display: none; }
|
|
932
|
+
.codeblk.collapsed { min-height: 1px; border-bottom: 0; margin-bottom: 18px; }
|
|
933
|
+
.codeblk :is(.codecopy, .codefold), .plugin-action { display: flex; align-items: center; justify-content: center;
|
|
934
|
+
flex-shrink: 0; width: 16px; height: 16px; padding: 0; border: none; border-radius: 6px; background: none;
|
|
935
|
+
color: var(--doc-fg-3); cursor: pointer; }
|
|
936
|
+
.codeblk :is(.codecopy, .codefold):hover, .codeblk :is(.codecopy, .codefold):focus-visible,
|
|
937
|
+
.plugin-action:hover, .plugin-action:focus-visible { color: var(--doc-accent);
|
|
938
|
+
background: color-mix(in srgb, currentColor 8%, transparent); }
|
|
939
|
+
.codeblk .codecopy .ic-ok { display: none; }
|
|
940
|
+
.codeblk .codecopy.ok .ic-copy { display: none; }
|
|
941
|
+
.textfile-unavailable { margin: 0; padding: 12px; color: var(--doc-fg-2); font-size: 12px; }
|
|
942
|
+
.textfile.unavailable .codeblk-head { cursor: default; }
|
|
943
|
+
.codeblk .codecopy.ok .ic-ok { display: block; color: var(--doc-ok); }
|
|
944
|
+
table { border-collapse: collapse; font-size: 14px; }
|
|
945
|
+
th, td { border: 1px solid color-mix(in srgb, currentColor 25%, transparent); padding: 4px 10px; }
|
|
946
|
+
.data-table { position: relative; min-width: 0; width: 100%; margin: 12px 0; }
|
|
947
|
+
.data-table button { color: inherit; background: transparent; font: inherit; cursor: pointer; }
|
|
948
|
+
.data-table button:focus-visible, .data-table-scroll:focus-visible { outline: 2px solid var(--doc-accent); outline-offset: -2px; }
|
|
949
|
+
.data-table-scroll { overflow: auto; max-height: min(480px, 65vh); border: 1px solid var(--doc-border); border-radius: 8px; box-sizing: border-box; }
|
|
950
|
+
.data-table-resize { position: absolute; bottom: -5px; left: 8px; right: 8px; height: 10px; z-index: 3; cursor: ns-resize; touch-action: none; }
|
|
951
|
+
.data-table-resize::after { content: ""; position: absolute; top: 4px; left: calc(50% - 14px); width: 28px; height: 2px; background: var(--doc-border-2); border-radius: 2px; }
|
|
952
|
+
.data-table-resize:hover::after, .data-table-resize:focus-visible::after { background: var(--doc-accent); }
|
|
953
|
+
.data-table-resize:focus-visible { outline: 2px solid var(--doc-accent); border-radius: 4px; }
|
|
954
|
+
.data-table table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; line-height: 1.4; }
|
|
955
|
+
.data-table :is(th, td) { border: 0; border-bottom: 1px solid color-mix(in srgb, var(--doc-border) 65%, transparent); padding: 5px 12px; text-align: left; white-space: pre-wrap; min-width: 90px; max-width: 480px; overflow-wrap: anywhere; }
|
|
956
|
+
.data-table thead th { position: sticky; top: 0; z-index: 2; background: var(--doc-bg-2, var(--doc-bg)); color: var(--doc-fg-2); font-size: 12px; font-weight: 500; }
|
|
957
|
+
.data-table[data-table-mode="header"] thead th { border-bottom: 3px double var(--doc-border-2); }
|
|
958
|
+
.data-table thead th:has(button) { padding: 0; }
|
|
959
|
+
.data-table thead button { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; text-align: left; padding: 5px 12px; border: 0; font-weight: inherit; }
|
|
960
|
+
.data-table th:first-child { min-width: 24px; width: 24px; color: var(--doc-fg-3); font-size: 11px; font-weight: 400; text-align: right; font-variant-numeric: tabular-nums; }
|
|
961
|
+
.data-table-sort-mark { flex: none; opacity: 0; }
|
|
962
|
+
.data-table thead button:is(:hover, :focus-visible) .data-table-sort-mark { opacity: .5; }
|
|
963
|
+
.data-table th[aria-sort] { color: var(--doc-accent); }
|
|
964
|
+
.data-table th[aria-sort] .data-table-sort-mark { opacity: 1; }
|
|
965
|
+
.data-table tbody tr:last-child > * { border-bottom: 0; }
|
|
966
|
+
.data-table tbody tr:hover { background: color-mix(in srgb, var(--doc-fg) 3%, transparent); }
|
|
967
|
+
|
|
968
|
+
article a { color: var(--doc-accent); }
|
|
969
|
+
.ph { border: 1px dashed color-mix(in srgb, currentColor 40%, transparent);
|
|
970
|
+
border-radius: 8px; padding: 18px; text-align: center; margin: 16px 0; }
|
|
971
|
+
.ph span { opacity: .5; font-size: 13px; }
|
|
972
|
+
.cap { opacity: .7; font-size: 13px; margin-top: 6px; white-space: pre-line; }
|
|
973
|
+
.callout { border: 1px solid var(--doc-border); border-radius: 8px; padding: 8px 14px; margin: 10px 0;
|
|
974
|
+
background: color-mix(in srgb, var(--doc-accent) 3%, var(--doc-bg-2)); overflow-wrap: anywhere; }
|
|
975
|
+
.callout-title { display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
|
|
976
|
+
color: var(--doc-fg); font-size: 13px; font-weight: 650; line-height: 1.5; }
|
|
977
|
+
.callout-title::before { content: ""; flex: none; width: 18px; height: 2px; background: var(--doc-accent); }
|
|
978
|
+
.callout-body { color: var(--doc-fg-2); font-size: 14px; line-height: 1.5; }
|
|
979
|
+
.callout-body > p { margin: 6px 0; }
|
|
980
|
+
.callout-body > p:first-child { color: var(--doc-fg); font-size: 15px; }
|
|
981
|
+
.callout-body > :is(ul, ol) { display: grid; gap: 2px; margin: 6px 0; padding-left: 20px; }
|
|
982
|
+
.callout-body > :is(ul, ol) > li { padding-left: 4px; }
|
|
983
|
+
.callout-body > :is(ul, ol) > li::marker { color: var(--doc-accent); }
|
|
984
|
+
.callout-body > :first-child { margin-top: 0; }
|
|
985
|
+
.callout-body > :last-child { margin-bottom: 0; }
|
|
986
|
+
.errors { border: 1px solid var(--doc-danger); border-radius: 8px; padding: 10px 16px;
|
|
987
|
+
margin-bottom: 16px; color: var(--doc-danger); font-size: 13px; }
|
|
988
|
+
.in-toc { flex: 1; min-width: 0; display: flex; align-items: center; gap: 12px; margin: 0 10px 0 0; }
|
|
989
|
+
.toc-current { position: relative; flex: none; width: 120px; min-width: 0; max-width: calc(100% - 36px); padding: 4px 20px 4px 4px;
|
|
990
|
+
overflow: hidden; white-space: nowrap; text-overflow: ellipsis; border: 0; border-radius: 4px;
|
|
991
|
+
background: transparent; color: var(--doc-fg-2); font: 12px/20px var(--font-sans); text-align: left; cursor: pointer; }
|
|
992
|
+
.toc-current::after { content: ""; position: absolute; right: 6px; top: 10px; width: 5px; height: 5px;
|
|
993
|
+
border-right: 1px solid currentColor; border-bottom: 1px solid currentColor; transform: rotate(45deg); }
|
|
994
|
+
.toc-current:hover, .toc-current[aria-expanded="true"] { background: var(--c-raised); color: var(--doc-accent); }
|
|
995
|
+
.toc-options { position: fixed; margin: 0; padding: 6px; max-height: min(420px, calc(100dvh - 60px)); overflow: auto;
|
|
996
|
+
list-style: none; border: 1px solid var(--doc-border); border-radius: 6px; background: var(--doc-bg); color: var(--doc-fg);
|
|
997
|
+
box-shadow: 0 8px 24px var(--c-shadow); }
|
|
998
|
+
.in-toc a { display: block; padding: 7px 10px; border-radius: 4px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
|
|
999
|
+
text-decoration: none; font: 12px/1.5 var(--font-sans); color: var(--doc-fg-2); }
|
|
1000
|
+
.in-toc .lvl2 a { padding-left: 20px; }
|
|
1001
|
+
.in-toc .lvl3 a { padding-left: 30px; }
|
|
1002
|
+
.in-toc a:hover, .in-toc a[aria-current] { color: var(--doc-accent); background: color-mix(in srgb, var(--doc-accent) 8%, transparent); }
|
|
1003
|
+
.toc-seek { flex: 1; appearance: none; width: 0; min-width: 24px; height: 24px; margin: 0; padding: 0; background: transparent; cursor: ew-resize; }
|
|
1004
|
+
.toc-seek::-webkit-slider-runnable-track { height: 2px; background: linear-gradient(to right, var(--doc-accent) var(--toc-progress, 0%), var(--doc-border) var(--toc-progress, 0%)); }
|
|
1005
|
+
.toc-seek::-moz-range-track { height: 2px; background: linear-gradient(to right, var(--doc-accent) var(--toc-progress, 0%), var(--doc-border) var(--toc-progress, 0%)); }
|
|
1006
|
+
.toc-seek::-webkit-slider-thumb { appearance: none; width: 8px; height: 8px; margin-top: -3px; border: 0; border-radius: 50%; background: var(--doc-accent); }
|
|
1007
|
+
.toc-seek::-moz-range-thumb { width: 8px; height: 8px; border: 0; border-radius: 50%; background: var(--doc-accent); }
|
|
1008
|
+
.toc-seek:disabled { opacity: .35; cursor: default; }
|
|
1009
|
+
html:has(.page-actions .toc-seek), .preview-pane:has(> .pane-actions .toc-seek) { scrollbar-width: none; }
|
|
1010
|
+
html:has(.page-actions .toc-seek)::-webkit-scrollbar,
|
|
1011
|
+
.preview-pane:has(> .pane-actions .toc-seek)::-webkit-scrollbar { display: none; }
|
|
1012
|
+
main :is(h1, h2, h3)[id] { scroll-margin-top: 52px; }
|
|
1013
|
+
article strong { font-weight: 700; }
|
|
1014
|
+
article em { font-style: italic; }
|
|
1015
|
+
article blockquote { margin: 16px 0; padding: 4px 16px; border-left: 3px solid var(--doc-border-2); color: var(--doc-fg-2); }
|
|
1016
|
+
article blockquote > :first-child { margin-top: 0; }
|
|
1017
|
+
article blockquote > :last-child { margin-bottom: 0; }
|
|
1018
|
+
ul.idx { list-style: none; padding: 0; }
|
|
1019
|
+
ul.idx li { margin: 10px 0; }
|
|
1020
|
+
details summary { cursor: pointer; opacity: .8; }
|
|
1021
|
+
/* Figure and term file links show previews on hover. */
|
|
1022
|
+
.figref, .dictref { color: var(--doc-accent-2); text-decoration: none;
|
|
1023
|
+
border-bottom: 1px dotted currentColor; position: relative; cursor: pointer; }
|
|
1024
|
+
.figref .figpop, .dictref .dictpop { display: none; position: absolute; left: 50%;
|
|
1025
|
+
transform: translateX(-50%); top: calc(100% + 6px); z-index: 5;
|
|
1026
|
+
width: min(560px, 78vw); padding: 12px 14px;
|
|
1027
|
+
background: var(--doc-panel); border: 1px solid var(--doc-border);
|
|
1028
|
+
border-radius: 8px; box-shadow: 0 4px 16px color-mix(in srgb, var(--doc-fg) 18%, transparent);
|
|
1029
|
+
color: var(--doc-fg); font-size: 13px; line-height: 1.7; }
|
|
1030
|
+
.figref:hover .figpop, .figref:focus-visible .figpop, .dictref:hover .dictpop { display: block; }
|
|
1031
|
+
.figpop svg { max-width: 100%; height: auto; display: block; }
|
|
1032
|
+
.figpop .fig-frame { width: 100% !important; }
|
|
1033
|
+
.dictref .dictpop { width: min(420px, 70vw); }
|
|
1034
|
+
.dictpop-description { display: block; margin-top: 8px; padding-top: 8px;
|
|
1035
|
+
border-top: 1px solid var(--doc-border); color: var(--doc-fg-3); font-size: 12px; }
|
|
1036
|
+
.dictref:focus-visible .dictpop { display: block; }
|
|
1037
|
+
.terms { margin: 18px 0; }
|
|
1038
|
+
.terms .dict-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; color: var(--doc-fg-3); font-size: 12px; }
|
|
1039
|
+
.terms .dict-toolbar label { display: flex; align-items: center; gap: 8px; }
|
|
1040
|
+
.terms select { font: inherit; color: var(--doc-fg); background: var(--doc-panel); border: 1px solid var(--doc-border); border-radius: 4px; padding: 4px 8px; }
|
|
1041
|
+
.terms .dict { display: grid; gap: 8px; margin: 0; }
|
|
1042
|
+
.terms .dict-card { margin: 0; padding: 0 12px 6px; border: 1px solid var(--doc-border);
|
|
1043
|
+
border-radius: 4px; min-width: 0; overflow-wrap: anywhere; }
|
|
1044
|
+
.terms legend { display: flex; align-items: center; gap: 6px; max-width: 100%; padding: 0 4px; font-size: 15px; font-weight: 600;
|
|
1045
|
+
line-height: 1.5; color: var(--doc-fg); scroll-margin-top: 16px; }
|
|
1046
|
+
.terms .term-definition { margin: 2px 0 0; font-size: 13.5px;
|
|
1047
|
+
line-height: 1.6; white-space: pre-line; }
|
|
1048
|
+
.terms .dict-card:has(:target) { border-color: var(--doc-accent-2); }
|
|
1049
|
+
.terms .term-icon { flex: none; color: var(--doc-fg-3); }
|
|
1050
|
+
.terms .term-name { position: relative; min-width: 0; text-decoration: none; overflow-wrap: anywhere; }
|
|
1051
|
+
.terms .term-name:hover { text-decoration: underline; text-underline-offset: 3px; }
|
|
1052
|
+
.terms .term-description { display: none; position: absolute; left: 0; top: 100%; z-index: 6;
|
|
1053
|
+
width: max-content; max-width: min(360px, calc(100vw - 96px)); box-sizing: border-box;
|
|
1054
|
+
padding: 10px 14px; margin-top: 8px; border: 1px solid var(--doc-border); border-radius: 6px;
|
|
1055
|
+
background: var(--doc-panel); color: var(--doc-fg); font-family: var(--font-sans);
|
|
1056
|
+
font-size: 13px; font-weight: 400; line-height: 1.7; white-space: pre-line;
|
|
1057
|
+
box-shadow: 0 4px 16px color-mix(in srgb, var(--doc-fg) 12%, transparent); }
|
|
1058
|
+
.terms .term-description::before { content: ""; position: absolute; left: 0; right: 0; top: -9px; height: 9px; }
|
|
1059
|
+
.terms .term-name:hover .term-description,
|
|
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
|
+
|
|
1095
|
+
.resource-library { width: 100%; max-width: none; }
|
|
1096
|
+
.git-page { padding-top: 8px; font: 12px/1.55 var(--font-sans); }
|
|
1097
|
+
.git-controls { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
1098
|
+
.git-toolbar, .git-actions, .git-branches { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-height: 30px; }
|
|
1099
|
+
.git-toolbar { border-bottom: 1px solid var(--c-line); margin: 4px 0 8px; padding-inline: 6px; }
|
|
1100
|
+
.git-toolbar strong { font-size: 13px; font-weight: 500; }
|
|
1101
|
+
.git-branches { gap: 12px; margin-bottom: 8px; }
|
|
1102
|
+
.git-branches > form { flex: 1; }
|
|
1103
|
+
.git-branches input { width: 150px; flex: 1; }
|
|
1104
|
+
.git-actions { margin: 6px 0; }
|
|
1105
|
+
.git-page :is(input:not([type=checkbox]), textarea, select) { box-sizing: border-box; min-width: 0; max-width: 100%; border: 1px solid var(--c-line); border-radius: 4px; padding: 4px 8px; background: var(--c-bg1); color: var(--c-fg); font: inherit; }
|
|
1106
|
+
.git-page :is(input:not([type=checkbox]), select) { height: 28px; }
|
|
1107
|
+
.git-branches :is(input, select), .git-actions select { border-color: transparent; background: transparent; }
|
|
1108
|
+
.git-page :is(input, textarea, select):focus-visible { outline: 1px solid var(--c-fg-muted); outline-offset: -1px; }
|
|
1109
|
+
.git-page select option { background: var(--c-bg1); color: var(--c-fg); }
|
|
1110
|
+
.git-page .git-text-button { width: auto; padding: 0 8px; font: inherit; }
|
|
1111
|
+
.git-page .pbtn:disabled { opacity: .45; cursor: default; }
|
|
1112
|
+
.git-page .pbtn:disabled:hover { background: transparent; color: var(--c-fg-muted); }
|
|
1113
|
+
.git-page .resource-item > summary .pbtn { width: 24px; height: 24px; }
|
|
1114
|
+
.git-field { display: flex; flex-direction: column; gap: 4px; color: var(--c-fg-muted); }
|
|
1115
|
+
.git-field textarea { resize: vertical; }
|
|
1116
|
+
.git-form { display: flex; flex-direction: column; gap: 8px; }
|
|
1117
|
+
.git-form > button { align-self: flex-start; }
|
|
1118
|
+
.git-commit { margin: 10px 0 18px; }
|
|
1119
|
+
.git-commit textarea { height: 54px; }
|
|
1120
|
+
.git-caption, .git-notice { margin: 6px 0; font-size: 11px; color: var(--c-fg-muted); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1121
|
+
.git-notice:empty { display: none; }
|
|
1122
|
+
.git-status { width: 2ch; flex: none; white-space: pre; color: var(--c-fg-muted); }
|
|
1123
|
+
.git-diff { max-height: 320px; overflow: auto; padding: 8px; background: var(--c-surface); font: 11px/1.6 var(--editor-font, ${MONO_FONT}); }
|
|
1124
|
+
.git-checkbox { display: flex; align-items: center; gap: 6px; }
|
|
1125
|
+
.resource-tabs { display: flex; gap: 4px; overflow-x: auto; border-bottom: 1px solid var(--doc-border); margin-bottom: 18px; }
|
|
1126
|
+
.resource-tabs a { padding: 9px 15px; color: var(--doc-fg-3); text-decoration: none; white-space: nowrap; font-size: 13px; border-bottom: 2px solid transparent; }
|
|
1127
|
+
.resource-tabs a:hover { color: var(--doc-fg); background: color-mix(in srgb, var(--doc-fg) 4%, transparent); }
|
|
1128
|
+
.resource-tabs a[aria-current] { color: var(--doc-accent); border-bottom-color: currentColor; }
|
|
1129
|
+
.resource-toolbar { display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 6px;
|
|
1130
|
+
border-bottom: 1px solid var(--c-line); color: var(--c-fg-muted); font-size: 11px; }
|
|
1131
|
+
.resource-toolbar > span { flex: none; }
|
|
1132
|
+
.resource-search {display:flex;align-items:center;gap:5px;flex:0 1 140px;min-width:0;height:24px;border-bottom:1px solid transparent;color:var(--c-fg-faint);}
|
|
1133
|
+
.resource-search > svg {flex:none;width:13px;height:13px;}
|
|
1134
|
+
.resource-search input {min-width:0;width:100%;height:22px;padding:0;border:0;border-radius:0;background:transparent;color:var(--c-fg);font:inherit;}
|
|
1135
|
+
.resource-search input::placeholder {color:var(--c-fg-faint);}
|
|
1136
|
+
.resource-search:focus-within {border-bottom-color:var(--c-accent);color:var(--c-fg-muted);}
|
|
1137
|
+
.resource-search input:focus-visible {outline:none;}
|
|
1138
|
+
.resource-toolbar select { flex: 0 1 auto; min-width: 50px; max-width: 110px; height: 24px; padding: 0 4px;
|
|
1139
|
+
border: 0; border-radius: 0; background: transparent; color: var(--c-fg-muted); font: inherit; }
|
|
1140
|
+
.resource-toolbar select option { background: var(--c-bg1); color: var(--c-fg); }
|
|
1141
|
+
.resource-count { margin-left: auto; color: var(--doc-fg-3); font-variant-numeric: tabular-nums; }
|
|
1142
|
+
.resource-items { display: flex; flex-direction: column; }
|
|
1143
|
+
.resource-item { min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
1144
|
+
.resource-item[hidden], .resource-empty[hidden] { display: none; }
|
|
1145
|
+
.resource-item > summary { display: flex; align-items: center; gap: 8px; min-width: 0; height: 28px; padding: 0 6px;
|
|
1146
|
+
list-style: none; color: var(--doc-fg); font-size: 13px; cursor: pointer; }
|
|
1147
|
+
.resource-item > summary::-webkit-details-marker { display: none; }
|
|
1148
|
+
.resource-item > summary::before { content: ""; flex: none; width: 5px; height: 5px; margin: 0 4px 0 2px;
|
|
1149
|
+
border-right: 1px solid var(--c-fg-faint); border-bottom: 1px solid var(--c-fg-faint); transform: rotate(-45deg); }
|
|
1150
|
+
.resource-item[open] > summary::before { transform: rotate(45deg); }
|
|
1151
|
+
.resource-item > summary:hover { background: var(--c-surface); }
|
|
1152
|
+
.resource-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1153
|
+
.resource-item a.resource-name { color: inherit; text-decoration: none; }
|
|
1154
|
+
.resource-item a.resource-name:hover { color: var(--doc-accent); text-decoration: underline; }
|
|
1155
|
+
.resource-body { min-width: 0; padding: 6px 8px 10px 24px; }
|
|
1156
|
+
.resource-source { display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; max-width: 100%;
|
|
1157
|
+
overflow: hidden; white-space: nowrap; color: var(--doc-fg-3); font-family: var(--doc-mono); font-size: 11px; text-decoration: none; }
|
|
1158
|
+
.resource-item > summary > .resource-source { margin-left: auto; max-width: 50%; }
|
|
1159
|
+
.resource-source:hover { color: var(--doc-accent); text-decoration: underline; }
|
|
1160
|
+
.resource-source span { flex: none; opacity: .7; }
|
|
1161
|
+
.resource-source .resource-source-file { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; opacity: 1; }
|
|
1162
|
+
.resource-badge { flex: none; color: var(--doc-fg-2); font: 11px var(--doc-mono); }
|
|
1163
|
+
.resource-item .term-definition { margin: 0; white-space: pre-wrap; font-size: 13px; }
|
|
1164
|
+
.resource-description { color: var(--doc-fg-3); white-space: pre-wrap; font-size: 12px; margin: 6px 0 0; }
|
|
1165
|
+
.resource-item .codeblk { margin: 0; }
|
|
1166
|
+
.resource-item .codeblk > pre { max-height: 260px; overflow: auto; }
|
|
1167
|
+
.resource-usage { flex: none; margin-left: auto; color: var(--doc-fg-3); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
1168
|
+
.resource-sources { display: flex; flex-direction: column; gap: 4px; }
|
|
1169
|
+
.resource-empty { padding: 20px 6px; color: var(--doc-fg-3); font-size: 12px; }
|
|
1170
|
+
.resource-errors { margin: 0 0 16px; color: var(--doc-danger); font-size: 12px; overflow-wrap: anywhere; }
|
|
1171
|
+
.resource-errors summary { cursor: pointer; }
|
|
1172
|
+
.resource-library .resource-tabs { margin-bottom: 8px; }
|
|
1173
|
+
.resource-library .files-viewport { max-height: calc(100dvh - 76px); min-height: 0; }
|
|
1174
|
+
.resource-library .files-table :is(th, td) { border: 0; }
|
|
1175
|
+
.resource-library .files-table thead th { position: sticky; top: 0; z-index: 1; border-bottom: 1px solid var(--c-line); background: var(--c-bg1); font-size: 11px; color: var(--c-fg-muted); font-weight: 500; }
|
|
1176
|
+
@media(max-width:600px) { .resource-tabs a { padding-inline: 10px; } .resource-toolbar { gap: 8px; } }
|
|
1177
|
+
`;
|
|
1178
|
+
// edit mode only (right panel / new doc form). Uses the viewer's shared background / rules and
|
|
1179
|
+
// button / input styles. To keep CM as wide as possible the fm is compact, and the toggles
|
|
1180
|
+
// (autosave / correspondence display / fm visibility) are gathered in the header gear's hover
|
|
1181
|
+
// menu. Width is drag-resizable.
|
|
1182
|
+
const EDIT_CSS = `
|
|
1183
|
+
body.git-diff-hidden .cm-lineNumbers :is(.cm-git-added, .cm-git-modified, .cm-git-deleted-start, .cm-git-deleted-end) { box-shadow: none; }
|
|
1184
|
+
body.git-diff-hidden .cm-line:is(.cm-git-added, .cm-git-modified, .cm-git-deleted-start, .cm-git-deleted-end) { background-color: transparent; }
|
|
1185
|
+
#site-icon-slot input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
|
1186
|
+
#site-icon-slot:hover, #site-icon-slot:focus-within, #sidebar-head.over #site-icon-slot { background: var(--c-line); border-radius: 3px; }
|
|
1187
|
+
#site-icon-slot:focus-within, #sidebar-head.over #site-icon-slot { outline: 1px solid var(--c-fg-muted); outline-offset: 2px; }
|
|
1188
|
+
body.sidebar-closed #site-icon-slot input { display: none; }
|
|
1189
|
+
/* Right panel = header / properties / editor stacked on one surface, separated by rules (no
|
|
1190
|
+
nested frames). */
|
|
1191
|
+
#panel { width: var(--panel-width, min(760px, 52vw)); flex-shrink: 0; position: sticky; top: 0; height: 100vh;
|
|
1192
|
+
display: flex; flex-direction: column; font-size: 12.5px; line-height: 1.6;
|
|
1193
|
+
border-left: 1px solid var(--c-line); background: var(--c-bg1); }
|
|
1194
|
+
body.textfile-open #phead :is(.fm-language, #status, #validate, #save, #split-preview) { display: none; }
|
|
1195
|
+
#file-editor { display: flex; flex-direction: column; flex: 1; min-height: 0; color: var(--doc-fg); background: var(--doc-bg); }
|
|
1196
|
+
.file-editor-heading { display: flex; align-items: center; flex: 1; min-width: 0; gap: 6px; margin-left: 6px; color: var(--c-fg-muted); }
|
|
1197
|
+
.file-editor-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--doc-mono); }
|
|
1198
|
+
.file-editor-heading .cm-textfile-controls { display: inline-flex; align-items: center; flex: none; gap: 2px; }
|
|
1199
|
+
.file-editor-heading .cm-textfile-save-status { max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1200
|
+
.file-editor-heading .cm-inline-fold { display: none; }
|
|
1201
|
+
.file-editor-heading [hidden] { display: none; }
|
|
1202
|
+
#file-editor > .cm-textfile { display: flex; flex-direction: column; flex: 1; min-height: 0; }
|
|
1203
|
+
#file-editor .cm-textfile-editor { flex: 1; min-height: 0; }
|
|
1204
|
+
#file-editor .cm-editor { height: 100%; }
|
|
1205
|
+
#file-editor .cm-scroller { overflow: auto; }
|
|
1206
|
+
#file-editor .cm-textfile-status { padding: 8px 12px; white-space: pre-wrap; }
|
|
1207
|
+
#file-editor .cm-textfile-status[hidden] { display: none; }
|
|
1208
|
+
#file-editor .cm-textfile-status.error { color: var(--doc-danger); }
|
|
1209
|
+
|
|
1210
|
+
/* header band (same height and rule as sidebar-head) */
|
|
1211
|
+
#phead { flex: none; display: flex; align-items: center; gap: 2px; height: 36px; padding: 0 10px 0 14px;
|
|
1212
|
+
border-bottom: 1px solid var(--c-line); }
|
|
1213
|
+
/* Document metadata is edited in the tree, without changing the row's size. */
|
|
1214
|
+
body.edit #nav-groups { --nav-rail: 22px; --nav-icon-half: 9px; }
|
|
1215
|
+
.nav-doc-row { padding-left: 12px; gap: 3px; min-height: 38px; }
|
|
1216
|
+
.nav-tree .nav-doc-row { padding-left: var(--nav-pad); }
|
|
1217
|
+
.nav-doc-row:has(> a.cur) { background: var(--c-raised); }
|
|
1218
|
+
.nav-row.nav-doc-row > a { padding: 4px 2px; background: transparent; }
|
|
1219
|
+
.nav-doc-row > a::before { display: none; }
|
|
1220
|
+
.nav-doc-name { display: flex; flex-direction: column; min-width: 0; line-height: 17px; }
|
|
1221
|
+
.nav-doc-icon { position: relative; flex: none; display: flex; align-items: center; justify-content: center;
|
|
1222
|
+
width: 20px; height: 24px; cursor: pointer; border-radius: 3px; color: var(--c-fg-faint); }
|
|
1223
|
+
.nav-doc-icon:hover, .nav-doc-icon.over, .nav-doc-icon:focus-within { background: var(--c-line); color: var(--c-fg); }
|
|
1224
|
+
.nav-doc-icon input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
|
|
1225
|
+
.nav-doc-icon > :is(.nav-icon, .doc-icon-frame) { width: 18px; height: 18px; }
|
|
1226
|
+
.nav-icon-clear { position: absolute; right: -4px; top: -4px; width: 13px; height: 13px; padding: 0;
|
|
1227
|
+
border: 1px solid var(--c-line); border-radius: 50%; font: 10px/11px sans-serif;
|
|
1228
|
+
color: var(--c-fg); background: var(--c-surface); cursor: pointer; visibility: hidden; }
|
|
1229
|
+
.nav-doc-icon:hover .nav-icon-clear, .nav-doc-icon:focus-within .nav-icon-clear { visibility: visible; }
|
|
1230
|
+
.nav-icon-clear:hover { color: var(--c-danger); }
|
|
1231
|
+
.nav-rename-form { position: absolute; top: 0; bottom: 0; z-index: 4; display: flex; flex-direction: column; justify-content: center;
|
|
1232
|
+
background: var(--c-bg0); }
|
|
1233
|
+
.nav-rename-form input { width: 100%; min-width: 0; box-sizing: border-box; color: var(--c-fg);
|
|
1234
|
+
background: transparent; border: 0; border-radius: 2px; padding: 0 2px; font: inherit; line-height: 17px; }
|
|
1235
|
+
.nav-rename-form input[name="name"] { font: 10px/12px var(--doc-mono); color: var(--c-fg-faint); }
|
|
1236
|
+
.nav-rename-form input:invalid { outline: 1px solid var(--c-danger); }
|
|
1237
|
+
.nav-rename-form input:focus, .nav-new input:focus { outline: 1px solid var(--c-line); }
|
|
1238
|
+
@media (forced-colors: active) { .nav-rename-form input:focus, .nav-new input:focus { outline: 1px solid CanvasText; } }
|
|
1239
|
+
.nav-doc-row.renaming > a { visibility: hidden; }
|
|
1240
|
+
.nav-delete:hover, .nav-delete:focus-visible { color: var(--c-danger); }
|
|
1241
|
+
#status { flex: 1; min-width: 0; padding: 0 8px; text-align: right; color: var(--c-fg-faint);
|
|
1242
|
+
font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1243
|
+
#status.err { color: var(--c-danger); }
|
|
1244
|
+
.cm-file-preview { display: inline-flex; align-items: center; justify-content: center;
|
|
1245
|
+
width: 20px; height: 20px; margin-left: 4px; vertical-align: middle; color: var(--c-fg-faint); }
|
|
1246
|
+
.cm-file-preview[data-state="valid"] { color: var(--c-ok); }
|
|
1247
|
+
.cm-file-preview[data-state="error"] { color: var(--c-danger); }
|
|
1248
|
+
.cm-file-preview[data-state="loading"] { color: var(--c-accent); }
|
|
1249
|
+
.cm-file-preview[data-state="loading"] svg { animation: preview-spin 1s linear infinite; }
|
|
1250
|
+
@keyframes preview-spin { to { transform: rotate(360deg); } }
|
|
1251
|
+
@media (prefers-reduced-motion: reduce) { .cm-file-preview[data-state="loading"] svg { animation: none; } }
|
|
1252
|
+
.app-dialog { width: min(460px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); padding: 22px;
|
|
1253
|
+
color: var(--c-fg); background: color-mix(in srgb, var(--c-bg1) 90%, transparent);
|
|
1254
|
+
border: 1px solid var(--c-fg-faint); border-radius: 10px; box-shadow: 0 16px 48px #0004; backdrop-filter: blur(16px); }
|
|
1255
|
+
.app-dialog::backdrop { background: #0005; backdrop-filter: blur(2px); }
|
|
1256
|
+
.app-dialog h2 { margin: 0 0 14px; font-size: 16px; overflow-wrap: anywhere; }
|
|
1257
|
+
.app-dialog p { margin: 0 0 22px; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--c-fg-muted); }
|
|
1258
|
+
.dialog-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
1259
|
+
.app-dialog button { padding: 7px 14px; color: inherit; background: var(--c-bg0); border: 1px solid var(--c-line); border-radius: 5px; cursor: pointer; }
|
|
1260
|
+
.app-dialog button:hover { background: var(--c-raised); border-color: var(--c-fg-muted); }
|
|
1261
|
+
.app-dialog button:focus-visible { outline-color: var(--c-fg-muted); }
|
|
1262
|
+
.app-dialog button.danger { color: var(--c-danger); }
|
|
1263
|
+
.app-dialog:has(.conflict-columns) { width: min(1000px, calc(100vw - 32px)); }
|
|
1264
|
+
.conflict-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; margin-bottom: 16px; }
|
|
1265
|
+
.conflict-columns h3 { font-size: 13px; margin: 0; }
|
|
1266
|
+
.conflict-columns pre { max-height: 45vh; overflow: auto; font-size: 12px; padding: 10px; border: 1px solid var(--c-line); }
|
|
1267
|
+
@media (max-width: 600px) { .conflict-columns { grid-template-columns: 1fr; } .conflict-columns pre { max-height: 25vh; } }
|
|
1268
|
+
#gear { position: relative; flex: none; }
|
|
1269
|
+
#gearmenu { display: none; position: absolute; right: 0; top: calc(100% + 6px); z-index: 8;
|
|
1270
|
+
min-width: 220px; padding: 4px; flex-direction: column; gap: 1px; color: var(--c-fg);
|
|
1271
|
+
background: var(--c-surface); border: 1px solid var(--c-line); border-radius: 6px;
|
|
1272
|
+
box-shadow: 0 10px 28px var(--c-shadow); font-size: 12.5px; }
|
|
1273
|
+
#gear:hover #gearmenu, #gear:focus-within #gearmenu, #gear.open #gearmenu { display: flex; }
|
|
1274
|
+
#gearmenu label { display: flex; gap: 10px; align-items: center; cursor: pointer;
|
|
1275
|
+
padding: 7px 10px; border-radius: 4px; }
|
|
1276
|
+
#gearmenu label:hover { background: var(--c-raised); }
|
|
1277
|
+
#gearmenu input { accent-color: var(--c-accent); margin: 0; }
|
|
1278
|
+
|
|
1279
|
+
.fm-language { display: flex; align-items: center; flex: 0 1 220px; min-width: 0; gap: 4px; }
|
|
1280
|
+
.fm-language > .language-select { flex: 1; width: 0; min-width: 0; }
|
|
1281
|
+
.language-select, .language-picker { font: 13px/18px var(--font-sans); font-weight: var(--fw-body); }
|
|
1282
|
+
.language-select { position: relative; }
|
|
1283
|
+
.language-select-input { width: 100%; min-width: 0; height: 28px; padding: 4px 26px 4px 8px;
|
|
1284
|
+
border: 1px solid var(--c-line); border-radius: 5px; color: var(--c-fg);
|
|
1285
|
+
background: var(--c-surface); font: inherit; text-overflow: ellipsis; }
|
|
1286
|
+
.language-select::after { content: ""; position: absolute; right: 10px; top: 9px; width: 6px; height: 6px; pointer-events: none;
|
|
1287
|
+
border-right: 1px solid currentColor; border-bottom: 1px solid currentColor; transform: rotate(45deg); }
|
|
1288
|
+
.language-select-input:hover { background: var(--c-raised); }
|
|
1289
|
+
.language-select-input:focus-visible, .language-picker :focus-visible { outline: 1px solid var(--c-fg-muted); outline-offset: 1px; }
|
|
1290
|
+
.language-select-input[aria-disabled="true"] { opacity: .5; cursor: wait; }
|
|
1291
|
+
.fm-language > .nav-icon { color: var(--c-fg-muted); }
|
|
1292
|
+
.fm-language .language-select::after { display: none; }
|
|
1293
|
+
.fm-language .language-select-input { padding: 4px; border: 0; border-bottom: 1px solid var(--c-line);
|
|
1294
|
+
border-radius: 0; background: transparent; }
|
|
1295
|
+
.fm-language .language-select-input:hover { border-bottom-color: var(--c-fg-muted); }
|
|
1296
|
+
.fm-language .language-select-input:focus-visible { outline: none; border-bottom-color: var(--c-accent); }
|
|
1297
|
+
.language-picker { position: fixed; margin: 0; padding: 0; overflow: hidden; border: 1px solid var(--c-line);
|
|
1298
|
+
border-radius: 5px; color: var(--c-fg); background: var(--c-surface); box-shadow: 0 8px 24px var(--c-shadow); }
|
|
1299
|
+
.language-picker:popover-open { display: flex; flex-direction: column; }
|
|
1300
|
+
.language-options { overflow-y: auto; min-height: 0; }
|
|
1301
|
+
.language-options button { display: block; width: 100%; padding: 6px 8px; border: 0;
|
|
1302
|
+
color: inherit; background: transparent; font: inherit; text-align: left;
|
|
1303
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
|
|
1304
|
+
.language-options button:hover, .language-options button[data-active="true"] { background: var(--c-raised); }
|
|
1305
|
+
.language-options button[aria-selected="true"] { color: var(--c-fg); }
|
|
1306
|
+
.language-toggle { display: inline-block; width: 24px; margin: -6px 0 -6px -8px; padding: 6px 0; text-align: center; }
|
|
1307
|
+
.language-toggle:hover { color: var(--c-accent); background: var(--c-line); }
|
|
1308
|
+
.language-options button:disabled { opacity: .5; cursor: wait; }
|
|
1309
|
+
.language-empty { padding: 8px; color: var(--c-fg-muted); }
|
|
1310
|
+
|
|
1311
|
+
main.editor-workspace { padding: 0; height: 100vh; display: flex; flex-direction: column; position: sticky; top: 0; }
|
|
1312
|
+
#split-preview[aria-pressed="true"] { color: var(--c-accent); background: var(--c-raised); }
|
|
1313
|
+
.pane-actions { top: -20px; margin: -20px -24px 20px; }
|
|
1314
|
+
#preview-panes { display: flex; flex: 1; min-height: 0; }
|
|
1315
|
+
#preview-panes[hidden] { display: none; }
|
|
1316
|
+
.preview-pane.resources-pane { min-height: 0; padding-bottom: 24px; }
|
|
1317
|
+
.preview-pane.resources-pane:has(.resource-library) { padding-top: 0; }
|
|
1318
|
+
.resources-pane .resource-tabs { margin-inline: -24px; padding: 0 12px; }
|
|
1319
|
+
.preview-pane { flex: 1; min-width: 0; overflow: auto; padding: 20px 24px 50vh; border-top: 2px solid transparent; }
|
|
1320
|
+
#preview-panes.split .preview-pane { padding: 16px; }
|
|
1321
|
+
#preview-panes.split .pane-actions { top: -16px; margin: -16px -16px 16px; padding-inline: 10px; }
|
|
1322
|
+
#preview-panes.split .preview-pane + .preview-pane { border-left: 1px solid var(--c-line); }
|
|
1323
|
+
#preview-panes.split .preview-pane.active { border-top-color: var(--c-accent); }
|
|
1324
|
+
.preview-pane:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
|
|
1325
|
+
#preview-language-menu { position: fixed; z-index: 9; min-width: 180px; max-height: min(360px, calc(100vh - 64px)); overflow-y: auto; padding: 6px; background: var(--c-surface); border: 1px solid var(--c-line); border-radius: 10px; box-shadow: 0 10px 28px var(--c-shadow); transform-origin: top right; }
|
|
1326
|
+
.language-option { display: flex; align-items: center; gap: 10px; width: 100%; padding: 9px 10px; border: 0; border-radius: 5px; background: transparent; color: var(--c-fg); text-align: left; font: inherit; font-size: 13px; cursor: pointer; }
|
|
1327
|
+
.language-option::before { content: ""; width: 14px; flex: none; }
|
|
1328
|
+
.language-option[aria-checked="true"] { color: var(--c-accent); }
|
|
1329
|
+
.language-option[aria-checked="true"]::before { content: "✓"; }
|
|
1330
|
+
.language-option:hover, .language-option:focus-visible { background: var(--c-raised); outline: none; }
|
|
1331
|
+
.language-option:disabled { opacity: .5; cursor: wait; }
|
|
1332
|
+
#language-setting { flex: none; display: flex; align-items: center; gap: 12px; padding: 8px 14px; border-bottom: 1px solid var(--c-line); }
|
|
1333
|
+
#language-setting .language-select { width: 160px; }
|
|
1334
|
+
.cm-code-diagnostic { text-decoration:underline wavy var(--c-warn); text-underline-offset:3px; }
|
|
1335
|
+
#validate[data-state="error"], .cm-textfile-validate[data-state="error"] { color:var(--c-danger); }
|
|
1336
|
+
#validate[data-state="valid"], .cm-textfile-validate[data-state="valid"] { color:var(--c-ok); }
|
|
1337
|
+
#validate[data-state="pending"], .cm-textfile-validate[data-state="pending"] { color:var(--c-fg-faint); }
|
|
1338
|
+
#validate[data-state="checking"], .cm-textfile-validate[data-state="checking"] { color:var(--c-info); }
|
|
1339
|
+
#save[data-state="error"], .cm-textfile-save-status[data-state="error"] { color:var(--c-danger); }
|
|
1340
|
+
#save[data-state="saved"] { color:var(--c-ok); }
|
|
1341
|
+
#save[data-state="pending"] { color:var(--c-accent); }
|
|
1342
|
+
.cm-textfile-save-status { font-size: 10px; color: var(--c-fg-faint); margin-left: 4px; }
|
|
1343
|
+
.cm-textfile-save-status[hidden], .cm-textfile-retry[hidden] { display: none; }
|
|
1344
|
+
.cm-textfile-retry { width: auto; padding: 0 5px; font-size: 11px; }
|
|
1345
|
+
#cm { flex: 1; min-height: 200px; overflow: hidden; }
|
|
1346
|
+
#cm .cm-inline-fold { display:inline-flex; align-items:center; justify-content:center; vertical-align:middle; width:16px; height:16px; margin-left:2px; padding:0; border:0; border-radius:3px; background:transparent; color:var(--c-fg-faint); cursor:pointer; }
|
|
1347
|
+
#cm .cm-inline-fold:hover, #cm .cm-inline-fold:focus-visible { color:var(--c-fg); background:var(--c-raised); }
|
|
1348
|
+
#cm .cm-inline-fold:focus-visible { outline:1px solid var(--c-accent); outline-offset:1px; }
|
|
1349
|
+
#cm .cm-inline-fold[aria-expanded="false"] svg { transform:rotate(-90deg); }
|
|
1350
|
+
#cm .cm-editor { height: 100%; }
|
|
1351
|
+
#cm .cm-editor.cm-focused { outline: none; }
|
|
1352
|
+
#cm .cm-textfile-controls { display: inline-flex; align-items: center; gap: 1px;
|
|
1353
|
+
vertical-align: middle; margin-left: 4px; user-select: none; }
|
|
1354
|
+
#cm .cm-textfile-controls .pbtn { width: 20px; height: 20px; border-radius: 3px; }
|
|
1355
|
+
#cm .cm-textfile-controls .pbtn[hidden] { display: none; }
|
|
1356
|
+
#cm .cm-textfile-controls .pbtn svg { width: 14px; height: 14px; }
|
|
1357
|
+
#cm .cm-textfile-controls .pbtn:disabled { opacity: .45; cursor: default; }
|
|
1358
|
+
#cm .cm-textfile { margin: 2px 6px 8px; border: 1px solid var(--c-line);
|
|
1359
|
+
border-radius: 4px; min-width: 0; overflow: hidden; }
|
|
1360
|
+
#cm .cm-textfile-status { padding: 4px 8px; color: var(--c-fg-muted); font-size: 11px; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1361
|
+
#cm .cm-textfile-status.error { color: var(--c-danger); }
|
|
1362
|
+
#cm .cm-textfile-editor > .cm-editor { height: auto; max-height: 360px; background: transparent; }
|
|
1363
|
+
#cm .cm-textfile-editor .cm-content { padding: 6px 0; }
|
|
1364
|
+
#cm .cm-textfile-editor .cm-gutters { background: transparent; border: 0; }
|
|
1365
|
+
#cm .cm-textfile-editor .cm-scroller { overflow: auto; }
|
|
1366
|
+
#cm.dropping { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--c-accent) 70%, transparent);
|
|
1367
|
+
background: color-mix(in srgb, var(--c-accent) 5%, transparent); }
|
|
1368
|
+
|
|
1369
|
+
/* Correspondence display (body element ↔ source line) — body.corr when enabled in the gear menu */
|
|
1370
|
+
body.corr main article > [data-ln] { cursor: pointer; }
|
|
1371
|
+
body.corr main article > [data-ln].corr-hi {
|
|
1372
|
+
background: color-mix(in srgb, var(--c-accent) 12%, transparent);
|
|
1373
|
+
box-shadow: -6px 0 0 color-mix(in srgb, var(--c-accent) 55%, transparent); border-radius: 2px; }
|
|
1374
|
+
|
|
1375
|
+
.files-toolbar {display:flex;gap:10px;align-items:center;min-width:0;}
|
|
1376
|
+
.files-toolbar > span {flex:none;}
|
|
1377
|
+
.files-toolbar button {flex:none;position:relative;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;margin-left:auto;cursor:pointer;}
|
|
1378
|
+
.files-toolbar button:disabled {opacity:.5;cursor:default;}.files-toolbar button[data-state=loading] {color:transparent;opacity:1;}
|
|
1379
|
+
.files-toolbar button[data-state=loading]:after {content:"";position:absolute;left:calc(50% - 6px);top:calc(50% - 6px);width:12px;height:12px;border:1px solid var(--c-line);border-top-color:var(--c-accent);border-radius:50%;animation:files-spin .8s linear infinite;}
|
|
1380
|
+
@keyframes files-spin {to{transform:rotate(360deg);}}
|
|
1381
|
+
@media(prefers-reduced-motion:reduce){.files-toolbar button[data-state=loading]:after{animation:none;}}
|
|
1382
|
+
.files-toolbar button[data-state=error] {color:var(--c-danger);}
|
|
1383
|
+
.files-viewport {overflow:auto;min-height:360px;max-height:70vh;}
|
|
1384
|
+
.files-table {width:100%;border-collapse:collapse;table-layout:fixed;font-size:13px;}
|
|
1385
|
+
.files-table th,.files-table td {height:26px;padding:0 6px;text-align:left;}
|
|
1386
|
+
.files-table th {height:30px;border-bottom:1px solid var(--c-line);}
|
|
1387
|
+
.files-table th:nth-child(2) {width:76px;}.files-table th:last-child {width:28px;}
|
|
1388
|
+
.files-table th:nth-child(2),.files-table td:nth-child(2) {text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;}
|
|
1389
|
+
.files-table td:nth-child(2) {font-size:11px;color:var(--c-fg-faint);}
|
|
1390
|
+
.files-table tbody tr:hover {background:var(--c-surface);}
|
|
1391
|
+
.files-table tbody {user-select:none;}
|
|
1392
|
+
.files-table tr.selected {background:var(--c-raised);}
|
|
1393
|
+
.files-table button {border:0;background:none;color:inherit;cursor:pointer;padding:3px;}
|
|
1394
|
+
.files-name {display:flex;min-width:0;}
|
|
1395
|
+
.files-name a,.files-name .files-folder {position:relative;display:flex;align-items:center;gap:6px;min-width:0;white-space:nowrap;width:100%;height:26px;padding:0 0 0 16px;color:var(--c-fg);font:inherit;text-align:left;text-decoration:none;}
|
|
1396
|
+
.files-folder:before {content:"";position:absolute;left:3px;top:9px;width:5px;height:5px;border-right:1px solid var(--c-fg-faint);border-bottom:1px solid var(--c-fg-faint);transform:rotate(45deg);}
|
|
1397
|
+
.files-folder[aria-expanded=false]:before {transform:rotate(-45deg);}
|
|
1398
|
+
.files-name span {display:flex;flex:none;}
|
|
1399
|
+
.files-name svg {width:15px;height:15px;color:var(--c-fg-muted);}
|
|
1400
|
+
.files-name .files-basename {display:block;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;}
|
|
1401
|
+
.files-copy {display:flex;opacity:0;}
|
|
1402
|
+
.files-copy svg {width:14px;height:14px;color:var(--c-fg-faint);}
|
|
1403
|
+
.files-table tr:hover .files-copy,.files-table tr:focus-within .files-copy {opacity:1;}
|
|
1404
|
+
@media(hover:none){.files-copy{opacity:1;}}
|
|
1405
|
+
.files-table [data-drop=before] {box-shadow:inset 0 2px var(--c-accent);}.files-table [data-drop=after] {box-shadow:inset 0 -2px var(--c-accent);}.files-table [data-drop=into] {outline:2px solid var(--c-accent);outline-offset:-2px;}
|
|
1406
|
+
.files-table .files-placeholder {height:96px;text-align:center;vertical-align:middle;}
|
|
1407
|
+
.app-dialog textarea {display:block;box-sizing:border-box;width:100%;min-height:64px;max-height:65vh;resize:both;font:inherit;}
|
|
1408
|
+
@media(max-width:600px){.files-table {min-width:0;}.files-table th:nth-child(2){width:65px;}.files-name{max-width:100%;}}
|
|
1409
|
+
|
|
1410
|
+
@media(max-width:400px){.files-table th:nth-child(2),.files-table td:nth-child(2){display:none;}}
|
|
1411
|
+
|
|
1412
|
+
/* Files pages */
|
|
1413
|
+
.res-page h1 { margin-bottom: 6px; }
|
|
1414
|
+
.files-page h1 { display: flex; align-items: center; gap: 10px; overflow-wrap: anywhere; }
|
|
1415
|
+
.res-crumbs { color: var(--c-fg-faint); font-size: 12.5px; margin: 0 0 20px; }
|
|
1416
|
+
.res-crumbs a { color: var(--c-fg-muted); text-decoration: none; }
|
|
1417
|
+
.res-crumbs a:hover { color: var(--c-fg); }
|
|
1418
|
+
.res-section h2 { font-size: 15px; border: 0; margin: 28px 0 10px; }
|
|
1419
|
+
.res-section h2 a { color: inherit; text-decoration: none; }
|
|
1420
|
+
.res-sub { color: var(--c-fg-faint); font-size: 11.5px; font-weight: 400; }
|
|
1421
|
+
.res-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
|
|
1422
|
+
.res-card { display: flex; flex-direction: column; border: 1px solid var(--c-line); border-radius: 6px;
|
|
1423
|
+
overflow: hidden; color: inherit; text-decoration: none; background: var(--c-bg1);
|
|
1424
|
+
transition: border-color 120ms ease; }
|
|
1425
|
+
.res-card:hover { border-color: var(--c-fg-faint); }
|
|
1426
|
+
.res-thumb { aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center; padding: 10px;
|
|
1427
|
+
background: var(--c-bg0); overflow: hidden; }
|
|
1428
|
+
.res-thumb .res-img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
|
1429
|
+
.res-thumb .res-icon { width: 48px; height: 48px; }
|
|
1430
|
+
.res-thumb .fig-frame { width: 100% !important; height: 100% !important; aspect-ratio: auto !important;
|
|
1431
|
+
pointer-events: none; }
|
|
1432
|
+
.res-preview .res-svg { max-height: 480px; }
|
|
1433
|
+
.res-preview .res-html { width: 100% !important; }
|
|
1434
|
+
.res-thumb .res-frame { width: 100%; height: 100%; border: 0; pointer-events: none; background: #fff; }
|
|
1435
|
+
.res-meta { display: flex; flex-direction: column; gap: 1px; padding: 7px 10px; border-top: 1px solid var(--c-line); }
|
|
1436
|
+
.res-name { font-family: var(--doc-mono); font-size: 11.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1437
|
+
.res-preview { display: flex; align-items: center; justify-content: center; min-height: 160px; padding: 16px;
|
|
1438
|
+
border: 1px solid var(--c-line); border-radius: 6px; background: var(--c-bg0); }
|
|
1439
|
+
.res-preview .res-img { max-width: 100%; max-height: 480px; }
|
|
1440
|
+
.res-preview.square .res-img { width: 160px; height: 160px; outline: 1px dashed var(--c-line); }
|
|
1441
|
+
.res-preview .res-frame { width: 100%; height: 420px; border: 0; background: #fff; }
|
|
1442
|
+
.res-markdown { width: 100%; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1443
|
+
.res-thumb .res-markdown { max-height: 100%; overflow: hidden; font-size: 11px; }
|
|
1444
|
+
.res-preview .res-markdown { max-height: 480px; overflow: auto; }
|
|
1445
|
+
.res-empty { color: var(--c-fg-faint); }
|
|
1446
|
+
.res-dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 16px; margin: 16px 0 0; font-size: 13px; }
|
|
1447
|
+
.res-dl dt { color: var(--c-fg-faint); }
|
|
1448
|
+
.res-dl dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
1449
|
+
.resources-pane .preview-content, .res-file-page { min-width: 0; width: 100%; max-width: none; }
|
|
1450
|
+
.resources-pane .res-file-page > h1 { margin: 8px 0 14px; font-size: 17px; overflow-wrap: anywhere; }
|
|
1451
|
+
.res-file-page .res-preview { display: block; min-width: 0; padding: 0; border: 0; background: transparent; }
|
|
1452
|
+
.res-file-page .res-preview > :is(img, video) { display: block; max-width: 100%; max-height: calc(100vh - 160px); margin: auto; object-fit: contain; }
|
|
1453
|
+
.res-file-page .video-player > video:not(:fullscreen) { max-height: calc(100vh - 210px); object-fit: contain; }
|
|
1454
|
+
.res-file-page .res-preview > :is(.codeblk, .data-table, pre) { margin-top: 0; width: 100%; min-width: 0; box-sizing: border-box; }
|
|
1455
|
+
.res-file-page .res-preview .codeblk > pre { max-height: calc(100vh - 180px); overflow: auto; }
|
|
1456
|
+
.res-file-page .res-preview .fig-frame { max-width: 100%; min-height: 300px; }
|
|
1457
|
+
.res-details { border-top: 1px solid var(--c-line); margin-top: 16px; padding-top: 8px; font-size: 12px; }
|
|
1458
|
+
.res-details > summary { cursor: pointer; color: var(--c-fg-muted); }
|
|
1459
|
+
|
|
1460
|
+
|
|
1461
|
+
/* Draft placeholders, the in-sidebar create form, and drag moves */
|
|
1462
|
+
.nav-row :is(.new, .nav-delete, .nav-create) { flex: none; width: 20px; height: 24px; display: flex;
|
|
1463
|
+
align-items: center; justify-content: center; border: none; border-radius: 4px; padding: 0;
|
|
1464
|
+
background: transparent; color: var(--c-fg-muted); cursor: pointer; opacity: 0; }
|
|
1465
|
+
.nav-row:hover :is(.new, .nav-delete), .nav-row:focus-within :is(.new, .nav-delete),
|
|
1466
|
+
.nav-row.renaming :is(.new, .nav-delete) { opacity: 1; }
|
|
1467
|
+
.nav-row :is(.new, .nav-delete, .nav-create):is(:hover, :focus-visible) { background: var(--c-line); }
|
|
1468
|
+
.nav-doc-row .nav-tree-toggle, .nav-tree-spacer { width: 18px; margin-right: 3px; }
|
|
1469
|
+
.nav-tree-spacer { flex: none; height: 28px; }
|
|
1470
|
+
@media (hover: none) { .nav-row :is(.new, .nav-delete) { opacity: 1; } }
|
|
1471
|
+
.nav-row > .nav-create { margin: 7px 12px; opacity: 1; }
|
|
1472
|
+
.nav-add-hover { visibility: hidden; }
|
|
1473
|
+
#sidebar:hover .nav-add-hover, #sidebar:focus-within .nav-add-hover { visibility: visible; }
|
|
1474
|
+
#sidebar .nav-list:has(> .nav-new-item) > .nav-placeholder { display: none; }
|
|
1475
|
+
/* Keep the title and filename together in a compact create form. */
|
|
1476
|
+
.nav-row > .nav-new { position: relative; display: flex; flex: 1; min-width: 0;
|
|
1477
|
+
flex-direction: column; justify-content: center; align-items: stretch; cursor: auto;
|
|
1478
|
+
margin: 4px 0; height: 42px; padding: 5px 38px 5px 36px;
|
|
1479
|
+
border: 1px dashed var(--c-line); border-radius: 6px; background: var(--c-raised); }
|
|
1480
|
+
.nav-tree .nav-new { margin-left: calc(var(--nav-pad) - 12px); }
|
|
1481
|
+
.nav-new > .nav-icon { position: absolute; left: 12px; top: 10px; color: var(--c-fg-faint); }
|
|
1482
|
+
.nav-new input { font: inherit; color: var(--c-fg); background: transparent; border: none;
|
|
1483
|
+
padding: 0; min-width: 0; outline: none; }
|
|
1484
|
+
.nav-new input::placeholder { color: var(--c-fg-faint); }
|
|
1485
|
+
.nav-new input[name="title"] { font-size: 13px; line-height: 17px; }
|
|
1486
|
+
.nav-new-file { display: flex; align-items: center; color: var(--c-fg-faint);
|
|
1487
|
+
font: 10px/12px var(--doc-mono); }
|
|
1488
|
+
.nav-new-file input { flex: 1; font-family: inherit; font-size: inherit; color: var(--c-fg-muted); }
|
|
1489
|
+
.nav-new-go { position: absolute; right: 7px; top: 50%; transform: translateY(-50%);
|
|
1490
|
+
display: flex; align-items: center; justify-content: center; width: 24px;
|
|
1491
|
+
height: 24px; padding: 0; font-size: 11px; color: var(--c-fg-faint); background: none;
|
|
1492
|
+
border: 1px solid var(--c-line); border-radius: 4px; cursor: pointer; }
|
|
1493
|
+
.nav-new-go:hover { color: var(--c-accent); border-color: var(--c-accent); }
|
|
1494
|
+
.nav-new .err { position: absolute; top: 100%; left: 0; right: 0; z-index: 5; padding: 4px;
|
|
1495
|
+
background: var(--c-surface); color: var(--c-danger); font-size: 11px; line-height: 1.4; }
|
|
1496
|
+
.nav-new .err:empty { display: none; }
|
|
1497
|
+
.nav-tree > .nav-new-item > .nav-row::before { width: calc(var(--nav-pad) - 12px - var(--nav-line)); }
|
|
1498
|
+
.nav-ancestor > .nav-tree > .nav-new-item { --nav-line: var(--nav-rail, 21px); }
|
|
1499
|
+
.nav-ancestor > .nav-tree > .nav-new-item > .nav-row::before,
|
|
1500
|
+
.nav-ancestor > .nav-tree > .nav-new-item > .nav-row::after { display: block; }
|
|
1501
|
+
.nav-ancestor > .nav-tree > .nav-item:has(~ .nav-new-item)::before {
|
|
1502
|
+
display: block; left: var(--nav-rail, 21px); background: var(--c-line); }
|
|
1503
|
+
body.edit .nav-row { cursor: grab; }
|
|
1504
|
+
.nav-item.dragging { opacity: .35; }
|
|
1505
|
+
.nav-row.drop-before { box-shadow: inset 0 2px 0 var(--c-accent); }
|
|
1506
|
+
.nav-row.drop-after { box-shadow: inset 0 -2px 0 var(--c-accent); }
|
|
1507
|
+
.nav-row.drop-into > a { background: color-mix(in srgb, var(--c-accent) 18%, transparent); }
|
|
1508
|
+
`;
|
|
1509
|
+
// Contents of #nav-groups (the doc tree). Used by both page()'s initial render and
|
|
1510
|
+
// `GET /api/nav` (edit mode; the client replaces just the innerHTML after an md change)
|
|
1511
|
+
// (= the same value isn't declared in two places).
|
|
1512
|
+
// Each item is a page row and its children list, emitted even when empty
|
|
1513
|
+
// (it's the insertion target for the sidebar create form / drag).
|
|
1514
|
+
export const navGroupsHtml = (mode, site, currentId, currentRes) => {
|
|
1515
|
+
const open = new Set(currentId == null
|
|
1516
|
+
? []
|
|
1517
|
+
: [...ancestorsOf(site.entries, currentId).map((e) => e.id), currentId]);
|
|
1518
|
+
const list = (parent, depth) => {
|
|
1519
|
+
const items = childrenOf(site.entries, parent)
|
|
1520
|
+
.map((e) => {
|
|
1521
|
+
const label = escapeHtml(docDisplayName(e.frontmatter, e.id));
|
|
1522
|
+
const cur = e.id === currentId ? " cur" : "";
|
|
1523
|
+
// Top level = nav-link with an icon, descendants = nav-tree-link with guide lines.
|
|
1524
|
+
// A doc-specific icon (icons/<id>.svg), if any, replaces the book icon at top
|
|
1525
|
+
// level and goes before the label below that.
|
|
1526
|
+
let link = depth === 0
|
|
1527
|
+
? `<a class="nav-link${cur}" href="/${e.id}" title="${label}">${e.icon ? svgImg(e.icon, "nav-icon doc-icon") : DOCS_ICON}<span class="nav-label">${label}</span></a>`
|
|
1528
|
+
: `<a class="nav-tree-link${cur}" href="/${e.id}" title="${label}">${e.icon ? svgImg(e.icon, "doc-icon") : DOCS_ICON}<span class="nav-label">${label}</span></a>`;
|
|
1529
|
+
let actions = "";
|
|
1530
|
+
let editableIcon = "";
|
|
1531
|
+
if (mode === "edit") {
|
|
1532
|
+
link = `<a class="${depth === 0 ? "nav-link" : "nav-tree-link"}${cur}" href="/${e.id}" title="${label}" aria-keyshortcuts="F2"><span class="nav-doc-name"><span class="nav-label">${label}</span></span></a>`;
|
|
1533
|
+
editableIcon = `<span class="nav-doc-icon" title="icon" data-i18n-title="icon">${e.icon ? svgImg(e.icon, "doc-icon") : DOCS_ICON}<input type="file" accept=".svg,image/svg+xml" aria-label="icon" data-i18n-aria-label="icon">${e.icon ? '<button class="nav-icon-clear" type="button" title="Remove icon" data-i18n-title="Remove icon" aria-label="Remove icon" data-i18n-aria-label="Remove icon">×</button>' : ""}</span>`;
|
|
1534
|
+
actions = `<button class="new" type="button" data-parent="${escapeHtml(e.id)}" title="Add doc" data-i18n-title="Add doc" aria-label="Add doc" data-i18n-aria-label="Add doc">${PLUS_ICON}</button><button class="nav-delete" type="button" data-doc-action="delete" title="Delete doc" data-i18n-title="Delete doc" aria-label="Delete doc" data-i18n-aria-label="Delete doc">${DELETE_ICON}</button>`;
|
|
1535
|
+
}
|
|
1536
|
+
const branch = childrenOf(site.entries, e.id).length > 0;
|
|
1537
|
+
const toggle = branch
|
|
1538
|
+
? `<button class="nav-tree-toggle" type="button" aria-label="Toggle ${label}">${CHEVRON_ICON}</button>`
|
|
1539
|
+
: mode === "edit"
|
|
1540
|
+
? '<span class="nav-tree-spacer" aria-hidden="true"></span>'
|
|
1541
|
+
: "";
|
|
1542
|
+
return `<div class="nav-item${open.has(e.id) ? " open" : ""}" data-doc-id="${escapeHtml(e.id)}"><div class="nav-row${mode === "edit" ? " nav-doc-row" : ""}"${mode === "edit" ? ' draggable="true"' : ""}>${editableIcon}${link}${actions}${toggle}</div>${list(e.id, depth + 1)}</div>`;
|
|
1543
|
+
})
|
|
1544
|
+
.join("");
|
|
1545
|
+
const placeholder = mode === "edit" && depth === 0
|
|
1546
|
+
? `<div class="nav-item nav-placeholder${items ? " nav-add-hover" : ""}"><div class="nav-row"><button class="nav-create" type="button" data-parent="" title="Add doc" data-i18n-title="Add doc" aria-label="Add doc" data-i18n-aria-label="Add doc">${PLUS_ICON}</button></div></div>`
|
|
1547
|
+
: "";
|
|
1548
|
+
return depth === 0
|
|
1549
|
+
? `<div class="nav-list nav-root" data-parent=""${mode === "edit" ? ' id="nav-docs"' : ""}>${items}${placeholder}</div>`
|
|
1550
|
+
: `<div class="nav-list nav-tree" data-parent="${escapeHtml(parent ?? "")}" style="--nav-depth:${depth + 1}">${items}${placeholder}</div>`;
|
|
1551
|
+
};
|
|
1552
|
+
const html = list(undefined, 0) + (mode === "edit" ? navFilesHtml(site, currentRes) : "");
|
|
1553
|
+
return docLinks(site, site.contentLanguage ? languageLinks(html, site.contentLanguage) : html);
|
|
1554
|
+
};
|
|
1555
|
+
// ── Resources (file management in edit mode; linked figures and terms also in preview) ──
|
|
1556
|
+
// Structure: Resources (list page of everything) › kind (list pages for figures / html / icons) › individual file.
|
|
1557
|
+
// Route is `/_files[/<kind>[/<file>]]` (doc ids can't start with `_`, so no collision).
|
|
1558
|
+
const RESOURCE_KINDS = ["markdown", "figures", "html", "icons", "terms"];
|
|
1559
|
+
const siteResources = (site) => [
|
|
1560
|
+
...site.entries.map((entry) => ({
|
|
1561
|
+
kind: "markdown",
|
|
1562
|
+
file: entry.file,
|
|
1563
|
+
path: entry.sourceFile ?? CONTENT_PREFIX + entry.file,
|
|
1564
|
+
})),
|
|
1565
|
+
...site.ctx.terms.map((term) => ({
|
|
1566
|
+
kind: "terms",
|
|
1567
|
+
file: term.id,
|
|
1568
|
+
path: isTermFile(term.id)
|
|
1569
|
+
? term.id
|
|
1570
|
+
: CONTENT_PREFIX +
|
|
1571
|
+
(site.entries.find((entry) => entry.blocks.some((block) => block.type === "term" && block.entry.id === term.id))?.file ?? ""),
|
|
1572
|
+
})),
|
|
1573
|
+
...[...site.ctx.figures.values()].map((f) => ({
|
|
1574
|
+
kind: f.kind === "html" ? "html" : "figures",
|
|
1575
|
+
file: f.file.replace(/^figures\//, ""),
|
|
1576
|
+
path: f.file,
|
|
1577
|
+
})),
|
|
1578
|
+
...site.entries
|
|
1579
|
+
.filter((e) => e.icon != null)
|
|
1580
|
+
.map((e) => ({
|
|
1581
|
+
kind: "icons",
|
|
1582
|
+
file: `${e.id}.svg`,
|
|
1583
|
+
path: e.iconFile ?? `${ICONS_DIR}/${e.id}.svg`,
|
|
1584
|
+
})),
|
|
1585
|
+
];
|
|
1586
|
+
const resourceHref = (kind, file) => `/_files${kind ? `/${kind}` : ""}${file ? `/${encodeURIComponent(file)}` : ""}`;
|
|
1587
|
+
const navFilesHtml = (site, currentRes) => {
|
|
1588
|
+
const source = site.workspace;
|
|
1589
|
+
const statics = currentRes?.startsWith("_statics") ||
|
|
1590
|
+
["dictionary", "code", "urls"].includes(currentRes ?? "");
|
|
1591
|
+
const folderIcon = icon('<path d="M2 5h6l2 2h8v10H2Z"/>');
|
|
1592
|
+
const hint = `<div class="files-drop-hint" aria-hidden="true">${UPLOAD_ICON}<span data-i18n="Upload files">Upload files</span></div>`;
|
|
1593
|
+
const location = source?.protocol === "git"
|
|
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>`
|
|
1595
|
+
: `<div class="workspace-location" id="nav-storage" data-workspace-protocol="storage" title="${escapeHtml(workspaceName(site))}">${workspaceIcon(site)}<span class="workspace-address">${escapeHtml(source?.folder ?? "Files")} / ${escapeHtml(source?.service ?? "Local")}</span></div>`;
|
|
1596
|
+
const git = source?.protocol === "git"
|
|
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>`
|
|
1598
|
+
: "";
|
|
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>`;
|
|
1601
|
+
};
|
|
1602
|
+
const formatSize = (bytes) => bytes < 1024
|
|
1603
|
+
? `${bytes} B`
|
|
1604
|
+
: bytes < 1024 * 1024
|
|
1605
|
+
? `${(bytes / 1024).toFixed(1)} KB`
|
|
1606
|
+
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
1607
|
+
const sizeOf = (path) => {
|
|
1608
|
+
try {
|
|
1609
|
+
return formatSize(statSync(join(root, path)).size);
|
|
1610
|
+
}
|
|
1611
|
+
catch {
|
|
1612
|
+
return "—";
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
// Thumbnail in the lists / preview on the individual page. svg is confined to an img (avoids id
|
|
1616
|
+
// collisions), raster is already <img data:>, and html figures are shown
|
|
1617
|
+
// isolated in a sandboxed iframe.
|
|
1618
|
+
const resourcePreview = (site, r) => {
|
|
1619
|
+
if (r.kind === "markdown") {
|
|
1620
|
+
const entry = site.entries.find((e) => e.file === r.file);
|
|
1621
|
+
return `<pre class="res-markdown">${escapeHtml(entry?.raw ?? "")}</pre>`;
|
|
1622
|
+
}
|
|
1623
|
+
if (r.kind === "terms")
|
|
1624
|
+
return termsHtml(site.ctx.terms.filter((term) => term.id === r.file));
|
|
1625
|
+
if (r.kind === "figures" || r.kind === "html") {
|
|
1626
|
+
const f = site.ctx.figures.get(figureName(r.path));
|
|
1627
|
+
if (!f) {
|
|
1628
|
+
return "";
|
|
1629
|
+
}
|
|
1630
|
+
// svg figures are inline, as in the body (figures.ts has already namespaced the ids and
|
|
1631
|
+
// embedded the theme).
|
|
1632
|
+
return f.kind === "image"
|
|
1633
|
+
? f.markup.replace("<img ", '<img class="res-img" ')
|
|
1634
|
+
: figureFrameHtml(f, `fig-frame res-${f.kind}`);
|
|
1635
|
+
}
|
|
1636
|
+
const e = site.entries.find((x) => `${x.id}.svg` === r.file);
|
|
1637
|
+
return e?.icon ? svgImg(e.icon, "res-img res-icon") : "";
|
|
1638
|
+
};
|
|
1639
|
+
const resourceUsage = (site, r) => r.kind === "markdown"
|
|
1640
|
+
? site.entries.filter((e) => e.file === r.file)
|
|
1641
|
+
: r.kind === "figures" || r.kind === "html"
|
|
1642
|
+
? site.entries.filter((e) => e.figureRefs?.includes(figureName(r.path)))
|
|
1643
|
+
: r.kind === "icons"
|
|
1644
|
+
? site.entries.filter((e) => `${e.id}.svg` === r.file)
|
|
1645
|
+
: site.entries.filter((e) => e.termRefs?.includes(r.file));
|
|
1646
|
+
const resourceCards = (site, list) => list.length === 0
|
|
1647
|
+
? ""
|
|
1648
|
+
: `<div class="res-grid">${list
|
|
1649
|
+
.map((r) => {
|
|
1650
|
+
const used = resourceUsage(site, r).length;
|
|
1651
|
+
return `<a class="res-card" href="${resourceHref(r.kind, r.file)}"><div class="res-thumb">${resourcePreview(site, r)}</div><div class="res-meta"><span class="res-name">${escapeHtml(r.file)}</span><span class="res-sub">${sizeOf(r.path)} · used by ${used}</span></div></a>`;
|
|
1652
|
+
})
|
|
1653
|
+
.join("")}</div>`;
|
|
1654
|
+
/** Path after `/_files` → main content (unknown → undefined = 404). */
|
|
1655
|
+
const textfilePreview = (site, file, source) => {
|
|
1656
|
+
const language = (file.split(".").at(-1) ?? "text").toLowerCase();
|
|
1657
|
+
const parsed = parseContentSource(language, source, file);
|
|
1658
|
+
const errors = parsed?.errors.map(({ message }) => message) ?? [];
|
|
1659
|
+
if (errors.length)
|
|
1660
|
+
return { html: "", errors };
|
|
1661
|
+
try {
|
|
1662
|
+
return {
|
|
1663
|
+
html: blocksHtml(parsed?.blocks ?? [
|
|
1664
|
+
{ type: "code", lang: language, lines: source.split(/\r?\n/), filename: file },
|
|
1665
|
+
], site.ctx),
|
|
1666
|
+
errors,
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
catch (error) {
|
|
1670
|
+
return { html: "", errors: [error instanceof Error ? error.message : String(error)] };
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
export const textfilePreviewHtml = (site, file, source) => {
|
|
1674
|
+
const { html, errors } = textfilePreview(site, file, source);
|
|
1675
|
+
return errors.length
|
|
1676
|
+
? `<div class="errors" role="status">${errors.map(escapeHtml).join("<br>")}</div>`
|
|
1677
|
+
: html;
|
|
1678
|
+
};
|
|
1679
|
+
export const textfilePreviewErrors = (site, file, source) => {
|
|
1680
|
+
const errors = textfilePreview(site, file, source).errors;
|
|
1681
|
+
if (/\.(md|markdown)$/i.test(file)) {
|
|
1682
|
+
const { doc, source: parsed } = compileDraft(draftContext(site), file, source);
|
|
1683
|
+
errors.push(...parsed.errors);
|
|
1684
|
+
try {
|
|
1685
|
+
blocksHtml(doc.blocks, site.ctx);
|
|
1686
|
+
}
|
|
1687
|
+
catch (error) {
|
|
1688
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return errors;
|
|
1692
|
+
};
|
|
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
|
+
};
|
|
1699
|
+
if (rest === "_git")
|
|
1700
|
+
return site.workspace?.protocol === "git"
|
|
1701
|
+
? {
|
|
1702
|
+
title: `${site.workspace.repo ?? "repo"} [${site.workspace.branch}]`,
|
|
1703
|
+
main: '<article class="res-page resource-library git-page"><div id="git-manager"></div></article>',
|
|
1704
|
+
}
|
|
1705
|
+
: undefined;
|
|
1706
|
+
if (rest === "_statics" || rest.startsWith("_statics/")) {
|
|
1707
|
+
const view = rest.slice("_statics".length).replace(/^\//, "") || "dictionary";
|
|
1708
|
+
return ["dictionary", "code", "urls"].includes(view)
|
|
1709
|
+
? resourcesMain(site, view)
|
|
1710
|
+
: undefined;
|
|
1711
|
+
}
|
|
1712
|
+
const all = siteResources(site);
|
|
1713
|
+
const [kind, ...tail] = rest === "" ? [] : rest.split("/");
|
|
1714
|
+
const file = tail.length > 0 ? decodeURIComponent(tail.join("/")) : undefined;
|
|
1715
|
+
if (kind == null) {
|
|
1716
|
+
return {
|
|
1717
|
+
title: workspaceName(site),
|
|
1718
|
+
main: `<article class="res-page files-page resource-library"><div id="files-manager"></div></article>`,
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
if (!file && (kind === "dictionary" || kind === "code" || kind === "urls")) {
|
|
1722
|
+
const index = resourceIndex(root, site.filePaths ?? filesSnapshot(root).files.map((item) => item.path));
|
|
1723
|
+
return {
|
|
1724
|
+
title: "Statics",
|
|
1725
|
+
main: `<article class="res-page files-page resource-library">${resourceTabs(kind)}${resourceLibraryHtml(index, kind)}</article>`,
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
if (kind === "file" && file) {
|
|
1729
|
+
const resource = all.find((item) => item.path === file);
|
|
1730
|
+
if (resource)
|
|
1731
|
+
return resourcesMain(site, `${resource.kind}/${encodeURIComponent(resource.file)}`);
|
|
1732
|
+
try {
|
|
1733
|
+
if (!filesSnapshot(root).files.some((item) => item.path === file))
|
|
1734
|
+
return undefined;
|
|
1735
|
+
const content = readFileSync(textfilePath(root, file));
|
|
1736
|
+
let preview = "";
|
|
1737
|
+
let sourceFile;
|
|
1738
|
+
if (/\.(svg|png|jpe?g|gif|webp)$/i.test(file)) {
|
|
1739
|
+
const extension = file.split(".").pop()?.toLowerCase();
|
|
1740
|
+
const mime = extension === "svg" ? "svg+xml" : extension === "jpg" ? "jpeg" : extension;
|
|
1741
|
+
preview = `<img alt="" src="data:image/${mime};base64,${content.toString("base64")}">`;
|
|
1742
|
+
if (extension === "svg")
|
|
1743
|
+
sourceFile = file;
|
|
1744
|
+
}
|
|
1745
|
+
else if (/\.(mp4|webm|ogg)$/i.test(file)) {
|
|
1746
|
+
preview = `<video data-dooc-video controls playsinline preload="metadata" aria-label="Video preview" src="data:video/${file.split(".").pop()};base64,${content.toString("base64")}"></video>`;
|
|
1747
|
+
}
|
|
1748
|
+
else {
|
|
1749
|
+
try {
|
|
1750
|
+
const source = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
1751
|
+
if (source.includes("\0"))
|
|
1752
|
+
throw new Error("Binary file");
|
|
1753
|
+
sourceFile = file;
|
|
1754
|
+
preview = textfilePreviewHtml(site, file, source);
|
|
1755
|
+
}
|
|
1756
|
+
catch {
|
|
1757
|
+
preview = BOX_ICON;
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return {
|
|
1761
|
+
title: file,
|
|
1762
|
+
sourceFile,
|
|
1763
|
+
main: `<article class="res-page res-file-page"><nav class="res-crumbs"><a href="/_files">${workspaceText(site)}</a></nav><h1>${escapeHtml(file)}</h1><div class="res-preview${sourceFile ? " res-text-preview" : ""}">${preview}</div></article>`,
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
catch {
|
|
1767
|
+
return undefined;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
if (!RESOURCE_KINDS.includes(kind)) {
|
|
1771
|
+
return undefined;
|
|
1772
|
+
}
|
|
1773
|
+
const k = kind;
|
|
1774
|
+
const crumbs = `<nav class="res-crumbs"><a href="/_files">${workspaceText(site)}</a> › ${file ? `<a href="${resourceHref(k)}" data-i18n="${k}">${k}</a> › ${escapeHtml(file)}` : k}</nav>`;
|
|
1775
|
+
if (file == null) {
|
|
1776
|
+
return {
|
|
1777
|
+
title: `${k} — ${workspaceName(site)}`,
|
|
1778
|
+
main: `<article class="res-page">${k === "terms" ? "" : crumbs}<h1 data-i18n="${k}">${translate("eng", k)}</h1>${k === "terms"
|
|
1779
|
+
? termsHtml(site.ctx.terms, resolveLanguage(site.contentLanguage ?? site.defaultLang, site.languages), true)
|
|
1780
|
+
: resourceCards(site, all.filter((r) => r.kind === k))}</article>`,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
const r = all.find((x) => x.kind === k && x.file === file);
|
|
1784
|
+
if (!r) {
|
|
1785
|
+
return undefined;
|
|
1786
|
+
}
|
|
1787
|
+
const fig = k === "figures" || k === "html" ? site.ctx.figures.get(figureName(r.path)) : undefined;
|
|
1788
|
+
const used = resourceUsage(site, r);
|
|
1789
|
+
const snippet = k === "terms"
|
|
1790
|
+
? isTermFile(r.file)
|
|
1791
|
+
? `[](${termFileUrl(r.file)})`
|
|
1792
|
+
: undefined
|
|
1793
|
+
: fig
|
|
1794
|
+
? ``
|
|
1795
|
+
: undefined;
|
|
1796
|
+
const rows = [
|
|
1797
|
+
["path", `<code>${escapeHtml(r.path)}</code>`],
|
|
1798
|
+
["size", sizeOf(r.path)],
|
|
1799
|
+
...(fig ? [["kind", fig.kind]] : []),
|
|
1800
|
+
...(fig?.title ? [["title", escapeHtml(fig.title)]] : []),
|
|
1801
|
+
...(fig?.desc ? [["desc", escapeHtml(fig.desc)]] : []),
|
|
1802
|
+
...(snippet ? [["syntax", `<code class="res-snippet">${escapeHtml(snippet)}</code>`]] : []),
|
|
1803
|
+
[
|
|
1804
|
+
k === "icons" || k === "markdown" ? "doc" : "used in",
|
|
1805
|
+
used.length === 0
|
|
1806
|
+
? `<span class="res-sub" data-i18n="none">none</span>`
|
|
1807
|
+
: used
|
|
1808
|
+
.map((e) => `<a href="/${e.id}">${escapeHtml(docDisplayName(e.frontmatter, e.id))}</a>`)
|
|
1809
|
+
.join(", "),
|
|
1810
|
+
],
|
|
1811
|
+
];
|
|
1812
|
+
return {
|
|
1813
|
+
title: `${r.file} — Files`,
|
|
1814
|
+
...(/\.(?:svg|html|termth|md|markdown)$/i.test(r.path) ? { sourceFile: r.path } : {}),
|
|
1815
|
+
main: `<article class="res-page res-file-page">${crumbs}<h1>${escapeHtml(r.file)}</h1><div class="res-preview${k === "icons" ? " square" : ""}">${resourcePreview(site, r)}</div><details class="res-details"><summary data-i18n="File details">File details</summary><dl class="res-dl">${rows.map(([dt, dd]) => `<dt data-i18n="${dt}">${dt}</dt><dd>${dd}</dd>`).join("")}</dl></details></article>`,
|
|
1816
|
+
};
|
|
1817
|
+
};
|
|
1818
|
+
const figurePage = (site, fig) => {
|
|
1819
|
+
const title = fig.title ?? fig.file;
|
|
1820
|
+
return {
|
|
1821
|
+
title,
|
|
1822
|
+
main: docHtml({ title }, [{ type: "figureRef", name: fig.name }], [], site.ctx),
|
|
1823
|
+
};
|
|
1824
|
+
};
|
|
1825
|
+
// Browser tab title — shared by page() and `/api/page` (doc switching via client-side navigation).
|
|
1826
|
+
const pageTitle = (mode, title, siteTitle) => title === siteTitle ? title : `${title} — ${siteTitle ?? `dooc ${mode}`}`;
|
|
1827
|
+
// Navigation follows the manifest hierarchy via routing.docPath.
|
|
1828
|
+
const page = (mode, site, opts) => {
|
|
1829
|
+
const nav = navGroupsHtml(mode, site, opts.currentId, opts.currentRes);
|
|
1830
|
+
const siteTitle = escapeHtml(site.title ?? "dooc");
|
|
1831
|
+
const siteIcon = `<span id="site-icon-slot"${mode === "edit" ? ' title="Change site icon" data-i18n-title="Change site icon"' : ""}><img id="site-icon" alt=""${site.icon ? ` src="${svgDataUri(site.icon)}"` : " hidden"}>${DOCS_ICON}${mode === "edit" ? '<input type="file" accept=".svg,image/svg+xml" aria-label="Change site icon" data-i18n-aria-label="Change site icon">' : ""}</span>`;
|
|
1832
|
+
const bootScripts = opts.boot != null
|
|
1833
|
+
? `<script id="boot" type="application/json">${JSON.stringify(opts.boot).replace(/</g, "\\u003c")}</script>
|
|
1834
|
+
<script src="/assets/edit.js"></script>`
|
|
1835
|
+
: !opts.publish
|
|
1836
|
+
? '<script src="/assets/ui.js"></script>'
|
|
1837
|
+
: "";
|
|
1838
|
+
// Interaction layer of the schema graph (zoom / pan / box drag / enlarged view). Shared by
|
|
1839
|
+
// preview / edit. Figures are readable without JS, so defer is enough (= progressive
|
|
1840
|
+
// enhancement).
|
|
1841
|
+
const main = opts.boot != null
|
|
1842
|
+
? `<main class="editor-workspace">
|
|
1843
|
+
|
|
1844
|
+
|
|
1845
|
+
<div id="preview-language-menu" role="menu" aria-label="Change language" data-i18n-aria-label="Change language" hidden></div>
|
|
1846
|
+
<div id="preview-panes"><section class="preview-pane active" tabindex="0" aria-label="Active pane" data-i18n-aria-label="Active pane"><div class="pane-actions"><button class="pbtn" type="button" data-theme-picker="document" aria-label="Preview theme"></button>${fontButton("viewer")}<button class="pbtn pane-language" type="button" aria-haspopup="menu" aria-expanded="false" aria-controls="preview-language-menu" aria-label="Change language" data-i18n-aria-label="Change language" title="Change language" data-i18n-title="Change language">${GLOBE_ICON}</button></div><div class="preview-content">${opts.main}</div></section></div></main>`
|
|
1847
|
+
: mode === "edit" && opts.currentRes !== undefined
|
|
1848
|
+
? `<main class="resources-pane">${opts.main}</main>`
|
|
1849
|
+
: `<main><div class="page-actions"><button class="pbtn" type="button" data-theme-picker="document" aria-label="Preview theme"></button>${fontButton("viewer")}</div>${opts.main}</main>`;
|
|
1850
|
+
const graphScript = opts.publish
|
|
1851
|
+
? `<script>${inlineScript(opts.publish.graphJs)}</script>`
|
|
1852
|
+
: '<script src="/assets/graph.js" defer></script>';
|
|
1853
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
1854
|
+
${opts.publish ? `<meta name="referrer" content="no-referrer"><meta http-equiv="x-dns-prefetch-control" content="off">${opts.publish.network ? "" : `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline' data:; img-src data: blob:; media-src data: blob:; font-src data: blob:; frame-src data: blob:; connect-src 'none'; base-uri 'none'; form-action 'none'">`}` : ""}
|
|
1855
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1856
|
+
<title>${escapeHtml(pageTitle(mode, opts.title, site.title))}</title>
|
|
1857
|
+
<script>try{const t=localStorage.getItem("docs-preview:theme");if(t)document.documentElement.dataset.theme=t;const e=localStorage.getItem("docs-editor:theme");if(e)document.documentElement.dataset.editorTheme=e}catch{}</script>
|
|
1858
|
+
<style>${fontCss(Boolean(opts.publish))}${CSS}${mode === "edit" ? EDIT_CSS : ""}</style></head>
|
|
1859
|
+
<body${mode === "edit" ? ' class="edit"' : ""}><nav id="sidebar"><div id="sidebar-resize" class="panel-resize" role="separator" tabindex="0" aria-orientation="vertical" aria-controls="sidebar" aria-label="Resize sidebar" data-i18n-aria-label="Resize sidebar" title="Resize sidebar" data-i18n-title="Resize sidebar"></div><div id="sidebar-head">${siteIcon}<div id="site-title" class="site-title" title="${siteTitle}">${siteTitle}</div><button id="sidebar-toggle" type="button" aria-controls="sidebar-content" aria-expanded="true" title="Close sidebar" data-i18n-title="Close sidebar">${SIDEBAR_ICON}</button></div><div id="sidebar-content"><div id="nav-groups">${nav}</div><div id="nav-status" role="status" aria-live="polite"></div></div></nav>${main}${opts.aside ?? ""}
|
|
1860
|
+
${bootScripts}${graphScript}
|
|
1861
|
+
<script id="code-grammars" type="application/json">${JSON.stringify(site.codeGrammars ?? []).replace(/</g, "\\u003c")}</script>
|
|
1862
|
+
${opts.publish ? `<script>${inlineScript(opts.publish.codeJs)}</script>` : '<script src="/assets/code.js" defer></script>'}
|
|
1863
|
+
<script>
|
|
1864
|
+
// Functions toString'd under tsx (esbuild keepNames) contain __name(...) calls; define a no-op.
|
|
1865
|
+
var __name = (f) => f;
|
|
1866
|
+
const contentLanguage = () => JSON.parse(document.getElementById("boot")?.textContent || "null")?.contentLanguage || ${JSON.stringify(site.contentLanguage ?? site.defaultLang)};
|
|
1867
|
+
const localeFetch = (url, init = {}) => fetch(url, { ...init, headers: { ...init.headers, "x-docs-language": contentLanguage() } });
|
|
1868
|
+
(${mountTerms.toString()})(document);
|
|
1869
|
+
(${mountTables.toString()})(document);
|
|
1870
|
+
(${mountToc.toString()})(document);
|
|
1871
|
+
(${mountVideos.toString()})(document);
|
|
1872
|
+
(${mountFigureFrames.toString()})(document);
|
|
1873
|
+
(${mountThemeVariants.toString()})(document);
|
|
1874
|
+
(${mountThemePickers.toString()})(${JSON.stringify(THEME_ICONS)}, ${JSON.stringify(site.themes)});
|
|
1875
|
+
void (${mountFontPickers.toString()})(${createFontSettings.toString()}, ${JSON.stringify(site.fonts ?? []).replace(/</g, "\\u003c")});
|
|
1876
|
+
(() => {
|
|
1877
|
+
const KEY = "docs:sidebar-open";
|
|
1878
|
+
const toggle = document.getElementById("sidebar-toggle");
|
|
1879
|
+
if (!toggle) return;
|
|
1880
|
+
const apply = (open) => {
|
|
1881
|
+
document.body.classList.toggle("sidebar-closed", !open);
|
|
1882
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
1883
|
+
toggle.dataset.i18nTitle = open ? "Close sidebar" : "Open sidebar";
|
|
1884
|
+
toggle.title = window.__docsTranslate ? window.__docsTranslate(toggle.dataset.i18nTitle) : toggle.dataset.i18nTitle;
|
|
1885
|
+
};
|
|
1886
|
+
const narrow = matchMedia("(max-width: 600px)");
|
|
1887
|
+
let preferred;
|
|
1888
|
+
try { const saved = localStorage.getItem(KEY); if (saved !== null) preferred = saved !== "0"; } catch {}
|
|
1889
|
+
let open = !narrow.matches && preferred !== false;
|
|
1890
|
+
apply(open);
|
|
1891
|
+
narrow.addEventListener("change", () => { open = !narrow.matches && preferred !== false; apply(open); });
|
|
1892
|
+
toggle.addEventListener("click", () => {
|
|
1893
|
+
open = !open;
|
|
1894
|
+
preferred = open;
|
|
1895
|
+
try { localStorage.setItem(KEY, open ? "1" : "0"); } catch {}
|
|
1896
|
+
apply(open);
|
|
1897
|
+
});
|
|
1898
|
+
document.getElementById("sidebar-content")?.addEventListener("click", (event) => {
|
|
1899
|
+
if (open || !(event.target instanceof Element) || !event.target.closest(".nav-row")) return;
|
|
1900
|
+
event.preventDefault();
|
|
1901
|
+
event.stopImmediatePropagation();
|
|
1902
|
+
toggle.click();
|
|
1903
|
+
}, true);
|
|
1904
|
+
})();
|
|
1905
|
+
(${mountPanelResize.toString()})();
|
|
1906
|
+
(${mountToolsResize.toString()})();
|
|
1907
|
+
(() => {
|
|
1908
|
+
// Open/close: by default "the branch containing the current doc is open" (the server adds
|
|
1909
|
+
// .open), overridden by explicit user actions (sessionStorage = per tab only).
|
|
1910
|
+
const OPEN_KEY = "docs:nav-open";
|
|
1911
|
+
const readOpen = () => { try { return JSON.parse(sessionStorage.getItem(OPEN_KEY) || "{}"); } catch { return {}; } };
|
|
1912
|
+
const applyOpen = () => {
|
|
1913
|
+
const o = readOpen();
|
|
1914
|
+
for (const it of document.querySelectorAll("#nav-groups .nav-item")) {
|
|
1915
|
+
const key = it.dataset.docId || it.dataset.nodeId;
|
|
1916
|
+
if (key in o) it.classList.toggle("open", o[key]);
|
|
1917
|
+
for (const button of it.querySelectorAll(":scope > .nav-row > [aria-expanded]")) button.setAttribute("aria-expanded", String(it.classList.contains("open")));
|
|
1918
|
+
}
|
|
1919
|
+
(${focusNavTree.toString()})(document.getElementById("nav-groups"));
|
|
1920
|
+
};
|
|
1921
|
+
window.__docsNavOpen = (id, v) => {
|
|
1922
|
+
const o = readOpen();
|
|
1923
|
+
o[id] = v;
|
|
1924
|
+
try { sessionStorage.setItem(OPEN_KEY, JSON.stringify(o)); } catch {}
|
|
1925
|
+
applyOpen();
|
|
1926
|
+
};
|
|
1927
|
+
document.addEventListener("click", (ev) => {
|
|
1928
|
+
const t = ev.target instanceof Element ? ev.target.closest(".nav-tree-toggle") : null;
|
|
1929
|
+
if (!t) return;
|
|
1930
|
+
ev.preventDefault();
|
|
1931
|
+
const it = t.closest(".nav-item");
|
|
1932
|
+
window.__docsNavOpen(it.dataset.docId || it.dataset.nodeId, !it.classList.contains("open"));
|
|
1933
|
+
});
|
|
1934
|
+
applyOpen();
|
|
1935
|
+
window.__docsNavRestore = applyOpen;
|
|
1936
|
+
const nav = document.getElementById("nav-groups");
|
|
1937
|
+
new ResizeObserver(() => (${focusNavTree.toString()})(nav)).observe(nav);
|
|
1938
|
+
})();
|
|
1939
|
+
${mode === "edit"
|
|
1940
|
+
? `(() => {
|
|
1941
|
+
// Sidebar tree editing (delegated to document — keeps working when HMR swaps #nav-groups).
|
|
1942
|
+
// drag: top 1/4 of a row = before, bottom 1/4 = after, middle = move into that doc's children
|
|
1943
|
+
// (at the end). The DOM isn't moved, only a marker is shown; on drop, POST /api/move →
|
|
1944
|
+
// re-fetch the nav regardless of the outcome (= the display is always a copy of
|
|
1945
|
+
// dooc.yaml; no rollback logic).
|
|
1946
|
+
// The root placeholder and row buttons open the create form.
|
|
1947
|
+
const status = document.getElementById("nav-status");
|
|
1948
|
+
const setStatus = (message, error) => {
|
|
1949
|
+
if (!status) return;
|
|
1950
|
+
status.textContent = message;
|
|
1951
|
+
status.classList.toggle("err", Boolean(error));
|
|
1952
|
+
};
|
|
1953
|
+
const refresh = async () => {
|
|
1954
|
+
if (window.__docsRefreshNav) return window.__docsRefreshNav();
|
|
1955
|
+
const cur = document.querySelector("#nav-groups .nav-row a.cur");
|
|
1956
|
+
const id = (cur && cur.closest(".nav-item").dataset.docId) || "";
|
|
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;
|
|
1958
|
+
const res = await localeFetch("/api/nav?current=" + encodeURIComponent(id) + (rp == null ? "" : "&res=" + encodeURIComponent(rp)));
|
|
1959
|
+
if (res.ok) document.getElementById("nav-groups").innerHTML = (await res.json()).html;
|
|
1960
|
+
if (window.__docsNavRestore) window.__docsNavRestore();
|
|
1961
|
+
document.dispatchEvent(new Event("docs:ui-refresh"));
|
|
1962
|
+
};
|
|
1963
|
+
const post = async (path, body) => {
|
|
1964
|
+
// Tell HMR, so the watch notification for our own write (dooc.yaml etc.) doesn't reload.
|
|
1965
|
+
const res = await localeFetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
1966
|
+
const data = await res.json().catch(() => ({}));
|
|
1967
|
+
if (!res.ok) throw new Error(data.error || String(res.status));
|
|
1968
|
+
return data;
|
|
1969
|
+
};
|
|
1970
|
+
|
|
1971
|
+
const docAction = (item, action) => {
|
|
1972
|
+
if (window.__docsDocAction) {
|
|
1973
|
+
void window.__docsDocAction(item.dataset.docId, action);
|
|
1974
|
+
} else {
|
|
1975
|
+
const url = new URL(item.querySelector(":scope > .nav-row > a").href);
|
|
1976
|
+
url.searchParams.set("doc-action", action);
|
|
1977
|
+
location.href = url.href;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
document.addEventListener("click", (event) => {
|
|
1981
|
+
const button = event.target instanceof Element ? event.target.closest("[data-doc-action]") : null;
|
|
1982
|
+
if (!button) return;
|
|
1983
|
+
event.preventDefault();
|
|
1984
|
+
docAction(button.closest(".nav-item"), button.dataset.docAction);
|
|
1985
|
+
});
|
|
1986
|
+
const editLabel = (event) => {
|
|
1987
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
|
|
1988
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
1989
|
+
if (!target || target.closest("button, input, form, .nav-doc-icon")) return;
|
|
1990
|
+
const row = target.closest("#nav-groups [data-doc-id] > .nav-row");
|
|
1991
|
+
if (!row) return;
|
|
1992
|
+
event.preventDefault();
|
|
1993
|
+
docAction(row.closest("[data-doc-id]"), "rename");
|
|
1994
|
+
};
|
|
1995
|
+
document.addEventListener("dblclick", editLabel);
|
|
1996
|
+
document.addEventListener("keydown", (event) => { if (event.key === "F2") editLabel(event); });
|
|
1997
|
+
const setIcon = async (slot, file) => {
|
|
1998
|
+
if (file && !/\\.svg$/i.test(file.name) && file.type !== "image/svg+xml") {
|
|
1999
|
+
setStatus("Icon must be svg", true);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
const id = slot.closest(".nav-item")?.dataset.docId;
|
|
2003
|
+
try {
|
|
2004
|
+
const data = file ? btoa(Array.from(new Uint8Array(await file.arrayBuffer()), (byte) => String.fromCharCode(byte)).join("")) : null;
|
|
2005
|
+
const result = await post(id ? "/api/icon" : "/api/site-icon", { file: id ? id + ".md" : undefined, data });
|
|
2006
|
+
if (!id) {
|
|
2007
|
+
const logo = document.getElementById("site-icon");
|
|
2008
|
+
logo.src = result.icon;
|
|
2009
|
+
logo.hidden = false;
|
|
2010
|
+
}
|
|
2011
|
+
setStatus("", false);
|
|
2012
|
+
await refresh();
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
setStatus(error.message, true);
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
document.addEventListener("change", (event) => {
|
|
2018
|
+
const input = event.target;
|
|
2019
|
+
if (!(input instanceof HTMLInputElement) || !input.closest(".nav-doc-icon, #site-icon-slot")) return;
|
|
2020
|
+
const file = input.files[0];
|
|
2021
|
+
input.value = "";
|
|
2022
|
+
if (file) void setIcon(input.closest(".nav-doc-icon, #site-icon-slot"), file);
|
|
2023
|
+
});
|
|
2024
|
+
document.addEventListener("click", (event) => {
|
|
2025
|
+
const clear = event.target instanceof Element ? event.target.closest(".nav-icon-clear") : null;
|
|
2026
|
+
if (!clear) return;
|
|
2027
|
+
event.preventDefault();
|
|
2028
|
+
void setIcon(clear.closest(".nav-doc-icon"), null);
|
|
2029
|
+
});
|
|
2030
|
+
document.addEventListener("dragover", (event) => {
|
|
2031
|
+
const slot = event.target instanceof Element ? event.target.closest(".nav-doc-icon, #sidebar-head") : null;
|
|
2032
|
+
if (!slot || !event.dataTransfer?.types.includes("Files")) return;
|
|
2033
|
+
event.preventDefault();
|
|
2034
|
+
slot.classList.add("over");
|
|
2035
|
+
});
|
|
2036
|
+
document.addEventListener("dragleave", (event) => {
|
|
2037
|
+
const slot = event.target instanceof Element ? event.target.closest(".nav-doc-icon, #sidebar-head") : null;
|
|
2038
|
+
if (slot && !slot.contains(event.relatedTarget)) slot.classList.remove("over");
|
|
2039
|
+
});
|
|
2040
|
+
document.addEventListener("drop", (event) => {
|
|
2041
|
+
const slot = event.target instanceof Element ? event.target.closest(".nav-doc-icon, #sidebar-head") : null;
|
|
2042
|
+
const file = event.dataTransfer?.files[0];
|
|
2043
|
+
if (!slot || !file) return;
|
|
2044
|
+
event.preventDefault();
|
|
2045
|
+
slot.classList.remove("over");
|
|
2046
|
+
void setIcon(slot, file);
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
let dragged = null;
|
|
2050
|
+
let drop = null; // { row, zone: "before" | "after" | "into" }
|
|
2051
|
+
const clearMarker = () => {
|
|
2052
|
+
for (const r of document.querySelectorAll(".nav-row.drop-before, .nav-row.drop-after, .nav-row.drop-into")) {
|
|
2053
|
+
r.classList.remove("drop-before", "drop-after", "drop-into");
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
document.addEventListener("dragstart", (event) => {
|
|
2057
|
+
const row = event.target instanceof Element ? event.target.closest(".nav-row") : null;
|
|
2058
|
+
if (!row || !event.dataTransfer) return;
|
|
2059
|
+
if (event.target.closest("input, button, .nav-doc-icon") || row.classList.contains("renaming")) {
|
|
2060
|
+
event.preventDefault();
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
dragged = row.closest(".nav-item");
|
|
2064
|
+
dragged.classList.add("dragging");
|
|
2065
|
+
event.dataTransfer.effectAllowed = "move";
|
|
2066
|
+
event.dataTransfer.setData("application/x-doc-id", dragged.dataset.docId || "");
|
|
2067
|
+
});
|
|
2068
|
+
document.addEventListener("dragend", () => {
|
|
2069
|
+
if (dragged) dragged.classList.remove("dragging");
|
|
2070
|
+
clearMarker();
|
|
2071
|
+
dragged = null;
|
|
2072
|
+
drop = null;
|
|
2073
|
+
});
|
|
2074
|
+
document.addEventListener("dragover", (event) => {
|
|
2075
|
+
if (!dragged || !event.dataTransfer) return;
|
|
2076
|
+
const row = event.target instanceof Element ? event.target.closest("#nav-groups .nav-row") : null;
|
|
2077
|
+
const item = row && row.closest(".nav-item");
|
|
2078
|
+
// Can't drop onto itself or its own descendants (cycle).
|
|
2079
|
+
if (!item || !item.dataset.docId || dragged.contains(item)) { clearMarker(); drop = null; return; }
|
|
2080
|
+
event.preventDefault();
|
|
2081
|
+
event.dataTransfer.dropEffect = "move";
|
|
2082
|
+
const rect = row.getBoundingClientRect();
|
|
2083
|
+
const y = (event.clientY - rect.top) / rect.height;
|
|
2084
|
+
const zone = y < 0.25 ? "before" : y > 0.75 ? "after" : "into";
|
|
2085
|
+
clearMarker();
|
|
2086
|
+
row.classList.add("drop-" + zone);
|
|
2087
|
+
drop = { row, zone };
|
|
2088
|
+
});
|
|
2089
|
+
document.addEventListener("drop", async (event) => {
|
|
2090
|
+
if (!dragged || !drop) return;
|
|
2091
|
+
event.preventDefault();
|
|
2092
|
+
const id = dragged.dataset.docId;
|
|
2093
|
+
const item = drop.row.closest(".nav-item");
|
|
2094
|
+
let parent;
|
|
2095
|
+
let index;
|
|
2096
|
+
if (drop.zone === "into") {
|
|
2097
|
+
parent = item.dataset.docId;
|
|
2098
|
+
index = [...item.querySelector(":scope > .nav-list").children].filter((n) => n.dataset.docId && n !== dragged).length;
|
|
2099
|
+
if (window.__docsNavOpen) window.__docsNavOpen(parent, true);
|
|
2100
|
+
} else {
|
|
2101
|
+
const list = item.parentElement;
|
|
2102
|
+
parent = list.dataset.parent || undefined;
|
|
2103
|
+
const siblings = [...list.children].filter((n) => n.dataset.docId && n !== dragged);
|
|
2104
|
+
index = siblings.indexOf(item) + (drop.zone === "after" ? 1 : 0);
|
|
2105
|
+
}
|
|
2106
|
+
clearMarker();
|
|
2107
|
+
setStatus("Moving…", false);
|
|
2108
|
+
try {
|
|
2109
|
+
await post("/api/move", { id, parent, index });
|
|
2110
|
+
setStatus("Moved", false);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
setStatus("Failed to move: " + error.message, true);
|
|
2113
|
+
}
|
|
2114
|
+
await refresh();
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
document.addEventListener("docs:files-changed", () => { void refresh(); });
|
|
2118
|
+
|
|
2119
|
+
// ── In-sidebar create form ──
|
|
2120
|
+
const suggestFileName = ${suggestDocFileName.toString()};
|
|
2121
|
+
const closeForm = () => { for (const f of document.querySelectorAll(".nav-new-item")) f.remove(); };
|
|
2122
|
+
const openForm = (parent) => {
|
|
2123
|
+
closeForm();
|
|
2124
|
+
const host = parent
|
|
2125
|
+
? document.querySelector('#nav-groups .nav-item[data-doc-id="' + CSS.escape(parent) + '"] > .nav-list')
|
|
2126
|
+
: document.querySelector("#nav-groups > .nav-list");
|
|
2127
|
+
if (!host) return;
|
|
2128
|
+
if (parent) {
|
|
2129
|
+
window.__docsNavOpen(parent, true);
|
|
2130
|
+
}
|
|
2131
|
+
const item = document.createElement("div");
|
|
2132
|
+
item.className = "nav-item nav-new-item";
|
|
2133
|
+
const row = document.createElement("div");
|
|
2134
|
+
row.className = "nav-row";
|
|
2135
|
+
const form = document.createElement("form");
|
|
2136
|
+
form.className = "nav-new";
|
|
2137
|
+
form.innerHTML =
|
|
2138
|
+
${JSON.stringify(DOCS_ICON)} +
|
|
2139
|
+
'<input name="title" placeholder="title" data-i18n-placeholder="title" required autocomplete="off">' +
|
|
2140
|
+
'<div class="nav-new-file"><input name="name" placeholder="File name" data-i18n-placeholder="File name" aria-label="File name" data-i18n-aria-label="File name" required autocomplete="off" spellcheck="false">' +
|
|
2141
|
+
'<button class="nav-new-go" type="submit" title="Create" data-i18n-title="Create" aria-label="Create" data-i18n-aria-label="Create">↵</button></div>' +
|
|
2142
|
+
'<div class="err"></div>';
|
|
2143
|
+
row.append(form);
|
|
2144
|
+
item.append(row);
|
|
2145
|
+
host.append(item);
|
|
2146
|
+
document.dispatchEvent(new Event("docs:ui-refresh"));
|
|
2147
|
+
const title = form.elements.namedItem("title");
|
|
2148
|
+
const nameInput = form.elements.namedItem("name");
|
|
2149
|
+
const submit = form.querySelector(".nav-new-go");
|
|
2150
|
+
const err = form.querySelector(".err");
|
|
2151
|
+
let nameTouched = false;
|
|
2152
|
+
let busy = false;
|
|
2153
|
+
title.addEventListener("input", () => { if (!nameTouched) nameInput.value = suggestFileName(title.value); });
|
|
2154
|
+
nameInput.addEventListener("input", () => { nameTouched = true; });
|
|
2155
|
+
form.addEventListener("keydown", (e) => { if (e.key === "Escape" && !busy) closeForm(); });
|
|
2156
|
+
// Close when focus moves out with nothing typed (keep partially filled forms).
|
|
2157
|
+
// (remove itself fires focusout, so defer one tick and close only if still there)
|
|
2158
|
+
form.addEventListener("focusout", (e) => {
|
|
2159
|
+
if (busy || form.contains(e.relatedTarget) || title.value.trim() || nameInput.value.trim()) return;
|
|
2160
|
+
setTimeout(() => { if (item.isConnected && !item.contains(document.activeElement)) item.remove(); }, 0);
|
|
2161
|
+
});
|
|
2162
|
+
form.addEventListener("submit", async (e) => {
|
|
2163
|
+
e.preventDefault();
|
|
2164
|
+
if (busy) return;
|
|
2165
|
+
busy = submit.disabled = title.readOnly = nameInput.readOnly = true;
|
|
2166
|
+
submit.setAttribute("aria-busy", "true");
|
|
2167
|
+
err.textContent = "";
|
|
2168
|
+
try {
|
|
2169
|
+
await post("/api/create", { parent: parent || undefined, name: nameInput.value.trim(), title: title.value.trim() });
|
|
2170
|
+
closeForm();
|
|
2171
|
+
await refresh();
|
|
2172
|
+
} catch (error) {
|
|
2173
|
+
err.textContent = error.message;
|
|
2174
|
+
} finally {
|
|
2175
|
+
busy = submit.disabled = title.readOnly = nameInput.readOnly = false;
|
|
2176
|
+
submit.removeAttribute("aria-busy");
|
|
2177
|
+
}
|
|
2178
|
+
});
|
|
2179
|
+
item.scrollIntoView({ block: "nearest" });
|
|
2180
|
+
title.focus();
|
|
2181
|
+
};
|
|
2182
|
+
document.addEventListener("click", (event) => {
|
|
2183
|
+
const btn = event.target instanceof Element ? event.target.closest("#sidebar .new, #sidebar .nav-create") : null;
|
|
2184
|
+
if (!btn) return;
|
|
2185
|
+
event.preventDefault();
|
|
2186
|
+
openForm(btn.dataset.parent || "");
|
|
2187
|
+
});
|
|
2188
|
+
})();`
|
|
2189
|
+
: ""}
|
|
2190
|
+
// code block copy — delegated to document so it keeps working after edit's innerHTML swaps.
|
|
2191
|
+
document.addEventListener("click", async (event) => {
|
|
2192
|
+
const button = event.target instanceof Element ? event.target.closest("button.section-copy") : null;
|
|
2193
|
+
if (!button) return;
|
|
2194
|
+
const label = (key) => window.__docsTranslate ? window.__docsTranslate(key) : key;
|
|
2195
|
+
button.classList.remove("ok", "error");
|
|
2196
|
+
try {
|
|
2197
|
+
await navigator.clipboard.writeText(new URL(button.dataset.sectionUrl, location.href).href);
|
|
2198
|
+
button.classList.add("ok");
|
|
2199
|
+
button.title = label("Copied");
|
|
2200
|
+
} catch {
|
|
2201
|
+
button.classList.add("error");
|
|
2202
|
+
button.title = label("Could not copy URL");
|
|
2203
|
+
}
|
|
2204
|
+
button.setAttribute("aria-label", button.title);
|
|
2205
|
+
setTimeout(() => {
|
|
2206
|
+
button.classList.remove("ok", "error");
|
|
2207
|
+
button.title = label("Copy section URL");
|
|
2208
|
+
button.setAttribute("aria-label", button.title);
|
|
2209
|
+
}, 1600);
|
|
2210
|
+
});
|
|
2211
|
+
document.addEventListener("click", (ev) => {
|
|
2212
|
+
const btn = ev.target instanceof Element ? ev.target.closest("button.codecopy") : null;
|
|
2213
|
+
if (!btn) return;
|
|
2214
|
+
const wrap = btn.closest(".codeblk");
|
|
2215
|
+
const code = wrap && wrap.querySelector("pre code");
|
|
2216
|
+
if (!code) return;
|
|
2217
|
+
navigator.clipboard.writeText(code.textContent || "").catch(() => {});
|
|
2218
|
+
btn.classList.add("ok");
|
|
2219
|
+
setTimeout(() => btn.classList.remove("ok"), 1200);
|
|
2220
|
+
});
|
|
2221
|
+
// Code blocks toggle from their border label.
|
|
2222
|
+
document.addEventListener("click", (ev) => {
|
|
2223
|
+
const head = ev.target instanceof Element ? ev.target.closest(".codeblk-head") : null;
|
|
2224
|
+
if (!head || (ev.target instanceof Element && ev.target.closest("button.codecopy"))) return;
|
|
2225
|
+
const wrap = head.closest(".codeblk");
|
|
2226
|
+
if (!wrap) return;
|
|
2227
|
+
const collapsed = wrap.classList.toggle("collapsed");
|
|
2228
|
+
const button = head.querySelector(".codefold");
|
|
2229
|
+
if (button) {
|
|
2230
|
+
const label = collapsed ? "Expand code" : "Collapse code";
|
|
2231
|
+
button.setAttribute("aria-expanded", String(!collapsed));
|
|
2232
|
+
button.dataset.i18nTitle = label;
|
|
2233
|
+
button.dataset.i18nAriaLabel = label;
|
|
2234
|
+
const text = window.__docsTranslate ? window.__docsTranslate(label) : label;
|
|
2235
|
+
button.title = text;
|
|
2236
|
+
button.setAttribute("aria-label", text);
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
</script>
|
|
2240
|
+
<script>${inlineScript(`(${mountSearch.toString()})(${JSON.stringify(opts.publish ? searchIndex(site) : []).replace(/</g, "\\u003c")}, ${searchSections.toString()}, ${JSON.stringify(Object.fromEntries(SEARCH_LABELS.map((key) => [key, translate(site.defaultLang, key)]))).replace(/</g, "\\u003c")}, ${Boolean(opts.publish)});`)}</script>
|
|
2241
|
+
${opts.publish ? `${opts.publish.pages}<script>${publishRouterScript(opts.publish.first)}${opts.publish.network ? `\n(${mountBundleNetwork.toString()})();` : ""}</script>` : `<script>${hmrClientScript(mode)}</script>`}</body></html>`;
|
|
2242
|
+
};
|
|
2243
|
+
// A `</script` inside an inline <script> would close the script early, so escape it.
|
|
2244
|
+
const inlineScript = (js) => js.replace(/<\/(script)/gi, "<\\/$1");
|
|
2245
|
+
// Router for the published output — keeps each route's main content in <template data-route>
|
|
2246
|
+
// and swaps <main> based on location.hash (`#/<ancestor/.../docId>[#<anchor>]`) (works without a server).
|
|
2247
|
+
const publishRouterScript = (first) => `
|
|
2248
|
+
(() => {
|
|
2249
|
+
const FIRST = ${JSON.stringify(first)};
|
|
2250
|
+
const main = document.querySelector("main");
|
|
2251
|
+
const pages = new Map([...document.querySelectorAll("template[data-route]")].map((t) => [t.dataset.route, t]));
|
|
2252
|
+
const show = () => {
|
|
2253
|
+
const [path, rawAnchor] = location.hash.slice(1).split("#");
|
|
2254
|
+
let anchor = "";
|
|
2255
|
+
try { anchor = decodeURIComponent(rawAnchor || ""); } catch {}
|
|
2256
|
+
const p = (path || "").split("?")[0].replace(/^\\/+|\\/+$/g, "");
|
|
2257
|
+
const route = pages.has(p) ? p : FIRST;
|
|
2258
|
+
const page = pages.get(route);
|
|
2259
|
+
const actions = main.querySelector(":scope > .page-actions");
|
|
2260
|
+
main.innerHTML = page.innerHTML;
|
|
2261
|
+
if (actions) main.prepend(actions);
|
|
2262
|
+
document.title = page.dataset.title;
|
|
2263
|
+
for (const a of document.querySelectorAll("#nav-groups a")) {
|
|
2264
|
+
a.classList.toggle("cur", a.getAttribute("href").split("?")[0] === "#/" + route);
|
|
2265
|
+
}
|
|
2266
|
+
const cur = document.querySelector("#nav-groups a.cur");
|
|
2267
|
+
for (let it = cur && cur.closest(".nav-item").parentElement.closest(".nav-item"); it; it = it.parentElement.closest(".nav-item")) {
|
|
2268
|
+
it.classList.add("open");
|
|
2269
|
+
}
|
|
2270
|
+
(${focusNavTree.toString()})(document.getElementById("nav-groups"));
|
|
2271
|
+
const target = anchor && document.getElementById(anchor);
|
|
2272
|
+
requestAnimationFrame(() => {
|
|
2273
|
+
if (target) target.scrollIntoView(); else window.scrollTo(0, 0);
|
|
2274
|
+
});
|
|
2275
|
+
};
|
|
2276
|
+
window.addEventListener("hashchange", show);
|
|
2277
|
+
show();
|
|
2278
|
+
})();`;
|
|
2279
|
+
// Maps the server's absolute-path links (`/<route>`) and in-body anchors (`#<id>`) to the
|
|
2280
|
+
// published output's hash routes.
|
|
2281
|
+
const toHashLinks = (html, route) => html
|
|
2282
|
+
.replace(/(href|data-section-url)="\/(?!\/)/g, '$1="#/')
|
|
2283
|
+
.replace(/href="#(?!\/)/g, `href="#/${route}#`);
|
|
2284
|
+
/**
|
|
2285
|
+
* Turns the whole Site into a single HTML file readable without a server (everything embedded,
|
|
2286
|
+
* down to figures / terms / schema graph JS). Each doc is held in a
|
|
2287
|
+
* <template> and switched via hash routes.
|
|
2288
|
+
*/
|
|
2289
|
+
export const publishHtml = (site, graphJs, title = site.title ?? "dooc", codeJs = "", mode = "static") => {
|
|
2290
|
+
const pages = site.entries.map((e) => ({
|
|
2291
|
+
route: docPath(site.entries, e.id).slice(1),
|
|
2292
|
+
title: docDisplayName(e.frontmatter, e.id),
|
|
2293
|
+
main: entryMain(site, e),
|
|
2294
|
+
}));
|
|
2295
|
+
const first = pages[0]?.route ?? "";
|
|
2296
|
+
if (site.ctx.terms.length > 0) {
|
|
2297
|
+
const terms = resourcesMain(site, "terms");
|
|
2298
|
+
if (terms)
|
|
2299
|
+
pages.push({ route: "_files/terms", ...terms });
|
|
2300
|
+
}
|
|
2301
|
+
const figureRefs = new Set(site.entries.flatMap((entry) => entry.figureRefs ?? []));
|
|
2302
|
+
for (const name of figureRefs) {
|
|
2303
|
+
const fig = site.ctx.figures.get(name);
|
|
2304
|
+
if (!fig)
|
|
2305
|
+
continue;
|
|
2306
|
+
const route = `_files/file/${encodeURIComponent(fig.file)}`;
|
|
2307
|
+
pages.push({ route, ...figurePage(site, fig) });
|
|
2308
|
+
}
|
|
2309
|
+
const templates = pages
|
|
2310
|
+
.map((p) => `<template data-route="${escapeHtml(p.route)}" data-title="${escapeHtml(pageTitle("preview", p.title, title))}">${toHashLinks(p.main, p.route)}</template>`)
|
|
2311
|
+
.join("");
|
|
2312
|
+
return toHashLinks(page("preview", { ...site, title }, {
|
|
2313
|
+
title,
|
|
2314
|
+
main: "",
|
|
2315
|
+
publish: { graphJs, codeJs, pages: templates, first, network: mode === "network" },
|
|
2316
|
+
}), first);
|
|
2317
|
+
};
|
|
2318
|
+
// A doc's main content. A doc with an empty body and children (a chapter-heading-only doc) shows
|
|
2319
|
+
// its children list in place of the body.
|
|
2320
|
+
const entryMain = (site, e) => {
|
|
2321
|
+
const html = renderEntryHtml(site, e).html;
|
|
2322
|
+
const kids = childrenOf(site.entries, e.id);
|
|
2323
|
+
if (e.blocks.length > 0 || kids.length === 0) {
|
|
2324
|
+
return html;
|
|
2325
|
+
}
|
|
2326
|
+
const items = kids
|
|
2327
|
+
.map((k) => `<li><a href="${docPath(site.entries, k.id)}">${escapeHtml(docDisplayName(k.frontmatter, k.id))}</a></li>`)
|
|
2328
|
+
.join("");
|
|
2329
|
+
return html.replace(/<\/article>\s*$/, `<ul class="idx">${items}</ul></article>`);
|
|
2330
|
+
};
|
|
2331
|
+
export const languageFieldHtml = (defaultLang, language = contentLanguageCode(defaultLang), translatedLanguages = [contentLanguageCode(defaultLang)], declaredLanguages) => {
|
|
2332
|
+
const existing = new Set(translatedLanguages);
|
|
2333
|
+
const options = [...contentLanguages]
|
|
2334
|
+
.filter(({ code }) => !declaredLanguages || declaredLanguages.includes(code))
|
|
2335
|
+
.sort((a, b) => Number(existing.has(b.code)) - Number(existing.has(a.code)))
|
|
2336
|
+
.map(({ code, name }) => `<option value="${code}" data-name="${escapeHtml(name)}" data-added="${existing.has(code)}"${code === language ? " selected" : ""}>${escapeHtml(name)} (${code})</option>`)
|
|
2337
|
+
.join("");
|
|
2338
|
+
return `<div class="fm-language"><button class="pbtn" type="button" data-language-open aria-label="Change language" data-i18n-aria-label="Change language" aria-haspopup="listbox" aria-expanded="false" aria-controls="fm-lang-options">${GLOBE_ICON}</button><select id="fm-lang" data-source="${contentLanguageCode(defaultLang)}" aria-label="Change language" data-i18n-aria-label="Change language">${options}</select></div>`;
|
|
2339
|
+
};
|
|
2340
|
+
// The same thing is passed for the initial render (editorAside) and doc switch (`/api/page`)
|
|
2341
|
+
// (= the client only has to handle one boot shape, initially and after a switch).
|
|
2342
|
+
export const editorBoot = (entry, site) => {
|
|
2343
|
+
// Completion candidates are files in the figures/ catalog.
|
|
2344
|
+
const figures = [...site.ctx.figures.values()].map((f) => f.file);
|
|
2345
|
+
const docs = site.entries.map((e) => ({
|
|
2346
|
+
ref: e.sourceFile ?? CONTENT_PREFIX + e.file,
|
|
2347
|
+
title: docDisplayName(e.frontmatter, e.id),
|
|
2348
|
+
}));
|
|
2349
|
+
return {
|
|
2350
|
+
gitBase: entryGitBase(entry, site),
|
|
2351
|
+
...(site.title === undefined ? {} : { siteTitle: site.title }),
|
|
2352
|
+
...(site.icon === undefined ? {} : { siteIcon: svgDataUri(site.icon) }),
|
|
2353
|
+
...(entry.frontmatter?.lang ? { docLanguage: entry.frontmatter.lang } : {}),
|
|
2354
|
+
...(entry.translatedLanguages ? { translatedLanguages: entry.translatedLanguages } : {}),
|
|
2355
|
+
...(site.contentLanguage
|
|
2356
|
+
? {
|
|
2357
|
+
contentLanguage: site.contentLanguage,
|
|
2358
|
+
sourceLanguage: contentLanguageCode(site.defaultLang),
|
|
2359
|
+
}
|
|
2360
|
+
: {}),
|
|
2361
|
+
file: entry.file,
|
|
2362
|
+
sourceFile: entry.sourceFile,
|
|
2363
|
+
docId: entry.id,
|
|
2364
|
+
title: entry.frontmatter?.title ?? "",
|
|
2365
|
+
path: docPath(site.entries, entry.id),
|
|
2366
|
+
body: entry.body,
|
|
2367
|
+
figures,
|
|
2368
|
+
codeLanguages: site.codeLanguages ?? CODE_LANGS,
|
|
2369
|
+
docs,
|
|
2370
|
+
terms: [...site.ctx.terms],
|
|
2371
|
+
blockLines: entry.blockLines,
|
|
2372
|
+
baseRaw: entry.raw,
|
|
2373
|
+
icon: entry.icon ? svgDataUri(entry.icon) : null,
|
|
2374
|
+
defaultLang: site.defaultLang,
|
|
2375
|
+
spellcheckLang: resolveLanguage(entry.lang, site.languages).spellcheckLang ?? entry.lang,
|
|
2376
|
+
filePreviews: filePreviewResults(entry, site),
|
|
2377
|
+
previews: site.previews,
|
|
2378
|
+
};
|
|
2379
|
+
};
|
|
2380
|
+
// Language and editor actions stay in the header; document actions live in the tree.
|
|
2381
|
+
const editorAside = (entry, site) => {
|
|
2382
|
+
const aside = `<aside id="panel"><div id="resize" class="panel-resize" role="separator" tabindex="0" aria-orientation="vertical" aria-controls="panel" aria-label="Resize editor" data-i18n-aria-label="Resize editor" title="Resize editor" data-i18n-title="Resize editor"></div>
|
|
2383
|
+
<div id="phead">
|
|
2384
|
+
<button class="pbtn" type="button" data-theme-picker="editor" aria-label="Editor theme"></button>
|
|
2385
|
+
${fontButton("editor")}
|
|
2386
|
+
${languageFieldHtml(site.defaultLang, site.contentLanguage, entry.translatedLanguages, site.contentLanguages)}
|
|
2387
|
+
<span id="status"></span>
|
|
2388
|
+
<button id="validate" class="pbtn" type="button" title="Validate and apply" data-i18n-title="Validate and apply" aria-label="Validate and apply" data-i18n-aria-label="Validate and apply">${icon('<path d="M5 5l-4 5 4 5M15 5l4 5-4 5M7 10l2 2 4-5"/>')}</button>
|
|
2389
|
+
<button id="save" class="pbtn" data-state="saved" type="button" title="Save (Ctrl+S)" data-i18n-title="Save (Ctrl+S)" aria-label="Save" data-i18n-aria-label="Save">${icon('<path d="M3 3h11l3 3v11H3z"/><path d="M6 3v5h7V3M6 17v-6h8v6"/>')}</button>
|
|
2390
|
+
<button id="split-preview" class="pbtn" type="button" aria-pressed="false" aria-label="Split view" data-i18n-aria-label="Split view" title="Split view" data-i18n-title="Split view">${icon('<rect x="3" y="3" width="14" height="14" rx="2"/><path d="M10 3v14"/>')}</button>
|
|
2391
|
+
<div id="gear"><button class="pbtn" type="button" aria-haspopup="true" aria-label="View settings" data-i18n-aria-label="View settings" title="View settings" data-i18n-title="View settings">${GEAR_ICON}</button><div id="gearmenu">
|
|
2392
|
+
<div id="language-setting"><select id="ui-language" aria-label="Editor language" data-i18n-aria-label="Language"><option value="eng">English</option><option value="jpn">日本語</option><option value="zho">中文</option></select></div>
|
|
2393
|
+
<label><input type="checkbox" id="opt-autovalidate"><span data-i18n="Auto validate">Auto validate</span></label>
|
|
2394
|
+
<label><input type="checkbox" id="opt-autosave"><span data-i18n="Auto save">Auto save</span></label>
|
|
2395
|
+
<label><input type="checkbox" id="opt-corr"><span data-i18n="Source mapping">Source mapping</span></label>
|
|
2396
|
+
<label><input type="checkbox" id="opt-git-diff"><span data-i18n="Show Git changes">Show Git changes</span></label>
|
|
2397
|
+
</div></div>
|
|
2398
|
+
</div>
|
|
2399
|
+
|
|
2400
|
+
<div id="cm"></div>
|
|
2401
|
+
</aside>`;
|
|
2402
|
+
return { aside, boot: editorBoot(entry, site) };
|
|
2403
|
+
};
|
|
2404
|
+
/**
|
|
2405
|
+
* Response for `GET /api/page?route=/<ancestor/.../docId>` (status + body). mode === "edit" only.
|
|
2406
|
+
* The diff for doing edit's doc switch via client-side navigation (= keep scroll position /
|
|
2407
|
+
* search term / open state without rebuilding the sidebar DOM); it returns only what the client
|
|
2408
|
+
* replaces: main html + the panel's variable parts (file name / front-matter fields) + the new
|
|
2409
|
+
* doc's boot.
|
|
2410
|
+
* File views replace the central workspace while retaining the current editor.
|
|
2411
|
+
*/
|
|
2412
|
+
export const pageDataResponse = (site, path) => {
|
|
2413
|
+
if (typeof path !== "string" || !path.startsWith("/")) {
|
|
2414
|
+
return { status: 400, body: { error: "route must be /<docId>" } };
|
|
2415
|
+
}
|
|
2416
|
+
if (path === "/_extensions" ||
|
|
2417
|
+
path === "/_git" ||
|
|
2418
|
+
path === "/_statics" ||
|
|
2419
|
+
path.startsWith("/_statics/") ||
|
|
2420
|
+
path === "/_files" ||
|
|
2421
|
+
path.startsWith("/_files/")) {
|
|
2422
|
+
try {
|
|
2423
|
+
const resource = resourcesMain(site, path.startsWith("/_statics")
|
|
2424
|
+
? path.slice(1)
|
|
2425
|
+
: ["/_git", "/_extensions"].includes(path)
|
|
2426
|
+
? path.slice(1)
|
|
2427
|
+
: path.slice("/_files".length).replace(/^\/|\/$/g, ""));
|
|
2428
|
+
return resource
|
|
2429
|
+
? {
|
|
2430
|
+
status: 200,
|
|
2431
|
+
body: {
|
|
2432
|
+
resource: true,
|
|
2433
|
+
path,
|
|
2434
|
+
title: pageTitle("edit", resource.title, site.title),
|
|
2435
|
+
html: docLinks(site, resource.main),
|
|
2436
|
+
sourceFile: resource.sourceFile,
|
|
2437
|
+
},
|
|
2438
|
+
}
|
|
2439
|
+
: { status: 404, body: { error: `${path} is not a file route` } };
|
|
2440
|
+
}
|
|
2441
|
+
catch {
|
|
2442
|
+
return { status: 400, body: { error: "Invalid file route" } };
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
const id = resolveDocPath(site.entries, path);
|
|
2446
|
+
const doc = site.entries.find((e) => e.id === id);
|
|
2447
|
+
if (!doc) {
|
|
2448
|
+
return { status: 404, body: { error: `${path} is not a doc route` } };
|
|
2449
|
+
}
|
|
2450
|
+
const html = entryMain(site, doc);
|
|
2451
|
+
const errors = [...site.errors, ...doc.errors];
|
|
2452
|
+
return {
|
|
2453
|
+
status: 200,
|
|
2454
|
+
body: {
|
|
2455
|
+
title: pageTitle("edit", docDisplayName(doc.frontmatter, doc.id), site.title),
|
|
2456
|
+
file: doc.file,
|
|
2457
|
+
html,
|
|
2458
|
+
errors,
|
|
2459
|
+
toc: doc.toc,
|
|
2460
|
+
boot: editorBoot(doc, site),
|
|
2461
|
+
filePreviews: filePreviewResults(doc, site),
|
|
2462
|
+
},
|
|
2463
|
+
};
|
|
2464
|
+
};
|
|
2465
|
+
const readJson = async (req, limit) => {
|
|
2466
|
+
try {
|
|
2467
|
+
const v = JSON.parse(await readBody(req, limit));
|
|
2468
|
+
return typeof v === "object" && v != null ? v : undefined;
|
|
2469
|
+
}
|
|
2470
|
+
catch {
|
|
2471
|
+
return undefined;
|
|
2472
|
+
}
|
|
2473
|
+
};
|
|
2474
|
+
const notFound = (res, path) => {
|
|
2475
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
2476
|
+
res.end(`not found: ${path}`);
|
|
2477
|
+
};
|
|
2478
|
+
const sendJson = (res, code, body) => {
|
|
2479
|
+
res.writeHead(code, { "content-type": "application/json; charset=utf-8" });
|
|
2480
|
+
res.end(JSON.stringify(body));
|
|
2481
|
+
};
|
|
2482
|
+
const sendOp = (res, r) => {
|
|
2483
|
+
if (isOpError(r)) {
|
|
2484
|
+
const { status, ...body } = r;
|
|
2485
|
+
sendJson(res, status, body);
|
|
2486
|
+
}
|
|
2487
|
+
else {
|
|
2488
|
+
sendJson(res, 200, { ok: true, ...r });
|
|
2489
|
+
}
|
|
2490
|
+
};
|
|
2491
|
+
const languageLinks = (html, locale) => html.replace(/href="(\/(?!\/)[^"#?]*)(#[^"]*)?"/g, (_match, path, hash = "") => `href="${path}?lang=${encodeURIComponent(locale)}${hash}"`);
|
|
2492
|
+
const sendPage = (res, mode, site, opts) => {
|
|
2493
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
2494
|
+
const html = docLinks(site, page(mode, site, opts));
|
|
2495
|
+
res.end(site.contentLanguage ? languageLinks(html, site.contentLanguage) : html);
|
|
2496
|
+
};
|
|
2497
|
+
const readBody = (req, limit = 5_000_000) => new Promise((resolve, reject) => {
|
|
2498
|
+
let data = "";
|
|
2499
|
+
req.setEncoding("utf8");
|
|
2500
|
+
req.on("data", (chunk) => {
|
|
2501
|
+
data += chunk;
|
|
2502
|
+
if (data.length > limit) {
|
|
2503
|
+
reject(new Error("body too large"));
|
|
2504
|
+
req.destroy();
|
|
2505
|
+
}
|
|
2506
|
+
});
|
|
2507
|
+
req.on("end", () => resolve(data));
|
|
2508
|
+
req.on("error", reject);
|
|
2509
|
+
});
|
|
2510
|
+
// Preview refresh (/api/render) and save (/api/save) — they differ only in the disk write and
|
|
2511
|
+
// share the response shape (the client can update the left pane straight from a save response).
|
|
2512
|
+
const handleDraft = async (req, res, save, locale) => {
|
|
2513
|
+
let parsedBody;
|
|
2514
|
+
try {
|
|
2515
|
+
parsedBody = JSON.parse(await readBody(req));
|
|
2516
|
+
}
|
|
2517
|
+
catch {
|
|
2518
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
const { file, frontmatter, body, baseRaw, textfiles } = (parsedBody ?? {});
|
|
2522
|
+
let fileDrafts;
|
|
2523
|
+
try {
|
|
2524
|
+
fileDrafts = previewTextfiles(root, textfiles);
|
|
2525
|
+
}
|
|
2526
|
+
catch (error) {
|
|
2527
|
+
sendJson(res, 400, { error: String(error) });
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
const target = resolveContentFile(file);
|
|
2531
|
+
if (!target) {
|
|
2532
|
+
sendJson(res, 400, { error: "file must be <id>.md" });
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
if (save && !existsSync(target.abs)) {
|
|
2536
|
+
sendJson(res, 404, { error: `${target.name} not found (use + in the nav to create)` });
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
// So that autosave never silently overwrites external changes (direct edits / git pull etc.),
|
|
2540
|
+
// compare the current disk content with the base (the raw the client last read) right before
|
|
2541
|
+
// saving. If they differ, skip the write and return the disk content for the client to load
|
|
2542
|
+
// into the editor (= the md file wins).
|
|
2543
|
+
const site = loadSite(locale);
|
|
2544
|
+
const currentRaw = save
|
|
2545
|
+
? (site.entries.find((entry) => entry.id === target.id)?.raw ??
|
|
2546
|
+
readFileSync(target.abs, "utf8"))
|
|
2547
|
+
: undefined;
|
|
2548
|
+
if (save && currentRaw !== undefined && isExternallyModified(baseRaw, currentRaw)) {
|
|
2549
|
+
const { raw: _raw, ...doc } = renderDocFile(site, target.name, currentRaw);
|
|
2550
|
+
sendJson(res, 200, { ...doc, saved: false, conflict: true, raw: currentRaw });
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
const fm = sanitizeFrontmatter(frontmatter);
|
|
2554
|
+
const text = typeof body === "string" ? body : "";
|
|
2555
|
+
if (locale !== contentLanguageCode(site.defaultLang))
|
|
2556
|
+
fm.lang = locale;
|
|
2557
|
+
const raw = assembleDoc(fm, text);
|
|
2558
|
+
if (save) {
|
|
2559
|
+
if (locale === contentLanguageCode(site.defaultLang))
|
|
2560
|
+
writeFileSync(target.abs, raw);
|
|
2561
|
+
else {
|
|
2562
|
+
const file = translationFile(sourceDocFile(target.id, readManifest(root).files), locale);
|
|
2563
|
+
const base = existsSync(textfilePath(root, file)) ? (currentRaw ?? null) : null;
|
|
2564
|
+
if (!writeTextfile(root, file, raw, base)) {
|
|
2565
|
+
sendJson(res, 409, { error: "Translation changed. Reload before saving." });
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
const draft = renderDraft(loadSite(locale), target.name, raw, fileDrafts);
|
|
2571
|
+
sendJson(res, 200, { ...draft, saved: save, conflict: false, raw });
|
|
2572
|
+
};
|
|
2573
|
+
// Prose lint (/api/lint) = the `lint` of the doc language's adapter (language socket); languages
|
|
2574
|
+
// without one return no findings. Lints the body string (same coordinate system as the CM doc)
|
|
2575
|
+
// and returns findings with offsets — spellchecking is left to the browser (client side).
|
|
2576
|
+
const handleLint = async (req, res) => {
|
|
2577
|
+
let parsedBody;
|
|
2578
|
+
try {
|
|
2579
|
+
parsedBody = JSON.parse(await readBody(req));
|
|
2580
|
+
}
|
|
2581
|
+
catch {
|
|
2582
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2585
|
+
const { body, lang } = (parsedBody ?? {});
|
|
2586
|
+
const tag = typeof lang === "string" && isLangTag(lang)
|
|
2587
|
+
? lang
|
|
2588
|
+
: (readManifest(root).defaultLang ?? DEFAULT_LANG);
|
|
2589
|
+
const { lint } = resolveLanguage(tag, siteLanguages.languages);
|
|
2590
|
+
const findings = lint ? await lint(typeof body === "string" ? body : "") : [];
|
|
2591
|
+
sendJson(res, 200, { findings });
|
|
2592
|
+
};
|
|
2593
|
+
// Create a new doc; the sidebar refresh keeps the current document open.
|
|
2594
|
+
const handleCreate = async (req, res) => {
|
|
2595
|
+
const body = await readJson(req);
|
|
2596
|
+
const name = typeof body?.name === "string" ? body.name.trim() : "";
|
|
2597
|
+
const title = typeof body?.title === "string" ? body.title.trim() : "";
|
|
2598
|
+
const parent = typeof body?.parent === "string" && body.parent !== "" ? body.parent : undefined;
|
|
2599
|
+
const result = ops.addFile(name, title, parent);
|
|
2600
|
+
sendOp(res, isOpError(result) ? result : { ...result, path: docPath(loadSite().entries, result.id) });
|
|
2601
|
+
};
|
|
2602
|
+
// Add a figure (sidebar file picker / drag-and-drop). Returns the written file name
|
|
2603
|
+
// (the client inserts ).
|
|
2604
|
+
const handleFigure = async (req, res) => {
|
|
2605
|
+
const body = await readJson(req);
|
|
2606
|
+
if (typeof body?.name !== "string" || typeof body?.data !== "string") {
|
|
2607
|
+
sendJson(res, 400, { error: "specify an svg / png / webp / jpeg / html file" });
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const result = ops.addFigure(body.name, Buffer.from(body.data, "base64"));
|
|
2611
|
+
sendOp(res, isOpError(result) ? result : { ...result, path: result.path ?? `figures/${result.file}` });
|
|
2612
|
+
};
|
|
2613
|
+
// Doc icon (the header's icon slot). data = base64 of an svg (null = remove). Pads it to 1:1 and
|
|
2614
|
+
// writes icons/<id>.svg. Response icon = the new data URI (null if removed).
|
|
2615
|
+
const handleIcon = async (req, res) => {
|
|
2616
|
+
const body = await readJson(req);
|
|
2617
|
+
const target = resolveContentFile(body?.file);
|
|
2618
|
+
if (!target || !existsSync(target.abs)) {
|
|
2619
|
+
sendJson(res, 404, { error: "target doc not found" });
|
|
2620
|
+
return;
|
|
2621
|
+
}
|
|
2622
|
+
const manifest = readManifest(root);
|
|
2623
|
+
if (manifest.errors.length) {
|
|
2624
|
+
sendJson(res, 409, { error: manifest.errors.join("\n") });
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
const previous = docIconFile(target.id, manifest.files);
|
|
2628
|
+
const file = manifest.files?.defaultLayout !== false &&
|
|
2629
|
+
!Object.hasOwn(manifest.files?.icons ?? {}, target.id) &&
|
|
2630
|
+
!existsSync(textfilePath(root, previous))
|
|
2631
|
+
? defaultFilePath(`${target.id}-icon.svg`)
|
|
2632
|
+
: previous;
|
|
2633
|
+
const path = textfilePath(root, file);
|
|
2634
|
+
if (body?.data == null) {
|
|
2635
|
+
if (existsSync(path)) {
|
|
2636
|
+
unlinkSync(path);
|
|
2637
|
+
}
|
|
2638
|
+
sendJson(res, 200, { ok: true, icon: null });
|
|
2639
|
+
return;
|
|
2640
|
+
}
|
|
2641
|
+
const svg = typeof body.data === "string"
|
|
2642
|
+
? squareSvg(Buffer.from(body.data, "base64").toString("utf8"))
|
|
2643
|
+
: undefined;
|
|
2644
|
+
if (!svg) {
|
|
2645
|
+
sendJson(res, 400, { error: "specify an svg (viewBox or width / height required)" });
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
if (file !== previous && existsSync(path)) {
|
|
2649
|
+
sendJson(res, 409, { error: "File already exists" });
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2653
|
+
writeFileSync(path, svg);
|
|
2654
|
+
if (file !== previous) {
|
|
2655
|
+
manifest.files ??= parseFileLayout({});
|
|
2656
|
+
manifest.files.icons ??= {};
|
|
2657
|
+
manifest.files.icons[target.id] = file;
|
|
2658
|
+
writeManifest(root, manifest.tree, manifest);
|
|
2659
|
+
}
|
|
2660
|
+
sendJson(res, 200, { ok: true, icon: svgDataUri(svg) });
|
|
2661
|
+
};
|
|
2662
|
+
// Sidebar DnD move (reorder + reparent). Rewrites only dooc.yaml (file / id unchanged).
|
|
2663
|
+
const handleMove = async (req, res) => {
|
|
2664
|
+
const body = await readJson(req);
|
|
2665
|
+
const { id, parent, index } = body ?? {};
|
|
2666
|
+
if (typeof id !== "string" ||
|
|
2667
|
+
(parent !== undefined && typeof parent !== "string") ||
|
|
2668
|
+
typeof index !== "number") {
|
|
2669
|
+
sendJson(res, 400, { error: "id / parent? / index required" });
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
sendOp(res, ops.move(id, parent, index));
|
|
2673
|
+
};
|
|
2674
|
+
// Rename the source file, update its references, and preserve the document's URL.
|
|
2675
|
+
const handleRename = async (req, res) => {
|
|
2676
|
+
const body = await readJson(req);
|
|
2677
|
+
const target = resolveContentFile(body?.file);
|
|
2678
|
+
if (!target) {
|
|
2679
|
+
sendJson(res, 404, { error: "target doc not found" });
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
try {
|
|
2683
|
+
const snapshot = filesSnapshot(root);
|
|
2684
|
+
const file = sourceDocFile(target.id, snapshot.layout);
|
|
2685
|
+
if (body?.sourceFile !== file)
|
|
2686
|
+
throw new Error("Files changed. Reload before retrying.");
|
|
2687
|
+
const result = updateFiles(root, {
|
|
2688
|
+
action: "rename",
|
|
2689
|
+
file,
|
|
2690
|
+
name: typeof body?.name === "string" ? body.name.trim() : "",
|
|
2691
|
+
revision: snapshot.revision,
|
|
2692
|
+
});
|
|
2693
|
+
sendOp(res, {
|
|
2694
|
+
path: docPath(loadSite().entries, target.id),
|
|
2695
|
+
sourceFile: sourceDocFile(target.id, result.layout),
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
catch (error) {
|
|
2699
|
+
sendJson(res, 409, { error: error instanceof Error ? error.message : String(error) });
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
// Delete a doc. If it's referenced, refuse without force and return the referrers (the client
|
|
2703
|
+
// confirms and resends with force). Response parent = where to navigate after deletion.
|
|
2704
|
+
const handleDelete = async (req, res) => {
|
|
2705
|
+
const body = await readJson(req);
|
|
2706
|
+
const target = resolveContentFile(body?.file);
|
|
2707
|
+
if (!target) {
|
|
2708
|
+
sendJson(res, 404, { error: "target doc not found" });
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
const result = ops.remove(target.id, body?.force === true);
|
|
2712
|
+
sendOp(res, isOpError(result)
|
|
2713
|
+
? result
|
|
2714
|
+
: {
|
|
2715
|
+
...result,
|
|
2716
|
+
path: result.parent ? docPath(loadSite().entries, result.parent) : "/",
|
|
2717
|
+
});
|
|
2718
|
+
};
|
|
2719
|
+
export const createBundleCache = () => {
|
|
2720
|
+
const store = new Map();
|
|
2721
|
+
return {
|
|
2722
|
+
get: async (entry, build) => {
|
|
2723
|
+
const hit = store.get(entry);
|
|
2724
|
+
if (hit) {
|
|
2725
|
+
return hit;
|
|
2726
|
+
}
|
|
2727
|
+
const js = await build();
|
|
2728
|
+
const etag = `"${createHash("sha256").update(js).digest("hex")}"`;
|
|
2729
|
+
const value = { js, etag };
|
|
2730
|
+
store.set(entry, value);
|
|
2731
|
+
return value;
|
|
2732
|
+
},
|
|
2733
|
+
clear: () => store.clear(),
|
|
2734
|
+
};
|
|
2735
|
+
};
|
|
2736
|
+
// The npm package (running as dist/*.js) has no src/, so read the prebuilt bundles.
|
|
2737
|
+
const PREBUILT = {
|
|
2738
|
+
"core/schema-graph-global.tsx": "flow.global.js",
|
|
2739
|
+
"viewer/edit-client.ts": "edit.global.js",
|
|
2740
|
+
"viewer/ui-client.ts": "ui.global.js",
|
|
2741
|
+
"viewer/code-highlight.ts": "code.global.js",
|
|
2742
|
+
};
|
|
2743
|
+
/** Client bundle (src/<entry> as an IIFE). Sourcemap for the server, minified for publish. */
|
|
2744
|
+
export const bundleClient = async (entry, publish = false) => {
|
|
2745
|
+
const prebuilt = PREBUILT[entry];
|
|
2746
|
+
if (usePrebuiltAssets && prebuilt) {
|
|
2747
|
+
return readViewerAsset(prebuilt).toString("utf8");
|
|
2748
|
+
}
|
|
2749
|
+
const esbuild = await import("esbuild");
|
|
2750
|
+
const out = await esbuild.build({
|
|
2751
|
+
entryPoints: [join(pkgRoot, "src", entry)],
|
|
2752
|
+
bundle: true,
|
|
2753
|
+
format: "iife",
|
|
2754
|
+
target: "es2022",
|
|
2755
|
+
write: false,
|
|
2756
|
+
...(publish ? { minify: true } : { sourcemap: "inline" }),
|
|
2757
|
+
});
|
|
2758
|
+
return out.outputFiles?.[0]?.text ?? "";
|
|
2759
|
+
};
|
|
2760
|
+
export const startServer = async (mode, port = 5179, pluginModule, startup) => {
|
|
2761
|
+
// Language adapters are read once; the manifest is re-read by the compiler.
|
|
2762
|
+
siteLanguages = await loadSiteLanguages(root);
|
|
2763
|
+
const viewerPlugins = await loadViewerPlugins(pluginModule);
|
|
2764
|
+
workspaceSource = viewerPlugins.workspace;
|
|
2765
|
+
let gitOrigin = viewerPlugins.gitOrigin;
|
|
2766
|
+
const handleGit = createGitHandler(root, () => gitOrigin);
|
|
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);
|
|
2772
|
+
// Client bundles (bundleClient) = edit's editing UI and the schema graph interaction layer
|
|
2773
|
+
// shared by preview/edit. React Flow's stylesheet isn't mixed into the bundle — the mount
|
|
2774
|
+
// layer (flow-css.ts) injects it into a <style> at runtime, so the page needs no CSS delivery
|
|
2775
|
+
// path (same conditions as the published output).
|
|
2776
|
+
// Cache the bundle result + etag per entry (graph.js / edit.js) — this path isn't hit by the
|
|
2777
|
+
// per-keystroke /api/render / /api/save, but tsx watch doesn't restart for these, so
|
|
2778
|
+
// invalidate explicitly on the watcher's "client" notification (changes to edit-client.ts /
|
|
2779
|
+
// the schema graph interaction layer's src/*.tsx).
|
|
2780
|
+
const bundleCache = createBundleCache();
|
|
2781
|
+
// HMR — bootId changes on every process start. It's how the client detects a tsx watch
|
|
2782
|
+
// restart.
|
|
2783
|
+
const hub = createHmrHub(`${process.pid}-${Date.now()}`);
|
|
2784
|
+
const viewer = {
|
|
2785
|
+
register: handlePlugins.register,
|
|
2786
|
+
unregister: handlePlugins.unregister,
|
|
2787
|
+
setWorkspace(workspace) {
|
|
2788
|
+
workspaceSource = workspace === undefined ? undefined : parseWorkspaceSource(workspace);
|
|
2789
|
+
hub.broadcast("content", []);
|
|
2790
|
+
},
|
|
2791
|
+
setGitOrigin(origin) {
|
|
2792
|
+
gitOrigin = origin === undefined ? undefined : validateGitOrigin(origin);
|
|
2793
|
+
},
|
|
2794
|
+
setAllowedDomains(domains) {
|
|
2795
|
+
if (!Array.isArray(domains))
|
|
2796
|
+
throw new Error("Allowed domains must be a list");
|
|
2797
|
+
const previews = parsePreviewSettings({ domains });
|
|
2798
|
+
applicationPreviews = previews;
|
|
2799
|
+
compiler = createCompiler(root, { previews });
|
|
2800
|
+
localeCompilers.clear();
|
|
2801
|
+
hub.broadcast("content", []);
|
|
2802
|
+
},
|
|
2803
|
+
};
|
|
2804
|
+
viewer.setAllowedDomains(viewerPlugins.allowedDomains ?? []);
|
|
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
|
+
}
|
|
2821
|
+
// DOCS_HMR=0 = no hot reload (nothing is watched = changes need a manual reload).
|
|
2822
|
+
if (process.env.DOCS_HMR !== "0") {
|
|
2823
|
+
const watcher = watchProject(root, pkgRoot, (kind, files) => {
|
|
2824
|
+
console.log(`hmr: ${kind} (${files.join(", ")})`);
|
|
2825
|
+
if (kind === "client") {
|
|
2826
|
+
bundleCache.clear();
|
|
2827
|
+
}
|
|
2828
|
+
hub.broadcast(kind, files);
|
|
2829
|
+
});
|
|
2830
|
+
watchTextfiles = watcher.watchTextfiles;
|
|
2831
|
+
process.once("exit", watcher.close);
|
|
2832
|
+
}
|
|
2833
|
+
const handle = async (req, res) => {
|
|
2834
|
+
if (await handlePlugins(req, res))
|
|
2835
|
+
return;
|
|
2836
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
2837
|
+
const requestedLanguage = req.headers["x-docs-language"] ??
|
|
2838
|
+
requestUrl.searchParams.get("lang") ??
|
|
2839
|
+
contentLanguageCode(readManifest(root).defaultLang ?? DEFAULT_LANG);
|
|
2840
|
+
if (typeof requestedLanguage !== "string" || !isContentLanguage(requestedLanguage)) {
|
|
2841
|
+
sendJson(res, 400, { error: "Expected an ISO 639-3 language code" });
|
|
2842
|
+
return;
|
|
2843
|
+
}
|
|
2844
|
+
const locale = requestedLanguage;
|
|
2845
|
+
const path = decodeURIComponent(requestUrl.pathname);
|
|
2846
|
+
if (req.method === "GET" && path.startsWith("/assets/fonts/")) {
|
|
2847
|
+
const font = readFont(path.slice("/assets/fonts/".length));
|
|
2848
|
+
if (!font) {
|
|
2849
|
+
notFound(res, path);
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
res.writeHead(200, {
|
|
2853
|
+
"content-type": "font/woff2",
|
|
2854
|
+
"cache-control": "public, max-age=86400",
|
|
2855
|
+
});
|
|
2856
|
+
res.end(font);
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
// No favicon (this only silences the 404 console error from the browser's auto-request).
|
|
2860
|
+
if (path === "/favicon.ico") {
|
|
2861
|
+
res.writeHead(204);
|
|
2862
|
+
res.end();
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
if (req.method === "GET" && path === HMR_PATH) {
|
|
2866
|
+
hub.handle(res);
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
// Client bundles — graph.js (the schema graph interaction layer) is shared by
|
|
2870
|
+
// preview / edit, edit.js is for the editing UI only.
|
|
2871
|
+
const clientEntry = path === "/assets/code.js"
|
|
2872
|
+
? "viewer/code-highlight.ts"
|
|
2873
|
+
: path === "/assets/graph.js"
|
|
2874
|
+
? "core/schema-graph-global.tsx"
|
|
2875
|
+
: mode === "edit" && path === "/assets/edit.js"
|
|
2876
|
+
? "viewer/edit-client.ts"
|
|
2877
|
+
: path === "/assets/ui.js"
|
|
2878
|
+
? "viewer/ui-client.ts"
|
|
2879
|
+
: undefined;
|
|
2880
|
+
if (req.method === "GET" && clientEntry != null) {
|
|
2881
|
+
const { js, etag } = await bundleCache.get(clientEntry, () => bundleClient(clientEntry));
|
|
2882
|
+
if (req.headers["if-none-match"] === etag) {
|
|
2883
|
+
res.writeHead(304, { etag, "cache-control": "no-cache" });
|
|
2884
|
+
res.end();
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
res.writeHead(200, {
|
|
2888
|
+
"content-type": "application/javascript; charset=utf-8",
|
|
2889
|
+
etag,
|
|
2890
|
+
"cache-control": "no-cache",
|
|
2891
|
+
});
|
|
2892
|
+
res.end(js);
|
|
2893
|
+
return;
|
|
2894
|
+
}
|
|
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
|
+
}
|
|
2922
|
+
if (path === "/api/git") {
|
|
2923
|
+
res.setHeader("cache-control", "no-store");
|
|
2924
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"]) ||
|
|
2925
|
+
(req.headers.host !== `localhost:${req.socket.localPort}` &&
|
|
2926
|
+
req.headers.host !== `127.0.0.1:${req.socket.localPort}` &&
|
|
2927
|
+
req.headers.host !== `[::1]:${req.socket.localPort}`)) {
|
|
2928
|
+
sendJson(res, 403, {
|
|
2929
|
+
error: "Git requires a same-origin local editor request",
|
|
2930
|
+
});
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
try {
|
|
2934
|
+
if ((workspaceSource ?? readWorkspaceSource(root)).protocol !== "git")
|
|
2935
|
+
throw new Error("Git workspace is not available");
|
|
2936
|
+
const input = await readJson(req, 1_048_576);
|
|
2937
|
+
const result = await handleGit(input);
|
|
2938
|
+
if (workspaceSource?.protocol === "git" &&
|
|
2939
|
+
(input?.action === "switch" || input?.action === "branch")) {
|
|
2940
|
+
const local = readWorkspaceSource(root);
|
|
2941
|
+
if (local.protocol === "git")
|
|
2942
|
+
workspaceSource = { ...workspaceSource, branch: local.branch };
|
|
2943
|
+
}
|
|
2944
|
+
sendJson(res, 200, result);
|
|
2945
|
+
}
|
|
2946
|
+
catch (error) {
|
|
2947
|
+
sendJson(res, 409, {
|
|
2948
|
+
error: error instanceof Error ? error.message : "Git operation failed",
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
if (path === "/api/files") {
|
|
2954
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
2955
|
+
sendJson(res, 403, {
|
|
2956
|
+
error: "Files require a same-origin local editor request",
|
|
2957
|
+
});
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
try {
|
|
2961
|
+
const input = await readJson(req, Math.ceil((MAX_FILE_UPLOAD_BYTES * 4) / 3) + 16_384);
|
|
2962
|
+
if (!input)
|
|
2963
|
+
throw new Error("Invalid file request");
|
|
2964
|
+
const result = input.action === "read"
|
|
2965
|
+
? filesSnapshot(root)
|
|
2966
|
+
: input.action === "upload"
|
|
2967
|
+
? uploadFile(root, input)
|
|
2968
|
+
: updateFiles(root, input);
|
|
2969
|
+
sendJson(res, 200, result);
|
|
2970
|
+
}
|
|
2971
|
+
catch (error) {
|
|
2972
|
+
sendJson(res, 409, {
|
|
2973
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2974
|
+
});
|
|
2975
|
+
}
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (path === "/api/code-file") {
|
|
2979
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
2980
|
+
sendJson(res, 403, {
|
|
2981
|
+
error: "Code tools require a same-origin local editor request",
|
|
2982
|
+
});
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const result = codeFileResponse(root, await readJson(req), applicationPreviews);
|
|
2986
|
+
sendJson(res, result.status, result.body);
|
|
2987
|
+
return;
|
|
2988
|
+
}
|
|
2989
|
+
if (path === "/api/textfile") {
|
|
2990
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
2991
|
+
sendJson(res, 403, {
|
|
2992
|
+
error: "Text files require a same-origin local editor request",
|
|
2993
|
+
});
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
const input = await readJson(req);
|
|
2997
|
+
if (input?.action === "preview") {
|
|
2998
|
+
try {
|
|
2999
|
+
if (typeof input.file !== "string")
|
|
3000
|
+
throw new Error("Expected a file path");
|
|
3001
|
+
const files = previewTextfiles(root, {
|
|
3002
|
+
[input.file]: input.content,
|
|
3003
|
+
});
|
|
3004
|
+
const [file, content] = [...files][0];
|
|
3005
|
+
sendJson(res, 200, {
|
|
3006
|
+
html: textfilePreviewHtml(loadSite(locale), file, content),
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
catch (error) {
|
|
3010
|
+
sendJson(res, 400, { error: String(error) });
|
|
3011
|
+
}
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
if (input?.action === "validate") {
|
|
3015
|
+
try {
|
|
3016
|
+
if (typeof input.file !== "string" ||
|
|
3017
|
+
typeof input.content !== "string" ||
|
|
3018
|
+
isRemoteTextfile(input.file))
|
|
3019
|
+
throw new Error("Invalid text file tool request");
|
|
3020
|
+
previewTextfiles(root, { [input.file]: input.content });
|
|
3021
|
+
const name = textfileName(input.file);
|
|
3022
|
+
const extension = (name.split(".").at(-1) || name).toLowerCase();
|
|
3023
|
+
const grammar = siteLanguages.codeGrammars?.find((entry) => entry.name === extension || entry.aliases?.includes(extension));
|
|
3024
|
+
const language = existsSync(join(root, ".dooc/editor-tools", `${extension}.json`))
|
|
3025
|
+
? extension
|
|
3026
|
+
: (grammar?.name ?? extension);
|
|
3027
|
+
const result = await validateTextfileCode(root, language, input.content);
|
|
3028
|
+
for (const message of textfilePreviewErrors(loadSite(locale), input.file, result.content))
|
|
3029
|
+
result.findings.push({
|
|
3030
|
+
message,
|
|
3031
|
+
range: [0, result.content.length],
|
|
3032
|
+
ruleId: "preview",
|
|
3033
|
+
});
|
|
3034
|
+
sendJson(res, 200, result);
|
|
3035
|
+
}
|
|
3036
|
+
catch (error) {
|
|
3037
|
+
sendJson(res, 400, {
|
|
3038
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
const result = await textfileResponse(root, input, applicationPreviews.domains);
|
|
3044
|
+
if (result.status === 200 &&
|
|
3045
|
+
typeof input?.file === "string" &&
|
|
3046
|
+
!isRemoteTextfile(input.file)) {
|
|
3047
|
+
result.body.gitBase =
|
|
3048
|
+
(workspaceSource ?? readWorkspaceSource(root)).protocol === "git"
|
|
3049
|
+
? gitBaseText(root, input.file)
|
|
3050
|
+
: null;
|
|
3051
|
+
}
|
|
3052
|
+
sendJson(res, result.status, result.body);
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
if (path === "/api/render" || path === "/api/save") {
|
|
3056
|
+
await handleDraft(req, res, path === "/api/save", locale);
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (path === "/api/translation") {
|
|
3060
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
3061
|
+
sendJson(res, 403, {
|
|
3062
|
+
error: "Translations require a same-origin local editor request",
|
|
3063
|
+
});
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
const input = await readJson(req);
|
|
3067
|
+
const target = resolveContentFile(input?.file);
|
|
3068
|
+
if (!target ||
|
|
3069
|
+
typeof input?.language !== "string" ||
|
|
3070
|
+
typeof input.added !== "boolean") {
|
|
3071
|
+
sendJson(res, 400, { error: "file / language / added required" });
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
try {
|
|
3075
|
+
setDocTranslation(root, contentLanguageCode(readManifest(root).defaultLang ?? DEFAULT_LANG), target.id, input.language, input.added);
|
|
3076
|
+
sendJson(res, 200, {
|
|
3077
|
+
translatedLanguages: loadSite().entries.find((entry) => entry.id === target.id)?.translatedLanguages,
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
catch (error) {
|
|
3081
|
+
sendJson(res, 400, { error: String(error) });
|
|
3082
|
+
}
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
if (path === "/api/code-tool") {
|
|
3086
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
3087
|
+
sendJson(res, 403, {
|
|
3088
|
+
error: "Code tools require a same-origin local editor request",
|
|
3089
|
+
});
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
const input = await readJson(req);
|
|
3093
|
+
const target = resolveContentFile(input?.file);
|
|
3094
|
+
if (!target ||
|
|
3095
|
+
!existsSync(target.abs) ||
|
|
3096
|
+
typeof input?.body !== "string" ||
|
|
3097
|
+
(input.action !== "validate" && typeof input.position !== "number") ||
|
|
3098
|
+
(input.action !== "format" &&
|
|
3099
|
+
input.action !== "lint" &&
|
|
3100
|
+
input.action !== "validate")) {
|
|
3101
|
+
sendJson(res, 400, { error: "Invalid code tool request" });
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
try {
|
|
3105
|
+
sendJson(res, 200, input.action === "validate"
|
|
3106
|
+
? await validateCodeTools(root, input.body)
|
|
3107
|
+
: await runCodeTool(root, input.body, Number(input.position), input.action));
|
|
3108
|
+
}
|
|
3109
|
+
catch (error) {
|
|
3110
|
+
sendJson(res, 400, {
|
|
3111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
if (path === "/api/lint") {
|
|
3117
|
+
await handleLint(req, res);
|
|
3118
|
+
return;
|
|
3119
|
+
}
|
|
3120
|
+
if (path === "/api/create") {
|
|
3121
|
+
await handleCreate(req, res);
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
if (path === "/api/icon") {
|
|
3125
|
+
await handleIcon(req, res);
|
|
3126
|
+
return;
|
|
3127
|
+
}
|
|
3128
|
+
if (path === "/api/site-icon") {
|
|
3129
|
+
if (!isLocalToolRequest(req.headers.host ?? "", req.headers.origin, req.headers["content-type"])) {
|
|
3130
|
+
sendJson(res, 403, {
|
|
3131
|
+
error: "Site icons require a same-origin local editor request",
|
|
3132
|
+
});
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
const result = siteIconResponse(root, await readJson(req));
|
|
3136
|
+
sendJson(res, result.status, result.body);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
if (path === "/api/figure") {
|
|
3140
|
+
await handleFigure(req, res);
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
if (path === "/api/move") {
|
|
3144
|
+
await handleMove(req, res);
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
if (path === "/api/rename") {
|
|
3148
|
+
await handleRename(req, res);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
if (path === "/api/delete") {
|
|
3152
|
+
await handleDelete(req, res);
|
|
3153
|
+
return;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
const site = loadSite(locale);
|
|
3157
|
+
if (req.method === "GET" && path === "/api/search") {
|
|
3158
|
+
sendJson(res, 200, searchIndex(site));
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
// edit-mode-only APIs (used by edit-client.ts). handle() strips the query from path, so
|
|
3162
|
+
// the query is read from req.url via URL/URLSearchParams.
|
|
3163
|
+
if (mode === "edit" && req.method === "GET" && path === "/api/nav") {
|
|
3164
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
3165
|
+
sendJson(res, 200, {
|
|
3166
|
+
siteTitle: site.title,
|
|
3167
|
+
siteIcon: site.icon ? svgDataUri(site.icon) : undefined,
|
|
3168
|
+
translatedLanguages: site.entries.find((entry) => entry.id === requestUrl.searchParams.get("current"))?.translatedLanguages,
|
|
3169
|
+
html: navGroupsHtml(mode, site, params.get("current") || undefined, params.get("res") ?? undefined),
|
|
3170
|
+
});
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
if (mode === "edit" && req.method === "GET" && path === "/api/page") {
|
|
3174
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
3175
|
+
const { status, body } = pageDataResponse(site, params.get("route"));
|
|
3176
|
+
sendJson(res, status, body);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
if (mode === "edit" && req.method === "GET" && path === "/api/doc") {
|
|
3180
|
+
const params = new URL(req.url ?? "/", "http://localhost").searchParams;
|
|
3181
|
+
const { status, body } = docQueryResponse(site, params.get("file"));
|
|
3182
|
+
sendJson(res, status, body);
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
// "/" redirects to the first doc (the canonical URL follows the manifest hierarchy). In edit with 0
|
|
3186
|
+
// docs, an empty page (the first doc is created via "add doc" at the end of the sidebar).
|
|
3187
|
+
if (path === "/") {
|
|
3188
|
+
const first = site.entries[0];
|
|
3189
|
+
if (first) {
|
|
3190
|
+
res.writeHead(302, {
|
|
3191
|
+
location: `${docPath(site.entries, first.id)}?lang=${encodeURIComponent(locale)}`,
|
|
3192
|
+
});
|
|
3193
|
+
res.end();
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
if (mode === "edit") {
|
|
3197
|
+
sendPage(res, mode, site, {
|
|
3198
|
+
title: "docs",
|
|
3199
|
+
main: "<article></article>",
|
|
3200
|
+
});
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
notFound(res, path);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const linkedFigure = path.startsWith("/_files/file/")
|
|
3207
|
+
? [...site.ctx.figures.values()].find((fig) => fig.file === path.slice("/_files/file/".length))
|
|
3208
|
+
: undefined;
|
|
3209
|
+
if (mode === "preview" && linkedFigure) {
|
|
3210
|
+
sendPage(res, mode, site, figurePage(site, linkedFigure));
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
if ((mode === "edit" &&
|
|
3214
|
+
(path === "/_extensions" ||
|
|
3215
|
+
path === "/_git" ||
|
|
3216
|
+
path === "/_statics" ||
|
|
3217
|
+
path.startsWith("/_statics/") ||
|
|
3218
|
+
path === "/_files" ||
|
|
3219
|
+
path.startsWith("/_files/"))) ||
|
|
3220
|
+
path === "/_files/terms") {
|
|
3221
|
+
const rest = path.startsWith("/_statics")
|
|
3222
|
+
? path.slice(1)
|
|
3223
|
+
: ["/_git", "/_extensions"].includes(path)
|
|
3224
|
+
? path.slice(1)
|
|
3225
|
+
: requestUrl.pathname.slice("/_files".length).replace(/^\/|\/$/g, "");
|
|
3226
|
+
const page = resourcesMain(site, rest);
|
|
3227
|
+
if (!page) {
|
|
3228
|
+
notFound(res, path);
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
const editor = mode === "edit"
|
|
3232
|
+
? (site.entries.find((entry) => entry.id === requestUrl.searchParams.get("editor")) ?? site.entries[0])
|
|
3233
|
+
: undefined;
|
|
3234
|
+
if (editor) {
|
|
3235
|
+
const { aside, boot } = editorAside(editor, site);
|
|
3236
|
+
sendPage(res, mode, site, {
|
|
3237
|
+
title: page.title,
|
|
3238
|
+
main: entryMain(site, editor),
|
|
3239
|
+
currentId: editor.id,
|
|
3240
|
+
currentRes: rest,
|
|
3241
|
+
aside,
|
|
3242
|
+
boot: {
|
|
3243
|
+
...boot,
|
|
3244
|
+
resource: {
|
|
3245
|
+
path,
|
|
3246
|
+
title: pageTitle(mode, page.title, site.title),
|
|
3247
|
+
html: docLinks(site, page.main),
|
|
3248
|
+
sourceFile: page.sourceFile,
|
|
3249
|
+
},
|
|
3250
|
+
},
|
|
3251
|
+
});
|
|
3252
|
+
return;
|
|
3253
|
+
}
|
|
3254
|
+
sendPage(res, mode, site, { ...page, currentRes: rest });
|
|
3255
|
+
return;
|
|
3256
|
+
}
|
|
3257
|
+
const id = resolveDocPath(site.entries, path);
|
|
3258
|
+
const doc = site.entries.find((e) => e.id === id);
|
|
3259
|
+
if (!doc) {
|
|
3260
|
+
notFound(res, path);
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
const opts = {
|
|
3264
|
+
title: docDisplayName(doc.frontmatter, doc.id),
|
|
3265
|
+
main: entryMain(site, doc),
|
|
3266
|
+
currentId: doc.id,
|
|
3267
|
+
};
|
|
3268
|
+
if (mode === "edit") {
|
|
3269
|
+
Object.assign(opts, editorAside(doc, site));
|
|
3270
|
+
}
|
|
3271
|
+
sendPage(res, mode, site, opts);
|
|
3272
|
+
};
|
|
3273
|
+
const server = createServer((req, res) => {
|
|
3274
|
+
handle(req, res).catch((e) => {
|
|
3275
|
+
if (!res.headersSent) {
|
|
3276
|
+
res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
3277
|
+
}
|
|
3278
|
+
res.end(`error: ${e instanceof Error ? e.message : e}`);
|
|
3279
|
+
});
|
|
3280
|
+
});
|
|
3281
|
+
// If the port is in use (e.g. a previous instance left running), automatically try +1, up to
|
|
3282
|
+
// 10 ports.
|
|
3283
|
+
// The URL is printed once, on "listening" (no callbacks left over from failed listens).
|
|
3284
|
+
server.once("listening", () => {
|
|
3285
|
+
const addr = server.address();
|
|
3286
|
+
const p = typeof addr === "object" && addr ? addr.port : port;
|
|
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.");
|
|
3298
|
+
});
|
|
3299
|
+
const listen = (p, left) => {
|
|
3300
|
+
server.once("error", (err) => {
|
|
3301
|
+
if (err.code === "EADDRINUSE" && left > 0) {
|
|
3302
|
+
console.log(`port ${p} in use → trying ${p + 1}`);
|
|
3303
|
+
listen(p + 1, left - 1);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
throw err;
|
|
3307
|
+
});
|
|
3308
|
+
server.listen(p, "127.0.0.1");
|
|
3309
|
+
};
|
|
3310
|
+
listen(port, 10);
|
|
3311
|
+
return viewer;
|
|
3312
|
+
};
|