feat: use native text segmenter where available (#2782)

This commit is contained in:
Niklas von Hertzen 2022-01-01 21:22:31 +08:00 committed by GitHub
parent 0476d06515
commit 6521a487d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 17 deletions

View File

@ -211,5 +211,12 @@ export const FEATURES = {
const value = 'withCredentials' in new XMLHttpRequest(); const value = 'withCredentials' in new XMLHttpRequest();
Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value}); Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {value});
return value; return value;
},
get SUPPORT_NATIVE_TEXT_SEGMENTATION(): boolean {
'use strict';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = !!(typeof Intl !== 'undefined' && (Intl as any).Segmenter);
Object.defineProperty(FEATURES, 'SUPPORT_NATIVE_TEXT_SEGMENTATION', {value});
return value;
} }
}; };

View File

@ -17,7 +17,7 @@ export class Bounds {
} }
static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds { static fromDOMRectList(context: Context, domRectList: DOMRectList): Bounds {
const domRect = Array.from(domRectList).find(rect => rect.width !== 0); const domRect = Array.from(domRectList).find((rect) => rect.width !== 0);
return domRect return domRect
? new Bounds( ? new Bounds(
domRect.x + context.windowBounds.left, domRect.x + context.windowBounds.left,

View File

@ -28,15 +28,24 @@ export const parseTextBounds = (
textList.forEach((text) => { textList.forEach((text) => {
if (styles.textDecorationLine.length || text.trim().length > 0) { if (styles.textDecorationLine.length || text.trim().length > 0) {
if (FEATURES.SUPPORT_RANGE_BOUNDS) { if (FEATURES.SUPPORT_RANGE_BOUNDS) {
if (!FEATURES.SUPPORT_WORD_BREAKING) { const clientRects = createRange(node, offset, text.length).getClientRects();
if (clientRects.length > 1) {
const subSegments = segmentGraphemes(text);
let subOffset = 0;
subSegments.forEach((subSegment) => {
textBounds.push( textBounds.push(
new TextBounds( new TextBounds(
text, subSegment,
Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects()) Bounds.fromDOMRectList(
context,
createRange(node, subOffset + offset, subSegment.length).getClientRects()
)
) )
); );
subOffset += subSegment.length;
});
} else { } else {
textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects)));
} }
} else { } else {
const replacementNode = node.splitText(text.length); const replacementNode = node.splitText(text.length);
@ -82,12 +91,32 @@ const createRange = (node: Text, offset: number, length: number): Range => {
return range; return range;
}; };
const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => { export const segmentGraphemes = (value: string): string[] => {
return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect()); if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const segmenter = new (Intl as any).Segmenter(void 0, {granularity: 'grapheme'});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
}
return splitGraphemes(value);
};
const segmentWords = (value: string, styles: CSSParsedDeclaration): string[] => {
if (FEATURES.SUPPORT_NATIVE_TEXT_SEGMENTATION) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const segmenter = new (Intl as any).Segmenter(void 0, {
granularity: 'word'
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Array.from(segmenter.segment(value)).map((segment: any) => segment.segment);
}
return breakWords(value, styles);
}; };
const breakText = (value: string, styles: CSSParsedDeclaration): string[] => { const breakText = (value: string, styles: CSSParsedDeclaration): string[] => {
return styles.letterSpacing !== 0 ? splitGraphemes(value) : breakWords(value, styles); return styles.letterSpacing !== 0 ? segmentGraphemes(value) : segmentWords(value, styles);
}; };
// https://drafts.csswg.org/css-text/#word-separator // https://drafts.csswg.org/css-text/#word-separator

View File

@ -2,7 +2,7 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking-
import {asString, Color, isTransparent} from '../../css/types/color'; import {asString, Color, isTransparent} from '../../css/types/color';
import {ElementContainer, FLAGS} from '../../dom/element-container'; import {ElementContainer, FLAGS} from '../../dom/element-container';
import {BORDER_STYLE} from '../../css/property-descriptors/border-style'; import {BORDER_STYLE} from '../../css/property-descriptors/border-style';
import {CSSParsedDeclaration} from '../../css/index'; import {CSSParsedDeclaration} from '../../css';
import {TextContainer} from '../../dom/text-container'; import {TextContainer} from '../../dom/text-container';
import {Path, transformPath} from '../path'; import {Path, transformPath} from '../path';
import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip'; import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip';
@ -18,12 +18,12 @@ import {
} from '../border'; } from '../border';
import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background'; import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background';
import {isDimensionToken} from '../../css/syntax/parser'; import {isDimensionToken} from '../../css/syntax/parser';
import {TextBounds} from '../../css/layout/text'; import {segmentGraphemes, TextBounds} from '../../css/layout/text';
import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container'; import {ImageElementContainer} from '../../dom/replaced-elements/image-element-container';
import {contentBox} from '../box-sizing'; import {contentBox} from '../box-sizing';
import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container'; import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-element-container';
import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container'; import {SVGElementContainer} from '../../dom/replaced-elements/svg-element-container';
import {ReplacedElementContainer} from '../../dom/replaced-elements/index'; import {ReplacedElementContainer} from '../../dom/replaced-elements';
import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects'; import {EffectTarget, IElementEffect, isClipEffect, isOpacityEffect, isTransformEffect} from '../effects';
import {contains} from '../../core/bitwise'; import {contains} from '../../core/bitwise';
import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient'; import {calculateGradientDirection, calculateRadius, processColorStops} from '../../css/types/functions/gradient';
@ -44,7 +44,6 @@ import {PAINT_ORDER_LAYER} from '../../css/property-descriptors/paint-order';
import {Renderer} from '../renderer'; import {Renderer} from '../renderer';
import {Context} from '../../core/context'; import {Context} from '../../core/context';
import {DIRECTION} from '../../css/property-descriptors/direction'; import {DIRECTION} from '../../css/property-descriptors/direction';
import {splitGraphemes} from 'text-segmentation';
export type RenderConfigurations = RenderOptions & { export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null; backgroundColor: Color | null;
@ -149,7 +148,7 @@ export class CanvasRenderer extends Renderer {
if (letterSpacing === 0) { if (letterSpacing === 0) {
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline); this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline);
} else { } else {
const letters = splitGraphemes(text.text); const letters = segmentGraphemes(text.text);
letters.reduce((left, letter) => { letters.reduce((left, letter) => {
this.ctx.fillText(letter, left, text.bounds.top + baseline); this.ctx.fillText(letter, left, text.bounds.top + baseline);