| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756 |
- 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 {
- if (!containerWidth) {
- 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.
- for (let i = 0, len = textLines.length; i < len; i++) {
- textLines[i] = truncateSingleLine(textLines[i], options as InnerPreparedTruncateOption);
- }
- return textLines.join('\n');
- }
- 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(textLine: string, options: InnerPreparedTruncateOption): string {
- const containerWidth = options.containerWidth;
- const font = options.font;
- const contentWidth = options.contentWidth;
- if (!containerWidth) {
- return '';
- }
- let lineWidth = getWidth(textLine, font);
- if (lineWidth <= containerWidth) {
- return textLine;
- }
- 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;
- }
- return textLine;
- }
- 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[]
- }
- 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 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);
- 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.
- for (let i = 0; i < lines.length; i++) {
- lines[i] = truncateSingleLine(lines[i], options);
- }
- }
- // 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
- };
- }
- 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[] = []
- }
- 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) {
- 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';
- // 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 =
- 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);
- }
- 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 {
- token.text = truncateText(
- token.text, remainTruncWidth - paddingH, font, style.ellipsis,
- {minChar: style.truncateMinChar}
- );
- 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 isLatin(ch: string) {
- let code = ch.charCodeAt(0);
- return code >= 0x21 && code <= 0x17F;
- }
- 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 (isLatin(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
- };
- }
|