create-asciidoc-multi-book-workspace 0.6.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/CHANGELOG.md +16 -0
- package/README.md +0 -1
- package/dist/runtime/adoc-books.mjs +48 -162
- package/package.json +2 -2
- package/templates/default-workspace/README.md +5 -7
- package/templates/default-workspace/books/04-reference-manual/backmatter/glossary.adoc +1 -2
- package/templates/default-workspace/books/04-reference-manual/chapters/030-xref-patterns.adoc +1 -1
- package/templates/default-workspace/books/05-upper-volume/chapters/020-upper-core-model.adoc +1 -1
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-decision-trees.adoc +4 -4
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/010-source-and-projection.adoc +14 -13
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/020-heading-address-and-xref.adoc +6 -6
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/010-names-and-addresses.adoc +3 -3
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/030-declaring-conventions.adoc +1 -1
- package/templates/default-workspace/catalog.adoc +1 -1
- package/templates/default-workspace/package.json.template +1 -2
- package/templates/solo-workspace/README.md +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Removed
|
|
6
|
+
|
|
7
|
+
- The generated workspace has no `check` command. `pnpm run build` is the only way to render, and `check` ran the same path only so the runtime's checks could be run on their own.
|
|
8
|
+
- The runtime no longer judges a workspace: `XREF_UNRESOLVED`, `XREF_AMBIGUOUS`, `HTML_RESOURCE_MISSING`, `HTML_ANCHOR_MISSING`, `CATALOG_TARGET_MISSING`, `BOOK_MISSING_FROM_CATALOG`, and `MISSING_DOCTYPE` are gone. Whether a reference reaches one heading is read from the `asciidoc-abundant-tree` projection by the tool that reads it; the build fails only where it cannot produce the pages: a missing `catalog.adoc` or book, any Asciidoctor message, an include graph that cannot be parsed within `documentRoot` or an error-level parse diagnostic, or a configuration or include resolver it cannot use.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Generated workspaces depend on `asciidoc-abundant-tree` `^0.6.0`. Its label space is four predicates on the heading nodes of the graph, and an xref whose address several headings own gets an edge to each of them. The runtime rewrites an HTML link only when its xref reaches exactly one section, inside a book and across books; any other xref is left as Asciidoctor wrote it. The document title owns labels but is not a section, so an xref to a title only the document title owns is left as written.
|
|
13
|
+
- The samples no longer say a check reports broken references. After renaming a heading, an author searches the book for the old name, which finds every reference written by it (qualified addresses included), its link text, and the prose. The structured writing conventions sample states where an unbound or ambiguous link lands on the page (an unbound one is broken; one that several headings share lands on the first of them), shows the projection as `asciidoc-abundant-tree` 0.6 writes it (heading nodes named by the projector, addresses as `aat:headline` and `aat:addressPath` values), and lists a title that starts with inline markup among those a reference cannot name by their text.
|
|
14
|
+
|
|
15
|
+
### Upgrading an existing workspace
|
|
16
|
+
|
|
17
|
+
Create a temporary workspace with `pnpm create asciidoc-multi-book-workspace@0.7.0 tmp-books` and copy its `tools/adoc-books.mjs` over yours. In your `package.json`, remove the `check` script, set `"asciidoc-abundant-tree": "^0.6.0"`, and run `pnpm install`. Books need no change. To find references that reach no heading or several, read the projection: an xref edge (`aat:XrefEdge`) carries one `aat:targetHeading` for each heading its address reaches.
|
|
18
|
+
|
|
3
19
|
## 0.6.0 - 2026-10-31
|
|
4
20
|
|
|
5
21
|
### Added
|
package/README.md
CHANGED
|
@@ -8,11 +8,10 @@ import asciidoctorFactory from "@asciidoctor/core";
|
|
|
8
8
|
import { parseAbundantTree, rdf12 } from "asciidoc-abundant-tree";
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const DIAGRAM_BLOCK_PATTERN = /^\[(?:actdiag|blockdiag|bpmn|bytefield|c4plantuml|d2|dbml|ditaa|erd|excalidraw|graphviz|mermaid|nomnoml|nwdiag|packetdiag|pikchr|plantuml|rackdiag|seqdiag|svgbob|symbolator|umlet|vega|vegalite|wavedrom|structurizr|diagramsnet|wireviz)(?:,|\])/m;
|
|
11
|
-
const CATALOG_BOOK_XREF_PATTERN = /xref:books\/([^/\]]+)\/book\.adoc(?:#[^\[]+)?\[/g;
|
|
12
|
-
const LOCAL_TARGET_PATTERN = /\b(href|src)="([^"]+)"/g;
|
|
13
11
|
const HTML_ANCHOR_PATTERN = /\b(?:id|name)="([^"]+)"/g;
|
|
14
|
-
const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
15
12
|
const AAT_NAMESPACE = "https://micheng.dev/ns/asciidoc-abundant-tree#";
|
|
13
|
+
// 标题投影的 label space:aat 图上标题节点的这四个谓词(aat 06 号书第 2 章「地址与 Label 空间」)。
|
|
14
|
+
const LABEL_PREDICATES = ["headline", "addressPath", "addressLabel", "generatedAddressLabel"];
|
|
16
15
|
const HTML_FRAGMENT_LINK_PATTERN = /<a href="#([^"]*)"([^>]*)>([\s\S]*?)<\/a>/g;
|
|
17
16
|
const HTML_DOCUMENT_LINK_PATTERN = /<a href="([^"#]+\.html)#([^"]*)"([^>]*)>([\s\S]*?)<\/a>/g;
|
|
18
17
|
const HOME_MARKER = "data-multi-book-home";
|
|
@@ -345,7 +344,6 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
|
|
|
345
344
|
}
|
|
346
345
|
return document;
|
|
347
346
|
}
|
|
348
|
-
// 交回每本书转换后的 Asciidoctor 文档:书内与跨书 xref 的目标要用它展开属性引用(bindReferences)。
|
|
349
347
|
async function buildHtml(rootDir, books, asciidoctor, extensionRegistry, useKroki, fetchDiagrams) {
|
|
350
348
|
await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
|
|
351
349
|
convertFile(asciidoctor, extensionRegistry, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
|
|
@@ -468,7 +466,7 @@ function sourceBundleFromDocument(document) {
|
|
|
468
466
|
}).join("\n");
|
|
469
467
|
}
|
|
470
468
|
// 解析失败就让构建失败,不退回只有封面的阅读页:退回时 include 越过 documentRoot 只剩一条 warning,
|
|
471
|
-
// 构建照常成功,没有人发现阅读页坏了(DESIGN.adoc 的
|
|
469
|
+
// 构建照常成功,没有人发现阅读页坏了(DESIGN.adoc 的 Rendering Failures)。
|
|
472
470
|
async function buildReaderBookData(config, book) {
|
|
473
471
|
let parsed;
|
|
474
472
|
let document;
|
|
@@ -557,9 +555,10 @@ async function buildReaderBookData(config, book) {
|
|
|
557
555
|
references: bindReferences(config, parsed)
|
|
558
556
|
};
|
|
559
557
|
}
|
|
560
|
-
// 书内 xref
|
|
561
|
-
//
|
|
562
|
-
//
|
|
558
|
+
// 书内 xref 落在哪一节,读 aat 的投影(DESIGN.adoc 的 Reference Binding):哪些是 xref、目标写的是什么(透传与转义
|
|
559
|
+
// 不算,属性已展开,指向别的文档的另记 targetDocument)由 aat 里的 Asciidoctor 读出,一处引用对上几节就有几条
|
|
560
|
+
// targetHeading。链接只能跳到一处,所以恰好一节才改写;零节或几节保持 Asciidoctor 写下的样子,不替人挑,也不报:
|
|
561
|
+
// 引用对不对是投影的读者(bookgraph)的解释,不是渲染的事(DESIGN.adoc 的 Division of Labour)。
|
|
563
562
|
function bindReferences(config, document) {
|
|
564
563
|
const projection = rdf12(document, { documentRoot: config.documentRoot });
|
|
565
564
|
const graph = projection.graph;
|
|
@@ -567,65 +566,53 @@ function bindReferences(config, document) {
|
|
|
567
566
|
return graph.match({ subject, predicate: { termType: "iri", value: `${AAT_NAMESPACE}${name}` } })
|
|
568
567
|
.map((triple) => triple.object.termType === "triple" ? "" : triple.object.value);
|
|
569
568
|
};
|
|
570
|
-
|
|
569
|
+
// 只数节:书名也是图上的一个标题节点、也拥有标签,可它不是书里的一节,网页上没有落点(nodeIndex 里它不是 section)。
|
|
570
|
+
const sections = new Map(projection.nodeIndex.entries()
|
|
571
571
|
.filter((entry) => entry.kind === "section")
|
|
572
572
|
.map((entry) => [entry.iri.value, entry]));
|
|
573
|
-
const located = (iris) => iris.flatMap((iri) => {
|
|
574
|
-
const entry =
|
|
573
|
+
const located = (iris) => [...iris].flatMap((iri) => {
|
|
574
|
+
const entry = sections.get(iri);
|
|
575
575
|
const id = entry?.node.ids?.[0];
|
|
576
576
|
if (entry === undefined || id === undefined)
|
|
577
577
|
return [];
|
|
578
|
-
return [{ id, title: entry.node.title ?? id
|
|
578
|
+
return [{ id, title: entry.node.title ?? id }];
|
|
579
579
|
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
580
|
+
// 别的书指过来的引用在这本书的 label space 里找:图上四个标签谓词的值,按整串相等找拥有者。
|
|
581
|
+
const ownersByLabel = new Map();
|
|
582
|
+
for (const name of LABEL_PREDICATES) {
|
|
583
|
+
for (const triple of graph.match({ predicate: { termType: "iri", value: `${AAT_NAMESPACE}${name}` } })) {
|
|
584
|
+
if (triple.object.termType !== "literal")
|
|
585
|
+
continue;
|
|
586
|
+
const iris = ownersByLabel.get(triple.object.value) ?? new Set();
|
|
587
|
+
iris.add(triple.subject.value);
|
|
588
|
+
ownersByLabel.set(triple.object.value, iris);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const owners = (selector) => located(ownersByLabel.get(selector) ?? []);
|
|
592
592
|
const headingsByFragment = new Map();
|
|
593
593
|
const crossBook = [];
|
|
594
|
-
|
|
595
|
-
for (const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
594
|
+
// 指向整本书的 xref(没有片段)没有 targetSelector:没有可改写的片段。
|
|
595
|
+
for (const triple of graph.match({ predicate: { termType: "iri", value: `${AAT_NAMESPACE}targetSelector` } })) {
|
|
596
|
+
const edge = triple.subject;
|
|
597
|
+
const fragment = triple.object.termType === "triple" ? "" : triple.object.value;
|
|
598
|
+
const document = values(edge, "targetDocument")[0];
|
|
599
|
+
if (document !== undefined) {
|
|
600
|
+
crossBook.push({ document, fragment });
|
|
599
601
|
continue;
|
|
600
602
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
if (found.length === 1) {
|
|
604
|
-
const heading = { id: found[0].id, title: found[0].title };
|
|
605
|
-
headingsByFragment.set(fragment, heading);
|
|
606
|
-
headingsByFragment.set(escapeHtmlAttribute(fragment), heading);
|
|
603
|
+
const found = located(values(edge, "targetHeading"));
|
|
604
|
+
if (found.length !== 1)
|
|
607
605
|
continue;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
issues.push(issue("XREF_AMBIGUOUS", `${where} -> ${describeCandidates(found)}`));
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
issues.push(issue("XREF_UNRESOLVED", where));
|
|
606
|
+
headingsByFragment.set(fragment, found[0]);
|
|
607
|
+
headingsByFragment.set(escapeHtmlAttribute(fragment), found[0]);
|
|
614
608
|
}
|
|
615
|
-
return { headingsByFragment, crossBook, owners
|
|
609
|
+
return { headingsByFragment, crossBook, owners };
|
|
616
610
|
}
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
.map((heading) => `${heading.title} (${heading.relativePath}:${heading.line})`)
|
|
621
|
-
.join(", ");
|
|
622
|
-
}
|
|
623
|
-
// 跨书 xref 同样以 aat 的绑定为准,只是 label space 在目标书里(DESIGN.adoc 的 Reference Binding)。aat 不打开别的
|
|
624
|
-
// 文档,工作区解析了每一本书,所以在这里绑定。路径按书目录解析:Asciidoctor 把它照写进 HTML,而 HTML 的输出目录
|
|
625
|
-
// 与 books/ 下的书目录一一对应。指向的不是工作区里的书、或只指向整本书(没有片段)的,留给 HTML 本地资源检查。
|
|
611
|
+
// 跨书 xref 同样跟着投影走,只是 label space 在目标书里(DESIGN.adoc 的 Reference Binding)。aat 不打开别的文档,
|
|
612
|
+
// 工作区解析了每一本书,所以在这里找。路径按书目录解析:Asciidoctor 把它照写进 HTML,而 HTML 的输出目录与 books/ 下的
|
|
613
|
+
// 书目录一一对应。指向的不是工作区里的书、只指向整本书(没有片段)、或在目标书里对上零节或几节的,保持原样。
|
|
626
614
|
async function bindCrossBookReferences(book, references, bindingsByInput, anchorCache) {
|
|
627
615
|
const linksByHref = new Map();
|
|
628
|
-
const issues = [];
|
|
629
616
|
for (const xref of references.crossBook) {
|
|
630
617
|
const { fragment } = xref;
|
|
631
618
|
// Asciidoctor 把 xref:other#x[] 与 xref:other.adoc#x[] 都读成文档 other:去掉 .adoc、没有扩展名的照写,
|
|
@@ -635,14 +622,8 @@ async function bindCrossBookReferences(book, references, bindingsByInput, anchor
|
|
|
635
622
|
if (fragment === "" || target === undefined)
|
|
636
623
|
continue;
|
|
637
624
|
const found = target.references.owners(fragment);
|
|
638
|
-
if (found.length
|
|
639
|
-
issues.push(issue("XREF_UNRESOLVED", `${xref.where} -> ${target.book.bookId}`));
|
|
625
|
+
if (found.length !== 1)
|
|
640
626
|
continue;
|
|
641
|
-
}
|
|
642
|
-
if (found.length > 1) {
|
|
643
|
-
issues.push(issue("XREF_AMBIGUOUS", `${xref.where} -> ${target.book.bookId}: ${describeCandidates(found)}`));
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
627
|
const heading = found[0];
|
|
647
628
|
// 指向本书自己的 book.adoc:Asciidoctor 把它当书内引用,写成 #片段。
|
|
648
629
|
if (target.book.bookId === book.bookId) {
|
|
@@ -658,9 +639,9 @@ async function bindCrossBookReferences(book, references, bindingsByInput, anchor
|
|
|
658
639
|
linksByHref.set(`${htmlPath}#${fragment}`, link);
|
|
659
640
|
linksByHref.set(escapeHtmlAttribute(`${htmlPath}#${fragment}`), link);
|
|
660
641
|
}
|
|
661
|
-
return
|
|
642
|
+
return linksByHref;
|
|
662
643
|
}
|
|
663
|
-
//
|
|
644
|
+
// 在目标书里恰好对上一节、而目标书的 HTML 里没有这个片段的跨书链接,改成指向那一节的标题。Asciidoctor 对没写显示文字的跨书 xref
|
|
664
645
|
// 显示文档路径;改写时换成标题。
|
|
665
646
|
function rewriteCrossBookXrefs(html, linksByHref) {
|
|
666
647
|
if (linksByHref.size === 0)
|
|
@@ -673,7 +654,7 @@ function rewriteCrossBookXrefs(html, linksByHref) {
|
|
|
673
654
|
return `<a href="${escapeHtmlAttribute(link.href)}"${rest}>${shown}</a>`;
|
|
674
655
|
});
|
|
675
656
|
}
|
|
676
|
-
// Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat
|
|
657
|
+
// Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat 的投影里恰好对上一节的,改成指向那一节的标题,
|
|
677
658
|
// 显示标题。片段已经是 HTML 里某个元素的 id 时,保持 Asciidoctor 写下的样子。
|
|
678
659
|
function rewriteBoundXrefs(html, headingsByFragment) {
|
|
679
660
|
if (headingsByFragment.size === 0)
|
|
@@ -1205,24 +1186,21 @@ ${readerStyles()}
|
|
|
1205
1186
|
}
|
|
1206
1187
|
async function addHomeLinks(rootDir, books, config) {
|
|
1207
1188
|
const catalog = path.join(rootDir, "build", "html", "catalog.html");
|
|
1208
|
-
// 跨书 xref 在目标书的 label space
|
|
1189
|
+
// 跨书 xref 在目标书的 label space 里找,所以先把每一本书都解析完。
|
|
1209
1190
|
const readerData = new Map();
|
|
1210
1191
|
for (const book of books) {
|
|
1211
1192
|
readerData.set(book.bookId, await buildReaderBookData(config, book));
|
|
1212
1193
|
}
|
|
1213
1194
|
const bindingsByInput = new Map(books.map((book) => [book.input, { book, references: readerData.get(book.bookId).references }]));
|
|
1214
1195
|
const anchorCache = new Map();
|
|
1215
|
-
const issues = [];
|
|
1216
1196
|
for (const book of books) {
|
|
1217
1197
|
const htmlFile = path.join(book.htmlOutputDir, "book.html");
|
|
1218
1198
|
const href = path.relative(path.dirname(htmlFile), catalog);
|
|
1219
1199
|
const data = readerData.get(book.bookId);
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
const html = rewriteCrossBookXrefs(rewriteBoundXrefs(await readFile(htmlFile, "utf8"), data.references.headingsByFragment), crossBook.linksByHref);
|
|
1200
|
+
const crossBookLinks = await bindCrossBookReferences(book, data.references, bindingsByInput, anchorCache);
|
|
1201
|
+
const html = rewriteCrossBookXrefs(rewriteBoundXrefs(await readFile(htmlFile, "utf8"), data.references.headingsByFragment), crossBookLinks);
|
|
1223
1202
|
await writeFile(htmlFile, addBookControlsToBookHtml(html, href, config.homeLink, data.sourceBundle, data.pageMap, book.bookId), "utf8");
|
|
1224
1203
|
}
|
|
1225
|
-
return issues;
|
|
1226
1204
|
}
|
|
1227
1205
|
async function writeRootIndex(rootDir, rootIndex) {
|
|
1228
1206
|
const outputDir = path.join(rootDir, "build", "html");
|
|
@@ -1241,55 +1219,6 @@ async function writeRootIndex(rootDir, rootIndex) {
|
|
|
1241
1219
|
</html>
|
|
1242
1220
|
`, "utf8");
|
|
1243
1221
|
}
|
|
1244
|
-
function extractCatalogBookIds(catalogSource) {
|
|
1245
|
-
const ids = new Set();
|
|
1246
|
-
for (const match of catalogSource.matchAll(CATALOG_BOOK_XREF_PATTERN))
|
|
1247
|
-
ids.add(match[1]);
|
|
1248
|
-
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
1249
|
-
}
|
|
1250
|
-
function issue(code, detail) {
|
|
1251
|
-
return { code, detail };
|
|
1252
|
-
}
|
|
1253
|
-
// xref 不在这里查:书内与跨书的引用以 aat 的绑定为准(bindReferences、bindCrossBookReferences),不存在的书与其余
|
|
1254
|
-
// 锚点由 missingLocalResources 在生成的 HTML 上核对(DESIGN.adoc 的 Workspace Contract Checks 与 Reference Binding)。
|
|
1255
|
-
async function workspaceContractIssues(rootDir, books) {
|
|
1256
|
-
const issues = [];
|
|
1257
|
-
const catalogSource = await readIfExists(path.join(rootDir, "catalog.adoc"));
|
|
1258
|
-
const catalogBookIds = extractCatalogBookIds(catalogSource);
|
|
1259
|
-
const bookSet = new Set(books.map((book) => book.bookId));
|
|
1260
|
-
const catalogBookSet = new Set(catalogBookIds);
|
|
1261
|
-
for (const bookId of catalogBookIds) {
|
|
1262
|
-
if (!bookSet.has(bookId))
|
|
1263
|
-
issues.push(issue("CATALOG_TARGET_MISSING", bookId));
|
|
1264
|
-
}
|
|
1265
|
-
for (const book of books) {
|
|
1266
|
-
if (!catalogBookSet.has(book.bookId))
|
|
1267
|
-
issues.push(issue("BOOK_MISSING_FROM_CATALOG", book.bookId));
|
|
1268
|
-
}
|
|
1269
|
-
for (const book of books) {
|
|
1270
|
-
const bookSource = await readFile(book.input, "utf8");
|
|
1271
|
-
if (!/^:doctype:\s+book$/m.test(bookSource))
|
|
1272
|
-
issues.push(issue("MISSING_DOCTYPE", book.bookId));
|
|
1273
|
-
}
|
|
1274
|
-
return issues;
|
|
1275
|
-
}
|
|
1276
|
-
async function collectHtmlFiles(dir) {
|
|
1277
|
-
return collectFiles(dir, (filePath) => filePath.endsWith(".html"));
|
|
1278
|
-
}
|
|
1279
|
-
function extractLocalTargets(html) {
|
|
1280
|
-
const targets = [];
|
|
1281
|
-
for (const match of html.matchAll(LOCAL_TARGET_PATTERN)) {
|
|
1282
|
-
const [, attribute, rawTarget] = match;
|
|
1283
|
-
if (rawTarget === "" || rawTarget === "#" || rawTarget.startsWith("//") || SCHEME_PATTERN.test(rawTarget)) {
|
|
1284
|
-
continue;
|
|
1285
|
-
}
|
|
1286
|
-
const targetWithoutFragment = rawTarget.split("#", 1)[0].split("?", 1)[0];
|
|
1287
|
-
const fragment = rawTarget.includes("#") ? rawTarget.split("#", 2)[1].split("?", 1)[0] : "";
|
|
1288
|
-
if (targetWithoutFragment !== "" || fragment !== "")
|
|
1289
|
-
targets.push({ attribute, rawTarget });
|
|
1290
|
-
}
|
|
1291
|
-
return targets;
|
|
1292
|
-
}
|
|
1293
1222
|
function decodeUrlFragment(fragment) {
|
|
1294
1223
|
try {
|
|
1295
1224
|
return decodeURIComponent(fragment);
|
|
@@ -1312,39 +1241,6 @@ async function anchorsForHtmlFile(filePath, cache) {
|
|
|
1312
1241
|
cache.set(filePath, anchors);
|
|
1313
1242
|
return anchors;
|
|
1314
1243
|
}
|
|
1315
|
-
async function missingLocalResources(rootDir) {
|
|
1316
|
-
const htmlDir = path.join(rootDir, "build", "html");
|
|
1317
|
-
const htmlFiles = await collectHtmlFiles(htmlDir);
|
|
1318
|
-
const issues = [];
|
|
1319
|
-
const anchorCache = new Map();
|
|
1320
|
-
for (const htmlFile of htmlFiles) {
|
|
1321
|
-
const html = await readFile(htmlFile, "utf8");
|
|
1322
|
-
for (const target of extractLocalTargets(html)) {
|
|
1323
|
-
const targetPath = target.rawTarget.split("#", 1)[0].split("?", 1)[0];
|
|
1324
|
-
const fragment = target.rawTarget.includes("#") ? target.rawTarget.split("#", 2)[1].split("?", 1)[0] : "";
|
|
1325
|
-
const resolved = targetPath === "" ? htmlFile : path.resolve(path.dirname(htmlFile), targetPath);
|
|
1326
|
-
if (targetPath !== "" && !await existsFile(resolved)) {
|
|
1327
|
-
issues.push(issue("HTML_RESOURCE_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target.rawTarget}`));
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
if (target.attribute === "href" && fragment !== "" && (targetPath === "" || resolved.endsWith(".html"))) {
|
|
1331
|
-
const anchors = await anchorsForHtmlFile(resolved, anchorCache);
|
|
1332
|
-
const decodedFragment = decodeUrlFragment(fragment);
|
|
1333
|
-
if (!anchors.has(fragment) && !anchors.has(decodedFragment)) {
|
|
1334
|
-
issues.push(issue("HTML_ANCHOR_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target.rawTarget}`));
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
}
|
|
1338
|
-
}
|
|
1339
|
-
return issues;
|
|
1340
|
-
}
|
|
1341
|
-
async function assertNoIssues(label, issues) {
|
|
1342
|
-
if (issues.length === 0)
|
|
1343
|
-
return;
|
|
1344
|
-
for (const entry of issues)
|
|
1345
|
-
console.error(`${entry.code}: ${entry.detail}`);
|
|
1346
|
-
throw new Error(`${label} failed with ${issues.length} issue(s)`);
|
|
1347
|
-
}
|
|
1348
1244
|
export async function buildWorkspace(rootDir = process.cwd()) {
|
|
1349
1245
|
const catalog = path.join(rootDir, "catalog.adoc");
|
|
1350
1246
|
if (!await existsFile(catalog))
|
|
@@ -1358,14 +1254,8 @@ export async function buildWorkspace(rootDir = process.cwd()) {
|
|
|
1358
1254
|
const asciidoctor = createAsciidoctor(useKroki);
|
|
1359
1255
|
await buildHtml(rootDir, books, asciidoctor, includeResolverRegistry(asciidoctor, config), useKroki, fetchDiagrams);
|
|
1360
1256
|
await copyAssets(rootDir, books);
|
|
1361
|
-
|
|
1257
|
+
await addHomeLinks(rootDir, books, config);
|
|
1362
1258
|
await writeRootIndex(rootDir, config.rootIndex);
|
|
1363
|
-
await assertNoIssues("reference binding check", referenceIssues);
|
|
1364
|
-
await assertNoIssues("HTML local resource check", await missingLocalResources(rootDir));
|
|
1365
|
-
await assertNoIssues("workspace contract check", await workspaceContractIssues(rootDir, books));
|
|
1366
|
-
}
|
|
1367
|
-
export async function checkWorkspace(rootDir = process.cwd()) {
|
|
1368
|
-
await buildWorkspace(rootDir);
|
|
1369
1259
|
}
|
|
1370
1260
|
export async function cleanWorkspace(rootDir = process.cwd()) {
|
|
1371
1261
|
await rm(path.join(rootDir, "build"), { force: true, recursive: true });
|
|
@@ -1376,15 +1266,11 @@ async function main() {
|
|
|
1376
1266
|
await buildWorkspace();
|
|
1377
1267
|
return;
|
|
1378
1268
|
}
|
|
1379
|
-
if (command === "check") {
|
|
1380
|
-
await checkWorkspace();
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
1269
|
if (command === "clean") {
|
|
1384
1270
|
await cleanWorkspace();
|
|
1385
1271
|
return;
|
|
1386
1272
|
}
|
|
1387
|
-
console.error("Usage: node tools/adoc-books.mjs <build|
|
|
1273
|
+
console.error("Usage: node tools/adoc-books.mjs <build|clean>");
|
|
1388
1274
|
process.exitCode = 1;
|
|
1389
1275
|
}
|
|
1390
1276
|
const executedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-asciidoc-multi-book-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Create a complete AsciiDoc multi-book workspace.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@asciidoctor/core": "^3.0.4",
|
|
56
56
|
"@types/node": "^24.10.1",
|
|
57
|
-
"asciidoc-abundant-tree": "
|
|
57
|
+
"asciidoc-abundant-tree": "link:../asciidoc-abundant-tree",
|
|
58
58
|
"asciidoctor-kroki": "^0.18.1",
|
|
59
59
|
"playwright": "^1.59.1",
|
|
60
60
|
"typescript": "^5.9.3"
|
|
@@ -29,13 +29,11 @@ To fetch diagram images into the local HTML output, run:
|
|
|
29
29
|
ADOC_BOOKS_FETCH_DIAGRAMS=1 pnpm run build
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## When The Build Fails
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
pnpm run check
|
|
36
|
-
```
|
|
34
|
+
The build fails only when it cannot produce the pages: Asciidoctor reports a message while rendering, a book's includes leave `documentRoot` (see Configure), or an include resolver returns something the build cannot use.
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
It does not judge your books. Whether an xref reaches the heading you meant, whether a local link resolves, and whether `catalog.adoc` lists every book are left to tools that read the RDF projection `asciidoc-abundant-tree` makes of each book. An xref whose address one heading owns links to that heading; one that no heading owns, or several do, keeps the link Asciidoctor wrote, and the build still succeeds.
|
|
39
37
|
|
|
40
38
|
## Configure
|
|
41
39
|
|
|
@@ -52,7 +50,7 @@ export default {
|
|
|
52
50
|
};
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
`documentRoot` matters when this workspace lives inside a project repository. Set it to the project root (`".."` when the workspace is a direct subdirectory) and books can include the project's own code instead of a copy of it. Source paths in the reader and the `// file:` markers of copied source then start at that root. It must contain this workspace. An include that leaves `documentRoot` fails
|
|
53
|
+
`documentRoot` matters when this workspace lives inside a project repository. Set it to the project root (`".."` when the workspace is a direct subdirectory) and books can include the project's own code instead of a copy of it. Source paths in the reader and the `// file:` markers of copied source then start at that root. It must contain this workspace. An include that leaves `documentRoot` fails the build and names the book.
|
|
56
54
|
|
|
57
55
|
`includeResolvers` lets books include lines chosen by your own code, for example one function out of a source file:
|
|
58
56
|
|
|
@@ -95,7 +93,7 @@ Delete `books/<book-id>/`, then remove the matching `xref:books/<book-id>/book.a
|
|
|
95
93
|
Run:
|
|
96
94
|
|
|
97
95
|
```bash
|
|
98
|
-
pnpm run
|
|
96
|
+
pnpm run build
|
|
99
97
|
```
|
|
100
98
|
|
|
101
99
|
## Add A Book
|
|
@@ -4,5 +4,4 @@ book:: 具有独立 `book.adoc` 和构建产物的书籍单位。
|
|
|
4
4
|
part:: 同一本 book 内部的 level 0 大分部。
|
|
5
5
|
catalog:: 多书工作区的读者导航入口。
|
|
6
6
|
shared resource:: 被 catalog 或多本书共同引用的资源。
|
|
7
|
-
custom ID:: 用 `[#id]` 给标题加的另一个地址。标题就是地址;同名的标题写上级标题、以 `::`
|
|
8
|
-
contract check:: 对工作区源文件约定进行静态检查的脚本行为。
|
|
7
|
+
custom ID:: 用 `[#id]` 给标题加的另一个地址。标题就是地址;同名的标题写上级标题、以 `::` 隔开来引用,同一个上级下也同名、标题里有 `#`、`::` 或标题以行内标记开头时,才给它再加一个。
|
package/templates/default-workspace/books/04-reference-manual/chapters/030-xref-patterns.adoc
CHANGED
|
@@ -16,4 +16,4 @@ xref + : + ../other-book/book.adoc#目标章节[]
|
|
|
16
16
|
|
|
17
17
|
上面的写法用分段文本展示,避免默认样本之间形成真实链接依赖。实际写作时去掉空格和加号,写成一个完整的 cross reference 宏。
|
|
18
18
|
|
|
19
|
-
被跨书引用的标题不需要另写 anchor
|
|
19
|
+
被跨书引用的标题不需要另写 anchor:标题就是地址,跨书与书内一样。目标标题改了名,在引用它的书里搜一遍旧名字,就能找到每一处没有跟上的引用。
|
|
@@ -44,20 +44,20 @@
|
|
|
44
44
|
[source,text]
|
|
45
45
|
----
|
|
46
46
|
1. 只是换一个更准确的名字吗?
|
|
47
|
-
├─
|
|
48
|
-
│
|
|
47
|
+
├─ 是:改标题。在书里搜一遍旧名字,找到每一处引用,连同显示文本和正文里的
|
|
48
|
+
│ 旧名字,逐处改成新名字。
|
|
49
49
|
│ 旧名字值得让读者查到时,把它写进别名。结束。
|
|
50
50
|
└─ 否:看第 2 步。
|
|
51
51
|
|
|
52
52
|
2. 这一节收的范围变了吗?
|
|
53
53
|
├─ 是:改标题,并重读这一节的说明、收什么与不收什么、相关的节和已经写下的结论。
|
|
54
|
-
│
|
|
54
|
+
│ 对搜旧名字找到的每一处引用,先判断它说的是留下的部分还是去掉的部分,
|
|
55
55
|
│ 再决定改成新名字,还是改指别的一节。看第 3 步。
|
|
56
56
|
└─ 否:看第 3 步。
|
|
57
57
|
|
|
58
58
|
3. 这一节被拆开,或不再使用了吗?
|
|
59
59
|
├─ 是:不留它。删掉它,或把它改成拆出来的某一节的名字。
|
|
60
|
-
│
|
|
60
|
+
│ 搜一遍旧名字找到旧引用,逐一判断每一处该改指哪一节。结束。
|
|
61
61
|
└─ 否:结束。
|
|
62
62
|
----
|
|
63
63
|
|
|
@@ -34,35 +34,36 @@ asciidoc-abundant-tree 把这份源文本投影成 RDF。下面摘出与这些
|
|
|
34
34
|
|
|
35
35
|
[source,turtle]
|
|
36
36
|
----
|
|
37
|
-
|
|
37
|
+
:heading-2 a aat:Heading ;
|
|
38
38
|
aat:headline "炒糖色" ;
|
|
39
39
|
aat:role "technique" .
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
:heading-3 a aat:Heading ;
|
|
42
42
|
aat:headline "红烧肉" ;
|
|
43
43
|
aat:role "dish" ;
|
|
44
|
-
aat:containsDirectly
|
|
45
|
-
rel:depends-on
|
|
44
|
+
aat:containsDirectly :heading-4 ;
|
|
45
|
+
rel:depends-on :heading-2 .
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
aat:headline "火候"
|
|
47
|
+
:heading-4 a aat:Heading ;
|
|
48
|
+
aat:headline "火候" ;
|
|
49
|
+
aat:addressPath "红烧肉::火候" , "家常菜谱::红烧肉::火候" .
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
:heading-5 a aat:Heading ;
|
|
51
52
|
aat:headline "红烧排骨" ;
|
|
52
53
|
aat:role "dish" ;
|
|
53
|
-
aat:references
|
|
54
|
+
aat:references :heading-3 .
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
:xref-edge-l11-c15-o0 a aat:XrefEdge ;
|
|
56
57
|
aat:targetSelector "炒糖色" ;
|
|
57
|
-
aat:targetHeading
|
|
58
|
+
aat:targetHeading :heading-2 ;
|
|
58
59
|
aat:rel "depends-on" .
|
|
59
60
|
----
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
`:` 代表这份源文件。标题节点的名字由投影器按标题在书里的先后起:书名是 `:heading-1`,《炒糖色》是第二个标题,所以是 `:heading-2`。前面添一个标题,后面每个节点的名字都变,因此节点名只在这一次投影里指称那一节。标题原文和地址作为值记在节点上:标题原文记在 `aat:headline`,补上上级标题的限定地址记在 `aat:addressPath`,例如《火候》的 `红烧肉::火候`,作者写的 custom ID 记在 `aat:addressLabel`。工具认一节,按这些值去找,不读节点名(见《xref:标题、寻址与引用[]》)。
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
role 记在 `aat:role`,上下级记成 `aat:containsDirectly`。普通 xref 投影为 `aat:references`;写了 `rel` 的 xref 投影为以谓词命名的边,这里是 `rel:depends-on`。每条 xref 另有一个节点,记下源文本里写的目标 `aat:targetSelector` 和它找到的标题 `aat:targetHeading`。引用写的是标题原文,所以找到的就是那个标题。一处引用的目标有几节拥有,就记几条 `aat:targetHeading`,投影不替作者挑。
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
写书的人不必自己运行投影。工作区的构建把网页上的链接接到哪一节,读的是这张图;读投影的工具检查引用、按 role 列出标题,读的也是这张图。源文本怎样写,决定了它们能从书里读出什么。
|
|
66
67
|
|
|
67
68
|
`rel` 用哪些谓词,由书自己定。如果菜谱要表明一道菜由另一道菜改良而来,就先在自己的关系谓词声明里加上这个谓词,再在 xref 里使用它:
|
|
68
69
|
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
|
|
31
31
|
Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的按出现的先后加上 `-2`、`-3`。这个 ID 随标题和标题的位置变化,因此引用不应写它。
|
|
32
32
|
|
|
33
|
-
工作区的构建(`pnpm run build
|
|
33
|
+
工作区的构建(`pnpm run build`)按地址把网页上的链接接到那一节。引用找不到目标时,构建不改它,网页上这条链接是断的,显示成方括号里的地址。目标不止一节时,一条链接只能跳到一处,构建也不替作者挑,链接照 Asciidoctor 自己的规则走:几节同名,它落到书里最前面的那一节。这样的链接看上去好好的,却可能落到作者不想指的那一节,比断链更难发现。单独用 Asciidoctor 或编辑器预览时,链接都照 Asciidoctor 自己的规则走:它把不含空格的引用目标当作 ID 去找,而它生成的 ID 去掉了标点,因此目标不含空格、却带顿号或冒号的引用,以及下面要讲的限定地址,在预览里是断的。这时源文件并没有写错,应以工作区的构建为准。
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
构建只在做不出页面时失败,例如 include 进来的文件不在工作区里。引用找不到目标或目标不止一节,构建照常完成;哪些引用找不到目标、哪些对上了几节,由读投影的工具检查,它读的就是《xref:源文本与投影[]》里的那张图。role、字段和字段的取值应照凡例里的声明写。
|
|
36
36
|
|
|
37
37
|
标题会重名。按类别组织的书,各个大标题下常有名字相同的小节:
|
|
38
38
|
|
|
@@ -51,7 +51,7 @@ Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的
|
|
|
51
51
|
=== 火候
|
|
52
52
|
----
|
|
53
53
|
|
|
54
|
-
三节《火候》各讲一道菜,一般不会被单独引用。某一处要指向《红烧肉》下的《火候》时,`pass:[xref:火候[]]`
|
|
54
|
+
三节《火候》各讲一道菜,一般不会被单独引用。某一处要指向《红烧肉》下的《火候》时,`pass:[xref:火候[]]` 对上三节,网页上的链接落到书里最前面的那一节,要指的若是另外两节,就落错了。读投影的工具会把这处引用连同三节各自的位置报出来。这时应把上级标题写在前面,用 `::` 隔开:
|
|
55
55
|
|
|
56
56
|
[source,asciidoc]
|
|
57
57
|
----
|
|
@@ -69,16 +69,16 @@ Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的
|
|
|
69
69
|
|
|
70
70
|
同一个上级下有两个同名的标题时,完整的路径也分不开它们,例如糖醋排骨要炸两遍,《糖醋排骨》下写了两节《炸》。两节同名,多半是标题没有说清这一节讲什么。这时宜先改标题,例如改成《初炸》与《复炸》,改好以后,两节的标题各是各的地址。
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
标题改不了时,标题和限定地址都指不出唯一的一节。还有两种标题,按原文写不出引用:标题原文里带 `#` 或 `::` 的,引用会把它们当作分隔;标题以行内标记开头的,例如 `+`book.adoc` 的职责+`,AsciiDoc 不把以反引号、星号这类标记起头的目标读成引用。要引用这样的一节,应在它的标题上方加 custom ID(`[#…]`),引用写这个 ID。加 custom ID 时,应符合下列规定:
|
|
73
73
|
|
|
74
74
|
* 名字说清这一节讲什么,不写序号和位置词,例如不写 `炸-2`、`第二个炸`,因为序号和位置会随编排变化;
|
|
75
|
-
*
|
|
75
|
+
* 名字不与书里的任何标题相同。例如给《卤味》下讲糖色的一节加了 `[#炒糖色]`,书里又有一节《炒糖色》,这个地址就对上两节;Asciidoctor 先按 ID 去找,网页上 `pass:[xref:炒糖色[]]` 落到加了 ID 的那一节,原来指《炒糖色》的引用便不声不响地指到了别处;
|
|
76
76
|
* 同一个上级下同名的几节,要引用哪一节,就给哪一节加;只给其中一节加时,另一节的标题和限定地址仍然有两个候选;
|
|
77
77
|
* 引用写上显示文本,写出能让读者分清是哪一节的说法。
|
|
78
78
|
|
|
79
79
|
custom ID 是同一个标题的第二个地址,不替代标题。作者是在要引用某一节时才发现标题当不了地址的,custom ID 也宜在那时再加,不必预先写下。
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
标题改名以后,按旧名字写的引用都找不到目标,网页上的这些链接是断的。按旧名字写的引用,地址里都含着旧名字,限定地址也一样,因此改名时在书里搜一遍旧名字,就能找到每一处引用,连同显示文本和正文里写着的旧名字,作者逐处改正即可。预先写下的 ID 会让旧引用在改名后照常落到这一节:这些引用写的是 ID,里面没有旧名字,搜旧名字搜不到它们,可这一节讲的可能已不是它们当初要指的事,ID 里也留下了旧的说法。因此,标题当得了地址时,不应写 custom ID。
|
|
82
82
|
|
|
83
83
|
role 只属于写了它的那个标题。希望查询工具把某个下级标题当作某种 role 列出时,应在它自己的标题上写这个 role。没有写 role 的下级标题只是父标题下的一节,读者按上下文理解它。
|
|
84
84
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
标题是一节现在的名字。读者读到的是它,引用写的也是它,因此标题就是这一节的地址,写法见《xref:标题、寻址与引用[]》。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
作者想到更准确的名字时,先改标题,再改引用。标题改好以后,按旧名字写的引用都找不到目标。它们的地址里都含着旧名字,作者在书里搜一遍旧名字,就能找到每一处引用,连同显示文本和正文里写着的旧名字,逐处改成新名字。
|
|
6
6
|
|
|
7
7
|
一样东西还有别的叫法、又值得让读者查到时,应把别的叫法写进标题下的别名字段,正文只用标题这一个名字(见《xref:一样东西一个名字[]》)。例如:
|
|
8
8
|
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
别名:: 凉拌与腌制(旧名,那时腌制也收在这一节)
|
|
24
24
|
----
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
这时作者除了改标题,还应重读这一节的说明、收录与不收录的内容、相关的节和已经写下的结论。对搜旧名字找到的每一处引用,作者先判断它说的是留下的凉拌,还是去掉的腌制,再决定改成《凉拌菜》,还是改指讲腌制的那一节。
|
|
27
27
|
|
|
28
28
|
改名、拆分或停用一节时,应符合下列规定:
|
|
29
29
|
|
|
30
30
|
* 不为旧标题保留一节“指路”,旧名字写进别名;
|
|
31
31
|
* 一节被拆开或不再使用时,删掉它,或把它改成拆出来的某一节的名字。
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
如果保留旧标题,旧引用仍能落到它,网页上的链接不断,作者也就不会回头看这些引用。删掉或改名以后,旧引用的链接断开,搜一遍旧名字就找到它们,作者再逐一判断每一处该改指哪一节。这一节从前的样子留在版本库的历史里。
|
|
34
34
|
|
|
35
35
|
custom ID 不替代标题,也不为旧名字保留位置。重名的标题怎样写地址、custom ID 什么时候加,见《xref:标题、寻址与引用[]》。
|
|
@@ -49,7 +49,7 @@ fields:: 要求某种 role 的标题在标题下第一个描述列表里写出
|
|
|
49
49
|
|
|
50
50
|
照这两条声明,每道菜都应在 attrlist 里写 `难度`,取易、中、难之一,并在标题下第一个描述列表里写出 `主料` 和 `用时`。`主料` 若写在 attrlist 里(如 `[.dish, 主料=五花肉]`),不算写在描述列表里,不满足 `declares=fields` 的要求。声明只列要求写出的字段,可写可不写的字段照常写,例如《xref:字段[]》里的《红烧肉》多写了 `出处`,仍符合这两条声明。这道菜写全了声明要求的字段,其余各章的菜谱例子只摘出与该章有关的几行。
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
是否照声明写,由作者或读投影的工具核对,工作区的构建不核对。
|
|
53
53
|
|
|
54
54
|
写声明时,应符合下列规定:
|
|
55
55
|
|
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
"packageManager": "pnpm@10.33.0",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "node tools/adoc-books.mjs build",
|
|
11
|
-
"check": "node tools/adoc-books.mjs check",
|
|
12
11
|
"clean": "node tools/adoc-books.mjs clean"
|
|
13
12
|
},
|
|
14
13
|
"devDependencies": {
|
|
15
14
|
"@asciidoctor/core": "^3.0.4",
|
|
16
|
-
"asciidoc-abundant-tree": "^0.
|
|
15
|
+
"asciidoc-abundant-tree": "^0.6.0",
|
|
17
16
|
"asciidoctor-kroki": "^0.18.1"
|
|
18
17
|
}
|
|
19
18
|
}
|
|
@@ -7,7 +7,6 @@ An AsciiDoc multi-book workspace that starts with one book: `books/01-structured
|
|
|
7
7
|
```bash
|
|
8
8
|
pnpm install
|
|
9
9
|
pnpm run build # writes build/html/index.html
|
|
10
|
-
pnpm run check # checks the catalog, books, xrefs, anchors, and local HTML resources
|
|
11
10
|
pnpm run clean # removes build/
|
|
12
11
|
```
|
|
13
12
|
|
|
@@ -37,4 +36,4 @@ The rules a resolver follows are in the
|
|
|
37
36
|
|
|
38
37
|
Copy `books/01-structured-writing-conventions` to `books/<NN-book-id>`, edit its `book.adoc`, then add an
|
|
39
38
|
`xref:books/<NN-book-id>/book.adoc[...]` line to `catalog.adoc`.
|
|
40
|
-
|
|
39
|
+
The build does not compare `catalog.adoc` with `books/`: a book you leave out of the catalog is built but not linked from it.
|