@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,109 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { languageAliasNames, languageNames } from "@shikijs/langs";
|
|
4
|
+
import { bundledLanguages } from "shiki/langs";
|
|
5
|
+
import { CODE_LANGS, CODE_TEMPLATES } from "../core/taxonomy.js";
|
|
6
|
+
export const CODE_LANGUAGES_DIR = ".dooc/code-languages";
|
|
7
|
+
const supported = new Set([
|
|
8
|
+
...languageNames,
|
|
9
|
+
...languageAliasNames,
|
|
10
|
+
...Object.keys(CODE_TEMPLATES),
|
|
11
|
+
]);
|
|
12
|
+
const safeName = /^[a-z0-9][a-z0-9_-]*$/;
|
|
13
|
+
const parseGrammars = (value, source) => {
|
|
14
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
15
|
+
if (!entries.length)
|
|
16
|
+
throw new Error(`${source}: at least one TextMate grammar is required`);
|
|
17
|
+
return entries.map((entry) => {
|
|
18
|
+
if (typeof entry !== "object" ||
|
|
19
|
+
entry === null ||
|
|
20
|
+
typeof entry.name !== "string" ||
|
|
21
|
+
!safeName.test(entry.name) ||
|
|
22
|
+
typeof entry.scopeName !== "string" ||
|
|
23
|
+
!entry.scopeName ||
|
|
24
|
+
!Array.isArray(entry.patterns) ||
|
|
25
|
+
(entry.aliases !== undefined &&
|
|
26
|
+
(!Array.isArray(entry.aliases) ||
|
|
27
|
+
entry.aliases.some((alias) => typeof alias !== "string" || !alias)))) {
|
|
28
|
+
throw new Error(`${source}: each grammar needs a safe name, scopeName, patterns array and optional string aliases`);
|
|
29
|
+
}
|
|
30
|
+
return entry;
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const readGrammars = (file) => {
|
|
34
|
+
try {
|
|
35
|
+
return parseGrammars(JSON.parse(readFileSync(file, "utf8")), file);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`${file}: ${error instanceof Error ? error.message : error}`);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
export const loadCodeGrammars = async (names) => {
|
|
42
|
+
const grammars = await Promise.all(names.map(async (name) => {
|
|
43
|
+
if (name === "graphth")
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
name,
|
|
47
|
+
scopeName: "source.graphth",
|
|
48
|
+
patterns: [
|
|
49
|
+
{ match: "^\\s*#.*$", name: "comment.line.graphth" },
|
|
50
|
+
{ match: "\\[[^\\[\\]\\n]*\\]", name: "variable.other.graphth" },
|
|
51
|
+
{ match: "-->|->|--", name: "keyword.operator.graphth" },
|
|
52
|
+
{ match: "(?<=\\s)@\\S+", name: "entity.name.tag.graphth" },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
if (name === "termth" || name === "schemath" || name === "callouth") {
|
|
57
|
+
const language = name === "callouth" ? "markdown" : "yaml";
|
|
58
|
+
return [
|
|
59
|
+
...(await bundledLanguages[language]()).default,
|
|
60
|
+
{
|
|
61
|
+
name,
|
|
62
|
+
scopeName: `source.${name}`,
|
|
63
|
+
patterns: [
|
|
64
|
+
{
|
|
65
|
+
include: language === "markdown"
|
|
66
|
+
? "text.html.markdown"
|
|
67
|
+
: `source.${language}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
if (!supported.has(name))
|
|
74
|
+
throw new Error(`Unsupported Shiki language: ${name}`);
|
|
75
|
+
return (await bundledLanguages[name]()).default;
|
|
76
|
+
}));
|
|
77
|
+
return [...new Map(grammars.flat().map((grammar) => [grammar.name, grammar])).values()];
|
|
78
|
+
};
|
|
79
|
+
/** Renderer-owned registrations; Markdown content is never changed. */
|
|
80
|
+
export const loadCodeLanguages = async (root) => {
|
|
81
|
+
const directory = join(root, CODE_LANGUAGES_DIR);
|
|
82
|
+
const extra = existsSync(directory)
|
|
83
|
+
? readdirSync(directory)
|
|
84
|
+
.filter((file) => file.endsWith(".json"))
|
|
85
|
+
.sort()
|
|
86
|
+
.flatMap((file) => readGrammars(join(directory, file)))
|
|
87
|
+
: [];
|
|
88
|
+
const defaults = await loadCodeGrammars(CODE_LANGS);
|
|
89
|
+
return {
|
|
90
|
+
codeLanguages: [...new Set([...CODE_LANGS, ...extra.map((grammar) => grammar.name)])],
|
|
91
|
+
codeGrammars: [
|
|
92
|
+
...new Map([...defaults, ...extra].map((grammar) => [grammar.name, grammar])).values(),
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** Persist a Shiki language or imported TextMate JSON under the renderer's directory. */
|
|
97
|
+
export const addCodeLanguage = async (root, input) => {
|
|
98
|
+
const grammars = supported.has(input)
|
|
99
|
+
? await loadCodeGrammars([input])
|
|
100
|
+
: readGrammars(resolve(input));
|
|
101
|
+
const primary = grammars.find((grammar) => grammar.name === input || grammar.aliases?.includes(input)) ??
|
|
102
|
+
grammars.at(-1);
|
|
103
|
+
if (!primary || !safeName.test(primary.name))
|
|
104
|
+
throw new Error("Invalid code language name");
|
|
105
|
+
const target = join(root, CODE_LANGUAGES_DIR, `${primary.name}.json`);
|
|
106
|
+
mkdirSync(join(root, CODE_LANGUAGES_DIR), { recursive: true });
|
|
107
|
+
writeFileSync(target, `${JSON.stringify(grammars, null, 2)}\n`, { flag: "wx" });
|
|
108
|
+
return relative(root, target);
|
|
109
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type TocEntry } from "../core/blocks.js";
|
|
2
|
+
import type { CompiledDoc, FigureEntry, TermEntry } from "../core/content-types.js";
|
|
3
|
+
import { type PreviewSettings } from "../core/previewers.js";
|
|
4
|
+
import { type DocRef } from "../core/routing.js";
|
|
5
|
+
import type { Frontmatter } from "../core/taxonomy.js";
|
|
6
|
+
export type { CompiledDoc, FigureEntry, } from "../core/content-types.js";
|
|
7
|
+
/** Authored links, including callout bodies; code examples do not create references. */
|
|
8
|
+
export declare const collectDocLinks: (body: string) => string[];
|
|
9
|
+
/** Source text and diagnostics for editing. key = CompiledDoc.file. */
|
|
10
|
+
export interface DocSource {
|
|
11
|
+
terms: TermEntry[];
|
|
12
|
+
/**
|
|
13
|
+
* Raw text on disk (including front-matter). The editor's initial value and the base for
|
|
14
|
+
* detecting external changes.
|
|
15
|
+
*/
|
|
16
|
+
raw: string;
|
|
17
|
+
/** Validated front-matter; undefined if broken (CompiledDoc then uses the id as title). */
|
|
18
|
+
frontmatter: Frontmatter | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* For the editor's source-mapping display: each block's start line, relative to the body
|
|
21
|
+
* (same index as CompiledDoc.blocks).
|
|
22
|
+
*/
|
|
23
|
+
lines: number[];
|
|
24
|
+
/**
|
|
25
|
+
* This doc's validation errors (dialect + resolution of figures/document links).
|
|
26
|
+
* No file prefix.
|
|
27
|
+
*/
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface Compiled {
|
|
31
|
+
previews: PreviewSettings;
|
|
32
|
+
title?: string;
|
|
33
|
+
icon?: string;
|
|
34
|
+
/** Display order follows dooc.yaml. Invalid sources retain their diagnostics for editing. */
|
|
35
|
+
docs: CompiledDoc[];
|
|
36
|
+
terms: TermEntry[];
|
|
37
|
+
figures: FigureEntry[];
|
|
38
|
+
sources: Map<string, DocSource>;
|
|
39
|
+
/** Errors not tied to a doc (layout convention / manifest / figures / terms). */
|
|
40
|
+
siteErrors: string[];
|
|
41
|
+
/** All errors (siteErrors + each doc's errors with a file prefix). */
|
|
42
|
+
errors: string[];
|
|
43
|
+
/** Site default language (docs without front-matter `lang` resolve to it). */
|
|
44
|
+
defaultLang: string;
|
|
45
|
+
contentLanguages?: string[];
|
|
46
|
+
themes?: ("light" | "dark")[];
|
|
47
|
+
}
|
|
48
|
+
export interface CompileOptions {
|
|
49
|
+
/** Network permissions supplied by the host application, never by document metadata. */
|
|
50
|
+
previews?: PreviewSettings;
|
|
51
|
+
/** Selected content language; translations mirror source paths under locales/<locale>/. */
|
|
52
|
+
locale?: string;
|
|
53
|
+
/** Override dooc.yaml's defaultLang for programmatic compilation. */
|
|
54
|
+
defaultLang?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface Compiler {
|
|
57
|
+
/**
|
|
58
|
+
* Compiles the whole docs root (content/ figures/). Unchanged files hit the
|
|
59
|
+
* cache.
|
|
60
|
+
*/
|
|
61
|
+
compile(): Compiled;
|
|
62
|
+
}
|
|
63
|
+
/** Site context needed to check a draft's references (document file targets / figures). */
|
|
64
|
+
export interface DraftContext {
|
|
65
|
+
previews?: PreviewSettings;
|
|
66
|
+
/** Docs root used to resolve embedded file paths. */
|
|
67
|
+
root?: string;
|
|
68
|
+
/** Unsaved local file contents, used only for draft previews. */
|
|
69
|
+
textfiles?: ReadonlyMap<string, string>;
|
|
70
|
+
/** file = CompiledDoc.file (`content/<id>.md`); used to look up a draft's id / parent. */
|
|
71
|
+
docs: readonly (DocRef & {
|
|
72
|
+
file: string;
|
|
73
|
+
toc?: readonly TocEntry[];
|
|
74
|
+
})[];
|
|
75
|
+
/** name → figure (file = `figures/<file>`). */
|
|
76
|
+
figures: {
|
|
77
|
+
get(name: string): {
|
|
78
|
+
file: string;
|
|
79
|
+
} | undefined;
|
|
80
|
+
};
|
|
81
|
+
/** Site default language for a draft without front-matter `lang`. Default: DEFAULT_LANG. */
|
|
82
|
+
defaultLang?: string;
|
|
83
|
+
}
|
|
84
|
+
export declare const createCompiler: (root: string, options?: CompileOptions) => Compiler;
|
|
85
|
+
/**
|
|
86
|
+
* Compiles a single unsaved draft (file = `content/<id>.md`) in the site's context.
|
|
87
|
+
* Not cached, since it changes on every keystroke. id / parent are looked up from the ctx.docs
|
|
88
|
+
* entry with the same file ("" for a new doc).
|
|
89
|
+
*/
|
|
90
|
+
export declare const compileDraft: (ctx: DraftContext, file: string, raw: string) => {
|
|
91
|
+
doc: CompiledDoc;
|
|
92
|
+
source: DocSource;
|
|
93
|
+
};
|
|
94
|
+
/** One-shot compile (for the build CLI / tests). */
|
|
95
|
+
export declare const compileSite: (root: string, options?: CompileOptions) => Compiled;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// Reads dooc.yaml and source files into validated, in-memory content for the viewer and HTML
|
|
2
|
+
// bundles. createCompiler caches unchanged files; compileSite performs a one-shot compilation.
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { walkBlocks } from "../core/blocks.js";
|
|
6
|
+
import { docIdOfFile, flattenTree, MANIFEST_FILE } from "../core/content-metadata.js";
|
|
7
|
+
import { termMap } from "../core/inline.js";
|
|
8
|
+
import { DEFAULT_LANG, isLangTag } from "../core/language.js";
|
|
9
|
+
import { contentLinks, parseMdast } from "../core/markdown.js";
|
|
10
|
+
import { defaultPreviewSettings, parsePreviewSettings, } from "../core/previewers.js";
|
|
11
|
+
import { parseDocLink } from "../core/routing.js";
|
|
12
|
+
import { isTermFile } from "../core/term-reference.js";
|
|
13
|
+
import { isRemoteTextfile } from "../core/textfile.js";
|
|
14
|
+
import { parseContentSource } from "./code-blocks.js";
|
|
15
|
+
import { collectRefs, parseDoc } from "./dialect.js";
|
|
16
|
+
import { figureName, loadFigures } from "./figures.js";
|
|
17
|
+
import { createFileCache, createSigCache, dirSig, fileSig } from "./file-cache.js";
|
|
18
|
+
import { projectFiles } from "./files.js";
|
|
19
|
+
import { toBlocks } from "./from-mdast.js";
|
|
20
|
+
import { loadIcons, squareSvg } from "./icons.js";
|
|
21
|
+
import { contentLanguageCode, isContentLanguage } from "./language-codes.js";
|
|
22
|
+
import { siteContentLanguages, translationFile } from "./locales.js";
|
|
23
|
+
import { parseManifestText } from "./manifest.js";
|
|
24
|
+
import { parseTermSource } from "./terms.js";
|
|
25
|
+
import { readTextfile, selectTextfileRange } from "./textfile.js";
|
|
26
|
+
import { TextfileUnavailable } from "./textfile-remote.js";
|
|
27
|
+
import { siteThemes, themeVariants } from "./theme-variants.js";
|
|
28
|
+
/** Authored links, including callout bodies; code examples do not create references. */
|
|
29
|
+
export const collectDocLinks = (body) => contentLinks(parseMdast(body))
|
|
30
|
+
.map((node) => node.url)
|
|
31
|
+
.filter((url) => {
|
|
32
|
+
try {
|
|
33
|
+
return parseDocLink(url) !== undefined;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Maps mdast line numbers (1-indexed in raw, which includes front-matter) to the body-relative
|
|
40
|
+
// lines the editor shows. body is a suffix of raw (parseDoc only strips the front-matter), so
|
|
41
|
+
// subtract the number of newlines before it.
|
|
42
|
+
const toBodyLines = (raw, body, lines) => {
|
|
43
|
+
const before = raw.slice(0, raw.length - body.length).split("\n").length - 1;
|
|
44
|
+
return lines.map((l) => Math.max(1, l - before));
|
|
45
|
+
};
|
|
46
|
+
const parseFile = (raw, previews, file) => {
|
|
47
|
+
const parsed = parseDoc(raw, previews, file);
|
|
48
|
+
const { blocks, toc, lines } = toBlocks(parsed.root, parsed.source, previews, file);
|
|
49
|
+
const refs = collectRefs(parsed.root, previews);
|
|
50
|
+
const figureFiles = refs.figures;
|
|
51
|
+
return {
|
|
52
|
+
raw,
|
|
53
|
+
parsed,
|
|
54
|
+
blocks,
|
|
55
|
+
toc,
|
|
56
|
+
lines: toBodyLines(raw, parsed.body, lines),
|
|
57
|
+
refs: { figures: figureFiles.map(figureName), terms: refs.terms },
|
|
58
|
+
figureFiles,
|
|
59
|
+
docLinks: collectDocLinks(parsed.body),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const resolveFiles = (f, root, textfiles, previews, themes) => {
|
|
63
|
+
if (!f.refs.terms.length &&
|
|
64
|
+
!walkBlocks(f.blocks).some((b) => b.type === "figureRef" || (b.type === "code" && b.filename)))
|
|
65
|
+
return f;
|
|
66
|
+
const errors = [...f.parsed.errors];
|
|
67
|
+
const terms = [...f.parsed.terms];
|
|
68
|
+
const termFiles = new Set(f.refs.terms);
|
|
69
|
+
const figureFiles = new Set(f.figureFiles);
|
|
70
|
+
const docLinks = [...f.docLinks];
|
|
71
|
+
const resolveItem = (item) => typeof item === "string"
|
|
72
|
+
? item
|
|
73
|
+
: "blocks" in item
|
|
74
|
+
? { blocks: item.blocks.map(resolveBlock) }
|
|
75
|
+
: { ...item, children: item.children.map(resolveItem) };
|
|
76
|
+
const resolveBlock = (b) => {
|
|
77
|
+
const file = b.type === "code"
|
|
78
|
+
? b.filename?.replace(/^(\.\/)+/, "")
|
|
79
|
+
: b.type === "figureRef"
|
|
80
|
+
? f.figureFiles.find((file) => figureName(file) === b.name)
|
|
81
|
+
: undefined;
|
|
82
|
+
let variants = {};
|
|
83
|
+
try {
|
|
84
|
+
if (file && root)
|
|
85
|
+
variants = themeVariants(root, file, themes);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
errors.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
const base = resolveContent(b);
|
|
91
|
+
if (!Object.keys(variants).length)
|
|
92
|
+
return base;
|
|
93
|
+
return {
|
|
94
|
+
type: "themeVariants",
|
|
95
|
+
default: base,
|
|
96
|
+
variants: Object.fromEntries(Object.entries(variants).map(([theme, path]) => {
|
|
97
|
+
if (b.type === "figureRef") {
|
|
98
|
+
figureFiles.add(path);
|
|
99
|
+
return [theme, { ...b, name: figureName(path) }];
|
|
100
|
+
}
|
|
101
|
+
return [theme, resolveContent({ ...b, filename: path })];
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
const resolveContent = (b) => {
|
|
106
|
+
if (b.type === "quote")
|
|
107
|
+
return { ...b, blocks: b.blocks.map(resolveBlock) };
|
|
108
|
+
if (b.type === "list")
|
|
109
|
+
return { ...b, items: b.items.map(resolveItem) };
|
|
110
|
+
if (b.type !== "code" || !b.filename)
|
|
111
|
+
return b;
|
|
112
|
+
try {
|
|
113
|
+
if (!root)
|
|
114
|
+
throw new Error("docs root is required to read text files");
|
|
115
|
+
const source = selectTextfileRange(textfiles?.get(b.filename.replace(/^(\.\/)+/, "")) ??
|
|
116
|
+
readTextfile(root, b.filename, false, previews?.domains), b.range);
|
|
117
|
+
const parsed = b.range === undefined && !isRemoteTextfile(b.filename)
|
|
118
|
+
? parseContentSource(b.lang ?? "", source, b.filename.replace(/^(\.\/)+/, ""))
|
|
119
|
+
: undefined;
|
|
120
|
+
if (parsed) {
|
|
121
|
+
if (b.lang === "callouth") {
|
|
122
|
+
docLinks.push(...collectDocLinks(source));
|
|
123
|
+
const refs = collectRefs(parseMdast(source));
|
|
124
|
+
for (const file of refs.terms)
|
|
125
|
+
termFiles.add(file);
|
|
126
|
+
for (const file of refs.figures)
|
|
127
|
+
figureFiles.add(file);
|
|
128
|
+
}
|
|
129
|
+
errors.push(...parsed.errors.map((error) => `${b.filename}: L${error.line + 1}: ${error.message}`));
|
|
130
|
+
for (const block of parsed.blocks)
|
|
131
|
+
if (block.type === "term")
|
|
132
|
+
terms.push(block.entry);
|
|
133
|
+
if (parsed.blocks[0]) {
|
|
134
|
+
const block = { ...parsed.blocks[0], ...(b.alt ? { alt: b.alt } : {}) };
|
|
135
|
+
return block.type === "table" && b.tableMode
|
|
136
|
+
? { ...block, tableMode: b.tableMode }
|
|
137
|
+
: block;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...b,
|
|
142
|
+
lines: source.split("\n"),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof TextfileUnavailable ||
|
|
147
|
+
error.code === "ENOENT")
|
|
148
|
+
return {
|
|
149
|
+
...b,
|
|
150
|
+
unavailable: error instanceof TextfileUnavailable ? error.reason : "missing",
|
|
151
|
+
};
|
|
152
|
+
errors.push(`${b.filename}${b.range ?? ""}: ${error instanceof Error ? error.message : error}`);
|
|
153
|
+
return b;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const blocks = f.blocks.map(resolveBlock);
|
|
157
|
+
for (const file of termFiles) {
|
|
158
|
+
try {
|
|
159
|
+
if (!root)
|
|
160
|
+
throw new Error("docs root is required to read term files");
|
|
161
|
+
const parsed = parseTermSource(file, textfiles?.get(file) ?? readTextfile(root, file));
|
|
162
|
+
terms.push(...parsed.entries);
|
|
163
|
+
errors.push(...parsed.errors.map((error) => `${file}: L${error.line + 1}: ${error.message}`));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
errors.push(`${file}: ${error instanceof Error ? error.message : error}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
...f,
|
|
171
|
+
blocks,
|
|
172
|
+
docLinks,
|
|
173
|
+
figureFiles: [...figureFiles],
|
|
174
|
+
refs: {
|
|
175
|
+
...f.refs,
|
|
176
|
+
figures: [...figureFiles].map(figureName),
|
|
177
|
+
terms: [
|
|
178
|
+
...new Set([
|
|
179
|
+
...termFiles,
|
|
180
|
+
...terms.filter((term) => isTermFile(term.id)).map((term) => term.id),
|
|
181
|
+
]),
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
parsed: { ...f.parsed, errors, terms },
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
const toDoc = (id, parent, file, f, defaultLang) => ({
|
|
188
|
+
id,
|
|
189
|
+
...(parent == null ? {} : { parent }),
|
|
190
|
+
file,
|
|
191
|
+
// Keep the doc even if its front-matter is broken (so it can be opened and fixed in the
|
|
192
|
+
// viewer). In that case parsed.errors is set, so it won't be distributed.
|
|
193
|
+
...(f.parsed.frontmatter ?? { title: id }),
|
|
194
|
+
lang: f.parsed.frontmatter?.lang ?? defaultLang,
|
|
195
|
+
body: f.parsed.body,
|
|
196
|
+
blocks: f.blocks,
|
|
197
|
+
toc: f.toc,
|
|
198
|
+
refs: f.refs,
|
|
199
|
+
});
|
|
200
|
+
// Validate the dialect and resolution of visual assets, terms and document links.
|
|
201
|
+
const docErrors = (f, { docs, figures }) => {
|
|
202
|
+
const errors = [...f.parsed.errors];
|
|
203
|
+
for (const file of f.figureFiles) {
|
|
204
|
+
if (figures.get(figureName(file))?.file !== file) {
|
|
205
|
+
errors.push(`"${file.replace(/^figures\//, "")}" is not in figures/`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const url of f.docLinks) {
|
|
209
|
+
const ref = parseDocLink(url);
|
|
210
|
+
const target = ref && docs.find((doc) => doc.file === ref.file);
|
|
211
|
+
if (ref && !target)
|
|
212
|
+
errors.push(`Document link "${url}" is unresolved (check the file path)`);
|
|
213
|
+
else if (ref?.fragment &&
|
|
214
|
+
target?.toc &&
|
|
215
|
+
!target.toc.some((heading) => heading.slug === ref.fragment))
|
|
216
|
+
errors.push(`Document link "${url}" refers to a missing heading`);
|
|
217
|
+
}
|
|
218
|
+
return errors;
|
|
219
|
+
};
|
|
220
|
+
import { docFile } from "../core/files.js";
|
|
221
|
+
import { textfilePath } from "./textfile.js";
|
|
222
|
+
export const createCompiler = (root, options = {}) => {
|
|
223
|
+
if (options.locale && !isContentLanguage(options.locale))
|
|
224
|
+
throw new Error("Expected an ISO 639-3 language code");
|
|
225
|
+
const contentDir = join(root, "content");
|
|
226
|
+
const figuresDir = join(root, "figures");
|
|
227
|
+
let docCache = createFileCache();
|
|
228
|
+
let previewKey = "";
|
|
229
|
+
const manifestCache = createFileCache();
|
|
230
|
+
const figuresCache = createSigCache();
|
|
231
|
+
const compile = () => {
|
|
232
|
+
const manifestPath = join(root, MANIFEST_FILE);
|
|
233
|
+
const manifest = existsSync(manifestPath)
|
|
234
|
+
? manifestCache.get(manifestPath, parseManifestText)
|
|
235
|
+
: { tree: [], errors: [`${MANIFEST_FILE} is required`] };
|
|
236
|
+
const defaultLang = options.defaultLang ?? manifest.defaultLang ?? DEFAULT_LANG;
|
|
237
|
+
if (options.locale &&
|
|
238
|
+
manifest.languages &&
|
|
239
|
+
!siteContentLanguages(root, manifest).includes(options.locale))
|
|
240
|
+
throw new Error(`Language ${options.locale} is not declared in dooc.yaml languages`);
|
|
241
|
+
const themes = siteThemes(root, manifest);
|
|
242
|
+
const visualFiles = projectFiles(root)
|
|
243
|
+
.map((file) => file.path)
|
|
244
|
+
.filter((file) => /\.(svg|html|png|webp|gif|mp4|webm|jpe?g)$/.test(file));
|
|
245
|
+
const { figures, errors: figErrors } = figuresCache.get(dirSig(figuresDir) +
|
|
246
|
+
visualFiles.map((file) => `${file}:${fileSig(textfilePath(root, file))}`).join("|"), () => loadFigures(figuresDir, visualFiles));
|
|
247
|
+
const siteErrors = figErrors.filter((error) => error.startsWith("figures/"));
|
|
248
|
+
if (!isLangTag(defaultLang)) {
|
|
249
|
+
siteErrors.push("defaultLang must be a BCP 47 language tag");
|
|
250
|
+
}
|
|
251
|
+
const previews = parsePreviewSettings(options.previews ?? defaultPreviewSettings());
|
|
252
|
+
let title;
|
|
253
|
+
let icon;
|
|
254
|
+
let iconFile;
|
|
255
|
+
const files = [];
|
|
256
|
+
if (!existsSync(contentDir) && !manifest.files) {
|
|
257
|
+
siteErrors.push(`${contentDir}: content/ is missing`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
const { tree, title: manifestTitle, icon: manifestIcon, errors: manifestErrors, } = manifest;
|
|
261
|
+
const nextPreviewKey = JSON.stringify(previews);
|
|
262
|
+
if (nextPreviewKey !== previewKey) {
|
|
263
|
+
docCache = createFileCache();
|
|
264
|
+
previewKey = nextPreviewKey;
|
|
265
|
+
}
|
|
266
|
+
title = manifestTitle;
|
|
267
|
+
iconFile = manifestIcon;
|
|
268
|
+
if (iconFile) {
|
|
269
|
+
try {
|
|
270
|
+
icon = squareSvg(readTextfile(root, iconFile));
|
|
271
|
+
if (!icon)
|
|
272
|
+
throw new Error("expected an SVG with a viewBox or width and height");
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
siteErrors.push(`${MANIFEST_FILE}: icon ${iconFile}: ${error instanceof Error ? error.message : error}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
siteErrors.push(...manifestErrors);
|
|
279
|
+
// Directly under content/: flat <id>.md files only (no folders).
|
|
280
|
+
const onDisk = new Set();
|
|
281
|
+
for (const entry of existsSync(contentDir)
|
|
282
|
+
? readdirSync(contentDir, { withFileTypes: true })
|
|
283
|
+
: []) {
|
|
284
|
+
if (entry.name.startsWith(".")) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const id = entry.isFile() ? docIdOfFile(entry.name) : undefined;
|
|
288
|
+
if (id == null) {
|
|
289
|
+
if (manifest.files)
|
|
290
|
+
continue;
|
|
291
|
+
siteErrors.push(`content/${entry.name}: only <id>.md (lowercase alphanumerics and hyphens) are allowed directly under content/`);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
onDisk.add(id);
|
|
295
|
+
}
|
|
296
|
+
// Manifest pre-order is the display order.
|
|
297
|
+
for (const { id, parent } of flattenTree(tree)) {
|
|
298
|
+
const rel = docFile(id, manifest.files);
|
|
299
|
+
if (rel.startsWith("content/")) {
|
|
300
|
+
const diskId = docIdOfFile(rel.slice("content/".length));
|
|
301
|
+
if (diskId)
|
|
302
|
+
onDisk.delete(diskId);
|
|
303
|
+
}
|
|
304
|
+
let path;
|
|
305
|
+
try {
|
|
306
|
+
path = textfilePath(root, rel);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
siteErrors.push(`${rel}: ${error}`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!existsSync(path)) {
|
|
313
|
+
siteErrors.push(`${rel}: listed in ${MANIFEST_FILE} but the file is missing`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
let parsed = docCache.get(path, (raw) => parseFile(raw, previews, rel));
|
|
317
|
+
let language = defaultLang;
|
|
318
|
+
if (options.locale && options.locale !== contentLanguageCode(defaultLang)) {
|
|
319
|
+
const file = translationFile(rel, options.locale);
|
|
320
|
+
try {
|
|
321
|
+
const translated = textfilePath(root, file);
|
|
322
|
+
if (existsSync(translated)) {
|
|
323
|
+
parsed = docCache.get(translated, (raw) => parseFile(raw, previews, rel));
|
|
324
|
+
language = options.locale;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
siteErrors.push(`${file}: ${error}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
parsed = resolveFiles(parsed, root, undefined, previews, themes);
|
|
332
|
+
files.push({ doc: toDoc(id, parent, rel, parsed, language), parsed });
|
|
333
|
+
}
|
|
334
|
+
for (const id of onDisk) {
|
|
335
|
+
siteErrors.push(`content/${id}.md: not registered in ${MANIFEST_FILE}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const file of new Set(files.flatMap(({ parsed }) => parsed.figureFiles)))
|
|
339
|
+
if (!file.startsWith("figures/"))
|
|
340
|
+
siteErrors.push(...figErrors.filter((error) => error.startsWith(`${file}:`)));
|
|
341
|
+
// Icons are small, so read them every time (no cache).
|
|
342
|
+
const { icons, errors: iconErrors } = loadIcons(root, new Set(files.map((f) => f.doc.id)), iconFile, manifest.files);
|
|
343
|
+
siteErrors.push(...iconErrors);
|
|
344
|
+
for (const { doc } of files) {
|
|
345
|
+
const icon = icons.get(doc.id);
|
|
346
|
+
if (icon != null) {
|
|
347
|
+
doc.icon = icon;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const docs = files.map((f) => f.doc);
|
|
351
|
+
if (docs.length === 0) {
|
|
352
|
+
siteErrors.push(`${contentDir}: no docs`);
|
|
353
|
+
}
|
|
354
|
+
const ctx = {
|
|
355
|
+
docs,
|
|
356
|
+
figures: new Map(figures.map((f) => [f.name, f])),
|
|
357
|
+
defaultLang,
|
|
358
|
+
};
|
|
359
|
+
const sources = new Map();
|
|
360
|
+
const errors = [...siteErrors];
|
|
361
|
+
for (const { doc, parsed } of files) {
|
|
362
|
+
const errs = docErrors(parsed, ctx);
|
|
363
|
+
errors.push(...errs.map((e) => `${doc.file}: ${e}`));
|
|
364
|
+
sources.set(doc.file, {
|
|
365
|
+
terms: parsed.parsed.terms,
|
|
366
|
+
raw: parsed.raw,
|
|
367
|
+
frontmatter: parsed.parsed.frontmatter,
|
|
368
|
+
lines: parsed.lines,
|
|
369
|
+
errors: errs,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
previews,
|
|
374
|
+
...(manifest.languages ? { contentLanguages: manifest.languages } : {}),
|
|
375
|
+
...(manifest.themes ? { themes: manifest.themes } : {}),
|
|
376
|
+
...(title === undefined ? {} : { title }),
|
|
377
|
+
...(icon === undefined ? {} : { icon }),
|
|
378
|
+
docs,
|
|
379
|
+
terms: [...termMap(files.flatMap(({ parsed }) => parsed.parsed.terms)).values()],
|
|
380
|
+
figures,
|
|
381
|
+
sources,
|
|
382
|
+
siteErrors,
|
|
383
|
+
errors,
|
|
384
|
+
defaultLang,
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
return { compile };
|
|
388
|
+
};
|
|
389
|
+
/**
|
|
390
|
+
* Compiles a single unsaved draft (file = `content/<id>.md`) in the site's context.
|
|
391
|
+
* Not cached, since it changes on every keystroke. id / parent are looked up from the ctx.docs
|
|
392
|
+
* entry with the same file ("" for a new doc).
|
|
393
|
+
*/
|
|
394
|
+
export const compileDraft = (ctx, file, raw) => {
|
|
395
|
+
const known = ctx.docs.find((d) => d.file === file);
|
|
396
|
+
const previews = ctx.previews;
|
|
397
|
+
const parsed = resolveFiles(parseFile(raw, previews, file), ctx.root, ctx.textfiles, previews);
|
|
398
|
+
return {
|
|
399
|
+
doc: toDoc(known?.id ?? "", known?.parent, file, parsed, ctx.defaultLang ?? DEFAULT_LANG),
|
|
400
|
+
source: {
|
|
401
|
+
terms: parsed.parsed.terms,
|
|
402
|
+
raw,
|
|
403
|
+
frontmatter: parsed.parsed.frontmatter,
|
|
404
|
+
lines: parsed.lines,
|
|
405
|
+
errors: docErrors(parsed, ctx),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
/** One-shot compile (for the build CLI / tests). */
|
|
410
|
+
export const compileSite = (root, options = {}) => createCompiler(root, options).compile();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type LanguageAdapter } from "../core/language.js";
|
|
2
|
+
export declare const CONFIG_FILE = "docs.config.mjs";
|
|
3
|
+
export interface ResolvedConfig {
|
|
4
|
+
languages: LanguageAdapter[];
|
|
5
|
+
}
|
|
6
|
+
/** Reads docs.config.mjs; defaults when absent. Throws with a readable message when invalid. */
|
|
7
|
+
export declare const loadDocsConfig: (root: string) => Promise<ResolvedConfig>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Loads `<docs root>/docs.config.mjs` (optional): language adapters.
|
|
2
|
+
// The file is a JS module, so loading it runs
|
|
3
|
+
// its code — it belongs to the docs root like a build config. Read once per process; restart
|
|
4
|
+
// after changing it.
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { isLangTag } from "../core/language.js";
|
|
9
|
+
export const CONFIG_FILE = "docs.config.mjs";
|
|
10
|
+
const FUNCTION_KEYS = ["isTermBoundary", "compareTerms", "lint"];
|
|
11
|
+
const checkAdapter = (value, i) => {
|
|
12
|
+
const at = `${CONFIG_FILE}: languages[${i}]`;
|
|
13
|
+
if (typeof value !== "object" || value == null || Array.isArray(value)) {
|
|
14
|
+
throw new Error(`${at} must be an object`);
|
|
15
|
+
}
|
|
16
|
+
const a = value;
|
|
17
|
+
if (typeof a.code !== "string" || !isLangTag(a.code)) {
|
|
18
|
+
throw new Error(`${at}.code must be a BCP 47 language tag`);
|
|
19
|
+
}
|
|
20
|
+
for (const key of FUNCTION_KEYS) {
|
|
21
|
+
if (a[key] !== undefined && typeof a[key] !== "function") {
|
|
22
|
+
throw new Error(`${at}.${key} must be a function`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (a.spellcheckLang !== undefined && typeof a.spellcheckLang !== "string") {
|
|
26
|
+
throw new Error(`${at}.spellcheckLang must be a string`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
/** Reads docs.config.mjs; defaults when absent. Throws with a readable message when invalid. */
|
|
31
|
+
export const loadDocsConfig = async (root) => {
|
|
32
|
+
const file = join(root, CONFIG_FILE);
|
|
33
|
+
if (!existsSync(file)) {
|
|
34
|
+
return {
|
|
35
|
+
languages: [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const mod = (await import(pathToFileURL(file).href));
|
|
39
|
+
const config = mod.default;
|
|
40
|
+
if (typeof config !== "object" || config == null || Array.isArray(config)) {
|
|
41
|
+
throw new Error(`${CONFIG_FILE}: the default export must be an object`);
|
|
42
|
+
}
|
|
43
|
+
if ("defaultLang" in config)
|
|
44
|
+
throw new Error(`${CONFIG_FILE}: move defaultLang to dooc.yaml`);
|
|
45
|
+
const { languages } = config;
|
|
46
|
+
if (languages !== undefined && !Array.isArray(languages)) {
|
|
47
|
+
throw new Error(`${CONFIG_FILE}: languages must be an array`);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
languages: (languages ?? []).map(checkAdapter),
|
|
51
|
+
};
|
|
52
|
+
};
|