create-asciidoc-multi-book-workspace 0.1.6 → 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 +23 -1
- package/README.md +1 -1
- package/dist/runtime/adoc-books.mjs +693 -37
- package/package.json +4 -3
- package/templates/default-workspace/README.md +2 -2
- package/templates/default-workspace/books/07-structured-writing-conventions/backmatter/appendix-a.adoc +2 -0
- package/templates/default-workspace/package.json.template +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.1.7 - 2026-06-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
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.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Keep the generated book page controls self-contained by embedding the source bundle in the HTML instead of adding another published source file.
|
|
24
|
+
|
|
3
25
|
## 0.1.6 - 2026-06-09
|
|
4
26
|
|
|
5
27
|
### Added
|
|
@@ -80,5 +102,5 @@
|
|
|
80
102
|
- Publish the project as `create-asciidoc-multi-book-workspace`, an npm initializer for complete AsciiDoc multi-book workspaces.
|
|
81
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.
|
|
82
104
|
- Keep maintainer-only files out of generated workspaces and the npm package, including tests, CI, TypeScript source, and `DESIGN.adoc`.
|
|
83
|
-
- Build and check generated workspaces through a single Node runtime using Asciidoctor.js,
|
|
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.
|
|
84
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
|
-
-
|
|
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;
|
|
@@ -11,6 +13,7 @@ const ANCHOR_PATTERN = /^\[#([A-Za-z0-9_-]+)(?:[.,][^\]]*)?\]$/gm;
|
|
|
11
13
|
const LOCAL_TARGET_PATTERN = /\b(?:href|src)="([^"]+)"/g;
|
|
12
14
|
const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
13
15
|
const HOME_MARKER = "data-multi-book-home";
|
|
16
|
+
const CONTROLS_MARKER = "data-multi-book-controls";
|
|
14
17
|
const FETCH_DIAGRAMS_ENV = "ADOC_BOOKS_FETCH_DIAGRAMS";
|
|
15
18
|
const CONFIG_FILE = "adoc-books.config.mjs";
|
|
16
19
|
const DEFAULT_CONFIG = {
|
|
@@ -106,10 +109,13 @@ async function readIfExists(filePath) {
|
|
|
106
109
|
return await existsFile(filePath) ? readFile(filePath, "utf8") : "";
|
|
107
110
|
}
|
|
108
111
|
async function combinedBookSource(bookDir) {
|
|
109
|
-
const files = await collectFiles(bookDir, (filePath) => filePath.endsWith(".adoc"));
|
|
112
|
+
const files = await collectFiles(bookDir, (filePath) => filePath.endsWith(".adoc") || filePath.endsWith(".mjs"));
|
|
110
113
|
const sources = [];
|
|
111
|
-
for (const file of files)
|
|
112
|
-
|
|
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
|
+
}
|
|
113
119
|
return sources.join("\n");
|
|
114
120
|
}
|
|
115
121
|
async function workspaceUsesDiagrams(rootDir, books) {
|
|
@@ -119,8 +125,7 @@ async function workspaceUsesDiagrams(rootDir, books) {
|
|
|
119
125
|
return sources.some((source) => DIAGRAM_BLOCK_PATTERN.test(source));
|
|
120
126
|
}
|
|
121
127
|
function createAsciidoctor(loadKroki) {
|
|
122
|
-
const asciidoctor =
|
|
123
|
-
require("@asciidoctor/reducer").register();
|
|
128
|
+
const asciidoctor = asciidoctorFactory();
|
|
124
129
|
if (loadKroki) {
|
|
125
130
|
try {
|
|
126
131
|
require("asciidoctor-kroki").register(asciidoctor.Extensions);
|
|
@@ -136,25 +141,6 @@ function shouldFetchDiagrams() {
|
|
|
136
141
|
const value = process.env[FETCH_DIAGRAMS_ENV];
|
|
137
142
|
return value === "1" || value === "true";
|
|
138
143
|
}
|
|
139
|
-
function reduceAdocSource(asciidoctor, input) {
|
|
140
|
-
const doc = asciidoctor.loadFile(input, { safe: "unsafe" });
|
|
141
|
-
const source = doc.getSource();
|
|
142
|
-
if (/^[ \t]*include::/m.test(source)) {
|
|
143
|
-
throw new Error(`reducer left unresolved include directives in ${input}`);
|
|
144
|
-
}
|
|
145
|
-
return source;
|
|
146
|
-
}
|
|
147
|
-
async function buildReducedAdoc(rootDir, books, asciidoctor) {
|
|
148
|
-
const adocDir = path.join(rootDir, "build", "adoc");
|
|
149
|
-
await rm(adocDir, { force: true, recursive: true });
|
|
150
|
-
await mkdir(path.join(adocDir, "books"), { recursive: true });
|
|
151
|
-
const catalogSource = reduceAdocSource(asciidoctor, path.join(rootDir, "catalog.adoc"));
|
|
152
|
-
await writeFile(path.join(adocDir, "catalog.adoc"), catalogSource, "utf8");
|
|
153
|
-
for (const book of books) {
|
|
154
|
-
const source = reduceAdocSource(asciidoctor, book.input);
|
|
155
|
-
await writeFile(path.join(adocDir, "books", `${book.bookId}.adoc`), source, "utf8");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
144
|
async function pruneStaleBookHtmlDirs(rootDir, books) {
|
|
159
145
|
const htmlBooksDir = path.join(rootDir, "build", "html", "books");
|
|
160
146
|
if (!await existsDir(htmlBooksDir))
|
|
@@ -208,29 +194,613 @@ function escapeHtmlAttribute(value) {
|
|
|
208
194
|
.replaceAll("<", "<")
|
|
209
195
|
.replaceAll(">", ">");
|
|
210
196
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
197
|
+
function escapeJsonScript(value) {
|
|
198
|
+
return JSON.stringify(value)
|
|
199
|
+
.replaceAll("<", "\\u003c")
|
|
200
|
+
.replaceAll("\u2028", "\\u2028")
|
|
201
|
+
.replaceAll("\u2029", "\\u2029");
|
|
202
|
+
}
|
|
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) {
|
|
782
|
+
if (html.includes(CONTROLS_MARKER))
|
|
213
783
|
return html;
|
|
214
784
|
const marker = '<div id="toc" class="toc2">';
|
|
215
785
|
const index = html.indexOf(marker);
|
|
216
786
|
if (index === -1)
|
|
217
|
-
throw new Error(
|
|
787
|
+
throw new Error(`${bookId}: missing left TOC container`);
|
|
218
788
|
const insertAt = index + marker.length;
|
|
219
|
-
const
|
|
789
|
+
const controlsBlock = `
|
|
220
790
|
<style>
|
|
221
|
-
.multi-book-
|
|
791
|
+
.multi-book-controls {
|
|
222
792
|
margin: 0 0 1rem;
|
|
223
793
|
padding-bottom: .75rem;
|
|
224
794
|
border-bottom: 1px solid #e5e7eb;
|
|
225
795
|
}
|
|
226
|
-
.multi-book-home
|
|
796
|
+
.multi-book-home {
|
|
227
797
|
color: #1f2937;
|
|
228
798
|
display: block;
|
|
229
799
|
font-weight: 600;
|
|
230
800
|
line-height: 1.35;
|
|
231
801
|
text-decoration: none;
|
|
232
802
|
}
|
|
233
|
-
.multi-book-home
|
|
803
|
+
.multi-book-home:hover {
|
|
234
804
|
color: #0f766e;
|
|
235
805
|
text-decoration: underline;
|
|
236
806
|
}
|
|
@@ -241,11 +811,96 @@ function addHomeLinkToBookHtml(html, href, homeLink) {
|
|
|
241
811
|
font-weight: 400;
|
|
242
812
|
margin-top: .15rem;
|
|
243
813
|
}
|
|
814
|
+
.multi-book-copy-source {
|
|
815
|
+
appearance: none;
|
|
816
|
+
background: #ffffff;
|
|
817
|
+
border: 1px solid #cbd5e1;
|
|
818
|
+
border-radius: 4px;
|
|
819
|
+
color: #1f2937;
|
|
820
|
+
cursor: pointer;
|
|
821
|
+
display: block;
|
|
822
|
+
font-size: .82rem;
|
|
823
|
+
font-weight: 600;
|
|
824
|
+
line-height: 1.2;
|
|
825
|
+
margin-top: .75rem;
|
|
826
|
+
padding: .42rem .5rem;
|
|
827
|
+
text-align: center;
|
|
828
|
+
width: 100%;
|
|
829
|
+
}
|
|
830
|
+
.multi-book-copy-source:hover,
|
|
831
|
+
.multi-book-copy-source:focus {
|
|
832
|
+
border-color: #0f766e;
|
|
833
|
+
color: #0f766e;
|
|
834
|
+
}
|
|
835
|
+
.multi-book-copy-status {
|
|
836
|
+
color: #64748b;
|
|
837
|
+
display: block;
|
|
838
|
+
font-size: .75rem;
|
|
839
|
+
line-height: 1.35;
|
|
840
|
+
margin-top: .4rem;
|
|
841
|
+
min-height: 1em;
|
|
842
|
+
}
|
|
843
|
+
${readerStyles()}
|
|
244
844
|
</style>
|
|
245
|
-
<div class="multi-book-
|
|
246
|
-
<a href="${escapeHtmlAttribute(href)}">${escapeHtmlAttribute(homeLink.label)}<span>${escapeHtmlAttribute(homeLink.subtitle)}</span></a>
|
|
247
|
-
|
|
248
|
-
|
|
845
|
+
<div class="multi-book-controls" ${CONTROLS_MARKER}>
|
|
846
|
+
<a class="multi-book-home" ${HOME_MARKER} href="${escapeHtmlAttribute(href)}">${escapeHtmlAttribute(homeLink.label)}<span>${escapeHtmlAttribute(homeLink.subtitle)}</span></a>
|
|
847
|
+
<button type="button" class="multi-book-copy-source" data-multi-book-source-copy>复制本书为纯文本</button>
|
|
848
|
+
<span class="multi-book-copy-status" data-multi-book-source-status aria-live="polite"></span>
|
|
849
|
+
</div>
|
|
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>
|
|
852
|
+
<script>
|
|
853
|
+
(function () {
|
|
854
|
+
var sourceElement = document.getElementById("multi-book-source-data");
|
|
855
|
+
var button = document.querySelector("[data-multi-book-source-copy]");
|
|
856
|
+
var status = document.querySelector("[data-multi-book-source-status]");
|
|
857
|
+
if (!sourceElement || !button) return;
|
|
858
|
+
|
|
859
|
+
var source = "";
|
|
860
|
+
var fallbackMode = false;
|
|
861
|
+
try {
|
|
862
|
+
source = JSON.parse(sourceElement.textContent || '""');
|
|
863
|
+
} catch (error) {
|
|
864
|
+
if (status) status.textContent = "纯文本数据读取失败";
|
|
865
|
+
button.disabled = true;
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function setStatus(message) {
|
|
870
|
+
if (status) status.textContent = message;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function openSourcePage() {
|
|
874
|
+
var blob = new Blob([source], { type: "text/plain;charset=utf-8" });
|
|
875
|
+
var url = URL.createObjectURL(blob);
|
|
876
|
+
window.open(url, "_blank", "noopener");
|
|
877
|
+
setTimeout(function () { URL.revokeObjectURL(url); }, 60000);
|
|
878
|
+
setStatus("已尝试打开纯文本页;如果没有出现,请允许弹出窗口后再试");
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
button.addEventListener("click", async function () {
|
|
882
|
+
if (fallbackMode) {
|
|
883
|
+
openSourcePage();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
if (!navigator.clipboard || !navigator.clipboard.writeText) throw new Error("clipboard unavailable");
|
|
889
|
+
await navigator.clipboard.writeText(source);
|
|
890
|
+
button.textContent = "已复制";
|
|
891
|
+
setStatus("");
|
|
892
|
+
setTimeout(function () { button.textContent = "复制本书为纯文本"; }, 1800);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
fallbackMode = true;
|
|
895
|
+
button.textContent = "打开纯文本页";
|
|
896
|
+
openSourcePage();
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
}());
|
|
900
|
+
</script>`;
|
|
901
|
+
const withReaderScript = `${controlsBlock}
|
|
902
|
+
<script>${readerScript()}</script>`;
|
|
903
|
+
return `${html.slice(0, insertAt)}${withReaderScript}${html.slice(insertAt)}`;
|
|
249
904
|
}
|
|
250
905
|
async function addHomeLinks(rootDir, books, homeLink) {
|
|
251
906
|
const catalog = path.join(rootDir, "build", "html", "catalog.html");
|
|
@@ -253,7 +908,9 @@ async function addHomeLinks(rootDir, books, homeLink) {
|
|
|
253
908
|
const htmlFile = path.join(book.htmlOutputDir, "book.html");
|
|
254
909
|
const html = await readFile(htmlFile, "utf8");
|
|
255
910
|
const href = path.relative(path.dirname(htmlFile), catalog);
|
|
256
|
-
|
|
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");
|
|
257
914
|
}
|
|
258
915
|
}
|
|
259
916
|
async function writeRootIndex(rootDir, rootIndex) {
|
|
@@ -394,7 +1051,6 @@ export async function buildWorkspace(rootDir = process.cwd()) {
|
|
|
394
1051
|
const useKroki = await workspaceUsesDiagrams(rootDir, books);
|
|
395
1052
|
const fetchDiagrams = shouldFetchDiagrams();
|
|
396
1053
|
const asciidoctor = createAsciidoctor(useKroki);
|
|
397
|
-
await buildReducedAdoc(rootDir, books, asciidoctor);
|
|
398
1054
|
await buildHtml(rootDir, books, asciidoctor, useKroki, fetchDiagrams);
|
|
399
1055
|
await copyAssets(rootDir, books);
|
|
400
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.
|
|
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/
|
|
55
|
+
"@asciidoctor/core": "3.0.4",
|
|
56
56
|
"@types/node": "24.10.1",
|
|
57
|
-
"
|
|
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
|
-
|
|
23
|
-
- `build/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` 进行交叉引用。
|