create-asciidoc-multi-book-workspace 0.1.7 → 0.1.8

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,15 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8 - 2026-06-14
4
+
5
+ ### Fixed
6
+
7
+ - Stop preview builds from emitting the Opal duplicate-load warning by loading Asciidoctor through the ESM `@asciidoctor/core` entry instead of mixing it with the CommonJS `asciidoctor` wrapper.
8
+
9
+ ### Changed
10
+
11
+ - Depend on `@asciidoctor/core` directly in generated workspaces and align `asciidoctor-kroki` with core 3.0.4 in the lockfile.
12
+ - Clarify the structured writing sample guidance for precisely referencing a source region with ordinary `xref` links.
13
+
3
14
  ## 0.1.7 - 2026-06-13
4
15
 
5
16
  ### Added
6
17
 
7
- - Add a `复制本书为纯文本` control to each generated book page so readers can copy the expanded single-file book text for saving, note-taking, or model analysis.
8
- - Add a browser fallback that opens the expanded book text as a UTF-8 plain-text page when clipboard access is unavailable.
18
+ - Add a `复制本书为纯文本` control to each generated book page so readers can copy the book source bundle for saving, note-taking, or model analysis.
19
+ - Add a browser fallback that opens the book source bundle as a UTF-8 plain-text page when clipboard access is unavailable.
9
20
 
10
21
  ### Changed
11
22
 
12
- - Keep the generated book page controls self-contained by embedding the already-reduced book text in the HTML instead of adding another published source file.
23
+ - Keep the generated book page controls self-contained by embedding the source bundle in the HTML instead of adding another published source file.
13
24
 
14
25
  ## 0.1.6 - 2026-06-09
15
26
 
@@ -91,5 +102,5 @@
91
102
  - Publish the project as `create-asciidoc-multi-book-workspace`, an npm initializer for complete AsciiDoc multi-book workspaces.
92
103
  - Generate user-owned workspaces with seven sample books, `catalog.adoc`, shared assets, explicit AsciiDoc dependencies, and an editable local `tools/adoc-books.mjs` runtime.
93
104
  - Keep maintainer-only files out of generated workspaces and the npm package, including tests, CI, TypeScript source, and `DESIGN.adoc`.
94
- - Build and check generated workspaces through a single Node runtime using Asciidoctor.js, reducer support, local resource checks, Kroki diagram URL rendering, and optional fetched diagram output.
105
+ - Build and check generated workspaces through a single Node runtime using Asciidoctor.js, local resource checks, Kroki diagram URL rendering, and optional fetched diagram output.
95
106
  - Validate that each default sample book can be deleted after removing its catalog entries, while user-authored real broken xrefs still fail checks.
package/README.md CHANGED
@@ -63,7 +63,7 @@ It does not include this repository's `test/`, `.github/`, `src/`, `dist/`, `DES
63
63
  The generated `package.json` directly lists the AsciiDoc toolchain:
64
64
 
65
65
  - `asciidoctor`
66
- - `@asciidoctor/reducer`
66
+ - `asciidoc-abundant-tree`
67
67
  - `asciidoctor-kroki`
68
68
 
69
69
  The generated scripts are:
@@ -3,6 +3,8 @@ import { createRequire } from "node:module";
3
3
  import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import asciidoctorFactory from "@asciidoctor/core";
7
+ import { parseAbundantTree } from "asciidoc-abundant-tree";
6
8
  const require = createRequire(import.meta.url);
7
9
  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;
8
10
  const CATALOG_BOOK_XREF_PATTERN = /xref:books\/([^/\]]+)\/book\.adoc(?:#[^\[]+)?\[/g;
@@ -107,10 +109,13 @@ async function readIfExists(filePath) {
107
109
  return await existsFile(filePath) ? readFile(filePath, "utf8") : "";
108
110
  }
109
111
  async function combinedBookSource(bookDir) {
110
- const files = await collectFiles(bookDir, (filePath) => filePath.endsWith(".adoc"));
112
+ const files = await collectFiles(bookDir, (filePath) => filePath.endsWith(".adoc") || filePath.endsWith(".mjs"));
111
113
  const sources = [];
112
- for (const file of files)
113
- sources.push(await readFile(file, "utf8"));
114
+ for (const file of files) {
115
+ const relativePath = path.relative(path.dirname(bookDir), file).split(path.sep).join("/");
116
+ const source = await readFile(file, "utf8");
117
+ sources.push(`// file: ${relativePath}\n${source.endsWith("\n") ? source : `${source}\n`}`);
118
+ }
114
119
  return sources.join("\n");
115
120
  }
116
121
  async function workspaceUsesDiagrams(rootDir, books) {
@@ -120,8 +125,7 @@ async function workspaceUsesDiagrams(rootDir, books) {
120
125
  return sources.some((source) => DIAGRAM_BLOCK_PATTERN.test(source));
121
126
  }
122
127
  function createAsciidoctor(loadKroki) {
123
- const asciidoctor = require("asciidoctor")();
124
- require("@asciidoctor/reducer").register();
128
+ const asciidoctor = asciidoctorFactory();
125
129
  if (loadKroki) {
126
130
  try {
127
131
  require("asciidoctor-kroki").register(asciidoctor.Extensions);
@@ -137,25 +141,6 @@ function shouldFetchDiagrams() {
137
141
  const value = process.env[FETCH_DIAGRAMS_ENV];
138
142
  return value === "1" || value === "true";
139
143
  }
140
- function reduceAdocSource(asciidoctor, input) {
141
- const doc = asciidoctor.loadFile(input, { safe: "unsafe" });
142
- const source = doc.getSource();
143
- if (/^[ \t]*include::/m.test(source)) {
144
- throw new Error(`reducer left unresolved include directives in ${input}`);
145
- }
146
- return source;
147
- }
148
- async function buildReducedAdoc(rootDir, books, asciidoctor) {
149
- const adocDir = path.join(rootDir, "build", "adoc");
150
- await rm(adocDir, { force: true, recursive: true });
151
- await mkdir(path.join(adocDir, "books"), { recursive: true });
152
- const catalogSource = reduceAdocSource(asciidoctor, path.join(rootDir, "catalog.adoc"));
153
- await writeFile(path.join(adocDir, "catalog.adoc"), catalogSource, "utf8");
154
- for (const book of books) {
155
- const source = reduceAdocSource(asciidoctor, book.input);
156
- await writeFile(path.join(adocDir, "books", `${book.bookId}.adoc`), source, "utf8");
157
- }
158
- }
159
144
  async function pruneStaleBookHtmlDirs(rootDir, books) {
160
145
  const htmlBooksDir = path.join(rootDir, "build", "html", "books");
161
146
  if (!await existsDir(htmlBooksDir))
@@ -215,13 +200,591 @@ function escapeJsonScript(value) {
215
200
  .replaceAll("\u2028", "\\u2028")
216
201
  .replaceAll("\u2029", "\\u2029");
217
202
  }
218
- function addBookControlsToBookHtml(html, href, homeLink, bookSource) {
203
+ function escapeJsonValueForScript(value) {
204
+ return JSON.stringify(value)
205
+ .replaceAll("<", "\\u003c")
206
+ .replaceAll("\u2028", "\\u2028")
207
+ .replaceAll("\u2029", "\\u2029");
208
+ }
209
+ function sectionStyle(section) {
210
+ for (const metadata of section.metadata ?? []) {
211
+ const style = metadata.attributes?.style;
212
+ if (typeof style === "string")
213
+ return style;
214
+ }
215
+ return undefined;
216
+ }
217
+ function firstSectionId(section) {
218
+ const id = section.ids?.[0];
219
+ if (!id)
220
+ throw new Error(`section is missing an id: ${section.title ?? "(untitled)"}`);
221
+ return id;
222
+ }
223
+ function childSections(section) {
224
+ return (section.children ?? []).filter((child) => child.kind === "section");
225
+ }
226
+ function sourceSpan(section) {
227
+ const relativePath = section.source?.relativePath;
228
+ if (!relativePath)
229
+ return undefined;
230
+ return {
231
+ relativePath,
232
+ ...(typeof section.source?.span?.startLine === "number" ? { startLine: section.source.span.startLine } : {}),
233
+ ...(typeof section.source?.span?.endLine === "number" ? { endLine: section.source.span.endLine } : {})
234
+ };
235
+ }
236
+ function sectionAnchorTree(section) {
237
+ const anchors = [firstSectionId(section)];
238
+ for (const child of childSections(section))
239
+ anchors.push(...sectionAnchorTree(child));
240
+ return anchors;
241
+ }
242
+ function sectionToc(section, baseDepth = 0, topTitle) {
243
+ const toc = [{ title: topTitle ?? section.title ?? firstSectionId(section), anchor: firstSectionId(section), depth: baseDepth }];
244
+ for (const child of childSections(section)) {
245
+ toc.push(...sectionToc(child, baseDepth + 1));
246
+ }
247
+ return toc;
248
+ }
249
+ function chapterPage(section, parentPageId) {
250
+ return {
251
+ id: firstSectionId(section),
252
+ kind: "chapter",
253
+ title: section.title ?? firstSectionId(section),
254
+ anchors: sectionAnchorTree(section),
255
+ toc: sectionToc(section, 0, "概述"),
256
+ source: sourceSpan(section),
257
+ ...(parentPageId ? { parentPageId } : {})
258
+ };
259
+ }
260
+ function backmatterPage(section, kind) {
261
+ return {
262
+ id: firstSectionId(section),
263
+ kind,
264
+ title: section.title ?? firstSectionId(section),
265
+ anchors: sectionAnchorTree(section),
266
+ toc: sectionToc(section, 0, "概述"),
267
+ source: sourceSpan(section)
268
+ };
269
+ }
270
+ function assertNoToolErrors(bookId, diagnostics) {
271
+ for (const diagnostic of diagnostics) {
272
+ const entry = objectValue(diagnostic);
273
+ if (entry.level === "error") {
274
+ throw new Error(`${bookId}: ${stringValue(entry.code, "error")}: ${stringValue(entry.message, "")}`);
275
+ }
276
+ }
277
+ }
278
+ function sourceBundleFromDocument(document, fallbackPath, fallbackSource) {
279
+ const sourceFiles = Array.isArray(document.sourceFiles) ? document.sourceFiles : [];
280
+ const files = sourceFiles.filter((file) => typeof file.relativePath === "string" && typeof file.raw === "string");
281
+ if (files.length === 0)
282
+ return fallbackSource;
283
+ return files.map((file) => {
284
+ const body = file.raw?.endsWith("\n") ? file.raw : `${file.raw ?? ""}\n`;
285
+ return `// file: ${file.relativePath ?? fallbackPath}\n${body}`;
286
+ }).join("\n");
287
+ }
288
+ function fallbackReaderBookData(rootDir, book, fallbackSource, reason) {
289
+ console.warn(`${book.bookId}: reader page map fell back to cover-only mode: ${reason}`);
290
+ const entry = path.relative(rootDir, book.input).split(path.sep).join("/");
291
+ return {
292
+ pageMap: {
293
+ version: 1,
294
+ book: {
295
+ id: book.bookId,
296
+ title: book.bookId,
297
+ entry
298
+ },
299
+ pages: [{
300
+ id: "cover",
301
+ kind: "cover",
302
+ title: book.bookId,
303
+ anchors: [],
304
+ toc: []
305
+ }]
306
+ },
307
+ sourceBundle: fallbackSource
308
+ };
309
+ }
310
+ function buildReaderBookData(rootDir, book, fallbackSource) {
311
+ let document;
312
+ try {
313
+ document = parseAbundantTree({
314
+ sourcePath: book.input,
315
+ mode: "book-entry",
316
+ documentRoot: rootDir
317
+ });
318
+ }
319
+ catch (error) {
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ return fallbackReaderBookData(rootDir, book, fallbackSource, message);
322
+ }
323
+ const diagnostics = Array.isArray(document.toolDiagnostics) ? document.toolDiagnostics : [];
324
+ assertNoToolErrors(book.bookId, diagnostics);
325
+ const title = objectValue(document.title).text;
326
+ const pages = [{
327
+ id: "cover",
328
+ kind: "cover",
329
+ title: typeof title === "string" ? title : book.bookId,
330
+ anchors: [],
331
+ toc: []
332
+ }];
333
+ const sections = (Array.isArray(document.children) ? document.children : []).filter((child) => {
334
+ return typeof child === "object" && child !== null && child.kind === "section";
335
+ });
336
+ const frontmatterStyles = new Set(["abstract", "colophon", "dedication", "preface", "acknowledgments"]);
337
+ const backmatterStyles = new Set(["appendix", "glossary", "bibliography", "index"]);
338
+ const frontmatter = sections.filter((section) => section.level === 1 && frontmatterStyles.has(sectionStyle(section) ?? ""));
339
+ if (frontmatter.length > 0) {
340
+ pages.push({
341
+ id: "frontmatter",
342
+ kind: "frontmatter",
343
+ title: "前置",
344
+ anchors: frontmatter.map(firstSectionId),
345
+ toc: frontmatter.map((section) => ({
346
+ title: section.title ?? firstSectionId(section),
347
+ anchor: firstSectionId(section),
348
+ depth: 0
349
+ }))
350
+ });
351
+ }
352
+ for (const section of sections) {
353
+ const style = sectionStyle(section);
354
+ if (frontmatterStyles.has(style ?? ""))
355
+ continue;
356
+ if (backmatterStyles.has(style)) {
357
+ pages.push(backmatterPage(section, style));
358
+ continue;
359
+ }
360
+ if (section.level === 0) {
361
+ const partId = `part-${firstSectionId(section)}`;
362
+ const children = childSections(section);
363
+ const childPages = children.map((child) => chapterPage(child, partId));
364
+ pages.push({
365
+ id: partId,
366
+ kind: "part",
367
+ title: section.title ?? firstSectionId(section),
368
+ anchors: [firstSectionId(section)],
369
+ toc: [{ title: "概述", anchor: firstSectionId(section), depth: 0 }],
370
+ source: sourceSpan(section),
371
+ childPageIds: childPages.map((page) => page.id)
372
+ });
373
+ pages.push(...childPages);
374
+ continue;
375
+ }
376
+ pages.push(chapterPage(section));
377
+ }
378
+ const pageMap = {
379
+ version: 1,
380
+ book: {
381
+ id: book.bookId,
382
+ title: pages[0].title,
383
+ entry: path.relative(rootDir, book.input).split(path.sep).join("/")
384
+ },
385
+ pages
386
+ };
387
+ return {
388
+ pageMap,
389
+ sourceBundle: sourceBundleFromDocument(document, pageMap.book.entry, fallbackSource)
390
+ };
391
+ }
392
+ function readerStyles() {
393
+ return `
394
+ .multi-book-reader-toggle {
395
+ display: grid;
396
+ gap: .35rem;
397
+ grid-template-columns: 1fr 1fr;
398
+ margin-top: .75rem;
399
+ }
400
+ .multi-book-reader-toggle button {
401
+ appearance: none;
402
+ background: #ffffff;
403
+ border: 1px solid #cbd5e1;
404
+ border-radius: 4px;
405
+ color: #1f2937;
406
+ cursor: pointer;
407
+ font-size: .82rem;
408
+ font-weight: 600;
409
+ padding: .42rem .5rem;
410
+ }
411
+ .multi-book-reader-toggle button[aria-pressed="true"] {
412
+ border-color: #0f766e;
413
+ color: #0f766e;
414
+ }
415
+ [data-multi-book-page-nav] {
416
+ border-top: 1px solid #e5e7eb;
417
+ margin-top: .75rem;
418
+ padding-top: .75rem;
419
+ }
420
+ [data-multi-book-page-nav][hidden] {
421
+ display: none;
422
+ }
423
+ [data-multi-book-page-nav] a {
424
+ border-left: 3px solid transparent;
425
+ color: #334155;
426
+ display: block;
427
+ font-size: .85rem;
428
+ line-height: 1.35;
429
+ padding: .3rem .35rem .3rem .55rem;
430
+ text-decoration: none;
431
+ }
432
+ [data-multi-book-page-nav] a[data-reader-depth="1"] {
433
+ padding-left: 1.45rem;
434
+ }
435
+ [data-multi-book-page-nav] a[aria-current="page"] {
436
+ border-left-color: #0f766e;
437
+ color: #0f766e;
438
+ font-weight: 700;
439
+ }
440
+ .multi-book-reader-cover {
441
+ border-bottom: 1px solid #e5e7eb;
442
+ margin-bottom: 1.5rem;
443
+ padding-bottom: 1rem;
444
+ }
445
+ .multi-book-reader-cover h2 {
446
+ margin-top: 0;
447
+ }
448
+ [data-multi-book-page-toc] {
449
+ border-left: 1px solid #e5e7eb;
450
+ color: #334155;
451
+ font-size: .85rem;
452
+ padding-left: 1rem;
453
+ }
454
+ [data-multi-book-page-toc][hidden] {
455
+ display: none;
456
+ }
457
+ [data-multi-book-page-toc] strong {
458
+ color: #1f2937;
459
+ display: block;
460
+ font-size: .78rem;
461
+ margin-bottom: .35rem;
462
+ }
463
+ [data-multi-book-page-toc] a {
464
+ color: #334155;
465
+ display: block;
466
+ margin: .35rem 0;
467
+ text-decoration: none;
468
+ }
469
+ [data-multi-book-pagination] {
470
+ border-top: 1px solid #e5e7eb;
471
+ display: flex;
472
+ gap: 1rem;
473
+ justify-content: space-between;
474
+ margin-bottom: 2rem;
475
+ margin-top: 2.5rem;
476
+ padding-top: 1rem;
477
+ }
478
+ [data-multi-book-pagination][hidden] {
479
+ display: none;
480
+ }
481
+ [data-multi-book-pagination] a {
482
+ border: 1px solid #cbd5e1;
483
+ border-radius: 4px;
484
+ color: #1f2937;
485
+ flex: 1 1 0;
486
+ padding: .65rem;
487
+ text-decoration: none;
488
+ }
489
+ [data-multi-book-pagination] span {
490
+ color: #64748b;
491
+ display: block;
492
+ font-size: .78rem;
493
+ }
494
+ @media (min-width: 1024px) {
495
+ body.toc2.toc-left.multi-book-reader-paged #content {
496
+ --multi-book-reader-content-gutter: max(0px, calc((100vw - 20em - 1000px) / 2));
497
+ box-sizing: border-box;
498
+ margin-left: var(--multi-book-reader-content-gutter);
499
+ margin-right: 16rem;
500
+ max-width: min(800px, calc(100vw - 20em - 20rem - var(--multi-book-reader-content-gutter)));
501
+ padding-left: 15px;
502
+ }
503
+ body.toc2.toc-left.multi-book-reader-paged [data-multi-book-pagination] {
504
+ --multi-book-reader-content-gutter: max(0px, calc((100vw - 20em - 1000px) / 2));
505
+ margin-left: var(--multi-book-reader-content-gutter);
506
+ max-width: min(800px, calc(100vw - 20em - 20rem - var(--multi-book-reader-content-gutter)));
507
+ width: calc(100vw - 20em - 20rem - var(--multi-book-reader-content-gutter));
508
+ }
509
+ [data-multi-book-page-toc] {
510
+ position: fixed;
511
+ right: 2rem;
512
+ top: 7rem;
513
+ width: 12rem;
514
+ }
515
+ }
516
+ @media (min-width: 1024px) and (max-width: 1359px) {
517
+ [data-multi-book-page-toc] {
518
+ right: 1rem;
519
+ width: 8rem;
520
+ }
521
+ }
522
+ body.multi-book-reader-paged #footer {
523
+ display: none;
524
+ }
525
+ @media (max-width: 767px) {
526
+ #toc.toc2 {
527
+ position: static;
528
+ width: auto;
529
+ }
530
+ [data-multi-book-pagination] {
531
+ flex-direction: column;
532
+ }
533
+ pre, table {
534
+ max-width: 100%;
535
+ overflow-x: auto;
536
+ }
537
+ }
538
+ `;
539
+ }
540
+ function readerScript() {
541
+ return `
542
+ (function () {
543
+ function initReader() {
544
+ var mapElement = document.getElementById("multi-book-page-map");
545
+ if (!mapElement) return;
546
+ var pageMap;
547
+ try {
548
+ pageMap = JSON.parse(mapElement.textContent || "{}");
549
+ } catch (error) {
550
+ return;
551
+ }
552
+ if (!pageMap || !Array.isArray(pageMap.pages)) return;
553
+
554
+ var content = document.getElementById("content");
555
+ var toc = document.getElementById("toc");
556
+ if (!content || !toc) return;
557
+
558
+ var pages = pageMap.pages;
559
+ var pagesById = new Map();
560
+ var anchorToPageId = new Map();
561
+ pages.forEach(function (page) {
562
+ pagesById.set(page.id, page);
563
+ (page.anchors || []).forEach(function (anchor) { anchorToPageId.set(anchor, page.id); });
564
+ });
565
+
566
+ var controls = document.querySelector("[data-multi-book-controls]");
567
+ var toggle = document.createElement("div");
568
+ toggle.className = "multi-book-reader-toggle";
569
+ toggle.setAttribute("data-multi-book-view-toggle", "");
570
+ toggle.innerHTML = '<button type="button" data-reader-view="continuous">连续</button><button type="button" data-reader-view="paged">页面</button>';
571
+ if (controls) controls.appendChild(toggle);
572
+
573
+ var nav = document.createElement("nav");
574
+ nav.setAttribute("data-multi-book-page-nav", "");
575
+ nav.setAttribute("aria-label", "页面");
576
+ if (controls && controls.nextSibling) toc.insertBefore(nav, controls.nextSibling);
577
+ else toc.appendChild(nav);
578
+
579
+ var pageToc = document.createElement("aside");
580
+ pageToc.setAttribute("data-multi-book-page-toc", "");
581
+ content.parentNode.insertBefore(pageToc, content.nextSibling);
582
+
583
+ var pagination = document.createElement("nav");
584
+ pagination.setAttribute("data-multi-book-pagination", "");
585
+ pagination.setAttribute("aria-label", "分页");
586
+ content.parentNode.insertBefore(pagination, pageToc.nextSibling);
587
+
588
+ var cover = document.createElement("section");
589
+ cover.id = "multi-book-reader-cover";
590
+ cover.className = "multi-book-reader-cover";
591
+ var sourceTitle = document.querySelector("#header > h1");
592
+ var details = document.querySelector("#header > .details");
593
+ cover.innerHTML = "<h2></h2>";
594
+ cover.querySelector("h2").textContent = sourceTitle ? sourceTitle.textContent : pageMap.book.title;
595
+ if (details) cover.appendChild(details.cloneNode(true));
596
+ content.insertBefore(cover, content.firstChild);
597
+
598
+ var originalTocNodes = Array.from(toc.children).filter(function (node) {
599
+ return node !== controls && node !== nav;
600
+ });
601
+ var originalNodes = Array.from(content.children).filter(function (node) { return node !== cover; });
602
+ var currentView = "continuous";
603
+ var currentPageId = "cover";
604
+
605
+ function query() {
606
+ return new URLSearchParams(window.location.search);
607
+ }
608
+
609
+ function writeQuery(values) {
610
+ var url = new URL(window.location.href);
611
+ Object.keys(values).forEach(function (key) {
612
+ if (values[key] == null) url.searchParams.delete(key);
613
+ else url.searchParams.set(key, values[key]);
614
+ });
615
+ history.pushState(null, "", url);
616
+ }
617
+
618
+ function pageNodes(page) {
619
+ if (page.kind === "cover") return [cover];
620
+ if (page.kind === "frontmatter") {
621
+ return (page.anchors || []).map(function (anchor) {
622
+ var heading = document.getElementById(anchor);
623
+ return heading ? heading.closest(".sect1") : null;
624
+ }).filter(Boolean);
625
+ }
626
+ if (page.kind === "part") {
627
+ var partHeading = document.getElementById(page.anchors[0]);
628
+ if (!partHeading) return [];
629
+ var nodes = [partHeading];
630
+ var next = partHeading.nextElementSibling;
631
+ while (next && !next.matches(".sect1") && !next.matches("h1.sect0")) {
632
+ nodes.push(next);
633
+ next = next.nextElementSibling;
634
+ }
635
+ return nodes;
636
+ }
637
+ var heading = document.getElementById(page.anchors[0]);
638
+ var section = heading ? heading.closest(".sect1") : null;
639
+ return section ? [section] : [];
640
+ }
641
+
642
+ function renderNav() {
643
+ nav.hidden = currentView !== "paged";
644
+ nav.textContent = "";
645
+ if (currentView !== "paged") return;
646
+ pages.forEach(function (page) {
647
+ var link = document.createElement("a");
648
+ link.href = "?view=paged&page=" + encodeURIComponent(page.id);
649
+ link.textContent = page.title;
650
+ link.dataset.pageId = page.id;
651
+ link.dataset.readerDepth = page.parentPageId ? "1" : "0";
652
+ if (page.id === currentPageId) link.setAttribute("aria-current", "page");
653
+ link.addEventListener("click", function (event) {
654
+ event.preventDefault();
655
+ setView("paged", page.id, true);
656
+ });
657
+ nav.appendChild(link);
658
+ });
659
+ }
660
+
661
+ function renderPageToc(page) {
662
+ pageToc.hidden = currentView !== "paged";
663
+ pageToc.textContent = "";
664
+ if (currentView !== "paged") return;
665
+ var title = document.createElement("strong");
666
+ title.textContent = "本页内容";
667
+ pageToc.appendChild(title);
668
+ (page.toc || []).forEach(function (item) {
669
+ var link = document.createElement("a");
670
+ link.href = "#" + item.anchor;
671
+ link.textContent = item.title;
672
+ link.style.paddingLeft = (item.depth * 0.8) + "rem";
673
+ link.addEventListener("click", function (event) {
674
+ event.preventDefault();
675
+ var target = document.getElementById(item.anchor);
676
+ if (target) target.scrollIntoView();
677
+ });
678
+ pageToc.appendChild(link);
679
+ });
680
+ }
681
+
682
+ function renderPagination(page) {
683
+ pagination.hidden = currentView !== "paged";
684
+ pagination.textContent = "";
685
+ if (currentView !== "paged") return;
686
+ var index = pages.findIndex(function (candidate) { return candidate.id === page.id; });
687
+ function addLink(label, target) {
688
+ if (!target) {
689
+ var spacer = document.createElement("span");
690
+ pagination.appendChild(spacer);
691
+ return;
692
+ }
693
+ var link = document.createElement("a");
694
+ link.href = "?view=paged&page=" + encodeURIComponent(target.id);
695
+ link.innerHTML = "<span>" + label + "</span>" + target.title;
696
+ link.addEventListener("click", function (event) {
697
+ event.preventDefault();
698
+ setView("paged", target.id, true);
699
+ window.scrollTo(0, 0);
700
+ });
701
+ pagination.appendChild(link);
702
+ }
703
+ addLink("上一页", pages[index - 1]);
704
+ addLink("下一页", pages[index + 1]);
705
+ }
706
+
707
+ function setView(view, pageId, persist) {
708
+ currentView = view;
709
+ currentPageId = pagesById.has(pageId) ? pageId : "cover";
710
+ var page = pagesById.get(currentPageId) || pages[0];
711
+ document.body.classList.toggle("multi-book-reader-paged", view === "paged");
712
+ document.body.classList.toggle("multi-book-reader-continuous", view === "continuous");
713
+ toggle.querySelectorAll("button").forEach(function (button) {
714
+ button.setAttribute("aria-pressed", button.dataset.readerView === view ? "true" : "false");
715
+ });
716
+ if (persist) {
717
+ localStorage.setItem("multi-book-reader-view", view);
718
+ writeQuery({ view: view, page: view === "paged" ? currentPageId : null });
719
+ }
720
+ if (view === "continuous") {
721
+ cover.hidden = true;
722
+ originalNodes.forEach(function (node) { node.hidden = false; });
723
+ originalTocNodes.forEach(function (node) { node.hidden = false; });
724
+ pageToc.hidden = true;
725
+ pagination.hidden = true;
726
+ renderNav();
727
+ return;
728
+ }
729
+ originalTocNodes.forEach(function (node) { node.hidden = true; });
730
+ var visible = new Set(pageNodes(page));
731
+ if (visible.size === 0 && page.kind !== "cover") {
732
+ cover.hidden = false;
733
+ cover.querySelector("h2").textContent = "当前页面无法在生成 HTML 中定位。";
734
+ visible.add(cover);
735
+ }
736
+ cover.hidden = !visible.has(cover);
737
+ originalNodes.forEach(function (node) { node.hidden = !visible.has(node); });
738
+ renderNav();
739
+ renderPageToc(page);
740
+ renderPagination(page);
741
+ }
742
+
743
+ toggle.addEventListener("click", function (event) {
744
+ var button = event.target.closest("button[data-reader-view]");
745
+ if (!button) return;
746
+ setView(button.dataset.readerView, currentPageId, true);
747
+ });
748
+
749
+ document.addEventListener("click", function (event) {
750
+ if (currentView !== "paged") return;
751
+ var link = event.target.closest('a[href^="#"]');
752
+ if (!link) return;
753
+ var anchor = decodeURIComponent(link.getAttribute("href").slice(1));
754
+ var targetPageId = anchorToPageId.get(anchor);
755
+ if (!targetPageId) return;
756
+ if (targetPageId !== currentPageId) {
757
+ event.preventDefault();
758
+ setView("paged", targetPageId, true);
759
+ var target = document.getElementById(anchor);
760
+ if (target) target.scrollIntoView();
761
+ }
762
+ });
763
+
764
+ window.addEventListener("popstate", function () {
765
+ var params = query();
766
+ setView(params.get("view") === "paged" ? "paged" : "continuous", params.get("page") || "cover", false);
767
+ });
768
+
769
+ var params = query();
770
+ var initialView = params.get("view") || localStorage.getItem("multi-book-reader-view") || "continuous";
771
+ setView(initialView === "paged" ? "paged" : "continuous", params.get("page") || "cover", false);
772
+ }
773
+ if (document.readyState === "loading") {
774
+ document.addEventListener("DOMContentLoaded", initReader);
775
+ } else {
776
+ initReader();
777
+ }
778
+ }());
779
+ `;
780
+ }
781
+ function addBookControlsToBookHtml(html, href, homeLink, bookSource, pageMap, bookId) {
219
782
  if (html.includes(CONTROLS_MARKER))
220
783
  return html;
221
784
  const marker = '<div id="toc" class="toc2">';
222
785
  const index = html.indexOf(marker);
223
786
  if (index === -1)
224
- throw new Error("book HTML is missing the left TOC container");
787
+ throw new Error(`${bookId}: missing left TOC container`);
225
788
  const insertAt = index + marker.length;
226
789
  const controlsBlock = `
227
790
  <style>
@@ -277,6 +840,7 @@ function addBookControlsToBookHtml(html, href, homeLink, bookSource) {
277
840
  margin-top: .4rem;
278
841
  min-height: 1em;
279
842
  }
843
+ ${readerStyles()}
280
844
  </style>
281
845
  <div class="multi-book-controls" ${CONTROLS_MARKER}>
282
846
  <a class="multi-book-home" ${HOME_MARKER} href="${escapeHtmlAttribute(href)}">${escapeHtmlAttribute(homeLink.label)}<span>${escapeHtmlAttribute(homeLink.subtitle)}</span></a>
@@ -284,6 +848,7 @@ function addBookControlsToBookHtml(html, href, homeLink, bookSource) {
284
848
  <span class="multi-book-copy-status" data-multi-book-source-status aria-live="polite"></span>
285
849
  </div>
286
850
  <script type="application/json" id="multi-book-source-data">${escapeJsonScript(bookSource)}</script>
851
+ <script type="application/json" id="multi-book-page-map">${escapeJsonValueForScript(pageMap)}</script>
287
852
  <script>
288
853
  (function () {
289
854
  var sourceElement = document.getElementById("multi-book-source-data");
@@ -333,7 +898,9 @@ function addBookControlsToBookHtml(html, href, homeLink, bookSource) {
333
898
  });
334
899
  }());
335
900
  </script>`;
336
- return `${html.slice(0, insertAt)}${controlsBlock}${html.slice(insertAt)}`;
901
+ const withReaderScript = `${controlsBlock}
902
+ <script>${readerScript()}</script>`;
903
+ return `${html.slice(0, insertAt)}${withReaderScript}${html.slice(insertAt)}`;
337
904
  }
338
905
  async function addHomeLinks(rootDir, books, homeLink) {
339
906
  const catalog = path.join(rootDir, "build", "html", "catalog.html");
@@ -341,8 +908,9 @@ async function addHomeLinks(rootDir, books, homeLink) {
341
908
  const htmlFile = path.join(book.htmlOutputDir, "book.html");
342
909
  const html = await readFile(htmlFile, "utf8");
343
910
  const href = path.relative(path.dirname(htmlFile), catalog);
344
- const source = await readFile(path.join(rootDir, "build", "adoc", "books", `${book.bookId}.adoc`), "utf8");
345
- await writeFile(htmlFile, addBookControlsToBookHtml(html, href, homeLink, source), "utf8");
911
+ const fallbackSource = await combinedBookSource(book.bookDir);
912
+ const readerData = buildReaderBookData(rootDir, book, fallbackSource);
913
+ await writeFile(htmlFile, addBookControlsToBookHtml(html, href, homeLink, readerData.sourceBundle, readerData.pageMap, book.bookId), "utf8");
346
914
  }
347
915
  }
348
916
  async function writeRootIndex(rootDir, rootIndex) {
@@ -483,7 +1051,6 @@ export async function buildWorkspace(rootDir = process.cwd()) {
483
1051
  const useKroki = await workspaceUsesDiagrams(rootDir, books);
484
1052
  const fetchDiagrams = shouldFetchDiagrams();
485
1053
  const asciidoctor = createAsciidoctor(useKroki);
486
- await buildReducedAdoc(rootDir, books, asciidoctor);
487
1054
  await buildHtml(rootDir, books, asciidoctor, useKroki, fetchDiagrams);
488
1055
  await copyAssets(rootDir, books);
489
1056
  await addHomeLinks(rootDir, books, config.homeLink);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asciidoc-multi-book-workspace",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Create a complete AsciiDoc multi-book workspace.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
@@ -52,10 +52,11 @@
52
52
  "node": ">=20.19.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@asciidoctor/reducer": "1.1.2",
55
+ "@asciidoctor/core": "3.0.4",
56
56
  "@types/node": "24.10.1",
57
- "asciidoctor": "2.2.9",
57
+ "asciidoc-abundant-tree": "0.1.14",
58
58
  "asciidoctor-kroki": "0.18.1",
59
+ "playwright": "1.59.1",
59
60
  "typescript": "5.9.3"
60
61
  },
61
62
  "scripts": {
@@ -19,8 +19,8 @@ Outputs:
19
19
  - `build/html/index.html`
20
20
  - `build/html/catalog.html`
21
21
  - `build/html/books/<book-id>/book.html`
22
- - `build/adoc/catalog.adoc`
23
- - `build/adoc/books/<book-id>.adoc`
22
+
23
+ Each book HTML includes continuous and paged reading views. The copy-source control copies an abundant-tree source bundle for the current book; default builds do not write expanded ADOC files under `build/adoc/`.
24
24
 
25
25
  Diagram blocks render as Kroki image URLs by default, so normal builds do not depend on the Kroki service being available at build time.
26
26
  To fetch diagram images into the local HTML output, run:
@@ -60,3 +60,5 @@ role 描述标题身份。rel 描述 xref 边上的关系谓词。named attribut
60
60
  对于跨文件的交叉引用,为了避免重复编写相对路径,你可以在文档的头部(Header)定义一个文档属性(Document Attribute)来存储这个基础路径。
61
61
 
62
62
  你可以在主文档的开头定义一个变量(比如 `:book-b-dir:`),后续引用时通过 `{book-b-dir}` 来调用它。
63
+
64
+ 如果你的建模中存在需要对一段区域精准指向、提及的情况,优先考虑使用 **更小一级/更小一节** 的标题把这段区域划分出来,之后就可以继续使用 `xref` 进行交叉引用。
@@ -12,8 +12,8 @@
12
12
  "clean": "node tools/adoc-books.mjs clean"
13
13
  },
14
14
  "devDependencies": {
15
- "@asciidoctor/reducer": "1.1.2",
16
- "asciidoctor": "2.2.9",
15
+ "@asciidoctor/core": "3.0.4",
16
+ "asciidoc-abundant-tree": "0.1.14",
17
17
  "asciidoctor-kroki": "0.18.1"
18
18
  }
19
19
  }