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.
- package/dist/index.d.ts +28 -108
- package/dist/index.js +123 -385
- package/package.json +12 -12
- 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
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
63
|
+
* The order in which the rule should be applied during processing.
|
|
64
|
+
* Defaults to 0.
|
|
87
65
|
*/
|
|
88
|
-
|
|
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: {
|
|
85
|
+
* { basic-meta: { order: 1, content: 'content'} }
|
|
108
86
|
* ```
|
|
109
87
|
*
|
|
110
88
|
* Rule with dynamic content and metadata:
|
|
111
|
-
* { basic-date: {
|
|
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<
|
|
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
|
-
|
|
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
|
|
163
|
-
*
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
};
|
|
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 =
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
11
|
+
//#endregion
|
|
13
12
|
//#region src/lib/mdat/log.ts
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
113
|
+
function parseCommentNode(node, parent) {
|
|
137
114
|
try {
|
|
138
|
-
const result = parseComment(node.value
|
|
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
|
-
*
|
|
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
|
|
135
|
+
function parseComment(text) {
|
|
159
136
|
if (!isComment(text)) return;
|
|
160
|
-
const
|
|
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
|
|
165
|
-
const
|
|
166
|
-
if (
|
|
167
|
-
|
|
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 (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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
|
|
280
|
-
const rootSchema = z.any();
|
|
256
|
+
const functionSchema = z.custom((value) => typeof value === "function");
|
|
281
257
|
const normalizedRuleSchema = z.lazy(() => z.object({
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
304
|
-
const
|
|
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-
|
|
336
|
+
//#region src/lib/mdast-utils/mdast-util-mdat-expand.ts
|
|
366
337
|
/**
|
|
367
|
-
* Mdast utility
|
|
338
|
+
* Mdast utility to expand mdat comments in the tree.
|
|
368
339
|
*/
|
|
369
|
-
async function
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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 {
|
|
566
|
-
const rule =
|
|
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: `<!--
|
|
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,
|
|
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
|
-
|
|
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
|
|
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(
|
|
719
|
-
const
|
|
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,
|
|
460
|
+
await mdat(tree, file, resolvedRules);
|
|
722
461
|
};
|
|
723
462
|
};
|
|
724
|
-
|
|
725
463
|
//#endregion
|
|
726
|
-
export {
|
|
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": "
|
|
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.
|
|
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.
|
|
57
|
+
"zod": "^4.3.6"
|
|
59
58
|
},
|
|
60
59
|
"devDependencies": {
|
|
61
60
|
"@arethetypeswrong/core": "^0.18.2",
|
|
62
|
-
"@kitschpatrol/shared-config": "^6.0
|
|
63
|
-
"@types/node": "~20.19.
|
|
64
|
-
"bumpp": "^
|
|
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.
|
|
65
|
+
"tsdown": "^0.21.4",
|
|
67
66
|
"typescript": "~5.9.3",
|
|
68
|
-
"vitest": "^4.
|
|
67
|
+
"vitest": "^4.1.1"
|
|
69
68
|
},
|
|
70
69
|
"engines": {
|
|
71
|
-
"node": ">=20.
|
|
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
|
-
"
|
|
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
|
[](https://npmjs.com/package/remark-mdat)
|
|
12
17
|
[](https://opensource.org/licenses/MIT)
|
|
18
|
+
[](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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
179
|
-
const markdownOutput = await remark().use(remarkMdat,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
222
|
+
- [**`mdast-util-mdat`**](./src/lib/mdast-utils/mdast-util-mdat.ts)
|
|
202
223
|
|
|
203
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|