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.
- package/README.md +28 -9
- package/dist/api/lint.js +41 -4
- package/dist/api/rules.js +1 -1
- package/dist/parsers/asciidoctor.d.ts +2 -1
- package/dist/parsers/asciidoctor.js +150 -1
- package/dist/parsers/tolerant.js +29 -5
- package/dist/rules/AD004.js +99 -0
- package/dist/rules/AD009.d.ts +2 -0
- package/dist/rules/AD009.js +80 -0
- package/dist/rules/AD010.js +17 -3
- package/dist/rules/AD011.js +20 -5
- package/dist/rules/AD012.js +14 -2
- package/dist/rules/AD013.js +2 -2
- package/dist/rules/AD016.js +16 -10
- package/dist/rules/AD017.js +4 -4
- package/dist/rules/AD020.js +41 -20
- package/dist/rules/AD025.js +2 -2
- package/dist/rules/AD028.js +77 -10
- package/dist/rules/AD029.js +2 -2
- package/dist/rules/AD039.js +2 -2
- package/dist/rules/AD046.d.ts +2 -0
- package/dist/rules/AD046.js +91 -0
- package/dist/rules/AD047.d.ts +2 -0
- package/dist/rules/AD047.js +47 -0
- package/dist/rules/AD048.d.ts +2 -0
- package/dist/rules/AD048.js +37 -0
- package/dist/rules/AD049.d.ts +2 -0
- package/dist/rules/AD049.js +46 -0
- package/dist/rules/AD050.d.ts +2 -0
- package/dist/rules/AD050.js +46 -0
- package/dist/rules/AD051.d.ts +2 -0
- package/dist/rules/AD051.js +52 -0
- package/dist/rules/AD052.d.ts +2 -0
- package/dist/rules/AD052.js +46 -0
- package/dist/rules/AD053.d.ts +2 -0
- package/dist/rules/AD053.js +46 -0
- package/dist/rules/AD054.d.ts +2 -0
- package/dist/rules/AD054.js +46 -0
- package/dist/rules/AD055.d.ts +2 -0
- package/dist/rules/AD055.js +41 -0
- package/dist/rules/AD056.d.ts +2 -0
- package/dist/rules/AD056.js +49 -0
- package/dist/rules/AD057.d.ts +2 -0
- package/dist/rules/AD057.js +79 -0
- package/dist/rules/AD058.d.ts +2 -0
- package/dist/rules/AD058.js +33 -0
- package/dist/rules/AD059.d.ts +2 -0
- package/dist/rules/AD059.js +99 -0
- package/dist/rules/builtin.js +30 -0
- package/dist/rules/specialSections.d.ts +13 -0
- package/dist/rules/specialSections.js +54 -0
- package/dist/types.d.ts +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/rules/AD009.md +59 -0
- package/docs/rules/AD010.md +12 -3
- package/docs/rules/AD011.md +17 -9
- package/docs/rules/AD012.md +14 -3
- package/docs/rules/AD013.md +3 -3
- package/docs/rules/AD016.md +11 -10
- package/docs/rules/AD017.md +5 -4
- package/docs/rules/AD020.md +57 -11
- package/docs/rules/AD025.md +2 -2
- package/docs/rules/AD028.md +14 -9
- package/docs/rules/AD029.md +2 -2
- package/docs/rules/AD046.md +118 -0
- package/docs/rules/AD047.md +46 -0
- package/docs/rules/AD048.md +43 -0
- package/docs/rules/AD049.md +43 -0
- package/docs/rules/AD050.md +41 -0
- package/docs/rules/AD051.md +49 -0
- package/docs/rules/AD052.md +39 -0
- package/docs/rules/AD053.md +41 -0
- package/docs/rules/AD054.md +40 -0
- package/docs/rules/AD055.md +60 -0
- package/docs/rules/AD056.md +36 -0
- package/docs/rules/AD057.md +44 -0
- package/docs/rules/AD058.md +28 -0
- package/docs/rules/AD059.md +68 -0
- package/docs/rules/rule-necessity.md +22 -7
- package/package.json +1 -1
- package/skills/asciidoclint/SKILL.md +21 -2
- package/skills/asciidoclint/references/feedback.md +8 -5
- package/skills/asciidoclint/references/rule-create.md +13 -2
- 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-
|
|
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
|
|
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
|
-
| `
|
|
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`, `
|
|
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
|
-
| `
|
|
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
|
|
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
|
-
|
|
107
|
+
const merged = [
|
|
83
108
|
...document.blocks.filter((block) => (!authoritativeTypes.has(block.type)
|
|
84
109
|
|| !authoritativeFiles.has(path.resolve(block.range.start.file)))),
|
|
85
|
-
|
|
86
|
-
|
|
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 = [
|
package/dist/parsers/tolerant.js
CHANGED
|
@@ -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;
|
package/dist/rules/AD004.js
CHANGED
|
@@ -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
|
+
}
|