remark-mdat 2.1.0 → 2.2.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/dist/index.d.ts +75 -44
- package/dist/index.js +175 -38
- package/package.json +3 -1
- package/readme.md +6 -0
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { JsonValue, MergeDeep, Simplify } from "type-fest";
|
|
|
6
6
|
import { Plugin } from "unified";
|
|
7
7
|
|
|
8
8
|
//#region src/lib/mdat/rules.d.ts
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Recursively simplifies a type for cleaner IDE hover display. Approximation of
|
|
11
|
+
* type-fest's internal `SimplifyDeep`.
|
|
12
|
+
*/
|
|
10
13
|
type SimplifyDeep<T> = Simplify<MergeDeep<T, T>>;
|
|
11
14
|
/**
|
|
12
15
|
* Context passed to rule content functions during expansion.
|
|
@@ -17,19 +20,19 @@ type RuleContext = {
|
|
|
17
20
|
tree: Root;
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
|
-
* Strict normalized rules used internally.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* Strict normalized rules used internally. Rules normalized to a form with
|
|
24
|
+
* async content functions and other default metadata Simplifies processing
|
|
25
|
+
* elsewhere, while retaining flexibility for rule authors
|
|
23
26
|
*/
|
|
24
27
|
type NormalizedRule = {
|
|
25
28
|
/**
|
|
26
|
-
* The function that generates the expanded Markdown string.
|
|
27
|
-
*
|
|
29
|
+
* The function that generates the expanded Markdown string. For 'compound'
|
|
30
|
+
* rules, this can be an array of rules (without keywords).
|
|
28
31
|
*/
|
|
29
32
|
content: ((options: JsonValue, context: RuleContext) => Promise<string>) | NormalizedRule[];
|
|
30
33
|
/**
|
|
31
|
-
* The order in which the rule should be applied during processing
|
|
32
|
-
*
|
|
34
|
+
* The order in which the rule should be applied during processing Helpful if
|
|
35
|
+
* a rule depends on the presence of content generated by another rule
|
|
33
36
|
* Defaults to 0.
|
|
34
37
|
*/
|
|
35
38
|
order: number;
|
|
@@ -40,9 +43,9 @@ type Rule =
|
|
|
40
43
|
*/
|
|
41
44
|
((options: JsonValue, context: RuleContext) => Promise<string> | string)
|
|
42
45
|
/**
|
|
43
|
-
* Compound rules may be defined an array of rules, without keywords.
|
|
44
|
-
*
|
|
45
|
-
* of a rule object with validation metadata.
|
|
46
|
+
* Compound rules may be defined an array of rules, without keywords. Can be
|
|
47
|
+
* defined at the top level, if no validation metadata is required, or as the
|
|
48
|
+
* 'content' value of a rule object with validation metadata.
|
|
46
49
|
*/
|
|
47
50
|
| Rule[]
|
|
48
51
|
/**
|
|
@@ -54,18 +57,22 @@ type Rule =
|
|
|
54
57
|
*/
|
|
55
58
|
| {
|
|
56
59
|
/**
|
|
57
|
-
* Gets content to expand into the comment.
|
|
58
|
-
*
|
|
60
|
+
* Gets content to expand into the comment. Can be a simple string for
|
|
61
|
+
* direct replacement, a function that returns a string, or an async
|
|
62
|
+
* function that returns a string.
|
|
59
63
|
*
|
|
60
64
|
* If a function is provided, it will be passed the following arguments:
|
|
61
|
-
*
|
|
62
|
-
* JSON value of options parsed immediately after the
|
|
63
|
-
* `<!-- keyword({something:
|
|
64
|
-
* `<!-- keyword {something: true}-->`
|
|
65
|
-
*
|
|
66
|
-
* @param context
|
|
67
|
-
*
|
|
68
|
-
*
|
|
65
|
+
*
|
|
66
|
+
* @param options JSON value of options parsed immediately after the
|
|
67
|
+
* comment keyword in the comment, e.g.: `<!-- keyword({something:
|
|
68
|
+
* true}) -->` or `<!-- keyword {something: true}-->` Sets options to
|
|
69
|
+
* {something: true}
|
|
70
|
+
* @param context Rule context containing the mdast AST, parsed
|
|
71
|
+
* frontmatter, and file path.
|
|
72
|
+
*
|
|
73
|
+
* @returns A string with the generated content. The string will be parsed
|
|
74
|
+
* as Markdown and inserted into the document at the comment's
|
|
75
|
+
* location.
|
|
69
76
|
*/
|
|
70
77
|
content: ((options: JsonValue, context: RuleContext) => Promise<string> | string) | Rule[] | string;
|
|
71
78
|
/**
|
|
@@ -75,27 +82,30 @@ type Rule =
|
|
|
75
82
|
order?: number;
|
|
76
83
|
};
|
|
77
84
|
/**
|
|
78
|
-
* Rules are record objects whose keys match strings inside a Markdown comment,
|
|
85
|
+
* Rules are record objects whose keys match strings inside a Markdown comment,
|
|
86
|
+
* and values explain what should be expanded at the comment site.
|
|
87
|
+
*
|
|
88
|
+
* The record value may be a string, or an object containing additional
|
|
89
|
+
* metadata, possibly with a function to invoke to generate content.
|
|
79
90
|
*
|
|
80
|
-
* The record value may be a string, or an object containing additional metadata, possibly with a function to invoke to generate content.
|
|
81
91
|
* @example
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
92
|
+
* Most basic rule:
|
|
93
|
+
* ```ts
|
|
94
|
+
* { basic: 'content' }
|
|
95
|
+
* ```
|
|
86
96
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
97
|
+
* Rule with dynamic content:
|
|
98
|
+
* ```ts
|
|
99
|
+
* { basic: () => `${new Date().toISOString()}` }
|
|
100
|
+
* ```
|
|
91
101
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
102
|
+
* Rule with metadata:
|
|
103
|
+
* ```ts
|
|
104
|
+
* { basic-meta: { order: 1, content: 'content'} }
|
|
105
|
+
* ```
|
|
96
106
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
107
|
+
* Rule with dynamic content and metadata:
|
|
108
|
+
* { basic-date: { order: 1, content: () => `${new Date().toISOString()}` } }
|
|
99
109
|
*/
|
|
100
110
|
type Rules = SimplifyDeep<Record<string, Rule>>;
|
|
101
111
|
/** A record mapping comment keywords to {@link NormalizedRule} objects. */
|
|
@@ -103,15 +113,15 @@ type NormalizedRules = SimplifyDeep<Record<string, NormalizedRule>>;
|
|
|
103
113
|
/** Zod schema for validating {@link Rules} records. */
|
|
104
114
|
declare const rulesSchema: z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
105
115
|
/**
|
|
106
|
-
* Returns the rule value from a single-rule record.
|
|
107
|
-
*
|
|
116
|
+
* Returns the rule value from a single-rule record. Useful when aliasing rules
|
|
117
|
+
* or invoking them programmatically.
|
|
108
118
|
*
|
|
109
119
|
* Throws if there are no entries or more than one entry.
|
|
110
120
|
*/
|
|
111
121
|
declare function getSoleRule<T extends NormalizedRules | Rules>(rules: T): T[keyof T];
|
|
112
122
|
/**
|
|
113
|
-
* Returns the rule key from a single-rule record.
|
|
114
|
-
*
|
|
123
|
+
* Returns the rule key from a single-rule record. Useful for comment
|
|
124
|
+
* placeholder validation.
|
|
115
125
|
*
|
|
116
126
|
* Throws if there are no entries or more than one entry.
|
|
117
127
|
*/
|
|
@@ -139,6 +149,26 @@ declare function mdatCollapse(tree: Root, file: VFile): void;
|
|
|
139
149
|
*/
|
|
140
150
|
declare const mdatClean: typeof mdatCollapse;
|
|
141
151
|
//#endregion
|
|
152
|
+
//#region src/lib/mdast-utils/mdast-util-mdat-diff.d.ts
|
|
153
|
+
/** Per-tag comparison result from {@link mdatDiff}. */
|
|
154
|
+
type MdatDiffResult = {
|
|
155
|
+
/** The keyword for this tag. */keyword: string; /** 1-based line number of the opening comment in the expanded document. */
|
|
156
|
+
line: number; /** Comparison status. */
|
|
157
|
+
status: 'added' | 'missing' | 'ok' | 'stale' | 'unexpanded';
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* Compare original and expanded documents per-tag. Walks both ASTs to extract
|
|
161
|
+
* content between open/close comment markers, then compares per-tag.
|
|
162
|
+
*
|
|
163
|
+
* Callers should run {@link mdatSplit} on both trees before calling this
|
|
164
|
+
* function to ensure multi-comment nodes are split into individual nodes.
|
|
165
|
+
*
|
|
166
|
+
* Adds diagnostic messages to `expandedFile` via the VFile message pipeline.
|
|
167
|
+
*
|
|
168
|
+
* @returns Per-tag comparison results.
|
|
169
|
+
*/
|
|
170
|
+
declare function mdatDiff(originalTree: Root, originalFile: VFile, expandedTree: Root, expandedFile: VFile): MdatDiffResult[];
|
|
171
|
+
//#endregion
|
|
142
172
|
//#region src/lib/mdast-utils/mdast-util-mdat-expand.d.ts
|
|
143
173
|
/**
|
|
144
174
|
* Mdast utility to expand mdat comments in the tree.
|
|
@@ -147,8 +177,9 @@ declare function mdatExpand(tree: Root, file: VFile, rules: NormalizedRules | Ru
|
|
|
147
177
|
//#endregion
|
|
148
178
|
//#region src/lib/mdast-utils/mdast-util-mdat-split.d.ts
|
|
149
179
|
/**
|
|
150
|
-
* Mdast utility plugin to split any multi-comment nodes and their content into
|
|
151
|
-
* nodes. They're wrapped in a paragraph so as not to
|
|
180
|
+
* Mdast utility plugin to split any multi-comment nodes and their content into
|
|
181
|
+
* individual MDAST HTML nodes. They're wrapped in a paragraph so as not to
|
|
182
|
+
* introduce new breaks.
|
|
152
183
|
*/
|
|
153
184
|
declare function mdatSplit(tree: Root, file: VFile): void;
|
|
154
185
|
//#endregion
|
|
@@ -204,4 +235,4 @@ declare const optionsSchema: z.ZodObject<{
|
|
|
204
235
|
*/
|
|
205
236
|
declare const remarkMdat: Plugin<[Options?], Root>;
|
|
206
237
|
//#endregion
|
|
207
|
-
export { type MdatFileReport, type MdatMessage, type NormalizedRule, type NormalizedRules, type Options, type Rule, type RuleContext, type Rules, type SimplifyDeep, remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatCollapse, mdatExpand, mdatSplit, mdatStrip, optionsSchema, reporterMdat, rulesSchema, setLogger };
|
|
238
|
+
export { type MdatDiffResult, type MdatFileReport, type MdatMessage, type NormalizedRule, type NormalizedRules, type Options, type Rule, type RuleContext, type Rules, type SimplifyDeep, remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, optionsSchema, reporterMdat, rulesSchema, setLogger };
|
package/dist/index.js
CHANGED
|
@@ -111,7 +111,9 @@ function highlightComments(text, level) {
|
|
|
111
111
|
//#region src/lib/mdat/parse.ts
|
|
112
112
|
/**
|
|
113
113
|
* Parse an Mdast HTML comment node into structured data.
|
|
114
|
-
*
|
|
114
|
+
*
|
|
115
|
+
* @returns A CommentMarkerNode or undefined if the node is not a recognized
|
|
116
|
+
* comment.
|
|
115
117
|
*/
|
|
116
118
|
function parseCommentNode(node, parent) {
|
|
117
119
|
try {
|
|
@@ -126,17 +128,20 @@ function parseCommentNode(node, parent) {
|
|
|
126
128
|
if (error instanceof VFileMessage) {
|
|
127
129
|
error.line = node.position?.start.line;
|
|
128
130
|
throw error;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
+
}
|
|
132
|
+
if (error instanceof Error) throw new VFileMessage(error.message, node);
|
|
133
|
+
throw new VFileMessage("Unknown error", node);
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
const HTML_COMMENT_OPEN_REGEX = /^\s*<!-{2,}\s*/;
|
|
134
137
|
const HTML_COMMENT_CLOSE_REGEX = /\s*-{2,}>\s*$/;
|
|
135
138
|
const WHITESPACE_REGEX = /\s/;
|
|
136
139
|
/**
|
|
137
|
-
* Parse any comment string into structured data.
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
+
* Parse any comment string into structured data. Comments using code-style
|
|
141
|
+
* notation (`//`, `#`, `/*`) are ignored and return `undefined`.
|
|
142
|
+
*
|
|
143
|
+
* @returns A CommentMarker or undefined if the node is not a recognized
|
|
144
|
+
* comment.
|
|
140
145
|
*/
|
|
141
146
|
function parseComment(text) {
|
|
142
147
|
if (!isComment(text)) return;
|
|
@@ -189,25 +194,23 @@ function mdatCollapse(tree, file) {
|
|
|
189
194
|
lastOpenMarker = marker;
|
|
190
195
|
return CONTINUE;
|
|
191
196
|
}
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return CONTINUE;
|
|
200
|
-
}
|
|
201
|
-
if (lastOpenMarker.keyword !== marker.keyword) {
|
|
202
|
-
saveLog(file, "error", "collapse", "Opening marker doesn't share a keyword", node);
|
|
203
|
-
return CONTINUE;
|
|
204
|
-
}
|
|
205
|
-
const openMarkerIndex = parent.children.indexOf(lastOpenMarker.node);
|
|
206
|
-
const nodesToRemove = parent.children.indexOf(marker.node) - openMarkerIndex + 1;
|
|
207
|
-
parent.children.splice(openMarkerIndex + 1, nodesToRemove - 1);
|
|
208
|
-
lastOpenMarker = void 0;
|
|
209
|
-
return [CONTINUE, index - nodesToRemove + 1];
|
|
197
|
+
if (lastOpenMarker === void 0) {
|
|
198
|
+
saveLog(file, "error", "collapse", "Found closing marker without opening marker", node);
|
|
199
|
+
return CONTINUE;
|
|
200
|
+
}
|
|
201
|
+
if (lastOpenMarker.parent !== marker.parent) {
|
|
202
|
+
saveLog(file, "error", "collapse", "Opening marker doesn't share a parent", node);
|
|
203
|
+
return CONTINUE;
|
|
210
204
|
}
|
|
205
|
+
if (lastOpenMarker.keyword !== marker.keyword) {
|
|
206
|
+
saveLog(file, "error", "collapse", "Opening marker doesn't share a keyword", node);
|
|
207
|
+
return CONTINUE;
|
|
208
|
+
}
|
|
209
|
+
const openMarkerIndex = parent.children.indexOf(lastOpenMarker.node);
|
|
210
|
+
const nodesToRemove = parent.children.indexOf(marker.node) - openMarkerIndex + 1;
|
|
211
|
+
parent.children.splice(openMarkerIndex + 1, nodesToRemove - 1);
|
|
212
|
+
lastOpenMarker = void 0;
|
|
213
|
+
return [CONTINUE, index - nodesToRemove + 1];
|
|
211
214
|
});
|
|
212
215
|
}
|
|
213
216
|
//#endregion
|
|
@@ -218,7 +221,10 @@ const NORMALIZED = Symbol("normalized");
|
|
|
218
221
|
function isNormalized(rules) {
|
|
219
222
|
return NORMALIZED in rules;
|
|
220
223
|
}
|
|
221
|
-
/**
|
|
224
|
+
/**
|
|
225
|
+
* Converts flexible {@link Rules} into strict {@link NormalizedRules} for
|
|
226
|
+
* internal processing.
|
|
227
|
+
*/
|
|
222
228
|
function normalizeRules(rules) {
|
|
223
229
|
validateRules(rules);
|
|
224
230
|
const normalizedRules = {};
|
|
@@ -281,9 +287,9 @@ const keywordSchema = z.string().check(z.refine((key) => !COMMENT_PREFIX_REGEX.t
|
|
|
281
287
|
/** Zod schema for validating {@link Rules} records. */
|
|
282
288
|
const rulesSchema = z.record(keywordSchema, ruleSchema).describe("MDAT Rules");
|
|
283
289
|
/**
|
|
284
|
-
* Expand rule content. For compound rules (content arrays), individual
|
|
285
|
-
*
|
|
286
|
-
*
|
|
290
|
+
* Expand rule content. For compound rules (content arrays), individual sub-rule
|
|
291
|
+
* failures are reported via `onWarning` and skipped. The entire expansion only
|
|
292
|
+
* fails if every sub-rule fails.
|
|
287
293
|
*/
|
|
288
294
|
async function getRuleContent(rule, options, context, onWarning) {
|
|
289
295
|
if (Array.isArray(rule.content)) {
|
|
@@ -309,8 +315,8 @@ async function getRuleContent(rule, options, context, onWarning) {
|
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
317
|
/**
|
|
312
|
-
* Returns the rule value from a single-rule record.
|
|
313
|
-
*
|
|
318
|
+
* Returns the rule value from a single-rule record. Useful when aliasing rules
|
|
319
|
+
* or invoking them programmatically.
|
|
314
320
|
*
|
|
315
321
|
* Throws if there are no entries or more than one entry.
|
|
316
322
|
*/
|
|
@@ -318,8 +324,8 @@ function getSoleRule(rules) {
|
|
|
318
324
|
return getSoleRecord(rules);
|
|
319
325
|
}
|
|
320
326
|
/**
|
|
321
|
-
* Returns the rule key from a single-rule record.
|
|
322
|
-
*
|
|
327
|
+
* Returns the rule key from a single-rule record. Useful for comment
|
|
328
|
+
* placeholder validation.
|
|
323
329
|
*
|
|
324
330
|
* Throws if there are no entries or more than one entry.
|
|
325
331
|
*/
|
|
@@ -331,9 +337,11 @@ function getSoleRuleKey(rules) {
|
|
|
331
337
|
/**
|
|
332
338
|
* Get the sole entry in a record.
|
|
333
339
|
*
|
|
334
|
-
* Useful for working with Rules records
|
|
335
|
-
*
|
|
340
|
+
* Useful for working with Rules records that are only supposed to contain a
|
|
341
|
+
* single rule.
|
|
342
|
+
*
|
|
336
343
|
* @param record The record to get the sole entry from
|
|
344
|
+
*
|
|
337
345
|
* @returns The value of the sole entry in the record
|
|
338
346
|
* @throws {Error} If there are no entries or more than one entry
|
|
339
347
|
*/
|
|
@@ -402,8 +410,9 @@ async function mdatExpand(tree, file, rules) {
|
|
|
402
410
|
//#endregion
|
|
403
411
|
//#region src/lib/mdast-utils/mdast-util-mdat-split.ts
|
|
404
412
|
/**
|
|
405
|
-
* Mdast utility plugin to split any multi-comment nodes and their content into
|
|
406
|
-
* nodes. They're wrapped in a paragraph so as not to
|
|
413
|
+
* Mdast utility plugin to split any multi-comment nodes and their content into
|
|
414
|
+
* individual MDAST HTML nodes. They're wrapped in a paragraph so as not to
|
|
415
|
+
* introduce new breaks.
|
|
407
416
|
*/
|
|
408
417
|
function mdatSplit(tree, file) {
|
|
409
418
|
visit(tree, "html", (node, index, parent) => {
|
|
@@ -420,7 +429,10 @@ function mdatSplit(tree, file) {
|
|
|
420
429
|
}
|
|
421
430
|
});
|
|
422
431
|
}
|
|
423
|
-
/**
|
|
432
|
+
/**
|
|
433
|
+
* Splits a single mdast HTML node containing multiple comments into individual
|
|
434
|
+
* HTML and text nodes. Exported for testing.
|
|
435
|
+
*/
|
|
424
436
|
function splitHtmlIntoMdastNodes(mdastNode) {
|
|
425
437
|
const htmlTree = fromHtml(mdastNode.value, { fragment: true });
|
|
426
438
|
const mdastNodes = [];
|
|
@@ -483,6 +495,131 @@ async function mdat(tree, file, rules) {
|
|
|
483
495
|
*/
|
|
484
496
|
const mdatClean = mdatCollapse;
|
|
485
497
|
//#endregion
|
|
498
|
+
//#region src/lib/mdast-utils/mdast-util-mdat-diff.ts
|
|
499
|
+
/**
|
|
500
|
+
* Compare original and expanded documents per-tag. Walks both ASTs to extract
|
|
501
|
+
* content between open/close comment markers, then compares per-tag.
|
|
502
|
+
*
|
|
503
|
+
* Callers should run {@link mdatSplit} on both trees before calling this
|
|
504
|
+
* function to ensure multi-comment nodes are split into individual nodes.
|
|
505
|
+
*
|
|
506
|
+
* Adds diagnostic messages to `expandedFile` via the VFile message pipeline.
|
|
507
|
+
*
|
|
508
|
+
* @returns Per-tag comparison results.
|
|
509
|
+
*/
|
|
510
|
+
function mdatDiff(originalTree, originalFile, expandedTree, expandedFile) {
|
|
511
|
+
const originalText = originalFile.toString();
|
|
512
|
+
const expandedText = expandedFile.toString();
|
|
513
|
+
const originalSections = extractSections(originalTree, originalText, originalFile);
|
|
514
|
+
const expandedSections = extractSections(expandedTree, expandedText, expandedFile);
|
|
515
|
+
const results = [];
|
|
516
|
+
const originalByKeyword = groupByKeyword(originalSections);
|
|
517
|
+
const expandedByKeyword = groupByKeyword(expandedSections);
|
|
518
|
+
for (const [keyword, expandedList] of expandedByKeyword) {
|
|
519
|
+
const originalList = originalByKeyword.get(keyword) ?? [];
|
|
520
|
+
for (const [i, exp] of expandedList.entries()) {
|
|
521
|
+
const orig = originalList.at(i);
|
|
522
|
+
if (orig === void 0) {
|
|
523
|
+
results.push({
|
|
524
|
+
keyword,
|
|
525
|
+
line: exp.line,
|
|
526
|
+
status: "added"
|
|
527
|
+
});
|
|
528
|
+
saveLog(expandedFile, "info", "diff", `Added: <!-- ${keyword} -->`, exp.line);
|
|
529
|
+
} else if (orig.content === void 0) {
|
|
530
|
+
results.push({
|
|
531
|
+
keyword,
|
|
532
|
+
line: exp.line,
|
|
533
|
+
status: "unexpanded"
|
|
534
|
+
});
|
|
535
|
+
saveLog(expandedFile, "warn", "diff", `Unexpanded: <!-- ${keyword} -->`, exp.line);
|
|
536
|
+
} else if (orig.content === exp.content) {
|
|
537
|
+
results.push({
|
|
538
|
+
keyword,
|
|
539
|
+
line: exp.line,
|
|
540
|
+
status: "ok"
|
|
541
|
+
});
|
|
542
|
+
saveLog(expandedFile, "info", "diff", `Up to date: <!-- ${keyword} -->`, exp.line);
|
|
543
|
+
} else {
|
|
544
|
+
results.push({
|
|
545
|
+
keyword,
|
|
546
|
+
line: exp.line,
|
|
547
|
+
status: "stale"
|
|
548
|
+
});
|
|
549
|
+
saveLog(expandedFile, "warn", "diff", `Stale: <!-- ${keyword} -->`, exp.line);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (const [keyword, originalList] of originalByKeyword) {
|
|
554
|
+
const expandedList = expandedByKeyword.get(keyword) ?? [];
|
|
555
|
+
for (const orig of originalList.slice(expandedList.length)) {
|
|
556
|
+
results.push({
|
|
557
|
+
keyword,
|
|
558
|
+
line: orig.line,
|
|
559
|
+
status: "missing"
|
|
560
|
+
});
|
|
561
|
+
saveLog(expandedFile, "warn", "diff", `Missing: <!-- ${keyword} -->`, orig.line);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return results;
|
|
565
|
+
}
|
|
566
|
+
function extractSections(tree, text, file) {
|
|
567
|
+
const sections = [];
|
|
568
|
+
let lastOpenKeyword;
|
|
569
|
+
let lastOpenLine = 0;
|
|
570
|
+
let lastOpenEndOffset;
|
|
571
|
+
visit(tree, "html", (node, index, parent) => {
|
|
572
|
+
if (parent === void 0 || index === void 0) return CONTINUE;
|
|
573
|
+
const marker = parseCommentNode(node, parent);
|
|
574
|
+
if (marker === void 0) return CONTINUE;
|
|
575
|
+
if (marker.type === "open") {
|
|
576
|
+
if (lastOpenKeyword !== void 0) sections.push({
|
|
577
|
+
content: void 0,
|
|
578
|
+
keyword: lastOpenKeyword,
|
|
579
|
+
line: lastOpenLine
|
|
580
|
+
});
|
|
581
|
+
lastOpenKeyword = marker.keyword;
|
|
582
|
+
lastOpenLine = marker.node.position?.start.line ?? 0;
|
|
583
|
+
lastOpenEndOffset = marker.node.position?.end.offset;
|
|
584
|
+
return CONTINUE;
|
|
585
|
+
}
|
|
586
|
+
if (lastOpenKeyword === void 0) {
|
|
587
|
+
saveLog(file, "warn", "diff", `Close marker without open: <!-- /${marker.keyword} -->`, node);
|
|
588
|
+
return CONTINUE;
|
|
589
|
+
}
|
|
590
|
+
if (lastOpenKeyword !== marker.keyword) {
|
|
591
|
+
saveLog(file, "warn", "diff", `Keyword mismatch: expected <!-- /${lastOpenKeyword} -->, found <!-- /${marker.keyword} -->`, node);
|
|
592
|
+
return CONTINUE;
|
|
593
|
+
}
|
|
594
|
+
const closeStartOffset = marker.node.position?.start.offset;
|
|
595
|
+
const sliced = closeStartOffset === void 0 || lastOpenEndOffset === void 0 ? void 0 : text.slice(lastOpenEndOffset, closeStartOffset).replaceAll("\r\n", "\n").trim();
|
|
596
|
+
const content = sliced === "" ? void 0 : sliced;
|
|
597
|
+
sections.push({
|
|
598
|
+
content,
|
|
599
|
+
keyword: lastOpenKeyword,
|
|
600
|
+
line: lastOpenLine
|
|
601
|
+
});
|
|
602
|
+
lastOpenKeyword = void 0;
|
|
603
|
+
lastOpenEndOffset = void 0;
|
|
604
|
+
return CONTINUE;
|
|
605
|
+
});
|
|
606
|
+
if (lastOpenKeyword !== void 0) sections.push({
|
|
607
|
+
content: void 0,
|
|
608
|
+
keyword: lastOpenKeyword,
|
|
609
|
+
line: lastOpenLine
|
|
610
|
+
});
|
|
611
|
+
return sections;
|
|
612
|
+
}
|
|
613
|
+
function groupByKeyword(sections) {
|
|
614
|
+
const map = /* @__PURE__ */ new Map();
|
|
615
|
+
for (const section of sections) {
|
|
616
|
+
const list = map.get(section.keyword);
|
|
617
|
+
if (list) list.push(section);
|
|
618
|
+
else map.set(section.keyword, [section]);
|
|
619
|
+
}
|
|
620
|
+
return map;
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
486
623
|
//#region src/lib/mdast-utils/mdast-util-mdat-strip.ts
|
|
487
624
|
/**
|
|
488
625
|
* Strips all mdat comment nodes (both opening and closing) from the tree,
|
|
@@ -515,4 +652,4 @@ const remarkMdat = function(options) {
|
|
|
515
652
|
};
|
|
516
653
|
};
|
|
517
654
|
//#endregion
|
|
518
|
-
export { remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatCollapse, mdatExpand, mdatSplit, mdatStrip, optionsSchema, reporterMdat, rulesSchema, setLogger };
|
|
655
|
+
export { remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatCollapse, mdatDiff, mdatExpand, mdatSplit, mdatStrip, optionsSchema, reporterMdat, rulesSchema, setLogger };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remark-mdat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A remark plugin implementing the Markdown Autophagic Template (MDAT) system.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mdat",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"@arethetypeswrong/core": "^0.18.2",
|
|
62
62
|
"@kitschpatrol/shared-config": "^7.0.1",
|
|
63
63
|
"@types/node": "~20.19.37",
|
|
64
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
64
65
|
"bumpp": "^11.0.1",
|
|
65
66
|
"publint": "^0.3.18",
|
|
66
67
|
"tsdown": "^0.21.7",
|
|
@@ -81,6 +82,7 @@
|
|
|
81
82
|
"bench:baseline": "vitest bench --run --no-file-parallelism --outputJson test/benchmarks/baseline.json",
|
|
82
83
|
"build": "tsdown",
|
|
83
84
|
"clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
|
|
85
|
+
"coverage": "vitest --coverage --run",
|
|
84
86
|
"dev": "pnpm run test",
|
|
85
87
|
"fix": "ksc fix",
|
|
86
88
|
"lint": "ksc lint",
|
package/readme.md
CHANGED
|
@@ -293,6 +293,12 @@ Errors and warnings are reported inline during expansion via [VFile messages](ht
|
|
|
293
293
|
|
|
294
294
|
_Exported as `mdatStrip(tree: Root, file: VFile): void`_
|
|
295
295
|
|
|
296
|
+
- [**`mdast-util-mdat-diff`**](./src/lib/mdast-utils/mdast-util-mdat-diff.ts)
|
|
297
|
+
|
|
298
|
+
Compares an original document against an expanded document per-tag, identifying which mdat comments have stale content, are unexpanded, missing, or are up to date. Reports results via VFile messages (`source: 'diff'`) and returns structured `MdatDiffResult[]`. Both trees should have `mdatSplit` applied before calling. Useful for implementing `check` commands that report which specific tags need updating.
|
|
299
|
+
|
|
300
|
+
_Exported as `mdatDiff(originalTree: Root, originalFile: VFile, expandedTree: Root, expandedFile: VFile): MdatDiffResult[]`_
|
|
301
|
+
|
|
296
302
|
## Migrating from 1.x to 2.x
|
|
297
303
|
|
|
298
304
|
Version 2.0 simplifies and solidifies the API by removing several configuration options and validation features that added complexity without sufficient benefit. The core expansion behavior is unchanged — the plugin still matches HTML comments to rules and expands them — but the way you configure it has changed.
|