@cudoment/cudoc 0.5.0 → 0.7.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 +3 -2
- package/dist/document.d.ts +25 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +21 -1
- package/dist/document.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/node/check.d.ts +8 -4
- package/dist/node/check.d.ts.map +1 -1
- package/dist/node/check.js +475 -65
- package/dist/node/check.js.map +1 -1
- package/dist/node/cli.js +2 -81
- 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.map +1 -1
- package/dist/node/dataset.js +4 -1
- package/dist/node/dataset.js.map +1 -1
- package/dist/node/library.d.ts +9 -5
- package/dist/node/library.d.ts.map +1 -1
- package/dist/node/library.js +15 -253
- 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 +11 -0
- package/dist/node/local-target.d.ts.map +1 -1
- package/dist/node/local-target.js +54 -2
- package/dist/node/local-target.js.map +1 -1
- package/dist/node/prepare-embeds.d.ts +36 -1
- package/dist/node/prepare-embeds.d.ts.map +1 -1
- package/dist/node/prepare-embeds.js +94 -4
- 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/report.d.ts.map +1 -1
- package/dist/node/report.js +5 -1
- package/dist/node/report.js.map +1 -1
- package/dist/node/resolve-embed.d.ts +99 -2
- package/dist/node/resolve-embed.d.ts.map +1 -1
- package/dist/node/resolve-embed.js +654 -116
- package/dist/node/resolve-embed.js.map +1 -1
- package/dist/node/roots.d.ts +12 -1
- package/dist/node/roots.d.ts.map +1 -1
- package/dist/node/roots.js +47 -11
- package/dist/node/roots.js.map +1 -1
- package/dist/node/storage.d.ts.map +1 -1
- package/dist/node/storage.js +202 -9
- package/dist/node/storage.js.map +1 -1
- package/dist/node/tree.d.ts +58 -0
- package/dist/node/tree.d.ts.map +1 -0
- package/dist/node/tree.js +151 -0
- package/dist/node/tree.js.map +1 -0
- package/dist/node/watch.d.ts +9 -4
- package/dist/node/watch.d.ts.map +1 -1
- package/dist/node/watch.js +51 -17
- package/dist/node/watch.js.map +1 -1
- package/dist/paged.d.ts +18 -0
- package/dist/paged.d.ts.map +1 -1
- package/dist/paged.js +21 -0
- package/dist/paged.js.map +1 -1
- 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 +15 -15
- package/styles.css +49 -0
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { fromHtml } from "hast-util-from-html";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { collectSections } from "../sections.js";
|
|
5
|
-
import { nodeText, visibleHeadingText } from "../document.js";
|
|
6
|
-
import { compileDocument } from "../markdown.js";
|
|
4
|
+
import { collectSections, } from "../sections.js";
|
|
5
|
+
import { idToken, nodeText, visibleHeadingText, } from "../document.js";
|
|
7
6
|
import { findHeadingByAnchorId, findParentHeading, getHeadingAnchorId, } from "../internal/core/query/sections.js";
|
|
7
|
+
import { TREE_CLASS, TREE_KIND, TREE_PRINT_ATTRIBUTE } from "../paged.js";
|
|
8
8
|
import { sourceFileOf } from "./roots.js";
|
|
9
|
+
import { compareCodePoints, compareNames, documentName, hierarchyOf, nfc, } from "./tree.js";
|
|
10
|
+
import { parseSrcSet } from "./local-target.js";
|
|
11
|
+
import { transformedSection } from "./replace.js";
|
|
12
|
+
import { EXTERNAL_URL, decodeComponent, declaredIds, documentIndex, idsInNode, } from "./references.js";
|
|
9
13
|
const SPEC_KEYS = ["sources", "select", "render", "replace"];
|
|
14
|
+
const TABLE_KEYS = ["type", "columns"];
|
|
15
|
+
const TREE_KEYS = [
|
|
16
|
+
"type",
|
|
17
|
+
"open",
|
|
18
|
+
"print",
|
|
19
|
+
"depth",
|
|
20
|
+
"headings",
|
|
21
|
+
"order",
|
|
22
|
+
"columns",
|
|
23
|
+
];
|
|
24
|
+
/** The `order` entry that stands for every name the list does not give. */
|
|
25
|
+
const REST = "...";
|
|
10
26
|
const SELECTION_KEYS = ["anchors", "titles", "depth", "includeChildren"];
|
|
11
27
|
const REPLACEMENT_KEYS = ["find", "replace", "regex", "flags"];
|
|
12
28
|
const COLUMN_KEYS = ["header", "value", "link", "minWidth"];
|
|
@@ -62,6 +78,38 @@ const validateColumn = (column, at) => {
|
|
|
62
78
|
cell.skipTablesWithHeaders.some((h) => typeof h !== "string")))
|
|
63
79
|
throw new Error(`cudoc: ${at}.value.skipTablesWithHeaders must be strings`);
|
|
64
80
|
};
|
|
81
|
+
const validateTree = (render) => {
|
|
82
|
+
const count = (key, least, most) => {
|
|
83
|
+
const value = render[key];
|
|
84
|
+
if (value === undefined)
|
|
85
|
+
return;
|
|
86
|
+
if (!Number.isInteger(value) ||
|
|
87
|
+
value < least ||
|
|
88
|
+
(most !== undefined && value > most))
|
|
89
|
+
throw new Error(`cudoc: render.${key} must be ${most === undefined
|
|
90
|
+
? `an integer of at least ${least}`
|
|
91
|
+
: `an integer from ${least} to ${most}`}`);
|
|
92
|
+
};
|
|
93
|
+
count("open", 0);
|
|
94
|
+
count("print", 1);
|
|
95
|
+
count("depth", 1);
|
|
96
|
+
count("headings", 0, 5);
|
|
97
|
+
const order = render.order;
|
|
98
|
+
if (order === undefined)
|
|
99
|
+
return;
|
|
100
|
+
if (!Array.isArray(order) ||
|
|
101
|
+
order.some((name) => typeof name !== "string" || !name.trim()))
|
|
102
|
+
throw new Error("cudoc: render.order must be a list of names");
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const name of order) {
|
|
105
|
+
const key = nfc(name);
|
|
106
|
+
if (seen.has(key))
|
|
107
|
+
throw new Error(name === REST
|
|
108
|
+
? `cudoc: render.order has ${REST} twice; it stands for every name not listed, once`
|
|
109
|
+
: `cudoc: render.order names "${name}" twice`);
|
|
110
|
+
seen.add(key);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
65
113
|
export function parseEmbedSpec(value) {
|
|
66
114
|
const spec = parseYaml(value, { maxAliasCount: 100 });
|
|
67
115
|
if (!spec || typeof spec !== "object" || Array.isArray(spec))
|
|
@@ -77,16 +125,26 @@ export function parseEmbedSpec(value) {
|
|
|
77
125
|
if (!spec.render ||
|
|
78
126
|
typeof spec.render !== "object" ||
|
|
79
127
|
Array.isArray(spec.render) ||
|
|
80
|
-
spec.render.type
|
|
81
|
-
throw new Error('cudoc: render must be "section" or a mapping with type: table');
|
|
128
|
+
!["table", "tree"].includes(spec.render.type))
|
|
129
|
+
throw new Error('cudoc: render must be "section" or a mapping with type: table or type: tree');
|
|
130
|
+
const known = spec.render.type === "tree" ? TREE_KEYS : TABLE_KEYS;
|
|
82
131
|
for (const key of Object.keys(spec.render))
|
|
83
|
-
if (!
|
|
84
|
-
throw new Error(`cudoc: render: unknown key "${key}". Known keys:
|
|
132
|
+
if (!known.includes(key))
|
|
133
|
+
throw new Error(`cudoc: render: unknown key "${key}". Known keys: ${known.join(", ")}`);
|
|
85
134
|
if (spec.render.columns !== undefined) {
|
|
86
135
|
if (!Array.isArray(spec.render.columns) || !spec.render.columns.length)
|
|
87
136
|
throw new Error("cudoc: render.columns must be a non-empty array");
|
|
88
137
|
spec.render.columns.forEach((column, index) => validateColumn(column, `render.columns[${index}]`));
|
|
89
138
|
}
|
|
139
|
+
if (spec.render.type === "tree") {
|
|
140
|
+
validateTree(spec.render);
|
|
141
|
+
// A tree copies no section text, so neither would change anything,
|
|
142
|
+
// and an author who wrote one expects it to.
|
|
143
|
+
if (spec.select !== undefined)
|
|
144
|
+
throw new Error("cudoc: select does not apply to a tree; its levels come from folders, and render.headings adds sections");
|
|
145
|
+
if (spec.replace !== undefined)
|
|
146
|
+
throw new Error("cudoc: replace does not apply to a tree, which copies no section text");
|
|
147
|
+
}
|
|
90
148
|
}
|
|
91
149
|
if (spec.select) {
|
|
92
150
|
if (typeof spec.select !== "object" || Array.isArray(spec.select))
|
|
@@ -163,77 +221,84 @@ export function parseEmbedBlock(value, documentId, number) {
|
|
|
163
221
|
throw new Error(`cudoc: ${where}: ${message}`, { cause });
|
|
164
222
|
}
|
|
165
223
|
}
|
|
166
|
-
|
|
224
|
+
/**
|
|
225
|
+
* What a source names, read without looking anything up: the library path it
|
|
226
|
+
* spells, relative to `from` or from the top with a leading `/`, and its
|
|
227
|
+
* anchor, percent-decoded. A URL, a backslash and a climb out of the library
|
|
228
|
+
* fail here.
|
|
229
|
+
*/
|
|
230
|
+
const sourcePath = (reference, from) => {
|
|
167
231
|
const hash = reference.indexOf("#");
|
|
168
232
|
const pathname = hash < 0 ? reference : reference.slice(0, hash);
|
|
169
|
-
|
|
233
|
+
let anchor;
|
|
234
|
+
try {
|
|
235
|
+
anchor =
|
|
236
|
+
hash < 0 ? undefined : decodeURIComponent(reference.slice(hash + 1));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
throw new Error(`cudoc: embed source has a malformed percent-escape: ${reference}`);
|
|
240
|
+
}
|
|
170
241
|
if (/^[a-z][\w+.-]*:/i.test(pathname) || pathname.includes("\\"))
|
|
171
242
|
throw new Error(`cudoc: embed source must be a local document: ${reference}`);
|
|
172
|
-
const
|
|
243
|
+
const target = pathname
|
|
173
244
|
? pathname.startsWith("/")
|
|
174
245
|
? pathname.slice(1)
|
|
175
246
|
: path.posix.join(path.posix.dirname(from), pathname)
|
|
176
|
-
: from
|
|
247
|
+
: from;
|
|
248
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
177
249
|
if (id === ".." || id.startsWith("../"))
|
|
178
250
|
throw new Error(`cudoc: embed source escapes root: ${reference}`);
|
|
179
|
-
|
|
251
|
+
return { pathname, target, ...(anchor === undefined ? {} : { anchor }) };
|
|
252
|
+
};
|
|
253
|
+
export function resolveDocumentReference(library, reference, from) {
|
|
254
|
+
const { target, anchor } = sourcePath(reference, from);
|
|
255
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
256
|
+
const document = documentIndex(library).byId.get(id);
|
|
180
257
|
if (!document)
|
|
181
258
|
throw new Error(`cudoc: missing document ${reference} referenced from ${from}`);
|
|
182
259
|
return { document, anchor };
|
|
183
260
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
(dependencies ? `\n\n${dependencies}` : "");
|
|
218
|
-
const options = { ...library.options, format: document.source.format };
|
|
219
|
-
if (!library.compiler &&
|
|
220
|
-
options.host &&
|
|
221
|
-
!["markdown", "next", "html"].includes(options.host))
|
|
222
|
-
throw new Error(`cudoc: replacing ${options.host} Markdown requires the original host compiler`);
|
|
223
|
-
try {
|
|
224
|
-
return library.compiler
|
|
225
|
-
? library.compiler(source, {
|
|
226
|
-
id: document.id,
|
|
227
|
-
filePath: (library.roots &&
|
|
228
|
-
sourceFileOf(library.roots, document.sourcePath)) ??
|
|
229
|
-
document.sourcePath,
|
|
230
|
-
options,
|
|
231
|
-
}).tree
|
|
232
|
-
: compileDocument(source, options).tree;
|
|
233
|
-
}
|
|
234
|
-
catch (cause) {
|
|
235
|
-
throw new Error(`cudoc: replaced Markdown could not compile in ${document.id} at source offset ${range?.start ?? 0}`, { cause });
|
|
261
|
+
/**
|
|
262
|
+
* Whether a tree on `page` lists private documents: only when the page is
|
|
263
|
+
* private itself. One that names no collected document counts as public.
|
|
264
|
+
*/
|
|
265
|
+
const listsPrivate = (library, page) => documentIndex(library).byId.get(page)?.private === true;
|
|
266
|
+
/**
|
|
267
|
+
* Reads one source of a tree. A path ending in `/` names a folder, and the
|
|
268
|
+
* documents directly below it become the tree's first level; any other path
|
|
269
|
+
* names a document, or with `#anchor` one of its sections, as for any embed.
|
|
270
|
+
* Ids are matched in NFC, so a source names a document whose file name is
|
|
271
|
+
* stored in NFD. A folder holding no document fails, and so does a document
|
|
272
|
+
* path that is really a folder, with a hint to add the `/`. Given `page`,
|
|
273
|
+
* the document the tree lands on, a folder whose documents are all private
|
|
274
|
+
* fails too unless that page is private, since the tree leaves them out.
|
|
275
|
+
*/
|
|
276
|
+
export function resolveTreeSource(library, reference, from, page) {
|
|
277
|
+
const { pathname, target, anchor } = sourcePath(reference, from);
|
|
278
|
+
const hierarchy = hierarchyOf(library);
|
|
279
|
+
if (pathname.endsWith("/")) {
|
|
280
|
+
if (anchor !== undefined)
|
|
281
|
+
throw new Error(`cudoc: a folder source takes no anchor: ${reference}`);
|
|
282
|
+
const normal = path.posix.normalize(target || ".").replace(/\/+$/, "");
|
|
283
|
+
if (normal === ".." || normal.startsWith("../"))
|
|
284
|
+
throw new Error(`cudoc: embed source escapes root: ${reference}`);
|
|
285
|
+
const folder = normal === "." ? "" : normal;
|
|
286
|
+
const documents = hierarchy.below(folder);
|
|
287
|
+
if (!documents.length)
|
|
288
|
+
throw new Error(`cudoc: no documents in folder ${reference} referenced from ${from}`);
|
|
289
|
+
if (page !== undefined &&
|
|
290
|
+
!listsPrivate(library, page) &&
|
|
291
|
+
documents.every((document) => document.private))
|
|
292
|
+
throw new Error(`cudoc: folder ${reference} referenced from ${from} holds only private documents, which a tree on ${page} leaves out`);
|
|
293
|
+
return { folder, documents };
|
|
236
294
|
}
|
|
295
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
296
|
+
const document = documentIndex(library).byId.get(id) ?? hierarchy.document(id);
|
|
297
|
+
if (document)
|
|
298
|
+
return { document, ...(anchor === undefined ? {} : { anchor }) };
|
|
299
|
+
throw new Error(`cudoc: missing document ${reference} referenced from ${from}${hierarchy.below(path.posix.normalize(id)).length
|
|
300
|
+
? `; a folder source ends with /, as in ${pathname}/`
|
|
301
|
+
: ""}`);
|
|
237
302
|
}
|
|
238
303
|
const visitNodes = (node, fn) => {
|
|
239
304
|
fn(node);
|
|
@@ -248,56 +313,237 @@ const htmlAttribute = (value, quote) => {
|
|
|
248
313
|
? String(element.properties.dataValue ?? value)
|
|
249
314
|
: value;
|
|
250
315
|
};
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
/**
|
|
317
|
+
* A relative module an attribute's expression requires first thing, after any
|
|
318
|
+
* webpack loaders: Docusaurus writes a Markdown image, and a link to a local
|
|
319
|
+
* file, as `require("<loaders>!./<path from the page's directory>").default`.
|
|
320
|
+
* Only an expression that starts with the call is read, so text that merely
|
|
321
|
+
* looks like one inside an authored string is left alone. The string runs to
|
|
322
|
+
* its own closing quote, so the other quote may be in a name.
|
|
323
|
+
*/
|
|
324
|
+
const REQUIRED_MODULE = /^(\s*require\(\s*(["'])(?:(?:(?!\2)[^\\\n]|\\[\s\S])*!)?)(\.{1,2}\/(?:(?!\2)[^\\\n]|\\[\s\S])*)(\2\s*\))/;
|
|
325
|
+
/**
|
|
326
|
+
* The directory a document's file sits in, which is what a relative module
|
|
327
|
+
* path starts from; the library path's own directory when the library was
|
|
328
|
+
* loaded without its roots, which is the file's only where every root's base
|
|
329
|
+
* mirrors its directory.
|
|
330
|
+
*/
|
|
331
|
+
const fileDirectory = (library, document) => path.dirname((library.roots?.length
|
|
332
|
+
? sourceFileOf(library.roots, document.sourcePath)
|
|
333
|
+
: undefined) ?? path.join("/", document.sourcePath));
|
|
334
|
+
/**
|
|
335
|
+
* `to` from `from`, both relative library paths that may climb above the
|
|
336
|
+
* root, worked out on the paths alone: resolved against the working
|
|
337
|
+
* directory, a climb above it would be clamped again.
|
|
338
|
+
*/
|
|
339
|
+
const climbingRelative = (from, to) => {
|
|
340
|
+
const depth = [...from.split("/"), ...to.split("/")].filter((part) => part === "..")
|
|
341
|
+
.length + 1;
|
|
342
|
+
const base = `/${Array.from({ length: depth }, (_, i) => `_${i}`).join("/")}`;
|
|
343
|
+
return path.posix.relative(path.posix.join(base, from), path.posix.join(base, to));
|
|
344
|
+
};
|
|
345
|
+
/** What a JavaScript string literal holds between its quotes, escapes read. */
|
|
346
|
+
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 = "") => {
|
|
347
|
+
const code = braced ?? unicode ?? hex;
|
|
348
|
+
if (code) {
|
|
349
|
+
const point = parseInt(code, 16);
|
|
350
|
+
// Past the last code point the escape is not JavaScript at all.
|
|
351
|
+
return point > 0x10ffff ? _match : String.fromCodePoint(point);
|
|
352
|
+
}
|
|
353
|
+
// A backslash before a line break continues the string on the next line.
|
|
354
|
+
if (/^(?:\r\n|[\n\r\u2028\u2029])$/.test(other))
|
|
355
|
+
return "";
|
|
356
|
+
const named = {
|
|
357
|
+
n: "\n",
|
|
358
|
+
r: "\r",
|
|
359
|
+
t: "\t",
|
|
360
|
+
b: "\b",
|
|
361
|
+
f: "\f",
|
|
362
|
+
v: "\v",
|
|
363
|
+
"0": "\0",
|
|
364
|
+
};
|
|
365
|
+
return named[other] ?? other;
|
|
366
|
+
});
|
|
367
|
+
/** `value` written between `quote`s as a JavaScript string literal. */
|
|
368
|
+
const stringLiteral = (value, quote) => value.replace(/[\\\n\r\u2028\u2029"']/g, (character) => {
|
|
369
|
+
if (character === "\\")
|
|
370
|
+
return "\\\\";
|
|
371
|
+
if (character === "\n")
|
|
372
|
+
return "\\n";
|
|
373
|
+
if (character === "\r")
|
|
374
|
+
return "\\r";
|
|
375
|
+
if (character === "\u2028")
|
|
376
|
+
return "\\u2028";
|
|
377
|
+
if (character === "\u2029")
|
|
378
|
+
return "\\u2029";
|
|
379
|
+
return character === quote ? `\\${character}` : character;
|
|
380
|
+
});
|
|
381
|
+
/**
|
|
382
|
+
* Re-expresses the modules a JSX element's attributes require from the
|
|
383
|
+
* directory of the document the copy lands in: a page in another directory
|
|
384
|
+
* would resolve the path from its own. The path is read as the string it is
|
|
385
|
+
* and written back as one, so a directory named with a quote or a backslash
|
|
386
|
+
* still gives an expression that parses.
|
|
387
|
+
*/
|
|
388
|
+
const moveRequiredModules = (node, from, to) => {
|
|
389
|
+
if (!Array.isArray(node.attributes))
|
|
390
|
+
return;
|
|
391
|
+
for (const attribute of node.attributes) {
|
|
392
|
+
const value = attribute.value;
|
|
393
|
+
if (attribute.type !== "mdxJsxAttribute" ||
|
|
394
|
+
typeof value !== "object" ||
|
|
395
|
+
value?.type !== "mdxJsxAttributeValueExpression" ||
|
|
396
|
+
typeof value.value !== "string")
|
|
397
|
+
continue;
|
|
398
|
+
value.value = value.value.replace(REQUIRED_MODULE, (match, head, quote, module, tail) => {
|
|
399
|
+
// A query the host appended stays as written: it is not part of
|
|
400
|
+
// the path, and joining it would normalize its slashes too.
|
|
401
|
+
const [, file = "", query = ""] = /^([^?]*)(.*)$/s.exec(stringValue(module));
|
|
402
|
+
const relative = path.relative(to, path.join(from, file));
|
|
403
|
+
// Another drive has no relative path; the copy keeps the original.
|
|
404
|
+
if (path.isAbsolute(relative))
|
|
405
|
+
return match;
|
|
406
|
+
const moved = relative.split(path.sep).join("/");
|
|
407
|
+
return `${head}${stringLiteral(`./${moved}${query}`, quote)}${tail}`;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
/** The raw HTML attributes that name a path: a link and every resource. */
|
|
412
|
+
const REBASED_ATTRIBUTES = new Set([
|
|
413
|
+
"href",
|
|
414
|
+
"src",
|
|
415
|
+
"poster",
|
|
416
|
+
"data",
|
|
417
|
+
"xlink:href",
|
|
418
|
+
]);
|
|
419
|
+
/**
|
|
420
|
+
* One attribute of a tag, read in order after the tag name as HTML reads it:
|
|
421
|
+
* what separates it from the one before, white space or a stray `/` and
|
|
422
|
+
* nothing at all after a quoted value, its name, and a value quoted either
|
|
423
|
+
* way, which may span lines, or bare, which runs to white space or `>`.
|
|
424
|
+
*/
|
|
425
|
+
const ATTRIBUTE = /([\s/]*)([^\s"'>/=]+)(?:(\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/y;
|
|
426
|
+
/**
|
|
427
|
+
* The tag with the values `rewrite` returns in place of the ones it has, the
|
|
428
|
+
* attributes it returns nothing for left exactly as written. The attributes
|
|
429
|
+
* are walked in order, so text inside one value is never read as another.
|
|
430
|
+
*/
|
|
431
|
+
const rewriteAttributes = (tag, rewrite) => {
|
|
432
|
+
const opening = /^<\/?[^\s/>]+/.exec(tag);
|
|
433
|
+
if (!opening)
|
|
434
|
+
return tag;
|
|
435
|
+
let result = opening[0];
|
|
436
|
+
let index = opening[0].length;
|
|
437
|
+
for (;;) {
|
|
438
|
+
ATTRIBUTE.lastIndex = index;
|
|
439
|
+
const match = ATTRIBUTE.exec(tag);
|
|
440
|
+
if (!match)
|
|
441
|
+
break;
|
|
442
|
+
index = ATTRIBUTE.lastIndex;
|
|
443
|
+
const [whole, space, name, equals, double, single, bare] = match;
|
|
444
|
+
const written = double ?? single ?? bare;
|
|
445
|
+
const delimiter = single === undefined ? '"' : "'";
|
|
446
|
+
const replacement = written === undefined
|
|
447
|
+
? undefined
|
|
448
|
+
: rewrite(name.toLowerCase(), htmlAttribute(written, delimiter));
|
|
449
|
+
result +=
|
|
450
|
+
replacement === undefined
|
|
451
|
+
? whole
|
|
452
|
+
: `${space}${name}${equals}${delimiter}${replacement
|
|
453
|
+
.replace(/&/g, "&")
|
|
454
|
+
.replace(new RegExp(delimiter, "g"), delimiter === '"' ? """ : "'")}${delimiter}`;
|
|
265
455
|
}
|
|
266
|
-
return
|
|
456
|
+
return result + tag.slice(index);
|
|
267
457
|
};
|
|
268
|
-
function rebase(tree, document, library, prefix) {
|
|
458
|
+
function rebase(tree, document, library, prefix, destination, placed) {
|
|
269
459
|
const ids = new Set();
|
|
270
460
|
visitNodes(tree, (node) => {
|
|
271
461
|
idsInNode(node).forEach((id) => ids.add(id));
|
|
272
462
|
});
|
|
463
|
+
// Worked out once, and only when the copy has an element to move.
|
|
464
|
+
let directories;
|
|
273
465
|
const sourceUrl = (url) => {
|
|
274
|
-
if (
|
|
466
|
+
if (EXTERNAL_URL.test(url))
|
|
275
467
|
return url;
|
|
276
468
|
if (url.startsWith("#")) {
|
|
277
|
-
|
|
278
|
-
try {
|
|
279
|
-
id = decodeURIComponent(id);
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
/* Preserve malformed URL. */
|
|
283
|
-
}
|
|
469
|
+
const id = decodeComponent(url.slice(1));
|
|
284
470
|
return ids.has(id) ? `#${prefix}${id}` : `${document.route}${url}`;
|
|
285
471
|
}
|
|
286
472
|
const match = url.match(/^([^?#]*)(.*)$/);
|
|
287
|
-
|
|
473
|
+
// No path means the page the link sits on, which in a copy is the
|
|
474
|
+
// document it was copied from, not the directory that document is in.
|
|
475
|
+
if (!match[1])
|
|
476
|
+
return `${document.route}${match[2]}`;
|
|
477
|
+
const joined = match[1].startsWith("/")
|
|
288
478
|
? match[1]
|
|
289
|
-
:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
479
|
+
: path.posix.join(path.posix.dirname(document.sourcePath), match[1]);
|
|
480
|
+
// A relative path that climbs out of the collection names no library
|
|
481
|
+
// path, and clamped at the root it would name another file. It is spelled
|
|
482
|
+
// from the page the copy lands on instead, which reaches the same place:
|
|
483
|
+
// from the files' own directories when the library has its roots, and
|
|
484
|
+
// from the library paths otherwise.
|
|
485
|
+
if (joined === ".." || joined.startsWith("../")) {
|
|
486
|
+
if (!destination)
|
|
487
|
+
return url;
|
|
488
|
+
// A directory keeps its trailing slash, which names its index.
|
|
489
|
+
const slash = /(?:^|\/)\.{0,2}$/.test(match[1]) ? "/" : "";
|
|
490
|
+
if (!library.roots?.length)
|
|
491
|
+
return `${climbingRelative(path.posix.dirname(destination.sourcePath), joined) || "."}${slash}${match[2]}`;
|
|
492
|
+
directories ??= {
|
|
493
|
+
from: fileDirectory(library, document),
|
|
494
|
+
to: fileDirectory(library, destination),
|
|
495
|
+
};
|
|
496
|
+
const relative = path.relative(directories.to, path.join(directories.from, decodeComponent(match[1])));
|
|
497
|
+
// Written back as a URL path: only what would end or change one is
|
|
498
|
+
// escaped, as the path was decoded to reach the file.
|
|
499
|
+
return `${relative
|
|
500
|
+
.split(path.sep)
|
|
501
|
+
.map((part) => part.replace(/[%\s#?]/g, encodeURIComponent))
|
|
502
|
+
.join("/") || "."}${slash}${match[2]}`;
|
|
503
|
+
}
|
|
504
|
+
let absolute = path.posix.normalize(joined.startsWith("/") ? joined : `/${joined}`);
|
|
505
|
+
// `.` and `..` name a directory, as a browser reads them.
|
|
506
|
+
if (/(?:^|\/)\.\.?$/.test(match[1]) && !absolute.endsWith("/"))
|
|
507
|
+
absolute += "/";
|
|
508
|
+
const target = documentIndex(library).byPath.get(absolute);
|
|
293
509
|
return `${target?.route ?? absolute}${match[2]}`;
|
|
294
510
|
};
|
|
511
|
+
// A node an inner copy already placed holds the page's addresses, and read
|
|
512
|
+
// as source paths again they could name another document, or climb from
|
|
513
|
+
// the wrong directory. Only a fragment naming an id this copy renames
|
|
514
|
+
// follows the new name.
|
|
515
|
+
const pageUrl = (url) => {
|
|
516
|
+
if (!url.startsWith("#"))
|
|
517
|
+
return url;
|
|
518
|
+
const id = decodeComponent(url.slice(1));
|
|
519
|
+
return ids.has(id) ? `#${prefix}${id}` : url;
|
|
520
|
+
};
|
|
295
521
|
visitNodes(tree, (node) => {
|
|
522
|
+
const url = placed.has(node) ? pageUrl : sourceUrl;
|
|
523
|
+
// Every candidate of a responsive image is a path of its own; the width
|
|
524
|
+
// or density after it stays as written.
|
|
525
|
+
const srcSet = (value) => parseSrcSet(value)
|
|
526
|
+
.map(({ url: candidate, descriptor }) => [url(candidate), descriptor].filter(Boolean).join(" "))
|
|
527
|
+
.join(", ");
|
|
296
528
|
const id = node.data?.hProperties?.id;
|
|
297
529
|
if (typeof id === "string")
|
|
298
530
|
node.data.hProperties.id = `${prefix}${id}`;
|
|
299
531
|
if (typeof node.url === "string")
|
|
300
|
-
node.url =
|
|
532
|
+
node.url = url(node.url);
|
|
533
|
+
// An image a host made a component of is still an image of the source.
|
|
534
|
+
const image = node.data?.cudocImage;
|
|
535
|
+
if (image && typeof image.url === "string")
|
|
536
|
+
node.data.cudocImage = { ...image, url: url(image.url) };
|
|
537
|
+
// Once per element, however deeply the copy was nested: an inner embed
|
|
538
|
+
// already moved it from its own source to `destination`.
|
|
539
|
+
if (destination && Array.isArray(node.attributes) && !placed.has(node)) {
|
|
540
|
+
directories ??= {
|
|
541
|
+
from: fileDirectory(library, document),
|
|
542
|
+
to: fileDirectory(library, destination),
|
|
543
|
+
};
|
|
544
|
+
if (directories.from !== directories.to)
|
|
545
|
+
moveRequiredModules(node, directories.from, directories.to);
|
|
546
|
+
}
|
|
301
547
|
if ([
|
|
302
548
|
"linkReference",
|
|
303
549
|
"imageReference",
|
|
@@ -310,18 +556,21 @@ function rebase(tree, document, library, prefix) {
|
|
|
310
556
|
if (node.type === "html" && node.value)
|
|
311
557
|
node.value = node.value.replace(/<!--[\s\S]*?-->|<(?:[^"'<>]|"[^"]*"|'[^']*')*>/g, (tag) => tag.startsWith("<!--")
|
|
312
558
|
? tag
|
|
313
|
-
: tag
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}));
|
|
559
|
+
: rewriteAttributes(tag, (name, value) => name === "id"
|
|
560
|
+
? `${prefix}${value}`
|
|
561
|
+
: name === "srcset"
|
|
562
|
+
? srcSet(value)
|
|
563
|
+
: REBASED_ATTRIBUTES.has(name)
|
|
564
|
+
? url(value)
|
|
565
|
+
: undefined));
|
|
321
566
|
const attrs = node.data?.hProperties;
|
|
322
|
-
for (const key of ["href", "src"])
|
|
567
|
+
for (const key of ["href", "src", "poster", "data", "xLinkHref"])
|
|
568
|
+
if (typeof attrs?.[key] === "string")
|
|
569
|
+
attrs[key] = url(attrs[key]);
|
|
570
|
+
for (const key of ["srcSet", "srcset"])
|
|
323
571
|
if (typeof attrs?.[key] === "string")
|
|
324
|
-
attrs[key] =
|
|
572
|
+
attrs[key] = srcSet(attrs[key]);
|
|
573
|
+
placed.add(node);
|
|
325
574
|
});
|
|
326
575
|
}
|
|
327
576
|
/** The default columns of a summary table, as they have always been. */
|
|
@@ -386,7 +635,9 @@ const sectionTables = (tree, skipHeaders) => {
|
|
|
386
635
|
* problem, which is worded as what the column asked for and what the section
|
|
387
636
|
* has, so an author can tell a wrong coordinate from a missing table.
|
|
388
637
|
*/
|
|
389
|
-
export function extractCell(library, column, row, context
|
|
638
|
+
export function extractCell(library, column, row, context,
|
|
639
|
+
/** The tree line the cell is for, handed to an extractor. */
|
|
640
|
+
node) {
|
|
390
641
|
const spec = columnSpec(column);
|
|
391
642
|
const where = `${row.document.id}${row.section.anchorId ? `#${row.section.anchorId}` : ""}`;
|
|
392
643
|
const linkTo = () => {
|
|
@@ -428,6 +679,7 @@ export function extractCell(library, column, row, context) {
|
|
|
428
679
|
library,
|
|
429
680
|
documentId: context.documentId,
|
|
430
681
|
column,
|
|
682
|
+
...(node ? { node } : {}),
|
|
431
683
|
});
|
|
432
684
|
const text = typeof result === "string" ? result : result?.text;
|
|
433
685
|
if (!text)
|
|
@@ -488,21 +740,309 @@ export function buildEmbedTable(library, columns, rows, context) {
|
|
|
488
740
|
],
|
|
489
741
|
};
|
|
490
742
|
}
|
|
743
|
+
/** The default line of a tree: the title linked to its node, then the summary. */
|
|
744
|
+
export const DEFAULT_TREE_COLUMNS = ["link", "summary"];
|
|
745
|
+
/** Whether a column is computed by a registered function, which may read anything. */
|
|
746
|
+
const usesExtractor = (columns) => columns.some((column) => typeof column === "object" &&
|
|
747
|
+
typeof column.value === "object" &&
|
|
748
|
+
"extractor" in column.value);
|
|
749
|
+
/** Whether an `order` entry names a node: its name, or its title, in NFC. */
|
|
750
|
+
export const namesTreeNode = (entry, node) => {
|
|
751
|
+
const name = nfc(entry);
|
|
752
|
+
return node.name === name || nfc(node.title) === name;
|
|
753
|
+
};
|
|
754
|
+
/**
|
|
755
|
+
* A document's `#` title, from the top-level block at `from` on: a `#`
|
|
756
|
+
* heading there, or one inside a top-level `header` element, where Docusaurus
|
|
757
|
+
* puts the heading it reads the page title from.
|
|
758
|
+
*/
|
|
759
|
+
const titleHeading = (tree, from = 0) => {
|
|
760
|
+
for (let index = from; index < tree.children.length; index++) {
|
|
761
|
+
const node = tree.children[index];
|
|
762
|
+
const heading = node.type === "heading"
|
|
763
|
+
? node
|
|
764
|
+
: node.data?.hName === "header"
|
|
765
|
+
? node.children?.find((child) => child.type === "heading")
|
|
766
|
+
: undefined;
|
|
767
|
+
if (heading?.depth === 1)
|
|
768
|
+
return {
|
|
769
|
+
heading: heading,
|
|
770
|
+
index,
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return undefined;
|
|
774
|
+
};
|
|
775
|
+
/** Heading depths from `from` to the deepest `headings` reaches, `##` being 2. */
|
|
776
|
+
const headingDepths = (from, headings) => {
|
|
777
|
+
const depths = [];
|
|
778
|
+
for (let depth = Math.max(from, 2); depth <= headings + 1; depth++)
|
|
779
|
+
depths.push(depth);
|
|
780
|
+
return depths;
|
|
781
|
+
};
|
|
782
|
+
/**
|
|
783
|
+
* The lines of a tree, without cells when `cells` is false, and every
|
|
784
|
+
* document they were read from.
|
|
785
|
+
*/
|
|
786
|
+
function buildTree(library, sources, render, from, context, cells = true) {
|
|
787
|
+
const hierarchy = hierarchyOf(library);
|
|
788
|
+
const depth = render.depth ?? Infinity;
|
|
789
|
+
const headings = render.headings ?? 0;
|
|
790
|
+
const columns = render.columns ?? DEFAULT_TREE_COLUMNS;
|
|
791
|
+
const rows = new Map();
|
|
792
|
+
const documents = new Set();
|
|
793
|
+
// A page that is not private lists no private document it was not asked
|
|
794
|
+
// for by name: a folder or a parent would otherwise put one on it, and the
|
|
795
|
+
// export, which leaves private documents out, could not link to it.
|
|
796
|
+
const privateListed = listsPrivate(library, context.documentId);
|
|
797
|
+
const listed = (document) => !document.private || privateListed;
|
|
798
|
+
const create = (row, kind, name, level) => {
|
|
799
|
+
documents.add(row.document.id);
|
|
800
|
+
const node = {
|
|
801
|
+
id: `${row.document.id}${row.section.anchorId ? `#${row.section.anchorId}` : ""}`,
|
|
802
|
+
kind,
|
|
803
|
+
documentId: row.document.id,
|
|
804
|
+
...(row.section.anchorId ? { anchorId: row.section.anchorId } : {}),
|
|
805
|
+
name,
|
|
806
|
+
title: row.section.title,
|
|
807
|
+
url: row.url,
|
|
808
|
+
sourcePath: row.document.sourcePath,
|
|
809
|
+
level,
|
|
810
|
+
cells: [],
|
|
811
|
+
children: [],
|
|
812
|
+
};
|
|
813
|
+
rows.set(node, row);
|
|
814
|
+
return node;
|
|
815
|
+
};
|
|
816
|
+
/** The headings of `sections` nested by depth under a line at `level`. */
|
|
817
|
+
const nest = (document, sections, level) => {
|
|
818
|
+
const top = [];
|
|
819
|
+
// A heading past `depth` is still pushed, without a node, so the ones
|
|
820
|
+
// under it are left out with it rather than moved up a level.
|
|
821
|
+
const stack = [];
|
|
822
|
+
for (const section of sections) {
|
|
823
|
+
while (stack.length && stack.at(-1).depth >= section.heading.depth)
|
|
824
|
+
stack.pop();
|
|
825
|
+
const above = stack.at(-1);
|
|
826
|
+
const at = (above?.level ?? level) + 1;
|
|
827
|
+
const node = at <= depth && (!above || above.node)
|
|
828
|
+
? headingNode(document, section, at)
|
|
829
|
+
: undefined;
|
|
830
|
+
stack.push({ depth: section.heading.depth, level: at, node });
|
|
831
|
+
if (node)
|
|
832
|
+
(above ? above.node.children : top).push(node);
|
|
833
|
+
}
|
|
834
|
+
return top;
|
|
835
|
+
};
|
|
836
|
+
const headingNode = (document, section, level) => {
|
|
837
|
+
const row = buildEmbedRow(document, section.anchorId, section.tree);
|
|
838
|
+
return create(row, "heading", nfc(row.section.title), level);
|
|
839
|
+
};
|
|
840
|
+
/** A section a source names, with the headings of its own below it. */
|
|
841
|
+
const sectionNode = (document, section) => {
|
|
842
|
+
const node = headingNode(document, section, 1);
|
|
843
|
+
const depths = headingDepths(section.heading.depth + 1, headings);
|
|
844
|
+
if (1 < depth && depths.length)
|
|
845
|
+
node.children = nest(document, collectSections(section.tree, { depth: depths }), 1);
|
|
846
|
+
return node;
|
|
847
|
+
};
|
|
848
|
+
const documentNode = (document, level) => {
|
|
849
|
+
// The `#` title is the line's title, and what follows it, up to the next
|
|
850
|
+
// `#`, is what the line summarizes; anything above it, such as an
|
|
851
|
+
// outliner's property lines, is not.
|
|
852
|
+
const tree = document.tree;
|
|
853
|
+
const found = titleHeading(tree);
|
|
854
|
+
const name = documentName(document.id);
|
|
855
|
+
const named = typeof document.frontmatter.title === "string"
|
|
856
|
+
? document.frontmatter.title.trim()
|
|
857
|
+
: "";
|
|
858
|
+
const title = (found && visibleHeadingText(found.heading)) ||
|
|
859
|
+
named ||
|
|
860
|
+
name;
|
|
861
|
+
const node = create({
|
|
862
|
+
document,
|
|
863
|
+
section: {
|
|
864
|
+
title,
|
|
865
|
+
tree: found
|
|
866
|
+
? {
|
|
867
|
+
...tree,
|
|
868
|
+
children: tree.children.slice(found.index, titleHeading(tree, found.index + 1)?.index),
|
|
869
|
+
}
|
|
870
|
+
: tree,
|
|
871
|
+
},
|
|
872
|
+
url: document.route,
|
|
873
|
+
}, "document", name, level);
|
|
874
|
+
if (level >= depth)
|
|
875
|
+
return node;
|
|
876
|
+
const sections = headings
|
|
877
|
+
? nest(document, collectSections(tree, { depth: headingDepths(2, headings) }), level)
|
|
878
|
+
: [];
|
|
879
|
+
node.children = [
|
|
880
|
+
...sections,
|
|
881
|
+
...sorted(hierarchy
|
|
882
|
+
.children(document)
|
|
883
|
+
.filter(listed)
|
|
884
|
+
.map((child) => documentNode(child, level + 1))),
|
|
885
|
+
];
|
|
886
|
+
return node;
|
|
887
|
+
};
|
|
888
|
+
const first = [];
|
|
889
|
+
for (const reference of sources) {
|
|
890
|
+
const source = resolveTreeSource(library, reference, from, context.documentId);
|
|
891
|
+
if ("folder" in source)
|
|
892
|
+
first.push(...sorted(source.documents
|
|
893
|
+
.filter(listed)
|
|
894
|
+
.map((document) => documentNode(document, 1))));
|
|
895
|
+
else if (source.anchor === undefined)
|
|
896
|
+
first.push(documentNode(source.document, 1));
|
|
897
|
+
else
|
|
898
|
+
first.push(...collectSections(source.document.tree, {
|
|
899
|
+
anchors: [source.anchor],
|
|
900
|
+
}).map((section) => sectionNode(source.document, section)));
|
|
901
|
+
}
|
|
902
|
+
let nodes = first;
|
|
903
|
+
if (render.order?.length) {
|
|
904
|
+
const order = render.order;
|
|
905
|
+
const rest = order.indexOf(REST);
|
|
906
|
+
const rank = (node) => {
|
|
907
|
+
const at = order.findIndex((entry, index) => index !== rest && namesTreeNode(entry, node));
|
|
908
|
+
return at >= 0 ? at : rest >= 0 ? rest : order.length;
|
|
909
|
+
};
|
|
910
|
+
nodes = first
|
|
911
|
+
.map((node, index) => ({ node, index, rank: rank(node) }))
|
|
912
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
913
|
+
.map(({ node }) => node);
|
|
914
|
+
}
|
|
915
|
+
if (cells) {
|
|
916
|
+
// Deepest first, so an extractor finds the lines below complete.
|
|
917
|
+
const fill = (node) => {
|
|
918
|
+
node.children.forEach(fill);
|
|
919
|
+
const row = rows.get(node);
|
|
920
|
+
node.cells = columns.map((column) => extractCell(library, column, row, context, node).cell);
|
|
921
|
+
};
|
|
922
|
+
nodes.forEach(fill);
|
|
923
|
+
}
|
|
924
|
+
return { nodes, documents };
|
|
925
|
+
}
|
|
926
|
+
/** Documents in the order a tree lists them: by title, then name, then id. */
|
|
927
|
+
const sorted = (nodes) => nodes.sort((a, b) => compareNames(a.title, b.title) ||
|
|
928
|
+
compareNames(a.name, b.name) ||
|
|
929
|
+
compareCodePoints(a.id, b.id));
|
|
930
|
+
/**
|
|
931
|
+
* The lines of a tree embed as data, the same lines the renderer draws, for
|
|
932
|
+
* a program that writes the tree in a form of its own, such as an outliner's
|
|
933
|
+
* blocks. `spec` is an embed whose render is a tree; sources resolve from
|
|
934
|
+
* `context.documentId`, which extractors also receive. Nodes come in the
|
|
935
|
+
* order the tree shows them, every level down to `depth`, whatever `open`
|
|
936
|
+
* and `print` say; equal input gives equal output, order and all.
|
|
937
|
+
*/
|
|
938
|
+
export function resolveTree(library, input, context) {
|
|
939
|
+
const spec = parseEmbedSpec(JSON.stringify(input));
|
|
940
|
+
if (typeof spec.render !== "object" || spec.render.type !== "tree")
|
|
941
|
+
throw new Error("cudoc: resolveTree needs an embed whose render is a tree");
|
|
942
|
+
return buildTree(library, spec.sources, spec.render, context.documentId, context).nodes;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* The mdast a tree renders to: nested lists, an item with children wrapped
|
|
946
|
+
* in a `details` element whose `summary` is the item's line, open down to
|
|
947
|
+
* `open` levels. Portable elements only, so it works without a script on
|
|
948
|
+
* every host. The outer list carries the `cudoc-tree` class, `data.cudoc.kind`
|
|
949
|
+
* `tree` and, when `print` is set, `data-cudoc-print`, which the print HTML
|
|
950
|
+
* and Word read.
|
|
951
|
+
*/
|
|
952
|
+
export function buildEmbedTree(nodes, render) {
|
|
953
|
+
const open = render.open ?? 1;
|
|
954
|
+
const text = (value) => ({ type: "text", value });
|
|
955
|
+
const line = (node) => {
|
|
956
|
+
const shown = node.cells.filter((cell) => cell.text);
|
|
957
|
+
// A line has to say something: with every column empty, the title.
|
|
958
|
+
return {
|
|
959
|
+
type: "paragraph",
|
|
960
|
+
children: (shown.length ? shown : [{ text: node.title }]).flatMap((cell, index) => [
|
|
961
|
+
...(index ? [text(" · ")] : []),
|
|
962
|
+
cell.url
|
|
963
|
+
? { type: "link", url: cell.url, children: [text(cell.text)] }
|
|
964
|
+
: text(cell.text),
|
|
965
|
+
]),
|
|
966
|
+
};
|
|
967
|
+
};
|
|
968
|
+
const list = (items, outer) => ({
|
|
969
|
+
type: "list",
|
|
970
|
+
ordered: false,
|
|
971
|
+
spread: false,
|
|
972
|
+
...(outer
|
|
973
|
+
? {
|
|
974
|
+
data: {
|
|
975
|
+
hProperties: {
|
|
976
|
+
className: [TREE_CLASS],
|
|
977
|
+
...(render.print ? { [TREE_PRINT_ATTRIBUTE]: render.print } : {}),
|
|
978
|
+
},
|
|
979
|
+
cudoc: { kind: TREE_KIND },
|
|
980
|
+
},
|
|
981
|
+
}
|
|
982
|
+
: {}),
|
|
983
|
+
children: items.map((node) => node.children.length
|
|
984
|
+
? {
|
|
985
|
+
type: "listItem",
|
|
986
|
+
spread: false,
|
|
987
|
+
children: [
|
|
988
|
+
{
|
|
989
|
+
type: "blockquote",
|
|
990
|
+
data: {
|
|
991
|
+
hName: "details",
|
|
992
|
+
...(node.level <= open
|
|
993
|
+
? { hProperties: { open: true } }
|
|
994
|
+
: {}),
|
|
995
|
+
},
|
|
996
|
+
children: [
|
|
997
|
+
{ ...line(node), data: { hName: "summary" } },
|
|
998
|
+
list(node.children, false),
|
|
999
|
+
],
|
|
1000
|
+
},
|
|
1001
|
+
],
|
|
1002
|
+
}
|
|
1003
|
+
: {
|
|
1004
|
+
type: "listItem",
|
|
1005
|
+
spread: false,
|
|
1006
|
+
data: { hProperties: { className: [`${TREE_CLASS}-leaf`] } },
|
|
1007
|
+
children: [line(node)],
|
|
1008
|
+
}),
|
|
1009
|
+
});
|
|
1010
|
+
return { type: "root", children: [list(nodes, true)] };
|
|
1011
|
+
}
|
|
491
1012
|
/** Resolve a configured embed from immutable persisted documents. */
|
|
492
1013
|
export function resolveEmbed(library, input, context) {
|
|
493
1014
|
const spec = parseEmbedSpec(JSON.stringify(input));
|
|
494
1015
|
let occurrence = 0;
|
|
495
1016
|
const reserved = new Set();
|
|
496
|
-
const destination = library.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
1017
|
+
const destination = documentIndex(library).byId.get(context.documentId);
|
|
1018
|
+
// Nodes already on the page: those a copy rebased, whose paths and
|
|
1019
|
+
// required modules now start from `destination`, and those a table or a
|
|
1020
|
+
// tree wrote there. A copy around them renames their ids, and leaves the
|
|
1021
|
+
// rest as it is.
|
|
1022
|
+
const placed = new WeakSet();
|
|
1023
|
+
const place = (root) => {
|
|
1024
|
+
visitNodes(root, (node) => {
|
|
1025
|
+
placed.add(node);
|
|
500
1026
|
});
|
|
1027
|
+
return root;
|
|
1028
|
+
};
|
|
1029
|
+
if (destination)
|
|
1030
|
+
for (const id of declaredIds(destination.tree))
|
|
1031
|
+
reserved.add(id);
|
|
501
1032
|
const active = [];
|
|
502
1033
|
// Every document a block read, so a later preparation can tell whether the
|
|
503
1034
|
// block is still current; `*` when an extractor ran, which may read anything.
|
|
504
1035
|
const dependencies = new Set();
|
|
505
1036
|
const expand = (spec, from) => {
|
|
1037
|
+
// A tree copies no section, so nothing in it expands or cycles; what it
|
|
1038
|
+
// depends on is every document a line was read from.
|
|
1039
|
+
if (typeof spec.render === "object" && spec.render.type === "tree") {
|
|
1040
|
+
const tree = buildTree(library, spec.sources, spec.render, from, context);
|
|
1041
|
+
tree.documents.forEach((id) => dependencies.add(id));
|
|
1042
|
+
if (usesExtractor(spec.render.columns ?? DEFAULT_TREE_COLUMNS))
|
|
1043
|
+
dependencies.add("*");
|
|
1044
|
+
return place(buildEmbedTree(tree.nodes, spec.render));
|
|
1045
|
+
}
|
|
506
1046
|
const children = [];
|
|
507
1047
|
const rows = [];
|
|
508
1048
|
for (const source of spec.sources) {
|
|
@@ -551,7 +1091,7 @@ export function resolveEmbed(library, input, context) {
|
|
|
551
1091
|
do {
|
|
552
1092
|
prefix = `${context.prefix ?? "embed"}-${++occurrence}-`;
|
|
553
1093
|
} while (sectionIds.some((id) => reserved.has(`${prefix}${id}`)));
|
|
554
|
-
rebase(section, document, library, prefix);
|
|
1094
|
+
rebase(section, document, library, prefix, destination, placed);
|
|
555
1095
|
sectionIds.forEach((id) => reserved.add(`${prefix}${id}`));
|
|
556
1096
|
children.push(...section.children);
|
|
557
1097
|
}
|
|
@@ -562,24 +1102,22 @@ export function resolveEmbed(library, input, context) {
|
|
|
562
1102
|
}
|
|
563
1103
|
if (spec.render && spec.render !== "section") {
|
|
564
1104
|
const columns = spec.render.columns ?? DEFAULT_TABLE_COLUMNS;
|
|
565
|
-
if (columns
|
|
566
|
-
typeof column.value === "object" &&
|
|
567
|
-
"extractor" in column.value))
|
|
1105
|
+
if (usesExtractor(columns))
|
|
568
1106
|
dependencies.add("*");
|
|
569
|
-
return buildEmbedTable(library, columns, rows, context);
|
|
1107
|
+
return place(buildEmbedTable(library, columns, rows, context));
|
|
570
1108
|
}
|
|
571
1109
|
return { type: "root", children };
|
|
572
1110
|
};
|
|
573
1111
|
const result = expand(spec, context.documentId);
|
|
574
1112
|
result.data = {
|
|
575
1113
|
...result.data,
|
|
576
|
-
cudocEmbedPrefix: `cudoc-${
|
|
1114
|
+
cudocEmbedPrefix: `cudoc-${idToken(context.documentId)}-${context.prefix ?? "embed"}-`,
|
|
577
1115
|
cudocDependencies: [...dependencies].sort(),
|
|
578
1116
|
};
|
|
579
1117
|
return result;
|
|
580
1118
|
}
|
|
581
1119
|
export function resolveDocumentEmbeds(library, documentId) {
|
|
582
|
-
const document = library.
|
|
1120
|
+
const document = documentIndex(library).byId.get(documentId);
|
|
583
1121
|
if (!document)
|
|
584
1122
|
throw new Error(`cudoc: missing document ${documentId}`);
|
|
585
1123
|
const tree = structuredClone(document.tree);
|