@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.
- package/dist/code/index.d.ts.map +1 -1
- package/dist/code/index.js +25 -24
- package/dist/code/index.js.map +1 -1
- package/dist/code-language/index.d.ts +1 -1
- package/dist/code-language/index.d.ts.map +1 -1
- package/dist/code-language/index.js +1 -1
- package/dist/code-language/index.js.map +1 -1
- package/dist/code-language/runs.d.ts +31 -0
- package/dist/code-language/runs.d.ts.map +1 -1
- package/dist/code-language/runs.js +105 -0
- package/dist/code-language/runs.js.map +1 -1
- package/dist/codeblock/index.d.ts +13 -0
- package/dist/codeblock/index.d.ts.map +1 -1
- package/dist/codeblock/index.js +37 -1
- package/dist/codeblock/index.js.map +1 -1
- package/dist/html/css/cascade.d.ts +32 -10
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +72 -27
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/style.d.ts +7 -4
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +37 -6
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/ua.js +8 -5
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +127 -9
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +27 -0
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +30 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +236 -14
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +118 -57
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +203 -37
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +27 -20
- package/dist/html/svg.js.map +1 -1
- package/dist/internal/codelines.d.ts +42 -0
- package/dist/internal/codelines.d.ts.map +1 -0
- package/dist/internal/codelines.js +170 -0
- package/dist/internal/codelines.js.map +1 -0
- package/dist/markdown/index.d.ts.map +1 -1
- package/dist/markdown/index.js +98 -26
- package/dist/markdown/index.js.map +1 -1
- package/dist/rich-text-editor/index.d.ts.map +1 -1
- package/dist/rich-text-editor/index.js +3 -8
- package/dist/rich-text-editor/index.js.map +1 -1
- package/dist/rich-text-editor/markdown.d.ts.map +1 -1
- package/dist/rich-text-editor/markdown.js +30 -1
- package/dist/rich-text-editor/markdown.js.map +1 -1
- package/dist/rich-text-editor/replace.d.ts +13 -0
- package/dist/rich-text-editor/replace.d.ts.map +1 -0
- package/dist/rich-text-editor/replace.js +64 -0
- package/dist/rich-text-editor/replace.js.map +1 -0
- package/dist/terminal-output/index.d.ts.map +1 -1
- package/dist/terminal-output/index.js +100 -25
- package/dist/terminal-output/index.js.map +1 -1
- package/dist/terminal-output/runs.d.ts +4 -1
- package/dist/terminal-output/runs.d.ts.map +1 -1
- package/dist/terminal-output/runs.js +5 -1
- package/dist/terminal-output/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/code/index.ts +34 -32
- package/src/code-language/index.ts +1 -1
- package/src/code-language/runs.ts +111 -1
- package/src/codeblock/index.ts +48 -1
- package/src/html/css/cascade.ts +117 -39
- package/src/html/css/style.ts +43 -15
- package/src/html/css/ua.ts +8 -5
- package/src/html/layout/block.ts +134 -8
- package/src/html/layout/boxes.ts +43 -2
- package/src/html/layout/inline.ts +257 -13
- package/src/html/node.ts +145 -79
- package/src/html/paint.ts +236 -45
- package/src/html/svg.ts +30 -19
- package/src/internal/codelines.ts +200 -0
- package/src/markdown/index.ts +145 -26
- package/src/rich-text-editor/index.ts +3 -11
- package/src/rich-text-editor/markdown.ts +36 -1
- package/src/rich-text-editor/replace.ts +85 -0
- package/src/terminal-output/index.ts +127 -34
- package/src/terminal-output/runs.ts +16 -2
package/src/html/layout/boxes.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
|
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
|