From 6521a487d78172f7179f7c973c1a3af40eb92009 Mon Sep 17 00:00:00 2001 From: Niklas von Hertzen Date: Sat, 1 Jan 2022 21:22:31 +0800 Subject: [PATCH] feat: use native text segmenter where available (#2782) --- src/core/features.ts | 7 ++++ src/css/layout/bounds.ts | 2 +- src/css/layout/text.ts | 51 ++++++++++++++++++++++------ src/render/canvas/canvas-renderer.ts | 9 +++-- 4 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/core/features.ts b/src/core/features.ts index 78514bd..64e8aea 100644 --- a/src/core/features.ts +++ b/src/core/features.ts @@ -211,5 +211,12 @@ export const FEATURES = { const value = 'withCredentials' in new XMLHttpRequest(); Object.defineProperty(FEATURES, 'SUPPORT_CORS_XHR', {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; } }; diff --git a/src/css/layout/bounds.ts b/src/css/layout/bounds.ts index 2f1c150..59da655 100644 --- a/src/css/layout/bounds.ts +++ b/src/css/layout/bounds.ts @@ -17,7 +17,7 @@ export class 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 ? new Bounds( domRect.x + context.windowBounds.left, diff --git a/src/css/layout/text.ts b/src/css/layout/text.ts index 59f0bde..0c68019 100644 --- a/src/css/layout/text.ts +++ b/src/css/layout/text.ts @@ -28,15 +28,24 @@ export const parseTextBounds = ( textList.forEach((text) => { if (styles.textDecorationLine.length || text.trim().length > 0) { if (FEATURES.SUPPORT_RANGE_BOUNDS) { - if (!FEATURES.SUPPORT_WORD_BREAKING) { - textBounds.push( - new TextBounds( - text, - Bounds.fromDOMRectList(context, createRange(node, offset, text.length).getClientRects()) - ) - ); + const clientRects = createRange(node, offset, text.length).getClientRects(); + if (clientRects.length > 1) { + const subSegments = segmentGraphemes(text); + let subOffset = 0; + subSegments.forEach((subSegment) => { + textBounds.push( + new TextBounds( + subSegment, + Bounds.fromDOMRectList( + context, + createRange(node, subOffset + offset, subSegment.length).getClientRects() + ) + ) + ); + subOffset += subSegment.length; + }); } else { - textBounds.push(new TextBounds(text, getRangeBounds(context, node, offset, text.length))); + textBounds.push(new TextBounds(text, Bounds.fromDOMRectList(context, clientRects))); } } else { const replacementNode = node.splitText(text.length); @@ -82,12 +91,32 @@ const createRange = (node: Text, offset: number, length: number): Range => { return range; }; -const getRangeBounds = (context: Context, node: Text, offset: number, length: number): Bounds => { - return Bounds.fromClientRect(context, createRange(node, offset, length).getBoundingClientRect()); +export const segmentGraphemes = (value: string): 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: '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[] => { - 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 diff --git a/src/render/canvas/canvas-renderer.ts b/src/render/canvas/canvas-renderer.ts index 077ddd0..6efb648 100644 --- a/src/render/canvas/canvas-renderer.ts +++ b/src/render/canvas/canvas-renderer.ts @@ -2,7 +2,7 @@ import {ElementPaint, parseStackingContexts, StackingContext} from '../stacking- import {asString, Color, isTransparent} from '../../css/types/color'; import {ElementContainer, FLAGS} from '../../dom/element-container'; 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 {Path, transformPath} from '../path'; import {BACKGROUND_CLIP} from '../../css/property-descriptors/background-clip'; @@ -18,12 +18,12 @@ import { } from '../border'; import {calculateBackgroundRendering, getBackgroundValueForIndex} from '../background'; 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 {contentBox} from '../box-sizing'; import {CanvasElementContainer} from '../../dom/replaced-elements/canvas-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 {contains} from '../../core/bitwise'; 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 {Context} from '../../core/context'; import {DIRECTION} from '../../css/property-descriptors/direction'; -import {splitGraphemes} from 'text-segmentation'; export type RenderConfigurations = RenderOptions & { backgroundColor: Color | null; @@ -149,7 +148,7 @@ export class CanvasRenderer extends Renderer { if (letterSpacing === 0) { this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + baseline); } else { - const letters = splitGraphemes(text.text); + const letters = segmentGraphemes(text.text); letters.reduce((left, letter) => { this.ctx.fillText(letter, left, text.bounds.top + baseline);