create-asciidoc-multi-book-workspace 0.1.13 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 - 2026-06-23
4
+
5
+ ### Added
6
+
7
+ - Make generated workspace builds fail when Asciidoctor reports parser diagnostics, including missing `include::` targets that previously only appeared on stderr.
8
+ - Extend generated HTML validation to fail on missing same-page and cross-page fragment anchors, not just missing local files.
9
+
10
+ ### Fixed
11
+
12
+ - Escape quick-reference `xref` syntax examples in the structured writing sample so example text does not render as broken links.
13
+
3
14
  ## 0.1.13 - 2026-06-21
4
15
 
5
16
  ### Changed
@@ -10,7 +10,8 @@ const DIAGRAM_BLOCK_PATTERN = /^\[(?:actdiag|blockdiag|bpmn|bytefield|c4plantuml
10
10
  const CATALOG_BOOK_XREF_PATTERN = /xref:books\/([^/\]]+)\/book\.adoc(?:#[^\[]+)?\[/g;
11
11
  const XREF_PATTERN = /xref:([^\[#]+)(?:#([A-Za-z0-9_-]+))?\[/g;
12
12
  const ANCHOR_PATTERN = /^\[#([A-Za-z0-9_-]+)(?:[.,][^\]]*)?\]$/gm;
13
- const LOCAL_TARGET_PATTERN = /\b(?:href|src)="([^"]+)"/g;
13
+ const LOCAL_TARGET_PATTERN = /\b(href|src)="([^"]+)"/g;
14
+ const HTML_ANCHOR_PATTERN = /\b(?:id|name)="([^"]+)"/g;
14
15
  const SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
15
16
  const HOME_MARKER = "data-multi-book-home";
16
17
  const CONTROLS_MARKER = "data-multi-book-controls";
@@ -153,14 +154,54 @@ async function pruneStaleBookHtmlDirs(rootDir, books) {
153
154
  }
154
155
  }
155
156
  }
157
+ function messageText(message) {
158
+ if (typeof message?.getText === "function")
159
+ return message.getText();
160
+ if (typeof message?.getMessage === "function")
161
+ return message.getMessage();
162
+ return String(message);
163
+ }
164
+ function messageLocation(message) {
165
+ const sourceLocation = typeof message?.getSourceLocation === "function" ? message.getSourceLocation() : undefined;
166
+ if (!sourceLocation)
167
+ return "";
168
+ const sourcePath = typeof sourceLocation.getPath === "function" ? sourceLocation.getPath() : undefined;
169
+ const lineNumber = typeof sourceLocation.getLineNumber === "function" ? sourceLocation.getLineNumber() : undefined;
170
+ if (sourcePath && lineNumber)
171
+ return `${sourcePath}: line ${lineNumber}`;
172
+ if (sourcePath)
173
+ return sourcePath;
174
+ if (lineNumber)
175
+ return `line ${lineNumber}`;
176
+ return "";
177
+ }
178
+ function formatAsciidoctorMessage(message) {
179
+ const severity = typeof message?.getSeverity === "function" ? message.getSeverity() : "UNKNOWN";
180
+ const location = messageLocation(message);
181
+ const text = messageText(message);
182
+ return location ? `${severity}: ${location}: ${text}` : `${severity}: ${text}`;
183
+ }
156
184
  function convertFile(asciidoctor, input, outputFile, baseDir, attributes = {}) {
157
- asciidoctor.convertFile(input, {
158
- safe: "unsafe",
159
- base_dir: baseDir,
160
- to_file: outputFile,
161
- mkdirs: true,
162
- attributes
163
- });
185
+ const loggerManager = asciidoctor.LoggerManager;
186
+ const defaultLogger = loggerManager.getLogger();
187
+ const memoryLogger = asciidoctor.MemoryLogger.create();
188
+ loggerManager.setLogger(memoryLogger);
189
+ try {
190
+ asciidoctor.convertFile(input, {
191
+ safe: "unsafe",
192
+ base_dir: baseDir,
193
+ to_file: outputFile,
194
+ mkdirs: true,
195
+ attributes
196
+ });
197
+ }
198
+ finally {
199
+ loggerManager.setLogger(defaultLogger);
200
+ }
201
+ const messages = memoryLogger.getMessages();
202
+ if (messages.length > 0) {
203
+ throw new Error(`AsciiDoc strict check failed for ${input}\n${messages.map(formatAsciidoctorMessage).join("\n")}`);
204
+ }
164
205
  }
165
206
  async function buildHtml(rootDir, books, asciidoctor, useKroki, fetchDiagrams) {
166
207
  await mkdir(path.join(rootDir, "build", "html"), { recursive: true });
@@ -1007,27 +1048,60 @@ async function collectHtmlFiles(dir) {
1007
1048
  function extractLocalTargets(html) {
1008
1049
  const targets = [];
1009
1050
  for (const match of html.matchAll(LOCAL_TARGET_PATTERN)) {
1010
- const rawTarget = match[1];
1011
- if (rawTarget === "" || rawTarget.startsWith("#") || rawTarget.startsWith("//") || SCHEME_PATTERN.test(rawTarget)) {
1051
+ const [, attribute, rawTarget] = match;
1052
+ if (rawTarget === "" || rawTarget === "#" || rawTarget.startsWith("//") || SCHEME_PATTERN.test(rawTarget)) {
1012
1053
  continue;
1013
1054
  }
1014
1055
  const targetWithoutFragment = rawTarget.split("#", 1)[0].split("?", 1)[0];
1015
- if (targetWithoutFragment !== "")
1016
- targets.push(rawTarget);
1056
+ const fragment = rawTarget.includes("#") ? rawTarget.split("#", 2)[1].split("?", 1)[0] : "";
1057
+ if (targetWithoutFragment !== "" || fragment !== "")
1058
+ targets.push({ attribute, rawTarget });
1017
1059
  }
1018
1060
  return targets;
1019
1061
  }
1062
+ function decodeUrlFragment(fragment) {
1063
+ try {
1064
+ return decodeURIComponent(fragment);
1065
+ }
1066
+ catch {
1067
+ return fragment;
1068
+ }
1069
+ }
1070
+ function htmlAnchors(html) {
1071
+ const anchors = new Set();
1072
+ for (const match of html.matchAll(HTML_ANCHOR_PATTERN))
1073
+ anchors.add(match[1]);
1074
+ return anchors;
1075
+ }
1076
+ async function anchorsForHtmlFile(filePath, cache) {
1077
+ const cached = cache.get(filePath);
1078
+ if (cached)
1079
+ return cached;
1080
+ const anchors = htmlAnchors(await readFile(filePath, "utf8"));
1081
+ cache.set(filePath, anchors);
1082
+ return anchors;
1083
+ }
1020
1084
  async function missingLocalResources(rootDir) {
1021
1085
  const htmlDir = path.join(rootDir, "build", "html");
1022
1086
  const htmlFiles = await collectHtmlFiles(htmlDir);
1023
1087
  const issues = [];
1088
+ const anchorCache = new Map();
1024
1089
  for (const htmlFile of htmlFiles) {
1025
1090
  const html = await readFile(htmlFile, "utf8");
1026
1091
  for (const target of extractLocalTargets(html)) {
1027
- const targetPath = target.split("#", 1)[0].split("?", 1)[0];
1028
- const resolved = path.resolve(path.dirname(htmlFile), targetPath);
1029
- if (!await existsFile(resolved)) {
1030
- issues.push(issue("HTML_RESOURCE_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target}`));
1092
+ const targetPath = target.rawTarget.split("#", 1)[0].split("?", 1)[0];
1093
+ const fragment = target.rawTarget.includes("#") ? target.rawTarget.split("#", 2)[1].split("?", 1)[0] : "";
1094
+ const resolved = targetPath === "" ? htmlFile : path.resolve(path.dirname(htmlFile), targetPath);
1095
+ if (targetPath !== "" && !await existsFile(resolved)) {
1096
+ issues.push(issue("HTML_RESOURCE_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target.rawTarget}`));
1097
+ continue;
1098
+ }
1099
+ if (target.attribute === "href" && fragment !== "" && (targetPath === "" || resolved.endsWith(".html"))) {
1100
+ const anchors = await anchorsForHtmlFile(resolved, anchorCache);
1101
+ const decodedFragment = decodeUrlFragment(fragment);
1102
+ if (!anchors.has(fragment) && !anchors.has(decodedFragment)) {
1103
+ issues.push(issue("HTML_ANCHOR_MISSING", `${path.relative(htmlDir, htmlFile)} -> ${target.rawTarget}`));
1104
+ }
1031
1105
  }
1032
1106
  }
1033
1107
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-asciidoc-multi-book-workspace",
3
- "version": "0.1.13",
3
+ "version": "0.2.0",
4
4
  "description": "Create a complete AsciiDoc multi-book workspace.",
5
5
  "license": "Apache-2.0",
6
6
  "author": {
@@ -48,10 +48,10 @@
48
48
  |`summary::` + 下一行说明文本
49
49
  |标题下描述列表字段。用于较多字段或多行字段值。
50
50
 
51
- |`xref:stable-id[显示文本]`
51
+ |`pass:[xref:stable-id[显示文本]]`
52
52
  |显示为链接,投影为 references。
53
53
 
54
- |`xref:stable-id[显示文本, rel=depends-on]`
54
+ |`pass:[xref:stable-id[显示文本, rel=depends-on]]`
55
55
  |显示为链接,同时声明 depends-on 关系。
56
56
 
57
57
  |`indexterm2:[role]`