@cudoment/cudoc 0.4.0 → 0.6.0
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 +4 -3
- package/dist/document.d.ts +36 -1
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +54 -3
- package/dist/document.js.map +1 -1
- package/dist/internal/core/mdx/imports.d.ts +21 -0
- package/dist/internal/core/mdx/imports.d.ts.map +1 -0
- package/dist/internal/core/mdx/imports.js +80 -0
- package/dist/internal/core/mdx/imports.js.map +1 -0
- package/dist/internal/core/mdx/index.d.ts +1 -0
- package/dist/internal/core/mdx/index.d.ts.map +1 -1
- package/dist/internal/core/mdx/index.js +1 -0
- package/dist/internal/core/mdx/index.js.map +1 -1
- package/dist/internal/core/query/sections.d.ts +6 -0
- package/dist/internal/core/query/sections.d.ts.map +1 -1
- package/dist/internal/core/query/sections.js +8 -2
- package/dist/internal/core/query/sections.js.map +1 -1
- package/dist/internal/transforms/table-column-layout/create-table.d.ts.map +1 -1
- package/dist/internal/transforms/table-column-layout/create-table.js +40 -17
- package/dist/internal/transforms/table-column-layout/create-table.js.map +1 -1
- package/dist/internal/transforms/table-column-layout/index.d.ts +32 -0
- package/dist/internal/transforms/table-column-layout/index.d.ts.map +1 -1
- package/dist/internal/transforms/table-column-layout/index.js +66 -0
- package/dist/internal/transforms/table-column-layout/index.js.map +1 -1
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +9 -1
- package/dist/markdown.js.map +1 -1
- package/dist/node/check.d.ts +12 -6
- package/dist/node/check.d.ts.map +1 -1
- package/dist/node/check.js +460 -79
- package/dist/node/check.js.map +1 -1
- package/dist/node/cli.js +2 -56
- package/dist/node/cli.js.map +1 -1
- package/dist/node/collect.d.ts +41 -0
- package/dist/node/collect.d.ts.map +1 -0
- package/dist/node/collect.js +351 -0
- package/dist/node/collect.js.map +1 -0
- package/dist/node/command.d.ts +23 -0
- package/dist/node/command.d.ts.map +1 -0
- package/dist/node/command.js +116 -0
- package/dist/node/command.js.map +1 -0
- package/dist/node/dataset.d.ts +14 -1
- package/dist/node/dataset.d.ts.map +1 -1
- package/dist/node/dataset.js +44 -5
- package/dist/node/dataset.js.map +1 -1
- package/dist/node/glob.d.ts +21 -0
- package/dist/node/glob.d.ts.map +1 -0
- package/dist/node/glob.js +61 -0
- package/dist/node/glob.js.map +1 -0
- package/dist/node/library.d.ts +76 -6
- package/dist/node/library.d.ts.map +1 -1
- package/dist/node/library.js +63 -137
- package/dist/node/library.js.map +1 -1
- package/dist/node/load-ast.d.ts.map +1 -1
- package/dist/node/load-ast.js +4 -1
- package/dist/node/load-ast.js.map +1 -1
- package/dist/node/local-target.d.ts +39 -8
- package/dist/node/local-target.d.ts.map +1 -1
- package/dist/node/local-target.js +94 -13
- package/dist/node/local-target.js.map +1 -1
- package/dist/node/prepare-embeds.d.ts +52 -3
- package/dist/node/prepare-embeds.d.ts.map +1 -1
- package/dist/node/prepare-embeds.js +134 -8
- package/dist/node/prepare-embeds.js.map +1 -1
- package/dist/node/references.d.ts +39 -0
- package/dist/node/references.d.ts.map +1 -0
- package/dist/node/references.js +101 -0
- package/dist/node/references.js.map +1 -0
- package/dist/node/replace.d.ts +65 -0
- package/dist/node/replace.d.ts.map +1 -0
- package/dist/node/replace.js +285 -0
- package/dist/node/replace.js.map +1 -0
- package/dist/node/resolve-embed.d.ts +79 -1
- package/dist/node/resolve-embed.d.ts.map +1 -1
- package/dist/node/resolve-embed.js +480 -143
- package/dist/node/resolve-embed.js.map +1 -1
- package/dist/node/roots.d.ts +78 -0
- package/dist/node/roots.d.ts.map +1 -0
- package/dist/node/roots.js +170 -0
- package/dist/node/roots.js.map +1 -0
- package/dist/node/storage.d.ts +21 -3
- package/dist/node/storage.d.ts.map +1 -1
- package/dist/node/storage.js +242 -20
- package/dist/node/storage.js.map +1 -1
- package/dist/node/watch.d.ts +86 -0
- package/dist/node/watch.d.ts.map +1 -0
- package/dist/node/watch.js +174 -0
- package/dist/node/watch.js.map +1 -0
- package/dist/paged.d.ts +34 -0
- package/dist/paged.d.ts.map +1 -0
- package/dist/paged.js +44 -0
- package/dist/paged.js.map +1 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +14 -0
- package/dist/render.js.map +1 -1
- package/dist/sections.d.ts.map +1 -1
- package/dist/sections.js +7 -3
- package/dist/sections.js.map +1 -1
- package/package.json +29 -16
- package/styles.css +5 -2
|
@@ -2,11 +2,68 @@ import path from "node:path";
|
|
|
2
2
|
import { fromHtml } from "hast-util-from-html";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { collectSections } from "../sections.js";
|
|
5
|
-
import { nodeText, visibleHeadingText } from "../document.js";
|
|
6
|
-
import {
|
|
5
|
+
import { idToken, nodeText, visibleHeadingText, } from "../document.js";
|
|
6
|
+
import { findHeadingByAnchorId, findParentHeading, getHeadingAnchorId, } from "../internal/core/query/sections.js";
|
|
7
|
+
import { sourceFileOf } from "./roots.js";
|
|
8
|
+
import { parseSrcSet } from "./local-target.js";
|
|
9
|
+
import { transformedSection } from "./replace.js";
|
|
10
|
+
import { EXTERNAL_URL, decodeComponent, declaredIds, documentIndex, idsInNode, } from "./references.js";
|
|
7
11
|
const SPEC_KEYS = ["sources", "select", "render", "replace"];
|
|
8
12
|
const SELECTION_KEYS = ["anchors", "titles", "depth", "includeChildren"];
|
|
9
13
|
const REPLACEMENT_KEYS = ["find", "replace", "regex", "flags"];
|
|
14
|
+
const COLUMN_KEYS = ["header", "value", "link", "minWidth"];
|
|
15
|
+
const CELL_KEYS = ["table", "row", "column", "skipTablesWithHeaders"];
|
|
16
|
+
const SHORTHAND_COLUMNS = ["title", "link", "summary"];
|
|
17
|
+
const CELL_LINKS = ["section", "parent", "document"];
|
|
18
|
+
/** A CSS length a header cell can carry: a number and a unit, nothing else. */
|
|
19
|
+
const CSS_LENGTH = /^\d+(?:\.\d+)?(?:px|rem|em|ch|%)$/;
|
|
20
|
+
const validateColumn = (column, at) => {
|
|
21
|
+
if (typeof column === "string") {
|
|
22
|
+
if (!SHORTHAND_COLUMNS.includes(column))
|
|
23
|
+
throw new Error(`cudoc: ${at}: unknown column "${column}". Known columns: ${SHORTHAND_COLUMNS.join(", ")}, or a mapping with value`);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!column || typeof column !== "object" || Array.isArray(column))
|
|
27
|
+
throw new Error(`cudoc: ${at} must be a column name or a mapping`);
|
|
28
|
+
const entry = column;
|
|
29
|
+
for (const key of Object.keys(entry))
|
|
30
|
+
if (!COLUMN_KEYS.includes(key))
|
|
31
|
+
throw new Error(`cudoc: ${at}: unknown key "${key}". Known keys: ${COLUMN_KEYS.join(", ")}`);
|
|
32
|
+
if (entry.header !== undefined && typeof entry.header !== "string")
|
|
33
|
+
throw new Error(`cudoc: ${at}.header must be a string`);
|
|
34
|
+
if (entry.link !== undefined && !CELL_LINKS.includes(entry.link))
|
|
35
|
+
throw new Error(`cudoc: ${at}.link must be one of ${CELL_LINKS.join(", ")}`);
|
|
36
|
+
if (entry.minWidth !== undefined &&
|
|
37
|
+
(typeof entry.minWidth !== "string" || !CSS_LENGTH.test(entry.minWidth)))
|
|
38
|
+
throw new Error(`cudoc: ${at}.minWidth must be a CSS length such as 120px or 8rem`);
|
|
39
|
+
const value = entry.value;
|
|
40
|
+
if (typeof value === "string") {
|
|
41
|
+
if (!["title", "summary", "parent"].includes(value))
|
|
42
|
+
throw new Error(`cudoc: ${at}.value: unknown value "${value}". Known values: title, summary, parent, a cell coordinate mapping, or { extractor }`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
46
|
+
throw new Error(`cudoc: ${at}.value is required`);
|
|
47
|
+
const cell = value;
|
|
48
|
+
if ("extractor" in cell) {
|
|
49
|
+
if (Object.keys(cell).length !== 1 || typeof cell.extractor !== "string")
|
|
50
|
+
throw new Error(`cudoc: ${at}.value.extractor must be a name and nothing else`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(cell))
|
|
54
|
+
if (!CELL_KEYS.includes(key))
|
|
55
|
+
throw new Error(`cudoc: ${at}.value: unknown key "${key}". Known keys: ${CELL_KEYS.join(", ")}, or extractor`);
|
|
56
|
+
for (const key of ["row", "column"])
|
|
57
|
+
if (!Number.isInteger(cell[key]) || cell[key] < 0)
|
|
58
|
+
throw new Error(`cudoc: ${at}.value.${key} must be a non-negative integer`);
|
|
59
|
+
if (cell.table !== undefined &&
|
|
60
|
+
(!Number.isInteger(cell.table) || cell.table < 0))
|
|
61
|
+
throw new Error(`cudoc: ${at}.value.table must be a non-negative integer`);
|
|
62
|
+
if (cell.skipTablesWithHeaders !== undefined &&
|
|
63
|
+
(!Array.isArray(cell.skipTablesWithHeaders) ||
|
|
64
|
+
cell.skipTablesWithHeaders.some((h) => typeof h !== "string")))
|
|
65
|
+
throw new Error(`cudoc: ${at}.value.skipTablesWithHeaders must be strings`);
|
|
66
|
+
};
|
|
10
67
|
export function parseEmbedSpec(value) {
|
|
11
68
|
const spec = parseYaml(value, { maxAliasCount: 100 });
|
|
12
69
|
if (!spec || typeof spec !== "object" || Array.isArray(spec))
|
|
@@ -18,14 +75,21 @@ export function parseEmbedSpec(value) {
|
|
|
18
75
|
!spec.sources.length ||
|
|
19
76
|
spec.sources.some((s) => typeof s !== "string" || !s))
|
|
20
77
|
throw new Error("cudoc: embed sources must be a non-empty string array");
|
|
21
|
-
if (spec.render &&
|
|
22
|
-
spec.render
|
|
23
|
-
|
|
24
|
-
(spec.render
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
78
|
+
if (spec.render !== undefined && spec.render !== "section") {
|
|
79
|
+
if (!spec.render ||
|
|
80
|
+
typeof spec.render !== "object" ||
|
|
81
|
+
Array.isArray(spec.render) ||
|
|
82
|
+
spec.render.type !== "table")
|
|
83
|
+
throw new Error('cudoc: render must be "section" or a mapping with type: table');
|
|
84
|
+
for (const key of Object.keys(spec.render))
|
|
85
|
+
if (!["type", "columns"].includes(key))
|
|
86
|
+
throw new Error(`cudoc: render: unknown key "${key}". Known keys: type, columns`);
|
|
87
|
+
if (spec.render.columns !== undefined) {
|
|
88
|
+
if (!Array.isArray(spec.render.columns) || !spec.render.columns.length)
|
|
89
|
+
throw new Error("cudoc: render.columns must be a non-empty array");
|
|
90
|
+
spec.render.columns.forEach((column, index) => validateColumn(column, `render.columns[${index}]`));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
29
93
|
if (spec.select) {
|
|
30
94
|
if (typeof spec.select !== "object" || Array.isArray(spec.select))
|
|
31
95
|
throw new Error("cudoc: select must be a mapping");
|
|
@@ -104,7 +168,14 @@ export function parseEmbedBlock(value, documentId, number) {
|
|
|
104
168
|
export function resolveDocumentReference(library, reference, from) {
|
|
105
169
|
const hash = reference.indexOf("#");
|
|
106
170
|
const pathname = hash < 0 ? reference : reference.slice(0, hash);
|
|
107
|
-
|
|
171
|
+
let anchor;
|
|
172
|
+
try {
|
|
173
|
+
anchor =
|
|
174
|
+
hash < 0 ? undefined : decodeURIComponent(reference.slice(hash + 1));
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new Error(`cudoc: embed source has a malformed percent-escape: ${reference}`);
|
|
178
|
+
}
|
|
108
179
|
if (/^[a-z][\w+.-]*:/i.test(pathname) || pathname.includes("\\"))
|
|
109
180
|
throw new Error(`cudoc: embed source must be a local document: ${reference}`);
|
|
110
181
|
const id = (pathname
|
|
@@ -114,65 +185,11 @@ export function resolveDocumentReference(library, reference, from) {
|
|
|
114
185
|
: from).replace(/\.mdx?$/i, "");
|
|
115
186
|
if (id === ".." || id.startsWith("../"))
|
|
116
187
|
throw new Error(`cudoc: embed source escapes root: ${reference}`);
|
|
117
|
-
const document = library.
|
|
188
|
+
const document = documentIndex(library).byId.get(id);
|
|
118
189
|
if (!document)
|
|
119
190
|
throw new Error(`cudoc: missing document ${reference} referenced from ${from}`);
|
|
120
191
|
return { document, anchor };
|
|
121
192
|
}
|
|
122
|
-
function replaceSource(source, rules, documentId) {
|
|
123
|
-
return rules.reduce((value, rule, index) => {
|
|
124
|
-
try {
|
|
125
|
-
return rule.regex
|
|
126
|
-
? value.replace(new RegExp(rule.find, rule.flags ?? "g"), rule.replace)
|
|
127
|
-
: value.split(rule.find).join(rule.replace);
|
|
128
|
-
}
|
|
129
|
-
catch (cause) {
|
|
130
|
-
throw new Error(`cudoc: invalid replacement ${index + 1} in ${documentId}`, { cause });
|
|
131
|
-
}
|
|
132
|
-
}, source);
|
|
133
|
-
}
|
|
134
|
-
function transformedSection(document, anchor, tree, rules, library, includeChildren = true) {
|
|
135
|
-
if (!rules.length)
|
|
136
|
-
return structuredClone(tree);
|
|
137
|
-
if (!document.source)
|
|
138
|
-
throw new Error(`cudoc: rebuild ${document.id} with source snapshots before replacing Markdown`);
|
|
139
|
-
const range = anchor ? document.source.sections[anchor] : undefined;
|
|
140
|
-
if (anchor && !range)
|
|
141
|
-
throw new Error(`cudoc: missing source range for ${document.id}#${anchor}; rebuild documents`);
|
|
142
|
-
const end = range
|
|
143
|
-
? includeChildren
|
|
144
|
-
? range.end
|
|
145
|
-
: (range.ownEnd ?? range.end)
|
|
146
|
-
: undefined;
|
|
147
|
-
const original = range
|
|
148
|
-
? document.source.text.slice(range.start, end)
|
|
149
|
-
: document.source.text;
|
|
150
|
-
const dependencies = range?.dependencies
|
|
151
|
-
.filter(([start, stop]) => start < range.start || stop > end)
|
|
152
|
-
.map(([start, end]) => document.source.text.slice(start, end))
|
|
153
|
-
.join("\n\n");
|
|
154
|
-
const source = replaceSource(original, rules, document.id) +
|
|
155
|
-
(dependencies ? `\n\n${dependencies}` : "");
|
|
156
|
-
const options = { ...library.options, format: document.source.format };
|
|
157
|
-
if (!library.compiler &&
|
|
158
|
-
options.host &&
|
|
159
|
-
!["markdown", "next", "html"].includes(options.host))
|
|
160
|
-
throw new Error(`cudoc: replacing ${options.host} Markdown requires the original host compiler`);
|
|
161
|
-
try {
|
|
162
|
-
return library.compiler
|
|
163
|
-
? library.compiler(source, {
|
|
164
|
-
id: document.id,
|
|
165
|
-
filePath: library.sourceRoot
|
|
166
|
-
? path.resolve(library.sourceRoot, document.sourcePath)
|
|
167
|
-
: document.sourcePath,
|
|
168
|
-
options,
|
|
169
|
-
}).tree
|
|
170
|
-
: compileDocument(source, options).tree;
|
|
171
|
-
}
|
|
172
|
-
catch (cause) {
|
|
173
|
-
throw new Error(`cudoc: replaced Markdown could not compile in ${document.id} at source offset ${range?.start ?? 0}`, { cause });
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
193
|
const visitNodes = (node, fn) => {
|
|
177
194
|
fn(node);
|
|
178
195
|
node.children?.forEach((n) => visitNodes(n, fn));
|
|
@@ -186,56 +203,227 @@ const htmlAttribute = (value, quote) => {
|
|
|
186
203
|
? String(element.properties.dataValue ?? value)
|
|
187
204
|
: value;
|
|
188
205
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
206
|
+
/**
|
|
207
|
+
* A relative module an attribute's expression requires first thing, after any
|
|
208
|
+
* webpack loaders: Docusaurus writes a Markdown image, and a link to a local
|
|
209
|
+
* file, as `require("<loaders>!./<path from the page's directory>").default`.
|
|
210
|
+
* Only an expression that starts with the call is read, so text that merely
|
|
211
|
+
* looks like one inside an authored string is left alone. The string runs to
|
|
212
|
+
* its own closing quote, so the other quote may be in a name.
|
|
213
|
+
*/
|
|
214
|
+
const REQUIRED_MODULE = /^(\s*require\(\s*(["'])(?:(?:(?!\2)[^\\\n]|\\[\s\S])*!)?)(\.{1,2}\/(?:(?!\2)[^\\\n]|\\[\s\S])*)(\2\s*\))/;
|
|
215
|
+
/**
|
|
216
|
+
* The directory a document's file sits in, which is what a relative module
|
|
217
|
+
* path starts from; the library path's own directory when the library was
|
|
218
|
+
* loaded without its roots, which is the file's only where every root's base
|
|
219
|
+
* mirrors its directory.
|
|
220
|
+
*/
|
|
221
|
+
const fileDirectory = (library, document) => path.dirname((library.roots?.length
|
|
222
|
+
? sourceFileOf(library.roots, document.sourcePath)
|
|
223
|
+
: undefined) ?? path.join("/", document.sourcePath));
|
|
224
|
+
/**
|
|
225
|
+
* `to` from `from`, both relative library paths that may climb above the
|
|
226
|
+
* root, worked out on the paths alone: resolved against the working
|
|
227
|
+
* directory, a climb above it would be clamped again.
|
|
228
|
+
*/
|
|
229
|
+
const climbingRelative = (from, to) => {
|
|
230
|
+
const depth = [...from.split("/"), ...to.split("/")].filter((part) => part === "..")
|
|
231
|
+
.length + 1;
|
|
232
|
+
const base = `/${Array.from({ length: depth }, (_, i) => `_${i}`).join("/")}`;
|
|
233
|
+
return path.posix.relative(path.posix.join(base, from), path.posix.join(base, to));
|
|
234
|
+
};
|
|
235
|
+
/** What a JavaScript string literal holds between its quotes, escapes read. */
|
|
236
|
+
const stringValue = (written) => written.replace(/\\(?:u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|(\r\n|[\s\S]))/g, (_match, braced, unicode, hex, other = "") => {
|
|
237
|
+
const code = braced ?? unicode ?? hex;
|
|
238
|
+
if (code) {
|
|
239
|
+
const point = parseInt(code, 16);
|
|
240
|
+
// Past the last code point the escape is not JavaScript at all.
|
|
241
|
+
return point > 0x10ffff ? _match : String.fromCodePoint(point);
|
|
242
|
+
}
|
|
243
|
+
// A backslash before a line break continues the string on the next line.
|
|
244
|
+
if (/^(?:\r\n|[\n\r\u2028\u2029])$/.test(other))
|
|
245
|
+
return "";
|
|
246
|
+
const named = {
|
|
247
|
+
n: "\n",
|
|
248
|
+
r: "\r",
|
|
249
|
+
t: "\t",
|
|
250
|
+
b: "\b",
|
|
251
|
+
f: "\f",
|
|
252
|
+
v: "\v",
|
|
253
|
+
"0": "\0",
|
|
254
|
+
};
|
|
255
|
+
return named[other] ?? other;
|
|
256
|
+
});
|
|
257
|
+
/** `value` written between `quote`s as a JavaScript string literal. */
|
|
258
|
+
const stringLiteral = (value, quote) => value.replace(/[\\\n\r\u2028\u2029"']/g, (character) => {
|
|
259
|
+
if (character === "\\")
|
|
260
|
+
return "\\\\";
|
|
261
|
+
if (character === "\n")
|
|
262
|
+
return "\\n";
|
|
263
|
+
if (character === "\r")
|
|
264
|
+
return "\\r";
|
|
265
|
+
if (character === "\u2028")
|
|
266
|
+
return "\\u2028";
|
|
267
|
+
if (character === "\u2029")
|
|
268
|
+
return "\\u2029";
|
|
269
|
+
return character === quote ? `\\${character}` : character;
|
|
270
|
+
});
|
|
271
|
+
/**
|
|
272
|
+
* Re-expresses the modules a JSX element's attributes require from the
|
|
273
|
+
* directory of the document the copy lands in: a page in another directory
|
|
274
|
+
* would resolve the path from its own. The path is read as the string it is
|
|
275
|
+
* and written back as one, so a directory named with a quote or a backslash
|
|
276
|
+
* still gives an expression that parses.
|
|
277
|
+
*/
|
|
278
|
+
const moveRequiredModules = (node, from, to) => {
|
|
279
|
+
if (!Array.isArray(node.attributes))
|
|
280
|
+
return;
|
|
281
|
+
for (const attribute of node.attributes) {
|
|
282
|
+
const value = attribute.value;
|
|
283
|
+
if (attribute.type !== "mdxJsxAttribute" ||
|
|
284
|
+
typeof value !== "object" ||
|
|
285
|
+
value?.type !== "mdxJsxAttributeValueExpression" ||
|
|
286
|
+
typeof value.value !== "string")
|
|
287
|
+
continue;
|
|
288
|
+
value.value = value.value.replace(REQUIRED_MODULE, (match, head, quote, module, tail) => {
|
|
289
|
+
// A query the host appended stays as written: it is not part of
|
|
290
|
+
// the path, and joining it would normalize its slashes too.
|
|
291
|
+
const [, file = "", query = ""] = /^([^?]*)(.*)$/s.exec(stringValue(module));
|
|
292
|
+
const relative = path.relative(to, path.join(from, file));
|
|
293
|
+
// Another drive has no relative path; the copy keeps the original.
|
|
294
|
+
if (path.isAbsolute(relative))
|
|
295
|
+
return match;
|
|
296
|
+
const moved = relative.split(path.sep).join("/");
|
|
297
|
+
return `${head}${stringLiteral(`./${moved}${query}`, quote)}${tail}`;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
/** The raw HTML attributes that name a path: a link and every resource. */
|
|
302
|
+
const REBASED_ATTRIBUTES = new Set([
|
|
303
|
+
"href",
|
|
304
|
+
"src",
|
|
305
|
+
"poster",
|
|
306
|
+
"data",
|
|
307
|
+
"xlink:href",
|
|
308
|
+
]);
|
|
309
|
+
/**
|
|
310
|
+
* One attribute of a tag, read in order after the tag name as HTML reads it:
|
|
311
|
+
* what separates it from the one before, white space or a stray `/` and
|
|
312
|
+
* nothing at all after a quoted value, its name, and a value quoted either
|
|
313
|
+
* way, which may span lines, or bare, which runs to white space or `>`.
|
|
314
|
+
*/
|
|
315
|
+
const ATTRIBUTE = /([\s/]*)([^\s"'>/=]+)(?:(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/y;
|
|
316
|
+
/**
|
|
317
|
+
* The tag with the values `rewrite` returns in place of the ones it has, the
|
|
318
|
+
* attributes it returns nothing for left exactly as written. The attributes
|
|
319
|
+
* are walked in order, so text inside one value is never read as another.
|
|
320
|
+
*/
|
|
321
|
+
const rewriteAttributes = (tag, rewrite) => {
|
|
322
|
+
const opening = /^<\/?[^\s/>]+/.exec(tag);
|
|
323
|
+
if (!opening)
|
|
324
|
+
return tag;
|
|
325
|
+
let result = opening[0];
|
|
326
|
+
let index = opening[0].length;
|
|
327
|
+
for (;;) {
|
|
328
|
+
ATTRIBUTE.lastIndex = index;
|
|
329
|
+
const match = ATTRIBUTE.exec(tag);
|
|
330
|
+
if (!match)
|
|
331
|
+
break;
|
|
332
|
+
index = ATTRIBUTE.lastIndex;
|
|
333
|
+
const [whole, space, name, equals, double, single, bare] = match;
|
|
334
|
+
const written = double ?? single ?? bare;
|
|
335
|
+
const delimiter = single === undefined ? '"' : "'";
|
|
336
|
+
const replacement = written === undefined
|
|
337
|
+
? undefined
|
|
338
|
+
: rewrite(name.toLowerCase(), htmlAttribute(written, delimiter));
|
|
339
|
+
result +=
|
|
340
|
+
replacement === undefined
|
|
341
|
+
? whole
|
|
342
|
+
: `${space}${name}${equals}${delimiter}${replacement
|
|
343
|
+
.replace(/&/g, "&")
|
|
344
|
+
.replace(new RegExp(delimiter, "g"), delimiter === '"' ? """ : "'")}${delimiter}`;
|
|
203
345
|
}
|
|
204
|
-
return
|
|
346
|
+
return result + tag.slice(index);
|
|
205
347
|
};
|
|
206
|
-
function rebase(tree, document, library, prefix) {
|
|
348
|
+
function rebase(tree, document, library, prefix, destination, moved) {
|
|
207
349
|
const ids = new Set();
|
|
208
350
|
visitNodes(tree, (node) => {
|
|
209
351
|
idsInNode(node).forEach((id) => ids.add(id));
|
|
210
352
|
});
|
|
353
|
+
// Worked out once, and only when the copy has an element to move.
|
|
354
|
+
let directories;
|
|
211
355
|
const sourceUrl = (url) => {
|
|
212
|
-
if (
|
|
356
|
+
if (EXTERNAL_URL.test(url))
|
|
213
357
|
return url;
|
|
214
358
|
if (url.startsWith("#")) {
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
id = decodeURIComponent(id);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
/* Preserve malformed URL. */
|
|
221
|
-
}
|
|
359
|
+
const id = decodeComponent(url.slice(1));
|
|
222
360
|
return ids.has(id) ? `#${prefix}${id}` : `${document.route}${url}`;
|
|
223
361
|
}
|
|
224
362
|
const match = url.match(/^([^?#]*)(.*)$/);
|
|
225
|
-
|
|
363
|
+
// No path means the page the link sits on, which in a copy is the
|
|
364
|
+
// document it was copied from, not the directory that document is in.
|
|
365
|
+
if (!match[1])
|
|
366
|
+
return `${document.route}${match[2]}`;
|
|
367
|
+
const joined = match[1].startsWith("/")
|
|
226
368
|
? match[1]
|
|
227
|
-
:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
369
|
+
: path.posix.join(path.posix.dirname(document.sourcePath), match[1]);
|
|
370
|
+
// A relative path that climbs out of the collection names no library
|
|
371
|
+
// path, and clamped at the root it would name another file. It is spelled
|
|
372
|
+
// from the page the copy lands on instead, which reaches the same place:
|
|
373
|
+
// from the files' own directories when the library has its roots, and
|
|
374
|
+
// from the library paths otherwise.
|
|
375
|
+
if (joined === ".." || joined.startsWith("../")) {
|
|
376
|
+
if (!destination)
|
|
377
|
+
return url;
|
|
378
|
+
// A directory keeps its trailing slash, which names its index.
|
|
379
|
+
const slash = /(?:^|\/)\.{0,2}$/.test(match[1]) ? "/" : "";
|
|
380
|
+
if (!library.roots?.length)
|
|
381
|
+
return `${climbingRelative(path.posix.dirname(destination.sourcePath), joined) || "."}${slash}${match[2]}`;
|
|
382
|
+
directories ??= {
|
|
383
|
+
from: fileDirectory(library, document),
|
|
384
|
+
to: fileDirectory(library, destination),
|
|
385
|
+
};
|
|
386
|
+
const relative = path.relative(directories.to, path.join(directories.from, decodeComponent(match[1])));
|
|
387
|
+
// Written back as a URL path: only what would end or change one is
|
|
388
|
+
// escaped, as the path was decoded to reach the file.
|
|
389
|
+
return `${relative
|
|
390
|
+
.split(path.sep)
|
|
391
|
+
.map((part) => part.replace(/[%\s#?]/g, encodeURIComponent))
|
|
392
|
+
.join("/") || "."}${slash}${match[2]}`;
|
|
393
|
+
}
|
|
394
|
+
let absolute = path.posix.normalize(joined.startsWith("/") ? joined : `/${joined}`);
|
|
395
|
+
// `.` and `..` name a directory, as a browser reads them.
|
|
396
|
+
if (/(?:^|\/)\.\.?$/.test(match[1]) && !absolute.endsWith("/"))
|
|
397
|
+
absolute += "/";
|
|
398
|
+
const target = documentIndex(library).byPath.get(absolute);
|
|
231
399
|
return `${target?.route ?? absolute}${match[2]}`;
|
|
232
400
|
};
|
|
401
|
+
// Every candidate of a responsive image is a path of its own; the width or
|
|
402
|
+
// density after it stays as written.
|
|
403
|
+
const sourceSrcSet = (value) => parseSrcSet(value)
|
|
404
|
+
.map(({ url, descriptor }) => [sourceUrl(url), descriptor].filter(Boolean).join(" "))
|
|
405
|
+
.join(", ");
|
|
233
406
|
visitNodes(tree, (node) => {
|
|
234
407
|
const id = node.data?.hProperties?.id;
|
|
235
408
|
if (typeof id === "string")
|
|
236
409
|
node.data.hProperties.id = `${prefix}${id}`;
|
|
237
410
|
if (typeof node.url === "string")
|
|
238
411
|
node.url = sourceUrl(node.url);
|
|
412
|
+
// An image a host made a component of is still an image of the source.
|
|
413
|
+
const image = node.data?.cudocImage;
|
|
414
|
+
if (image && typeof image.url === "string")
|
|
415
|
+
node.data.cudocImage = { ...image, url: sourceUrl(image.url) };
|
|
416
|
+
// Once per element, however deeply the copy was nested: an inner embed
|
|
417
|
+
// already moved it from its own source to `destination`.
|
|
418
|
+
if (destination && Array.isArray(node.attributes) && !moved.has(node)) {
|
|
419
|
+
moved.add(node);
|
|
420
|
+
directories ??= {
|
|
421
|
+
from: fileDirectory(library, document),
|
|
422
|
+
to: fileDirectory(library, destination),
|
|
423
|
+
};
|
|
424
|
+
if (directories.from !== directories.to)
|
|
425
|
+
moveRequiredModules(node, directories.from, directories.to);
|
|
426
|
+
}
|
|
239
427
|
if ([
|
|
240
428
|
"linkReference",
|
|
241
429
|
"imageReference",
|
|
@@ -248,36 +436,207 @@ function rebase(tree, document, library, prefix) {
|
|
|
248
436
|
if (node.type === "html" && node.value)
|
|
249
437
|
node.value = node.value.replace(/<!--[\s\S]*?-->|<(?:[^"'<>]|"[^"]*"|'[^']*')*>/g, (tag) => tag.startsWith("<!--")
|
|
250
438
|
? tag
|
|
251
|
-
: tag
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}));
|
|
439
|
+
: rewriteAttributes(tag, (name, value) => name === "id"
|
|
440
|
+
? `${prefix}${value}`
|
|
441
|
+
: name === "srcset"
|
|
442
|
+
? sourceSrcSet(value)
|
|
443
|
+
: REBASED_ATTRIBUTES.has(name)
|
|
444
|
+
? sourceUrl(value)
|
|
445
|
+
: undefined));
|
|
259
446
|
const attrs = node.data?.hProperties;
|
|
260
|
-
for (const key of ["href", "src"])
|
|
447
|
+
for (const key of ["href", "src", "poster", "data", "xLinkHref"])
|
|
261
448
|
if (typeof attrs?.[key] === "string")
|
|
262
449
|
attrs[key] = sourceUrl(attrs[key]);
|
|
450
|
+
for (const key of ["srcSet", "srcset"])
|
|
451
|
+
if (typeof attrs?.[key] === "string")
|
|
452
|
+
attrs[key] = sourceSrcSet(attrs[key]);
|
|
263
453
|
});
|
|
264
454
|
}
|
|
455
|
+
/** The default columns of a summary table, as they have always been. */
|
|
456
|
+
export const DEFAULT_TABLE_COLUMNS = ["title", "link", "summary"];
|
|
457
|
+
/** A shorthand column written out as the mapping it stands for. */
|
|
458
|
+
const columnSpec = (column) => {
|
|
459
|
+
if (column === "title")
|
|
460
|
+
return { header: "title", value: "title" };
|
|
461
|
+
if (column === "link")
|
|
462
|
+
return { header: "link", value: "title", link: "section" };
|
|
463
|
+
if (column === "summary")
|
|
464
|
+
return { header: "summary", value: "summary" };
|
|
465
|
+
return {
|
|
466
|
+
header: column.header ?? (typeof column.value === "string" ? column.value : ""),
|
|
467
|
+
value: column.value,
|
|
468
|
+
link: column.link,
|
|
469
|
+
minWidth: column.minWidth,
|
|
470
|
+
};
|
|
471
|
+
};
|
|
472
|
+
/**
|
|
473
|
+
* One row for a selected section: what it is called, where it lives and which
|
|
474
|
+
* heading introduces the part of the document it sits in.
|
|
475
|
+
*/
|
|
476
|
+
export function buildEmbedRow(document, anchorId, tree) {
|
|
477
|
+
const heading = tree.children.find((n) => n.type === "heading");
|
|
478
|
+
const title = heading
|
|
479
|
+
? visibleHeadingText(heading)
|
|
480
|
+
: String(document.frontmatter.title ?? document.id);
|
|
481
|
+
let parent;
|
|
482
|
+
if (anchorId) {
|
|
483
|
+
const location = findHeadingByAnchorId(document.tree, anchorId);
|
|
484
|
+
const above = location &&
|
|
485
|
+
findParentHeading(location.parent, location.index, location.heading.depth);
|
|
486
|
+
if (above)
|
|
487
|
+
parent = {
|
|
488
|
+
title: visibleHeadingText(above),
|
|
489
|
+
anchorId: getHeadingAnchorId(above),
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
document,
|
|
494
|
+
section: { anchorId, title, tree },
|
|
495
|
+
parent,
|
|
496
|
+
url: `${document.route}${anchorId ? `#${anchorId}` : ""}`,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/** Tables inside a section, in document order, minus those a coordinate skips. */
|
|
500
|
+
const sectionTables = (tree, skipHeaders) => {
|
|
501
|
+
const tables = [];
|
|
502
|
+
visitNodes(tree, (node) => {
|
|
503
|
+
if (node.type === "table")
|
|
504
|
+
tables.push(node);
|
|
505
|
+
});
|
|
506
|
+
if (!skipHeaders?.length)
|
|
507
|
+
return tables;
|
|
508
|
+
const skip = new Set(skipHeaders.map((header) => header.trim()));
|
|
509
|
+
return tables.filter((table) => !(table.children?.[0]?.children ?? []).some((cell) => skip.has(nodeText(cell).trim())));
|
|
510
|
+
};
|
|
511
|
+
/**
|
|
512
|
+
* The text and link one column shows for one row, and why it is empty when it
|
|
513
|
+
* is. The resolver renders the cell either way; the checker reports the
|
|
514
|
+
* problem, which is worded as what the column asked for and what the section
|
|
515
|
+
* has, so an author can tell a wrong coordinate from a missing table.
|
|
516
|
+
*/
|
|
517
|
+
export function extractCell(library, column, row, context) {
|
|
518
|
+
const spec = columnSpec(column);
|
|
519
|
+
const where = `${row.document.id}${row.section.anchorId ? `#${row.section.anchorId}` : ""}`;
|
|
520
|
+
const linkTo = () => {
|
|
521
|
+
if (spec.link === "section")
|
|
522
|
+
return row.url;
|
|
523
|
+
if (spec.link === "document")
|
|
524
|
+
return row.document.route;
|
|
525
|
+
if (spec.link === "parent")
|
|
526
|
+
return row.parent
|
|
527
|
+
? `${row.document.route}${row.parent.anchorId ? `#${row.parent.anchorId}` : ""}`
|
|
528
|
+
: undefined;
|
|
529
|
+
return undefined;
|
|
530
|
+
};
|
|
531
|
+
const finish = (text, problem, url) => ({
|
|
532
|
+
cell: {
|
|
533
|
+
text: text ?? "",
|
|
534
|
+
...((url ?? linkTo()) ? { url: url ?? linkTo() } : {}),
|
|
535
|
+
},
|
|
536
|
+
...(problem ? { problem } : {}),
|
|
537
|
+
});
|
|
538
|
+
const value = spec.value;
|
|
539
|
+
if (value === "title")
|
|
540
|
+
return finish(row.section.title);
|
|
541
|
+
if (value === "summary") {
|
|
542
|
+
const paragraph = row.section.tree.children.find((n) => n.type === "paragraph");
|
|
543
|
+
return paragraph
|
|
544
|
+
? finish(nodeText(paragraph).trim())
|
|
545
|
+
: finish(undefined, `expected a paragraph in ${where}, found none`);
|
|
546
|
+
}
|
|
547
|
+
if (value === "parent")
|
|
548
|
+
return row.parent
|
|
549
|
+
? finish(row.parent.title)
|
|
550
|
+
: finish(undefined, `expected a heading above ${where}, found none`);
|
|
551
|
+
if ("extractor" in value) {
|
|
552
|
+
const extractor = library.extractors?.[value.extractor];
|
|
553
|
+
if (!extractor)
|
|
554
|
+
throw new Error(`cudoc: extractor "${value.extractor}" is not registered; add it to extractors in the collection options`);
|
|
555
|
+
const result = extractor.extract(row, {
|
|
556
|
+
library,
|
|
557
|
+
documentId: context.documentId,
|
|
558
|
+
column,
|
|
559
|
+
});
|
|
560
|
+
const text = typeof result === "string" ? result : result?.text;
|
|
561
|
+
if (!text)
|
|
562
|
+
return finish(undefined, `extractor "${value.extractor}" returned nothing for ${where}`);
|
|
563
|
+
return finish(text, undefined, typeof result === "object" ? result.url : undefined);
|
|
564
|
+
}
|
|
565
|
+
const tables = sectionTables(row.section.tree, value.skipTablesWithHeaders);
|
|
566
|
+
const index = value.table ?? 0;
|
|
567
|
+
const table = tables[index];
|
|
568
|
+
if (!table)
|
|
569
|
+
return finish(undefined, `expected table ${index} in ${where}, found ${tables.length} table${tables.length === 1 ? "" : "s"}${value.skipTablesWithHeaders?.length ? " after skipping those headed " + value.skipTablesWithHeaders.map((h) => JSON.stringify(h)).join(", ") : ""}`);
|
|
570
|
+
const tableRow = table.children?.[value.row];
|
|
571
|
+
if (!tableRow)
|
|
572
|
+
return finish(undefined, `expected row ${value.row} of table ${index} in ${where}, found ${table.children?.length ?? 0} rows`);
|
|
573
|
+
const cell = tableRow.children?.[value.column];
|
|
574
|
+
if (!cell)
|
|
575
|
+
return finish(undefined, `expected column ${value.column} of row ${value.row} in table ${index} of ${where}, found ${tableRow.children?.length ?? 0} cells`);
|
|
576
|
+
const text = nodeText(cell).trim();
|
|
577
|
+
return text
|
|
578
|
+
? finish(text)
|
|
579
|
+
: finish(undefined, `cell ${value.row}:${value.column} of table ${index} in ${where} is empty`);
|
|
580
|
+
}
|
|
581
|
+
/** The mdast table the extracted cells make, header cells carrying any width. */
|
|
582
|
+
export function buildEmbedTable(library, columns, rows, context) {
|
|
583
|
+
const cell = (value, style) => ({
|
|
584
|
+
type: "tableCell",
|
|
585
|
+
children: value.url
|
|
586
|
+
? [
|
|
587
|
+
{
|
|
588
|
+
type: "link",
|
|
589
|
+
url: value.url,
|
|
590
|
+
children: [{ type: "text", value: value.text }],
|
|
591
|
+
},
|
|
592
|
+
]
|
|
593
|
+
: [{ type: "text", value: value.text }],
|
|
594
|
+
...(style ? { data: { hProperties: { style } } } : {}),
|
|
595
|
+
});
|
|
596
|
+
return {
|
|
597
|
+
type: "root",
|
|
598
|
+
children: [
|
|
599
|
+
{
|
|
600
|
+
type: "table",
|
|
601
|
+
align: columns.map(() => null),
|
|
602
|
+
children: [
|
|
603
|
+
{
|
|
604
|
+
type: "tableRow",
|
|
605
|
+
children: columns.map((column) => {
|
|
606
|
+
const spec = columnSpec(column);
|
|
607
|
+
return cell({ text: spec.header }, spec.minWidth ? `min-width: ${spec.minWidth}` : undefined);
|
|
608
|
+
}),
|
|
609
|
+
},
|
|
610
|
+
...rows.map((row) => ({
|
|
611
|
+
type: "tableRow",
|
|
612
|
+
children: columns.map((column) => cell(extractCell(library, column, row, context).cell)),
|
|
613
|
+
})),
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
265
619
|
/** Resolve a configured embed from immutable persisted documents. */
|
|
266
620
|
export function resolveEmbed(library, input, context) {
|
|
267
621
|
const spec = parseEmbedSpec(JSON.stringify(input));
|
|
268
622
|
let occurrence = 0;
|
|
269
623
|
const reserved = new Set();
|
|
270
|
-
const destination = library.
|
|
624
|
+
const destination = documentIndex(library).byId.get(context.documentId);
|
|
625
|
+
// Elements whose required modules already start from `destination`.
|
|
626
|
+
const moved = new WeakSet();
|
|
271
627
|
if (destination)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
628
|
+
for (const id of declaredIds(destination.tree))
|
|
629
|
+
reserved.add(id);
|
|
275
630
|
const active = [];
|
|
631
|
+
// Every document a block read, so a later preparation can tell whether the
|
|
632
|
+
// block is still current; `*` when an extractor ran, which may read anything.
|
|
633
|
+
const dependencies = new Set();
|
|
276
634
|
const expand = (spec, from) => {
|
|
277
635
|
const children = [];
|
|
278
636
|
const rows = [];
|
|
279
637
|
for (const source of spec.sources) {
|
|
280
638
|
const { document, anchor } = resolveDocumentReference(library, source, from);
|
|
639
|
+
dependencies.add(document.id);
|
|
281
640
|
const key = `${document.id}#${anchor ?? "*"}`;
|
|
282
641
|
if (active.includes(key) || active.length >= 64)
|
|
283
642
|
throw new Error(`cudoc: cyclic embed: ${[...active, key].join(" -> ")}`);
|
|
@@ -310,15 +669,9 @@ export function resolveEmbed(library, input, context) {
|
|
|
310
669
|
});
|
|
311
670
|
};
|
|
312
671
|
expandNodes(section);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
rows.push(
|
|
316
|
-
title: heading
|
|
317
|
-
? visibleHeadingText(heading)
|
|
318
|
-
: selectedSection.title,
|
|
319
|
-
link: `${document.route}${selectedSection.anchorId ? `#${selectedSection.anchorId}` : ""}`,
|
|
320
|
-
summary: paragraph ? nodeText(paragraph) : "",
|
|
321
|
-
});
|
|
672
|
+
// Rows read the section before its ids are rebased, so a cell's link
|
|
673
|
+
// points into the source document rather than at the copy.
|
|
674
|
+
rows.push(buildEmbedRow(document, selectedSection.anchorId, structuredClone(section)));
|
|
322
675
|
let prefix;
|
|
323
676
|
const sectionIds = [];
|
|
324
677
|
visitNodes(section, (node) => {
|
|
@@ -327,7 +680,7 @@ export function resolveEmbed(library, input, context) {
|
|
|
327
680
|
do {
|
|
328
681
|
prefix = `${context.prefix ?? "embed"}-${++occurrence}-`;
|
|
329
682
|
} while (sectionIds.some((id) => reserved.has(`${prefix}${id}`)));
|
|
330
|
-
rebase(section, document, library, prefix);
|
|
683
|
+
rebase(section, document, library, prefix, destination, moved);
|
|
331
684
|
sectionIds.forEach((id) => reserved.add(`${prefix}${id}`));
|
|
332
685
|
children.push(...section.children);
|
|
333
686
|
}
|
|
@@ -337,41 +690,25 @@ export function resolveEmbed(library, input, context) {
|
|
|
337
690
|
}
|
|
338
691
|
}
|
|
339
692
|
if (spec.render && spec.render !== "section") {
|
|
340
|
-
const columns = spec.render.columns ??
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
return {
|
|
348
|
-
type: "root",
|
|
349
|
-
children: [
|
|
350
|
-
{
|
|
351
|
-
type: "table",
|
|
352
|
-
align: columns.map(() => null),
|
|
353
|
-
children: [
|
|
354
|
-
{ type: "tableRow", children: columns.map((c) => cell(c)) },
|
|
355
|
-
...rows.map((row) => ({
|
|
356
|
-
type: "tableRow",
|
|
357
|
-
children: columns.map((c) => cell(c === "link" ? row.title : row[c], c === "link" ? row.link : undefined)),
|
|
358
|
-
})),
|
|
359
|
-
],
|
|
360
|
-
},
|
|
361
|
-
],
|
|
362
|
-
};
|
|
693
|
+
const columns = spec.render.columns ?? DEFAULT_TABLE_COLUMNS;
|
|
694
|
+
if (columns.some((column) => typeof column === "object" &&
|
|
695
|
+
typeof column.value === "object" &&
|
|
696
|
+
"extractor" in column.value))
|
|
697
|
+
dependencies.add("*");
|
|
698
|
+
return buildEmbedTable(library, columns, rows, context);
|
|
363
699
|
}
|
|
364
700
|
return { type: "root", children };
|
|
365
701
|
};
|
|
366
702
|
const result = expand(spec, context.documentId);
|
|
367
703
|
result.data = {
|
|
368
704
|
...result.data,
|
|
369
|
-
cudocEmbedPrefix: `cudoc-${
|
|
705
|
+
cudocEmbedPrefix: `cudoc-${idToken(context.documentId)}-${context.prefix ?? "embed"}-`,
|
|
706
|
+
cudocDependencies: [...dependencies].sort(),
|
|
370
707
|
};
|
|
371
708
|
return result;
|
|
372
709
|
}
|
|
373
710
|
export function resolveDocumentEmbeds(library, documentId) {
|
|
374
|
-
const document = library.
|
|
711
|
+
const document = documentIndex(library).byId.get(documentId);
|
|
375
712
|
if (!document)
|
|
376
713
|
throw new Error(`cudoc: missing document ${documentId}`);
|
|
377
714
|
const tree = structuredClone(document.tree);
|