@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mui/internal-docs-infra",
3
- "version": "0.11.1-canary.15",
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": "6ad5e4fa528c6617251224a57814890fdf1c4389"
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
- htmlInsideTag = true;
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
- if (htmlInsideTag && savedSpanFlag) {
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
- // Split text around = and wrap in di-ae span
260
- const before = value.slice(0, equalsIndex);
261
- const after = value.slice(equalsIndex + 1);
262
- const equalsSpan = {
263
- type: 'element',
264
- tagName: 'span',
265
- properties: {
266
- className: ['di-ae']
267
- },
268
- children: [{
269
- type: 'text',
270
- value: '='
271
- }]
272
- };
273
- const newNodes = [];
274
- if (before) {
275
- newNodes.push({
276
- type: 'text',
277
- value: before
278
- });
279
- }
280
- newNodes.push(equalsSpan);
281
- if (after) {
282
- newNodes.push({
283
- type: 'text',
284
- value: after
285
- });
286
- }
287
- children.splice(index, 1, ...newNodes);
288
- index += newNodes.length - 1;
289
- hasSpanSinceLastText = newNodes[newNodes.length - 1].type === 'element';
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
- addClass(child, 'di-jsx');
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
- const sourceToHighlight = highlightingPrefix ? `${highlightingPrefix}${source}` : source;
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)