text.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. "use strict";
  2. /* eslint-disable no-control-regex */
  3. Object.defineProperty(exports, "__esModule", { value: true });
  4. exports.breakText = exports.splitTextByLength = exports.measureText = exports.text = void 0;
  5. const string_1 = require("../string");
  6. const text_1 = require("../text");
  7. const attr_1 = require("./attr");
  8. const vector_1 = require("../vector");
  9. const elem_1 = require("./elem");
  10. function createTextPathNode(attrs, elem) {
  11. const vel = vector_1.Vector.create(elem);
  12. const textPath = vector_1.Vector.create('textPath');
  13. const d = attrs.d;
  14. if (d && attrs['xlink:href'] === undefined) {
  15. const path = vector_1.Vector.create('path').attr('d', d).appendTo(vel.defs());
  16. textPath.attr('xlink:href', `#${path.id}`);
  17. }
  18. if (typeof attrs === 'object') {
  19. textPath.attr(attrs);
  20. }
  21. return textPath.node;
  22. }
  23. function annotateTextLine(lineNode, lineAnnotations, options) {
  24. const eol = options.eol;
  25. const baseSize = options.baseSize;
  26. const lineHeight = options.lineHeight;
  27. let maxFontSize = 0;
  28. let tspanNode;
  29. const fontMetrics = {};
  30. const lastJ = lineAnnotations.length - 1;
  31. for (let j = 0; j <= lastJ; j += 1) {
  32. let annotation = lineAnnotations[j];
  33. let fontSize = null;
  34. if (typeof annotation === 'object') {
  35. const annotationAttrs = annotation.attrs;
  36. const vTSpan = vector_1.Vector.create('tspan', annotationAttrs);
  37. tspanNode = vTSpan.node;
  38. let t = annotation.t;
  39. if (eol && j === lastJ) {
  40. t += eol;
  41. }
  42. tspanNode.textContent = t;
  43. // Per annotation className
  44. const annotationClass = annotationAttrs.class;
  45. if (annotationClass) {
  46. vTSpan.addClass(annotationClass);
  47. }
  48. // set the list of indices of all the applied annotations
  49. // in the `annotations` attribute. This list is a comma
  50. // separated list of indices.
  51. if (options.includeAnnotationIndices) {
  52. vTSpan.attr('annotations', annotation.annotations.join(','));
  53. }
  54. // Check for max font size
  55. fontSize = parseFloat(annotationAttrs['font-size']);
  56. if (fontSize === undefined)
  57. fontSize = baseSize;
  58. if (fontSize && fontSize > maxFontSize)
  59. maxFontSize = fontSize;
  60. }
  61. else {
  62. if (eol && j === lastJ) {
  63. annotation += eol;
  64. }
  65. tspanNode = document.createTextNode(annotation || ' ');
  66. if (baseSize && baseSize > maxFontSize) {
  67. maxFontSize = baseSize;
  68. }
  69. }
  70. lineNode.appendChild(tspanNode);
  71. }
  72. if (maxFontSize) {
  73. fontMetrics.maxFontSize = maxFontSize;
  74. }
  75. if (lineHeight) {
  76. fontMetrics.lineHeight = lineHeight;
  77. }
  78. else if (maxFontSize) {
  79. fontMetrics.lineHeight = maxFontSize * 1.2;
  80. }
  81. return fontMetrics;
  82. }
  83. const emRegex = /em$/;
  84. function emToPx(em, fontSize) {
  85. const numerical = parseFloat(em);
  86. if (emRegex.test(em)) {
  87. return numerical * fontSize;
  88. }
  89. return numerical;
  90. }
  91. function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) {
  92. if (!Array.isArray(linesMetrics)) {
  93. return 0;
  94. }
  95. const n = linesMetrics.length;
  96. if (!n)
  97. return 0;
  98. let lineMetrics = linesMetrics[0];
  99. const flMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
  100. let rLineHeights = 0;
  101. const lineHeightPx = emToPx(lineHeight, baseSizePx);
  102. for (let i = 1; i < n; i += 1) {
  103. lineMetrics = linesMetrics[i];
  104. const iLineHeight = emToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx;
  105. rLineHeights += iLineHeight;
  106. }
  107. const llMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
  108. let dy;
  109. switch (alignment) {
  110. case 'middle':
  111. dy = flMaxFont / 2 - 0.15 * llMaxFont - rLineHeights / 2;
  112. break;
  113. case 'bottom':
  114. dy = -(0.25 * llMaxFont) - rLineHeights;
  115. break;
  116. case 'top':
  117. default:
  118. dy = 0.8 * flMaxFont;
  119. break;
  120. }
  121. return dy;
  122. }
  123. function text(elem, content, options = {}) {
  124. content = text_1.Text.sanitize(content); // eslint-disable-line
  125. const eol = options.eol;
  126. let textPath = options.textPath;
  127. const verticalAnchor = options.textVerticalAnchor;
  128. const namedVerticalAnchor = verticalAnchor === 'middle' ||
  129. verticalAnchor === 'bottom' ||
  130. verticalAnchor === 'top';
  131. // Horizontal shift applied to all the lines but the first.
  132. let x = options.x;
  133. if (x === undefined) {
  134. x = elem.getAttribute('x') || 0;
  135. }
  136. // Annotations
  137. const iai = options.includeAnnotationIndices;
  138. let annotations = options.annotations;
  139. if (annotations && !Array.isArray(annotations)) {
  140. annotations = [annotations];
  141. }
  142. // Shift all the <tspan> but first by one line (`1em`)
  143. const defaultLineHeight = options.lineHeight;
  144. const autoLineHeight = defaultLineHeight === 'auto';
  145. const lineHeight = autoLineHeight ? '1.5em' : defaultLineHeight || '1em';
  146. let needEmpty = true;
  147. const childNodes = elem.childNodes;
  148. if (childNodes.length === 1) {
  149. const node = childNodes[0];
  150. if (node && node.tagName.toUpperCase() === 'TITLE') {
  151. needEmpty = false;
  152. }
  153. }
  154. if (needEmpty) {
  155. (0, elem_1.empty)(elem);
  156. }
  157. (0, attr_1.attr)(elem, {
  158. // Preserve spaces, do not consecutive spaces to get collapsed to one.
  159. 'xml:space': 'preserve',
  160. // An empty text gets rendered into the DOM in webkit-based browsers.
  161. // In order to unify this behaviour across all browsers
  162. // we rather hide the text element when it's empty.
  163. display: content || options.displayEmpty ? null : 'none',
  164. });
  165. // Set default font-size if none
  166. const strFontSize = (0, attr_1.attr)(elem, 'font-size');
  167. let fontSize = parseFloat(strFontSize);
  168. if (!fontSize) {
  169. fontSize = 16;
  170. if ((namedVerticalAnchor || annotations) && !strFontSize) {
  171. (0, attr_1.attr)(elem, 'font-size', `${fontSize}`);
  172. }
  173. }
  174. let containerNode;
  175. if (textPath) {
  176. // Now all the `<tspan>`s will be inside the `<textPath>`.
  177. if (typeof textPath === 'string') {
  178. textPath = { d: textPath };
  179. }
  180. containerNode = createTextPathNode(textPath, elem);
  181. }
  182. else {
  183. containerNode = document.createDocumentFragment();
  184. }
  185. let dy;
  186. let offset = 0;
  187. let annotatedY;
  188. const lines = content.split('\n');
  189. const linesMetrics = [];
  190. const lastI = lines.length - 1;
  191. for (let i = 0; i <= lastI; i += 1) {
  192. dy = lineHeight;
  193. let lineClassName = 'v-line';
  194. const lineNode = (0, elem_1.createSvgElement)('tspan');
  195. let lineMetrics;
  196. let line = lines[i];
  197. if (line) {
  198. if (annotations) {
  199. // Find the *compacted* annotations for this line.
  200. const lineAnnotations = text_1.Text.annotate(line, annotations, {
  201. offset: -offset,
  202. includeAnnotationIndices: iai,
  203. });
  204. lineMetrics = annotateTextLine(lineNode, lineAnnotations, {
  205. eol: i !== lastI && eol,
  206. baseSize: fontSize,
  207. lineHeight: autoLineHeight ? null : lineHeight,
  208. includeAnnotationIndices: iai,
  209. });
  210. // Get the line height based on the biggest font size
  211. // in the annotations for this line.
  212. const iLineHeight = lineMetrics.lineHeight;
  213. if (iLineHeight && autoLineHeight && i !== 0) {
  214. dy = iLineHeight;
  215. }
  216. if (i === 0) {
  217. annotatedY = lineMetrics.maxFontSize * 0.8;
  218. }
  219. }
  220. else {
  221. if (eol && i !== lastI) {
  222. line += eol;
  223. }
  224. lineNode.textContent = line;
  225. }
  226. }
  227. else {
  228. // Make sure the textContent is never empty. If it is, add a dummy
  229. // character and make it invisible, making the following lines correctly
  230. // relatively positioned. `dy=1em` won't work with empty lines otherwise.
  231. lineNode.textContent = '-';
  232. lineClassName += ' v-empty-line';
  233. const lineNodeStyle = lineNode.style;
  234. lineNodeStyle.fillOpacity = 0;
  235. lineNodeStyle.strokeOpacity = 0;
  236. if (annotations) {
  237. lineMetrics = {};
  238. }
  239. }
  240. if (lineMetrics) {
  241. linesMetrics.push(lineMetrics);
  242. }
  243. if (i > 0) {
  244. lineNode.setAttribute('dy', dy);
  245. }
  246. // Firefox requires 'x' to be set on the first line
  247. if (i > 0 || textPath) {
  248. lineNode.setAttribute('x', x);
  249. }
  250. lineNode.className.baseVal = lineClassName;
  251. containerNode.appendChild(lineNode);
  252. offset += line.length + 1; // + 1 = newline character.
  253. }
  254. // Y Alignment calculation
  255. if (namedVerticalAnchor) {
  256. if (annotations) {
  257. dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight);
  258. }
  259. else if (verticalAnchor === 'top') {
  260. // A shortcut for top alignment. It does not depend on font-size nor line-height
  261. dy = '0.8em';
  262. }
  263. else {
  264. let rh; // remaining height
  265. if (lastI > 0) {
  266. rh = parseFloat(lineHeight) || 1;
  267. rh *= lastI;
  268. if (!emRegex.test(lineHeight))
  269. rh /= fontSize;
  270. }
  271. else {
  272. // Single-line text
  273. rh = 0;
  274. }
  275. switch (verticalAnchor) {
  276. case 'middle':
  277. dy = `${0.3 - rh / 2}em`;
  278. break;
  279. case 'bottom':
  280. dy = `${-rh - 0.3}em`;
  281. break;
  282. default:
  283. break;
  284. }
  285. }
  286. }
  287. else if (verticalAnchor === 0) {
  288. dy = '0em';
  289. }
  290. else if (verticalAnchor) {
  291. dy = verticalAnchor;
  292. }
  293. else {
  294. // No vertical anchor is defined
  295. dy = 0;
  296. // Backwards compatibility - we change the `y` attribute instead of `dy`.
  297. if (elem.getAttribute('y') == null) {
  298. elem.setAttribute('y', `${annotatedY || '0.8em'}`);
  299. }
  300. }
  301. const firstLine = containerNode.firstChild;
  302. firstLine.setAttribute('dy', dy);
  303. elem.appendChild(containerNode);
  304. }
  305. exports.text = text;
  306. function measureText(text, styles = {}) {
  307. const canvasContext = document.createElement('canvas').getContext('2d');
  308. if (!text) {
  309. return { width: 0 };
  310. }
  311. const font = [];
  312. const fontSize = styles['font-size']
  313. ? `${parseFloat(styles['font-size'])}px`
  314. : '14px';
  315. font.push(styles['font-style'] || 'normal');
  316. font.push(styles['font-variant'] || 'normal');
  317. font.push(styles['font-weight'] || 400);
  318. font.push(fontSize);
  319. font.push(styles['font-family'] || 'sans-serif');
  320. canvasContext.font = font.join(' ');
  321. return canvasContext.measureText(text);
  322. }
  323. exports.measureText = measureText;
  324. function splitTextByLength(text, splitWidth, totalWidth, style = {}) {
  325. if (splitWidth >= totalWidth) {
  326. return [text, ''];
  327. }
  328. const length = text.length;
  329. const caches = {};
  330. let index = Math.round((splitWidth / totalWidth) * length - 1);
  331. if (index < 0) {
  332. index = 0;
  333. }
  334. // eslint-disable-next-line
  335. while (index >= 0 && index < length) {
  336. const frontText = text.slice(0, index);
  337. const frontWidth = caches[frontText] || measureText(frontText, style).width;
  338. const behindText = text.slice(0, index + 1);
  339. const behindWidth = caches[behindText] || measureText(behindText, style).width;
  340. caches[frontText] = frontWidth;
  341. caches[behindText] = behindWidth;
  342. if (frontWidth > splitWidth) {
  343. index -= 1;
  344. }
  345. else if (behindWidth <= splitWidth) {
  346. index += 1;
  347. }
  348. else {
  349. break;
  350. }
  351. }
  352. return [text.slice(0, index), text.slice(index)];
  353. }
  354. exports.splitTextByLength = splitTextByLength;
  355. function breakText(text, size, styles = {}, options = {}) {
  356. const width = size.width;
  357. const height = size.height;
  358. const eol = options.eol || '\n';
  359. const fontSize = styles.fontSize || 14;
  360. const lineHeight = styles.lineHeight
  361. ? parseFloat(styles.lineHeight)
  362. : Math.ceil(fontSize * 1.4);
  363. const maxLines = Math.floor(height / lineHeight);
  364. if (text.indexOf(eol) > -1) {
  365. const delimiter = string_1.StringExt.uuid();
  366. const splitText = [];
  367. text.split(eol).map((line) => {
  368. const part = breakText(line, Object.assign(Object.assign({}, size), { height: Number.MAX_SAFE_INTEGER }), styles, Object.assign(Object.assign({}, options), { eol: delimiter }));
  369. if (part) {
  370. splitText.push(...part.split(delimiter));
  371. }
  372. });
  373. return splitText.slice(0, maxLines).join(eol);
  374. }
  375. const { width: textWidth } = measureText(text, styles);
  376. if (textWidth < width) {
  377. return text;
  378. }
  379. const lines = [];
  380. let remainText = text;
  381. let remainWidth = textWidth;
  382. let ellipsis = options.ellipsis;
  383. let ellipsisWidth = 0;
  384. if (ellipsis) {
  385. if (typeof ellipsis !== 'string') {
  386. ellipsis = '\u2026';
  387. }
  388. ellipsisWidth = measureText(ellipsis, styles).width;
  389. }
  390. for (let i = 0; i < maxLines; i += 1) {
  391. if (remainWidth > width) {
  392. const isLast = i === maxLines - 1;
  393. if (isLast) {
  394. const [front] = splitTextByLength(remainText, width - ellipsisWidth, remainWidth, styles);
  395. lines.push(ellipsis ? `${front}${ellipsis}` : front);
  396. }
  397. else {
  398. const [front, behind] = splitTextByLength(remainText, width, remainWidth, styles);
  399. lines.push(front);
  400. remainText = behind;
  401. remainWidth = measureText(remainText, styles).width;
  402. }
  403. }
  404. else {
  405. lines.push(remainText);
  406. break;
  407. }
  408. }
  409. return lines.join(eol);
  410. }
  411. exports.breakText = breakText;
  412. //# sourceMappingURL=text.js.map