@mui/internal-docs-infra 0.11.1-canary.15 → 0.11.1-canary.16
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/package.json +2 -2
- package/pipeline/parseSource/extendSyntaxTokens.mjs +501 -43
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.d.mts +12 -0
- package/pipeline/transformHtmlCodeInline/removeSuffixFromHighlightedNodes.mjs +52 -0
- package/pipeline/transformHtmlCodeInline/transformHtmlCodeInline.mjs +22 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.11.1-canary.
|
|
3
|
+
"version": "0.11.1-canary.16",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -768,5 +768,5 @@
|
|
|
768
768
|
"bin": {
|
|
769
769
|
"docs-infra": "./cli/index.mjs"
|
|
770
770
|
},
|
|
771
|
-
"gitSha": "
|
|
771
|
+
"gitSha": "4c7e314cf1f7d63fe80e86d6e3c84fc7d15a053e"
|
|
772
772
|
}
|
|
@@ -140,6 +140,334 @@ function enhanceStringSpan(element) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Tests whether a `pl-k` token's text consists entirely of symbol characters
|
|
145
|
+
* (no letters, digits, or underscore). Used to distinguish symbolic operators
|
|
146
|
+
* (`=`, `=>`, `&&`, `...`) from word keywords (`const`, `if`, `function`).
|
|
147
|
+
*/
|
|
148
|
+
function isSymbolicPunctuation(text) {
|
|
149
|
+
if (text.length === 0) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
153
|
+
const code = text.charCodeAt(i);
|
|
154
|
+
// 0-9
|
|
155
|
+
if (code >= 48 && code <= 57) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
// A-Z
|
|
159
|
+
if (code >= 65 && code <= 90) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
// a-z
|
|
163
|
+
if (code >= 97 && code <= 122) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
// _
|
|
167
|
+
if (code === 95) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Splits a text value into nodes, wrapping bare identifier object-literal keys
|
|
176
|
+
* (e.g. `height` in `{ height: 400 }`) in a `<span>` carrying `di-op` plus, when
|
|
177
|
+
* `inJsx` is true, also `di-jv`. Returns `null` if no key pattern is found,
|
|
178
|
+
* leaving the original text node untouched.
|
|
179
|
+
*
|
|
180
|
+
* A key is detected as `[A-Za-z_$][\w$]*` immediately preceded by `{` or `,`
|
|
181
|
+
* (with optional whitespace) and followed by optional whitespace, then a single
|
|
182
|
+
* `:` not part of `::`. The leading-context check avoids tagging ternary/label
|
|
183
|
+
* patterns; the trailing check avoids `::` (TypeScript namespace, pseudo-elements).
|
|
184
|
+
*/
|
|
185
|
+
function splitObjectKeys(value, inJsx) {
|
|
186
|
+
const nodes = [];
|
|
187
|
+
let lastEnd = 0;
|
|
188
|
+
let i = 0;
|
|
189
|
+
while (i < value.length) {
|
|
190
|
+
const code = value.charCodeAt(i);
|
|
191
|
+
const isIdentStart = code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95 || code === 36;
|
|
192
|
+
if (!isIdentStart) {
|
|
193
|
+
i += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find preceding non-whitespace character; must be `{` or `,` (object key context)
|
|
198
|
+
let prev = i - 1;
|
|
199
|
+
while (prev >= 0) {
|
|
200
|
+
const pc = value.charCodeAt(prev);
|
|
201
|
+
if (pc !== 32 && pc !== 9 && pc !== 10 && pc !== 13) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
prev -= 1;
|
|
205
|
+
}
|
|
206
|
+
if (prev < 0) {
|
|
207
|
+
i += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const prevCode = value.charCodeAt(prev);
|
|
211
|
+
if (prevCode !== 123 /* { */ && prevCode !== 44 /* , */) {
|
|
212
|
+
i += 1;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Consume identifier chars
|
|
217
|
+
let end = i + 1;
|
|
218
|
+
while (end < value.length) {
|
|
219
|
+
const ec = value.charCodeAt(end);
|
|
220
|
+
const isIdentPart = ec >= 48 && ec <= 57 || ec >= 65 && ec <= 90 || ec >= 97 && ec <= 122 || ec === 95 || ec === 36;
|
|
221
|
+
if (!isIdentPart) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
end += 1;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Skip whitespace, expect a single `:` (not `::`)
|
|
228
|
+
let after = end;
|
|
229
|
+
while (after < value.length) {
|
|
230
|
+
const ac = value.charCodeAt(after);
|
|
231
|
+
if (ac !== 32 && ac !== 9 && ac !== 10 && ac !== 13) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
after += 1;
|
|
235
|
+
}
|
|
236
|
+
if (after >= value.length || value.charCodeAt(after) !== 58 /* : */) {
|
|
237
|
+
i = end;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (after + 1 < value.length && value.charCodeAt(after + 1) === 58) {
|
|
241
|
+
i = end;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (i > lastEnd) {
|
|
245
|
+
nodes.push({
|
|
246
|
+
type: 'text',
|
|
247
|
+
value: value.slice(lastEnd, i)
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const className = inJsx ? ['di-op', 'di-jv'] : ['di-op'];
|
|
251
|
+
nodes.push({
|
|
252
|
+
type: 'element',
|
|
253
|
+
tagName: 'span',
|
|
254
|
+
properties: {
|
|
255
|
+
className
|
|
256
|
+
},
|
|
257
|
+
children: [{
|
|
258
|
+
type: 'text',
|
|
259
|
+
value: value.slice(i, end)
|
|
260
|
+
}]
|
|
261
|
+
});
|
|
262
|
+
lastEnd = end;
|
|
263
|
+
i = end;
|
|
264
|
+
}
|
|
265
|
+
if (nodes.length === 0) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (lastEnd < value.length) {
|
|
269
|
+
nodes.push({
|
|
270
|
+
type: 'text',
|
|
271
|
+
value: value.slice(lastEnd)
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return nodes;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Tests whether a string starts with optional whitespace followed by `:`.
|
|
279
|
+
* Used to detect that a `pl-s` span sits in object property-key position.
|
|
280
|
+
*/
|
|
281
|
+
function startsWithColon(text) {
|
|
282
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
283
|
+
const ch = text.charCodeAt(i);
|
|
284
|
+
if (ch === 58) {
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
// space, tab, newline, carriage return
|
|
288
|
+
if (ch !== 32 && ch !== 9 && ch !== 10 && ch !== 13) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Frame in the template-literal interpolation stack. A `string` frame means we
|
|
297
|
+
* are inside template-string content; an `expr` frame means we are inside a
|
|
298
|
+
* `${ ... }` interpolation expression, tracking `{`/`}` nesting via `braceDepth`
|
|
299
|
+
* so the matching close brace can be found across object literals and lines.
|
|
300
|
+
*/
|
|
301
|
+
|
|
302
|
+
/** Creates an empty `di-te` interpolation-region span. */
|
|
303
|
+
function createInterpolationRegion() {
|
|
304
|
+
return {
|
|
305
|
+
type: 'element',
|
|
306
|
+
tagName: 'span',
|
|
307
|
+
properties: {
|
|
308
|
+
className: ['di-te']
|
|
309
|
+
},
|
|
310
|
+
children: []
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Creates a `di-td` delimiter span wrapping the given `${` or `}` glyph. */
|
|
315
|
+
function createInterpolationDelimiter(value) {
|
|
316
|
+
return {
|
|
317
|
+
type: 'element',
|
|
318
|
+
tagName: 'span',
|
|
319
|
+
properties: {
|
|
320
|
+
className: ['di-td']
|
|
321
|
+
},
|
|
322
|
+
children: [{
|
|
323
|
+
type: 'text',
|
|
324
|
+
value
|
|
325
|
+
}]
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** True when a node is a `pl-pds` span whose text is a backtick. */
|
|
330
|
+
function isBacktickDelimiter(node) {
|
|
331
|
+
return !!node && node.type === 'element' && node.tagName === 'span' && getFirstClass(node) === 'pl-pds' && getShallowTextContent(node) === '`';
|
|
332
|
+
}
|
|
333
|
+
function pushText(target, value) {
|
|
334
|
+
target.push({
|
|
335
|
+
type: 'text',
|
|
336
|
+
value
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Scans one text node of a template literal, splitting it around interpolation
|
|
342
|
+
* boundaries. In `string` mode it looks for `${` (opening a `di-te` region with a
|
|
343
|
+
* `di-td` delimiter); in `expr` mode it counts `{`/`}` to find the matching close
|
|
344
|
+
* (emitting the closing `di-td`). Mutates `stack` and `targets` in place as it
|
|
345
|
+
* crosses boundaries, appending nodes to the innermost current target.
|
|
346
|
+
*/
|
|
347
|
+
function processTemplateText(value, stack, targets) {
|
|
348
|
+
let i = 0;
|
|
349
|
+
let segStart = 0;
|
|
350
|
+
while (i < value.length) {
|
|
351
|
+
const top = stack[stack.length - 1];
|
|
352
|
+
const target = targets[targets.length - 1];
|
|
353
|
+
if (top.mode === 'string') {
|
|
354
|
+
const open = value.indexOf('${', i);
|
|
355
|
+
if (open === -1) {
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
if (open > segStart) {
|
|
359
|
+
pushText(target, value.slice(segStart, open));
|
|
360
|
+
}
|
|
361
|
+
const region = createInterpolationRegion();
|
|
362
|
+
target.push(region);
|
|
363
|
+
region.children.push(createInterpolationDelimiter('${'));
|
|
364
|
+
stack.push({
|
|
365
|
+
mode: 'expr',
|
|
366
|
+
braceDepth: 1
|
|
367
|
+
});
|
|
368
|
+
targets.push(region.children);
|
|
369
|
+
i = open + 2;
|
|
370
|
+
segStart = i;
|
|
371
|
+
} else {
|
|
372
|
+
const code = value.charCodeAt(i);
|
|
373
|
+
if (code === 123 /* { */) {
|
|
374
|
+
top.braceDepth += 1;
|
|
375
|
+
i += 1;
|
|
376
|
+
} else if (code === 125 /* } */) {
|
|
377
|
+
top.braceDepth -= 1;
|
|
378
|
+
if (top.braceDepth === 0) {
|
|
379
|
+
if (i > segStart) {
|
|
380
|
+
pushText(target, value.slice(segStart, i));
|
|
381
|
+
}
|
|
382
|
+
target.push(createInterpolationDelimiter('}'));
|
|
383
|
+
stack.pop();
|
|
384
|
+
targets.pop();
|
|
385
|
+
i += 1;
|
|
386
|
+
segStart = i;
|
|
387
|
+
} else {
|
|
388
|
+
i += 1;
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
i += 1;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (segStart < value.length) {
|
|
396
|
+
pushText(targets[targets.length - 1], value.slice(segStart));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Restructures one `pl-s` template-literal line, wrapping each `${ ... }`
|
|
402
|
+
* interpolation slice on the line in a `di-te` region with `di-td` delimiters.
|
|
403
|
+
*
|
|
404
|
+
* `entryStack` carries the interpolation state from previous lines (a single
|
|
405
|
+
* `string` frame for the opening line). `isOpener` is true for the line that
|
|
406
|
+
* holds the opening backtick. Because starry-night emits one `pl-s` span per line
|
|
407
|
+
* and the line gutter splits on top-level newlines, a region can never cross a
|
|
408
|
+
* line boundary — each line's slice is wrapped on its own, so a continuation
|
|
409
|
+
* line opens a fresh `di-te` with no leading `${`. Returns the stack to carry to
|
|
410
|
+
* the next line, or `null` once the closing backtick is consumed (run complete).
|
|
411
|
+
*/
|
|
412
|
+
function restructureTemplateLine(pls, entryStack, isOpener) {
|
|
413
|
+
const source = pls.children;
|
|
414
|
+
const out = [];
|
|
415
|
+
const stack = entryStack.map(frame => ({
|
|
416
|
+
...frame
|
|
417
|
+
}));
|
|
418
|
+
|
|
419
|
+
// Rebuild the physical target chain for the carried stack: each open `expr`
|
|
420
|
+
// frame gets a fresh `di-te` region on this line; nested `string` frames share
|
|
421
|
+
// their parent expression's region as the target.
|
|
422
|
+
const targets = [out];
|
|
423
|
+
for (let depth = 1; depth < stack.length; depth += 1) {
|
|
424
|
+
if (stack[depth].mode === 'expr') {
|
|
425
|
+
const region = createInterpolationRegion();
|
|
426
|
+
targets[depth - 1].push(region);
|
|
427
|
+
targets.push(region.children);
|
|
428
|
+
} else {
|
|
429
|
+
targets.push(targets[depth - 1]);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
let runEnded = false;
|
|
433
|
+
let index = 0;
|
|
434
|
+
if (isOpener) {
|
|
435
|
+
out.push(source[0]);
|
|
436
|
+
index = 1;
|
|
437
|
+
}
|
|
438
|
+
for (; index < source.length; index += 1) {
|
|
439
|
+
const node = source[index];
|
|
440
|
+
const target = targets[targets.length - 1];
|
|
441
|
+
if (node.type === 'text') {
|
|
442
|
+
processTemplateText(node.value, stack, targets);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (isBacktickDelimiter(node)) {
|
|
446
|
+
if (stack[stack.length - 1].mode === 'expr') {
|
|
447
|
+
// A nested template literal opens inside the interpolation expression.
|
|
448
|
+
target.push(node);
|
|
449
|
+
stack.push({
|
|
450
|
+
mode: 'string'
|
|
451
|
+
});
|
|
452
|
+
targets.push(target);
|
|
453
|
+
} else if (stack.length === 1) {
|
|
454
|
+
// The outer template's closing backtick — the run is complete.
|
|
455
|
+
out.push(node);
|
|
456
|
+
runEnded = true;
|
|
457
|
+
} else {
|
|
458
|
+
// A nested template literal's closing backtick.
|
|
459
|
+
target.push(node);
|
|
460
|
+
stack.pop();
|
|
461
|
+
targets.pop();
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
target.push(node);
|
|
466
|
+
}
|
|
467
|
+
pls.children = out;
|
|
468
|
+
return runEnded ? null : stack;
|
|
469
|
+
}
|
|
470
|
+
|
|
143
471
|
/**
|
|
144
472
|
* Single-pass enhancement of a HAST children array. Processes each child exactly
|
|
145
473
|
* once, applying all per-element and sibling-context enhancements in one iteration.
|
|
@@ -148,6 +476,7 @@ function enhanceStringSpan(element) {
|
|
|
148
476
|
* Per-element enhancements (applied to individual spans):
|
|
149
477
|
* - `pl-c1` → `di-num`, `di-bool`, `di-n`, `di-this`, `di-bt` via enhanceConstantSpan
|
|
150
478
|
* - `pl-s` → `di-n` for empty strings via enhanceStringSpan
|
|
479
|
+
* - `pl-k` symbolic operators (`=`, `=>`, `&&`, `...`) → `di-pu`
|
|
151
480
|
*
|
|
152
481
|
* Sibling-context enhancements (depend on neighbor nodes or positional state):
|
|
153
482
|
* - CSS `&` nesting selector → wraps in `pl-ent` span
|
|
@@ -155,6 +484,9 @@ function enhanceStringSpan(element) {
|
|
|
155
484
|
* - CSS `property: value` → `di-cp` / `di-cv` based on colon position
|
|
156
485
|
* - HTML/JSX `<tag attr=value>` → `di-ak`, `di-ae`, `di-av`
|
|
157
486
|
* - JSX `<Component>` → `di-jsx` on component name spans
|
|
487
|
+
* - JSX `{expression}` → `di-jv` on `pl-smi`/`pl-v` identifier spans inside braces
|
|
488
|
+
* - JS `'key':` object property string → `di-ps` on `pl-s` spans
|
|
489
|
+
* - JS template literals → `di-te` region / `di-td` delimiters around `${ ... }`
|
|
158
490
|
*/
|
|
159
491
|
function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
160
492
|
// CSS declaration state: tracks position relative to { } : ; [ ]
|
|
@@ -165,9 +497,17 @@ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
|
165
497
|
// HTML/JSX tag state: whether we're between < and >
|
|
166
498
|
let htmlInsideTag = false;
|
|
167
499
|
|
|
500
|
+
// JSX expression depth: how many `pl-pse` `{` braces are currently open.
|
|
501
|
+
// Identifiers (`pl-smi`, `pl-v`) inside an expression are tagged as JSX variables.
|
|
502
|
+
let jsxExpressionDepth = 0;
|
|
503
|
+
|
|
168
504
|
// Whether a span appeared between the last text node and the current position.
|
|
169
505
|
// Used to detect attribute context for = wrapping (replaces backward scanning).
|
|
170
506
|
let hasSpanSinceLastText = false;
|
|
507
|
+
|
|
508
|
+
// Template-literal interpolation state, carried across the per-line `pl-s` spans
|
|
509
|
+
// of one multi-line literal. `null` when not inside a template-literal run.
|
|
510
|
+
let templateRun = null;
|
|
171
511
|
for (let index = 0; index < children.length; index += 1) {
|
|
172
512
|
const child = children[index];
|
|
173
513
|
|
|
@@ -238,56 +578,91 @@ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
|
238
578
|
}
|
|
239
579
|
}
|
|
240
580
|
|
|
241
|
-
// HTML/JSX: track < > tag boundaries and wrap bare = in attribute context
|
|
581
|
+
// HTML/JSX: track < > tag boundaries and wrap bare = in attribute context.
|
|
582
|
+
// A trailing `<` (next sibling = element) only enters tag mode when the next
|
|
583
|
+
// element looks like a JSX tag — `pl-ent` (HTML element) or `pl-c1` whose text
|
|
584
|
+
// isn't a TS built-in primitive. This avoids treating TS generics like
|
|
585
|
+
// `useState<number | null>` or `<T = string>` as JSX tags.
|
|
242
586
|
if (isHtmlJsx) {
|
|
243
587
|
for (let ci = 0; ci < value.length; ci += 1) {
|
|
244
588
|
if (value[ci] === '<') {
|
|
245
|
-
|
|
589
|
+
if (ci === value.length - 1) {
|
|
590
|
+
const nextChild = children[index + 1];
|
|
591
|
+
if (isJsx && nextChild && nextChild.type === 'element' && nextChild.tagName === 'span') {
|
|
592
|
+
const nextClass = getFirstClass(nextChild);
|
|
593
|
+
if (nextClass === 'pl-ent') {
|
|
594
|
+
htmlInsideTag = true;
|
|
595
|
+
} else if (nextClass === 'pl-c1') {
|
|
596
|
+
const nextText = getShallowTextContent(nextChild);
|
|
597
|
+
if (nextText && !BUILT_IN_TYPES.has(nextText)) {
|
|
598
|
+
htmlInsideTag = true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} else {
|
|
602
|
+
htmlInsideTag = true;
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
htmlInsideTag = true;
|
|
606
|
+
}
|
|
246
607
|
} else if (value[ci] === '>') {
|
|
247
608
|
htmlInsideTag = false;
|
|
248
609
|
}
|
|
249
610
|
}
|
|
250
|
-
|
|
251
|
-
const equalsIndex = value.indexOf('=');
|
|
252
|
-
if (equalsIndex !== -1) {
|
|
253
|
-
// Tag the following pl-s span as attribute value
|
|
254
|
-
const nextChild = children[index + 1];
|
|
255
|
-
if (nextChild && nextChild.type === 'element' && nextChild.tagName === 'span' && getFirstClass(nextChild) === 'pl-s') {
|
|
256
|
-
addClass(nextChild, 'di-av');
|
|
257
|
-
}
|
|
611
|
+
}
|
|
258
612
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
613
|
+
// Bare object-literal keys (e.g. `height` in `{ height: 400 }`) become di-op spans.
|
|
614
|
+
// Inside a JSX attribute expression they also receive di-jv. Done before the `=` split
|
|
615
|
+
// below, which only fires in attribute context (htmlInsideTag) — the two paths don't conflict.
|
|
616
|
+
// Children expressions (e.g. `<Comp>{children}</Comp>`) are excluded by the htmlInsideTag check.
|
|
617
|
+
if (isJs) {
|
|
618
|
+
const split = splitObjectKeys(value, isJsx && jsxExpressionDepth > 0 && htmlInsideTag);
|
|
619
|
+
if (split) {
|
|
620
|
+
children.splice(index, 1, ...split);
|
|
621
|
+
index += split.length - 1;
|
|
622
|
+
hasSpanSinceLastText = split[split.length - 1].type === 'element';
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (isHtmlJsx && htmlInsideTag && savedSpanFlag) {
|
|
627
|
+
const equalsIndex = value.indexOf('=');
|
|
628
|
+
if (equalsIndex !== -1) {
|
|
629
|
+
// Tag the following pl-s span as attribute value
|
|
630
|
+
const nextChild = children[index + 1];
|
|
631
|
+
if (nextChild && nextChild.type === 'element' && nextChild.tagName === 'span' && getFirstClass(nextChild) === 'pl-s') {
|
|
632
|
+
addClass(nextChild, 'di-av');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Split text around = and wrap in di-ae span
|
|
636
|
+
const before = value.slice(0, equalsIndex);
|
|
637
|
+
const after = value.slice(equalsIndex + 1);
|
|
638
|
+
const equalsSpan = {
|
|
639
|
+
type: 'element',
|
|
640
|
+
tagName: 'span',
|
|
641
|
+
properties: {
|
|
642
|
+
className: ['di-ae']
|
|
643
|
+
},
|
|
644
|
+
children: [{
|
|
645
|
+
type: 'text',
|
|
646
|
+
value: '='
|
|
647
|
+
}]
|
|
648
|
+
};
|
|
649
|
+
const newNodes = [];
|
|
650
|
+
if (before) {
|
|
651
|
+
newNodes.push({
|
|
652
|
+
type: 'text',
|
|
653
|
+
value: before
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
newNodes.push(equalsSpan);
|
|
657
|
+
if (after) {
|
|
658
|
+
newNodes.push({
|
|
659
|
+
type: 'text',
|
|
660
|
+
value: after
|
|
661
|
+
});
|
|
290
662
|
}
|
|
663
|
+
children.splice(index, 1, ...newNodes);
|
|
664
|
+
index += newNodes.length - 1;
|
|
665
|
+
hasSpanSinceLastText = newNodes[newNodes.length - 1].type === 'element';
|
|
291
666
|
}
|
|
292
667
|
}
|
|
293
668
|
continue;
|
|
@@ -298,6 +673,34 @@ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
|
298
673
|
continue;
|
|
299
674
|
}
|
|
300
675
|
|
|
676
|
+
// ── Template-literal interpolation (JS family) ──
|
|
677
|
+
// starry-night tokenizes a backtick string as a `pl-s` span (one per line for
|
|
678
|
+
// multi-line literals). Wrap each `${ ... }` slice in a `di-te` region with
|
|
679
|
+
// `di-td` delimiters so the interpolated expression resets from the string
|
|
680
|
+
// color. `templateRun` carries the brace/nesting state across the per-line
|
|
681
|
+
// `pl-s` spans; a run starts on the line whose first child is the opening
|
|
682
|
+
// backtick. Handled here, before the generic recursion, so the expression
|
|
683
|
+
// tokens are enhanced inside their regions and the outer `pl-s` is skipped.
|
|
684
|
+
if (isJs && child.tagName === 'span' && getFirstClass(child) === 'pl-s') {
|
|
685
|
+
const opensRun = templateRun === null && isBacktickDelimiter(child.children[0]);
|
|
686
|
+
if (templateRun !== null || opensRun) {
|
|
687
|
+
templateRun = restructureTemplateLine(child, templateRun ?? [{
|
|
688
|
+
mode: 'string'
|
|
689
|
+
}], opensRun);
|
|
690
|
+
// Empty backtick literals (`` `` ``) keep their nullish (`di-n`) classification.
|
|
691
|
+
enhanceStringSpan(child);
|
|
692
|
+
// Enhance the interpolated expressions (e.g. `di-num` on `${42}`) within
|
|
693
|
+
// each region; nested regions are reached by the recursion.
|
|
694
|
+
for (const region of child.children) {
|
|
695
|
+
if (region.type === 'element' && getFirstClass(region) === 'di-te') {
|
|
696
|
+
enhanceChildren(region.children, isCss, isHtmlJsx, isJs, isTs, isJsx);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
hasSpanSinceLastText = true;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
301
704
|
// Recurse into nested elements (frames, lines, nested spans)
|
|
302
705
|
if (child.children.length > 0) {
|
|
303
706
|
enhanceChildren(child.children, isCss, isHtmlJsx, isJs, isTs, isJsx);
|
|
@@ -314,6 +717,56 @@ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
|
314
717
|
enhanceConstantSpan(child, isJs, isTs);
|
|
315
718
|
} else if (firstClass === 'pl-s') {
|
|
316
719
|
enhanceStringSpan(child);
|
|
720
|
+
} else if (firstClass === 'pl-k') {
|
|
721
|
+
const text = getShallowTextContent(child);
|
|
722
|
+
if (text && isSymbolicPunctuation(text)) {
|
|
723
|
+
addClass(child, 'di-pu');
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// ── JSX expression brace tracking ──
|
|
728
|
+
if (isJsx && firstClass === 'pl-pse') {
|
|
729
|
+
const text = getShallowTextContent(child);
|
|
730
|
+
if (text === '{') {
|
|
731
|
+
jsxExpressionDepth += 1;
|
|
732
|
+
} else if (text === '}' && jsxExpressionDepth > 0) {
|
|
733
|
+
jsxExpressionDepth -= 1;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ── JSX variable: identifier-like spans inside an attribute expression ──
|
|
738
|
+
// - `pl-smi` plain identifier (e.g. `row` in `{row.name}`)
|
|
739
|
+
// - `pl-v` parameter / variable (e.g. arrow function params)
|
|
740
|
+
// - `pl-c1` after a `.` text node — member-access property (e.g. `name` in `{row.name}`).
|
|
741
|
+
// Skips numbers/booleans/nullish (which `enhanceConstantSpan` has already classified)
|
|
742
|
+
// and JSX components (handled below by the `<`/`</` detection).
|
|
743
|
+
//
|
|
744
|
+
// Restricted to attribute context (`htmlInsideTag`) so children expressions like
|
|
745
|
+
// `<Comp>{children}</Comp>` are not tagged.
|
|
746
|
+
if (isJsx && jsxExpressionDepth > 0 && htmlInsideTag) {
|
|
747
|
+
if (firstClass === 'pl-smi' || firstClass === 'pl-v') {
|
|
748
|
+
addClass(child, 'di-jv');
|
|
749
|
+
} else if (firstClass === 'pl-c1' && index > 0) {
|
|
750
|
+
const prev = children[index - 1];
|
|
751
|
+
if (prev.type === 'text' && prev.value.endsWith('.')) {
|
|
752
|
+
addClass(child, 'di-jv');
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ── JS object property string: pl-s followed by text starting with `:` ──
|
|
758
|
+
// String keys (e.g. `'aria-label': value`) get the dedicated `di-op` class plus
|
|
759
|
+
// `di-ps` for the string-shape detail. Inside JSX expressions, also add `di-jv`
|
|
760
|
+
// so themes that style JSX variables can include string keys.
|
|
761
|
+
if (isJs && firstClass === 'pl-s') {
|
|
762
|
+
const next = children[index + 1];
|
|
763
|
+
if (next && next.type === 'text' && startsWithColon(next.value)) {
|
|
764
|
+
addClass(child, 'di-op');
|
|
765
|
+
addClass(child, 'di-ps');
|
|
766
|
+
if (isJsx && jsxExpressionDepth > 0 && htmlInsideTag) {
|
|
767
|
+
addClass(child, 'di-jv');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
317
770
|
}
|
|
318
771
|
|
|
319
772
|
// ── CSS-specific enhancements ──
|
|
@@ -353,10 +806,15 @@ function enhanceChildren(children, isCss, isHtmlJsx, isJs, isTs, isJsx) {
|
|
|
353
806
|
if (isJsx && index > 0) {
|
|
354
807
|
const prev = children[index - 1];
|
|
355
808
|
|
|
356
|
-
// Opening/closing: text ending in < or </ followed by pl-c1
|
|
809
|
+
// Opening/closing: text ending in < or </ followed by pl-c1.
|
|
810
|
+
// Skip TS built-in types (e.g. `number` in `useState<number>`) so generic
|
|
811
|
+
// type arguments aren't mistaken for JSX components.
|
|
357
812
|
if (firstClass === 'pl-c1' && prev.type === 'text') {
|
|
358
813
|
if (prev.value.endsWith('<') || prev.value.endsWith('</')) {
|
|
359
|
-
|
|
814
|
+
const text = getShallowTextContent(child);
|
|
815
|
+
if (!text || !BUILT_IN_TYPES.has(text)) {
|
|
816
|
+
addClass(child, 'di-jsx');
|
|
817
|
+
}
|
|
360
818
|
}
|
|
361
819
|
}
|
|
362
820
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ElementContent } from 'hast';
|
|
2
|
+
/**
|
|
3
|
+
* Removes a suffix from the end of highlighted HAST nodes.
|
|
4
|
+
*
|
|
5
|
+
* Mirror of `removePrefixFromHighlightedNodes`: used to strip temporary
|
|
6
|
+
* trailing characters that were appended to the source before highlighting
|
|
7
|
+
* (e.g., closing `)` for object-literal wrapping).
|
|
8
|
+
*
|
|
9
|
+
* @param children - The array of HAST nodes to modify
|
|
10
|
+
* @param suffixLength - The number of characters to remove from the end
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeSuffixFromHighlightedNodes(children: ElementContent[], suffixLength: number): void;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes a suffix from the end of highlighted HAST nodes.
|
|
3
|
+
*
|
|
4
|
+
* Mirror of `removePrefixFromHighlightedNodes`: used to strip temporary
|
|
5
|
+
* trailing characters that were appended to the source before highlighting
|
|
6
|
+
* (e.g., closing `)` for object-literal wrapping).
|
|
7
|
+
*
|
|
8
|
+
* @param children - The array of HAST nodes to modify
|
|
9
|
+
* @param suffixLength - The number of characters to remove from the end
|
|
10
|
+
*/
|
|
11
|
+
export function removeSuffixFromHighlightedNodes(children, suffixLength) {
|
|
12
|
+
let removedLength = 0;
|
|
13
|
+
while (removedLength < suffixLength && children.length > 0) {
|
|
14
|
+
const lastChild = children[children.length - 1];
|
|
15
|
+
if (lastChild.type === 'text') {
|
|
16
|
+
const textLength = lastChild.value.length;
|
|
17
|
+
if (removedLength + textLength <= suffixLength) {
|
|
18
|
+
children.pop();
|
|
19
|
+
removedLength += textLength;
|
|
20
|
+
} else {
|
|
21
|
+
const charsToRemove = suffixLength - removedLength;
|
|
22
|
+
lastChild.value = lastChild.value.slice(0, textLength - charsToRemove);
|
|
23
|
+
removedLength = suffixLength;
|
|
24
|
+
}
|
|
25
|
+
} else if (lastChild.type === 'element') {
|
|
26
|
+
const element = lastChild;
|
|
27
|
+
if (element.children && element.children.length > 0) {
|
|
28
|
+
const lastElementChild = element.children[element.children.length - 1];
|
|
29
|
+
if (lastElementChild.type === 'text') {
|
|
30
|
+
const textLength = lastElementChild.value.length;
|
|
31
|
+
if (removedLength + textLength <= suffixLength) {
|
|
32
|
+
element.children.pop();
|
|
33
|
+
removedLength += textLength;
|
|
34
|
+
if (element.children.length === 0) {
|
|
35
|
+
children.pop();
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const charsToRemove = suffixLength - removedLength;
|
|
39
|
+
lastElementChild.value = lastElementChild.value.slice(0, textLength - charsToRemove);
|
|
40
|
+
removedLength = suffixLength;
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
children.pop();
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -3,8 +3,10 @@ import { visit } from 'unist-util-visit';
|
|
|
3
3
|
import { grammars } from "../parseSource/grammars.mjs";
|
|
4
4
|
import { extensionMap } from "../parseSource/grammarMaps.mjs";
|
|
5
5
|
import { extendSyntaxTokens } from "../parseSource/extendSyntaxTokens.mjs";
|
|
6
|
+
import { getLanguageCapabilitiesFromScope } from "../parseSource/languageCapabilities.mjs";
|
|
6
7
|
import { getHastTextContent } from "../hastUtils/index.mjs";
|
|
7
8
|
import { removePrefixFromHighlightedNodes } from "./removePrefixFromHighlightedNodes.mjs";
|
|
9
|
+
import { removeSuffixFromHighlightedNodes } from "./removeSuffixFromHighlightedNodes.mjs";
|
|
8
10
|
const STARRY_NIGHT_KEY = '__docs_infra_starry_night_instance__';
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -67,7 +69,14 @@ export default function transformHtmlCodeInline(options = {}) {
|
|
|
67
69
|
const highlightingPrefix = typeof node.properties?.dataHighlightingPrefix === 'string' ? node.properties.dataHighlightingPrefix : undefined;
|
|
68
70
|
|
|
69
71
|
// Temporarily prepend the prefix for proper syntax highlighting
|
|
70
|
-
|
|
72
|
+
let sourceToHighlight = highlightingPrefix ? `${highlightingPrefix}${source}` : source;
|
|
73
|
+
|
|
74
|
+
// Inline JS-family snippets that look like a bare object literal (e.g. `{ height: 400 }`)
|
|
75
|
+
// are tokenized by starry-night as a block statement with labeled statements, which makes
|
|
76
|
+
// the keys appear as `pl-en` (entity name) tokens rather than property names. Wrap them
|
|
77
|
+
// in `(...)` so the snippet parses as an expression and `extendSyntaxTokens` can split
|
|
78
|
+
// out the keys via `splitObjectKeys`. The wrapping characters are stripped after highlighting.
|
|
79
|
+
let objectWrap = false;
|
|
71
80
|
|
|
72
81
|
// Determine language from className (e.g., 'language-ts')
|
|
73
82
|
const className = node.properties?.className;
|
|
@@ -103,6 +112,14 @@ export default function transformHtmlCodeInline(options = {}) {
|
|
|
103
112
|
if (!fileType || !extensionMap[fileType]) {
|
|
104
113
|
return;
|
|
105
114
|
}
|
|
115
|
+
const grammarScope = extensionMap[fileType];
|
|
116
|
+
if (!highlightingPrefix && getLanguageCapabilitiesFromScope(grammarScope).semantics === 'js') {
|
|
117
|
+
const trimmed = sourceToHighlight.trim();
|
|
118
|
+
if (trimmed.length >= 2 && trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
119
|
+
sourceToHighlight = `(${sourceToHighlight})`;
|
|
120
|
+
objectWrap = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
106
123
|
|
|
107
124
|
// Apply syntax highlighting
|
|
108
125
|
const highlighted = starryNight.highlight(sourceToHighlight, extensionMap[fileType]);
|
|
@@ -116,6 +133,10 @@ export default function transformHtmlCodeInline(options = {}) {
|
|
|
116
133
|
if (highlightingPrefix && node.children.length > 0) {
|
|
117
134
|
removePrefixFromHighlightedNodes(node.children, highlightingPrefix.length);
|
|
118
135
|
}
|
|
136
|
+
if (objectWrap && node.children.length > 0) {
|
|
137
|
+
removePrefixFromHighlightedNodes(node.children, 1);
|
|
138
|
+
removeSuffixFromHighlightedNodes(node.children, 1);
|
|
139
|
+
}
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
// Mark this code element as inline highlighted (only for inline code, not pre>code)
|