@mui/internal-docs-infra 0.11.1-canary.11 → 0.11.1-canary.13

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 (80) hide show
  1. package/CodeHighlighter/CodeHighlighter.mjs +6 -5
  2. package/CodeHighlighter/CodeHighlighterClient.mjs +141 -25
  3. package/CodeHighlighter/CodeHighlighterContext.d.mts +27 -0
  4. package/CodeHighlighter/codeToFallbackProps.mjs +23 -2
  5. package/CodeHighlighter/index.d.mts +2 -1
  6. package/CodeHighlighter/index.mjs +2 -1
  7. package/CodeHighlighter/mergeComments.d.mts +38 -0
  8. package/CodeHighlighter/mergeComments.mjs +80 -0
  9. package/CodeHighlighter/types.d.mts +127 -3
  10. package/CodeProvider/CodeContext.d.mts +0 -3
  11. package/CodeProvider/CodeProvider.mjs +2 -3
  12. package/LICENSE +1 -1
  13. package/package.json +23 -3
  14. package/pipeline/enhanceCodeEmphasis/enhanceCodeEmphasis.mjs +19 -0
  15. package/pipeline/hastUtils/hast.d.mts +27 -0
  16. package/pipeline/hastUtils/stripHighlightingSpans.d.mts +6 -2
  17. package/pipeline/hastUtils/stripHighlightingSpans.mjs +26 -9
  18. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.d.mts +52 -6
  19. package/pipeline/loadIsomorphicCodeVariant/applyCodeTransform.mjs +305 -23
  20. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.d.mts +5 -5
  21. package/pipeline/loadIsomorphicCodeVariant/computeHastDeltas.mjs +36 -66
  22. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.d.mts +15 -0
  23. package/pipeline/loadIsomorphicCodeVariant/decodeHastSource.mjs +51 -0
  24. package/pipeline/loadIsomorphicCodeVariant/diffHast.d.mts +26 -2
  25. package/pipeline/loadIsomorphicCodeVariant/diffHast.mjs +474 -19
  26. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.d.mts +49 -0
  27. package/pipeline/loadIsomorphicCodeVariant/embedTransforms.mjs +152 -0
  28. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.d.mts +51 -0
  29. package/pipeline/loadIsomorphicCodeVariant/findExpandingRanges.mjs +161 -0
  30. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.d.mts +12 -0
  31. package/pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs +44 -0
  32. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.d.mts +16 -0
  33. package/pipeline/loadIsomorphicCodeVariant/getInitialVisibleSourceLines.mjs +71 -0
  34. package/pipeline/loadIsomorphicCodeVariant/loadIsomorphicCodeVariant.mjs +45 -4
  35. package/pipeline/loadIsomorphicCodeVariant/transformSource.d.mts +2 -2
  36. package/pipeline/loadIsomorphicCodeVariant/transformSource.mjs +59 -22
  37. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.d.mts +8 -0
  38. package/pipeline/loadPrecomputedCodeHighlighter/loadPrecomputedCodeHighlighter.mjs +11 -7
  39. package/pipeline/parseSource/addLineGutters.mjs +27 -6
  40. package/pipeline/parseSource/restructureFrames.mjs +103 -14
  41. package/pipeline/transformHtmlCodeBlock/transformHtmlCodeBlock.mjs +146 -113
  42. package/pipeline/transformTypescriptToJavascript/removeTypes.d.mts +5 -8
  43. package/pipeline/transformTypescriptToJavascript/removeTypes.mjs +27 -93
  44. package/useCode/Pre.d.mts +90 -1
  45. package/useCode/Pre.mjs +286 -125
  46. package/useCode/sourceLineCounts.d.mts +79 -0
  47. package/useCode/sourceLineCounts.mjs +266 -0
  48. package/useCode/subscribeToggleNudge.d.mts +3 -0
  49. package/useCode/subscribeToggleNudge.mjs +95 -0
  50. package/useCode/useCode.d.mts +121 -0
  51. package/useCode/useCode.mjs +203 -14
  52. package/useCode/useCodeUtils.d.mts +141 -5
  53. package/useCode/useCodeUtils.mjs +339 -91
  54. package/useCode/useEditable.mjs +10 -0
  55. package/useCode/useFileNavigation.d.mts +64 -2
  56. package/useCode/useFileNavigation.mjs +106 -29
  57. package/useCode/useHighlightGate.d.mts +17 -0
  58. package/useCode/useHighlightGate.mjs +147 -0
  59. package/useCode/useSourceEnhancing.mjs +9 -32
  60. package/useCode/useTransformManagement.d.mts +88 -1
  61. package/useCode/useTransformManagement.mjs +405 -28
  62. package/useCode/useTransitionPhase.d.mts +24 -0
  63. package/useCode/useTransitionPhase.mjs +49 -0
  64. package/useCode/useVariantSelection.d.mts +130 -6
  65. package/useCode/useVariantSelection.mjs +515 -93
  66. package/useCoordinated/coordinatePreference.d.mts +439 -0
  67. package/useCoordinated/coordinatePreference.mjs +972 -0
  68. package/useCoordinated/coordinatePreference.testUtils.d.mts +21 -0
  69. package/useCoordinated/coordinatePreference.testUtils.mjs +69 -0
  70. package/useCoordinated/index.d.mts +3 -0
  71. package/useCoordinated/index.mjs +3 -0
  72. package/useCoordinated/useCoordinated.d.mts +193 -0
  73. package/useCoordinated/useCoordinated.mjs +403 -0
  74. package/useCoordinated/useCoordinatedLocalStorage.d.mts +16 -0
  75. package/useCoordinated/useCoordinatedLocalStorage.mjs +22 -0
  76. package/useCoordinated/useCoordinatedPreference.d.mts +20 -0
  77. package/useCoordinated/useCoordinatedPreference.mjs +26 -0
  78. package/useDemo/useDemo.d.mts +4 -2
  79. package/withDocsInfra/withDocsInfra.d.mts +8 -0
  80. package/withDocsInfra/withDocsInfra.mjs +6 -2
@@ -23,13 +23,13 @@ function createClientProps(props) {
23
23
  // input).
24
24
  const url = props.urlPrefix && props.url ? replaceUrlPrefix(props.url, props.urlPrefix) : props.url;
25
25
  const contentProps = {
26
+ ...props.contentProps,
26
27
  code: props.code || props.precompute,
27
28
  components: props.components,
28
29
  name: props.name,
29
30
  slug: props.slug,
30
31
  url,
31
- variantType: props.variantType,
32
- ...props.contentProps
32
+ variantType: props.variantType
33
33
  };
34
34
  return {
35
35
  url,
@@ -213,14 +213,15 @@ function renderWithInitialSource(props) {
213
213
  // Only include components (plural) if we're also including extraVariants
214
214
  const components = fallbackProps.extraVariants ? props.components : undefined;
215
215
  const contentProps = {
216
+ ...props.contentProps,
217
+ ...fallbackProps,
216
218
  name,
217
219
  slug,
218
220
  url,
219
221
  initialFilename,
222
+ initialVariant,
220
223
  component,
221
- components,
222
- ...fallbackProps,
223
- ...props.contentProps
224
+ components
224
225
  };
225
226
  const fallback = /*#__PURE__*/_jsx(ContentLoading, {
226
227
  ...contentProps
@@ -9,6 +9,7 @@ import { CodeHighlighterFallbackContext } from "./CodeHighlighterFallbackContext
9
9
  import { useControlledCode } from "../CodeControllerContext/index.mjs";
10
10
  import { codeToFallbackProps } from "./codeToFallbackProps.mjs";
11
11
  import { mergeCodeMetadata } from "../pipeline/loadIsomorphicCodeVariant/mergeCodeMetadata.mjs";
12
+ import { getAvailableTransforms } from "../pipeline/loadIsomorphicCodeVariant/getAvailableTransforms.mjs";
12
13
  import * as Errors from "./errors.mjs";
13
14
  import { jsx as _jsx } from "react/jsx-runtime";
14
15
  const DEBUG = false; // Set to true for debugging purposes
@@ -82,7 +83,7 @@ function useInitialData({
82
83
  variants,
83
84
  globalsCode // Let loadCodeFallback handle processing
84
85
  }).catch(error => ({
85
- error
86
+ error: error instanceof Error ? error : new Error(String(error))
86
87
  }));
87
88
  if ('error' in loaded) {
88
89
  console.error(new Errors.ErrorCodeHighlighterClientLoadFallbackFailure(loaded.error));
@@ -210,7 +211,7 @@ function useAllVariants({
210
211
  name,
211
212
  variant
212
213
  })).catch(error => ({
213
- error
214
+ error: error instanceof Error ? error : new Error(String(error))
214
215
  }));
215
216
  }));
216
217
  const resultCode = {};
@@ -237,8 +238,9 @@ function useAllVariants({
237
238
  };
238
239
  }
239
240
  function yieldToMain() {
240
- if (globalThis.scheduler?.yield) {
241
- return globalThis.scheduler.yield();
241
+ const scheduler = globalThis.scheduler;
242
+ if (scheduler?.yield) {
243
+ return scheduler.yield();
242
244
  }
243
245
 
244
246
  // Fall back to yielding with setTimeout.
@@ -289,9 +291,16 @@ function useCodeParsing({
289
291
  return isHighlightAllowed;
290
292
  }, [readyForContent, isHighlightAllowed]);
291
293
 
294
+ // Memoize the "every variant is already in HAST form" check so it
295
+ // doesn't re-walk the variant + extraFiles trees on every render.
296
+ // Used both as the short-circuit inside the `parseCode` memo (fully-
297
+ // precomputed sites skip parsing entirely) and as the unmemoized
298
+ // `waitingForParsedCode` gate just below.
299
+ const allVariantsAlreadyHighlighted = React.useMemo(() => code ? hasAllVariants(Object.keys(code), code, true) : false, [code]);
300
+
292
301
  // Parse the internal code state when ready and timing conditions are met
293
302
  const parsedCode = React.useMemo(() => {
294
- if (!code || !shouldHighlight || hasAllVariants(Object.keys(code), code, true)) {
303
+ if (!code || !shouldHighlight || allVariantsAlreadyHighlighted) {
295
304
  return undefined;
296
305
  }
297
306
  if (!parseSource) {
@@ -317,36 +326,80 @@ function useCodeParsing({
317
326
  return undefined;
318
327
  }
319
328
  return parseCode(code, parseSource);
320
- }, [code, shouldHighlight, sourceParser, parseSource, parseCode, forceClient, url]);
321
- const deferHighlight = !shouldHighlight;
329
+ }, [code, shouldHighlight, allVariantsAlreadyHighlighted, sourceParser, parseSource, parseCode, forceClient, url]);
330
+
331
+ // Keep highlighting deferred until parsed HAST is actually available for the
332
+ // variants that need it. `shouldHighlight` can flip true ~30ms after
333
+ // hydration, but `parseCode` only runs once the async `sourceParser` promise
334
+ // resolves. Without this wait, downstream consumers (e.g. the transform
335
+ // swap) would commit while the visible variant is still rendered from its
336
+ // raw string source, producing a structure swap on the DOM moments later.
337
+ const waitingForParsedCode = shouldHighlight && !!code && !allVariantsAlreadyHighlighted && !parsedCode;
338
+
339
+ // Only signal `deferHighlight` while a highlight pass is actively in
340
+ // flight. When `shouldHighlight` is `false` (e.g. `highlightAt: 'idle'`
341
+ // before the idle window fires, or `'view'` before the block scrolls
342
+ // into view) we render the un-highlighted source as-is — downstream
343
+ // consumers like `useTransformManagement`'s `awaitHighlight` gate must
344
+ // commit eagerly against that source instead of blocking the barrier
345
+ // indefinitely. Once the trigger fires, `shouldHighlight` flips true,
346
+ // `waitingForParsedCode` becomes true while `parseCode` runs, and
347
+ // `deferHighlight` engages for the brief window before the next
348
+ // commit paints the highlighted tree.
349
+ const deferHighlight = waitingForParsedCode;
350
+
351
+ // Render-side readiness gate. `<Pre>` (via `useCode.shouldHighlight`)
352
+ // needs to know whether the published `code` should be rendered as
353
+ // highlighted HAST *now*. That answer is false in two distinct
354
+ // windows that `deferHighlight` deliberately collapses out:
355
+ // 1. The trigger for `highlightAt: 'hydration' | 'idle' | 'visible'`
356
+ // hasn't fired yet — `shouldHighlight` is still false. The
357
+ // precomputed `codeWithGlobals` already contains HAST, so
358
+ // without a render-side gate `<Pre>` would render highlighted
359
+ // spans on the SSR pass and on first client paint, defeating
360
+ // the whole point of deferred highlighting.
361
+ // 2. The trigger has fired (`shouldHighlight = true`) but
362
+ // `parseCode` hasn't resolved yet (`waitingForParsedCode`).
363
+ // Rendering would briefly flash un-highlighted text against
364
+ // the same tree position before the highlighted HAST lands.
365
+ //
366
+ // `highlightReady` is the inverse of the pre-`e7cc08b7` wide
367
+ // `deferHighlight` semantic, exposed separately so the narrow
368
+ // `deferHighlight` (barrier consumers only block on real in-flight
369
+ // work) and the render gate can diverge without coupling.
370
+ const highlightReady = shouldHighlight && !waitingForParsedCode;
322
371
  return {
323
372
  parsedCode,
324
- deferHighlight
373
+ deferHighlight,
374
+ highlightReady
325
375
  };
326
376
  }
327
377
  function useCodeTransforms({
328
378
  parsedCode,
379
+ loadedCode,
329
380
  variantName
330
381
  }) {
331
382
  const {
332
383
  sourceParser,
333
- getAvailableTransforms,
334
384
  computeHastDeltas
335
385
  } = useCodeContext();
336
- const [transformedCode, setTransformedCode] = React.useState(undefined);
386
+ // Track which `parsedCode` the cached `transformedCode` was computed from
387
+ // so a fresh `parsedCode` (e.g. a newly-loaded variant being added to the
388
+ // map) re-engages `waitingForTransformedCode` instead of returning the
389
+ // stale output for one render cycle. Storing input + output together lets
390
+ // callers detect staleness with reference equality.
391
+ const [transformedState, setTransformedState] = React.useState({});
337
392
 
338
393
  // Get available transforms from the current variant (separate memo for efficiency)
339
- const availableTransforms = React.useMemo(() => {
340
- if (!getAvailableTransforms) {
341
- return [];
342
- }
343
- return getAvailableTransforms(parsedCode, variantName);
344
- }, [parsedCode, variantName, getAvailableTransforms]);
394
+ const availableTransforms = React.useMemo(() => getAvailableTransforms(parsedCode ?? loadedCode, variantName), [parsedCode, loadedCode, variantName]);
345
395
 
346
396
  // Effect to compute transformations for all variants
347
397
  React.useEffect(() => {
348
398
  if (!parsedCode || !sourceParser || !computeHastDeltas) {
349
- setTransformedCode(parsedCode);
399
+ setTransformedState({
400
+ input: parsedCode,
401
+ output: parsedCode
402
+ });
350
403
  return;
351
404
  }
352
405
 
@@ -355,16 +408,49 @@ function useCodeTransforms({
355
408
  try {
356
409
  const parseSource = await sourceParser;
357
410
  const enhanced = await computeHastDeltas(parsedCode, parseSource);
358
- setTransformedCode(enhanced);
411
+ setTransformedState({
412
+ input: parsedCode,
413
+ output: enhanced
414
+ });
359
415
  } catch (error) {
360
416
  console.error(new Errors.ErrorCodeHighlighterClientTransformProcessingFailure(error));
361
- setTransformedCode(parsedCode);
417
+ setTransformedState({
418
+ input: parsedCode,
419
+ output: parsedCode
420
+ });
362
421
  }
363
422
  })();
364
423
  }, [parsedCode, sourceParser, computeHastDeltas]);
424
+
425
+ // Expose the cached output regardless of whether `parsedCode` changed since
426
+ // the last computation — falling back to `undefined` here would yank the
427
+ // currently-displayed HAST for a frame while the async pipeline catches up.
428
+ // Staleness is signalled via `waitingForTransformedCode` so downstream
429
+ // gates (e.g. `useTransformManagement` / `useVariantSelection`) hold off
430
+ // committing a swap until fresh deltas land.
431
+ const transformedCode = transformedState.output;
432
+
433
+ // Async hast-deltas pipeline status. While true, consumers (notably
434
+ // `useTransformManagement`'s `deferHighlight` gate) should treat
435
+ // highlighting as not-yet-settled and hold off committing a transform
436
+ // swap. Without this, the swap can commit after `parsedCode` is ready
437
+ // but *before* `computeHastDeltas` resolves: the incoming tree first
438
+ // renders without the transform deltas, then re-renders a frame or
439
+ // two later when `transformedCode` arrives, producing a visible jump
440
+ // on top of the just-played collapse animation.
441
+ //
442
+ // Only relevant when both a worker (`sourceParser`) and a deltas
443
+ // computer (`computeHastDeltas`) are wired up — environments without
444
+ // them resolve `transformedCode` synchronously to `parsedCode` in the
445
+ // effect above, so the deltas phase is a no-op. We compare the cached
446
+ // `input` against the live `parsedCode` instead of just checking
447
+ // `!transformedCode` so a freshly-arriving variant re-engages the wait
448
+ // until its deltas land.
449
+ const waitingForTransformedCode = !!parsedCode && !!sourceParser && !!computeHastDeltas && transformedState.input !== parsedCode;
365
450
  return {
366
451
  transformedCode,
367
- availableTransforms
452
+ availableTransforms,
453
+ waitingForTransformedCode
368
454
  };
369
455
  }
370
456
  function useControlledCodeParsing({
@@ -794,7 +880,8 @@ export function CodeHighlighterClient(props) {
794
880
  const codeWithGlobals = propsCodeWithGlobals || stateCodeWithGlobals;
795
881
  const {
796
882
  parsedCode,
797
- deferHighlight
883
+ deferHighlight: deferHighlightForParsing,
884
+ highlightReady
798
885
  } = useCodeParsing({
799
886
  code: codeWithGlobals,
800
887
  readyForContent: readyForContent || Boolean(props.code),
@@ -805,12 +892,26 @@ export function CodeHighlighterClient(props) {
805
892
  });
806
893
  const {
807
894
  transformedCode,
808
- availableTransforms
895
+ availableTransforms,
896
+ waitingForTransformedCode
809
897
  } = useCodeTransforms({
810
898
  parsedCode,
899
+ loadedCode: codeWithGlobals,
811
900
  variantName
812
901
  });
813
902
 
903
+ // Combined highlight-readiness gate consumed via context (notably by
904
+ // `useTransformManagement`). Stay deferred while either the sync
905
+ // `parseCode` pass or the async `computeHastDeltas` pass is still in
906
+ // flight — committing a transform swap with `transformedCode` still
907
+ // pending causes the incoming pre to first render without the
908
+ // transform deltas and then re-flow a frame or two later when the
909
+ // deltas land, producing a visible jump on top of the collapse
910
+ // animation. The wait only matters for highlighters with at least one
911
+ // applicable transform; plain (variant-only) highlighters skip it so
912
+ // their stored-preference resolution doesn't pay the deltas latency.
913
+ const deferHighlight = deferHighlightForParsing || availableTransforms.length > 0 && waitingForTransformedCode;
914
+
814
915
  // Per-highlighter pre-parsed HAST cache. Lives in a ref so the same Map
815
916
  // instance is shared across renders without becoming a React dep. The
816
917
  // editable populates it via `useSourceEditing` (which reads it from
@@ -839,16 +940,31 @@ export function CodeHighlighterClient(props) {
839
940
  selection: controlled?.selection || selection,
840
941
  setSelection: controlled?.setSelection || setSelection,
841
942
  components: controlled?.components || props.components,
842
- availableTransforms: isControlled ? [] : availableTransforms,
943
+ // Only suppress when an external CodeController owns the code; static
944
+ // `props.code` still needs the locally-computed list.
945
+ availableTransforms: controlled?.code ? [] : availableTransforms,
843
946
  url: props.url,
844
947
  deferHighlight,
948
+ highlightReady,
949
+ highlightAfter,
845
950
  preParsedCache
846
- }), [overlaidCode, controlled?.setCode, selection, controlled?.selection, controlled?.setSelection, controlled?.components, props.components, isControlled, availableTransforms, props.url, deferHighlight, preParsedCache]);
951
+ }), [overlaidCode, controlled?.setCode, selection, controlled?.selection, controlled?.setSelection, controlled?.components, props.components, controlled?.code, availableTransforms, props.url, deferHighlight, highlightReady, highlightAfter, preParsedCache]);
847
952
  if (!props.variants && !props.components && !activeCode) {
848
953
  throw new Errors.ErrorCodeHighlighterClientMissingData();
849
954
  }
955
+
956
+ // If this CodeHighlighter is nested inside another CodeHighlighter that is
957
+ // currently rendering its fallback, hold our own fallback->full transition
958
+ // until the outer one swaps. Otherwise, when the outer swaps from its
959
+ // fallback element to its children element, our subtree unmounts and a fresh
960
+ // inner instance mounts and re-runs its own fallback->full transition,
961
+ // producing a visible "fallback -> full -> fallback -> full" flicker. By
962
+ // staying in fallback while nested, we collapse this to a single transition
963
+ // that happens after the outer is fully rendered.
964
+ const outerFallbackContext = React.useContext(CodeHighlighterFallbackContext);
965
+ const isNestedInsideOuterFallback = outerFallbackContext !== undefined;
850
966
  const fallback = props.fallback;
851
- if (fallback && !props.skipFallback && !activeCodeReady) {
967
+ if (fallback && !props.skipFallback && (!activeCodeReady || isNestedInsideOuterFallback)) {
852
968
  return /*#__PURE__*/_jsx(CodeHighlighterFallbackContext.Provider, {
853
969
  value: fallbackContext,
854
970
  children: fallback
@@ -21,6 +21,33 @@ export interface CodeHighlighterContextType {
21
21
  availableTransforms?: string[];
22
22
  url?: string;
23
23
  deferHighlight?: boolean;
24
+ /**
25
+ * Render-side readiness gate. `true` once the highlight trigger
26
+ * (`init` / `hydration` / `idle` / `visible`) has fired *and* the
27
+ * sync `parseCode` pass has resolved, so consumers like `<Pre>`
28
+ * can render the published `code` as highlighted HAST. While
29
+ * `false` they should render the un-highlighted fallback (plain
30
+ * text) — the published `code` may still contain precomputed HAST
31
+ * left over from SSR, so without this gate non-`init` demos would
32
+ * render highlighted spans on the first paint and defeat the
33
+ * deferred-highlighting trigger.
34
+ *
35
+ * Distinct from `deferHighlight`, which is the narrower
36
+ * "highlight pass is actively in flight" signal consumed by
37
+ * barrier gates (e.g. `useTransformManagement.awaitHighlight`)
38
+ * that must not block when no work is queued.
39
+ */
40
+ highlightReady?: boolean;
41
+ /**
42
+ * Echo of the `highlightAfter` prop on the surrounding
43
+ * `CodeHighlighter` / `CodeHighlighterClient`. Consumers such as
44
+ * `useCode` use this to skip transient highlighting-suppression
45
+ * gates that only matter when highlighting is asynchronous — in
46
+ * `'init'` mode the precomputed HAST already carries the highlight
47
+ * spans, so those gates would just cause a visible flash of
48
+ * unhighlighted content during variant swaps.
49
+ */
50
+ highlightAfter?: 'init' | 'hydration' | 'idle';
24
51
  /**
25
52
  * Per-file pre-parsed HAST cache. Populated by `useSourceEditing` when the
26
53
  * editable supplies a worker-parsed result alongside a source change, and
@@ -1,8 +1,22 @@
1
1
  import { stringOrHastToJsx } from "../pipeline/hastUtils/index.mjs";
2
+ import { getLanguageFromExtension } from "../pipeline/loaderUtils/getLanguageFromExtension.mjs";
3
+ function getLanguageFromFileName(fileName) {
4
+ if (!fileName) {
5
+ return undefined;
6
+ }
7
+ const dotIndex = fileName.lastIndexOf('.');
8
+ if (dotIndex === -1) {
9
+ return undefined;
10
+ }
11
+ return getLanguageFromExtension(fileName.slice(dotIndex));
12
+ }
2
13
  function toExtraSource(variantCode, fileName) {
3
14
  return Object.entries(variantCode.extraFiles || {}).reduce((acc, [name, file]) => {
4
15
  if (name !== fileName && typeof file === 'object' && file?.source) {
5
- acc[name] = stringOrHastToJsx(file.source);
16
+ acc[name] = {
17
+ source: stringOrHastToJsx(file.source),
18
+ language: file.language ?? getLanguageFromFileName(name)
19
+ };
6
20
  }
7
21
  return acc;
8
22
  }, {});
@@ -14,13 +28,16 @@ export function codeToFallbackProps(variant, code, fileName, needsAllFiles = fal
14
28
  }
15
29
  const fileNames = [variantCode.fileName, ...Object.keys(variantCode.extraFiles || {})].filter(name => Boolean(name));
16
30
  let source;
31
+ let language;
17
32
  if (fileName && fileName !== variantCode.fileName) {
18
33
  const fileData = variantCode.extraFiles?.[fileName];
19
34
  if (fileData && typeof fileData === 'object' && 'source' in fileData && fileData.source) {
20
35
  source = stringOrHastToJsx(fileData.source);
36
+ language = fileData.language ?? getLanguageFromFileName(fileName);
21
37
  }
22
38
  } else if (variantCode.source) {
23
39
  source = stringOrHastToJsx(variantCode.source);
40
+ language = variantCode.language ?? getLanguageFromFileName(variantCode.fileName);
24
41
  }
25
42
  if (needsAllVariants || needsAllFiles) {
26
43
  const extraSource = toExtraSource(variantCode, fileName);
@@ -32,6 +49,7 @@ export function codeToFallbackProps(variant, code, fileName, needsAllFiles = fal
32
49
  fileNames: [vCode.fileName, ...Object.keys(vCode.extraFiles || {})].filter(fn => Boolean(fn)),
33
50
  // TODO: use filesOrder if provided
34
51
  source: vCode.source && stringOrHastToJsx(vCode.source),
52
+ language: vCode.source ? vCode.language ?? getLanguageFromFileName(vCode.fileName) : undefined,
35
53
  extraSource: extraVariantExtraSource
36
54
  };
37
55
  }
@@ -40,6 +58,7 @@ export function codeToFallbackProps(variant, code, fileName, needsAllFiles = fal
40
58
  return {
41
59
  fileNames,
42
60
  source,
61
+ language,
43
62
  extraSource,
44
63
  extraVariants
45
64
  };
@@ -47,11 +66,13 @@ export function codeToFallbackProps(variant, code, fileName, needsAllFiles = fal
47
66
  return {
48
67
  fileNames,
49
68
  source,
69
+ language,
50
70
  extraSource
51
71
  };
52
72
  }
53
73
  return {
54
74
  fileNames,
55
- source
75
+ source,
76
+ language
56
77
  };
57
78
  }
@@ -1 +1,2 @@
1
- export * from "./CodeHighlighter.mjs";
1
+ export * from "./CodeHighlighter.mjs";
2
+ export { mergeComments } from "./mergeComments.mjs";
@@ -1 +1,2 @@
1
- export * from "./CodeHighlighter.mjs";
1
+ export * from "./CodeHighlighter.mjs";
2
+ export { mergeComments } from "./mergeComments.mjs";
@@ -0,0 +1,38 @@
1
+ import type { SourceComments } from "./types.mjs";
2
+ /**
3
+ * Merges two `SourceComments` maps by concatenating entries per line.
4
+ *
5
+ * Both maps are keyed by line number. The function does not interpret
6
+ * keys — it only matches them by value — so 0-indexed and 1-indexed
7
+ * conventions are both supported, but **both inputs must use the same
8
+ * convention**. The repository's `SourceTransformer` contract supplies
9
+ * 1-indexed line numbers; if you build `mine` by hand, match the
10
+ * upstream indexing of `input` or your markers will land on the wrong
11
+ * lines.
12
+ *
13
+ * In non-production builds a heuristic dev warning is emitted when the
14
+ * two inputs look like they disagree about indexing (one contains a
15
+ * `0` key and the other does not). The check has no runtime cost in
16
+ * production builds.
17
+ *
18
+ * For any line present in either map, the resulting entry is
19
+ * `[...input[line] ?? [], ...mine[line] ?? []]` — `input` markers come
20
+ * first, the transformer's own markers (`mine`) are appended.
21
+ *
22
+ * Returns `undefined` when the merge would produce no entries (both
23
+ * inputs absent, both empty, or every per-line array empty). Otherwise
24
+ * returns a fresh object whose per-line arrays are also fresh copies,
25
+ * so callers may safely mutate the result without affecting either
26
+ * input.
27
+ *
28
+ * Intended to be called by `SourceTransformer` implementations that
29
+ * receive an upstream `comments` map as their 3rd argument and want to
30
+ * preserve those entries alongside the markers they themselves emit.
31
+ *
32
+ * @param input - Comments map received by the transformer (may be
33
+ * `undefined` when no upstream comments exist).
34
+ * @param mine - Comments map the transformer wants to emit (may be
35
+ * `undefined` when the transformer has none of its own). Must use
36
+ * the same line-indexing convention as `input`.
37
+ */
38
+ export declare function mergeComments(input: SourceComments | undefined, mine: SourceComments | undefined): SourceComments | undefined;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Merges two `SourceComments` maps by concatenating entries per line.
3
+ *
4
+ * Both maps are keyed by line number. The function does not interpret
5
+ * keys — it only matches them by value — so 0-indexed and 1-indexed
6
+ * conventions are both supported, but **both inputs must use the same
7
+ * convention**. The repository's `SourceTransformer` contract supplies
8
+ * 1-indexed line numbers; if you build `mine` by hand, match the
9
+ * upstream indexing of `input` or your markers will land on the wrong
10
+ * lines.
11
+ *
12
+ * In non-production builds a heuristic dev warning is emitted when the
13
+ * two inputs look like they disagree about indexing (one contains a
14
+ * `0` key and the other does not). The check has no runtime cost in
15
+ * production builds.
16
+ *
17
+ * For any line present in either map, the resulting entry is
18
+ * `[...input[line] ?? [], ...mine[line] ?? []]` — `input` markers come
19
+ * first, the transformer's own markers (`mine`) are appended.
20
+ *
21
+ * Returns `undefined` when the merge would produce no entries (both
22
+ * inputs absent, both empty, or every per-line array empty). Otherwise
23
+ * returns a fresh object whose per-line arrays are also fresh copies,
24
+ * so callers may safely mutate the result without affecting either
25
+ * input.
26
+ *
27
+ * Intended to be called by `SourceTransformer` implementations that
28
+ * receive an upstream `comments` map as their 3rd argument and want to
29
+ * preserve those entries alongside the markers they themselves emit.
30
+ *
31
+ * @param input - Comments map received by the transformer (may be
32
+ * `undefined` when no upstream comments exist).
33
+ * @param mine - Comments map the transformer wants to emit (may be
34
+ * `undefined` when the transformer has none of its own). Must use
35
+ * the same line-indexing convention as `input`.
36
+ */
37
+ export function mergeComments(input, mine) {
38
+ if (!input && !mine) {
39
+ return undefined;
40
+ }
41
+ if (process.env.NODE_ENV !== 'production' && input && mine && Object.keys(input).length > 0 && Object.keys(mine).length > 0) {
42
+ warnOnIndexingMismatch(input, mine);
43
+ }
44
+ const result = {};
45
+ const lines = new Set();
46
+ if (input) {
47
+ for (const key of Object.keys(input)) {
48
+ lines.add(Number(key));
49
+ }
50
+ }
51
+ if (mine) {
52
+ for (const key of Object.keys(mine)) {
53
+ lines.add(Number(key));
54
+ }
55
+ }
56
+ let hasAny = false;
57
+ for (const line of lines) {
58
+ const merged = [...(input?.[line] ?? []), ...(mine?.[line] ?? [])];
59
+ if (merged.length > 0) {
60
+ result[line] = merged;
61
+ hasAny = true;
62
+ }
63
+ }
64
+ return hasAny ? result : undefined;
65
+ }
66
+ function warnOnIndexingMismatch(input, mine) {
67
+ const inputHasZero = Object.prototype.hasOwnProperty.call(input, 0);
68
+ const mineHasZero = Object.prototype.hasOwnProperty.call(mine, 0);
69
+ if (inputHasZero === mineHasZero) {
70
+ return;
71
+ }
72
+
73
+ // Only warn when the side without a `0` key has at least one entry
74
+ // — otherwise we can't tell whether it would have included one.
75
+ const other = inputHasZero ? mine : input;
76
+ if (Object.keys(other).length === 0) {
77
+ return;
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.");
80
+ }