@mui/internal-docs-infra 0.5.1-canary.0 → 0.6.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/index.mjs +1 -1
- package/package.json +12 -2
- package/pipeline/enhanceCodeEmphasis/calculateFrameIndent.d.mts +12 -0
- package/pipeline/enhanceCodeEmphasis/calculateFrameIndent.mjs +62 -0
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.d.mts +27 -6
- package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +210 -65
- package/pipeline/parseSource/addLineGutters.mjs +4 -19
- package/pipeline/parseSource/calculateFrameRanges.d.mts +59 -0
- package/pipeline/parseSource/calculateFrameRanges.mjs +209 -0
- package/pipeline/parseSource/createFrame.d.mts +9 -0
- package/pipeline/parseSource/createFrame.mjs +29 -0
- package/pipeline/parseSource/restructureFrames.d.mts +15 -0
- package/pipeline/parseSource/restructureFrames.mjs +100 -0
- package/useCode/Pre.mjs +2 -0
package/cli/index.mjs
CHANGED
|
@@ -7,4 +7,4 @@ function getVersion() {
|
|
|
7
7
|
}
|
|
8
8
|
yargs().scriptName('docs-infra').usage('$0 <command> [args]').command(runValidate).demandCommand(1, 'You need at least one command before moving on').strict().help()
|
|
9
9
|
// MUI_VERSION is set through the code-infra build command.
|
|
10
|
-
.version("0.
|
|
10
|
+
.version("0.6.0" || getVersion()).parse(hideBin(process.argv));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1-canary.0",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
6
|
"keywords": [
|
|
@@ -452,6 +452,16 @@
|
|
|
452
452
|
"default": "./pipeline/loadServerSource/index.mjs"
|
|
453
453
|
}
|
|
454
454
|
},
|
|
455
|
+
"./pipeline/enhanceCodeEmphasis": {
|
|
456
|
+
"import": {
|
|
457
|
+
"types": "./pipeline/enhanceCodeEmphasis/index.d.mts",
|
|
458
|
+
"default": "./pipeline/enhanceCodeEmphasis/index.mjs"
|
|
459
|
+
},
|
|
460
|
+
"default": {
|
|
461
|
+
"types": "./pipeline/enhanceCodeEmphasis/index.d.mts",
|
|
462
|
+
"default": "./pipeline/enhanceCodeEmphasis/index.mjs"
|
|
463
|
+
}
|
|
464
|
+
},
|
|
455
465
|
"./pipeline/parseSource": {
|
|
456
466
|
"import": {
|
|
457
467
|
"types": "./pipeline/parseSource/index.d.mts",
|
|
@@ -506,5 +516,5 @@
|
|
|
506
516
|
"bin": {
|
|
507
517
|
"docs-infra": "./cli/index.mjs"
|
|
508
518
|
},
|
|
509
|
-
"gitSha": "
|
|
519
|
+
"gitSha": "cad146f1682e0cb63baeeab28454423d77ef7c89"
|
|
510
520
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Element } from 'hast';
|
|
2
|
+
/**
|
|
3
|
+
* Calculates the shared indent level for a set of line elements.
|
|
4
|
+
*
|
|
5
|
+
* Finds the minimum leading whitespace across all non-empty lines,
|
|
6
|
+
* then divides by the indent size (2 spaces) and floors to get
|
|
7
|
+
* the indent level.
|
|
8
|
+
*
|
|
9
|
+
* @param lineElements - Array of HAST line elements to analyze
|
|
10
|
+
* @returns The shared indent level (e.g., 2 for 4 leading spaces with 2-space indent)
|
|
11
|
+
*/
|
|
12
|
+
export declare function calculateFrameIndent(lineElements: Element[]): number;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const INDENT_SIZE = 2;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Counts leading spaces in an element by walking the HAST tree.
|
|
5
|
+
*
|
|
6
|
+
* Returns the number of leading space characters before the first
|
|
7
|
+
* non-space character, or -1 if the line is empty/whitespace-only.
|
|
8
|
+
* Only counts space characters. Tab indentation is not supported
|
|
9
|
+
* since the input is HAST output from starry-night which uses spaces.
|
|
10
|
+
*/
|
|
11
|
+
function countLeadingSpaces(element) {
|
|
12
|
+
let spaces = 0;
|
|
13
|
+
function walk(node) {
|
|
14
|
+
for (const child of node.children) {
|
|
15
|
+
if (child.type === 'text') {
|
|
16
|
+
for (const char of child.value) {
|
|
17
|
+
if (char === ' ') {
|
|
18
|
+
spaces += 1;
|
|
19
|
+
} else {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
} else if (child.type === 'element') {
|
|
24
|
+
if (walk(child)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const foundNonSpace = walk(element);
|
|
32
|
+
return foundNonSpace ? spaces : -1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Calculates the shared indent level for a set of line elements.
|
|
37
|
+
*
|
|
38
|
+
* Finds the minimum leading whitespace across all non-empty lines,
|
|
39
|
+
* then divides by the indent size (2 spaces) and floors to get
|
|
40
|
+
* the indent level.
|
|
41
|
+
*
|
|
42
|
+
* @param lineElements - Array of HAST line elements to analyze
|
|
43
|
+
* @returns The shared indent level (e.g., 2 for 4 leading spaces with 2-space indent)
|
|
44
|
+
*/
|
|
45
|
+
export function calculateFrameIndent(lineElements) {
|
|
46
|
+
let minLeadingSpaces = Infinity;
|
|
47
|
+
for (const element of lineElements) {
|
|
48
|
+
const leadingSpaces = countLeadingSpaces(element);
|
|
49
|
+
|
|
50
|
+
// Skip empty/whitespace-only lines
|
|
51
|
+
if (leadingSpaces === -1) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (leadingSpaces < minLeadingSpaces) {
|
|
55
|
+
minLeadingSpaces = leadingSpaces;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (minLeadingSpaces === Infinity) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
return Math.floor(minLeadingSpaces / INDENT_SIZE);
|
|
62
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { SourceEnhancer } from "../../CodeHighlighter/types.mjs";
|
|
2
|
+
import type { EnhanceCodeEmphasisOptions } from "../parseSource/calculateFrameRanges.mjs";
|
|
3
|
+
export type { EmphasisMeta, EnhanceCodeEmphasisOptions, FrameRange } from "../parseSource/calculateFrameRanges.mjs";
|
|
2
4
|
/**
|
|
3
5
|
* The prefix used to identify emphasis comments in source code.
|
|
4
6
|
* Comments starting with this prefix will be processed for emphasis.
|
|
5
7
|
*/
|
|
6
8
|
export declare const EMPHASIS_COMMENT_PREFIX = "@highlight";
|
|
7
9
|
/**
|
|
8
|
-
*
|
|
10
|
+
* Creates a source enhancer that adds emphasis to code lines based on `@highlight` comments
|
|
11
|
+
* and restructures frames around highlighted regions.
|
|
9
12
|
*
|
|
10
|
-
* Supports
|
|
13
|
+
* Supports five patterns:
|
|
11
14
|
*
|
|
12
15
|
* 1. **Single line emphasis** - emphasizes the line containing the comment:
|
|
13
16
|
* ```jsx
|
|
@@ -37,12 +40,30 @@ export declare const EMPHASIS_COMMENT_PREFIX = "@highlight";
|
|
|
37
40
|
* <h1>Heading 1</h1> {/* @highlight-text "Heading 1" *\/}
|
|
38
41
|
* ```
|
|
39
42
|
*
|
|
43
|
+
* 5. **Focus override** - mark a region for padding focus:
|
|
44
|
+
* ```jsx
|
|
45
|
+
* <h1>Heading 1</h1> {/* @highlight @focus *\/}
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
40
48
|
* Emphasized lines receive a `data-hl` attribute on their `<span class="line">` element.
|
|
49
|
+
* When highlights exist, frames are restructured with `data-frame-type` attributes
|
|
50
|
+
* (`highlighted`, `padding-top`, `padding-bottom`, or omitted for normal).
|
|
51
|
+
* Highlighted frames also receive `data-frame-indent` with the shared indent level.
|
|
52
|
+
*
|
|
53
|
+
* @param options - Optional configuration for padding frames
|
|
54
|
+
* @returns A `SourceEnhancer` function
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import { createEnhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
|
|
41
59
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
* const enhancers = [createEnhanceCodeEmphasis({ paddingFrameMaxSize: 5, focusFramesMaxSize: 8 })];
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createEnhanceCodeEmphasis(options?: EnhanceCodeEmphasisOptions): SourceEnhancer;
|
|
64
|
+
/**
|
|
65
|
+
* Default source enhancer that adds emphasis to code lines based on `@highlight` comments.
|
|
66
|
+
* Uses no padding frames by default. Use `createEnhanceCodeEmphasis` for configurable padding.
|
|
46
67
|
*
|
|
47
68
|
* @example
|
|
48
69
|
* ```ts
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { calculateFrameRanges } from "../parseSource/calculateFrameRanges.mjs";
|
|
2
|
+
import { calculateFrameIndent } from "./calculateFrameIndent.mjs";
|
|
3
|
+
import { restructureFrames } from "../parseSource/restructureFrames.mjs";
|
|
1
4
|
/**
|
|
2
5
|
* The prefix used to identify emphasis comments in source code.
|
|
3
6
|
* Comments starting with this prefix will be processed for emphasis.
|
|
@@ -8,19 +11,15 @@ export const EMPHASIS_COMMENT_PREFIX = '@highlight';
|
|
|
8
11
|
* Parsed emphasis directive from a comment.
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
* Metadata for an emphasized line.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
14
|
/**
|
|
16
15
|
* Extracts a quoted string from content.
|
|
17
16
|
* Supports both double quotes ("...") and single quotes ('...').
|
|
17
|
+
* Escaped quotes within the string are not supported.
|
|
18
18
|
*
|
|
19
19
|
* @param content - The content to extract the quoted string from
|
|
20
20
|
* @returns The extracted string (without quotes) or undefined if no quoted string found
|
|
21
21
|
*/
|
|
22
22
|
function extractQuotedString(content) {
|
|
23
|
-
// Match either double-quoted or single-quoted string
|
|
24
23
|
const match = content.match(/^["'](.*)["']$/);
|
|
25
24
|
if (match) {
|
|
26
25
|
return match[1];
|
|
@@ -30,14 +29,40 @@ function extractQuotedString(content) {
|
|
|
30
29
|
return anyMatch?.[1];
|
|
31
30
|
}
|
|
32
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Extracts and removes the `@focus` keyword from content.
|
|
34
|
+
*
|
|
35
|
+
* @param content - The content to check for `@focus`
|
|
36
|
+
* @returns An object with `focus` boolean and the `remaining` content with `@focus` removed
|
|
37
|
+
*/
|
|
38
|
+
function extractFocus(content) {
|
|
39
|
+
// Match @focus only as a standalone token (not inside quotes)
|
|
40
|
+
const match = content.match(/(^|\s)@focus(\s|$)/);
|
|
41
|
+
if (!match) {
|
|
42
|
+
return {
|
|
43
|
+
focus: false,
|
|
44
|
+
remaining: content
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const start = match.index + match[1].length;
|
|
48
|
+
const remaining = (content.slice(0, start) + content.slice(start + '@focus'.length)).trim();
|
|
49
|
+
return {
|
|
50
|
+
focus: true,
|
|
51
|
+
remaining
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
/**
|
|
34
56
|
* Parses emphasis comments and returns structured directives.
|
|
35
57
|
*
|
|
36
58
|
* Supported formats:
|
|
37
59
|
* - Single line: `@highlight` or `@highlight "description"`
|
|
60
|
+
* - Single line focused: `@highlight @focus` or `@highlight @focus "description"`
|
|
38
61
|
* - Multiline start: `@highlight-start` or `@highlight-start "description"`
|
|
62
|
+
* - Multiline start focused: `@highlight-start @focus` or `@highlight-start @focus "description"`
|
|
39
63
|
* - Multiline end: `@highlight-end`
|
|
40
64
|
* - Text highlight: `@highlight-text "text to highlight"`
|
|
65
|
+
* - Text highlight focused: `@highlight-text @focus "text to highlight"`
|
|
41
66
|
*
|
|
42
67
|
* @param comments - Source comments keyed by line number
|
|
43
68
|
* @returns Array of parsed emphasis directives
|
|
@@ -63,31 +88,46 @@ function parseEmphasisDirectives(comments) {
|
|
|
63
88
|
} else if (content.startsWith('-start')) {
|
|
64
89
|
// Start of multiline emphasis: @highlight-start or @highlight-start "description"
|
|
65
90
|
const afterStart = content.slice('-start'.length).trim();
|
|
66
|
-
const
|
|
91
|
+
const {
|
|
92
|
+
focus,
|
|
93
|
+
remaining: remainingStart
|
|
94
|
+
} = extractFocus(afterStart);
|
|
95
|
+
const description = extractQuotedString(remainingStart);
|
|
67
96
|
directives.push({
|
|
68
97
|
line,
|
|
69
98
|
type: 'start',
|
|
70
|
-
description
|
|
99
|
+
description,
|
|
100
|
+
focus
|
|
71
101
|
});
|
|
72
102
|
} else if (content.startsWith('-text')) {
|
|
73
103
|
// Text highlight: @highlight-text "text to highlight"
|
|
74
104
|
const afterText = content.slice('-text'.length).trim();
|
|
75
|
-
const
|
|
105
|
+
const {
|
|
106
|
+
focus,
|
|
107
|
+
remaining: remainingText
|
|
108
|
+
} = extractFocus(afterText);
|
|
109
|
+
const highlightText = extractQuotedString(remainingText);
|
|
76
110
|
if (highlightText) {
|
|
77
111
|
directives.push({
|
|
78
112
|
line,
|
|
79
113
|
type: 'text',
|
|
80
|
-
highlightText
|
|
114
|
+
highlightText,
|
|
115
|
+
focus
|
|
81
116
|
});
|
|
82
117
|
}
|
|
83
118
|
} else {
|
|
84
119
|
// Single line emphasis: @highlight or @highlight "description"
|
|
85
120
|
const afterHighlight = content.trim();
|
|
86
|
-
const
|
|
121
|
+
const {
|
|
122
|
+
focus,
|
|
123
|
+
remaining: remainingSingle
|
|
124
|
+
} = extractFocus(afterHighlight);
|
|
125
|
+
const description = extractQuotedString(remainingSingle) || undefined;
|
|
87
126
|
directives.push({
|
|
88
127
|
line,
|
|
89
128
|
type: 'single',
|
|
90
|
-
description
|
|
129
|
+
description,
|
|
130
|
+
focus
|
|
91
131
|
});
|
|
92
132
|
}
|
|
93
133
|
}
|
|
@@ -231,13 +271,15 @@ function calculateEmphasizedLines(directives, lineElements) {
|
|
|
231
271
|
emphasizedLines.set(directive.line, {
|
|
232
272
|
description,
|
|
233
273
|
strong,
|
|
234
|
-
position: 'single'
|
|
274
|
+
position: 'single',
|
|
275
|
+
focus: directive.focus
|
|
235
276
|
});
|
|
236
277
|
} else if (directive.type === 'text') {
|
|
237
278
|
// Text highlight - emphasize specific text within the line
|
|
238
279
|
emphasizedLines.set(directive.line, {
|
|
239
280
|
position: 'single',
|
|
240
|
-
highlightText: directive.highlightText
|
|
281
|
+
highlightText: directive.highlightText,
|
|
282
|
+
focus: directive.focus
|
|
241
283
|
});
|
|
242
284
|
}
|
|
243
285
|
}
|
|
@@ -286,11 +328,14 @@ function calculateEmphasizedLines(directives, lineElements) {
|
|
|
286
328
|
strong: true,
|
|
287
329
|
// Nested = always strong
|
|
288
330
|
description: existing.description ?? (line === startLine ? description : undefined),
|
|
289
|
-
position: existing.position ?? position
|
|
331
|
+
position: existing.position ?? position,
|
|
332
|
+
// Inner range position takes precedence
|
|
333
|
+
focus: existing.focus || startDirective.focus
|
|
290
334
|
} : {
|
|
291
335
|
strong,
|
|
292
336
|
description: line === startLine ? description : undefined,
|
|
293
|
-
position
|
|
337
|
+
position,
|
|
338
|
+
focus: startDirective.focus
|
|
294
339
|
};
|
|
295
340
|
emphasizedLines.set(line, meta);
|
|
296
341
|
}
|
|
@@ -362,52 +407,113 @@ function wrapTextInHighlightSpan(children, textToHighlight) {
|
|
|
362
407
|
}
|
|
363
408
|
|
|
364
409
|
/**
|
|
365
|
-
*
|
|
410
|
+
* Single-pass traversal that applies emphasis attributes to line elements
|
|
411
|
+
* AND collects leading whitespace for indent calculation on highlighted lines.
|
|
412
|
+
*
|
|
413
|
+
* This merges what would otherwise be two separate traversals into one.
|
|
366
414
|
*
|
|
367
415
|
* @param node - The node to process
|
|
368
416
|
* @param emphasizedLines - Map of line numbers to their emphasis metadata
|
|
417
|
+
* @returns Array of line elements that are highlighted, grouped by region
|
|
369
418
|
*/
|
|
370
|
-
function
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const child = node.children[i];
|
|
376
|
-
if (child.type !== 'element') {
|
|
377
|
-
continue;
|
|
419
|
+
function applyEmphasisAndCollectHighlightedElements(node, emphasizedLines) {
|
|
420
|
+
const highlightedLineElements = [];
|
|
421
|
+
function traverse(n) {
|
|
422
|
+
if (!('children' in n) || !n.children) {
|
|
423
|
+
return;
|
|
378
424
|
}
|
|
425
|
+
for (let i = 0; i < n.children.length; i += 1) {
|
|
426
|
+
const child = n.children[i];
|
|
427
|
+
if (child.type !== 'element') {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
379
430
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
431
|
+
// Check if this is a line element
|
|
432
|
+
if (child.tagName === 'span' && child.properties?.className === 'line' && typeof child.properties.dataLn === 'number') {
|
|
433
|
+
const lineNumber = child.properties.dataLn;
|
|
434
|
+
const meta = emphasizedLines.get(lineNumber);
|
|
435
|
+
if (meta !== undefined) {
|
|
436
|
+
if (meta.highlightText) {
|
|
437
|
+
// For text highlight, wrap the specific text in a span with data-hl
|
|
438
|
+
// Don't add data-hl to the line itself
|
|
439
|
+
child.children = wrapTextInHighlightSpan(child.children, meta.highlightText);
|
|
440
|
+
} else {
|
|
441
|
+
// Use data-hl with optional "strong" value on the line
|
|
442
|
+
child.properties.dataHl = meta.strong ? 'strong' : '';
|
|
443
|
+
if (meta.description) {
|
|
444
|
+
child.properties.dataHlDescription = meta.description;
|
|
445
|
+
}
|
|
446
|
+
if (meta.position) {
|
|
447
|
+
child.properties.dataHlPosition = meta.position;
|
|
448
|
+
}
|
|
397
449
|
}
|
|
450
|
+
|
|
451
|
+
// Collect this line element for indent calculation
|
|
452
|
+
highlightedLineElements.push(child);
|
|
398
453
|
}
|
|
399
454
|
}
|
|
455
|
+
|
|
456
|
+
// Recurse into children (for frames containing lines)
|
|
457
|
+
traverse(child);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
traverse(node);
|
|
461
|
+
return highlightedLineElements;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Groups highlighted line elements by their highlight regions and calculates
|
|
466
|
+
* the indent level for each region.
|
|
467
|
+
*
|
|
468
|
+
* @param highlightedElements - Line elements that are highlighted, in order
|
|
469
|
+
* @param emphasizedLines - The emphasis metadata map
|
|
470
|
+
* @returns Map from region index to indent level
|
|
471
|
+
*/
|
|
472
|
+
function calculateRegionIndentLevels(highlightedElements, emphasizedLines) {
|
|
473
|
+
const regionIndentLevels = new Map();
|
|
474
|
+
if (highlightedElements.length === 0) {
|
|
475
|
+
return regionIndentLevels;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Group elements by consecutive regions
|
|
479
|
+
const sortedLines = Array.from(emphasizedLines.keys()).sort((a, b) => a - b);
|
|
480
|
+
let regionIndex = 0;
|
|
481
|
+
let regionElements = [];
|
|
482
|
+
let prevLine = -1;
|
|
483
|
+
|
|
484
|
+
// Build a quick lookup from lineNumber to element
|
|
485
|
+
const elementByLine = new Map();
|
|
486
|
+
for (const el of highlightedElements) {
|
|
487
|
+
const ln = el.properties?.dataLn;
|
|
488
|
+
elementByLine.set(ln, el);
|
|
489
|
+
}
|
|
490
|
+
for (const line of sortedLines) {
|
|
491
|
+
const el = elementByLine.get(line);
|
|
492
|
+
if (!el) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (prevLine >= 0 && line !== prevLine + 1) {
|
|
496
|
+
// Gap: close current region
|
|
497
|
+
regionIndentLevels.set(regionIndex, calculateFrameIndent(regionElements));
|
|
498
|
+
regionIndex += 1;
|
|
499
|
+
regionElements = [];
|
|
400
500
|
}
|
|
501
|
+
regionElements.push(el);
|
|
502
|
+
prevLine = line;
|
|
503
|
+
}
|
|
401
504
|
|
|
402
|
-
|
|
403
|
-
|
|
505
|
+
// Close the last region
|
|
506
|
+
if (regionElements.length > 0) {
|
|
507
|
+
regionIndentLevels.set(regionIndex, calculateFrameIndent(regionElements));
|
|
404
508
|
}
|
|
509
|
+
return regionIndentLevels;
|
|
405
510
|
}
|
|
406
511
|
|
|
407
512
|
/**
|
|
408
|
-
*
|
|
513
|
+
* Creates a source enhancer that adds emphasis to code lines based on `@highlight` comments
|
|
514
|
+
* and restructures frames around highlighted regions.
|
|
409
515
|
*
|
|
410
|
-
* Supports
|
|
516
|
+
* Supports five patterns:
|
|
411
517
|
*
|
|
412
518
|
* 1. **Single line emphasis** - emphasizes the line containing the comment:
|
|
413
519
|
* ```jsx
|
|
@@ -437,12 +543,66 @@ function addEmphasisToLines(node, emphasizedLines) {
|
|
|
437
543
|
* <h1>Heading 1</h1> {/* @highlight-text "Heading 1" *\/}
|
|
438
544
|
* ```
|
|
439
545
|
*
|
|
546
|
+
* 5. **Focus override** - mark a region for padding focus:
|
|
547
|
+
* ```jsx
|
|
548
|
+
* <h1>Heading 1</h1> {/* @highlight @focus *\/}
|
|
549
|
+
* ```
|
|
550
|
+
*
|
|
440
551
|
* Emphasized lines receive a `data-hl` attribute on their `<span class="line">` element.
|
|
552
|
+
* When highlights exist, frames are restructured with `data-frame-type` attributes
|
|
553
|
+
* (`highlighted`, `padding-top`, `padding-bottom`, or omitted for normal).
|
|
554
|
+
* Highlighted frames also receive `data-frame-indent` with the shared indent level.
|
|
441
555
|
*
|
|
442
|
-
* @param
|
|
443
|
-
* @
|
|
444
|
-
*
|
|
445
|
-
* @
|
|
556
|
+
* @param options - Optional configuration for padding frames
|
|
557
|
+
* @returns A `SourceEnhancer` function
|
|
558
|
+
*
|
|
559
|
+
* @example
|
|
560
|
+
* ```ts
|
|
561
|
+
* import { createEnhanceCodeEmphasis } from '@mui/internal-docs-infra/pipeline/enhanceCodeEmphasis';
|
|
562
|
+
*
|
|
563
|
+
* const enhancers = [createEnhanceCodeEmphasis({ paddingFrameMaxSize: 5, focusFramesMaxSize: 8 })];
|
|
564
|
+
* ```
|
|
565
|
+
*/
|
|
566
|
+
export function createEnhanceCodeEmphasis(options = {}) {
|
|
567
|
+
return (root, comments) => {
|
|
568
|
+
if (!comments || Object.keys(comments).length === 0) {
|
|
569
|
+
return root;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Step 1: Parse directives from comments (no tree traversal)
|
|
573
|
+
const directives = parseEmphasisDirectives(comments);
|
|
574
|
+
if (directives.length === 0) {
|
|
575
|
+
return root;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Step 2 (Traversal 1): Build line element map
|
|
579
|
+
const lineElements = buildLineElementMap(root);
|
|
580
|
+
|
|
581
|
+
// Step 3: Calculate which lines are emphasized (no tree traversal)
|
|
582
|
+
const emphasizedLines = calculateEmphasizedLines(directives, lineElements);
|
|
583
|
+
if (emphasizedLines.size === 0) {
|
|
584
|
+
return root;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Step 4 (Traversal 2): Apply emphasis attributes AND collect highlighted elements
|
|
588
|
+
const highlightedElements = applyEmphasisAndCollectHighlightedElements(root, emphasizedLines);
|
|
589
|
+
|
|
590
|
+
// Step 5: Calculate indent levels per region (uses collected elements, no tree traversal)
|
|
591
|
+
const regionIndentLevels = calculateRegionIndentLevels(highlightedElements, emphasizedLines);
|
|
592
|
+
|
|
593
|
+
// Step 6: Calculate frame ranges (pure math, no tree traversal)
|
|
594
|
+
const totalLines = root.data?.totalLines ?? lineElements.size;
|
|
595
|
+
const frameRanges = calculateFrameRanges(emphasizedLines, totalLines, options);
|
|
596
|
+
|
|
597
|
+
// Step 7: Restructure frames (flat iteration, not deep recursive traversal)
|
|
598
|
+
restructureFrames(root, frameRanges, regionIndentLevels);
|
|
599
|
+
return root;
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Default source enhancer that adds emphasis to code lines based on `@highlight` comments.
|
|
605
|
+
* Uses no padding frames by default. Use `createEnhanceCodeEmphasis` for configurable padding.
|
|
446
606
|
*
|
|
447
607
|
* @example
|
|
448
608
|
* ```ts
|
|
@@ -451,19 +611,4 @@ function addEmphasisToLines(node, emphasizedLines) {
|
|
|
451
611
|
* const enhancers = [enhanceCodeEmphasis];
|
|
452
612
|
* ```
|
|
453
613
|
*/
|
|
454
|
-
export const enhanceCodeEmphasis = (
|
|
455
|
-
if (!comments || Object.keys(comments).length === 0) {
|
|
456
|
-
return root;
|
|
457
|
-
}
|
|
458
|
-
const directives = parseEmphasisDirectives(comments);
|
|
459
|
-
if (directives.length === 0) {
|
|
460
|
-
return root;
|
|
461
|
-
}
|
|
462
|
-
const lineElements = buildLineElementMap(root);
|
|
463
|
-
const emphasizedLines = calculateEmphasizedLines(directives, lineElements);
|
|
464
|
-
if (emphasizedLines.size === 0) {
|
|
465
|
-
return root;
|
|
466
|
-
}
|
|
467
|
-
addEmphasisToLines(root, emphasizedLines);
|
|
468
|
-
return root;
|
|
469
|
-
};
|
|
614
|
+
export const enhanceCodeEmphasis = createEnhanceCodeEmphasis();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Example copied from https://github.com/wooorm/starry-night#example-adding-line-numbers
|
|
2
2
|
|
|
3
|
+
import { createFrame } from "./createFrame.mjs";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Counts the number of lines in a HAST tree without mutating it.
|
|
5
7
|
* Uses the same logic as starryNightGutter but only returns the count.
|
|
@@ -94,7 +96,7 @@ export function starryNightGutter(tree, sourceLines, frameSize = 120) {
|
|
|
94
96
|
|
|
95
97
|
// Check if we need to create a frame (only if sourceLines provided, otherwise keep everything in one frame)
|
|
96
98
|
if (sourceLines && lineNumber % frameSize === 0) {
|
|
97
|
-
replacement.push(createFrame(frameLines,
|
|
99
|
+
replacement.push(createFrame(frameLines, frameStartLine, lineNumber));
|
|
98
100
|
frameLines = [];
|
|
99
101
|
frameStartLine = lineNumber + 1;
|
|
100
102
|
}
|
|
@@ -125,7 +127,7 @@ export function starryNightGutter(tree, sourceLines, frameSize = 120) {
|
|
|
125
127
|
|
|
126
128
|
// Add any remaining lines as the final frame
|
|
127
129
|
if (frameLines.length > 0) {
|
|
128
|
-
replacement.push(createFrame(frameLines,
|
|
130
|
+
replacement.push(createFrame(frameLines, frameStartLine, lineNumber));
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
// If there are multiple frames and sourceLines provided, add dataAsString to each frame
|
|
@@ -158,21 +160,4 @@ function createLine(children, line) {
|
|
|
158
160
|
},
|
|
159
161
|
children
|
|
160
162
|
};
|
|
161
|
-
}
|
|
162
|
-
function createFrame(frameChildren, sourceLines, startLine, endLine) {
|
|
163
|
-
const properties = {
|
|
164
|
-
className: 'frame'
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Store line range information if provided (for dataAsString generation)
|
|
168
|
-
if (sourceLines && startLine !== undefined && endLine !== undefined) {
|
|
169
|
-
properties.dataFrameStartLine = startLine;
|
|
170
|
-
properties.dataFrameEndLine = endLine;
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
type: 'element',
|
|
174
|
-
tagName: 'span',
|
|
175
|
-
properties,
|
|
176
|
-
children: frameChildren
|
|
177
|
-
};
|
|
178
163
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata for an emphasized line.
|
|
3
|
+
*/
|
|
4
|
+
export interface EmphasisMeta {
|
|
5
|
+
/** Optional description for this emphasis */
|
|
6
|
+
description?: string;
|
|
7
|
+
/** Position: 'single' for single-line, 'start'/'end' for multiline range bounds, undefined for middle */
|
|
8
|
+
position?: 'single' | 'start' | 'end';
|
|
9
|
+
/** Whether this is a strong emphasis (description ended with !) */
|
|
10
|
+
strong?: boolean;
|
|
11
|
+
/** For text highlighting: the specific text to highlight within the line */
|
|
12
|
+
highlightText?: string;
|
|
13
|
+
/** Whether this line's region is the focused region (for padding) */
|
|
14
|
+
focus?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A range of lines that forms a frame in the output.
|
|
18
|
+
*/
|
|
19
|
+
export interface FrameRange {
|
|
20
|
+
/** First line number (1-based, inclusive) */
|
|
21
|
+
startLine: number;
|
|
22
|
+
/** Last line number (1-based, inclusive) */
|
|
23
|
+
endLine: number;
|
|
24
|
+
/** The type of frame */
|
|
25
|
+
type: 'normal' | 'padding-top' | 'highlighted' | 'highlighted-unfocused' | 'padding-bottom' | 'comment';
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Options for the enhance code emphasis factory.
|
|
29
|
+
*/
|
|
30
|
+
export interface EnhanceCodeEmphasisOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Maximum number of padding lines above and below the focused highlight region.
|
|
33
|
+
* Padding frames provide surrounding context for the highlighted code.
|
|
34
|
+
* Set to 0 or omit to disable padding frames.
|
|
35
|
+
*/
|
|
36
|
+
paddingFrameMaxSize?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Maximum total number of lines in the focus area (padding-top + highlighted + padding-bottom).
|
|
39
|
+
* When set, padding sizes are reduced so the total focus area fits within this limit.
|
|
40
|
+
* The remainder after subtracting the highlighted size is split: floor(remainder/2) for
|
|
41
|
+
* padding-top and ceil(remainder/2) for padding-bottom.
|
|
42
|
+
*/
|
|
43
|
+
focusFramesMaxSize?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Calculates frame ranges for the code block based on emphasized lines.
|
|
47
|
+
*
|
|
48
|
+
* This is a pure function that operates on line numbers — no HAST traversal.
|
|
49
|
+
* It groups consecutive highlighted lines into regions, determines the focused
|
|
50
|
+
* region (first by default, or the one with `focus: true`), computes padding
|
|
51
|
+
* for the focused region, and returns an ordered array of frame ranges covering
|
|
52
|
+
* all lines 1 through totalLines.
|
|
53
|
+
*
|
|
54
|
+
* @param emphasizedLines - Map of line numbers to their emphasis metadata
|
|
55
|
+
* @param totalLines - Total number of lines in the code block
|
|
56
|
+
* @param options - Optional padding configuration
|
|
57
|
+
* @returns Ordered array of frame ranges covering all lines
|
|
58
|
+
*/
|
|
59
|
+
export declare function calculateFrameRanges(emphasizedLines: Map<number, EmphasisMeta>, totalLines: number, options?: EnhanceCodeEmphasisOptions): FrameRange[];
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata for an emphasized line.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A range of lines that forms a frame in the output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A contiguous region of highlighted lines.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Options for the enhance code emphasis factory.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Groups consecutive emphasized line numbers into highlight regions.
|
|
19
|
+
*
|
|
20
|
+
* @param emphasizedLines - Map of line numbers to their emphasis metadata
|
|
21
|
+
* @returns Sorted array of highlight regions
|
|
22
|
+
*/
|
|
23
|
+
function groupHighlightRegions(emphasizedLines) {
|
|
24
|
+
if (emphasizedLines.size === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const sortedLines = Array.from(emphasizedLines.keys()).sort((a, b) => a - b);
|
|
28
|
+
const regions = [];
|
|
29
|
+
let regionStart = sortedLines[0];
|
|
30
|
+
let regionEnd = sortedLines[0];
|
|
31
|
+
let hasFocus = emphasizedLines.get(sortedLines[0])?.focus ?? false;
|
|
32
|
+
for (let i = 1; i < sortedLines.length; i += 1) {
|
|
33
|
+
const line = sortedLines[i];
|
|
34
|
+
if (line === regionEnd + 1) {
|
|
35
|
+
// Consecutive line, extend current region
|
|
36
|
+
regionEnd = line;
|
|
37
|
+
if (emphasizedLines.get(line)?.focus) {
|
|
38
|
+
hasFocus = true;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// Gap found, close current region and start a new one
|
|
42
|
+
regions.push({
|
|
43
|
+
startLine: regionStart,
|
|
44
|
+
endLine: regionEnd,
|
|
45
|
+
index: regions.length,
|
|
46
|
+
focused: hasFocus
|
|
47
|
+
});
|
|
48
|
+
regionStart = line;
|
|
49
|
+
regionEnd = line;
|
|
50
|
+
hasFocus = emphasizedLines.get(line)?.focus ?? false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Close the last region
|
|
55
|
+
regions.push({
|
|
56
|
+
startLine: regionStart,
|
|
57
|
+
endLine: regionEnd,
|
|
58
|
+
index: regions.length,
|
|
59
|
+
focused: hasFocus
|
|
60
|
+
});
|
|
61
|
+
return regions;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Determines the focused region index.
|
|
66
|
+
* Returns the region explicitly marked with `focus: true`, or the first region.
|
|
67
|
+
*
|
|
68
|
+
* @param regions - Highlight regions
|
|
69
|
+
* @returns The index of the focused region
|
|
70
|
+
*/
|
|
71
|
+
function determineFocusedRegionIndex(regions) {
|
|
72
|
+
const focusedIndex = regions.findIndex(r => r.focused);
|
|
73
|
+
return focusedIndex >= 0 ? focusedIndex : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculates padding sizes for the focused highlight region.
|
|
78
|
+
*
|
|
79
|
+
* @param region - The focused highlight region
|
|
80
|
+
* @param prevRegionEnd - End line of the previous highlight region (or 0)
|
|
81
|
+
* @param nextRegionStart - Start line of the next highlight region (or totalLines + 1)
|
|
82
|
+
* @param options - Padding configuration options
|
|
83
|
+
* @returns Padding sizes [paddingTop, paddingBottom]
|
|
84
|
+
*/
|
|
85
|
+
function calculatePadding(region, prevRegionEnd, nextRegionStart, options) {
|
|
86
|
+
const {
|
|
87
|
+
paddingFrameMaxSize = 0,
|
|
88
|
+
focusFramesMaxSize
|
|
89
|
+
} = options;
|
|
90
|
+
if (paddingFrameMaxSize <= 0) {
|
|
91
|
+
return [0, 0];
|
|
92
|
+
}
|
|
93
|
+
const highlightSize = region.endLine - region.startLine + 1;
|
|
94
|
+
let paddingTop = paddingFrameMaxSize;
|
|
95
|
+
let paddingBottom = paddingFrameMaxSize;
|
|
96
|
+
|
|
97
|
+
// Apply focusFramesMaxSize constraint
|
|
98
|
+
if (focusFramesMaxSize !== undefined) {
|
|
99
|
+
const remaining = focusFramesMaxSize - highlightSize;
|
|
100
|
+
if (remaining <= 0) {
|
|
101
|
+
return [0, 0];
|
|
102
|
+
}
|
|
103
|
+
paddingTop = Math.min(paddingTop, Math.floor(remaining / 2));
|
|
104
|
+
paddingBottom = Math.min(paddingBottom, Math.ceil(remaining / 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Clamp to available lines before the highlight (don't overlap previous region)
|
|
108
|
+
const availableBefore = region.startLine - 1 - prevRegionEnd;
|
|
109
|
+
paddingTop = Math.min(paddingTop, Math.max(0, availableBefore));
|
|
110
|
+
|
|
111
|
+
// Clamp to available lines after the highlight (don't overlap next region)
|
|
112
|
+
const availableAfter = nextRegionStart - 1 - region.endLine;
|
|
113
|
+
paddingBottom = Math.min(paddingBottom, Math.max(0, availableAfter));
|
|
114
|
+
return [paddingTop, paddingBottom];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calculates frame ranges for the code block based on emphasized lines.
|
|
119
|
+
*
|
|
120
|
+
* This is a pure function that operates on line numbers — no HAST traversal.
|
|
121
|
+
* It groups consecutive highlighted lines into regions, determines the focused
|
|
122
|
+
* region (first by default, or the one with `focus: true`), computes padding
|
|
123
|
+
* for the focused region, and returns an ordered array of frame ranges covering
|
|
124
|
+
* all lines 1 through totalLines.
|
|
125
|
+
*
|
|
126
|
+
* @param emphasizedLines - Map of line numbers to their emphasis metadata
|
|
127
|
+
* @param totalLines - Total number of lines in the code block
|
|
128
|
+
* @param options - Optional padding configuration
|
|
129
|
+
* @returns Ordered array of frame ranges covering all lines
|
|
130
|
+
*/
|
|
131
|
+
export function calculateFrameRanges(emphasizedLines, totalLines, options = {}) {
|
|
132
|
+
if (totalLines <= 0) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
const regions = groupHighlightRegions(emphasizedLines);
|
|
136
|
+
if (regions.length === 0) {
|
|
137
|
+
return [{
|
|
138
|
+
startLine: 1,
|
|
139
|
+
endLine: totalLines,
|
|
140
|
+
type: 'normal'
|
|
141
|
+
}];
|
|
142
|
+
}
|
|
143
|
+
const focusedIndex = determineFocusedRegionIndex(regions);
|
|
144
|
+
|
|
145
|
+
// Calculate padding for the focused region
|
|
146
|
+
const focusedRegion = regions[focusedIndex];
|
|
147
|
+
const prevRegionEnd = focusedIndex > 0 ? regions[focusedIndex - 1].endLine : 0;
|
|
148
|
+
const nextRegionStart = focusedIndex < regions.length - 1 ? regions[focusedIndex + 1].startLine : totalLines + 1;
|
|
149
|
+
const [paddingTop, paddingBottom] = calculatePadding(focusedRegion, prevRegionEnd, nextRegionStart, options);
|
|
150
|
+
|
|
151
|
+
// Build frame ranges by iterating through all regions
|
|
152
|
+
const frames = [];
|
|
153
|
+
let currentLine = 1;
|
|
154
|
+
for (let i = 0; i < regions.length; i += 1) {
|
|
155
|
+
const region = regions[i];
|
|
156
|
+
const isFocused = i === focusedIndex;
|
|
157
|
+
if (isFocused && paddingTop > 0) {
|
|
158
|
+
// Normal lines before padding-top
|
|
159
|
+
const paddingTopStart = region.startLine - paddingTop;
|
|
160
|
+
if (currentLine < paddingTopStart) {
|
|
161
|
+
frames.push({
|
|
162
|
+
startLine: currentLine,
|
|
163
|
+
endLine: paddingTopStart - 1,
|
|
164
|
+
type: 'normal'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// Padding-top frame
|
|
168
|
+
frames.push({
|
|
169
|
+
startLine: paddingTopStart,
|
|
170
|
+
endLine: region.startLine - 1,
|
|
171
|
+
type: 'padding-top'
|
|
172
|
+
});
|
|
173
|
+
} else if (currentLine < region.startLine) {
|
|
174
|
+
// Normal lines before this region
|
|
175
|
+
frames.push({
|
|
176
|
+
startLine: currentLine,
|
|
177
|
+
endLine: region.startLine - 1,
|
|
178
|
+
type: 'normal'
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Highlighted frame (focused gets 'highlighted', others get 'highlighted-unfocused')
|
|
183
|
+
frames.push({
|
|
184
|
+
startLine: region.startLine,
|
|
185
|
+
endLine: region.endLine,
|
|
186
|
+
type: isFocused ? 'highlighted' : 'highlighted-unfocused'
|
|
187
|
+
});
|
|
188
|
+
currentLine = region.endLine + 1;
|
|
189
|
+
if (isFocused && paddingBottom > 0) {
|
|
190
|
+
// Padding-bottom frame
|
|
191
|
+
frames.push({
|
|
192
|
+
startLine: currentLine,
|
|
193
|
+
endLine: currentLine + paddingBottom - 1,
|
|
194
|
+
type: 'padding-bottom'
|
|
195
|
+
});
|
|
196
|
+
currentLine = currentLine + paddingBottom;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Remaining normal lines after all regions
|
|
201
|
+
if (currentLine <= totalLines) {
|
|
202
|
+
frames.push({
|
|
203
|
+
startLine: currentLine,
|
|
204
|
+
endLine: totalLines,
|
|
205
|
+
type: 'normal'
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return frames;
|
|
209
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Element, ElementContent } from 'hast';
|
|
2
|
+
import type { FrameRange } from "./calculateFrameRanges.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a HAST frame element (`span.frame`) with the given children and optional metadata.
|
|
5
|
+
*
|
|
6
|
+
* Used by both `addLineGutters` (initial frame creation) and `restructureFrames`
|
|
7
|
+
* (splitting/rebuilding frames for highlighting or comment extraction).
|
|
8
|
+
*/
|
|
9
|
+
export declare function createFrame(children: Array<ElementContent>, startLine?: number, endLine?: number, frameType?: FrameRange['type'], indentLevel?: number): Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a HAST frame element (`span.frame`) with the given children and optional metadata.
|
|
3
|
+
*
|
|
4
|
+
* Used by both `addLineGutters` (initial frame creation) and `restructureFrames`
|
|
5
|
+
* (splitting/rebuilding frames for highlighting or comment extraction).
|
|
6
|
+
*/
|
|
7
|
+
export function createFrame(children, startLine, endLine, frameType, indentLevel) {
|
|
8
|
+
const properties = {
|
|
9
|
+
className: 'frame'
|
|
10
|
+
};
|
|
11
|
+
if (startLine !== undefined && endLine !== undefined) {
|
|
12
|
+
properties.dataFrameStartLine = startLine;
|
|
13
|
+
properties.dataFrameEndLine = endLine;
|
|
14
|
+
}
|
|
15
|
+
if (frameType && frameType !== 'normal') {
|
|
16
|
+
properties.dataFrameType = frameType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Set indent level on highlighted frames (focused or unfocused)
|
|
20
|
+
if ((frameType === 'highlighted' || frameType === 'highlighted-unfocused') && indentLevel !== undefined) {
|
|
21
|
+
properties.dataFrameIndent = indentLevel;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
type: 'element',
|
|
25
|
+
tagName: 'span',
|
|
26
|
+
properties,
|
|
27
|
+
children
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { HastRoot } from "../../CodeHighlighter/types.mjs";
|
|
2
|
+
import type { FrameRange } from "./calculateFrameRanges.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Restructures the HAST frame tree based on computed frame ranges.
|
|
5
|
+
*
|
|
6
|
+
* This function flattens all existing frame children into a single ordered array,
|
|
7
|
+
* then redistributes them into new frames based on the provided frame ranges.
|
|
8
|
+
* It's a flat iteration (not a deep recursive traversal) since the HAST structure
|
|
9
|
+
* is always Root → Frame(s) → Line(s).
|
|
10
|
+
*
|
|
11
|
+
* @param root - The HAST root node to restructure (mutated in place)
|
|
12
|
+
* @param frameRanges - Ordered array of frame ranges
|
|
13
|
+
* @param regionIndentLevels - Map of highlighted region index to indent level
|
|
14
|
+
*/
|
|
15
|
+
export declare function restructureFrames(root: HastRoot, frameRanges: FrameRange[], regionIndentLevels: Map<number, number>): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createFrame } from "./createFrame.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Represents a line element and its trailing newline text node (if any).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Flattens all line elements from existing frames into a single ordered array.
|
|
9
|
+
* This is a flat iteration over root.children (frames) and their direct children
|
|
10
|
+
* (lines + newline text nodes). Not a deep recursive traversal.
|
|
11
|
+
*
|
|
12
|
+
* @param root - The HAST root node
|
|
13
|
+
* @returns Ordered array of line entries
|
|
14
|
+
*/
|
|
15
|
+
function flattenLineEntries(root) {
|
|
16
|
+
const entries = [];
|
|
17
|
+
for (const frame of root.children) {
|
|
18
|
+
if (frame.type !== 'element' || frame.tagName !== 'span') {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const children = frame.children ?? [];
|
|
22
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
23
|
+
const child = children[i];
|
|
24
|
+
if (child.type === 'element' && child.tagName === 'span' && child.properties?.className === 'line' && typeof child.properties.dataLn === 'number') {
|
|
25
|
+
const entry = {
|
|
26
|
+
lineNumber: child.properties.dataLn,
|
|
27
|
+
element: child
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Check if the next child is a trailing newline text node
|
|
31
|
+
const next = children[i + 1];
|
|
32
|
+
if (next && next.type === 'text' && /^[\r\n]+$/.test(next.value)) {
|
|
33
|
+
entry.trailingNewline = next;
|
|
34
|
+
i += 1; // Skip the newline in the next iteration
|
|
35
|
+
}
|
|
36
|
+
entries.push(entry);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Restructures the HAST frame tree based on computed frame ranges.
|
|
45
|
+
*
|
|
46
|
+
* This function flattens all existing frame children into a single ordered array,
|
|
47
|
+
* then redistributes them into new frames based on the provided frame ranges.
|
|
48
|
+
* It's a flat iteration (not a deep recursive traversal) since the HAST structure
|
|
49
|
+
* is always Root → Frame(s) → Line(s).
|
|
50
|
+
*
|
|
51
|
+
* @param root - The HAST root node to restructure (mutated in place)
|
|
52
|
+
* @param frameRanges - Ordered array of frame ranges
|
|
53
|
+
* @param regionIndentLevels - Map of highlighted region index to indent level
|
|
54
|
+
*/
|
|
55
|
+
export function restructureFrames(root, frameRanges, regionIndentLevels) {
|
|
56
|
+
// Step 1: Flatten all lines from existing frames
|
|
57
|
+
const lineEntries = flattenLineEntries(root);
|
|
58
|
+
|
|
59
|
+
// Build a lookup from line number to entry for O(1) access
|
|
60
|
+
const lineEntryMap = new Map();
|
|
61
|
+
for (const entry of lineEntries) {
|
|
62
|
+
lineEntryMap.set(entry.lineNumber, entry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 2: Track which highlighted region index each frame range belongs to
|
|
66
|
+
let highlightedRegionIndex = 0;
|
|
67
|
+
|
|
68
|
+
// Step 3: Build new frames
|
|
69
|
+
const newFrames = [];
|
|
70
|
+
for (const range of frameRanges) {
|
|
71
|
+
const children = [];
|
|
72
|
+
for (let line = range.startLine; line <= range.endLine; line += 1) {
|
|
73
|
+
const entry = lineEntryMap.get(line);
|
|
74
|
+
if (!entry) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
children.push(entry.element);
|
|
78
|
+
|
|
79
|
+
// Always add trailing newline when present to preserve original line breaks
|
|
80
|
+
if (entry.trailingNewline) {
|
|
81
|
+
children.push(entry.trailingNewline);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Only create frame if it has children
|
|
86
|
+
if (children.length > 0) {
|
|
87
|
+
const isHighlighted = range.type === 'highlighted' || range.type === 'highlighted-unfocused';
|
|
88
|
+
const indentLevel = isHighlighted ? regionIndentLevels.get(highlightedRegionIndex) : undefined;
|
|
89
|
+
newFrames.push(createFrame(children, range.startLine, range.endLine, range.type, indentLevel));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Increment region index after each highlighted frame (focused or unfocused)
|
|
93
|
+
if (range.type === 'highlighted' || range.type === 'highlighted-unfocused') {
|
|
94
|
+
highlightedRegionIndex += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Step 4: Replace root children
|
|
99
|
+
root.children = newFrames;
|
|
100
|
+
}
|
package/useCode/Pre.mjs
CHANGED
|
@@ -146,6 +146,8 @@ export function Pre({
|
|
|
146
146
|
className: "frame",
|
|
147
147
|
"data-frame": index,
|
|
148
148
|
"data-lined": shouldRenderHast ? '' : undefined,
|
|
149
|
+
"data-frame-type": child.properties.dataFrameType ? String(child.properties.dataFrameType) : undefined,
|
|
150
|
+
"data-frame-indent": child.properties.dataFrameIndent != null ? String(child.properties.dataFrameIndent) : undefined,
|
|
149
151
|
ref: observeFrame,
|
|
150
152
|
children: renderCode(child.children, shouldRenderHast, child.properties?.dataAsString ? String(child.properties?.dataAsString) : undefined)
|
|
151
153
|
}, index);
|