asciidoclint 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/assets/README.md +12 -0
  4. package/assets/icon.svg +198 -0
  5. package/assets/logo.svg +203 -0
  6. package/dist/api/fixes.d.ts +6 -0
  7. package/dist/api/fixes.js +61 -0
  8. package/dist/api/lint.d.ts +2 -0
  9. package/dist/api/lint.js +191 -0
  10. package/dist/api/rules.d.ts +33 -0
  11. package/dist/api/rules.js +115 -0
  12. package/dist/cli/index.d.ts +2 -0
  13. package/dist/cli/index.js +86 -0
  14. package/dist/cli/init-rule.d.ts +7 -0
  15. package/dist/cli/init-rule.js +74 -0
  16. package/dist/cli/install-skill.d.ts +10 -0
  17. package/dist/cli/install-skill.js +37 -0
  18. package/dist/formatters/json.d.ts +2 -0
  19. package/dist/formatters/json.js +30 -0
  20. package/dist/formatters/pretty.d.ts +2 -0
  21. package/dist/formatters/pretty.js +41 -0
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.js +3 -0
  24. package/dist/parsers/asciidoctor.d.ts +4 -0
  25. package/dist/parsers/asciidoctor.js +444 -0
  26. package/dist/parsers/tolerant.d.ts +4 -0
  27. package/dist/parsers/tolerant.js +528 -0
  28. package/dist/rules/AD001.d.ts +2 -0
  29. package/dist/rules/AD001.js +28 -0
  30. package/dist/rules/AD002.d.ts +2 -0
  31. package/dist/rules/AD002.js +30 -0
  32. package/dist/rules/AD003.d.ts +2 -0
  33. package/dist/rules/AD003.js +28 -0
  34. package/dist/rules/AD004.d.ts +2 -0
  35. package/dist/rules/AD004.js +58 -0
  36. package/dist/rules/AD005.d.ts +2 -0
  37. package/dist/rules/AD005.js +31 -0
  38. package/dist/rules/AD006.d.ts +2 -0
  39. package/dist/rules/AD006.js +53 -0
  40. package/dist/rules/AD007.d.ts +2 -0
  41. package/dist/rules/AD007.js +39 -0
  42. package/dist/rules/AD008.d.ts +2 -0
  43. package/dist/rules/AD008.js +88 -0
  44. package/dist/rules/AD010.d.ts +2 -0
  45. package/dist/rules/AD010.js +39 -0
  46. package/dist/rules/AD011.d.ts +2 -0
  47. package/dist/rules/AD011.js +31 -0
  48. package/dist/rules/AD012.d.ts +2 -0
  49. package/dist/rules/AD012.js +28 -0
  50. package/dist/rules/AD013.d.ts +2 -0
  51. package/dist/rules/AD013.js +43 -0
  52. package/dist/rules/AD016.d.ts +2 -0
  53. package/dist/rules/AD016.js +83 -0
  54. package/dist/rules/AD017.d.ts +2 -0
  55. package/dist/rules/AD017.js +53 -0
  56. package/dist/rules/AD019.d.ts +2 -0
  57. package/dist/rules/AD019.js +58 -0
  58. package/dist/rules/AD020.d.ts +2 -0
  59. package/dist/rules/AD020.js +40 -0
  60. package/dist/rules/AD022.d.ts +2 -0
  61. package/dist/rules/AD022.js +55 -0
  62. package/dist/rules/AD023.d.ts +2 -0
  63. package/dist/rules/AD023.js +59 -0
  64. package/dist/rules/AD024.d.ts +2 -0
  65. package/dist/rules/AD024.js +30 -0
  66. package/dist/rules/AD025.d.ts +2 -0
  67. package/dist/rules/AD025.js +32 -0
  68. package/dist/rules/AD026.d.ts +2 -0
  69. package/dist/rules/AD026.js +26 -0
  70. package/dist/rules/AD027.d.ts +2 -0
  71. package/dist/rules/AD027.js +31 -0
  72. package/dist/rules/AD028.d.ts +2 -0
  73. package/dist/rules/AD028.js +113 -0
  74. package/dist/rules/AD029.d.ts +2 -0
  75. package/dist/rules/AD029.js +46 -0
  76. package/dist/rules/AD030.d.ts +2 -0
  77. package/dist/rules/AD030.js +33 -0
  78. package/dist/rules/AD031.d.ts +2 -0
  79. package/dist/rules/AD031.js +66 -0
  80. package/dist/rules/AD032.d.ts +2 -0
  81. package/dist/rules/AD032.js +81 -0
  82. package/dist/rules/AD034.d.ts +2 -0
  83. package/dist/rules/AD034.js +50 -0
  84. package/dist/rules/AD035.d.ts +2 -0
  85. package/dist/rules/AD035.js +77 -0
  86. package/dist/rules/AD036.d.ts +2 -0
  87. package/dist/rules/AD036.js +34 -0
  88. package/dist/rules/AD037.d.ts +2 -0
  89. package/dist/rules/AD037.js +34 -0
  90. package/dist/rules/AD039.d.ts +2 -0
  91. package/dist/rules/AD039.js +58 -0
  92. package/dist/rules/AD040.d.ts +2 -0
  93. package/dist/rules/AD040.js +56 -0
  94. package/dist/rules/AD041.d.ts +2 -0
  95. package/dist/rules/AD041.js +66 -0
  96. package/dist/rules/AD042.d.ts +2 -0
  97. package/dist/rules/AD042.js +62 -0
  98. package/dist/rules/AD043.d.ts +2 -0
  99. package/dist/rules/AD043.js +30 -0
  100. package/dist/rules/AD044.d.ts +2 -0
  101. package/dist/rules/AD044.js +54 -0
  102. package/dist/rules/AD045.d.ts +2 -0
  103. package/dist/rules/AD045.js +66 -0
  104. package/dist/rules/builtin.d.ts +3 -0
  105. package/dist/rules/builtin.js +81 -0
  106. package/dist/rules/helpers.d.ts +2 -0
  107. package/dist/rules/helpers.js +11 -0
  108. package/dist/rules/registry.d.ts +3 -0
  109. package/dist/rules/registry.js +34 -0
  110. package/dist/rules/utils.d.ts +42 -0
  111. package/dist/rules/utils.js +274 -0
  112. package/dist/types.d.ts +166 -0
  113. package/dist/types.js +1 -0
  114. package/dist/version.d.ts +2 -0
  115. package/dist/version.js +4 -0
  116. package/package.json +70 -0
  117. package/skills/asciidoclint/SKILL.md +84 -0
  118. package/skills/asciidoclint/references/ai-fix-policy.md +11 -0
  119. package/skills/asciidoclint/references/result-schema.md +22 -0
@@ -0,0 +1,2 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare const AD045: Rule;
@@ -0,0 +1,66 @@
1
+ import { isLineInProtectedBlock } from "./utils.js";
2
+ const headingPattern = /^(=+|#+)\s+\S/;
3
+ export const AD045 = {
4
+ id: "AD045",
5
+ alias: "markdown-heading-mix",
6
+ description: "Markdown-compatible headings should not be mixed with AsciiDoc headings",
7
+ tags: ["headings", "markdown-compatibility", "maintainability"],
8
+ parser: "document",
9
+ docs: {
10
+ summary: "Do not mix AsciiDoc = headings and Markdown-compatible # headings in one document graph.",
11
+ rationale: "Asciidoctor accepts Markdown-style headings as optional compatibility syntax, so using them is not invalid. Mixing both marker styles in one document tree makes hierarchy review and style maintenance harder.",
12
+ fixability: "unsafe",
13
+ fixHelper: "Convert the less common heading marker style to the dominant style in the document graph, preserving the marker length.",
14
+ badExamples: [{ code: "= Title\n\n== Overview\n\n## Markdown-style Section" }],
15
+ goodExamples: [
16
+ { code: "= Title\n\n== Overview\n\n== AsciiDoc-style Section" },
17
+ { code: "## Overview\n\n## Markdown-style Section" },
18
+ ],
19
+ },
20
+ function: ({ document }, onError) => {
21
+ const headings = [];
22
+ for (const file of document.files) {
23
+ for (const [index, line] of file.lines.entries()) {
24
+ if (isLineInProtectedBlock(document, file.file, index + 1)) {
25
+ continue;
26
+ }
27
+ const match = line.match(headingPattern);
28
+ if (!match) {
29
+ continue;
30
+ }
31
+ const marker = match[1] ?? "";
32
+ headings.push({
33
+ style: marker.startsWith("#") ? "markdown" : "asciidoc",
34
+ marker,
35
+ range: {
36
+ start: { file: file.file, line: index + 1, column: 1 },
37
+ end: { file: file.file, line: index + 1, column: marker.length + 1 },
38
+ },
39
+ });
40
+ }
41
+ }
42
+ const asciidocCount = headings.filter((heading) => heading.style === "asciidoc").length;
43
+ const markdownCount = headings.filter((heading) => heading.style === "markdown").length;
44
+ if (asciidocCount === 0 || markdownCount === 0) {
45
+ return;
46
+ }
47
+ const dominant = asciidocCount >= markdownCount ? "asciidoc" : "markdown";
48
+ for (const heading of headings.filter((candidate) => candidate.style !== dominant)) {
49
+ const replacementMarker = dominant === "asciidoc" ? "=".repeat(heading.marker.length) : "#".repeat(heading.marker.length);
50
+ onError({
51
+ severity: "info",
52
+ message: `Markdown-compatible heading style is mixed with AsciiDoc heading style: ${heading.marker}`,
53
+ range: heading.range,
54
+ fixHelper: `Use ${replacementMarker} to match the dominant ${dominant === "asciidoc" ? "AsciiDoc" : "Markdown-compatible"} heading style in this document graph.`,
55
+ fix: {
56
+ applicability: "unsafe",
57
+ edits: [{
58
+ file: heading.range.start.file,
59
+ range: heading.range,
60
+ replacement: replacementMarker,
61
+ }],
62
+ },
63
+ });
64
+ }
65
+ },
66
+ };
@@ -0,0 +1,3 @@
1
+ import type { LintFinding, Rule } from "../types.js";
2
+ export declare const builtInRules: Rule[];
3
+ export declare function ruleLabel(finding: Pick<LintFinding, "ruleId" | "alias">): string;
@@ -0,0 +1,81 @@
1
+ import { AD001 } from "./AD001.js";
2
+ import { AD002 } from "./AD002.js";
3
+ import { AD003 } from "./AD003.js";
4
+ import { AD004 } from "./AD004.js";
5
+ import { AD005 } from "./AD005.js";
6
+ import { AD006 } from "./AD006.js";
7
+ import { AD007 } from "./AD007.js";
8
+ import { AD008 } from "./AD008.js";
9
+ import { AD010 } from "./AD010.js";
10
+ import { AD011 } from "./AD011.js";
11
+ import { AD012 } from "./AD012.js";
12
+ import { AD013 } from "./AD013.js";
13
+ import { AD016 } from "./AD016.js";
14
+ import { AD017 } from "./AD017.js";
15
+ import { AD019 } from "./AD019.js";
16
+ import { AD020 } from "./AD020.js";
17
+ import { AD022 } from "./AD022.js";
18
+ import { AD023 } from "./AD023.js";
19
+ import { AD032 } from "./AD032.js";
20
+ import { AD034 } from "./AD034.js";
21
+ import { AD035 } from "./AD035.js";
22
+ import { AD036 } from "./AD036.js";
23
+ import { AD037 } from "./AD037.js";
24
+ import { AD039 } from "./AD039.js";
25
+ import { AD040 } from "./AD040.js";
26
+ import { AD041 } from "./AD041.js";
27
+ import { AD042 } from "./AD042.js";
28
+ import { AD043 } from "./AD043.js";
29
+ import { AD044 } from "./AD044.js";
30
+ import { AD045 } from "./AD045.js";
31
+ import { AD024 } from "./AD024.js";
32
+ import { AD025 } from "./AD025.js";
33
+ import { AD026 } from "./AD026.js";
34
+ import { AD027 } from "./AD027.js";
35
+ import { AD028 } from "./AD028.js";
36
+ import { AD029 } from "./AD029.js";
37
+ import { AD030 } from "./AD030.js";
38
+ import { AD031 } from "./AD031.js";
39
+ export const builtInRules = [
40
+ AD001,
41
+ AD002,
42
+ AD003,
43
+ AD004,
44
+ AD005,
45
+ AD006,
46
+ AD007,
47
+ AD008,
48
+ AD010,
49
+ AD011,
50
+ AD012,
51
+ AD013,
52
+ AD016,
53
+ AD017,
54
+ AD019,
55
+ AD020,
56
+ AD022,
57
+ AD023,
58
+ AD024,
59
+ AD025,
60
+ AD026,
61
+ AD027,
62
+ AD028,
63
+ AD029,
64
+ AD030,
65
+ AD031,
66
+ AD032,
67
+ AD034,
68
+ AD035,
69
+ AD036,
70
+ AD037,
71
+ AD039,
72
+ AD040,
73
+ AD041,
74
+ AD042,
75
+ AD043,
76
+ AD044,
77
+ AD045,
78
+ ];
79
+ export function ruleLabel(finding) {
80
+ return finding.alias ? `${finding.ruleId}/${finding.alias}` : finding.ruleId;
81
+ }
@@ -0,0 +1,2 @@
1
+ import type { RuleHelpers } from "../types.js";
2
+ export declare const helpers: RuleHelpers;
@@ -0,0 +1,11 @@
1
+ export const helpers = {
2
+ findSections(document) {
3
+ return document.sections;
4
+ },
5
+ findBlocks(document, type) {
6
+ return type ? document.blocks.filter((block) => block.type === type) : document.blocks;
7
+ },
8
+ isSourceLikeBlock(block) {
9
+ return ["source", "listing", "literal", "passthrough", "stem"].includes(block.type);
10
+ },
11
+ };
@@ -0,0 +1,3 @@
1
+ import type { Rule } from "../types.js";
2
+ export declare function validateRules(rules: Rule[]): void;
3
+ export declare function resolveRuleReference(rules: Rule[], reference: string): Rule | undefined;
@@ -0,0 +1,34 @@
1
+ export function validateRules(rules) {
2
+ const ids = new Set();
3
+ const aliases = new Set();
4
+ for (const rule of rules) {
5
+ if (!rule.id) {
6
+ throw new Error("Rule is missing required id");
7
+ }
8
+ if (ids.has(rule.id)) {
9
+ throw new Error(`Duplicate rule id: ${rule.id}`);
10
+ }
11
+ if (aliases.has(rule.id)) {
12
+ throw new Error(`Rule id collides with an alias: ${rule.id}`);
13
+ }
14
+ ids.add(rule.id);
15
+ if (rule.alias) {
16
+ if (aliases.has(rule.alias)) {
17
+ throw new Error(`Duplicate rule alias: ${rule.alias}`);
18
+ }
19
+ if (ids.has(rule.alias)) {
20
+ throw new Error(`Rule alias collides with an id: ${rule.alias}`);
21
+ }
22
+ aliases.add(rule.alias);
23
+ }
24
+ if (!rule.description) {
25
+ throw new Error(`Rule ${rule.id} is missing description`);
26
+ }
27
+ if (!rule.docs?.summary) {
28
+ throw new Error(`Rule ${rule.id} is missing docs.summary`);
29
+ }
30
+ }
31
+ }
32
+ export function resolveRuleReference(rules, reference) {
33
+ return rules.find((rule) => rule.id === reference || rule.alias === reference);
34
+ }
@@ -0,0 +1,42 @@
1
+ import type { BlockType, NormalizedDocument, SectionNode } from "../types.js";
2
+ export declare function groupSectionsByFile(sections: SectionNode[]): Map<string, SectionNode[]>;
3
+ export declare function parseColumnCount(line: string): number | undefined;
4
+ export declare function countTableCells(line: string): number;
5
+ export declare function isComplexTableCellLine(line: string): boolean;
6
+ export declare function isBlockDelimiter(value: string): boolean;
7
+ export declare function blockDelimiterType(value: string): BlockType | undefined;
8
+ export declare function isListMarkerLine(line: string): boolean;
9
+ export declare function isListMarkerResidueLine(line: string): boolean;
10
+ export declare function isUnderlineResidueLine(line: string): boolean;
11
+ export declare function isLineComment(line: string): boolean;
12
+ export declare function isLineInCommentParagraph(lines: string[], index: number): boolean;
13
+ export declare function listMarkerContent(line: string): {
14
+ content: string;
15
+ offset: number;
16
+ };
17
+ export declare function isAsciiDocTitleLine(line: string): boolean;
18
+ export declare function getAsciiDocTitle(line: string): string | undefined;
19
+ export declare function isAsciiDocSectionTitleLine(line: string): boolean;
20
+ export declare function isAsciiDocAnchorLine(line: string): boolean;
21
+ export declare function isAsciiDocBlockAttributeLine(line: string): boolean;
22
+ export declare function isAsciiDocAttributeEntryLine(line: string): boolean;
23
+ export declare function isExemptBeforeListLine(line: string): boolean;
24
+ export declare function hasTitleImmediatelyBefore(lines: string[], index: number): boolean;
25
+ export declare function findPrecedingTitleLineIndex(lines: string[], index: number): number | undefined;
26
+ export declare function hasAnchorForTitledBlock(lines: string[], index: number): boolean;
27
+ export declare function getAnchorForTitledBlock(lines: string[], index: number): string | undefined;
28
+ export declare function parseAsciiDocAnchor(line: string): string | undefined;
29
+ export declare function hasExplicitId(id: string | boolean | undefined): boolean;
30
+ export declare function precedingAttributeLines(lines: string[], index: number): string[];
31
+ export declare function isDiagramStyleLine(line: string): boolean;
32
+ export declare function isLineInProtectedBlock(document: NormalizedDocument, file: string, line: number): boolean;
33
+ export declare function isLineInTableBlock(document: NormalizedDocument, file: string, line: number): boolean;
34
+ export declare function markdownResidueIssue(line: string): {
35
+ message: string;
36
+ fixHelper: string;
37
+ column: number;
38
+ endColumn: number;
39
+ replacement?: string;
40
+ severity: "warning" | "error";
41
+ } | undefined;
42
+ export declare function getRequiredSections(config: unknown): string[];
@@ -0,0 +1,274 @@
1
+ import path from "node:path";
2
+ export function groupSectionsByFile(sections) {
3
+ const result = new Map();
4
+ for (const section of sections) {
5
+ const list = result.get(section.range.start.file) ?? [];
6
+ list.push(section);
7
+ result.set(section.range.start.file, list);
8
+ }
9
+ return result;
10
+ }
11
+ export function parseColumnCount(line) {
12
+ const match = line.match(/cols="([^"]+)"/);
13
+ if (!match) {
14
+ return undefined;
15
+ }
16
+ return (match[1] ?? "").split(",").filter(Boolean).length;
17
+ }
18
+ export function countTableCells(line) {
19
+ return [...line.matchAll(/(?<!\\)\|/g)].length;
20
+ }
21
+ export function isComplexTableCellLine(line) {
22
+ const trimmed = line.trim();
23
+ return /(^|\s)(?:\.\d+\+|\d+\+|\d+\.\d+\+|[a-z])\|/i.test(trimmed)
24
+ || /\b[aehlmdsv]\|/.test(trimmed)
25
+ || /\+$/.test(trimmed);
26
+ }
27
+ export function isBlockDelimiter(value) {
28
+ return blockDelimiterType(value) !== undefined;
29
+ }
30
+ export function blockDelimiterType(value) {
31
+ const trimmed = value.trim();
32
+ if (/^={4,}$/.test(trimmed)) {
33
+ return "example";
34
+ }
35
+ if (/^-{4,}$/.test(trimmed)) {
36
+ return "listing";
37
+ }
38
+ if (/^\.{4,}$/.test(trimmed)) {
39
+ return "literal";
40
+ }
41
+ if (/^\+{4,}$/.test(trimmed)) {
42
+ return "passthrough";
43
+ }
44
+ if (/^_{4,}$/.test(trimmed)) {
45
+ return "quote";
46
+ }
47
+ if (/^\*{4,}$/.test(trimmed)) {
48
+ return "sidebar";
49
+ }
50
+ if (/^\/{4,}$/.test(trimmed)) {
51
+ return "comment";
52
+ }
53
+ if (trimmed === "--") {
54
+ return "unknown";
55
+ }
56
+ if (/^[|,!:]={3,}$/.test(trimmed)) {
57
+ return "table";
58
+ }
59
+ return undefined;
60
+ }
61
+ export function isListMarkerLine(line) {
62
+ return isUnorderedOrOrderedListMarkerLine(line) || isDescriptionListMarkerLine(line);
63
+ }
64
+ export function isListMarkerResidueLine(line) {
65
+ const trimmed = line.trim();
66
+ return !isBlockDelimiter(trimmed) && /^(?:\*+|-|\.+|\d+\.)$/.test(trimmed);
67
+ }
68
+ export function isUnderlineResidueLine(line) {
69
+ return line.trim() === "___";
70
+ }
71
+ function isUnorderedOrOrderedListMarkerLine(line) {
72
+ return /^(\s*)([*-]+|\d+\.|\.+)\s+\S/.test(line);
73
+ }
74
+ function isDescriptionListMarkerLine(line) {
75
+ return /^\s*\S.*?(?::::|:::|::|;;)(?:\s+\S|\s*$)/.test(line);
76
+ }
77
+ export function isLineComment(line) {
78
+ return line.trimStart().startsWith("//");
79
+ }
80
+ export function isLineInCommentParagraph(lines, index) {
81
+ if ((lines[index] ?? "").trim() === "") {
82
+ return false;
83
+ }
84
+ let cursor = index - 1;
85
+ while (cursor >= 0 && (lines[cursor] ?? "").trim() !== "") {
86
+ if ((lines[cursor] ?? "").trim() === "[comment]") {
87
+ return true;
88
+ }
89
+ cursor -= 1;
90
+ }
91
+ return false;
92
+ }
93
+ export function listMarkerContent(line) {
94
+ const match = line.match(/^(\s*)([*-]+|\d+\.|\.+)\s+(\S.*)$/);
95
+ if (!match) {
96
+ const descriptionMatch = line.match(/^(\s*)\S.*?(?::::|:::|::|;;)(?:\s+(\S.*)|\s*$)/);
97
+ if (!descriptionMatch) {
98
+ return { content: line, offset: 0 };
99
+ }
100
+ const offset = (descriptionMatch[0]?.length ?? 0) - (descriptionMatch[2]?.length ?? 0);
101
+ return { content: descriptionMatch[2] ?? "", offset };
102
+ }
103
+ return {
104
+ content: match[3] ?? "",
105
+ offset: (match[1]?.length ?? 0) + (match[2]?.length ?? 0) + 1,
106
+ };
107
+ }
108
+ export function isAsciiDocTitleLine(line) {
109
+ const trimmed = line.trim();
110
+ return /^\.[^\s.].+/.test(trimmed) || /^\[[^\]]*\btitle\s*=/.test(trimmed);
111
+ }
112
+ export function getAsciiDocTitle(line) {
113
+ const trimmed = line.trim();
114
+ const blockTitle = trimmed.match(/^\.(?!\s|\.)((?:.|\s)+)$/);
115
+ if (blockTitle) {
116
+ return blockTitle[1]?.trim();
117
+ }
118
+ const attributeTitle = trimmed.match(/^\[[^\]]*\btitle\s*=\s*(?:"([^"]*)"|'([^']*)'|([^,\]]+))/);
119
+ return attributeTitle ? (attributeTitle[1] ?? attributeTitle[2] ?? attributeTitle[3] ?? "").trim() : undefined;
120
+ }
121
+ export function isAsciiDocSectionTitleLine(line) {
122
+ return /^(=+|#+)\s+\S/.test(line.trim());
123
+ }
124
+ export function isAsciiDocAnchorLine(line) {
125
+ return parseAsciiDocAnchor(line) !== undefined;
126
+ }
127
+ export function isAsciiDocBlockAttributeLine(line) {
128
+ return /^\[[^\]]*]\s*$/.test(line.trim()) && !isAsciiDocAnchorLine(line);
129
+ }
130
+ export function isAsciiDocAttributeEntryLine(line) {
131
+ return /^:[^:\s][^:\n]*:\s*.*$/.test(line.trim());
132
+ }
133
+ export function isExemptBeforeListLine(line) {
134
+ const trimmed = line.trim();
135
+ return trimmed === ""
136
+ || trimmed === "+"
137
+ || /^\/\//.test(trimmed)
138
+ || isAsciiDocSectionTitleLine(line)
139
+ || isListMarkerLine(line)
140
+ || isBlockDelimiter(trimmed)
141
+ || isAsciiDocTitleLine(line)
142
+ || isAsciiDocAnchorLine(line)
143
+ || isAsciiDocBlockAttributeLine(line);
144
+ }
145
+ export function hasTitleImmediatelyBefore(lines, index) {
146
+ return findPrecedingTitleLineIndex(lines, index) !== undefined;
147
+ }
148
+ export function findPrecedingTitleLineIndex(lines, index) {
149
+ let cursor = index - 1;
150
+ while (cursor >= 0) {
151
+ const line = lines[cursor] ?? "";
152
+ if (isAsciiDocTitleLine(line)) {
153
+ return cursor;
154
+ }
155
+ if (!isAsciiDocAnchorLine(line) && !isAsciiDocBlockAttributeLine(line) && line.trim() !== "") {
156
+ return undefined;
157
+ }
158
+ cursor -= 1;
159
+ }
160
+ return undefined;
161
+ }
162
+ export function hasAnchorForTitledBlock(lines, index) {
163
+ return getAnchorForTitledBlock(lines, index) !== undefined;
164
+ }
165
+ export function getAnchorForTitledBlock(lines, index) {
166
+ const titleIndex = findPrecedingTitleLineIndex(lines, index);
167
+ if (titleIndex === undefined) {
168
+ return undefined;
169
+ }
170
+ for (let cursor = titleIndex + 1; cursor < index; cursor += 1) {
171
+ const anchor = parseAsciiDocAnchor(lines[cursor] ?? "");
172
+ if (anchor) {
173
+ return anchor;
174
+ }
175
+ }
176
+ let cursor = titleIndex - 1;
177
+ while (cursor >= 0 && isAsciiDocBlockAttributeLine(lines[cursor] ?? "")) {
178
+ cursor -= 1;
179
+ }
180
+ return cursor >= 0 ? parseAsciiDocAnchor(lines[cursor] ?? "") : undefined;
181
+ }
182
+ export function parseAsciiDocAnchor(line) {
183
+ const match = line.trim().match(/^\[\[([^\]]+)]]\s*$|^\[#([^,\]\s]+)(?:,[^\]]*)?]\s*$/);
184
+ return match ? (match[1] ?? match[2]) : undefined;
185
+ }
186
+ export function hasExplicitId(id) {
187
+ return typeof id === "string" ? id.trim().length > 0 : id === true;
188
+ }
189
+ export function precedingAttributeLines(lines, index) {
190
+ const attributes = [];
191
+ let cursor = index - 1;
192
+ while (cursor >= 0) {
193
+ const line = lines[cursor] ?? "";
194
+ if (isAsciiDocBlockAttributeLine(line)) {
195
+ attributes.unshift(line.trim());
196
+ cursor -= 1;
197
+ continue;
198
+ }
199
+ if (isAsciiDocAnchorLine(line) || isAsciiDocTitleLine(line)) {
200
+ cursor -= 1;
201
+ continue;
202
+ }
203
+ break;
204
+ }
205
+ return attributes;
206
+ }
207
+ export function isDiagramStyleLine(line) {
208
+ return /^\[(?:a2s|actdiag|blockdiag|bytefield|dbml|ditaa|dot|dpic|drawio|erd|gnuplot|goat|graphviz|lilypond|matplotlib|mermaid|mmpviz|mscgen|nomnoml|nwdiag|packetdiag|penrose|pikchr|pintora|plantuml|rackdiag|seqdiag|shaape|smcat|state-machine-cat|structurizr|svgbob|symbolator|syntrax|umlet|vega|vega-lite|vegalite|wavedrom)(?:,|\])/.test(line.trim());
209
+ }
210
+ export function isLineInProtectedBlock(document, file, line) {
211
+ return document.blocks.some((block) => (path.resolve(block.range.start.file) === path.resolve(file)
212
+ && ["listing", "literal", "passthrough", "source", "stem", "diagram", "comment"].includes(block.type)
213
+ && block.range.start.line < line
214
+ && (block.range.end?.line ?? block.range.start.line) > line));
215
+ }
216
+ export function isLineInTableBlock(document, file, line) {
217
+ return document.blocks.some((block) => (path.resolve(block.range.start.file) === path.resolve(file)
218
+ && block.type === "table"
219
+ && block.range.start.line < line
220
+ && (block.range.end?.line ?? block.range.start.line) > line));
221
+ }
222
+ export function markdownResidueIssue(line) {
223
+ const markdownImage = line.match(/!\[[^\]]*]\([^)]+\)/);
224
+ if (markdownImage?.index !== undefined) {
225
+ const parsed = markdownImage[0].match(/^!\[([^\]]*)]\(([^)]+)\)$/);
226
+ const standalone = line.trim() === markdownImage[0];
227
+ return {
228
+ severity: "warning",
229
+ message: "Markdown image syntax renders as text in AsciiDoc",
230
+ fixHelper: "Use image::target[Alt text].",
231
+ column: markdownImage.index + 1,
232
+ endColumn: markdownImage.index + markdownImage[0].length + 1,
233
+ replacement: parsed ? `${standalone ? "image::" : "image:"}${parsed[2] ?? ""}[${parsed[1] ?? ""}]` : undefined,
234
+ };
235
+ }
236
+ const markdownLink = line.match(/(?<!!)\[[^\]]+]\([^)]+\)/);
237
+ if (markdownLink?.index !== undefined) {
238
+ const parsed = markdownLink[0].match(/^\[([^\]]+)]\(([^)]+)\)$/);
239
+ return {
240
+ severity: "warning",
241
+ message: "Markdown link syntax renders as text in AsciiDoc",
242
+ fixHelper: "Use link:target[text] or xref:target[text].",
243
+ column: markdownLink.index + 1,
244
+ endColumn: markdownLink.index + markdownLink[0].length + 1,
245
+ replacement: parsed ? `${adocLinkMacro(parsed[2] ?? "")}:${parsed[2] ?? ""}[${parsed[1] ?? ""}]` : undefined,
246
+ };
247
+ }
248
+ const reversedMarkdownLink = line.match(/\([^)]+\)\[[^\]]+]/);
249
+ if (reversedMarkdownLink?.index !== undefined) {
250
+ const parsed = reversedMarkdownLink[0].match(/^\(([^)]+)\)\[([^\]]+)]$/);
251
+ return {
252
+ severity: "warning",
253
+ message: "Reversed Markdown link residue renders as text in AsciiDoc",
254
+ fixHelper: "Use link:target[text] or xref:target[text].",
255
+ column: reversedMarkdownLink.index + 1,
256
+ endColumn: reversedMarkdownLink.index + reversedMarkdownLink[0].length + 1,
257
+ replacement: parsed ? `${adocLinkMacro(parsed[1] ?? "")}:${parsed[1] ?? ""}[${parsed[2] ?? ""}]` : undefined,
258
+ };
259
+ }
260
+ return undefined;
261
+ }
262
+ function adocLinkMacro(target) {
263
+ return /\.(?:adoc|asciidoc|asc)(?:#|$)/i.test(target) ? "xref" : "link";
264
+ }
265
+ export function getRequiredSections(config) {
266
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
267
+ return [];
268
+ }
269
+ const requiredSections = config.requiredSections;
270
+ if (!Array.isArray(requiredSections)) {
271
+ return [];
272
+ }
273
+ return requiredSections.filter((section) => typeof section === "string");
274
+ }