remark-mdat 2.0.0-preview.1 → 2.0.0-preview.3

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
@@ -7,6 +7,14 @@ import { Plugin } from "unified";
7
7
 
8
8
  //#region src/lib/mdat/rules.d.ts
9
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
+ };
10
18
  /**
11
19
  * Strict normalized rules used internally.
12
20
  * Rules normalized to a form with async content functions and other default metadata
@@ -17,7 +25,7 @@ type NormalizedRule = {
17
25
  * The function that generates the expanded Markdown string.
18
26
  * For 'compound' rules, this can be an array of rules (without keywords).
19
27
  */
20
- content: ((options: JsonValue, tree: Root) => Promise<string>) | NormalizedRule[];
28
+ content: ((options: JsonValue, context: RuleContext) => Promise<string>) | NormalizedRule[];
21
29
  /**
22
30
  * The order in which the rule should be applied during processing
23
31
  * Helpful if a rule depends on the presence of content generated by another rule
@@ -29,7 +37,7 @@ type Rule =
29
37
  /**
30
38
  * Function that returns the Markdown string to expand at the comment site.
31
39
  */
32
- ((options: JsonValue, tree: Root) => Promise<string> | string)
40
+ ((options: JsonValue, context: RuleContext) => Promise<string> | string)
33
41
  /**
34
42
  * Compound rules may be defined an array of rules, without keywords.
35
43
  * Can be defined at the top level, if no validation metadata is required, or as the 'content' value
@@ -54,11 +62,11 @@ type Rule =
54
62
  * `<!-- keyword({something: true}) -->` or
55
63
  * `<!-- keyword {something: true}-->`
56
64
  * Sets options to {something: true}
57
- * @param tree
58
- * 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.
59
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.
60
68
  */
61
- content: ((options: JsonValue, tree: Root) => Promise<string> | string) | Rule[] | string;
69
+ content: ((options: JsonValue, context: RuleContext) => Promise<string> | string) | Rule[] | string;
62
70
  /**
63
71
  * The order in which the rule should be applied during processing.
64
72
  * Defaults to 0.
@@ -165,4 +173,4 @@ type Options = Rules;
165
173
  */
166
174
  declare const remarkMdat: Plugin<[Options], Root>;
167
175
  //#endregion
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 };
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
@@ -4,6 +4,7 @@ import picocolors from "picocolors";
4
4
  import { createLogger, injectionHelper } from "lognow";
5
5
  import json5 from "json5";
6
6
  import { VFileMessage } from "vfile-message";
7
+ import { matter } from "gray-matter-es";
7
8
  import { remark } from "remark";
8
9
  import remarkGfm from "remark-gfm";
9
10
  import { z } from "zod";
@@ -212,7 +213,7 @@ function normalizeRules(rules) {
212
213
  order: 0
213
214
  };
214
215
  else if (typeof rule === "function") normalizedRules[keyword] = {
215
- content: async (options, tree) => rule(options, tree),
216
+ content: async (options, context) => rule(options, context),
216
217
  order: 0
217
218
  };
218
219
  else if (Array.isArray(rule)) normalizedRules[keyword] = {
@@ -232,7 +233,7 @@ function normalizeRules(rules) {
232
233
  else {
233
234
  const ruleContent = rule.content;
234
235
  normalizedRules[keyword] = {
235
- content: async (options, tree) => ruleContent(options, tree),
236
+ content: async (options, context) => ruleContent(options, context),
236
237
  order: rule.order ?? 0
237
238
  };
238
239
  }
@@ -275,25 +276,30 @@ const keywordSchema = z.string().check(z.refine((key) => !/^[/*#]/.test(key), {
275
276
  const rulesSchema = z.record(keywordSchema, ruleSchema).describe("MDAT Rules");
276
277
  const normalizedRulesSchema = z.record(keywordSchema, normalizedRuleSchema).describe("MDAT Rules");
277
278
  /**
278
- * Compound rule helpers, used in both "expand" and "check" utilities
279
+ * Expand rule content. For compound rules (content arrays), individual
280
+ * sub-rule failures are reported via `onWarning` and skipped. The entire
281
+ * expansion only fails if every sub-rule fails.
279
282
  */
280
- async function getRuleContent(rule, options, tree, check = false) {
283
+ async function getRuleContent(rule, options, context, onWarning) {
281
284
  if (Array.isArray(rule.content)) {
282
285
  const subruleContent = [];
286
+ const errors = [];
283
287
  for (const [index, subrule] of rule.content.entries()) {
284
288
  const subruleOptions = Array.isArray(options) ? options.at(index) : void 0;
285
289
  try {
286
- subruleContent.push(await getRuleContent(subrule, subruleOptions ?? {}, tree));
290
+ subruleContent.push(await getRuleContent(subrule, subruleOptions ?? {}, context, onWarning));
287
291
  } catch (error) {
288
- if (check) throw error;
292
+ const message = error instanceof Error ? error.cause instanceof Error ? error.cause.message : error.message : String(error);
293
+ onWarning?.(`Sub-rule ${String(index)} failed: ${message}`);
294
+ errors.push(error instanceof Error ? error : new Error(String(error)));
289
295
  }
290
296
  }
297
+ if (subruleContent.length === 0) throw new AggregateError(errors, "All sub-rules failed in compound rule");
291
298
  return subruleContent.join("\n\n");
292
299
  }
293
300
  try {
294
- return await rule.content(options, tree);
301
+ return await rule.content(options, context);
295
302
  } catch (error) {
296
- if (check) throw error;
297
303
  throw new Error("Failed to expand content", { cause: error });
298
304
  }
299
305
  }
@@ -340,6 +346,16 @@ function getSoleRecord(record) {
340
346
  async function mdatExpand(tree, file, rules) {
341
347
  validateRules(rules);
342
348
  const normalizedRules = normalizeRules(rules);
349
+ const frontmatter = (() => {
350
+ if (typeof file.value !== "string") return;
351
+ const { data } = matter(file.value);
352
+ return Object.keys(data).length > 0 ? data : void 0;
353
+ })();
354
+ const context = {
355
+ filePath: file.history.length > 0 ? file.path : void 0,
356
+ frontmatter,
357
+ tree
358
+ };
343
359
  const commentMarkers = [];
344
360
  visit(tree, "html", (node, index, parent) => {
345
361
  if (parent === void 0 || index === void 0) return CONTINUE;
@@ -357,7 +373,9 @@ async function mdatExpand(tree, file, rules) {
357
373
  const rule = normalizedRules[keyword];
358
374
  let newMarkdownString = "";
359
375
  try {
360
- newMarkdownString = await getRuleContent(rule, options, tree);
376
+ newMarkdownString = await getRuleContent(rule, options, context, (warning) => {
377
+ saveLog(file, "warn", "expand", `${html}: ${warning}`, node);
378
+ });
361
379
  if (newMarkdownString.trim() === "") saveLog(file, "error", "expand", `Got empty content when expanding ${html}`, node);
362
380
  } catch (error) {
363
381
  if (error instanceof Error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remark-mdat",
3
- "version": "2.0.0-preview.1",
3
+ "version": "2.0.0-preview.3",
4
4
  "description": "A remark plugin implementing the Markdown Autophagic Template (MDAT) system.",
5
5
  "keywords": [
6
6
  "mdat",
@@ -43,6 +43,7 @@
43
43
  "dependencies": {
44
44
  "@types/mdast": "^4.0.4",
45
45
  "@types/unist": "^3.0.3",
46
+ "gray-matter-es": "^0.2.1",
46
47
  "hast-util-from-html": "^2.0.3",
47
48
  "json5": "^2.2.3",
48
49
  "lognow": "^0.5.2",
@@ -67,7 +68,7 @@
67
68
  "vitest": "^4.1.1"
68
69
  },
69
70
  "engines": {
70
- "node": ">=20.19.0"
71
+ "node": ">=20.19.6"
71
72
  },
72
73
  "devEngines": {
73
74
  "runtime": {
@@ -82,7 +83,7 @@
82
83
  "fix": "ksc fix",
83
84
  "lint": "ksc lint",
84
85
  "release": "bumpp --commit 'Release: %s' && pnpm run build && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish",
85
- "release-preview": "bumpp --preid preview --commit 'Release: %s' && NPM_AUTH_TOKEN=$(op read 'op://Personal/npm/token') && pnpm publish --tag preview",
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
@@ -128,8 +128,38 @@ const rules: Rules = {
128
128
  // Array: compound rule combining multiple sub-rules
129
129
  header: ['# My Project', () => getDescription()],
130
130
 
131
- // Function with document access: receives the full mdast tree
132
- toc: (_options, tree) => generateTocFromTree(tree),
131
+ // Function with context: access the document tree, frontmatter, and file path
132
+ toc: (_options, context) => generateTocFromTree(context.tree),
133
+ }
134
+ ```
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),
133
163
  }
134
164
  ```
135
165