create-asciidoc-multi-book-workspace 0.8.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 CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## 0.8.0 - 2026-10-31
4
16
 
5
17
  ### Changed
@@ -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 asciidoctorFactory from "@asciidoctor/core";
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
- function createAsciidoctor(loadKroki) {
162
- const asciidoctor = asciidoctorFactory();
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
- require("asciidoctor-kroki").register(asciidoctor.Extensions);
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 asciidoctor;
173
+ return registry;
173
174
  }
174
175
  // 书架登记的 include 解析器,在 HTML 这一侧由 Asciidoctor 的 include 处理器调用;阅读数据一侧由 aat 调用同一份,
175
- // 两侧读同一份行(DESIGN.adoc 的 Workspace Configuration)。每次构建单独一个 registry:Asciidoctor 3 的全局扩展
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 asciidoctor.Extensions.create();
179
+ return;
180
180
  const claiming = (target) => config.includeResolvers.find((resolver) => target.startsWith(resolver.prefix));
181
- // 以分组登记:registry 每绑定一个文档就重置一次,只有分组会重新登记。catalog 与各本书共用这一个 registry,
182
- // 直接调 registry.includeProcessor 时只有第一个文档用得上;之后的文档把 include::前缀:… 当作 URI,
183
- // 悄悄转成链接,不报任何消息。
184
- return asciidoctor.Extensions.create(function () {
185
- this.includeProcessor(function () {
186
- this.handles((target) => claiming(target) !== undefined);
187
- this.process((document, reader, target, attributes) => {
188
- const resolver = claiming(target);
189
- const cursor = reader.getCursor();
190
- const sourcePath = cursor.getFile();
191
- // 游标已越过 directive 这一行。
192
- const location = `include::${target}[] in ${sourcePath}: line ${cursor.getLineNumber() - 1}`;
193
- let resolution;
194
- try {
195
- resolution = resolver.resolve({
196
- target: target.slice(resolver.prefix.length),
197
- attributes: includeAttributes(attributes),
198
- sourcePath
199
- });
200
- }
201
- catch (error) {
202
- const message = error instanceof Error ? error.message : String(error);
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(asciidoctor, extensionRegistry, input, outputFile, baseDir, attributes = {}) {
323
- const loggerManager = asciidoctor.LoggerManager;
324
- const defaultLogger = loggerManager.getLogger();
325
- const memoryLogger = asciidoctor.MemoryLogger.create();
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
- document = asciidoctor.convertFile(input, {
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
- loggerManager.setLogger(defaultLogger);
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, asciidoctor, extensionRegistry, useKroki, fetchDiagrams) {
320
+ async function buildHtml(rootDir, books, extensionRegistry, useKroki, fetchDiagrams) {
348
321
  await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
349
- convertFile(asciidoctor, extensionRegistry, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
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(asciidoctor, extensionRegistry, book.input, path.join(book.htmlOutputDir, "book.html"), book.bookDir, {
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
  }
@@ -1255,8 +1228,8 @@ export async function buildWorkspace(rootDir = process.cwd()) {
1255
1228
  const config = await loadRuntimeConfig(rootDir);
1256
1229
  const useKroki = await workspaceUsesDiagrams(rootDir, books);
1257
1230
  const fetchDiagrams = shouldFetchDiagrams();
1258
- const asciidoctor = createAsciidoctor(useKroki);
1259
- await buildHtml(rootDir, books, asciidoctor, includeResolverRegistry(asciidoctor, config), useKroki, fetchDiagrams);
1231
+ const extensionRegistry = await createExtensionRegistry(config, useKroki);
1232
+ await buildHtml(rootDir, books, extensionRegistry, useKroki, fetchDiagrams);
1260
1233
  await copyAssets(rootDir, books);
1261
1234
  await addHomeLinks(rootDir, books, config);
1262
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.8.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": ">=20.19.0"
52
+ "node": ">=22"
53
53
  },
54
54
  "devDependencies": {
55
- "@asciidoctor/core": "^3.0.4",
56
- "@types/node": "^24.10.1",
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.18.1",
59
- "playwright": "^1.59.1",
60
- "typescript": "^5.9.3"
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",
@@ -3,7 +3,7 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "engines": {
6
- "node": ">=20.19.0"
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": "^3.0.4",
14
+ "@asciidoctor/core": "^4.1.1",
15
15
  "asciidoc-abundant-tree": "^0.6.0",
16
- "asciidoctor-kroki": "^0.18.1"
16
+ "asciidoctor-kroki": "^2.0.0"
17
17
  }
18
18
  }