madr-lint 0.4.0 → 0.4.2
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/CHANGELOG.md +39 -0
- package/README.md +18 -11
- package/dist/cli.js +76 -33
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +21 -12
- package/dist/index.js +68 -31
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.d.ts
CHANGED
|
@@ -429,9 +429,10 @@ interface ParsedFile {
|
|
|
429
429
|
/** Parsed YAML frontmatter (v3/v4), or null if absent. */
|
|
430
430
|
frontmatter: Record<string, unknown> | null;
|
|
431
431
|
/**
|
|
432
|
-
* v2 body-list metadata extracted from the leading
|
|
433
|
-
*
|
|
434
|
-
*
|
|
432
|
+
* v2 body-list metadata extracted from the leading metadata block, or null if
|
|
433
|
+
* absent. The block is the leading RUN of lists, joined across HTML comments
|
|
434
|
+
* (which render as nothing). Covers both the bold-key (`- **Status**:`) and
|
|
435
|
+
* the canonical MADR v2.1.2 plain-key (`* Status:`) shapes. See ADR-0006.
|
|
435
436
|
*/
|
|
436
437
|
listMetadata: Record<string, unknown> | null;
|
|
437
438
|
/**
|
|
@@ -477,25 +478,33 @@ declare function frontmatterOffset(content: string): number;
|
|
|
477
478
|
* Parse a Markdown file: extract YAML frontmatter via gray-matter, then
|
|
478
479
|
* feed the body into mdast-util-from-markdown directly (per ADR-0002,
|
|
479
480
|
* we skip the unified+remark pipeline overhead). Additionally, extract
|
|
480
|
-
* v2-style list metadata from the body's
|
|
481
|
-
* canonical plain-key shapes (see ADR-0006).
|
|
481
|
+
* v2-style list metadata from the body's leading metadata block — both
|
|
482
|
+
* bold-key and canonical plain-key shapes (see ADR-0006).
|
|
482
483
|
*/
|
|
483
484
|
declare function parseFile(content: string): ParsedFile;
|
|
484
485
|
/**
|
|
485
|
-
* Extract v2-style list metadata from an mdast Root. The metadata block
|
|
486
|
-
* the
|
|
487
|
-
*
|
|
488
|
-
* metadata
|
|
489
|
-
*
|
|
486
|
+
* Extract v2-style list metadata from an mdast Root. The metadata block leads
|
|
487
|
+
* the body: only H1 headings and HTML nodes may precede it — an H2-or-deeper
|
|
488
|
+
* heading, or any other block before the first list, means there is no
|
|
489
|
+
* metadata block. The block itself is the leading RUN of lists, joined across
|
|
490
|
+
* HTML comments — a comment ends the CommonMark list yet renders as nothing,
|
|
491
|
+
* so a field below it is still metadata (#73). A second list with nothing
|
|
492
|
+
* between it and the first (a bullet-marker change, or an ordered list) joins
|
|
493
|
+
* the run when EVERY one of its items reads as a `Key: value` pair (#115); a
|
|
494
|
+
* list carrying any other item is prose and ends the block. Once the run has
|
|
495
|
+
* started, visible HTML, a paragraph, a code fence, a thematic break, a
|
|
496
|
+
* blockquote, and a heading of ANY depth all end the block. Each `listItem` is
|
|
497
|
+
* read as one `Key: value` pair in either of two shapes:
|
|
490
498
|
*
|
|
491
499
|
* - Bold key: `- **Status**: accepted` (some MADR v2 authors)
|
|
492
500
|
* - Plain key: `* Status: accepted` (official MADR v2.1.2 template)
|
|
493
501
|
*
|
|
494
502
|
* Key normalization: trim → lowercase → spaces become hyphens
|
|
495
503
|
* (matches v4 frontmatter convention, e.g. "Decision Makers" →
|
|
496
|
-
* "decision-makers"). On duplicate keys, first occurrence wins
|
|
504
|
+
* "decision-makers"). On duplicate keys, first occurrence wins — across
|
|
505
|
+
* segments too, so an earlier segment always shadows a later one.
|
|
497
506
|
*
|
|
498
|
-
* Returns null unless the
|
|
507
|
+
* Returns null unless the block carries a recognized MADR key, so ordinary
|
|
499
508
|
* leading prose lists are not mistaken for metadata. (A leading list keyed by
|
|
500
509
|
* a recognized field is still treated as metadata even if its value reads
|
|
501
510
|
* prose-like, e.g. `Status: under discussion` — that position + key is
|
package/dist/index.js
CHANGED
|
@@ -8,11 +8,19 @@ var __export = (target, all) => {
|
|
|
8
8
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
9
9
|
import { toString } from "mdast-util-to-string";
|
|
10
10
|
import grayMatter from "gray-matter";
|
|
11
|
+
|
|
12
|
+
// src/core/null-proto-map.ts
|
|
13
|
+
function nullProtoMap() {
|
|
14
|
+
return /* @__PURE__ */ Object.create(null);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/core/parser.ts
|
|
18
|
+
var GRAY_MATTER_OPTIONS = Object.freeze({});
|
|
11
19
|
function frontmatterOffset(content) {
|
|
12
|
-
return content.length - grayMatter(content).content.length;
|
|
20
|
+
return content.length - grayMatter(content, GRAY_MATTER_OPTIONS).content.length;
|
|
13
21
|
}
|
|
14
22
|
function parseFile(content) {
|
|
15
|
-
const matter = grayMatter(content);
|
|
23
|
+
const matter = grayMatter(content, GRAY_MATTER_OPTIONS);
|
|
16
24
|
const data = matter.data;
|
|
17
25
|
const frontmatter = Object.keys(data).length > 0 ? data : null;
|
|
18
26
|
const ast = fromMarkdown(matter.content);
|
|
@@ -22,7 +30,7 @@ function parseFile(content) {
|
|
|
22
30
|
let metadataLoc = listResult && Object.keys(listResult.loc).length > 0 ? { ...listResult.loc } : null;
|
|
23
31
|
let metadataValueLoc = listResult && Object.keys(listResult.valueOffsets).length > 0 ? { ...listResult.valueOffsets } : null;
|
|
24
32
|
if (frontmatter && listMetadata) {
|
|
25
|
-
const frontmatterDefined =
|
|
33
|
+
const frontmatterDefined = nullProtoMap();
|
|
26
34
|
for (const [k, v] of Object.entries(frontmatter)) {
|
|
27
35
|
if (v !== null && v !== void 0) frontmatterDefined[k] = v;
|
|
28
36
|
}
|
|
@@ -61,39 +69,65 @@ var RECOGNIZED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
|
61
69
|
function extractListMetadata(ast) {
|
|
62
70
|
return extractListMetadataWithLoc(ast)?.values ?? null;
|
|
63
71
|
}
|
|
72
|
+
function isHtmlComment(value) {
|
|
73
|
+
let rest = value.trim();
|
|
74
|
+
if (rest.length === 0) return false;
|
|
75
|
+
while (rest.length > 0) {
|
|
76
|
+
if (!rest.startsWith("<!--")) return false;
|
|
77
|
+
const end = rest.indexOf("-->", 2);
|
|
78
|
+
if (end === -1) return false;
|
|
79
|
+
rest = rest.slice(end + 3).trim();
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
64
83
|
function extractListMetadataWithLoc(ast, body) {
|
|
65
|
-
|
|
84
|
+
const segments = [];
|
|
85
|
+
let bridged = false;
|
|
66
86
|
for (const child of ast.children) {
|
|
67
|
-
if (
|
|
68
|
-
if (child.
|
|
87
|
+
if (segments.length === 0) {
|
|
88
|
+
if (child.type === "heading") {
|
|
89
|
+
if (child.depth >= 2) break;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (child.type === "html") continue;
|
|
93
|
+
if (child.type === "list") {
|
|
94
|
+
segments.push(child);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
if (child.type === "html" && isHtmlComment(child.value)) {
|
|
100
|
+
bridged = true;
|
|
69
101
|
continue;
|
|
70
102
|
}
|
|
71
|
-
if (child.type === "
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
103
|
+
if (child.type === "list" && (bridged || child.children.length > 0 && child.children.every((item) => extractListItemKV(item)))) {
|
|
104
|
+
segments.push(child);
|
|
105
|
+
bridged = false;
|
|
106
|
+
continue;
|
|
75
107
|
}
|
|
76
108
|
break;
|
|
77
109
|
}
|
|
78
|
-
if (
|
|
79
|
-
const values =
|
|
80
|
-
const loc =
|
|
81
|
-
const valueOffsets =
|
|
82
|
-
for (const
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
values
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
if (segments.length === 0) return null;
|
|
111
|
+
const values = nullProtoMap();
|
|
112
|
+
const loc = nullProtoMap();
|
|
113
|
+
const valueOffsets = nullProtoMap();
|
|
114
|
+
for (const list of segments) {
|
|
115
|
+
for (const item of list.children) {
|
|
116
|
+
const pair = extractListItemKV(item);
|
|
117
|
+
if (pair && !Object.hasOwn(values, pair.key)) {
|
|
118
|
+
values[pair.key] = pair.value;
|
|
119
|
+
const start = item.position?.start;
|
|
120
|
+
if (start) loc[pair.key] = { line: start.line, column: start.column };
|
|
121
|
+
if (body !== void 0 && pair.valueOffset && body.slice(pair.valueOffset.start, pair.valueOffset.end) === pair.value) {
|
|
122
|
+
valueOffsets[pair.key] = pair.valueOffset;
|
|
123
|
+
}
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
}
|
|
93
127
|
const hasRecognizedKey = Object.keys(values).some(
|
|
94
128
|
(k) => RECOGNIZED_METADATA_KEYS.has(k)
|
|
95
129
|
);
|
|
96
|
-
return hasRecognizedKey ? { values, loc, valueOffsets } : null;
|
|
130
|
+
return hasRecognizedKey ? { values: { ...values }, loc, valueOffsets } : null;
|
|
97
131
|
}
|
|
98
132
|
var KEY_PATTERN = /^[A-Za-z][A-Za-z0-9 \-_]*$/;
|
|
99
133
|
var SEPARATOR_PATTERN = /^\s*:\s*/;
|
|
@@ -354,7 +388,7 @@ var DIRECTIVE_KINDS = /* @__PURE__ */ new Set([
|
|
|
354
388
|
"disable-next-line"
|
|
355
389
|
]);
|
|
356
390
|
var DIRECTIVE_PREFIX = "madr-lint-";
|
|
357
|
-
function parseDirective(value, line) {
|
|
391
|
+
function parseDirective(value, line, endLine) {
|
|
358
392
|
const trimmed = value.trim();
|
|
359
393
|
if (!trimmed.startsWith("<!--") || !trimmed.endsWith("-->")) return null;
|
|
360
394
|
const inner = trimmed.slice(4, -3).trim();
|
|
@@ -365,7 +399,12 @@ function parseDirective(value, line) {
|
|
|
365
399
|
const keyword = wsIndex === -1 ? afterPrefix : afterPrefix.slice(0, wsIndex);
|
|
366
400
|
if (!DIRECTIVE_KINDS.has(keyword)) return null;
|
|
367
401
|
const rest = wsIndex === -1 ? "" : afterPrefix.slice(wsIndex);
|
|
368
|
-
return {
|
|
402
|
+
return {
|
|
403
|
+
kind: keyword,
|
|
404
|
+
rules: parseRuleList(rest),
|
|
405
|
+
line,
|
|
406
|
+
endLine
|
|
407
|
+
};
|
|
369
408
|
}
|
|
370
409
|
function parseRuleList(rest) {
|
|
371
410
|
const trimmed = rest.trim();
|
|
@@ -376,8 +415,9 @@ function parseRuleList(rest) {
|
|
|
376
415
|
function walkHtml(node, out) {
|
|
377
416
|
if (node.type === "html") {
|
|
378
417
|
const line = node.position?.start.line;
|
|
379
|
-
|
|
380
|
-
|
|
418
|
+
const endLine = node.position?.end.line;
|
|
419
|
+
if (typeof line === "number" && typeof endLine === "number") {
|
|
420
|
+
const directive = parseDirective(node.value, line, endLine);
|
|
381
421
|
if (directive) out.push(directive);
|
|
382
422
|
}
|
|
383
423
|
}
|
|
@@ -416,7 +456,7 @@ function collectDirectives(ast, body) {
|
|
|
416
456
|
break;
|
|
417
457
|
}
|
|
418
458
|
case "disable-next-line": {
|
|
419
|
-
const target = nextNonBlankLine(directive.
|
|
459
|
+
const target = nextNonBlankLine(directive.endLine);
|
|
420
460
|
if (target === null) break;
|
|
421
461
|
let entry = index.nextLine.get(target);
|
|
422
462
|
if (!entry) {
|
|
@@ -1542,9 +1582,6 @@ var BASELINE_VERSION = 1;
|
|
|
1542
1582
|
function baselinePath(cwd) {
|
|
1543
1583
|
return join(cwd, ".madr-lint", "baseline.json");
|
|
1544
1584
|
}
|
|
1545
|
-
function nullProtoMap() {
|
|
1546
|
-
return /* @__PURE__ */ Object.create(null);
|
|
1547
|
-
}
|
|
1548
1585
|
function buildBaseline(diagnostics) {
|
|
1549
1586
|
const entries = nullProtoMap();
|
|
1550
1587
|
for (const d of diagnostics) {
|