remark-mdat 1.2.5 → 2.0.0-preview.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/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ILogBasic, ILogLayer } from "lognow";
1
2
  import { z } from "zod";
2
3
  import { Root } from "mdast";
3
4
  import { VFile } from "vfile";
@@ -6,42 +7,37 @@ import { Plugin } from "unified";
6
7
 
7
8
  //#region src/lib/mdat/rules.d.ts
8
9
  type SimplifyDeep<T> = Simplify<MergeDeep<T, T>>;
10
+ /**
11
+ * Context passed to rule content functions during expansion.
12
+ */
13
+ type RuleContext = {
14
+ /** File path of the source document, if known. */filePath: string | undefined; /** Parsed YAML frontmatter from the document, if present. */
15
+ frontmatter: Record<string, unknown> | undefined; /** The full mdast AST of the document. Do not mutate. */
16
+ tree: Root;
17
+ };
9
18
  /**
10
19
  * Strict normalized rules used internally.
11
20
  * Rules normalized to a form with async content functions and other default metadata
12
21
  * Simplifies processing elsewhere, while retaining flexibility for rule authors
13
22
  */
14
23
  type NormalizedRule = {
15
- /**
16
- * The order in which the rule should be applied during processing
17
- * Helpful if a rule depends on the presence of content generated by another rule
18
- * Defaults to 0.
19
- */
20
- applicationOrder: number;
21
24
  /**
22
25
  * The function that generates the expanded Markdown string.
23
26
  * For 'compound' rules, this can be an array of rules (without keywords).
24
27
  */
25
- content: ((options: JsonValue, tree: Root) => Promise<string>) | NormalizedRule[];
28
+ content: ((options: JsonValue, context: RuleContext) => Promise<string>) | NormalizedRule[];
26
29
  /**
27
- * The expected order of the keyword in the document relative to other expander comments.
28
- * Used for validation purposes.
29
- * Leave undefined to order skip validation.
30
- * Defaults to undefined, which means order is not enforced.
31
- */
32
- order: number | undefined;
33
- /**
34
- * Whether the presence of the keyword comment in the document is required.
35
- * Used for validation purposes.
36
- * Defaults to false.
30
+ * The order in which the rule should be applied during processing
31
+ * Helpful if a rule depends on the presence of content generated by another rule
32
+ * Defaults to 0.
37
33
  */
38
- required: boolean;
34
+ order: number;
39
35
  };
40
36
  type Rule =
41
37
  /**
42
38
  * Function that returns the Markdown string to expand at the comment site.
43
39
  */
44
- ((options: JsonValue, tree: Root) => Promise<string> | string)
40
+ ((options: JsonValue, context: RuleContext) => Promise<string> | string)
45
41
  /**
46
42
  * Compound rules may be defined an array of rules, without keywords.
47
43
  * Can be defined at the top level, if no validation metadata is required, or as the 'content' value
@@ -53,14 +49,9 @@ type Rule =
53
49
  */
54
50
  | string
55
51
  /**
56
- * Rule object with optional validation metadata.
52
+ * Rule object with optional metadata.
57
53
  */
58
54
  | {
59
- /**
60
- * The order in which the rule should be applied during processing.
61
- * Defaults to 0.
62
- */
63
- applicationOrder?: number;
64
55
  /**
65
56
  * Gets content to expand into the comment.
66
57
  * Can be a simple string for direct replacement, a function that returns a string, or an async function that returns a string.
@@ -71,21 +62,16 @@ type Rule =
71
62
  * `<!-- keyword({something: true}) -->` or
72
63
  * `<!-- keyword {something: true}-->`
73
64
  * Sets options to {something: true}
74
- * @param tree
75
- * Markdown (mdast) abstract syntax tree containing the entire parsed document. Useful for expanders that need the entire document context, such as when generating a table of contents. Do not mutate the AST, instead return a new string.
65
+ * @param context
66
+ * Rule context containing the mdast AST, parsed frontmatter, and file path.
76
67
  * @returns A string with the generated content. The string will be parsed as Markdown and inserted into the document at the comment's location.
77
68
  */
78
- content: ((options: JsonValue, tree: Root) => Promise<string> | string) | Rule[] | string;
69
+ content: ((options: JsonValue, context: RuleContext) => Promise<string> | string) | Rule[] | string;
79
70
  /**
80
- * The expected order of the keyword in the document relative to other expander comments.
81
- * Defaults to undefined, which means order is not enforced.
82
- */
83
- order?: number | undefined;
84
- /**
85
- * Whether the presence of the keyword comment in the document is required.
86
- * Defaults to false.
71
+ * The order in which the rule should be applied during processing.
72
+ * Defaults to 0.
87
73
  */
88
- required?: boolean;
74
+ order?: number;
89
75
  };
90
76
  /**
91
77
  * Rules are record objects whose keys match strings inside a Markdown comment, and values explain what should be expanded at the comment site.
@@ -104,15 +90,15 @@ type Rule =
104
90
  *
105
91
  * Rule with metadata:
106
92
  * ```ts
107
- * { basic-meta: { required: true, content: 'content'} }
93
+ * { basic-meta: { order: 1, content: 'content'} }
108
94
  * ```
109
95
  *
110
96
  * Rule with dynamic content and metadata:
111
- * { basic-date: { required: true, content: () => `${new Date().toISOString()}` } }
97
+ * { basic-date: { order: 1, content: () => `${new Date().toISOString()}` } }
112
98
  */
113
99
  type Rules = SimplifyDeep<Record<string, Rule>>;
114
100
  type NormalizedRules = SimplifyDeep<Record<string, NormalizedRule>>;
115
- declare const rulesSchema: z.ZodRecord<z.ZodString, z.ZodType<any, z.ZodTypeDef, any>>;
101
+ declare const rulesSchema: z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
116
102
  /**
117
103
  * Returns the rule value from a single-rule record.
118
104
  * Useful when aliasing rules or invoking them programmatically.
@@ -129,51 +115,20 @@ declare function getSoleRule<T extends NormalizedRules | Rules>(rules: T): T[key
129
115
  declare function getSoleRuleKey<T extends NormalizedRules | Rules>(rules: T): keyof T;
130
116
  //#endregion
131
117
  //#region src/lib/mdast-utils/mdast-util-mdat.d.ts
132
- type MdatOptions = {
133
- addMetaComment: boolean | string;
134
- closingPrefix: string;
135
- keywordPrefix: string;
136
- metaCommentIdentifier: string;
137
- rules: Rules;
138
- };
139
- declare function mdat(tree: Root, file: VFile, options: MdatOptions): Promise<void>;
140
- //#endregion
141
- //#region src/lib/mdast-utils/mdast-util-mdat-check.d.ts
142
- type MdatCheckOptions = {
143
- addMetaComment: boolean | string;
144
- closingPrefix: string;
145
- keywordPrefix: string;
146
- metaCommentIdentifier: string; /** Enable extra checks, too noisy for real life. */
147
- paranoid: boolean;
148
- rules: Rules;
149
- };
150
- /**
151
- * Mdast utility function to check mdat source document, and output.
152
- */
153
- declare function mdatCheck(tree: Root, file: VFile, options: MdatCheckOptions): Promise<void>;
118
+ declare function mdat(tree: Root, file: VFile, rules: Rules): Promise<void>;
154
119
  //#endregion
155
120
  //#region src/lib/mdast-utils/mdast-util-mdat-clean.d.ts
156
- type MdatCleanOptions = {
157
- closingPrefix: string;
158
- keywordPrefix: string;
159
- metaCommentIdentifier: string;
160
- };
161
121
  /**
162
- * Collapses any expanded mdat comments and removes meta comments,
163
- * effectively resetting the document to its pre-expansion state. No-op if no
164
- * mdat comments are found.
122
+ * Collapses any expanded mdat comments, effectively resetting the document to
123
+ * its pre-expansion state. No-op if no mdat comments are found.
165
124
  */
166
- declare function mdatClean(tree: Root, file: VFile, options: MdatCleanOptions): void;
125
+ declare function mdatClean(tree: Root, file: VFile): void;
167
126
  //#endregion
168
127
  //#region src/lib/mdast-utils/mdast-util-mdat-expand.d.ts
169
- type MdatExpandOptions = {
170
- addMetaComment: boolean | string;
171
- closingPrefix: string;
172
- keywordPrefix: string;
173
- metaCommentIdentifier: string;
174
- rules: Rules;
175
- };
176
- declare function mdatExpand(tree: Root, file: VFile, options: MdatExpandOptions): Promise<void>;
128
+ /**
129
+ * Mdast utility to expand mdat comments in the tree.
130
+ */
131
+ declare function mdatExpand(tree: Root, file: VFile, rules: Rules): Promise<void>;
177
132
  //#endregion
178
133
  //#region src/lib/mdast-utils/mdast-util-mdat-split.d.ts
179
134
  /**
@@ -182,21 +137,13 @@ declare function mdatExpand(tree: Root, file: VFile, options: MdatExpandOptions)
182
137
  */
183
138
  declare function mdatSplit(tree: Root, file: VFile): void;
184
139
  //#endregion
185
- //#region src/lib/mdat/deep-merge-defined.d.ts
186
- declare function deepMergeDefined<T extends Record<string, unknown>>(...objects: T[]): T;
187
- //#endregion
188
140
  //#region src/lib/mdat/log.d.ts
189
- declare const log: {
190
- verbose: boolean;
191
- log(...data: unknown[]): void;
192
- logPrefixed(prefix: string, ...data: unknown[]): void;
193
- info(...data: unknown[]): void;
194
- infoPrefixed(prefix: string, ...data: unknown[]): void;
195
- warn(...data: unknown[]): void;
196
- warnPrefixed(prefix: string, ...data: unknown[]): void;
197
- error(...data: unknown[]): void;
198
- errorPrefixed(prefix: string, ...data: unknown[]): void;
199
- };
141
+ /**
142
+ * Set the logger instance for the module.
143
+ * Export this for library consumers to inject their own logger.
144
+ * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
145
+ */
146
+ declare function setLogger(logger?: ILogBasic | ILogLayer): void;
200
147
  //#endregion
201
148
  //#region src/lib/mdat/mdat-log.d.ts
202
149
  /**
@@ -220,29 +167,10 @@ declare function getMdatReports(files: VFile[]): MdatFileReport[];
220
167
  declare function reporterMdat(files: VFile[]): void;
221
168
  //#endregion
222
169
  //#region src/lib/remark-mdat.d.ts
223
- type Options = Partial<MdatOptions>;
224
- declare const optionsSchema: z.ZodObject<{
225
- addMetaComment: z.ZodOptional<z.ZodUnion<[z.ZodBoolean, z.ZodString]>>;
226
- closingPrefix: z.ZodOptional<z.ZodString>;
227
- keywordPrefix: z.ZodOptional<z.ZodString>;
228
- metaCommentIdentifier: z.ZodOptional<z.ZodString>;
229
- rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<any, z.ZodTypeDef, any>>>;
230
- }, "strip", z.ZodTypeAny, {
231
- closingPrefix?: string | undefined;
232
- keywordPrefix?: string | undefined;
233
- addMetaComment?: string | boolean | undefined;
234
- metaCommentIdentifier?: string | undefined;
235
- rules?: Record<string, any> | undefined;
236
- }, {
237
- closingPrefix?: string | undefined;
238
- keywordPrefix?: string | undefined;
239
- addMetaComment?: string | boolean | undefined;
240
- metaCommentIdentifier?: string | undefined;
241
- rules?: Record<string, any> | undefined;
242
- }>;
170
+ type Options = Rules;
243
171
  /**
244
172
  * A remark plugin that expands HTML comments in Markdown files.
245
173
  */
246
174
  declare const remarkMdat: Plugin<[Options], Root>;
247
175
  //#endregion
248
- export { type MdatCheckOptions, type MdatCleanOptions, type MdatExpandOptions, type MdatFileReport, type MdatMessage, type MdatOptions, type NormalizedRule, type NormalizedRules, type Options, type Rule, type Rules, type SimplifyDeep, deepMergeDefined, remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, log, mdat, mdatCheck, mdatClean, mdatExpand, mdatSplit, optionsSchema, reporterMdat, rulesSchema };
176
+ 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, rulesSchema as optionsSchema, rulesSchema, reporterMdat, setLogger };
package/dist/index.js CHANGED
@@ -1,49 +1,31 @@
1
- import Table from "cli-table3";
2
- import picocolors from "picocolors";
3
1
  import { CONTINUE, SKIP, visit } from "unist-util-visit";
4
2
  import path from "node:path";
3
+ import picocolors from "picocolors";
4
+ import { createLogger, injectionHelper } from "lognow";
5
5
  import json5 from "json5";
6
6
  import { VFileMessage } from "vfile-message";
7
- import { z } from "zod";
7
+ import { matter } from "gray-matter-es";
8
8
  import { remark } from "remark";
9
9
  import remarkGfm from "remark-gfm";
10
+ import { z } from "zod";
10
11
  import { fromHtml } from "hast-util-from-html";
11
- import { deepmerge } from "deepmerge-ts";
12
+ //#endregion
12
13
  //#region src/lib/mdat/log.ts
13
- const isNode = process?.versions?.node !== void 0;
14
- const log = {
15
- verbose: false,
16
- log(...data) {
17
- if (!this.verbose) return;
18
- const levelPrefix = picocolors.gray("[Log]");
19
- if (isNode) console.warn(levelPrefix, ...data);
20
- else console.log(levelPrefix, ...data);
21
- },
22
- logPrefixed(prefix, ...data) {
23
- this.info(picocolors.blue(`[${prefix}]`), ...data);
24
- },
25
- info(...data) {
26
- if (!this.verbose) return;
27
- const levelPrefix = picocolors.green("[Info]");
28
- if (isNode) console.warn(levelPrefix, ...data);
29
- else console.info(levelPrefix, ...data);
30
- },
31
- infoPrefixed(prefix, ...data) {
32
- this.info(picocolors.blue(`[${prefix}]`), ...data);
33
- },
34
- warn(...data) {
35
- console.warn(picocolors.yellow("[Warning]"), ...data);
36
- },
37
- warnPrefixed(prefix, ...data) {
38
- this.warn(picocolors.blue(`[${prefix}]`), ...data);
39
- },
40
- error(...data) {
41
- console.error(picocolors.red("[Error]"), ...data);
42
- },
43
- errorPrefixed(prefix, ...data) {
44
- this.error(picocolors.blue(`[${prefix}]`), ...data);
45
- }
46
- };
14
+ /**
15
+ * The default logger instance for the library.
16
+ */
17
+ let log = createLogger({
18
+ logToConsole: { showTime: false },
19
+ name: "remark-mdat"
20
+ });
21
+ /**
22
+ * Set the logger instance for the module.
23
+ * Export this for library consumers to inject their own logger.
24
+ * @param logger - Accepts either a LogLayer instance or a Console- or Stream-like log target
25
+ */
26
+ function setLogger(logger) {
27
+ log = injectionHelper(logger);
28
+ }
47
29
  //#endregion
48
30
  //#region src/lib/mdat/mdat-log.ts
49
31
  function saveLog(file, level, source, message, lineOrNode, maybeColumn) {
@@ -127,12 +109,11 @@ function highlightComments(text, level) {
127
109
  //#region src/lib/mdat/parse.ts
128
110
  /**
129
111
  * Parse an Mdast HTML comment node into structured data.
130
- * @returns A discriminated union of CommentMarkerNode based on comment type, or
131
- * undefined if the node is not a comment.
112
+ * @returns A CommentMarkerNode or undefined if the node is not a recognized comment.
132
113
  */
133
- function parseCommentNode(node, parent, options) {
114
+ function parseCommentNode(node, parent) {
134
115
  try {
135
- const result = parseComment(node.value, options);
116
+ const result = parseComment(node.value);
136
117
  if (result === void 0) return;
137
118
  return {
138
119
  ...result,
@@ -149,110 +130,111 @@ function parseCommentNode(node, parent, options) {
149
130
  }
150
131
  /**
151
132
  * Parse any comment string into structured data.
152
- * @returns A discriminated union of CommentMarker based on comment type, or
153
- * undefined if the node is not a comment.
133
+ * Comments using code-style notation (`//`, `#`, `/*`) are ignored and return `undefined`.
134
+ * @returns A CommentMarker or undefined if the node is not a recognized comment.
154
135
  */
155
- function parseComment(text, options) {
136
+ function parseComment(text) {
156
137
  if (!isComment(text)) return;
157
- const { closingPrefix, keywordPrefix, metaCommentIdentifier } = options;
158
- if (closingPrefix === "") throw new VFileMessage("closingPrefix must not be an empty string");
138
+ const closingPrefix = "/";
159
139
  const commentHtml = text.trim();
160
140
  const commentBody = commentHtml.replace(/^\s*<!-{2,}\s*/, "").replace(/\s*-{2,}>\s*$/, "");
161
- const [rawKeyword, ...argumentParts] = commentBody.split(/(\s+|\(|\{)/);
162
- const type = rawKeyword.startsWith(metaCommentIdentifier) ? "meta" : keywordPrefix !== "" && !rawKeyword.startsWith(keywordPrefix) && !rawKeyword.startsWith(`${closingPrefix}${keywordPrefix}`) ? "native" : rawKeyword.startsWith(closingPrefix) ? "close" : "open";
163
- if (type === "meta") return {
164
- content: trimMetaIdentifiers(commentBody, metaCommentIdentifier),
165
- html: commentHtml,
166
- type
167
- };
168
- if (type === "native") return {
169
- content: commentBody,
170
- html: commentHtml,
171
- type
172
- };
141
+ const parenIndex = commentBody.indexOf("(");
142
+ const rawKeyword = parenIndex === -1 ? commentBody.split(/\s/)[0] : commentBody.slice(0, parenIndex).trim();
143
+ if (rawKeyword.startsWith("//") || rawKeyword.startsWith("#") || rawKeyword.startsWith("/*")) return;
144
+ const type = rawKeyword.startsWith(closingPrefix) ? "close" : "open";
173
145
  let keyword = rawKeyword;
174
- if (keyword.startsWith(closingPrefix)) keyword = keyword.slice(closingPrefix.length);
175
- if (keyword.startsWith(keywordPrefix)) keyword = keyword.slice(keywordPrefix.length);
176
- const optionText = makeValidJson(argumentParts.join(""));
177
- if (type === "open" || type === "close") {
178
- let options = {};
179
- try {
180
- options = json5.parse(optionText);
181
- } catch (error) {
182
- if (error instanceof Error) throw new VFileMessage(`Failed to parse comment options "${optionText}" for keyword "${keyword}": ${error.message}`);
146
+ if (type === "close") keyword = keyword.slice(1);
147
+ let options = {};
148
+ if (parenIndex !== -1) {
149
+ const lastParen = commentBody.lastIndexOf(")");
150
+ if (lastParen > parenIndex) {
151
+ const argText = commentBody.slice(parenIndex + 1, lastParen).trim();
152
+ if (argText.length > 0) try {
153
+ options = json5.parse(argText);
154
+ } catch (error) {
155
+ if (error instanceof Error) throw new VFileMessage(`Failed to parse comment options "${argText}" for keyword "${keyword}": ${error.message}`);
156
+ }
183
157
  }
184
- return {
185
- closingPrefix,
186
- html: commentHtml,
187
- keyword,
188
- keywordPrefix,
189
- options,
190
- type
191
- };
192
158
  }
159
+ return {
160
+ html: commentHtml,
161
+ keyword,
162
+ options,
163
+ type
164
+ };
193
165
  }
194
166
  function isComment(text) {
195
167
  const trimmed = text.trim();
196
168
  return trimmed.startsWith("<!--") && trimmed.endsWith("-->");
197
169
  }
198
- function makeValidJson(text) {
199
- text = text.trim();
200
- text = text.startsWith("(") ? text.slice(1) : text;
201
- text = text.endsWith(")") ? text.slice(0, -1) : text;
202
- text = text.trim();
203
- if (!text.startsWith("{") && !text.startsWith("[")) text = "{" + text;
204
- if (!text.endsWith("}") && !text.endsWith("]")) text += "}";
205
- return text;
206
- }
207
- function trimMetaIdentifiers(text, metaCommentIdentifier) {
208
- text = text.trim();
209
- text = text.startsWith(metaCommentIdentifier) ? text.slice(metaCommentIdentifier.length) : text;
210
- text = text.endsWith(metaCommentIdentifier) ? text.slice(0, -metaCommentIdentifier.length) : text;
211
- return text;
170
+ //#endregion
171
+ //#region src/lib/mdast-utils/mdast-util-mdat-clean.ts
172
+ /**
173
+ * Collapses any expanded mdat comments, effectively resetting the document to
174
+ * its pre-expansion state. No-op if no mdat comments are found.
175
+ */
176
+ function mdatClean(tree, file) {
177
+ let lastOpenMarker;
178
+ visit(tree, "html", (node, index, parent) => {
179
+ if (parent === void 0 || index === void 0) return CONTINUE;
180
+ const marker = parseCommentNode(node, parent);
181
+ if (marker === void 0) return CONTINUE;
182
+ if (marker.type === "open") {
183
+ lastOpenMarker = marker;
184
+ return CONTINUE;
185
+ }
186
+ if (marker.type === "close") {
187
+ if (lastOpenMarker === void 0) {
188
+ saveLog(file, "error", "clean", "Found closing marker without opening marker", node);
189
+ return CONTINUE;
190
+ }
191
+ if (lastOpenMarker.parent !== marker.parent) {
192
+ saveLog(file, "error", "clean", "Opening marker doesn't share a parent", node);
193
+ return CONTINUE;
194
+ }
195
+ if (lastOpenMarker.keyword !== marker.keyword) {
196
+ saveLog(file, "error", "clean", "Opening marker doesn't share a keyword", node);
197
+ return CONTINUE;
198
+ }
199
+ const openMarkerIndex = parent.children.indexOf(lastOpenMarker.node);
200
+ const nodesToRemove = parent.children.indexOf(marker.node) - openMarkerIndex + 1;
201
+ parent.children.splice(openMarkerIndex + 1, nodesToRemove - 1);
202
+ lastOpenMarker = void 0;
203
+ return [CONTINUE, index - nodesToRemove + 1];
204
+ }
205
+ });
212
206
  }
213
207
  //#endregion
214
208
  //#region src/lib/mdat/rules.ts
215
209
  function normalizeRules(rules) {
216
210
  const normalizedRules = {};
217
211
  for (const [keyword, rule] of Object.entries(rules)) if (typeof rule === "string") normalizedRules[keyword] = {
218
- applicationOrder: 0,
219
212
  content: async () => rule,
220
- order: void 0,
221
- required: false
213
+ order: 0
222
214
  };
223
215
  else if (typeof rule === "function") normalizedRules[keyword] = {
224
- applicationOrder: 0,
225
- content: async (options, tree) => rule(options, tree),
226
- order: void 0,
227
- required: false
216
+ content: async (options, context) => rule(options, context),
217
+ order: 0
228
218
  };
229
219
  else if (Array.isArray(rule)) normalizedRules[keyword] = {
230
- applicationOrder: 0,
231
220
  content: Object.values(normalizeRules(Object.fromEntries(rule.entries()))),
232
- order: void 0,
233
- required: false
221
+ order: 0
234
222
  };
235
223
  else if (typeof rule.content === "string") {
236
224
  const ruleContent = rule.content;
237
225
  normalizedRules[keyword] = {
238
- applicationOrder: rule.applicationOrder ?? 0,
239
226
  content: async () => ruleContent,
240
- order: rule.order ?? void 0,
241
- required: rule.required ?? false
227
+ order: rule.order ?? 0
242
228
  };
243
229
  } else if (Array.isArray(rule.content)) normalizedRules[keyword] = {
244
- applicationOrder: rule.applicationOrder ?? 0,
245
230
  content: Object.values(normalizeRules(Object.fromEntries(rule.content.entries()))),
246
- order: rule.order ?? void 0,
247
- required: rule.required ?? false
231
+ order: rule.order ?? 0
248
232
  };
249
233
  else {
250
234
  const ruleContent = rule.content;
251
235
  normalizedRules[keyword] = {
252
- applicationOrder: rule.applicationOrder ?? 0,
253
- content: async (options, tree) => ruleContent(options, tree),
254
- order: rule.order ?? void 0,
255
- required: rule.required ?? false
236
+ content: async (options, context) => ruleContent(options, context),
237
+ order: rule.order ?? 0
256
238
  };
257
239
  }
258
240
  validateNormalizedRules(normalizedRules);
@@ -272,52 +254,42 @@ function validateNormalizedRules(rules) {
272
254
  if (error instanceof Error) throw new TypeError(`Error validating rules: ${error.message}`);
273
255
  }
274
256
  }
275
- const jsonValueSchema = z.any();
276
- const rootSchema = z.any();
257
+ const functionSchema = z.custom((value) => typeof value === "function");
277
258
  const normalizedRuleSchema = z.lazy(() => z.object({
278
- applicationOrder: z.number(),
279
- content: z.union([z.function().args(jsonValueSchema.optional(), rootSchema.optional()).returns(z.promise(z.string())), z.array(normalizedRuleSchema)]),
280
- order: z.number().optional(),
281
- required: z.boolean().default(false)
259
+ content: z.union([functionSchema, z.array(normalizedRuleSchema)]),
260
+ order: z.number()
282
261
  }));
283
- const ruleContentFunctionSchema = z.function().args(jsonValueSchema.optional(), rootSchema.optional()).returns(z.union([z.string(), z.promise(z.string())]));
284
262
  const ruleSchema = z.lazy(() => z.union([
285
- ruleContentFunctionSchema,
263
+ functionSchema,
286
264
  z.array(ruleSchema),
287
265
  z.string(),
288
266
  z.object({
289
- applicationOrder: z.number().optional(),
290
267
  content: z.union([
291
- ruleContentFunctionSchema,
268
+ functionSchema,
292
269
  z.array(ruleSchema),
293
270
  z.string()
294
271
  ]),
295
- order: z.number().optional(),
296
- required: z.boolean().optional()
272
+ order: z.number().optional()
297
273
  })
298
274
  ]));
299
- const rulesSchema = z.record(ruleSchema).describe("MDAT Rules");
300
- const normalizedRulesSchema = z.record(normalizedRuleSchema).describe("MDAT Rules");
275
+ const keywordSchema = z.string().check(z.refine((key) => !/^[/*#]/.test(key), { message: "Rule keywords must not start with \"/\", \"*\", or \"#\" — these prefixes are reserved for comment syntax" }));
276
+ const rulesSchema = z.record(keywordSchema, ruleSchema).describe("MDAT Rules");
277
+ const normalizedRulesSchema = z.record(keywordSchema, normalizedRuleSchema).describe("MDAT Rules");
301
278
  /**
302
- * Compound rule helpers, used in both "expand" and "check" utilities
279
+ * Compound rule helpers for expanding rule content.
303
280
  */
304
- async function getRuleContent(rule, options, tree, check = false) {
281
+ async function getRuleContent(rule, options, context) {
305
282
  if (Array.isArray(rule.content)) {
306
283
  const subruleContent = [];
307
284
  for (const [index, subrule] of rule.content.entries()) {
308
285
  const subruleOptions = Array.isArray(options) ? options.at(index) : void 0;
309
- try {
310
- subruleContent.push(await getRuleContent(subrule, subruleOptions ?? {}, tree));
311
- } catch (error) {
312
- if (check) throw error;
313
- }
286
+ subruleContent.push(await getRuleContent(subrule, subruleOptions ?? {}, context));
314
287
  }
315
288
  return subruleContent.join("\n\n");
316
289
  }
317
290
  try {
318
- return await rule.content(options, tree);
291
+ return await rule.content(options, context);
319
292
  } catch (error) {
320
- if (check) throw error;
321
293
  throw new Error("Failed to expand content", { cause: error });
322
294
  }
323
295
  }
@@ -357,209 +329,41 @@ function getSoleRecord(record) {
357
329
  return recordValues[0];
358
330
  }
359
331
  //#endregion
360
- //#region src/lib/mdast-utils/mdast-util-mdat-check.ts
332
+ //#region src/lib/mdast-utils/mdast-util-mdat-expand.ts
361
333
  /**
362
- * Mdast utility function to check mdat source document, and output.
334
+ * Mdast utility to expand mdat comments in the tree.
363
335
  */
364
- async function mdatCheck(tree, file, options) {
365
- const { closingPrefix, keywordPrefix, metaCommentIdentifier, paranoid, rules: rawRules } = options;
366
- validateRules(rawRules);
367
- const rules = normalizeRules(rawRules);
336
+ async function mdatExpand(tree, file, rules) {
337
+ validateRules(rules);
338
+ const normalizedRules = normalizeRules(rules);
339
+ const frontmatter = (() => {
340
+ if (typeof file.value !== "string") return;
341
+ const { data } = matter(file.value);
342
+ return Object.keys(data).length > 0 ? data : void 0;
343
+ })();
344
+ const context = {
345
+ filePath: file.history.length > 0 ? file.path : void 0,
346
+ frontmatter,
347
+ tree
348
+ };
368
349
  const commentMarkers = [];
369
350
  visit(tree, "html", (node, index, parent) => {
370
351
  if (parent === void 0 || index === void 0) return CONTINUE;
371
- const commentMarker = parseCommentNode(node, parent, {
372
- closingPrefix,
373
- keywordPrefix,
374
- metaCommentIdentifier
375
- });
376
- if (commentMarker !== void 0) {
377
- const rule = commentMarker.type === "open" || commentMarker.type === "close" ? rules[commentMarker.keyword] : void 0;
378
- commentMarkers.push({
379
- ...commentMarker,
380
- rule
381
- });
382
- }
383
- });
384
- checkMissingRequiredComments(file, commentMarkers, rules, rawRules);
385
- checkCommentOrder(file, commentMarkers);
386
- checkMetaCommentPresence(file, commentMarkers, options);
387
- await checkRulesReturnedContent(file, commentMarkers, tree);
388
- if (paranoid) checkMissingOptionalComments(file, commentMarkers, rules, rawRules);
389
- checkMissingRules(file, commentMarkers);
390
- checkMissingPrefix(file, commentMarkers, rules, options);
391
- }
392
- /**
393
- * Check that all the rules are working by getting their content
394
- */
395
- async function checkRulesReturnedContent(file, comments, tree) {
396
- for (const comment of comments) if (comment.type === "open" && comment.rule !== void 0) try {
397
- if ((await getRuleContent(comment.rule, comment.options, tree, true)).trim() === "") saveLog(file, comment.rule.required ? "error" : "warn", "check", `${comment.html} returned an empty string.`, comment.node);
398
- } catch (error) {
399
- if (error instanceof Error) saveLog(file, comment.rule.required ? "error" : "warn", "check", `Could not get content for ${comment.html}. ${error.message}`, comment.node);
400
- }
401
- }
402
- /**
403
- * Check for comments with missing prefix (have an un-prefixed comment that matches a rule)
404
- */
405
- function checkMissingPrefix(file, comments, rules, options) {
406
- if (options.keywordPrefix === "") return;
407
- const ruleKeywords = Object.keys(rules);
408
- for (const comment of comments) if (comment.type === "native" && ruleKeywords.includes(comment.content)) saveLog(file, "warn", "check", `Missing prefix: ${comment.html}`, comment.node);
409
- }
410
- /**
411
- * Check for missing "optional" rules. These are instances where we have the comment, but not the rule
412
- */
413
- function checkMissingRules(file, comments) {
414
- for (const comment of comments) if (comment.type === "open" && comment.rule === void 0) saveLog(file, "warn", "check", `Missing rule for: ${comment.html}`, comment.node);
415
- }
416
- /**
417
- * Check for missing optional comments. We have defined the rule, but not written a matching comment.
418
- */
419
- function checkMissingOptionalComments(file, comments, rules, rawRules) {
420
- for (const [keyword, rule] of Object.entries(rules)) if (!rule.required && !comments.some((comment) => comment.type === "open" && comment.keyword === keyword) && !satisfiedByCompoundRule(keyword, rawRules)) saveLog(file, "warn", "check", `Missing optional: <!-- ${keyword} -->`);
421
- }
422
- /**
423
- * Check for missing required comments.
424
- * The rule set includes a rule with `required: true`, but no matching comment was found in the document.
425
- */
426
- function checkMissingRequiredComments(file, comments, rules, rawRules) {
427
- for (const [keyword, rule] of Object.entries(rules)) if (rule.required && !comments.some((comment) => comment.type === "open" && comment.keyword === keyword) && !satisfiedByCompoundRule(keyword, rawRules)) saveLog(file, "error", "check", `Missing required: <!-- ${keyword} -->`);
428
- }
429
- /**
430
- * Extract the content value from a raw rule, unwrapping object-form rules.
431
- */
432
- function getRawRuleContent(rule) {
433
- if (typeof rule === "object" && !Array.isArray(rule)) return rule.content;
434
- return rule;
435
- }
436
- /**
437
- * Get the sub-rule array from a compound rule, if it is one.
438
- */
439
- function getCompoundSubRules(rule) {
440
- if (Array.isArray(rule)) return rule;
441
- if (typeof rule === "object" && !Array.isArray(rule) && Array.isArray(rule.content)) return rule.content;
442
- }
443
- /**
444
- * Helper to see if a rule keyword is covered by a compound rule in the rule set.
445
- * Checks whether any compound rule contains the same raw content value (by reference)
446
- * as the rule for the given keyword. This works when the sub-rule was imported
447
- * directly into the compound rule definition.
448
- */
449
- function satisfiedByCompoundRule(keyword, rawRules) {
450
- const rawRule = rawRules[keyword];
451
- const ruleContent = getRawRuleContent(rawRule);
452
- for (const otherRule of Object.values(rawRules)) {
453
- const subRules = getCompoundSubRules(otherRule);
454
- if (subRules === void 0) continue;
455
- if (subRules.some((subRule) => getRawRuleContent(subRule) === ruleContent)) return true;
456
- }
457
- return false;
458
- }
459
- /**
460
- * Check if comment order in document is different from order specified in the rules
461
- */
462
- function checkCommentOrder(file, comments) {
463
- const commentsInOrderOfAppearance = comments.filter((commentMarker) => commentMarker.type === "open" && commentMarker.rule?.order !== void 0);
464
- const commentsInCorrectOrder = [...commentsInOrderOfAppearance].toSorted((a, b) => {
465
- const orderA = a.rule?.order;
466
- const orderB = b.rule?.order;
467
- if (orderA === void 0 || orderB === void 0) throw new Error("Unexpected undefined rule order");
468
- return orderA - orderB;
469
- });
470
- const currentOrderList = commentOrderList(commentsInOrderOfAppearance);
471
- const correctOrderList = commentOrderList(commentsInCorrectOrder);
472
- const table = new Table({
473
- head: [picocolors.red(picocolors.bold("Current Order")), picocolors.green(picocolors.bold("Required Order"))],
474
- style: { compact: true }
475
- });
476
- if (currentOrderList.join(",") !== correctOrderList.join(",")) {
477
- table.push(...currentOrderList.map((currentOrder, index) => [currentOrder, correctOrderList[index]]));
478
- saveLog(file, "error", "check", `Out of order:\n${table.toString()}`);
479
- }
480
- }
481
- /**
482
- * Check that meta presence / absence comment matches options.
483
- */
484
- function checkMetaCommentPresence(file, comments, options) {
485
- const { addMetaComment } = options;
486
- const metaCommentCount = comments.filter((comment) => comment.type === "meta").length;
487
- const shouldHaveMetaComment = typeof addMetaComment === "string" ? true : addMetaComment;
488
- if (shouldHaveMetaComment && metaCommentCount !== 1) saveLog(file, "error", "check", `Missing meta comment`);
489
- if (!shouldHaveMetaComment && metaCommentCount !== 0) saveLog(file, "error", "check", `Unexpected meta comment`);
490
- if (metaCommentCount > 1) saveLog(file, "error", "check", `Multiple meta comments`);
491
- }
492
- function commentOrderList(comments) {
493
- return comments.map((comment, index) => {
494
- if (comment.type === "open" || comment.type === "close") return `${index + 1}. ${comment.html}`;
495
- throw new Error("Unexpected comment type");
496
- });
497
- }
498
- //#endregion
499
- //#region src/lib/mdast-utils/mdast-util-mdat-clean.ts
500
- /**
501
- * Collapses any expanded mdat comments and removes meta comments,
502
- * effectively resetting the document to its pre-expansion state. No-op if no
503
- * mdat comments are found.
504
- */
505
- function mdatClean(tree, file, options) {
506
- let lastOpenMarker;
507
- visit(tree, "html", (node, index, parent) => {
508
- if (parent === void 0 || index === void 0) return CONTINUE;
509
- const marker = parseCommentNode(node, parent, options);
510
- if (marker === void 0 || marker.type === "native") return CONTINUE;
511
- if (marker.type === "meta") {
512
- parent.children.splice(index, 1);
513
- return [CONTINUE, index];
514
- }
515
- if (marker.type === "open") {
516
- lastOpenMarker = marker;
352
+ const commentMarker = parseCommentNode(node, parent);
353
+ if (commentMarker?.type !== "open") return CONTINUE;
354
+ if (normalizedRules[commentMarker.keyword] === void 0) {
355
+ saveLog(file, "warn", "expand", `Missing rule for: ${commentMarker.html}`, node);
517
356
  return CONTINUE;
518
357
  }
519
- if (marker.type === "close") {
520
- if (lastOpenMarker === void 0) {
521
- saveLog(file, "error", "clean", "Found closing marker without opening marker", node);
522
- return CONTINUE;
523
- }
524
- if (lastOpenMarker.parent !== marker.parent) {
525
- saveLog(file, "error", "clean", "Opening marker doesn't share a parent", node);
526
- return CONTINUE;
527
- }
528
- if (lastOpenMarker.keyword !== marker.keyword) {
529
- saveLog(file, "error", "clean", "Opening marker doesn't share a keyword", node);
530
- return CONTINUE;
531
- }
532
- const openMarkerIndex = parent.children.indexOf(lastOpenMarker.node);
533
- const nodesToRemove = parent.children.indexOf(marker.node) - openMarkerIndex + 1;
534
- parent.children.splice(openMarkerIndex + 1, nodesToRemove - 1);
535
- lastOpenMarker = void 0;
536
- return [CONTINUE, index - nodesToRemove + 1];
537
- }
358
+ commentMarkers.push(commentMarker);
538
359
  });
539
- }
540
- //#endregion
541
- //#region src/lib/mdast-utils/mdast-util-mdat-expand.ts
542
- async function mdatExpand(tree, file, options) {
543
- const { addMetaComment, closingPrefix, keywordPrefix, metaCommentIdentifier, rules: rawRules } = options;
544
- validateRules(rawRules);
545
- const rules = normalizeRules(rawRules);
546
- const commentMarkers = [];
547
- visit(tree, "html", (node, index, parent) => {
548
- if (parent === void 0 || index === void 0) return CONTINUE;
549
- const commentMarker = parseCommentNode(node, parent, {
550
- closingPrefix,
551
- keywordPrefix,
552
- metaCommentIdentifier
553
- });
554
- if (commentMarker?.type === "open" && rules[commentMarker.keyword] !== void 0) commentMarkers.push(commentMarker);
555
- });
556
- commentMarkers.sort((a, b) => rules[a.keyword].applicationOrder - rules[b.keyword].applicationOrder);
360
+ commentMarkers.sort((a, b) => normalizedRules[a.keyword].order - normalizedRules[b.keyword].order);
557
361
  for (const comment of commentMarkers) {
558
- const { closingPrefix, html, keyword, keywordPrefix, node, options, parent } = comment;
559
- const rule = rules[keyword];
362
+ const { html, keyword, node, options, parent } = comment;
363
+ const rule = normalizedRules[keyword];
560
364
  let newMarkdownString = "";
561
365
  try {
562
- newMarkdownString = await getRuleContent(rule, options, tree);
366
+ newMarkdownString = await getRuleContent(rule, options, context);
563
367
  if (newMarkdownString.trim() === "") saveLog(file, "error", "expand", `Got empty content when expanding ${html}`, node);
564
368
  } catch (error) {
565
369
  if (error instanceof Error) {
@@ -571,19 +375,12 @@ async function mdatExpand(tree, file, options) {
571
375
  const newNodes = remark().use(remarkGfm).parse(newMarkdownString).children;
572
376
  const closingNode = {
573
377
  type: "html",
574
- value: `<!-- ${closingPrefix}${keywordPrefix}${keyword} -->`
378
+ value: `<!-- /${keyword} -->`
575
379
  };
576
380
  const openingCommentIndex = parent.children.indexOf(node);
577
381
  parent.children.splice(openingCommentIndex + 1, 0, ...newNodes, closingNode);
578
382
  saveLog(file, "info", "expand", `Expanded: ${html}`, node);
579
383
  }
580
- if (addMetaComment) {
581
- const metaComment = {
582
- type: "html",
583
- value: `<!--${metaCommentIdentifier} ${typeof addMetaComment === "string" ? addMetaComment : "Warning: Content inside HTML comment blocks was generated by mdat and may be overwritten."} ${metaCommentIdentifier}-->`
584
- };
585
- tree.children.unshift(metaComment);
586
- }
587
384
  }
588
385
  //#endregion
589
386
  //#region src/lib/mdast-utils/mdast-util-mdat-split.ts
@@ -649,66 +446,25 @@ function getOriginalMarkup(mdastNode, hastNode) {
649
446
  }
650
447
  //#endregion
651
448
  //#region src/lib/mdast-utils/mdast-util-mdat.ts
652
- async function mdat(tree, file, options) {
653
- const { addMetaComment, closingPrefix, keywordPrefix, metaCommentIdentifier, rules } = options;
449
+ async function mdat(tree, file, rules) {
654
450
  mdatSplit(tree, file);
655
- mdatClean(tree, file, {
656
- closingPrefix,
657
- keywordPrefix,
658
- metaCommentIdentifier
659
- });
660
- await mdatExpand(tree, file, {
661
- addMetaComment,
662
- closingPrefix,
663
- keywordPrefix,
664
- metaCommentIdentifier,
665
- rules
666
- });
667
- await mdatCheck(tree, file, {
668
- addMetaComment,
669
- closingPrefix,
670
- keywordPrefix,
671
- metaCommentIdentifier,
672
- paranoid: false,
673
- rules
674
- });
675
- }
676
- //#endregion
677
- //#region src/lib/mdat/deep-merge-defined.ts
678
- function stripUndefinedDeep(object) {
679
- if (Array.isArray(object)) return object.map((v) => v && typeof v === "object" ? stripUndefinedDeep(v) : v).filter((v) => v !== void 0);
680
- return Object.entries(object).map(([k, v]) => [k, v && typeof v === "object" ? stripUndefinedDeep(v) : v]).reduce((acc, [k, v]) => v === void 0 ? acc : {
681
- ...acc,
682
- [k]: v
683
- }, {});
684
- }
685
- function deepMergeDefined(...objects) {
686
- return deepmerge(...objects.map((v, i) => i === 0 ? v : stripUndefinedDeep(v)));
451
+ mdatClean(tree, file);
452
+ await mdatExpand(tree, file, rules);
687
453
  }
688
454
  //#endregion
689
455
  //#region src/lib/remark-mdat.ts
690
- const defaultOptions = {
691
- addMetaComment: false,
692
- closingPrefix: "/",
693
- keywordPrefix: "",
694
- metaCommentIdentifier: "+",
695
- rules: { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` }
696
- };
697
- const optionsSchema = z.object({
698
- addMetaComment: z.union([z.boolean(), z.string()]).optional(),
699
- closingPrefix: z.string().min(1).optional(),
700
- keywordPrefix: z.string().optional(),
701
- metaCommentIdentifier: z.string().optional(),
702
- rules: rulesSchema.optional()
703
- }).describe("MDAT Options");
456
+ const defaultRules = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
704
457
  /**
705
458
  * A remark plugin that expands HTML comments in Markdown files.
706
459
  */
707
- const remarkMdat = function(options) {
708
- const resolvedOptions = deepMergeDefined(defaultOptions, options);
460
+ const remarkMdat = function(rules) {
461
+ const resolvedRules = {
462
+ ...defaultRules,
463
+ ...rules
464
+ };
709
465
  return async function(tree, file) {
710
- await mdat(tree, file, resolvedOptions);
466
+ await mdat(tree, file, resolvedRules);
711
467
  };
712
468
  };
713
469
  //#endregion
714
- export { deepMergeDefined, remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, log, mdat, mdatCheck, mdatClean, mdatExpand, mdatSplit, optionsSchema, reporterMdat, rulesSchema };
470
+ export { remarkMdat as default, getMdatReports, getSoleRule, getSoleRuleKey, mdat, mdatClean, mdatExpand, mdatSplit, rulesSchema as optionsSchema, rulesSchema, reporterMdat, setLogger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remark-mdat",
3
- "version": "1.2.5",
3
+ "version": "2.0.0-preview.2",
4
4
  "description": "A remark plugin implementing the Markdown Autophagic Template (MDAT) system.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -43,10 +43,10 @@
43
43
  "dependencies": {
44
44
  "@types/mdast": "^4.0.4",
45
45
  "@types/unist": "^3.0.3",
46
- "cli-table3": "^0.6.5",
47
- "deepmerge-ts": "^7.1.5",
46
+ "gray-matter-es": "^0.2.1",
48
47
  "hast-util-from-html": "^2.0.3",
49
48
  "json5": "^2.2.3",
49
+ "lognow": "^0.5.2",
50
50
  "picocolors": "^1.1.1",
51
51
  "remark": "^15.0.1",
52
52
  "remark-gfm": "^4.0.1",
@@ -55,7 +55,7 @@
55
55
  "unist-util-visit": "^5.1.0",
56
56
  "vfile": "^6.0.3",
57
57
  "vfile-message": "^4.0.3",
58
- "zod": "^3.25.76"
58
+ "zod": "^4.3.6"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@arethetypeswrong/core": "^0.18.2",
@@ -68,7 +68,7 @@
68
68
  "vitest": "^4.1.1"
69
69
  },
70
70
  "engines": {
71
- "node": ">=20.0.0"
71
+ "node": ">=20.19.6"
72
72
  },
73
73
  "devEngines": {
74
74
  "runtime": {
@@ -83,6 +83,7 @@
83
83
  "fix": "ksc fix",
84
84
  "lint": "ksc lint",
85
85
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
86
+ "release-preview": "bumpp --preid preview --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish --tag preview",
86
87
  "test": "vitest run"
87
88
  }
88
89
  }
package/readme.md CHANGED
@@ -1,5 +1,3 @@
1
- <!--+ Warning: Content inside HTML comment blocks was generated by mdat and may be overwritten. +-->
2
-
3
1
  <!-- title -->
4
2
 
5
3
  # remark-mdat
@@ -44,7 +42,6 @@
44
42
  - [Examples](#examples)
45
43
  - [Utilities](#utilities)
46
44
  - [Implementation notes](#implementation-notes)
47
- - [The future](#the-future)
48
45
  - [Maintainers](#maintainers)
49
46
  - [Acknowledgments](#acknowledgments)
50
47
  - [Contributing](#contributing)
@@ -105,19 +102,9 @@ remark().use(remarkMdat)
105
102
 
106
103
  #### Options
107
104
 
108
- The plugin accepts an optional options object. All fields are optional:
109
-
110
- | Option | Type | Default | Description |
111
- | ----------------------- | ------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
112
- | `rules` | `Rules` | `{}` | A record mapping comment keywords to rules that determine what content is expanded at each comment site. See [Rules](#rules) below. |
113
- | `addMetaComment` | `boolean \| string` | `false` | If `true`, prepends a warning comment to the document noting that content was auto-generated. If a `string`, uses that string as the warning message. |
114
- | `closingPrefix` | `string` | `'/'` | The prefix used to identify closing comment tags, e.g. the `/` in `<!-- /keyword -->`. |
115
- | `keywordPrefix` | `string` | `''` | A prefix required on all mdat comments. Useful for namespacing, e.g. setting `'mm-'` means only `<!-- mm-keyword -->` comments are processed. |
116
- | `metaCommentIdentifier` | `string` | `'+'` | The character used to identify auto-generated meta comments, e.g. `<!--+ ... +-->`. |
105
+ The plugin accepts an optional `Rules` object as its options. This is a `Record<string, Rule>` where each key is a keyword matching an HTML comment in the Markdown file (e.g. `title` matches `<!-- title -->`).
117
106
 
118
- #### Rules
119
-
120
- Rules are defined as a `Record<string, Rule>` where each key is a keyword matching an HTML comment in the Markdown file (e.g. `title` matches `<!-- title -->`).
107
+ HTML comments using code-style notation (`<!-- // ... -->`, `<!-- # ... -->`, `<!-- /* ... */ -->`) are ignored and will not be treated as mdat keywords. Rule keywords cannot start with `/`, `*`, or `#`.
121
108
 
122
109
  A `Rule` value can take several forms:
123
110
 
@@ -125,25 +112,81 @@ A `Rule` value can take several forms:
125
112
  const rules: Rules = {
126
113
  // String: direct replacement
127
114
  greeting: 'Hello, world!',
128
- // Array: compound rule combining multiple sub-rules
129
- header: ['# My Project', () => getDescription()],
130
- // Function with arguments: receives parsed options from the comment
131
- // e.g. <!-- greeting({name: "Alice"}) --> or <!-- greeting {name: "Alice"} -->
132
- personalGreeting: (options) => `Hello, ${options.name}!`,
115
+
133
116
  // Function: dynamic content (sync or async)
134
117
  time: () => new Date().toDateString(),
118
+
119
+ // Function with arguments: receives parsed options from the comment
120
+ personalGreeting: (options) => `Hello, ${options.name}!`,
121
+
135
122
  // Object: rule with validation metadata
136
123
  title: {
137
- applicationOrder: 0, // Processing priority (default: 0)
124
+ order: 0, // Processing priority (default: 0)
138
125
  content: () => getTitle(), // String, function, or array
139
- order: 1, // Expected position relative to other comments
140
- required: true, // Error if comment is missing (default: false)
141
126
  },
142
- // Function with document access: receives the full mdast tree
143
- toc: (_options, tree) => generateTocFromTree(tree),
127
+
128
+ // Array: compound rule combining multiple sub-rules
129
+ header: ['# My Project', () => getDescription()],
130
+
131
+ // Function with context: access the document tree, frontmatter, and file path
132
+ toc: (_options, context) => generateTocFromTree(context.tree),
144
133
  }
145
134
  ```
146
135
 
136
+ #### Rule context
137
+
138
+ Rule content functions receive a `RuleContext` object as their second argument, providing access to the document being processed:
139
+
140
+ ```ts
141
+ type RuleContext = {
142
+ /** File path of the source document, if known. */
143
+ filePath: string | undefined
144
+ /** Parsed YAML frontmatter from the document, if present. */
145
+ frontmatter: Record<string, unknown> | undefined
146
+ /** The full mdast AST of the document. Do not mutate. */
147
+ tree: Root
148
+ }
149
+ ```
150
+
151
+ Frontmatter is automatically extracted from the raw Markdown source using [gray-matter-es](https://github.com/ryoppippi/gray-matter-es), so it works regardless of whether `remark-frontmatter` is in your pipeline. If the document has no frontmatter block, `context.frontmatter` is `undefined`.
152
+
153
+ ```ts
154
+ const rules: Rules = {
155
+ // Access frontmatter values
156
+ title: (_options, context) => `# ${context.frontmatter?.title ?? 'Untitled'}`,
157
+
158
+ // Use the file path
159
+ source: (_options, context) => `Source: ${context.filePath ?? 'unknown'}`,
160
+
161
+ // Traverse the AST
162
+ toc: (_options, context) => generateTocFromTree(context.tree),
163
+ }
164
+ ```
165
+
166
+ #### Passing arguments to rules
167
+
168
+ Arguments are passed using function-call syntax: `<!-- keyword(...) -->`. The value inside the parentheses is parsed as [JSON5](https://json5.org/), which means unquoted keys and single quotes are allowed.
169
+
170
+ ```md
171
+ Options as JSON5 (unquoted keys, single quotes):
172
+
173
+ <!-- greeting({name: 'Alice', shout: true}) -->
174
+
175
+ Options as strict JSON:
176
+
177
+ <!-- greeting({"name": "Alice", "shout": true}) -->
178
+
179
+ Single primitive value:
180
+
181
+ <!-- repeat(3) -->
182
+ ```
183
+
184
+ Any JSON5 value is supported: objects, arrays, strings, numbers, and booleans. Comments without parentheses receive an empty object `{}` as their options.
185
+
186
+ For simplicity's sake, only a single argument position is supported. If you need pass multiple arguments, wrap them in an object.
187
+
188
+ Prefer object arguments for all but the most contextually clear argument values.
189
+
147
190
  ### Examples
148
191
 
149
192
  #### Basic
@@ -167,7 +210,7 @@ console.log(markdownOutput.toString())
167
210
  // <!-- /mdat -->
168
211
  ```
169
212
 
170
- #### With options
213
+ #### With rules
171
214
 
172
215
  If you wanted to replace `<!-- time -->` comments in your Markdown file with the current time, you could pass in a rule:
173
216
 
@@ -176,15 +219,15 @@ import type { Rules } from 'remark-mdat'
176
219
  import { remark } from 'remark'
177
220
  import remarkMdat from 'remark-mdat'
178
221
 
179
- // Create the rule
222
+ // Create the rules
180
223
  const rules: Rules = {
181
224
  time: () => new Date().toDateString(),
182
225
  }
183
226
 
184
227
  const markdownInput = '<!-- time -->'
185
228
 
186
- // Pass the time rule to remarkMdat
187
- const markdownOutput = await remark().use(remarkMdat, { rules }).process(markdownInput)
229
+ // Pass the rules to remarkMdat
230
+ const markdownOutput = await remark().use(remarkMdat, rules).process(markdownInput)
188
231
 
189
232
  console.log(markdownOutput.toString())
190
233
 
@@ -202,15 +245,15 @@ See the [`mdat`](https://github.com/kitschpatrol/mdat) package for a higher-leve
202
245
 
203
246
  The plugin bundles a number of [mdast](https://github.com/syntax-tree/mdast) utilities designed to operate directly on syntax trees. These are exported to support customized Unified.js processors and enforce modularity and separation of concerns in mdat's internal implementation, but you do not need to use them directly — all functionality is encapsulated in the single `remarkMdat` plugin export.
204
247
 
205
- The remark-mdat plugin chains these utilities together to accommodate the typical use case of end-to-end expansion and validation of mdat comments. For now, the individual utility transformers are not published individually to NPM, and are instead bundled with `remark-mdat`.
248
+ The remark-mdat plugin chains these utilities together to accommodate the typical use case of end-to-end expansion of mdat comments. For now, the individual utility transformers are not published individually to NPM, and are instead bundled with `remark-mdat`.
206
249
 
207
- - [**`mdast-util-mdat`**](./src/lib/mdast-utils/mdast-util-mdat.ts)
250
+ Errors and warnings are reported inline during expansion via [VFile messages](https://github.com/vfile/vfile-message), following remark ecosystem conventions. Use `reporterMdat` to extract and format these messages for console output.
208
251
 
209
- Composite transformer function performing end-to-end mdat comment expansion and validation on Markdown ASTs by chaining the other utility functions described below.
252
+ - [**`mdast-util-mdat`**](./src/lib/mdast-utils/mdast-util-mdat.ts)
210
253
 
211
- _Exported as `mdat(tree: Root, file: VFile, options: MdatOptions): Promise<void>`_
254
+ Composite transformer function performing end-to-end mdat comment expansion on Markdown ASTs by chaining the other utility functions described below.
212
255
 
213
- `MdatOptions` includes `addMetaComment`, `closingPrefix`, `keywordPrefix`, `metaCommentIdentifier`, and `rules` (all required, unlike the plugin's `Options` where they are optional with defaults).
256
+ _Exported as `mdat(tree: Root, file: VFile, rules: Rules): Promise<void>`_
214
257
 
215
258
  Utilities wrapped by `mdast-util-mdat`:
216
259
  - [**`mdast-util-mdat-split`**](./src/lib/mdast-utils/mdast-util-mdat-split.ts)
@@ -221,37 +264,21 @@ The remark-mdat plugin chains these utilities together to accommodate the typica
221
264
 
222
265
  - [**`mdast-util-mdat-clean`**](./src/lib/mdast-utils/mdast-util-mdat-clean.ts)
223
266
 
224
- Transformer function that "resets" all mdat comment expansions in a file, collapsing expanded comments back into single-line placeholders.
225
-
226
- _Exported as `mdatClean(tree: Root, file: VFile, options: MdatCleanOptions): void`_
267
+ Transformer function that resets all mdat comment expansions in a file, collapsing expanded comments back into single-line placeholders.
227
268
 
228
- `MdatCleanOptions` includes `closingPrefix`, `keywordPrefix`, and `metaCommentIdentifier`.
269
+ _Exported as `mdatClean(tree: Root, file: VFile): void`_
229
270
 
230
271
  - [**`mdast-util-mdat-expand`**](./src/lib/mdast-utils/mdast-util-mdat-expand.ts)
231
272
 
232
- Transformer function that expands mdat comments (e.g. `<!-- title -->`) in a Markdown file according to the rule set passed in to the options argument.
233
-
234
- _Exported as `mdatExpand(tree: Root, file: VFile, options: MdatExpandOptions): Promise<void>`_
273
+ Transformer function that expands mdat comments (e.g. `<!-- title -->`) in a Markdown file according to the provided rules. Reports errors for rules that throw or return empty content, and warnings for comments with no matching rule.
235
274
 
236
- `MdatExpandOptions` includes `addMetaComment`, `closingPrefix`, `keywordPrefix`, `metaCommentIdentifier`, and `rules`.
237
-
238
- - [**`mdast-util-mdat-check`**](./src/lib/mdast-utils/mdast-util-mdat-check.ts)
239
-
240
- Transformer function that validates an expanded Markdown document against the requirements defined in the rules passed in to the options argument. Does not modify the tree, it only appends messages to the VFile.
241
-
242
- _Exported as `mdatCheck(tree: Root, file: VFile, options: MdatCheckOptions): Promise<void>`_
243
-
244
- `MdatCheckOptions` extends `MdatExpandOptions` with a `paranoid` boolean for extra validation checks.
245
-
246
- See `reporterMdat` to extract, format, and log results from VFile messages written by `mdatCheck`.
275
+ _Exported as `mdatExpand(tree: Root, file: VFile, rules: Rules): Promise<void>`_
247
276
 
248
277
  ## Implementation notes
249
278
 
250
279
  This project was split from a monorepo containing both `mdat` and `remark-mdat` into separate repos in July 2024.
251
280
 
252
- ## The future
253
-
254
- - Consider making remark a peer dependency? Though perhaps not [strip-markdown/issues/24](https://github.com/remarkjs/strip-markdown/issues/24)...
281
+ Remark is not a peer dependency on account of this discussion: [strip-markdown/issues/24](https://github.com/remarkjs/strip-markdown/issues/24)
255
282
 
256
283
  ## Maintainers
257
284