@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,14 @@
|
|
|
1
|
+
export declare const fileSig: (path: string) => string;
|
|
2
|
+
/**
|
|
3
|
+
* Memoizes a single file. If the signature matches, returns the previous value (same reference)
|
|
4
|
+
* without reading/computing.
|
|
5
|
+
*/
|
|
6
|
+
export declare const createFileCache: <T>() => {
|
|
7
|
+
get: (path: string, compute: (raw: string) => T) => T;
|
|
8
|
+
};
|
|
9
|
+
/** Memoizes by signature (wraps a per-directory loader). */
|
|
10
|
+
export declare const createSigCache: <T>() => {
|
|
11
|
+
get: (sig: string, compute: () => T) => T;
|
|
12
|
+
};
|
|
13
|
+
/** Names directly under dir + each name's file signature; changes on any add/delete/modify. */
|
|
14
|
+
export declare const dirSig: (dir: string, fileOf?: (name: string) => string) => string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Makes the compiler incremental (even when the viewer calls compile() in full on every
|
|
2
|
+
// keystroke / save, files unchanged on disk aren't re-read/re-parsed). signature = mtimeMs + size.
|
|
3
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export const fileSig = (path) => {
|
|
6
|
+
let st;
|
|
7
|
+
try {
|
|
8
|
+
st = statSync(path);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "-";
|
|
12
|
+
}
|
|
13
|
+
return `${st.mtimeMs}:${st.size}`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Memoizes a single file. If the signature matches, returns the previous value (same reference)
|
|
17
|
+
* without reading/computing.
|
|
18
|
+
*/
|
|
19
|
+
export const createFileCache = () => {
|
|
20
|
+
const store = new Map();
|
|
21
|
+
return {
|
|
22
|
+
get: (path, compute) => {
|
|
23
|
+
const sig = fileSig(path);
|
|
24
|
+
const hit = store.get(path);
|
|
25
|
+
if (sig !== "-" && hit?.sig === sig) {
|
|
26
|
+
return hit.value;
|
|
27
|
+
}
|
|
28
|
+
const value = compute(readFileSync(path, "utf8"));
|
|
29
|
+
if (sig === "-") {
|
|
30
|
+
store.delete(path);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
store.set(path, { sig, value });
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/** Memoizes by signature (wraps a per-directory loader). */
|
|
40
|
+
export const createSigCache = () => {
|
|
41
|
+
let hit;
|
|
42
|
+
return {
|
|
43
|
+
get: (sig, compute) => {
|
|
44
|
+
if (hit?.sig !== sig) {
|
|
45
|
+
hit = { sig, value: compute() };
|
|
46
|
+
}
|
|
47
|
+
return hit.value;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/** Names directly under dir + each name's file signature; changes on any add/delete/modify. */
|
|
52
|
+
export const dirSig = (dir, fileOf = (n) => n) => {
|
|
53
|
+
let names;
|
|
54
|
+
try {
|
|
55
|
+
names = readdirSync(dir).sort();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return "-";
|
|
59
|
+
}
|
|
60
|
+
return names.map((n) => `${n}:${fileSig(join(dir, fileOf(n)))}`).join("|");
|
|
61
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FileLayout } from "../core/files.js";
|
|
2
|
+
export declare const managedFile: (file: string) => boolean;
|
|
3
|
+
export interface ProjectFile {
|
|
4
|
+
path: string;
|
|
5
|
+
size: number;
|
|
6
|
+
extension: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const projectFiles: (root: string) => ProjectFile[];
|
|
9
|
+
export declare const filesSnapshot: (root: string) => {
|
|
10
|
+
files: ProjectFile[];
|
|
11
|
+
layout: FileLayout;
|
|
12
|
+
revision: string;
|
|
13
|
+
documents: {
|
|
14
|
+
[k: string]: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare const uploadFile: (root: string, input: Record<string, unknown>) => {
|
|
18
|
+
file: string;
|
|
19
|
+
};
|
|
20
|
+
/** Rewrite destinations only: examples, labels, titles and code stay byte-for-byte intact. */
|
|
21
|
+
export declare const rewriteFileLinks: (raw: string, moves: ReadonlyMap<string, string>) => string;
|
|
22
|
+
export declare const updateFiles: (root: string, input: Record<string, unknown>) => {
|
|
23
|
+
files: ProjectFile[];
|
|
24
|
+
layout: FileLayout;
|
|
25
|
+
revision: string;
|
|
26
|
+
documents: {
|
|
27
|
+
[k: string]: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join, posix } from "node:path";
|
|
5
|
+
import { visit } from "unist-util-visit";
|
|
6
|
+
import { Document, isSeq, parseDocument } from "yaml";
|
|
7
|
+
import { flattenTree, MANIFEST_FILE } from "../core/content-metadata.js";
|
|
8
|
+
import { defaultFilePath, docFile, docIconFile, fileExtension, isDocumentFileName, localFile, MAX_FILE_UPLOAD_BYTES, parseFileLayout, } from "../core/files.js";
|
|
9
|
+
import { codeBlockAt, parseMdast } from "../core/markdown.js";
|
|
10
|
+
import { docPath } from "../core/routing.js";
|
|
11
|
+
import { termFileUrl } from "../core/term-reference.js";
|
|
12
|
+
import { isRemoteTextfile, parseFileReference } from "../core/textfile.js";
|
|
13
|
+
import { fileSig } from "./file-cache.js";
|
|
14
|
+
import { translationFile, translationFiles, translationLanguages } from "./locales.js";
|
|
15
|
+
import { readManifest } from "./manifest.js";
|
|
16
|
+
import { textfilePath, writeTextfile } from "./textfile.js";
|
|
17
|
+
import { themeNames, themeVariants } from "./theme-variants.js";
|
|
18
|
+
// Project settings and translations have their own layout, outside file restructuring.
|
|
19
|
+
const reserved = new Set([MANIFEST_FILE, "docs.config.mjs", "node_modules", "dist", "locales"]);
|
|
20
|
+
export const managedFile = (file) => localFile(file) && !reserved.has(file.split("/")[0] ?? "");
|
|
21
|
+
export const projectFiles = (root) => {
|
|
22
|
+
const result = [];
|
|
23
|
+
const walk = (directory) => {
|
|
24
|
+
for (const entry of readdirSync(join(root, directory), { withFileTypes: true })) {
|
|
25
|
+
const path = directory ? `${directory}/${entry.name}` : entry.name;
|
|
26
|
+
if (!managedFile(path) || entry.isSymbolicLink())
|
|
27
|
+
continue;
|
|
28
|
+
if (entry.isDirectory())
|
|
29
|
+
walk(path);
|
|
30
|
+
else if (entry.isFile())
|
|
31
|
+
result.push({
|
|
32
|
+
path,
|
|
33
|
+
size: statSync(textfilePath(root, path)).size,
|
|
34
|
+
extension: fileExtension(path),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
walk("");
|
|
39
|
+
return result.sort((a, b) => a.path.localeCompare(b.path));
|
|
40
|
+
};
|
|
41
|
+
export const filesSnapshot = (root) => {
|
|
42
|
+
const manifest = readManifest(root);
|
|
43
|
+
if (manifest.errors.length)
|
|
44
|
+
throw new Error(manifest.errors.join("\n"));
|
|
45
|
+
let files = projectFiles(root);
|
|
46
|
+
if (files.length) {
|
|
47
|
+
const ignored = spawnSync("git", ["-C", root, "check-ignore", "--stdin", "-z"], {
|
|
48
|
+
input: `${files.map((file) => file.path).join("\0")}\0`,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
timeout: 2_000,
|
|
51
|
+
maxBuffer: 5_000_000,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
if (ignored.status === 0) {
|
|
55
|
+
const paths = new Set(ignored.stdout.split("\0"));
|
|
56
|
+
files = files.filter((file) => !paths.has(file.path));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const layout = manifest.files ?? parseFileLayout({});
|
|
60
|
+
const rank = new Map(layout.order.map((path, index) => [path, index]));
|
|
61
|
+
files.sort((a, b) => (rank.get(a.path) ?? Infinity) - (rank.get(b.path) ?? Infinity) ||
|
|
62
|
+
a.path.localeCompare(b.path));
|
|
63
|
+
const hash = createHash("sha256");
|
|
64
|
+
hash.update(existsSync(join(root, MANIFEST_FILE)) ? readFileSync(join(root, MANIFEST_FILE)) : "");
|
|
65
|
+
for (const file of files)
|
|
66
|
+
hash.update(file.path)
|
|
67
|
+
.update(String(statSync(textfilePath(root, file.path)).mtimeMs))
|
|
68
|
+
.update(String(file.size));
|
|
69
|
+
for (const file of translationFiles(root))
|
|
70
|
+
hash.update(file).update(fileSig(textfilePath(root, file)));
|
|
71
|
+
const docs = flattenTree(manifest.tree);
|
|
72
|
+
return {
|
|
73
|
+
files,
|
|
74
|
+
layout,
|
|
75
|
+
revision: hash.digest("hex"),
|
|
76
|
+
documents: Object.fromEntries(docs.map(({ id }) => [docFile(id, layout), docPath(docs, id)])),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
export const uploadFile = (root, input) => {
|
|
80
|
+
if (typeof input.name !== "string" || !managedFile(input.name) || input.name.includes("/"))
|
|
81
|
+
throw new Error("Invalid file name");
|
|
82
|
+
if (typeof input.data !== "string")
|
|
83
|
+
throw new Error("Invalid file content");
|
|
84
|
+
const data = Buffer.from(input.data, "base64");
|
|
85
|
+
if (data.length > MAX_FILE_UPLOAD_BYTES)
|
|
86
|
+
throw new Error("File exceeds 64 MiB");
|
|
87
|
+
if (data.toString("base64") !== input.data)
|
|
88
|
+
throw new Error("Invalid file content");
|
|
89
|
+
const manifest = readManifest(root);
|
|
90
|
+
if (manifest.errors.length)
|
|
91
|
+
throw new Error(manifest.errors.join("\n"));
|
|
92
|
+
const initial = manifest.files?.defaultLayout !== false ? defaultFilePath(input.name) : input.name;
|
|
93
|
+
const extension = posix.extname(initial);
|
|
94
|
+
const stem = initial.slice(0, initial.length - extension.length);
|
|
95
|
+
let file = initial;
|
|
96
|
+
for (let index = 2; existsSync(textfilePath(root, file)); index++)
|
|
97
|
+
file = `${stem}-${index}${extension}`;
|
|
98
|
+
const path = textfilePath(root, file);
|
|
99
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
100
|
+
const temp = join(dirname(path), `.upload-${randomUUID()}.tmp`);
|
|
101
|
+
try {
|
|
102
|
+
writeFileSync(temp, data, { flag: "wx" });
|
|
103
|
+
linkSync(temp, path);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
rmSync(temp, { force: true });
|
|
107
|
+
}
|
|
108
|
+
return { file };
|
|
109
|
+
};
|
|
110
|
+
const imageDestination = (source) => {
|
|
111
|
+
let depth = 0;
|
|
112
|
+
for (let i = source.indexOf("["); i < source.length; i++) {
|
|
113
|
+
const char = source[i];
|
|
114
|
+
if (char === "\\") {
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (char === "`") {
|
|
119
|
+
const marker = /^`+/.exec(source.slice(i))?.[0] ?? "`";
|
|
120
|
+
const end = source.indexOf(marker, i + marker.length);
|
|
121
|
+
if (end >= 0) {
|
|
122
|
+
i = end + marker.length - 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (char === "[")
|
|
127
|
+
depth++;
|
|
128
|
+
if (char === "]" && --depth === 0)
|
|
129
|
+
return source[i + 1] === "(" ? i + 2 : -1;
|
|
130
|
+
}
|
|
131
|
+
return -1;
|
|
132
|
+
};
|
|
133
|
+
/** Rewrite destinations only: examples, labels, titles and code stay byte-for-byte intact. */
|
|
134
|
+
export const rewriteFileLinks = (raw, moves) => {
|
|
135
|
+
if (!moves.size)
|
|
136
|
+
return raw;
|
|
137
|
+
const changes = [];
|
|
138
|
+
visit(parseMdast(raw), (node) => {
|
|
139
|
+
if (node.type === "code" && node.lang === "callouth" && node.position) {
|
|
140
|
+
const block = codeBlockAt(raw, node.position.start.offset ?? 0);
|
|
141
|
+
const before = block.code.split("\n");
|
|
142
|
+
const after = rewriteFileLinks(block.code, moves).split("\n");
|
|
143
|
+
for (const [index, text] of after.entries()) {
|
|
144
|
+
const from = block.offsets[index];
|
|
145
|
+
if (text !== before[index] && from !== undefined)
|
|
146
|
+
changes.push({ from, to: from + (before[index]?.length ?? 0), text });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (node.type !== "image" && node.type !== "link" && node.type !== "definition")
|
|
150
|
+
return;
|
|
151
|
+
let file;
|
|
152
|
+
try {
|
|
153
|
+
file = parseFileReference(node.url).file.replace(/^(\.\/)+/, "");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (isRemoteTextfile(file))
|
|
159
|
+
return;
|
|
160
|
+
const next = moves.get(file);
|
|
161
|
+
if (!next)
|
|
162
|
+
return;
|
|
163
|
+
const from = node.position?.start.offset;
|
|
164
|
+
const to = node.position?.end.offset;
|
|
165
|
+
if (from === undefined || to === undefined)
|
|
166
|
+
throw new Error(`Cannot update reference: ${node.url}`);
|
|
167
|
+
const source = raw.slice(from, to);
|
|
168
|
+
const head = node.type === "definition"
|
|
169
|
+
? /^\[[\s\S]*?\]:\s*/.exec(source)?.[0].length
|
|
170
|
+
: node.type === "image"
|
|
171
|
+
? imageDestination(source)
|
|
172
|
+
: source.indexOf("](", Math.max(0, (node.children.at(-1)?.position?.end.offset ?? from) - from)) + 2;
|
|
173
|
+
if (!head || head < 2)
|
|
174
|
+
throw new Error(`Cannot update reference: ${node.url}`);
|
|
175
|
+
const start = head + (source.slice(head).match(/^\s*/)?.[0].length ?? 0);
|
|
176
|
+
let end = start;
|
|
177
|
+
if (source[start] === "<")
|
|
178
|
+
end = source.indexOf(">", start) + 1;
|
|
179
|
+
else {
|
|
180
|
+
let depth = 0;
|
|
181
|
+
for (; end < source.length; end++) {
|
|
182
|
+
const ch = source[end];
|
|
183
|
+
if (ch === "\\") {
|
|
184
|
+
end++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "(")
|
|
188
|
+
depth++;
|
|
189
|
+
if (ch === ")") {
|
|
190
|
+
if (!depth)
|
|
191
|
+
break;
|
|
192
|
+
depth--;
|
|
193
|
+
}
|
|
194
|
+
if (/\s/.test(ch ?? ""))
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (end <= start)
|
|
199
|
+
throw new Error(`Cannot update reference: ${node.url}`);
|
|
200
|
+
const check = parseMdast(`[x](${source.slice(start, end)})`).children[0];
|
|
201
|
+
if (check?.type !== "paragraph" ||
|
|
202
|
+
check.children[0]?.type !== "link" ||
|
|
203
|
+
check.children[0].url !== node.url)
|
|
204
|
+
throw new Error(`Cannot update reference: ${node.url}`);
|
|
205
|
+
const fragment = node.url.includes("#") ? node.url.slice(node.url.indexOf("#")) : "";
|
|
206
|
+
changes.push({ from: from + start, to: from + end, text: termFileUrl(next) + fragment });
|
|
207
|
+
});
|
|
208
|
+
for (const change of changes.sort((a, b) => b.from - a.from))
|
|
209
|
+
raw = raw.slice(0, change.from) + change.text + raw.slice(change.to);
|
|
210
|
+
return raw;
|
|
211
|
+
};
|
|
212
|
+
export const updateFiles = (root, input) => {
|
|
213
|
+
const snapshot = filesSnapshot(root);
|
|
214
|
+
if (input.revision !== snapshot.revision)
|
|
215
|
+
throw new Error("Files changed. Reload before retrying.");
|
|
216
|
+
const manifest = readManifest(root);
|
|
217
|
+
const layout = snapshot.layout;
|
|
218
|
+
const paths = snapshot.files.map((file) => file.path);
|
|
219
|
+
const moves = new Map();
|
|
220
|
+
let order = [...paths];
|
|
221
|
+
if (input.action === "layout") {
|
|
222
|
+
if (typeof input.enabled !== "boolean")
|
|
223
|
+
throw new Error("Invalid layout setting");
|
|
224
|
+
layout.defaultLayout = input.enabled;
|
|
225
|
+
}
|
|
226
|
+
else if (input.action === "restructure") {
|
|
227
|
+
const taken = new Set(paths);
|
|
228
|
+
for (const file of paths) {
|
|
229
|
+
if (file.startsWith("themes/"))
|
|
230
|
+
continue;
|
|
231
|
+
let target = defaultFilePath(file);
|
|
232
|
+
if (target === file)
|
|
233
|
+
continue;
|
|
234
|
+
const ext = posix.extname(target);
|
|
235
|
+
const stem = target.slice(0, target.length - ext.length);
|
|
236
|
+
for (let index = 2; taken.has(target); index++)
|
|
237
|
+
target = `${stem}-${index}${ext}`;
|
|
238
|
+
taken.add(target);
|
|
239
|
+
moves.set(file, target);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (input.action === "rename") {
|
|
243
|
+
if (typeof input.file !== "string" || !paths.includes(input.file))
|
|
244
|
+
throw new Error("File not found");
|
|
245
|
+
if (!localFile(input.name) || input.name.includes("/"))
|
|
246
|
+
throw new Error("Invalid file name");
|
|
247
|
+
if (flattenTree(manifest.tree).some(({ id }) => docFile(id, layout) === input.file) &&
|
|
248
|
+
!isDocumentFileName(input.name))
|
|
249
|
+
throw new Error("Enter a file name ending in .md");
|
|
250
|
+
const target = posix.join(posix.dirname(input.file), input.name);
|
|
251
|
+
if (!managedFile(target))
|
|
252
|
+
throw new Error("Invalid file name");
|
|
253
|
+
if (target !== input.file)
|
|
254
|
+
moves.set(input.file, target);
|
|
255
|
+
}
|
|
256
|
+
else if (input.action === "move") {
|
|
257
|
+
if (!Array.isArray(input.paths) ||
|
|
258
|
+
!input.paths.length ||
|
|
259
|
+
input.paths.some((path) => !managedFile(path)) ||
|
|
260
|
+
typeof input.directory !== "string" ||
|
|
261
|
+
(input.directory && !managedFile(input.directory)))
|
|
262
|
+
throw new Error("Invalid move");
|
|
263
|
+
const selected = input.paths.filter((path, i, all) => !all.some((parent, j) => i !== j && path.startsWith(`${parent}/`)));
|
|
264
|
+
const directory = input.directory;
|
|
265
|
+
const picked = paths.filter((path) => selected.some((item) => path === item || path.startsWith(`${item}/`)));
|
|
266
|
+
if (!picked.length ||
|
|
267
|
+
selected.some((path) => !picked.some((file) => file === path || file.startsWith(`${path}/`))))
|
|
268
|
+
throw new Error("File not found");
|
|
269
|
+
for (const item of selected) {
|
|
270
|
+
if (directory === item || directory.startsWith(`${item}/`))
|
|
271
|
+
throw new Error("Cannot move a folder into itself");
|
|
272
|
+
const target = directory
|
|
273
|
+
? `${directory}/${posix.basename(item)}`
|
|
274
|
+
: posix.basename(item);
|
|
275
|
+
for (const file of picked.filter((path) => path === item || path.startsWith(`${item}/`))) {
|
|
276
|
+
const next = target + file.slice(item.length);
|
|
277
|
+
if (next !== file)
|
|
278
|
+
moves.set(file, next);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
order = paths.filter((path) => !picked.includes(path));
|
|
282
|
+
const before = input.before;
|
|
283
|
+
const index = before === undefined
|
|
284
|
+
? order.length
|
|
285
|
+
: order.findIndex((path) => path === before || path.startsWith(`${before}/`));
|
|
286
|
+
if (index < 0)
|
|
287
|
+
throw new Error("Invalid drop position");
|
|
288
|
+
order.splice(index, 0, ...picked);
|
|
289
|
+
}
|
|
290
|
+
else
|
|
291
|
+
throw new Error("Unknown file action");
|
|
292
|
+
for (const [source, target] of [...moves]) {
|
|
293
|
+
for (const [theme, file] of Object.entries(themeVariants(root, source, themeNames(root))))
|
|
294
|
+
moves.set(file, `themes/${theme}/${target}`);
|
|
295
|
+
}
|
|
296
|
+
const translated = translationFiles(root);
|
|
297
|
+
for (const language of translationLanguages(root)) {
|
|
298
|
+
for (const [source, target] of [...moves]) {
|
|
299
|
+
if (!source.endsWith(".md") || source.startsWith("locales/"))
|
|
300
|
+
continue;
|
|
301
|
+
const file = translationFile(source, language);
|
|
302
|
+
if (translated.includes(file))
|
|
303
|
+
moves.set(file, translationFile(target, language));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (new Set(moves.values()).size !== moves.size)
|
|
307
|
+
throw new Error("Duplicate destination");
|
|
308
|
+
for (const target of moves.values()) {
|
|
309
|
+
const path = textfilePath(root, target);
|
|
310
|
+
if (existsSync(path))
|
|
311
|
+
throw new Error(`File already exists: ${target}`);
|
|
312
|
+
}
|
|
313
|
+
for (const { id } of flattenTree(manifest.tree)) {
|
|
314
|
+
const file = docFile(id, layout);
|
|
315
|
+
if (moves.has(file))
|
|
316
|
+
layout.documents[id] = moves.get(file);
|
|
317
|
+
const icon = docIconFile(id, layout);
|
|
318
|
+
if (moves.has(icon)) {
|
|
319
|
+
layout.icons ??= {};
|
|
320
|
+
layout.icons[id] = moves.get(icon);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
layout.order = order.map((file) => moves.get(file) ?? file);
|
|
324
|
+
const rawManifest = existsSync(join(root, MANIFEST_FILE))
|
|
325
|
+
? readFileSync(join(root, MANIFEST_FILE), "utf8")
|
|
326
|
+
: null;
|
|
327
|
+
const parsed = parseDocument(rawManifest ?? "docs: []\n");
|
|
328
|
+
const yaml = isSeq(parsed.contents) ? new Document({ docs: parsed.toJS() }) : parsed;
|
|
329
|
+
yaml.set("files", layout);
|
|
330
|
+
if (manifest.icon && moves.has(manifest.icon))
|
|
331
|
+
yaml.set("icon", moves.get(manifest.icon));
|
|
332
|
+
const writes = [];
|
|
333
|
+
const links = new Map(moves);
|
|
334
|
+
for (const file of [...paths, ...translated].filter((file) => /\.(md|callouth)$/.test(file))) {
|
|
335
|
+
const base = readFileSync(textfilePath(root, file));
|
|
336
|
+
const next = rewriteFileLinks(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(base), moves);
|
|
337
|
+
const data = Buffer.from(next);
|
|
338
|
+
if (base.equals(data))
|
|
339
|
+
continue;
|
|
340
|
+
links.delete(file);
|
|
341
|
+
const target = moves.get(file) ?? file;
|
|
342
|
+
writes.push({
|
|
343
|
+
file: target,
|
|
344
|
+
data,
|
|
345
|
+
base: target === file ? base : null,
|
|
346
|
+
mode: statSync(textfilePath(root, file)).mode,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
writes.push({
|
|
350
|
+
file: MANIFEST_FILE,
|
|
351
|
+
base: rawManifest === null ? null : Buffer.from(rawManifest),
|
|
352
|
+
data: Buffer.from(yaml.toString()),
|
|
353
|
+
});
|
|
354
|
+
const originalModes = new Map([...moves.keys()].map((file) => [file, statSync(textfilePath(root, file)).mode]));
|
|
355
|
+
const originals = new Map([...moves.keys()].map((file) => [
|
|
356
|
+
file,
|
|
357
|
+
{
|
|
358
|
+
signature: fileSig(textfilePath(root, file)),
|
|
359
|
+
data: links.has(file) ? undefined : readFileSync(textfilePath(root, file)),
|
|
360
|
+
},
|
|
361
|
+
]));
|
|
362
|
+
const linked = [];
|
|
363
|
+
const completed = [];
|
|
364
|
+
const removed = [];
|
|
365
|
+
try {
|
|
366
|
+
for (const [source, target] of links) {
|
|
367
|
+
const destination = textfilePath(root, target);
|
|
368
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
369
|
+
linkSync(textfilePath(root, source), destination);
|
|
370
|
+
linked.push([source, target]);
|
|
371
|
+
}
|
|
372
|
+
for (const write of writes) {
|
|
373
|
+
const path = textfilePath(root, write.file);
|
|
374
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
375
|
+
if (write.base === null)
|
|
376
|
+
writeFileSync(path, write.data, { flag: "wx", mode: write.mode });
|
|
377
|
+
else if (!writeTextfile(root, write.file, write.data.toString("utf8"), write.base.toString("utf8")))
|
|
378
|
+
throw new Error(`File changed: ${write.file}`);
|
|
379
|
+
completed.push(write);
|
|
380
|
+
}
|
|
381
|
+
for (const [file, base] of originals) {
|
|
382
|
+
if (fileSig(textfilePath(root, file)) !== base.signature)
|
|
383
|
+
throw new Error(`File changed: ${file}`);
|
|
384
|
+
}
|
|
385
|
+
for (const file of originals.keys()) {
|
|
386
|
+
unlinkSync(textfilePath(root, file));
|
|
387
|
+
removed.push(file);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
for (const file of removed) {
|
|
392
|
+
const original = originals.get(file);
|
|
393
|
+
if (original?.data)
|
|
394
|
+
writeFileSync(textfilePath(root, file), original.data, {
|
|
395
|
+
flag: "wx",
|
|
396
|
+
mode: originalModes.get(file),
|
|
397
|
+
});
|
|
398
|
+
else
|
|
399
|
+
linkSync(textfilePath(root, moves.get(file)), textfilePath(root, file));
|
|
400
|
+
}
|
|
401
|
+
for (const write of completed.reverse()) {
|
|
402
|
+
const path = textfilePath(root, write.file);
|
|
403
|
+
if (!readFileSync(path).equals(write.data))
|
|
404
|
+
continue;
|
|
405
|
+
if (write.base === null)
|
|
406
|
+
unlinkSync(path);
|
|
407
|
+
else
|
|
408
|
+
writeFileSync(path, write.base);
|
|
409
|
+
}
|
|
410
|
+
for (const [source, target] of linked) {
|
|
411
|
+
const src = textfilePath(root, source);
|
|
412
|
+
const dest = textfilePath(root, target);
|
|
413
|
+
if (existsSync(dest) && statSync(src).ino === statSync(dest).ino)
|
|
414
|
+
unlinkSync(dest);
|
|
415
|
+
}
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
for (const file of moves.keys()) {
|
|
419
|
+
let dir = posix.dirname(file);
|
|
420
|
+
while (dir !== ".") {
|
|
421
|
+
try {
|
|
422
|
+
rmdirSync(join(root, dir));
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
dir = posix.dirname(dir);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return filesSnapshot(root);
|
|
431
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const PROJECT_FONTS_DIR = ".dooc/fonts";
|
|
2
|
+
export interface ProjectFont {
|
|
3
|
+
name: string;
|
|
4
|
+
family: string;
|
|
5
|
+
source: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const listProjectFonts: (root: string) => string[];
|
|
8
|
+
export declare const addProjectFont: (root: string, file: string) => string;
|
|
9
|
+
export declare const removeProjectFont: (root: string, name: string) => void;
|
|
10
|
+
export declare const loadProjectFonts: (root: string) => ProjectFont[];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, extname } from "node:path";
|
|
4
|
+
import { localFile } from "../core/files.js";
|
|
5
|
+
import { textfilePath } from "./textfile.js";
|
|
6
|
+
export const PROJECT_FONTS_DIR = ".dooc/fonts";
|
|
7
|
+
const fontName = (name) => {
|
|
8
|
+
if (!localFile(name) || name.includes("/") || !/\.(ttf|otf|woff2?)$/i.test(name))
|
|
9
|
+
throw new Error("Expected a TTF, OTF, WOFF or WOFF2 file name");
|
|
10
|
+
return name;
|
|
11
|
+
};
|
|
12
|
+
export const listProjectFonts = (root) => {
|
|
13
|
+
const directory = textfilePath(root, PROJECT_FONTS_DIR);
|
|
14
|
+
if (!lstatSync(directory, { throwIfNoEntry: false }))
|
|
15
|
+
return [];
|
|
16
|
+
return readdirSync(directory, { withFileTypes: true })
|
|
17
|
+
.filter((entry) => entry.isFile() && /\.(ttf|otf|woff2?)$/i.test(entry.name))
|
|
18
|
+
.map((entry) => fontName(entry.name))
|
|
19
|
+
.sort();
|
|
20
|
+
};
|
|
21
|
+
export const addProjectFont = (root, file) => {
|
|
22
|
+
const name = fontName(basename(file));
|
|
23
|
+
const data = readFileSync(file);
|
|
24
|
+
const extension = extname(name).slice(1).toLowerCase();
|
|
25
|
+
const signature = data.subarray(0, 4).toString("latin1");
|
|
26
|
+
const signatures = {
|
|
27
|
+
ttf: ["\x00\x01\x00\x00", "true"],
|
|
28
|
+
otf: ["OTTO"],
|
|
29
|
+
woff: ["wOFF"],
|
|
30
|
+
woff2: ["wOF2"],
|
|
31
|
+
};
|
|
32
|
+
if (data.length < (extension === "woff2" ? 48 : extension === "woff" ? 44 : 12) ||
|
|
33
|
+
!signatures[extension]?.includes(signature) ||
|
|
34
|
+
(extension.startsWith("woff") && data.readUInt32BE(8) !== data.length))
|
|
35
|
+
throw new Error(`${name}: invalid font header`);
|
|
36
|
+
mkdirSync(textfilePath(root, PROJECT_FONTS_DIR), { recursive: true });
|
|
37
|
+
// Exclusive creation preserves a registered font until it is explicitly removed.
|
|
38
|
+
writeFileSync(textfilePath(root, `${PROJECT_FONTS_DIR}/${name}`), data, { flag: "wx" });
|
|
39
|
+
return `${PROJECT_FONTS_DIR}/${name}`;
|
|
40
|
+
};
|
|
41
|
+
export const removeProjectFont = (root, name) => {
|
|
42
|
+
if (!listProjectFonts(root).includes(fontName(name)))
|
|
43
|
+
throw new Error(`Font not found: ${name}`);
|
|
44
|
+
unlinkSync(textfilePath(root, `${PROJECT_FONTS_DIR}/${name}`));
|
|
45
|
+
};
|
|
46
|
+
export const loadProjectFonts = (root) => listProjectFonts(root).map((name) => {
|
|
47
|
+
const data = readFileSync(textfilePath(root, `${PROJECT_FONTS_DIR}/${name}`));
|
|
48
|
+
const hash = createHash("sha256").update(data).digest("hex").slice(0, 16);
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
family: `dooc-font-${hash}`,
|
|
52
|
+
source: `url(data:font/${extname(name).slice(1).toLowerCase()};base64,${data.toString("base64")})`,
|
|
53
|
+
};
|
|
54
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatMarkdown(body: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
2
|
+
import remarkGfm from "remark-gfm";
|
|
3
|
+
import remarkStringify from "remark-stringify";
|
|
4
|
+
import stringWidth from "string-width";
|
|
5
|
+
import { unified } from "unified";
|
|
6
|
+
import { parseMdast } from "./dialect.js";
|
|
7
|
+
export function formatMarkdown(body) {
|
|
8
|
+
if (!body.trim())
|
|
9
|
+
return "";
|
|
10
|
+
const lines = body.replaceAll("\r\n", "\n").split("\n");
|
|
11
|
+
// Semantic code fences carry their own syntax; preserve their source verbatim.
|
|
12
|
+
const preserve = (node) => {
|
|
13
|
+
const position = node.position;
|
|
14
|
+
if (!position)
|
|
15
|
+
throw new Error("Missing source position for Markdown formatting");
|
|
16
|
+
const raw = lines.slice(position.start.line - 1, position.end.line);
|
|
17
|
+
const indent = position.start.column - 1;
|
|
18
|
+
return raw
|
|
19
|
+
.map((line, index) => index === 0 ? line.slice(indent) : line.replace(new RegExp(`^ {0,${indent}}`), ""))
|
|
20
|
+
.join("\n");
|
|
21
|
+
};
|
|
22
|
+
const printer = unified()
|
|
23
|
+
.use(remarkFrontmatter)
|
|
24
|
+
.use(remarkGfm, { stringLength: stringWidth })
|
|
25
|
+
.use(remarkStringify, {
|
|
26
|
+
bullet: "-",
|
|
27
|
+
listItemIndent: "one",
|
|
28
|
+
fences: true,
|
|
29
|
+
handlers: {
|
|
30
|
+
code: (node, parent) => {
|
|
31
|
+
// Nested blocks need the printer to reconstruct list/quote prefixes.
|
|
32
|
+
if (parent?.type !== "root") {
|
|
33
|
+
const marker = "`".repeat(Math.max(3, ...[...node.value.matchAll(/`+/g)].map((match) => match[0].length + 1)));
|
|
34
|
+
return `${marker}${node.lang ?? ""}${node.meta ? ` ${node.meta}` : ""}\n${node.value}\n${marker}`;
|
|
35
|
+
}
|
|
36
|
+
return preserve(node);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const formatted = printer.stringify(parseMdast(body));
|
|
41
|
+
return body.includes("\r\n") ? formatted.replaceAll("\n", "\r\n") : formatted;
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
import { type Block, type TocEntry } from "../core/blocks.js";
|
|
3
|
+
import type { PreviewSettings } from "../core/previewers.js";
|
|
4
|
+
/**
|
|
5
|
+
* Dialect mdast → normalized Block[] + TOC. Heading slugs are made unique within the document.
|
|
6
|
+
* `raw` is the whole source document, used to validate semantic code fences.
|
|
7
|
+
* `lines` holds the source start line at the same index as blocks (1-indexed relative to the
|
|
8
|
+
* whole raw, as root is).
|
|
9
|
+
* Only the edit tool's "body element ↔ source line" mapping uses it (the rendering model
|
|
10
|
+
* is unaffected).
|
|
11
|
+
*/
|
|
12
|
+
export declare const toBlocks: (root: Root, raw: string, settings?: PreviewSettings, file?: string) => {
|
|
13
|
+
blocks: Block[];
|
|
14
|
+
toc: TocEntry[];
|
|
15
|
+
lines: number[];
|
|
16
|
+
};
|