create-asciidoc-multi-book-workspace 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/runtime/adoc-books.mjs +167 -12
- package/package.json +2 -2
- package/templates/default-workspace/books/00-book-anatomy/backmatter/{appendix-a.adoc → appendix-structure-reference.adoc} +1 -1
- package/templates/default-workspace/books/00-book-anatomy/book.adoc +11 -11
- 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
- package/templates/default-workspace/books/00-book-anatomy/parts/010-front-structure/_partintro.adoc +1 -0
- 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
- package/templates/default-workspace/books/00-book-anatomy/parts/020-body-structure/020-inline-structure.adoc +31 -0
- package/templates/default-workspace/books/01-starter-book/backmatter/{appendix-a.adoc → appendix-minimal-files.adoc} +1 -1
- package/templates/default-workspace/books/01-starter-book/book.adoc +4 -3
- package/templates/default-workspace/books/01-starter-book/chapters/{01-opening.adoc → 010-opening.adoc} +0 -1
- package/templates/default-workspace/books/02-multipart-monograph/backmatter/appendix-part-reference.adoc +5 -0
- package/templates/default-workspace/books/02-multipart-monograph/book.adoc +12 -9
- package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain/01-problem-world.adoc → 010-domain/010-problem-world.adoc} +0 -1
- package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain → 010-domain}/_partintro.adoc +0 -1
- package/templates/default-workspace/books/03-technical-book-workflow/book.adoc +3 -3
- package/templates/default-workspace/books/03-technical-book-workflow/chapters/{01-build-flow.adoc → 010-build-flow.adoc} +0 -1
- package/templates/default-workspace/books/04-reference-manual/backmatter/glossary.adoc +1 -1
- package/templates/default-workspace/books/04-reference-manual/book.adoc +4 -4
- package/templates/default-workspace/books/04-reference-manual/chapters/{01-decision-table.adoc → 010-decision-table.adoc} +0 -1
- package/templates/default-workspace/books/04-reference-manual/chapters/{02-path-rules.adoc → 020-path-rules.adoc} +0 -1
- package/templates/default-workspace/books/04-reference-manual/chapters/030-xref-patterns.adoc +19 -0
- package/templates/default-workspace/books/04-reference-manual/chapters/{04-growth-paths.adoc → 040-growth-paths.adoc} +0 -1
- package/templates/default-workspace/books/05-upper-volume/book.adoc +2 -2
- package/templates/default-workspace/books/05-upper-volume/chapters/{01-upper-opening.adoc → 010-upper-opening.adoc} +0 -1
- package/templates/default-workspace/books/05-upper-volume/chapters/020-upper-core-model.adoc +3 -0
- package/templates/default-workspace/books/06-lower-volume/backmatter/bibliography.adoc +1 -1
- package/templates/default-workspace/books/06-lower-volume/book.adoc +2 -2
- package/templates/default-workspace/books/06-lower-volume/chapters/{01-lower-opening.adoc → 010-lower-opening.adoc} +0 -1
- package/templates/default-workspace/books/06-lower-volume/chapters/{02-cross-volume-practice.adoc → 020-cross-volume-practice.adoc} +2 -2
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-decision-trees.adoc +198 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-quick-reference.adoc +82 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/bibliography.adoc +13 -3
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/glossary.adoc +14 -16
- package/templates/default-workspace/books/07-structured-writing-conventions/book.adoc +24 -12
- package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/acknowledgments.adoc +1 -1
- package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/colophon.adoc +1 -3
- package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/conventions.adoc +17 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/preface.adoc +3 -3
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/010-source-and-projection.adoc +66 -52
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/020-heading-address-and-xref.adoc +65 -24
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/030-source-order-notation.adoc +20 -21
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/010-source-surface/_partintro.adoc +1 -1
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/010-names-and-addresses.adoc +35 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/020-heading-roles.adoc +19 -19
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/030-relation-predicate.adoc +23 -14
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/_partintro.adoc +1 -1
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/010-surface-fields.adoc +51 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/030-declaring-conventions.adoc +66 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-conventions/_partintro.adoc +1 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/010-whom-a-book-speaks-to.adoc +64 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/020-what-a-sentence-does.adoc +57 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/030-names-and-examples.adoc +53 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/040-print-conventions.adoc +89 -0
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/_partintro.adoc +1 -0
- package/templates/default-workspace/catalog.adoc +2 -2
- package/templates/default-workspace/package.json.template +1 -1
- package/templates/solo-workspace/catalog.adoc +1 -1
- package/templates/default-workspace/books/00-book-anatomy/frontmatter/abstract.adoc +0 -5
- package/templates/default-workspace/books/00-book-anatomy/parts/01-front-structure/_partintro.adoc +0 -1
- package/templates/default-workspace/books/00-book-anatomy/parts/02-body-structure/02-inline-structure.adoc +0 -32
- package/templates/default-workspace/books/02-multipart-monograph/backmatter/appendix-a.adoc +0 -5
- package/templates/default-workspace/books/04-reference-manual/chapters/03-xref-patterns.adoc +0 -20
- package/templates/default-workspace/books/05-upper-volume/chapters/02-upper-core-model.adoc +0 -4
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-a.adoc +0 -82
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-b-decision-trees.adoc +0 -187
- package/templates/default-workspace/books/07-structured-writing-conventions/frontmatter/abstract.adoc +0 -3
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/020-names-roles-and-relations/010-names-and-address-labels.adoc +0 -20
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/010-surface-fields.adoc +0 -72
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/020-glossary.adoc +0 -6
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/030-declaring-conventions.adoc +0 -70
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/030-fields-and-terminology/_partintro.adoc +0 -1
- /package/templates/default-workspace/books/00-book-anatomy/parts/{02-body-structure → 020-body-structure}/_partintro.adoc +0 -0
- /package/templates/default-workspace/books/01-starter-book/chapters/{02-main-flow.adoc → 020-main-flow.adoc} +0 -0
- /package/templates/default-workspace/books/02-multipart-monograph/parts/{01-domain/02-boundary.adoc → 010-domain/020-boundary.adoc} +0 -0
- /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification/01-book-contract.adoc → 020-specification/010-book-contract.adoc} +0 -0
- /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification/02-verification.adoc → 020-specification/020-verification.adoc} +0 -0
- /package/templates/default-workspace/books/02-multipart-monograph/parts/{02-specification → 020-specification}/_partintro.adoc +0 -0
- /package/templates/default-workspace/books/03-technical-book-workflow/chapters/{02-diagrams.adoc → 020-diagrams.adoc} +0 -0
- /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,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 0.5.1 - 2026-10-23
|
|
4
19
|
|
|
5
20
|
### Changed
|
|
@@ -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";
|
|
@@ -322,8 +325,9 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
|
|
|
322
325
|
const defaultLogger = loggerManager.getLogger();
|
|
323
326
|
const memoryLogger = asciidoctor.MemoryLogger.create();
|
|
324
327
|
loggerManager.setLogger(memoryLogger);
|
|
328
|
+
let document;
|
|
325
329
|
try {
|
|
326
|
-
asciidoctor.convertFile(input, {
|
|
330
|
+
document = asciidoctor.convertFile(input, {
|
|
327
331
|
safe: "unsafe",
|
|
328
332
|
base_dir: baseDir,
|
|
329
333
|
to_file: outputFile,
|
|
@@ -339,7 +343,9 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
|
|
|
339
343
|
if (messages.length > 0) {
|
|
340
344
|
throw new Error(`AsciiDoc strict check failed for ${input}\n${messages.map(formatAsciidoctorMessage).join("\n")}`);
|
|
341
345
|
}
|
|
346
|
+
return document;
|
|
342
347
|
}
|
|
348
|
+
// 交回每本书转换后的 Asciidoctor 文档:书内与跨书 xref 的目标要用它展开属性引用(bindReferences)。
|
|
343
349
|
async function buildHtml(rootDir, books, asciidoctor, extensionRegistry, useKroki, fetchDiagrams) {
|
|
344
350
|
await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
|
|
345
351
|
convertFile(asciidoctor, extensionRegistry, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
|
|
@@ -464,14 +470,16 @@ function sourceBundleFromDocument(document) {
|
|
|
464
470
|
// 解析失败就让构建失败,不退回只有封面的阅读页:退回时 include 越过 documentRoot 只剩一条 warning,
|
|
465
471
|
// 构建照常成功,没有人发现阅读页坏了(DESIGN.adoc 的 Workspace Contract Checks)。
|
|
466
472
|
async function buildReaderBookData(config, book) {
|
|
473
|
+
let parsed;
|
|
467
474
|
let document;
|
|
468
475
|
try {
|
|
469
|
-
|
|
476
|
+
parsed = await parseAbundantTree({
|
|
470
477
|
sourcePath: book.input,
|
|
471
478
|
mode: "book-entry",
|
|
472
479
|
documentRoot: config.documentRoot,
|
|
473
480
|
includeResolvers: config.includeResolvers
|
|
474
481
|
});
|
|
482
|
+
document = parsed;
|
|
475
483
|
}
|
|
476
484
|
catch (error) {
|
|
477
485
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -490,7 +498,9 @@ async function buildReaderBookData(config, book) {
|
|
|
490
498
|
const sections = (Array.isArray(document.children) ? document.children : []).filter((child) => {
|
|
491
499
|
return typeof child === "object" && child !== null && child.kind === "section";
|
|
492
500
|
});
|
|
493
|
-
|
|
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"]);
|
|
494
504
|
const backmatterStyles = new Set(["appendix", "glossary", "bibliography", "index"]);
|
|
495
505
|
const frontmatter = sections.filter((section) => section.level === 1 && frontmatterStyles.has(sectionStyle(section) ?? ""));
|
|
496
506
|
if (frontmatter.length > 0) {
|
|
@@ -543,9 +553,143 @@ async function buildReaderBookData(config, book) {
|
|
|
543
553
|
};
|
|
544
554
|
return {
|
|
545
555
|
pageMap,
|
|
546
|
-
sourceBundle: sourceBundleFromDocument(document)
|
|
556
|
+
sourceBundle: sourceBundleFromDocument(document),
|
|
557
|
+
references: bindReferences(config, parsed)
|
|
547
558
|
};
|
|
548
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
|
+
}
|
|
549
693
|
function readerStyles() {
|
|
550
694
|
return `
|
|
551
695
|
.multi-book-reader-toggle {
|
|
@@ -1061,13 +1205,24 @@ ${readerStyles()}
|
|
|
1061
1205
|
}
|
|
1062
1206
|
async function addHomeLinks(rootDir, books, config) {
|
|
1063
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 = [];
|
|
1064
1216
|
for (const book of books) {
|
|
1065
1217
|
const htmlFile = path.join(book.htmlOutputDir, "book.html");
|
|
1066
|
-
const html = await readFile(htmlFile, "utf8");
|
|
1067
1218
|
const href = path.relative(path.dirname(htmlFile), catalog);
|
|
1068
|
-
const
|
|
1069
|
-
|
|
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");
|
|
1070
1224
|
}
|
|
1225
|
+
return issues;
|
|
1071
1226
|
}
|
|
1072
1227
|
async function writeRootIndex(rootDir, rootIndex) {
|
|
1073
1228
|
const outputDir = path.join(rootDir, "build", "html");
|
|
@@ -1095,9 +1250,8 @@ function extractCatalogBookIds(catalogSource) {
|
|
|
1095
1250
|
function issue(code, detail) {
|
|
1096
1251
|
return { code, detail };
|
|
1097
1252
|
}
|
|
1098
|
-
// xref
|
|
1099
|
-
//
|
|
1100
|
-
// 只有 Asciidoctor 认得全。
|
|
1253
|
+
// xref 不在这里查:书内与跨书的引用以 aat 的绑定为准(bindReferences、bindCrossBookReferences),不存在的书与其余
|
|
1254
|
+
// 锚点由 missingLocalResources 在生成的 HTML 上核对(DESIGN.adoc 的 Workspace Contract Checks 与 Reference Binding)。
|
|
1101
1255
|
async function workspaceContractIssues(rootDir, books) {
|
|
1102
1256
|
const issues = [];
|
|
1103
1257
|
const catalogSource = await readIfExists(path.join(rootDir, "catalog.adoc"));
|
|
@@ -1204,8 +1358,9 @@ export async function buildWorkspace(rootDir = process.cwd()) {
|
|
|
1204
1358
|
const asciidoctor = createAsciidoctor(useKroki);
|
|
1205
1359
|
await buildHtml(rootDir, books, asciidoctor, includeResolverRegistry(asciidoctor, config), useKroki, fetchDiagrams);
|
|
1206
1360
|
await copyAssets(rootDir, books);
|
|
1207
|
-
await addHomeLinks(rootDir, books, config);
|
|
1361
|
+
const referenceIssues = await addHomeLinks(rootDir, books, config);
|
|
1208
1362
|
await writeRootIndex(rootDir, config.rootIndex);
|
|
1363
|
+
await assertNoIssues("reference binding check", referenceIssues);
|
|
1209
1364
|
await assertNoIssues("HTML local resource check", await missingLocalResources(rootDir));
|
|
1210
1365
|
await assertNoIssues("workspace contract check", await workspaceContractIssues(rootDir, books));
|
|
1211
1366
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-asciidoc-multi-book-workspace",
|
|
3
|
-
"version": "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.
|
|
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"
|
|
@@ -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/
|
|
33
|
+
include::parts/010-front-structure/_partintro.adoc[]
|
|
34
34
|
|
|
35
|
-
include::parts/
|
|
35
|
+
include::parts/010-front-structure/010-frontmatter-map.adoc[]
|
|
36
36
|
|
|
37
|
-
=
|
|
37
|
+
= 正文结构
|
|
38
38
|
|
|
39
39
|
[partintro]
|
|
40
|
-
include::parts/
|
|
40
|
+
include::parts/020-body-structure/_partintro.adoc[]
|
|
41
41
|
|
|
42
|
-
include::parts/
|
|
42
|
+
include::parts/020-body-structure/010-body-map.adoc[]
|
|
43
43
|
|
|
44
|
-
include::parts/
|
|
44
|
+
include::parts/020-body-structure/020-inline-structure.adoc[]
|
|
45
45
|
|
|
46
46
|
[appendix]
|
|
47
|
-
include::backmatter/appendix-
|
|
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 会把那一节当作普通的一章编号,它后面每一章的编号都跟着多一。一本书覆盖什么、怎样读,写在前言里。
|
package/templates/default-workspace/books/00-book-anatomy/parts/010-front-structure/_partintro.adoc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
本部展示一本书进入正文之前的结构位置。版本说明、献辞、前言和致谢都属于书籍前置内容。
|
|
@@ -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/` 标记路径片段。
|
|
@@ -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/
|
|
19
|
+
include::chapters/010-opening.adoc[]
|
|
19
20
|
|
|
20
|
-
include::chapters/
|
|
21
|
+
include::chapters/020-main-flow.adoc[]
|
|
21
22
|
|
|
22
23
|
[appendix]
|
|
23
|
-
include::backmatter/appendix-
|
|
24
|
+
include::backmatter/appendix-minimal-files.adoc[]
|
|
24
25
|
|
|
25
26
|
[bibliography]
|
|
26
27
|
include::backmatter/bibliography.adoc[]
|
|
@@ -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/
|
|
21
|
+
include::parts/010-domain/_partintro.adoc[]
|
|
19
22
|
|
|
20
|
-
include::parts/
|
|
23
|
+
include::parts/010-domain/010-problem-world.adoc[]
|
|
21
24
|
|
|
22
|
-
include::parts/
|
|
25
|
+
include::parts/010-domain/020-boundary.adoc[]
|
|
23
26
|
|
|
24
|
-
=
|
|
27
|
+
= 书籍规约
|
|
25
28
|
|
|
26
29
|
[partintro]
|
|
27
|
-
include::parts/
|
|
30
|
+
include::parts/020-specification/_partintro.adoc[]
|
|
28
31
|
|
|
29
|
-
include::parts/
|
|
32
|
+
include::parts/020-specification/010-book-contract.adoc[]
|
|
30
33
|
|
|
31
|
-
include::parts/
|
|
34
|
+
include::parts/020-specification/020-verification.adoc[]
|
|
32
35
|
|
|
33
36
|
[appendix]
|
|
34
|
-
include::backmatter/appendix-
|
|
37
|
+
include::backmatter/appendix-part-reference.adoc[]
|
|
35
38
|
|
|
36
39
|
[bibliography]
|
|
37
40
|
include::backmatter/bibliography.adoc[]
|
|
@@ -15,11 +15,11 @@ include::../../shared/attributes.adoc[]
|
|
|
15
15
|
[preface]
|
|
16
16
|
include::frontmatter/preface.adoc[]
|
|
17
17
|
|
|
18
|
-
include::chapters/
|
|
18
|
+
include::chapters/010-build-flow.adoc[]
|
|
19
19
|
|
|
20
|
-
include::chapters/
|
|
20
|
+
include::chapters/020-diagrams.adoc[]
|
|
21
21
|
|
|
22
|
-
include::chapters/
|
|
22
|
+
include::chapters/030-code-includes.adoc[]
|
|
23
23
|
|
|
24
24
|
[bibliography]
|
|
25
25
|
include::backmatter/bibliography.adoc[]
|
|
@@ -4,5 +4,5 @@ book:: 具有独立 `book.adoc` 和构建产物的书籍单位。
|
|
|
4
4
|
part:: 同一本 book 内部的 level 0 大分部。
|
|
5
5
|
catalog:: 多书工作区的读者导航入口。
|
|
6
6
|
shared resource:: 被 catalog 或多本书共同引用的资源。
|
|
7
|
-
|
|
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/
|
|
18
|
+
include::chapters/010-decision-table.adoc[]
|
|
19
19
|
|
|
20
|
-
include::chapters/
|
|
20
|
+
include::chapters/020-path-rules.adoc[]
|
|
21
21
|
|
|
22
|
-
include::chapters/
|
|
22
|
+
include::chapters/030-xref-patterns.adoc[]
|
|
23
23
|
|
|
24
|
-
include::chapters/
|
|
24
|
+
include::chapters/040-growth-paths.adoc[]
|
|
25
25
|
|
|
26
26
|
[glossary]
|
|
27
27
|
include::backmatter/glossary.adoc[]
|
|
@@ -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` 会指出每一处没有跟上的引用。
|
|
@@ -15,9 +15,9 @@ include::../../shared/attributes.adoc[]
|
|
|
15
15
|
[preface]
|
|
16
16
|
include::frontmatter/preface.adoc[]
|
|
17
17
|
|
|
18
|
-
include::chapters/
|
|
18
|
+
include::chapters/010-upper-opening.adoc[]
|
|
19
19
|
|
|
20
|
-
include::chapters/
|
|
20
|
+
include::chapters/020-upper-core-model.adoc[]
|
|
21
21
|
|
|
22
22
|
[bibliography]
|
|
23
23
|
include::backmatter/bibliography.adoc[]
|