@mui/internal-docs-infra 0.11.1-canary.17 → 0.11.1-canary.19

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.
Files changed (58) hide show
  1. package/CodeHighlighter/CodeHighlighterClient.mjs +9 -1
  2. package/CodeHighlighter/buildStringFallback.d.mts +29 -0
  3. package/CodeHighlighter/buildStringFallback.mjs +42 -0
  4. package/CodeHighlighter/codeToFallbackProps.d.mts +4 -1
  5. package/CodeHighlighter/codeToFallbackProps.mjs +94 -11
  6. package/CodeHighlighter/createClientProps.mjs +10 -1
  7. package/CodeHighlighter/fallbackCompression.d.mts +19 -2
  8. package/CodeHighlighter/fallbackCompression.mjs +26 -3
  9. package/CodeHighlighter/fallbackFormat.d.mts +7 -1
  10. package/CodeHighlighter/fallbackFormat.mjs +11 -5
  11. package/CodeHighlighter/mergeComments.mjs +1 -1
  12. package/CodeHighlighter/prepareInitialSource.mjs +145 -4
  13. package/CodeHighlighter/types.d.mts +92 -12
  14. package/CodeHighlighter/useCodeFallback.d.mts +45 -2
  15. package/CodeHighlighter/useCodeFallback.mjs +82 -10
  16. package/abstractCreateDemo/abstractCreateDemo.d.mts +52 -3
  17. package/abstractCreateDemo/abstractCreateDemo.mjs +35 -1
  18. package/abstractCreateDemo/resolveDemoFlag.d.mts +20 -0
  19. package/abstractCreateDemo/resolveDemoFlag.mjs +25 -0
  20. package/package.json +2 -2
  21. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +11 -0
  22. package/pipeline/hastUtils/hastDictionary.mjs +9 -0
  23. package/pipeline/hastUtils/stripHighlightingSpans.mjs +2 -7
  24. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransformWithComments.mjs +2 -1
  25. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +10 -2
  26. package/pipeline/loadIsomorphicCodeVariant/loadCodeFallback.mjs +17 -5
  27. package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +80 -16
  28. package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +3 -6
  29. package/pipeline/loadServerTypesText/order.mjs +1 -1
  30. package/pipeline/loaderUtils/index.d.mts +0 -1
  31. package/pipeline/loaderUtils/index.mjs +0 -1
  32. package/pipeline/loaderUtils/parseImportsAndComments.d.mts +5 -1
  33. package/pipeline/loaderUtils/parseImportsAndComments.mjs +19 -9
  34. package/pipeline/loaderUtils/resolveModulePath.mjs +23 -1
  35. package/pipeline/parseSource/addLineGutters.mjs +2 -1
  36. package/pipeline/parseSource/calculateFrameRanges.d.mts +22 -0
  37. package/pipeline/parseSource/calculateFrameRanges.mjs +69 -25
  38. package/pipeline/parseSource/frameVisibility.d.mts +26 -1
  39. package/pipeline/parseSource/frameVisibility.mjs +42 -1
  40. package/pipeline/parseSource/isFrameSpan.d.mts +19 -0
  41. package/pipeline/parseSource/isFrameSpan.mjs +24 -0
  42. package/pipeline/parseSource/parseSource.d.mts +16 -0
  43. package/pipeline/parseSource/parseSource.mjs +17 -0
  44. package/pipeline/parseSource/restructureFrames.mjs +2 -1
  45. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.d.mts +26 -0
  46. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +37 -3
  47. package/useCode/Pre.d.mts +19 -0
  48. package/useCode/Pre.mjs +99 -20
  49. package/useCode/sourceLineCounts.d.mts +3 -3
  50. package/useCode/sourceLineCounts.mjs +38 -20
  51. package/useCode/useCode.d.mts +0 -1
  52. package/useCode/useCode.mjs +15 -2
  53. package/useCode/useFileNavigation.d.mts +7 -0
  54. package/useCode/useFileNavigation.mjs +26 -4
  55. package/useCode/useUIState.d.mts +2 -2
  56. package/useCode/useUIState.mjs +2 -2
  57. package/pipeline/loaderUtils/convertCommentsToOneIndexed.d.mts +0 -8
  58. package/pipeline/loaderUtils/convertCommentsToOneIndexed.mjs +0 -16
@@ -1025,7 +1025,15 @@ export function CodeHighlighterClient(props) {
1025
1025
  }
1026
1026
  let restored = residualMap ? scatterResidualFallbacks(base, residualMap) : base;
1027
1027
  if (!props.fallbackCollapsed) {
1028
- restored = scatterResidualFallbacks(restored, hoistedFallbackHasts);
1028
+ // `preserveExisting`: never let the hoist overwrite a `fallback` already
1029
+ // on the variant. A fully-loaded `hastCompressed` source carries its own
1030
+ // source-paired (structured) `fallback`, which is the only valid DEFLATE
1031
+ // dictionary. The hoist can be an un-highlighted *raw-string* fallback
1032
+ // whose text keeps a trailing newline `buildRootFallback` drops, so
1033
+ // overwriting the structured one makes `decodeHastSource` throw a
1034
+ // dictionary mismatch. The hoist is the dictionary only when the variant's
1035
+ // own was stripped, so apply it solely where one isn't already present.
1036
+ restored = scatterResidualFallbacks(restored, hoistedFallbackHasts, true);
1029
1037
  }
1030
1038
  return restored;
1031
1039
  }, [residualMap, hoistedFallbackHasts, props.fallbackCollapsed]);
@@ -0,0 +1,29 @@
1
+ import type { SourceComments, SourceEnhancers } from "./types.mjs";
2
+ import { type FallbackNode } from "./fallbackFormat.mjs";
3
+ export interface StringFallbackResult {
4
+ /** Compact, windowed fallback frames (text only — `.line` spans stripped). */
5
+ fallback: FallbackNode[];
6
+ /** Total source lines. */
7
+ totalLines: number;
8
+ /** Lines visible in the collapsed window (the sum of visible frame sizes). */
9
+ focusedLines: number;
10
+ /** Whether the enhanced frame structure has hidden content to expand into. */
11
+ collapsible: boolean;
12
+ }
13
+ /**
14
+ * Derive a *windowed* fallback for a plain-string source by running the same
15
+ * `sourceEnhancers` the live render uses over a cheap line-guttered HAST
16
+ * (`parsePlainText` — gutters, no syntax highlighting). The inline-string
17
+ * fallback path otherwise wraps the whole source in one un-windowed focus frame,
18
+ * so an oversized / `@focus` / `@highlight` block paints its full text before
19
+ * hydration then snaps to the collapsed window. Running the enhancers here makes
20
+ * the loading frames match the live render, and the resulting `root.data` carries
21
+ * the `totalLines` / `focusedLines` the compact fallback can't preserve.
22
+ *
23
+ * Synchronous by design — it runs at server fallback-prep time inside the
24
+ * (sync) `prepareInitialSource`. An enhancer that returns a promise is skipped
25
+ * (returns `undefined`) so the caller falls back to the naive single-frame wrap
26
+ * rather than blocking; the built-in `enhanceCodeEmphasis` is synchronous, so
27
+ * the common case windows.
28
+ */
29
+ export declare function buildStringFallback(source: string, comments: SourceComments | undefined, fileName: string, sourceEnhancers: SourceEnhancers): StringFallbackResult | undefined;
@@ -0,0 +1,42 @@
1
+ import { buildRootFallback } from "./fallbackFormat.mjs";
2
+ import { parsePlainText } from "../pipeline/parseSource/index.mjs";
3
+ function isPromiseLike(value) {
4
+ return typeof value === 'object' && value !== null && typeof value.then === 'function';
5
+ }
6
+
7
+ /**
8
+ * Derive a *windowed* fallback for a plain-string source by running the same
9
+ * `sourceEnhancers` the live render uses over a cheap line-guttered HAST
10
+ * (`parsePlainText` — gutters, no syntax highlighting). The inline-string
11
+ * fallback path otherwise wraps the whole source in one un-windowed focus frame,
12
+ * so an oversized / `@focus` / `@highlight` block paints its full text before
13
+ * hydration then snaps to the collapsed window. Running the enhancers here makes
14
+ * the loading frames match the live render, and the resulting `root.data` carries
15
+ * the `totalLines` / `focusedLines` the compact fallback can't preserve.
16
+ *
17
+ * Synchronous by design — it runs at server fallback-prep time inside the
18
+ * (sync) `prepareInitialSource`. An enhancer that returns a promise is skipped
19
+ * (returns `undefined`) so the caller falls back to the naive single-frame wrap
20
+ * rather than blocking; the built-in `enhanceCodeEmphasis` is synchronous, so
21
+ * the common case windows.
22
+ */
23
+ export function buildStringFallback(source, comments, fileName, sourceEnhancers) {
24
+ let root = parsePlainText(source);
25
+ for (const enhancer of sourceEnhancers) {
26
+ const result = enhancer(root, comments, fileName);
27
+ if (isPromiseLike(result)) {
28
+ return undefined;
29
+ }
30
+ root = result;
31
+ }
32
+ const data = root.data;
33
+ const totalLines = data?.totalLines ?? 0;
34
+ const focusedLines = data?.focusedLines ?? totalLines;
35
+ const collapsible = data?.collapsible === true;
36
+ return {
37
+ fallback: buildRootFallback(root),
38
+ totalLines,
39
+ focusedLines,
40
+ collapsible
41
+ };
42
+ }
@@ -1,5 +1,8 @@
1
1
  import type { BaseContentLoadingProps, Code, Fallbacks } from "./types.mjs";
2
- export declare function codeToFallbackProps(variant: string, code?: Code, _fileName?: string, _needsAllFiles?: boolean, needsAllVariants?: boolean, allFallbackHasts?: Record<string, Fallbacks>): BaseContentLoadingProps;
2
+ import { type SourceLineCounts } from "../useCode/sourceLineCounts.mjs";
3
+ /** Per-variant → per-file line metadata threaded for the fallback. */
4
+ export type LineCountsByVariant = Record<string, Record<string, SourceLineCounts>>;
5
+ export declare function codeToFallbackProps(variant: string, code?: Code, _fileName?: string, _needsAllFiles?: boolean, needsAllVariants?: boolean, allFallbackHasts?: Record<string, Fallbacks>, allLineCounts?: LineCountsByVariant): BaseContentLoadingProps;
3
6
  /**
4
7
  * Read a variant's per-file fallbacks straight off its `VariantCode` `fallback`
5
8
  * fields (main + extra files), returning a `Fallbacks` map keyed by file name.
@@ -1,5 +1,8 @@
1
1
  import { hastToFallback } from "./fallbackFormat.mjs";
2
2
  import { getLanguageFromExtension } from "../pipeline/loaderUtils/getLanguageFromExtension.mjs";
3
+ import { getVariantFileLineCounts } from "../useCode/sourceLineCounts.mjs";
4
+
5
+ /** Per-variant → per-file line metadata threaded for the fallback. */
3
6
 
4
7
  /**
5
8
  * Resolve a `language-{language}` hint for a file from its extension, used to
@@ -23,9 +26,10 @@ function getLanguageFromFileName(fileName) {
23
26
  * one from the source for live/dev trees that never went through the loader.
24
27
  *
25
28
  * A plain-string source (an unparsed code block, e.g. `<CodeHighlighter>{code}`)
26
- * becomes a single text node so the fallback renders the raw code before
27
- * highlighting. `hastCompressed` payloads can't be decoded here (no DEFLATE
28
- * dictionary), so without a variant `fallback` they yield `undefined`.
29
+ * is wrapped in a single focus frame so the fallback always has the same frame
30
+ * structure as the highlighted render never a bare text node. `hastCompressed`
31
+ * payloads can't be decoded here (no DEFLATE dictionary), so without a variant
32
+ * `fallback` they yield `undefined`.
29
33
  */
30
34
  function sourceToFallback(source, fallback) {
31
35
  if (fallback) {
@@ -35,8 +39,13 @@ function sourceToFallback(source, fallback) {
35
39
  return undefined;
36
40
  }
37
41
  if (typeof source === 'string') {
38
- // A `FallbackNode` string is a text node render the raw code as-is.
39
- return [source];
42
+ // Wrap the raw code in a single focus frame so the fallback always carries a
43
+ // frame, matching the highlighted render (`buildRootFallback` likewise emits
44
+ // frames with text children) — never a bare text node. The whole source is
45
+ // the visible window; collapse-to-empty demotes it like any other focus frame.
46
+ return [['span', 'frame', {
47
+ dataFrameType: 'focus'
48
+ }, source]];
40
49
  }
41
50
  if ('type' in source && source.type === 'root') {
42
51
  return hastToFallback(source);
@@ -57,9 +66,40 @@ function sourceToFallback(source, fallback) {
57
66
  * `source` is present, mirroring how consumers gate the `language-{language}`
58
67
  * class behind a rendered source.
59
68
  */
60
- function deriveVariantSources(variantCode, variantHasts) {
69
+ function deriveVariantSources(variantCode, variantHasts, variantLineCounts) {
61
70
  const fileNames = [variantCode.fileName, ...Object.keys(variantCode.extraFiles || {})].filter(name => Boolean(name));
62
71
  const mainFile = variantCode.fileName || fileNames[0];
72
+
73
+ // Per-file line counts: prefer render-time windowing (`variantLineCounts`), else
74
+ // the counts the loader stored on the code (`VariantCode` / extra-file `totalLines`
75
+ // / `focusedLines`). So every file/variant carries its window, not just the main one.
76
+ const fileCounts = fileName => {
77
+ const threaded = variantLineCounts?.[fileName];
78
+ if (threaded) {
79
+ return threaded;
80
+ }
81
+ const file = variantCode.fileName === fileName ? variantCode : variantCode.extraFiles?.[fileName];
82
+ if (file && typeof file !== 'string' && file.totalLines !== undefined) {
83
+ return {
84
+ totalLines: file.totalLines,
85
+ focusedLines: file.focusedLines ?? file.totalLines,
86
+ collapsible: file.collapsible === true
87
+ };
88
+ }
89
+ // Last resort: count lines off the source (a plain string with no enhancers ⇒
90
+ // `focusedLines === totalLines`, matching `<Pre>`'s `getSourceLineCounts`). Guarded
91
+ // because a `hastCompressed` source can't be decoded without its dictionary (which
92
+ // the server strips before this runs) — the server passes `variantLineCounts`
93
+ // instead, so this branch only really fires client-side where the dictionary is on
94
+ // the code.
95
+ try {
96
+ const counts = getVariantFileLineCounts(variantCode, fileName);
97
+ // `totalLines === 0` means a hast with no `root.data` counts (not a real count).
98
+ return counts && counts.totalLines > 0 ? counts : undefined;
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ };
63
103
  let source;
64
104
  let extraSource;
65
105
  if (variantHasts) {
@@ -70,7 +110,10 @@ function deriveVariantSources(variantCode, variantHasts) {
70
110
  const extra = {};
71
111
  for (const [fName, nodes] of Object.entries(variantHasts)) {
72
112
  if (fName !== mainFile) {
73
- extra[fName] = nodes;
113
+ extra[fName] = {
114
+ source: nodes,
115
+ ...fileCounts(fName)
116
+ };
74
117
  }
75
118
  }
76
119
  if (Object.keys(extra).length > 0) {
@@ -85,7 +128,10 @@ function deriveVariantSources(variantCode, variantHasts) {
85
128
  if (typeof fData === 'object' && fData.source) {
86
129
  const fb = sourceToFallback(fData.source, fData.fallback);
87
130
  if (fb) {
88
- extra[fName] = fb;
131
+ extra[fName] = {
132
+ source: fb,
133
+ ...fileCounts(fName)
134
+ };
89
135
  }
90
136
  }
91
137
  }
@@ -94,9 +140,13 @@ function deriveVariantSources(variantCode, variantHasts) {
94
140
  }
95
141
  }
96
142
  const language = source ? variantCode.language ?? getLanguageFromFileName(mainFile) : undefined;
143
+ const mainCounts = source && mainFile ? fileCounts(mainFile) : undefined;
97
144
  return {
98
145
  fileNames,
99
146
  source,
147
+ totalLines: mainCounts?.totalLines,
148
+ focusedLines: mainCounts?.focusedLines,
149
+ collapsible: mainCounts?.collapsible,
100
150
  extraSource,
101
151
  language
102
152
  };
@@ -107,7 +157,7 @@ export function codeToFallbackProps(variant, code,
107
157
  // upstream in `stripFallbackHastsFromCode` (which only hoists the allowed
108
158
  // files into `allFallbackHasts`), so the derivation below reads them off the
109
159
  // already-gated `allFallbackHasts` rather than re-applying the flags here.
110
- _fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts) {
160
+ _fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts, allLineCounts) {
111
161
  const variantCode = code?.[variant];
112
162
  if (!variantCode || typeof variantCode === 'string') {
113
163
  return {};
@@ -115,23 +165,38 @@ _fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts) {
115
165
  const {
116
166
  fileNames,
117
167
  source,
168
+ totalLines,
169
+ focusedLines,
170
+ collapsible,
118
171
  extraSource,
119
172
  language
120
- } = deriveVariantSources(variantCode, allFallbackHasts?.[variant]);
173
+ } = deriveVariantSources(variantCode, allFallbackHasts?.[variant], allLineCounts?.[variant]);
121
174
  if (needsAllVariants) {
122
175
  const extraVariants = Object.entries(code || {}).reduce((acc, [name, vCode]) => {
123
176
  if (name !== variant && vCode && typeof vCode !== 'string') {
124
177
  const {
125
178
  fileNames: evFileNames,
126
179
  source: evSource,
180
+ totalLines: evTotalLines,
181
+ focusedLines: evFocusedLines,
182
+ collapsible: evCollapsible,
127
183
  extraSource: evExtraSource,
128
184
  language: evLanguage
129
- } = deriveVariantSources(vCode, allFallbackHasts?.[name]);
185
+ } = deriveVariantSources(vCode, allFallbackHasts?.[name], allLineCounts?.[name]);
130
186
  acc[name] = {
131
187
  fileNames: evFileNames,
132
188
  ...(evSource ? {
133
189
  source: evSource
134
190
  } : undefined),
191
+ ...(evTotalLines !== undefined ? {
192
+ totalLines: evTotalLines
193
+ } : undefined),
194
+ ...(evFocusedLines !== undefined ? {
195
+ focusedLines: evFocusedLines
196
+ } : undefined),
197
+ ...(evCollapsible !== undefined ? {
198
+ collapsible: evCollapsible
199
+ } : undefined),
135
200
  ...(evLanguage ? {
136
201
  language: evLanguage
137
202
  } : undefined),
@@ -147,6 +212,15 @@ _fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts) {
147
212
  ...(source ? {
148
213
  source
149
214
  } : undefined),
215
+ ...(totalLines !== undefined ? {
216
+ totalLines
217
+ } : undefined),
218
+ ...(focusedLines !== undefined ? {
219
+ focusedLines
220
+ } : undefined),
221
+ ...(collapsible !== undefined ? {
222
+ collapsible
223
+ } : undefined),
150
224
  ...(language ? {
151
225
  language
152
226
  } : undefined),
@@ -161,6 +235,15 @@ _fileName, _needsAllFiles = false, needsAllVariants = false, allFallbackHasts) {
161
235
  ...(source ? {
162
236
  source
163
237
  } : undefined),
238
+ ...(totalLines !== undefined ? {
239
+ totalLines
240
+ } : undefined),
241
+ ...(focusedLines !== undefined ? {
242
+ focusedLines
243
+ } : undefined),
244
+ ...(collapsible !== undefined ? {
245
+ collapsible
246
+ } : undefined),
164
247
  ...(language ? {
165
248
  language
166
249
  } : undefined),
@@ -25,7 +25,16 @@ export function createClientProps(props) {
25
25
  name: props.name,
26
26
  slug: props.slug,
27
27
  url,
28
- variantType: props.variantType
28
+ variantType: props.variantType,
29
+ // Thread the render-time display flags into the content channel so both
30
+ // `useCode`/`<Pre>` and the loading fallback honor them. A top-level prop
31
+ // wins over one already present in `contentProps`.
32
+ ...(props.collapseToEmpty !== undefined ? {
33
+ collapseToEmpty: props.collapseToEmpty
34
+ } : undefined),
35
+ ...(props.initialExpanded !== undefined ? {
36
+ initialExpanded: props.initialExpanded
37
+ } : undefined)
29
38
  };
30
39
  return {
31
40
  url,
@@ -61,16 +61,33 @@ export declare function extractResidualFallbacks(code: Code): {
61
61
  * non-consolidated payload would have had, so downstream consumers are unaware
62
62
  * the residual ever travelled compressed.
63
63
  *
64
+ * `preserveExisting` keeps any `fallback` already on the variant/extra file
65
+ * instead of overwriting it. The hoisted-fallback scatter passes `true`: the
66
+ * hoist is an *initial-paint* dictionary that, for an un-highlighted load, is a
67
+ * raw-string fallback whose text keeps a trailing newline that `buildRootFallback`
68
+ * drops — so it's the WRONG DEFLATE dictionary for a fully-loaded `hastCompressed`
69
+ * source, which already carries its own source-paired (structured) `fallback`.
70
+ * Overwriting that with the hoist makes `decodeHastSource` throw a dictionary
71
+ * mismatch. The hoist is only the dictionary when the variant's own was stripped,
72
+ * so apply it only where one isn't already present. The residual-blob scatter
73
+ * keeps the default (`false`): it always writes onto server-stripped, fallback-less
74
+ * variants, so there is nothing to preserve.
75
+ *
64
76
  * Pure: only the variants that regain a fallback are shallow-cloned.
65
77
  */
66
- export declare function scatterResidualFallbacks(code: Code, residual: ResidualFallbacks): Code;
78
+ export declare function scatterResidualFallbacks(code: Code, residual: ResidualFallbacks, preserveExisting?: boolean): Code;
67
79
  /**
68
80
  * Reduce every fallback in a rendered-subset map to its collapsed window (see
69
81
  * `collapsedVisibleFallback`). Used by `fallbackCollapsed` to hand
70
82
  * `ContentLoading` only the on-screen lines while the full fallbacks ride along
71
83
  * in the residual blob.
84
+ *
85
+ * `collapsesToEmpty(variantName, fileName)` reports the `oversizedFocus: 'hide'`
86
+ * collapse-to-nothing case (the source's `focusedLines === 0`): such files get
87
+ * an empty collapsed window so the loading UI matches the hydrated render
88
+ * instead of briefly painting the first frame.
72
89
  */
73
- export declare function collapseRenderedFallbacks(rendered: ResidualFallbacks): ResidualFallbacks;
90
+ export declare function collapseRenderedFallbacks(rendered: ResidualFallbacks, collapsesToEmpty?: (variantName: string, fileName: string) => boolean): ResidualFallbacks;
74
91
  /**
75
92
  * Deep-merge two residual maps (`variant → fileName → fallback`), with `b`
76
93
  * winning on conflicts. Used to fold the rendered files' full fallbacks into
@@ -146,9 +146,21 @@ export function extractResidualFallbacks(code) {
146
146
  * non-consolidated payload would have had, so downstream consumers are unaware
147
147
  * the residual ever travelled compressed.
148
148
  *
149
+ * `preserveExisting` keeps any `fallback` already on the variant/extra file
150
+ * instead of overwriting it. The hoisted-fallback scatter passes `true`: the
151
+ * hoist is an *initial-paint* dictionary that, for an un-highlighted load, is a
152
+ * raw-string fallback whose text keeps a trailing newline that `buildRootFallback`
153
+ * drops — so it's the WRONG DEFLATE dictionary for a fully-loaded `hastCompressed`
154
+ * source, which already carries its own source-paired (structured) `fallback`.
155
+ * Overwriting that with the hoist makes `decodeHastSource` throw a dictionary
156
+ * mismatch. The hoist is only the dictionary when the variant's own was stripped,
157
+ * so apply it only where one isn't already present. The residual-blob scatter
158
+ * keeps the default (`false`): it always writes onto server-stripped, fallback-less
159
+ * variants, so there is nothing to preserve.
160
+ *
149
161
  * Pure: only the variants that regain a fallback are shallow-cloned.
150
162
  */
151
- export function scatterResidualFallbacks(code, residual) {
163
+ export function scatterResidualFallbacks(code, residual, preserveExisting = false) {
152
164
  const restored = {};
153
165
  for (const [variantName, variant] of Object.entries(code)) {
154
166
  const files = residual[variantName];
@@ -160,6 +172,9 @@ export function scatterResidualFallbacks(code, residual) {
160
172
  let nextExtraFiles;
161
173
  for (const [fileName, fallback] of Object.entries(files)) {
162
174
  if (fileName === variant.fileName) {
175
+ if (preserveExisting && nextVariant.fallback) {
176
+ continue;
177
+ }
163
178
  nextVariant = {
164
179
  ...nextVariant,
165
180
  fallback
@@ -167,6 +182,9 @@ export function scatterResidualFallbacks(code, residual) {
167
182
  } else {
168
183
  const fileData = variant.extraFiles?.[fileName];
169
184
  if (fileData && typeof fileData === 'object') {
185
+ if (preserveExisting && fileData.fallback) {
186
+ continue;
187
+ }
170
188
  if (!nextExtraFiles) {
171
189
  nextExtraFiles = {
172
190
  ...variant.extraFiles
@@ -195,13 +213,18 @@ export function scatterResidualFallbacks(code, residual) {
195
213
  * `collapsedVisibleFallback`). Used by `fallbackCollapsed` to hand
196
214
  * `ContentLoading` only the on-screen lines while the full fallbacks ride along
197
215
  * in the residual blob.
216
+ *
217
+ * `collapsesToEmpty(variantName, fileName)` reports the `oversizedFocus: 'hide'`
218
+ * collapse-to-nothing case (the source's `focusedLines === 0`): such files get
219
+ * an empty collapsed window so the loading UI matches the hydrated render
220
+ * instead of briefly painting the first frame.
198
221
  */
199
- export function collapseRenderedFallbacks(rendered) {
222
+ export function collapseRenderedFallbacks(rendered, collapsesToEmpty) {
200
223
  const collapsed = {};
201
224
  for (const [variantName, files] of Object.entries(rendered)) {
202
225
  const collapsedFiles = {};
203
226
  for (const [fileName, fallback] of Object.entries(files)) {
204
- collapsedFiles[fileName] = collapsedVisibleFallback(fallback);
227
+ collapsedFiles[fileName] = collapsedVisibleFallback(fallback, collapsesToEmpty?.(variantName, fileName) ?? false);
205
228
  }
206
229
  collapsed[variantName] = collapsedFiles;
207
230
  }
@@ -86,8 +86,14 @@ export declare function redistributeRootFallback(root: HastRoot, fallback: Fallb
86
86
  * (the whole source is the focused window) the first frame stands in. Returns
87
87
  * the input unchanged when it has no frames at all.
88
88
  *
89
+ * When `collapsesToEmpty` is `true` the source records `focusedLines === 0`
90
+ * (the `oversizedFocus: 'hide'` collapse-to-nothing case): the collapsed window
91
+ * is intentionally empty, so the first-frame fallback is skipped and an empty
92
+ * array is returned. Mirrors the runtime rule in `Pre.tsx` /
93
+ * `getInitialVisibleSourceLines`.
94
+ *
89
95
  * Used by `fallbackCollapsed` to paint only the on-screen lines while the
90
96
  * file's full fallback rides along compressed (see the prop-compression
91
97
  * pattern's "Splitting the Fallback by Visibility").
92
98
  */
93
- export declare function collapsedVisibleFallback(fallback: FallbackNode[]): FallbackNode[];
99
+ export declare function collapsedVisibleFallback(fallback: FallbackNode[], collapsesToEmpty?: boolean): FallbackNode[];
@@ -1,4 +1,5 @@
1
1
  import { COLLAPSED_VISIBLE_FRAME_TYPES } from "../pipeline/parseSource/frameVisibility.mjs";
2
+ import { isFrameSpan } from "../pipeline/parseSource/isFrameSpan.mjs";
2
3
 
3
4
  /**
4
5
  * Compact serialization format for fallback HAST trees.
@@ -155,10 +156,6 @@ function nodeText(node) {
155
156
  }
156
157
  return children.map(nodeText).join('');
157
158
  }
158
- function isFrameSpan(element) {
159
- const className = element.properties?.className;
160
- return className === 'frame' || Array.isArray(className) && className.includes('frame');
161
- }
162
159
 
163
160
  /**
164
161
  * Collects the plain text of a frame from its `.line` spans and the newline
@@ -291,11 +288,20 @@ function fallbackFrameType(frame) {
291
288
  * (the whole source is the focused window) the first frame stands in. Returns
292
289
  * the input unchanged when it has no frames at all.
293
290
  *
291
+ * When `collapsesToEmpty` is `true` the source records `focusedLines === 0`
292
+ * (the `oversizedFocus: 'hide'` collapse-to-nothing case): the collapsed window
293
+ * is intentionally empty, so the first-frame fallback is skipped and an empty
294
+ * array is returned. Mirrors the runtime rule in `Pre.tsx` /
295
+ * `getInitialVisibleSourceLines`.
296
+ *
294
297
  * Used by `fallbackCollapsed` to paint only the on-screen lines while the
295
298
  * file's full fallback rides along compressed (see the prop-compression
296
299
  * pattern's "Splitting the Fallback by Visibility").
297
300
  */
298
- export function collapsedVisibleFallback(fallback) {
301
+ export function collapsedVisibleFallback(fallback, collapsesToEmpty = false) {
302
+ if (collapsesToEmpty) {
303
+ return [];
304
+ }
299
305
  let firstFrame = -1;
300
306
  let firstVisible = -1;
301
307
  let lastVisible = -1;
@@ -76,5 +76,5 @@ function warnOnIndexingMismatch(input, mine) {
76
76
  if (Object.keys(other).length === 0) {
77
77
  return;
78
78
  }
79
- console.warn('mergeComments: inputs appear to use different line-indexing conventions ' + '(one contains a `0` key, the other does not). Both inputs must use the ' + 'same convention or markers will land on the wrong lines. The repository ' + "convention is 1-indexed; convert with `convertCommentsToOneIndexed` if you're emitting 0-indexed comments.");
79
+ console.warn('mergeComments: inputs appear to use different line-indexing conventions ' + '(one contains a `0` key, the other does not). Comments are 1-indexed everywhere ' + '(a `0` key means something emitted 0-indexed comments); both inputs must be ' + '1-indexed or markers will land on the wrong lines.');
80
80
  }