remark-mdat 1.2.4 → 2.0.0-preview.1

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