@dooc-dev/dooc 0.0.0-stage → 0.0.1
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 +244 -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/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 +1 -0
- package/dist/core/icons.js +1 -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 +305 -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 +92 -0
- package/dist/plugins/blocks.js +99 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +164 -0
- package/dist/plugins/runtime.d.ts +24 -0
- package/dist/plugins/runtime.js +339 -0
- package/dist/plugins/types.d.ts +56 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +29 -0
- package/dist/plugins/view.js +60 -0
- package/dist/ui.global.js +77 -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/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 +335 -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 +294 -0
- package/dist/viewer/locales/jpn.json +294 -0
- package/dist/viewer/locales/zho.json +294 -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 +3 -0
- package/dist/viewer/plugin-view.js +28 -0
- package/dist/viewer/plugins.d.ts +8 -0
- package/dist/viewer/plugins.js +214 -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 +206 -0
- package/dist/viewer/serve.js +3233 -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 +127 -5
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// HMR for the dev server (shared by preview / edit). Funnels changes into a single SSE stream
|
|
2
|
+
// (`/api/hmr`), by source:
|
|
3
|
+
// data side (content/*.md, dooc.yaml, figures/*) = picked up by this module's watcher;
|
|
4
|
+
// preview swaps only the main body + nav (keeping scroll). edit likewise does a partial
|
|
5
|
+
// update of just the body + nav (no reload; keeps CodeMirror cursor/scroll/undo and
|
|
6
|
+
// unsaved content) — only on pages where edit-client.ts registered `onContent`
|
|
7
|
+
// (unregistered, e.g. the new page, reloads).
|
|
8
|
+
// client bundle side (src/viewer/edit-client.ts, src/core/*.tsx = bundle sources of the schema
|
|
9
|
+
// graph interaction layer `/assets/graph.js`) = not imported by the server (esbuild
|
|
10
|
+
// bundles them on demand), so tsx watch doesn't react → the data-side watcher picks
|
|
11
|
+
// them up as "client" and triggers a reload.
|
|
12
|
+
// server side (all other src/*.ts) = tsx watch restarts the whole process, so not watched.
|
|
13
|
+
// The client notices the swap from the bootId difference in `hello` on reconnect and
|
|
14
|
+
// reloads.
|
|
15
|
+
// All content events reach the editor, which compares disk contents with its saved base.
|
|
16
|
+
import { existsSync, readFileSync, realpathSync, unwatchFile, watch, watchFile } from "node:fs";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { managedFile } from "../compiler/files.js";
|
|
19
|
+
import { textfilePath } from "../compiler/textfile.js";
|
|
20
|
+
import { MANIFEST_FILE } from "../core/content-metadata.js";
|
|
21
|
+
import { isRemoteTextfile } from "../core/textfile.js";
|
|
22
|
+
import { prebuilt } from "./assets.js";
|
|
23
|
+
import { gitHeadFile } from "./workspace.js";
|
|
24
|
+
/** SSE endpoint. SoT for the server route and the client script's connection target. */
|
|
25
|
+
export const HMR_PATH = "/api/hmr";
|
|
26
|
+
/**
|
|
27
|
+
* Changed path (repo-root relative) → HMR kind. undefined for out-of-scope files
|
|
28
|
+
* (dist output, temp files etc.).
|
|
29
|
+
*/
|
|
30
|
+
export const classifyChange = (rel) => {
|
|
31
|
+
const p = rel.replace(/\\/g, "/");
|
|
32
|
+
// edit's client bundle itself (src/viewer/edit-client.ts) and src/core/*.tsx
|
|
33
|
+
// (schema-graph-*.tsx = bundle sources of `/assets/graph.js`) aren't imported by the server
|
|
34
|
+
// (esbuild bundles them per request), so tsx watch can't pick them up → explicitly classify
|
|
35
|
+
// them as "client" here to trigger a reload. Other src/*.ts (the server itself) stay out of
|
|
36
|
+
// scope (= tsx watch restart → the client picks it up from the bootId difference in hello).
|
|
37
|
+
if (p === "src/viewer/edit-client.ts" ||
|
|
38
|
+
p === "src/viewer/files-ui.ts" ||
|
|
39
|
+
p === "src/viewer/resource-library-ui.ts" ||
|
|
40
|
+
p === "src/viewer/edit-code-file.ts" ||
|
|
41
|
+
p === "src/core/code-file.ts" ||
|
|
42
|
+
p === "src/core/files.ts" ||
|
|
43
|
+
p === "src/viewer/edit-schema-completion.ts" ||
|
|
44
|
+
p === "src/viewer/edit-textfile.ts" ||
|
|
45
|
+
p === "src/viewer/file-references.ts" ||
|
|
46
|
+
p === "src/core/markdown.ts" ||
|
|
47
|
+
p === "src/core/mermaid-mount.ts" ||
|
|
48
|
+
p === "src/core/textfile.ts" ||
|
|
49
|
+
p === "src/core/file-embed.ts" ||
|
|
50
|
+
p === "src/core/term-reference.ts" ||
|
|
51
|
+
p === "src/core/taxonomy.ts" ||
|
|
52
|
+
p === "src/core/previewers.ts" ||
|
|
53
|
+
p === "src/viewer/edit-file-preview.ts" ||
|
|
54
|
+
p === "src/viewer/edit-conflict.ts" ||
|
|
55
|
+
p === "src/viewer/edit-code-highlight.ts" ||
|
|
56
|
+
p === "src/viewer/edit-git-diff.ts" ||
|
|
57
|
+
p === "src/viewer/code-tokens.ts" ||
|
|
58
|
+
p === "src/viewer/i18n.ts" ||
|
|
59
|
+
p === "src/viewer/language-picker.ts" ||
|
|
60
|
+
p === "src/viewer/theme-picker.ts" ||
|
|
61
|
+
p === "src/viewer/ui-client.ts" ||
|
|
62
|
+
p === "src/viewer/plugins-ui.ts" ||
|
|
63
|
+
p === "src/viewer/plugin-view.ts" ||
|
|
64
|
+
p === "src/viewer/plugin-resize.ts" ||
|
|
65
|
+
p === "src/viewer/preview-update.ts" ||
|
|
66
|
+
p === "src/plugins/view.ts" ||
|
|
67
|
+
p === "src/viewer/nav-focus.ts" ||
|
|
68
|
+
p === "src/viewer/code-highlight.ts" ||
|
|
69
|
+
(p.startsWith("src/viewer/locales/") && p.endsWith(".json")) ||
|
|
70
|
+
(p.startsWith("src/core/") && p.endsWith(".tsx"))) {
|
|
71
|
+
return "client";
|
|
72
|
+
}
|
|
73
|
+
// content = body text (.md) and dooc.yaml (= SoT for hierarchy and ordering).
|
|
74
|
+
if (p === MANIFEST_FILE || (p.startsWith("content/") && p.endsWith(".md"))) {
|
|
75
|
+
return "content";
|
|
76
|
+
}
|
|
77
|
+
if (p.startsWith("locales/") && p.endsWith(".md"))
|
|
78
|
+
return "content";
|
|
79
|
+
if (p.startsWith("figures/") && /\.(svg|html|png|webp|jpe?g)$/.test(p)) {
|
|
80
|
+
return "content";
|
|
81
|
+
}
|
|
82
|
+
// icons/<id>.svg (doc icons) is data side too = partial update.
|
|
83
|
+
if (p.startsWith("icons/") && p.endsWith(".svg")) {
|
|
84
|
+
return "content";
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
/** One SSE frame (in the format the client's EventSource reads). */
|
|
89
|
+
export const sseFrame = (event, data) => `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
90
|
+
export const createHmrHub = (bootId) => {
|
|
91
|
+
const clients = new Set();
|
|
92
|
+
// Ping periodically to clean up dead sockets, even on localhost without a proxy.
|
|
93
|
+
const ping = setInterval(() => {
|
|
94
|
+
for (const res of clients) {
|
|
95
|
+
res.write(": ping\n\n");
|
|
96
|
+
}
|
|
97
|
+
}, 20_000);
|
|
98
|
+
ping.unref();
|
|
99
|
+
return {
|
|
100
|
+
handle: (res) => {
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
103
|
+
"cache-control": "no-cache, no-transform",
|
|
104
|
+
connection: "keep-alive",
|
|
105
|
+
});
|
|
106
|
+
// Make clients reconnect quickly right after a restart (tsx watch); the default
|
|
107
|
+
// reconnect interval is browser-dependent.
|
|
108
|
+
res.write("retry: 300\n\n");
|
|
109
|
+
res.write(sseFrame("hello", { bootId }));
|
|
110
|
+
clients.add(res);
|
|
111
|
+
res.on("close", () => clients.delete(res));
|
|
112
|
+
},
|
|
113
|
+
broadcast: (kind, files) => {
|
|
114
|
+
const frame = sseFrame("change", { kind, files });
|
|
115
|
+
for (const res of clients) {
|
|
116
|
+
res.write(frame);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Folders to watch. The docs-side ones are relative to docsRoot, src to pkgRoot (only files that
|
|
123
|
+
* classifyChange marks "client" count).
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* Notifies data-side changes batched by kind. An editor save fires multiple events
|
|
127
|
+
* (rename + change), so debounce collapses them into one. Referenced text files can be added
|
|
128
|
+
* as drafts are compiled; close() releases all watchers.
|
|
129
|
+
*/
|
|
130
|
+
export const watchProject = (docsRoot, pkgRoot, onChange, debounceMs = 60) => {
|
|
131
|
+
const pending = new Map();
|
|
132
|
+
let timer;
|
|
133
|
+
const flush = () => {
|
|
134
|
+
timer = undefined;
|
|
135
|
+
const byKind = new Map();
|
|
136
|
+
for (const [file, kind] of pending) {
|
|
137
|
+
byKind.set(kind, [...(byKind.get(kind) ?? []), file]);
|
|
138
|
+
}
|
|
139
|
+
pending.clear();
|
|
140
|
+
for (const [kind, files] of byKind) {
|
|
141
|
+
onChange(kind, files);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const note = (rel, kind = classifyChange(rel)) => {
|
|
145
|
+
if (!kind) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
pending.set(rel, kind);
|
|
149
|
+
if (!timer) {
|
|
150
|
+
timer = setTimeout(flush, debounceMs);
|
|
151
|
+
timer.unref();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
// Skip missing folders (checkouts / test fixtures without optional asset folders) to avoid ENOENT.
|
|
155
|
+
const dirs = [["src", join(pkgRoot, "src")]];
|
|
156
|
+
const watchers = dirs
|
|
157
|
+
.filter(([, abs]) => !prebuilt && existsSync(abs))
|
|
158
|
+
.map(([dir, abs]) => watch(abs, { recursive: true }, (_event, name) => {
|
|
159
|
+
if (name) {
|
|
160
|
+
note(`${dir}/${String(name)}`);
|
|
161
|
+
}
|
|
162
|
+
}));
|
|
163
|
+
// Watch the directory so atomic replacements of the manifest are detected too.
|
|
164
|
+
watchers.push(watch(docsRoot, { recursive: true }, (_event, name) => {
|
|
165
|
+
if (!name)
|
|
166
|
+
return;
|
|
167
|
+
const path = String(name).replaceAll("\\", "/");
|
|
168
|
+
if (path === MANIFEST_FILE ||
|
|
169
|
+
path.startsWith("locales/") ||
|
|
170
|
+
/(^|\/)\.gitignore$/.test(path))
|
|
171
|
+
note(path, "content");
|
|
172
|
+
else if (managedFile(path) && !path.startsWith("src/") && !/\.(tmp|swp)$/.test(path))
|
|
173
|
+
note(path, "content");
|
|
174
|
+
}));
|
|
175
|
+
const textfiles = new Map();
|
|
176
|
+
const headFile = gitHeadFile(docsRoot);
|
|
177
|
+
if (headFile) {
|
|
178
|
+
for (let parent = realpathSync(docsRoot); !existsSync(join(parent, ".git"));) {
|
|
179
|
+
const next = dirname(parent);
|
|
180
|
+
if (next === parent)
|
|
181
|
+
break;
|
|
182
|
+
parent = next;
|
|
183
|
+
watchers.push(watch(parent, (_event, name) => {
|
|
184
|
+
if (String(name) === ".gitignore")
|
|
185
|
+
note(".gitignore", "content");
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
const directory = dirname(headFile);
|
|
189
|
+
const common = join(directory, "commondir");
|
|
190
|
+
const directories = new Set([
|
|
191
|
+
directory,
|
|
192
|
+
existsSync(common)
|
|
193
|
+
? resolve(directory, readFileSync(common, "utf8").trim())
|
|
194
|
+
: directory,
|
|
195
|
+
]);
|
|
196
|
+
for (const path of directories)
|
|
197
|
+
watchers.push(watch(path, { recursive: true }, (_event, name) => {
|
|
198
|
+
const file = String(name).replaceAll("\\", "/");
|
|
199
|
+
if (file === "HEAD" || file === "packed-refs" || file.startsWith("refs/"))
|
|
200
|
+
note(".git/HEAD", "content");
|
|
201
|
+
else if (file === "index" || file === "info/exclude" || file === "config")
|
|
202
|
+
note(`.git/${file}`, "content");
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
// shortcut: keep encountered file watches until shutdown; prune if sessions collect many removed references.
|
|
206
|
+
const watchTextfiles = (files) => {
|
|
207
|
+
for (const file of files) {
|
|
208
|
+
if (isRemoteTextfile(file))
|
|
209
|
+
continue;
|
|
210
|
+
try {
|
|
211
|
+
textfilePath(docsRoot, file);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const path = resolve(docsRoot, file);
|
|
217
|
+
if (textfiles.has(path))
|
|
218
|
+
continue;
|
|
219
|
+
const changed = () => note(file.replace(/^(\.\/)+/, ""), "content");
|
|
220
|
+
// Poll referenced paths so atomic saves, deletion and creation are all detected.
|
|
221
|
+
watchFile(path, { persistent: false, interval: 200 }, changed);
|
|
222
|
+
textfiles.set(path, changed);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const close = () => {
|
|
226
|
+
if (timer) {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
timer = undefined;
|
|
229
|
+
}
|
|
230
|
+
for (const w of watchers) {
|
|
231
|
+
w.close();
|
|
232
|
+
}
|
|
233
|
+
for (const [path, listener] of textfiles)
|
|
234
|
+
unwatchFile(path, listener);
|
|
235
|
+
};
|
|
236
|
+
return { close, watchTextfiles };
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Client script injected into page(). How changes are applied depends on mode:
|
|
240
|
+
* preview = swap the main <main> + nav (keeping scroll / sidebar state).
|
|
241
|
+
* edit = delegate content changes to `window.__docsHmr.onContent` (registered by
|
|
242
|
+
* edit-client.ts) without reloading (pages without it, e.g. the new page, reload).
|
|
243
|
+
* Changes to the client bundle (`src/viewer/edit-client.ts` / `src/core/*.tsx`) and
|
|
244
|
+
* a bootId difference in `hello` (= the server itself restarted) reload. While there
|
|
245
|
+
* are unsaved (dirty) changes, the reload is downgraded to a badge and waits.
|
|
246
|
+
*/
|
|
247
|
+
export const hmrClientScript = (mode) => `
|
|
248
|
+
(() => {
|
|
249
|
+
const MODE = ${JSON.stringify(mode)};
|
|
250
|
+
let bootId = null;
|
|
251
|
+
let isDirty = false;
|
|
252
|
+
let pendingReload = false;
|
|
253
|
+
|
|
254
|
+
let badge = null;
|
|
255
|
+
const showBadge = () => {
|
|
256
|
+
pendingReload = true;
|
|
257
|
+
if (badge) return;
|
|
258
|
+
badge = document.createElement("button");
|
|
259
|
+
badge.type = "button";
|
|
260
|
+
badge.textContent = "Updated externally — click to reload";
|
|
261
|
+
badge.style.cssText = "position:fixed;right:14px;bottom:14px;z-index:9999;font:inherit;font-size:12px;padding:6px 10px;border-radius:6px;cursor:pointer;border:1px solid var(--doc-border-2,#c4ccd8);background:var(--doc-panel,#fff);color:var(--doc-fg-2,#2a3140);box-shadow:0 2px 8px rgba(0,0,0,.18)";
|
|
262
|
+
badge.addEventListener("click", () => location.reload());
|
|
263
|
+
document.body.appendChild(badge);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Never silently discard unsaved changes (downgrade to a badge and wait).
|
|
267
|
+
const reload = () => { if (isDirty) { showBadge(); return; } location.reload(); };
|
|
268
|
+
|
|
269
|
+
// preview partial swap — refetch the same URL and replace only the contents of <main> and nav.
|
|
270
|
+
const swap = async () => {
|
|
271
|
+
let next;
|
|
272
|
+
try {
|
|
273
|
+
const res = await fetch(location.href, { headers: { "cache-control": "no-cache" } });
|
|
274
|
+
if (!res.ok) { reload(); return; }
|
|
275
|
+
next = new DOMParser().parseFromString(await res.text(), "text/html");
|
|
276
|
+
} catch { return; } // e.g. server restarting — picked up by the next hello
|
|
277
|
+
const main = document.querySelector("main");
|
|
278
|
+
const nextMain = next.querySelector("main");
|
|
279
|
+
if (!main || !nextMain) { reload(); return; }
|
|
280
|
+
const y = window.scrollY;
|
|
281
|
+
document.title = next.title;
|
|
282
|
+
const title = document.getElementById("site-title");
|
|
283
|
+
const nextTitle = next.getElementById("site-title");
|
|
284
|
+
if (title && nextTitle) {
|
|
285
|
+
title.textContent = nextTitle.textContent;
|
|
286
|
+
title.title = nextTitle.title;
|
|
287
|
+
}
|
|
288
|
+
const logo = document.getElementById("site-icon");
|
|
289
|
+
const nextLogo = next.getElementById("site-icon");
|
|
290
|
+
if (logo && nextLogo) logo.replaceWith(nextLogo);
|
|
291
|
+
const updatePreview = window.__docsHmr && window.__docsHmr.updatePreview;
|
|
292
|
+
if (updatePreview) updatePreview(main, nextMain);
|
|
293
|
+
else main.innerHTML = nextMain.innerHTML;
|
|
294
|
+
const nav = document.getElementById("nav-groups");
|
|
295
|
+
const nextNav = next.getElementById("nav-groups");
|
|
296
|
+
if (nav && nextNav) {
|
|
297
|
+
nav.innerHTML = nextNav.innerHTML;
|
|
298
|
+
if (window.__docsNavRestore) window.__docsNavRestore();
|
|
299
|
+
}
|
|
300
|
+
window.scrollTo(0, y);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// Notification hooks for the edit client (src/viewer/edit-client.ts). edit-client.ts may run
|
|
304
|
+
// first (bootScripts sit near the top of body, this script at the end of body), so merge
|
|
305
|
+
// instead of clobbering what's already there (an earlier onContent registration).
|
|
306
|
+
window.__docsHmr = Object.assign(window.__docsHmr || {}, {
|
|
307
|
+
setDirty: (v) => {
|
|
308
|
+
isDirty = Boolean(v);
|
|
309
|
+
if (!isDirty && pendingReload) location.reload();
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const es = new EventSource(${JSON.stringify(HMR_PATH)});
|
|
314
|
+
es.addEventListener("hello", (ev) => {
|
|
315
|
+
const id = JSON.parse(ev.data).bootId;
|
|
316
|
+
if (bootId === null) { bootId = id; return; }
|
|
317
|
+
// bootId changed = the server itself (src/*.ts) was replaced and restarted.
|
|
318
|
+
if (id !== bootId) reload();
|
|
319
|
+
});
|
|
320
|
+
es.addEventListener("change", (ev) => {
|
|
321
|
+
const { kind, files } = JSON.parse(ev.data);
|
|
322
|
+
if (MODE === "preview") { if (kind === "content") void swap(); return; }
|
|
323
|
+
if (kind === "content") {
|
|
324
|
+
const onContent = window.__docsHmr && window.__docsHmr.onContent;
|
|
325
|
+
// If registered, leave it to the in-place update and don't reload (the editor swaps
|
|
326
|
+
// body/nav itself). Unregistered (pages without an editor, e.g. the empty 0-doc page)
|
|
327
|
+
// → reload.
|
|
328
|
+
if (onContent) { onContent(files); return; }
|
|
329
|
+
reload();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
// Client bundle (src/viewer/edit-client.ts / src/core/*.tsx) changed — reload to refetch it.
|
|
333
|
+
reload();
|
|
334
|
+
});
|
|
335
|
+
})();`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const messages: Record<string, Record<string, string>>;
|
|
2
|
+
export declare const resolveUiLanguage: (tag: string) => string;
|
|
3
|
+
export declare const translate: (language: string, key: string, values?: Record<string, string | number>) => string;
|
|
4
|
+
export declare const t: (key: string, values?: Record<string, string | number>) => string;
|
|
5
|
+
export declare const applyUiLanguage: () => void;
|
|
6
|
+
export declare const mountUiLanguage: () => void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { iso6393To1 } from "iso-639-3";
|
|
2
|
+
import { mountLanguagePickers } from "./language-picker.js";
|
|
3
|
+
import eng from "./locales/eng.json" with { type: "json" };
|
|
4
|
+
import jpn from "./locales/jpn.json" with { type: "json" };
|
|
5
|
+
import zho from "./locales/zho.json" with { type: "json" };
|
|
6
|
+
export const messages = { eng, jpn, zho };
|
|
7
|
+
export const resolveUiLanguage = (tag) => {
|
|
8
|
+
const primary = tag.toLowerCase().split("-")[0] ?? "en";
|
|
9
|
+
return (Object.keys(messages).find((code) => code === primary || iso6393To1[code] === primary) ??
|
|
10
|
+
"eng");
|
|
11
|
+
};
|
|
12
|
+
export const translate = (language, key, values = {}) => {
|
|
13
|
+
const text = messages[resolveUiLanguage(language)]?.[key] ?? eng[key] ?? key;
|
|
14
|
+
return text.replace(/\{(\w+)\}/g, (token, name) => Object.hasOwn(values, name) ? String(values[name]) : token);
|
|
15
|
+
};
|
|
16
|
+
const STORAGE_KEY = "docs:ui-language";
|
|
17
|
+
let language = "eng";
|
|
18
|
+
export const t = (key, values) => translate(language, key, values);
|
|
19
|
+
export const applyUiLanguage = () => {
|
|
20
|
+
for (const element of document.querySelectorAll("button.codecopy")) {
|
|
21
|
+
element.setAttribute("title", t("Copy code"));
|
|
22
|
+
element.setAttribute("aria-label", t("Copy code"));
|
|
23
|
+
}
|
|
24
|
+
for (const attr of ["text", "title", "aria-label", "placeholder"]) {
|
|
25
|
+
const marker = attr === "text" ? "data-i18n" : `data-i18n-${attr}`;
|
|
26
|
+
for (const element of document.querySelectorAll(`[${marker}]`)) {
|
|
27
|
+
const value = t(element.getAttribute(marker) ?? "");
|
|
28
|
+
if (attr === "text")
|
|
29
|
+
element.textContent = value;
|
|
30
|
+
else
|
|
31
|
+
element.setAttribute(attr, value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
export const mountUiLanguage = () => {
|
|
36
|
+
const select = document.querySelector("#ui-language");
|
|
37
|
+
let preferred = navigator.language;
|
|
38
|
+
try {
|
|
39
|
+
preferred = localStorage.getItem(STORAGE_KEY) || preferred;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* Storage is optional; switching still works for this session. */
|
|
43
|
+
}
|
|
44
|
+
const apply = (tag) => {
|
|
45
|
+
language = resolveUiLanguage(tag);
|
|
46
|
+
if (select)
|
|
47
|
+
select.value = language;
|
|
48
|
+
document.documentElement.lang = iso6393To1[language] ?? language;
|
|
49
|
+
applyUiLanguage();
|
|
50
|
+
document.dispatchEvent(new Event("docs:ui-language"));
|
|
51
|
+
};
|
|
52
|
+
window.__docsTranslate = t;
|
|
53
|
+
apply(preferred);
|
|
54
|
+
select?.addEventListener("change", () => {
|
|
55
|
+
apply(select.value);
|
|
56
|
+
try {
|
|
57
|
+
localStorage.setItem(STORAGE_KEY, language);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* Persistence may be blocked by the browser. */
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
document.addEventListener("docs:ui-refresh", applyUiLanguage);
|
|
64
|
+
mountLanguagePickers(t);
|
|
65
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Japanese typo detection = the `ja` adapter's prose lint (language socket; attached in
|
|
2
|
+
// languages.ts, served via /api/lint for dooc edit). textlint is not bundled into the
|
|
3
|
+
// client (edit-client.ts) to avoid bloating the bundle; only offset-annotated findings are sent.
|
|
4
|
+
import { TextlintKernel, } from "@textlint/kernel";
|
|
5
|
+
import { moduleInterop } from "@textlint/module-interop";
|
|
6
|
+
import markdownPluginModule from "@textlint/textlint-plugin-markdown";
|
|
7
|
+
import presetJaTechnicalWriting from "textlint-rule-preset-ja-technical-writing";
|
|
8
|
+
// moduleInterop unwraps CommonJS defaults at runtime but keeps the input type.
|
|
9
|
+
const markdownPlugin = moduleInterop(markdownPluginModule);
|
|
10
|
+
// Enable only the typo-oriented rules from textlint-rule-preset-ja-technical-writing.
|
|
11
|
+
// Style rules (sentence length, number of commas, consecutive kanji length, desu/masu
|
|
12
|
+
// consistency, etc.) are excluded (this tool is for typo highlighting, not style checking).
|
|
13
|
+
const TYPO_RULE_IDS = [
|
|
14
|
+
"no-doubled-joshi", // duplicated particles
|
|
15
|
+
"no-doubled-conjunctive-particle-ga", // repeated adversative particles
|
|
16
|
+
"no-doubled-conjunction", // repeated identical conjunctions
|
|
17
|
+
"no-dropping-the-ra", // ra-dropping in potential verb forms (ra-nuki kotoba)
|
|
18
|
+
"no-double-negative-ja", // double negatives
|
|
19
|
+
"no-nfd", // UTF8-MAC (NFD) dakuten
|
|
20
|
+
"no-invalid-control-character", // stray invalid control characters
|
|
21
|
+
"no-zero-width-spaces", // stray zero-width spaces (from copy-paste)
|
|
22
|
+
"no-hankaku-kana", // stray half-width kana
|
|
23
|
+
"ja-no-successive-word", // same word repeated (mistyping)
|
|
24
|
+
"ja-no-abusage", // common Japanese misusages
|
|
25
|
+
"ja-unnatural-alphabet", // unnatural alphabet letters caused by input mistakes
|
|
26
|
+
"no-unmatched-pair", // unmatched brackets
|
|
27
|
+
];
|
|
28
|
+
const rules = TYPO_RULE_IDS.map((ruleId) => {
|
|
29
|
+
const rule = presetJaTechnicalWriting.rules[ruleId];
|
|
30
|
+
if (!rule) {
|
|
31
|
+
throw new Error(`textlint-rule-preset-ja-technical-writing has no rule "${ruleId}"`);
|
|
32
|
+
}
|
|
33
|
+
return { ruleId, rule, options: presetJaTechnicalWriting.rulesConfig[ruleId] };
|
|
34
|
+
});
|
|
35
|
+
const kernel = new TextlintKernel();
|
|
36
|
+
/** Lints body (md content without front matter) and returns typo findings with offsets. */
|
|
37
|
+
export const lintJa = async (body) => {
|
|
38
|
+
const result = await kernel.lintText(body, {
|
|
39
|
+
ext: ".md",
|
|
40
|
+
plugins: [{ pluginId: "markdown", plugin: markdownPlugin }],
|
|
41
|
+
rules,
|
|
42
|
+
});
|
|
43
|
+
return result.messages.map((m) => ({ ruleId: m.ruleId, message: m.message, range: m.range }));
|
|
44
|
+
};
|