@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,12 @@
|
|
|
1
|
+
export interface BuildOptions {
|
|
2
|
+
format: "html" | "zip";
|
|
3
|
+
urlPreview?: "embed" | "network";
|
|
4
|
+
}
|
|
5
|
+
/** Build one artifact from the manifest and its managed content. */
|
|
6
|
+
export declare const buildBundle: (root: string, options: BuildOptions) => Promise<{
|
|
7
|
+
data: Buffer<ArrayBufferLike>;
|
|
8
|
+
docs: number;
|
|
9
|
+
} | {
|
|
10
|
+
data: string;
|
|
11
|
+
docs: number;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { bundleEntries } from "../compiler/bundle.js";
|
|
5
|
+
import { embedBundleUrls } from "../compiler/bundle-urls.js";
|
|
6
|
+
import { compileSite } from "../compiler/compile.js";
|
|
7
|
+
import { loadProjectFonts } from "../compiler/fonts.js";
|
|
8
|
+
import { textfilePath } from "../compiler/textfile.js";
|
|
9
|
+
import { zip } from "../compiler/zip.js";
|
|
10
|
+
import { loadSiteLanguages } from "./languages.js";
|
|
11
|
+
/** Build one artifact from the manifest and its managed content. */
|
|
12
|
+
export const buildBundle = async (root, options) => {
|
|
13
|
+
const { format } = options;
|
|
14
|
+
const urlPreview = options.urlPreview ?? "embed";
|
|
15
|
+
if (format !== "html" && format !== "zip")
|
|
16
|
+
throw new Error("build format must be html | zip");
|
|
17
|
+
if (urlPreview !== "embed" && urlPreview !== "network")
|
|
18
|
+
throw new Error("--url_preview must be embed | network");
|
|
19
|
+
const validate = (path) => {
|
|
20
|
+
const compiled = compileSite(path);
|
|
21
|
+
if (compiled.errors.length)
|
|
22
|
+
throw new Error(`Content validation failed:\n${compiled.errors.join("\n")}`);
|
|
23
|
+
return compiled;
|
|
24
|
+
};
|
|
25
|
+
let compiled = validate(root);
|
|
26
|
+
let entries = format === "zip" || urlPreview === "embed" ? bundleEntries(root) : [];
|
|
27
|
+
if (urlPreview === "embed") {
|
|
28
|
+
entries = await embedBundleUrls(root, entries);
|
|
29
|
+
const temp = mkdtempSync(join(tmpdir(), "dooc-bundle-"));
|
|
30
|
+
try {
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const path = textfilePath(temp, entry.name);
|
|
33
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
34
|
+
writeFileSync(path, entry.data);
|
|
35
|
+
}
|
|
36
|
+
compiled = validate(temp);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
rmSync(temp, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (format === "zip")
|
|
43
|
+
return { data: zip(entries), docs: compiled.docs.length };
|
|
44
|
+
const config = await loadSiteLanguages(root);
|
|
45
|
+
const { bundleClient, publishHtml, siteFrom } = await import("./serve.js");
|
|
46
|
+
return {
|
|
47
|
+
data: publishHtml({
|
|
48
|
+
...siteFrom(compiled, config.languages),
|
|
49
|
+
fonts: loadProjectFonts(root),
|
|
50
|
+
codeLanguages: config.codeLanguages,
|
|
51
|
+
codeGrammars: config.codeGrammars,
|
|
52
|
+
}, await bundleClient("core/schema-graph-global.tsx", true), undefined, await bundleClient("viewer/code-highlight.ts", true), urlPreview === "embed" ? "static" : "network"),
|
|
53
|
+
docs: compiled.docs.length,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Self-contained: standalone bundles cannot rely on the local viewer API. */
|
|
2
|
+
export const mountBundleNetwork = () => {
|
|
3
|
+
const load = async (host) => {
|
|
4
|
+
if (host.dataset.loaded)
|
|
5
|
+
return;
|
|
6
|
+
host.dataset.loaded = "true";
|
|
7
|
+
const source = host.dataset.embedUrl;
|
|
8
|
+
if (!source)
|
|
9
|
+
return;
|
|
10
|
+
const url = new URL(source);
|
|
11
|
+
const path = url.pathname.toLowerCase();
|
|
12
|
+
const label = host.textContent || source;
|
|
13
|
+
const link = host.querySelector("a");
|
|
14
|
+
const failure = () => {
|
|
15
|
+
if (link && !host.contains(link))
|
|
16
|
+
host.append(link);
|
|
17
|
+
const status = document.createElement("p");
|
|
18
|
+
status.setAttribute("role", "status");
|
|
19
|
+
status.textContent = "Could not load this URL. Open the source link to view it.";
|
|
20
|
+
host.append(status);
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
if (url.protocol !== "https:" || url.username || url.password)
|
|
24
|
+
throw new Error();
|
|
25
|
+
if (/\.(png|jpe?g|webp|gif|svg)$/.test(path)) {
|
|
26
|
+
const image = document.createElement("img");
|
|
27
|
+
image.alt = label;
|
|
28
|
+
image.referrerPolicy = "no-referrer";
|
|
29
|
+
image.style.maxWidth = "100%";
|
|
30
|
+
image.onerror = failure;
|
|
31
|
+
image.src = source;
|
|
32
|
+
host.replaceChildren(image);
|
|
33
|
+
}
|
|
34
|
+
else if (/\.(mp4|webm)$/.test(path)) {
|
|
35
|
+
const video = document.createElement("video");
|
|
36
|
+
video.controls = true;
|
|
37
|
+
video.preload = "metadata";
|
|
38
|
+
video.style.maxWidth = "100%";
|
|
39
|
+
video.onerror = failure;
|
|
40
|
+
video.src = source;
|
|
41
|
+
host.replaceChildren(video);
|
|
42
|
+
}
|
|
43
|
+
else if (/\.(txt|md|markdown|csv|tsv|json|yaml|yml|toml|xml|css|[cm]?js|[jt]sx?|py|rs|go|sh|sql)$/.test(path)) {
|
|
44
|
+
const response = await fetch(source, {
|
|
45
|
+
credentials: "omit",
|
|
46
|
+
referrerPolicy: "no-referrer",
|
|
47
|
+
signal: AbortSignal.timeout(10000),
|
|
48
|
+
});
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
throw new Error();
|
|
51
|
+
const code = document.createElement("code");
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
const range = /^#L([1-9]\d*)(?:-L([1-9]\d*))?$/.exec(url.hash);
|
|
54
|
+
code.textContent = range
|
|
55
|
+
? text
|
|
56
|
+
.split(/\r\n|\n|\r/)
|
|
57
|
+
.slice(Number(range[1]) - 1, Number(range[2] ?? range[1]))
|
|
58
|
+
.join("\n")
|
|
59
|
+
: text;
|
|
60
|
+
const pre = document.createElement("pre");
|
|
61
|
+
pre.style.cssText = "overflow:auto;white-space:pre-wrap";
|
|
62
|
+
pre.append(code);
|
|
63
|
+
host.replaceChildren(pre);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const frame = document.createElement("iframe");
|
|
67
|
+
frame.title = label;
|
|
68
|
+
frame.sandbox.add("allow-scripts");
|
|
69
|
+
frame.referrerPolicy = "no-referrer";
|
|
70
|
+
frame.style.cssText = "width:100%;height:400px;border:0";
|
|
71
|
+
frame.src = source;
|
|
72
|
+
host.prepend(frame);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
failure();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const mount = () => {
|
|
80
|
+
for (const host of document.querySelectorAll(".external-embed[data-embed-url]"))
|
|
81
|
+
void load(host);
|
|
82
|
+
};
|
|
83
|
+
new MutationObserver(mount).observe(document.querySelector("main") ?? document.body, {
|
|
84
|
+
childList: true,
|
|
85
|
+
subtree: true,
|
|
86
|
+
});
|
|
87
|
+
mount();
|
|
88
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PreviewSettings } from "../core/previewers.js";
|
|
2
|
+
export declare const codeFileResponse: (root: string, input: Record<string, unknown> | undefined, previews?: PreviewSettings) => {
|
|
3
|
+
status: number;
|
|
4
|
+
body: {
|
|
5
|
+
error: string;
|
|
6
|
+
file?: undefined;
|
|
7
|
+
change?: undefined;
|
|
8
|
+
};
|
|
9
|
+
} | {
|
|
10
|
+
status: number;
|
|
11
|
+
body: {
|
|
12
|
+
file: string;
|
|
13
|
+
change: {
|
|
14
|
+
from: number;
|
|
15
|
+
to: number;
|
|
16
|
+
insert: string;
|
|
17
|
+
};
|
|
18
|
+
error?: undefined;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readManifest } from "../compiler/manifest.js";
|
|
3
|
+
import { textfilePath, writeTextfile } from "../compiler/textfile.js";
|
|
4
|
+
import { codeFileName, codeFilePlan } from "../core/code-file.js";
|
|
5
|
+
import { isDocId } from "../core/content-metadata.js";
|
|
6
|
+
import { defaultFilePath } from "../core/files.js";
|
|
7
|
+
export const codeFileResponse = (root, input, previews) => {
|
|
8
|
+
if (typeof input?.body !== "string" || typeof input.position !== "number")
|
|
9
|
+
return { status: 400, body: { error: "Invalid code file request" } };
|
|
10
|
+
try {
|
|
11
|
+
const manifest = readManifest(root);
|
|
12
|
+
if (manifest.errors.length)
|
|
13
|
+
throw new Error(manifest.errors.join("\n"));
|
|
14
|
+
const automatic = input.file === undefined && manifest.files?.defaultLayout !== false;
|
|
15
|
+
if (automatic && (typeof input.documentId !== "string" || !isDocId(input.documentId)))
|
|
16
|
+
throw new Error("Invalid document id");
|
|
17
|
+
if (!automatic && typeof input.file !== "string")
|
|
18
|
+
throw new Error("File path is required");
|
|
19
|
+
const target = automatic
|
|
20
|
+
? defaultFilePath(codeFileName(input.body, input.position, input.documentId))
|
|
21
|
+
: input.file;
|
|
22
|
+
let plan = codeFilePlan(input.body, input.position, target, previews);
|
|
23
|
+
const create = () => !existsSync(textfilePath(root, plan.file)) &&
|
|
24
|
+
writeTextfile(root, plan.file, plan.content, null);
|
|
25
|
+
if (automatic) {
|
|
26
|
+
for (let index = 2; !create(); index++) {
|
|
27
|
+
const dot = target.lastIndexOf(".");
|
|
28
|
+
plan = codeFilePlan(input.body, input.position, `${target.slice(0, dot)}-${index}${target.slice(dot)}`, previews);
|
|
29
|
+
}
|
|
30
|
+
return { status: 200, body: { file: plan.file, change: plan.change } };
|
|
31
|
+
}
|
|
32
|
+
if (!create())
|
|
33
|
+
return { status: 409, body: { error: "File already exists" } };
|
|
34
|
+
return { status: 200, body: { file: plan.file, change: plan.change } };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
status: 400,
|
|
39
|
+
body: { error: error instanceof Error ? error.message : String(error) },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export { codeBlockAt } from "../core/markdown.js";
|
|
2
|
+
export declare function isLocalToolRequest(host: string, origin: string | undefined, contentType: string | undefined): boolean;
|
|
3
|
+
export declare function runCodeTool(root: string, body: string, position: number, action: "format" | "lint"): Promise<{
|
|
4
|
+
change: {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
insert: string;
|
|
8
|
+
};
|
|
9
|
+
findings?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
findings: {
|
|
12
|
+
range: [number, number];
|
|
13
|
+
message: string;
|
|
14
|
+
ruleId: string;
|
|
15
|
+
}[];
|
|
16
|
+
change?: undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function validateCodeTools(root: string, body: string): Promise<{
|
|
19
|
+
changes: {
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
insert: string;
|
|
23
|
+
}[];
|
|
24
|
+
findings: {
|
|
25
|
+
range: [number, number];
|
|
26
|
+
message: string;
|
|
27
|
+
ruleId: string;
|
|
28
|
+
}[];
|
|
29
|
+
}>;
|
|
30
|
+
/** Apply the same registered formatter/linter to a complete file without writing to disk. */
|
|
31
|
+
export declare function validateTextfileCode(root: string, language: string, content: string): Promise<{
|
|
32
|
+
content: string;
|
|
33
|
+
findings: {
|
|
34
|
+
range: [number, number];
|
|
35
|
+
message: string;
|
|
36
|
+
ruleId: string;
|
|
37
|
+
}[];
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { visit } from "unist-util-visit";
|
|
5
|
+
import { codeBlockAt, offsetAt, parseMdast } from "../core/markdown.js";
|
|
6
|
+
export { codeBlockAt } from "../core/markdown.js";
|
|
7
|
+
import { formatMarkdown } from "../compiler/format.js";
|
|
8
|
+
export function isLocalToolRequest(host, origin, contentType) {
|
|
9
|
+
return (["localhost", "127.0.0.1", "[::1]"].includes(host.replace(/:\d+$/, "")) &&
|
|
10
|
+
origin === `http://${host}` &&
|
|
11
|
+
contentType?.split(";")[0]?.trim() === "application/json");
|
|
12
|
+
}
|
|
13
|
+
function command(value) {
|
|
14
|
+
if (!Array.isArray(value) ||
|
|
15
|
+
!value.length ||
|
|
16
|
+
value.some((v) => typeof v !== "string" || v.includes("\0")) ||
|
|
17
|
+
!value[0]) {
|
|
18
|
+
throw new Error("Tool command must be a nonempty array of strings");
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function execute(args, cwd, input, lint) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const child = execFile(args[0], args.slice(1), {
|
|
25
|
+
cwd,
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
killSignal: "SIGKILL",
|
|
28
|
+
maxBuffer: 1024 * 1024,
|
|
29
|
+
encoding: "utf8",
|
|
30
|
+
}, (error, stdout, stderr) => {
|
|
31
|
+
if (error && !(lint && error.code === 1 && !error.killed)) {
|
|
32
|
+
reject(new Error(stderr.trim().slice(0, 2000) || error.message));
|
|
33
|
+
}
|
|
34
|
+
else
|
|
35
|
+
resolve(stdout);
|
|
36
|
+
});
|
|
37
|
+
child.stdin?.on("error", () => {
|
|
38
|
+
/* Process exit is handled by execFile. */
|
|
39
|
+
});
|
|
40
|
+
child.stdin?.end(input);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export async function runCodeTool(root, body, position, action) {
|
|
44
|
+
const block = codeBlockAt(body, position);
|
|
45
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_+-]*$/.test(block.language))
|
|
46
|
+
throw new Error("Unsupported code language identifier");
|
|
47
|
+
const path = join(root, ".dooc", "editor-tools", `${block.language}.json`);
|
|
48
|
+
let config;
|
|
49
|
+
try {
|
|
50
|
+
config = JSON.parse(await readFile(path, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error(`Create .dooc/editor-tools/${block.language}.json to configure this language`);
|
|
54
|
+
}
|
|
55
|
+
if (!config || typeof config !== "object")
|
|
56
|
+
throw new Error("Invalid editor tool configuration");
|
|
57
|
+
const output = await execute(command(config[action === "format" ? "formatter" : "linter"]), root, `${block.code}\n`, action === "lint");
|
|
58
|
+
if (action === "format") {
|
|
59
|
+
const normalized = output.replace(/\r\n/g, "\n").replace(/\n$/, "");
|
|
60
|
+
if (!normalized && block.code.trim())
|
|
61
|
+
throw new Error("Formatter returned empty output; code was kept unchanged");
|
|
62
|
+
const lines = normalized ? normalized.split("\n") : [];
|
|
63
|
+
const fence = new RegExp(`^ {0,3}${block.marker[0]}{${block.marker.length},}[ \\t]*$`);
|
|
64
|
+
if (lines.some((line) => fence.test(line)))
|
|
65
|
+
throw new Error("Formatter output would close the code fence");
|
|
66
|
+
return {
|
|
67
|
+
change: {
|
|
68
|
+
from: block.from,
|
|
69
|
+
to: block.to,
|
|
70
|
+
insert: lines.map((line) => block.indent + line + block.eol).join(""),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const diagnostics = JSON.parse(output);
|
|
75
|
+
if (!Array.isArray(diagnostics))
|
|
76
|
+
throw new Error("Linter must return a JSON array");
|
|
77
|
+
const lines = block.code.split("\n");
|
|
78
|
+
const at = (line, column) => {
|
|
79
|
+
if (typeof line !== "number" ||
|
|
80
|
+
typeof column !== "number" ||
|
|
81
|
+
!Number.isInteger(line) ||
|
|
82
|
+
!Number.isInteger(column) ||
|
|
83
|
+
line < 1 ||
|
|
84
|
+
line > block.offsets.length ||
|
|
85
|
+
column < 1 ||
|
|
86
|
+
column > (lines[line - 1]?.length ?? 0) + 1)
|
|
87
|
+
throw new Error("Invalid diagnostic position");
|
|
88
|
+
return (block.offsets[line - 1] ?? block.from) + column - 1;
|
|
89
|
+
};
|
|
90
|
+
const findings = diagnostics.map((d) => {
|
|
91
|
+
if (!d || typeof d !== "object" || !("message" in d) || typeof d.message !== "string")
|
|
92
|
+
throw new Error("Invalid diagnostic");
|
|
93
|
+
const item = d;
|
|
94
|
+
const from = at(item.line, item.column);
|
|
95
|
+
const to = item.endLine == null && item.endColumn == null
|
|
96
|
+
? Math.min(from + 1, block.to)
|
|
97
|
+
: at(item.endLine, item.endColumn);
|
|
98
|
+
if (to < from)
|
|
99
|
+
throw new Error("Invalid diagnostic range");
|
|
100
|
+
const severity = item.severity ?? "warning";
|
|
101
|
+
if (!["error", "warning", "info"].includes(String(severity)))
|
|
102
|
+
throw new Error("Invalid diagnostic severity");
|
|
103
|
+
return {
|
|
104
|
+
range: [from, to],
|
|
105
|
+
message: d.message,
|
|
106
|
+
ruleId: String(severity),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
return { findings };
|
|
110
|
+
}
|
|
111
|
+
export async function validateCodeTools(root, body) {
|
|
112
|
+
const original = body;
|
|
113
|
+
const configuredBlocks = async (text, tool) => {
|
|
114
|
+
const blocks = [];
|
|
115
|
+
const nodes = [];
|
|
116
|
+
visit(parseMdast(text), (node) => {
|
|
117
|
+
if (node.type === "code")
|
|
118
|
+
nodes.push(node);
|
|
119
|
+
});
|
|
120
|
+
for (const node of nodes) {
|
|
121
|
+
if (node.position) {
|
|
122
|
+
const position = offsetAt(text, node.position.start.line, node.position.start.column);
|
|
123
|
+
if (/^ {0,3}(`{3,}|~{3,})/.test(text.slice(position))) {
|
|
124
|
+
codeBlockAt(text, position);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (node.type !== "code" ||
|
|
128
|
+
node.position?.start.offset == null ||
|
|
129
|
+
!node.lang ||
|
|
130
|
+
!/^[a-zA-Z0-9][a-zA-Z0-9_+-]*$/.test(node.lang))
|
|
131
|
+
continue;
|
|
132
|
+
let raw;
|
|
133
|
+
try {
|
|
134
|
+
raw = await readFile(join(root, ".dooc/editor-tools", `${node.lang}.json`), "utf8");
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (error.code === "ENOENT")
|
|
138
|
+
continue;
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const config = JSON.parse(raw);
|
|
142
|
+
if (!config || typeof config !== "object")
|
|
143
|
+
throw new Error("Invalid editor tool configuration");
|
|
144
|
+
if (config[tool] != null)
|
|
145
|
+
blocks.push(offsetAt(text, node.position.start.line, node.position.start.column));
|
|
146
|
+
}
|
|
147
|
+
return blocks;
|
|
148
|
+
};
|
|
149
|
+
const originalFindings = [];
|
|
150
|
+
for (const position of await configuredBlocks(body, "linter")) {
|
|
151
|
+
const result = await runCodeTool(root, body, position, "lint");
|
|
152
|
+
originalFindings.push(...(result.findings ?? []));
|
|
153
|
+
}
|
|
154
|
+
if (originalFindings.some((finding) => finding.ruleId === "error")) {
|
|
155
|
+
return { changes: [], findings: originalFindings };
|
|
156
|
+
}
|
|
157
|
+
for (const position of (await configuredBlocks(body, "formatter")).reverse()) {
|
|
158
|
+
const { change } = await runCodeTool(root, body, position, "format");
|
|
159
|
+
if (change && body.slice(change.from, change.to) !== change.insert) {
|
|
160
|
+
body = body.slice(0, change.from) + change.insert + body.slice(change.to);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
body = formatMarkdown(body);
|
|
164
|
+
if (body === original)
|
|
165
|
+
return { changes: [], findings: originalFindings };
|
|
166
|
+
const findings = [];
|
|
167
|
+
for (const position of await configuredBlocks(body, "linter")) {
|
|
168
|
+
const result = await runCodeTool(root, body, position, "lint");
|
|
169
|
+
findings.push(...(result.findings ?? []));
|
|
170
|
+
}
|
|
171
|
+
if (findings.some((finding) => finding.ruleId === "error")) {
|
|
172
|
+
throw new Error("Formatted code failed validation; code was kept unchanged");
|
|
173
|
+
}
|
|
174
|
+
let from = 0;
|
|
175
|
+
while (from < original.length && from < body.length && original[from] === body[from])
|
|
176
|
+
from++;
|
|
177
|
+
let to = original.length;
|
|
178
|
+
let end = body.length;
|
|
179
|
+
while (to > from && end > from && original[to - 1] === body[end - 1]) {
|
|
180
|
+
to--;
|
|
181
|
+
end--;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
changes: from === to && from === end ? [] : [{ from, to, insert: body.slice(from, end) }],
|
|
185
|
+
findings,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Apply the same registered formatter/linter to a complete file without writing to disk. */
|
|
189
|
+
export async function validateTextfileCode(root, language, content) {
|
|
190
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_+-]*$/.test(language) || content.includes("\0"))
|
|
191
|
+
throw new Error("Invalid text file tool request");
|
|
192
|
+
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
193
|
+
const marker = "`".repeat([...content.matchAll(/`+/g)].reduce((length, match) => Math.max(length, match[0].length + 1), 3));
|
|
194
|
+
const prefix = `${marker}${language}${eol}`;
|
|
195
|
+
const separator = content && !content.endsWith("\n") ? eol : "";
|
|
196
|
+
let body = `${prefix}${content}${separator}${marker}${eol}`;
|
|
197
|
+
const result = await validateCodeTools(root, body);
|
|
198
|
+
for (const change of [...result.changes].reverse())
|
|
199
|
+
body = body.slice(0, change.from) + change.insert + body.slice(change.to);
|
|
200
|
+
const block = codeBlockAt(body, 0);
|
|
201
|
+
let formatted = body.slice(block.from, block.to);
|
|
202
|
+
if (separator)
|
|
203
|
+
formatted = formatted.replace(/\r?\n$/, "");
|
|
204
|
+
return {
|
|
205
|
+
content: formatted,
|
|
206
|
+
findings: result.findings.map((finding) => ({
|
|
207
|
+
...finding,
|
|
208
|
+
range: finding.range.map((offset) => Math.max(0, Math.min(formatted.length, offset - block.from))),
|
|
209
|
+
})),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PreviewSettings } from "../core/previewers.js";
|
|
2
|
+
export interface FileReference {
|
|
3
|
+
line: number;
|
|
4
|
+
file: string;
|
|
5
|
+
kind: "textfile" | "figure" | "html" | "term";
|
|
6
|
+
}
|
|
7
|
+
/** Use the compiler's Markdown grammar so examples, escapes and URL encodings agree. */
|
|
8
|
+
export declare const fileReferences: (body: string, settings?: PreviewSettings) => FileReference[];
|
|
9
|
+
export interface FilePreview {
|
|
10
|
+
state: "pending" | "loading" | "valid" | "error";
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
/** An empty list means that this file compiled successfully. */
|
|
14
|
+
export type FilePreviewResults = Record<string, string[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { visit } from "unist-util-visit";
|
|
2
|
+
import { parseFileEmbed } from "../core/file-embed.js";
|
|
3
|
+
import { contentLinks, embeddedImages, parseMdast } from "../core/markdown.js";
|
|
4
|
+
import { parseTermReference } from "../core/term-reference.js";
|
|
5
|
+
/** Use the compiler's Markdown grammar so examples, escapes and URL encodings agree. */
|
|
6
|
+
export const fileReferences = (body, settings) => {
|
|
7
|
+
const root = parseMdast(body);
|
|
8
|
+
const images = new Set(embeddedImages(root));
|
|
9
|
+
const references = [];
|
|
10
|
+
for (const node of contentLinks(root)) {
|
|
11
|
+
try {
|
|
12
|
+
const file = parseTermReference(node.url);
|
|
13
|
+
if (file)
|
|
14
|
+
references.push({ line: node.position?.start.line ?? 1, file, kind: "term" });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* The compiler reports invalid paths. */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
visit(root, (node) => {
|
|
21
|
+
if (node.type === "image" && images.has(node)) {
|
|
22
|
+
try {
|
|
23
|
+
const { file, kind } = parseFileEmbed(node.url, settings);
|
|
24
|
+
if (kind === "external")
|
|
25
|
+
return;
|
|
26
|
+
references.push({
|
|
27
|
+
line: node.position?.start.line ?? 1,
|
|
28
|
+
file: file.replace(/^(\.\/)+/, ""),
|
|
29
|
+
kind,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// The compiler reports invalid URLs; no editor should open for them.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return references.sort((a, b) => a.line - b.line);
|
|
38
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ProjectFont } from "../compiler/fonts.js";
|
|
2
|
+
import type { createFontSettings } from "./font-settings.js";
|
|
3
|
+
/** Self-contained because published viewers embed this function directly. */
|
|
4
|
+
export declare const mountFontPickers: (createSettings: typeof createFontSettings, registered: readonly ProjectFont[]) => Promise<void>;
|