create-asciidoc-multi-book-workspace 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +0 -1
- package/dist/runtime/adoc-books.mjs +57 -167
- 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 +6 -6
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-quick-reference.adoc +3 -3
- 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 +11 -14
- 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/books/07-structured-writing-conventions/parts/040-book-prose/040-print-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,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 - 2026-10-31
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- A link with no link text shows the name of its address. When the xref's address is the heading's title or one of its qualified addresses, the link shows it as written, its titles joined with an interpunct: `xref:红烧肉::火候[]` shows 红烧肉·火候 and `xref:家常菜::红烧肉::火候[]` shows all three titles, inside a book and across books. It used to show the last title alone (火候), so a book that referred to one of several 火候 wrote its address twice, once as the target and once as link text. An xref whose address is an ID still shows the heading's title, as Asciidoctor does for a section it resolves by ID. Link text the author writes is shown as written.
|
|
8
|
+
- The structured writing conventions sample writes a qualified address once (《xref:红烧肉::火候[]》) and writes link text only where it says something other than the address's name. Its addressing chapter, print conventions, decision trees, and quick reference say so.
|
|
9
|
+
|
|
10
|
+
### Upgrading an existing workspace
|
|
11
|
+
|
|
12
|
+
Create a temporary workspace with `pnpm create asciidoc-multi-book-workspace@0.8.0 tmp-books` and copy its `tools/adoc-books.mjs` over yours. The `asciidoc-abundant-tree` range stays `^0.6.0`. Books need no change: link text written as the address's name, such as `xref:红烧肉::火候[红烧肉·火候]`, still shows the same, and can be removed.
|
|
13
|
+
|
|
14
|
+
## 0.7.0 - 2026-10-31
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- 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.
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Upgrading an existing workspace
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
3
30
|
## 0.6.0 - 2026-10-31
|
|
4
31
|
|
|
5
32
|
### 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,57 @@ 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
|
-
|
|
574
|
-
|
|
573
|
+
// 地址的名字:选择器是这一节的标题或某个限定地址时,照写下的样子显示,各级标题之间用间隔号;是 ID(作者声明的或生成的)
|
|
574
|
+
// 时显示标题——ID 是给引用用的第二个地址,不是给读者看的名字。按图上是哪个标签判,不按字符串里有没有 :: 猜。
|
|
575
|
+
const located = (iris, selector) => [...iris].flatMap((iri) => {
|
|
576
|
+
const entry = sections.get(iri);
|
|
575
577
|
const id = entry?.node.ids?.[0];
|
|
576
578
|
if (entry === undefined || id === undefined)
|
|
577
579
|
return [];
|
|
578
|
-
|
|
580
|
+
const node = { termType: "iri", value: iri };
|
|
581
|
+
const named = values(node, "headline").includes(selector) || values(node, "addressPath").includes(selector);
|
|
582
|
+
return [{ id, shown: named ? selector.split("::").join("·") : entry.node.title ?? id }];
|
|
579
583
|
});
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
584
|
+
// 别的书指过来的引用在这本书的 label space 里找:图上四个标签谓词的值,按整串相等找拥有者。
|
|
585
|
+
const ownersByLabel = new Map();
|
|
586
|
+
for (const name of LABEL_PREDICATES) {
|
|
587
|
+
for (const triple of graph.match({ predicate: { termType: "iri", value: `${AAT_NAMESPACE}${name}` } })) {
|
|
588
|
+
if (triple.object.termType !== "literal")
|
|
589
|
+
continue;
|
|
590
|
+
const iris = ownersByLabel.get(triple.object.value) ?? new Set();
|
|
591
|
+
iris.add(triple.subject.value);
|
|
592
|
+
ownersByLabel.set(triple.object.value, iris);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const owners = (selector) => located(ownersByLabel.get(selector) ?? [], selector);
|
|
592
596
|
const headingsByFragment = new Map();
|
|
593
597
|
const crossBook = [];
|
|
594
|
-
|
|
595
|
-
for (const
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
598
|
+
// 指向整本书的 xref(没有片段)没有 targetSelector:没有可改写的片段。
|
|
599
|
+
for (const triple of graph.match({ predicate: { termType: "iri", value: `${AAT_NAMESPACE}targetSelector` } })) {
|
|
600
|
+
const edge = triple.subject;
|
|
601
|
+
const fragment = triple.object.termType === "triple" ? "" : triple.object.value;
|
|
602
|
+
const document = values(edge, "targetDocument")[0];
|
|
603
|
+
if (document !== undefined) {
|
|
604
|
+
crossBook.push({ document, fragment });
|
|
599
605
|
continue;
|
|
600
606
|
}
|
|
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);
|
|
607
|
+
const found = located(values(edge, "targetHeading"), fragment);
|
|
608
|
+
if (found.length !== 1)
|
|
607
609
|
continue;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
issues.push(issue("XREF_AMBIGUOUS", `${where} -> ${describeCandidates(found)}`));
|
|
611
|
-
continue;
|
|
612
|
-
}
|
|
613
|
-
issues.push(issue("XREF_UNRESOLVED", where));
|
|
610
|
+
headingsByFragment.set(fragment, found[0]);
|
|
611
|
+
headingsByFragment.set(escapeHtmlAttribute(fragment), found[0]);
|
|
614
612
|
}
|
|
615
|
-
return { headingsByFragment, crossBook, owners
|
|
613
|
+
return { headingsByFragment, crossBook, owners };
|
|
616
614
|
}
|
|
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 本地资源检查。
|
|
615
|
+
// 跨书 xref 同样跟着投影走,只是 label space 在目标书里(DESIGN.adoc 的 Reference Binding)。aat 不打开别的文档,
|
|
616
|
+
// 工作区解析了每一本书,所以在这里找。路径按书目录解析:Asciidoctor 把它照写进 HTML,而 HTML 的输出目录与 books/ 下的
|
|
617
|
+
// 书目录一一对应。指向的不是工作区里的书、只指向整本书(没有片段)、或在目标书里对上零节或几节的,保持原样。
|
|
626
618
|
async function bindCrossBookReferences(book, references, bindingsByInput, anchorCache) {
|
|
627
619
|
const linksByHref = new Map();
|
|
628
|
-
const issues = [];
|
|
629
620
|
for (const xref of references.crossBook) {
|
|
630
621
|
const { fragment } = xref;
|
|
631
622
|
// Asciidoctor 把 xref:other#x[] 与 xref:other.adoc#x[] 都读成文档 other:去掉 .adoc、没有扩展名的照写,
|
|
@@ -635,14 +626,8 @@ async function bindCrossBookReferences(book, references, bindingsByInput, anchor
|
|
|
635
626
|
if (fragment === "" || target === undefined)
|
|
636
627
|
continue;
|
|
637
628
|
const found = target.references.owners(fragment);
|
|
638
|
-
if (found.length
|
|
639
|
-
issues.push(issue("XREF_UNRESOLVED", `${xref.where} -> ${target.book.bookId}`));
|
|
629
|
+
if (found.length !== 1)
|
|
640
630
|
continue;
|
|
641
|
-
}
|
|
642
|
-
if (found.length > 1) {
|
|
643
|
-
issues.push(issue("XREF_AMBIGUOUS", `${xref.where} -> ${target.book.bookId}: ${describeCandidates(found)}`));
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
631
|
const heading = found[0];
|
|
647
632
|
// 指向本书自己的 book.adoc:Asciidoctor 把它当书内引用,写成 #片段。
|
|
648
633
|
if (target.book.bookId === book.bookId) {
|
|
@@ -654,14 +639,14 @@ async function bindCrossBookReferences(book, references, bindingsByInput, anchor
|
|
|
654
639
|
if (anchors.has(fragment))
|
|
655
640
|
continue;
|
|
656
641
|
const htmlPath = `${documentName}.html`;
|
|
657
|
-
const link = { href: `${htmlPath}#${heading.id}`,
|
|
642
|
+
const link = { href: `${htmlPath}#${heading.id}`, shown: heading.shown };
|
|
658
643
|
linksByHref.set(`${htmlPath}#${fragment}`, link);
|
|
659
644
|
linksByHref.set(escapeHtmlAttribute(`${htmlPath}#${fragment}`), link);
|
|
660
645
|
}
|
|
661
|
-
return
|
|
646
|
+
return linksByHref;
|
|
662
647
|
}
|
|
663
|
-
//
|
|
664
|
-
//
|
|
648
|
+
// 在目标书里恰好对上一节、而目标书的 HTML 里没有这个片段的跨书链接,改成指向那一节的标题。Asciidoctor 对没有显示文字的跨书 xref
|
|
649
|
+
// 显示文档路径;改写时换成地址的名字。
|
|
665
650
|
function rewriteCrossBookXrefs(html, linksByHref) {
|
|
666
651
|
if (linksByHref.size === 0)
|
|
667
652
|
return html;
|
|
@@ -669,12 +654,12 @@ function rewriteCrossBookXrefs(html, linksByHref) {
|
|
|
669
654
|
const link = linksByHref.get(`${htmlPath}#${fragment}`) ?? linksByHref.get(`${htmlPath}#${decodeUrlFragment(fragment)}`);
|
|
670
655
|
if (link === undefined)
|
|
671
656
|
return whole;
|
|
672
|
-
const shown = text === htmlPath ? escapeHtmlAttribute(link.
|
|
657
|
+
const shown = text === htmlPath ? escapeHtmlAttribute(link.shown) : text;
|
|
673
658
|
return `<a href="${escapeHtmlAttribute(link.href)}"${rest}>${shown}</a>`;
|
|
674
659
|
});
|
|
675
660
|
}
|
|
676
|
-
// Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat
|
|
677
|
-
//
|
|
661
|
+
// Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat 的投影里恰好对上一节的,改成指向那一节的标题,
|
|
662
|
+
// 显示地址的名字。片段已经是 HTML 里某个元素的 id 时,保持 Asciidoctor 写下的样子。
|
|
678
663
|
function rewriteBoundXrefs(html, headingsByFragment) {
|
|
679
664
|
if (headingsByFragment.size === 0)
|
|
680
665
|
return html;
|
|
@@ -686,7 +671,7 @@ function rewriteBoundXrefs(html, headingsByFragment) {
|
|
|
686
671
|
const heading = headingsByFragment.get(fragment) ?? headingsByFragment.get(decoded);
|
|
687
672
|
if (heading === undefined)
|
|
688
673
|
return whole;
|
|
689
|
-
const shown = text === `[${fragment}]` ? escapeHtmlAttribute(heading.
|
|
674
|
+
const shown = text === `[${fragment}]` ? escapeHtmlAttribute(heading.shown) : text;
|
|
690
675
|
return `<a href="#${escapeHtmlAttribute(heading.id)}"${rest}>${shown}</a>`;
|
|
691
676
|
});
|
|
692
677
|
}
|
|
@@ -1205,24 +1190,21 @@ ${readerStyles()}
|
|
|
1205
1190
|
}
|
|
1206
1191
|
async function addHomeLinks(rootDir, books, config) {
|
|
1207
1192
|
const catalog = path.join(rootDir, "build", "html", "catalog.html");
|
|
1208
|
-
// 跨书 xref 在目标书的 label space
|
|
1193
|
+
// 跨书 xref 在目标书的 label space 里找,所以先把每一本书都解析完。
|
|
1209
1194
|
const readerData = new Map();
|
|
1210
1195
|
for (const book of books) {
|
|
1211
1196
|
readerData.set(book.bookId, await buildReaderBookData(config, book));
|
|
1212
1197
|
}
|
|
1213
1198
|
const bindingsByInput = new Map(books.map((book) => [book.input, { book, references: readerData.get(book.bookId).references }]));
|
|
1214
1199
|
const anchorCache = new Map();
|
|
1215
|
-
const issues = [];
|
|
1216
1200
|
for (const book of books) {
|
|
1217
1201
|
const htmlFile = path.join(book.htmlOutputDir, "book.html");
|
|
1218
1202
|
const href = path.relative(path.dirname(htmlFile), catalog);
|
|
1219
1203
|
const data = readerData.get(book.bookId);
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
const html = rewriteCrossBookXrefs(rewriteBoundXrefs(await readFile(htmlFile, "utf8"), data.references.headingsByFragment), crossBook.linksByHref);
|
|
1204
|
+
const crossBookLinks = await bindCrossBookReferences(book, data.references, bindingsByInput, anchorCache);
|
|
1205
|
+
const html = rewriteCrossBookXrefs(rewriteBoundXrefs(await readFile(htmlFile, "utf8"), data.references.headingsByFragment), crossBookLinks);
|
|
1223
1206
|
await writeFile(htmlFile, addBookControlsToBookHtml(html, href, config.homeLink, data.sourceBundle, data.pageMap, book.bookId), "utf8");
|
|
1224
1207
|
}
|
|
1225
|
-
return issues;
|
|
1226
1208
|
}
|
|
1227
1209
|
async function writeRootIndex(rootDir, rootIndex) {
|
|
1228
1210
|
const outputDir = path.join(rootDir, "build", "html");
|
|
@@ -1241,55 +1223,6 @@ async function writeRootIndex(rootDir, rootIndex) {
|
|
|
1241
1223
|
</html>
|
|
1242
1224
|
`, "utf8");
|
|
1243
1225
|
}
|
|
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
1226
|
function decodeUrlFragment(fragment) {
|
|
1294
1227
|
try {
|
|
1295
1228
|
return decodeURIComponent(fragment);
|
|
@@ -1312,39 +1245,6 @@ async function anchorsForHtmlFile(filePath, cache) {
|
|
|
1312
1245
|
cache.set(filePath, anchors);
|
|
1313
1246
|
return anchors;
|
|
1314
1247
|
}
|
|
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
1248
|
export async function buildWorkspace(rootDir = process.cwd()) {
|
|
1349
1249
|
const catalog = path.join(rootDir, "catalog.adoc");
|
|
1350
1250
|
if (!await existsFile(catalog))
|
|
@@ -1358,14 +1258,8 @@ export async function buildWorkspace(rootDir = process.cwd()) {
|
|
|
1358
1258
|
const asciidoctor = createAsciidoctor(useKroki);
|
|
1359
1259
|
await buildHtml(rootDir, books, asciidoctor, includeResolverRegistry(asciidoctor, config), useKroki, fetchDiagrams);
|
|
1360
1260
|
await copyAssets(rootDir, books);
|
|
1361
|
-
|
|
1261
|
+
await addHomeLinks(rootDir, books, config);
|
|
1362
1262
|
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
1263
|
}
|
|
1370
1264
|
export async function cleanWorkspace(rootDir = process.cwd()) {
|
|
1371
1265
|
await rm(path.join(rootDir, "build"), { force: true, recursive: true });
|
|
@@ -1376,15 +1270,11 @@ async function main() {
|
|
|
1376
1270
|
await buildWorkspace();
|
|
1377
1271
|
return;
|
|
1378
1272
|
}
|
|
1379
|
-
if (command === "check") {
|
|
1380
|
-
await checkWorkspace();
|
|
1381
|
-
return;
|
|
1382
|
-
}
|
|
1383
1273
|
if (command === "clean") {
|
|
1384
1274
|
await cleanWorkspace();
|
|
1385
1275
|
return;
|
|
1386
1276
|
}
|
|
1387
|
-
console.error("Usage: node tools/adoc-books.mjs <build|
|
|
1277
|
+
console.error("Usage: node tools/adoc-books.mjs <build|clean>");
|
|
1388
1278
|
process.exitCode = 1;
|
|
1389
1279
|
}
|
|
1390
1280
|
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.8.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
|
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
└─ 否:同一本书里经 include 拼起来的各个文件之间,xref 直接写标题。看第 2 步。
|
|
92
92
|
|
|
93
93
|
2. 目标标题在它所在的书里与别的标题同名吗?
|
|
94
|
-
├─ 是:把上级标题由近及远用 `::` 接在前面,补到候选只剩一个,如
|
|
95
|
-
│
|
|
94
|
+
├─ 是:把上级标题由近及远用 `::` 接在前面,补到候选只剩一个,如 `红烧肉::火候`。
|
|
95
|
+
│ 不必写显示文本:网页显示地址的名字,各级之间用间隔号,如《红烧肉·火候》。
|
|
96
96
|
│ 补到书名之下最高的一级仍分不开(同一个上级下同名)时,先把标题改得说得清这一节讲什么;
|
|
97
97
|
│ 改不了时,在那一节标题上方加 custom ID,xref 写这个 ID,
|
|
98
98
|
│ 并写上能分清是哪一节的显示文本。看第 3 步。
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
|`[.dish]`
|
|
35
35
|
|role:这一节在书里是什么。
|
|
36
36
|
|
|
37
|
-
|`pass:[xref:红烧肉::火候[
|
|
38
|
-
|限定地址:几个标题同名时,把上级标题用 `::`
|
|
37
|
+
|`pass:[xref:红烧肉::火候[]]`
|
|
38
|
+
|限定地址:几个标题同名时,把上级标题用 `::` 接在前面,指明其中一个;网页显示《红烧肉·火候》,各级之间用间隔号。
|
|
39
39
|
|
|
40
40
|
|`[#…]`
|
|
41
41
|
|custom ID:标题当不了地址时(同一个上级下同名,或标题里有 `#`、`::`)给它的第二个地址。
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
写引用时,应符合下列规定:
|
|
75
75
|
|
|
76
76
|
* 同一本书里经 include 拼起来的各个文件之间,引用直接写标题;
|
|
77
|
-
* 几个标题同名时,引用写限定地址,例如 `pass:[xref:红烧肉::火候[
|
|
77
|
+
* 几个标题同名时,引用写限定地址,例如 `pass:[xref:红烧肉::火候[]]`,不必再写显示文本;
|
|
78
78
|
* 引用另一本书时,在书头定义一个文档属性,存那本书的入口路径(如 `:snack-book: ../02-home-snacks/book.adoc`),引用写成 `pass:[xref:{snack-book}#糖油饼[]]`,`#` 后面写那本书里的标题。
|
|
79
79
|
|
|
80
80
|
反引号只改字体,反引号里的行内宏(如 xref)和属性引用照样会被替换。要原样展示它们,应把它们包在 `pass:[pass:[…]]` 里,外面再加反引号得到等宽字体。`= 书名`、`[.dish]` 这类块写法出现在行内时不会被当作结构,用反引号即可。
|
|
@@ -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,34 +51,31 @@ Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的
|
|
|
51
51
|
=== 火候
|
|
52
52
|
----
|
|
53
53
|
|
|
54
|
-
三节《火候》各讲一道菜,一般不会被单独引用。某一处要指向《红烧肉》下的《火候》时,`pass:[xref:火候[]]`
|
|
54
|
+
三节《火候》各讲一道菜,一般不会被单独引用。某一处要指向《红烧肉》下的《火候》时,`pass:[xref:火候[]]` 对上三节,网页上的链接落到书里最前面的那一节,要指的若是另外两节,就落错了。读投影的工具会把这处引用连同三节各自的位置报出来。这时应把上级标题写在前面,用 `::` 隔开:
|
|
55
55
|
|
|
56
56
|
[source,asciidoc]
|
|
57
57
|
----
|
|
58
|
-
糖色下锅以后怎样转火,见《xref:红烧肉::火候[
|
|
58
|
+
糖色下锅以后怎样转火,见《xref:红烧肉::火候[]》。
|
|
59
59
|
----
|
|
60
60
|
|
|
61
|
-
`红烧肉::火候`
|
|
61
|
+
`红烧肉::火候` 是这一节的限定地址。上级标题由近及远逐级补上,补到候选只剩一个为止。
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
* 写上显示文本,各级标题之间用间隔号,例如 `红烧肉·火候`(见《xref:篇名用书名号[]》)。
|
|
63
|
+
限定地址只写一遍,不必再写显示文本。链接没有显示文本时,网页上显示地址的名字,各级标题之间用间隔号(见《xref:篇名用书名号[]》),上面这句读者看到的是“见《红烧肉·火候》”。作者补上的上级标题,读者也要靠它分清是哪道菜的火候,所以显示时一级不少。想在链接上说别的话时,才写显示文本,例如 `pass:[xref:红烧肉::火候[那一锅的火候]]`,网页显示写下的这句。
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
补一级不够时,再往上补一级。例如菜谱分了《家常菜》《宴客菜》两部,两部里都有《红烧肉》,引用家常菜里那一道的火候时,写 `pass:[xref:家常菜::红烧肉::火候[家常菜·红烧肉·火候]]`。补到书名之下最高的一级(分部的书是部,不分部的书是章),就是这一节完整的路径;书名全书只有一个,不必写。
|
|
65
|
+
补一级不够时,再往上补一级。例如菜谱分了《家常菜》《宴客菜》两部,两部里都有《红烧肉》,引用家常菜里那一道的火候时,写 `pass:[xref:家常菜::红烧肉::火候[]]`,网页显示《家常菜·红烧肉·火候》。补到书名之下最高的一级(分部的书是部,不分部的书是章),就是这一节完整的路径;书名全书只有一个,不必写。
|
|
69
66
|
|
|
70
67
|
同一个上级下有两个同名的标题时,完整的路径也分不开它们,例如糖醋排骨要炸两遍,《糖醋排骨》下写了两节《炸》。两节同名,多半是标题没有说清这一节讲什么。这时宜先改标题,例如改成《初炸》与《复炸》,改好以后,两节的标题各是各的地址。
|
|
71
68
|
|
|
72
|
-
|
|
69
|
+
标题改不了时,标题和限定地址都指不出唯一的一节。还有两种标题,按原文写不出引用:标题原文里带 `#` 或 `::` 的,引用会把它们当作分隔;标题以行内标记开头的,例如 `+`book.adoc` 的职责+`,AsciiDoc 不把以反引号、星号这类标记起头的目标读成引用。要引用这样的一节,应在它的标题上方加 custom ID(`[#…]`),引用写这个 ID。加 custom ID 时,应符合下列规定:
|
|
73
70
|
|
|
74
71
|
* 名字说清这一节讲什么,不写序号和位置词,例如不写 `炸-2`、`第二个炸`,因为序号和位置会随编排变化;
|
|
75
|
-
*
|
|
72
|
+
* 名字不与书里的任何标题相同。例如给《卤味》下讲糖色的一节加了 `[#炒糖色]`,书里又有一节《炒糖色》,这个地址就对上两节;Asciidoctor 先按 ID 去找,网页上 `pass:[xref:炒糖色[]]` 落到加了 ID 的那一节,原来指《炒糖色》的引用便不声不响地指到了别处;
|
|
76
73
|
* 同一个上级下同名的几节,要引用哪一节,就给哪一节加;只给其中一节加时,另一节的标题和限定地址仍然有两个候选;
|
|
77
|
-
*
|
|
74
|
+
* 引用写上显示文本,写出能让读者分清是哪一节的说法:引用 custom ID 的链接显示的是标题,同名的几节显示得一样。
|
|
78
75
|
|
|
79
76
|
custom ID 是同一个标题的第二个地址,不替代标题。作者是在要引用某一节时才发现标题当不了地址的,custom ID 也宜在那时再加,不必预先写下。
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
标题改名以后,按旧名字写的引用都找不到目标,网页上的这些链接是断的。按旧名字写的引用,地址里都含着旧名字,限定地址也一样,因此改名时在书里搜一遍旧名字,就能找到每一处引用,连同显示文本和正文里写着的旧名字,作者逐处改正即可。预先写下的 ID 会让旧引用在改名后照常落到这一节:这些引用写的是 ID,里面没有旧名字,搜旧名字搜不到它们,可这一节讲的可能已不是它们当初要指的事,ID 里也留下了旧的说法。因此,标题当得了地址时,不应写 custom ID。
|
|
82
79
|
|
|
83
80
|
role 只属于写了它的那个标题。希望查询工具把某个下级标题当作某种 role 列出时,应在它自己的标题上写这个 role。没有写 role 的下级标题只是父标题下的一节,读者按上下文理解它。
|
|
84
81
|
|
|
@@ -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
|
|
|
@@ -18,7 +18,7 @@ ____
|
|
|
18
18
|
做法见《红烧肉》。
|
|
19
19
|
____
|
|
20
20
|
|
|
21
|
-
书名号标书名、卷名和篇名(4.15.3.1);题目是一部特定的作品时用书名号,只是谈论的话题时才用引号(B.1.8)。书里的一节是这本书里的一篇,提到它时应加书名号。几篇并列时,书名号应紧挨着写,中间不加顿号(A.13)。提到的一节带链接时,xref 应写在书名号里面,例如 `《pass:[xref:红烧肉[]]
|
|
21
|
+
书名号标书名、卷名和篇名(4.15.3.1);题目是一部特定的作品时用书名号,只是谈论的话题时才用引号(B.1.8)。书里的一节是这本书里的一篇,提到它时应加书名号。几篇并列时,书名号应紧挨着写,中间不加顿号(A.13)。提到的一节带链接时,xref 应写在书名号里面,例如 `《pass:[xref:红烧肉[]]》`,读者看到的是《红烧肉》,链接落在书名号里的标题上。书名和其中的篇名连着写时,中间用间隔号,例如《随园食单·须知单》;链接没有显示文本时,网页把限定地址也这样显示,`《pass:[xref:红烧肉::火候[]]》` 读者看到的是《红烧肉·火候》。
|
|
22
22
|
|
|
23
23
|
[.touchstone]
|
|
24
24
|
=== 中西文混排照规范
|
|
@@ -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.
|