parseText.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. import * as imageHelper from '../helper/image.js';
  2. import { extend, retrieve2, retrieve3, reduce } from '../../core/util.js';
  3. import { getLineHeight, getWidth, parsePercent } from '../../contain/text.js';
  4. var STYLE_REG = /\{([a-zA-Z0-9_]+)\|([^}]*)\}/g;
  5. export function truncateText(text, containerWidth, font, ellipsis, options) {
  6. var out = {};
  7. truncateText2(out, text, containerWidth, font, ellipsis, options);
  8. return out.text;
  9. }
  10. function truncateText2(out, text, containerWidth, font, ellipsis, options) {
  11. if (!containerWidth) {
  12. out.text = '';
  13. out.isTruncated = false;
  14. return;
  15. }
  16. var textLines = (text + '').split('\n');
  17. options = prepareTruncateOptions(containerWidth, font, ellipsis, options);
  18. var isTruncated = false;
  19. var truncateOut = {};
  20. for (var i = 0, len = textLines.length; i < len; i++) {
  21. truncateSingleLine(truncateOut, textLines[i], options);
  22. textLines[i] = truncateOut.textLine;
  23. isTruncated = isTruncated || truncateOut.isTruncated;
  24. }
  25. out.text = textLines.join('\n');
  26. out.isTruncated = isTruncated;
  27. }
  28. function prepareTruncateOptions(containerWidth, font, ellipsis, options) {
  29. options = options || {};
  30. var preparedOpts = extend({}, options);
  31. preparedOpts.font = font;
  32. ellipsis = retrieve2(ellipsis, '...');
  33. preparedOpts.maxIterations = retrieve2(options.maxIterations, 2);
  34. var minChar = preparedOpts.minChar = retrieve2(options.minChar, 0);
  35. preparedOpts.cnCharWidth = getWidth('国', font);
  36. var ascCharWidth = preparedOpts.ascCharWidth = getWidth('a', font);
  37. preparedOpts.placeholder = retrieve2(options.placeholder, '');
  38. var contentWidth = containerWidth = Math.max(0, containerWidth - 1);
  39. for (var i = 0; i < minChar && contentWidth >= ascCharWidth; i++) {
  40. contentWidth -= ascCharWidth;
  41. }
  42. var ellipsisWidth = getWidth(ellipsis, font);
  43. if (ellipsisWidth > contentWidth) {
  44. ellipsis = '';
  45. ellipsisWidth = 0;
  46. }
  47. contentWidth = containerWidth - ellipsisWidth;
  48. preparedOpts.ellipsis = ellipsis;
  49. preparedOpts.ellipsisWidth = ellipsisWidth;
  50. preparedOpts.contentWidth = contentWidth;
  51. preparedOpts.containerWidth = containerWidth;
  52. return preparedOpts;
  53. }
  54. function truncateSingleLine(out, textLine, options) {
  55. var containerWidth = options.containerWidth;
  56. var font = options.font;
  57. var contentWidth = options.contentWidth;
  58. if (!containerWidth) {
  59. out.textLine = '';
  60. out.isTruncated = false;
  61. return;
  62. }
  63. var lineWidth = getWidth(textLine, font);
  64. if (lineWidth <= containerWidth) {
  65. out.textLine = textLine;
  66. out.isTruncated = false;
  67. return;
  68. }
  69. for (var j = 0;; j++) {
  70. if (lineWidth <= contentWidth || j >= options.maxIterations) {
  71. textLine += options.ellipsis;
  72. break;
  73. }
  74. var subLength = j === 0
  75. ? estimateLength(textLine, contentWidth, options.ascCharWidth, options.cnCharWidth)
  76. : lineWidth > 0
  77. ? Math.floor(textLine.length * contentWidth / lineWidth)
  78. : 0;
  79. textLine = textLine.substr(0, subLength);
  80. lineWidth = getWidth(textLine, font);
  81. }
  82. if (textLine === '') {
  83. textLine = options.placeholder;
  84. }
  85. out.textLine = textLine;
  86. out.isTruncated = true;
  87. }
  88. function estimateLength(text, contentWidth, ascCharWidth, cnCharWidth) {
  89. var width = 0;
  90. var i = 0;
  91. for (var len = text.length; i < len && width < contentWidth; i++) {
  92. var charCode = text.charCodeAt(i);
  93. width += (0 <= charCode && charCode <= 127) ? ascCharWidth : cnCharWidth;
  94. }
  95. return i;
  96. }
  97. export function parsePlainText(text, style) {
  98. text != null && (text += '');
  99. var overflow = style.overflow;
  100. var padding = style.padding;
  101. var font = style.font;
  102. var truncate = overflow === 'truncate';
  103. var calculatedLineHeight = getLineHeight(font);
  104. var lineHeight = retrieve2(style.lineHeight, calculatedLineHeight);
  105. var bgColorDrawn = !!(style.backgroundColor);
  106. var truncateLineOverflow = style.lineOverflow === 'truncate';
  107. var isTruncated = false;
  108. var width = style.width;
  109. var lines;
  110. if (width != null && (overflow === 'break' || overflow === 'breakAll')) {
  111. lines = text ? wrapText(text, style.font, width, overflow === 'breakAll', 0).lines : [];
  112. }
  113. else {
  114. lines = text ? text.split('\n') : [];
  115. }
  116. var contentHeight = lines.length * lineHeight;
  117. var height = retrieve2(style.height, contentHeight);
  118. if (contentHeight > height && truncateLineOverflow) {
  119. var lineCount = Math.floor(height / lineHeight);
  120. isTruncated = isTruncated || (lines.length > lineCount);
  121. lines = lines.slice(0, lineCount);
  122. }
  123. if (text && truncate && width != null) {
  124. var options = prepareTruncateOptions(width, font, style.ellipsis, {
  125. minChar: style.truncateMinChar,
  126. placeholder: style.placeholder
  127. });
  128. var singleOut = {};
  129. for (var i = 0; i < lines.length; i++) {
  130. truncateSingleLine(singleOut, lines[i], options);
  131. lines[i] = singleOut.textLine;
  132. isTruncated = isTruncated || singleOut.isTruncated;
  133. }
  134. }
  135. var outerHeight = height;
  136. var contentWidth = 0;
  137. for (var i = 0; i < lines.length; i++) {
  138. contentWidth = Math.max(getWidth(lines[i], font), contentWidth);
  139. }
  140. if (width == null) {
  141. width = contentWidth;
  142. }
  143. var outerWidth = contentWidth;
  144. if (padding) {
  145. outerHeight += padding[0] + padding[2];
  146. outerWidth += padding[1] + padding[3];
  147. width += padding[1] + padding[3];
  148. }
  149. if (bgColorDrawn) {
  150. outerWidth = width;
  151. }
  152. return {
  153. lines: lines,
  154. height: height,
  155. outerWidth: outerWidth,
  156. outerHeight: outerHeight,
  157. lineHeight: lineHeight,
  158. calculatedLineHeight: calculatedLineHeight,
  159. contentWidth: contentWidth,
  160. contentHeight: contentHeight,
  161. width: width,
  162. isTruncated: isTruncated
  163. };
  164. }
  165. var RichTextToken = (function () {
  166. function RichTextToken() {
  167. }
  168. return RichTextToken;
  169. }());
  170. var RichTextLine = (function () {
  171. function RichTextLine(tokens) {
  172. this.tokens = [];
  173. if (tokens) {
  174. this.tokens = tokens;
  175. }
  176. }
  177. return RichTextLine;
  178. }());
  179. var RichTextContentBlock = (function () {
  180. function RichTextContentBlock() {
  181. this.width = 0;
  182. this.height = 0;
  183. this.contentWidth = 0;
  184. this.contentHeight = 0;
  185. this.outerWidth = 0;
  186. this.outerHeight = 0;
  187. this.lines = [];
  188. this.isTruncated = false;
  189. }
  190. return RichTextContentBlock;
  191. }());
  192. export { RichTextContentBlock };
  193. export function parseRichText(text, style) {
  194. var contentBlock = new RichTextContentBlock();
  195. text != null && (text += '');
  196. if (!text) {
  197. return contentBlock;
  198. }
  199. var topWidth = style.width;
  200. var topHeight = style.height;
  201. var overflow = style.overflow;
  202. var wrapInfo = (overflow === 'break' || overflow === 'breakAll') && topWidth != null
  203. ? { width: topWidth, accumWidth: 0, breakAll: overflow === 'breakAll' }
  204. : null;
  205. var lastIndex = STYLE_REG.lastIndex = 0;
  206. var result;
  207. while ((result = STYLE_REG.exec(text)) != null) {
  208. var matchedIndex = result.index;
  209. if (matchedIndex > lastIndex) {
  210. pushTokens(contentBlock, text.substring(lastIndex, matchedIndex), style, wrapInfo);
  211. }
  212. pushTokens(contentBlock, result[2], style, wrapInfo, result[1]);
  213. lastIndex = STYLE_REG.lastIndex;
  214. }
  215. if (lastIndex < text.length) {
  216. pushTokens(contentBlock, text.substring(lastIndex, text.length), style, wrapInfo);
  217. }
  218. var pendingList = [];
  219. var calculatedHeight = 0;
  220. var calculatedWidth = 0;
  221. var stlPadding = style.padding;
  222. var truncate = overflow === 'truncate';
  223. var truncateLine = style.lineOverflow === 'truncate';
  224. var tmpTruncateOut = {};
  225. function finishLine(line, lineWidth, lineHeight) {
  226. line.width = lineWidth;
  227. line.lineHeight = lineHeight;
  228. calculatedHeight += lineHeight;
  229. calculatedWidth = Math.max(calculatedWidth, lineWidth);
  230. }
  231. outer: for (var i = 0; i < contentBlock.lines.length; i++) {
  232. var line = contentBlock.lines[i];
  233. var lineHeight = 0;
  234. var lineWidth = 0;
  235. for (var j = 0; j < line.tokens.length; j++) {
  236. var token = line.tokens[j];
  237. var tokenStyle = token.styleName && style.rich[token.styleName] || {};
  238. var textPadding = token.textPadding = tokenStyle.padding;
  239. var paddingH = textPadding ? textPadding[1] + textPadding[3] : 0;
  240. var font = token.font = tokenStyle.font || style.font;
  241. token.contentHeight = getLineHeight(font);
  242. var tokenHeight = retrieve2(tokenStyle.height, token.contentHeight);
  243. token.innerHeight = tokenHeight;
  244. textPadding && (tokenHeight += textPadding[0] + textPadding[2]);
  245. token.height = tokenHeight;
  246. token.lineHeight = retrieve3(tokenStyle.lineHeight, style.lineHeight, tokenHeight);
  247. token.align = tokenStyle && tokenStyle.align || style.align;
  248. token.verticalAlign = tokenStyle && tokenStyle.verticalAlign || 'middle';
  249. if (truncateLine && topHeight != null && calculatedHeight + token.lineHeight > topHeight) {
  250. var originalLength = contentBlock.lines.length;
  251. if (j > 0) {
  252. line.tokens = line.tokens.slice(0, j);
  253. finishLine(line, lineWidth, lineHeight);
  254. contentBlock.lines = contentBlock.lines.slice(0, i + 1);
  255. }
  256. else {
  257. contentBlock.lines = contentBlock.lines.slice(0, i);
  258. }
  259. contentBlock.isTruncated = contentBlock.isTruncated || (contentBlock.lines.length < originalLength);
  260. break outer;
  261. }
  262. var styleTokenWidth = tokenStyle.width;
  263. var tokenWidthNotSpecified = styleTokenWidth == null || styleTokenWidth === 'auto';
  264. if (typeof styleTokenWidth === 'string' && styleTokenWidth.charAt(styleTokenWidth.length - 1) === '%') {
  265. token.percentWidth = styleTokenWidth;
  266. pendingList.push(token);
  267. token.contentWidth = getWidth(token.text, font);
  268. }
  269. else {
  270. if (tokenWidthNotSpecified) {
  271. var textBackgroundColor = tokenStyle.backgroundColor;
  272. var bgImg = textBackgroundColor && textBackgroundColor.image;
  273. if (bgImg) {
  274. bgImg = imageHelper.findExistImage(bgImg);
  275. if (imageHelper.isImageReady(bgImg)) {
  276. token.width = Math.max(token.width, bgImg.width * tokenHeight / bgImg.height);
  277. }
  278. }
  279. }
  280. var remainTruncWidth = truncate && topWidth != null
  281. ? topWidth - lineWidth : null;
  282. if (remainTruncWidth != null && remainTruncWidth < token.width) {
  283. if (!tokenWidthNotSpecified || remainTruncWidth < paddingH) {
  284. token.text = '';
  285. token.width = token.contentWidth = 0;
  286. }
  287. else {
  288. truncateText2(tmpTruncateOut, token.text, remainTruncWidth - paddingH, font, style.ellipsis, { minChar: style.truncateMinChar });
  289. token.text = tmpTruncateOut.text;
  290. contentBlock.isTruncated = contentBlock.isTruncated || tmpTruncateOut.isTruncated;
  291. token.width = token.contentWidth = getWidth(token.text, font);
  292. }
  293. }
  294. else {
  295. token.contentWidth = getWidth(token.text, font);
  296. }
  297. }
  298. token.width += paddingH;
  299. lineWidth += token.width;
  300. tokenStyle && (lineHeight = Math.max(lineHeight, token.lineHeight));
  301. }
  302. finishLine(line, lineWidth, lineHeight);
  303. }
  304. contentBlock.outerWidth = contentBlock.width = retrieve2(topWidth, calculatedWidth);
  305. contentBlock.outerHeight = contentBlock.height = retrieve2(topHeight, calculatedHeight);
  306. contentBlock.contentHeight = calculatedHeight;
  307. contentBlock.contentWidth = calculatedWidth;
  308. if (stlPadding) {
  309. contentBlock.outerWidth += stlPadding[1] + stlPadding[3];
  310. contentBlock.outerHeight += stlPadding[0] + stlPadding[2];
  311. }
  312. for (var i = 0; i < pendingList.length; i++) {
  313. var token = pendingList[i];
  314. var percentWidth = token.percentWidth;
  315. token.width = parseInt(percentWidth, 10) / 100 * contentBlock.width;
  316. }
  317. return contentBlock;
  318. }
  319. function pushTokens(block, str, style, wrapInfo, styleName) {
  320. var isEmptyStr = str === '';
  321. var tokenStyle = styleName && style.rich[styleName] || {};
  322. var lines = block.lines;
  323. var font = tokenStyle.font || style.font;
  324. var newLine = false;
  325. var strLines;
  326. var linesWidths;
  327. if (wrapInfo) {
  328. var tokenPadding = tokenStyle.padding;
  329. var tokenPaddingH = tokenPadding ? tokenPadding[1] + tokenPadding[3] : 0;
  330. if (tokenStyle.width != null && tokenStyle.width !== 'auto') {
  331. var outerWidth_1 = parsePercent(tokenStyle.width, wrapInfo.width) + tokenPaddingH;
  332. if (lines.length > 0) {
  333. if (outerWidth_1 + wrapInfo.accumWidth > wrapInfo.width) {
  334. strLines = str.split('\n');
  335. newLine = true;
  336. }
  337. }
  338. wrapInfo.accumWidth = outerWidth_1;
  339. }
  340. else {
  341. var res = wrapText(str, font, wrapInfo.width, wrapInfo.breakAll, wrapInfo.accumWidth);
  342. wrapInfo.accumWidth = res.accumWidth + tokenPaddingH;
  343. linesWidths = res.linesWidths;
  344. strLines = res.lines;
  345. }
  346. }
  347. else {
  348. strLines = str.split('\n');
  349. }
  350. for (var i = 0; i < strLines.length; i++) {
  351. var text = strLines[i];
  352. var token = new RichTextToken();
  353. token.styleName = styleName;
  354. token.text = text;
  355. token.isLineHolder = !text && !isEmptyStr;
  356. if (typeof tokenStyle.width === 'number') {
  357. token.width = tokenStyle.width;
  358. }
  359. else {
  360. token.width = linesWidths
  361. ? linesWidths[i]
  362. : getWidth(text, font);
  363. }
  364. if (!i && !newLine) {
  365. var tokens = (lines[lines.length - 1] || (lines[0] = new RichTextLine())).tokens;
  366. var tokensLen = tokens.length;
  367. (tokensLen === 1 && tokens[0].isLineHolder)
  368. ? (tokens[0] = token)
  369. : ((text || !tokensLen || isEmptyStr) && tokens.push(token));
  370. }
  371. else {
  372. lines.push(new RichTextLine([token]));
  373. }
  374. }
  375. }
  376. function isAlphabeticLetter(ch) {
  377. var code = ch.charCodeAt(0);
  378. return code >= 0x20 && code <= 0x24F
  379. || code >= 0x370 && code <= 0x10FF
  380. || code >= 0x1200 && code <= 0x13FF
  381. || code >= 0x1E00 && code <= 0x206F;
  382. }
  383. var breakCharMap = reduce(',&?/;] '.split(''), function (obj, ch) {
  384. obj[ch] = true;
  385. return obj;
  386. }, {});
  387. function isWordBreakChar(ch) {
  388. if (isAlphabeticLetter(ch)) {
  389. if (breakCharMap[ch]) {
  390. return true;
  391. }
  392. return false;
  393. }
  394. return true;
  395. }
  396. function wrapText(text, font, lineWidth, isBreakAll, lastAccumWidth) {
  397. var lines = [];
  398. var linesWidths = [];
  399. var line = '';
  400. var currentWord = '';
  401. var currentWordWidth = 0;
  402. var accumWidth = 0;
  403. for (var i = 0; i < text.length; i++) {
  404. var ch = text.charAt(i);
  405. if (ch === '\n') {
  406. if (currentWord) {
  407. line += currentWord;
  408. accumWidth += currentWordWidth;
  409. }
  410. lines.push(line);
  411. linesWidths.push(accumWidth);
  412. line = '';
  413. currentWord = '';
  414. currentWordWidth = 0;
  415. accumWidth = 0;
  416. continue;
  417. }
  418. var chWidth = getWidth(ch, font);
  419. var inWord = isBreakAll ? false : !isWordBreakChar(ch);
  420. if (!lines.length
  421. ? lastAccumWidth + accumWidth + chWidth > lineWidth
  422. : accumWidth + chWidth > lineWidth) {
  423. if (!accumWidth) {
  424. if (inWord) {
  425. lines.push(currentWord);
  426. linesWidths.push(currentWordWidth);
  427. currentWord = ch;
  428. currentWordWidth = chWidth;
  429. }
  430. else {
  431. lines.push(ch);
  432. linesWidths.push(chWidth);
  433. }
  434. }
  435. else if (line || currentWord) {
  436. if (inWord) {
  437. if (!line) {
  438. line = currentWord;
  439. currentWord = '';
  440. currentWordWidth = 0;
  441. accumWidth = currentWordWidth;
  442. }
  443. lines.push(line);
  444. linesWidths.push(accumWidth - currentWordWidth);
  445. currentWord += ch;
  446. currentWordWidth += chWidth;
  447. line = '';
  448. accumWidth = currentWordWidth;
  449. }
  450. else {
  451. if (currentWord) {
  452. line += currentWord;
  453. currentWord = '';
  454. currentWordWidth = 0;
  455. }
  456. lines.push(line);
  457. linesWidths.push(accumWidth);
  458. line = ch;
  459. accumWidth = chWidth;
  460. }
  461. }
  462. continue;
  463. }
  464. accumWidth += chWidth;
  465. if (inWord) {
  466. currentWord += ch;
  467. currentWordWidth += chWidth;
  468. }
  469. else {
  470. if (currentWord) {
  471. line += currentWord;
  472. currentWord = '';
  473. currentWordWidth = 0;
  474. }
  475. line += ch;
  476. }
  477. }
  478. if (!lines.length && !line) {
  479. line = text;
  480. currentWord = '';
  481. currentWordWidth = 0;
  482. }
  483. if (currentWord) {
  484. line += currentWord;
  485. }
  486. if (line) {
  487. lines.push(line);
  488. linesWidths.push(accumWidth);
  489. }
  490. if (lines.length === 1) {
  491. accumWidth += lastAccumWidth;
  492. }
  493. return {
  494. accumWidth: accumWidth,
  495. lines: lines,
  496. linesWidths: linesWidths
  497. };
  498. }