/** 
 | 
 * RichText is a container that manages complex text label. 
 | 
 * It will parse text string and create sub displayble elements respectively. 
 | 
 */ 
 | 
import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle } from '../core/types'; 
 | 
import { parseRichText, parsePlainText } from './helper/parseText'; 
 | 
import TSpan, { TSpanStyleProps } from './TSpan'; 
 | 
import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util'; 
 | 
import { adjustTextX, adjustTextY } from '../contain/text'; 
 | 
import ZRImage from './Image'; 
 | 
import Rect from './shape/Rect'; 
 | 
import BoundingRect from '../core/BoundingRect'; 
 | 
import { MatrixArray } from '../core/matrix'; 
 | 
import Displayable, { 
 | 
    DisplayableStatePropNames, 
 | 
    DisplayableProps, 
 | 
    DEFAULT_COMMON_ANIMATION_PROPS 
 | 
} from './Displayable'; 
 | 
import { ZRenderType } from '../zrender'; 
 | 
import Animator from '../animation/Animator'; 
 | 
import Transformable from '../core/Transformable'; 
 | 
import { ElementCommonState } from '../Element'; 
 | 
import { GroupLike } from './Group'; 
 | 
import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform'; 
 | 
  
 | 
type TextContentBlock = ReturnType<typeof parseRichText> 
 | 
type TextLine = TextContentBlock['lines'][0] 
 | 
type TextToken = TextLine['tokens'][0] 
 | 
  
 | 
// TODO Default value? 
 | 
export interface TextStylePropsPart { 
 | 
    // TODO Text is assigned inside zrender 
 | 
    text?: string 
 | 
  
 | 
    fill?: string 
 | 
    stroke?: string 
 | 
    strokeNoScale?: boolean 
 | 
  
 | 
    opacity?: number 
 | 
    fillOpacity?: number 
 | 
    strokeOpacity?: number 
 | 
    /** 
 | 
     * textStroke may be set as some color as a default 
 | 
     * value in upper applicaion, where the default value 
 | 
     * of lineWidth should be 0 to make sure that 
 | 
     * user can choose to do not use text stroke. 
 | 
     */ 
 | 
    lineWidth?: number 
 | 
    lineDash?: false | number[] 
 | 
    lineDashOffset?: number 
 | 
    borderDash?: false | number[] 
 | 
    borderDashOffset?: number 
 | 
  
 | 
    /** 
 | 
     * If `fontSize` or `fontFamily` exists, `font` will be reset by 
 | 
     * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`. 
 | 
     * So do not visit it directly in upper application (like echarts), 
 | 
     * but use `contain/text#makeFont` instead. 
 | 
     */ 
 | 
    font?: string 
 | 
    /** 
 | 
     * The same as font. Use font please. 
 | 
     * @deprecated 
 | 
     */ 
 | 
    textFont?: string 
 | 
  
 | 
    /** 
 | 
     * It helps merging respectively, rather than parsing an entire font string. 
 | 
     */ 
 | 
    fontStyle?: FontStyle 
 | 
    /** 
 | 
     * It helps merging respectively, rather than parsing an entire font string. 
 | 
     */ 
 | 
    fontWeight?: FontWeight 
 | 
    /** 
 | 
     * It helps merging respectively, rather than parsing an entire font string. 
 | 
     */ 
 | 
    fontFamily?: string 
 | 
    /** 
 | 
     * It helps merging respectively, rather than parsing an entire font string. 
 | 
     * Should be 12 but not '12px'. 
 | 
     */ 
 | 
    fontSize?: number | string 
 | 
  
 | 
    align?: TextAlign 
 | 
    verticalAlign?: TextVerticalAlign 
 | 
  
 | 
    /** 
 | 
     * Line height. Default to be text height of '国' 
 | 
     */ 
 | 
    lineHeight?: number 
 | 
    /** 
 | 
     * Width of text block. Not include padding 
 | 
     * Used for background, truncate, wrap 
 | 
     */ 
 | 
    width?: number | string 
 | 
    /** 
 | 
     * Height of text block. Not include padding 
 | 
     * Used for background, truncate 
 | 
     */ 
 | 
    height?: number 
 | 
    /** 
 | 
     * Reserved for special functinality, like 'hr'. 
 | 
     */ 
 | 
    tag?: string 
 | 
  
 | 
    textShadowColor?: string 
 | 
    textShadowBlur?: number 
 | 
    textShadowOffsetX?: number 
 | 
    textShadowOffsetY?: number 
 | 
  
 | 
    // Shadow, background, border of text box. 
 | 
    backgroundColor?: string | { 
 | 
        image: ImageLike | string 
 | 
    } 
 | 
  
 | 
    /** 
 | 
     * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]` 
 | 
     */ 
 | 
    padding?: number | number[] 
 | 
    /** 
 | 
     * Margin of label. Used when layouting the label. 
 | 
     */ 
 | 
    margin?: number 
 | 
  
 | 
    borderColor?: string 
 | 
    borderWidth?: number 
 | 
    borderRadius?: number | number[] 
 | 
  
 | 
    /** 
 | 
     * Shadow color for background box. 
 | 
     */ 
 | 
    shadowColor?: string 
 | 
    /** 
 | 
     * Shadow blur for background box. 
 | 
     */ 
 | 
    shadowBlur?: number 
 | 
    /** 
 | 
     * Shadow offset x for background box. 
 | 
     */ 
 | 
    shadowOffsetX?: number 
 | 
    /** 
 | 
     * Shadow offset y for background box. 
 | 
     */ 
 | 
    shadowOffsetY?: number 
 | 
} 
 | 
export interface TextStyleProps extends TextStylePropsPart { 
 | 
  
 | 
    text?: string 
 | 
  
 | 
    x?: number 
 | 
    y?: number 
 | 
  
 | 
    /** 
 | 
     * Only support number in the top block. 
 | 
     */ 
 | 
    width?: number 
 | 
    /** 
 | 
     * Text styles for rich text. 
 | 
     */ 
 | 
    rich?: Dictionary<TextStylePropsPart> 
 | 
  
 | 
    /** 
 | 
     * Strategy when calculated text width exceeds textWidth. 
 | 
     * break: break by word 
 | 
     * break: will break inside the word 
 | 
     * truncate: truncate the text and show ellipsis 
 | 
     * Do nothing if not set 
 | 
     */ 
 | 
    overflow?: 'break' | 'breakAll' | 'truncate' | 'none' 
 | 
  
 | 
    /** 
 | 
     * Strategy when text lines exceeds textHeight. 
 | 
     * Do nothing if not set 
 | 
     */ 
 | 
    lineOverflow?: 'truncate' 
 | 
  
 | 
    /** 
 | 
     * Epllipsis used if text is truncated 
 | 
     */ 
 | 
    ellipsis?: string 
 | 
    /** 
 | 
     * Placeholder used if text is truncated to empty 
 | 
     */ 
 | 
    placeholder?: string 
 | 
    /** 
 | 
     * Min characters for truncating 
 | 
     */ 
 | 
    truncateMinChar?: number 
 | 
} 
 | 
  
 | 
export interface TextProps extends DisplayableProps { 
 | 
    style?: TextStyleProps 
 | 
  
 | 
    zlevel?: number 
 | 
    z?: number 
 | 
    z2?: number 
 | 
  
 | 
    culling?: boolean 
 | 
    cursor?: string 
 | 
} 
 | 
  
 | 
export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonState 
 | 
  
 | 
export type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & { 
 | 
    autoStroke?: boolean 
 | 
}; 
 | 
  
 | 
const DEFAULT_RICH_TEXT_COLOR = { 
 | 
    fill: '#000' 
 | 
}; 
 | 
const DEFAULT_STROKE_LINE_WIDTH = 2; 
 | 
  
 | 
// const DEFAULT_TEXT_STYLE: TextStyleProps = { 
 | 
//     x: 0, 
 | 
//     y: 0, 
 | 
//     fill: '#000', 
 | 
//     stroke: null, 
 | 
//     opacity: 0, 
 | 
//     fillOpacity: 
 | 
// } 
 | 
  
 | 
export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = { 
 | 
    style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({ 
 | 
        fill: true, 
 | 
        stroke: true, 
 | 
        fillOpacity: true, 
 | 
        strokeOpacity: true, 
 | 
        lineWidth: true, 
 | 
        fontSize: true, 
 | 
        lineHeight: true, 
 | 
        width: true, 
 | 
        height: true, 
 | 
        textShadowColor: true, 
 | 
        textShadowBlur: true, 
 | 
        textShadowOffsetX: true, 
 | 
        textShadowOffsetY: true, 
 | 
        backgroundColor: true, 
 | 
        padding: true,  // TODO needs normalize padding before animate 
 | 
        borderColor: true, 
 | 
        borderWidth: true, 
 | 
        borderRadius: true  // TODO needs normalize radius before animate 
 | 
    }, DEFAULT_COMMON_ANIMATION_PROPS.style) 
 | 
 }; 
 | 
  
 | 
  
 | 
interface ZRText { 
 | 
    animate(key?: '', loop?: boolean): Animator<this> 
 | 
    animate(key: 'style', loop?: boolean): Animator<this['style']> 
 | 
  
 | 
    getState(stateName: string): TextState 
 | 
    ensureState(stateName: string): TextState 
 | 
  
 | 
    states: Dictionary<TextState> 
 | 
    stateProxy: (stateName: string) => TextState 
 | 
} 
 | 
  
 | 
class ZRText extends Displayable<TextProps> implements GroupLike { 
 | 
  
 | 
    type = 'text' 
 | 
  
 | 
    style: TextStyleProps 
 | 
  
 | 
    /** 
 | 
     * How to handling label overlap 
 | 
     * 
 | 
     * hidden: 
 | 
     */ 
 | 
    overlap: 'hidden' | 'show' | 'blur' 
 | 
  
 | 
    /** 
 | 
     * Will use this to calculate transform matrix 
 | 
     * instead of Element itseelf if it's give. 
 | 
     * Not exposed to developers 
 | 
     */ 
 | 
    innerTransformable: Transformable 
 | 
  
 | 
    // Be `true` if and only if the result text is modified due to overflow, due to 
 | 
    // settings on either `overflow` or `lineOverflow`. Based on this the caller can 
 | 
    // take some action like showing the original text in a particular tip. 
 | 
    // Only take effect after rendering. So do not visit it before it. 
 | 
    isTruncated: boolean 
 | 
  
 | 
    private _children: (ZRImage | Rect | TSpan)[] = [] 
 | 
  
 | 
    private _childCursor: 0 
 | 
  
 | 
    private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR 
 | 
  
 | 
    constructor(opts?: TextProps) { 
 | 
        super(); 
 | 
        this.attr(opts); 
 | 
    } 
 | 
  
 | 
    childrenRef() { 
 | 
        return this._children; 
 | 
    } 
 | 
  
 | 
    update() { 
 | 
  
 | 
        super.update(); 
 | 
  
 | 
        // Update children 
 | 
        if (this.styleChanged()) { 
 | 
            this._updateSubTexts(); 
 | 
        } 
 | 
  
 | 
        for (let i = 0; i < this._children.length; i++) { 
 | 
            const child = this._children[i]; 
 | 
            // Set common properties. 
 | 
            child.zlevel = this.zlevel; 
 | 
            child.z = this.z; 
 | 
            child.z2 = this.z2; 
 | 
            child.culling = this.culling; 
 | 
            child.cursor = this.cursor; 
 | 
            child.invisible = this.invisible; 
 | 
        } 
 | 
    } 
 | 
  
 | 
     updateTransform() { 
 | 
        const innerTransformable = this.innerTransformable; 
 | 
        if (innerTransformable) { 
 | 
            innerTransformable.updateTransform(); 
 | 
            if (innerTransformable.transform) { 
 | 
                this.transform = innerTransformable.transform; 
 | 
            } 
 | 
        } 
 | 
        else { 
 | 
            super.updateTransform(); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    getLocalTransform(m?: MatrixArray): MatrixArray { 
 | 
        const innerTransformable = this.innerTransformable; 
 | 
        return innerTransformable 
 | 
            ? innerTransformable.getLocalTransform(m) 
 | 
            : super.getLocalTransform(m); 
 | 
    } 
 | 
  
 | 
    // TODO override setLocalTransform? 
 | 
    getComputedTransform() { 
 | 
        if (this.__hostTarget) { 
 | 
            // Update host target transform 
 | 
            this.__hostTarget.getComputedTransform(); 
 | 
            // Update text position. 
 | 
            this.__hostTarget.updateInnerText(true); 
 | 
        } 
 | 
  
 | 
        return super.getComputedTransform(); 
 | 
    } 
 | 
  
 | 
    private _updateSubTexts() { 
 | 
        // Reset child visit cursor 
 | 
        this._childCursor = 0; 
 | 
  
 | 
        normalizeTextStyle(this.style); 
 | 
        this.style.rich 
 | 
            ? this._updateRichTexts() 
 | 
            : this._updatePlainTexts(); 
 | 
  
 | 
        this._children.length = this._childCursor; 
 | 
  
 | 
        this.styleUpdated(); 
 | 
    } 
 | 
  
 | 
    addSelfToZr(zr: ZRenderType) { 
 | 
        super.addSelfToZr(zr); 
 | 
        for (let i = 0; i < this._children.length; i++) { 
 | 
            // Also need mount __zr for case like hover detection. 
 | 
            // The case: hover on a label (position: 'top') causes host el 
 | 
            // scaled and label Y position lifts a bit so that out of the 
 | 
            // pointer, then mouse move should be able to trigger "mouseout". 
 | 
            this._children[i].__zr = zr; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    removeSelfFromZr(zr: ZRenderType) { 
 | 
        super.removeSelfFromZr(zr); 
 | 
        for (let i = 0; i < this._children.length; i++) { 
 | 
            this._children[i].__zr = null; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    getBoundingRect(): BoundingRect { 
 | 
        if (this.styleChanged()) { 
 | 
            this._updateSubTexts(); 
 | 
        } 
 | 
        if (!this._rect) { 
 | 
            // TODO: Optimize when using width and overflow: wrap/truncate 
 | 
            const tmpRect = new BoundingRect(0, 0, 0, 0); 
 | 
            const children = this._children; 
 | 
            const tmpMat: MatrixArray = []; 
 | 
            let rect = null; 
 | 
  
 | 
            for (let i = 0; i < children.length; i++) { 
 | 
                const child = children[i]; 
 | 
                const childRect = child.getBoundingRect(); 
 | 
                const transform = child.getLocalTransform(tmpMat); 
 | 
  
 | 
                if (transform) { 
 | 
                    tmpRect.copy(childRect); 
 | 
                    tmpRect.applyTransform(transform); 
 | 
                    rect = rect || tmpRect.clone(); 
 | 
                    rect.union(tmpRect); 
 | 
                } 
 | 
                else { 
 | 
                    rect = rect || childRect.clone(); 
 | 
                    rect.union(childRect); 
 | 
                } 
 | 
            } 
 | 
            this._rect = rect || tmpRect; 
 | 
        } 
 | 
        return this._rect; 
 | 
    } 
 | 
  
 | 
    // Can be set in Element. To calculate text fill automatically when textContent is inside element 
 | 
    setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) { 
 | 
        // Use builtin if defaultTextStyle is not given. 
 | 
        this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR; 
 | 
    } 
 | 
  
 | 
    setTextContent(textContent: never) { 
 | 
        if (process.env.NODE_ENV !== 'production') { 
 | 
            throw new Error('Can\'t attach text on another text'); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    // getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] { 
 | 
    //     // Default value is on the prototype. 
 | 
    //     return this.style.prototype[key]; 
 | 
    // } 
 | 
  
 | 
    protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) { 
 | 
        if (!sourceStyle) { 
 | 
            return targetStyle; 
 | 
        } 
 | 
  
 | 
        // DO deep merge on rich configurations. 
 | 
        const sourceRich = sourceStyle.rich; 
 | 
        const targetRich = targetStyle.rich || (sourceRich && {});  // Create a new one if source have rich but target don't 
 | 
  
 | 
        extend(targetStyle, sourceStyle); 
 | 
  
 | 
        if (sourceRich && targetRich) { 
 | 
            // merge rich and assign rich again. 
 | 
            this._mergeRich(targetRich, sourceRich); 
 | 
            targetStyle.rich = targetRich; 
 | 
        } 
 | 
        else if (targetRich) { 
 | 
            // If source rich not exists. DON'T override the target rich 
 | 
            targetStyle.rich = targetRich; 
 | 
        } 
 | 
  
 | 
        return targetStyle; 
 | 
    } 
 | 
  
 | 
    private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) { 
 | 
        const richNames = keys(sourceRich); 
 | 
        // Merge by rich names. 
 | 
        for (let i = 0; i < richNames.length; i++) { 
 | 
            const richName = richNames[i]; 
 | 
            targetRich[richName] = targetRich[richName] || {}; 
 | 
            extend(targetRich[richName], sourceRich[richName]); 
 | 
        } 
 | 
    } 
 | 
  
 | 
    getAnimationStyleProps() { 
 | 
        return DEFAULT_TEXT_ANIMATION_PROPS; 
 | 
    } 
 | 
  
 | 
  
 | 
    private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan 
 | 
    private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage 
 | 
    private _getOrCreateChild(Ctor: {new(): Rect}): Rect 
 | 
    private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage { 
 | 
        let child = this._children[this._childCursor]; 
 | 
        if (!child || !(child instanceof Ctor)) { 
 | 
            child = new Ctor(); 
 | 
        } 
 | 
        this._children[this._childCursor++] = child; 
 | 
        child.__zr = this.__zr; 
 | 
        // TODO to users parent can only be group. 
 | 
        child.parent = this as any; 
 | 
        return child; 
 | 
    } 
 | 
  
 | 
    private _updatePlainTexts() { 
 | 
        const style = this.style; 
 | 
        const textFont = style.font || DEFAULT_FONT; 
 | 
        const textPadding = style.padding as number[]; 
 | 
  
 | 
        const text = getStyleText(style); 
 | 
        const contentBlock = parsePlainText(text, style); 
 | 
        const needDrawBg = needDrawBackground(style); 
 | 
        const bgColorDrawn = !!(style.backgroundColor); 
 | 
  
 | 
        const outerHeight = contentBlock.outerHeight; 
 | 
        const outerWidth = contentBlock.outerWidth; 
 | 
        const contentWidth = contentBlock.contentWidth; 
 | 
  
 | 
        const textLines = contentBlock.lines; 
 | 
        const lineHeight = contentBlock.lineHeight; 
 | 
  
 | 
        const defaultStyle = this._defaultStyle; 
 | 
  
 | 
        this.isTruncated = !!contentBlock.isTruncated; 
 | 
  
 | 
        const baseX = style.x || 0; 
 | 
        const baseY = style.y || 0; 
 | 
        const textAlign = style.align || defaultStyle.align || 'left'; 
 | 
        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top'; 
 | 
  
 | 
        let textX = baseX; 
 | 
        let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign); 
 | 
  
 | 
        if (needDrawBg || textPadding) { 
 | 
            // Consider performance, do not call getTextWidth util necessary. 
 | 
            const boxX = adjustTextX(baseX, outerWidth, textAlign); 
 | 
            const boxY = adjustTextY(baseY, outerHeight, verticalAlign); 
 | 
            needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight); 
 | 
        } 
 | 
  
 | 
        // `textBaseline` is set as 'middle'. 
 | 
        textY += lineHeight / 2; 
 | 
  
 | 
        if (textPadding) { 
 | 
            textX = getTextXForPadding(baseX, textAlign, textPadding); 
 | 
            if (verticalAlign === 'top') { 
 | 
                textY += textPadding[0]; 
 | 
            } 
 | 
            else if (verticalAlign === 'bottom') { 
 | 
                textY -= textPadding[2]; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        let defaultLineWidth = 0; 
 | 
        let useDefaultFill = false; 
 | 
        const textFill = getFill( 
 | 
            'fill' in style 
 | 
                ? style.fill 
 | 
                : (useDefaultFill = true, defaultStyle.fill) 
 | 
        ); 
 | 
        const textStroke = getStroke( 
 | 
            'stroke' in style 
 | 
                ? style.stroke 
 | 
                : (!bgColorDrawn 
 | 
                    // If we use "auto lineWidth" widely, it probably bring about some bad case. 
 | 
                    // So the current strategy is: 
 | 
                    // If `style.fill` is specified (i.e., `useDefaultFill` is `false`) 
 | 
                    // (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color 
 | 
                    //   (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display 
 | 
                    //   the auto stroke because we can not make sure wether the stoke is approperiate to 
 | 
                    //   the given `fill`. 
 | 
                    // (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color, 
 | 
                    // we give the auto lineWidth to display the given stoke color. 
 | 
                    && (!defaultStyle.autoStroke || useDefaultFill) 
 | 
                ) 
 | 
                ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke) 
 | 
                : null 
 | 
        ); 
 | 
  
 | 
        const hasShadow = style.textShadowBlur > 0; 
 | 
  
 | 
        const fixedBoundingRect = style.width != null 
 | 
            && (style.overflow === 'truncate' || style.overflow === 'break' || style.overflow === 'breakAll'); 
 | 
        const calculatedLineHeight = contentBlock.calculatedLineHeight; 
 | 
  
 | 
        for (let i = 0; i < textLines.length; i++) { 
 | 
            const el = this._getOrCreateChild(TSpan); 
 | 
            // Always create new style. 
 | 
            const subElStyle: TSpanStyleProps = el.createStyle(); 
 | 
            el.useStyle(subElStyle); 
 | 
            subElStyle.text = textLines[i]; 
 | 
            subElStyle.x = textX; 
 | 
            subElStyle.y = textY; 
 | 
            // Always set textAlign and textBase line, because it is difficute to calculate 
 | 
            // textAlign from prevEl, and we dont sure whether textAlign will be reset if 
 | 
            // font set happened. 
 | 
            if (textAlign) { 
 | 
                subElStyle.textAlign = textAlign; 
 | 
            } 
 | 
            // Force baseline to be "middle". Otherwise, if using "top", the 
 | 
            // text will offset downward a little bit in font "Microsoft YaHei". 
 | 
            subElStyle.textBaseline = 'middle'; 
 | 
            subElStyle.opacity = style.opacity; 
 | 
            // Fill after stroke so the outline will not cover the main part. 
 | 
            subElStyle.strokeFirst = true; 
 | 
  
 | 
            if (hasShadow) { 
 | 
                subElStyle.shadowBlur = style.textShadowBlur || 0; 
 | 
                subElStyle.shadowColor = style.textShadowColor || 'transparent'; 
 | 
                subElStyle.shadowOffsetX = style.textShadowOffsetX || 0; 
 | 
                subElStyle.shadowOffsetY = style.textShadowOffsetY || 0; 
 | 
            } 
 | 
  
 | 
            // Always override default fill and stroke value. 
 | 
            subElStyle.stroke = textStroke as string; 
 | 
            subElStyle.fill = textFill as string; 
 | 
  
 | 
            if (textStroke) { 
 | 
                subElStyle.lineWidth = style.lineWidth || defaultLineWidth; 
 | 
                subElStyle.lineDash = style.lineDash; 
 | 
                subElStyle.lineDashOffset = style.lineDashOffset || 0; 
 | 
            } 
 | 
  
 | 
            subElStyle.font = textFont; 
 | 
            setSeparateFont(subElStyle, style); 
 | 
  
 | 
            textY += lineHeight; 
 | 
  
 | 
            if (fixedBoundingRect) { 
 | 
                el.setBoundingRect(new BoundingRect( 
 | 
                    adjustTextX(subElStyle.x, contentWidth, subElStyle.textAlign as TextAlign), 
 | 
                    adjustTextY(subElStyle.y, calculatedLineHeight, subElStyle.textBaseline as TextVerticalAlign), 
 | 
                    /** 
 | 
                     * Text boundary should be the real text width. 
 | 
                     * Otherwise, there will be extra space in the 
 | 
                     * bounding rect calculated. 
 | 
                     */ 
 | 
                    contentWidth, 
 | 
                    calculatedLineHeight 
 | 
                )); 
 | 
            } 
 | 
        } 
 | 
    } 
 | 
  
 | 
  
 | 
    private _updateRichTexts() { 
 | 
        const style = this.style; 
 | 
  
 | 
        // TODO Only parse when text changed? 
 | 
        const text = getStyleText(style); 
 | 
        const contentBlock = parseRichText(text, style); 
 | 
  
 | 
        const contentWidth = contentBlock.width; 
 | 
        const outerWidth = contentBlock.outerWidth; 
 | 
        const outerHeight = contentBlock.outerHeight; 
 | 
        const textPadding = style.padding as number[]; 
 | 
  
 | 
        const baseX = style.x || 0; 
 | 
        const baseY = style.y || 0; 
 | 
        const defaultStyle = this._defaultStyle; 
 | 
        const textAlign = style.align || defaultStyle.align; 
 | 
        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign; 
 | 
  
 | 
        this.isTruncated = !!contentBlock.isTruncated; 
 | 
  
 | 
        const boxX = adjustTextX(baseX, outerWidth, textAlign); 
 | 
        const boxY = adjustTextY(baseY, outerHeight, verticalAlign); 
 | 
        let xLeft = boxX; 
 | 
        let lineTop = boxY; 
 | 
  
 | 
        if (textPadding) { 
 | 
            xLeft += textPadding[3]; 
 | 
            lineTop += textPadding[0]; 
 | 
        } 
 | 
  
 | 
        let xRight = xLeft + contentWidth; 
 | 
  
 | 
        if (needDrawBackground(style)) { 
 | 
            this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight); 
 | 
        } 
 | 
        const bgColorDrawn = !!(style.backgroundColor); 
 | 
  
 | 
        for (let i = 0; i < contentBlock.lines.length; i++) { 
 | 
            const line = contentBlock.lines[i]; 
 | 
            const tokens = line.tokens; 
 | 
            const tokenCount = tokens.length; 
 | 
            const lineHeight = line.lineHeight; 
 | 
  
 | 
            let remainedWidth = line.width; 
 | 
            let leftIndex = 0; 
 | 
            let lineXLeft = xLeft; 
 | 
            let lineXRight = xRight; 
 | 
            let rightIndex = tokenCount - 1; 
 | 
            let token; 
 | 
  
 | 
            while ( 
 | 
                leftIndex < tokenCount 
 | 
                && (token = tokens[leftIndex], !token.align || token.align === 'left') 
 | 
            ) { 
 | 
                this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn); 
 | 
                remainedWidth -= token.width; 
 | 
                lineXLeft += token.width; 
 | 
                leftIndex++; 
 | 
            } 
 | 
  
 | 
            while ( 
 | 
                rightIndex >= 0 
 | 
                && (token = tokens[rightIndex], token.align === 'right') 
 | 
            ) { 
 | 
                this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn); 
 | 
                remainedWidth -= token.width; 
 | 
                lineXRight -= token.width; 
 | 
                rightIndex--; 
 | 
            } 
 | 
  
 | 
            // The other tokens are placed as textAlign 'center' if there is enough space. 
 | 
            lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - remainedWidth) / 2; 
 | 
            while (leftIndex <= rightIndex) { 
 | 
                token = tokens[leftIndex]; 
 | 
                // Consider width specified by user, use 'center' rather than 'left'. 
 | 
                this._placeToken( 
 | 
                    token, style, lineHeight, lineTop, 
 | 
                    lineXLeft + token.width / 2, 'center', bgColorDrawn 
 | 
                ); 
 | 
                lineXLeft += token.width; 
 | 
                leftIndex++; 
 | 
            } 
 | 
  
 | 
            lineTop += lineHeight; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    private _placeToken( 
 | 
        token: TextToken, 
 | 
        style: TextStyleProps, 
 | 
        lineHeight: number, 
 | 
        lineTop: number, 
 | 
        x: number, 
 | 
        textAlign: string, 
 | 
        parentBgColorDrawn: boolean 
 | 
    ) { 
 | 
        const tokenStyle = style.rich[token.styleName] || {}; 
 | 
        tokenStyle.text = token.text; 
 | 
  
 | 
        // 'ctx.textBaseline' is always set as 'middle', for sake of 
 | 
        // the bias of "Microsoft YaHei". 
 | 
        const verticalAlign = token.verticalAlign; 
 | 
        let y = lineTop + lineHeight / 2; 
 | 
        if (verticalAlign === 'top') { 
 | 
            y = lineTop + token.height / 2; 
 | 
        } 
 | 
        else if (verticalAlign === 'bottom') { 
 | 
            y = lineTop + lineHeight - token.height / 2; 
 | 
        } 
 | 
  
 | 
        const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle); 
 | 
        needDrawBg && this._renderBackground( 
 | 
            tokenStyle, 
 | 
            style, 
 | 
            textAlign === 'right' 
 | 
                ? x - token.width 
 | 
                : textAlign === 'center' 
 | 
                ? x - token.width / 2 
 | 
                : x, 
 | 
            y - token.height / 2, 
 | 
            token.width, 
 | 
            token.height 
 | 
        ); 
 | 
        const bgColorDrawn = !!tokenStyle.backgroundColor; 
 | 
  
 | 
        const textPadding = token.textPadding; 
 | 
        if (textPadding) { 
 | 
            x = getTextXForPadding(x, textAlign, textPadding); 
 | 
            y -= token.height / 2 - textPadding[0] - token.innerHeight / 2; 
 | 
        } 
 | 
  
 | 
        const el = this._getOrCreateChild(TSpan); 
 | 
        const subElStyle: TSpanStyleProps = el.createStyle(); 
 | 
        // Always create new style. 
 | 
        el.useStyle(subElStyle); 
 | 
  
 | 
        const defaultStyle = this._defaultStyle; 
 | 
        let useDefaultFill = false; 
 | 
        let defaultLineWidth = 0; 
 | 
        const textFill = getFill( 
 | 
            'fill' in tokenStyle ? tokenStyle.fill 
 | 
                : 'fill' in style ? style.fill 
 | 
                : (useDefaultFill = true, defaultStyle.fill) 
 | 
        ); 
 | 
        const textStroke = getStroke( 
 | 
            'stroke' in tokenStyle ? tokenStyle.stroke 
 | 
                : 'stroke' in style ? style.stroke 
 | 
                : ( 
 | 
                    !bgColorDrawn 
 | 
                    && !parentBgColorDrawn 
 | 
                    // See the strategy explained above. 
 | 
                    && (!defaultStyle.autoStroke || useDefaultFill) 
 | 
                ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke) 
 | 
                : null 
 | 
        ); 
 | 
  
 | 
        const hasShadow = tokenStyle.textShadowBlur > 0 
 | 
            || style.textShadowBlur > 0; 
 | 
  
 | 
        subElStyle.text = token.text; 
 | 
        subElStyle.x = x; 
 | 
        subElStyle.y = y; 
 | 
        if (hasShadow) { 
 | 
            subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0; 
 | 
            subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent'; 
 | 
            subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0; 
 | 
            subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0; 
 | 
        } 
 | 
  
 | 
        subElStyle.textAlign = textAlign as CanvasTextAlign; 
 | 
        // Force baseline to be "middle". Otherwise, if using "top", the 
 | 
        // text will offset downward a little bit in font "Microsoft YaHei". 
 | 
        subElStyle.textBaseline = 'middle'; 
 | 
        subElStyle.font = token.font || DEFAULT_FONT; 
 | 
        subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1); 
 | 
  
 | 
  
 | 
        // TODO inherit each item from top style in token style? 
 | 
        setSeparateFont(subElStyle, tokenStyle); 
 | 
  
 | 
        if (textStroke) { 
 | 
            subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth); 
 | 
            subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash); 
 | 
            subElStyle.lineDashOffset = style.lineDashOffset || 0; 
 | 
            subElStyle.stroke = textStroke; 
 | 
        } 
 | 
        if (textFill) { 
 | 
            subElStyle.fill = textFill; 
 | 
        } 
 | 
  
 | 
        const textWidth = token.contentWidth; 
 | 
        const textHeight = token.contentHeight; 
 | 
        // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared. 
 | 
        el.setBoundingRect(new BoundingRect( 
 | 
            adjustTextX(subElStyle.x, textWidth, subElStyle.textAlign as TextAlign), 
 | 
            adjustTextY(subElStyle.y, textHeight, subElStyle.textBaseline as TextVerticalAlign), 
 | 
            textWidth, 
 | 
            textHeight 
 | 
        )); 
 | 
    } 
 | 
  
 | 
    private _renderBackground( 
 | 
        style: TextStylePropsPart, 
 | 
        topStyle: TextStylePropsPart, 
 | 
        x: number, 
 | 
        y: number, 
 | 
        width: number, 
 | 
        height: number 
 | 
    ) { 
 | 
        const textBackgroundColor = style.backgroundColor; 
 | 
        const textBorderWidth = style.borderWidth; 
 | 
        const textBorderColor = style.borderColor; 
 | 
        const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image; 
 | 
        const isPlainOrGradientBg = textBackgroundColor && !isImageBg; 
 | 
        const textBorderRadius = style.borderRadius; 
 | 
        const self = this; 
 | 
  
 | 
        let rectEl: Rect; 
 | 
        let imgEl: ZRImage; 
 | 
        if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) { 
 | 
            // Background is color 
 | 
            rectEl = this._getOrCreateChild(Rect); 
 | 
            rectEl.useStyle(rectEl.createStyle());    // Create an empty style. 
 | 
            rectEl.style.fill = null; 
 | 
            const rectShape = rectEl.shape; 
 | 
            rectShape.x = x; 
 | 
            rectShape.y = y; 
 | 
            rectShape.width = width; 
 | 
            rectShape.height = height; 
 | 
            rectShape.r = textBorderRadius; 
 | 
            rectEl.dirtyShape(); 
 | 
        } 
 | 
  
 | 
        if (isPlainOrGradientBg) { 
 | 
            const rectStyle = rectEl.style; 
 | 
            rectStyle.fill = textBackgroundColor as string || null; 
 | 
            rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1); 
 | 
        } 
 | 
        else if (isImageBg) { 
 | 
            imgEl = this._getOrCreateChild(ZRImage); 
 | 
            imgEl.onload = function () { 
 | 
                // Refresh and relayout after image loaded. 
 | 
                self.dirtyStyle(); 
 | 
            }; 
 | 
            const imgStyle = imgEl.style; 
 | 
            imgStyle.image = (textBackgroundColor as {image: ImageLike}).image; 
 | 
            imgStyle.x = x; 
 | 
            imgStyle.y = y; 
 | 
            imgStyle.width = width; 
 | 
            imgStyle.height = height; 
 | 
        } 
 | 
  
 | 
        if (textBorderWidth && textBorderColor) { 
 | 
            const rectStyle = rectEl.style; 
 | 
            rectStyle.lineWidth = textBorderWidth; 
 | 
            rectStyle.stroke = textBorderColor; 
 | 
            rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1); 
 | 
            rectStyle.lineDash = style.borderDash; 
 | 
            rectStyle.lineDashOffset = style.borderDashOffset || 0; 
 | 
            rectEl.strokeContainThreshold = 0; 
 | 
  
 | 
            // Making shadow looks better. 
 | 
            if (rectEl.hasFill() && rectEl.hasStroke()) { 
 | 
                rectStyle.strokeFirst = true; 
 | 
                rectStyle.lineWidth *= 2; 
 | 
            } 
 | 
        } 
 | 
  
 | 
        const commonStyle = (rectEl || imgEl).style; 
 | 
        commonStyle.shadowBlur = style.shadowBlur || 0; 
 | 
        commonStyle.shadowColor = style.shadowColor || 'transparent'; 
 | 
        commonStyle.shadowOffsetX = style.shadowOffsetX || 0; 
 | 
        commonStyle.shadowOffsetY = style.shadowOffsetY || 0; 
 | 
        commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1); 
 | 
    } 
 | 
  
 | 
    static makeFont(style: TextStylePropsPart): string { 
 | 
        // FIXME in node-canvas fontWeight is before fontStyle 
 | 
        // Use `fontSize` `fontFamily` to check whether font properties are defined. 
 | 
        let font = ''; 
 | 
        if (hasSeparateFont(style)) { 
 | 
            font = [ 
 | 
                style.fontStyle, 
 | 
                style.fontWeight, 
 | 
                parseFontSize(style.fontSize), 
 | 
                // If font properties are defined, `fontFamily` should not be ignored. 
 | 
                style.fontFamily || 'sans-serif' 
 | 
            ].join(' '); 
 | 
        } 
 | 
        return font && trim(font) || style.textFont || style.font; 
 | 
    } 
 | 
} 
 | 
  
 | 
  
 | 
const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1}; 
 | 
const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1}; 
 | 
  
 | 
const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const; 
 | 
  
 | 
export function parseFontSize(fontSize: number | string) { 
 | 
    if ( 
 | 
        typeof fontSize === 'string' 
 | 
        && ( 
 | 
            fontSize.indexOf('px') !== -1 
 | 
            || fontSize.indexOf('rem') !== -1 
 | 
            || fontSize.indexOf('em') !== -1 
 | 
        ) 
 | 
    ) { 
 | 
        return fontSize; 
 | 
    } 
 | 
    else if (!isNaN(+fontSize)) { 
 | 
        return fontSize + 'px'; 
 | 
    } 
 | 
    else { 
 | 
        return DEFAULT_FONT_SIZE + 'px'; 
 | 
    } 
 | 
} 
 | 
  
 | 
function setSeparateFont( 
 | 
    targetStyle: TSpanStyleProps, 
 | 
    sourceStyle: TextStylePropsPart 
 | 
) { 
 | 
    for (let i = 0; i < FONT_PARTS.length; i++) { 
 | 
        const fontProp = FONT_PARTS[i]; 
 | 
        const val = sourceStyle[fontProp]; 
 | 
        if (val != null) { 
 | 
            (targetStyle as any)[fontProp] = val; 
 | 
        } 
 | 
    } 
 | 
} 
 | 
  
 | 
export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) { 
 | 
    return style.fontSize != null || style.fontFamily || style.fontWeight; 
 | 
} 
 | 
  
 | 
export function normalizeTextStyle(style: TextStyleProps): TextStyleProps { 
 | 
    normalizeStyle(style); 
 | 
    // TODO inherit each item from top style in token style? 
 | 
    each(style.rich, normalizeStyle); 
 | 
    return style; 
 | 
} 
 | 
  
 | 
function normalizeStyle(style: TextStylePropsPart) { 
 | 
    if (style) { 
 | 
        style.font = ZRText.makeFont(style); 
 | 
        let textAlign = style.align; 
 | 
        // 'middle' is invalid, convert it to 'center' 
 | 
        (textAlign as string) === 'middle' && (textAlign = 'center'); 
 | 
        style.align = ( 
 | 
            textAlign == null || VALID_TEXT_ALIGN[textAlign] 
 | 
        ) ? textAlign : 'left'; 
 | 
  
 | 
        // Compatible with textBaseline. 
 | 
        let verticalAlign = style.verticalAlign; 
 | 
        (verticalAlign as string) === 'center' && (verticalAlign = 'middle'); 
 | 
        style.verticalAlign = ( 
 | 
            verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign] 
 | 
        ) ? verticalAlign : 'top'; 
 | 
  
 | 
        // TODO Should not change the orignal value. 
 | 
        const textPadding = style.padding; 
 | 
        if (textPadding) { 
 | 
            style.padding = normalizeCssArray(style.padding); 
 | 
        } 
 | 
    } 
 | 
} 
 | 
  
 | 
/** 
 | 
 * @param stroke If specified, do not check style.textStroke. 
 | 
 * @param lineWidth If specified, do not check style.textStroke. 
 | 
 */ 
 | 
function getStroke( 
 | 
    stroke?: TextStylePropsPart['stroke'], 
 | 
    lineWidth?: number 
 | 
) { 
 | 
    return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none') 
 | 
        ? null 
 | 
        : ((stroke as any).image || (stroke as any).colorStops) 
 | 
        ? '#000' 
 | 
        : stroke; 
 | 
} 
 | 
  
 | 
function getFill( 
 | 
    fill?: TextStylePropsPart['fill'] 
 | 
) { 
 | 
    return (fill == null || fill === 'none') 
 | 
        ? null 
 | 
        // TODO pattern and gradient? 
 | 
        : ((fill as any).image || (fill as any).colorStops) 
 | 
        ? '#000' 
 | 
        : fill; 
 | 
} 
 | 
  
 | 
function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number { 
 | 
    return textAlign === 'right' 
 | 
        ? (x - textPadding[1]) 
 | 
        : textAlign === 'center' 
 | 
        ? (x + textPadding[3] / 2 - textPadding[1] / 2) 
 | 
        : (x + textPadding[3]); 
 | 
} 
 | 
  
 | 
function getStyleText(style: TextStylePropsPart): string { 
 | 
    // Compat: set number to text is supported. 
 | 
    // set null/undefined to text is supported. 
 | 
    let text = style.text; 
 | 
    text != null && (text += ''); 
 | 
    return text; 
 | 
} 
 | 
  
 | 
/** 
 | 
 * If needs draw background 
 | 
 * @param style Style of element 
 | 
 */ 
 | 
function needDrawBackground(style: TextStylePropsPart): boolean { 
 | 
    return !!( 
 | 
        style.backgroundColor 
 | 
        || style.lineHeight 
 | 
        || (style.borderWidth && style.borderColor) 
 | 
    ); 
 | 
} 
 | 
  
 | 
export default ZRText; 
 |