@cudoment/cudoc 0.6.0 → 0.8.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/dist/node/check.d.ts +4 -3
- package/dist/node/check.d.ts.map +1 -1
- package/dist/node/check.js +108 -1
- package/dist/node/check.js.map +1 -1
- package/dist/node/library.d.ts +2 -0
- package/dist/node/library.d.ts.map +1 -1
- package/dist/node/library.js +2 -0
- package/dist/node/library.js.map +1 -1
- 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 +104 -2
- package/dist/node/resolve-embed.d.ts.map +1 -1
- package/dist/node/resolve-embed.js +444 -30
- package/dist/node/resolve-embed.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/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/package.json +1 -1
- package/styles.css +49 -0
|
@@ -1,14 +1,33 @@
|
|
|
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";
|
|
4
|
+
import { collectSections, } from "../sections.js";
|
|
5
5
|
import { idToken, nodeText, visibleHeadingText, } from "../document.js";
|
|
6
6
|
import { findHeadingByAnchorId, findParentHeading, getHeadingAnchorId, } from "../internal/core/query/sections.js";
|
|
7
|
+
import { TREE_CLASS, TREE_KIND, TREE_PRINT_ATTRIBUTE } from "../paged.js";
|
|
7
8
|
import { sourceFileOf } from "./roots.js";
|
|
9
|
+
import { compareCodePoints, compareNames, documentName, hierarchyOf, nfc, } from "./tree.js";
|
|
10
|
+
/**
|
|
11
|
+
* How a tree names and sorts documents, for a consumer that lists documents
|
|
12
|
+
* the way a tree embed does, such as an export's navigation.
|
|
13
|
+
*/
|
|
14
|
+
export { compareNames, documentName, nfc } from "./tree.js";
|
|
8
15
|
import { parseSrcSet } from "./local-target.js";
|
|
9
16
|
import { transformedSection } from "./replace.js";
|
|
10
17
|
import { EXTERNAL_URL, decodeComponent, declaredIds, documentIndex, idsInNode, } from "./references.js";
|
|
11
18
|
const SPEC_KEYS = ["sources", "select", "render", "replace"];
|
|
19
|
+
const TABLE_KEYS = ["type", "columns"];
|
|
20
|
+
const TREE_KEYS = [
|
|
21
|
+
"type",
|
|
22
|
+
"open",
|
|
23
|
+
"print",
|
|
24
|
+
"depth",
|
|
25
|
+
"headings",
|
|
26
|
+
"order",
|
|
27
|
+
"columns",
|
|
28
|
+
];
|
|
29
|
+
/** The `order` entry that stands for every name the list does not give. */
|
|
30
|
+
const REST = "...";
|
|
12
31
|
const SELECTION_KEYS = ["anchors", "titles", "depth", "includeChildren"];
|
|
13
32
|
const REPLACEMENT_KEYS = ["find", "replace", "regex", "flags"];
|
|
14
33
|
const COLUMN_KEYS = ["header", "value", "link", "minWidth"];
|
|
@@ -64,6 +83,38 @@ const validateColumn = (column, at) => {
|
|
|
64
83
|
cell.skipTablesWithHeaders.some((h) => typeof h !== "string")))
|
|
65
84
|
throw new Error(`cudoc: ${at}.value.skipTablesWithHeaders must be strings`);
|
|
66
85
|
};
|
|
86
|
+
const validateTree = (render) => {
|
|
87
|
+
const count = (key, least, most) => {
|
|
88
|
+
const value = render[key];
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return;
|
|
91
|
+
if (!Number.isInteger(value) ||
|
|
92
|
+
value < least ||
|
|
93
|
+
(most !== undefined && value > most))
|
|
94
|
+
throw new Error(`cudoc: render.${key} must be ${most === undefined
|
|
95
|
+
? `an integer of at least ${least}`
|
|
96
|
+
: `an integer from ${least} to ${most}`}`);
|
|
97
|
+
};
|
|
98
|
+
count("open", 0);
|
|
99
|
+
count("print", 1);
|
|
100
|
+
count("depth", 1);
|
|
101
|
+
count("headings", 0, 5);
|
|
102
|
+
const order = render.order;
|
|
103
|
+
if (order === undefined)
|
|
104
|
+
return;
|
|
105
|
+
if (!Array.isArray(order) ||
|
|
106
|
+
order.some((name) => typeof name !== "string" || !name.trim()))
|
|
107
|
+
throw new Error("cudoc: render.order must be a list of names");
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
for (const name of order) {
|
|
110
|
+
const key = nfc(name);
|
|
111
|
+
if (seen.has(key))
|
|
112
|
+
throw new Error(name === REST
|
|
113
|
+
? `cudoc: render.order has ${REST} twice; it stands for every name not listed, once`
|
|
114
|
+
: `cudoc: render.order names "${name}" twice`);
|
|
115
|
+
seen.add(key);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
67
118
|
export function parseEmbedSpec(value) {
|
|
68
119
|
const spec = parseYaml(value, { maxAliasCount: 100 });
|
|
69
120
|
if (!spec || typeof spec !== "object" || Array.isArray(spec))
|
|
@@ -79,16 +130,26 @@ export function parseEmbedSpec(value) {
|
|
|
79
130
|
if (!spec.render ||
|
|
80
131
|
typeof spec.render !== "object" ||
|
|
81
132
|
Array.isArray(spec.render) ||
|
|
82
|
-
spec.render.type
|
|
83
|
-
throw new Error('cudoc: render must be "section" or a mapping with type: table');
|
|
133
|
+
!["table", "tree"].includes(spec.render.type))
|
|
134
|
+
throw new Error('cudoc: render must be "section" or a mapping with type: table or type: tree');
|
|
135
|
+
const known = spec.render.type === "tree" ? TREE_KEYS : TABLE_KEYS;
|
|
84
136
|
for (const key of Object.keys(spec.render))
|
|
85
|
-
if (!
|
|
86
|
-
throw new Error(`cudoc: render: unknown key "${key}". Known keys:
|
|
137
|
+
if (!known.includes(key))
|
|
138
|
+
throw new Error(`cudoc: render: unknown key "${key}". Known keys: ${known.join(", ")}`);
|
|
87
139
|
if (spec.render.columns !== undefined) {
|
|
88
140
|
if (!Array.isArray(spec.render.columns) || !spec.render.columns.length)
|
|
89
141
|
throw new Error("cudoc: render.columns must be a non-empty array");
|
|
90
142
|
spec.render.columns.forEach((column, index) => validateColumn(column, `render.columns[${index}]`));
|
|
91
143
|
}
|
|
144
|
+
if (spec.render.type === "tree") {
|
|
145
|
+
validateTree(spec.render);
|
|
146
|
+
// A tree copies no section text, so neither would change anything,
|
|
147
|
+
// and an author who wrote one expects it to.
|
|
148
|
+
if (spec.select !== undefined)
|
|
149
|
+
throw new Error("cudoc: select does not apply to a tree; its levels come from folders, and render.headings adds sections");
|
|
150
|
+
if (spec.replace !== undefined)
|
|
151
|
+
throw new Error("cudoc: replace does not apply to a tree, which copies no section text");
|
|
152
|
+
}
|
|
92
153
|
}
|
|
93
154
|
if (spec.select) {
|
|
94
155
|
if (typeof spec.select !== "object" || Array.isArray(spec.select))
|
|
@@ -165,7 +226,13 @@ export function parseEmbedBlock(value, documentId, number) {
|
|
|
165
226
|
throw new Error(`cudoc: ${where}: ${message}`, { cause });
|
|
166
227
|
}
|
|
167
228
|
}
|
|
168
|
-
|
|
229
|
+
/**
|
|
230
|
+
* What a source names, read without looking anything up: the library path it
|
|
231
|
+
* spells, relative to `from` or from the top with a leading `/`, and its
|
|
232
|
+
* anchor, percent-decoded. A URL, a backslash and a climb out of the library
|
|
233
|
+
* fail here.
|
|
234
|
+
*/
|
|
235
|
+
const sourcePath = (reference, from) => {
|
|
169
236
|
const hash = reference.indexOf("#");
|
|
170
237
|
const pathname = hash < 0 ? reference : reference.slice(0, hash);
|
|
171
238
|
let anchor;
|
|
@@ -178,18 +245,66 @@ export function resolveDocumentReference(library, reference, from) {
|
|
|
178
245
|
}
|
|
179
246
|
if (/^[a-z][\w+.-]*:/i.test(pathname) || pathname.includes("\\"))
|
|
180
247
|
throw new Error(`cudoc: embed source must be a local document: ${reference}`);
|
|
181
|
-
const
|
|
248
|
+
const target = pathname
|
|
182
249
|
? pathname.startsWith("/")
|
|
183
250
|
? pathname.slice(1)
|
|
184
251
|
: path.posix.join(path.posix.dirname(from), pathname)
|
|
185
|
-
: from
|
|
252
|
+
: from;
|
|
253
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
186
254
|
if (id === ".." || id.startsWith("../"))
|
|
187
255
|
throw new Error(`cudoc: embed source escapes root: ${reference}`);
|
|
256
|
+
return { pathname, target, ...(anchor === undefined ? {} : { anchor }) };
|
|
257
|
+
};
|
|
258
|
+
export function resolveDocumentReference(library, reference, from) {
|
|
259
|
+
const { target, anchor } = sourcePath(reference, from);
|
|
260
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
188
261
|
const document = documentIndex(library).byId.get(id);
|
|
189
262
|
if (!document)
|
|
190
263
|
throw new Error(`cudoc: missing document ${reference} referenced from ${from}`);
|
|
191
264
|
return { document, anchor };
|
|
192
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Whether a tree on `page` lists private documents: only when the page is
|
|
268
|
+
* private itself. One that names no collected document counts as public.
|
|
269
|
+
*/
|
|
270
|
+
const listsPrivate = (library, page) => documentIndex(library).byId.get(page)?.private === true;
|
|
271
|
+
/**
|
|
272
|
+
* Reads one source of a tree. A path ending in `/` names a folder, and the
|
|
273
|
+
* documents directly below it become the tree's first level; any other path
|
|
274
|
+
* names a document, or with `#anchor` one of its sections, as for any embed.
|
|
275
|
+
* Ids are matched in NFC, so a source names a document whose file name is
|
|
276
|
+
* stored in NFD. A folder holding no document fails, and so does a document
|
|
277
|
+
* path that is really a folder, with a hint to add the `/`. Given `page`,
|
|
278
|
+
* the document the tree lands on, a folder whose documents are all private
|
|
279
|
+
* fails too unless that page is private, since the tree leaves them out.
|
|
280
|
+
*/
|
|
281
|
+
export function resolveTreeSource(library, reference, from, page) {
|
|
282
|
+
const { pathname, target, anchor } = sourcePath(reference, from);
|
|
283
|
+
const hierarchy = hierarchyOf(library);
|
|
284
|
+
if (pathname.endsWith("/")) {
|
|
285
|
+
if (anchor !== undefined)
|
|
286
|
+
throw new Error(`cudoc: a folder source takes no anchor: ${reference}`);
|
|
287
|
+
const normal = path.posix.normalize(target || ".").replace(/\/+$/, "");
|
|
288
|
+
if (normal === ".." || normal.startsWith("../"))
|
|
289
|
+
throw new Error(`cudoc: embed source escapes root: ${reference}`);
|
|
290
|
+
const folder = normal === "." ? "" : normal;
|
|
291
|
+
const documents = hierarchy.below(folder);
|
|
292
|
+
if (!documents.length)
|
|
293
|
+
throw new Error(`cudoc: no documents in folder ${reference} referenced from ${from}`);
|
|
294
|
+
if (page !== undefined &&
|
|
295
|
+
!listsPrivate(library, page) &&
|
|
296
|
+
documents.every((document) => document.private))
|
|
297
|
+
throw new Error(`cudoc: folder ${reference} referenced from ${from} holds only private documents, which a tree on ${page} leaves out`);
|
|
298
|
+
return { folder, documents };
|
|
299
|
+
}
|
|
300
|
+
const id = target.replace(/\.mdx?$/i, "");
|
|
301
|
+
const document = documentIndex(library).byId.get(id) ?? hierarchy.document(id);
|
|
302
|
+
if (document)
|
|
303
|
+
return { document, ...(anchor === undefined ? {} : { anchor }) };
|
|
304
|
+
throw new Error(`cudoc: missing document ${reference} referenced from ${from}${hierarchy.below(path.posix.normalize(id)).length
|
|
305
|
+
? `; a folder source ends with /, as in ${pathname}/`
|
|
306
|
+
: ""}`);
|
|
307
|
+
}
|
|
193
308
|
const visitNodes = (node, fn) => {
|
|
194
309
|
fn(node);
|
|
195
310
|
node.children?.forEach((n) => visitNodes(n, fn));
|
|
@@ -345,7 +460,7 @@ const rewriteAttributes = (tag, rewrite) => {
|
|
|
345
460
|
}
|
|
346
461
|
return result + tag.slice(index);
|
|
347
462
|
};
|
|
348
|
-
function rebase(tree, document, library, prefix, destination,
|
|
463
|
+
function rebase(tree, document, library, prefix, destination, placed) {
|
|
349
464
|
const ids = new Set();
|
|
350
465
|
visitNodes(tree, (node) => {
|
|
351
466
|
idsInNode(node).forEach((id) => ids.add(id));
|
|
@@ -398,25 +513,35 @@ function rebase(tree, document, library, prefix, destination, moved) {
|
|
|
398
513
|
const target = documentIndex(library).byPath.get(absolute);
|
|
399
514
|
return `${target?.route ?? absolute}${match[2]}`;
|
|
400
515
|
};
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
516
|
+
// A node an inner copy already placed holds the page's addresses, and read
|
|
517
|
+
// as source paths again they could name another document, or climb from
|
|
518
|
+
// the wrong directory. Only a fragment naming an id this copy renames
|
|
519
|
+
// follows the new name.
|
|
520
|
+
const pageUrl = (url) => {
|
|
521
|
+
if (!url.startsWith("#"))
|
|
522
|
+
return url;
|
|
523
|
+
const id = decodeComponent(url.slice(1));
|
|
524
|
+
return ids.has(id) ? `#${prefix}${id}` : url;
|
|
525
|
+
};
|
|
406
526
|
visitNodes(tree, (node) => {
|
|
527
|
+
const url = placed.has(node) ? pageUrl : sourceUrl;
|
|
528
|
+
// Every candidate of a responsive image is a path of its own; the width
|
|
529
|
+
// or density after it stays as written.
|
|
530
|
+
const srcSet = (value) => parseSrcSet(value)
|
|
531
|
+
.map(({ url: candidate, descriptor }) => [url(candidate), descriptor].filter(Boolean).join(" "))
|
|
532
|
+
.join(", ");
|
|
407
533
|
const id = node.data?.hProperties?.id;
|
|
408
534
|
if (typeof id === "string")
|
|
409
535
|
node.data.hProperties.id = `${prefix}${id}`;
|
|
410
536
|
if (typeof node.url === "string")
|
|
411
|
-
node.url =
|
|
537
|
+
node.url = url(node.url);
|
|
412
538
|
// An image a host made a component of is still an image of the source.
|
|
413
539
|
const image = node.data?.cudocImage;
|
|
414
540
|
if (image && typeof image.url === "string")
|
|
415
|
-
node.data.cudocImage = { ...image, url:
|
|
541
|
+
node.data.cudocImage = { ...image, url: url(image.url) };
|
|
416
542
|
// Once per element, however deeply the copy was nested: an inner embed
|
|
417
543
|
// already moved it from its own source to `destination`.
|
|
418
|
-
if (destination && Array.isArray(node.attributes) && !
|
|
419
|
-
moved.add(node);
|
|
544
|
+
if (destination && Array.isArray(node.attributes) && !placed.has(node)) {
|
|
420
545
|
directories ??= {
|
|
421
546
|
from: fileDirectory(library, document),
|
|
422
547
|
to: fileDirectory(library, destination),
|
|
@@ -439,17 +564,18 @@ function rebase(tree, document, library, prefix, destination, moved) {
|
|
|
439
564
|
: rewriteAttributes(tag, (name, value) => name === "id"
|
|
440
565
|
? `${prefix}${value}`
|
|
441
566
|
: name === "srcset"
|
|
442
|
-
?
|
|
567
|
+
? srcSet(value)
|
|
443
568
|
: REBASED_ATTRIBUTES.has(name)
|
|
444
|
-
?
|
|
569
|
+
? url(value)
|
|
445
570
|
: undefined));
|
|
446
571
|
const attrs = node.data?.hProperties;
|
|
447
572
|
for (const key of ["href", "src", "poster", "data", "xLinkHref"])
|
|
448
573
|
if (typeof attrs?.[key] === "string")
|
|
449
|
-
attrs[key] =
|
|
574
|
+
attrs[key] = url(attrs[key]);
|
|
450
575
|
for (const key of ["srcSet", "srcset"])
|
|
451
576
|
if (typeof attrs?.[key] === "string")
|
|
452
|
-
attrs[key] =
|
|
577
|
+
attrs[key] = srcSet(attrs[key]);
|
|
578
|
+
placed.add(node);
|
|
453
579
|
});
|
|
454
580
|
}
|
|
455
581
|
/** The default columns of a summary table, as they have always been. */
|
|
@@ -514,7 +640,9 @@ const sectionTables = (tree, skipHeaders) => {
|
|
|
514
640
|
* problem, which is worded as what the column asked for and what the section
|
|
515
641
|
* has, so an author can tell a wrong coordinate from a missing table.
|
|
516
642
|
*/
|
|
517
|
-
export function extractCell(library, column, row, context
|
|
643
|
+
export function extractCell(library, column, row, context,
|
|
644
|
+
/** The tree line the cell is for, handed to an extractor. */
|
|
645
|
+
node) {
|
|
518
646
|
const spec = columnSpec(column);
|
|
519
647
|
const where = `${row.document.id}${row.section.anchorId ? `#${row.section.anchorId}` : ""}`;
|
|
520
648
|
const linkTo = () => {
|
|
@@ -556,6 +684,7 @@ export function extractCell(library, column, row, context) {
|
|
|
556
684
|
library,
|
|
557
685
|
documentId: context.documentId,
|
|
558
686
|
column,
|
|
687
|
+
...(node ? { node } : {}),
|
|
559
688
|
});
|
|
560
689
|
const text = typeof result === "string" ? result : result?.text;
|
|
561
690
|
if (!text)
|
|
@@ -616,14 +745,292 @@ export function buildEmbedTable(library, columns, rows, context) {
|
|
|
616
745
|
],
|
|
617
746
|
};
|
|
618
747
|
}
|
|
748
|
+
/** The default line of a tree: the title linked to its node, then the summary. */
|
|
749
|
+
export const DEFAULT_TREE_COLUMNS = ["link", "summary"];
|
|
750
|
+
/** Whether a column is computed by a registered function, which may read anything. */
|
|
751
|
+
const usesExtractor = (columns) => columns.some((column) => typeof column === "object" &&
|
|
752
|
+
typeof column.value === "object" &&
|
|
753
|
+
"extractor" in column.value);
|
|
754
|
+
/** Whether an `order` entry names a node: its name, or its title, in NFC. */
|
|
755
|
+
export const namesTreeNode = (entry, node) => {
|
|
756
|
+
const name = nfc(entry);
|
|
757
|
+
return node.name === name || nfc(node.title) === name;
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* A document's `#` title, from the top-level block at `from` on: a `#`
|
|
761
|
+
* heading there, or one inside a top-level `header` element, where Docusaurus
|
|
762
|
+
* puts the heading it reads the page title from.
|
|
763
|
+
*/
|
|
764
|
+
const titleHeading = (tree, from = 0) => {
|
|
765
|
+
for (let index = from; index < tree.children.length; index++) {
|
|
766
|
+
const node = tree.children[index];
|
|
767
|
+
const heading = node.type === "heading"
|
|
768
|
+
? node
|
|
769
|
+
: node.data?.hName === "header"
|
|
770
|
+
? node.children?.find((child) => child.type === "heading")
|
|
771
|
+
: undefined;
|
|
772
|
+
if (heading?.depth === 1)
|
|
773
|
+
return {
|
|
774
|
+
heading: heading,
|
|
775
|
+
index,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return undefined;
|
|
779
|
+
};
|
|
780
|
+
/** Heading depths from `from` to the deepest `headings` reaches, `##` being 2. */
|
|
781
|
+
const headingDepths = (from, headings) => {
|
|
782
|
+
const depths = [];
|
|
783
|
+
for (let depth = Math.max(from, 2); depth <= headings + 1; depth++)
|
|
784
|
+
depths.push(depth);
|
|
785
|
+
return depths;
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* The lines of a tree, without cells when `cells` is false, and every
|
|
789
|
+
* document they were read from.
|
|
790
|
+
*/
|
|
791
|
+
function buildTree(library, sources, render, from, context, cells = true) {
|
|
792
|
+
const hierarchy = hierarchyOf(library);
|
|
793
|
+
const depth = render.depth ?? Infinity;
|
|
794
|
+
const headings = render.headings ?? 0;
|
|
795
|
+
const columns = render.columns ?? DEFAULT_TREE_COLUMNS;
|
|
796
|
+
const rows = new Map();
|
|
797
|
+
const documents = new Set();
|
|
798
|
+
// A page that is not private lists no private document it was not asked
|
|
799
|
+
// for by name: a folder or a parent would otherwise put one on it, and the
|
|
800
|
+
// export, which leaves private documents out, could not link to it.
|
|
801
|
+
const privateListed = listsPrivate(library, context.documentId);
|
|
802
|
+
const listed = (document) => !document.private || privateListed;
|
|
803
|
+
const create = (row, kind, name, level) => {
|
|
804
|
+
documents.add(row.document.id);
|
|
805
|
+
const node = {
|
|
806
|
+
id: `${row.document.id}${row.section.anchorId ? `#${row.section.anchorId}` : ""}`,
|
|
807
|
+
kind,
|
|
808
|
+
documentId: row.document.id,
|
|
809
|
+
...(row.section.anchorId ? { anchorId: row.section.anchorId } : {}),
|
|
810
|
+
name,
|
|
811
|
+
title: row.section.title,
|
|
812
|
+
url: row.url,
|
|
813
|
+
sourcePath: row.document.sourcePath,
|
|
814
|
+
level,
|
|
815
|
+
cells: [],
|
|
816
|
+
children: [],
|
|
817
|
+
};
|
|
818
|
+
rows.set(node, row);
|
|
819
|
+
return node;
|
|
820
|
+
};
|
|
821
|
+
/** The headings of `sections` nested by depth under a line at `level`. */
|
|
822
|
+
const nest = (document, sections, level) => {
|
|
823
|
+
const top = [];
|
|
824
|
+
// A heading past `depth` is still pushed, without a node, so the ones
|
|
825
|
+
// under it are left out with it rather than moved up a level.
|
|
826
|
+
const stack = [];
|
|
827
|
+
for (const section of sections) {
|
|
828
|
+
while (stack.length && stack.at(-1).depth >= section.heading.depth)
|
|
829
|
+
stack.pop();
|
|
830
|
+
const above = stack.at(-1);
|
|
831
|
+
const at = (above?.level ?? level) + 1;
|
|
832
|
+
const node = at <= depth && (!above || above.node)
|
|
833
|
+
? headingNode(document, section, at)
|
|
834
|
+
: undefined;
|
|
835
|
+
stack.push({ depth: section.heading.depth, level: at, node });
|
|
836
|
+
if (node)
|
|
837
|
+
(above ? above.node.children : top).push(node);
|
|
838
|
+
}
|
|
839
|
+
return top;
|
|
840
|
+
};
|
|
841
|
+
const headingNode = (document, section, level) => {
|
|
842
|
+
const row = buildEmbedRow(document, section.anchorId, section.tree);
|
|
843
|
+
return create(row, "heading", nfc(row.section.title), level);
|
|
844
|
+
};
|
|
845
|
+
/** A section a source names, with the headings of its own below it. */
|
|
846
|
+
const sectionNode = (document, section) => {
|
|
847
|
+
const node = headingNode(document, section, 1);
|
|
848
|
+
const depths = headingDepths(section.heading.depth + 1, headings);
|
|
849
|
+
if (1 < depth && depths.length)
|
|
850
|
+
node.children = nest(document, collectSections(section.tree, { depth: depths }), 1);
|
|
851
|
+
return node;
|
|
852
|
+
};
|
|
853
|
+
const documentNode = (document, level) => {
|
|
854
|
+
// The `#` title is the line's title, and what follows it, up to the next
|
|
855
|
+
// `#`, is what the line summarizes; anything above it, such as an
|
|
856
|
+
// outliner's property lines, is not.
|
|
857
|
+
const tree = document.tree;
|
|
858
|
+
const found = titleHeading(tree);
|
|
859
|
+
const name = documentName(document.id);
|
|
860
|
+
const named = typeof document.frontmatter.title === "string"
|
|
861
|
+
? document.frontmatter.title.trim()
|
|
862
|
+
: "";
|
|
863
|
+
const title = (found && visibleHeadingText(found.heading)) ||
|
|
864
|
+
named ||
|
|
865
|
+
name;
|
|
866
|
+
const node = create({
|
|
867
|
+
document,
|
|
868
|
+
section: {
|
|
869
|
+
title,
|
|
870
|
+
tree: found
|
|
871
|
+
? {
|
|
872
|
+
...tree,
|
|
873
|
+
children: tree.children.slice(found.index, titleHeading(tree, found.index + 1)?.index),
|
|
874
|
+
}
|
|
875
|
+
: tree,
|
|
876
|
+
},
|
|
877
|
+
url: document.route,
|
|
878
|
+
}, "document", name, level);
|
|
879
|
+
if (level >= depth)
|
|
880
|
+
return node;
|
|
881
|
+
const sections = headings
|
|
882
|
+
? nest(document, collectSections(tree, { depth: headingDepths(2, headings) }), level)
|
|
883
|
+
: [];
|
|
884
|
+
node.children = [
|
|
885
|
+
...sections,
|
|
886
|
+
...sorted(hierarchy
|
|
887
|
+
.children(document)
|
|
888
|
+
.filter(listed)
|
|
889
|
+
.map((child) => documentNode(child, level + 1))),
|
|
890
|
+
];
|
|
891
|
+
return node;
|
|
892
|
+
};
|
|
893
|
+
const first = [];
|
|
894
|
+
for (const reference of sources) {
|
|
895
|
+
const source = resolveTreeSource(library, reference, from, context.documentId);
|
|
896
|
+
if ("folder" in source)
|
|
897
|
+
first.push(...sorted(source.documents
|
|
898
|
+
.filter(listed)
|
|
899
|
+
.map((document) => documentNode(document, 1))));
|
|
900
|
+
else if (source.anchor === undefined)
|
|
901
|
+
first.push(documentNode(source.document, 1));
|
|
902
|
+
else
|
|
903
|
+
first.push(...collectSections(source.document.tree, {
|
|
904
|
+
anchors: [source.anchor],
|
|
905
|
+
}).map((section) => sectionNode(source.document, section)));
|
|
906
|
+
}
|
|
907
|
+
let nodes = first;
|
|
908
|
+
if (render.order?.length) {
|
|
909
|
+
const order = render.order;
|
|
910
|
+
const rest = order.indexOf(REST);
|
|
911
|
+
const rank = (node) => {
|
|
912
|
+
const at = order.findIndex((entry, index) => index !== rest && namesTreeNode(entry, node));
|
|
913
|
+
return at >= 0 ? at : rest >= 0 ? rest : order.length;
|
|
914
|
+
};
|
|
915
|
+
nodes = first
|
|
916
|
+
.map((node, index) => ({ node, index, rank: rank(node) }))
|
|
917
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index)
|
|
918
|
+
.map(({ node }) => node);
|
|
919
|
+
}
|
|
920
|
+
if (cells) {
|
|
921
|
+
// Deepest first, so an extractor finds the lines below complete.
|
|
922
|
+
const fill = (node) => {
|
|
923
|
+
node.children.forEach(fill);
|
|
924
|
+
const row = rows.get(node);
|
|
925
|
+
node.cells = columns.map((column) => extractCell(library, column, row, context, node).cell);
|
|
926
|
+
};
|
|
927
|
+
nodes.forEach(fill);
|
|
928
|
+
}
|
|
929
|
+
return { nodes, documents };
|
|
930
|
+
}
|
|
931
|
+
/** Documents in the order a tree lists them: by title, then name, then id. */
|
|
932
|
+
const sorted = (nodes) => nodes.sort((a, b) => compareNames(a.title, b.title) ||
|
|
933
|
+
compareNames(a.name, b.name) ||
|
|
934
|
+
compareCodePoints(a.id, b.id));
|
|
935
|
+
/**
|
|
936
|
+
* The lines of a tree embed as data, the same lines the renderer draws, for
|
|
937
|
+
* a program that writes the tree in a form of its own, such as an outliner's
|
|
938
|
+
* blocks. `spec` is an embed whose render is a tree; sources resolve from
|
|
939
|
+
* `context.documentId`, which extractors also receive. Nodes come in the
|
|
940
|
+
* order the tree shows them, every level down to `depth`, whatever `open`
|
|
941
|
+
* and `print` say; equal input gives equal output, order and all.
|
|
942
|
+
*/
|
|
943
|
+
export function resolveTree(library, input, context) {
|
|
944
|
+
const spec = parseEmbedSpec(JSON.stringify(input));
|
|
945
|
+
if (typeof spec.render !== "object" || spec.render.type !== "tree")
|
|
946
|
+
throw new Error("cudoc: resolveTree needs an embed whose render is a tree");
|
|
947
|
+
return buildTree(library, spec.sources, spec.render, context.documentId, context).nodes;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* The mdast a tree renders to: nested lists, an item with children wrapped
|
|
951
|
+
* in a `details` element whose `summary` is the item's line, open down to
|
|
952
|
+
* `open` levels. Portable elements only, so it works without a script on
|
|
953
|
+
* every host. The outer list carries the `cudoc-tree` class, `data.cudoc.kind`
|
|
954
|
+
* `tree` and, when `print` is set, `data-cudoc-print`, which the print HTML
|
|
955
|
+
* and Word read.
|
|
956
|
+
*/
|
|
957
|
+
export function buildEmbedTree(nodes, render) {
|
|
958
|
+
const open = render.open ?? 1;
|
|
959
|
+
const text = (value) => ({ type: "text", value });
|
|
960
|
+
const line = (node) => {
|
|
961
|
+
const shown = node.cells.filter((cell) => cell.text);
|
|
962
|
+
// A line has to say something: with every column empty, the title.
|
|
963
|
+
return {
|
|
964
|
+
type: "paragraph",
|
|
965
|
+
children: (shown.length ? shown : [{ text: node.title }]).flatMap((cell, index) => [
|
|
966
|
+
...(index ? [text(" · ")] : []),
|
|
967
|
+
cell.url
|
|
968
|
+
? { type: "link", url: cell.url, children: [text(cell.text)] }
|
|
969
|
+
: text(cell.text),
|
|
970
|
+
]),
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
const list = (items, outer) => ({
|
|
974
|
+
type: "list",
|
|
975
|
+
ordered: false,
|
|
976
|
+
spread: false,
|
|
977
|
+
...(outer
|
|
978
|
+
? {
|
|
979
|
+
data: {
|
|
980
|
+
hProperties: {
|
|
981
|
+
className: [TREE_CLASS],
|
|
982
|
+
...(render.print ? { [TREE_PRINT_ATTRIBUTE]: render.print } : {}),
|
|
983
|
+
},
|
|
984
|
+
cudoc: { kind: TREE_KIND },
|
|
985
|
+
},
|
|
986
|
+
}
|
|
987
|
+
: {}),
|
|
988
|
+
children: items.map((node) => node.children.length
|
|
989
|
+
? {
|
|
990
|
+
type: "listItem",
|
|
991
|
+
spread: false,
|
|
992
|
+
children: [
|
|
993
|
+
{
|
|
994
|
+
type: "blockquote",
|
|
995
|
+
data: {
|
|
996
|
+
hName: "details",
|
|
997
|
+
...(node.level <= open
|
|
998
|
+
? { hProperties: { open: true } }
|
|
999
|
+
: {}),
|
|
1000
|
+
},
|
|
1001
|
+
children: [
|
|
1002
|
+
{ ...line(node), data: { hName: "summary" } },
|
|
1003
|
+
list(node.children, false),
|
|
1004
|
+
],
|
|
1005
|
+
},
|
|
1006
|
+
],
|
|
1007
|
+
}
|
|
1008
|
+
: {
|
|
1009
|
+
type: "listItem",
|
|
1010
|
+
spread: false,
|
|
1011
|
+
data: { hProperties: { className: [`${TREE_CLASS}-leaf`] } },
|
|
1012
|
+
children: [line(node)],
|
|
1013
|
+
}),
|
|
1014
|
+
});
|
|
1015
|
+
return { type: "root", children: [list(nodes, true)] };
|
|
1016
|
+
}
|
|
619
1017
|
/** Resolve a configured embed from immutable persisted documents. */
|
|
620
1018
|
export function resolveEmbed(library, input, context) {
|
|
621
1019
|
const spec = parseEmbedSpec(JSON.stringify(input));
|
|
622
1020
|
let occurrence = 0;
|
|
623
1021
|
const reserved = new Set();
|
|
624
1022
|
const destination = documentIndex(library).byId.get(context.documentId);
|
|
625
|
-
//
|
|
626
|
-
|
|
1023
|
+
// Nodes already on the page: those a copy rebased, whose paths and
|
|
1024
|
+
// required modules now start from `destination`, and those a table or a
|
|
1025
|
+
// tree wrote there. A copy around them renames their ids, and leaves the
|
|
1026
|
+
// rest as it is.
|
|
1027
|
+
const placed = new WeakSet();
|
|
1028
|
+
const place = (root) => {
|
|
1029
|
+
visitNodes(root, (node) => {
|
|
1030
|
+
placed.add(node);
|
|
1031
|
+
});
|
|
1032
|
+
return root;
|
|
1033
|
+
};
|
|
627
1034
|
if (destination)
|
|
628
1035
|
for (const id of declaredIds(destination.tree))
|
|
629
1036
|
reserved.add(id);
|
|
@@ -632,6 +1039,15 @@ export function resolveEmbed(library, input, context) {
|
|
|
632
1039
|
// block is still current; `*` when an extractor ran, which may read anything.
|
|
633
1040
|
const dependencies = new Set();
|
|
634
1041
|
const expand = (spec, from) => {
|
|
1042
|
+
// A tree copies no section, so nothing in it expands or cycles; what it
|
|
1043
|
+
// depends on is every document a line was read from.
|
|
1044
|
+
if (typeof spec.render === "object" && spec.render.type === "tree") {
|
|
1045
|
+
const tree = buildTree(library, spec.sources, spec.render, from, context);
|
|
1046
|
+
tree.documents.forEach((id) => dependencies.add(id));
|
|
1047
|
+
if (usesExtractor(spec.render.columns ?? DEFAULT_TREE_COLUMNS))
|
|
1048
|
+
dependencies.add("*");
|
|
1049
|
+
return place(buildEmbedTree(tree.nodes, spec.render));
|
|
1050
|
+
}
|
|
635
1051
|
const children = [];
|
|
636
1052
|
const rows = [];
|
|
637
1053
|
for (const source of spec.sources) {
|
|
@@ -680,7 +1096,7 @@ export function resolveEmbed(library, input, context) {
|
|
|
680
1096
|
do {
|
|
681
1097
|
prefix = `${context.prefix ?? "embed"}-${++occurrence}-`;
|
|
682
1098
|
} while (sectionIds.some((id) => reserved.has(`${prefix}${id}`)));
|
|
683
|
-
rebase(section, document, library, prefix, destination,
|
|
1099
|
+
rebase(section, document, library, prefix, destination, placed);
|
|
684
1100
|
sectionIds.forEach((id) => reserved.add(`${prefix}${id}`));
|
|
685
1101
|
children.push(...section.children);
|
|
686
1102
|
}
|
|
@@ -691,11 +1107,9 @@ export function resolveEmbed(library, input, context) {
|
|
|
691
1107
|
}
|
|
692
1108
|
if (spec.render && spec.render !== "section") {
|
|
693
1109
|
const columns = spec.render.columns ?? DEFAULT_TABLE_COLUMNS;
|
|
694
|
-
if (columns
|
|
695
|
-
typeof column.value === "object" &&
|
|
696
|
-
"extractor" in column.value))
|
|
1110
|
+
if (usesExtractor(columns))
|
|
697
1111
|
dependencies.add("*");
|
|
698
|
-
return buildEmbedTable(library, columns, rows, context);
|
|
1112
|
+
return place(buildEmbedTable(library, columns, rows, context));
|
|
699
1113
|
}
|
|
700
1114
|
return { type: "root", children };
|
|
701
1115
|
};
|