@react-x11/components 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/code/index.d.ts.map +1 -1
  2. package/dist/code/index.js +25 -24
  3. package/dist/code/index.js.map +1 -1
  4. package/dist/code-language/index.d.ts +1 -1
  5. package/dist/code-language/index.d.ts.map +1 -1
  6. package/dist/code-language/index.js +1 -1
  7. package/dist/code-language/index.js.map +1 -1
  8. package/dist/code-language/runs.d.ts +31 -0
  9. package/dist/code-language/runs.d.ts.map +1 -1
  10. package/dist/code-language/runs.js +105 -0
  11. package/dist/code-language/runs.js.map +1 -1
  12. package/dist/codeblock/index.d.ts +13 -0
  13. package/dist/codeblock/index.d.ts.map +1 -1
  14. package/dist/codeblock/index.js +37 -1
  15. package/dist/codeblock/index.js.map +1 -1
  16. package/dist/html/css/cascade.d.ts +32 -10
  17. package/dist/html/css/cascade.d.ts.map +1 -1
  18. package/dist/html/css/cascade.js +72 -27
  19. package/dist/html/css/cascade.js.map +1 -1
  20. package/dist/html/css/style.d.ts +7 -4
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +37 -6
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +8 -5
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/layout/block.d.ts.map +1 -1
  27. package/dist/html/layout/block.js +127 -9
  28. package/dist/html/layout/block.js.map +1 -1
  29. package/dist/html/layout/boxes.d.ts +27 -0
  30. package/dist/html/layout/boxes.d.ts.map +1 -1
  31. package/dist/html/layout/boxes.js +30 -2
  32. package/dist/html/layout/boxes.js.map +1 -1
  33. package/dist/html/layout/inline.d.ts +3 -0
  34. package/dist/html/layout/inline.d.ts.map +1 -1
  35. package/dist/html/layout/inline.js +236 -14
  36. package/dist/html/layout/inline.js.map +1 -1
  37. package/dist/html/node.d.ts.map +1 -1
  38. package/dist/html/node.js +118 -57
  39. package/dist/html/node.js.map +1 -1
  40. package/dist/html/paint.d.ts.map +1 -1
  41. package/dist/html/paint.js +203 -37
  42. package/dist/html/paint.js.map +1 -1
  43. package/dist/html/svg.d.ts.map +1 -1
  44. package/dist/html/svg.js +27 -20
  45. package/dist/html/svg.js.map +1 -1
  46. package/dist/internal/codelines.d.ts +42 -0
  47. package/dist/internal/codelines.d.ts.map +1 -0
  48. package/dist/internal/codelines.js +170 -0
  49. package/dist/internal/codelines.js.map +1 -0
  50. package/dist/markdown/index.d.ts.map +1 -1
  51. package/dist/markdown/index.js +98 -26
  52. package/dist/markdown/index.js.map +1 -1
  53. package/dist/rich-text-editor/index.d.ts.map +1 -1
  54. package/dist/rich-text-editor/index.js +3 -8
  55. package/dist/rich-text-editor/index.js.map +1 -1
  56. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  57. package/dist/rich-text-editor/markdown.js +30 -1
  58. package/dist/rich-text-editor/markdown.js.map +1 -1
  59. package/dist/rich-text-editor/replace.d.ts +13 -0
  60. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  61. package/dist/rich-text-editor/replace.js +64 -0
  62. package/dist/rich-text-editor/replace.js.map +1 -0
  63. package/dist/terminal-output/index.d.ts.map +1 -1
  64. package/dist/terminal-output/index.js +100 -25
  65. package/dist/terminal-output/index.js.map +1 -1
  66. package/dist/terminal-output/runs.d.ts +4 -1
  67. package/dist/terminal-output/runs.d.ts.map +1 -1
  68. package/dist/terminal-output/runs.js +5 -1
  69. package/dist/terminal-output/runs.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/code/index.ts +34 -32
  72. package/src/code-language/index.ts +1 -1
  73. package/src/code-language/runs.ts +111 -1
  74. package/src/codeblock/index.ts +48 -1
  75. package/src/html/css/cascade.ts +117 -39
  76. package/src/html/css/style.ts +43 -15
  77. package/src/html/css/ua.ts +8 -5
  78. package/src/html/layout/block.ts +134 -8
  79. package/src/html/layout/boxes.ts +43 -2
  80. package/src/html/layout/inline.ts +257 -13
  81. package/src/html/node.ts +145 -79
  82. package/src/html/paint.ts +236 -45
  83. package/src/html/svg.ts +30 -19
  84. package/src/internal/codelines.ts +200 -0
  85. package/src/markdown/index.ts +145 -26
  86. package/src/rich-text-editor/index.ts +3 -11
  87. package/src/rich-text-editor/markdown.ts +36 -1
  88. package/src/rich-text-editor/replace.ts +85 -0
  89. package/src/terminal-output/index.ts +127 -34
  90. package/src/terminal-output/runs.ts +16 -2
@@ -556,6 +556,14 @@ export const INLINE_OFFSETS = new WeakMap<Box, { x: number; y: number }>();
556
556
  export const TEXT_SHIFTS = new WeakMap<LineText, { x: number; y: number }>();
557
557
  export const SHIFTED_LINES = new WeakSet<LineBox>();
558
558
 
559
+ /** Of a block's lines, by the array they are, those `position: relative`
560
+ * or a translation moved something off — a text in a moved inline box, or
561
+ * an inline-block or an image on it — which is drawn where it went while
562
+ * the line stays where it was: what the paint's cull of a block's lines
563
+ * has to look at past the lines the damage meets (`paintLines`). By the
564
+ * array, so that a layout that makes new lines leaves none of the old. */
565
+ export const MOVED_OFF_LINES = new WeakMap<LineBox[], Set<LineBox>>();
566
+
559
567
  /** How far `vertical-align` raised a text above its line's baseline, and an
560
568
  * inline box's own baseline, where it did: beside the lines and the boxes
561
569
  * for the reason the offsets above are. A line holding a raised text is
@@ -670,6 +678,11 @@ export interface BoxTree {
670
678
  /** Whether any box paints its background through its text
671
679
  * (`background-clip: text`): where none does, no paragraph looks. */
672
680
  clipText: boolean;
681
+ /** The style of the `<html>` a document with neither an `<html>` nor a
682
+ * `<body>` implies around the body the root box stands in for
683
+ * (`Cascade.rootStyle`), whose background is the canvas's; null where
684
+ * the document has either. */
685
+ impliedHtml: ComputedStyle | null;
673
686
  }
674
687
 
675
688
  export interface BuildOptions {
@@ -774,7 +787,10 @@ class Builder {
774
787
  run(root: Element): BoxTree {
775
788
  const cascade = this._options.cascade;
776
789
  cascade.beginSharing();
777
- const rootStyle = cascade.rootStyle(hasBody(root), hasHtml(root));
790
+ const { style: rootStyle, html: impliedHtml } = cascade.rootStyle(
791
+ hasBody(root),
792
+ hasHtml(root),
793
+ );
778
794
  const rootBox = new Box('block', null, rootStyle);
779
795
  // a fragment's root stands in for a `<body>`, counters and all
780
796
  this._scopes.open();
@@ -808,6 +824,7 @@ class Builder {
808
824
  nestedOutOfLine: this._nestedOutOfLine,
809
825
  movedInline: this._movedInline,
810
826
  clipText: this._clipText,
827
+ impliedHtml,
811
828
  };
812
829
  }
813
830
 
@@ -2017,6 +2034,30 @@ function transformText(
2017
2034
  }
2018
2035
  }
2019
2036
 
2037
+ /**
2038
+ * `text` transformed as `transform` says, where it stands for text laid out
2039
+ * already — a first line's, whose `::first-line` transforms it (CSS 2.1
2040
+ * 5.12.1): a character at a time where the transform would change its
2041
+ * length, which moves every offset after it — `ß` is `SS` in capitals —
2042
+ * and that character kept as it is.
2043
+ */
2044
+ export function transformInPlace(
2045
+ text: string,
2046
+ transform: ComputedStyle['textTransform'],
2047
+ lettered = false,
2048
+ ): string {
2049
+ const whole = transformText(text, transform, lettered);
2050
+ if (whole.length === text.length) return whole;
2051
+ let out = '';
2052
+ let state = lettered;
2053
+ for (const ch of text) {
2054
+ const one = transformText(ch, transform, state);
2055
+ out += one.length === ch.length ? one : ch;
2056
+ state = letteredAfter(ch, state);
2057
+ }
2058
+ return out;
2059
+ }
2060
+
2020
2061
  /** A letter or a number: what a word's first typographic letter unit is
2021
2062
  * (CSS Text 3, 2.1). */
2022
2063
  const LETTER_UNIT = /[\p{L}\p{N}]/u;
@@ -2089,7 +2130,7 @@ function titleCase(ch: string): string {
2089
2130
  * nothing but joining punctuation leaves it as it was. Asked of every text,
2090
2131
  * from the end, where it is almost always decided by the last character.
2091
2132
  */
2092
- function letteredAfter(text: string, lettered: boolean): boolean {
2133
+ export function letteredAfter(text: string, lettered: boolean): boolean {
2093
2134
  for (let i = text.length - 1; i >= 0; i -= 1) {
2094
2135
  const c = text.charCodeAt(i);
2095
2136
  if (c < 0x80) {
@@ -43,9 +43,11 @@ import {
43
43
  BOX_RAISES,
44
44
  LINE_BOX_RAISES,
45
45
  isOffset,
46
+ letteredAfter,
46
47
  SHADOWED_TEXT,
47
48
  SHIFTED_LINES,
48
49
  TEXT_RAISES,
50
+ transformInPlace,
49
51
  } from './boxes.js';
50
52
  import type {
51
53
  AtomicPlacement,
@@ -300,6 +302,9 @@ export interface InlineOptions {
300
302
  * — which the line is found in, a piece at a time (CSS 2.1 5.12.1).
301
303
  */
302
304
  firstLineStyle?: { style: ComputedStyle; styler: FirstLineStyler };
305
+ /** Room the first line has to hold about its baseline besides the
306
+ * strut's: a list item's marker (`MARKER_ROOM`). */
307
+ firstStrut?: InlineDecoration;
303
308
  /** Whether any box in the document paints its background through its
304
309
  * text (`BoxTree.clipText`). */
305
310
  clipText?: boolean;
@@ -431,6 +436,8 @@ function linesOf(
431
436
  CLIPPED_TEXT.add(block);
432
437
  fonts = recording(fonts);
433
438
  }
439
+ // small capitals the face does not have, made of its capitals
440
+ synthesizeSmallCaps(items, fonts);
434
441
  if (wraps(block.style)) holdNoWrap(items, block);
435
442
  // one walk for what few paragraphs have: text that casts a shadow, and a
436
443
  // tab, which only a `white-space` that keeps it leaves — so no text is
@@ -537,6 +544,13 @@ function linesOf(
537
544
  hasEdges && !hasAtomics && !hasOffset && spacersHold(style, items);
538
545
  // a first line in fonts of its own is found a piece at a time, in them
539
546
  const restyled = options.firstLineStyle !== undefined;
547
+ // and where one layout makes a line shorter than the strut, its text all
548
+ // smaller than the block's, the lines are made a piece at a time too,
549
+ // where every line holds it (`shortOfStrut`)
550
+ const strut = fonts ? strutOf(fonts, style) : null;
551
+ // and the first line holds what it is handed besides: a marker's room
552
+ const firstStrut = withRoom(strut, options.firstStrut);
553
+ let strutted = false;
540
554
 
541
555
  // Inline boxes' edges as spacers in one layout, where the engine left
542
556
  // every edge on the line of the content it belongs to (`layoutSpaced`);
@@ -551,19 +565,21 @@ function linesOf(
551
565
  align,
552
566
  fonts,
553
567
  );
554
- if (laid) return laid;
568
+ if (laid && !shortOfStrut(laid.lines, strut, firstStrut)) return laid;
569
+ if (laid) strutted = true;
555
570
  }
556
571
 
557
572
  // The text-only, float-free, unindented case: one call, every line — or
558
573
  // one call per *chunk*, when the text is long and carries hard breaks.
559
- if (
574
+ fast: if (
560
575
  !hasAtomics &&
561
576
  !hasEdges &&
562
577
  !raised &&
563
578
  !floated &&
564
579
  !indent &&
565
580
  !alignedApart &&
566
- !restyled
581
+ !restyled &&
582
+ !strutted
567
583
  ) {
568
584
  // no atomics and no edges, so everything is text
569
585
  const textItems = items as Extract<Item, { kind: 'text' }>[];
@@ -596,7 +612,7 @@ function linesOf(
596
612
  }
597
613
  if (perLine) cut = null;
598
614
  if (total > CHUNK_TRIGGER_CHARS && hasNewline && !hasControls(items)) {
599
- return layoutChunked(
615
+ const chunked = layoutChunked(
600
616
  textItems,
601
617
  base,
602
618
  style,
@@ -605,6 +621,9 @@ function linesOf(
605
621
  align,
606
622
  fonts,
607
623
  );
624
+ if (!shortOfStrut(chunked.lines, strut, firstStrut)) return chunked;
625
+ strutted = true;
626
+ break fast;
608
627
  }
609
628
  let runs: TextRun[] = [];
610
629
  let spans = new SpanMap();
@@ -705,6 +724,11 @@ function linesOf(
705
724
  ? unwrappedPlacer(style, options.width, false)
706
725
  : null,
707
726
  );
727
+ // a clamp and a cut are the one layout's to make
728
+ if (!clamp && !cut && shortOfStrut(lines, strut, firstStrut)) {
729
+ strutted = true;
730
+ break fast;
731
+ }
708
732
  return clampCut
709
733
  ? { lines, height: layout.height, width: widest, cut: true }
710
734
  : { lines, height: layout.height, width: widest };
@@ -751,7 +775,6 @@ function linesOf(
751
775
  deferred = [];
752
776
  };
753
777
 
754
- const strut = fonts ? strutOf(fonts, style) : null;
755
778
  // How tall a line is taken to be before it is made, for its room beside
756
779
  // the floats: the paragraph's strut, a line of its text. An item taller
757
780
  // asks over its own height, and the line made over its whole (`close`).
@@ -799,7 +822,9 @@ function linesOf(
799
822
  (height) => bandAt(options, y, Math.max(height, guess)),
800
823
  last || !wraps(style) ? lastShift : restShift,
801
824
  rtl,
802
- onFirst ? lineOne.strut : strut,
825
+ lines.length
826
+ ? strut
827
+ : withRoom(onFirst ? lineOne.strut : strut, options.firstStrut),
803
828
  onFirst ? lineOne.lifts : lifts,
804
829
  (last || !wraps(style) ? justify.last : justify.rest)
805
830
  ? onFirst
@@ -970,6 +995,7 @@ function linesOf(
970
995
  const tailIsPlain =
971
996
  !alignedApart &&
972
997
  !onFirst &&
998
+ !strutted &&
973
999
  segment.nextIndex >= items.length &&
974
1000
  open.x === 0 &&
975
1001
  !open.atomics.length &&
@@ -998,6 +1024,7 @@ function linesOf(
998
1024
  Math.max(1, available),
999
1025
  justify,
1000
1026
  ).layout;
1027
+ const tail: LineBox[] = [];
1001
1028
  for (let i = 0; i < layout.lines.length; i += 1) {
1002
1029
  const natural = layout.lines[i];
1003
1030
  const text: LineText = {
@@ -1011,7 +1038,7 @@ function linesOf(
1011
1038
  spans: segment.spans,
1012
1039
  };
1013
1040
  segment.spans.giveGaps(text, natural.start, natural.end);
1014
- lines.push({
1041
+ tail.push({
1015
1042
  x: band.left + natural.x,
1016
1043
  y: y + natural.y,
1017
1044
  width: natural.width,
@@ -1022,12 +1049,19 @@ function linesOf(
1022
1049
  textEnd: text.textEnd,
1023
1050
  atomics: [],
1024
1051
  });
1025
- widest = Math.max(widest, natural.width);
1026
1052
  }
1027
- y += layout.height;
1028
- index = segment.nextIndex;
1029
- offset = 0;
1030
- continue;
1053
+ // or a line at a time, where one of them is shorter than the strut
1054
+ if (!shortOfStrut(tail, strut, lines.length ? null : firstStrut)) {
1055
+ for (const line of tail) {
1056
+ lines.push(line);
1057
+ widest = Math.max(widest, line.width);
1058
+ }
1059
+ y += layout.height;
1060
+ index = segment.nextIndex;
1061
+ offset = 0;
1062
+ continue;
1063
+ }
1064
+ strutted = true;
1031
1065
  }
1032
1066
 
1033
1067
  // One line: `maxLines: 1` cuts the layout at the first break and its
@@ -2848,8 +2882,13 @@ export function inlineDecoration(
2848
2882
  box: Box,
2849
2883
  s: ComputedStyle,
2850
2884
  ): InlineDecoration | null {
2885
+ // a background is a colour, a gradient or an image: a span a gradient
2886
+ // underlines, a highlighter's, was painted only where it had a border
2851
2887
  const decorated =
2852
2888
  !isTransparent(s.backgroundColor) ||
2889
+ s.backgroundGradient !== null ||
2890
+ s.backgroundImage !== null ||
2891
+ s.backgroundImages !== null ||
2853
2892
  box.borderTop + box.borderRight + box.borderBottom + box.borderLeft > 0 ||
2854
2893
  s.outlineStyle !== 'none';
2855
2894
  return !decorated ? null : fonts ? faceExtent(fonts, s) : NO_EXTENT;
@@ -2952,9 +2991,26 @@ function firstLineSetting(
2952
2991
  box === block || !box.parent
2953
2992
  ? style
2954
2993
  : styleOnFirstLine(box, of(box.parent), how.styler);
2994
+ // and in its `text-transform` where the line has one of its own, which
2995
+ // the boxes' text was not made with: a word carried from one run into
2996
+ // the next, as the box builder carries it (`letteredAfter`)
2997
+ let lettered = false;
2955
2998
  const restyled = items.map((item): Item => {
2999
+ if (item.kind === 'atomic') lettered = false;
2956
3000
  if (item.kind !== 'text') return item;
2957
- const run = restyledRun(item.run, item.box.style, of(item.box));
3001
+ const own = item.box.style;
3002
+ const on = of(item.box);
3003
+ let run = restyledRun(item.run, own, on);
3004
+ const transform = on.textTransform;
3005
+ if (
3006
+ !item.control &&
3007
+ transform !== own.textTransform &&
3008
+ transform !== 'none'
3009
+ ) {
3010
+ const text = transformInPlace(run.text, transform, lettered);
3011
+ if (text !== run.text) run = { ...run, text };
3012
+ }
3013
+ lettered = letteredAfter(run.text, lettered);
2958
3014
  return run === item.run ? item : { ...item, run };
2959
3015
  });
2960
3016
  const lineHeight = lineHeightMultiplier(fonts, style);
@@ -3307,6 +3363,194 @@ function heldText(box: Box): string {
3307
3363
 
3308
3364
  const HELD = new WeakMap<Box, string>();
3309
3365
 
3366
+ /**
3367
+ * Whether a line one layout made is shorter about its baseline than the
3368
+ * block's strut. Every line box starts with the strut, the block's font at
3369
+ * its line height (CSS 2.1 10.8.1), and a layout has none: it makes a line
3370
+ * as tall as the text on it, at the block's line height as a multiple of
3371
+ * each face's natural one. So a line of `<small>` text alone came out as
3372
+ * short as the small text, and overlapped what followed. A paragraph with
3373
+ * such a line is made a line at a time instead, where `finishLine` puts
3374
+ * the strut on every line; one whose lines all hold text of the block's
3375
+ * own face is kept as it was laid out.
3376
+ */
3377
+ function shortOfStrut(
3378
+ lines: readonly LineBox[],
3379
+ strut: InlineDecoration | null,
3380
+ /** The first line's, where it holds more: a marker's room. */
3381
+ first: InlineDecoration | null = null,
3382
+ ): boolean {
3383
+ for (let i = 0; i < lines.length; i += 1) {
3384
+ const line = lines[i];
3385
+ const holds = i === 0 && first ? first : strut;
3386
+ if (!holds) continue;
3387
+ if (
3388
+ line.baseline < holds.ascent - 0.5 ||
3389
+ line.height - line.baseline < holds.descent - 0.5
3390
+ ) {
3391
+ return true;
3392
+ }
3393
+ }
3394
+ return false;
3395
+ }
3396
+
3397
+ /** A strut with room besides it about the same baseline, or the strut
3398
+ * where there is none. */
3399
+ function withRoom(
3400
+ strut: InlineDecoration | null,
3401
+ room: InlineDecoration | undefined,
3402
+ ): InlineDecoration | null {
3403
+ if (!room) return strut;
3404
+ if (!strut) return room;
3405
+ return {
3406
+ ascent: Math.max(strut.ascent, room.ascent),
3407
+ descent: Math.max(strut.descent, room.descent),
3408
+ };
3409
+ }
3410
+
3411
+ /** The share of the size a synthesized small capital is set at, rounded to
3412
+ * a whole pixel as Blink rounds it (`SimpleFontData`'s scaled font data):
3413
+ * CSS Fonts 4 (6.2) leaves the size to the user agent. */
3414
+ const SMALL_CAPS_SCALE = 0.7;
3415
+
3416
+ /**
3417
+ * Small capitals a face does not have, made of its capitals set smaller
3418
+ * (CSS Fonts 4, 6.2): where `font-variant-caps` asks for `smcp` — or
3419
+ * `c2sc` too, for `all-small-caps` — and the face does not answer it,
3420
+ * each letter that would be a small capital is its capital at 70% of the
3421
+ * size, in a run of its own: a lower-case letter, and for `all-small-caps`
3422
+ * the rest of the text too. Under `small-caps` what has no case — a space,
3423
+ * a digit, a mark of punctuation — keeps its size, as Blink keeps it. Only
3424
+ * a letter whose capital is one character is made one, so every offset
3425
+ * holds.
3426
+ */
3427
+ function synthesizeSmallCaps(items: Item[], fonts: FontsLike): void {
3428
+ for (let i = 0; i < items.length; i++) {
3429
+ const item = items[i];
3430
+ if (item.kind !== 'text' || item.control) continue;
3431
+ // `font-variant-caps`'s, which is synthesized where the face has none;
3432
+ // not `font-feature-settings`', which asks for the feature as it is
3433
+ const caps = item.box.style.fontVariantCaps;
3434
+ const features = item.run.features;
3435
+ if (!caps.includes('smcp') || !features?.smcp) continue;
3436
+ const all = caps.includes('c2sc');
3437
+ if (hasSmallCaps(fonts, item.run, all)) continue;
3438
+ const pieces = smallCapsPieces(item, all, withoutCaps(features));
3439
+ if (!pieces) continue;
3440
+ items.splice(i, 1, ...pieces);
3441
+ i += pieces.length - 1;
3442
+ }
3443
+ }
3444
+
3445
+ /** A text item cut where its letters turn to synthesized small capitals
3446
+ * and back, or null where none does. */
3447
+ function smallCapsPieces(
3448
+ item: Extract<Item, { kind: 'text' }>,
3449
+ all: boolean,
3450
+ features: Readonly<Record<string, number>> | null,
3451
+ ): Extract<Item, { kind: 'text' }>[] | null {
3452
+ const text = item.run.text;
3453
+ const pieces: Extract<Item, { kind: 'text' }>[] = [];
3454
+ let from = 0;
3455
+ let small = false;
3456
+ let capitals = '';
3457
+ const cut = (to: number): void => {
3458
+ if (to === from) return;
3459
+ const run: TextRun = small
3460
+ ? {
3461
+ ...item.run,
3462
+ text: capitals,
3463
+ size: Math.round((item.run.size ?? 16) * SMALL_CAPS_SCALE),
3464
+ }
3465
+ : { ...item.run, text: text.slice(from, to) };
3466
+ if (small) {
3467
+ if (features) run.features = features;
3468
+ else delete run.features;
3469
+ }
3470
+ pieces.push({ ...item, run, start: item.start + from, length: to - from });
3471
+ from = to;
3472
+ capitals = '';
3473
+ };
3474
+ let any = false;
3475
+ for (let at = 0; at < text.length;) {
3476
+ const ch = String.fromCodePoint(text.codePointAt(at)!);
3477
+ const upper = ch.toUpperCase();
3478
+ // everything, for `all-small-caps`: its spaces are small too
3479
+ const becomes = (all || upper !== ch) && upper.length === ch.length;
3480
+ if (becomes !== small) {
3481
+ cut(at);
3482
+ small = becomes;
3483
+ }
3484
+ if (becomes) {
3485
+ capitals += upper;
3486
+ any = true;
3487
+ }
3488
+ at += ch.length;
3489
+ }
3490
+ cut(text.length);
3491
+ return any ? pieces : null;
3492
+ }
3493
+
3494
+ /** Features without the small capitals a synthesis stands in for, one
3495
+ * object for each set, as `featuresOf` keeps them. */
3496
+ function withoutCaps(
3497
+ features: Readonly<Record<string, number>>,
3498
+ ): Readonly<Record<string, number>> | null {
3499
+ let kept = WITHOUT_CAPS.get(features);
3500
+ if (kept === undefined) {
3501
+ const out: Record<string, number> = {};
3502
+ for (const [tag, value] of Object.entries(features)) {
3503
+ if (tag !== 'smcp' && tag !== 'c2sc') out[tag] = value;
3504
+ }
3505
+ kept = Object.keys(out).length ? out : null;
3506
+ WITHOUT_CAPS.set(features, kept);
3507
+ }
3508
+ return kept;
3509
+ }
3510
+
3511
+ const WITHOUT_CAPS = new WeakMap<
3512
+ Readonly<Record<string, number>>,
3513
+ Readonly<Record<string, number>> | null
3514
+ >();
3515
+
3516
+ /**
3517
+ * Whether a run's face has small capitals of its own — and capitals to
3518
+ * small capitals, for `all-small-caps`: the alphabet set with the feature
3519
+ * and without it comes out another width where it does. Asked of the
3520
+ * engine's layout rather than of the font's tables, which a CoreText face
3521
+ * does not hand over. Kept per face.
3522
+ */
3523
+ function hasSmallCaps(fonts: FontsLike, run: TextRun, all: boolean): boolean {
3524
+ let known = SMALL_CAPS.get(fonts);
3525
+ if (!known) SMALL_CAPS.set(fonts, (known = new Map()));
3526
+ const key = `${run.family}|${run.weight}|${run.style}|${all}`;
3527
+ let has = known.get(key);
3528
+ if (has === undefined) {
3529
+ const face = {
3530
+ family: run.family,
3531
+ weight: run.weight,
3532
+ style: run.style,
3533
+ size: 16,
3534
+ };
3535
+ const width = (text: string, features: Record<string, number> | null) =>
3536
+ fonts.layout(
3537
+ [{ text, ...face, ...(features ? { features } : null) }],
3538
+ face,
3539
+ {},
3540
+ ).width;
3541
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
3542
+ has = Math.abs(width(lower, { smcp: 1 }) - width(lower, null)) > 0.01;
3543
+ if (has && all) {
3544
+ const upper = lower.toUpperCase();
3545
+ has = Math.abs(width(upper, { c2sc: 1 }) - width(upper, null)) > 0.01;
3546
+ }
3547
+ known.set(key, has);
3548
+ }
3549
+ return has;
3550
+ }
3551
+
3552
+ const SMALL_CAPS = new WeakMap<FontsLike, Map<string, boolean>>();
3553
+
3310
3554
  /**
3311
3555
  * The OpenType features a style's text is shaped with: the `font-variant`
3312
3556
  * longhands' and `font-kerning`'s, then `font-feature-settings`, which has