@markuplint/parser-utils 5.4.0 → 5.6.0

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/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # [5.6.0](https://github.com/markuplint/markuplint/compare/v5.5.0...v5.6.0) (2026-11-12)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **parser-utils:** map the IDL attribute acceptCharset to accept-charset ([6f78de8](https://github.com/markuplint/markuplint/commit/6f78de8f503cb88f4e3e433989318fdff83e18f5)), closes [#4571](https://github.com/markuplint/markuplint/issues/4571)
11
+
12
+ ### Features
13
+
14
+ * **parser-utils:** let a parser declare slotElement ([c6f97c5](https://github.com/markuplint/markuplint/commit/c6f97c5cd4f190843c00d1d8e89fbff4ca9097db))
15
+
16
+
17
+ # [5.5.0](https://github.com/markuplint/markuplint/compare/v5.4.0...v5.5.0) (2026-11-10)
18
+
19
+ ### Bug Fixes
20
+
21
+ * **parser-utils:** skip a leading BOM before tokenization ([9fa8c85](https://github.com/markuplint/markuplint/commit/9fa8c8584b765667530e1d7224530ff7ce37474d)), closes [#4488](https://github.com/markuplint/markuplint/issues/4488)
22
+
23
+ ### Features
24
+
25
+ * **parser-utils:** read a literal in the code of an attribute value ([d83b4a0](https://github.com/markuplint/markuplint/commit/d83b4a091b1e78975ab12b81be7096356a95e9c0))
26
+
27
+
6
28
  # [5.4.0](https://github.com/markuplint/markuplint/compare/v5.3.0...v5.4.0) (2026-10-29)
7
29
 
8
30
  **Note:** Version bump only for package @markuplint/parser-utils
@@ -6,7 +6,7 @@
6
6
  const idlContentMap = {
7
7
  // HTML
8
8
  accept: 'accept',
9
- acceptCharset: 'acceptcharset',
9
+ acceptCharset: 'accept-charset',
10
10
  accessKey: 'accesskey',
11
11
  action: 'action',
12
12
  allowFullScreen: 'allowfullscreen',
@@ -402,8 +402,8 @@ const idlContentMap = {
402
402
  * PerformanceElementTiming API
403
403
  *
404
404
  * @experimental
405
- * @see https://wicg.github.io/element-timing/#sec-modifications-DOM
406
- * @see https://wicg.github.io/element-timing/#sec-elements-exposed
405
+ * @see https://w3c.github.io/element-timing/#sec-modifications-DOM
406
+ * @see https://w3c.github.io/element-timing/#sec-elements-exposed
407
407
  */
408
408
  elementTiming: 'elementtiming',
409
409
  /**
package/lib/index.d.ts CHANGED
@@ -3,6 +3,8 @@ export * from './decision.js';
3
3
  export * from './enums.js';
4
4
  export * from './get-namespace.js';
5
5
  export * from './idl-attributes.js';
6
+ export * from './is-braced-expression.js';
7
+ export * from './parse-literal-expression.js';
6
8
  export * from './parser-error.js';
7
9
  export * from './parser.js';
8
10
  export * from './script-parser.js';
package/lib/index.js CHANGED
@@ -3,6 +3,8 @@ export * from './decision.js';
3
3
  export * from './enums.js';
4
4
  export * from './get-namespace.js';
5
5
  export * from './idl-attributes.js';
6
+ export * from './is-braced-expression.js';
7
+ export * from './parse-literal-expression.js';
6
8
  export * from './parser-error.js';
7
9
  export * from './parser.js';
8
10
  export * from './script-parser.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Whether a text is one expression in braces and nothing else (`{false}`, `{ {a: 1} }`), and not
3
+ * several (`{a}{b}`) or an expression with text around it (`page/{p}`, `{a} text`). Svelte reads an
4
+ * attribute value in quotes that is one expression as the expression
5
+ * (`disabled="{false}"` is `disabled={false}`).
6
+ *
7
+ * The check is of the braces: the first `{` is closed by the last `}`, so no `}` closes it before
8
+ * the end. Strings and template literals are read as such, so a brace in them does not count. The
9
+ * code in the braces is not checked to be valid JavaScript (it may be TypeScript).
10
+ *
11
+ * The catch below does not use `isFatalError`, as for `parseLiteralExpression`: espree reports the
12
+ * author's code by a `SyntaxError`, which here means that the text is not one expression.
13
+ *
14
+ * @param raw - The text of an attribute value
15
+ * @returns `true` if the whole text is one expression in braces
16
+ */
17
+ export declare function isBracedExpression(raw: string): boolean;
@@ -0,0 +1,52 @@
1
+ import { scriptParser } from './script-parser.js';
2
+ /**
3
+ * Whether a text is one expression in braces and nothing else (`{false}`, `{ {a: 1} }`), and not
4
+ * several (`{a}{b}`) or an expression with text around it (`page/{p}`, `{a} text`). Svelte reads an
5
+ * attribute value in quotes that is one expression as the expression
6
+ * (`disabled="{false}"` is `disabled={false}`).
7
+ *
8
+ * The check is of the braces: the first `{` is closed by the last `}`, so no `}` closes it before
9
+ * the end. Strings and template literals are read as such, so a brace in them does not count. The
10
+ * code in the braces is not checked to be valid JavaScript (it may be TypeScript).
11
+ *
12
+ * The catch below does not use `isFatalError`, as for `parseLiteralExpression`: espree reports the
13
+ * author's code by a `SyntaxError`, which here means that the text is not one expression.
14
+ *
15
+ * @param raw - The text of an attribute value
16
+ * @returns `true` if the whole text is one expression in braces
17
+ */
18
+ export function isBracedExpression(raw) {
19
+ if (raw.length < 2 || !raw.startsWith('{') || !raw.endsWith('}')) {
20
+ return false;
21
+ }
22
+ const inner = raw.slice(1, -1);
23
+ if (inner.trim() === '') {
24
+ return false;
25
+ }
26
+ let tokens;
27
+ try {
28
+ tokens = scriptParser(inner);
29
+ }
30
+ catch (error) {
31
+ if (error instanceof SyntaxError) {
32
+ return false;
33
+ }
34
+ throw error;
35
+ }
36
+ let depth = 0;
37
+ for (const token of tokens) {
38
+ if (token.type !== 'Punctuator') {
39
+ continue;
40
+ }
41
+ if (token.value === '{') {
42
+ depth++;
43
+ }
44
+ else if (token.value === '}') {
45
+ depth--;
46
+ if (depth < 0) {
47
+ return false;
48
+ }
49
+ }
50
+ }
51
+ return depth === 0;
52
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A JavaScript expression that is nothing but a literal, as {@link parseLiteralExpression} reads it.
3
+ */
4
+ export type LiteralExpression = {
5
+ readonly type: 'string';
6
+ readonly value: string;
7
+ readonly key: string;
8
+ } | {
9
+ readonly type: 'number';
10
+ readonly value: number;
11
+ readonly key: string;
12
+ } | {
13
+ readonly type: 'boolean';
14
+ readonly value: boolean;
15
+ readonly key: 'true' | 'false';
16
+ } | {
17
+ readonly type: 'null';
18
+ readonly value: null;
19
+ readonly key: 'null';
20
+ } | {
21
+ readonly type: 'undefined';
22
+ readonly value: undefined;
23
+ readonly key: 'undefined';
24
+ };
25
+ /**
26
+ * Reads a JavaScript expression and tells whether it is nothing but a literal: a string, a template
27
+ * literal without a substitution, a number, `true`, `false`, `null` or `undefined`, with a leading
28
+ * minus sign for a number, any number of balanced parentheses, and a TypeScript `as` / `satisfies`
29
+ * assertion after it. An identifier, a call, an object, an array, a template literal with a
30
+ * substitution, a regular expression, an operator, or `NaN` is not.
31
+ *
32
+ * The result carries the value that the code evaluates to, and `key`, the normal form of the literal:
33
+ * two literals of one value and one type have the same key however they are written (`'x'`, `"x"`
34
+ * and `` `x` ``; `0x0`, `-0` and `0`), and literals that differ in value or in type do not
35
+ * (`true` and `"true"`, `0` and `"0"` and `false`). A string's key is its JSON, a number's key is
36
+ * `String(value)`, and the rest are their keyword. The keys are those of `literalValues` of a spec.
37
+ *
38
+ * The code is expected to be an expression that a parser has already read, but a template language
39
+ * hands over whatever its author wrote (a Vue `:x="a b"`), so a code that is not JavaScript is
40
+ * `null` as well.
41
+ *
42
+ * JavaScript is parsed once and its tree says whether it is a literal. What only TypeScript has (`as` and
43
+ * `satisfies` after the literal) is not JavaScript, so a code that does not parse is read by its tokens
44
+ * ({@link parseLiteralByTokens}), which give the same answer for the code that does parse.
45
+ *
46
+ * The catches in this file do not use `isFatalError`: espree reports the code that it is given, which is
47
+ * the author's own, by a `SyntaxError`, and that is the answer "not a literal" here. Anything else is
48
+ * thrown again.
49
+ *
50
+ * @param code - The text of the expression, without the braces or quotes around it
51
+ * @returns The literal, or `null` when the code is not a literal
52
+ */
53
+ export declare function parseLiteralExpression(code: string): LiteralExpression | null;
54
+ /**
55
+ * Reads the code by its tokens: any number of `(`, one literal, any number of `)`, and a TypeScript
56
+ * `as` / `satisfies` assertion after it, which a parser of JavaScript does not read.
57
+ *
58
+ * It is what {@link parseLiteralExpression} falls back on, and is exported to be compared with the reading
59
+ * of the tree.
60
+ *
61
+ * @param code - The text of the expression
62
+ * @returns The literal, or `null` when the code is not a literal
63
+ */
64
+ export declare function parseLiteralByTokens(code: string): LiteralExpression | null;
@@ -0,0 +1,300 @@
1
+ // @ts-ignore
2
+ import { parse } from 'espree';
3
+ import { scriptParser } from './script-parser.js';
4
+ /**
5
+ * The characters a literal can start with: a quote, a digit, a dot (`.5`), a minus sign, an opening
6
+ * parenthesis, and the first letters of `true` / `false` / `null` / `undefined`. An expression that
7
+ * starts with anything else cannot be a literal, which spares the tokenizer for the many that are
8
+ * identifiers or calls.
9
+ */
10
+ const LITERAL_START = /^\s*["'`\d.\-(tfnu]/;
11
+ /**
12
+ * Reads a JavaScript expression and tells whether it is nothing but a literal: a string, a template
13
+ * literal without a substitution, a number, `true`, `false`, `null` or `undefined`, with a leading
14
+ * minus sign for a number, any number of balanced parentheses, and a TypeScript `as` / `satisfies`
15
+ * assertion after it. An identifier, a call, an object, an array, a template literal with a
16
+ * substitution, a regular expression, an operator, or `NaN` is not.
17
+ *
18
+ * The result carries the value that the code evaluates to, and `key`, the normal form of the literal:
19
+ * two literals of one value and one type have the same key however they are written (`'x'`, `"x"`
20
+ * and `` `x` ``; `0x0`, `-0` and `0`), and literals that differ in value or in type do not
21
+ * (`true` and `"true"`, `0` and `"0"` and `false`). A string's key is its JSON, a number's key is
22
+ * `String(value)`, and the rest are their keyword. The keys are those of `literalValues` of a spec.
23
+ *
24
+ * The code is expected to be an expression that a parser has already read, but a template language
25
+ * hands over whatever its author wrote (a Vue `:x="a b"`), so a code that is not JavaScript is
26
+ * `null` as well.
27
+ *
28
+ * JavaScript is parsed once and its tree says whether it is a literal. What only TypeScript has (`as` and
29
+ * `satisfies` after the literal) is not JavaScript, so a code that does not parse is read by its tokens
30
+ * ({@link parseLiteralByTokens}), which give the same answer for the code that does parse.
31
+ *
32
+ * The catches in this file do not use `isFatalError`: espree reports the code that it is given, which is
33
+ * the author's own, by a `SyntaxError`, and that is the answer "not a literal" here. Anything else is
34
+ * thrown again.
35
+ *
36
+ * @param code - The text of the expression, without the braces or quotes around it
37
+ * @returns The literal, or `null` when the code is not a literal
38
+ */
39
+ export function parseLiteralExpression(code) {
40
+ if (!LITERAL_START.test(code)) {
41
+ return null;
42
+ }
43
+ const fromTree = parseLiteralByTree(code);
44
+ return fromTree === undefined ? parseLiteralByTokens(code) : fromTree;
45
+ }
46
+ /**
47
+ * Reads the code as a JavaScript expression, in parentheses so that an object literal or a function is an
48
+ * expression too, and a line comment at the end does not take the parenthesis.
49
+ *
50
+ * @returns The literal, `null` when the code is JavaScript and is not a literal, or `undefined` when it is
51
+ * not JavaScript
52
+ */
53
+ function parseLiteralByTree(code) {
54
+ let program;
55
+ try {
56
+ program = parse(`(${code}\n)`, { ecmaVersion: 'latest' });
57
+ }
58
+ catch (error) {
59
+ if (error instanceof SyntaxError) {
60
+ return undefined;
61
+ }
62
+ throw error;
63
+ }
64
+ const statement = program.body.length === 1 ? program.body[0] : null;
65
+ return statement?.type === 'ExpressionStatement' ? literalOfNode(statement.expression) : null;
66
+ }
67
+ function literalOfNode(node) {
68
+ switch (node.type) {
69
+ case 'Literal': {
70
+ return literalOfValue(node, false);
71
+ }
72
+ case 'UnaryExpression': {
73
+ return node.operator === '-' && node.argument.type === 'Literal'
74
+ ? literalOfValue(node.argument, true)
75
+ : null;
76
+ }
77
+ case 'TemplateLiteral': {
78
+ const cooked = node.expressions.length === 0 ? node.quasis[0]?.value.cooked : undefined;
79
+ return typeof cooked === 'string' ? stringLiteral(cooked) : null;
80
+ }
81
+ case 'Identifier': {
82
+ return node.name === 'undefined' ? { type: 'undefined', value: undefined, key: 'undefined' } : null;
83
+ }
84
+ default: {
85
+ return null;
86
+ }
87
+ }
88
+ }
89
+ function literalOfValue(node, negative) {
90
+ const { value } = node;
91
+ if (node.regex !== undefined) {
92
+ return null;
93
+ }
94
+ if (typeof value === 'number' || typeof value === 'bigint') {
95
+ return numberLiteral(value, negative);
96
+ }
97
+ if (negative) {
98
+ return null;
99
+ }
100
+ switch (typeof value) {
101
+ case 'string': {
102
+ return stringLiteral(value);
103
+ }
104
+ case 'boolean': {
105
+ return value
106
+ ? { type: 'boolean', value: true, key: 'true' }
107
+ : { type: 'boolean', value: false, key: 'false' };
108
+ }
109
+ default: {
110
+ return value === null ? { type: 'null', value: null, key: 'null' } : null;
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Reads the code by its tokens: any number of `(`, one literal, any number of `)`, and a TypeScript
116
+ * `as` / `satisfies` assertion after it, which a parser of JavaScript does not read.
117
+ *
118
+ * It is what {@link parseLiteralExpression} falls back on, and is exported to be compared with the reading
119
+ * of the tree.
120
+ *
121
+ * @param code - The text of the expression
122
+ * @returns The literal, or `null` when the code is not a literal
123
+ */
124
+ export function parseLiteralByTokens(code) {
125
+ let tokens;
126
+ try {
127
+ tokens = scriptParser(code);
128
+ }
129
+ catch (error) {
130
+ if (error instanceof SyntaxError) {
131
+ return null;
132
+ }
133
+ throw error;
134
+ }
135
+ let index = 0;
136
+ let opened = 0;
137
+ while (tokens[index]?.value === '(' && tokens[index]?.type === 'Punctuator') {
138
+ opened++;
139
+ index++;
140
+ }
141
+ const negative = tokens[index]?.type === 'Punctuator' && tokens[index]?.value === '-';
142
+ if (negative) {
143
+ index++;
144
+ }
145
+ const token = tokens[index];
146
+ if (!token) {
147
+ return null;
148
+ }
149
+ index++;
150
+ const literal = readLiteral(token.type, token.value, negative);
151
+ if (!literal) {
152
+ return null;
153
+ }
154
+ let closed = 0;
155
+ while (tokens[index]?.value === ')' && tokens[index]?.type === 'Punctuator') {
156
+ closed++;
157
+ index++;
158
+ }
159
+ const rest = tokens[index];
160
+ if (rest) {
161
+ // Tokens after an `as` / `satisfies` are a type and not an expression, which has to end the code
162
+ const isAssertion = rest.type === 'Identifier' && (rest.value === 'as' || rest.value === 'satisfies');
163
+ return isAssertion && isType(tokens.slice(index + 1), opened - closed) ? literal : null;
164
+ }
165
+ return closed === opened ? literal : null;
166
+ }
167
+ /**
168
+ * Tokens that a type is made of: names, keywords, literal types, and the punctuation of unions, generics,
169
+ * arrays, objects and parentheses. An operator (`+`, `||`, `?`, `,`) makes it code that goes on after the
170
+ * assertion, which is not a literal. `:` and `;` are the punctuation of an object type, so they are only
171
+ * inside its braces: `"x" as A : b` is not a type.
172
+ */
173
+ const TYPE_PUNCTUATORS = new Set(['|', '&', '<', '>', '>>', '[', ']', '.', '{', '}']);
174
+ const OBJECT_TYPE_PUNCTUATORS = new Set([':', ';']);
175
+ /**
176
+ * @param tokens - The tokens after `as` / `satisfies`
177
+ * @param depth - The number of parentheses that are still open
178
+ * @returns `true` if the tokens are a type that closes the parentheses that are open, and nothing else
179
+ */
180
+ function isType(tokens, depth) {
181
+ if (tokens.length === 0) {
182
+ return false;
183
+ }
184
+ let open = depth;
185
+ let braces = 0;
186
+ for (const token of tokens) {
187
+ if (token.type !== 'Punctuator') {
188
+ continue;
189
+ }
190
+ if (token.value === '(') {
191
+ open++;
192
+ }
193
+ else if (token.value === ')') {
194
+ open--;
195
+ if (open < 0) {
196
+ return false;
197
+ }
198
+ }
199
+ else if (token.value === '{') {
200
+ braces++;
201
+ }
202
+ else if (token.value === '}') {
203
+ braces--;
204
+ if (braces < 0) {
205
+ return false;
206
+ }
207
+ }
208
+ else if (!TYPE_PUNCTUATORS.has(token.value) && !(braces > 0 && OBJECT_TYPE_PUNCTUATORS.has(token.value))) {
209
+ return false;
210
+ }
211
+ }
212
+ return open === 0 && braces === 0;
213
+ }
214
+ function readLiteral(type, value, negative) {
215
+ if (negative) {
216
+ if (type !== 'Numeric') {
217
+ return null;
218
+ }
219
+ return readNumber(value, true);
220
+ }
221
+ switch (type) {
222
+ case 'String': {
223
+ const literal = evaluate(value);
224
+ return typeof literal === 'string' ? stringLiteral(literal) : null;
225
+ }
226
+ case 'Template': {
227
+ // A template literal with a substitution is split into several tokens, and its first one does not end with a backquote
228
+ if (value.length < 2 || !value.startsWith('`') || !value.endsWith('`')) {
229
+ return null;
230
+ }
231
+ const cooked = evaluate(value);
232
+ return typeof cooked === 'string' ? stringLiteral(cooked) : null;
233
+ }
234
+ case 'Numeric': {
235
+ return readNumber(value, false);
236
+ }
237
+ case 'Boolean': {
238
+ return value === 'true'
239
+ ? { type: 'boolean', value: true, key: 'true' }
240
+ : { type: 'boolean', value: false, key: 'false' };
241
+ }
242
+ case 'Null': {
243
+ return { type: 'null', value: null, key: 'null' };
244
+ }
245
+ case 'Identifier': {
246
+ return value === 'undefined' ? { type: 'undefined', value: undefined, key: 'undefined' } : null;
247
+ }
248
+ default: {
249
+ return null;
250
+ }
251
+ }
252
+ }
253
+ function readNumber(source, negative) {
254
+ const evaluated = evaluate(source);
255
+ return typeof evaluated === 'number' || typeof evaluated === 'bigint' ? numberLiteral(evaluated, negative) : null;
256
+ }
257
+ function numberLiteral(evaluated, negative) {
258
+ if (typeof evaluated === 'bigint') {
259
+ // A BigInt keeps its digits: the number is only the approximation of it
260
+ const signed = negative ? -evaluated : evaluated;
261
+ return { type: 'number', value: Number(signed), key: signed.toString() };
262
+ }
263
+ const value = negative ? -evaluated : evaluated;
264
+ // `String(-0)` is `'0'`, which is what the key of `0` is
265
+ return { type: 'number', value, key: String(value) };
266
+ }
267
+ function stringLiteral(value) {
268
+ return { type: 'string', value, key: JSON.stringify(value) };
269
+ }
270
+ /**
271
+ * Evaluates the source of one literal token with the parser of the language, which resolves the
272
+ * escapes (`\n`, `\x41`, `A`) and the notations of a number (`0x10`, `1e3`, `1_000`).
273
+ *
274
+ * The catch does not use `isFatalError`, for the reason that is given at {@link parseLiteralExpression}:
275
+ * the `SyntaxError` of espree is the answer "not a literal".
276
+ *
277
+ * @returns The value of the literal, or `undefined` when the source is not a single literal whose
278
+ * value the parser could tell (a template literal with an invalid escape has no cooked value)
279
+ */
280
+ function evaluate(source) {
281
+ let program;
282
+ try {
283
+ program = parse(`(${source})`, { ecmaVersion: 'latest' });
284
+ }
285
+ catch (error) {
286
+ if (error instanceof SyntaxError) {
287
+ return undefined;
288
+ }
289
+ throw error;
290
+ }
291
+ const statement = program.body[0];
292
+ const expression = statement?.type === 'ExpressionStatement' ? statement.expression : null;
293
+ if (expression?.type === 'Literal') {
294
+ return expression.value;
295
+ }
296
+ if (expression?.type === 'TemplateLiteral' && expression.expressions.length === 0) {
297
+ return expression.quasis[0]?.value.cooked ?? undefined;
298
+ }
299
+ return undefined;
300
+ }
package/lib/parser.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Token, ChildToken, QuoteSet, ParseOptions, ParserOptions, Tokenized, ValueType } from './types.js';
2
- import type { AttrNameRoute, EndTagType, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior, MLASTParseError } from '@markuplint/ml-ast';
2
+ import type { AttrNameRoute, EndTagType, ExpressionLanguage, MLASTDocument, MLASTParentNode, MLParser, ParserAuthoredElementNameDistinguishing, MLASTElement, MLASTElementCloseTag, MLASTToken, MLASTNodeTreeItem, MLASTTag, MLASTText, MLASTAttr, MLASTChildNode, MLASTSpreadAttr, ElementType, Walker, MLASTHTMLAttr, MLASTBlockBehavior, MLASTParseError, LiteralValueDeclaration } from '@markuplint/ml-ast';
3
3
  import { AttrState } from './enums.js';
4
4
  import { ParserError } from './parser-error.js';
5
5
  /**
@@ -20,12 +20,25 @@ import { ParserError } from './parser-error.js';
20
20
  * - **Direct `Parser` subclass** only when the document structure itself
21
21
  * diverges from HTML (JSX, Vue SFC, Svelte, Pug, Astro).
22
22
  *
23
+ * A framework whose attribute values are code (`{…}`, `:x="…"`) says so for `@markuplint/ml-core`, which
24
+ * reads a value that is nothing but a literal and renders it as the framework does (`aria-busy={true}`
25
+ * is `aria-busy="true"`; `disabled={false}` is no attribute). The parser only records what is written
26
+ * (`isDynamicValue`) and declares the language of that code (`attrExpressionLanguage`); a template language
27
+ * that an HTML parser reads says it in a `DirectivePattern` of its spec (`expressionLanguage`). How the
28
+ * framework renders is `literalValues` of its spec, or of its parser when it has no spec package.
29
+ *
23
30
  * Direct subclasses should delegate tokenization to the framework's
24
31
  * established parser library rather than hand-rolling one, and those
25
32
  * libraries are chosen to span multiple major framework versions (e.g.
26
33
  * `vue-eslint-parser` covers both Vue 2 and Vue 3 template syntax) so the
27
34
  * parsers keep working across version ranges without frequent updates.
28
35
  *
36
+ * A direct subclass also decides what happens to a leading BOM (`leadingBom` of
37
+ * {@link ParserOptions}): `'mask'` when its tokenizer takes the BOM for content,
38
+ * `'strip'` when the library strips it itself or reads a leading space as
39
+ * meaning something, unset when it already ignores a BOM without moving any
40
+ * position. Check it with a BOM-prefixed source, positions included.
41
+ *
29
42
  * @template Node - The language-specific AST node type produced by the tokenizer
30
43
  * @template State - An optional parser state type that persists across tokenization
31
44
  */
@@ -55,6 +68,20 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
55
68
  * In the above, the `aria-hidden` is `true`.
56
69
  */
57
70
  get booleanish(): boolean;
71
+ /**
72
+ * The language of the code that an attribute value is written in when it is written as code.
73
+ * See {@link MLParser.attrExpressionLanguage}.
74
+ */
75
+ get attrExpressionLanguage(): ExpressionLanguage | undefined;
76
+ /**
77
+ * How the framework renders a literal in an attribute value, where it differs from the default
78
+ * of `@markuplint/ml-core`. See {@link MLParser.literalValues}.
79
+ */
80
+ get literalValues(): LiteralValueDeclaration | undefined;
81
+ /**
82
+ * What the `slot` element is in the language of the parser. See {@link MLParser.slotElement}.
83
+ */
84
+ get slotElement(): 'html' | 'framework';
58
85
  /**
59
86
  * The end tag omittable type.
60
87
  *
@@ -112,6 +139,10 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
112
139
  * two hooks must stay symmetric or position reporting for embedded code
113
140
  * fragments (e.g., a `<template>` block inside a `.vue` file) breaks.
114
141
  *
142
+ * A leading BOM is handled after this hook (`leadingBom` of {@link ParserOptions}),
143
+ * and only if the returned code still starts with it: an override that moves or
144
+ * drops it turns that handling off.
145
+ *
115
146
  * @param rawCode - The raw source code about to be parsed
116
147
  * @param options - Parse options that may specify offset positioning
117
148
  * @returns The preprocessed source code to be used for tokenization
@@ -122,9 +153,23 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
122
153
  * traversal, flattening, ignore-block restoration, and post-processing.
123
154
  * Returns the complete markuplint AST document.
124
155
  *
156
+ * A leading U+FEFF (BOM) is not part of the document: the HTML Standard skips it
157
+ * before tokenization, so a parser that handed it on would see text before the
158
+ * doctype (parse5 then opens `<html><head><body>` implicitly) and a Svelte
159
+ * template would be counted from the character after it. A parser that sets
160
+ * `leadingBom` (see {@link ParserOptions}) has it replaced with one space
161
+ * (`'mask'`), or taken away while the positions are moved one character later
162
+ * afterwards (`'strip'`), so that every offset still points into the source that
163
+ * `fix` edits; the returned `raw` keeps it. Code that starts at an offset (an
164
+ * embedded block) does not start a document, and its U+FEFF is a character like
165
+ * any other. The BOM is handled after {@link Parser.beforeParse}, and only if that
166
+ * hook left it first.
167
+ *
125
168
  * @param rawCode - The raw source code to parse
126
169
  * @param options - Parse options controlling offsets, depth, front matter, and authored element names
127
170
  * @returns The parsed AST document containing the node list and fragment flag
171
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#the-input-byte-stream
172
+ * > A leading Byte Order Mark (BOM) causes the character encoding argument to be ignored and will itself be skipped.
128
173
  */
129
174
  parse(rawCode: string, options?: ParseOptions): MLASTDocument;
130
175
  /**
@@ -333,7 +378,7 @@ export declare abstract class Parser<Node extends {} = {}, State extends unknown
333
378
  * Re-parses a text token to discover embedded HTML/XML tags within it,
334
379
  * splitting the content into a sequence of tag and text AST nodes.
335
380
  * Handles self-closing detection, depth tracking, void element recognition,
336
- * and the raw-text element body short-circuit (HTML LS §13.2.5.1).
381
+ * and the raw-text element body short-circuit (HTML LS §13.1.2.6).
337
382
  *
338
383
  * Raw-text element handling only covers the elements listed in
339
384
  * `rawTextElements` (default `['style', 'script']`). HTML LS *escapable* raw
package/lib/parser.js CHANGED
@@ -10,9 +10,20 @@ import { getEndCol, getEndLine, getOffsetsFromCode, getPosition } from './get-lo
10
10
  import { ignoreBlock, restoreNode } from './ignore-block.js';
11
11
  import { ignoreFrontMatter } from './ignore-front-matter.js';
12
12
  import { ParserError } from './parser-error.js';
13
+ import { shiftPositions } from './shift-leading-bom.js';
13
14
  import { sortNodes } from './sort-nodes.js';
14
15
  import { getNamespace } from './get-namespace.js';
15
16
  const timer = new PerformanceTimer();
17
+ /** U+FEFF as the first character of a source: a byte order mark, not content. */
18
+ const BOM = '\uFEFF';
19
+ /**
20
+ * Whether the code of a parse starts the source: no offset places it inside a
21
+ * larger one (an embedded block). `offsetLine` and `offsetColumn` are 1-based, so
22
+ * 1 is no offset.
23
+ */
24
+ function startsAtDocumentHead(options) {
25
+ return (options?.offsetOffset ?? 0) <= 0 && (options?.offsetLine ?? 1) <= 1 && (options?.offsetColumn ?? 1) <= 1;
26
+ }
16
27
  /**
17
28
  * Abstract base class for all markuplint parsers. Provides the core parsing pipeline
18
29
  * including tokenization, tree traversal, node flattening, and error handling.
@@ -31,17 +42,33 @@ const timer = new PerformanceTimer();
31
42
  * - **Direct `Parser` subclass** only when the document structure itself
32
43
  * diverges from HTML (JSX, Vue SFC, Svelte, Pug, Astro).
33
44
  *
45
+ * A framework whose attribute values are code (`{…}`, `:x="…"`) says so for `@markuplint/ml-core`, which
46
+ * reads a value that is nothing but a literal and renders it as the framework does (`aria-busy={true}`
47
+ * is `aria-busy="true"`; `disabled={false}` is no attribute). The parser only records what is written
48
+ * (`isDynamicValue`) and declares the language of that code (`attrExpressionLanguage`); a template language
49
+ * that an HTML parser reads says it in a `DirectivePattern` of its spec (`expressionLanguage`). How the
50
+ * framework renders is `literalValues` of its spec, or of its parser when it has no spec package.
51
+ *
34
52
  * Direct subclasses should delegate tokenization to the framework's
35
53
  * established parser library rather than hand-rolling one, and those
36
54
  * libraries are chosen to span multiple major framework versions (e.g.
37
55
  * `vue-eslint-parser` covers both Vue 2 and Vue 3 template syntax) so the
38
56
  * parsers keep working across version ranges without frequent updates.
39
57
  *
58
+ * A direct subclass also decides what happens to a leading BOM (`leadingBom` of
59
+ * {@link ParserOptions}): `'mask'` when its tokenizer takes the BOM for content,
60
+ * `'strip'` when the library strips it itself or reads a leading space as
61
+ * meaning something, unset when it already ignores a BOM without moving any
62
+ * position. Check it with a BOM-prefixed source, positions included.
63
+ *
40
64
  * @template Node - The language-specific AST node type produced by the tokenizer
41
65
  * @template State - An optional parser state type that persists across tokenization
42
66
  */
43
67
  export class Parser {
44
68
  #booleanish = false;
69
+ #attrExpressionLanguage;
70
+ #literalValues;
71
+ #slotElement = 'html';
45
72
  #defaultState;
46
73
  #endTagType = 'omittable';
47
74
  #ignoreTags = [];
@@ -53,6 +80,7 @@ export class Parser {
53
80
  #selfCloseType = 'html';
54
81
  #spaceChars = defaultSpaces;
55
82
  #rawTextElements = ['style', 'script'];
83
+ #leadingBom;
56
84
  /**
57
85
  * Buffer for parse errors collected from **embedded** parse() calls
58
86
  * (e.g., Markdown's inline HTML blocks, Pug's raw HTML lines — these
@@ -86,6 +114,9 @@ export class Parser {
86
114
  */
87
115
  constructor(options, defaultState) {
88
116
  this.#booleanish = options?.booleanish ?? this.#booleanish;
117
+ this.#attrExpressionLanguage = options?.attrExpressionLanguage ?? this.#attrExpressionLanguage;
118
+ this.#literalValues = options?.literalValues ?? this.#literalValues;
119
+ this.#slotElement = options?.slotElement ?? this.#slotElement;
89
120
  this.#endTagType = options?.endTagType ?? this.#endTagType;
90
121
  this.#ignoreTags = options?.ignoreTags ?? this.#ignoreTags;
91
122
  this.#maskChar = options?.maskChar ?? this.#maskChar;
@@ -96,6 +127,7 @@ export class Parser {
96
127
  this.#selfCloseType = options?.selfCloseType ?? this.#selfCloseType;
97
128
  this.#spaceChars = options?.spaceChars ?? this.#spaceChars;
98
129
  this.#rawTextElements = options?.rawTextElements ?? this.#rawTextElements;
130
+ this.#leadingBom = options?.leadingBom ?? this.#leadingBom;
99
131
  this.#defaultState = defaultState ?? null;
100
132
  this.state = structuredClone(this.#defaultState);
101
133
  }
@@ -119,6 +151,26 @@ export class Parser {
119
151
  get booleanish() {
120
152
  return this.#booleanish;
121
153
  }
154
+ /**
155
+ * The language of the code that an attribute value is written in when it is written as code.
156
+ * See {@link MLParser.attrExpressionLanguage}.
157
+ */
158
+ get attrExpressionLanguage() {
159
+ return this.#attrExpressionLanguage;
160
+ }
161
+ /**
162
+ * How the framework renders a literal in an attribute value, where it differs from the default
163
+ * of `@markuplint/ml-core`. See {@link MLParser.literalValues}.
164
+ */
165
+ get literalValues() {
166
+ return this.#literalValues;
167
+ }
168
+ /**
169
+ * What the `slot` element is in the language of the parser. See {@link MLParser.slotElement}.
170
+ */
171
+ get slotElement() {
172
+ return this.#slotElement;
173
+ }
122
174
  /**
123
175
  * The end tag omittable type.
124
176
  *
@@ -193,6 +245,10 @@ export class Parser {
193
245
  * two hooks must stay symmetric or position reporting for embedded code
194
246
  * fragments (e.g., a `<template>` block inside a `.vue` file) breaks.
195
247
  *
248
+ * A leading BOM is handled after this hook (`leadingBom` of {@link ParserOptions}),
249
+ * and only if the returned code still starts with it: an override that moves or
250
+ * drops it turns that handling off.
251
+ *
196
252
  * @param rawCode - The raw source code about to be parsed
197
253
  * @param options - Parse options that may specify offset positioning
198
254
  * @returns The preprocessed source code to be used for tokenization
@@ -206,25 +262,46 @@ export class Parser {
206
262
  * traversal, flattening, ignore-block restoration, and post-processing.
207
263
  * Returns the complete markuplint AST document.
208
264
  *
265
+ * A leading U+FEFF (BOM) is not part of the document: the HTML Standard skips it
266
+ * before tokenization, so a parser that handed it on would see text before the
267
+ * doctype (parse5 then opens `<html><head><body>` implicitly) and a Svelte
268
+ * template would be counted from the character after it. A parser that sets
269
+ * `leadingBom` (see {@link ParserOptions}) has it replaced with one space
270
+ * (`'mask'`), or taken away while the positions are moved one character later
271
+ * afterwards (`'strip'`), so that every offset still points into the source that
272
+ * `fix` edits; the returned `raw` keeps it. Code that starts at an offset (an
273
+ * embedded block) does not start a document, and its U+FEFF is a character like
274
+ * any other. The BOM is handled after {@link Parser.beforeParse}, and only if that
275
+ * hook left it first.
276
+ *
209
277
  * @param rawCode - The raw source code to parse
210
278
  * @param options - Parse options controlling offsets, depth, front matter, and authored element names
211
279
  * @returns The parsed AST document containing the node list and fragment flag
280
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#the-input-byte-stream
281
+ * > A leading Byte Order Mark (BOM) causes the character encoding argument to be ignored and will itself be skipped.
212
282
  */
213
283
  parse(rawCode, options) {
284
+ let strippedBom = false;
214
285
  try {
215
286
  // Initialize raw code
216
287
  this.#setRawCode(rawCode, rawCode);
217
288
  timer.push('beforeParse');
218
289
  const beforeParsedCode = this.beforeParse(this.rawCode, options);
219
290
  // Override raw code
220
- this.#setRawCode(beforeParsedCode);
291
+ const bom = this.#leadingBom && startsAtDocumentHead(options) && beforeParsedCode.startsWith(BOM)
292
+ ? this.#leadingBom
293
+ : null;
294
+ const masked = bom === 'mask';
295
+ strippedBom = bom === 'strip';
296
+ this.#setRawCode(bom ? (masked ? ' ' : '') + beforeParsedCode.slice(BOM.length) : beforeParsedCode);
221
297
  this.#authoredElementName = options?.authoredElementName;
222
298
  let frontMatter = null;
223
299
  if (options?.ignoreFrontMatter) {
224
300
  timer.push('ignoreFrontMatter');
225
- const fm = ignoreFrontMatter(this.rawCode);
226
- this.#setRawCode(fm.code);
227
- frontMatter = fm.frontMatter;
301
+ // The masked BOM is part of the front matter's raw, not of what it is looked for in
302
+ const fm = ignoreFrontMatter(masked ? this.rawCode.slice(BOM.length) : this.rawCode);
303
+ this.#setRawCode(masked ? ' ' + fm.code : fm.code);
304
+ frontMatter = fm.frontMatter == null || !masked ? fm.frontMatter : BOM + fm.frontMatter;
228
305
  }
229
306
  timer.push('ignoreBlock');
230
307
  const blocks = ignoreBlock(this.rawCode, this.#ignoreTags, this.#maskChar);
@@ -288,6 +365,10 @@ export class Parser {
288
365
  const embeddedErrors = this.embeddedParseErrors;
289
366
  const mergedParseErrors = parseErrors || embeddedErrors.length > 0 ? [...(parseErrors ?? []), ...embeddedErrors] : undefined;
290
367
  this.#reset();
368
+ if (strippedBom) {
369
+ // Everything above counted from the character after the BOM
370
+ shiftPositions([nodeList, mergedParseErrors], BOM.length);
371
+ }
291
372
  return {
292
373
  raw: rawCode,
293
374
  nodeList,
@@ -296,7 +377,17 @@ export class Parser {
296
377
  };
297
378
  }
298
379
  catch (error) {
299
- throw this.parseError(error);
380
+ const parserError = this.parseError(error);
381
+ if (strippedBom) {
382
+ // Only the fields are put back; the message that the library wrote is left as it is.
383
+ // A column of `0` is a position that is not known.
384
+ Object.assign(parserError, {
385
+ ...(parserError.line === 1 && parserError.col > 0 ? { col: parserError.col + BOM.length } : {}),
386
+ // The source an error quotes by default is the one without the BOM
387
+ ...(parserError.raw === this.rawCode ? { raw: rawCode } : {}),
388
+ });
389
+ }
390
+ throw parserError;
300
391
  }
301
392
  }
302
393
  /**
@@ -744,7 +835,7 @@ export class Parser {
744
835
  * Re-parses a text token to discover embedded HTML/XML tags within it,
745
836
  * splitting the content into a sequence of tag and text AST nodes.
746
837
  * Handles self-closing detection, depth tracking, void element recognition,
747
- * and the raw-text element body short-circuit (HTML LS §13.2.5.1).
838
+ * and the raw-text element body short-circuit (HTML LS §13.1.2.6).
748
839
  *
749
840
  * Raw-text element handling only covers the elements listed in
750
841
  * `rawTextElements` (default `['style', 'script']`). HTML LS *escapable* raw
@@ -846,10 +937,10 @@ export class Parser {
846
937
  nodes.push(tag);
847
938
  }
848
939
  /**
849
- * Raw-text element body short-circuit (HTML Living Standard §13.2.5.1
850
- * — "Restrictions on the contents of raw text and escapable raw text
851
- * elements". The spec section anchor is `cdata-rcdata-restrictions`,
852
- * where RCDATA stands for "Raw text + Character REF data" — the broader
940
+ * Raw-text element body short-circuit (HTML Living Standard §13.1.2.6,
941
+ * Restrictions on the contents of raw text and escapable raw text
942
+ * elements. The spec section anchor is `cdata-rcdata-restrictions`,
943
+ * where RCDATA stands for 'Raw text + Character REF data' — the broader
853
944
  * class that covers escapable raw text such as `<title>` / `<textarea>`).
854
945
  *
855
946
  * Per spec, the contents of `<script>` / `<style>` (and any caller-supplied
@@ -1221,7 +1312,7 @@ export class Parser {
1221
1312
  }
1222
1313
  /**
1223
1314
  * The pattern matches `</tagName` followed by a tab/LF/FF/CR/space/`>`/`/`
1224
- * (HTML LS §13.2.5.1) ASCII-case-insensitively. Caching avoids recompiling the
1315
+ * (HTML LS §13.1.2.6) ASCII-case-insensitively. Caching avoids recompiling the
1225
1316
  * `RegExp` for every `<script>` / `<style>` start tag in large documents.
1226
1317
  */
1227
1318
  #getRawTextCloseTagPattern(tagName) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Moves every position found in `value` one character later, in place: the
3
+ * positions a tokenizer reports for a source whose leading BOM it did not see.
4
+ *
5
+ * A parser whose library strips the BOM itself (`pug-lexer`, `remark`) counts
6
+ * from the character after it. Parsing the source without the BOM keeps `raw`
7
+ * and the slices consistent, and this puts the positions back where the source
8
+ * that `fix` edits has them. The BOM itself belongs to no node, as the HTML
9
+ * Standard skips it.
10
+ *
11
+ * A token (`offset`, `line`, `col`) and a parse error (`startOffset` … `endCol`)
12
+ * are recognised by their keys, whatever plain object carries them, so it is only
13
+ * safe on what the parser itself produced (the node list and the parse errors); an
14
+ * instance of a class is not entered. The
15
+ * column moves only for a position on the first line, the one the BOM is on. A
16
+ * token that several nodes refer to is moved once.
17
+ *
18
+ * @param value - A node list, a node, a token or a list of parse errors
19
+ * @param length - How many characters later the positions move
20
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#the-input-byte-stream
21
+ * > A leading Byte Order Mark (BOM) causes the character encoding argument to be ignored and will itself be skipped.
22
+ */
23
+ export declare function shiftPositions(value: unknown, length?: number): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Moves every position found in `value` one character later, in place: the
3
+ * positions a tokenizer reports for a source whose leading BOM it did not see.
4
+ *
5
+ * A parser whose library strips the BOM itself (`pug-lexer`, `remark`) counts
6
+ * from the character after it. Parsing the source without the BOM keeps `raw`
7
+ * and the slices consistent, and this puts the positions back where the source
8
+ * that `fix` edits has them. The BOM itself belongs to no node, as the HTML
9
+ * Standard skips it.
10
+ *
11
+ * A token (`offset`, `line`, `col`) and a parse error (`startOffset` … `endCol`)
12
+ * are recognised by their keys, whatever plain object carries them, so it is only
13
+ * safe on what the parser itself produced (the node list and the parse errors); an
14
+ * instance of a class is not entered. The
15
+ * column moves only for a position on the first line, the one the BOM is on. A
16
+ * token that several nodes refer to is moved once.
17
+ *
18
+ * @param value - A node list, a node, a token or a list of parse errors
19
+ * @param length - How many characters later the positions move
20
+ * @see https://html.spec.whatwg.org/multipage/parsing.html#the-input-byte-stream
21
+ * > A leading Byte Order Mark (BOM) causes the character encoding argument to be ignored and will itself be skipped.
22
+ */
23
+ export function shiftPositions(value, length = 1) {
24
+ walk(value, length, new WeakSet());
25
+ }
26
+ function walk(value, length, seen) {
27
+ if (typeof value !== 'object' || value === null || seen.has(value)) {
28
+ return;
29
+ }
30
+ seen.add(value);
31
+ if (Array.isArray(value)) {
32
+ for (const item of value) {
33
+ walk(item, length, seen);
34
+ }
35
+ return;
36
+ }
37
+ // A node and a token are plain objects; an instance of a class is state that something else holds
38
+ const prototype = Object.getPrototypeOf(value);
39
+ if (prototype !== Object.prototype && prototype !== null) {
40
+ return;
41
+ }
42
+ const record = value;
43
+ shift(record, 'offset', 'line', 'col', length);
44
+ shift(record, 'startOffset', 'startLine', 'startCol', length);
45
+ shift(record, 'endOffset', 'endLine', 'endCol', length);
46
+ for (const child of Object.values(record)) {
47
+ walk(child, length, seen);
48
+ }
49
+ }
50
+ function shift(record, offsetKey, lineKey, colKey, length) {
51
+ const { [offsetKey]: offset, [lineKey]: line, [colKey]: col } = record;
52
+ if (typeof offset !== 'number' || typeof line !== 'number' || typeof col !== 'number') {
53
+ return;
54
+ }
55
+ record[offsetKey] = offset + length;
56
+ if (line === 1) {
57
+ record[colKey] = col + length;
58
+ }
59
+ }
package/lib/types.d.ts CHANGED
@@ -1,10 +1,13 @@
1
- import type { AttrNameRoute, EndTagType, MLASTParentNode, MLASTParseError, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
1
+ import type { AttrNameRoute, EndTagType, ExpressionLanguage, LiteralValueDeclaration, MLASTParentNode, MLASTParseError, ParserOptions as ConfigParserOptions } from '@markuplint/ml-ast';
2
2
  /**
3
3
  * Configuration options for initializing a Parser instance,
4
4
  * controlling how the parser handles tags, attributes, and whitespace.
5
5
  */
6
6
  export type ParserOptions = {
7
7
  readonly booleanish?: boolean;
8
+ readonly attrExpressionLanguage?: ExpressionLanguage;
9
+ readonly literalValues?: LiteralValueDeclaration;
10
+ readonly slotElement?: 'html' | 'framework';
8
11
  readonly endTagType?: EndTagType;
9
12
  readonly ignoreTags?: readonly IgnoreTag[];
10
13
  readonly maskChar?: string;
@@ -15,6 +18,22 @@ export type ParserOptions = {
15
18
  readonly selfCloseType?: SelfCloseType;
16
19
  readonly spaceChars?: readonly string[];
17
20
  readonly rawTextElements?: readonly string[];
21
+ /**
22
+ * What {@link Parser.parse} does with a leading U+FEFF (BOM) before tokenization.
23
+ * The HTML Standard skips it, so it is not content.
24
+ *
25
+ * - `'mask'`: it becomes one space. For a tokenizer that takes the BOM for text
26
+ * (parse5) or counts from the character after it (`svelte/compiler`) and that
27
+ * does not read a leading space as meaning anything.
28
+ * - `'strip'`: the tokenizer is given the source without it, and the positions
29
+ * are moved one character later afterwards. For a tokenizer that strips the
30
+ * BOM itself (`pug-lexer`, `remark`) or reads a leading space as meaning
31
+ * something (Pug's indentation, a Markdown front matter), where `'mask'`
32
+ * would change the tree.
33
+ *
34
+ * Leave it unset when the tokenizer already handles a BOM correctly.
35
+ */
36
+ readonly leadingBom?: 'mask' | 'strip';
18
37
  };
19
38
  /**
20
39
  * Options passed to a single parse invocation, extending the base config parser options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markuplint/parser-utils",
3
- "version": "5.4.0",
3
+ "version": "5.6.0",
4
4
  "description": "Utility module for markuplint parser plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,25 +26,22 @@
26
26
  "publishConfig": {
27
27
  "access": "public"
28
28
  },
29
- "typedoc": {
30
- "entryPoint": "./src/index.ts"
31
- },
32
29
  "scripts": {
33
30
  "build": "tsc --project tsconfig.build.json",
34
31
  "dev": "tsc --watch --project tsconfig.build.json",
35
32
  "clean": "tsc --build --clean tsconfig.build.json"
36
33
  },
37
34
  "dependencies": {
38
- "@markuplint/ml-ast": "5.3.0",
39
- "@markuplint/ml-spec": "5.4.0",
40
- "@markuplint/shared": "5.4.0",
41
- "@markuplint/types": "5.4.0",
35
+ "@markuplint/ml-ast": "5.6.0",
36
+ "@markuplint/ml-spec": "5.6.0",
37
+ "@markuplint/shared": "5.6.0",
38
+ "@markuplint/types": "5.6.0",
42
39
  "debug": "^4.4.3",
43
40
  "espree": "^11.2.0",
44
41
  "type-fest": "^5.10.0"
45
42
  },
46
43
  "devDependencies": {
47
- "@typescript-eslint/typescript-estree": "8.71.2"
44
+ "@typescript-eslint/typescript-estree": "8.72.1"
48
45
  },
49
- "gitHead": "ab56aa8e12b67fbf793ddf1267aaba9fda90269d"
46
+ "gitHead": "eade3d7202682e567f3c1b6985478f1d4b5c5481"
50
47
  }