@dogsbay/markdown-it-mdx-jsx 0.2.0-beta.132 → 0.2.0-beta.133
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.js +107 -42
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -101,41 +101,69 @@ export function parseJsxAttrs(str) {
|
|
|
101
101
|
* Handles multi-line by accepting the full remaining source.
|
|
102
102
|
*/
|
|
103
103
|
function parseTag(src, pos) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
104
|
+
const r = scanTag(src, pos);
|
|
105
|
+
return r && "length" in r ? r : null;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* {@link parseTag}, or — when an opening tag's attributes run past the end of `src` — where the
|
|
109
|
+
* scan stopped, to resume on a longer `src` (`resume`). parseBlockTag appends a tag's lines one
|
|
110
|
+
* at a time; re-scanning from the start each time was quadratic: camunda's OpenAPI pages open
|
|
111
|
+
* `<RequestSchema` with 3,700 lines of attributes, 12 s for one page, 114 s for the site.
|
|
112
|
+
*/
|
|
113
|
+
function scanTag(src, pos, resume,
|
|
114
|
+
/** Where `src` starts in the whole tag text: a resumed scan is fed one new line at a time. */
|
|
115
|
+
base = 0,
|
|
116
|
+
/** The whole tag text, for the attributes, when `src` is only its last piece. */
|
|
117
|
+
whole,
|
|
118
|
+
/** Scan no further than this index: a tag that has not closed by then is not a tag. */
|
|
119
|
+
maxEnd = Infinity) {
|
|
120
|
+
// Indices are into the whole text; `src` holds [base, len). Each line is scanned on its own and
|
|
121
|
+
// joined once at the end: reading a character of a string built by `+=` flattens the rope,
|
|
122
|
+
// so scanning the growing string was quadratic even when resumed.
|
|
123
|
+
const len = Math.min(base + src.length, maxEnd);
|
|
124
|
+
const at = (k) => src[k - base];
|
|
125
|
+
let i;
|
|
126
|
+
let tag;
|
|
127
|
+
let attrStart;
|
|
128
|
+
let braceDepth = 0;
|
|
129
|
+
let inQuote = null;
|
|
130
|
+
if (resume) {
|
|
131
|
+
({ tag, attrStart, i, braceDepth, inQuote } = resume);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
if (src.charCodeAt(pos) !== 0x3c /* < */)
|
|
135
|
+
return null;
|
|
136
|
+
i = pos + 1;
|
|
137
|
+
// Skip whitespace after <
|
|
125
138
|
while (i < len && /\s/.test(src[i]))
|
|
126
139
|
i++;
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
// Check for closing tag: </Tag>
|
|
141
|
+
const isClose = src[i] === "/";
|
|
142
|
+
if (isClose)
|
|
143
|
+
i++;
|
|
144
|
+
// Must start with uppercase letter (JSX convention)
|
|
145
|
+
if (i >= len || !/[A-Z]/.test(src[i]))
|
|
146
|
+
return null;
|
|
147
|
+
// Read tag name
|
|
148
|
+
const nameStart = i;
|
|
149
|
+
while (i < len && /[\w.]/.test(src[i]))
|
|
150
|
+
i++;
|
|
151
|
+
tag = src.slice(nameStart, i);
|
|
152
|
+
if (isClose) {
|
|
153
|
+
// Closing tag: skip whitespace, expect >
|
|
154
|
+
while (i < len && /\s/.test(src[i]))
|
|
155
|
+
i++;
|
|
156
|
+
if (i < len && src[i] === ">") {
|
|
157
|
+
return { tag, attrs: {}, selfClosing: false, isClose: true, length: i - pos + 1 };
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
129
160
|
}
|
|
130
|
-
|
|
161
|
+
// Opening tag — parse attributes until > or />
|
|
162
|
+
// Track brace depth for JSX expression attributes
|
|
163
|
+
attrStart = i;
|
|
131
164
|
}
|
|
132
|
-
// Opening tag — parse attributes until > or />
|
|
133
|
-
// Track brace depth for JSX expression attributes
|
|
134
|
-
let braceDepth = 0;
|
|
135
|
-
let inQuote = null;
|
|
136
|
-
const attrStart = i;
|
|
137
165
|
while (i < len) {
|
|
138
|
-
const ch =
|
|
166
|
+
const ch = at(i);
|
|
139
167
|
if (inQuote) {
|
|
140
168
|
if (ch === "\\") {
|
|
141
169
|
i += 2;
|
|
@@ -163,8 +191,8 @@ function parseTag(src, pos) {
|
|
|
163
191
|
}
|
|
164
192
|
// Only check for tag end when not inside braces or quotes
|
|
165
193
|
if (braceDepth === 0) {
|
|
166
|
-
if (ch === "/" && i + 1 < len &&
|
|
167
|
-
const attrsStr = src.slice(attrStart, i).trim();
|
|
194
|
+
if (ch === "/" && i + 1 < len && at(i + 1) === ">") {
|
|
195
|
+
const attrsStr = (whole ? whole() : src).slice(attrStart, i).trim();
|
|
168
196
|
return {
|
|
169
197
|
tag,
|
|
170
198
|
attrs: parseJsxAttrs(attrsStr),
|
|
@@ -174,7 +202,7 @@ function parseTag(src, pos) {
|
|
|
174
202
|
};
|
|
175
203
|
}
|
|
176
204
|
if (ch === ">") {
|
|
177
|
-
const attrsStr = src.slice(attrStart, i).trim();
|
|
205
|
+
const attrsStr = (whole ? whole() : src).slice(attrStart, i).trim();
|
|
178
206
|
return {
|
|
179
207
|
tag,
|
|
180
208
|
attrs: parseJsxAttrs(attrsStr),
|
|
@@ -186,7 +214,8 @@ function parseTag(src, pos) {
|
|
|
186
214
|
}
|
|
187
215
|
i++;
|
|
188
216
|
}
|
|
189
|
-
|
|
217
|
+
// Never found closing > — where to resume when there is more
|
|
218
|
+
return { tag, attrStart, i, braceDepth, inQuote };
|
|
190
219
|
}
|
|
191
220
|
// ── Block rule helpers ─────────────────────────────────────
|
|
192
221
|
function getLine(state, line) {
|
|
@@ -253,6 +282,23 @@ function findCloseTag(state, startLine, endLine, tag) {
|
|
|
253
282
|
* Concatenates lines as needed for multi-line tags.
|
|
254
283
|
* Returns null if no valid tag found.
|
|
255
284
|
*/
|
|
285
|
+
/**
|
|
286
|
+
* How much multi-line tag scanning one document may do, per parse: each line that opens a tag
|
|
287
|
+
* with no `>` scans to the end of the document, so a page of unclosed `<A x={` lines was
|
|
288
|
+
* quadratic (review 2026-10-21: 56 KB, 15 s). Real pages scan a few times their length
|
|
289
|
+
* (camunda's 3,700-line `<RequestSchema>`); past the budget a tag that has not closed is
|
|
290
|
+
* not a tag, and the line is read as text.
|
|
291
|
+
*/
|
|
292
|
+
const scanBudgets = new WeakMap();
|
|
293
|
+
function scanBudget(state) {
|
|
294
|
+
const key = (state.env && typeof state.env === "object" ? state.env : state);
|
|
295
|
+
let b = scanBudgets.get(key);
|
|
296
|
+
if (!b || b.src !== state.src) {
|
|
297
|
+
b = { src: state.src, left: 8 * state.src.length + 100_000 };
|
|
298
|
+
scanBudgets.set(key, b);
|
|
299
|
+
}
|
|
300
|
+
return b;
|
|
301
|
+
}
|
|
256
302
|
function parseBlockTag(state, startLine, endLine, offset = 0) {
|
|
257
303
|
const linePos = state.bMarks[startLine] + state.tShift[startLine];
|
|
258
304
|
const lineEnd = state.eMarks[startLine];
|
|
@@ -267,18 +313,27 @@ function parseBlockTag(state, startLine, endLine, offset = 0) {
|
|
|
267
313
|
const tagStart = remaining.match(/^<\/?([A-Z]\w*)/);
|
|
268
314
|
if (!tagStart)
|
|
269
315
|
return null;
|
|
270
|
-
//
|
|
271
|
-
|
|
316
|
+
// Add lines until we find > or />, resuming the scan where the last line left it
|
|
317
|
+
const budget = scanBudget(state);
|
|
318
|
+
const parts = [remaining];
|
|
319
|
+
let total = remaining.length;
|
|
320
|
+
const whole = () => parts.join("");
|
|
321
|
+
let scan = scanTag(remaining, 0);
|
|
272
322
|
let currentLine = startLine + 1;
|
|
273
323
|
while (currentLine < endLine) {
|
|
274
324
|
// The whole line, indentation included: a multi-line attribute VALUE
|
|
275
325
|
// (`<Code code={`…`} />`) is content, and trimming each line flattened
|
|
276
326
|
// every code sample written that way.
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
327
|
+
const piece = "\n" + state.src.slice(state.bMarks[currentLine], state.eMarks[currentLine]);
|
|
328
|
+
budget.left -= piece.length;
|
|
329
|
+
if (budget.left < 0)
|
|
330
|
+
return null;
|
|
331
|
+
parts.push(piece);
|
|
332
|
+
// A closing tag split over lines (`</Tag\n>`) is rare and short: scanned from the start
|
|
333
|
+
scan = scan && !("length" in scan) ? scanTag(piece, 0, scan, total, whole) : scanTag(whole(), 0);
|
|
334
|
+
total += piece.length;
|
|
335
|
+
if (scan && "length" in scan) {
|
|
336
|
+
return { ...scan, endLine: currentLine };
|
|
282
337
|
}
|
|
283
338
|
currentLine++;
|
|
284
339
|
}
|
|
@@ -421,6 +476,9 @@ function processFlowTags(state, startLine, endLine, linePos, lineText, component
|
|
|
421
476
|
}
|
|
422
477
|
// ── Inline rule ────────────────────────────────────────────
|
|
423
478
|
const openInline = new WeakMap();
|
|
479
|
+
/** What one inline run may still scan for tags (see `scanBudgets`): each `<` scanned to the end
|
|
480
|
+
* of the paragraph when no `>` came, quadratic in a paragraph of unclosed `<A x={` lines. */
|
|
481
|
+
const inlineBudgets = new WeakMap();
|
|
424
482
|
function createJsxInlineRule(options) {
|
|
425
483
|
const componentSet = options.components ? new Set(options.components) : null;
|
|
426
484
|
const selfClosingSet = new Set(options.selfClosing || []);
|
|
@@ -428,7 +486,14 @@ function createJsxInlineRule(options) {
|
|
|
428
486
|
const pos = state.pos;
|
|
429
487
|
if (state.src.charCodeAt(pos) !== 0x3c /* < */)
|
|
430
488
|
return false;
|
|
431
|
-
|
|
489
|
+
let budget = inlineBudgets.get(state);
|
|
490
|
+
if (!budget)
|
|
491
|
+
inlineBudgets.set(state, (budget = { left: 8 * state.src.length + 100_000 }));
|
|
492
|
+
if (budget.left <= 0)
|
|
493
|
+
return false;
|
|
494
|
+
const scan = scanTag(state.src, pos, undefined, 0, undefined, pos + budget.left);
|
|
495
|
+
budget.left -= scan && "length" in scan ? scan.length : scan ? scan.i - pos : 1;
|
|
496
|
+
const parsed = scan && "length" in scan ? scan : null;
|
|
432
497
|
if (!parsed)
|
|
433
498
|
return false;
|
|
434
499
|
if (componentSet && !componentSet.has(parsed.tag))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/markdown-it-mdx-jsx",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.133",
|
|
4
4
|
"description": "markdown-it plugin that tokenizes JSX-style component tags inside Markdown — used by @dogsbay/format-starlight",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|