@mui/internal-docs-infra 0.11.1-canary.21 → 0.11.1-canary.22
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/CodeHighlighter/CodeHighlighter.mjs +11 -2
- package/CodeHighlighter/CodeHighlighterClient.mjs +60 -51
- package/CodeHighlighter/createClientProps.mjs +14 -3
- package/CodeHighlighter/fallbackFormat.d.mts +38 -0
- package/CodeHighlighter/fallbackFormat.mjs +96 -3
- package/CodeHighlighter/prepareInitialSource.d.mts +11 -0
- package/CodeHighlighter/prepareInitialSource.mjs +67 -8
- package/CodeHighlighter/resolveFallbackCritical.d.mts +23 -0
- package/CodeHighlighter/resolveFallbackCritical.mjs +44 -0
- package/CodeHighlighter/types.d.mts +16 -0
- package/CoordinatedLazy/useChunk.mjs +10 -8
- package/CoordinatedLazy/useCoordinatedSwap.mjs +1 -0
- package/abstractCreateTypes/TypeCode.mjs +12 -11
- package/abstractCreateTypes/typesToJsx.mjs +13 -8
- package/package.json +2 -2
- package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +41 -1
- package/pipeline/parseSource/frameVisibility.d.mts +17 -1
- package/pipeline/parseSource/frameVisibility.mjs +53 -0
- package/useCode/EditableEngine.mjs +15 -5
- package/useCode/Pre.mjs +43 -48
- package/useCode/SourceEditingEngine.mjs +29 -8
- package/useCode/useCode.mjs +11 -3
- package/useCode/{liveEditingBugs.browser.mjs → useEditable.integration.browser.mjs} +114 -69
- package/useCode/useFileNavigation.mjs +20 -16
- package/useCode/useTransformManagement.mjs +13 -5
- package/useCode/useUIState.mjs +6 -6
- package/useCode/useVariantSelection.mjs +20 -6
- package/useCoordinated/coordinatePreference.mjs +4 -25
- package/useCoordinated/scheduleTasks.d.mts +23 -0
- package/useCoordinated/scheduleTasks.mjs +45 -0
- package/useCoordinated/useCoordinated.mjs +33 -6
- package/useStream/useStream.mjs +2 -4
- package/useStream/useStreamController.mjs +6 -1
- /package/useCode/{liveEditingBugs.browser.d.mts → useEditable.integration.browser.d.mts} +0 -0
|
@@ -163,6 +163,22 @@ export type VariantCode = CodeMeta & {
|
|
|
163
163
|
* decompressing `hastCompressed` payloads.
|
|
164
164
|
*/
|
|
165
165
|
fallback?: FallbackNode[];
|
|
166
|
+
/**
|
|
167
|
+
* Staging-only highlighted-visible fallback, stored **sparsely**: a map from frame
|
|
168
|
+
* index to the highlighted `FallbackNode` for ONLY the frames visible on the initial
|
|
169
|
+
* (collapsed, `collapseToEmpty: false`) render. Off-screen frames are omitted — they
|
|
170
|
+
* are byte-identical to `fallback`'s plain output, so storing them would just
|
|
171
|
+
* duplicate `fallback` in the precompute. Computed at load time (where the source is
|
|
172
|
+
* still a live `HastRoot`, so no decompression) and carried in precomputed payloads
|
|
173
|
+
* alongside `fallback`. Under `highlightAt: 'init'` the server/client boundary
|
|
174
|
+
* `promoteCriticalFallback`s it over `fallback` so the first paint is highlighted with
|
|
175
|
+
* no client-side decompression, then deletes it: `fallbackCritical` must NEVER reach
|
|
176
|
+
* the `Content`/`ContentLoading` components. The promoted `fallback` has byte-identical
|
|
177
|
+
* text, so it stays a valid DEFLATE dictionary. See `resolveFallbackCritical`.
|
|
178
|
+
*/
|
|
179
|
+
fallbackCritical?: {
|
|
180
|
+
[frameIndex: number]: FallbackNode;
|
|
181
|
+
};
|
|
166
182
|
/**
|
|
167
183
|
* Line counts for this variant's main source — the same for the full (highlighted)
|
|
168
184
|
* source and its `fallback`, since they're the same file. `totalLines` is the whole
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { useChunkContext } from "../ChunkProvider/ChunkContext.mjs";
|
|
5
|
+
import { requestIdle } from "../useCoordinated/scheduleTasks.mjs";
|
|
5
6
|
|
|
6
7
|
/** Result of {@link useChunk}. */
|
|
7
8
|
|
|
@@ -40,11 +41,15 @@ export function useChunk(config, props = {}) {
|
|
|
40
41
|
return undefined;
|
|
41
42
|
}, [preloaded, config, options]);
|
|
42
43
|
const [data, setData] = React.useState(isLoaded ? preloaded : initialData);
|
|
43
|
-
|
|
44
|
+
// `true` once the async `data`-mode load has resolved. Derive `loading` during
|
|
45
|
+
// render so that when `isLoaded` becomes true after mount (a `preloaded` value
|
|
46
|
+
// or `controlled` flag arriving later via props) `loading` flips to false on
|
|
47
|
+
// the next render without an extra effect pass.
|
|
48
|
+
const [loaded, setLoaded] = React.useState(false);
|
|
44
49
|
const [revalidating, setRevalidating] = React.useState(false);
|
|
50
|
+
const loading = !isLoaded && !loaded;
|
|
45
51
|
React.useEffect(() => {
|
|
46
52
|
if (isLoaded) {
|
|
47
|
-
setLoading(false);
|
|
48
53
|
return undefined;
|
|
49
54
|
}
|
|
50
55
|
const controller = new AbortController();
|
|
@@ -65,7 +70,7 @@ export function useChunk(config, props = {}) {
|
|
|
65
70
|
const result = await source.load(options, controller.signal);
|
|
66
71
|
if (!controller.signal.aborted) {
|
|
67
72
|
setData(result);
|
|
68
|
-
|
|
73
|
+
setLoaded(true);
|
|
69
74
|
}
|
|
70
75
|
} catch {
|
|
71
76
|
// Aborted by a newer load, or the load failed - leave the loading
|
|
@@ -95,7 +100,7 @@ export function useChunk(config, props = {}) {
|
|
|
95
100
|
const result = await source.load(options, controller.signal);
|
|
96
101
|
if (!controller.signal.aborted) {
|
|
97
102
|
setData(result);
|
|
98
|
-
|
|
103
|
+
setLoaded(true);
|
|
99
104
|
setRevalidating(false);
|
|
100
105
|
}
|
|
101
106
|
} catch {
|
|
@@ -112,12 +117,9 @@ export function useChunk(config, props = {}) {
|
|
|
112
117
|
if (!config.revalidateOnIdle || loading || typeof window === 'undefined') {
|
|
113
118
|
return undefined;
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
const cancelIdle = window.cancelIdleCallback ?? clearTimeout;
|
|
117
|
-
const handle = requestIdle(() => {
|
|
120
|
+
return requestIdle(() => {
|
|
118
121
|
refresh().catch(() => {});
|
|
119
122
|
});
|
|
120
|
-
return () => cancelIdle(handle);
|
|
121
123
|
}, [config.revalidateOnIdle, loading, refresh]);
|
|
122
124
|
|
|
123
125
|
// Abort any in-flight refresh on unmount.
|
|
@@ -61,6 +61,7 @@ export function useCoordinatedSwap(options) {
|
|
|
61
61
|
// flip `fallbackMounted` and the swap proceeds.
|
|
62
62
|
React.useEffect(() => {
|
|
63
63
|
if (!fallbackMounted && hasFallback && !skipFallback) {
|
|
64
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- force-mount-once latch: must flip AFTER the first fallback commit so child hoist effects run first; see comment above
|
|
64
65
|
setFallbackMounted(true);
|
|
65
66
|
}
|
|
66
67
|
}, [fallbackMounted, hasFallback, skipFallback]);
|
|
@@ -4,6 +4,7 @@ import * as React from 'react';
|
|
|
4
4
|
import { decompressHast, hastToJsx } from "../pipeline/hastUtils/index.mjs";
|
|
5
5
|
import { useCodeComponents } from "../useCode/CodeComponentsContext.mjs";
|
|
6
6
|
import { fallbackToHast, fallbackToText } from "../pipeline/hastUtils/fallbackFormat.mjs";
|
|
7
|
+
import { requestIdle } from "../useCoordinated/scheduleTasks.mjs";
|
|
7
8
|
/**
|
|
8
9
|
* Find the children of the first `<code>` element in a parsed HAST tree.
|
|
9
10
|
*/
|
|
@@ -50,10 +51,15 @@ export function TypeCode({
|
|
|
50
51
|
const [isVisible, setIsVisible] = React.useState(effectiveMode !== 'visible');
|
|
51
52
|
const [codeElement, setCodeElement] = React.useState(null);
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
|
|
54
|
+
// Re-seed visibility state during render when the effective mode changes
|
|
55
|
+
// (the 'store previous prop value, set state during render' pattern). The
|
|
56
|
+
// IntersectionObserver effect still owns runtime true/false toggling because
|
|
57
|
+
// it runs after this render-time seed.
|
|
58
|
+
const [prevEffectiveMode, setPrevEffectiveMode] = React.useState(effectiveMode);
|
|
59
|
+
if (prevEffectiveMode !== effectiveMode) {
|
|
60
|
+
setPrevEffectiveMode(effectiveMode);
|
|
55
61
|
setIsVisible(effectiveMode !== 'visible');
|
|
56
|
-
}
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
// Convert compact fallback to HAST for rendering.
|
|
59
65
|
const fallbackHastRoot = React.useMemo(() => fallback ? fallbackToHast(fallback) : undefined, [fallback]);
|
|
@@ -143,14 +149,9 @@ export function TypeCode({
|
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
// 'idle' and 'visible' both defer to idle time to avoid blocking the main thread.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
150
|
-
return () => cancelIdleCallback(id);
|
|
151
|
-
}
|
|
152
|
-
const id = setTimeout(parse, 0);
|
|
153
|
-
return () => clearTimeout(id);
|
|
152
|
+
return requestIdle(parse, {
|
|
153
|
+
timeout: 2000
|
|
154
|
+
});
|
|
154
155
|
}, [isVisible, hastJson, hastCompressed, effectiveMode, textDictionary]);
|
|
155
156
|
const highlighted = React.useMemo(() => hast !== null ? hastToJsx(hast, components) : null, [hast, components]);
|
|
156
157
|
const content = highlighted ?? fallbackJsx;
|
|
@@ -313,14 +313,19 @@ function hastToJsxDeferred(hastOrJson, components, enhancers, highlightAt) {
|
|
|
313
313
|
// Derive dictionary text from the fallback, then compress the full
|
|
314
314
|
// highlighted HAST with that dictionary. On the client, TypeCode
|
|
315
315
|
// calls fallbackToText(fallback) to reconstruct the same dictionary.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
// TypeCode
|
|
321
|
-
//
|
|
316
|
+
// Serialize the enhanced HAST (post-enhancer) for the client. DEFLATE-compressing it
|
|
317
|
+
// (with the fallback text as the dictionary) only shrinks the server→client wire, so
|
|
318
|
+
// do it ONLY on the server. On the client there is no wire — the JSON is consumed
|
|
319
|
+
// in-process by TypeCode — so skip the synchronous, main-thread compress and hand it
|
|
320
|
+
// the JSON directly; TypeCode reads `hastJson` without decompressing (it only rebuilds
|
|
321
|
+
// the DEFLATE dictionary from `fallback` when it actually has a `hastCompressed`
|
|
322
|
+
// payload). `typeof window` is the isomorphic server check.
|
|
322
323
|
const enhancedJson = JSON.stringify(hast);
|
|
323
|
-
const
|
|
324
|
+
const hastPayload = typeof window === 'undefined' ? {
|
|
325
|
+
hastCompressed: compressHast(enhancedJson, fallbackToText(fallback))
|
|
326
|
+
} : {
|
|
327
|
+
hastJson: enhancedJson
|
|
328
|
+
};
|
|
324
329
|
|
|
325
330
|
// Find the <pre> element for wrapper props
|
|
326
331
|
const preElement = findPreElement(hast);
|
|
@@ -330,7 +335,7 @@ function hastToJsxDeferred(hastOrJson, components, enhancers, highlightAt) {
|
|
|
330
335
|
// fallback crosses the boundary as a serialized prop — no separate
|
|
331
336
|
// text dictionary string is needed.
|
|
332
337
|
return /*#__PURE__*/React.createElement(PreComponent, hastPropsToReactProps(preElement?.properties), /*#__PURE__*/React.createElement(TypeCode, {
|
|
333
|
-
|
|
338
|
+
...hastPayload,
|
|
334
339
|
highlightAt,
|
|
335
340
|
fallback,
|
|
336
341
|
codeProps: hastPropsToReactProps(codeElement.properties)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mui/internal-docs-infra",
|
|
3
|
-
"version": "0.11.1-canary.
|
|
3
|
+
"version": "0.11.1-canary.22",
|
|
4
4
|
"author": "MUI Team",
|
|
5
5
|
"description": "MUI Infra - internal documentation creation tools.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -768,5 +768,5 @@
|
|
|
768
768
|
"bin": {
|
|
769
769
|
"docs-infra": "./cli/index.mjs"
|
|
770
770
|
},
|
|
771
|
-
"gitSha": "
|
|
771
|
+
"gitSha": "c0380f22916148bebba5dee261c1a2a3a9211a5e"
|
|
772
772
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from 'path-module';
|
|
2
2
|
import { compressHastAsync } from "../hastUtils/index.mjs";
|
|
3
|
-
import { buildRootFallback, fallbackToText } from "../../CodeHighlighter/fallbackFormat.mjs";
|
|
3
|
+
import { buildRootFallback, buildCriticalFallback, fallbackToText } from "../../CodeHighlighter/fallbackFormat.mjs";
|
|
4
|
+
import { getInitialVisibleFrames } from "../parseSource/frameVisibility.mjs";
|
|
4
5
|
import { transformSource } from "./transformSource.mjs";
|
|
5
6
|
import { diffHast } from "./diffHast.mjs";
|
|
6
7
|
import { isFrameSpan } from "../parseSource/isFrameSpan.mjs";
|
|
@@ -182,6 +183,7 @@ async function loadSingleFile(variantName, fileName, source, url, loadSource, so
|
|
|
182
183
|
} = options;
|
|
183
184
|
let finalSource = source;
|
|
184
185
|
let finalFallback;
|
|
186
|
+
let finalFallbackCritical;
|
|
185
187
|
let finalTotalLines;
|
|
186
188
|
let finalFocusedLines;
|
|
187
189
|
let finalCollapsible;
|
|
@@ -373,6 +375,32 @@ async function loadSingleFile(variantName, fileName, source, url, loadSource, so
|
|
|
373
375
|
// decompressed on the client once the fallback travels over via context.
|
|
374
376
|
if (finalSource && typeof finalSource === 'object' && !('hastJson' in finalSource) && !('hastCompressed' in finalSource)) {
|
|
375
377
|
finalFallback = buildRootFallback(finalSource);
|
|
378
|
+
// Sparse highlighted-visible companion (see `VariantCode.fallbackCritical`),
|
|
379
|
+
// computed here while the source is still a live `HastRoot` (no
|
|
380
|
+
// decompression) and BEFORE `stripFrameFallbacks` removes the per-frame
|
|
381
|
+
// text it reuses. `false` builds the `collapseToEmpty: false` form (the only
|
|
382
|
+
// one carrying highlighting); the boundary skips promoting it under
|
|
383
|
+
// `collapseToEmpty`. Empty (no visible frames) → omit it entirely.
|
|
384
|
+
const critical = buildCriticalFallback(finalSource, getInitialVisibleFrames(finalSource, false));
|
|
385
|
+
finalFallbackCritical = Object.keys(critical).length > 0 ? critical : undefined;
|
|
386
|
+
|
|
387
|
+
// Hoist the window counts off `root.data` while the source is still a live
|
|
388
|
+
// `HastRoot`. They then ride on the variant (see the return below) so every
|
|
389
|
+
// downstream reader (`prepareInitialSource`, `getVariantFileLineCounts`,
|
|
390
|
+
// layout-shift classification) gets `totalLines`/`focusedLines`/`collapsible`
|
|
391
|
+
// WITHOUT decompressing the payload — the compact fallback and the compressed
|
|
392
|
+
// source both drop `root.data`, so without this the only way to recover the
|
|
393
|
+
// counts is to decode the hast (the first-render decompression we want to avoid).
|
|
394
|
+
const rootData = finalSource.data;
|
|
395
|
+
if (rootData?.totalLines !== undefined) {
|
|
396
|
+
const total = Number(rootData.totalLines);
|
|
397
|
+
if (Number.isFinite(total) && total >= 0) {
|
|
398
|
+
finalTotalLines = total;
|
|
399
|
+
const focused = Number(rootData.focusedLines);
|
|
400
|
+
finalFocusedLines = Number.isFinite(focused) && focused >= 0 ? focused : total;
|
|
401
|
+
finalCollapsible = rootData.collapsible === true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
376
404
|
}
|
|
377
405
|
if (options.output === 'hastCompressed' && process.env.NODE_ENV === 'production') {
|
|
378
406
|
if (finalFallback) {
|
|
@@ -439,6 +467,7 @@ async function loadSingleFile(variantName, fileName, source, url, loadSource, so
|
|
|
439
467
|
return {
|
|
440
468
|
source: finalSource,
|
|
441
469
|
fallback: finalFallback,
|
|
470
|
+
fallbackCritical: finalFallbackCritical,
|
|
442
471
|
totalLines: finalTotalLines,
|
|
443
472
|
focusedLines: finalFocusedLines,
|
|
444
473
|
collapsible: finalCollapsible,
|
|
@@ -782,6 +811,7 @@ export async function loadIsomorphicCodeVariant(url, variantName, variant, optio
|
|
|
782
811
|
if (!fileName && !url) {
|
|
783
812
|
let finalSource = variant.source;
|
|
784
813
|
let finalFallback;
|
|
814
|
+
let finalFallbackCritical;
|
|
785
815
|
|
|
786
816
|
// Parse the source if we have language and sourceParser
|
|
787
817
|
if (typeof finalSource === 'string' && language && sourceParser && !disableParsing) {
|
|
@@ -814,6 +844,10 @@ export async function loadIsomorphicCodeVariant(url, variantName, variant, optio
|
|
|
814
844
|
// Always derive a variant-level root fallback from the per-frame text so a
|
|
815
845
|
// `ContentLoading` component can render before the hast is decoded.
|
|
816
846
|
finalFallback = buildRootFallback(finalSource);
|
|
847
|
+
// Sparse highlighted-visible companion (see `VariantCode.fallbackCritical`),
|
|
848
|
+
// built from the live `HastRoot` before `stripFrameFallbacks` runs.
|
|
849
|
+
const critical = buildCriticalFallback(finalSource, getInitialVisibleFrames(finalSource, false));
|
|
850
|
+
finalFallbackCritical = Object.keys(critical).length > 0 ? critical : undefined;
|
|
817
851
|
if (options.output === 'hastCompressed' && process.env.NODE_ENV === 'production') {
|
|
818
852
|
if (finalFallback) {
|
|
819
853
|
stripFrameFallbacks(finalSource);
|
|
@@ -832,6 +866,9 @@ export async function loadIsomorphicCodeVariant(url, variantName, variant, optio
|
|
|
832
866
|
source: finalSource,
|
|
833
867
|
...(finalFallback ? {
|
|
834
868
|
fallback: finalFallback
|
|
869
|
+
} : {}),
|
|
870
|
+
...(finalFallbackCritical ? {
|
|
871
|
+
fallbackCritical: finalFallbackCritical
|
|
835
872
|
} : {})
|
|
836
873
|
};
|
|
837
874
|
return {
|
|
@@ -1084,6 +1121,9 @@ export async function loadIsomorphicCodeVariant(url, variantName, variant, optio
|
|
|
1084
1121
|
...(mainFileResult.fallback && {
|
|
1085
1122
|
fallback: mainFileResult.fallback
|
|
1086
1123
|
}),
|
|
1124
|
+
...(mainFileResult.fallbackCritical && {
|
|
1125
|
+
fallbackCritical: mainFileResult.fallbackCritical
|
|
1126
|
+
}),
|
|
1087
1127
|
...(mainFileResult.totalLines !== undefined && {
|
|
1088
1128
|
totalLines: mainFileResult.totalLines
|
|
1089
1129
|
}),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HastRoot } from "../../CodeHighlighter/types.mjs";
|
|
1
2
|
/**
|
|
2
3
|
* Set form of {@link COLLAPSED_VISIBLE_FRAME_TYPE_LIST} for fast membership
|
|
3
4
|
* checks. Typed as `ReadonlySet<string>` so callers can pass a raw
|
|
@@ -28,4 +29,19 @@ export declare const COLLAPSED_VISIBLE_FRAME_TYPES: ReadonlySet<string>;
|
|
|
28
29
|
* @param frameType - The frame's `data-frame-type` (may be `undefined` for `normal`)
|
|
29
30
|
* @param collapseToEmpty - Whether the block is rendered collapse-to-empty
|
|
30
31
|
*/
|
|
31
|
-
export declare function resolveCollapsedFrameType(frameType: string | undefined, collapseToEmpty: boolean): string | undefined;
|
|
32
|
+
export declare function resolveCollapsedFrameType(frameType: string | undefined, collapseToEmpty: boolean): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The set of frame indices that are visible on the initial (collapsed) render of
|
|
35
|
+
* a code block: the contiguous focused window
|
|
36
|
+
* ({@link COLLAPSED_VISIBLE_FRAME_TYPES}), falling back to the first frame when no
|
|
37
|
+
* frame carries an emphasis type. Returns an empty set for `collapseToEmpty` (an
|
|
38
|
+
* empty collapsed window) and for a `focusedLines === 0` carve-out
|
|
39
|
+
* (`oversizedFocus: 'hide'`).
|
|
40
|
+
*
|
|
41
|
+
* Shared by the runtime rule in `useCode/Pre.tsx` and the server-side
|
|
42
|
+
* highlighted-visible fallback builder, so the frames highlighted on the first
|
|
43
|
+
* paint match exactly. Isomorphic — reads only precomputed HAST attributes.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getInitialVisibleFrames(hast: HastRoot | null, collapseToEmpty?: boolean): {
|
|
46
|
+
[key: number]: boolean;
|
|
47
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isFrameSpan } from "./isFrameSpan.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The `data-frame-type` values whose frames make up the window a collapsible
|
|
3
5
|
* code block shows while collapsed (the contiguous focused window:
|
|
@@ -58,4 +60,55 @@ export function resolveCollapsedFrameType(frameType, collapseToEmpty) {
|
|
|
58
60
|
default:
|
|
59
61
|
return frameType;
|
|
60
62
|
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The set of frame indices that are visible on the initial (collapsed) render of
|
|
67
|
+
* a code block: the contiguous focused window
|
|
68
|
+
* ({@link COLLAPSED_VISIBLE_FRAME_TYPES}), falling back to the first frame when no
|
|
69
|
+
* frame carries an emphasis type. Returns an empty set for `collapseToEmpty` (an
|
|
70
|
+
* empty collapsed window) and for a `focusedLines === 0` carve-out
|
|
71
|
+
* (`oversizedFocus: 'hide'`).
|
|
72
|
+
*
|
|
73
|
+
* Shared by the runtime rule in `useCode/Pre.tsx` and the server-side
|
|
74
|
+
* highlighted-visible fallback builder, so the frames highlighted on the first
|
|
75
|
+
* paint match exactly. Isomorphic — reads only precomputed HAST attributes.
|
|
76
|
+
*/
|
|
77
|
+
export function getInitialVisibleFrames(hast, collapseToEmpty = false) {
|
|
78
|
+
if (!hast) {
|
|
79
|
+
return collapseToEmpty ? {} : {
|
|
80
|
+
0: true
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Collapse-to-empty renders an empty collapsed window — no frame is visible while
|
|
85
|
+
// collapsed, regardless of the precomputed frame types.
|
|
86
|
+
if (collapseToEmpty) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
const visibleFrames = {};
|
|
90
|
+
let frameIndex = 0;
|
|
91
|
+
let hasVisibleEmphasisFrame = false;
|
|
92
|
+
hast.children.forEach(child => {
|
|
93
|
+
if (child.type !== 'element' || !isFrameSpan(child)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const frameType = child.properties.dataFrameType;
|
|
97
|
+
if (typeof frameType === 'string' && COLLAPSED_VISIBLE_FRAME_TYPES.has(frameType)) {
|
|
98
|
+
visibleFrames[frameIndex] = true;
|
|
99
|
+
hasVisibleEmphasisFrame = true;
|
|
100
|
+
}
|
|
101
|
+
frameIndex += 1;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Collapse-to-nothing (oversizedFocus: 'hide'): `focusedLines === 0` means
|
|
105
|
+
// the collapsed window is intentionally empty, so skip the first-frame
|
|
106
|
+
// fallback and keep every frame hidden when collapsed.
|
|
107
|
+
if (hast.data?.focusedLines === 0) {
|
|
108
|
+
return visibleFrames;
|
|
109
|
+
}
|
|
110
|
+
if (!hasVisibleEmphasisFrame && frameIndex > 0) {
|
|
111
|
+
visibleFrames[0] = true;
|
|
112
|
+
}
|
|
113
|
+
return visibleFrames;
|
|
61
114
|
}
|
|
@@ -939,10 +939,16 @@ export const createEditableEngine = ctx => {
|
|
|
939
939
|
const beforePosition = getPosition(element);
|
|
940
940
|
const range = getCurrentRange();
|
|
941
941
|
if (!range.collapsed) {
|
|
942
|
-
// Whether the
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
|
|
942
|
+
// Whether the deletion removed WHOLE lines from the first line down. True
|
|
943
|
+
// only when the selection BOTH started at column 0 (no content before it)
|
|
944
|
+
// AND ended at a line boundary (its text ends with a newline) — then the
|
|
945
|
+
// first line is gone and the post-delete caret lands on the line that
|
|
946
|
+
// shifted up from below, so the comment-map anchor sits one line higher
|
|
947
|
+
// (see `deletedFromLineStart`). A selection that ends MID-line instead
|
|
948
|
+
// collapses the spanned lines INTO the first line, which survives (emptied)
|
|
949
|
+
// under the caret — no shift-up — so the flag must stay false, or a marker
|
|
950
|
+
// on that surviving line is dragged one line too high.
|
|
951
|
+
const deletedFromLineStart = beforePosition.content.length === 0 && range.toString().endsWith('\n');
|
|
946
952
|
edit.insert('', 0);
|
|
947
953
|
// A multi-line selection delete can natively remove whole `.frame`
|
|
948
954
|
// wrapper elements (e.g. selecting exactly one emphasis frame). That
|
|
@@ -1014,7 +1020,11 @@ export const createEditableEngine = ctx => {
|
|
|
1014
1020
|
event.preventDefault();
|
|
1015
1021
|
const range = getCurrentRange();
|
|
1016
1022
|
if (!range.collapsed) {
|
|
1017
|
-
|
|
1023
|
+
// See the Backspace branch above: deletedFromLineStart holds only when the
|
|
1024
|
+
// selection removed whole lines (started at column 0 AND ended at a line
|
|
1025
|
+
// boundary). A mid-line end collapses the lines in place, leaving the first
|
|
1026
|
+
// line emptied under the caret — no shift-up — so the flag must stay false.
|
|
1027
|
+
const deletedFromLineStart = getPosition(element).content.length === 0 && range.toString().endsWith('\n');
|
|
1018
1028
|
edit.insert('', 0);
|
|
1019
1029
|
// Same frame-wrapper detach crash as the Backspace branch above: a
|
|
1020
1030
|
// multi-line selection delete must reconcile synchronously so React
|
package/useCode/Pre.mjs
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
var _kbd;
|
|
4
4
|
import * as React from 'react';
|
|
5
5
|
import { useEditable } from "./useEditable.mjs";
|
|
6
|
-
import { fallbackToHast } from "../CodeHighlighter/fallbackFormat.mjs";
|
|
6
|
+
import { fallbackToHast, fallbackIsHighlighted } from "../CodeHighlighter/fallbackFormat.mjs";
|
|
7
7
|
import { useCodeContext } from "../CodeProvider/CodeContext.mjs";
|
|
8
8
|
import { hastToJsx, frameFallbackFromSpans } from "../pipeline/hastUtils/index.mjs";
|
|
9
9
|
import { stripHighlightingSpans } from "../pipeline/hastUtils/stripHighlightingSpans.mjs";
|
|
10
10
|
import { decodeHastSource } from "../pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs";
|
|
11
|
-
import { COLLAPSED_VISIBLE_FRAME_TYPES, resolveCollapsedFrameType } from "../pipeline/parseSource/frameVisibility.mjs";
|
|
11
|
+
import { COLLAPSED_VISIBLE_FRAME_TYPES, resolveCollapsedFrameType, getInitialVisibleFrames } from "../pipeline/parseSource/frameVisibility.mjs";
|
|
12
12
|
import { isFrameSpan } from "../pipeline/parseSource/isFrameSpan.mjs";
|
|
13
13
|
import { getSourceLineCounts } from "./sourceLineCounts.mjs";
|
|
14
14
|
import { subscribeToggleNudge } from "./subscribeToggleNudge.mjs";
|
|
@@ -25,44 +25,6 @@ function resolveFrameTypeAttribute(frameType, collapseToEmpty) {
|
|
|
25
25
|
const resolved = resolveCollapsedFrameType(frameType, collapseToEmpty);
|
|
26
26
|
return resolved && resolved !== 'normal' ? resolved : undefined;
|
|
27
27
|
}
|
|
28
|
-
function getInitialVisibleFrames(hast, collapseToEmpty = false) {
|
|
29
|
-
if (!hast) {
|
|
30
|
-
return collapseToEmpty ? {} : {
|
|
31
|
-
0: true
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Collapse-to-empty renders an empty collapsed window — no frame is visible while
|
|
36
|
-
// collapsed, regardless of the precomputed frame types.
|
|
37
|
-
if (collapseToEmpty) {
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
const visibleFrames = {};
|
|
41
|
-
let frameIndex = 0;
|
|
42
|
-
let hasVisibleEmphasisFrame = false;
|
|
43
|
-
hast.children.forEach(child => {
|
|
44
|
-
if (child.type !== 'element' || !isFrameSpan(child)) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const frameType = child.properties.dataFrameType;
|
|
48
|
-
if (typeof frameType === 'string' && COLLAPSED_VISIBLE_FRAME_TYPES.has(frameType)) {
|
|
49
|
-
visibleFrames[frameIndex] = true;
|
|
50
|
-
hasVisibleEmphasisFrame = true;
|
|
51
|
-
}
|
|
52
|
-
frameIndex += 1;
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Collapse-to-nothing (oversizedFocus: 'hide'): `focusedLines === 0` means
|
|
56
|
-
// the collapsed window is intentionally empty, so skip the first-frame
|
|
57
|
-
// fallback and keep every frame hidden when collapsed.
|
|
58
|
-
if (hast.data?.focusedLines === 0) {
|
|
59
|
-
return visibleFrames;
|
|
60
|
-
}
|
|
61
|
-
if (!hasVisibleEmphasisFrame && frameIndex > 0) {
|
|
62
|
-
visibleFrames[0] = true;
|
|
63
|
-
}
|
|
64
|
-
return visibleFrames;
|
|
65
|
-
}
|
|
66
28
|
|
|
67
29
|
/**
|
|
68
30
|
* Bounds describing the visible region of a collapsible code block in its
|
|
@@ -280,16 +242,32 @@ export function Pre({
|
|
|
280
242
|
editActivation,
|
|
281
243
|
onActivate
|
|
282
244
|
}) {
|
|
245
|
+
// Defer the decompressing `decodeHastSource` to a post-paint render ONLY when the
|
|
246
|
+
// first-paint `.fallback` is ALREADY highlighted — i.e. the promoted highlighted-visible
|
|
247
|
+
// fallback the server ships for `highlightAt: 'init'`. Then paint that highlighted
|
|
248
|
+
// fallback first (no decompression on the critical path) and swap in the full decoded
|
|
249
|
+
// tree after. When the fallback is plain — every other mode, including a late-mounted
|
|
250
|
+
// `'hydration'` block where `shouldHighlight` is also true on the first render — decode
|
|
251
|
+
// on mount instead, so we never flash plain → highlighted.
|
|
252
|
+
const [deferInitialDecode] = React.useState(() => shouldHighlight === true && !!fallback && fallbackIsHighlighted(fallback) && typeof children !== 'string');
|
|
253
|
+
const [decodeAllowed, setDecodeAllowed] = React.useState(!deferInitialDecode);
|
|
254
|
+
React.useEffect(() => {
|
|
255
|
+
if (deferInitialDecode) {
|
|
256
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional post-paint latch: flip after the first paint so the highlighted fallback shows before the decompressing decode runs
|
|
257
|
+
setDecodeAllowed(true);
|
|
258
|
+
}
|
|
259
|
+
}, [deferInitialDecode]);
|
|
260
|
+
|
|
283
261
|
// The variant `fallback` is forwarded to `decodeHastSource` so the
|
|
284
262
|
// `hastCompressed` payload is decompressed with the matching DEFLATE
|
|
285
263
|
// dictionary and each frame's `data.fallback` is restored. The decoded
|
|
286
264
|
// tree stays shared (read-only), since `Pre` only reads it.
|
|
287
265
|
const hast = React.useMemo(() => {
|
|
288
|
-
if (!children || typeof children === 'string') {
|
|
266
|
+
if (!children || typeof children === 'string' || !decodeAllowed) {
|
|
289
267
|
return null;
|
|
290
268
|
}
|
|
291
269
|
return decodeHastSource(children, fallback);
|
|
292
|
-
}, [children, fallback]);
|
|
270
|
+
}, [children, fallback, decodeAllowed]);
|
|
293
271
|
|
|
294
272
|
// Variant-swap bridge descriptor. While a variant swap is in flight
|
|
295
273
|
// and the partner variant is taller than this one, we render an
|
|
@@ -356,6 +334,12 @@ export function Pre({
|
|
|
356
334
|
// cursor on first paint.
|
|
357
335
|
const [editableReady, setEditableReady] = React.useState(false);
|
|
358
336
|
React.useLayoutEffect(() => {
|
|
337
|
+
// Deliberate two-pass mount gate: defer engine activation until the
|
|
338
|
+
// `bindPre` callback ref has committed (see 527-533). Flipping this true
|
|
339
|
+
// in a layout effect is the documented trigger for the no-flash /
|
|
340
|
+
// cursor-retention behavior and isn't derivable during render (the ref is
|
|
341
|
+
// intentionally null in render 1).
|
|
342
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
359
343
|
setEditableReady(true);
|
|
360
344
|
}, []);
|
|
361
345
|
const onEditableChange = React.useCallback((text, position, preParsed) => {
|
|
@@ -398,6 +382,12 @@ export function Pre({
|
|
|
398
382
|
// keeping the update outside the render phase while still avoiding a
|
|
399
383
|
// visible flash of un-hydrated emphasis frames.
|
|
400
384
|
React.useLayoutEffect(() => {
|
|
385
|
+
// Next state unions the new initial-visible set onto `prev` rather than
|
|
386
|
+
// replacing it (see 564-597): replacing would drop frames already
|
|
387
|
+
// hydrated by IO/editing on the prior tree, causing the visible flash this
|
|
388
|
+
// guards against. Depends on both a prop (`hast`) and prior state, so it
|
|
389
|
+
// can't be derived during render.
|
|
390
|
+
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
401
391
|
setVisibleFrames(prev => {
|
|
402
392
|
const initial = getInitialVisibleFrames(hast, collapseToEmpty);
|
|
403
393
|
let merged;
|
|
@@ -976,13 +966,18 @@ export function Pre({
|
|
|
976
966
|
// FRAMED — the compact `fallback` (the loader's windowed plain-text frames) when one
|
|
977
967
|
// travelled with it, otherwise a single-frame wrap — so the `<code>` is never bare
|
|
978
968
|
// text. The highlighted tree swaps in via `frames` once parsing completes.
|
|
979
|
-
|
|
980
|
-
|
|
969
|
+
// The fallback render, used whenever the decoded `hast` isn't in hand: a string
|
|
970
|
+
// source (highlighted on the client after hydration) or an object source whose
|
|
971
|
+
// decode is deferred off the first paint (`deferInitialDecode`). For `init` the
|
|
972
|
+
// server-built `fallback` already carries the initially-visible frames
|
|
973
|
+
// highlighted, so this first paint is highlighted with no decompression.
|
|
974
|
+
const framedFallback = React.useMemo(() => {
|
|
975
|
+
const frameNodes = fallback ?? (typeof children === 'string' ? [['span', 'frame', {
|
|
976
|
+
dataFrameType: 'focus'
|
|
977
|
+
}, children]] : null);
|
|
978
|
+
if (!frameNodes) {
|
|
981
979
|
return null;
|
|
982
980
|
}
|
|
983
|
-
const frameNodes = fallback ?? [['span', 'frame', {
|
|
984
|
-
dataFrameType: 'focus'
|
|
985
|
-
}, children]];
|
|
986
981
|
const root = fallbackToHast(frameNodes);
|
|
987
982
|
if (collapseToEmpty) {
|
|
988
983
|
for (const child of root.children) {
|
|
@@ -1020,7 +1015,7 @@ export function Pre({
|
|
|
1020
1015
|
"data-collapsible": hasCollapsibleFrames ? '' : undefined,
|
|
1021
1016
|
"data-total-lines": sourceTotalLines,
|
|
1022
1017
|
"data-focused-lines": sourceFocusedLines,
|
|
1023
|
-
children:
|
|
1018
|
+
children: hast ? frames : framedFallback
|
|
1024
1019
|
})
|
|
1025
1020
|
});
|
|
1026
1021
|
if (!isEditable) {
|
|
@@ -207,20 +207,33 @@ export function shiftComments(comments, lineDelta, position, existingCollapseMap
|
|
|
207
207
|
// O(1) lookup against the precomputed empty-line set from the old source.
|
|
208
208
|
const oldEmptyLineSet = oldEmptyLines && oldEmptyLines.length > 0 ? new Set(oldEmptyLines) : undefined;
|
|
209
209
|
|
|
210
|
-
// For a deletion, find range bases
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
210
|
+
// For a deletion, find range bases that are removed entirely — every line they
|
|
211
|
+
// highlighted is gone — so the frame must disappear (its `-start`, which is in
|
|
212
|
+
// the deleted block, is stashed rather than collapsed onto a surviving line
|
|
213
|
+
// above as a phantom highlight) and an undo can rebuild it.
|
|
214
|
+
//
|
|
215
|
+
// Two shapes qualify. Both require the `-start` to fall inside the deleted block
|
|
216
|
+
// `(editLine, editLine - lineDelta]`:
|
|
217
|
+
// 1. The `-end` is also inside the block (the selection deleted right through
|
|
218
|
+
// it). Both markers are stashed and an undo restores them at their offsets.
|
|
219
|
+
// 2. The `-end` is on `boundaryLine` — the first SURVIVING line just past the
|
|
220
|
+
// block. A range's `-end` is EXCLUSIVE, sitting one line below its last
|
|
221
|
+
// highlighted line, so a selection that removes every highlighted line stops
|
|
222
|
+
// with the caret on that `-end` line, leaving it intact. The range is still
|
|
223
|
+
// empty, so it is fully deleted; the surviving `-end` then shifts up as a
|
|
224
|
+
// lone marker (rendering nothing) and is the undo memory that re-pairs with
|
|
225
|
+
// the restored `-start`.
|
|
226
|
+
// (A range whose `-start` survives OUTSIDE the block only shrinks, so its `-end`
|
|
227
|
+
// keeps the editLine+1 placement below and stays untracked.)
|
|
217
228
|
let fullyDeletedRanges;
|
|
218
229
|
if (lineDelta < 0) {
|
|
219
230
|
const startBases = new Set();
|
|
220
231
|
const endBases = new Set();
|
|
232
|
+
const boundaryLine = editLine - lineDelta + 1;
|
|
221
233
|
for (const [lineStr, commentArr] of Object.entries(comments ?? {})) {
|
|
222
234
|
const line = Number(lineStr);
|
|
223
|
-
|
|
235
|
+
const inBlock = line > editLine && line <= editLine - lineDelta;
|
|
236
|
+
if (inBlock) {
|
|
224
237
|
for (const comment of commentArr) {
|
|
225
238
|
if (comment.endsWith('-end')) {
|
|
226
239
|
endBases.add(comment.slice(0, -'-end'.length));
|
|
@@ -228,6 +241,14 @@ export function shiftComments(comments, lineDelta, position, existingCollapseMap
|
|
|
228
241
|
startBases.add(comment.slice(0, -'-start'.length));
|
|
229
242
|
}
|
|
230
243
|
}
|
|
244
|
+
} else if (line === boundaryLine) {
|
|
245
|
+
// Only an exclusive `-end` on the boundary closes a fully-deleted range; a
|
|
246
|
+
// `-start` here belongs to a surviving line below and must not be paired.
|
|
247
|
+
for (const comment of commentArr) {
|
|
248
|
+
if (comment.endsWith('-end')) {
|
|
249
|
+
endBases.add(comment.slice(0, -'-end'.length));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
231
252
|
}
|
|
232
253
|
}
|
|
233
254
|
for (const base of startBases) {
|