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 +41 -113
- package/dist/index.js +141 -385
- package/package.json +6 -5
- package/readme.md +82 -55
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,
|
|
28
|
+
content: ((options: JsonValue, context: RuleContext) => Promise<string>) | NormalizedRule[];
|
|
26
29
|
/**
|
|
27
|
-
* The
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
75
|
-
*
|
|
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,
|
|
69
|
+
content: ((options: JsonValue, context: RuleContext) => Promise<string> | string) | Rule[] | string;
|
|
79
70
|
/**
|
|
80
|
-
* The
|
|
81
|
-
* Defaults to
|
|
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
|
-
|
|
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: {
|
|
93
|
+
* { basic-meta: { order: 1, content: 'content'} }
|
|
108
94
|
* ```
|
|
109
95
|
*
|
|
110
96
|
* Rule with dynamic content and metadata:
|
|
111
|
-
* { basic-date: {
|
|
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<
|
|
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
|
-
|
|
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
|
|
163
|
-
*
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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 =
|
|
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
|
|
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 {
|
|
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
|
-
|
|
12
|
+
//#endregion
|
|
12
13
|
//#region src/lib/mdat/log.ts
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
114
|
+
function parseCommentNode(node, parent) {
|
|
134
115
|
try {
|
|
135
|
-
const result = parseComment(node.value
|
|
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
|
-
*
|
|
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
|
|
136
|
+
function parseComment(text) {
|
|
156
137
|
if (!isComment(text)) return;
|
|
157
|
-
const
|
|
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
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
164
|
-
|
|
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 (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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:
|
|
221
|
-
required: false
|
|
213
|
+
order: 0
|
|
222
214
|
};
|
|
223
215
|
else if (typeof rule === "function") normalizedRules[keyword] = {
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
253
|
-
|
|
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
|
|
276
|
-
const rootSchema = z.any();
|
|
257
|
+
const functionSchema = z.custom((value) => typeof value === "function");
|
|
277
258
|
const normalizedRuleSchema = z.lazy(() => z.object({
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
300
|
-
const
|
|
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
|
|
279
|
+
* Compound rule helpers for expanding rule content.
|
|
303
280
|
*/
|
|
304
|
-
async function getRuleContent(rule, options,
|
|
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
|
-
|
|
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,
|
|
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-
|
|
332
|
+
//#region src/lib/mdast-utils/mdast-util-mdat-expand.ts
|
|
361
333
|
/**
|
|
362
|
-
* Mdast utility
|
|
334
|
+
* Mdast utility to expand mdat comments in the tree.
|
|
363
335
|
*/
|
|
364
|
-
async function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
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 {
|
|
559
|
-
const rule =
|
|
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,
|
|
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: `<!--
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
708
|
-
const
|
|
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,
|
|
466
|
+
await mdat(tree, file, resolvedRules);
|
|
711
467
|
};
|
|
712
468
|
};
|
|
713
469
|
//#endregion
|
|
714
|
-
export {
|
|
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": "
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
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
|
|
187
|
-
const markdownOutput = await remark().use(remarkMdat,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
252
|
+
- [**`mdast-util-mdat`**](./src/lib/mdast-utils/mdast-util-mdat.ts)
|
|
210
253
|
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|