"use strict"; /* eslint-disable no-control-regex */ Object.defineProperty(exports, "__esModule", { value: true }); exports.breakText = exports.splitTextByLength = exports.measureText = exports.text = void 0; const string_1 = require("../string"); const text_1 = require("../text"); const attr_1 = require("./attr"); const vector_1 = require("../vector"); const elem_1 = require("./elem"); function createTextPathNode(attrs, elem) { const vel = vector_1.Vector.create(elem); const textPath = vector_1.Vector.create('textPath'); const d = attrs.d; if (d && attrs['xlink:href'] === undefined) { const path = vector_1.Vector.create('path').attr('d', d).appendTo(vel.defs()); textPath.attr('xlink:href', `#${path.id}`); } if (typeof attrs === 'object') { textPath.attr(attrs); } return textPath.node; } function annotateTextLine(lineNode, lineAnnotations, options) { const eol = options.eol; const baseSize = options.baseSize; const lineHeight = options.lineHeight; let maxFontSize = 0; let tspanNode; const fontMetrics = {}; const lastJ = lineAnnotations.length - 1; for (let j = 0; j <= lastJ; j += 1) { let annotation = lineAnnotations[j]; let fontSize = null; if (typeof annotation === 'object') { const annotationAttrs = annotation.attrs; const vTSpan = vector_1.Vector.create('tspan', annotationAttrs); tspanNode = vTSpan.node; let t = annotation.t; if (eol && j === lastJ) { t += eol; } tspanNode.textContent = t; // Per annotation className const annotationClass = annotationAttrs.class; if (annotationClass) { vTSpan.addClass(annotationClass); } // set the list of indices of all the applied annotations // in the `annotations` attribute. This list is a comma // separated list of indices. if (options.includeAnnotationIndices) { vTSpan.attr('annotations', annotation.annotations.join(',')); } // Check for max font size fontSize = parseFloat(annotationAttrs['font-size']); if (fontSize === undefined) fontSize = baseSize; if (fontSize && fontSize > maxFontSize) maxFontSize = fontSize; } else { if (eol && j === lastJ) { annotation += eol; } tspanNode = document.createTextNode(annotation || ' '); if (baseSize && baseSize > maxFontSize) { maxFontSize = baseSize; } } lineNode.appendChild(tspanNode); } if (maxFontSize) { fontMetrics.maxFontSize = maxFontSize; } if (lineHeight) { fontMetrics.lineHeight = lineHeight; } else if (maxFontSize) { fontMetrics.lineHeight = maxFontSize * 1.2; } return fontMetrics; } const emRegex = /em$/; function emToPx(em, fontSize) { const numerical = parseFloat(em); if (emRegex.test(em)) { return numerical * fontSize; } return numerical; } function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) { if (!Array.isArray(linesMetrics)) { return 0; } const n = linesMetrics.length; if (!n) return 0; let lineMetrics = linesMetrics[0]; const flMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; let rLineHeights = 0; const lineHeightPx = emToPx(lineHeight, baseSizePx); for (let i = 1; i < n; i += 1) { lineMetrics = linesMetrics[i]; const iLineHeight = emToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx; rLineHeights += iLineHeight; } const llMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx; let dy; switch (alignment) { case 'middle': dy = flMaxFont / 2 - 0.15 * llMaxFont - rLineHeights / 2; break; case 'bottom': dy = -(0.25 * llMaxFont) - rLineHeights; break; case 'top': default: dy = 0.8 * flMaxFont; break; } return dy; } function text(elem, content, options = {}) { content = text_1.Text.sanitize(content); // eslint-disable-line const eol = options.eol; let textPath = options.textPath; const verticalAnchor = options.textVerticalAnchor; const namedVerticalAnchor = verticalAnchor === 'middle' || verticalAnchor === 'bottom' || verticalAnchor === 'top'; // Horizontal shift applied to all the lines but the first. let x = options.x; if (x === undefined) { x = elem.getAttribute('x') || 0; } // Annotations const iai = options.includeAnnotationIndices; let annotations = options.annotations; if (annotations && !Array.isArray(annotations)) { annotations = [annotations]; } // Shift all the but first by one line (`1em`) const defaultLineHeight = options.lineHeight; const autoLineHeight = defaultLineHeight === 'auto'; const lineHeight = autoLineHeight ? '1.5em' : defaultLineHeight || '1em'; let needEmpty = true; const childNodes = elem.childNodes; if (childNodes.length === 1) { const node = childNodes[0]; if (node && node.tagName.toUpperCase() === 'TITLE') { needEmpty = false; } } if (needEmpty) { (0, elem_1.empty)(elem); } (0, attr_1.attr)(elem, { // Preserve spaces, do not consecutive spaces to get collapsed to one. 'xml:space': 'preserve', // An empty text gets rendered into the DOM in webkit-based browsers. // In order to unify this behaviour across all browsers // we rather hide the text element when it's empty. display: content || options.displayEmpty ? null : 'none', }); // Set default font-size if none const strFontSize = (0, attr_1.attr)(elem, 'font-size'); let fontSize = parseFloat(strFontSize); if (!fontSize) { fontSize = 16; if ((namedVerticalAnchor || annotations) && !strFontSize) { (0, attr_1.attr)(elem, 'font-size', `${fontSize}`); } } let containerNode; if (textPath) { // Now all the ``s will be inside the ``. if (typeof textPath === 'string') { textPath = { d: textPath }; } containerNode = createTextPathNode(textPath, elem); } else { containerNode = document.createDocumentFragment(); } let dy; let offset = 0; let annotatedY; const lines = content.split('\n'); const linesMetrics = []; const lastI = lines.length - 1; for (let i = 0; i <= lastI; i += 1) { dy = lineHeight; let lineClassName = 'v-line'; const lineNode = (0, elem_1.createSvgElement)('tspan'); let lineMetrics; let line = lines[i]; if (line) { if (annotations) { // Find the *compacted* annotations for this line. const lineAnnotations = text_1.Text.annotate(line, annotations, { offset: -offset, includeAnnotationIndices: iai, }); lineMetrics = annotateTextLine(lineNode, lineAnnotations, { eol: i !== lastI && eol, baseSize: fontSize, lineHeight: autoLineHeight ? null : lineHeight, includeAnnotationIndices: iai, }); // Get the line height based on the biggest font size // in the annotations for this line. const iLineHeight = lineMetrics.lineHeight; if (iLineHeight && autoLineHeight && i !== 0) { dy = iLineHeight; } if (i === 0) { annotatedY = lineMetrics.maxFontSize * 0.8; } } else { if (eol && i !== lastI) { line += eol; } lineNode.textContent = line; } } else { // Make sure the textContent is never empty. If it is, add a dummy // character and make it invisible, making the following lines correctly // relatively positioned. `dy=1em` won't work with empty lines otherwise. lineNode.textContent = '-'; lineClassName += ' v-empty-line'; const lineNodeStyle = lineNode.style; lineNodeStyle.fillOpacity = 0; lineNodeStyle.strokeOpacity = 0; if (annotations) { lineMetrics = {}; } } if (lineMetrics) { linesMetrics.push(lineMetrics); } if (i > 0) { lineNode.setAttribute('dy', dy); } // Firefox requires 'x' to be set on the first line if (i > 0 || textPath) { lineNode.setAttribute('x', x); } lineNode.className.baseVal = lineClassName; containerNode.appendChild(lineNode); offset += line.length + 1; // + 1 = newline character. } // Y Alignment calculation if (namedVerticalAnchor) { if (annotations) { dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight); } else if (verticalAnchor === 'top') { // A shortcut for top alignment. It does not depend on font-size nor line-height dy = '0.8em'; } else { let rh; // remaining height if (lastI > 0) { rh = parseFloat(lineHeight) || 1; rh *= lastI; if (!emRegex.test(lineHeight)) rh /= fontSize; } else { // Single-line text rh = 0; } switch (verticalAnchor) { case 'middle': dy = `${0.3 - rh / 2}em`; break; case 'bottom': dy = `${-rh - 0.3}em`; break; default: break; } } } else if (verticalAnchor === 0) { dy = '0em'; } else if (verticalAnchor) { dy = verticalAnchor; } else { // No vertical anchor is defined dy = 0; // Backwards compatibility - we change the `y` attribute instead of `dy`. if (elem.getAttribute('y') == null) { elem.setAttribute('y', `${annotatedY || '0.8em'}`); } } const firstLine = containerNode.firstChild; firstLine.setAttribute('dy', dy); elem.appendChild(containerNode); } exports.text = text; function measureText(text, styles = {}) { const canvasContext = document.createElement('canvas').getContext('2d'); if (!text) { return { width: 0 }; } const font = []; const fontSize = styles['font-size'] ? `${parseFloat(styles['font-size'])}px` : '14px'; font.push(styles['font-style'] || 'normal'); font.push(styles['font-variant'] || 'normal'); font.push(styles['font-weight'] || 400); font.push(fontSize); font.push(styles['font-family'] || 'sans-serif'); canvasContext.font = font.join(' '); return canvasContext.measureText(text); } exports.measureText = measureText; function splitTextByLength(text, splitWidth, totalWidth, style = {}) { if (splitWidth >= totalWidth) { return [text, '']; } const length = text.length; const caches = {}; let index = Math.round((splitWidth / totalWidth) * length - 1); if (index < 0) { index = 0; } // eslint-disable-next-line while (index >= 0 && index < length) { const frontText = text.slice(0, index); const frontWidth = caches[frontText] || measureText(frontText, style).width; const behindText = text.slice(0, index + 1); const behindWidth = caches[behindText] || measureText(behindText, style).width; caches[frontText] = frontWidth; caches[behindText] = behindWidth; if (frontWidth > splitWidth) { index -= 1; } else if (behindWidth <= splitWidth) { index += 1; } else { break; } } return [text.slice(0, index), text.slice(index)]; } exports.splitTextByLength = splitTextByLength; function breakText(text, size, styles = {}, options = {}) { const width = size.width; const height = size.height; const eol = options.eol || '\n'; const fontSize = styles.fontSize || 14; const lineHeight = styles.lineHeight ? parseFloat(styles.lineHeight) : Math.ceil(fontSize * 1.4); const maxLines = Math.floor(height / lineHeight); if (text.indexOf(eol) > -1) { const delimiter = string_1.StringExt.uuid(); const splitText = []; text.split(eol).map((line) => { const part = breakText(line, Object.assign(Object.assign({}, size), { height: Number.MAX_SAFE_INTEGER }), styles, Object.assign(Object.assign({}, options), { eol: delimiter })); if (part) { splitText.push(...part.split(delimiter)); } }); return splitText.slice(0, maxLines).join(eol); } const { width: textWidth } = measureText(text, styles); if (textWidth < width) { return text; } const lines = []; let remainText = text; let remainWidth = textWidth; let ellipsis = options.ellipsis; let ellipsisWidth = 0; if (ellipsis) { if (typeof ellipsis !== 'string') { ellipsis = '\u2026'; } ellipsisWidth = measureText(ellipsis, styles).width; } for (let i = 0; i < maxLines; i += 1) { if (remainWidth > width) { const isLast = i === maxLines - 1; if (isLast) { const [front] = splitTextByLength(remainText, width - ellipsisWidth, remainWidth, styles); lines.push(ellipsis ? `${front}${ellipsis}` : front); } else { const [front, behind] = splitTextByLength(remainText, width, remainWidth, styles); lines.push(front); remainText = behind; remainWidth = measureText(remainText, styles).width; } } else { lines.push(remainText); break; } } return lines.join(eol); } exports.breakText = breakText; //# sourceMappingURL=text.js.map