create-asciidoc-multi-book-workspace 0.1.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +202 -0
  3. package/README.md +93 -0
  4. package/dist/create.js +41 -0
  5. package/dist/init-workspace.js +58 -0
  6. package/dist/runtime/adoc-books.mjs +395 -0
  7. package/dist/template-manifest.js +11 -0
  8. package/package.json +55 -0
  9. package/templates/default-workspace/README.md +66 -0
  10. package/templates/default-workspace/books/00-book-anatomy/backmatter/appendix-a.adoc +18 -0
  11. package/templates/default-workspace/books/00-book-anatomy/backmatter/bibliography.adoc +4 -0
  12. package/templates/default-workspace/books/00-book-anatomy/backmatter/glossary.adoc +10 -0
  13. package/templates/default-workspace/books/00-book-anatomy/backmatter/index.adoc +9 -0
  14. package/templates/default-workspace/books/00-book-anatomy/book.adoc +54 -0
  15. package/templates/default-workspace/books/00-book-anatomy/frontmatter/abstract.adoc +5 -0
  16. package/templates/default-workspace/books/00-book-anatomy/frontmatter/acknowledgments.adoc +3 -0
  17. package/templates/default-workspace/books/00-book-anatomy/frontmatter/colophon.adoc +5 -0
  18. package/templates/default-workspace/books/00-book-anatomy/frontmatter/dedication.adoc +3 -0
  19. package/templates/default-workspace/books/00-book-anatomy/frontmatter/preface.adoc +5 -0
  20. package/templates/default-workspace/books/00-book-anatomy/parts/01-front-structure/01-frontmatter-map.adoc +24 -0
  21. package/templates/default-workspace/books/00-book-anatomy/parts/01-front-structure/_partintro.adoc +1 -0
  22. package/templates/default-workspace/books/00-book-anatomy/parts/02-body-structure/01-body-map.adoc +13 -0
  23. package/templates/default-workspace/books/00-book-anatomy/parts/02-body-structure/_partintro.adoc +1 -0
  24. package/templates/default-workspace/books/01-starter-book/assets/images/starter-map.svg +30 -0
  25. package/templates/default-workspace/books/01-starter-book/backmatter/appendix-a.adoc +9 -0
  26. package/templates/default-workspace/books/01-starter-book/backmatter/bibliography.adoc +3 -0
  27. package/templates/default-workspace/books/01-starter-book/book.adoc +26 -0
  28. package/templates/default-workspace/books/01-starter-book/chapters/01-opening.adoc +6 -0
  29. package/templates/default-workspace/books/01-starter-book/chapters/02-main-flow.adoc +5 -0
  30. package/templates/default-workspace/books/01-starter-book/frontmatter/preface.adoc +3 -0
  31. package/templates/default-workspace/books/02-multipart-monograph/backmatter/appendix-a.adoc +5 -0
  32. package/templates/default-workspace/books/02-multipart-monograph/backmatter/bibliography.adoc +3 -0
  33. package/templates/default-workspace/books/02-multipart-monograph/book.adoc +37 -0
  34. package/templates/default-workspace/books/02-multipart-monograph/parts/01-domain/01-problem-world.adoc +6 -0
  35. package/templates/default-workspace/books/02-multipart-monograph/parts/01-domain/02-boundary.adoc +5 -0
  36. package/templates/default-workspace/books/02-multipart-monograph/parts/01-domain/_partintro.adoc +2 -0
  37. package/templates/default-workspace/books/02-multipart-monograph/parts/02-specification/01-book-contract.adoc +5 -0
  38. package/templates/default-workspace/books/02-multipart-monograph/parts/02-specification/02-verification.adoc +5 -0
  39. package/templates/default-workspace/books/02-multipart-monograph/parts/02-specification/_partintro.adoc +1 -0
  40. package/templates/default-workspace/books/03-technical-book-workflow/assets/images/technical-flow.svg +30 -0
  41. package/templates/default-workspace/books/03-technical-book-workflow/backmatter/bibliography.adoc +3 -0
  42. package/templates/default-workspace/books/03-technical-book-workflow/book.adoc +25 -0
  43. package/templates/default-workspace/books/03-technical-book-workflow/chapters/01-build-flow.adoc +12 -0
  44. package/templates/default-workspace/books/03-technical-book-workflow/chapters/02-diagrams.adoc +19 -0
  45. package/templates/default-workspace/books/03-technical-book-workflow/chapters/03-code-includes.adoc +8 -0
  46. package/templates/default-workspace/books/03-technical-book-workflow/examples/minimal-tool.mjs +5 -0
  47. package/templates/default-workspace/books/03-technical-book-workflow/frontmatter/preface.adoc +3 -0
  48. package/templates/default-workspace/books/04-reference-manual/backmatter/bibliography.adoc +4 -0
  49. package/templates/default-workspace/books/04-reference-manual/backmatter/glossary.adoc +8 -0
  50. package/templates/default-workspace/books/04-reference-manual/book.adoc +30 -0
  51. package/templates/default-workspace/books/04-reference-manual/chapters/01-decision-table.adoc +27 -0
  52. package/templates/default-workspace/books/04-reference-manual/chapters/02-path-rules.adoc +14 -0
  53. package/templates/default-workspace/books/04-reference-manual/chapters/03-xref-patterns.adoc +20 -0
  54. package/templates/default-workspace/books/04-reference-manual/chapters/04-growth-paths.adoc +8 -0
  55. package/templates/default-workspace/books/04-reference-manual/frontmatter/preface.adoc +3 -0
  56. package/templates/default-workspace/books/05-upper-volume/backmatter/bibliography.adoc +3 -0
  57. package/templates/default-workspace/books/05-upper-volume/book.adoc +23 -0
  58. package/templates/default-workspace/books/05-upper-volume/chapters/01-upper-opening.adoc +4 -0
  59. package/templates/default-workspace/books/05-upper-volume/chapters/02-upper-core-model.adoc +4 -0
  60. package/templates/default-workspace/books/05-upper-volume/frontmatter/preface.adoc +3 -0
  61. package/templates/default-workspace/books/06-lower-volume/backmatter/bibliography.adoc +3 -0
  62. package/templates/default-workspace/books/06-lower-volume/book.adoc +24 -0
  63. package/templates/default-workspace/books/06-lower-volume/chapters/01-lower-opening.adoc +4 -0
  64. package/templates/default-workspace/books/06-lower-volume/chapters/02-cross-volume-practice.adoc +11 -0
  65. package/templates/default-workspace/books/06-lower-volume/frontmatter/preface.adoc +3 -0
  66. package/templates/default-workspace/catalog.adoc +68 -0
  67. package/templates/default-workspace/gitignore.template +8 -0
  68. package/templates/default-workspace/package.json.template +19 -0
  69. package/templates/default-workspace/shared/attributes.adoc +2 -0
  70. package/templates/default-workspace/shared/images/workspace-map.svg +23 -0
@@ -0,0 +1,395 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const require = createRequire(import.meta.url);
7
+ 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
+ const CATALOG_BOOK_XREF_PATTERN = /xref:books\/([^/\]]+)\/book\.adoc(?:#[^\[]+)?\[/g;
9
+ const XREF_PATTERN = /xref:([^\[#]+)(?:#([A-Za-z0-9_-]+))?\[/g;
10
+ const ANCHOR_PATTERN = /^\[#([A-Za-z0-9_-]+)\]$/gm;
11
+ const LOCAL_TARGET_PATTERN = /\b(?:href|src)="([^"]+)"/g;
12
+ const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
13
+ const HOME_MARKER = "data-multi-book-home";
14
+ const FETCH_DIAGRAMS_ENV = "ADOC_BOOKS_FETCH_DIAGRAMS";
15
+ async function existsFile(filePath) {
16
+ try {
17
+ const stats = await stat(filePath);
18
+ return stats.isFile();
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function existsDir(dir) {
25
+ try {
26
+ const stats = await stat(dir);
27
+ return stats.isDirectory();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function collectFiles(dir, predicate) {
34
+ const entries = await readdir(dir, { withFileTypes: true });
35
+ const files = [];
36
+ for (const entry of entries) {
37
+ const entryPath = path.join(dir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ files.push(...await collectFiles(entryPath, predicate));
40
+ }
41
+ else if (entry.isFile() && predicate(entryPath)) {
42
+ files.push(entryPath);
43
+ }
44
+ }
45
+ return files.sort();
46
+ }
47
+ async function discoverBooks(rootDir) {
48
+ const booksDir = path.join(rootDir, "books");
49
+ const entries = await readdir(booksDir, { withFileTypes: true });
50
+ const books = [];
51
+ for (const entry of entries) {
52
+ if (!entry.isDirectory())
53
+ continue;
54
+ const bookDir = path.join(booksDir, entry.name);
55
+ const input = path.join(bookDir, "book.adoc");
56
+ if (await existsFile(input)) {
57
+ books.push({
58
+ bookId: entry.name,
59
+ bookDir,
60
+ input,
61
+ htmlOutputDir: path.join(rootDir, "build", "html", "books", entry.name)
62
+ });
63
+ }
64
+ }
65
+ return books.sort((a, b) => a.bookId.localeCompare(b.bookId));
66
+ }
67
+ async function readIfExists(filePath) {
68
+ return await existsFile(filePath) ? readFile(filePath, "utf8") : "";
69
+ }
70
+ async function combinedBookSource(bookDir) {
71
+ const files = await collectFiles(bookDir, (filePath) => filePath.endsWith(".adoc"));
72
+ const sources = [];
73
+ for (const file of files)
74
+ sources.push(await readFile(file, "utf8"));
75
+ return sources.join("\n");
76
+ }
77
+ async function workspaceUsesDiagrams(rootDir, books) {
78
+ const sources = [await readIfExists(path.join(rootDir, "catalog.adoc"))];
79
+ for (const book of books)
80
+ sources.push(await combinedBookSource(book.bookDir));
81
+ return sources.some((source) => DIAGRAM_BLOCK_PATTERN.test(source));
82
+ }
83
+ function createAsciidoctor(loadKroki) {
84
+ const asciidoctor = require("asciidoctor")();
85
+ require("@asciidoctor/reducer").register();
86
+ if (loadKroki) {
87
+ try {
88
+ require("asciidoctor-kroki").register(asciidoctor.Extensions);
89
+ }
90
+ catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ throw new Error(`current workspace contains diagram blocks, but asciidoctor-kroki could not be loaded: ${message}`);
93
+ }
94
+ }
95
+ return asciidoctor;
96
+ }
97
+ function shouldFetchDiagrams() {
98
+ const value = process.env[FETCH_DIAGRAMS_ENV];
99
+ return value === "1" || value === "true";
100
+ }
101
+ function reduceAdocSource(asciidoctor, input) {
102
+ const doc = asciidoctor.loadFile(input, { safe: "unsafe" });
103
+ const source = doc.getSource();
104
+ if (/^[ \t]*include::/m.test(source)) {
105
+ throw new Error(`reducer left unresolved include directives in ${input}`);
106
+ }
107
+ return source;
108
+ }
109
+ async function buildReducedAdoc(rootDir, books, asciidoctor) {
110
+ const adocDir = path.join(rootDir, "build", "adoc");
111
+ await rm(adocDir, { force: true, recursive: true });
112
+ await mkdir(path.join(adocDir, "books"), { recursive: true });
113
+ const catalogSource = reduceAdocSource(asciidoctor, path.join(rootDir, "catalog.adoc"));
114
+ await writeFile(path.join(adocDir, "catalog.adoc"), catalogSource, "utf8");
115
+ for (const book of books) {
116
+ const source = reduceAdocSource(asciidoctor, book.input);
117
+ await writeFile(path.join(adocDir, "books", `${book.bookId}.adoc`), source, "utf8");
118
+ }
119
+ }
120
+ async function pruneStaleBookHtmlDirs(rootDir, books) {
121
+ const htmlBooksDir = path.join(rootDir, "build", "html", "books");
122
+ if (!await existsDir(htmlBooksDir))
123
+ return;
124
+ const current = new Set(books.map((book) => book.bookId));
125
+ const entries = await readdir(htmlBooksDir, { withFileTypes: true });
126
+ for (const entry of entries) {
127
+ if (entry.isDirectory() && !current.has(entry.name)) {
128
+ await rm(path.join(htmlBooksDir, entry.name), { force: true, recursive: true });
129
+ }
130
+ }
131
+ }
132
+ function convertFile(asciidoctor, input, outputFile, baseDir, attributes = {}) {
133
+ asciidoctor.convertFile(input, {
134
+ safe: "unsafe",
135
+ base_dir: baseDir,
136
+ to_file: outputFile,
137
+ mkdirs: true,
138
+ attributes
139
+ });
140
+ }
141
+ async function buildHtml(rootDir, books, asciidoctor, useKroki, fetchDiagrams) {
142
+ await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
143
+ convertFile(asciidoctor, path.join(rootDir, "catalog.adoc"), path.join(rootDir, "build", "html", "catalog.html"), rootDir);
144
+ await pruneStaleBookHtmlDirs(rootDir, books);
145
+ for (const book of books) {
146
+ await mkdir(book.htmlOutputDir, { recursive: true });
147
+ convertFile(asciidoctor, book.input, path.join(book.htmlOutputDir, "book.html"), book.bookDir, {
148
+ ...(useKroki && fetchDiagrams ? { "kroki-fetch-diagram": "", "kroki-http-method": "post" } : {})
149
+ });
150
+ }
151
+ }
152
+ async function copyAssets(rootDir, books) {
153
+ const sharedImages = path.join(rootDir, "shared", "images");
154
+ if (await existsDir(sharedImages)) {
155
+ await rm(path.join(rootDir, "build", "html", "shared", "images"), { force: true, recursive: true });
156
+ await cp(sharedImages, path.join(rootDir, "build", "html", "shared", "images"), { recursive: true });
157
+ }
158
+ for (const book of books) {
159
+ const assetsDir = path.join(book.bookDir, "assets");
160
+ if (await existsDir(assetsDir)) {
161
+ await rm(path.join(book.htmlOutputDir, "assets"), { force: true, recursive: true });
162
+ await cp(assetsDir, path.join(book.htmlOutputDir, "assets"), { recursive: true });
163
+ }
164
+ }
165
+ }
166
+ function escapeHtmlAttribute(value) {
167
+ return value
168
+ .replaceAll("&", "&")
169
+ .replaceAll('"', """)
170
+ .replaceAll("<", "&lt;")
171
+ .replaceAll(">", "&gt;");
172
+ }
173
+ function addHomeLinkToBookHtml(html, href) {
174
+ if (html.includes(HOME_MARKER))
175
+ return html;
176
+ const marker = '<div id="toc" class="toc2">';
177
+ const index = html.indexOf(marker);
178
+ if (index === -1)
179
+ throw new Error("book HTML is missing the left TOC container");
180
+ const insertAt = index + marker.length;
181
+ const homeBlock = `
182
+ <style>
183
+ .multi-book-home {
184
+ margin: 0 0 1rem;
185
+ padding-bottom: .75rem;
186
+ border-bottom: 1px solid #e5e7eb;
187
+ }
188
+ .multi-book-home a {
189
+ color: #1f2937;
190
+ display: block;
191
+ font-weight: 600;
192
+ line-height: 1.35;
193
+ text-decoration: none;
194
+ }
195
+ .multi-book-home a:hover {
196
+ color: #0f766e;
197
+ text-decoration: underline;
198
+ }
199
+ .multi-book-home span {
200
+ color: #64748b;
201
+ display: block;
202
+ font-size: .78rem;
203
+ font-weight: 400;
204
+ margin-top: .15rem;
205
+ }
206
+ </style>
207
+ <div class="multi-book-home" ${HOME_MARKER}>
208
+ <a href="${escapeHtmlAttribute(href)}">Back to catalog<span>AsciiDoc multi-book workspace</span></a>
209
+ </div>`;
210
+ return `${html.slice(0, insertAt)}${homeBlock}${html.slice(insertAt)}`;
211
+ }
212
+ async function addHomeLinks(rootDir, books) {
213
+ const catalog = path.join(rootDir, "build", "html", "catalog.html");
214
+ for (const book of books) {
215
+ const htmlFile = path.join(book.htmlOutputDir, "book.html");
216
+ const html = await readFile(htmlFile, "utf8");
217
+ const href = path.relative(path.dirname(htmlFile), catalog);
218
+ await writeFile(htmlFile, addHomeLinkToBookHtml(html, href), "utf8");
219
+ }
220
+ }
221
+ async function writeRootIndex(rootDir) {
222
+ const outputDir = path.join(rootDir, "build", "html");
223
+ await mkdir(outputDir, { recursive: true });
224
+ await writeFile(path.join(outputDir, "index.html"), `<!doctype html>
225
+ <html lang="en">
226
+ <head>
227
+ <meta charset="utf-8">
228
+ <meta http-equiv="refresh" content="0; url=catalog.html">
229
+ <title>AsciiDoc Multi-Book Workspace</title>
230
+ </head>
231
+ <body>
232
+ <p><a href="catalog.html">catalog.html</a></p>
233
+ </body>
234
+ </html>
235
+ `, "utf8");
236
+ }
237
+ function extractCatalogBookIds(catalogSource) {
238
+ const ids = new Set();
239
+ for (const match of catalogSource.matchAll(CATALOG_BOOK_XREF_PATTERN))
240
+ ids.add(match[1]);
241
+ return [...ids].sort((a, b) => a.localeCompare(b));
242
+ }
243
+ function explicitAnchors(source) {
244
+ const anchors = new Set();
245
+ for (const match of source.matchAll(ANCHOR_PATTERN))
246
+ anchors.add(match[1]);
247
+ return anchors;
248
+ }
249
+ function attributesFromBookSource(bookSource) {
250
+ const attributes = new Map();
251
+ for (const line of bookSource.split(/\r?\n/)) {
252
+ const match = line.match(/^:([A-Za-z0-9_-]+):\s+(.+)$/);
253
+ if (match)
254
+ attributes.set(match[1], match[2]);
255
+ }
256
+ return attributes;
257
+ }
258
+ function expandXrefTarget(target, attributes) {
259
+ return target.replaceAll(/\{([A-Za-z0-9_-]+)\}/g, (_, name) => attributes.get(name) ?? `{${name}}`);
260
+ }
261
+ function issue(code, detail) {
262
+ return { code, detail };
263
+ }
264
+ async function workspaceContractIssues(rootDir, books) {
265
+ const issues = [];
266
+ const catalogSource = await readIfExists(path.join(rootDir, "catalog.adoc"));
267
+ const catalogBookIds = extractCatalogBookIds(catalogSource);
268
+ const bookSet = new Set(books.map((book) => book.bookId));
269
+ const catalogBookSet = new Set(catalogBookIds);
270
+ for (const bookId of catalogBookIds) {
271
+ if (!bookSet.has(bookId))
272
+ issues.push(issue("CATALOG_TARGET_MISSING", bookId));
273
+ }
274
+ for (const book of books) {
275
+ if (!catalogBookSet.has(book.bookId))
276
+ issues.push(issue("BOOK_MISSING_FROM_CATALOG", book.bookId));
277
+ }
278
+ for (const book of books) {
279
+ const bookSource = await readFile(book.input, "utf8");
280
+ const allSource = await combinedBookSource(book.bookDir);
281
+ const attributes = attributesFromBookSource(bookSource);
282
+ if (!/^:doctype:\s+book$/m.test(bookSource))
283
+ issues.push(issue("MISSING_DOCTYPE", book.bookId));
284
+ for (const match of allSource.matchAll(XREF_PATTERN)) {
285
+ const [, target, anchor] = match;
286
+ if (/^https?:/.test(target))
287
+ continue;
288
+ const expandedTarget = expandXrefTarget(target, attributes);
289
+ if (!expandedTarget.endsWith(".adoc"))
290
+ continue;
291
+ const resolved = path.resolve(book.bookDir, expandedTarget);
292
+ if (!await existsFile(resolved)) {
293
+ issues.push(issue("XREF_TARGET_MISSING", `${book.bookId} -> ${expandedTarget}`));
294
+ if (anchor)
295
+ issues.push(issue("MISSING_ANCHOR", `${book.bookId} -> ${expandedTarget}#${anchor}`));
296
+ continue;
297
+ }
298
+ if (anchor) {
299
+ const targetSource = await combinedBookSource(path.dirname(resolved));
300
+ if (!explicitAnchors(targetSource).has(anchor)) {
301
+ issues.push(issue("MISSING_ANCHOR", `${book.bookId} -> ${expandedTarget}#${anchor}`));
302
+ }
303
+ }
304
+ }
305
+ }
306
+ return issues;
307
+ }
308
+ async function collectHtmlFiles(dir) {
309
+ return collectFiles(dir, (filePath) => filePath.endsWith(".html"));
310
+ }
311
+ function extractLocalTargets(html) {
312
+ const targets = [];
313
+ for (const match of html.matchAll(LOCAL_TARGET_PATTERN)) {
314
+ const rawTarget = match[1];
315
+ if (rawTarget === "" || rawTarget.startsWith("#") || rawTarget.startsWith("//") || SCHEME_PATTERN.test(rawTarget)) {
316
+ continue;
317
+ }
318
+ const targetWithoutFragment = rawTarget.split("#", 1)[0].split("?", 1)[0];
319
+ if (targetWithoutFragment !== "")
320
+ targets.push(rawTarget);
321
+ }
322
+ return targets;
323
+ }
324
+ async function missingLocalResources(rootDir) {
325
+ const htmlDir = path.join(rootDir, "build", "html");
326
+ const htmlFiles = await collectHtmlFiles(htmlDir);
327
+ const issues = [];
328
+ for (const htmlFile of htmlFiles) {
329
+ const html = await readFile(htmlFile, "utf8");
330
+ for (const target of extractLocalTargets(html)) {
331
+ const targetPath = target.split("#", 1)[0].split("?", 1)[0];
332
+ const resolved = path.resolve(path.dirname(htmlFile), targetPath);
333
+ if (!await existsFile(resolved)) {
334
+ issues.push(issue("HTML_RESOURCE_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target}`));
335
+ }
336
+ }
337
+ }
338
+ return issues;
339
+ }
340
+ async function assertNoIssues(label, issues) {
341
+ if (issues.length === 0)
342
+ return;
343
+ for (const entry of issues)
344
+ console.error(`${entry.code}: ${entry.detail}`);
345
+ throw new Error(`${label} failed with ${issues.length} issue(s)`);
346
+ }
347
+ export async function buildWorkspace(rootDir = process.cwd()) {
348
+ const catalog = path.join(rootDir, "catalog.adoc");
349
+ if (!await existsFile(catalog))
350
+ throw new Error(`missing catalog.adoc in ${rootDir}`);
351
+ const books = await discoverBooks(rootDir);
352
+ if (books.length === 0)
353
+ throw new Error(`missing book.adoc entries in ${path.join(rootDir, "books")}`);
354
+ const useKroki = await workspaceUsesDiagrams(rootDir, books);
355
+ const fetchDiagrams = shouldFetchDiagrams();
356
+ const asciidoctor = createAsciidoctor(useKroki);
357
+ await buildReducedAdoc(rootDir, books, asciidoctor);
358
+ await buildHtml(rootDir, books, asciidoctor, useKroki, fetchDiagrams);
359
+ await copyAssets(rootDir, books);
360
+ await addHomeLinks(rootDir, books);
361
+ await writeRootIndex(rootDir);
362
+ await assertNoIssues("HTML local resource check", await missingLocalResources(rootDir));
363
+ await assertNoIssues("workspace contract check", await workspaceContractIssues(rootDir, books));
364
+ }
365
+ export async function checkWorkspace(rootDir = process.cwd()) {
366
+ await buildWorkspace(rootDir);
367
+ }
368
+ export async function cleanWorkspace(rootDir = process.cwd()) {
369
+ await rm(path.join(rootDir, "build"), { force: true, recursive: true });
370
+ }
371
+ async function main() {
372
+ const command = process.argv[2];
373
+ if (command === "build") {
374
+ await buildWorkspace();
375
+ return;
376
+ }
377
+ if (command === "check") {
378
+ await checkWorkspace();
379
+ return;
380
+ }
381
+ if (command === "clean") {
382
+ await cleanWorkspace();
383
+ return;
384
+ }
385
+ console.error("Usage: node tools/adoc-books.mjs <build|check|clean>");
386
+ process.exitCode = 1;
387
+ }
388
+ const executedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
389
+ const modulePath = fileURLToPath(import.meta.url);
390
+ if (executedPath === modulePath) {
391
+ main().catch((error) => {
392
+ console.error(error instanceof Error ? error.message : error);
393
+ process.exitCode = 1;
394
+ });
395
+ }
@@ -0,0 +1,11 @@
1
+ export const defaultWorkspaceTemplate = "default-workspace";
2
+ export const templatePackageNamePlaceholder = "{{name}}";
3
+ export const fixedTemplateOutputPaths = [
4
+ "README.md",
5
+ "catalog.adoc",
6
+ "package.json",
7
+ ".gitignore",
8
+ "tools/adoc-books.mjs",
9
+ "books",
10
+ "shared"
11
+ ];
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "create-asciidoc-multi-book-workspace",
3
+ "version": "0.1.0",
4
+ "description": "Create a complete AsciiDoc multi-book workspace.",
5
+ "license": "Apache-2.0",
6
+ "author": {
7
+ "name": "弥澄亮",
8
+ "email": "t103ooooo@stu.mju.edu.cn",
9
+ "url": "https://github.com/MichengLiang"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/MichengLiang/asciidoc-multi-book-workspace.git"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/MichengLiang/asciidoc-multi-book-workspace/issues"
17
+ },
18
+ "homepage": "https://michengliang.github.io/asciidoc-multi-book-workspace/",
19
+ "type": "module",
20
+ "keywords": [
21
+ "asciidoc",
22
+ "asciidoctor",
23
+ "books",
24
+ "create",
25
+ "initializer",
26
+ "multi-book"
27
+ ],
28
+ "bin": {
29
+ "create-asciidoc-multi-book-workspace": "dist/create.js"
30
+ },
31
+ "files": [
32
+ "dist/",
33
+ "templates/",
34
+ "README.md",
35
+ "CHANGELOG.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=20.19.0"
40
+ },
41
+ "devDependencies": {
42
+ "@asciidoctor/reducer": "1.1.2",
43
+ "@types/node": "24.10.1",
44
+ "asciidoctor": "2.2.9",
45
+ "asciidoctor-kroki": "0.18.1",
46
+ "typescript": "5.9.3"
47
+ },
48
+ "scripts": {
49
+ "build": "tsc -p tsconfig.json",
50
+ "test": "pnpm run build && node --test test/*.test.mjs",
51
+ "pack:check": "pnpm pack --dry-run",
52
+ "check": "pnpm run test && pnpm run pack:check",
53
+ "clean": "rm -rf dist build"
54
+ }
55
+ }
@@ -0,0 +1,66 @@
1
+ # My Books
2
+
3
+ This is an AsciiDoc multi-book workspace. It starts with seven sample books that show common ways to organize independent books, multipart books, technical books, reference material, and split volumes.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm install
9
+ ```
10
+
11
+ ## Build
12
+
13
+ ```bash
14
+ pnpm run build
15
+ ```
16
+
17
+ Outputs:
18
+
19
+ - `build/html/index.html`
20
+ - `build/html/catalog.html`
21
+ - `build/html/books/<book-id>/book.html`
22
+ - `build/adoc/catalog.adoc`
23
+ - `build/adoc/books/<book-id>.adoc`
24
+
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
+ To fetch diagram images into the local HTML output, run:
27
+
28
+ ```bash
29
+ ADOC_BOOKS_FETCH_DIAGRAMS=1 pnpm run build
30
+ ```
31
+
32
+ ## Check
33
+
34
+ ```bash
35
+ pnpm run check
36
+ ```
37
+
38
+ This checks the current catalog, existing books, book doctypes, cross-book xrefs, anchors, and local HTML resources.
39
+
40
+ ## Clean
41
+
42
+ ```bash
43
+ pnpm run clean
44
+ ```
45
+
46
+ This removes `build/`.
47
+
48
+ ## Delete Sample Books
49
+
50
+ Delete `books/<book-id>/`, then remove the matching `xref:books/<book-id>/book.adoc[...]` entry from `catalog.adoc`. Each default sample is independent, so no other sample book needs to be edited.
51
+
52
+ Run:
53
+
54
+ ```bash
55
+ pnpm run check
56
+ ```
57
+
58
+ ## Add A Book
59
+
60
+ Copy `books/01-starter-book` to `books/<your-book-id>`, edit `book.adoc` and chapter files, then add the new book to `catalog.adoc`.
61
+
62
+ To look at the original samples again, initialize a temporary workspace:
63
+
64
+ ```bash
65
+ pnpm create asciidoc-multi-book-workspace sample-look
66
+ ```
@@ -0,0 +1,18 @@
1
+ = 附录 A:结构速查
2
+
3
+ [cols="1,2", options="header"]
4
+ |===
5
+ |写法 |含义
6
+
7
+ |`= 书名`
8
+ |document title。
9
+
10
+ |`= 第一部`
11
+ |book doctype 中的 part。
12
+
13
+ |`== 章`
14
+ |chapter。
15
+
16
+ |`=== 节`
17
+ |section。
18
+ |===
@@ -0,0 +1,4 @@
1
+ = 参考资料
2
+
3
+ * [[[asciidoctor-section-styles]]] Asciidoctor Docs, Section Styles for Articles and Books, https://docs.asciidoctor.org/asciidoc/latest/sections/styles/
4
+ * [[[asciidoctor-parts]]] Asciidoctor Docs, Book Parts, https://docs.asciidoctor.org/asciidoc/latest/sections/parts/
@@ -0,0 +1,10 @@
1
+ = 术语表
2
+
3
+ book:: 具有独立 `book.adoc`、章节顺序和构建产物的书籍单位。
4
+ part:: 同一本 book 内部的 level 0 大分部。
5
+ chapter:: book 中的主要正文单位,通常使用 `==` 标题。
6
+ section:: chapter 内部的层级标题。
7
+ appendix:: 放置补充材料的后置章节。
8
+ glossary:: 使用定义列表解释术语的后置章节。
9
+ bibliography:: 放置参考资料的后置章节。
10
+ index:: 放置索引入口的后置章节。
@@ -0,0 +1,9 @@
1
+ = 索引
2
+
3
+ 索引 section 位于书籍后置区域。正文中的索引词宏是索引词来源:
4
+
5
+ [source,asciidoc]
6
+ ----
7
+ (((section)))
8
+ (((discrete heading)))
9
+ ----
@@ -0,0 +1,54 @@
1
+ = 完整书籍结构标本
2
+ 作者 <author@example.com>
3
+ v0.1, 2026-05
4
+ :doctype: book
5
+ :toc: left
6
+ :toclevels: 3
7
+ :sectnums:
8
+ :icons: font
9
+ :experimental:
10
+ :idprefix:
11
+ :idseparator: -
12
+
13
+ include::../../shared/attributes.adoc[]
14
+
15
+ [abstract]
16
+ include::frontmatter/abstract.adoc[]
17
+
18
+ [colophon]
19
+ include::frontmatter/colophon.adoc[]
20
+
21
+ [dedication]
22
+ include::frontmatter/dedication.adoc[]
23
+
24
+ [preface]
25
+ include::frontmatter/preface.adoc[]
26
+
27
+ [acknowledgments]
28
+ include::frontmatter/acknowledgments.adoc[]
29
+
30
+ = 第一部:前置结构
31
+
32
+ [partintro]
33
+ include::parts/01-front-structure/_partintro.adoc[]
34
+
35
+ include::parts/01-front-structure/01-frontmatter-map.adoc[]
36
+
37
+ = 第二部:正文结构
38
+
39
+ [partintro]
40
+ include::parts/02-body-structure/_partintro.adoc[]
41
+
42
+ include::parts/02-body-structure/01-body-map.adoc[]
43
+
44
+ [appendix]
45
+ include::backmatter/appendix-a.adoc[]
46
+
47
+ [glossary]
48
+ include::backmatter/glossary.adoc[]
49
+
50
+ [bibliography]
51
+ include::backmatter/bibliography.adoc[]
52
+
53
+ [index]
54
+ include::backmatter/index.adoc[]
@@ -0,0 +1,5 @@
1
+ == 摘要
2
+
3
+ 本书是 AsciiDoc book 的结构标本。它集中展示前置内容、正文分部、章节层级、附录、术语表、参考资料和索引在书稿中的位置。
4
+
5
+ 本书用于学习完整结构,不作为日常新书的复制起点。
@@ -0,0 +1,3 @@
1
+ == 致谢
2
+
3
+ 感谢 Asciidoctor 文档提供稳定术语和结构坐标。
@@ -0,0 +1,5 @@
1
+ == 版本说明
2
+
3
+ 本文档属于 `{series-name}`。colophon 位于书籍前置区域,记录版本、来源或出版信息。
4
+
5
+ 示例版本:v0.1。
@@ -0,0 +1,3 @@
1
+ == 献辞
2
+
3
+ 献给每一位需要把书稿结构先想清楚再开始写作的人。
@@ -0,0 +1,5 @@
1
+ == 前言
2
+
3
+ 这本标本书按结构位置组织,而不是按主题展开。阅读时先看 `book.adoc`,再对照各个被 include 的文件。
4
+
5
+ 日常写作起点可以参考样本 `01-starter-book`。
@@ -0,0 +1,24 @@
1
+ [#frontmatter-map]
2
+ == 前置内容地图
3
+
4
+ 前置内容用于建立读者进入正文之前需要知道的说明、来源和阅读位置。
5
+
6
+ [cols="1,2", options="header"]
7
+ |===
8
+ |结构 |职责
9
+
10
+ |`[abstract]`
11
+ |说明整本书覆盖什么。
12
+
13
+ |`[colophon]`
14
+ |记录版本、出版或来源信息。
15
+
16
+ |`[dedication]`
17
+ |放置献辞。
18
+
19
+ |`[preface]`
20
+ |说明写作目的和阅读方式。
21
+
22
+ |`[acknowledgments]`
23
+ |放置致谢。
24
+ |===