create-asciidoc-multi-book-workspace 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/runtime/adoc-books.mjs +167 -13
  3. package/package.json +2 -2
  4. package/templates/default-workspace/books/00-book-anatomy/backmatter/{appendix-a.adoc → appendix-structure-reference.adoc} +1 -1
  5. package/templates/default-workspace/books/00-book-anatomy/book.adoc +11 -11
  6. package/templates/default-workspace/books/00-book-anatomy/parts/{01-front-structure/01-frontmatter-map.adoc → 010-front-structure/010-frontmatter-map.adoc} +2 -4
  7. package/templates/default-workspace/books/00-book-anatomy/parts/010-front-structure/_partintro.adoc +1 -0
  8. package/templates/default-workspace/books/00-book-anatomy/parts/{02-body-structure/01-body-map.adoc → 020-body-structure/010-body-map.adoc} +0 -1
  9. package/templates/default-workspace/books/00-book-anatomy/parts/020-body-structure/020-inline-structure.adoc +31 -0
  10. package/templates/default-workspace/books/01-starter-book/backmatter/{appendix-a.adoc → appendix-minimal-files.adoc} +1 -1
  11. package/templates/default-workspace/books/01-starter-book/book.adoc +4 -3
  12. package/templates/default-workspace/books/01-starter-book/chapters/{01-opening.adoc → 010-opening.adoc} +0 -1
  13. package/templates/default-workspace/books/02-multipart-monograph/backmatter/appendix-part-reference.adoc +5 -0
  14. package/templates/default-workspace/books/02-multipart-monograph/book.adoc +12 -9
  15. package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain/01-problem-world.adoc → 010-domain/010-problem-world.adoc} +0 -1
  16. package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain → 010-domain}/_partintro.adoc +0 -1
  17. package/templates/default-workspace/books/03-technical-book-workflow/book.adoc +3 -3
  18. package/templates/default-workspace/books/03-technical-book-workflow/chapters/{01-build-flow.adoc → 010-build-flow.adoc} +0 -1
  19. package/templates/default-workspace/books/04-reference-manual/backmatter/glossary.adoc +1 -1
  20. package/templates/default-workspace/books/04-reference-manual/book.adoc +4 -4
  21. package/templates/default-workspace/books/04-reference-manual/chapters/{01-decision-table.adoc → 010-decision-table.adoc} +0 -1
  22. package/templates/default-workspace/books/04-reference-manual/chapters/{02-path-rules.adoc → 020-path-rules.adoc} +0 -1
  23. package/templates/default-workspace/books/04-reference-manual/chapters/030-xref-patterns.adoc +19 -0
  24. package/templates/default-workspace/books/04-reference-manual/chapters/{04-growth-paths.adoc → 040-growth-paths.adoc} +0 -1
  25. package/templates/default-workspace/books/05-upper-volume/book.adoc +2 -2
  26. package/templates/default-workspace/books/05-upper-volume/chapters/{01-upper-opening.adoc → 010-upper-opening.adoc} +0 -1
  27. package/templates/default-workspace/books/05-upper-volume/chapters/020-upper-core-model.adoc +3 -0
  28. package/templates/default-workspace/books/06-lower-volume/backmatter/bibliography.adoc +1 -1
  29. package/templates/default-workspace/books/06-lower-volume/book.adoc +2 -2
  30. package/templates/default-workspace/books/06-lower-volume/chapters/{01-lower-opening.adoc → 010-lower-opening.adoc} +0 -1
  31. package/templates/default-workspace/books/06-lower-volume/chapters/{02-cross-volume-practice.adoc → 020-cross-volume-practice.adoc} +2 -2
  32. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-decision-trees.adoc +198 -0
  33. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-quick-reference.adoc +82 -0
  34. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/bibliography.adoc +13 -3
  35. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/glossary.adoc +14 -16
  36. package/templates/default-workspace/books/07-structured-writing-conventions/book.adoc +24 -12
  37. package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/acknowledgments.adoc +1 -1
  38. package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/colophon.adoc +1 -3
  39. package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/conventions.adoc +17 -0
  40. package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/preface.adoc +3 -3
  41. package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/010-source-and-projection.adoc +66 -52
  42. package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/020-heading-address-and-xref.adoc +65 -24
  43. package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/030-source-order-notation.adoc +20 -21
  44. package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/_partintro.adoc +1 -1
  45. package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/010-names-and-addresses.adoc +35 -0
  46. package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/020-heading-roles.adoc +19 -19
  47. package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/030-relation-predicate.adoc +23 -14
  48. package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/_partintro.adoc +1 -1
  49. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/010-surface-fields.adoc +51 -0
  50. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/030-declaring-conventions.adoc +66 -0
  51. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/_partintro.adoc +1 -0
  52. package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/010-whom-a-book-speaks-to.adoc +64 -0
  53. package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/020-what-a-sentence-does.adoc +57 -0
  54. package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/030-names-and-examples.adoc +53 -0
  55. package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/040-print-conventions.adoc +89 -0
  56. package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/_partintro.adoc +1 -0
  57. package/templates/default-workspace/catalog.adoc +2 -2
  58. package/templates/default-workspace/package.json.template +1 -1
  59. package/templates/solo-workspace/catalog.adoc +1 -1
  60. package/templates/default-workspace/books/00-book-anatomy/frontmatter/abstract.adoc +0 -5
  61. package/templates/default-workspace/books/00-book-anatomy/parts/01-front-structure/_partintro.adoc +0 -1
  62. package/templates/default-workspace/books/00-book-anatomy/parts/02-body-structure/02-inline-structure.adoc +0 -32
  63. package/templates/default-workspace/books/02-multipart-monograph/backmatter/appendix-a.adoc +0 -5
  64. package/templates/default-workspace/books/04-reference-manual/chapters/03-xref-patterns.adoc +0 -20
  65. package/templates/default-workspace/books/05-upper-volume/chapters/02-upper-core-model.adoc +0 -4
  66. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-a.adoc +0 -82
  67. package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-b-decision-trees.adoc +0 -187
  68. package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/abstract.adoc +0 -3
  69. package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/010-names-and-address-labels.adoc +0 -20
  70. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/010-surface-fields.adoc +0 -72
  71. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/020-glossary.adoc +0 -6
  72. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/030-declaring-conventions.adoc +0 -70
  73. package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/_partintro.adoc +0 -1
  74. /package/templates/default-workspace/books/00-book-anatomy/parts/{02-body-structure → 020-body-structure}/_partintro.adoc +0 -0
  75. /package/templates/default-workspace/books/01-starter-book/chapters/{02-main-flow.adoc → 020-main-flow.adoc} +0 -0
  76. /package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain/02-boundary.adoc → 010-domain/020-boundary.adoc} +0 -0
  77. /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification/01-book-contract.adoc → 020-specification/010-book-contract.adoc} +0 -0
  78. /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification/02-verification.adoc → 020-specification/020-verification.adoc} +0 -0
  79. /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification → 020-specification}/_partintro.adoc +0 -0
  80. /package/templates/default-workspace/books/03-technical-book-workflow/chapters/{02-diagrams.adoc → 020-diagrams.adoc} +0 -0
  81. /package/templates/default-workspace/books/03-technical-book-workflow/chapters/{03-code-includes.adoc → 030-code-includes.adoc} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0 - 2026-10-31
4
+
5
+ ### Added
6
+
7
+ - `pnpm run check` binds a qualified address: when several headings share a title, a book refers to one of them by writing the titles above it, joined with `::`, as in `xref:红烧肉::火候[]` or, across books, `xref:{dish-book}#红烧肉::火候[]`. The HTML link reaches that heading.
8
+
9
+ ### Changed
10
+
11
+ - Generated workspaces depend on `asciidoc-abundant-tree` `^0.3.0`. Which xrefs exist and what their targets say are now read by Asciidoctor inside it, and the runtime no longer works around its gaps: xrefs inside passthroughs (`pass:[…]`, `+++…+++`, `$$…$$`, `` `+…+` ``) or escaped with `\` are not checked; `xref:` targets with spaces are bound and checked; `xref:#id[]` names `id`; an attribute reference in a target takes its value at the xref's position, where it used to take the value the book held after conversion; `xref:other#x[]` without `.adoc` names the document `other`, as Asciidoctor links it. The projection itself changed in 0.3.0 (fields are `field:<name>`, heading nodes are named by their address); tools that read it follow that release's changelog.
12
+ - The structured writing conventions sample teaches qualified addresses for shared titles, written with link text that joins the titles with an interpunct (《红烧肉·火候》). Two headings that share a title under one parent are renamed so each title says what its section is (糖醋排骨's two 炸 become 初炸 and 复炸); a custom ID is the second address only for a title that cannot serve, with rules for naming it. After a rename, an author also searches the book for the old name in link text and prose, which the check does not report. The projection example shows heading nodes named by their address and the field predicate `field:难度`, and the fields chapter states that a field name in an attrlist uses only letters, digits, `_`, and `-`. `00-book-anatomy` and the `04-reference-manual` glossary say the same about custom IDs.
13
+
14
+ ### Upgrading an existing workspace
15
+
16
+ A generated workspace owns its `tools/adoc-books.mjs`, and this release changes it. Create a temporary workspace with `pnpm create asciidoc-multi-book-workspace@0.6.0 tmp-books`, copy its `tools/adoc-books.mjs` over yours, set `"asciidoc-abundant-tree": "^0.3.0"` in your `package.json`, and run `pnpm install`. Books need no change; a heading that declares an ID only to tell it apart from others with the same title can drop the ID once its references use the qualified address.
17
+
18
+ ## 0.5.1 - 2026-10-23
19
+
20
+ ### Changed
21
+
22
+ - Generated workspaces depend on `asciidoc-abundant-tree` `^0.2.6`. Releases 0.2.2–0.2.6 correct facts the reader pages are built from: edges with no source heading in included files, bibliography references, the Asciidoctor binding (`href`, reftext) of xrefs in descriptions and list items, and the span of description lists. The type of the attributes an include resolver receives now admits `null` (a blank positional attribute), as the runtime already passed it.
23
+
24
+ ### Upgrading an existing workspace
25
+
26
+ Set `"asciidoc-abundant-tree": "^0.2.6"` in your `package.json` and run `pnpm install`. `tools/adoc-books.mjs` is unchanged in behavior; there is no need to copy it.
27
+
3
28
  ## 0.5.0 - 2026-10-21
4
29
 
5
30
  ### Added
@@ -5,13 +5,16 @@ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promi
5
5
  import path from "node:path";
6
6
  import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import asciidoctorFactory from "@asciidoctor/core";
8
- import { parseAbundantTree } from "asciidoc-abundant-tree";
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
11
  const CATALOG_BOOK_XREF_PATTERN = /xref:books\/([^/\]]+)\/book\.adoc(?:#[^\[]+)?\[/g;
12
12
  const LOCAL_TARGET_PATTERN = /\b(href|src)="([^"]+)"/g;
13
13
  const HTML_ANCHOR_PATTERN = /\b(?:id|name)="([^"]+)"/g;
14
14
  const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
15
+ const AAT_NAMESPACE = "https://micheng.dev/ns/asciidoc-abundant-tree#";
16
+ const HTML_FRAGMENT_LINK_PATTERN = /<a href="#([^"]*)"([^>]*)>([\s\S]*?)<\/a>/g;
17
+ const HTML_DOCUMENT_LINK_PATTERN = /<a href="([^"#]+\.html)#([^"]*)"([^>]*)>([\s\S]*?)<\/a>/g;
15
18
  const HOME_MARKER = "data-multi-book-home";
16
19
  const CONTROLS_MARKER = "data-multi-book-controls";
17
20
  const FETCH_DIAGRAMS_ENV = "ADOC_BOOKS_FETCH_DIAGRAMS";
@@ -192,7 +195,6 @@ function includeResolverRegistry(asciidoctor, config) {
192
195
  try {
193
196
  resolution = resolver.resolve({
194
197
  target: target.slice(resolver.prefix.length),
195
- // aat 一侧对留空的位置属性同样交 null,它的类型声明没有写出来。
196
198
  attributes: includeAttributes(attributes),
197
199
  sourcePath
198
200
  });
@@ -323,8 +325,9 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
323
325
  const defaultLogger = loggerManager.getLogger();
324
326
  const memoryLogger = asciidoctor.MemoryLogger.create();
325
327
  loggerManager.setLogger(memoryLogger);
328
+ let document;
326
329
  try {
327
- asciidoctor.convertFile(input, {
330
+ document = asciidoctor.convertFile(input, {
328
331
  safe: "unsafe",
329
332
  base_dir: baseDir,
330
333
  to_file: outputFile,
@@ -340,7 +343,9 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
340
343
  if (messages.length > 0) {
341
344
  throw new Error(`AsciiDoc strict check failed for ${input}\n${messages.map(formatAsciidoctorMessage).join("\n")}`);
342
345
  }
346
+ return document;
343
347
  }
348
+ // 交回每本书转换后的 Asciidoctor 文档:书内与跨书 xref 的目标要用它展开属性引用(bindReferences)。
344
349
  async function buildHtml(rootDir, books, asciidoctor, extensionRegistry, useKroki, fetchDiagrams) {
345
350
  await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
346
351
  convertFile(asciidoctor, extensionRegistry, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
@@ -465,14 +470,16 @@ function sourceBundleFromDocument(document) {
465
470
  // 解析失败就让构建失败,不退回只有封面的阅读页:退回时 include 越过 documentRoot 只剩一条 warning,
466
471
  // 构建照常成功,没有人发现阅读页坏了(DESIGN.adoc 的 Workspace Contract Checks)。
467
472
  async function buildReaderBookData(config, book) {
473
+ let parsed;
468
474
  let document;
469
475
  try {
470
- document = await parseAbundantTree({
476
+ parsed = await parseAbundantTree({
471
477
  sourcePath: book.input,
472
478
  mode: "book-entry",
473
479
  documentRoot: config.documentRoot,
474
480
  includeResolvers: config.includeResolvers
475
481
  });
482
+ document = parsed;
476
483
  }
477
484
  catch (error) {
478
485
  const message = error instanceof Error ? error.message : String(error);
@@ -491,7 +498,9 @@ async function buildReaderBookData(config, book) {
491
498
  const sections = (Array.isArray(document.children) ? document.children : []).filter((child) => {
492
499
  return typeof child === "object" && child !== null && child.kind === "section";
493
500
  });
494
- const frontmatterStyles = new Set(["abstract", "colophon", "dedication", "preface", "acknowledgments"]);
501
+ // abstract 不在其中:它是 article 的样式,book 里的 [abstract] 被 Asciidoctor 当作编号的一章,阅读器照它渲染的样子
502
+ // 当章节显示(docs/design/book-dual-reader-view.adoc 的 Page Map)。
503
+ const frontmatterStyles = new Set(["colophon", "dedication", "preface", "acknowledgments"]);
495
504
  const backmatterStyles = new Set(["appendix", "glossary", "bibliography", "index"]);
496
505
  const frontmatter = sections.filter((section) => section.level === 1 && frontmatterStyles.has(sectionStyle(section) ?? ""));
497
506
  if (frontmatter.length > 0) {
@@ -544,9 +553,143 @@ async function buildReaderBookData(config, book) {
544
553
  };
545
554
  return {
546
555
  pageMap,
547
- sourceBundle: sourceBundleFromDocument(document)
556
+ sourceBundle: sourceBundleFromDocument(document),
557
+ references: bindReferences(config, parsed)
548
558
  };
549
559
  }
560
+ // 书内 xref 指向谁,以 aat 的绑定为准(DESIGN.adoc 的 Reference Binding):标题原文、限定地址、手写 ID、生成的 ID
561
+ // 都在它的 label space 里,重名给出候选而不替人挑。哪些是 xref、目标写的是什么(透传与转义不算,属性已展开,
562
+ // 指向别的文档的另记 targetDocument)也由 aat 里的 Asciidoctor 读出;这里只读投影里每条 xref 边的结果。
563
+ function bindReferences(config, document) {
564
+ const projection = rdf12(document, { documentRoot: config.documentRoot });
565
+ const graph = projection.graph;
566
+ const values = (subject, name) => {
567
+ return graph.match({ subject, predicate: { termType: "iri", value: `${AAT_NAMESPACE}${name}` } })
568
+ .map((triple) => triple.object.termType === "triple" ? "" : triple.object.value);
569
+ };
570
+ const headings = new Map(projection.nodeIndex.entries()
571
+ .filter((entry) => entry.kind === "section")
572
+ .map((entry) => [entry.iri.value, entry]));
573
+ const located = (iris) => iris.flatMap((iri) => {
574
+ const entry = headings.get(iri);
575
+ const id = entry?.node.ids?.[0];
576
+ if (entry === undefined || id === undefined)
577
+ return [];
578
+ return [{ id, title: entry.node.title ?? id, relativePath: entry.relativePath ?? "", line: entry.sourceStartLine ?? entry.startLine }];
579
+ });
580
+ const owners = (selector) => {
581
+ return located(projection.labelCatalog.owners(selector).map((owner) => owner.value));
582
+ };
583
+ // 指向整本书的 xref(没有片段)没有 targetSelector,不在这里:它没有可绑定的东西,留给 HTML 本地资源检查。
584
+ const edges = graph.match({ predicate: { termType: "iri", value: `${AAT_NAMESPACE}targetSelector` } }).map((triple) => ({
585
+ iri: triple.subject,
586
+ selector: triple.object.termType === "triple" ? "" : triple.object.value,
587
+ document: values(triple.subject, "targetDocument")[0],
588
+ relativePath: values(triple.subject, "relativePath")[0] ?? "",
589
+ line: Number(values(triple.subject, "startLine")[0] ?? 0),
590
+ column: Number(values(triple.subject, "startColumn")[0] ?? 0)
591
+ })).sort((left, right) => left.relativePath.localeCompare(right.relativePath) || left.line - right.line || left.column - right.column);
592
+ const headingsByFragment = new Map();
593
+ const crossBook = [];
594
+ const issues = [];
595
+ for (const edge of edges) {
596
+ const where = `${edge.relativePath}:${edge.line} ${values(edge.iri, "raw")[0] ?? edge.selector}`;
597
+ if (edge.document !== undefined) {
598
+ crossBook.push({ where, document: edge.document, fragment: edge.selector });
599
+ continue;
600
+ }
601
+ const fragment = edge.selector;
602
+ const found = located([...values(edge.iri, "targetHeading"), ...values(edge.iri, "candidateHeading")]);
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);
607
+ continue;
608
+ }
609
+ if (found.length > 1) {
610
+ issues.push(issue("XREF_AMBIGUOUS", `${where} -> ${describeCandidates(found)}`));
611
+ continue;
612
+ }
613
+ issues.push(issue("XREF_UNRESOLVED", where));
614
+ }
615
+ return { headingsByFragment, crossBook, owners, issues };
616
+ }
617
+ function describeCandidates(headings) {
618
+ return [...headings]
619
+ .sort((left, right) => left.relativePath.localeCompare(right.relativePath) || left.line - right.line)
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 本地资源检查。
626
+ async function bindCrossBookReferences(book, references, bindingsByInput, anchorCache) {
627
+ const linksByHref = new Map();
628
+ const issues = [];
629
+ for (const xref of references.crossBook) {
630
+ const { fragment } = xref;
631
+ // Asciidoctor 把 xref:other#x[] 与 xref:other.adoc#x[] 都读成文档 other:去掉 .adoc、没有扩展名的照写,
632
+ // 再加 .html 写进 href。书的入口按 .adoc 登记,链接按 .html 改写,两边都从这个文档名出发。
633
+ const documentName = path.extname(xref.document) === ".adoc" ? xref.document.slice(0, -".adoc".length) : xref.document;
634
+ const target = bindingsByInput.get(path.resolve(book.bookDir, `${documentName}.adoc`));
635
+ if (fragment === "" || target === undefined)
636
+ continue;
637
+ const found = target.references.owners(fragment);
638
+ if (found.length === 0) {
639
+ issues.push(issue("XREF_UNRESOLVED", `${xref.where} -> ${target.book.bookId}`));
640
+ continue;
641
+ }
642
+ if (found.length > 1) {
643
+ issues.push(issue("XREF_AMBIGUOUS", `${xref.where} -> ${target.book.bookId}: ${describeCandidates(found)}`));
644
+ continue;
645
+ }
646
+ const heading = found[0];
647
+ // 指向本书自己的 book.adoc:Asciidoctor 把它当书内引用,写成 #片段。
648
+ if (target.book.bookId === book.bookId) {
649
+ references.headingsByFragment.set(fragment, heading);
650
+ references.headingsByFragment.set(escapeHtmlAttribute(fragment), heading);
651
+ continue;
652
+ }
653
+ const anchors = await anchorsForHtmlFile(path.join(target.book.htmlOutputDir, "book.html"), anchorCache);
654
+ if (anchors.has(fragment))
655
+ continue;
656
+ const htmlPath = `${documentName}.html`;
657
+ const link = { href: `${htmlPath}#${heading.id}`, title: heading.title };
658
+ linksByHref.set(`${htmlPath}#${fragment}`, link);
659
+ linksByHref.set(escapeHtmlAttribute(`${htmlPath}#${fragment}`), link);
660
+ }
661
+ return { linksByHref, issues };
662
+ }
663
+ // 绑定上、而目标书的 HTML 里没有这个片段的跨书链接,改成指向绑定的标题。Asciidoctor 对没写显示文字的跨书 xref
664
+ // 显示文档路径;改写时换成标题。
665
+ function rewriteCrossBookXrefs(html, linksByHref) {
666
+ if (linksByHref.size === 0)
667
+ return html;
668
+ return html.replace(HTML_DOCUMENT_LINK_PATTERN, (whole, htmlPath, fragment, rest, text) => {
669
+ const link = linksByHref.get(`${htmlPath}#${fragment}`) ?? linksByHref.get(`${htmlPath}#${decodeUrlFragment(fragment)}`);
670
+ if (link === undefined)
671
+ return whole;
672
+ const shown = text === htmlPath ? escapeHtmlAttribute(link.title) : text;
673
+ return `<a href="${escapeHtmlAttribute(link.href)}"${rest}>${shown}</a>`;
674
+ });
675
+ }
676
+ // Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat 绑定上的,改成指向绑定的标题,
677
+ // 显示标题。片段已经是 HTML 里某个元素的 id 时,保持 Asciidoctor 写下的样子。
678
+ function rewriteBoundXrefs(html, headingsByFragment) {
679
+ if (headingsByFragment.size === 0)
680
+ return html;
681
+ const anchors = htmlAnchors(html);
682
+ return html.replace(HTML_FRAGMENT_LINK_PATTERN, (whole, fragment, rest, text) => {
683
+ const decoded = decodeUrlFragment(fragment);
684
+ if (anchors.has(fragment) || anchors.has(decoded))
685
+ return whole;
686
+ const heading = headingsByFragment.get(fragment) ?? headingsByFragment.get(decoded);
687
+ if (heading === undefined)
688
+ return whole;
689
+ const shown = text === `[${fragment}]` ? escapeHtmlAttribute(heading.title) : text;
690
+ return `<a href="#${escapeHtmlAttribute(heading.id)}"${rest}>${shown}</a>`;
691
+ });
692
+ }
550
693
  function readerStyles() {
551
694
  return `
552
695
  .multi-book-reader-toggle {
@@ -1062,13 +1205,24 @@ ${readerStyles()}
1062
1205
  }
1063
1206
  async function addHomeLinks(rootDir, books, config) {
1064
1207
  const catalog = path.join(rootDir, "build", "html", "catalog.html");
1208
+ // 跨书 xref 在目标书的 label space 里绑定,所以先把每一本书都解析完。
1209
+ const readerData = new Map();
1210
+ for (const book of books) {
1211
+ readerData.set(book.bookId, await buildReaderBookData(config, book));
1212
+ }
1213
+ const bindingsByInput = new Map(books.map((book) => [book.input, { book, references: readerData.get(book.bookId).references }]));
1214
+ const anchorCache = new Map();
1215
+ const issues = [];
1065
1216
  for (const book of books) {
1066
1217
  const htmlFile = path.join(book.htmlOutputDir, "book.html");
1067
- const html = await readFile(htmlFile, "utf8");
1068
1218
  const href = path.relative(path.dirname(htmlFile), catalog);
1069
- const readerData = await buildReaderBookData(config, book);
1070
- await writeFile(htmlFile, addBookControlsToBookHtml(html, href, config.homeLink, readerData.sourceBundle, readerData.pageMap, book.bookId), "utf8");
1219
+ const data = readerData.get(book.bookId);
1220
+ const crossBook = await bindCrossBookReferences(book, data.references, bindingsByInput, anchorCache);
1221
+ issues.push(...data.references.issues, ...crossBook.issues);
1222
+ const html = rewriteCrossBookXrefs(rewriteBoundXrefs(await readFile(htmlFile, "utf8"), data.references.headingsByFragment), crossBook.linksByHref);
1223
+ await writeFile(htmlFile, addBookControlsToBookHtml(html, href, config.homeLink, data.sourceBundle, data.pageMap, book.bookId), "utf8");
1071
1224
  }
1225
+ return issues;
1072
1226
  }
1073
1227
  async function writeRootIndex(rootDir, rootIndex) {
1074
1228
  const outputDir = path.join(rootDir, "build", "html");
@@ -1096,9 +1250,8 @@ function extractCatalogBookIds(catalogSource) {
1096
1250
  function issue(code, detail) {
1097
1251
  return { code, detail };
1098
1252
  }
1099
- // xref 的目标与锚点不在这里查:Asciidoctor 把它们写成 HTML 里的链接与 ID,missingLocalResources 在生成的 HTML 上
1100
- // 核对(DESIGN.adoc 的 Workspace Contract Checks)。ID 有 [#id]、[.role#id]、[[id]]、[id=id] 与标题生成等写法,
1101
- // 只有 Asciidoctor 认得全。
1253
+ // xref 不在这里查:书内与跨书的引用以 aat 的绑定为准(bindReferences、bindCrossBookReferences),不存在的书与其余
1254
+ // 锚点由 missingLocalResources 在生成的 HTML 上核对(DESIGN.adoc 的 Workspace Contract Checks 与 Reference Binding)。
1102
1255
  async function workspaceContractIssues(rootDir, books) {
1103
1256
  const issues = [];
1104
1257
  const catalogSource = await readIfExists(path.join(rootDir, "catalog.adoc"));
@@ -1205,8 +1358,9 @@ export async function buildWorkspace(rootDir = process.cwd()) {
1205
1358
  const asciidoctor = createAsciidoctor(useKroki);
1206
1359
  await buildHtml(rootDir, books, asciidoctor, includeResolverRegistry(asciidoctor, config), useKroki, fetchDiagrams);
1207
1360
  await copyAssets(rootDir, books);
1208
- await addHomeLinks(rootDir, books, config);
1361
+ const referenceIssues = await addHomeLinks(rootDir, books, config);
1209
1362
  await writeRootIndex(rootDir, config.rootIndex);
1363
+ await assertNoIssues("reference binding check", referenceIssues);
1210
1364
  await assertNoIssues("HTML local resource check", await missingLocalResources(rootDir));
1211
1365
  await assertNoIssues("workspace contract check", await workspaceContractIssues(rootDir, books));
1212
1366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asciidoc-multi-book-workspace",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": "^0.2.1",
57
+ "asciidoc-abundant-tree": "^0.3.0",
58
58
  "asciidoctor-kroki": "^0.18.1",
59
59
  "playwright": "^1.59.1",
60
60
  "typescript": "^5.9.3"
@@ -1,4 +1,4 @@
1
- = 附录 A:结构速查
1
+ = 结构速查
2
2
 
3
3
  [cols="1,2", options="header"]
4
4
  |===
@@ -5,16 +5,16 @@ v0.1, 2026-05
5
5
  :toc: left
6
6
  :toclevels: 3
7
7
  :sectnums:
8
+ :partnums:
9
+ :part-signifier: Part
8
10
  :icons: font
9
11
  :experimental:
12
+ :appendix-caption: 附录
10
13
  :idprefix:
11
14
  :idseparator: -
12
15
 
13
16
  include::../../shared/attributes.adoc[]
14
17
 
15
- [abstract]
16
- include::frontmatter/abstract.adoc[]
17
-
18
18
  [colophon]
19
19
  include::frontmatter/colophon.adoc[]
20
20
 
@@ -27,24 +27,24 @@ include::frontmatter/preface.adoc[]
27
27
  [acknowledgments]
28
28
  include::frontmatter/acknowledgments.adoc[]
29
29
 
30
- = 第一部:前置结构
30
+ = 前置结构
31
31
 
32
32
  [partintro]
33
- include::parts/01-front-structure/_partintro.adoc[]
33
+ include::parts/010-front-structure/_partintro.adoc[]
34
34
 
35
- include::parts/01-front-structure/01-frontmatter-map.adoc[]
35
+ include::parts/010-front-structure/010-frontmatter-map.adoc[]
36
36
 
37
- = 第二部:正文结构
37
+ = 正文结构
38
38
 
39
39
  [partintro]
40
- include::parts/02-body-structure/_partintro.adoc[]
40
+ include::parts/020-body-structure/_partintro.adoc[]
41
41
 
42
- include::parts/02-body-structure/01-body-map.adoc[]
42
+ include::parts/020-body-structure/010-body-map.adoc[]
43
43
 
44
- include::parts/02-body-structure/02-inline-structure.adoc[]
44
+ include::parts/020-body-structure/020-inline-structure.adoc[]
45
45
 
46
46
  [appendix]
47
- include::backmatter/appendix-a.adoc[]
47
+ include::backmatter/appendix-structure-reference.adoc[]
48
48
 
49
49
  [glossary]
50
50
  include::backmatter/glossary.adoc[]
@@ -1,4 +1,3 @@
1
- [#frontmatter-map]
2
1
  == 前置内容地图
3
2
 
4
3
  前置内容用于建立读者进入正文之前需要知道的说明、来源和阅读位置。
@@ -9,9 +8,6 @@
9
8
  |===
10
9
  |结构 |职责
11
10
 
12
- |`[abstract]`
13
- |说明整本书覆盖什么。
14
-
15
11
  |`[colophon]`
16
12
  |记录版本、出版或来源信息。
17
13
 
@@ -24,3 +20,5 @@
24
20
  |`[acknowledgments]`
25
21
  |放置致谢。
26
22
  |===
23
+
24
+ 书没有摘要。`[abstract]` 是 article 的 section style:摘要概述一篇文章,书的前置内容里没有这一项。在 book 里写 `[abstract]`,Asciidoctor 会把那一节当作普通的一章编号,它后面每一章的编号都跟着多一。一本书覆盖什么、怎样读,写在前言里。
@@ -0,0 +1 @@
1
+ 本部展示一本书进入正文之前的结构位置。版本说明、献辞、前言和致谢都属于书籍前置内容。
@@ -1,4 +1,3 @@
1
- [#body-map]
2
1
  == 正文结构地图
3
2
 
4
3
  part 是 book doctype 中 document title 之后的 level 0 section。level 1 section 通常形成 chapter;multipart book 中 chapter 位于 part 下,普通 book 中 chapter 可以直接位于 book 下。
@@ -0,0 +1,31 @@
1
+ == 内联结构标记
2
+
3
+ 内联结构标记给正文中的术语、路径和索引入口标出它们的身份,并写出正文对别的章节的引用。它不改变 book 的章节层级,但会提高正文片段的可维护性和可检索性。
4
+
5
+ [cols="1,2", options="header"]
6
+ |===
7
+ |写法 |结构作用
8
+
9
+ |`xref:正文结构地图[]`
10
+ |按标题引用同一本书里的一节:标题就是这一节的地址。
11
+
12
+ |`[#id]`
13
+ |custom ID:给标题再加一个地址。同名的标题先写上级标题、以 `::` 隔开来引用(`pass:[xref:上级标题::本节标题[]]`);同一个上级下也同名,或标题里有 `#`、`::` 时才写。
14
+
15
+ |`indexterm2:[term]`
16
+ |把正文中已经出现的词作为可见索引词。
17
+
18
+ |`indexterm:[term, detail]`
19
+ |正文没有出现该词时,补充隐藏索引入口。
20
+
21
+ |`[.term]#inline role#`
22
+ |给术语片段提供语义身份。
23
+
24
+ |`[.path]` + monospace path
25
+ |给路径文本提供语义身份。
26
+
27
+ |`{series-name}`
28
+ |引用共享属性,减少多处重复文本。
29
+ |===
30
+
31
+ 语义 inline role 提供结构身份;具体视觉效果取决于输出样式。本书使用 [.term]#inline role# 标记术语片段,使用 [.path]`parts/020-body-structure/` 标记路径片段。
@@ -1,4 +1,4 @@
1
- == 附录 A:最小文件清单
1
+ == 最小文件清单
2
2
 
3
3
  普通新书通常需要:
4
4
 
@@ -7,6 +7,7 @@ v0.1, 2026-05
7
7
  :sectnums:
8
8
  :icons: font
9
9
  :experimental:
10
+ :appendix-caption: 附录
10
11
  :idprefix:
11
12
  :idseparator: -
12
13
 
@@ -15,12 +16,12 @@ include::../../shared/attributes.adoc[]
15
16
  [preface]
16
17
  include::frontmatter/preface.adoc[]
17
18
 
18
- include::chapters/01-opening.adoc[]
19
+ include::chapters/010-opening.adoc[]
19
20
 
20
- include::chapters/02-main-flow.adoc[]
21
+ include::chapters/020-main-flow.adoc[]
21
22
 
22
23
  [appendix]
23
- include::backmatter/appendix-a.adoc[]
24
+ include::backmatter/appendix-minimal-files.adoc[]
24
25
 
25
26
  [bibliography]
26
27
  include::backmatter/bibliography.adoc[]
@@ -1,4 +1,3 @@
1
- [#starter-book-boundary]
2
1
  == 单书边界
3
2
 
4
3
  一本普通书由自己的 `book.adoc` 定义边界。入口文件负责书名、属性、章节顺序、前置内容和后置内容。
@@ -0,0 +1,5 @@
1
+ = part 速查
2
+
3
+ * `= 问题世界`:同一本 book 内部的 part。标题只写语义名;`Part I`、`Part II` 这样的序号由书头的 `:partnums:` 生成,不写进标题。
4
+ * `[partintro]`:part 开头的说明块。
5
+ * `== 章标题`:part 下的 chapter。
@@ -5,33 +5,36 @@ v0.1, 2026-05
5
5
  :toc: left
6
6
  :toclevels: 3
7
7
  :sectnums:
8
+ :partnums:
9
+ :part-signifier: Part
8
10
  :icons: font
9
11
  :experimental:
12
+ :appendix-caption: 附录
10
13
  :idprefix:
11
14
  :idseparator: -
12
15
 
13
16
  include::../../shared/attributes.adoc[]
14
17
 
15
- = 第一部:问题世界
18
+ = 问题世界
16
19
 
17
20
  [partintro]
18
- include::parts/01-domain/_partintro.adoc[]
21
+ include::parts/010-domain/_partintro.adoc[]
19
22
 
20
- include::parts/01-domain/01-problem-world.adoc[]
23
+ include::parts/010-domain/010-problem-world.adoc[]
21
24
 
22
- include::parts/01-domain/02-boundary.adoc[]
25
+ include::parts/010-domain/020-boundary.adoc[]
23
26
 
24
- = 第二部:书籍规约
27
+ = 书籍规约
25
28
 
26
29
  [partintro]
27
- include::parts/02-specification/_partintro.adoc[]
30
+ include::parts/020-specification/_partintro.adoc[]
28
31
 
29
- include::parts/02-specification/01-book-contract.adoc[]
32
+ include::parts/020-specification/010-book-contract.adoc[]
30
33
 
31
- include::parts/02-specification/02-verification.adoc[]
34
+ include::parts/020-specification/020-verification.adoc[]
32
35
 
33
36
  [appendix]
34
- include::backmatter/appendix-a.adoc[]
37
+ include::backmatter/appendix-part-reference.adoc[]
35
38
 
36
39
  [bibliography]
37
40
  include::backmatter/bibliography.adoc[]
@@ -1,4 +1,3 @@
1
- [#multipart-book-boundary]
2
1
  == 长书的问题世界
3
2
 
4
3
  长书仍然只有一个书名、一个入口和一个连续的阅读产物。第一部、第二部只是同一本书内部的大分部。
@@ -1,2 +1 @@
1
- [#partintro-example]
2
1
  本部说明为什么同一本长书需要 part。part intro 为该部提供进入正文之前的过渡说明。
@@ -15,11 +15,11 @@ include::../../shared/attributes.adoc[]
15
15
  [preface]
16
16
  include::frontmatter/preface.adoc[]
17
17
 
18
- include::chapters/01-build-flow.adoc[]
18
+ include::chapters/010-build-flow.adoc[]
19
19
 
20
- include::chapters/02-diagrams.adoc[]
20
+ include::chapters/020-diagrams.adoc[]
21
21
 
22
- include::chapters/03-code-includes.adoc[]
22
+ include::chapters/030-code-includes.adoc[]
23
23
 
24
24
  [bibliography]
25
25
  include::backmatter/bibliography.adoc[]
@@ -1,4 +1,3 @@
1
- [#technical-build-flow]
2
1
  == 构建流程
3
2
 
4
3
  技术书仍然是一本文档边界独立的 book。它可以使用共享属性和共享图片,但自己的示例代码保留在书内。
@@ -4,5 +4,5 @@ book:: 具有独立 `book.adoc` 和构建产物的书籍单位。
4
4
  part:: 同一本 book 内部的 level 0 大分部。
5
5
  catalog:: 多书工作区的读者导航入口。
6
6
  shared resource:: 被 catalog 或多本书共同引用的资源。
7
- explicit anchor:: 使用 `[#id]` 写出的稳定引用目标。
7
+ custom ID:: 用 `[#id]` 给标题加的另一个地址。标题就是地址;同名的标题写上级标题、以 `::` 隔开来引用,同一个上级下也同名或标题里有 `#`、`::` 时,才给它再加一个。
8
8
  contract check:: 对工作区源文件约定进行静态检查的脚本行为。
@@ -15,13 +15,13 @@ include::../../shared/attributes.adoc[]
15
15
  [preface]
16
16
  include::frontmatter/preface.adoc[]
17
17
 
18
- include::chapters/01-decision-table.adoc[]
18
+ include::chapters/010-decision-table.adoc[]
19
19
 
20
- include::chapters/02-path-rules.adoc[]
20
+ include::chapters/020-path-rules.adoc[]
21
21
 
22
- include::chapters/03-xref-patterns.adoc[]
22
+ include::chapters/030-xref-patterns.adoc[]
23
23
 
24
- include::chapters/04-growth-paths.adoc[]
24
+ include::chapters/040-growth-paths.adoc[]
25
25
 
26
26
  [glossary]
27
27
  include::backmatter/glossary.adoc[]
@@ -1,4 +1,3 @@
1
- [#decision-table]
2
1
  == 结构选择表
3
2
 
4
3
  [cols="1,2,2", options="header"]
@@ -1,4 +1,3 @@
1
- [#path-rules]
2
1
  == 路径规则
3
2
 
4
3
  Book directory:: `books/<book-id>/`,一本独立 book 的边界。
@@ -0,0 +1,19 @@
1
+ == 引用模式
2
+
3
+ 书内引用写目标那一节的标题:
4
+
5
+ [source,asciidoc]
6
+ ----
7
+ xref:结构选择表[]
8
+ ----
9
+
10
+ 跨书引用写向 AsciiDoc 源文件,`#` 后面写目标书里那一节的标题:
11
+
12
+ [source,asciidoc]
13
+ ----
14
+ xref + : + ../other-book/book.adoc#目标章节[]
15
+ ----
16
+
17
+ 上面的写法用分段文本展示,避免默认样本之间形成真实链接依赖。实际写作时去掉空格和加号,写成一个完整的 cross reference 宏。
18
+
19
+ 被跨书引用的标题不需要另写 anchor:标题就是地址,跨书与书内一样。目标标题改了名,`pnpm run check` 会指出每一处没有跟上的引用。
@@ -1,4 +1,3 @@
1
- [#growth-paths]
2
1
  == 工作区增长路径
3
2
 
4
3
  当前模板只把已经服务样本书、公共文档或脚本的目录放入源目录树。
@@ -15,9 +15,9 @@ include::../../shared/attributes.adoc[]
15
15
  [preface]
16
16
  include::frontmatter/preface.adoc[]
17
17
 
18
- include::chapters/01-upper-opening.adoc[]
18
+ include::chapters/010-upper-opening.adoc[]
19
19
 
20
- include::chapters/02-upper-core-model.adoc[]
20
+ include::chapters/020-upper-core-model.adoc[]
21
21
 
22
22
  [bibliography]
23
23
  include::backmatter/bibliography.adoc[]