remark-mdat 2.0.0 → 2.0.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
@@ -6,6 +6,7 @@ import { JsonValue, MergeDeep, Simplify } from "type-fest";
6
6
  import { Plugin } from "unified";
7
7
 
8
8
  //#region src/lib/mdat/rules.d.ts
9
+ /** Recursively simplifies a type for cleaner IDE hover display. Approximation of type-fest's internal `SimplifyDeep`. */
9
10
  type SimplifyDeep<T> = Simplify<MergeDeep<T, T>>;
10
11
  /**
11
12
  * Context passed to rule content functions during expansion.
@@ -97,7 +98,9 @@ type Rule =
97
98
  * { basic-date: { order: 1, content: () => `${new Date().toISOString()}` } }
98
99
  */
99
100
  type Rules = SimplifyDeep<Record<string, Rule>>;
101
+ /** A record mapping comment keywords to {@link NormalizedRule} objects. */
100
102
  type NormalizedRules = SimplifyDeep<Record<string, NormalizedRule>>;
103
+ /** Zod schema for validating {@link Rules} records. */
101
104
  declare const rulesSchema: z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
102
105
  /**
103
106
  * Returns the rule value from a single-rule record.
@@ -115,7 +118,8 @@ declare function getSoleRule<T extends NormalizedRules | Rules>(rules: T): T[key
115
118
  declare function getSoleRuleKey<T extends NormalizedRules | Rules>(rules: T): keyof T;
116
119
  //#endregion
117
120
  //#region src/lib/mdast-utils/mdast-util-mdat.d.ts
118
- declare function mdat(tree: Root, file: VFile, rules: Rules): Promise<void>;
121
+ /** Mdast utility that splits, cleans, and expands all mdat comments in the tree. */
122
+ declare function mdat(tree: Root, file: VFile, rules: NormalizedRules | Rules): Promise<void>;
119
123
  //#endregion
120
124
  //#region src/lib/mdast-utils/mdast-util-mdat-clean.d.ts
121
125
  /**
@@ -128,7 +132,7 @@ declare function mdatClean(tree: Root, file: VFile): void;
128
132
  /**
129
133
  * Mdast utility to expand mdat comments in the tree.
130
134
  */
131
- declare function mdatExpand(tree: Root, file: VFile, rules: Rules): Promise<void>;
135
+ declare function mdatExpand(tree: Root, file: VFile, rules: NormalizedRules | Rules): Promise<void>;
132
136
  //#endregion
133
137
  //#region src/lib/mdast-utils/mdast-util-mdat-split.d.ts
134
138
  /**
@@ -146,30 +150,33 @@ declare function mdatSplit(tree: Root, file: VFile): void;
146
150
  declare function setLogger(logger?: ILogBasic | ILogLayer): void;
147
151
  //#endregion
148
152
  //#region src/lib/mdat/mdat-log.d.ts
149
- /**
150
- * Tries to provide a simpler wrapper to vfile.message
151
- */
153
+ /** A simplified representation of a {@link VFileMessage}. */
152
154
  type MdatMessage = {
153
- column?: number;
154
- level: 'error' | 'info' | 'warn';
155
- line?: number;
156
- message: string;
155
+ /** Starting column of the message origin. */column?: number; /** Severity level. */
156
+ level: 'error' | 'info' | 'warn'; /** Starting line of the message origin. */
157
+ line?: number; /** Human-readable description of the issue. */
158
+ message: string; /** Namespace that produced the message (e.g. the rule name). */
157
159
  source?: string;
158
160
  };
161
+ /** Aggregated processing report for a single file. */
159
162
  type MdatFileReport = {
160
- destinationPath?: string;
161
- errors: MdatMessage[];
162
- infos: MdatMessage[];
163
- sourcePath: string;
163
+ /** Output path if the file was written to a different location. */destinationPath?: string; /** Fatal errors that prevented successful processing. */
164
+ errors: MdatMessage[]; /** Informational messages. */
165
+ infos: MdatMessage[]; /** Original input file path. */
166
+ sourcePath: string; /** Non-fatal warnings encountered during processing. */
164
167
  warnings: MdatMessage[];
165
168
  };
169
+ /** Converts an array of processed VFiles into {@link MdatFileReport} objects. */
166
170
  declare function getMdatReports(files: VFile[]): MdatFileReport[];
171
+ /** Logs a human-readable processing report for each VFile to the library logger. */
167
172
  declare function reporterMdat(files: VFile[]): void;
168
173
  //#endregion
169
174
  //#region src/lib/remark-mdat.d.ts
175
+ /** Configuration for the remarkMdat plugin. */
170
176
  type Options = {
171
- rules?: Rules;
177
+ /** Rules mapping comment keywords to expansion content. Merged with built-in defaults. */rules?: Rules;
172
178
  };
179
+ /** Zod schema for validating {@link Options}. */
173
180
  declare const optionsSchema: z.ZodObject<{
174
181
  rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
175
182
  }, z.core.$strip>;
package/dist/index.js CHANGED
@@ -63,6 +63,7 @@ function vFileMessageToMdatMessage(vFileMessage) {
63
63
  source: vFileMessage.source
64
64
  };
65
65
  }
66
+ /** Converts an array of processed VFiles into {@link MdatFileReport} objects. */
66
67
  function getMdatReports(files) {
67
68
  return files.map((file) => getMdatReport(file));
68
69
  }
@@ -83,6 +84,7 @@ function getMdatReport(file) {
83
84
  }
84
85
  return mdatFileReport;
85
86
  }
87
+ /** Logs a human-readable processing report for each VFile to the library logger. */
86
88
  function reporterMdat(files) {
87
89
  for (const file of files) {
88
90
  const { destinationPath, errors, infos, sourcePath, warnings } = getMdatReport(file);
@@ -128,6 +130,9 @@ function parseCommentNode(node, parent) {
128
130
  else throw new VFileMessage("Unknown error", node);
129
131
  }
130
132
  }
133
+ const HTML_COMMENT_OPEN_REGEX = /^\s*<!-{2,}\s*/;
134
+ const HTML_COMMENT_CLOSE_REGEX = /\s*-{2,}>\s*$/;
135
+ const WHITESPACE_REGEX = /\s/;
131
136
  /**
132
137
  * Parse any comment string into structured data.
133
138
  * Comments using code-style notation (`//`, `#`, `/*`) are ignored and return `undefined`.
@@ -137,9 +142,9 @@ function parseComment(text) {
137
142
  if (!isComment(text)) return;
138
143
  const closingPrefix = "/";
139
144
  const commentHtml = text.trim();
140
- const commentBody = commentHtml.replace(/^\s*<!-{2,}\s*/, "").replace(/\s*-{2,}>\s*$/, "");
145
+ const commentBody = commentHtml.replace(HTML_COMMENT_OPEN_REGEX, "").replace(HTML_COMMENT_CLOSE_REGEX, "");
141
146
  const parenIndex = commentBody.indexOf("(");
142
- const rawKeyword = parenIndex === -1 ? commentBody.split(/\s/)[0] : commentBody.slice(0, parenIndex).trim();
147
+ const rawKeyword = parenIndex === -1 ? commentBody.split(WHITESPACE_REGEX)[0] : commentBody.slice(0, parenIndex).trim();
143
148
  if (rawKeyword.startsWith("//") || rawKeyword.startsWith("#") || rawKeyword.startsWith("/*")) return;
144
149
  const type = rawKeyword.startsWith(closingPrefix) ? "close" : "open";
145
150
  let keyword = rawKeyword;
@@ -206,7 +211,15 @@ function mdatClean(tree, file) {
206
211
  }
207
212
  //#endregion
208
213
  //#region src/lib/mdat/rules.ts
214
+ /** Brand symbol to detect pre-normalized rules and skip re-validation. */
215
+ const NORMALIZED = Symbol("normalized");
216
+ /** Check whether rules have already been normalized. */
217
+ function isNormalized(rules) {
218
+ return NORMALIZED in rules;
219
+ }
220
+ /** Converts flexible {@link Rules} into strict {@link NormalizedRules} for internal processing. */
209
221
  function normalizeRules(rules) {
222
+ validateRules(rules);
210
223
  const normalizedRules = {};
211
224
  for (const [keyword, rule] of Object.entries(rules)) if (typeof rule === "string") normalizedRules[keyword] = {
212
225
  content: async () => rule,
@@ -237,9 +250,10 @@ function normalizeRules(rules) {
237
250
  order: rule.order ?? 0
238
251
  };
239
252
  }
240
- validateNormalizedRules(normalizedRules);
253
+ Object.defineProperty(normalizedRules, NORMALIZED, { value: true });
241
254
  return normalizedRules;
242
255
  }
256
+ /** Validates rules against {@link rulesSchema}, throwing on invalid input. */
243
257
  function validateRules(rules) {
244
258
  try {
245
259
  rulesSchema.parse(rules);
@@ -247,18 +261,7 @@ function validateRules(rules) {
247
261
  if (error instanceof Error) throw new TypeError(`Error validating rules: ${error.message}`);
248
262
  }
249
263
  }
250
- function validateNormalizedRules(rules) {
251
- try {
252
- normalizedRulesSchema.parse(rules);
253
- } catch (error) {
254
- if (error instanceof Error) throw new TypeError(`Error validating rules: ${error.message}`);
255
- }
256
- }
257
264
  const functionSchema = z.custom((value) => typeof value === "function");
258
- const normalizedRuleSchema = z.lazy(() => z.object({
259
- content: z.union([functionSchema, z.array(normalizedRuleSchema)]),
260
- order: z.number()
261
- }));
262
265
  const ruleSchema = z.lazy(() => z.union([
263
266
  functionSchema,
264
267
  z.array(ruleSchema),
@@ -272,9 +275,10 @@ const ruleSchema = z.lazy(() => z.union([
272
275
  order: z.number().optional()
273
276
  })
274
277
  ]));
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" }));
278
+ const COMMENT_PREFIX_REGEX = /^[/*#]/;
279
+ const keywordSchema = z.string().check(z.refine((key) => !COMMENT_PREFIX_REGEX.test(key), { message: "Rule keywords must not start with \"/\", \"*\", or \"#\" — these prefixes are reserved for comment syntax" }));
280
+ /** Zod schema for validating {@link Rules} records. */
276
281
  const rulesSchema = z.record(keywordSchema, ruleSchema).describe("MDAT Rules");
277
- const normalizedRulesSchema = z.record(keywordSchema, normalizedRuleSchema).describe("MDAT Rules");
278
282
  /**
279
283
  * Expand rule content. For compound rules (content arrays), individual
280
284
  * sub-rule failures are reported via `onWarning` and skipped. The entire
@@ -344,8 +348,7 @@ function getSoleRecord(record) {
344
348
  * Mdast utility to expand mdat comments in the tree.
345
349
  */
346
350
  async function mdatExpand(tree, file, rules) {
347
- validateRules(rules);
348
- const normalizedRules = normalizeRules(rules);
351
+ const normalizedRules = isNormalized(rules) ? rules : normalizeRules(rules);
349
352
  const frontmatter = (() => {
350
353
  if (typeof file.value !== "string") return;
351
354
  const { data } = matter(file.value);
@@ -368,6 +371,7 @@ async function mdatExpand(tree, file, rules) {
368
371
  commentMarkers.push(commentMarker);
369
372
  });
370
373
  commentMarkers.sort((a, b) => normalizedRules[a.keyword].order - normalizedRules[b.keyword].order);
374
+ const parser = remark().use(remarkGfm);
371
375
  for (const comment of commentMarkers) {
372
376
  const { html, keyword, node, options, parent } = comment;
373
377
  const rule = normalizedRules[keyword];
@@ -384,7 +388,7 @@ async function mdatExpand(tree, file, rules) {
384
388
  }
385
389
  continue;
386
390
  }
387
- const newNodes = remark().use(remarkGfm).parse(newMarkdownString).children;
391
+ const newNodes = parser.parse(newMarkdownString).children;
388
392
  const closingNode = {
389
393
  type: "html",
390
394
  value: `<!-- /${keyword} -->`
@@ -403,6 +407,8 @@ async function mdatExpand(tree, file, rules) {
403
407
  function mdatSplit(tree, file) {
404
408
  visit(tree, "html", (node, index, parent) => {
405
409
  if (parent === void 0 || index === void 0) return CONTINUE;
410
+ const v = node.value;
411
+ if (v.startsWith("<!--") && v.endsWith("-->") && !v.includes("<!--", 4)) return CONTINUE;
406
412
  const htmlNodes = splitHtmlIntoMdastNodes(node);
407
413
  if (htmlNodes.length > 1) {
408
414
  saveLog(file, "warn", "split", "Multiple comments in a single HTML node.", node);
@@ -413,6 +419,7 @@ function mdatSplit(tree, file) {
413
419
  }
414
420
  });
415
421
  }
422
+ /** Splits a single mdast HTML node containing multiple comments into individual HTML and text nodes. Exported for testing. */
416
423
  function splitHtmlIntoMdastNodes(mdastNode) {
417
424
  const htmlTree = fromHtml(mdastNode.value, { fragment: true });
418
425
  const mdastNodes = [];
@@ -458,6 +465,7 @@ function getOriginalMarkup(mdastNode, hastNode) {
458
465
  }
459
466
  //#endregion
460
467
  //#region src/lib/mdast-utils/mdast-util-mdat.ts
468
+ /** Mdast utility that splits, cleans, and expands all mdat comments in the tree. */
461
469
  async function mdat(tree, file, rules) {
462
470
  mdatSplit(tree, file);
463
471
  mdatClean(tree, file);
@@ -466,17 +474,18 @@ async function mdat(tree, file, rules) {
466
474
  //#endregion
467
475
  //#region src/lib/remark-mdat.ts
468
476
  const defaultRules = { mdat: `Powered by the Markdown Autophagic Template system: [mdat](https://github.com/kitschpatrol/mdat).` };
477
+ /** Zod schema for validating {@link Options}. */
469
478
  const optionsSchema = z.object({ rules: rulesSchema.optional() }).describe("MDAT Plugin Options");
470
479
  /**
471
480
  * A remark plugin that expands HTML comments in Markdown files.
472
481
  */
473
482
  const remarkMdat = function(options) {
474
- const resolvedRules = {
483
+ const normalizedRules = normalizeRules({
475
484
  ...defaultRules,
476
485
  ...options?.rules
477
- };
486
+ });
478
487
  return async function(tree, file) {
479
- await mdat(tree, file, resolvedRules);
488
+ await mdat(tree, file, normalizedRules);
480
489
  };
481
490
  };
482
491
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remark-mdat",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "A remark plugin implementing the Markdown Autophagic Template (MDAT) system.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -46,7 +46,7 @@
46
46
  "gray-matter-es": "^0.2.1",
47
47
  "hast-util-from-html": "^2.0.3",
48
48
  "json5": "^2.2.3",
49
- "lognow": "^0.5.2",
49
+ "lognow": "^0.6.0",
50
50
  "picocolors": "^1.1.1",
51
51
  "remark": "^15.0.1",
52
52
  "remark-gfm": "^4.0.1",
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@arethetypeswrong/core": "^0.18.2",
62
- "@kitschpatrol/shared-config": "^6.2.0",
62
+ "@kitschpatrol/shared-config": "^7.0.0",
63
63
  "@types/node": "~20.19.37",
64
64
  "bumpp": "^11.0.1",
65
65
  "publint": "^0.3.18",
@@ -77,6 +77,8 @@
77
77
  }
78
78
  },
79
79
  "scripts": {
80
+ "bench": "vitest bench --no-file-parallelism --compare test/benchmarks/baseline.json",
81
+ "bench:baseline": "vitest bench --no-file-parallelism --outputJson test/benchmarks/baseline.json",
80
82
  "build": "tsdown",
81
83
  "clean": "git rm -f pnpm-lock.yaml ; git clean -fdX",
82
84
  "dev": "pnpm run test",