Skip to content

Commit 6521a48

Browse files
authored
feat: use native text segmenter where available (#2782)
1 parent 0476d06 commit 6521a48

File tree

4 files changed

+52
-17
lines changed

4 files changed

+52
-17
lines changed

src/core/features.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,12 @@ export const FEATURES = {
211211
const value = 'withCredentials' in new XMLHttpRequest();
212212
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
213213
return value;
214+
},
215+
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
216+
'use strict';
217+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
218+
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
219+
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
220+
return value;
214221
}
215222
};

src/css/layout/bounds.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class Bounds {
1717
}
1818

1919
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
20-
const domRect = Array.from(domRectList).find(rect => rect.width !== 0);
20+
const domRect = Array.from(domRectList).find((rect) => rect.width !== 0);
2121
return domRect
2222
? new Bounds(
2323
domRect.x + context.windowBounds.left,

src/css/layout/text.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,24 @@ export const parseTextBounds = (
2828
textList.forEach((text) => {
2929
if (styles.textDecorationLine.length || text.trim().length > 0) {
3030
if (FEATURES.SUPPORT_RANGE_BOUNDS) {
31-
if (!FEATURES.SUPPORT_WORD_BREAKING) {
32-
textBounds.push(
33-
new TextBounds(
34-
text,
35-
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects())
36-
)
37-
);
31+
const clientRects = createRange(node, offset, text.length).getClientRects();
32+
if (clientRects.length > 1) {
33+
const subSegments = segmentGraphemes(text);
34+
let subOffset = 0;
35+
subSegments.forEach((subSegment) => {
36+
textBounds.push(
37+
new TextBounds(
38+
subSegment,
39+
Bounds.fromDOMRectList(
40+
context,
41+
createRange(node, subOffset + offset, subSegment.length).getClientRects()
42+
)
43+
)
44+
);
45+
subOffset += subSegment.length;
46+
});
3847
} else {
39-
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length)));
48+
textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
4049
}
4150
} else {
4251
const replacementNode = node.splitText(text.length);
@@ -82,12 +91,32 @@ const createRange = (node: Text, offset: number, length: number): Range => {
8291
return range;
8392
};
8493

85-
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => {
86-
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect());
94+
export const segmentGraphemes = (value: string): string[] => {
95+
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
96+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
97+
const segmenter = new (Intl as any).Segmenter(void 0, {granularity: 'grapheme'});
98+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
99+
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
100+
}
101+
102+
return splitGraphemes(value);
103+
};
104+
105+
const segmentWords = (value: string, styles: CSSParsedDeclaration): string[] => {
106+
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
107+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
108+
const segmenter = new (Intl as any).Segmenter(void 0, {
109+
granularity: 'word'
110+
});
111+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
112+
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
113+
}
114+
115+
return breakWords(value, styles);
87116
};
88117

89118
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
90-
return styles.letterSpacing !== 0 ? splitGraphemes(value) : breakWords(value, styles);
119+
return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
91120
};
92121

93122
// https://drafts.csswg.org/css-text/#word-separator

src/render/canvas/canvas-renderer.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-
22
import {asString, Color, isTransparent} from '../../css/types/color';
33
import {ElementContainer, FLAGS} from '../../dom/element-container';
44
import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
5-
import {CSSParsedDeclaration} from '../../css/index';
5+
import {CSSParsedDeclaration} from '../../css';
66
import {TextContainer} from '../../dom/text-container';
77
import {Path, transformPath} from '../path';
88
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
@@ -18,12 +18,12 @@ import {
1818
} from '../border';
1919
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
2020
import {isDimensionToken} from '../../css/syntax/parser';
21-
import {TextBounds} from '../../css/layout/text';
21+
import {segmentGraphemes, TextBounds} from '../../css/layout/text';
2222
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
2323
import {contentBox} from '../box-sizing';
2424
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
2525
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
26-
import {ReplacedElementContainer} from '../../dom/replaced-elements/index';
26+
import {ReplacedElementContainer} from '../../dom/replaced-elements';
2727
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
2828
import {contains} from '../../core/bitwise';
2929
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
@@ -44,7 +44,6 @@ import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
4444
import {Renderer} from '../renderer';
4545
import {Context} from '../../core/context';
4646
import {DIRECTION} from '../../css/property-descriptors/direction';
47-
import {splitGraphemes} from 'text-segmentation';
4847

4948
export type RenderConfigurations = RenderOptions & {
5049
backgroundColor: Color | null;
@@ -149,7 +148,7 @@ export class CanvasRenderer extends Renderer {
149148
if (letterSpacing === 0) {
150149
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
151150
} else {
152-
const letters = splitGraphemes(text.text);
151+
const letters = segmentGraphemes(text.text);
153152
letters.reduce((left, letter) => {
154153
this.ctx.fillText(letter, left, text.bounds.top + baseline);
155154

0 commit comments

Comments
 (0)