PageSourceSearch

https://www.visiterouen.com/app/dist/rouen/addons/woody-library/st…/Core/Renderer/SVG/TextBuilder.js

js visiterouen.com collected 2026-10-01 13:05:53 UTC 19,407 bytes, 457 lines download raw bytes

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 || '', ['&lt;', '&gt;']) // #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.