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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/assets/README.md +12 -0
- package/assets/icon.svg +198 -0
- package/assets/logo.svg +203 -0
- package/dist/api/fixes.d.ts +6 -0
- package/dist/api/fixes.js +61 -0
- package/dist/api/lint.d.ts +2 -0
- package/dist/api/lint.js +191 -0
- package/dist/api/rules.d.ts +33 -0
- package/dist/api/rules.js +115 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +86 -0
- package/dist/cli/init-rule.d.ts +7 -0
- package/dist/cli/init-rule.js +74 -0
- package/dist/cli/install-skill.d.ts +10 -0
- package/dist/cli/install-skill.js +37 -0
- package/dist/formatters/json.d.ts +2 -0
- package/dist/formatters/json.js +30 -0
- package/dist/formatters/pretty.d.ts +2 -0
- package/dist/formatters/pretty.js +41 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/parsers/asciidoctor.d.ts +4 -0
- package/dist/parsers/asciidoctor.js +444 -0
- package/dist/parsers/tolerant.d.ts +4 -0
- package/dist/parsers/tolerant.js +528 -0
- package/dist/rules/AD001.d.ts +2 -0
- package/dist/rules/AD001.js +28 -0
- package/dist/rules/AD002.d.ts +2 -0
- package/dist/rules/AD002.js +30 -0
- package/dist/rules/AD003.d.ts +2 -0
- package/dist/rules/AD003.js +28 -0
- package/dist/rules/AD004.d.ts +2 -0
- package/dist/rules/AD004.js +58 -0
- package/dist/rules/AD005.d.ts +2 -0
- package/dist/rules/AD005.js +31 -0
- package/dist/rules/AD006.d.ts +2 -0
- package/dist/rules/AD006.js +53 -0
- package/dist/rules/AD007.d.ts +2 -0
- package/dist/rules/AD007.js +39 -0
- package/dist/rules/AD008.d.ts +2 -0
- package/dist/rules/AD008.js +88 -0
- package/dist/rules/AD010.d.ts +2 -0
- package/dist/rules/AD010.js +39 -0
- package/dist/rules/AD011.d.ts +2 -0
- package/dist/rules/AD011.js +31 -0
- package/dist/rules/AD012.d.ts +2 -0
- package/dist/rules/AD012.js +28 -0
- package/dist/rules/AD013.d.ts +2 -0
- package/dist/rules/AD013.js +43 -0
- package/dist/rules/AD016.d.ts +2 -0
- package/dist/rules/AD016.js +83 -0
- package/dist/rules/AD017.d.ts +2 -0
- package/dist/rules/AD017.js +53 -0
- package/dist/rules/AD019.d.ts +2 -0
- package/dist/rules/AD019.js +58 -0
- package/dist/rules/AD020.d.ts +2 -0
- package/dist/rules/AD020.js +40 -0
- package/dist/rules/AD022.d.ts +2 -0
- package/dist/rules/AD022.js +55 -0
- package/dist/rules/AD023.d.ts +2 -0
- package/dist/rules/AD023.js +59 -0
- package/dist/rules/AD024.d.ts +2 -0
- package/dist/rules/AD024.js +30 -0
- package/dist/rules/AD025.d.ts +2 -0
- package/dist/rules/AD025.js +32 -0
- package/dist/rules/AD026.d.ts +2 -0
- package/dist/rules/AD026.js +26 -0
- package/dist/rules/AD027.d.ts +2 -0
- package/dist/rules/AD027.js +31 -0
- package/dist/rules/AD028.d.ts +2 -0
- package/dist/rules/AD028.js +113 -0
- package/dist/rules/AD029.d.ts +2 -0
- package/dist/rules/AD029.js +46 -0
- package/dist/rules/AD030.d.ts +2 -0
- package/dist/rules/AD030.js +33 -0
- package/dist/rules/AD031.d.ts +2 -0
- package/dist/rules/AD031.js +66 -0
- package/dist/rules/AD032.d.ts +2 -0
- package/dist/rules/AD032.js +81 -0
- package/dist/rules/AD034.d.ts +2 -0
- package/dist/rules/AD034.js +50 -0
- package/dist/rules/AD035.d.ts +2 -0
- package/dist/rules/AD035.js +77 -0
- package/dist/rules/AD036.d.ts +2 -0
- package/dist/rules/AD036.js +34 -0
- package/dist/rules/AD037.d.ts +2 -0
- package/dist/rules/AD037.js +34 -0
- package/dist/rules/AD039.d.ts +2 -0
- package/dist/rules/AD039.js +58 -0
- package/dist/rules/AD040.d.ts +2 -0
- package/dist/rules/AD040.js +56 -0
- package/dist/rules/AD041.d.ts +2 -0
- package/dist/rules/AD041.js +66 -0
- package/dist/rules/AD042.d.ts +2 -0
- package/dist/rules/AD042.js +62 -0
- package/dist/rules/AD043.d.ts +2 -0
- package/dist/rules/AD043.js +30 -0
- package/dist/rules/AD044.d.ts +2 -0
- package/dist/rules/AD044.js +54 -0
- package/dist/rules/AD045.d.ts +2 -0
- package/dist/rules/AD045.js +66 -0
- package/dist/rules/builtin.d.ts +3 -0
- package/dist/rules/builtin.js +81 -0
- package/dist/rules/helpers.d.ts +2 -0
- package/dist/rules/helpers.js +11 -0
- package/dist/rules/registry.d.ts +3 -0
- package/dist/rules/registry.js +34 -0
- package/dist/rules/utils.d.ts +42 -0
- package/dist/rules/utils.js +274 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/package.json +70 -0
- package/skills/asciidoclint/SKILL.md +84 -0
- package/skills/asciidoclint/references/ai-fix-policy.md +11 -0
- package/skills/asciidoclint/references/result-schema.md +22 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { countTableCells } from "./utils.js";
|
|
2
|
+
export const AD004 = {
|
|
3
|
+
id: "AD004",
|
|
4
|
+
alias: "table-cell-count",
|
|
5
|
+
description: "Table source should not contain cells omitted from rendered output",
|
|
6
|
+
tags: ["core", "table"],
|
|
7
|
+
parser: "document",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "A table should not end with incomplete cells that Asciidoctor omits from the rendered output.",
|
|
10
|
+
fixability: "no",
|
|
11
|
+
fixHelper: "Add missing table cells, remove leftover cells, or make intended row and column spans explicit so every source cell appears in the rendered table.",
|
|
12
|
+
badExamples: [{ code: "[cols=\"1,1\"]\n|===\n| one | two | omitted\n|===" }],
|
|
13
|
+
goodExamples: [{ code: "[cols=\"1,1\"]\n|===\n| one | two\n|===" }],
|
|
14
|
+
},
|
|
15
|
+
function: ({ document }, onError) => {
|
|
16
|
+
for (const block of document.blocks.filter((entry) => entry.type === "table")) {
|
|
17
|
+
if (!block.range.end || block.attributes.format || block.attributes.separator) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const file = document.files.find((entry) => entry.file === block.range.start.file);
|
|
21
|
+
if (!file) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (containsNestedDefaultPsvTable(file.lines, block.range.start.line, block.range.end.line)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const sourceCells = countSourceCells(file.lines, block.range.start.line, block.range.end.line);
|
|
28
|
+
const renderedCells = block.table?.renderedCellCount;
|
|
29
|
+
if (renderedCells !== undefined && sourceCells > renderedCells) {
|
|
30
|
+
onError({
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: `Table source has ${sourceCells - renderedCells} cell${sourceCells - renderedCells === 1 ? "" : "s"} that Asciidoctor did not render`,
|
|
33
|
+
range: { start: block.range.start },
|
|
34
|
+
fixHelper: "Add the missing cells, remove the extra cells, or make the intended row and column spans explicit.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
function countSourceCells(lines, startLine, endLine) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
for (let index = startLine; index < endLine - 1; index += 1) {
|
|
43
|
+
const line = lines[index] ?? "";
|
|
44
|
+
if (line.trim() === "|===") {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
count += countTableCells(line);
|
|
48
|
+
}
|
|
49
|
+
return count;
|
|
50
|
+
}
|
|
51
|
+
function containsNestedDefaultPsvTable(lines, startLine, endLine) {
|
|
52
|
+
for (let index = startLine; index < endLine - 1; index += 1) {
|
|
53
|
+
if ((lines[index] ?? "").trim() === "|===") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const AD005 = {
|
|
2
|
+
id: "AD005",
|
|
3
|
+
alias: "explicit-document-title",
|
|
4
|
+
description: "Root documents should provide an explicit document title",
|
|
5
|
+
tags: ["core", "headings"],
|
|
6
|
+
parser: "document",
|
|
7
|
+
docs: {
|
|
8
|
+
summary: "A root AsciiDoc document should have an explicit document title, either as a level-0 title, :doctitle:, :title:, or a wrapper title include.",
|
|
9
|
+
rationale: "Document titles are optional in Asciidoctor article and book documents, but explicit titles make generated metadata and visible document structure more predictable.",
|
|
10
|
+
fixability: "no",
|
|
11
|
+
fixHelper: "Add an explicit '= Title', ':doctitle:', or ':title:' to the root document, or make the wrapper document include a title page as its first include.",
|
|
12
|
+
badExamples: [{ code: "== Overview\n\nContent." }],
|
|
13
|
+
goodExamples: [
|
|
14
|
+
{ code: "= Product Guide\n\n== Overview\n\nContent." },
|
|
15
|
+
{ code: ":doctitle: Product Guide\n\n== Overview\n\nContent." },
|
|
16
|
+
{ code: ":title: Product Guide\n\n== Overview\n\nContent." },
|
|
17
|
+
{ code: ":doctype: book\n\n= Product Guide\n\n= Part One\n\n== Chapter One" },
|
|
18
|
+
{ code: "include::title-page.adoc[]\ninclude::chapter.adoc[]\n\n// title-page.adoc\n= Product Guide" },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
function: ({ document }, onError) => {
|
|
22
|
+
if (!document.sections.some((section) => section.level === 0) && !document.attributes.title && !document.attributes.doctitle) {
|
|
23
|
+
onError({
|
|
24
|
+
severity: "info",
|
|
25
|
+
message: "Document is missing an explicit document title",
|
|
26
|
+
range: { start: { file: document.file, line: 1, column: 1 } },
|
|
27
|
+
fixHelper: "Add a document title such as '= Title' before the first section, or set :title: / :doctitle: in the header.",
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export const AD006 = {
|
|
3
|
+
id: "AD006",
|
|
4
|
+
alias: "included-document-title",
|
|
5
|
+
description: "Included AsciiDoc files should not introduce a level-0 title without level offset",
|
|
6
|
+
tags: ["core", "include", "headings"],
|
|
7
|
+
parser: "document",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "Included files should not introduce article level-0 sections unless the include applies leveloffset or intentionally supplies the root document title.",
|
|
10
|
+
rationale: "A level-0 title inside an article include creates an invalid extra level-0 section unless the including document explicitly offsets section levels. Wrapper documents may intentionally use their first include as the root document title, and book documents may intentionally include level-0 parts.",
|
|
11
|
+
fixability: "no",
|
|
12
|
+
fixHelper: "For article parents, add leveloffset to the include or change the included file heading to the section level expected by the parent. Keep the level-0 heading only for wrapper-title includes or doctype=book part files.",
|
|
13
|
+
badExamples: [{ code: "= Root Title\n\ninclude::chapter.adoc[]\n\n// chapter.adoc\n= Chapter Title" }],
|
|
14
|
+
goodExamples: [
|
|
15
|
+
{ code: "include::chapter.adoc[leveloffset=+1]\n\n// chapter.adoc\n= Chapter Title" },
|
|
16
|
+
{ code: "include::title-page.adoc[]\ninclude::chapter.adoc[]\n\n// title-page.adoc\n= Document Title" },
|
|
17
|
+
{ code: ":doctype: book\n\n= Book Title\n\ninclude::part-one.adoc[]\n\n// part-one.adoc\n= Part One\n\n== Chapter One" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
function: ({ document }, onError) => {
|
|
21
|
+
const includesWithoutLevelOffset = document.includes
|
|
22
|
+
.filter((include) => include.status === "resolved" && include.resolvedTarget)
|
|
23
|
+
.filter((include) => include.attributes.leveloffset === undefined)
|
|
24
|
+
.map((include) => ({ ...include, resolvedTarget: path.resolve(include.resolvedTarget) }));
|
|
25
|
+
for (const section of document.sections) {
|
|
26
|
+
const include = includesWithoutLevelOffset.find((record) => record.resolvedTarget === path.resolve(section.range.start.file));
|
|
27
|
+
if (section.level === 0
|
|
28
|
+
&& include
|
|
29
|
+
&& document.attributes.doctype !== "book"
|
|
30
|
+
&& !isRootTitleInclude(document.file, include, section, document.sections)) {
|
|
31
|
+
onError({
|
|
32
|
+
severity: "error",
|
|
33
|
+
message: "Included file starts with a level-0 document title",
|
|
34
|
+
range: section.range,
|
|
35
|
+
fixHelper: "Add leveloffset to the include directive in the parent file or change this included heading to the section level expected by the parent.",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
function isRootTitleInclude(rootFile, include, section, sections) {
|
|
42
|
+
const root = path.resolve(rootFile);
|
|
43
|
+
if (path.resolve(include.range.start.file) !== root) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const firstLevelZero = sections.find((candidate) => candidate.level === 0);
|
|
47
|
+
if (!firstLevelZero || path.resolve(firstLevelZero.range.start.file) !== path.resolve(section.range.start.file)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return !sections.some((candidate) => candidate.level === 0
|
|
51
|
+
&& path.resolve(candidate.range.start.file) === root
|
|
52
|
+
&& candidate.range.start.line < include.range.start.line);
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { isLineInProtectedBlock } from "./utils.js";
|
|
2
|
+
export const AD007 = {
|
|
3
|
+
id: "AD007",
|
|
4
|
+
alias: "heading-depth-limit",
|
|
5
|
+
description: "Section headings should not exceed Asciidoctor's supported depth",
|
|
6
|
+
tags: ["core", "headings"],
|
|
7
|
+
parser: "text",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "AsciiDoc section heading markers should stay within Asciidoctor's supported level range.",
|
|
10
|
+
rationale: "Asciidoctor only recognizes section levels 0 through 5. Deeper heading-like lines render as paragraph text, so the intended section disappears from the document outline.",
|
|
11
|
+
fixability: "no",
|
|
12
|
+
fixHelper: "Reduce the heading marker to six or fewer = / # characters, or split the content into a separate document if the hierarchy is too deep.",
|
|
13
|
+
badExamples: [{ code: "= Title\n\n======= Too Deep" }],
|
|
14
|
+
goodExamples: [{ code: "= Title\n\n== Parent\n\n=== Child\n\n==== Level 3\n\n===== Level 4\n\n====== Deepest Supported" }],
|
|
15
|
+
},
|
|
16
|
+
function: ({ document }, onError) => {
|
|
17
|
+
for (const file of document.files) {
|
|
18
|
+
for (let index = 0; index < file.lines.length; index += 1) {
|
|
19
|
+
const line = file.lines[index] ?? "";
|
|
20
|
+
if (isLineInProtectedBlock(document, file.file, index + 1)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const marker = unsupportedHeadingMarker(line);
|
|
24
|
+
if (!marker) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
onError({
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `Heading marker ${marker} exceeds Asciidoctor's supported section depth`,
|
|
30
|
+
range: { start: { file: file.file, line: index + 1, column: 1 } },
|
|
31
|
+
fixHelper: "Reduce the heading depth to level 5 or less, or split the content into a separate document.",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
function unsupportedHeadingMarker(line) {
|
|
38
|
+
return line.match(/^(={7,}|#{7,})\s+\S/)?.[1];
|
|
39
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { isExemptBeforeListLine, isLineComment, isLineInProtectedBlock, isLineInTableBlock, isListMarkerLine } from "./utils.js";
|
|
2
|
+
export const AD008 = {
|
|
3
|
+
id: "AD008",
|
|
4
|
+
alias: "blank-before-list",
|
|
5
|
+
description: "Lists should be separated from preceding paragraph text",
|
|
6
|
+
tags: ["core", "lists", "blank_lines"],
|
|
7
|
+
parser: "text",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "A list should have a blank line before the first list item unless it follows another structural line.",
|
|
10
|
+
rationale: "Without a blank line, Asciidoctor treats a would-be list marker after paragraph text as paragraph continuation text, so the intended list is not created.",
|
|
11
|
+
fixability: "safe",
|
|
12
|
+
fixHelper: "Insert one blank line before the list marker.",
|
|
13
|
+
badExamples: [{ code: "Paragraph\n* item" }],
|
|
14
|
+
goodExamples: [{ code: "Paragraph\n\n* item" }],
|
|
15
|
+
},
|
|
16
|
+
function: ({ document }, onError) => {
|
|
17
|
+
for (const file of document.files) {
|
|
18
|
+
for (let index = 1; index < file.lines.length; index += 1) {
|
|
19
|
+
const line = file.lines[index] ?? "";
|
|
20
|
+
if (isLineComment(line)
|
|
21
|
+
|| !isListMarkerLine(line)
|
|
22
|
+
|| isIndentedListMarkerLine(line)
|
|
23
|
+
|| isLineInProtectedBlock(document, file.file, index + 1)
|
|
24
|
+
|| isLineInTableBlock(document, file.file, index + 1)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const previous = file.lines[index - 1] ?? "";
|
|
28
|
+
if (isExemptBeforeListLine(previous)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (hasTableCellContextBefore(file.lines, index)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (hasContinuationContextBefore(file.lines, index)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const lineNumber = index + 1;
|
|
38
|
+
onError({
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message: "List should be preceded by a blank line",
|
|
41
|
+
range: { start: { file: file.file, line: lineNumber, column: 1 } },
|
|
42
|
+
fix: {
|
|
43
|
+
applicability: "safe",
|
|
44
|
+
edits: [{
|
|
45
|
+
file: file.file,
|
|
46
|
+
range: {
|
|
47
|
+
start: { file: file.file, line: lineNumber, column: 1 },
|
|
48
|
+
end: { file: file.file, line: lineNumber, column: 1 },
|
|
49
|
+
},
|
|
50
|
+
replacement: "\n",
|
|
51
|
+
}],
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
function hasContinuationContextBefore(lines, index) {
|
|
59
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
60
|
+
const trimmed = (lines[cursor] ?? "").trim();
|
|
61
|
+
if (trimmed === "") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (trimmed === "+") {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (isListMarkerLine(lines[cursor] ?? "")) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
function isIndentedListMarkerLine(line) {
|
|
74
|
+
return /^\s+\S/.test(line);
|
|
75
|
+
}
|
|
76
|
+
function hasTableCellContextBefore(lines, index) {
|
|
77
|
+
for (let cursor = index - 1; cursor >= 0; cursor -= 1) {
|
|
78
|
+
const trimmed = (lines[cursor] ?? "").trim();
|
|
79
|
+
if (trimmed === "") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (/^a\|/.test(trimmed) || /(^|\s)a\|\s*$/.test(trimmed) || /\sa\|\s*\S*$/.test(trimmed)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { hasTitleImmediatelyBefore } from "./utils.js";
|
|
2
|
+
export const AD010 = {
|
|
3
|
+
id: "AD010",
|
|
4
|
+
alias: "table-title",
|
|
5
|
+
description: "Table blocks should have a title",
|
|
6
|
+
tags: ["core", "table"],
|
|
7
|
+
parser: "document",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "A block table should have a preceding .Title line.",
|
|
10
|
+
rationale: "Table titles make rendered tables referenceable and easier to review.",
|
|
11
|
+
fixability: "no",
|
|
12
|
+
fixHelper: "Add a table title using .Title immediately before the table metadata, or set title=\"...\" in the table attribute list.",
|
|
13
|
+
badExamples: [{ code: "|===\n| A | B\n|===" }],
|
|
14
|
+
goodExamples: [{ code: ".Register fields\n|===\n| A | B\n|===" }],
|
|
15
|
+
},
|
|
16
|
+
function: ({ document }, onError) => {
|
|
17
|
+
const files = new Map(document.files.map((file) => [file.file, file.lines]));
|
|
18
|
+
for (const block of document.blocks.filter((entry) => entry.type === "table")) {
|
|
19
|
+
const lines = files.get(block.range.start.file);
|
|
20
|
+
if (!lines) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const index = block.range.start.line - 1;
|
|
24
|
+
if (isTableCellLine(lines[index - 1] ?? "") || block.title || hasTitleImmediatelyBefore(lines, index)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
onError({
|
|
28
|
+
severity: "warning",
|
|
29
|
+
message: "Table block should have a title",
|
|
30
|
+
range: { start: { file: block.range.start.file, line: block.range.start.line, column: 1 } },
|
|
31
|
+
fixHelper: "Add a .Title line immediately before the table.",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
function isTableCellLine(line) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
return trimmed.startsWith("|") && trimmed !== "|===";
|
|
39
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const AD011 = {
|
|
2
|
+
id: "AD011",
|
|
3
|
+
alias: "image-title",
|
|
4
|
+
description: "Block images should have a title",
|
|
5
|
+
tags: ["core", "image"],
|
|
6
|
+
parser: "document",
|
|
7
|
+
docs: {
|
|
8
|
+
summary: "A block image should have a preceding .Title line.",
|
|
9
|
+
rationale: "Image titles preserve figure intent and make generated output easier to navigate.",
|
|
10
|
+
fixability: "no",
|
|
11
|
+
fixHelper: "Add a figure title using .Title immediately before the image, or set title=\"...\" in the block image macro.",
|
|
12
|
+
badExamples: [{ code: "image::diagram.png[Architecture]" }],
|
|
13
|
+
goodExamples: [{ code: ".Architecture overview\nimage::diagram.png[Architecture]" }],
|
|
14
|
+
},
|
|
15
|
+
function: ({ document }, onError) => {
|
|
16
|
+
for (const block of document.blocks.filter((entry) => entry.type === "image")) {
|
|
17
|
+
if (block.title || hasExemptImageRole(block.attributes.role)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
onError({
|
|
21
|
+
severity: "warning",
|
|
22
|
+
message: "Block image should have a title",
|
|
23
|
+
range: { start: block.range.start },
|
|
24
|
+
fixHelper: "Add a .Title line, [title=\"...\"], or a title attribute to the image macro.",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
function hasExemptImageRole(role) {
|
|
30
|
+
return typeof role === "string" && /\bcover-image\b/i.test(role);
|
|
31
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const AD012 = {
|
|
2
|
+
id: "AD012",
|
|
3
|
+
alias: "diagram-title",
|
|
4
|
+
description: "Diagram blocks should have a title",
|
|
5
|
+
tags: ["core", "diagram"],
|
|
6
|
+
parser: "document",
|
|
7
|
+
docs: {
|
|
8
|
+
summary: "A diagram block should have a preceding .Title line.",
|
|
9
|
+
rationale: "Diagram titles make generated figures understandable and referenceable.",
|
|
10
|
+
fixability: "no",
|
|
11
|
+
fixHelper: "Add a diagram title using .Title immediately before the diagram block metadata, or set title=\"...\" in the diagram attribute list.",
|
|
12
|
+
badExamples: [{ code: "[mermaid]\n----\ngraph LR\n----" }],
|
|
13
|
+
goodExamples: [{ code: ".System flow\n[mermaid]\n----\ngraph LR\n----" }],
|
|
14
|
+
},
|
|
15
|
+
function: ({ document }, onError) => {
|
|
16
|
+
for (const block of document.blocks.filter((entry) => entry.type === "diagram")) {
|
|
17
|
+
if (block.title) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
onError({
|
|
21
|
+
severity: "warning",
|
|
22
|
+
message: "Diagram block should have a title",
|
|
23
|
+
range: { start: block.range.start },
|
|
24
|
+
fixHelper: "Add a .Title line or title=\"...\" metadata to the diagram block.",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isLineInProtectedBlock } from "./utils.js";
|
|
2
|
+
export const AD013 = {
|
|
3
|
+
id: "AD013",
|
|
4
|
+
alias: "standalone-inline-image",
|
|
5
|
+
description: "Standalone image macros should use block image syntax",
|
|
6
|
+
tags: ["core", "image"],
|
|
7
|
+
parser: "text",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "A line containing only an inline image macro should be written as a block image macro.",
|
|
10
|
+
rationale: "Standalone inline image macros can render with paragraph spacing and caption behavior that differs from figures.",
|
|
11
|
+
fixability: "unsafe",
|
|
12
|
+
fixHelper: "Use image::target[alt] for standalone figures, and reserve image:target[alt] for inline images inside text.",
|
|
13
|
+
badExamples: [{ code: "image:diagram.png[Architecture]" }],
|
|
14
|
+
goodExamples: [{ code: "image::diagram.png[Architecture]" }],
|
|
15
|
+
},
|
|
16
|
+
function: ({ document }, onError) => {
|
|
17
|
+
for (const file of document.files) {
|
|
18
|
+
for (const [index, line] of file.lines.entries()) {
|
|
19
|
+
const match = line.match(/^image:(?!:)(\S.*\[[^\]]*]\s*)$/);
|
|
20
|
+
if (!match || isLineInProtectedBlock(document, file.file, index + 1)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const lineNumber = index + 1;
|
|
24
|
+
onError({
|
|
25
|
+
severity: "warning",
|
|
26
|
+
message: "Standalone image macro should use block image syntax",
|
|
27
|
+
range: { start: { file: file.file, line: lineNumber, column: 1 } },
|
|
28
|
+
fix: {
|
|
29
|
+
applicability: "unsafe",
|
|
30
|
+
edits: [{
|
|
31
|
+
file: file.file,
|
|
32
|
+
range: {
|
|
33
|
+
start: { file: file.file, line: lineNumber, column: 7 },
|
|
34
|
+
end: { file: file.file, line: lineNumber, column: 7 },
|
|
35
|
+
},
|
|
36
|
+
replacement: ":",
|
|
37
|
+
}],
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { isLineInProtectedBlock } from "./utils.js";
|
|
2
|
+
const captionPattern = /^Figure\s+\d+(?:[-.]\d+)?[:.]\s+(\S.*)$/i;
|
|
3
|
+
export const AD016 = {
|
|
4
|
+
id: "AD016",
|
|
5
|
+
alias: "malformed-figure-caption",
|
|
6
|
+
description: "Figure captions should use AsciiDoc title syntax",
|
|
7
|
+
tags: ["core", "image", "diagram"],
|
|
8
|
+
parser: "document",
|
|
9
|
+
docs: {
|
|
10
|
+
summary: "A figure caption line adjacent to an image or diagram should be an AsciiDoc block title.",
|
|
11
|
+
rationale: "Plain Figure lines imported from DOCX or Markdown do not become AsciiDoc block titles.",
|
|
12
|
+
badExamples: [{ code: "Figure 1: Architecture\nimage::architecture.png[Architecture]" }],
|
|
13
|
+
goodExamples: [{ code: ".Architecture\nimage::architecture.png[Architecture]" }],
|
|
14
|
+
fixability: "no",
|
|
15
|
+
fixHelper: "Move the caption immediately before the image or diagram and convert it to a .Title line without the generated Figure number.",
|
|
16
|
+
},
|
|
17
|
+
function: ({ document }, onError) => {
|
|
18
|
+
const files = new Map(document.files.map((file) => [file.file, file.lines]));
|
|
19
|
+
for (const file of document.files) {
|
|
20
|
+
for (const [index, line] of file.lines.entries()) {
|
|
21
|
+
if (!line.trim().startsWith("image::") || isLineInProtectedBlock(document, file.file, index + 1)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const before = findCaptionBefore(file.lines, index);
|
|
25
|
+
if (before !== undefined) {
|
|
26
|
+
reportCaption(onError, file.file, before + 1, file.lines[before] ?? "", "before");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const block of document.blocks.filter((entry) => entry.type === "image" || entry.type === "diagram")) {
|
|
31
|
+
const lines = files.get(block.range.start.file);
|
|
32
|
+
if (!lines) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const before = findCaptionBefore(lines, block.range.start.line - 1, block.type === "diagram");
|
|
36
|
+
if (before !== undefined) {
|
|
37
|
+
reportCaption(onError, block.range.start.file, before + 1, lines[before] ?? "", "before");
|
|
38
|
+
}
|
|
39
|
+
const after = findCaptionAfter(lines, block.range.end?.line ?? findDelimitedBlockEnd(lines, block.range.start.line - 1));
|
|
40
|
+
if (after !== undefined) {
|
|
41
|
+
reportCaption(onError, block.range.start.file, after + 1, lines[after] ?? "", "after");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
function findCaptionBefore(lines, blockStartIndex, allowDiagramStyleLine = false) {
|
|
47
|
+
let cursor = blockStartIndex - 1;
|
|
48
|
+
if (allowDiagramStyleLine && isDiagramStyleLine(lines[cursor] ?? "")) {
|
|
49
|
+
cursor -= 1;
|
|
50
|
+
}
|
|
51
|
+
return captionPattern.test((lines[cursor] ?? "").trim()) ? cursor : undefined;
|
|
52
|
+
}
|
|
53
|
+
function findCaptionAfter(lines, blockEndLine) {
|
|
54
|
+
if (!blockEndLine) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const index = blockEndLine;
|
|
58
|
+
return captionPattern.test((lines[index] ?? "").trim()) ? index : undefined;
|
|
59
|
+
}
|
|
60
|
+
function findDelimitedBlockEnd(lines, blockStartIndex) {
|
|
61
|
+
const delimiter = lines[blockStartIndex]?.trim();
|
|
62
|
+
if (!delimiter || !["----", "....", "++++", "____", "===="].includes(delimiter)) {
|
|
63
|
+
return blockStartIndex + 1;
|
|
64
|
+
}
|
|
65
|
+
for (let index = blockStartIndex + 1; index < lines.length; index += 1) {
|
|
66
|
+
if ((lines[index] ?? "").trim() === delimiter) {
|
|
67
|
+
return index + 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return blockStartIndex + 1;
|
|
71
|
+
}
|
|
72
|
+
function isDiagramStyleLine(line) {
|
|
73
|
+
return /^\[[^\]]+\]\s*$/.test(line.trim());
|
|
74
|
+
}
|
|
75
|
+
function reportCaption(onError, file, line, caption, position) {
|
|
76
|
+
const title = caption.trim().match(captionPattern)?.[1] ?? "caption text";
|
|
77
|
+
onError({
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: `Figure caption should use AsciiDoc title syntax${position === "after" ? " before the figure" : ""}`,
|
|
80
|
+
range: { start: { file, line, column: 1 } },
|
|
81
|
+
fixHelper: `Use .${title} immediately before the image or diagram so Asciidoctor generates the Figure number.`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const captionPattern = /^Table\s+\d+(?:[-.]\d+)?[:.]\s+(\S.*)$/i;
|
|
2
|
+
export const AD017 = {
|
|
3
|
+
id: "AD017",
|
|
4
|
+
alias: "malformed-table-caption",
|
|
5
|
+
description: "Table captions should use AsciiDoc title syntax",
|
|
6
|
+
tags: ["core", "table"],
|
|
7
|
+
parser: "document",
|
|
8
|
+
docs: {
|
|
9
|
+
summary: "A table caption line adjacent to a table should be an AsciiDoc block title.",
|
|
10
|
+
rationale: "Plain Table lines imported from DOCX or Markdown do not become AsciiDoc block titles.",
|
|
11
|
+
badExamples: [{ code: "Table 1: Registers\n|===\n| Name | Value\n|===" }],
|
|
12
|
+
goodExamples: [{ code: ".Registers\n|===\n| Name | Value\n|===" }],
|
|
13
|
+
fixability: "no",
|
|
14
|
+
fixHelper: "Move the caption immediately before the table and convert it to a .Title line without the generated Table number.",
|
|
15
|
+
},
|
|
16
|
+
function: ({ document }, onError) => {
|
|
17
|
+
const files = new Map(document.files.map((file) => [file.file, file.lines]));
|
|
18
|
+
for (const block of document.blocks.filter((entry) => entry.type === "table")) {
|
|
19
|
+
const lines = files.get(block.range.start.file);
|
|
20
|
+
if (!lines) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const before = findCaptionBefore(lines, block.range.start.line - 1);
|
|
24
|
+
if (before !== undefined) {
|
|
25
|
+
reportCaption(onError, block.range.start.file, before + 1, lines[before] ?? "", "before");
|
|
26
|
+
}
|
|
27
|
+
const after = findCaptionAfter(lines, block.range.end?.line);
|
|
28
|
+
if (after !== undefined) {
|
|
29
|
+
reportCaption(onError, block.range.start.file, after + 1, lines[after] ?? "", "after");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
function findCaptionBefore(lines, blockStartIndex) {
|
|
35
|
+
const cursor = blockStartIndex - 1;
|
|
36
|
+
return captionPattern.test((lines[cursor] ?? "").trim()) ? cursor : undefined;
|
|
37
|
+
}
|
|
38
|
+
function findCaptionAfter(lines, blockEndLine) {
|
|
39
|
+
if (!blockEndLine) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const index = blockEndLine;
|
|
43
|
+
return captionPattern.test((lines[index] ?? "").trim()) ? index : undefined;
|
|
44
|
+
}
|
|
45
|
+
function reportCaption(onError, file, line, caption, position) {
|
|
46
|
+
const title = caption.trim().match(captionPattern)?.[1] ?? "caption text";
|
|
47
|
+
onError({
|
|
48
|
+
severity: "warning",
|
|
49
|
+
message: `Table caption should use AsciiDoc title syntax${position === "after" ? " before the table" : ""}`,
|
|
50
|
+
range: { start: { file, line, column: 1 } },
|
|
51
|
+
fixHelper: `Use .${title} immediately before the table so Asciidoctor generates the Table number.`,
|
|
52
|
+
});
|
|
53
|
+
}
|