@mui/internal-docs-infra 0.7.1-canary.1 → 0.7.1-canary.3
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 +3 -3
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +401 -89
- package/pipeline/enhanceCodeTypes/enhanceChildren.mjs +2 -14
- package/pipeline/loadServerTypes/typeHighlighting.mjs +2 -12
- package/pipeline/parseSource/calculateFrameRanges.d.mts +11 -2
- package/pipeline/parseSource/createFrame.mjs +2 -1
- package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +5 -14
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.7.1-canary.
|
|
3
|
+
"version": "0.7.1-canary.3",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"keywords": [
|
|
7
8
|
"react",
|
|
8
9
|
"react-component",
|
|
@@ -15,7 +16,6 @@
|
|
|
15
16
|
"url": "git+https://github.com/mui/mui-public.git",
|
|
16
17
|
"directory": "packages/docs-infra"
|
|
17
18
|
},
|
|
18
|
-
"license": "MIT",
|
|
19
19
|
"bugs": {
|
|
20
20
|
"url": "https://github.com/mui/mui-public/issues"
|
|
21
21
|
},
|
|
@@ -633,5 +633,5 @@
|
|
|
633
633
|
"bin": {
|
|
634
634
|
"docs-infra": "./cli/index.mjs"
|
|
635
635
|
},
|
|
636
|
-
"gitSha": "
|
|
636
|
+
"gitSha": "ae05209d2cf5d9188b942dee401846b821f58130"
|
|
637
637
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getHastTextContent } from "../loadServerTypes/hastTypeUtils.mjs";
|
|
1
2
|
import { calculateFrameRanges } from "../parseSource/calculateFrameRanges.mjs";
|
|
2
3
|
import { calculateFrameIndent } from "./calculateFrameIndent.mjs";
|
|
3
4
|
import { restructureFrames } from "../parseSource/restructureFrames.mjs";
|
|
@@ -20,15 +21,32 @@ export const EMPHASIS_COMMENT_PREFIX = '@highlight';
|
|
|
20
21
|
* @returns The extracted string (without quotes) or undefined if no quoted string found
|
|
21
22
|
*/
|
|
22
23
|
function extractQuotedString(content) {
|
|
23
|
-
const match = content.match(/^["'](.*)
|
|
24
|
+
const match = content.match(/^(["'])(.*)\1$/);
|
|
24
25
|
if (match) {
|
|
25
|
-
return match[
|
|
26
|
+
return match[2];
|
|
26
27
|
}
|
|
27
28
|
// Also try to find quoted string anywhere in the content
|
|
28
|
-
const anyMatch = content.match(/["']([^"']+)
|
|
29
|
-
return anyMatch?.[
|
|
29
|
+
const anyMatch = content.match(/(["'])([^"']+)\1/);
|
|
30
|
+
return anyMatch?.[2];
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Extracts all quoted strings from content.
|
|
35
|
+
* Supports both double quotes ("...") and single quotes ('...').
|
|
36
|
+
*
|
|
37
|
+
* @param content - The content to extract quoted strings from
|
|
38
|
+
* @returns Array of extracted strings (without quotes), empty if none found
|
|
39
|
+
*/
|
|
40
|
+
function extractAllQuotedStrings(content) {
|
|
41
|
+
const results = [];
|
|
42
|
+
const regex = /(["'])([^"']+)\1/g;
|
|
43
|
+
let match = regex.exec(content);
|
|
44
|
+
while (match) {
|
|
45
|
+
results.push(match[2]);
|
|
46
|
+
match = regex.exec(content);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
32
50
|
/**
|
|
33
51
|
* Extracts and removes the `@focus` keyword from content.
|
|
34
52
|
*
|
|
@@ -61,7 +79,7 @@ function extractFocus(content) {
|
|
|
61
79
|
* - Multiline start: `@highlight-start` or `@highlight-start "description"`
|
|
62
80
|
* - Multiline start focused: `@highlight-start @focus` or `@highlight-start @focus "description"`
|
|
63
81
|
* - Multiline end: `@highlight-end`
|
|
64
|
-
* - Text highlight: `@highlight-text "text to highlight"`
|
|
82
|
+
* - Text highlight: `@highlight-text "text to highlight"` or `@highlight-text "one" "two"`
|
|
65
83
|
* - Text highlight focused: `@highlight-text @focus "text to highlight"`
|
|
66
84
|
*
|
|
67
85
|
* @param comments - Source comments keyed by line number
|
|
@@ -100,18 +118,18 @@ function parseEmphasisDirectives(comments) {
|
|
|
100
118
|
focus
|
|
101
119
|
});
|
|
102
120
|
} else if (content.startsWith('-text')) {
|
|
103
|
-
// Text highlight: @highlight-text "text
|
|
121
|
+
// Text highlight: @highlight-text "text" or @highlight-text "one" "two" "three"
|
|
104
122
|
const afterText = content.slice('-text'.length).trim();
|
|
105
123
|
const {
|
|
106
124
|
focus,
|
|
107
125
|
remaining: remainingText
|
|
108
126
|
} = extractFocus(afterText);
|
|
109
|
-
const
|
|
110
|
-
if (
|
|
127
|
+
const highlightTexts = extractAllQuotedStrings(remainingText);
|
|
128
|
+
if (highlightTexts.length > 0) {
|
|
111
129
|
directives.push({
|
|
112
130
|
line,
|
|
113
131
|
type: 'text',
|
|
114
|
-
|
|
132
|
+
highlightTexts,
|
|
115
133
|
focus
|
|
116
134
|
});
|
|
117
135
|
}
|
|
@@ -179,21 +197,6 @@ function buildLineElementMap(node) {
|
|
|
179
197
|
return map;
|
|
180
198
|
}
|
|
181
199
|
|
|
182
|
-
/**
|
|
183
|
-
* Gets the text content of an element recursively.
|
|
184
|
-
*/
|
|
185
|
-
function getElementText(element) {
|
|
186
|
-
let text = '';
|
|
187
|
-
for (const child of element.children || []) {
|
|
188
|
-
if (child.type === 'text') {
|
|
189
|
-
text += child.value;
|
|
190
|
-
} else if (child.type === 'element') {
|
|
191
|
-
text += getElementText(child);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return text;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
200
|
/**
|
|
198
201
|
* Checks if a line element contains only a comment with the given text.
|
|
199
202
|
* A line is considered "comment-only" if it contains only whitespace, a .pl-c element,
|
|
@@ -222,7 +225,7 @@ function isCommentOnlyLine(lineElement, commentText) {
|
|
|
222
225
|
const classNames = Array.isArray(className) ? className : [className];
|
|
223
226
|
if (classNames.includes('pl-c')) {
|
|
224
227
|
// This is a comment element - check if it contains the expected text
|
|
225
|
-
const text =
|
|
228
|
+
const text = getHastTextContent(child);
|
|
226
229
|
if (text.includes(commentText)) {
|
|
227
230
|
hasMatchingComment = true;
|
|
228
231
|
} else {
|
|
@@ -232,14 +235,14 @@ function isCommentOnlyLine(lineElement, commentText) {
|
|
|
232
235
|
} else if (classNames.includes('pl-pse')) {
|
|
233
236
|
// This is punctuation for special expressions (JSX braces for comments)
|
|
234
237
|
// Check if it's just `{` or `}` which are used for JSX comment syntax
|
|
235
|
-
const text =
|
|
238
|
+
const text = getHastTextContent(child);
|
|
236
239
|
if (text !== '{' && text !== '}') {
|
|
237
240
|
hasNonWhitespaceContent = true;
|
|
238
241
|
}
|
|
239
242
|
// Otherwise ignore - these are just JSX comment syntax
|
|
240
243
|
} else {
|
|
241
244
|
// Non-comment element - check if it has non-whitespace content
|
|
242
|
-
const text =
|
|
245
|
+
const text = getHastTextContent(child);
|
|
243
246
|
if (text.trim() !== '') {
|
|
244
247
|
hasNonWhitespaceContent = true;
|
|
245
248
|
}
|
|
@@ -272,14 +275,21 @@ function calculateEmphasizedLines(directives, lineElements) {
|
|
|
272
275
|
description,
|
|
273
276
|
strong,
|
|
274
277
|
position: 'single',
|
|
278
|
+
lineHighlight: true,
|
|
275
279
|
focus: directive.focus
|
|
276
280
|
});
|
|
277
281
|
} else if (directive.type === 'text') {
|
|
278
|
-
// Text highlight - emphasize specific text within the line
|
|
282
|
+
// Text highlight - emphasize specific text(s) within the line.
|
|
283
|
+
// Merge with any existing entry (e.g. when @highlight and @highlight-text
|
|
284
|
+
// map to the same line after comment removal).
|
|
285
|
+
const existing = emphasizedLines.get(directive.line);
|
|
286
|
+
// Concatenate highlight texts when multiple directives target the same line.
|
|
287
|
+
const mergedTexts = existing?.highlightTexts ? [...existing.highlightTexts, ...(directive.highlightTexts ?? [])] : directive.highlightTexts;
|
|
279
288
|
emphasizedLines.set(directive.line, {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
289
|
+
...existing,
|
|
290
|
+
position: existing?.position ?? 'single',
|
|
291
|
+
highlightTexts: mergedTexts,
|
|
292
|
+
focus: directive.focus || existing?.focus
|
|
283
293
|
});
|
|
284
294
|
}
|
|
285
295
|
}
|
|
@@ -325,16 +335,22 @@ function calculateEmphasizedLines(directives, lineElements) {
|
|
|
325
335
|
// If this line is already emphasized (from an inner range), mark it as strong
|
|
326
336
|
// since it's now nested inside multiple emphasis ranges, and preserve inner positions
|
|
327
337
|
const meta = existing ? {
|
|
328
|
-
strong
|
|
329
|
-
|
|
338
|
+
// Nested ranges are strong unless the inner is a text highlight
|
|
339
|
+
strong: existing.highlightTexts ? strong : true,
|
|
330
340
|
description: existing.description ?? (line === startLine ? description : undefined),
|
|
331
|
-
|
|
332
|
-
//
|
|
341
|
+
// Inner range position takes precedence, but 'single' from a standalone
|
|
342
|
+
// @highlight-text should be replaced by the multiline range's position
|
|
343
|
+
position: existing.position && existing.position !== 'single' ? existing.position : position,
|
|
344
|
+
highlightTexts: existing.highlightTexts,
|
|
345
|
+
// Preserve text highlights from @highlight-text
|
|
346
|
+
lineHighlight: true,
|
|
347
|
+
// Inside a multiline region = always line highlight
|
|
333
348
|
focus: existing.focus || startDirective.focus
|
|
334
349
|
} : {
|
|
335
350
|
strong,
|
|
336
351
|
description: line === startLine ? description : undefined,
|
|
337
352
|
position,
|
|
353
|
+
lineHighlight: true,
|
|
338
354
|
focus: startDirective.focus
|
|
339
355
|
};
|
|
340
356
|
emphasizedLines.set(line, meta);
|
|
@@ -345,65 +361,345 @@ function calculateEmphasizedLines(directives, lineElements) {
|
|
|
345
361
|
}
|
|
346
362
|
|
|
347
363
|
/**
|
|
348
|
-
*
|
|
349
|
-
* with
|
|
364
|
+
* Like {@link getHastTextContent} but replaces any text inside a
|
|
365
|
+
* `data-hl` element with sentinel null characters so that those
|
|
366
|
+
* regions are invisible to the text search in `wrapTextInHighlightSpan`.
|
|
367
|
+
* This prevents nesting highlights when successive tokens overlap.
|
|
368
|
+
*/
|
|
369
|
+
function getSearchableText(node) {
|
|
370
|
+
if (node.type === 'text') {
|
|
371
|
+
return node.value;
|
|
372
|
+
}
|
|
373
|
+
if (node.type === 'element') {
|
|
374
|
+
if (node.properties?.dataHl !== undefined) {
|
|
375
|
+
return '\0'.repeat(getHastTextContent(node).length);
|
|
376
|
+
}
|
|
377
|
+
if (node.children) {
|
|
378
|
+
return node.children.map(getSearchableText).join('');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return '';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Injects a `data-hl` highlight span for a character range inside an
|
|
386
|
+
* element's children, without splitting the element itself.
|
|
387
|
+
* Used when a semantic element partially overlaps a match — the element
|
|
388
|
+
* stays intact and the highlight is placed inside it.
|
|
350
389
|
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
390
|
+
* Uses a plan-based approach (like {@link wrapTextInHighlightSpan}) to
|
|
391
|
+
* detect when the range produces multiple highlight fragments. When the
|
|
392
|
+
* caller already provides a `part` value, every fragment inherits it.
|
|
393
|
+
* Otherwise, if multiple fragments are detected, `data-hl-part` values
|
|
394
|
+
* (`"start"`, `"middle"`, `"end"`) are computed locally.
|
|
395
|
+
*
|
|
396
|
+
* @param children - The children of the element to modify
|
|
397
|
+
* @param from - Start offset within the element's text content
|
|
398
|
+
* @param to - End offset within the element's text content
|
|
399
|
+
* @param part - Optional `data-hl-part` value inherited from the parent
|
|
354
400
|
*/
|
|
355
|
-
function
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
401
|
+
function injectHighlightInChildren(children, from, to, part, strict, textToHighlight) {
|
|
402
|
+
const before = [];
|
|
403
|
+
const after = [];
|
|
404
|
+
const plan = [];
|
|
405
|
+
let currentGroup = [];
|
|
406
|
+
let offset = 0;
|
|
407
|
+
let pastRange = false;
|
|
408
|
+
|
|
409
|
+
// Precompute text lengths to avoid repeated recursive walks per child.
|
|
410
|
+
const childLengths = children.map(child => getHastTextContent(child).length);
|
|
411
|
+
function flushGroup() {
|
|
412
|
+
if (currentGroup.length > 0) {
|
|
413
|
+
plan.push({
|
|
414
|
+
kind: 'group',
|
|
415
|
+
nodes: currentGroup
|
|
416
|
+
});
|
|
417
|
+
currentGroup = [];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
421
|
+
const child = children[i];
|
|
422
|
+
const len = childLengths[i];
|
|
423
|
+
const childStart = offset;
|
|
424
|
+
const childEnd = offset + len;
|
|
425
|
+
offset = childEnd;
|
|
426
|
+
if (pastRange) {
|
|
427
|
+
after.push(child);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (childEnd <= from) {
|
|
431
|
+
before.push(child);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (childStart >= to) {
|
|
435
|
+
flushGroup();
|
|
436
|
+
after.push(child);
|
|
437
|
+
pastRange = true;
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (childStart >= from && childEnd <= to) {
|
|
441
|
+
currentGroup.push(child);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
372
444
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
445
|
+
// Straddling
|
|
446
|
+
const overlapFrom = Math.max(from, childStart) - childStart;
|
|
447
|
+
const overlapTo = Math.min(to, childEnd) - childStart;
|
|
448
|
+
if (child.type === 'text') {
|
|
449
|
+
const beforeText = child.value.slice(0, overlapFrom);
|
|
450
|
+
const matchedText = child.value.slice(overlapFrom, overlapTo);
|
|
451
|
+
const afterText = child.value.slice(overlapTo);
|
|
452
|
+
if (beforeText) {
|
|
453
|
+
flushGroup();
|
|
454
|
+
before.push({
|
|
455
|
+
type: 'text',
|
|
456
|
+
value: beforeText
|
|
384
457
|
});
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
458
|
+
}
|
|
459
|
+
if (matchedText) {
|
|
460
|
+
currentGroup.push({
|
|
461
|
+
type: 'text',
|
|
462
|
+
value: matchedText
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
if (afterText) {
|
|
466
|
+
flushGroup();
|
|
467
|
+
after.push({
|
|
468
|
+
type: 'text',
|
|
469
|
+
value: afterText
|
|
470
|
+
});
|
|
471
|
+
pastRange = true;
|
|
472
|
+
}
|
|
473
|
+
} else if (child.type === 'element' && child.children) {
|
|
474
|
+
// Nested element straddling — this is fragmentation across an element boundary
|
|
475
|
+
if (strict) {
|
|
476
|
+
throw new Error(`Base UI: @highlight-text "${textToHighlight}" straddles an element boundary. ` + 'In strict mode, highlighted text must not be fragmented across elements. ' + 'Adjust the highlighted text so it aligns with syntax token boundaries.');
|
|
477
|
+
}
|
|
478
|
+
flushGroup();
|
|
479
|
+
plan.push({
|
|
480
|
+
kind: 'inject',
|
|
481
|
+
element: child,
|
|
482
|
+
from: overlapFrom,
|
|
483
|
+
to: overlapTo
|
|
484
|
+
});
|
|
485
|
+
if (childEnd > to) {
|
|
486
|
+
pastRange = true;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
currentGroup.push(child);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
flushGroup();
|
|
493
|
+
|
|
494
|
+
// When the caller already supplied a part, every fragment inherits it.
|
|
495
|
+
// Otherwise, when multiple fragments exist, compute parts locally.
|
|
496
|
+
const needsParts = part === undefined && plan.length > 1;
|
|
497
|
+
const highlighted = [];
|
|
498
|
+
for (let i = 0; i < plan.length; i += 1) {
|
|
499
|
+
const item = plan[i];
|
|
500
|
+
let effectivePart = part;
|
|
501
|
+
if (needsParts) {
|
|
502
|
+
if (i === 0) {
|
|
503
|
+
effectivePart = 'start';
|
|
504
|
+
} else if (i === plan.length - 1) {
|
|
505
|
+
effectivePart = 'end';
|
|
393
506
|
} else {
|
|
394
|
-
|
|
507
|
+
effectivePart = 'middle';
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (item.kind === 'group') {
|
|
511
|
+
const props = {
|
|
512
|
+
dataHl: ''
|
|
513
|
+
};
|
|
514
|
+
if (effectivePart !== undefined) {
|
|
515
|
+
props.dataHlPart = effectivePart;
|
|
516
|
+
}
|
|
517
|
+
highlighted.push({
|
|
518
|
+
type: 'element',
|
|
519
|
+
tagName: 'span',
|
|
520
|
+
properties: props,
|
|
521
|
+
children: item.nodes
|
|
522
|
+
});
|
|
523
|
+
} else {
|
|
524
|
+
highlighted.push({
|
|
525
|
+
...item.element,
|
|
526
|
+
children: injectHighlightInChildren(item.element.children, item.from, item.to, effectivePart, strict, textToHighlight)
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return [...before, ...highlighted, ...after];
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Wraps all occurrences of a specific text within a line's children in
|
|
535
|
+
* highlight spans with `data-hl`.
|
|
536
|
+
*
|
|
537
|
+
* Semantic element nodes (syntax-highlighting spans) are never split or
|
|
538
|
+
* cloned. When a match partially overlaps an element, the highlight span
|
|
539
|
+
* is injected *inside* the element via {@link injectHighlightInChildren}.
|
|
540
|
+
* When a match covers entire elements, a single wrapper `data-hl` span
|
|
541
|
+
* groups them all.
|
|
542
|
+
*
|
|
543
|
+
* If a match is fragmented (spans a partial element boundary), each
|
|
544
|
+
* fragment gets a `data-hl-part` attribute (`"start"`, `"middle"`, or
|
|
545
|
+
* `"end"`) so the segments can be styled together (e.g. border-radius).
|
|
546
|
+
*
|
|
547
|
+
* Already-highlighted nodes (`dataHl`) are excluded from matching so that
|
|
548
|
+
* successive calls for different tokens don't nest or double-highlight.
|
|
549
|
+
*/
|
|
550
|
+
function wrapTextInHighlightSpan(children, textToHighlight, strict) {
|
|
551
|
+
// Build searchable text, masking already-highlighted regions with sentinels.
|
|
552
|
+
const segments = children.map(getSearchableText);
|
|
553
|
+
const fullText = segments.join('');
|
|
554
|
+
const matchIndex = fullText.indexOf(textToHighlight);
|
|
555
|
+
if (matchIndex === -1) {
|
|
556
|
+
return children;
|
|
557
|
+
}
|
|
558
|
+
const matchEnd = matchIndex + textToHighlight.length;
|
|
559
|
+
|
|
560
|
+
// Classify each child relative to [matchIndex, matchEnd).
|
|
561
|
+
// "group" items are fully-contained nodes wrapped in a single data-hl span.
|
|
562
|
+
// "inject" items are elements that straddle a boundary — the highlight goes
|
|
563
|
+
// inside them, preserving the semantic element.
|
|
564
|
+
|
|
565
|
+
const before = [];
|
|
566
|
+
const after = [];
|
|
567
|
+
const plan = [];
|
|
568
|
+
let currentGroup = [];
|
|
569
|
+
let offset = 0;
|
|
570
|
+
let pastMatch = false;
|
|
571
|
+
function flushGroup() {
|
|
572
|
+
if (currentGroup.length > 0) {
|
|
573
|
+
plan.push({
|
|
574
|
+
kind: 'group',
|
|
575
|
+
nodes: currentGroup
|
|
576
|
+
});
|
|
577
|
+
currentGroup = [];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
581
|
+
const child = children[i];
|
|
582
|
+
const len = segments[i].length;
|
|
583
|
+
const childStart = offset;
|
|
584
|
+
const childEnd = offset + len;
|
|
585
|
+
offset = childEnd;
|
|
586
|
+
if (pastMatch) {
|
|
587
|
+
after.push(child);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Entirely before match
|
|
592
|
+
if (childEnd <= matchIndex) {
|
|
593
|
+
before.push(child);
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Entirely after match
|
|
598
|
+
if (childStart >= matchEnd) {
|
|
599
|
+
flushGroup();
|
|
600
|
+
after.push(child);
|
|
601
|
+
pastMatch = true;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Entirely within match
|
|
606
|
+
if (childStart >= matchIndex && childEnd <= matchEnd) {
|
|
607
|
+
currentGroup.push(child);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Straddling a boundary
|
|
612
|
+
const overlapFrom = Math.max(matchIndex, childStart) - childStart;
|
|
613
|
+
const overlapTo = Math.min(matchEnd, childEnd) - childStart;
|
|
614
|
+
if (child.type === 'text') {
|
|
615
|
+
// Text nodes can be split freely — they carry no semantic class.
|
|
616
|
+
const beforeText = child.value.slice(0, overlapFrom);
|
|
617
|
+
const matchedText = child.value.slice(overlapFrom, overlapTo);
|
|
618
|
+
const afterText = child.value.slice(overlapTo);
|
|
619
|
+
if (beforeText) {
|
|
620
|
+
flushGroup();
|
|
621
|
+
before.push({
|
|
622
|
+
type: 'text',
|
|
623
|
+
value: beforeText
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
if (matchedText) {
|
|
627
|
+
currentGroup.push({
|
|
628
|
+
type: 'text',
|
|
629
|
+
value: matchedText
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
if (afterText) {
|
|
633
|
+
flushGroup();
|
|
634
|
+
after.push({
|
|
635
|
+
type: 'text',
|
|
636
|
+
value: afterText
|
|
637
|
+
});
|
|
638
|
+
pastMatch = true;
|
|
395
639
|
}
|
|
396
640
|
} else if (child.type === 'element' && child.children) {
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
641
|
+
// Element nodes are never split — inject highlight inside them.
|
|
642
|
+
flushGroup();
|
|
643
|
+
plan.push({
|
|
644
|
+
kind: 'inject',
|
|
645
|
+
element: child,
|
|
646
|
+
from: overlapFrom,
|
|
647
|
+
to: overlapTo
|
|
648
|
+
});
|
|
649
|
+
if (childEnd > matchEnd) {
|
|
650
|
+
pastMatch = true;
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
currentGroup.push(child);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
flushGroup();
|
|
657
|
+
|
|
658
|
+
// When multiple highlight pieces exist (due to element boundary straddling),
|
|
659
|
+
// mark each with data-hl-part so they can be styled as a group.
|
|
660
|
+
const needsParts = plan.length > 1;
|
|
661
|
+
if (strict && needsParts) {
|
|
662
|
+
throw new Error(`Base UI: @highlight-text "${textToHighlight}" straddles an element boundary. ` + 'In strict mode, highlighted text must not be fragmented across elements. ' + 'Adjust the highlighted text so it aligns with syntax token boundaries.');
|
|
663
|
+
}
|
|
664
|
+
const highlighted = [];
|
|
665
|
+
for (let i = 0; i < plan.length; i += 1) {
|
|
666
|
+
const item = plan[i];
|
|
667
|
+
let part;
|
|
668
|
+
if (needsParts) {
|
|
669
|
+
if (i === 0) {
|
|
670
|
+
part = 'start';
|
|
671
|
+
} else if (i === plan.length - 1) {
|
|
672
|
+
part = 'end';
|
|
673
|
+
} else {
|
|
674
|
+
part = 'middle';
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (item.kind === 'group') {
|
|
678
|
+
const props = {
|
|
679
|
+
dataHl: ''
|
|
680
|
+
};
|
|
681
|
+
if (part !== undefined) {
|
|
682
|
+
props.dataHlPart = part;
|
|
683
|
+
}
|
|
684
|
+
highlighted.push({
|
|
685
|
+
type: 'element',
|
|
686
|
+
tagName: 'span',
|
|
687
|
+
properties: props,
|
|
688
|
+
children: item.nodes
|
|
401
689
|
});
|
|
402
690
|
} else {
|
|
403
|
-
|
|
691
|
+
const injectedChildren = injectHighlightInChildren(item.element.children, item.from, item.to, part, strict, textToHighlight);
|
|
692
|
+
highlighted.push({
|
|
693
|
+
...item.element,
|
|
694
|
+
// Re-scan: the injected element may contain additional occurrences of the
|
|
695
|
+
// text beyond the just-highlighted region (e.g. repeated tokens in its tail).
|
|
696
|
+
children: wrapTextInHighlightSpan(injectedChildren, textToHighlight, strict)
|
|
697
|
+
});
|
|
404
698
|
}
|
|
405
699
|
}
|
|
406
|
-
return
|
|
700
|
+
return [...before, ...highlighted,
|
|
701
|
+
// Recursively process the remainder for additional occurrences
|
|
702
|
+
...wrapTextInHighlightSpan(after, textToHighlight, strict)];
|
|
407
703
|
}
|
|
408
704
|
|
|
409
705
|
/**
|
|
@@ -416,7 +712,7 @@ function wrapTextInHighlightSpan(children, textToHighlight) {
|
|
|
416
712
|
* @param emphasizedLines - Map of line numbers to their emphasis metadata
|
|
417
713
|
* @returns Array of line elements that are highlighted, grouped by region
|
|
418
714
|
*/
|
|
419
|
-
function applyEmphasisAndCollectHighlightedElements(node, emphasizedLines) {
|
|
715
|
+
function applyEmphasisAndCollectHighlightedElements(node, emphasizedLines, options) {
|
|
420
716
|
const highlightedLineElements = [];
|
|
421
717
|
function traverse(n) {
|
|
422
718
|
if (!('children' in n) || !n.children) {
|
|
@@ -433,10 +729,26 @@ function applyEmphasisAndCollectHighlightedElements(node, emphasizedLines) {
|
|
|
433
729
|
const lineNumber = child.properties.dataLn;
|
|
434
730
|
const meta = emphasizedLines.get(lineNumber);
|
|
435
731
|
if (meta !== undefined) {
|
|
436
|
-
if (meta.
|
|
437
|
-
// For text highlight, wrap the specific text in a span with data-hl
|
|
438
|
-
|
|
439
|
-
|
|
732
|
+
if (meta.highlightTexts) {
|
|
733
|
+
// For text highlight, wrap the specific text(s) in a span with data-hl
|
|
734
|
+
let children = child.children;
|
|
735
|
+
for (const text of meta.highlightTexts) {
|
|
736
|
+
children = wrapTextInHighlightSpan(children, text, options.strictHighlightText === true);
|
|
737
|
+
}
|
|
738
|
+
child.children = children;
|
|
739
|
+
|
|
740
|
+
// Only mark the line with data-hl when the line also has a line-level
|
|
741
|
+
// highlight (from @highlight, or from being inside a @highlight-start region).
|
|
742
|
+
// Standalone @highlight-text lines should not get line-level highlights.
|
|
743
|
+
if (meta.lineHighlight) {
|
|
744
|
+
child.properties.dataHl = meta.strong ? 'strong' : '';
|
|
745
|
+
if (meta.description) {
|
|
746
|
+
child.properties.dataHlDescription = meta.description;
|
|
747
|
+
}
|
|
748
|
+
if (meta.position) {
|
|
749
|
+
child.properties.dataHlPosition = meta.position;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
440
752
|
} else {
|
|
441
753
|
// Use data-hl with optional "strong" value on the line
|
|
442
754
|
child.properties.dataHl = meta.strong ? 'strong' : '';
|
|
@@ -585,7 +897,7 @@ export function createEnhanceCodeEmphasis(options = {}) {
|
|
|
585
897
|
}
|
|
586
898
|
|
|
587
899
|
// Step 4 (Traversal 2): Apply emphasis attributes AND collect highlighted elements
|
|
588
|
-
const highlightedElements = applyEmphasisAndCollectHighlightedElements(root, emphasizedLines);
|
|
900
|
+
const highlightedElements = applyEmphasisAndCollectHighlightedElements(root, emphasizedLines, options);
|
|
589
901
|
|
|
590
902
|
// Step 5: Calculate indent levels per region (uses collected elements, no tree traversal)
|
|
591
903
|
const regionIndentLevels = calculateRegionIndentLevels(highlightedElements, emphasizedLines);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getHastTextContent } from "../loadServerTypes/hastTypeUtils.mjs";
|
|
1
2
|
import { isLinkableSpan, isPropertySpan, isKeywordSpan, isCommentSpan, isSmiSpan, isStringLiteralSpan, getTextContent, getClassName, propPathToString } from "./hastUtils.mjs";
|
|
2
3
|
import { createLinkElement, createPropRefElement, createParamRefElement, createValueRefElement } from "./createElements.mjs";
|
|
3
4
|
import { currentOwner, buildPropHref, buildParamHref, finalizePendingDefaultExport, getResolvedValueExportAt, resetImportState, resetExportState, recordExport } from "./scanState.mjs";
|
|
@@ -1506,19 +1507,6 @@ function extractStringLiteralValue(node) {
|
|
|
1506
1507
|
return `${quote}${raw}${quote}`;
|
|
1507
1508
|
}
|
|
1508
1509
|
|
|
1509
|
-
/**
|
|
1510
|
-
* Recursively extracts all text content from an element, including nested elements.
|
|
1511
|
-
*/
|
|
1512
|
-
function getDeepTextContent(node) {
|
|
1513
|
-
if (node.type === 'text') {
|
|
1514
|
-
return node.value;
|
|
1515
|
-
}
|
|
1516
|
-
if (node.type === 'element') {
|
|
1517
|
-
return node.children.map(getDeepTextContent).join('');
|
|
1518
|
-
}
|
|
1519
|
-
return '';
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
1510
|
/**
|
|
1523
1511
|
* Checks whether a pl-s span is a template literal with interpolations.
|
|
1524
1512
|
* Looks for a backtick pl-pds delimiter and pl-pse interpolation boundaries.
|
|
@@ -1601,7 +1589,7 @@ function extractTemplateLiteralTokens(node, state) {
|
|
|
1601
1589
|
|
|
1602
1590
|
// Interpolation boundary: pl-pse wrapping `${` or `}`
|
|
1603
1591
|
if (cls.includes('pl-pse')) {
|
|
1604
|
-
const delimText =
|
|
1592
|
+
const delimText = getHastTextContent(child);
|
|
1605
1593
|
if (delimText === '${') {
|
|
1606
1594
|
inInterpolation = true;
|
|
1607
1595
|
interpolationContentCount = 0;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { unified } from 'unified';
|
|
10
10
|
import transformHtmlCodeInline from "../transformHtmlCodeInline/index.mjs";
|
|
11
11
|
import { starryNightGutter } from "../parseSource/addLineGutters.mjs";
|
|
12
|
+
import { getHastTextContent } from "./hastTypeUtils.mjs";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Options for formatting inline types as HAST.
|
|
@@ -42,17 +43,6 @@ export function formatMultilineUnionHast(hast) {
|
|
|
42
43
|
if (!codeElement || codeElement.type !== 'element') {
|
|
43
44
|
return hast;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
-
// Helper to get text content from a node (needed for depth tracking)
|
|
47
|
-
const getTextContent = node => {
|
|
48
|
-
if (node.type === 'text') {
|
|
49
|
-
return node.value || '';
|
|
50
|
-
}
|
|
51
|
-
if (node.children) {
|
|
52
|
-
return node.children.map(getTextContent).join('');
|
|
53
|
-
}
|
|
54
|
-
return '';
|
|
55
|
-
};
|
|
56
46
|
const children = codeElement.children || [];
|
|
57
47
|
|
|
58
48
|
// Group children by top-level pipes (matching TableCode.tsx behavior)
|
|
@@ -62,7 +52,7 @@ export function formatMultilineUnionHast(hast) {
|
|
|
62
52
|
let angleDepth = 0;
|
|
63
53
|
let groupIndex = 0;
|
|
64
54
|
children.forEach((child, index) => {
|
|
65
|
-
const nodeText =
|
|
55
|
+
const nodeText = getHastTextContent(child);
|
|
66
56
|
|
|
67
57
|
// Track depth changes
|
|
68
58
|
for (const char of nodeText) {
|
|
@@ -8,10 +8,12 @@ export interface EmphasisMeta {
|
|
|
8
8
|
position?: 'single' | 'start' | 'end';
|
|
9
9
|
/** Whether this is a strong emphasis (description ended with !) */
|
|
10
10
|
strong?: boolean;
|
|
11
|
-
/** For text highlighting: the specific
|
|
12
|
-
|
|
11
|
+
/** For text highlighting: the specific texts to highlight within the line */
|
|
12
|
+
highlightTexts?: string[];
|
|
13
13
|
/** Whether this line's region is the focused region (for padding) */
|
|
14
14
|
focus?: boolean;
|
|
15
|
+
/** Whether the line itself should receive data-hl (from @highlight or multiline region) */
|
|
16
|
+
lineHighlight?: boolean;
|
|
15
17
|
}
|
|
16
18
|
/**
|
|
17
19
|
* A range of lines that forms a frame in the output.
|
|
@@ -41,6 +43,13 @@ export interface EnhanceCodeEmphasisOptions {
|
|
|
41
43
|
* padding-top and ceil(remainder/2) for padding-bottom.
|
|
42
44
|
*/
|
|
43
45
|
focusFramesMaxSize?: number;
|
|
46
|
+
/**
|
|
47
|
+
* When `true`, throws an error if a `@highlight-text` match has to be
|
|
48
|
+
* fragmented across element boundaries (producing `data-hl-part` spans).
|
|
49
|
+
* Wrapping multiple complete elements in a single `data-hl` span is still
|
|
50
|
+
* allowed — only boundary-straddling matches are rejected.
|
|
51
|
+
*/
|
|
52
|
+
strictHighlightText?: boolean;
|
|
44
53
|
}
|
|
45
54
|
/**
|
|
46
55
|
* Calculates frame ranges for the code block based on emphasized lines.
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export function createFrame(children, startLine, endLine, frameType, indentLevel) {
|
|
8
8
|
const properties = {
|
|
9
|
-
className: 'frame'
|
|
9
|
+
className: 'frame',
|
|
10
|
+
dataLined: ''
|
|
10
11
|
};
|
|
11
12
|
if (startLine !== undefined && endLine !== undefined) {
|
|
12
13
|
properties.dataFrameStartLine = startLine;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { visit } from 'unist-util-visit';
|
|
2
|
+
import { getHastTextContent } from "../loadServerTypes/hastTypeUtils.mjs";
|
|
2
3
|
import { loadCodeVariant } from "../loadCodeVariant/loadCodeVariant.mjs";
|
|
3
4
|
import { createParseSource } from "../parseSource/index.mjs";
|
|
4
5
|
import { TypescriptToJavascriptTransformer } from "../transformTypescriptToJavascript/index.mjs";
|
|
@@ -99,25 +100,15 @@ const JSX_LANGUAGES = new Set(['jsx', 'tsx']);
|
|
|
99
100
|
* from MDX parsing. If the source ends with `>;`, the trailing `;` is removed.
|
|
100
101
|
*/
|
|
101
102
|
function stripJsxExpressionSemicolon(source) {
|
|
103
|
+
if (source.endsWith('>;\n')) {
|
|
104
|
+
return source.slice(0, -2);
|
|
105
|
+
}
|
|
102
106
|
if (source.endsWith('>;')) {
|
|
103
107
|
return source.slice(0, -1);
|
|
104
108
|
}
|
|
105
109
|
return source;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
/**
|
|
109
|
-
* Extracts text content from HAST nodes
|
|
110
|
-
*/
|
|
111
|
-
function extractTextContent(node) {
|
|
112
|
-
if (node.type === 'text') {
|
|
113
|
-
return node.value;
|
|
114
|
-
}
|
|
115
|
-
if (node.type === 'element' && node.children) {
|
|
116
|
-
return node.children.map(child => extractTextContent(child)).join('');
|
|
117
|
-
}
|
|
118
|
-
return '';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
112
|
/**
|
|
122
113
|
* Extracts code elements and filenames from semantic HTML structure
|
|
123
114
|
* Handles both section/figure/dl and standalone dl structures
|
|
@@ -254,7 +245,7 @@ export const transformHtmlCodeBlock = () => {
|
|
|
254
245
|
|
|
255
246
|
// Process each extracted element to extract comments and prepare variants
|
|
256
247
|
const processElementForVariant = async (codeElement, filename, language, explicitVariantName, index) => {
|
|
257
|
-
let sourceCode =
|
|
248
|
+
let sourceCode = getHastTextContent(codeElement);
|
|
258
249
|
const derivedFilename = filename || getFileName(codeElement);
|
|
259
250
|
|
|
260
251
|
// Strip trailing semicolon from JSX expressions
|