import * as imageHelper from '../helper/image'; 
 | 
import { 
 | 
    extend, 
 | 
    retrieve2, 
 | 
    retrieve3, 
 | 
    reduce 
 | 
} from '../../core/util'; 
 | 
import { TextAlign, TextVerticalAlign, ImageLike, Dictionary } from '../../core/types'; 
 | 
import { TextStyleProps } from '../Text'; 
 | 
import { getLineHeight, getWidth, parsePercent } from '../../contain/text'; 
 | 
  
 | 
const STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g; 
 | 
  
 | 
interface InnerTruncateOption { 
 | 
    maxIteration?: number 
 | 
    // If truncate result are less than minChar, ellipsis will not show 
 | 
    // which is better for user hint in some cases 
 | 
    minChar?: number 
 | 
    // When all truncated, use the placeholder 
 | 
    placeholder?: string 
 | 
  
 | 
    maxIterations?: number 
 | 
} 
 | 
  
 | 
interface InnerPreparedTruncateOption extends Required<InnerTruncateOption> { 
 | 
    font: string 
 | 
  
 | 
    ellipsis: string 
 | 
    ellipsisWidth: number 
 | 
    contentWidth: number 
 | 
  
 | 
    containerWidth: number 
 | 
    cnCharWidth: number 
 | 
    ascCharWidth: number 
 | 
} 
 | 
  
 | 
/** 
 | 
 * Show ellipsis if overflow. 
 | 
 */ 
 | 
export function truncateText( 
 | 
    text: string, 
 | 
    containerWidth: number, 
 | 
    font: string, 
 | 
    ellipsis?: string, 
 | 
    options?: InnerTruncateOption 
 | 
): string { 
 | 
    const out = {} as Parameters<typeof truncateText2>[0]; 
 | 
    truncateText2(out, text, containerWidth, font, ellipsis, options); 
 | 
    return out.text; 
 | 
} 
 | 
  
 | 
// PENDING: not sure whether `truncateText` is used outside zrender, since it has an `export` 
 | 
// specifier. So keep it and perform the interface modification in `truncateText2`. 
 | 
function truncateText2( 
 | 
    out: {text: string, isTruncated: boolean}, 
 | 
    text: string, 
 | 
    containerWidth: number, 
 | 
    font: string, 
 | 
    ellipsis?: string, 
 | 
    options?: InnerTruncateOption 
 | 
): void { 
 | 
    if (!containerWidth) { 
 | 
        out.text = ''; 
 | 
        out.isTruncated = false; 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    const textLines = (text + '').split('\n'); 
 | 
    options = prepareTruncateOptions(containerWidth, font, ellipsis, options); 
 | 
  
 | 
    // FIXME 
 | 
    // It is not appropriate that every line has '...' when truncate multiple lines. 
 | 
    let isTruncated = false; 
 | 
    const truncateOut = {} as Parameters<typeof truncateSingleLine>[0]; 
 | 
    for (let i = 0, len = textLines.length; i < len; i++) { 
 | 
        truncateSingleLine(truncateOut, textLines[i], options as InnerPreparedTruncateOption); 
 | 
        textLines[i] = truncateOut.textLine; 
 | 
        isTruncated = isTruncated || truncateOut.isTruncated; 
 | 
    } 
 | 
  
 | 
    out.text = textLines.join('\n'); 
 | 
    out.isTruncated = isTruncated; 
 | 
} 
 | 
  
 | 
function prepareTruncateOptions( 
 | 
    containerWidth: number, 
 | 
    font: string, 
 | 
    ellipsis?: string, 
 | 
    options?: InnerTruncateOption 
 | 
): InnerPreparedTruncateOption { 
 | 
    options = options || {}; 
 | 
    let preparedOpts = extend({}, options) as InnerPreparedTruncateOption; 
 | 
  
 | 
    preparedOpts.font = font; 
 | 
    ellipsis = retrieve2(ellipsis, '...'); 
 | 
    preparedOpts.maxIterations = retrieve2(options.maxIterations, 2); 
 | 
    const minChar = preparedOpts.minChar = retrieve2(options.minChar, 0); 
 | 
    // FIXME 
 | 
    // Other languages? 
 | 
    preparedOpts.cnCharWidth = getWidth('国', font); 
 | 
    // FIXME 
 | 
    // Consider proportional font? 
 | 
    const ascCharWidth = preparedOpts.ascCharWidth = getWidth('a', font); 
 | 
    preparedOpts.placeholder = retrieve2(options.placeholder, ''); 
 | 
  
 | 
    // Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'. 
 | 
    // Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'. 
 | 
    let contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap. 
 | 
    for (let i = 0; i < minChar && contentWidth >= ascCharWidth; i++) { 
 | 
        contentWidth -= ascCharWidth; 
 | 
    } 
 | 
  
 | 
    let ellipsisWidth = getWidth(ellipsis, font); 
 | 
    if (ellipsisWidth > contentWidth) { 
 | 
        ellipsis = ''; 
 | 
        ellipsisWidth = 0; 
 | 
    } 
 | 
  
 | 
    contentWidth = containerWidth - ellipsisWidth; 
 | 
  
 | 
    preparedOpts.ellipsis = ellipsis; 
 | 
    preparedOpts.ellipsisWidth = ellipsisWidth; 
 | 
    preparedOpts.contentWidth = contentWidth; 
 | 
    preparedOpts.containerWidth = containerWidth; 
 | 
  
 | 
    return preparedOpts; 
 | 
} 
 | 
  
 | 
function truncateSingleLine( 
 | 
    out: {textLine: string, isTruncated: boolean}, 
 | 
    textLine: string, 
 | 
    options: InnerPreparedTruncateOption 
 | 
): void { 
 | 
    const containerWidth = options.containerWidth; 
 | 
    const font = options.font; 
 | 
    const contentWidth = options.contentWidth; 
 | 
  
 | 
    if (!containerWidth) { 
 | 
        out.textLine = ''; 
 | 
        out.isTruncated = false; 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    let lineWidth = getWidth(textLine, font); 
 | 
  
 | 
    if (lineWidth <= containerWidth) { 
 | 
        out.textLine = textLine; 
 | 
        out.isTruncated = false; 
 | 
        return; 
 | 
    } 
 | 
  
 | 
    for (let j = 0; ; j++) { 
 | 
        if (lineWidth <= contentWidth || j >= options.maxIterations) { 
 | 
            textLine += options.ellipsis; 
 | 
            break; 
 | 
        } 
 | 
  
 | 
        const subLength = j === 0 
 | 
            ? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth) 
 | 
            : lineWidth > 0 
 | 
            ? Math.floor(textLine.length * contentWidth / lineWidth) 
 | 
            : 0; 
 | 
  
 | 
        textLine = textLine.substr(0, subLength); 
 | 
        lineWidth = getWidth(textLine, font); 
 | 
    } 
 | 
  
 | 
    if (textLine === '') { 
 | 
        textLine = options.placeholder; 
 | 
    } 
 | 
  
 | 
    out.textLine = textLine; 
 | 
    out.isTruncated = true; 
 | 
} 
 | 
  
 | 
function estimateLength( 
 | 
    text: string, contentWidth: number, ascCharWidth: number, cnCharWidth: number 
 | 
): number { 
 | 
    let width = 0; 
 | 
    let i = 0; 
 | 
    for (let len = text.length; i < len && width < contentWidth; i++) { 
 | 
        const charCode = text.charCodeAt(i); 
 | 
        width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth; 
 | 
    } 
 | 
    return i; 
 | 
} 
 | 
  
 | 
export interface PlainTextContentBlock { 
 | 
    lineHeight: number 
 | 
    // Line height of actual content. 
 | 
    calculatedLineHeight: number 
 | 
  
 | 
    contentWidth: number 
 | 
    contentHeight: number 
 | 
  
 | 
    width: number 
 | 
    height: number 
 | 
  
 | 
    /** 
 | 
     * Real text width containing padding. 
 | 
     * It should be the same as `width` if background is rendered 
 | 
     * and `width` is set by user. 
 | 
     */ 
 | 
    outerWidth: number 
 | 
    outerHeight: number 
 | 
  
 | 
    lines: string[] 
 | 
  
 | 
    // Be `true` if and only if the result text is modified due to overflow, due to 
 | 
    // settings on either `overflow` or `lineOverflow` 
 | 
    isTruncated: boolean 
 | 
} 
 | 
  
 | 
export function parsePlainText( 
 | 
    text: string, 
 | 
    style?: TextStyleProps 
 | 
): PlainTextContentBlock { 
 | 
    text != null && (text += ''); 
 | 
  
 | 
    // textPadding has been normalized 
 | 
    const overflow = style.overflow; 
 | 
    const padding = style.padding as number[]; 
 | 
    const font = style.font; 
 | 
    const truncate = overflow === 'truncate'; 
 | 
    const calculatedLineHeight = getLineHeight(font); 
 | 
    const lineHeight = retrieve2(style.lineHeight, calculatedLineHeight); 
 | 
    const bgColorDrawn = !!(style.backgroundColor); 
 | 
  
 | 
    const truncateLineOverflow = style.lineOverflow === 'truncate'; 
 | 
    let isTruncated = false; 
 | 
  
 | 
    let width = style.width; 
 | 
    let lines: string[]; 
 | 
  
 | 
    if (width != null && (overflow === 'break' || overflow === 'breakAll')) { 
 | 
        lines = text ? wrapText(text, style.font, width, overflow === 'breakAll', 0).lines : []; 
 | 
    } 
 | 
    else { 
 | 
        lines = text ? text.split('\n') : []; 
 | 
    } 
 | 
  
 | 
    const contentHeight = lines.length * lineHeight; 
 | 
    const height = retrieve2(style.height, contentHeight); 
 | 
  
 | 
    // Truncate lines. 
 | 
    if (contentHeight > height && truncateLineOverflow) { 
 | 
        const lineCount = Math.floor(height / lineHeight); 
 | 
  
 | 
        isTruncated = isTruncated || (lines.length > lineCount); 
 | 
        lines = lines.slice(0, lineCount); 
 | 
  
 | 
        // TODO If show ellipse for line truncate 
 | 
        // if (style.ellipsis) { 
 | 
        //     const options = prepareTruncateOptions(width, font, style.ellipsis, { 
 | 
        //         minChar: style.truncateMinChar, 
 | 
        //         placeholder: style.placeholder 
 | 
        //     }); 
 | 
        //     lines[lineCount - 1] = truncateSingleLine(lastLine, options); 
 | 
        // } 
 | 
    } 
 | 
  
 | 
    if (text && truncate && width != null) { 
 | 
        const options = prepareTruncateOptions(width, font, style.ellipsis, { 
 | 
            minChar: style.truncateMinChar, 
 | 
            placeholder: style.placeholder 
 | 
        }); 
 | 
        // Having every line has '...' when truncate multiple lines. 
 | 
        const singleOut = {} as Parameters<typeof truncateSingleLine>[0]; 
 | 
        for (let i = 0; i < lines.length; i++) { 
 | 
            truncateSingleLine(singleOut, lines[i], options); 
 | 
            lines[i] = singleOut.textLine; 
 | 
            isTruncated = isTruncated || singleOut.isTruncated; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    // Calculate real text width and height 
 | 
    let outerHeight = height; 
 | 
    let contentWidth = 0; 
 | 
    for (let i = 0; i < lines.length; i++) { 
 | 
        contentWidth = Math.max(getWidth(lines[i], font), contentWidth); 
 | 
    } 
 | 
    if (width == null) { 
 | 
        // When width is not explicitly set, use outerWidth as width. 
 | 
        width = contentWidth; 
 | 
    } 
 | 
  
 | 
    let outerWidth = contentWidth; 
 | 
    if (padding) { 
 | 
        outerHeight += padding[0] + padding[2]; 
 | 
        outerWidth += padding[1] + padding[3]; 
 | 
        width += padding[1] + padding[3]; 
 | 
    } 
 | 
  
 | 
    if (bgColorDrawn) { 
 | 
        // When render background, outerWidth should be the same as width. 
 | 
        outerWidth = width; 
 | 
    } 
 | 
  
 | 
    return { 
 | 
        lines: lines, 
 | 
        height: height, 
 | 
        outerWidth: outerWidth, 
 | 
        outerHeight: outerHeight, 
 | 
        lineHeight: lineHeight, 
 | 
        calculatedLineHeight: calculatedLineHeight, 
 | 
        contentWidth: contentWidth, 
 | 
        contentHeight: contentHeight, 
 | 
        width: width, 
 | 
        isTruncated: isTruncated 
 | 
    }; 
 | 
} 
 | 
  
 | 
class RichTextToken { 
 | 
    styleName: string 
 | 
    text: string 
 | 
    width: number 
 | 
    height: number 
 | 
  
 | 
    // Inner height exclude padding 
 | 
    innerHeight: number 
 | 
  
 | 
    // Width and height of actual text content. 
 | 
    contentHeight: number 
 | 
    contentWidth: number 
 | 
  
 | 
    lineHeight: number 
 | 
    font: string 
 | 
    align: TextAlign 
 | 
    verticalAlign: TextVerticalAlign 
 | 
  
 | 
    textPadding: number[] 
 | 
    percentWidth?: string 
 | 
  
 | 
    isLineHolder: boolean 
 | 
} 
 | 
class RichTextLine { 
 | 
    lineHeight: number 
 | 
    width: number 
 | 
    tokens: RichTextToken[] = [] 
 | 
  
 | 
    constructor(tokens?: RichTextToken[]) { 
 | 
        if (tokens) { 
 | 
            this.tokens = tokens; 
 | 
        } 
 | 
    } 
 | 
} 
 | 
export class RichTextContentBlock { 
 | 
    // width/height of content 
 | 
    width: number = 0 
 | 
    height: number = 0 
 | 
    // Calculated text height 
 | 
    contentWidth: number = 0 
 | 
    contentHeight: number = 0 
 | 
    // outerWidth/outerHeight with padding 
 | 
    outerWidth: number = 0 
 | 
    outerHeight: number = 0 
 | 
    lines: RichTextLine[] = [] 
 | 
    // Be `true` if and only if the result text is modified due to overflow, due to 
 | 
    // settings on either `overflow` or `lineOverflow` 
 | 
    isTruncated: boolean = false 
 | 
} 
 | 
  
 | 
type WrapInfo = { 
 | 
    width: number, 
 | 
    accumWidth: number, 
 | 
    breakAll: boolean 
 | 
} 
 | 
/** 
 | 
 * For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx' 
 | 
 * Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'. 
 | 
 * If styleName is undefined, it is plain text. 
 | 
 */ 
 | 
export function parseRichText(text: string, style: TextStyleProps): RichTextContentBlock { 
 | 
    const contentBlock = new RichTextContentBlock(); 
 | 
  
 | 
    text != null && (text += ''); 
 | 
    if (!text) { 
 | 
        return contentBlock; 
 | 
    } 
 | 
  
 | 
    const topWidth = style.width; 
 | 
    const topHeight = style.height; 
 | 
    const overflow = style.overflow; 
 | 
    let wrapInfo: WrapInfo = (overflow === 'break' || overflow === 'breakAll') && topWidth != null 
 | 
        ? {width: topWidth, accumWidth: 0, breakAll: overflow === 'breakAll'} 
 | 
        : null; 
 | 
  
 | 
    let lastIndex = STYLE_REG.lastIndex = 0; 
 | 
    let result; 
 | 
    while ((result = STYLE_REG.exec(text)) != null) { 
 | 
        const matchedIndex = result.index; 
 | 
        if (matchedIndex > lastIndex) { 
 | 
            pushTokens(contentBlock, text.substring(lastIndex, matchedIndex), style, wrapInfo); 
 | 
        } 
 | 
        pushTokens(contentBlock, result[2], style, wrapInfo, result[1]); 
 | 
        lastIndex = STYLE_REG.lastIndex; 
 | 
    } 
 | 
  
 | 
    if (lastIndex < text.length) { 
 | 
        pushTokens(contentBlock, text.substring(lastIndex, text.length), style, wrapInfo); 
 | 
    } 
 | 
  
 | 
    // For `textWidth: xx%` 
 | 
    let pendingList = []; 
 | 
  
 | 
    let calculatedHeight = 0; 
 | 
    let calculatedWidth = 0; 
 | 
  
 | 
    const stlPadding = style.padding as number[]; 
 | 
  
 | 
    const truncate = overflow === 'truncate'; 
 | 
    const truncateLine = style.lineOverflow === 'truncate'; 
 | 
    const tmpTruncateOut = {} as Parameters<typeof truncateText2>[0]; 
 | 
  
 | 
    // let prevToken: RichTextToken; 
 | 
  
 | 
    function finishLine(line: RichTextLine, lineWidth: number, lineHeight: number) { 
 | 
        line.width = lineWidth; 
 | 
        line.lineHeight = lineHeight; 
 | 
        calculatedHeight += lineHeight; 
 | 
        calculatedWidth = Math.max(calculatedWidth, lineWidth); 
 | 
    } 
 | 
    // Calculate layout info of tokens. 
 | 
    outer: for (let i = 0; i < contentBlock.lines.length; i++) { 
 | 
        const line = contentBlock.lines[i]; 
 | 
        let lineHeight = 0; 
 | 
        let lineWidth = 0; 
 | 
  
 | 
        for (let j = 0; j < line.tokens.length; j++) { 
 | 
            const token = line.tokens[j]; 
 | 
            const tokenStyle = token.styleName && style.rich[token.styleName] || {}; 
 | 
            // textPadding should not inherit from style. 
 | 
            const textPadding = token.textPadding = tokenStyle.padding as number[]; 
 | 
            const paddingH = textPadding ? textPadding[1] + textPadding[3] : 0; 
 | 
  
 | 
            const font = token.font = tokenStyle.font || style.font; 
 | 
  
 | 
            token.contentHeight = getLineHeight(font); 
 | 
            // textHeight can be used when textVerticalAlign is specified in token. 
 | 
            let tokenHeight = retrieve2( 
 | 
                // textHeight should not be inherited, consider it can be specified 
 | 
                // as box height of the block. 
 | 
                tokenStyle.height, token.contentHeight 
 | 
            ); 
 | 
            token.innerHeight = tokenHeight; 
 | 
  
 | 
            textPadding && (tokenHeight += textPadding[0] + textPadding[2]); 
 | 
            token.height = tokenHeight; 
 | 
            // Inlcude padding in lineHeight. 
 | 
            token.lineHeight = retrieve3( 
 | 
                tokenStyle.lineHeight, style.lineHeight, tokenHeight 
 | 
            ); 
 | 
  
 | 
            token.align = tokenStyle && tokenStyle.align || style.align; 
 | 
            token.verticalAlign = tokenStyle && tokenStyle.verticalAlign || 'middle'; 
 | 
  
 | 
            if (truncateLine && topHeight != null && calculatedHeight + token.lineHeight > topHeight) { 
 | 
                // TODO Add ellipsis on the previous token. 
 | 
                // prevToken.text = 
 | 
                const originalLength = contentBlock.lines.length; 
 | 
                if (j > 0) { 
 | 
                    line.tokens = line.tokens.slice(0, j); 
 | 
                    finishLine(line, lineWidth, lineHeight); 
 | 
                    contentBlock.lines = contentBlock.lines.slice(0, i + 1); 
 | 
                } 
 | 
                else { 
 | 
                    contentBlock.lines = contentBlock.lines.slice(0, i); 
 | 
                } 
 | 
                contentBlock.isTruncated = contentBlock.isTruncated || (contentBlock.lines.length < originalLength); 
 | 
                break outer; 
 | 
            } 
 | 
  
 | 
            let styleTokenWidth = tokenStyle.width; 
 | 
            let tokenWidthNotSpecified = styleTokenWidth == null || styleTokenWidth === 'auto'; 
 | 
  
 | 
            // Percent width, can be `100%`, can be used in drawing separate 
 | 
            // line when box width is needed to be auto. 
 | 
            if (typeof styleTokenWidth === 'string' && styleTokenWidth.charAt(styleTokenWidth.length - 1) === '%') { 
 | 
                token.percentWidth = styleTokenWidth; 
 | 
                pendingList.push(token); 
 | 
  
 | 
                token.contentWidth = getWidth(token.text, font); 
 | 
                // Do not truncate in this case, because there is no user case 
 | 
                // and it is too complicated. 
 | 
            } 
 | 
            else { 
 | 
                if (tokenWidthNotSpecified) { 
 | 
                    // FIXME: If image is not loaded and textWidth is not specified, calling 
 | 
                    // `getBoundingRect()` will not get correct result. 
 | 
                    const textBackgroundColor = tokenStyle.backgroundColor; 
 | 
                    let bgImg = textBackgroundColor && (textBackgroundColor as { image: ImageLike }).image; 
 | 
  
 | 
                    if (bgImg) { 
 | 
                        bgImg = imageHelper.findExistImage(bgImg); 
 | 
                        if (imageHelper.isImageReady(bgImg)) { 
 | 
                            // Update token width from image size. 
 | 
                            token.width = Math.max(token.width, bgImg.width * tokenHeight / bgImg.height); 
 | 
                        } 
 | 
                    } 
 | 
                } 
 | 
  
 | 
                const remainTruncWidth = truncate && topWidth != null 
 | 
                    ? topWidth - lineWidth : null; 
 | 
  
 | 
                if (remainTruncWidth != null && remainTruncWidth < token.width) { 
 | 
                    if (!tokenWidthNotSpecified || remainTruncWidth < paddingH) { 
 | 
                        token.text = ''; 
 | 
                        token.width = token.contentWidth = 0; 
 | 
                    } 
 | 
                    else { 
 | 
                        truncateText2( 
 | 
                            tmpTruncateOut, 
 | 
                            token.text, remainTruncWidth - paddingH, font, style.ellipsis, 
 | 
                            {minChar: style.truncateMinChar} 
 | 
                        ); 
 | 
                        token.text = tmpTruncateOut.text; 
 | 
                        contentBlock.isTruncated = contentBlock.isTruncated || tmpTruncateOut.isTruncated; 
 | 
                        token.width = token.contentWidth = getWidth(token.text, font); 
 | 
                    } 
 | 
                } 
 | 
                else { 
 | 
                    token.contentWidth = getWidth(token.text, font); 
 | 
                } 
 | 
            } 
 | 
  
 | 
            token.width += paddingH; 
 | 
  
 | 
            lineWidth += token.width; 
 | 
            tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight)); 
 | 
  
 | 
            // prevToken = token; 
 | 
        } 
 | 
  
 | 
        finishLine(line, lineWidth, lineHeight); 
 | 
    } 
 | 
  
 | 
    contentBlock.outerWidth = contentBlock.width = retrieve2(topWidth, calculatedWidth); 
 | 
    contentBlock.outerHeight = contentBlock.height = retrieve2(topHeight, calculatedHeight); 
 | 
    contentBlock.contentHeight = calculatedHeight; 
 | 
    contentBlock.contentWidth = calculatedWidth; 
 | 
  
 | 
    if (stlPadding) { 
 | 
        contentBlock.outerWidth += stlPadding[1] + stlPadding[3]; 
 | 
        contentBlock.outerHeight += stlPadding[0] + stlPadding[2]; 
 | 
    } 
 | 
  
 | 
    for (let i = 0; i < pendingList.length; i++) { 
 | 
        const token = pendingList[i]; 
 | 
        const percentWidth = token.percentWidth; 
 | 
        // Should not base on outerWidth, because token can not be placed out of padding. 
 | 
        token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width; 
 | 
    } 
 | 
  
 | 
    return contentBlock; 
 | 
} 
 | 
  
 | 
type TokenStyle = TextStyleProps['rich'][string]; 
 | 
  
 | 
function pushTokens( 
 | 
    block: RichTextContentBlock, 
 | 
    str: string, 
 | 
    style: TextStyleProps, 
 | 
    wrapInfo: WrapInfo, 
 | 
    styleName?: string 
 | 
) { 
 | 
    const isEmptyStr = str === ''; 
 | 
    const tokenStyle: TokenStyle = styleName && style.rich[styleName] || {}; 
 | 
    const lines = block.lines; 
 | 
    const font = tokenStyle.font || style.font; 
 | 
    let newLine = false; 
 | 
    let strLines; 
 | 
    let linesWidths; 
 | 
  
 | 
    if (wrapInfo) { 
 | 
        const tokenPadding = tokenStyle.padding as number[]; 
 | 
        let tokenPaddingH = tokenPadding ? tokenPadding[1] + tokenPadding[3] : 0; 
 | 
        if (tokenStyle.width != null && tokenStyle.width !== 'auto') { 
 | 
            // Wrap the whole token if tokenWidth if fixed. 
 | 
            const outerWidth = parsePercent(tokenStyle.width, wrapInfo.width) + tokenPaddingH; 
 | 
            if (lines.length > 0) { // Not first line 
 | 
                if (outerWidth + wrapInfo.accumWidth > wrapInfo.width) { 
 | 
                    // TODO Support wrap text in token. 
 | 
                    strLines = str.split('\n'); 
 | 
                    newLine = true; 
 | 
                } 
 | 
            } 
 | 
  
 | 
            wrapInfo.accumWidth = outerWidth; 
 | 
        } 
 | 
        else { 
 | 
            const res = wrapText(str, font, wrapInfo.width, wrapInfo.breakAll, wrapInfo.accumWidth); 
 | 
            wrapInfo.accumWidth = res.accumWidth + tokenPaddingH; 
 | 
            linesWidths = res.linesWidths; 
 | 
            strLines = res.lines; 
 | 
        } 
 | 
    } 
 | 
    else { 
 | 
        strLines = str.split('\n'); 
 | 
    } 
 | 
  
 | 
    for (let i = 0; i < strLines.length; i++) { 
 | 
        const text = strLines[i]; 
 | 
        const token = new RichTextToken(); 
 | 
        token.styleName = styleName; 
 | 
        token.text = text; 
 | 
        token.isLineHolder = !text && !isEmptyStr; 
 | 
  
 | 
        if (typeof tokenStyle.width === 'number') { 
 | 
            token.width = tokenStyle.width; 
 | 
        } 
 | 
        else { 
 | 
            token.width = linesWidths 
 | 
                ? linesWidths[i] // Caculated width in the wrap 
 | 
                : getWidth(text, font); 
 | 
        } 
 | 
  
 | 
        // The first token should be appended to the last line if not new line. 
 | 
        if (!i && !newLine) { 
 | 
            const tokens = (lines[lines.length - 1] || (lines[0] = new RichTextLine())).tokens; 
 | 
  
 | 
            // Consider cases: 
 | 
            // (1) ''.split('\n') => ['', '\n', ''], the '' at the first item 
 | 
            // (which is a placeholder) should be replaced by new token. 
 | 
            // (2) A image backage, where token likes {a|}. 
 | 
            // (3) A redundant '' will affect textAlign in line. 
 | 
            // (4) tokens with the same tplName should not be merged, because 
 | 
            // they should be displayed in different box (with border and padding). 
 | 
            const tokensLen = tokens.length; 
 | 
            (tokensLen === 1 && tokens[0].isLineHolder) 
 | 
                ? (tokens[0] = token) 
 | 
                // Consider text is '', only insert when it is the "lineHolder" or 
 | 
                // "emptyStr". Otherwise a redundant '' will affect textAlign in line. 
 | 
                : ((text || !tokensLen || isEmptyStr) && tokens.push(token)); 
 | 
        } 
 | 
        // Other tokens always start a new line. 
 | 
        else { 
 | 
            // If there is '', insert it as a placeholder. 
 | 
            lines.push(new RichTextLine([token])); 
 | 
        } 
 | 
    } 
 | 
} 
 | 
  
 | 
  
 | 
function isAlphabeticLetter(ch: string) { 
 | 
    // Unicode Character Ranges 
 | 
    // https://jrgraphix.net/research/unicode_blocks.php 
 | 
    // The following ranges may not cover all letter ranges but only the more 
 | 
    // popular ones. Developers could make pull requests when they find those 
 | 
    // not covered. 
 | 
    let code = ch.charCodeAt(0); 
 | 
    return code >= 0x20 && code <= 0x24F // Latin 
 | 
        || code >= 0x370 && code <= 0x10FF // Greek, Coptic, Cyrilic, and etc. 
 | 
        || code >= 0x1200 && code <= 0x13FF // Ethiopic and Cherokee 
 | 
        || code >= 0x1E00 && code <= 0x206F; // Latin and Greek extended 
 | 
} 
 | 
  
 | 
const breakCharMap = reduce(',&?/;] '.split(''), function (obj, ch) { 
 | 
    obj[ch] = true; 
 | 
    return obj; 
 | 
}, {} as Dictionary<boolean>); 
 | 
/** 
 | 
 * If break by word. For latin languages. 
 | 
 */ 
 | 
function isWordBreakChar(ch: string) { 
 | 
    if (isAlphabeticLetter(ch)) { 
 | 
        if (breakCharMap[ch]) { 
 | 
            return true; 
 | 
        } 
 | 
        return false; 
 | 
    } 
 | 
    return true; 
 | 
} 
 | 
  
 | 
function wrapText( 
 | 
    text: string, 
 | 
    font: string, 
 | 
    lineWidth: number, 
 | 
    isBreakAll: boolean, 
 | 
    lastAccumWidth: number 
 | 
) { 
 | 
    let lines: string[] = []; 
 | 
    let linesWidths: number[] = []; 
 | 
    let line = ''; 
 | 
    let currentWord = ''; 
 | 
    let currentWordWidth = 0; 
 | 
    let accumWidth = 0; 
 | 
  
 | 
    for (let i = 0; i < text.length; i++) { 
 | 
  
 | 
        const ch = text.charAt(i); 
 | 
        if (ch === '\n') { 
 | 
            if (currentWord) { 
 | 
                line += currentWord; 
 | 
                accumWidth += currentWordWidth; 
 | 
            } 
 | 
            lines.push(line); 
 | 
            linesWidths.push(accumWidth); 
 | 
            // Reset 
 | 
            line = ''; 
 | 
            currentWord = ''; 
 | 
            currentWordWidth = 0; 
 | 
            accumWidth = 0; 
 | 
            continue; 
 | 
        } 
 | 
  
 | 
        const chWidth = getWidth(ch, font); 
 | 
        const inWord = isBreakAll ? false : !isWordBreakChar(ch); 
 | 
  
 | 
        if (!lines.length 
 | 
            ? lastAccumWidth + accumWidth + chWidth > lineWidth 
 | 
            : accumWidth + chWidth > lineWidth 
 | 
        ) { 
 | 
            if (!accumWidth) {  // If nothing appended yet. 
 | 
                if (inWord) { 
 | 
                    // The word length is still too long for one line 
 | 
                    // Force break the word 
 | 
                    lines.push(currentWord); 
 | 
                    linesWidths.push(currentWordWidth); 
 | 
  
 | 
                    currentWord = ch; 
 | 
                    currentWordWidth = chWidth; 
 | 
                } 
 | 
                else { 
 | 
                    // lineWidth is too small for ch 
 | 
                    lines.push(ch); 
 | 
                    linesWidths.push(chWidth); 
 | 
                } 
 | 
            } 
 | 
            else if (line || currentWord) { 
 | 
                if (inWord) { 
 | 
                    if (!line) { 
 | 
                        // The one word is still too long for one line 
 | 
                        // Force break the word 
 | 
                        // TODO Keep the word? 
 | 
                        line = currentWord; 
 | 
                        currentWord = ''; 
 | 
                        currentWordWidth = 0; 
 | 
                        accumWidth = currentWordWidth; 
 | 
                    } 
 | 
  
 | 
                    lines.push(line); 
 | 
                    linesWidths.push(accumWidth - currentWordWidth); 
 | 
  
 | 
                    // Break the whole word 
 | 
                    currentWord += ch; 
 | 
                    currentWordWidth += chWidth; 
 | 
                    line = ''; 
 | 
                    accumWidth = currentWordWidth; 
 | 
                } 
 | 
                else { 
 | 
                    // Append lastWord if have 
 | 
                    if (currentWord) { 
 | 
                        line += currentWord; 
 | 
                        currentWord = ''; 
 | 
                        currentWordWidth = 0; 
 | 
                    } 
 | 
                    lines.push(line); 
 | 
                    linesWidths.push(accumWidth); 
 | 
  
 | 
                    line = ch; 
 | 
                    accumWidth = chWidth; 
 | 
                } 
 | 
            } 
 | 
  
 | 
            continue; 
 | 
        } 
 | 
  
 | 
        accumWidth += chWidth; 
 | 
  
 | 
        if (inWord) { 
 | 
            currentWord += ch; 
 | 
            currentWordWidth += chWidth; 
 | 
        } 
 | 
        else { 
 | 
            // Append whole word 
 | 
            if (currentWord) { 
 | 
                line += currentWord; 
 | 
                // Reset 
 | 
                currentWord = ''; 
 | 
                currentWordWidth = 0; 
 | 
            } 
 | 
  
 | 
            // Append character 
 | 
            line += ch; 
 | 
        } 
 | 
    } 
 | 
  
 | 
    if (!lines.length && !line) { 
 | 
        line = text; 
 | 
        currentWord = ''; 
 | 
        currentWordWidth = 0; 
 | 
    } 
 | 
  
 | 
    // Append last line. 
 | 
    if (currentWord) { 
 | 
        line += currentWord; 
 | 
    } 
 | 
    if (line) { 
 | 
        lines.push(line); 
 | 
        linesWidths.push(accumWidth); 
 | 
    } 
 | 
  
 | 
    if (lines.length === 1) { 
 | 
        // No new line. 
 | 
        accumWidth += lastAccumWidth; 
 | 
    } 
 | 
  
 | 
    return { 
 | 
        // Accum width of last line 
 | 
        accumWidth, 
 | 
        lines: lines, 
 | 
        linesWidths 
 | 
    }; 
 | 
} 
 |