asciidoclint 1.0.0 → 1.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 (85) hide show
  1. package/README.md +28 -9
  2. package/dist/api/lint.js +41 -4
  3. package/dist/api/rules.js +1 -1
  4. package/dist/parsers/asciidoctor.d.ts +2 -1
  5. package/dist/parsers/asciidoctor.js +150 -1
  6. package/dist/parsers/tolerant.js +29 -5
  7. package/dist/rules/AD004.js +99 -0
  8. package/dist/rules/AD009.d.ts +2 -0
  9. package/dist/rules/AD009.js +80 -0
  10. package/dist/rules/AD010.js +17 -3
  11. package/dist/rules/AD011.js +20 -5
  12. package/dist/rules/AD012.js +14 -2
  13. package/dist/rules/AD013.js +2 -2
  14. package/dist/rules/AD016.js +16 -10
  15. package/dist/rules/AD017.js +4 -4
  16. package/dist/rules/AD020.js +41 -20
  17. package/dist/rules/AD025.js +2 -2
  18. package/dist/rules/AD028.js +77 -10
  19. package/dist/rules/AD029.js +2 -2
  20. package/dist/rules/AD039.js +2 -2
  21. package/dist/rules/AD046.d.ts +2 -0
  22. package/dist/rules/AD046.js +91 -0
  23. package/dist/rules/AD047.d.ts +2 -0
  24. package/dist/rules/AD047.js +47 -0
  25. package/dist/rules/AD048.d.ts +2 -0
  26. package/dist/rules/AD048.js +37 -0
  27. package/dist/rules/AD049.d.ts +2 -0
  28. package/dist/rules/AD049.js +46 -0
  29. package/dist/rules/AD050.d.ts +2 -0
  30. package/dist/rules/AD050.js +46 -0
  31. package/dist/rules/AD051.d.ts +2 -0
  32. package/dist/rules/AD051.js +52 -0
  33. package/dist/rules/AD052.d.ts +2 -0
  34. package/dist/rules/AD052.js +46 -0
  35. package/dist/rules/AD053.d.ts +2 -0
  36. package/dist/rules/AD053.js +46 -0
  37. package/dist/rules/AD054.d.ts +2 -0
  38. package/dist/rules/AD054.js +46 -0
  39. package/dist/rules/AD055.d.ts +2 -0
  40. package/dist/rules/AD055.js +41 -0
  41. package/dist/rules/AD056.d.ts +2 -0
  42. package/dist/rules/AD056.js +49 -0
  43. package/dist/rules/AD057.d.ts +2 -0
  44. package/dist/rules/AD057.js +79 -0
  45. package/dist/rules/AD058.d.ts +2 -0
  46. package/dist/rules/AD058.js +33 -0
  47. package/dist/rules/AD059.d.ts +2 -0
  48. package/dist/rules/AD059.js +99 -0
  49. package/dist/rules/builtin.js +30 -0
  50. package/dist/rules/specialSections.d.ts +13 -0
  51. package/dist/rules/specialSections.js +54 -0
  52. package/dist/types.d.ts +5 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/docs/rules/AD009.md +59 -0
  56. package/docs/rules/AD010.md +12 -3
  57. package/docs/rules/AD011.md +17 -9
  58. package/docs/rules/AD012.md +14 -3
  59. package/docs/rules/AD013.md +3 -3
  60. package/docs/rules/AD016.md +11 -10
  61. package/docs/rules/AD017.md +5 -4
  62. package/docs/rules/AD020.md +57 -11
  63. package/docs/rules/AD025.md +2 -2
  64. package/docs/rules/AD028.md +14 -9
  65. package/docs/rules/AD029.md +2 -2
  66. package/docs/rules/AD046.md +118 -0
  67. package/docs/rules/AD047.md +46 -0
  68. package/docs/rules/AD048.md +43 -0
  69. package/docs/rules/AD049.md +43 -0
  70. package/docs/rules/AD050.md +41 -0
  71. package/docs/rules/AD051.md +49 -0
  72. package/docs/rules/AD052.md +39 -0
  73. package/docs/rules/AD053.md +41 -0
  74. package/docs/rules/AD054.md +40 -0
  75. package/docs/rules/AD055.md +60 -0
  76. package/docs/rules/AD056.md +36 -0
  77. package/docs/rules/AD057.md +44 -0
  78. package/docs/rules/AD058.md +28 -0
  79. package/docs/rules/AD059.md +68 -0
  80. package/docs/rules/rule-necessity.md +22 -7
  81. package/package.json +1 -1
  82. package/skills/asciidoclint/SKILL.md +21 -2
  83. package/skills/asciidoclint/references/feedback.md +8 -5
  84. package/skills/asciidoclint/references/rule-create.md +13 -2
  85. package/skills/asciidoclint/references/rule-review.md +8 -1
package/README.md CHANGED
@@ -122,6 +122,7 @@ extension commands and settings.
122
122
  | `AD006/included-document-title` | Included AsciiDoc files should not introduce a level-0 title without level offset |
123
123
  | `AD007/heading-depth-limit` | Section headings should not exceed Asciidoctor's supported depth |
124
124
  | `AD008/blank-before-list` | Lists should be separated from preceding paragraph text |
125
+ | `AD009/blank-after-list` | Lists should be separated from following structural blocks |
125
126
  | `AD010/table-title` | Table blocks should have a title |
126
127
  | `AD011/image-title` | Block images should have a title |
127
128
  | `AD012/diagram-title` | Diagram blocks should have a title |
@@ -129,14 +130,14 @@ extension commands and settings.
129
130
  | `AD016/malformed-figure-caption` | Figure captions should use AsciiDoc title syntax |
130
131
  | `AD017/malformed-table-caption` | Table captions should use AsciiDoc title syntax |
131
132
  | `AD019/content-after-include` | Text should not be attached directly after include directives |
132
- | `AD020/appendix-section-level` | Appendices should be section-level blocks in article documents |
133
+ | `AD020/appendix-placement` | Appendix markers should apply to documented appendix section levels |
133
134
  | `AD022/circular-include` | Include trees must not contain cycles |
134
135
  | `AD023/empty-section` | Sections should contain body content or child sections |
135
136
  | `AD024/missing-include` | Include targets should exist after attribute substitution |
136
137
  | `AD025/missing-image` | Image targets should exist after attribute substitution |
137
138
  | `AD026/missing-xref` | Cross-reference targets should resolve to an anchor or file |
138
139
  | `AD027/missing-local-link` | Local link targets should resolve to existing files |
139
- | `AD028/image-alt-text` | Images should not explicitly set empty alt text |
140
+ | `AD028/image-alt-text` | Images should provide meaningful alt text |
140
141
  | `AD029/markdown-link-image-residue` | Markdown link and image residue should not render as text |
141
142
  | `AD030/markdown-table-residue` | Markdown pipe table residue should not render as text |
142
143
  | `AD031/no-nested-link-text` | Link text should not contain nested links or cross references |
@@ -152,6 +153,20 @@ extension commands and settings.
152
153
  | `AD043/section-title-start-left` | Section title syntax should start at the beginning of the line |
153
154
  | `AD044/local-adoc-link` | Local AsciiDoc files should be referenced with xref, not link |
154
155
  | `AD045/markdown-heading-mix` | Markdown-compatible headings should not be mixed with AsciiDoc headings |
156
+ | `AD046/preface-placement` | Preface sections should match documented book placement |
157
+ | `AD047/abstract-placement` | Abstract sections should match documented article placement |
158
+ | `AD048/bibliography-placement` | Bibliography markers should apply to documented bibliography sections |
159
+ | `AD049/glossary-placement` | Glossary markers should apply to documented glossary section levels |
160
+ | `AD050/index-placement` | Index markers should apply to documented index section levels |
161
+ | `AD051/partintro-placement` | Part introduction markers should apply to the introductory block of a book part |
162
+ | `AD052/acknowledgments-placement` | Acknowledgments markers should apply to documented book section levels |
163
+ | `AD053/dedication-placement` | Dedication markers should apply to documented book section levels |
164
+ | `AD054/colophon-placement` | Colophon markers should apply to documented book section levels |
165
+ | `AD055/docx-anchor-caption-residue` | DOCX anchor caption residue should be converted to AsciiDoc titles |
166
+ | `AD056/dangling-blank-list-continuation` | Blank list continuations should not capture following content |
167
+ | `AD057/semantic-anchor-target` | Semantic anchors should attach to the matching block type |
168
+ | `AD058/docx-bookmark-hash-residue` | DOCX bookmark/hash residue should be cleaned up |
169
+ | `AD059/docx-nested-table-structure` | DOCX-converted nested tables should use valid nested table separators and shallow nesting |
155
170
  | `ADW01/unknown-waiver-directive` | Waiver directive names should be known |
156
171
  | `ADW02/missing-waiver-rule-list` | Waiver directives should include a rule list |
157
172
  | `ADW03/malformed-waiver-rule-list` | Waiver rule lists should use comma-separated rule IDs |
@@ -168,26 +183,30 @@ rules. `ADW##` waiver diagnostics use tags for discovery, but remain always on.
168
183
 
169
184
  | Group | IDs |
170
185
  |---|---|
171
- | `blank_lines` | `AD008` |
186
+ | `anchor` | `AD057` |
187
+ | `blank_lines` | `AD008`, `AD009` |
172
188
  | `blocks` | `AD003`, `AD032`, `AD035` |
173
189
  | `accessibility` | `AD028`, `AD042` |
174
- | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041` |
175
- | `conversion` | `AD029`, `AD030`, `AD031`, `AD036`, `AD037`, `AD039`, `AD040` |
190
+ | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041`, `AD055`, `AD058`, `AD059` |
191
+ | `conversion` | `AD029`, `AD030`, `AD031`, `AD037`, `AD039`, `AD040`, `AD055`, `AD056`, `AD058`, `AD059` |
192
+ | `docx` | `AD016`, `AD017`, `AD028`, `AD055`, `AD056`, `AD058`, `AD059` |
176
193
  | `dependencies` | `AD024`, `AD025`, `AD026`, `AD027` |
177
194
  | `diagram` | `AD012` |
178
195
  | `format` | `AD041` |
179
196
  | `headings` | `AD001`, `AD002`, `AD005`, `AD006`, `AD007`, `AD043`, `AD045` |
180
- | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028` |
197
+ | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028`, `AD057` |
181
198
  | `include` | `AD006`, `AD019`, `AD022`, `AD024` |
182
199
  | `inline` | `AD041` |
183
- | `lists` | `AD008`, `AD036` |
200
+ | `list` | `AD056` |
201
+ | `lists` | `AD008`, `AD009`, `AD036` |
202
+ | `pandoc` | `AD059` |
184
203
  | `parser` | `AD000` |
185
- | `table` | `AD004`, `AD010`, `AD017`, `AD030` |
204
+ | `table` | `AD004`, `AD010`, `AD017`, `AD030`, `AD057`, `AD059` |
186
205
  | `waiver` | `ADW01`, `ADW02`, `ADW03`, `ADW04`, `ADW05`, `ADW06`, `ADW07`, `ADW08` |
187
206
  | `whitespace` | `AD034` |
188
207
  | `references` | `AD023` |
189
208
  | `links` | `AD027`, `AD031`, `AD042`, `AD044` |
190
- | `structure` | `AD043` |
209
+ | `structure` | `AD009`, `AD019`, `AD020`, `AD022`, `AD023`, `AD043`, `AD046`, `AD047`, `AD048`, `AD049`, `AD050`, `AD051`, `AD052`, `AD053`, `AD054`, `AD056`, `AD057` |
191
210
  | `markdown-compatibility` | `AD045` |
192
211
  | `maintainability` | `AD045` |
193
212
  | `xref` | `AD026`, `AD042`, `AD044` |
package/dist/api/lint.js CHANGED
@@ -23,6 +23,7 @@ async function lintFilesInternal(patterns, options, afterFix) {
23
23
  for (const parsedFile of document.files) {
24
24
  parsedFiles.set(path.resolve(parsedFile.file), parsedFile);
25
25
  }
26
+ mergeAsciidoctorSections(document, await collectParserSections(file));
26
27
  mergeAsciidoctorBlocks(document, await collectParserBlocks(file));
27
28
  mergeAsciidoctorReferenceTargets(document, await collectParserReferenceTargets(file));
28
29
  resolveDocumentXrefs(document);
@@ -69,24 +70,60 @@ async function collectParserBlocks(file) {
69
70
  const { collectAsciidoctorBlocks } = await import("../parsers/asciidoctor.js");
70
71
  return collectAsciidoctorBlocks(file);
71
72
  }
73
+ async function collectParserSections(file) {
74
+ const { collectAsciidoctorSections } = await import("../parsers/asciidoctor.js");
75
+ return collectAsciidoctorSections(file);
76
+ }
72
77
  async function collectParserReferenceTargets(file) {
73
78
  const { collectAsciidoctorReferenceTargets } = await import("../parsers/asciidoctor.js");
74
79
  return collectAsciidoctorReferenceTargets(file);
75
80
  }
81
+ function mergeAsciidoctorSections(document, sections) {
82
+ if (!sections.length) {
83
+ return;
84
+ }
85
+ for (const section of sections) {
86
+ const existing = document.sections.find((candidate) => (path.resolve(candidate.range.start.file) === path.resolve(section.range.start.file)
87
+ && candidate.range.start.line === section.range.start.line
88
+ && candidate.title === section.title));
89
+ if (!existing) {
90
+ continue;
91
+ }
92
+ existing.sectname = section.sectname;
93
+ existing.source = "asciidoctor";
94
+ if (!existing.style && section.style) {
95
+ existing.style = section.style;
96
+ }
97
+ }
98
+ }
76
99
  function mergeAsciidoctorBlocks(document, blocks) {
77
100
  if (!blocks.length) {
78
101
  return;
79
102
  }
80
- const authoritativeTypes = new Set(blocks.map((block) => block.type));
103
+ const authoritativeTypes = new Set(blocks
104
+ .filter((block) => ["table", "image", "diagram"].includes(block.type))
105
+ .map((block) => block.type));
81
106
  const authoritativeFiles = new Set(blocks.map((block) => path.resolve(block.range.start.file)));
82
- document.blocks = [
107
+ const merged = [
83
108
  ...document.blocks.filter((block) => (!authoritativeTypes.has(block.type)
84
109
  || !authoritativeFiles.has(path.resolve(block.range.start.file)))),
85
- ...blocks,
86
- ].sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
110
+ ];
111
+ for (const block of blocks) {
112
+ if (!merged.some((candidate) => sameBlock(candidate, block))) {
113
+ merged.push(block);
114
+ }
115
+ }
116
+ document.blocks = merged.sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
87
117
  || a.range.start.line - b.range.start.line
88
118
  || a.range.start.column - b.range.start.column));
89
119
  }
120
+ function sameBlock(a, b) {
121
+ return path.resolve(a.range.start.file) === path.resolve(b.range.start.file)
122
+ && a.range.start.line === b.range.start.line
123
+ && a.range.start.column === b.range.start.column
124
+ && a.type === b.type
125
+ && a.style === b.style;
126
+ }
90
127
  function mergeAsciidoctorReferenceTargets(document, targets) {
91
128
  const byKey = new Map();
92
129
  for (const target of document.referenceTargets) {
package/dist/api/rules.js CHANGED
@@ -90,7 +90,7 @@ function configSources(options) {
90
90
  return sources;
91
91
  }
92
92
  const project = findProjectConfig(options.cwd);
93
- if (project) {
93
+ if (project && !sources.some((source) => path.resolve(source.file) === path.resolve(project))) {
94
94
  sources.push({ kind: "project", file: project });
95
95
  }
96
96
  return sources;
@@ -1,4 +1,5 @@
1
- import type { BlockNode, LintFinding, ReferenceTarget } from "../types.js";
1
+ import type { BlockNode, LintFinding, ReferenceTarget, SectionNode } from "../types.js";
2
2
  export declare function collectAsciidoctorDiagnostics(file: string): LintFinding[];
3
3
  export declare function collectAsciidoctorBlocks(file: string): BlockNode[];
4
+ export declare function collectAsciidoctorSections(file: string): SectionNode[];
4
5
  export declare function collectAsciidoctorReferenceTargets(file: string): ReferenceTarget[];
@@ -2,8 +2,10 @@ import { spawnSync } from "node:child_process";
2
2
  import { createRequire } from "node:module";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
+ import { specialSectionStyles } from "../rules/specialSections.js";
5
6
  const require = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
6
7
  let asciidoctor;
8
+ const specialSectionStyleSet = new Set(specialSectionStyles);
7
9
  export function collectAsciidoctorDiagnostics(file) {
8
10
  if (shouldIsolateAsciidoctor()) {
9
11
  return collectAsciidoctorDiagnosticsInChild(file);
@@ -43,6 +45,24 @@ export function collectAsciidoctorBlocks(file) {
43
45
  return [];
44
46
  }
45
47
  }
48
+ export function collectAsciidoctorSections(file) {
49
+ if (shouldIsolateAsciidoctor()) {
50
+ return collectAsciidoctorSectionsInChild(file);
51
+ }
52
+ try {
53
+ const processor = getAsciidoctor();
54
+ const logger = processor.MemoryLogger.create();
55
+ processor.LoggerManager.setLogger(logger);
56
+ const document = processor.loadFile(file, {
57
+ safe: "unsafe",
58
+ sourcemap: true,
59
+ });
60
+ return sectionsFromDocument(document, file);
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
46
66
  export function collectAsciidoctorReferenceTargets(file) {
47
67
  if (shouldIsolateAsciidoctor()) {
48
68
  return collectAsciidoctorReferenceTargetsInChild(file);
@@ -106,6 +126,27 @@ function collectAsciidoctorBlocksInChild(file) {
106
126
  return [];
107
127
  }
108
128
  }
129
+ function collectAsciidoctorSectionsInChild(file) {
130
+ const asciidoctorEntry = require.resolve("asciidoctor");
131
+ const child = spawnSync(process.execPath, ["-e", isolatedAsciidoctorSectionsScript(), file, asciidoctorEntry], {
132
+ encoding: "utf8",
133
+ env: {
134
+ ...process.env,
135
+ ELECTRON_RUN_AS_NODE: "1",
136
+ ASCIIDOCLINT_ISOLATE_ASCIIDOCTOR: "0",
137
+ },
138
+ maxBuffer: 10 * 1024 * 1024,
139
+ });
140
+ if (child.status !== 0) {
141
+ return [];
142
+ }
143
+ try {
144
+ return JSON.parse(child.stdout);
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ }
109
150
  function collectAsciidoctorReferenceTargetsInChild(file) {
110
151
  const asciidoctorEntry = require.resolve("asciidoctor");
111
152
  const child = spawnSync(process.execPath, ["-e", isolatedAsciidoctorReferenceTargetsScript(), file, asciidoctorEntry], {
@@ -127,6 +168,51 @@ function collectAsciidoctorReferenceTargetsInChild(file) {
127
168
  return [];
128
169
  }
129
170
  }
171
+ function isolatedAsciidoctorSectionsScript() {
172
+ return `
173
+ const path = require("node:path");
174
+ const file = process.argv[1];
175
+ const asciidoctorEntry = process.argv[2];
176
+ delete globalThis.Opal;
177
+ const asciidoctor = require(asciidoctorEntry)();
178
+ const logger = asciidoctor.MemoryLogger.create();
179
+ asciidoctor.LoggerManager.setLogger(logger);
180
+ function pos(file, line, column) {
181
+ return { file, line, column };
182
+ }
183
+ function sectionsFromDocument(document) {
184
+ return (document.findBy({ context: "section" }) || []).flatMap((section) => {
185
+ const location = section.getSourceLocation && section.getSourceLocation();
186
+ const sourceFile = location && location.file ? path.resolve(String(location.file)) : path.resolve(file);
187
+ const line = Number((location && (location.getLineNumber && location.getLineNumber())) || (location && location.lineno) || 1);
188
+ const title = String((section.getTitle && section.getTitle()) || section.title || "");
189
+ if (!sourceFile || !line || !title) {
190
+ return [];
191
+ }
192
+ const sectname = (section.getSectionName && section.getSectionName()) || section.sectname;
193
+ const style = (section.getStyle && section.getStyle()) || undefined;
194
+ return [{
195
+ kind: "section",
196
+ title,
197
+ level: Number((section.getLevel && section.getLevel()) || section.level || 0),
198
+ style: style ? String(style) : undefined,
199
+ sectname: sectname ? String(sectname) : undefined,
200
+ source: "asciidoctor",
201
+ range: { start: pos(sourceFile, line, 1) },
202
+ titleRange: { start: pos(sourceFile, line, 1) },
203
+ children: [],
204
+ blocks: [],
205
+ }];
206
+ });
207
+ }
208
+ try {
209
+ const document = asciidoctor.loadFile(file, { safe: "unsafe", sourcemap: true });
210
+ process.stdout.write(JSON.stringify(sectionsFromDocument(document)));
211
+ } catch {
212
+ process.stdout.write("[]");
213
+ }
214
+ `;
215
+ }
130
216
  function isolatedAsciidoctorScript() {
131
217
  return `
132
218
  const path = require("node:path");
@@ -157,6 +243,7 @@ process.stdout.write(JSON.stringify(findings));
157
243
  `;
158
244
  }
159
245
  function isolatedAsciidoctorBlocksScript() {
246
+ const specialStyles = JSON.stringify(specialSectionStyles);
160
247
  return `
161
248
  const fs = require("node:fs");
162
249
  const path = require("node:path");
@@ -166,6 +253,7 @@ delete globalThis.Opal;
166
253
  const asciidoctor = require(asciidoctorEntry)();
167
254
  const logger = asciidoctor.MemoryLogger.create();
168
255
  asciidoctor.LoggerManager.setLogger(logger);
256
+ const specialSectionStyleSet = new Set(${specialStyles});
169
257
  function pos(file, line, column) {
170
258
  return { file, line, column };
171
259
  }
@@ -187,6 +275,12 @@ function blocksFromDocument(document) {
187
275
  .concat(document.findBy({ context: "listing" }) || [])
188
276
  .concat(document.findBy({ context: "literal" }) || [])
189
277
  .filter((block) => diagramStyles.has(String((block.getStyle && block.getStyle()) || "")));
278
+ const styledNonSections = []
279
+ .concat(document.findBy((block) => {
280
+ const context = String((block.getContext && block.getContext()) || "");
281
+ const style = String((block.getStyle && block.getStyle()) || "");
282
+ return context !== "section" && specialSectionStyleSet.has(style);
283
+ }) || []);
190
284
  return []
191
285
  .concat(tables.map((block) => {
192
286
  const location = block.getSourceLocation && block.getSourceLocation();
@@ -197,6 +291,8 @@ function blocksFromDocument(document) {
197
291
  return {
198
292
  kind: "block",
199
293
  type: "table",
294
+ context: "table",
295
+ source: "asciidoctor",
200
296
  style: block.getStyle && block.getStyle() ? String(block.getStyle()) : undefined,
201
297
  title: block.getTitle && block.getTitle() ? String(block.getTitle()) : undefined,
202
298
  attributes: Object.fromEntries(Object.entries(attributes).filter(([, value]) => ["string", "number", "boolean"].includes(typeof value)).map(([key, value]) => [key, String(value)])),
@@ -206,7 +302,8 @@ function blocksFromDocument(document) {
206
302
  };
207
303
  }))
208
304
  .concat(images.map((block) => blockNode(block, "image")))
209
- .concat(diagrams.map((block) => blockNode(block, "diagram")));
305
+ .concat(diagrams.map((block) => blockNode(block, "diagram")))
306
+ .concat(styledNonSections.map((block) => blockNode(block, blockTypeForContext(String((block.getContext && block.getContext()) || "")))));
210
307
  }
211
308
  function blockNode(block, type) {
212
309
  const location = block.getSourceLocation && block.getSourceLocation();
@@ -216,12 +313,17 @@ function blockNode(block, type) {
216
313
  return {
217
314
  kind: "block",
218
315
  type,
316
+ context: block.getContext && block.getContext() ? String(block.getContext()) : undefined,
317
+ source: "asciidoctor",
219
318
  style: block.getStyle && block.getStyle() ? String(block.getStyle()) : undefined,
220
319
  title: block.getTitle && block.getTitle() ? String(block.getTitle()) : undefined,
221
320
  attributes: Object.fromEntries(Object.entries(attributes).filter(([, value]) => ["string", "number", "boolean"].includes(typeof value)).map(([key, value]) => [key, typeof value === "boolean" ? value : String(value)])),
222
321
  range: { start: pos(sourceFile, startLine, 1) },
223
322
  };
224
323
  }
324
+ function blockTypeForContext(context) {
325
+ return ["paragraph","listing","literal","example","sidebar","quote","table","image","admonition","passthrough","stem"].includes(context) ? context : "unknown";
326
+ }
225
327
  function primitiveNumber(value) {
226
328
  return typeof value === "number" ? value : undefined;
227
329
  }
@@ -286,6 +388,11 @@ function blocksFromDocument(document) {
286
388
  ...(document.findBy?.({ context: "listing" }) ?? []),
287
389
  ...(document.findBy?.({ context: "literal" }) ?? []),
288
390
  ].filter((block) => diagramStyles.has(String(block.getStyle?.() ?? "")));
391
+ const styledNonSections = (document.findBy?.((block) => {
392
+ const context = String(block.getContext?.() ?? "");
393
+ const style = String(block.getStyle?.() ?? "");
394
+ return context !== "section" && specialSectionStyleSet.has(style);
395
+ }) ?? []);
289
396
  return [
290
397
  ...tables.map((block) => {
291
398
  const location = block.getSourceLocation?.();
@@ -299,6 +406,8 @@ function blocksFromDocument(document) {
299
406
  return {
300
407
  kind: "block",
301
408
  type: "table",
409
+ context: "table",
410
+ source: "asciidoctor",
302
411
  style: block.getStyle?.() ? String(block.getStyle()) : undefined,
303
412
  title: block.getTitle?.() ? String(block.getTitle()) : undefined,
304
413
  attributes,
@@ -315,8 +424,40 @@ function blocksFromDocument(document) {
315
424
  }),
316
425
  ...images.map((block) => genericBlockNode(block, "image")),
317
426
  ...diagrams.map((block) => genericBlockNode(block, "diagram")),
427
+ ...styledNonSections.map((block) => genericBlockNode(block, blockTypeForContext(String(block.getContext?.() ?? "")))),
318
428
  ].filter((block) => !!block);
319
429
  }
430
+ function sectionsFromDocument(document, rootFile) {
431
+ return (document.findBy?.({ context: "section" }) ?? [])
432
+ .map((section) => {
433
+ const location = section.getSourceLocation?.();
434
+ const sourceFile = location?.file ? path.resolve(String(location.file)) : path.resolve(rootFile);
435
+ const line = Number(location?.getLineNumber?.() ?? location?.lineno ?? 1);
436
+ const title = String(section.getTitle?.() ?? section.title ?? "");
437
+ if (!sourceFile || !line || !title) {
438
+ return undefined;
439
+ }
440
+ const sectname = section.getSectionName?.() ?? section.sectname;
441
+ const style = section.getStyle?.();
442
+ return {
443
+ kind: "section",
444
+ title,
445
+ level: Number(section.getLevel?.() ?? section.level ?? 0),
446
+ style: style ? String(style) : undefined,
447
+ sectname: sectname ? String(sectname) : undefined,
448
+ source: "asciidoctor",
449
+ range: {
450
+ start: { file: sourceFile, line, column: 1 },
451
+ },
452
+ titleRange: {
453
+ start: { file: sourceFile, line, column: 1 },
454
+ },
455
+ children: [],
456
+ blocks: [],
457
+ };
458
+ })
459
+ .filter((section) => !!section);
460
+ }
320
461
  function referenceTargetsFromDocument(document, rootFile) {
321
462
  const refs = document.getCatalog?.().refs;
322
463
  const map = refs?.$$smap ?? {};
@@ -351,6 +492,8 @@ function genericBlockNode(block, type) {
351
492
  return {
352
493
  kind: "block",
353
494
  type,
495
+ context: block.getContext?.() ? String(block.getContext()) : undefined,
496
+ source: "asciidoctor",
354
497
  style: block.getStyle?.() ? String(block.getStyle()) : undefined,
355
498
  title: block.getTitle?.() ? String(block.getTitle()) : undefined,
356
499
  attributes: primitiveAttributes(block.getAttributes?.() ?? {}),
@@ -359,6 +502,12 @@ function genericBlockNode(block, type) {
359
502
  },
360
503
  };
361
504
  }
505
+ function blockTypeForContext(context) {
506
+ if (["paragraph", "listing", "literal", "example", "sidebar", "quote", "table", "image", "admonition", "passthrough", "stem"].includes(context)) {
507
+ return context;
508
+ }
509
+ return "unknown";
510
+ }
362
511
  function tableInfo(block) {
363
512
  const rows = block.getRows?.();
364
513
  const cells = [
@@ -29,7 +29,7 @@ export function parseDocument(file) {
29
29
  sourceMap: [],
30
30
  expandedLine: 1,
31
31
  };
32
- parseFile(absolute, state, { ...state.attributes });
32
+ parseFile(absolute, state, { ...state.attributes }, []);
33
33
  finalizeXrefDependencies(state);
34
34
  const rootLines = state.files.find((entry) => entry.file === absolute)?.lines ?? [];
35
35
  const dependencies = { records: state.dependencies };
@@ -48,7 +48,7 @@ export function parseDocument(file) {
48
48
  files: state.files,
49
49
  };
50
50
  }
51
- function parseFile(file, state, inheritedAttributes) {
51
+ function parseFile(file, state, inheritedAttributes, sectionStack) {
52
52
  const absolute = path.resolve(file);
53
53
  if (state.visited.has(absolute)) {
54
54
  return { ...inheritedAttributes };
@@ -64,7 +64,6 @@ function parseFile(file, state, inheritedAttributes) {
64
64
  const lines = text.split(/\r?\n/);
65
65
  state.files.push({ file: absolute, lines });
66
66
  const attributes = { ...inheritedAttributes };
67
- const sectionStack = [];
68
67
  let openBlock;
69
68
  const conditionalStack = [];
70
69
  for (let index = 0; index < lines.length; index += 1) {
@@ -158,7 +157,7 @@ function parseFile(file, state, inheritedAttributes) {
158
157
  status,
159
158
  });
160
159
  if (status === "resolved" && isAsciiDocSourceFile(resolved)) {
161
- Object.assign(attributes, parseFile(resolved, state, attributes));
160
+ Object.assign(attributes, parseFile(resolved, state, attributes, sectionStack));
162
161
  }
163
162
  continue;
164
163
  }
@@ -220,6 +219,7 @@ function parseFile(file, state, inheritedAttributes) {
220
219
  kind: "section",
221
220
  title,
222
221
  level,
222
+ style: detectSectionStyle(lines, index),
223
223
  range,
224
224
  titleRange: range,
225
225
  children: [],
@@ -319,7 +319,7 @@ function resolveXref(file, target, state) {
319
319
  if (!targetAnchor) {
320
320
  return { status: "resolved", resolvedTarget: resolvedFile };
321
321
  }
322
- parseFile(resolvedFile, state, { ...state.attributes });
322
+ parseFile(resolvedFile, state, { ...state.attributes }, []);
323
323
  return hasAnchor(state, resolvedFile, targetAnchor)
324
324
  ? { status: "resolved", resolvedTarget: resolvedFile }
325
325
  : { status: "missing" };
@@ -455,6 +455,30 @@ function detectStyle(previousLine) {
455
455
  const content = previous.slice(1, -1);
456
456
  return content.split(",")[0];
457
457
  }
458
+ function detectSectionStyle(lines, headingIndex) {
459
+ for (let index = headingIndex - 1; index >= 0; index -= 1) {
460
+ const line = (lines[index] ?? "").trim();
461
+ if (!line) {
462
+ return undefined;
463
+ }
464
+ if (!line.startsWith("[") || !line.endsWith("]")) {
465
+ return undefined;
466
+ }
467
+ const style = parseStyleName(line.slice(1, -1));
468
+ if (style) {
469
+ return style;
470
+ }
471
+ }
472
+ return undefined;
473
+ }
474
+ function parseStyleName(attributeList) {
475
+ const firstAttribute = attributeList.split(",")[0]?.trim() ?? "";
476
+ if (!firstAttribute || firstAttribute.startsWith("#") || firstAttribute.startsWith(".") || firstAttribute.startsWith("%")) {
477
+ return undefined;
478
+ }
479
+ const style = firstAttribute.match(/^[^#.%=]+/)?.[0]?.trim();
480
+ return style || undefined;
481
+ }
458
482
  function isProtectedOpenBlock(openBlock) {
459
483
  if (!openBlock) {
460
484
  return false;
@@ -13,6 +13,7 @@ export const AD004 = {
13
13
  goodExamples: [{ code: "[cols=\"1,1\"]\n|===\n| one | two\n|===" }],
14
14
  },
15
15
  function: ({ document }, onError) => {
16
+ const reportedNestedTables = new Set();
16
17
  for (const block of document.blocks.filter((entry) => entry.type === "table")) {
17
18
  if (!block.range.end || block.attributes.format || block.attributes.separator) {
18
19
  continue;
@@ -34,6 +35,19 @@ export const AD004 = {
34
35
  fixHelper: "Add the missing cells, remove the extra cells, or make the intended row and column spans explicit.",
35
36
  });
36
37
  }
38
+ for (const finding of findNestedAlternateTableRowIssues(file.lines, block.range.start.line, block.range.end.line)) {
39
+ const key = `${file.file}:${finding.line}`;
40
+ if (reportedNestedTables.has(key)) {
41
+ continue;
42
+ }
43
+ reportedNestedTables.add(key);
44
+ onError({
45
+ severity: "warning",
46
+ message: `Nested table row has ${finding.actual} cell${finding.actual === 1 ? "" : "s"} but declares ${finding.expected} columns`,
47
+ range: { start: { file: file.file, line: finding.line, column: 1 } },
48
+ fixHelper: "Add the missing nested table cell, remove the extra cell marker, or make the intended span explicit so Asciidoctor does not drop cells.",
49
+ });
50
+ }
37
51
  }
38
52
  },
39
53
  };
@@ -56,3 +70,88 @@ function containsNestedDefaultPsvTable(lines, startLine, endLine) {
56
70
  }
57
71
  return false;
58
72
  }
73
+ function findNestedAlternateTableRowIssues(lines, startLine, endLine) {
74
+ const issues = [];
75
+ for (let index = startLine; index < endLine - 1; index += 1) {
76
+ const delimiter = parseNestedTableDelimiter(lines[index] ?? "");
77
+ if (!delimiter || delimiter.marker === "|") {
78
+ continue;
79
+ }
80
+ const expected = findDeclaredColumnCount(lines, index);
81
+ if (!expected) {
82
+ continue;
83
+ }
84
+ const close = findClosingDelimiter(lines, index + 1, endLine - 1, delimiter.marker);
85
+ if (close === undefined) {
86
+ continue;
87
+ }
88
+ issues.push(...findIncompleteRows(lines, index + 1, close, delimiter.marker, expected));
89
+ index = close;
90
+ }
91
+ return issues;
92
+ }
93
+ function parseNestedTableDelimiter(line) {
94
+ const match = line.match(/^\s*([!,/:])={3,}\s*$/);
95
+ return match ? { marker: match[1] ?? "!" } : undefined;
96
+ }
97
+ function findDeclaredColumnCount(lines, delimiterIndex) {
98
+ for (let index = delimiterIndex - 1; index >= 0; index -= 1) {
99
+ const line = lines[index] ?? "";
100
+ if (line.trim() === "") {
101
+ continue;
102
+ }
103
+ const match = line.match(/^\s*\[.*\bcols\s*=\s*(?:"([^"]+)"|'([^']+)'|([^,\]]+)).*]\s*$/);
104
+ if (!match) {
105
+ return undefined;
106
+ }
107
+ return parseColumnSpecCount((match[1] ?? match[2] ?? match[3] ?? "").trim());
108
+ }
109
+ return undefined;
110
+ }
111
+ function parseColumnSpecCount(value) {
112
+ const compact = value.match(/^(\d+)\*$/);
113
+ if (compact) {
114
+ return Math.max(1, Number(compact[1] ?? "1"));
115
+ }
116
+ const numeric = value.match(/^\d+$/);
117
+ if (numeric) {
118
+ return Math.max(1, Number(value));
119
+ }
120
+ const parts = value.split(",");
121
+ return Math.max(1, value.includes(",") ? parts.length : parts.filter(Boolean).length);
122
+ }
123
+ function findClosingDelimiter(lines, startIndex, endIndex, marker) {
124
+ const delimiter = new RegExp(`^\\s*\\${marker}={3,}\\s*$`);
125
+ for (let index = startIndex; index < endIndex; index += 1) {
126
+ if (delimiter.test(lines[index] ?? "")) {
127
+ return index;
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ function findIncompleteRows(lines, startIndex, endIndex, marker, expected) {
133
+ const issues = [];
134
+ let rowCells = 0;
135
+ let rowStartLine;
136
+ for (let index = startIndex; index < endIndex; index += 1) {
137
+ const cells = countNestedTableCells(lines[index] ?? "", marker);
138
+ if (cells === 0) {
139
+ continue;
140
+ }
141
+ rowStartLine ??= index + 1;
142
+ rowCells += cells;
143
+ while (rowCells >= expected) {
144
+ rowCells -= expected;
145
+ rowStartLine = rowCells > 0 ? index + 1 : undefined;
146
+ }
147
+ }
148
+ if (rowCells > 0) {
149
+ issues.push({ line: rowStartLine ?? endIndex, expected, actual: rowCells });
150
+ }
151
+ return issues;
152
+ }
153
+ function countNestedTableCells(line, marker) {
154
+ const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
155
+ const markerPattern = new RegExp(`(?<!\\\\)(?:^|\\s)(?:\\d+\\+)?[a-z]?${escapedMarker}`, "g");
156
+ return [...line.matchAll(markerPattern)].length;
157
+ }
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD009: Rule;