remark-mdat 2.0.2 → 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,30 +113,61 @@ 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
  */
118
128
  declare function getSoleRuleKey<T extends NormalizedRules | Rules>(rules: T): keyof T;
119
129
  //#endregion
120
130
  //#region src/lib/mdast-utils/mdast-util-mdat.d.ts
121
- /** Mdast utility that splits, cleans, and expands all mdat comments in the tree. */
131
+ /**
132
+ * Mdast utility that splits, collapses, and then re-expands all mdat comments
133
+ * in the tree.
134
+ */
122
135
  declare function mdat(tree: Root, file: VFile, rules: NormalizedRules | Rules): Promise<void>;
123
136
  //#endregion
124
- //#region src/lib/mdast-utils/mdast-util-mdat-clean.d.ts
137
+ //#region src/lib/mdast-utils/mdast-util-mdat-collapse.d.ts
125
138
  /**
126
139
  * Collapses any expanded mdat comments, effectively resetting the document to
127
- * its pre-expansion state. No-op if no mdat comments are found.
140
+ * its pre-expansion state, preserving the original comments. No-op if no mdat
141
+ * comments are found.
142
+ */
143
+ declare function mdatCollapse(tree: Root, file: VFile): void;
144
+ //#endregion
145
+ //#region src/lib/mdast-utils/mdast-util-mdat-clean.d.ts
146
+ /**
147
+ * @deprecated Use {@link mdatCollapse} instead. This alias will be removed in a
148
+ * future major version.
128
149
  */
129
- declare function mdatClean(tree: Root, file: VFile): void;
150
+ declare const mdatClean: typeof mdatCollapse;
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[];
130
171
  //#endregion
131
172
  //#region src/lib/mdast-utils/mdast-util-mdat-expand.d.ts
132
173
  /**
@@ -136,11 +177,20 @@ declare function mdatExpand(tree: Root, file: VFile, rules: NormalizedRules | Ru
136
177
  //#endregion
137
178
  //#region src/lib/mdast-utils/mdast-util-mdat-split.d.ts
138
179
  /**
139
- * Mdast utility plugin to split any multi-comment nodes and their content into individual MDAST HTML
140
- * 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.
141
183
  */
142
184
  declare function mdatSplit(tree: Root, file: VFile): void;
143
185
  //#endregion
186
+ //#region src/lib/mdast-utils/mdast-util-mdat-strip.d.ts
187
+ /**
188
+ * Strips all mdat comment nodes (both opening and closing) from the tree,
189
+ * preserving any content between them. Code-style comments (`//`, `#`, `/*`)
190
+ * are left untouched.
191
+ */
192
+ declare function mdatStrip(tree: Root, _file: VFile): void;
193
+ //#endregion
144
194
  //#region src/lib/mdat/log.d.ts
145
195
  /**
146
196
  * Set the logger instance for the module.
@@ -185,4 +235,4 @@ declare const optionsSchema: z.ZodObject<{
185
235
  */
186
236
  declare const remarkMdat: Plugin<[Options?], Root>;
187
237
  //#endregion
188
- 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, mdatExpand, mdatSplit, 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;
@@ -173,12 +178,13 @@ function isComment(text) {
173
178
  return trimmed.startsWith("<!--") && trimmed.endsWith("-->");
174
179
  }
175
180
  //#endregion
176
- //#region src/lib/mdast-utils/mdast-util-mdat-clean.ts
181
+ //#region src/lib/mdast-utils/mdast-util-mdat-collapse.ts
177
182
  /**
178
183
  * Collapses any expanded mdat comments, effectively resetting the document to
179
- * its pre-expansion state. No-op if no mdat comments are found.
184
+ * its pre-expansion state, preserving the original comments. No-op if no mdat
185
+ * comments are found.
180
186
  */
181
- function mdatClean(tree, file) {
187
+ function mdatCollapse(tree, file) {
182
188
  let lastOpenMarker;
183
189
  visit(tree, "html", (node, index, parent) => {
184
190
  if (parent === void 0 || index === void 0) return CONTINUE;
@@ -188,25 +194,23 @@ function mdatClean(tree, file) {
188
194
  lastOpenMarker = marker;
189
195
  return CONTINUE;
190
196
  }
191
- if (marker.type === "close") {
192
- if (lastOpenMarker === void 0) {
193
- saveLog(file, "error", "clean", "Found closing marker without opening marker", node);
194
- return CONTINUE;
195
- }
196
- if (lastOpenMarker.parent !== marker.parent) {
197
- saveLog(file, "error", "clean", "Opening marker doesn't share a parent", node);
198
- return CONTINUE;
199
- }
200
- if (lastOpenMarker.keyword !== marker.keyword) {
201
- saveLog(file, "error", "clean", "Opening marker doesn't share a keyword", node);
202
- return CONTINUE;
203
- }
204
- const openMarkerIndex = parent.children.indexOf(lastOpenMarker.node);
205
- const nodesToRemove = parent.children.indexOf(marker.node) - openMarkerIndex + 1;
206
- parent.children.splice(openMarkerIndex + 1, nodesToRemove - 1);
207
- lastOpenMarker = void 0;
208
- 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;
209
200
  }
201
+ if (lastOpenMarker.parent !== marker.parent) {
202
+ saveLog(file, "error", "collapse", "Opening marker doesn't share a parent", node);
203
+ return CONTINUE;
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];
210
214
  });
211
215
  }
212
216
  //#endregion
@@ -217,7 +221,10 @@ const NORMALIZED = Symbol("normalized");
217
221
  function isNormalized(rules) {
218
222
  return NORMALIZED in rules;
219
223
  }
220
- /** 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
+ */
221
228
  function normalizeRules(rules) {
222
229
  validateRules(rules);
223
230
  const normalizedRules = {};
@@ -280,9 +287,9 @@ const keywordSchema = z.string().check(z.refine((key) => !COMMENT_PREFIX_REGEX.t
280
287
  /** Zod schema for validating {@link Rules} records. */
281
288
  const rulesSchema = z.record(keywordSchema, ruleSchema).describe("MDAT Rules");
282
289
  /**
283
- * Expand rule content. For compound rules (content arrays), individual
284
- * sub-rule failures are reported via `onWarning` and skipped. The entire
285
- * 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.
286
293
  */
287
294
  async function getRuleContent(rule, options, context, onWarning) {
288
295
  if (Array.isArray(rule.content)) {
@@ -308,8 +315,8 @@ async function getRuleContent(rule, options, context, onWarning) {
308
315
  }
309
316
  }
310
317
  /**
311
- * Returns the rule value from a single-rule record.
312
- * 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.
313
320
  *
314
321
  * Throws if there are no entries or more than one entry.
315
322
  */
@@ -317,8 +324,8 @@ function getSoleRule(rules) {
317
324
  return getSoleRecord(rules);
318
325
  }
319
326
  /**
320
- * Returns the rule key from a single-rule record.
321
- * Useful for comment placeholder validation.
327
+ * Returns the rule key from a single-rule record. Useful for comment
328
+ * placeholder validation.
322
329
  *
323
330
  * Throws if there are no entries or more than one entry.
324
331
  */
@@ -330,9 +337,11 @@ function getSoleRuleKey(rules) {
330
337
  /**
331
338
  * Get the sole entry in a record.
332
339
  *
333
- * Useful for working with Rules records
334
- * 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
+ *
335
343
  * @param record The record to get the sole entry from
344
+ *
336
345
  * @returns The value of the sole entry in the record
337
346
  * @throws {Error} If there are no entries or more than one entry
338
347
  */
@@ -401,8 +410,9 @@ async function mdatExpand(tree, file, rules) {
401
410
  //#endregion
402
411
  //#region src/lib/mdast-utils/mdast-util-mdat-split.ts
403
412
  /**
404
- * Mdast utility plugin to split any multi-comment nodes and their content into individual MDAST HTML
405
- * 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.
406
416
  */
407
417
  function mdatSplit(tree, file) {
408
418
  visit(tree, "html", (node, index, parent) => {
@@ -419,7 +429,10 @@ function mdatSplit(tree, file) {
419
429
  }
420
430
  });
421
431
  }
422
- /** 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
+ */
423
436
  function splitHtmlIntoMdastNodes(mdastNode) {
424
437
  const htmlTree = fromHtml(mdastNode.value, { fragment: true });
425
438
  const mdastNodes = [];
@@ -465,13 +478,163 @@ function getOriginalMarkup(mdastNode, hastNode) {
465
478
  }
466
479
  //#endregion
467
480
  //#region src/lib/mdast-utils/mdast-util-mdat.ts
468
- /** Mdast utility that splits, cleans, and expands all mdat comments in the tree. */
481
+ /**
482
+ * Mdast utility that splits, collapses, and then re-expands all mdat comments
483
+ * in the tree.
484
+ */
469
485
  async function mdat(tree, file, rules) {
470
486
  mdatSplit(tree, file);
471
- mdatClean(tree, file);
487
+ mdatCollapse(tree, file);
472
488
  await mdatExpand(tree, file, rules);
473
489
  }
474
490
  //#endregion
491
+ //#region src/lib/mdast-utils/mdast-util-mdat-clean.ts
492
+ /**
493
+ * @deprecated Use {@link mdatCollapse} instead. This alias will be removed in a
494
+ * future major version.
495
+ */
496
+ const mdatClean = mdatCollapse;
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
623
+ //#region src/lib/mdast-utils/mdast-util-mdat-strip.ts
624
+ /**
625
+ * Strips all mdat comment nodes (both opening and closing) from the tree,
626
+ * preserving any content between them. Code-style comments (`//`, `#`, `/*`)
627
+ * are left untouched.
628
+ */
629
+ function mdatStrip(tree, _file) {
630
+ visit(tree, "html", (node, index, parent) => {
631
+ if (parent === void 0 || index === void 0) return CONTINUE;
632
+ if (parseCommentNode(node, parent) === void 0) return CONTINUE;
633
+ parent.children.splice(index, 1);
634
+ return [CONTINUE, index];
635
+ });
636
+ }
637
+ //#endregion
475
638
  //#region src/lib/remark-mdat.ts
476
639
  const defaultRules = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
477
640
  /** Zod schema for validating {@link Options}. */
@@ -489,4 +652,4 @@ const remarkMdat = function(options) {
489
652
  };
490
653
  };
491
654
  //#endregion
492
- export { remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatExpand, mdatSplit, 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.0.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",
@@ -59,8 +59,9 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@arethetypeswrong/core": "^0.18.2",
62
- "@kitschpatrol/shared-config": "^7.0.0",
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",
@@ -77,10 +78,11 @@
77
78
  }
78
79
  },
79
80
  "scripts": {
80
- "bench": "vitest bench --no-file-parallelism --compare test/benchmarks/baseline.json",
81
- "bench:baseline": "vitest bench --no-file-parallelism --outputJson test/benchmarks/baseline.json",
81
+ "bench": "vitest bench --run --no-file-parallelism --compare test/benchmarks/baseline.json",
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
@@ -46,11 +46,13 @@
46
46
  - [Removed options](#removed-options)
47
47
  - [Removed rule properties](#removed-rule-properties)
48
48
  - [Changed rule properties](#changed-rule-properties)
49
+ - [Renamed `clean` to `collapse`](#renamed-clean-to-collapse)
49
50
  - [Removed validation utility](#removed-validation-utility)
50
51
  - [Rule function signature change](#rule-function-signature-change)
51
52
  - [Stricter argument syntax](#stricter-argument-syntax)
52
53
  - [Comment-style comments are ignored](#comment-style-comments-are-ignored)
53
54
  - [Compound rule error handling](#compound-rule-error-handling)
55
+ - [New utility: `mdatStrip`](#new-utility-mdatstrip)
54
56
  - [Removed export: `deepMergeDefined`](#removed-export-deepmergedefined)
55
57
  - [Implementation notes](#implementation-notes)
56
58
  - [Maintainers](#maintainers)
@@ -273,11 +275,11 @@ Errors and warnings are reported inline during expansion via [VFile messages](ht
273
275
 
274
276
  _Exported as `mdatSplit(tree: Root, file: VFile): void`_
275
277
 
276
- - [**`mdast-util-mdat-clean`**](./src/lib/mdast-utils/mdast-util-mdat-clean.ts)
278
+ - [**`mdast-util-mdat-collapse`**](./src/lib/mdast-utils/mdast-util-mdat-collapse.ts)
277
279
 
278
280
  Transformer function that resets all mdat comment expansions in a file, collapsing expanded comments back into single-line placeholders.
279
281
 
280
- _Exported as `mdatClean(tree: Root, file: VFile): void`_
282
+ _Exported as `mdatCollapse(tree: Root, file: VFile): void`_
281
283
 
282
284
  - [**`mdast-util-mdat-expand`**](./src/lib/mdast-utils/mdast-util-mdat-expand.ts)
283
285
 
@@ -285,6 +287,18 @@ Errors and warnings are reported inline during expansion via [VFile messages](ht
285
287
 
286
288
  _Exported as `mdatExpand(tree: Root, file: VFile, rules: Rules): Promise<void>`_
287
289
 
290
+ - [**`mdast-util-mdat-strip`**](./src/lib/mdast-utils/mdast-util-mdat-strip.ts)
291
+
292
+ Transformer function that strips all mdat comment nodes (both opening and closing) from the tree, preserving any content between them. Code-style comments (`<!-- // ... -->`, `<!-- # ... -->`, `<!-- /* ... */ -->`) are left untouched. Useful for producing a final Markdown document with all mdat scaffolding removed.
293
+
294
+ _Exported as `mdatStrip(tree: Root, file: VFile): void`_
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
+
288
302
  ## Migrating from 1.x to 2.x
289
303
 
290
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.
@@ -339,6 +353,10 @@ The following plugin options have been removed entirely:
339
353
  | ------------------ | ------------------ |
340
354
  | `applicationOrder` | Change to `order`. |
341
355
 
356
+ ### Renamed `clean` to `collapse`
357
+
358
+ The former `mdast-util-mdat-clean` / `mdatClean` is now `mdast-util-mdat-collapse` / `mdatCollapse` since this is more clearly the opposite of "expand", and aligns with language used in the Mdat CLI tool. (Note that `mdatClean` is still available as a deprecated alias, but it will be removed in 3.0.)
359
+
342
360
  ### Removed validation utility
343
361
 
344
362
  The `mdast-util-mdat-check` utility and its export `mdatCheck` have been removed. Validation logic (missing rules, empty content, rule errors) is now handled inline during expansion by `mdatExpand`, which reports issues as VFile messages. Use `reporterMdat` to format and display these messages.
@@ -391,6 +409,10 @@ HTML comments using code-style prefixes (`<!-- // ... -->`, `<!-- # ... -->`, `<
391
409
 
392
410
  In 1.x, a failing sub-rule in a compound rule (array of rules) caused the entire expansion to fail. In 2.x, individual sub-rule failures are reported as warnings and skipped — the expansion only fails if every sub-rule fails.
393
411
 
412
+ ### New utility: `mdatStrip`
413
+
414
+ A new `mdast-util-mdat-strip` utility is available for removing all mdat comment nodes from a document while preserving the content between them. Code-style comments (`//`, `#`, `/*`) are left untouched. This is useful for producing a final Markdown document with no mdat scaffolding. See the [Utilities](#utilities) section for details.
415
+
394
416
  ### Removed export: `deepMergeDefined`
395
417
 
396
418
  The `deepMergeDefined` utility has been moved to the [`mdat`](https://github.com/kitschpatrol/mdat) package. If you were importing it from `remark-mdat`, import it from `mdat` instead.