6be0860b963a11c5850a13c9dfb4ec08309a26c47911976f040254fa28860ee488becd6d840ed9ffe2010ff0c0e6a8f1958257e7143938c956337810f62a26 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756
  1. import * as imageHelper from '../helper/image';
  2. import {
  3. extend,
  4. retrieve2,
  5. retrieve3,
  6. reduce
  7. } from '../../core/util';
  8. import { TextAlign, TextVerticalAlign, ImageLike, Dictionary } from '../../core/types';
  9. import { TextStyleProps } from '../Text';
  10. import { getLineHeight, getWidth, parsePercent } from '../../contain/text';
  11. const STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g;
  12. interface InnerTruncateOption {
  13. maxIteration?: number
  14. // If truncate result are less than minChar, ellipsis will not show
  15. // which is better for user hint in some cases
  16. minChar?: number
  17. // When all truncated, use the placeholder
  18. placeholder?: string
  19. maxIterations?: number
  20. }
  21. interface InnerPreparedTruncateOption extends Required<InnerTruncateOption> {
  22. font: string
  23. ellipsis: string
  24. ellipsisWidth: number
  25. contentWidth: number
  26. containerWidth: number
  27. cnCharWidth: number
  28. ascCharWidth: number
  29. }
  30. /**
  31. * Show ellipsis if overflow.
  32. */
  33. export function truncateText(
  34. text: string,
  35. containerWidth: number,
  36. font: string,
  37. ellipsis: string,
  38. options: InnerTruncateOption
  39. ): string {
  40. if (!containerWidth) {
  41. return '';
  42. }
  43. const textLines = (text + '').split('\n');
  44. options = prepareTruncateOptions(containerWidth, font, ellipsis, options);
  45. // FIXME
  46. // It is not appropriate that every line has '...' when truncate multiple lines.
  47. for (let i = 0, len = textLines.length; i < len; i++) {
  48. textLines[i] = truncateSingleLine(textLines[i], options as InnerPreparedTruncateOption);
  49. }
  50. return textLines.join('\n');
  51. }
  52. function prepareTruncateOptions(
  53. containerWidth: number,
  54. font: string,
  55. ellipsis: string,
  56. options: InnerTruncateOption
  57. ): InnerPreparedTruncateOption {
  58. options = options || {};
  59. let preparedOpts = extend({}, options) as InnerPreparedTruncateOption;
  60. preparedOpts.font = font;
  61. ellipsis = retrieve2(ellipsis, '...');
  62. preparedOpts.maxIterations = retrieve2(options.maxIterations, 2);
  63. const minChar = preparedOpts.minChar = retrieve2(options.minChar, 0);
  64. // FIXME
  65. // Other languages?
  66. preparedOpts.cnCharWidth = getWidth('国', font);
  67. // FIXME
  68. // Consider proportional font?
  69. const ascCharWidth = preparedOpts.ascCharWidth = getWidth('a', font);
  70. preparedOpts.placeholder = retrieve2(options.placeholder, '');
  71. // Example 1: minChar: 3, text: 'asdfzxcv', truncate result: 'asdf', but not: 'a...'.
  72. // Example 2: minChar: 3, text: '维度', truncate result: '维', but not: '...'.
  73. let contentWidth = containerWidth = Math.max(0, containerWidth - 1); // Reserve some gap.
  74. for (let i = 0; i < minChar && contentWidth >= ascCharWidth; i++) {
  75. contentWidth -= ascCharWidth;
  76. }
  77. let ellipsisWidth = getWidth(ellipsis, font);
  78. if (ellipsisWidth > contentWidth) {
  79. ellipsis = '';
  80. ellipsisWidth = 0;
  81. }
  82. contentWidth = containerWidth - ellipsisWidth;
  83. preparedOpts.ellipsis = ellipsis;
  84. preparedOpts.ellipsisWidth = ellipsisWidth;
  85. preparedOpts.contentWidth = contentWidth;
  86. preparedOpts.containerWidth = containerWidth;
  87. return preparedOpts;
  88. }
  89. function truncateSingleLine(textLine: string, options: InnerPreparedTruncateOption): string {
  90. const containerWidth = options.containerWidth;
  91. const font = options.font;
  92. const contentWidth = options.contentWidth;
  93. if (!containerWidth) {
  94. return '';
  95. }
  96. let lineWidth = getWidth(textLine, font);
  97. if (lineWidth <= containerWidth) {
  98. return textLine;
  99. }
  100. for (let j = 0; ; j++) {
  101. if (lineWidth <= contentWidth || j >= options.maxIterations) {
  102. textLine += options.ellipsis;
  103. break;
  104. }
  105. const subLength = j === 0
  106. ? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth)
  107. : lineWidth > 0
  108. ? Math.floor(textLine.length * contentWidth / lineWidth)
  109. : 0;
  110. textLine = textLine.substr(0, subLength);
  111. lineWidth = getWidth(textLine, font);
  112. }
  113. if (textLine === '') {
  114. textLine = options.placeholder;
  115. }
  116. return textLine;
  117. }
  118. function estimateLength(
  119. text: string, contentWidth: number, ascCharWidth: number, cnCharWidth: number
  120. ): number {
  121. let width = 0;
  122. let i = 0;
  123. for (let len = text.length; i < len && width < contentWidth; i++) {
  124. const charCode = text.charCodeAt(i);
  125. width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth;
  126. }
  127. return i;
  128. }
  129. export interface PlainTextContentBlock {
  130. lineHeight: number
  131. // Line height of actual content.
  132. calculatedLineHeight: number
  133. contentWidth: number
  134. contentHeight: number
  135. width: number
  136. height: number
  137. /**
  138. * Real text width containing padding.
  139. * It should be the same as `width` if background is rendered
  140. * and `width` is set by user.
  141. */
  142. outerWidth: number
  143. outerHeight: number
  144. lines: string[]
  145. }
  146. export function parsePlainText(
  147. text: string,
  148. style?: TextStyleProps
  149. ): PlainTextContentBlock {
  150. text != null && (text += '');
  151. // textPadding has been normalized
  152. const overflow = style.overflow;
  153. const padding = style.padding as number[];
  154. const font = style.font;
  155. const truncate = overflow === 'truncate';
  156. const calculatedLineHeight = getLineHeight(font);
  157. const lineHeight = retrieve2(style.lineHeight, calculatedLineHeight);
  158. const bgColorDrawn = !!(style.backgroundColor);
  159. const truncateLineOverflow = style.lineOverflow === 'truncate';
  160. let width = style.width;
  161. let lines: string[];
  162. if (width != null && (overflow === 'break' || overflow === 'breakAll')) {
  163. lines = text ? wrapText(text, style.font, width, overflow === 'breakAll', 0).lines : [];
  164. }
  165. else {
  166. lines = text ? text.split('\n') : [];
  167. }
  168. const contentHeight = lines.length * lineHeight;
  169. const height = retrieve2(style.height, contentHeight);
  170. // Truncate lines.
  171. if (contentHeight > height && truncateLineOverflow) {
  172. const lineCount = Math.floor(height / lineHeight);
  173. lines = lines.slice(0, lineCount);
  174. // TODO If show ellipse for line truncate
  175. // if (style.ellipsis) {
  176. // const options = prepareTruncateOptions(width, font, style.ellipsis, {
  177. // minChar: style.truncateMinChar,
  178. // placeholder: style.placeholder
  179. // });
  180. // lines[lineCount - 1] = truncateSingleLine(lastLine, options);
  181. // }
  182. }
  183. if (text && truncate && width != null) {
  184. const options = prepareTruncateOptions(width, font, style.ellipsis, {
  185. minChar: style.truncateMinChar,
  186. placeholder: style.placeholder
  187. });
  188. // Having every line has '...' when truncate multiple lines.
  189. for (let i = 0; i < lines.length; i++) {
  190. lines[i] = truncateSingleLine(lines[i], options);
  191. }
  192. }
  193. // Calculate real text width and height
  194. let outerHeight = height;
  195. let contentWidth = 0;
  196. for (let i = 0; i < lines.length; i++) {
  197. contentWidth = Math.max(getWidth(lines[i], font), contentWidth);
  198. }
  199. if (width == null) {
  200. // When width is not explicitly set, use outerWidth as width.
  201. width = contentWidth;
  202. }
  203. let outerWidth = contentWidth;
  204. if (padding) {
  205. outerHeight += padding[0] + padding[2];
  206. outerWidth += padding[1] + padding[3];
  207. width += padding[1] + padding[3];
  208. }
  209. if (bgColorDrawn) {
  210. // When render background, outerWidth should be the same as width.
  211. outerWidth = width;
  212. }
  213. return {
  214. lines: lines,
  215. height: height,
  216. outerWidth: outerWidth,
  217. outerHeight: outerHeight,
  218. lineHeight: lineHeight,
  219. calculatedLineHeight: calculatedLineHeight,
  220. contentWidth: contentWidth,
  221. contentHeight: contentHeight,
  222. width: width
  223. };
  224. }
  225. class RichTextToken {
  226. styleName: string
  227. text: string
  228. width: number
  229. height: number
  230. // Inner height exclude padding
  231. innerHeight: number
  232. // Width and height of actual text content.
  233. contentHeight: number
  234. contentWidth: number
  235. lineHeight: number
  236. font: string
  237. align: TextAlign
  238. verticalAlign: TextVerticalAlign
  239. textPadding: number[]
  240. percentWidth?: string
  241. isLineHolder: boolean
  242. }
  243. class RichTextLine {
  244. lineHeight: number
  245. width: number
  246. tokens: RichTextToken[] = []
  247. constructor(tokens?: RichTextToken[]) {
  248. if (tokens) {
  249. this.tokens = tokens;
  250. }
  251. }
  252. }
  253. export class RichTextContentBlock {
  254. // width/height of content
  255. width: number = 0
  256. height: number = 0
  257. // Calculated text height
  258. contentWidth: number = 0
  259. contentHeight: number = 0
  260. // outerWidth/outerHeight with padding
  261. outerWidth: number = 0
  262. outerHeight: number = 0
  263. lines: RichTextLine[] = []
  264. }
  265. type WrapInfo = {
  266. width: number,
  267. accumWidth: number,
  268. breakAll: boolean
  269. }
  270. /**
  271. * For example: 'some text {a|some text}other text{b|some text}xxx{c|}xxx'
  272. * Also consider 'bbbb{a|xxx\nzzz}xxxx\naaaa'.
  273. * If styleName is undefined, it is plain text.
  274. */
  275. export function parseRichText(text: string, style: TextStyleProps) {
  276. const contentBlock = new RichTextContentBlock();
  277. text != null && (text += '');
  278. if (!text) {
  279. return contentBlock;
  280. }
  281. const topWidth = style.width;
  282. const topHeight = style.height;
  283. const overflow = style.overflow;
  284. let wrapInfo: WrapInfo = (overflow === 'break' || overflow === 'breakAll') && topWidth != null
  285. ? {width: topWidth, accumWidth: 0, breakAll: overflow === 'breakAll'}
  286. : null;
  287. let lastIndex = STYLE_REG.lastIndex = 0;
  288. let result;
  289. while ((result = STYLE_REG.exec(text)) != null) {
  290. const matchedIndex = result.index;
  291. if (matchedIndex > lastIndex) {
  292. pushTokens(contentBlock, text.substring(lastIndex, matchedIndex), style, wrapInfo);
  293. }
  294. pushTokens(contentBlock, result[2], style, wrapInfo, result[1]);
  295. lastIndex = STYLE_REG.lastIndex;
  296. }
  297. if (lastIndex < text.length) {
  298. pushTokens(contentBlock, text.substring(lastIndex, text.length), style, wrapInfo);
  299. }
  300. // For `textWidth: xx%`
  301. let pendingList = [];
  302. let calculatedHeight = 0;
  303. let calculatedWidth = 0;
  304. const stlPadding = style.padding as number[];
  305. const truncate = overflow === 'truncate';
  306. const truncateLine = style.lineOverflow === 'truncate';
  307. // let prevToken: RichTextToken;
  308. function finishLine(line: RichTextLine, lineWidth: number, lineHeight: number) {
  309. line.width = lineWidth;
  310. line.lineHeight = lineHeight;
  311. calculatedHeight += lineHeight;
  312. calculatedWidth = Math.max(calculatedWidth, lineWidth);
  313. }
  314. // Calculate layout info of tokens.
  315. outer: for (let i = 0; i < contentBlock.lines.length; i++) {
  316. const line = contentBlock.lines[i];
  317. let lineHeight = 0;
  318. let lineWidth = 0;
  319. for (let j = 0; j < line.tokens.length; j++) {
  320. const token = line.tokens[j];
  321. const tokenStyle = token.styleName && style.rich[token.styleName] || {};
  322. // textPadding should not inherit from style.
  323. const textPadding = token.textPadding = tokenStyle.padding as number[];
  324. const paddingH = textPadding ? textPadding[1] + textPadding[3] : 0;
  325. const font = token.font = tokenStyle.font || style.font;
  326. token.contentHeight = getLineHeight(font);
  327. // textHeight can be used when textVerticalAlign is specified in token.
  328. let tokenHeight = retrieve2(
  329. // textHeight should not be inherited, consider it can be specified
  330. // as box height of the block.
  331. tokenStyle.height, token.contentHeight
  332. );
  333. token.innerHeight = tokenHeight;
  334. textPadding && (tokenHeight += textPadding[0] + textPadding[2]);
  335. token.height = tokenHeight;
  336. // Inlcude padding in lineHeight.
  337. token.lineHeight = retrieve3(
  338. tokenStyle.lineHeight, style.lineHeight, tokenHeight
  339. );
  340. token.align = tokenStyle && tokenStyle.align || style.align;
  341. token.verticalAlign = tokenStyle && tokenStyle.verticalAlign || 'middle';
  342. if (truncateLine && topHeight != null && calculatedHeight + token.lineHeight > topHeight) {
  343. // TODO Add ellipsis on the previous token.
  344. // prevToken.text =
  345. if (j > 0) {
  346. line.tokens = line.tokens.slice(0, j);
  347. finishLine(line, lineWidth, lineHeight);
  348. contentBlock.lines = contentBlock.lines.slice(0, i + 1);
  349. }
  350. else {
  351. contentBlock.lines = contentBlock.lines.slice(0, i);
  352. }
  353. break outer;
  354. }
  355. let styleTokenWidth = tokenStyle.width;
  356. let tokenWidthNotSpecified = styleTokenWidth == null || styleTokenWidth === 'auto';
  357. // Percent width, can be `100%`, can be used in drawing separate
  358. // line when box width is needed to be auto.
  359. if (typeof styleTokenWidth === 'string' && styleTokenWidth.charAt(styleTokenWidth.length - 1) === '%') {
  360. token.percentWidth = styleTokenWidth;
  361. pendingList.push(token);
  362. token.contentWidth = getWidth(token.text, font);
  363. // Do not truncate in this case, because there is no user case
  364. // and it is too complicated.
  365. }
  366. else {
  367. if (tokenWidthNotSpecified) {
  368. // FIXME: If image is not loaded and textWidth is not specified, calling
  369. // `getBoundingRect()` will not get correct result.
  370. const textBackgroundColor = tokenStyle.backgroundColor;
  371. let bgImg = textBackgroundColor && (textBackgroundColor as { image: ImageLike }).image;
  372. if (bgImg) {
  373. bgImg = imageHelper.findExistImage(bgImg);
  374. if (imageHelper.isImageReady(bgImg)) {
  375. // Update token width from image size.
  376. token.width = Math.max(token.width, bgImg.width * tokenHeight / bgImg.height);
  377. }
  378. }
  379. }
  380. const remainTruncWidth = truncate && topWidth != null
  381. ? topWidth - lineWidth : null;
  382. if (remainTruncWidth != null && remainTruncWidth < token.width) {
  383. if (!tokenWidthNotSpecified || remainTruncWidth < paddingH) {
  384. token.text = '';
  385. token.width = token.contentWidth = 0;
  386. }
  387. else {
  388. token.text = truncateText(
  389. token.text, remainTruncWidth - paddingH, font, style.ellipsis,
  390. {minChar: style.truncateMinChar}
  391. );
  392. token.width = token.contentWidth = getWidth(token.text, font);
  393. }
  394. }
  395. else {
  396. token.contentWidth = getWidth(token.text, font);
  397. }
  398. }
  399. token.width += paddingH;
  400. lineWidth += token.width;
  401. tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight));
  402. // prevToken = token;
  403. }
  404. finishLine(line, lineWidth, lineHeight);
  405. }
  406. contentBlock.outerWidth = contentBlock.width = retrieve2(topWidth, calculatedWidth);
  407. contentBlock.outerHeight = contentBlock.height = retrieve2(topHeight, calculatedHeight);
  408. contentBlock.contentHeight = calculatedHeight;
  409. contentBlock.contentWidth = calculatedWidth;
  410. if (stlPadding) {
  411. contentBlock.outerWidth += stlPadding[1] + stlPadding[3];
  412. contentBlock.outerHeight += stlPadding[0] + stlPadding[2];
  413. }
  414. for (let i = 0; i < pendingList.length; i++) {
  415. const token = pendingList[i];
  416. const percentWidth = token.percentWidth;
  417. // Should not base on outerWidth, because token can not be placed out of padding.
  418. token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width;
  419. }
  420. return contentBlock;
  421. }
  422. type TokenStyle = TextStyleProps['rich'][string];
  423. function pushTokens(
  424. block: RichTextContentBlock,
  425. str: string,
  426. style: TextStyleProps,
  427. wrapInfo: WrapInfo,
  428. styleName?: string
  429. ) {
  430. const isEmptyStr = str === '';
  431. const tokenStyle: TokenStyle = styleName && style.rich[styleName] || {};
  432. const lines = block.lines;
  433. const font = tokenStyle.font || style.font;
  434. let newLine = false;
  435. let strLines;
  436. let linesWidths;
  437. if (wrapInfo) {
  438. const tokenPadding = tokenStyle.padding as number[];
  439. let tokenPaddingH = tokenPadding ? tokenPadding[1] + tokenPadding[3] : 0;
  440. if (tokenStyle.width != null && tokenStyle.width !== 'auto') {
  441. // Wrap the whole token if tokenWidth if fixed.
  442. const outerWidth = parsePercent(tokenStyle.width, wrapInfo.width) + tokenPaddingH;
  443. if (lines.length > 0) { // Not first line
  444. if (outerWidth + wrapInfo.accumWidth > wrapInfo.width) {
  445. // TODO Support wrap text in token.
  446. strLines = str.split('\n');
  447. newLine = true;
  448. }
  449. }
  450. wrapInfo.accumWidth = outerWidth;
  451. }
  452. else {
  453. const res = wrapText(str, font, wrapInfo.width, wrapInfo.breakAll, wrapInfo.accumWidth);
  454. wrapInfo.accumWidth = res.accumWidth + tokenPaddingH;
  455. linesWidths = res.linesWidths;
  456. strLines = res.lines;
  457. }
  458. }
  459. else {
  460. strLines = str.split('\n');
  461. }
  462. for (let i = 0; i < strLines.length; i++) {
  463. const text = strLines[i];
  464. const token = new RichTextToken();
  465. token.styleName = styleName;
  466. token.text = text;
  467. token.isLineHolder = !text && !isEmptyStr;
  468. if (typeof tokenStyle.width === 'number') {
  469. token.width = tokenStyle.width;
  470. }
  471. else {
  472. token.width = linesWidths
  473. ? linesWidths[i] // Caculated width in the wrap
  474. : getWidth(text, font);
  475. }
  476. // The first token should be appended to the last line if not new line.
  477. if (!i && !newLine) {
  478. const tokens = (lines[lines.length - 1] || (lines[0] = new RichTextLine())).tokens;
  479. // Consider cases:
  480. // (1) ''.split('\n') => ['', '\n', ''], the '' at the first item
  481. // (which is a placeholder) should be replaced by new token.
  482. // (2) A image backage, where token likes {a|}.
  483. // (3) A redundant '' will affect textAlign in line.
  484. // (4) tokens with the same tplName should not be merged, because
  485. // they should be displayed in different box (with border and padding).
  486. const tokensLen = tokens.length;
  487. (tokensLen === 1 && tokens[0].isLineHolder)
  488. ? (tokens[0] = token)
  489. // Consider text is '', only insert when it is the "lineHolder" or
  490. // "emptyStr". Otherwise a redundant '' will affect textAlign in line.
  491. : ((text || !tokensLen || isEmptyStr) && tokens.push(token));
  492. }
  493. // Other tokens always start a new line.
  494. else {
  495. // If there is '', insert it as a placeholder.
  496. lines.push(new RichTextLine([token]));
  497. }
  498. }
  499. }
  500. function isLatin(ch: string) {
  501. let code = ch.charCodeAt(0);
  502. return code >= 0x21 && code <= 0x17F;
  503. }
  504. const breakCharMap = reduce(',&?/;] '.split(''), function (obj, ch) {
  505. obj[ch] = true;
  506. return obj;
  507. }, {} as Dictionary<boolean>);
  508. /**
  509. * If break by word. For latin languages.
  510. */
  511. function isWordBreakChar(ch: string) {
  512. if (isLatin(ch)) {
  513. if (breakCharMap[ch]) {
  514. return true;
  515. }
  516. return false;
  517. }
  518. return true;
  519. }
  520. function wrapText(
  521. text: string,
  522. font: string,
  523. lineWidth: number,
  524. isBreakAll: boolean,
  525. lastAccumWidth: number
  526. ) {
  527. let lines: string[] = [];
  528. let linesWidths: number[] = [];
  529. let line = '';
  530. let currentWord = '';
  531. let currentWordWidth = 0;
  532. let accumWidth = 0;
  533. for (let i = 0; i < text.length; i++) {
  534. const ch = text.charAt(i);
  535. if (ch === '\n') {
  536. if (currentWord) {
  537. line += currentWord;
  538. accumWidth += currentWordWidth;
  539. }
  540. lines.push(line);
  541. linesWidths.push(accumWidth);
  542. // Reset
  543. line = '';
  544. currentWord = '';
  545. currentWordWidth = 0;
  546. accumWidth = 0;
  547. continue;
  548. }
  549. const chWidth = getWidth(ch, font);
  550. const inWord = isBreakAll ? false : !isWordBreakChar(ch);
  551. if (!lines.length
  552. ? lastAccumWidth + accumWidth + chWidth > lineWidth
  553. : accumWidth + chWidth > lineWidth
  554. ) {
  555. if (!accumWidth) { // If nothing appended yet.
  556. if (inWord) {
  557. // The word length is still too long for one line
  558. // Force break the word
  559. lines.push(currentWord);
  560. linesWidths.push(currentWordWidth);
  561. currentWord = ch;
  562. currentWordWidth = chWidth;
  563. }
  564. else {
  565. // lineWidth is too small for ch
  566. lines.push(ch);
  567. linesWidths.push(chWidth);
  568. }
  569. }
  570. else if (line || currentWord) {
  571. if (inWord) {
  572. if (!line) {
  573. // The one word is still too long for one line
  574. // Force break the word
  575. // TODO Keep the word?
  576. line = currentWord;
  577. currentWord = '';
  578. currentWordWidth = 0;
  579. accumWidth = currentWordWidth;
  580. }
  581. lines.push(line);
  582. linesWidths.push(accumWidth - currentWordWidth);
  583. // Break the whole word
  584. currentWord += ch;
  585. currentWordWidth += chWidth;
  586. line = '';
  587. accumWidth = currentWordWidth;
  588. }
  589. else {
  590. // Append lastWord if have
  591. if (currentWord) {
  592. line += currentWord;
  593. currentWord = '';
  594. currentWordWidth = 0;
  595. }
  596. lines.push(line);
  597. linesWidths.push(accumWidth);
  598. line = ch;
  599. accumWidth = chWidth;
  600. }
  601. }
  602. continue;
  603. }
  604. accumWidth += chWidth;
  605. if (inWord) {
  606. currentWord += ch;
  607. currentWordWidth += chWidth;
  608. }
  609. else {
  610. // Append whole word
  611. if (currentWord) {
  612. line += currentWord;
  613. // Reset
  614. currentWord = '';
  615. currentWordWidth = 0;
  616. }
  617. // Append character
  618. line += ch;
  619. }
  620. }
  621. if (!lines.length && !line) {
  622. line = text;
  623. currentWord = '';
  624. currentWordWidth = 0;
  625. }
  626. // Append last line.
  627. if (currentWord) {
  628. line += currentWord;
  629. }
  630. if (line) {
  631. lines.push(line);
  632. linesWidths.push(accumWidth);
  633. }
  634. if (lines.length === 1) {
  635. // No new line.
  636. accumWidth += lastAccumWidth;
  637. }
  638. return {
  639. // Accum width of last line
  640. accumWidth,
  641. lines: lines,
  642. linesWidths
  643. };
  644. }