asciidoclint 0.5.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/assets/README.md +12 -0
  4. package/assets/icon.svg +198 -0
  5. package/assets/logo.svg +203 -0
  6. package/dist/api/fixes.d.ts +6 -0
  7. package/dist/api/fixes.js +61 -0
  8. package/dist/api/lint.d.ts +2 -0
  9. package/dist/api/lint.js +191 -0
  10. package/dist/api/rules.d.ts +33 -0
  11. package/dist/api/rules.js +115 -0
  12. package/dist/cli/index.d.ts +2 -0
  13. package/dist/cli/index.js +86 -0
  14. package/dist/cli/init-rule.d.ts +7 -0
  15. package/dist/cli/init-rule.js +74 -0
  16. package/dist/cli/install-skill.d.ts +10 -0
  17. package/dist/cli/install-skill.js +37 -0
  18. package/dist/formatters/json.d.ts +2 -0
  19. package/dist/formatters/json.js +30 -0
  20. package/dist/formatters/pretty.d.ts +2 -0
  21. package/dist/formatters/pretty.js +41 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.js +3 -0
  24. package/dist/parsers/asciidoctor.d.ts +4 -0
  25. package/dist/parsers/asciidoctor.js +444 -0
  26. package/dist/parsers/tolerant.d.ts +4 -0
  27. package/dist/parsers/tolerant.js +528 -0
  28. package/dist/rules/AD001.d.ts +2 -0
  29. package/dist/rules/AD001.js +28 -0
  30. package/dist/rules/AD002.d.ts +2 -0
  31. package/dist/rules/AD002.js +30 -0
  32. package/dist/rules/AD003.d.ts +2 -0
  33. package/dist/rules/AD003.js +28 -0
  34. package/dist/rules/AD004.d.ts +2 -0
  35. package/dist/rules/AD004.js +58 -0
  36. package/dist/rules/AD005.d.ts +2 -0
  37. package/dist/rules/AD005.js +31 -0
  38. package/dist/rules/AD006.d.ts +2 -0
  39. package/dist/rules/AD006.js +53 -0
  40. package/dist/rules/AD007.d.ts +2 -0
  41. package/dist/rules/AD007.js +39 -0
  42. package/dist/rules/AD008.d.ts +2 -0
  43. package/dist/rules/AD008.js +88 -0
  44. package/dist/rules/AD010.d.ts +2 -0
  45. package/dist/rules/AD010.js +39 -0
  46. package/dist/rules/AD011.d.ts +2 -0
  47. package/dist/rules/AD011.js +31 -0
  48. package/dist/rules/AD012.d.ts +2 -0
  49. package/dist/rules/AD012.js +28 -0
  50. package/dist/rules/AD013.d.ts +2 -0
  51. package/dist/rules/AD013.js +43 -0
  52. package/dist/rules/AD016.d.ts +2 -0
  53. package/dist/rules/AD016.js +83 -0
  54. package/dist/rules/AD017.d.ts +2 -0
  55. package/dist/rules/AD017.js +53 -0
  56. package/dist/rules/AD019.d.ts +2 -0
  57. package/dist/rules/AD019.js +58 -0
  58. package/dist/rules/AD020.d.ts +2 -0
  59. package/dist/rules/AD020.js +40 -0
  60. package/dist/rules/AD022.d.ts +2 -0
  61. package/dist/rules/AD022.js +55 -0
  62. package/dist/rules/AD023.d.ts +2 -0
  63. package/dist/rules/AD023.js +59 -0
  64. package/dist/rules/AD024.d.ts +2 -0
  65. package/dist/rules/AD024.js +30 -0
  66. package/dist/rules/AD025.d.ts +2 -0
  67. package/dist/rules/AD025.js +32 -0
  68. package/dist/rules/AD026.d.ts +2 -0
  69. package/dist/rules/AD026.js +26 -0
  70. package/dist/rules/AD027.d.ts +2 -0
  71. package/dist/rules/AD027.js +31 -0
  72. package/dist/rules/AD028.d.ts +2 -0
  73. package/dist/rules/AD028.js +113 -0
  74. package/dist/rules/AD029.d.ts +2 -0
  75. package/dist/rules/AD029.js +46 -0
  76. package/dist/rules/AD030.d.ts +2 -0
  77. package/dist/rules/AD030.js +33 -0
  78. package/dist/rules/AD031.d.ts +2 -0
  79. package/dist/rules/AD031.js +66 -0
  80. package/dist/rules/AD032.d.ts +2 -0
  81. package/dist/rules/AD032.js +81 -0
  82. package/dist/rules/AD034.d.ts +2 -0
  83. package/dist/rules/AD034.js +50 -0
  84. package/dist/rules/AD035.d.ts +2 -0
  85. package/dist/rules/AD035.js +77 -0
  86. package/dist/rules/AD036.d.ts +2 -0
  87. package/dist/rules/AD036.js +34 -0
  88. package/dist/rules/AD037.d.ts +2 -0
  89. package/dist/rules/AD037.js +34 -0
  90. package/dist/rules/AD039.d.ts +2 -0
  91. package/dist/rules/AD039.js +58 -0
  92. package/dist/rules/AD040.d.ts +2 -0
  93. package/dist/rules/AD040.js +56 -0
  94. package/dist/rules/AD041.d.ts +2 -0
  95. package/dist/rules/AD041.js +66 -0
  96. package/dist/rules/AD042.d.ts +2 -0
  97. package/dist/rules/AD042.js +62 -0
  98. package/dist/rules/AD043.d.ts +2 -0
  99. package/dist/rules/AD043.js +30 -0
  100. package/dist/rules/AD044.d.ts +2 -0
  101. package/dist/rules/AD044.js +54 -0
  102. package/dist/rules/AD045.d.ts +2 -0
  103. package/dist/rules/AD045.js +66 -0
  104. package/dist/rules/builtin.d.ts +3 -0
  105. package/dist/rules/builtin.js +81 -0
  106. package/dist/rules/helpers.d.ts +2 -0
  107. package/dist/rules/helpers.js +11 -0
  108. package/dist/rules/registry.d.ts +3 -0
  109. package/dist/rules/registry.js +34 -0
  110. package/dist/rules/utils.d.ts +42 -0
  111. package/dist/rules/utils.js +274 -0
  112. package/dist/types.d.ts +166 -0
  113. package/dist/types.js +1 -0
  114. package/dist/version.d.ts +2 -0
  115. package/dist/version.js +4 -0
  116. package/package.json +70 -0
  117. package/skills/asciidoclint/SKILL.md +84 -0
  118. package/skills/asciidoclint/references/ai-fix-policy.md +11 -0
  119. package/skills/asciidoclint/references/result-schema.md +22 -0
@@ -0,0 +1,528 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { blockDelimiterType } from "../rules/utils.js";
4
+ const headingPattern = /^(=+|#+)\s+(.+?)\s*$/;
5
+ const attributePattern = /^:([^:]+):\s*(.*)$/;
6
+ const includePattern = /^include::([^\[]+)\[(.*)\]\s*$/;
7
+ const imagePattern = /^image::([^\[]+)\[(.*)\]\s*$/;
8
+ const inlineImagePattern = /(^|[^\w:])image:(?!:)([^\s\[]+)\[[^\]]*]/g;
9
+ const xrefPattern = /xref:([^\[]+)\[[^\]]*\]|<<([^,>\s]+)(?:,[^>]*)?>>/g;
10
+ const localLinkPattern = /(^|[^\w:])link:(?!:)([^\s\[]+)\[[^\]]*]/g;
11
+ const explicitAnchorPattern = /^\[\[([^\]]+)\]\]\s*$|^\[#([^,\]\s]+)(?:,[^\]]*)?]\s*$|^\[id=([^,\]\s]+)(?:,[^\]]*)?]\s*$/;
12
+ const inlineAnchorPattern = /\[\[([^\]]+)]]|\[#([^,\]\s]+)(?:,[^\]]*)?]/g;
13
+ const ifdefPattern = /^(ifdef|ifndef)::([^\[]+)\[\]\s*$/;
14
+ const endifPattern = /^endif::(?:[^\[]*)\[\]\s*$/;
15
+ export function parseDocument(file) {
16
+ const absolute = path.resolve(file);
17
+ const state = {
18
+ rootFile: absolute,
19
+ attributes: {},
20
+ sections: [],
21
+ blocks: [],
22
+ includes: [],
23
+ dependencies: [],
24
+ conditionals: [],
25
+ diagnostics: [],
26
+ files: [],
27
+ visited: new Set(),
28
+ anchors: new Map(),
29
+ sourceMap: [],
30
+ expandedLine: 1,
31
+ };
32
+ parseFile(absolute, state, { ...state.attributes });
33
+ finalizeXrefDependencies(state);
34
+ const rootLines = state.files.find((entry) => entry.file === absolute)?.lines ?? [];
35
+ const dependencies = { records: state.dependencies };
36
+ return {
37
+ file: absolute,
38
+ lines: rootLines,
39
+ attributes: state.attributes,
40
+ sections: state.sections,
41
+ blocks: state.blocks,
42
+ referenceTargets: referenceTargetsFromAnchors(state.anchors),
43
+ includes: state.includes,
44
+ dependencies,
45
+ diagnostics: state.diagnostics,
46
+ sourceMap: state.sourceMap,
47
+ conditionals: state.conditionals,
48
+ files: state.files,
49
+ };
50
+ }
51
+ function parseFile(file, state, inheritedAttributes) {
52
+ const absolute = path.resolve(file);
53
+ if (state.visited.has(absolute)) {
54
+ return { ...inheritedAttributes };
55
+ }
56
+ state.visited.add(absolute);
57
+ let text;
58
+ try {
59
+ text = fs.readFileSync(absolute, "utf8");
60
+ }
61
+ catch {
62
+ return { ...inheritedAttributes };
63
+ }
64
+ const lines = text.split(/\r?\n/);
65
+ state.files.push({ file: absolute, lines });
66
+ const attributes = { ...inheritedAttributes };
67
+ const sectionStack = [];
68
+ let openBlock;
69
+ const conditionalStack = [];
70
+ for (let index = 0; index < lines.length; index += 1) {
71
+ const lineNumber = index + 1;
72
+ const line = lines[index] ?? "";
73
+ const position = pos(absolute, lineNumber, 1);
74
+ const trimmed = line.trim();
75
+ const protectedBlock = isProtectedOpenBlock(openBlock);
76
+ addSourceMap(state, absolute, lineNumber);
77
+ if (!protectedBlock) {
78
+ const conditional = line.match(ifdefPattern);
79
+ if (conditional) {
80
+ const directive = (conditional[1] ?? "ifdef");
81
+ const attribute = conditional[2] ?? "";
82
+ const attributeNames = attribute.split(",").map((name) => name.trim()).filter(Boolean);
83
+ const hasAny = attributeNames.some((name) => attributes[name] !== undefined);
84
+ const active = directive === "ifdef" ? hasAny : !hasAny;
85
+ conditionalStack.push(active);
86
+ state.conditionals.push({
87
+ directive,
88
+ attribute,
89
+ active,
90
+ range: lineRange(absolute, lineNumber, 1, line.length + 1),
91
+ });
92
+ continue;
93
+ }
94
+ if (endifPattern.test(line)) {
95
+ conditionalStack.pop();
96
+ continue;
97
+ }
98
+ }
99
+ const conditionalActive = conditionalStack.every(Boolean);
100
+ if (!conditionalActive) {
101
+ continue;
102
+ }
103
+ if (!protectedBlock && trimmed.startsWith("//") && blockDelimiterType(trimmed) === undefined) {
104
+ continue;
105
+ }
106
+ const commentParagraphRange = !openBlock ? findCommentParagraphRange(lines, index) : undefined;
107
+ if (commentParagraphRange) {
108
+ if (commentParagraphRange.start === index) {
109
+ state.blocks.push({
110
+ kind: "block",
111
+ type: "comment",
112
+ style: "comment",
113
+ attributes: {},
114
+ range: {
115
+ start: pos(absolute, index, 1),
116
+ end: pos(absolute, commentParagraphRange.end + 1, (lines[commentParagraphRange.end] ?? "").length + 1),
117
+ },
118
+ contentRange: lineRange(absolute, lineNumber, 1, line.length + 1),
119
+ });
120
+ }
121
+ continue;
122
+ }
123
+ const attr = line.match(attributePattern);
124
+ if (!openBlock && attr) {
125
+ attributes[attr[1] ?? ""] = attr[2] ?? "";
126
+ state.attributes[attr[1] ?? ""] = attr[2] ?? "";
127
+ continue;
128
+ }
129
+ if (!protectedBlock) {
130
+ collectInlineDependencies(line, absolute, lineNumber, attributes, state);
131
+ collectInlineAnchors(line, absolute, state);
132
+ }
133
+ const anchor = !protectedBlock ? line.match(explicitAnchorPattern) : undefined;
134
+ if (anchor) {
135
+ addAnchor(state, absolute, (anchor[1] ?? anchor[2] ?? anchor[3] ?? "").trim());
136
+ continue;
137
+ }
138
+ const include = !protectedBlock ? line.match(includePattern) : undefined;
139
+ if (include) {
140
+ const target = substituteAttributes(include[1] ?? "", attributes);
141
+ const includeAttributes = parseBracketAttributes(include[2] ?? "");
142
+ const resolved = isExternalTarget(target) ? target : path.resolve(path.dirname(absolute), target);
143
+ const range = lineRange(absolute, lineNumber, 1, line.length + 1);
144
+ const missingStatus = isOptionalInclude(includeAttributes) || isExternalTarget(target) ? "skipped" : "missing";
145
+ const status = !isExternalTarget(target) && fs.existsSync(resolved) ? "resolved" : missingStatus;
146
+ state.includes.push({
147
+ target,
148
+ resolvedTarget: status === "resolved" ? resolved : undefined,
149
+ range,
150
+ status,
151
+ attributes: includeAttributes,
152
+ });
153
+ state.dependencies.push({
154
+ type: "include",
155
+ target,
156
+ resolvedTarget: status === "resolved" ? resolved : undefined,
157
+ range,
158
+ status,
159
+ });
160
+ if (status === "resolved" && isAsciiDocSourceFile(resolved)) {
161
+ Object.assign(attributes, parseFile(resolved, state, attributes));
162
+ }
163
+ continue;
164
+ }
165
+ const image = !protectedBlock ? line.match(imagePattern) : undefined;
166
+ if (image) {
167
+ const target = substituteAttributes(image[1] ?? "", attributes);
168
+ const resolved = resolveImageTarget(absolute, target, attributes);
169
+ const range = lineRange(absolute, lineNumber, 1, line.length + 1);
170
+ const status = isExternalTarget(resolved) ? "skipped" : fs.existsSync(resolved) ? "resolved" : "missing";
171
+ state.blocks.push({
172
+ kind: "block",
173
+ type: "image",
174
+ attributes: { ...parseBracketAttributes(image[2] ?? ""), target },
175
+ range,
176
+ });
177
+ state.dependencies.push({
178
+ type: "image",
179
+ target,
180
+ resolvedTarget: status === "resolved" ? resolved : undefined,
181
+ range,
182
+ status,
183
+ });
184
+ continue;
185
+ }
186
+ const markdownFence = trimmed.startsWith("```") ? "```" : undefined;
187
+ const delimiterType = markdownFence ? "listing" : blockDelimiterType(trimmed);
188
+ if (delimiterType) {
189
+ if (openBlock?.delimiter === (markdownFence ?? trimmed)) {
190
+ state.blocks.push({
191
+ kind: "block",
192
+ type: blockTypeForOpenBlock(openBlock),
193
+ style: openBlock.style,
194
+ attributes: {},
195
+ range: { start: openBlock.range.start, end: pos(absolute, lineNumber, line.length + 1) },
196
+ contentRange: lineRange(absolute, openBlock.range.start.line + 1, 1, lineNumber - 1 > openBlock.range.start.line ? 1 : 1),
197
+ });
198
+ openBlock = undefined;
199
+ }
200
+ else if (!openBlock) {
201
+ openBlock = {
202
+ delimiter: markdownFence ?? trimmed,
203
+ type: delimiterType,
204
+ style: markdownFence ? "source" : detectStyle(lines[index - 1]),
205
+ range: lineRange(absolute, lineNumber, 1, line.length + 1),
206
+ };
207
+ }
208
+ continue;
209
+ }
210
+ if (protectedBlock) {
211
+ continue;
212
+ }
213
+ const heading = line.match(headingPattern);
214
+ if (heading) {
215
+ const marker = heading[1] ?? "";
216
+ const title = heading[2] ?? "";
217
+ const level = marker.length - 1;
218
+ const range = lineRange(absolute, lineNumber, 1, line.length + 1);
219
+ const section = {
220
+ kind: "section",
221
+ title,
222
+ level,
223
+ range,
224
+ titleRange: range,
225
+ children: [],
226
+ blocks: [],
227
+ };
228
+ while (sectionStack.length && (sectionStack[sectionStack.length - 1]?.level ?? 0) >= level) {
229
+ sectionStack.pop();
230
+ }
231
+ const parent = sectionStack[sectionStack.length - 1];
232
+ if (parent) {
233
+ section.parent = parent;
234
+ parent.children.push(section);
235
+ }
236
+ sectionStack.push(section);
237
+ state.sections.push(section);
238
+ addAnchor(state, absolute, title);
239
+ if (!explicitAnchorImmediatelyBefore(lines, index)) {
240
+ addAnchor(state, absolute, sectionId(title));
241
+ }
242
+ continue;
243
+ }
244
+ }
245
+ return attributes;
246
+ }
247
+ function collectInlineDependencies(line, file, lineNumber, attributes, state) {
248
+ for (const match of line.matchAll(inlineImagePattern)) {
249
+ const rawTarget = match[2] ?? "";
250
+ const target = substituteAttributes(rawTarget, attributes);
251
+ if (!target) {
252
+ continue;
253
+ }
254
+ const prefixLength = match[1]?.length ?? 0;
255
+ const column = (match.index ?? 0) + prefixLength + 1;
256
+ const range = lineRange(file, lineNumber, column, column + match[0].length - prefixLength);
257
+ const resolved = resolveImageTarget(file, target, attributes);
258
+ const status = isExternalTarget(resolved) ? "skipped" : fs.existsSync(resolved) ? "resolved" : "missing";
259
+ state.dependencies.push({
260
+ type: "image",
261
+ target,
262
+ resolvedTarget: status === "resolved" ? resolved : undefined,
263
+ range,
264
+ status,
265
+ });
266
+ }
267
+ for (const match of line.matchAll(xrefPattern)) {
268
+ const rawTarget = match[1] ?? match[2] ?? "";
269
+ const target = normalizeXrefTarget(substituteAttributes(rawTarget, attributes));
270
+ if (!target) {
271
+ continue;
272
+ }
273
+ const column = (match.index ?? 0) + 1;
274
+ const range = lineRange(file, lineNumber, column, column + match[0].length);
275
+ const resolved = resolveXref(file, target, state);
276
+ state.dependencies.push({
277
+ type: "xref",
278
+ target,
279
+ resolvedTarget: resolved.status === "resolved" ? resolved.resolvedTarget : undefined,
280
+ range,
281
+ status: resolved.status,
282
+ });
283
+ }
284
+ for (const match of line.matchAll(localLinkPattern)) {
285
+ const rawTarget = match[2] ?? "";
286
+ const target = substituteAttributes(rawTarget, attributes);
287
+ if (!target || isExternalTarget(target)) {
288
+ continue;
289
+ }
290
+ const prefixLength = match[1]?.length ?? 0;
291
+ const column = (match.index ?? 0) + prefixLength + 1;
292
+ const range = lineRange(file, lineNumber, column, column + match[0].length - prefixLength);
293
+ const resolved = path.resolve(path.dirname(file), localFilePathFromLinkTarget(target));
294
+ const status = fs.existsSync(resolved) ? "resolved" : "missing";
295
+ state.dependencies.push({
296
+ type: "attachment",
297
+ target,
298
+ resolvedTarget: status === "resolved" ? resolved : undefined,
299
+ range,
300
+ status,
301
+ });
302
+ }
303
+ }
304
+ function localFilePathFromLinkTarget(target) {
305
+ return target.split(/[?#]/, 1)[0] ?? target;
306
+ }
307
+ function collectInlineAnchors(line, file, state) {
308
+ for (const match of line.matchAll(inlineAnchorPattern)) {
309
+ addAnchor(state, file, (match[1] ?? match[2] ?? "").trim());
310
+ }
311
+ }
312
+ function resolveXref(file, target, state) {
313
+ const [targetFile, targetAnchor] = splitXrefTarget(target);
314
+ if (targetFile) {
315
+ const resolvedFile = path.resolve(path.dirname(file), targetFile);
316
+ if (!fs.existsSync(resolvedFile)) {
317
+ return { status: "missing" };
318
+ }
319
+ if (!targetAnchor) {
320
+ return { status: "resolved", resolvedTarget: resolvedFile };
321
+ }
322
+ parseFile(resolvedFile, state, { ...state.attributes });
323
+ return hasAnchor(state, resolvedFile, targetAnchor)
324
+ ? { status: "resolved", resolvedTarget: resolvedFile }
325
+ : { status: "missing" };
326
+ }
327
+ return hasAnyAnchor(state, targetAnchor ?? target)
328
+ ? { status: "resolved", resolvedTarget: file }
329
+ : { status: "missing" };
330
+ }
331
+ export function resolveDocumentXrefs(document) {
332
+ const targets = new Map();
333
+ for (const target of document.referenceTargets) {
334
+ for (const id of [target.id, ...(target.aliases ?? [])]) {
335
+ const key = referenceKey(target.file, id);
336
+ targets.set(key, [...(targets.get(key) ?? []), target]);
337
+ }
338
+ }
339
+ for (const record of document.dependencies.records) {
340
+ if (record.type !== "xref") {
341
+ continue;
342
+ }
343
+ const resolved = resolveDocumentXref(record.range.start.file, record.target, targets);
344
+ record.status = resolved.status;
345
+ record.resolvedTarget = resolved.resolvedTarget;
346
+ }
347
+ }
348
+ function resolveDocumentXref(sourceFile, target, targets) {
349
+ const [targetFile, targetAnchor] = splitXrefTarget(target);
350
+ if (targetFile) {
351
+ const resolvedFile = path.resolve(path.dirname(sourceFile), targetFile);
352
+ if (!fs.existsSync(resolvedFile)) {
353
+ return { status: "missing" };
354
+ }
355
+ if (!targetAnchor) {
356
+ return { status: "resolved", resolvedTarget: resolvedFile };
357
+ }
358
+ return targets.has(referenceKey(resolvedFile, targetAnchor))
359
+ ? { status: "resolved", resolvedTarget: resolvedFile }
360
+ : { status: "missing" };
361
+ }
362
+ const anchor = targetAnchor ?? target;
363
+ if (targets.has(referenceKey(sourceFile, anchor))) {
364
+ return { status: "resolved", resolvedTarget: sourceFile };
365
+ }
366
+ for (const key of targets.keys()) {
367
+ if (key.endsWith(`#${anchor}`)) {
368
+ return { status: "resolved", resolvedTarget: sourceFile };
369
+ }
370
+ }
371
+ return { status: "missing" };
372
+ }
373
+ function finalizeXrefDependencies(state) {
374
+ for (const record of state.dependencies) {
375
+ if (record.type !== "xref") {
376
+ continue;
377
+ }
378
+ const resolved = resolveXref(record.range.start.file, record.target, state);
379
+ record.status = resolved.status;
380
+ record.resolvedTarget = resolved.resolvedTarget;
381
+ }
382
+ }
383
+ function splitXrefTarget(target) {
384
+ const hash = target.indexOf("#");
385
+ if (hash === -1) {
386
+ return hasFileExtension(target) ? [target, undefined] : [undefined, target.replace(/^#/, "")];
387
+ }
388
+ const file = target.slice(0, hash);
389
+ const anchor = target.slice(hash + 1);
390
+ return [file || undefined, anchor || undefined];
391
+ }
392
+ function normalizeXrefTarget(target) {
393
+ return target.replace(/^#/, "").trim();
394
+ }
395
+ function hasFileExtension(target) {
396
+ return /\.[A-Za-z0-9]+(?:#.*)?$/.test(target);
397
+ }
398
+ function isExternalTarget(target) {
399
+ return /^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("mailto:");
400
+ }
401
+ function resolveImageTarget(file, target, attributes) {
402
+ if (path.isAbsolute(target) || isExternalTarget(target)) {
403
+ return target;
404
+ }
405
+ const imagesdir = attributes.imagesdir;
406
+ if (imagesdir && isExternalTarget(imagesdir)) {
407
+ return new URL(target, imagesdir.endsWith("/") ? imagesdir : `${imagesdir}/`).toString();
408
+ }
409
+ const imageTarget = imagesdir ? path.join(imagesdir, target) : target;
410
+ return path.resolve(path.dirname(file), imageTarget);
411
+ }
412
+ function isOptionalInclude(attributes) {
413
+ return attributes.optional === true
414
+ || attributes.opts === "optional"
415
+ || attributes.options === "optional"
416
+ || (typeof attributes.opts === "string" && attributes.opts.split(/\s+/).includes("optional"))
417
+ || (typeof attributes.options === "string" && attributes.options.split(/\s+/).includes("optional"));
418
+ }
419
+ function addAnchor(state, file, anchor) {
420
+ if (!anchor) {
421
+ return;
422
+ }
423
+ const absolute = path.resolve(file);
424
+ const anchors = state.anchors.get(absolute) ?? new Set();
425
+ anchors.add(anchor);
426
+ state.anchors.set(absolute, anchors);
427
+ }
428
+ function referenceTargetsFromAnchors(anchors) {
429
+ return [...anchors.entries()].flatMap(([file, ids]) => ([...ids].map((id) => ({ id, file, source: "tolerant" }))));
430
+ }
431
+ function referenceKey(file, id) {
432
+ return `${path.resolve(file)}#${id}`;
433
+ }
434
+ function explicitAnchorImmediatelyBefore(lines, index) {
435
+ const previous = lines[index - 1] ?? "";
436
+ return explicitAnchorPattern.test(previous);
437
+ }
438
+ function hasAnchor(state, file, anchor) {
439
+ return state.anchors.get(path.resolve(file))?.has(anchor) ?? false;
440
+ }
441
+ function hasAnyAnchor(state, anchor) {
442
+ return [...state.anchors.values()].some((anchors) => anchors.has(anchor));
443
+ }
444
+ function sectionId(title) {
445
+ return `_${title.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
446
+ }
447
+ export function substituteAttributes(value, attributes) {
448
+ return value.replace(/\{([^}]+)\}/g, (_match, name) => attributes[name] ?? `{${name}}`);
449
+ }
450
+ function detectStyle(previousLine) {
451
+ const previous = previousLine?.trim();
452
+ if (!previous?.startsWith("[") || !previous.endsWith("]")) {
453
+ return undefined;
454
+ }
455
+ const content = previous.slice(1, -1);
456
+ return content.split(",")[0];
457
+ }
458
+ function isProtectedOpenBlock(openBlock) {
459
+ if (!openBlock) {
460
+ return false;
461
+ }
462
+ return openBlock.style === "source"
463
+ || openBlock.style === "comment"
464
+ || ["listing", "literal", "passthrough", "comment"].includes(openBlock.type);
465
+ }
466
+ function isAsciiDocSourceFile(file) {
467
+ return /\.(?:adoc|asciidoc|asc)$/i.test(file);
468
+ }
469
+ function blockTypeForOpenBlock(openBlock) {
470
+ if (openBlock.style === "source") {
471
+ return "source";
472
+ }
473
+ if (openBlock.style === "comment") {
474
+ return "comment";
475
+ }
476
+ return openBlock.type;
477
+ }
478
+ function findCommentParagraphRange(lines, index) {
479
+ const line = lines[index] ?? "";
480
+ if (line.trim() === "" || blockDelimiterType(line.trim()) !== undefined) {
481
+ return undefined;
482
+ }
483
+ let start = index;
484
+ while (start > 0 && (lines[start - 1] ?? "").trim() !== "" && (lines[start - 1] ?? "").trim() !== "[comment]") {
485
+ start -= 1;
486
+ }
487
+ if ((lines[start - 1] ?? "").trim() !== "[comment]") {
488
+ return undefined;
489
+ }
490
+ let end = index;
491
+ while (end + 1 < lines.length && (lines[end + 1] ?? "").trim() !== "") {
492
+ end += 1;
493
+ }
494
+ return { start, end };
495
+ }
496
+ function parseBracketAttributes(value) {
497
+ const result = {};
498
+ if (!value) {
499
+ return result;
500
+ }
501
+ const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
502
+ for (const [index, part] of parts.entries()) {
503
+ const named = part.match(/^([A-Za-z_][A-Za-z0-9_.-]*)=(.*)$/);
504
+ if (!named) {
505
+ result[index === 0 ? "alt" : part] = index === 0 ? part : true;
506
+ }
507
+ else {
508
+ result[named[1] ?? ""] = (named[2] ?? "").replace(/^"|"$/g, "");
509
+ }
510
+ }
511
+ return result;
512
+ }
513
+ function pos(file, line, column) {
514
+ return { file, line, column };
515
+ }
516
+ function addSourceMap(state, file, line) {
517
+ state.sourceMap.push({
518
+ expandedLine: state.expandedLine,
519
+ source: pos(file, line, 1),
520
+ });
521
+ state.expandedLine += 1;
522
+ }
523
+ function lineRange(file, line, startColumn, endColumn) {
524
+ return {
525
+ start: pos(file, line, startColumn),
526
+ end: pos(file, line, endColumn),
527
+ };
528
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD001: Rule;
@@ -0,0 +1,28 @@
1
+ export const AD001 = {
2
+ id: "AD001",
3
+ alias: "heading-level-progression",
4
+ description: "Section headings must not skip levels",
5
+ tags: ["core", "headings"],
6
+ parser: "document",
7
+ docs: {
8
+ summary: "A heading should advance by at most one level from the previous heading.",
9
+ rationale: "Skipped levels usually indicate a broken document hierarchy.",
10
+ fixability: "no",
11
+ fixHelper: "Add the missing intermediate parent section, or reduce the skipped heading marker so the section advances by only one level from the previous section.",
12
+ badExamples: [{ code: "= Title\n\n=== Skipped" }],
13
+ goodExamples: [{ code: "= Title\n\n== Parent\n\n=== Child" }],
14
+ },
15
+ function: ({ parserDiagnostics }, onError) => {
16
+ for (const diagnostic of parserDiagnostics.filter(isOutOfSequenceDiagnostic)) {
17
+ onError({
18
+ severity: "error",
19
+ message: `Skipped section level: ${diagnostic.message}`,
20
+ range: diagnostic.range,
21
+ fixHelper: "Add the missing intermediate parent section, or reduce the skipped heading marker so the section advances by only one level from the previous section.",
22
+ });
23
+ }
24
+ },
25
+ };
26
+ function isOutOfSequenceDiagnostic(finding) {
27
+ return finding.ruleId === "AD000" && /section title out of sequence/i.test(finding.message);
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD002: Rule;
@@ -0,0 +1,30 @@
1
+ export const AD002 = {
2
+ id: "AD002",
3
+ alias: "single-document-title",
4
+ description: "Article documents should have only one level-0 title",
5
+ tags: ["core", "headings"],
6
+ parser: "document",
7
+ docs: {
8
+ summary: "Only book documents may have multiple level-0 sections.",
9
+ fixability: "no",
10
+ fixHelper: "In article documents, change the extra level-0 heading to the appropriate section level, or change the document to :doctype: book only when the extra level-0 headings are intended to be book parts.",
11
+ badExamples: [{ code: "= Title\n\n= Second Title" }],
12
+ goodExamples: [
13
+ { code: "= Title\n\n== Section" },
14
+ { code: ":doctype: book\n\n= Book\n\n= Part One\n\n== Chapter" },
15
+ ],
16
+ },
17
+ function: ({ parserDiagnostics }, onError) => {
18
+ for (const diagnostic of parserDiagnostics.filter(isExtraLevelZeroDiagnostic)) {
19
+ onError({
20
+ severity: "error",
21
+ message: "Multiple level-0 document titles found",
22
+ range: diagnostic.range,
23
+ fixHelper: "In article documents, change the extra level-0 heading to the appropriate section level, or change the document to :doctype: book only when the extra level-0 headings are intended to be book parts.",
24
+ });
25
+ }
26
+ },
27
+ };
28
+ function isExtraLevelZeroDiagnostic(finding) {
29
+ return finding.ruleId === "AD000" && /level 0 sections can only be used when doctype is book/i.test(finding.message);
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD003: Rule;
@@ -0,0 +1,28 @@
1
+ export const AD003 = {
2
+ id: "AD003",
3
+ alias: "unterminated-block",
4
+ description: "Delimited blocks should have matching closing delimiters",
5
+ tags: ["core", "blocks"],
6
+ parser: "document",
7
+ docs: {
8
+ summary: "A delimited block should be closed with the matching delimiter required by Asciidoctor.",
9
+ rationale: "Unterminated blocks can cause large portions of a document to render as the wrong block type.",
10
+ fixability: "no",
11
+ fixHelper: "Add the missing closing delimiter that matches the opener. The correct location depends on where the block content should end.",
12
+ badExamples: [{ code: "====\ncontent" }],
13
+ goodExamples: [{ code: "====\ncontent\n====" }],
14
+ },
15
+ function: ({ parserDiagnostics }, onError) => {
16
+ for (const diagnostic of parserDiagnostics.filter(isUnterminatedBlockDiagnostic)) {
17
+ onError({
18
+ severity: "error",
19
+ message: `Unterminated delimited block: ${diagnostic.message}`,
20
+ range: diagnostic.range,
21
+ fixHelper: "Add the matching closing delimiter for the block.",
22
+ });
23
+ }
24
+ },
25
+ };
26
+ function isUnterminatedBlockDiagnostic(finding) {
27
+ return finding.ruleId === "AD000" && /unterminated .*block/i.test(finding.message);
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD004: Rule;