create-asciidoc-multi-book-workspace 0.7.0 → 0.9.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 +23 -0
- package/dist/runtime/adoc-books.mjs +58 -81
- package/package.json +7 -7
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-decision-trees.adoc +2 -2
- 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/020-heading-address-and-xref.adoc +5 -8
- package/templates/default-workspace/books/07-structured-writing-conventions/parts/040-book-prose/040-print-conventions.adoc +1 -1
- package/templates/default-workspace/package.json.template +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-11-01
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Generated workspaces require Node.js 22 or later (`engines.node` is `>=22`). `asciidoctor-kroki` 2, the release line for Asciidoctor.js 4, requires it.
|
|
8
|
+
- The HTML is rendered by Asciidoctor.js 4 (`@asciidoctor/core` `^4.1.1`), the same Asciidoctor `asciidoc-abundant-tree` reads the books with, and diagrams by `asciidoctor-kroki` `^2.0.0`. The runtime rewrites links to the heading IDs the projection read, and the copied source bundle promises the lines the HTML shows; with the HTML rendered by Asciidoctor.js 3 neither was guaranteed, and the two versions already read some includes differently (`include::f[lines=1_0]` is line 1 in version 4, line 10 in version 3). Links, anchors, the page map, and the copied source bundle of the sample books are unchanged. Visible differences come from Asciidoctor.js 4 itself: the generator meta tag and its default stylesheet, the class names of the table of contents in books with front or back matter, table column widths written as `width` attributes, and role classes written once instead of twice.
|
|
9
|
+
- An include resolver may return a Promise; the HTML side waits for it as the reader data side does. A positional attribute left empty between others (`[a,,b]`) is `null` on both sides and no longer shifts the ones after it, so it is no longer forbidden.
|
|
10
|
+
|
|
11
|
+
### Upgrading an existing workspace
|
|
12
|
+
|
|
13
|
+
Make sure Node.js 22 or later runs the build. Create a temporary workspace with `pnpm create asciidoc-multi-book-workspace@0.9.0 tmp-books` and copy its `tools/adoc-books.mjs` over yours. In your `package.json`, set `"engines": { "node": ">=22" }`, `"@asciidoctor/core": "^4.1.1"`, and `"asciidoctor-kroki": "^2.0.0"`, then run `pnpm install`. Books need no change, and include resolvers that return their result directly keep working.
|
|
14
|
+
|
|
15
|
+
## 0.8.0 - 2026-10-31
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
### Upgrading an existing workspace
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
3
26
|
## 0.7.0 - 2026-10-31
|
|
4
27
|
|
|
5
28
|
### Removed
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
3
|
import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import
|
|
6
|
+
import { convertFile as convertAsciiDocFile, Extensions, LoggerManager, MemoryLogger } from "@asciidoctor/core";
|
|
8
7
|
import { parseAbundantTree, rdf12 } from "asciidoc-abundant-tree";
|
|
9
|
-
const require = createRequire(import.meta.url);
|
|
10
8
|
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
9
|
const HTML_ANCHOR_PATTERN = /\b(?:id|name)="([^"]+)"/g;
|
|
12
10
|
const AAT_NAMESPACE = "https://micheng.dev/ns/asciidoc-abundant-tree#";
|
|
@@ -158,61 +156,50 @@ async function workspaceUsesDiagrams(rootDir, books) {
|
|
|
158
156
|
sources.push(await combinedBookSource(book.bookDir));
|
|
159
157
|
return sources.some((source) => DIAGRAM_BLOCK_PATTERN.test(source));
|
|
160
158
|
}
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
// 每次构建单独一个 registry,include 解析器与 Kroki 都登记在它上面:登记在全局会留在进程里,把上一个工作区的解析器
|
|
160
|
+
// 带进下一个(DESIGN.adoc 的 Cheap Performance Rules)。
|
|
161
|
+
async function createExtensionRegistry(config, loadKroki) {
|
|
162
|
+
const registry = Extensions.create();
|
|
163
|
+
registerIncludeResolvers(registry, config);
|
|
163
164
|
if (loadKroki) {
|
|
164
165
|
try {
|
|
165
|
-
|
|
166
|
+
(await import("asciidoctor-kroki")).register(registry);
|
|
166
167
|
}
|
|
167
168
|
catch (error) {
|
|
168
169
|
const message = error instanceof Error ? error.message : String(error);
|
|
169
170
|
throw new Error(`current workspace contains diagram blocks, but asciidoctor-kroki could not be loaded: ${message}`);
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
|
-
return
|
|
173
|
+
return registry;
|
|
173
174
|
}
|
|
174
175
|
// 书架登记的 include 解析器,在 HTML 这一侧由 Asciidoctor 的 include 处理器调用;阅读数据一侧由 aat 调用同一份,
|
|
175
|
-
// 两侧读同一份行(DESIGN.adoc 的 Workspace Configuration
|
|
176
|
-
|
|
177
|
-
function includeResolverRegistry(asciidoctor, config) {
|
|
176
|
+
// 两侧读同一份行(DESIGN.adoc 的 Workspace Configuration)。
|
|
177
|
+
function registerIncludeResolvers(registry, config) {
|
|
178
178
|
if (config.includeResolvers.length === 0)
|
|
179
|
-
return
|
|
179
|
+
return;
|
|
180
180
|
const claiming = (target) => config.includeResolvers.find((resolver) => target.startsWith(resolver.prefix));
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
this.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
throw new Error(`include resolver '${resolver.prefix}' failed for ${location}: ${message}`, { cause: error });
|
|
204
|
-
}
|
|
205
|
-
if (typeof objectValue(resolution).then === "function") {
|
|
206
|
-
// 不再等它:它若 reject,没人接住就成了进程级的未处理 rejection。
|
|
207
|
-
Promise.resolve(resolution).catch(() => { });
|
|
208
|
-
throw new Error(`include resolver '${resolver.prefix}' returned a Promise for ${location}; ` +
|
|
209
|
-
"the HTML build runs on Asciidoctor.js 3, whose include processors are synchronous");
|
|
210
|
-
}
|
|
211
|
-
const { text, absolutePath, startLine } = checkedResolution(resolution, config.documentRoot, `include resolver '${resolver.prefix}'`, location);
|
|
212
|
-
// 显示用的路径照 Asciidoctor 自己 include 文件时的写法,相对文档的 base_dir。
|
|
213
|
-
const displayPath = path.relative(document.getBaseDir(), absolutePath).split(path.sep).join("/");
|
|
214
|
-
reader.pushInclude(text, absolutePath, displayPath, startLine, attributes);
|
|
215
|
-
});
|
|
181
|
+
// 直接登记在 registry 上:Asciidoctor.js 4 里 Extensions.create(分组函数) 登记的处理器从不被调用,include::前缀:…
|
|
182
|
+
// 被当作 URI 悄悄转成链接、不报任何消息;直接登记的处理器对 catalog 与各本书都有效。
|
|
183
|
+
registry.includeProcessor(function () {
|
|
184
|
+
this.handles((target) => claiming(target) !== undefined);
|
|
185
|
+
this.process(async (document, reader, target, attributes) => {
|
|
186
|
+
const resolver = claiming(target);
|
|
187
|
+
const cursor = reader.getCursor();
|
|
188
|
+
const sourcePath = cursor.getFile();
|
|
189
|
+
// 游标已越过 directive 这一行。
|
|
190
|
+
const location = `include::${target}[] in ${sourcePath}: line ${cursor.getLineNumber() - 1}`;
|
|
191
|
+
let resolution;
|
|
192
|
+
try {
|
|
193
|
+
resolution = await resolver.resolve({ target: target.slice(resolver.prefix.length), attributes, sourcePath });
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
throw new Error(`include resolver '${resolver.prefix}' failed for ${location}: ${message}`, { cause: error });
|
|
198
|
+
}
|
|
199
|
+
const { text, absolutePath, startLine } = checkedResolution(resolution, config.documentRoot, `include resolver '${resolver.prefix}'`, location);
|
|
200
|
+
// 显示用的路径照 Asciidoctor 自己 include 文件时的写法,相对文档的 base_dir。
|
|
201
|
+
const displayPath = path.relative(document.getBaseDir(), absolutePath).split(path.sep).join("/");
|
|
202
|
+
reader.pushInclude(text, absolutePath, displayPath, startLine, attributes);
|
|
216
203
|
});
|
|
217
204
|
});
|
|
218
205
|
}
|
|
@@ -265,17 +252,6 @@ function shown(line) {
|
|
|
265
252
|
const clipped = line.length > 60 ? `${line.slice(0, 60)}…` : line;
|
|
266
253
|
return JSON.stringify(clipped).replaceAll("", "\\uFEFF");
|
|
267
254
|
}
|
|
268
|
-
// Asciidoctor.js 3 把位置属性放在 $positional 列表里;aat 跑在 4 上,位置属性以 "1"、"2" 为键。
|
|
269
|
-
// 解析器只认后一种形状,同一个解析器才能服务两侧。留空的位置属性([,x])在 3 里是 undefined、在 4 里是 null,
|
|
270
|
-
// 这里交 null;中间留空([a,,b])时 3 已把空位丢掉、编号与 4 不同,无从还原,规约写明位置属性不能留空。
|
|
271
|
-
function includeAttributes(attributes) {
|
|
272
|
-
const { $positional, ...named } = attributes;
|
|
273
|
-
const positional = Array.isArray($positional) ? $positional : [];
|
|
274
|
-
return {
|
|
275
|
-
...Object.fromEntries(positional.map((value, index) => [String(index + 1), value === undefined ? null : String(value)])),
|
|
276
|
-
...Object.fromEntries(Object.entries(named).map(([name, value]) => [name, String(value)]))
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
255
|
function shouldFetchDiagrams() {
|
|
280
256
|
const value = process.env[FETCH_DIAGRAMS_ENV];
|
|
281
257
|
return value === "1" || value === "true";
|
|
@@ -319,14 +295,12 @@ function formatAsciidoctorMessage(message) {
|
|
|
319
295
|
const text = messageText(message);
|
|
320
296
|
return location ? `${severity}: ${location}: ${text}` : `${severity}: ${text}`;
|
|
321
297
|
}
|
|
322
|
-
function convertFile(
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
loggerManager.setLogger(memoryLogger);
|
|
327
|
-
let document;
|
|
298
|
+
async function convertFile(extensionRegistry, input, outputFile, baseDir, attributes = {}) {
|
|
299
|
+
const defaultLogger = LoggerManager.getLogger();
|
|
300
|
+
const memoryLogger = MemoryLogger.create();
|
|
301
|
+
LoggerManager.setLogger(memoryLogger);
|
|
328
302
|
try {
|
|
329
|
-
|
|
303
|
+
await convertAsciiDocFile(input, {
|
|
330
304
|
safe: "unsafe",
|
|
331
305
|
base_dir: baseDir,
|
|
332
306
|
to_file: outputFile,
|
|
@@ -336,21 +310,20 @@ function convertFile(asciidoctor, extensionRegistry, input, outputFile, baseDir,
|
|
|
336
310
|
});
|
|
337
311
|
}
|
|
338
312
|
finally {
|
|
339
|
-
|
|
313
|
+
LoggerManager.setLogger(defaultLogger);
|
|
340
314
|
}
|
|
341
315
|
const messages = memoryLogger.getMessages();
|
|
342
316
|
if (messages.length > 0) {
|
|
343
317
|
throw new Error(`AsciiDoc strict check failed for ${input}\n${messages.map(formatAsciidoctorMessage).join("\n")}`);
|
|
344
318
|
}
|
|
345
|
-
return document;
|
|
346
319
|
}
|
|
347
|
-
async function buildHtml(rootDir, books,
|
|
320
|
+
async function buildHtml(rootDir, books, extensionRegistry, useKroki, fetchDiagrams) {
|
|
348
321
|
await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
|
|
349
|
-
convertFile(
|
|
322
|
+
await convertFile(extensionRegistry, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
|
|
350
323
|
await pruneStaleBookHtmlDirs(rootDir, books);
|
|
351
324
|
for (const book of books) {
|
|
352
325
|
await mkdir(book.htmlOutputDir, { recursive: true });
|
|
353
|
-
convertFile(
|
|
326
|
+
await convertFile(extensionRegistry, book.input, path.join(book.htmlOutputDir, "book.html"), book.bookDir, {
|
|
354
327
|
...(useKroki && fetchDiagrams ? { "kroki-fetch-diagram": "", "kroki-http-method": "post" } : {})
|
|
355
328
|
});
|
|
356
329
|
}
|
|
@@ -570,12 +543,16 @@ function bindReferences(config, document) {
|
|
|
570
543
|
const sections = new Map(projection.nodeIndex.entries()
|
|
571
544
|
.filter((entry) => entry.kind === "section")
|
|
572
545
|
.map((entry) => [entry.iri.value, entry]));
|
|
573
|
-
|
|
546
|
+
// 地址的名字:选择器是这一节的标题或某个限定地址时,照写下的样子显示,各级标题之间用间隔号;是 ID(作者声明的或生成的)
|
|
547
|
+
// 时显示标题——ID 是给引用用的第二个地址,不是给读者看的名字。按图上是哪个标签判,不按字符串里有没有 :: 猜。
|
|
548
|
+
const located = (iris, selector) => [...iris].flatMap((iri) => {
|
|
574
549
|
const entry = sections.get(iri);
|
|
575
550
|
const id = entry?.node.ids?.[0];
|
|
576
551
|
if (entry === undefined || id === undefined)
|
|
577
552
|
return [];
|
|
578
|
-
|
|
553
|
+
const node = { termType: "iri", value: iri };
|
|
554
|
+
const named = values(node, "headline").includes(selector) || values(node, "addressPath").includes(selector);
|
|
555
|
+
return [{ id, shown: named ? selector.split("::").join("·") : entry.node.title ?? id }];
|
|
579
556
|
});
|
|
580
557
|
// 别的书指过来的引用在这本书的 label space 里找:图上四个标签谓词的值,按整串相等找拥有者。
|
|
581
558
|
const ownersByLabel = new Map();
|
|
@@ -588,7 +565,7 @@ function bindReferences(config, document) {
|
|
|
588
565
|
ownersByLabel.set(triple.object.value, iris);
|
|
589
566
|
}
|
|
590
567
|
}
|
|
591
|
-
const owners = (selector) => located(ownersByLabel.get(selector) ?? []);
|
|
568
|
+
const owners = (selector) => located(ownersByLabel.get(selector) ?? [], selector);
|
|
592
569
|
const headingsByFragment = new Map();
|
|
593
570
|
const crossBook = [];
|
|
594
571
|
// 指向整本书的 xref(没有片段)没有 targetSelector:没有可改写的片段。
|
|
@@ -600,7 +577,7 @@ function bindReferences(config, document) {
|
|
|
600
577
|
crossBook.push({ document, fragment });
|
|
601
578
|
continue;
|
|
602
579
|
}
|
|
603
|
-
const found = located(values(edge, "targetHeading"));
|
|
580
|
+
const found = located(values(edge, "targetHeading"), fragment);
|
|
604
581
|
if (found.length !== 1)
|
|
605
582
|
continue;
|
|
606
583
|
headingsByFragment.set(fragment, found[0]);
|
|
@@ -635,14 +612,14 @@ async function bindCrossBookReferences(book, references, bindingsByInput, anchor
|
|
|
635
612
|
if (anchors.has(fragment))
|
|
636
613
|
continue;
|
|
637
614
|
const htmlPath = `${documentName}.html`;
|
|
638
|
-
const link = { href: `${htmlPath}#${heading.id}`,
|
|
615
|
+
const link = { href: `${htmlPath}#${heading.id}`, shown: heading.shown };
|
|
639
616
|
linksByHref.set(`${htmlPath}#${fragment}`, link);
|
|
640
617
|
linksByHref.set(escapeHtmlAttribute(`${htmlPath}#${fragment}`), link);
|
|
641
618
|
}
|
|
642
619
|
return linksByHref;
|
|
643
620
|
}
|
|
644
|
-
// 在目标书里恰好对上一节、而目标书的 HTML 里没有这个片段的跨书链接,改成指向那一节的标题。Asciidoctor
|
|
645
|
-
//
|
|
621
|
+
// 在目标书里恰好对上一节、而目标书的 HTML 里没有这个片段的跨书链接,改成指向那一节的标题。Asciidoctor 对没有显示文字的跨书 xref
|
|
622
|
+
// 显示文档路径;改写时换成地址的名字。
|
|
646
623
|
function rewriteCrossBookXrefs(html, linksByHref) {
|
|
647
624
|
if (linksByHref.size === 0)
|
|
648
625
|
return html;
|
|
@@ -650,12 +627,12 @@ function rewriteCrossBookXrefs(html, linksByHref) {
|
|
|
650
627
|
const link = linksByHref.get(`${htmlPath}#${fragment}`) ?? linksByHref.get(`${htmlPath}#${decodeUrlFragment(fragment)}`);
|
|
651
628
|
if (link === undefined)
|
|
652
629
|
return whole;
|
|
653
|
-
const shown = text === htmlPath ? escapeHtmlAttribute(link.
|
|
630
|
+
const shown = text === htmlPath ? escapeHtmlAttribute(link.shown) : text;
|
|
654
631
|
return `<a href="${escapeHtmlAttribute(link.href)}"${rest}>${shown}</a>`;
|
|
655
632
|
});
|
|
656
633
|
}
|
|
657
634
|
// Asciidoctor 把它解析不了的书内 xref 写成 <a href="#选择器">[选择器]</a>;aat 的投影里恰好对上一节的,改成指向那一节的标题,
|
|
658
|
-
//
|
|
635
|
+
// 显示地址的名字。片段已经是 HTML 里某个元素的 id 时,保持 Asciidoctor 写下的样子。
|
|
659
636
|
function rewriteBoundXrefs(html, headingsByFragment) {
|
|
660
637
|
if (headingsByFragment.size === 0)
|
|
661
638
|
return html;
|
|
@@ -667,7 +644,7 @@ function rewriteBoundXrefs(html, headingsByFragment) {
|
|
|
667
644
|
const heading = headingsByFragment.get(fragment) ?? headingsByFragment.get(decoded);
|
|
668
645
|
if (heading === undefined)
|
|
669
646
|
return whole;
|
|
670
|
-
const shown = text === `[${fragment}]` ? escapeHtmlAttribute(heading.
|
|
647
|
+
const shown = text === `[${fragment}]` ? escapeHtmlAttribute(heading.shown) : text;
|
|
671
648
|
return `<a href="#${escapeHtmlAttribute(heading.id)}"${rest}>${shown}</a>`;
|
|
672
649
|
});
|
|
673
650
|
}
|
|
@@ -1251,8 +1228,8 @@ export async function buildWorkspace(rootDir = process.cwd()) {
|
|
|
1251
1228
|
const config = await loadRuntimeConfig(rootDir);
|
|
1252
1229
|
const useKroki = await workspaceUsesDiagrams(rootDir, books);
|
|
1253
1230
|
const fetchDiagrams = shouldFetchDiagrams();
|
|
1254
|
-
const
|
|
1255
|
-
await buildHtml(rootDir, books,
|
|
1231
|
+
const extensionRegistry = await createExtensionRegistry(config, useKroki);
|
|
1232
|
+
await buildHtml(rootDir, books, extensionRegistry, useKroki, fetchDiagrams);
|
|
1256
1233
|
await copyAssets(rootDir, books);
|
|
1257
1234
|
await addHomeLinks(rootDir, books, config);
|
|
1258
1235
|
await writeRootIndex(rootDir, config.rootIndex);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-asciidoc-multi-book-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Create a complete AsciiDoc multi-book workspace.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": {
|
|
@@ -49,15 +49,15 @@
|
|
|
49
49
|
"LICENSE"
|
|
50
50
|
],
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
52
|
+
"node": ">=22"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@asciidoctor/core": "^
|
|
56
|
-
"@types/node": "^
|
|
55
|
+
"@asciidoctor/core": "^4.1.1",
|
|
56
|
+
"@types/node": "^22.20.5",
|
|
57
57
|
"asciidoc-abundant-tree": "link:../asciidoc-abundant-tree",
|
|
58
|
-
"asciidoctor-kroki": "^0.
|
|
59
|
-
"playwright": "^1.
|
|
60
|
-
"typescript": "^
|
|
58
|
+
"asciidoctor-kroki": "^2.0.0",
|
|
59
|
+
"playwright": "^1.64.1",
|
|
60
|
+
"typescript": "^7.0.2"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "tsc -p tsconfig.json",
|
|
@@ -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]` 这类块写法出现在行内时不会被当作结构,用反引号即可。
|
|
@@ -55,17 +55,14 @@ Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的
|
|
|
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
|
|
|
@@ -74,7 +71,7 @@ Asciidoctor 还会从标题生成一个 ID,生成时去掉标点,同名的
|
|
|
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
|
|
|
@@ -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
|
=== 中西文混排照规范
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=22"
|
|
7
7
|
},
|
|
8
8
|
"packageManager": "pnpm@10.33.0",
|
|
9
9
|
"scripts": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"clean": "node tools/adoc-books.mjs clean"
|
|
12
12
|
},
|
|
13
13
|
"devDependencies": {
|
|
14
|
-
"@asciidoctor/core": "^
|
|
14
|
+
"@asciidoctor/core": "^4.1.1",
|
|
15
15
|
"asciidoc-abundant-tree": "^0.6.0",
|
|
16
|
-
"asciidoctor-kroki": "^0.
|
|
16
|
+
"asciidoctor-kroki": "^2.0.0"
|
|
17
17
|
}
|
|
18
18
|
}
|