https://www.visiterouen.com/app/dist/rouen/addons/woody-library/st…/Core/Renderer/SVG/TextBuilder.js
1/* * 2 * 3 * (c) 2010-2024 Torstein Honsi 4 * 5 * License: www.highcharts.com/license 6 * 7 * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! 8 * 9 * */ 10'use strict'; 11import AST from '../HTML/AST.js'; 12import H from '../../Globals.js'; 13const { doc, SVG_NS, win } = H; 14import U from '../../Utilities.js'; 15const { attr, extend, fireEvent, isString, objectEach, pick } = U; 16// Function used to test string length including an ellipsis 17const stringWithEllipsis = (text, currentIndex) => text.substring(0, currentIndex) + '\u2026'; 18/* * 19 * 20 * Class 21 * 22 * */ 23/** 24 * SVG Text Builder 25 * @private 26 * @class 27 * @name Highcharts.TextBuilder 28 */ 29class TextBuilder { 30 constructor(svgElement) { 31 const textStyles = svgElement.styles; 32 this.renderer = svgElement.renderer; 33 this.svgElement = svgElement; 34 this.width = svgElement.textWidth; 35 this.textLineHeight = textStyles && textStyles.lineHeight; 36 this.textOutline = textStyles && textStyles.textOutline; 37 this.ellipsis = Boolean(textStyles && textStyles.textOverflow === 'ellipsis'); 38 this.lineClamp = textStyles?.lineClamp; 39 this.noWrap = Boolean(textStyles && textStyles.whiteSpace === 'nowrap'); 40 } 41 /** 42 * Build an SVG representation of the pseudo HTML given in the object's 43 * svgElement. 44 * 45 * @private 46 * 47 * @return {void}. 48 */ 49 buildSVG() { 50 const wrapper = this.svgElement, textNode = wrapper.element, renderer = wrapper.renderer, textStr = pick(wrapper.textStr, '').toString(), hasMarkup = textStr.indexOf('<') !== -1, childNodes = textNode.childNodes, tempParent = !wrapper.added && renderer.box, regexMatchBreaks = /<br.*?>/g, 51 // The buildText code is quite heavy, so if we're not changing 52 // something that affects the text, skip it (#6113). 53 textCache = [ 54 textStr, 55 this.ellipsis, 56 this.noWrap, 57 this.textLineHeight, 58 this.textOutline, 59 wrapper.getStyle('font-size'), 60 wrapper.styles.lineClamp, 61 this.width 62 ].join(','); 63 if (textCache === wrapper.textCache) { 64 return; 65 } 66 wrapper.textCache = textCache; 67 delete wrapper.actualWidth; 68 // Remove old text 69 for (let i = childNodes.length; i--;) { 70 textNode.removeChild(childNodes[i]); 71 } 72 // Simple strings, add text directly and return 73 if (!hasMarkup && 74 !this.ellipsis && 75 !this.width && 76 !wrapper.textPath && 77 (textStr.indexOf(' ') === -1 || 78 (this.noWrap && !regexMatchBreaks.test(textStr)))) { 79 textNode.appendChild(doc.createTextNode(this.unescapeEntities(textStr))); 80 // Complex strings, add more logic 81 } 82 else if (textStr !== '') { 83 if (tempParent) { 84 // Attach it to the DOM to read offset width and font size 85 tempParent.appendChild(textNode); 86 } 87 // Step 1. Parse the markup safely and directly into a tree 88 // structure. 89 const ast = new AST(textStr); 90 // Step 2. Do as many as we can of the modifications to the tree 91 // structure before it is added to the DOM 92 this.modifyTree(ast.nodes); 93 ast.addToDOM(textNode); 94 // Step 3. Some modifications can't be done until the structure is 95 // in the DOM, because we need to read computed metrics. 96 this.modifyDOM(); 97 // Add title if an ellipsis was added 98 if (this.ellipsis && 99 (textNode.textContent || '').indexOf('\u2026') !== -1) { 100 wrapper.attr('title', this.unescapeEntities(wrapper.textStr || '', ['<', '>']) // #7179 101 ); 102 } 103 if (tempParent) { 104 tempParent.removeChild(textNode); 105 } 106 } 107 // Apply the text outline 108 if (isString(this.textOutline) && wrapper.applyTextOutline) { 109 wrapper.applyTextOutline(this.textOutline); 110 } 111 } 112 /** 113 * Modify the DOM of the generated SVG structure. This function only does 114 * operations that cannot be done until the elements are attached to the 115 * DOM, like doing layout based on rendered metrics of the added elements. 116 * 117 * @private 118 * 119 */ 120 modifyDOM() { 121 const wrapper = this.svgElement; 122 const x = attr(wrapper.element, 'x'); 123 wrapper.firstLineMetrics = void 0; 124 // Remove empty tspans (including breaks) from the beginning because 125 // SVG's getBBox doesn't count empty lines. The use case is tooltip 126 // where the header is empty. By doing this in the DOM rather than
vendor: 4,367 bytes, lines 126-215
126in 127 // the AST, we can inspect the textContent directly and don't have to 128 // recurse down to look for valid content. 129 let firstChild; 130 while ((firstChild = wrapper.element.firstChild)) { 131 if (/^[\s\u200B]*$/.test(firstChild.textContent || ' ')) { 132 wrapper.element.removeChild(firstChild); 133 } 134 else { 135 break; 136 } 137 } 138 // Modify hard line breaks by applying the rendered line height 139 [].forEach.call(wrapper.element.querySelectorAll('tspan.highcharts-br'), (br, i) => { 140 if (br.nextSibling && br.previousSibling) { // #5261 141 if (i === 0 && br.previousSibling.nodeType === 1) { 142 wrapper.firstLineMetrics = wrapper.renderer 143 .fontMetrics(br.previousSibling); 144 } 145 attr(br, { 146 // Since the break is inserted in front of the next 147 // line, we need to use the next sibling for the line 148 // height 149 dy: this.getLineHeight(br.nextSibling), 150 x 151 }); 152 } 153 }); 154 // Constrain the line width, either by ellipsis or wrapping 155 const width = this.width || 0; 156 if (!width) { 157 return; 158 } 159 // Insert soft line breaks into each text node 160 const modifyTextNode = (textNode, parentElement) => { 161 const text = textNode.textContent || ''; 162 const words = text 163 .replace(/([^\^])-/g, '$1- ') // Split on hyphens 164 // .trim() 165 .split(' '); // #1273 166 const hasWhiteSpace = !this.noWrap && (words.length > 1 || wrapper.element.childNodes.length > 1); 167 const dy = this.getLineHeight(parentElement), ellipsisWidth = Math.max(0, 168 // Subtract the font face to make room for 169 // the ellipsis itself 170 width - 0.8 * dy); 171 let lineNo = 0; 172 let startAt = wrapper.actualWidth; 173 if (hasWhiteSpace) { 174 const lines = []; 175 // Remove preceding siblings in order to make the text length 176 // calculation correct in the truncate function 177 const precedingSiblings = []; 178 while (parentElement.firstChild && 179 parentElement.firstChild !== textNode) { 180 precedingSiblings.push(parentElement.firstChild); 181 parentElement.removeChild(parentElement.firstChild); 182 } 183 while (words.length) { 184 // Apply the previous line 185 if (words.length && !this.noWrap && lineNo > 0) { 186 lines.push(textNode.textContent || ''); 187 textNode.textContent = words.join(' ') 188 .replace(/- /g, '-'); 189 } 190 // For each line, truncate the remaining 191 // words into the line length. 192 this.truncate(textNode, void 0, words, lineNo === 0 ? (startAt || 0) : 0, width, ellipsisWidth, 193 // Build the text to test for 194 (t, currentIndex) => words 195 .slice(0, currentIndex) 196 .join(' ') 197 .replace(/- /g, '-')); 198 startAt = wrapper.actualWidth; 199 lineNo++; 200 // Line clamp. Break out after n lines and append an 201 // ellipsis regardless of the text length. 202 if (this.lineClamp && lineNo >= this.lineClamp) { 203 // Only if there are remaining words that should have 204 // been rendered. 205 if (words.length) { 206 this.truncate(textNode, textNode.textContent || '', void 0, 0, 207 // Target width 208 width, ellipsisWidth, stringWithEllipsis); 209 textNode.textContent = textNode.textContent 210 ?.replace('\u2026', '') + '\u2026'; 211 } 212 break; 213 } 214 } 215 // Reinsert the preceding child nodes
vendor: 10,236 bytes, lines 216-457
216 precedingSiblings.forEach((childNode) => { 217 parentElement.insertBefore(childNode, textNode); 218 }); 219 // Insert the previous lines before the original text node 220 lines.forEach((line) => { 221 // Insert the line 222 parentElement.insertBefore(doc.createTextNode(line), textNode); 223 // Insert a break 224 const br = doc.createElementNS(SVG_NS, 'tspan'); 225 br.textContent = '\u200B'; // Zero-width space 226 attr(br, { dy, x }); 227 parentElement.insertBefore(br, textNode); 228 }); 229 } 230 else if (this.ellipsis) { 231 if (text) { 232 this.truncate(textNode, text, void 0, 0, 233 // Target width 234 width, ellipsisWidth, stringWithEllipsis); 235 } 236 } 237 }; 238 // Recurse down the DOM tree and handle line breaks for each text node 239 const modifyChildren = ((node) => { 240 const childNodes = [].slice.call(node.childNodes); 241 childNodes.forEach((childNode) => { 242 if (childNode.nodeType === win.Node.TEXT_NODE) { 243 modifyTextNode(childNode, node); 244 } 245 else { 246 // Reset word-wrap width readings after hard breaks 247 if (childNode.className.baseVal 248 .indexOf('highcharts-br') !== -1) { 249 wrapper.actualWidth = 0; 250 } 251 // Recurse down to child node 252 modifyChildren(childNode); 253 } 254 }); 255 }); 256 modifyChildren(wrapper.element); 257 } 258 /** 259 * Get the rendered line height of a <text>, <tspan> or pure text node. 260 * @private 261 * @param {DOMElementType|Text} node The node to check for 262 * @return {number} The rendered line height 263 */ 264 getLineHeight(node) { 265 // If the node is a text node, use its parent 266 const element = (node.nodeType === win.Node.TEXT_NODE) ? 267 node.parentElement : 268 node; 269 return this.textLineHeight ? 270 parseInt(this.textLineHeight.toString(), 10) : 271 this.renderer.fontMetrics(element || this.svgElement.element).h; 272 } 273 /** 274 * Transform a pseudo HTML AST node tree into an SVG structure. We do as 275 * much heavy lifting as we can here, before doing the final processing in 276 * the modifyDOM function. The original data is mutated. 277 * 278 * @private 279 * 280 * @param {ASTNode[]} nodes The AST nodes 281 * 282 */ 283 modifyTree(nodes) { 284 const modifyChild = (node, i) => { 285 const { attributes = {}, children, style = {}, tagName } = node, styledMode = this.renderer.styledMode; 286 // Apply styling to text tags 287 if (tagName === 'b' || tagName === 'strong') { 288 if (styledMode) { 289 // eslint-disable-next-line dot-notation 290 attributes['class'] = 'highcharts-strong'; 291 } 292 else { 293 style.fontWeight = 'bold'; 294 } 295 } 296 else if (tagName === 'i' || tagName === 'em') { 297 if (styledMode) { 298 // eslint-disable-next-line dot-notation 299 attributes['class'] = 'highcharts-emphasized'; 300 } 301 else { 302 style.fontStyle = 'italic'; 303 } 304 } 305 // Modify styling 306 if (style && style.color) { 307 style.fill = style.color; 308 } 309 // Handle breaks 310 if (tagName === 'br') { 311 attributes['class'] = 'highcharts-br'; // eslint-disable-line dot-notation 312 node.textContent = '\u200B'; // Zero-width space 313 // Trim whitespace off the beginning of new lines 314 const nextNode = nodes[i + 1]; 315 if (nextNode && nextNode.textContent) { 316 nextNode.textContent = 317 nextNode.textContent.replace(/^ +/gm, ''); 318 } 319 // If an anchor has direct text node children, the text is unable to 320 // wrap because there is no `getSubStringLength` function on the 321 // element. Therefore we need to wrap the child text node or nodes 322 // in a tspan. #16173. 323 } 324 else if (tagName === 'a' && 325 children && 326 children.some((child) => child.tagName === '#text')) { 327 node.children = [{ children, tagName: 'tspan' }]; 328 } 329 if (tagName !== '#text' && tagName !== 'a') { 330 node.tagName = 'tspan'; 331 } 332 extend(node, { attributes, style }); 333 // Recurse 334 if (children) { 335 children 336 .filter((c) => c.tagName !== '#text') 337 .forEach(modifyChild); 338 } 339 }; 340 nodes.forEach(modifyChild); 341 fireEvent(this.svgElement, 'afterModifyTree', { nodes }); 342 } 343 /* 344 * Truncate the text node contents to a given length. Used when the css 345 * width is set. If the `textOverflow` is `ellipsis`, the text is truncated 346 * character by character to the given length. If not, the text is 347 * word-wrapped line by line. 348 */ 349 truncate(textNode, text, words, startAt, width, ellipsisWidth, getString) { 350 const svgElement = this.svgElement; 351 const { rotation } = svgElement; 352 // Cache the lengths to avoid checking the same twice 353 const lengths = []; 354 // Word wrap cannot be truncated to shorter than one word, ellipsis 355 // text can be completely blank. 356 let minIndex = words && !startAt ? 1 : 0; 357 let maxIndex = (text || words || '').length; 358 let currentIndex = maxIndex; 359 let str; 360 let actualWidth; 361 if (!words) { 362 width = ellipsisWidth; 363 } 364 const getSubStringLength = function (charEnd, concatenatedEnd) { 365 // `charEnd` is used when finding the character-by-character 366 // break for ellipsis, concatenatedEnd is used for word-by-word 367 // break for word wrapping. 368 const end = concatenatedEnd || charEnd; 369 const parentNode = textNode.parentNode; 370 if (parentNode && typeof lengths[end] === 'undefined') { 371 // Modern browsers 372 if (parentNode.getSubStringLength) { 373 // Fails with DOM exception on unit-tests/legend/members 374 // of unknown reason. Desired width is 0, text content 375 // is "5" and end is 1. 376 try { 377 lengths[end] = startAt + 378 parentNode.getSubStringLength(0, words ? end + 1 : end); 379 } 380 catch (e) { 381 ''; 382 } 383 } 384 } 385 return lengths[end]; 386 }; 387 svgElement.rotation = 0; // Discard rotation when computing box 388 actualWidth = getSubStringLength(textNode.textContent.length); 389 if (startAt + actualWidth > width) { 390 // Do a binary search for the index where to truncate the text 391 while (minIndex <= maxIndex) { 392 currentIndex = Math.ceil((minIndex + maxIndex) / 2); 393 // When checking words for word-wrap, we need to build the 394 // string and measure the subStringLength at the concatenated 395 // word length. 396 if (words) { 397 str = getString(words, currentIndex); 398 } 399 actualWidth = getSubStringLength(currentIndex, str && str.length - 1); 400 if (minIndex === maxIndex) { 401 // Complete 402 minIndex = maxIndex + 1; 403 } 404 else if (actualWidth > width) { 405 // Too large. Set max index to current. 406 maxIndex = currentIndex - 1; 407 } 408 else { 409 // Within width. Set min index to current. 410 minIndex = currentIndex; 411 } 412 } 413 // If max index was 0 it means the shortest possible text was also 414 // too large. For ellipsis that means only the ellipsis, while for 415 // word wrap it means the whole first word. 416 if (maxIndex === 0) { 417 // Remove ellipsis 418 textNode.textContent = ''; 419 // If the new text length is one less than the original, we don't 420 // need the ellipsis 421 } 422 else if (!(text && maxIndex === text.length - 1)) { 423 textNode.textContent = str || getString(text || words, currentIndex); 424 } 425 // Add ellipsis on individual lines 426 if (this.ellipsis && actualWidth > width) { 427 this.truncate(textNode, textNode.textContent || '', void 0, 0, width, ellipsisWidth, stringWithEllipsis); 428 } 429 } 430 // When doing line wrapping, prepare for the next line by removing the 431 // items from this line. 432 if (words) { 433 words.splice(0, currentIndex); 434 } 435 svgElement.actualWidth = actualWidth; 436 svgElement.rotation = rotation; // Apply rotation again. 437 } 438 /* 439 * Un-escape HTML entities based on the public `renderer.escapes` list 440 * 441 * @private 442 * 443 * @param {string} inputStr The string to unescape 444 * @param {Array<string>} [except] Exceptions 445 * 446 * @return {string} The processed string 447 */ 448 unescapeEntities(inputStr, except) { 449 objectEach(this.renderer.escapes, function (value, key) { 450 if (!except || except.indexOf(value) === -1) { 451 inputStr = inputStr.toString().replace(new RegExp(value, 'g'), key); 452 } 453 }); 454 return inputStr; 455 } 456} 457export default TextBuilder;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.