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 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
- /** Recursively simplifies a type for cleaner IDE hover display. Approximation of type-fest's internal `SimplifyDeep`. */
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
- * Rules normalized to a form with async content functions and other default metadata
22
- * Simplifies processing elsewhere, while retaining flexibility for rule authors
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
- * For 'compound' rules, this can be an array of rules (without keywords).
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
- * Helpful if a rule depends on the presence of content generated by another rule
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
- * Can be defined at the top level, if no validation metadata is required, or as the 'content' value
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
- * Can be a simple string for direct replacement, a function that returns a string, or an async function that returns a string.
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
- * @param options
62
- * JSON value of options parsed immediately after the comment keyword in the comment, e.g.:
63
- * `<!-- keyword({something: true}) -->` or
64
- * `<!-- keyword {something: true}-->`
65
- * Sets options to {something: true}
66
- * @param context
67
- * Rule context containing the mdast AST, parsed frontmatter, and file path.
68
- * @returns A string with the generated content. The string will be parsed as Markdown and inserted into the document at the comment's location.
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, and values explain what should be expanded at the comment site.
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
- * Most basic rule:
83
- * ```ts
84
- * { basic: 'content' }
85
- * ```
92
+ * Most basic rule:
93
+ * ```ts
94
+ * { basic: 'content' }
95
+ * ```
86
96
  *
87
- * Rule with dynamic content:
88
- * ```ts
89
- * { basic: () => `${new Date().toISOString()}` }
90
- * ```
97
+ * Rule with dynamic content:
98
+ * ```ts
99
+ * { basic: () => `${new Date().toISOString()}` }
100
+ * ```
91
101
  *
92
- * Rule with metadata:
93
- * ```ts
94
- * { basic-meta: { order: 1, content: 'content'} }
95
- * ```
102
+ * Rule with metadata:
103
+ * ```ts
104
+ * { basic-meta: { order: 1, content: 'content'} }
105
+ * ```
96
106
  *
97
- * Rule with dynamic content and metadata:
98
- * { basic-date: { order: 1, content: () => `${new Date().toISOString()}` } }
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
- * Useful when aliasing rules or invoking them programmatically.
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
- * Useful for comment placeholder validation.
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 individual MDAST HTML
151
- * nodes. They're wrapped in a paragraph so as not to introduce new breaks.
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
- * @returns A CommentMarkerNode or undefined if the node is not a recognized comment.
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
- } else if (error instanceof Error) throw new VFileMessage(error.message, node);
130
- else throw new VFileMessage("Unknown error", node);
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
- * Comments using code-style notation (`//`, `#`, `/*`) are ignored and return `undefined`.
139
- * @returns A CommentMarker or undefined if the node is not a recognized comment.
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 (marker.type === "close") {
193
- if (lastOpenMarker === void 0) {
194
- saveLog(file, "error", "collapse", "Found closing marker without opening marker", node);
195
- return CONTINUE;
196
- }
197
- if (lastOpenMarker.parent !== marker.parent) {
198
- saveLog(file, "error", "collapse", "Opening marker doesn't share a parent", node);
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
- /** Converts flexible {@link Rules} into strict {@link NormalizedRules} for internal processing. */
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
- * sub-rule failures are reported via `onWarning` and skipped. The entire
286
- * expansion only fails if every sub-rule fails.
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
- * Useful when aliasing rules or invoking them programmatically.
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
- * Useful for comment placeholder validation.
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
- * that are only supposed to contain a single rule.
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 individual MDAST HTML
406
- * nodes. They're wrapped in a paragraph so as not to introduce new breaks.
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
- /** Splits a single mdast HTML node containing multiple comments into individual HTML and text nodes. Exported for testing. */
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.1.0",
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.