PageSourceSearch

https://symfony.com/assets/code-IwnH37E.js

js symfony.com collected 2026-09-24 06:40:05 UTC 19,233 bytes, 463 lines download raw bytes

1import './styles/color-palette.css';
2import './styles/_webfont-jetbrains-mono.css';
3import './styles/code.css';
4import './styles/component-terminal.css';
5
6window.addEventListener('DOMContentLoaded', () => {
7    new Code();
8});
9
10class Code {
11    #clipboardIcon;
12    #diffCache = new WeakMap();
13
14    constructor() {
15        this.#clipboardIcon = `
16            <svg xmlns="http://www.w3.org/2000/svg" width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
17                <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
18                <path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" />
19                <path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" />
20            </svg>
21        `;
22
23        // initialize only visible code blocks on page load
24        this.#initializeConfigurationBlocks();
25        this.#initializeDiffBlocks();
26        this.#setupCopyCodeButtonsEventDelegation();
27        this.#initializeVisibleCodeBlocks();
28
29        // set up intersection observer for lazy loading
30        this.#setupLazyLoading();
31    }
32
33    #setupLazyLoading() {
34        // create an intersection observer to detect when elements enter the viewport
35        this.observer = new IntersectionObserver(
36            (entries) => {
37                entries.forEach((entry) => {
38                    if (entry.isIntersecting) {
39                        const element = entry.target;
40
41                        // initialize copy button if needed
42                        if (element.classList.contains('codeblock') && !element.dataset.initialized) {
43                            this.#initializeCopyCodeButton(element);
44                            element.dataset.initialized = 'true';
45                        }
46
47                        // stop observing once initialized
48                        this.observer.unobserve(element);
49                    }
50                });
51            },
52            {
53                rootMargin: '100px', // start loading slightly before elements enter viewport
54                threshold: 0.1,
55            },
56        );
57
58        // observe all uninitiated code blocks
59        document.querySelectorAll('.codeblock:not([data-initialized])').forEach((element) => {
60            this.observer.observe(element);
61        });
62    }
63
64    #initializeVisibleCodeBlocks() {
65        // initialize only the code blocks that are currently visible in the viewport
66        const viewportHeight = window.innerHeight;
67
68        document.querySelectorAll('.codeblock').forEach((codeBlock) => {
69            const rect = codeBlock.getBoundingClientRect();
70            // check if the element is in or near the viewport
71            if (rect.top < viewportHeight + 200) {
72                this.#initializeCopyCodeButton(codeBlock);
73                codeBlock.dataset.initialized = 'true';
74            }
75        });
76    }
77
78    #initializeConfigurationBlocks() {
79        this.#updateSelectedLanguageInConfigurationBlocks();
80
81        this.#getConfigurationBlockElements().forEach((configBlock) => {
82            if (null !== configBlock.querySelector('.configuration-tabs button[role="tab"]')) {
83                this.#initializeAccessibleTabs(configBlock);
84            } else {
85                this.#initializeInaccessibleTabs(configBlock);
86            }
87        });
88    }
89
90    #initializeAccessibleTabs(configBlock) {
91        configBlock.querySelectorAll('.configuration-tabs button[role="tab"]').forEach((tabControl) 
91=> {
92            tabControl.addEventListener('click', (event) => {
93                // save the selected format to display it by default from now on
94                localStorage.setItem('selectedFormat', tabControl.getAttribute('data-language'));
95
96                this.#updateSelectedLanguageInConfigurationBlocks();
97                event.target.closest('.configuration-block').scrollIntoView(true);
98            });
99        });
100    }
101
102    #initializeInaccessibleTabs(configBlock) {
103        configBlock.querySelectorAll('.configuration-tabs li').forEach((tabControl) => {
104            tabControl.addEventListener('click', (event) => {
105                // save the selected format to display it by default from now on
106                localStorage.setItem('selectedFormat', tabControl.getAttribute('data-language'));
107
108                this.#updateSelectedLanguageInConfigurationBlocks();
109                event.target.closest('.configuration-block').scrollIntoView(true);
110            });
111        });
112    }
113
114    #initializeDiffBlocks() {
115        document.querySelectorAll('.codeblock-diff').forEach((codeBlockElement) => {
116            if (this.#diffCache.has(codeBlockElement)) {
117                return;
118            }
119
120            const codeElement = codeBlockElement.querySelector('.codeblock-code code');
121            const linesElement = codeBlockElement.querySelector('.codeblock-lines');
122            if (null === codeElement || null === linesElement) {
123                return;
124            }
125
126            const diffLines = this.#parseDiffLines(codeElement);
127            const hasChangedLines = diffLines.some((line) => 'context' !== line.type);
128            // blocks with hunk/file headers ('@@ ', '--- a/...') are real patches, whose
129            // before/after contents can't be derived reliably from their lines
130            const hasPatchHeaders = diffLines.some((line) => /^(@@|\+\+\+ |--- )/.test(line.text));
131            if (!hasChangedLines || hasPatchHeaders) {
132                return;
133            }
134
135            this.#diffCache.set(codeBlockElement, {
136                diffLines: diffLines,
137                markerWidth: this.#getDiffMarkerWidth(diffLines),
138                originalCodeHtml: codeElement.innerHTML,
139                originalLineNumbers: linesElement.textContent,
140                copyButtonElement: this.#createDiffCopyButton(codeBlockElement),
141            });
142
143            codeBlockElement.before(this.#createDiffTabs(codeBlockElement));
144        });
145    }
146
147    #parseDiffLines(codeElement) {
148        const diffLines = [];
149        let currentLine = null;
150
151        codeElement.childNodes.forEach((node) => {
152            if (Node.TEXT_NODE === node.nodeType) {
153                node.textContent.split('\n').forEach((segment, index) => {
154                    if (index > 0) {
155                        diffLines.push(currentLine ?? { type: 'context', text: '' });
156                        currentLine = null;
157                    }
158                    if ('' !== segment) {
159                        currentLine = currentLine ?? { type: 'context', text: '' };
160                        currentLine.text += segment;
161                    }
162                });
163
164                return;
165            }
166
167            currentLine = currentLine ?? { type: 'context', text: '' };
168            if (node.classList?.contains('hljs-addition')) {
169                currentLine.type = 'addition';
170            } else if (node.classList?.contains('hljs-deletion')) {
171                currentLine.type = 'deletion';
172            }
173            currentLine.text += node.textContent;
174        });
175
176        if (null !== currentLine) {
177            diffLines.push(currentLine);
178        }
179
180        return diffLines;
181    }
182
183    // docs use two diff conventions: a 1-char marker ('+added') or a 2-char
184    // marker ('+ added'); the convention of each block is detected by looking
185    // at its added/removed lines, because context lines are ambiguous
186    #getDiffMarkerWidth(diffLines) {
187        const changedLines = diffLines.filter((line) => 'context' !== line.type);
188
189        return changedLines.every((line) => /^[-+]( |$)/.test(line.text)) ? 2 : 1;
190    }
191
192    #removeDiffLineMarker(text, markerWidth) {
193        if (!/^[-+ ]/.test(text)) {
194            return text;
195        }
196
197        const cleanedText = text.substring(1);
198
199        return 2 === markerWidth && cleanedText.startsWith(' ') ? cleanedText.substring(1) : cleanedText;
200    }
201
202    #createDiffTabs(codeBlockElement) {
203        const tabsElement = document.createElement('div');
204        tabsElement.classList.add('diff-tabs');
205        tabsElement.setAttribute('role', 'tablist');
206        tabsElement.setAttribute('aria-label', 'Code diff view');
207
208        Object.entries({ diff: 'Diff', before: 'Before', after: 'After' }
208).forEach(([view, label]) => {
209            const tabElement = document.createElement('button');
210            tabElement.type = 'button';
211            tabElement.textContent = label;
212            tabElement.setAttribute('role', 'tab');
213            tabElement.setAttribute('data-diff-view', view);
214            tabElement.setAttribute('aria-selected', 'diff' === view ? 'true' : 'false');
215            if ('diff' === view) {
216                tabElement.setAttribute('data-active', 'true');
217            } else {
218                tabElement.setAttribute('tabindex', '-1');
219            }
220            tabElement.addEventListener('click', () => this.#selectDiffTab(codeBlockElement, tabElement));
221            tabsElement.appendChild(tabElement);
222        });
223
224        tabsElement.addEventListener('keydown', (event) => {
225            const tabElements = [...tabsElement.querySelectorAll('button[role="tab"]')];
226            const currentIndex = tabElements.indexOf(document.activeElement);
227            if (-1 === currentIndex) {
228                return;
229            }
230
231            let newIndex = null;
232            if ('ArrowRight' === event.key) {
233                newIndex = (currentIndex + 1) % tabElements.length;
234            } else if ('ArrowLeft' === event.key) {
235                newIndex = (currentIndex - 1 + tabElements.length) % tabElements.length;
236            } else if ('Home' === event.key) {
237                newIndex = 0;
238            } else if ('End' === event.key) {
239                newIndex = tabElements.length - 1;
240            }
241
242            if (null === newIndex) {
243                return;
244            }
245
246            event.preventDefault();
247            tabElements[newIndex].focus();
248            this.#selectDiffTab(codeBlockElement, tabElements[newIndex]);
249        });
250
251        return tabsElement;
252    }
253
254    #selectDiffTab(codeBlockElement, selectedTabElement) {
255        const tabElements = selectedTabElement.closest('.diff-tabs').querySelectorAll('button[role="tab"]');
256        tabElements.forEach((tabElement) => {
257            if (tabElement === selectedTabElement) {
258                tabElement.setAttribute('data-active', 'true');
259                tabElement.setAttribute('aria-selected', 'true');
260                tabElement.removeAttribute('tabindex');
261            } else {
262                tabElement.removeAttribute('data-active');
263                tabElement.setAttribute('aria-selected', 'false');
264                tabElement.setAttribute('tabindex', '-1');
265            }
266        });
267
268        this.#renderDiffView(codeBlockElement, selectedTabElement.getAttribute('data-diff-view'));
269    }
270
271    #renderDiffView(codeBlockElement, view) {
272        const cache = this.#diffCache.get(codeBlockElement);
273        const codeElement = codeBlockElement.querySelector('.codeblock-code code');
274        const linesElement = codeBlockElement.querySelector('.codeblock-lines');
275
276        if ('diff' === view) {
277            codeElement.innerHTML = cache.originalCodeHtml;
278            linesElement.textContent = cache.originalLineNumbers;
279        } else {
280            const hiddenType = 'before' === view ? 'addition' : 'deletion';
281            const visibleLines = cache.diffLines.filter((line) => hiddenType !== line.type);
282            codeElement.textContent = visibleLines
283                .map((line) => this.#removeDiffLineMarker(line.text, cache.markerWidth))
284                .join('\n');
285            linesElement.textContent = visibleLines.map((_line, index) => index + 1).join('\n');
286        }
287
288        cache.copyButtonElement.hidden = 'diff' === view;
289    }
290
291    #createDiffCopyButton(codeBlockElement) {
292        const copyButtonElement = document.createElement('button');
293        copyButtonElement.innerHTML = this.#clipboardIcon;
294        copyButtonElement.classList.add('btn-codeblock-copy-code');
295        copyButtonElement.hidden = true;
296        codeBlockElement.prepend(copyButtonElement);
297
298        return copyButtonElement;
299    }
300
301    #setupCopyCodeButtonsEventDelegation() {
302        // use event delegation for copy buttons
303        document.addEventListener('click', (event) => {
304            const copyButton = event.target.closest('.btn-codeblock-copy-code');
305            if (null === copyButton) {
306                return;
307            }
308
309            const codeBlockElement = copyButton.closest('.codeblock');
310            if (!codeBlockElement) return;
311
312            const codeElement = codeBlockElement.querySelector('.codeblock-code');
313            if (!codeElement) return;
314
315            let textToCopy = codeElement.textContent;
316
317            const isTerminalCodeBlock = codeBlockElement.matches('.codeblock-terminal, .codeblock-bash');
318            if (isTerminalCodeBlock) {
319                const clone = codeElement.cloneNode(true);
320                clone.querySelectorAll('span.hljs-prompt').forEach((el) => el.remove());
321                textToCopy = clone.textContent;
322            }
323
324            this.#copyTextToClipboard(textToCopy.trim());
325
326            // change the button icon to give feedback
327            const originalCopyButtonInnerHTML = copyButton.innerHTML;
328            copyButton.classList.add('btn-codeblock-copy-code-active');
329            copyButton.innerHTML = `${this.#clipboardIcon} Copied!`;
330
331            setTimeout(() => {
332                copyButton.innerHTML = originalCopyButtonInnerHTML;
333                copyButton.classList.remove('btn-codeblock-copy-code-active');
334            }, 1500);
335
336            event.preventDefault();
337        });
338    }
339
340    #initializeCopyCodeButton(codeBlockElement) {
341        if (
342            // skip buttons that have already been initialized
343            codeBlockElement.querySelector('.btn-codeblock-copy-code') ||
344            // skip code blocks that already include a copy button (e.g. the Terminal component)
345            codeBlockElement.querySelector('.ui-button-copy') ||
346            // skip code blocks related to diff changes
347            codeBlockElement.classList.contains('codeblock-diff')
348        ) {
349            return;
350        }
351
352        // enable the copy button only for terminal related blocks that only contain a single line;
353        // otherwise, the code block contains output or multiple commands, so it's messy to copy
354        const isTerminalCodeBlock = codeBlockElement.matches('.codeblock-terminal, .codeblock-bash');
355        if (
356            isTerminalCodeBlock &&
357            codeBlockElement.querySelector('.codeblock-code').textContent.split('\n').length > 1
358        ) {
359            return;
360        }
361
362        const copyButtonElement = document.createElement('button');
363        copyButtonElement.innerHTML = this.#clipboardIcon;
364        if (isTerminalCodeBlock) {
365            copyButtonElement.innerHTML = `${this.#clipboardIcon} Copy`;
366        }
367        copyButtonElement.classList.add('btn-codeblock-copy-code');
368        codeBlockElement.prepend(copyButtonElement);
369    }
370
371    #getConfigurationBlockElements() {
372        return document.querySelectorAll('div.configuration-block');
373    }
374
375    #updateSelectedLanguageInConfigurationBlocks() {
376        const selectedLanguage = localStorage.getItem('selectedFormat');
377
378        this.#getConfigurationBlockElements().forEach((configBlock) => {
379            const selectedLanguageIsInConfigurationBlock =
380                null !== configBlock.querySelector(`[data-language="${selectedLanguage}"]`);
381            if (!selectedLanguageIsInConfigurationBlock) {
382                return;
383            }
384
385            if (null !== configBlock.querySelector('.configuration-tabs button[role="tab"]')) {
386                this.#handleAccessibleTabs(configBlock, selectedLanguage);
387            } else {
388                this.#handleInaccessibleTabs(configBlock, selectedLanguage);
389            }
390        });
391    }
392
393    #handleAccessibleTabs(configBlock, selectedLanguage) {
394        // unselect all tabs of the config block...
395        configBlock.querySelectorAll('.configuration-tabs button[role="tab"]').forEach((tabControl) 
395=> {
396            tabControl.removeAttribute('data-active');
397            tabControl.removeAttribute('aria-selected');
398            tabControl.setAttribute('tabindex', '-1');
399        });
400        configBlock.querySelectorAll('.configuration-codeblock').forEach((codeBlock) => {
401            codeBlock.style.display = 'none';
402        });
403
404        // and then select only the one that corresponds to the selected language
405        const selectedTabControl = configBlock.querySelector(
406            `.configuration-tabs button[role="tab"][data-language="${selectedLanguage}"]`,
407        );
408        selectedTabControl.setAttribute('data-active', 'true');
409        selectedTabControl.setAttribute('aria-selected', 'true');
410        selectedTabControl.removeAttribute('tabindex');
411        configBlock.querySelector(`.configuration-codeblock[data-language="${selectedLanguage}"]`).style.display =
412            'block';
413    }
414
415    #handleInaccessibleTabs(configBlock, selectedLanguage) {
416        // unselect all tabs of the config block...
417        configBlock.querySelectorAll('.configuration-tabs li').forEach((tabControl) 
417=> {
418            tabControl.removeAttribute('data-active');
419        });
420        configBlock.querySelectorAll('.configuration-codeblock').forEach((codeBlock) => {
421            codeBlock.style.display = 'none';
422        });
423
424        // and then select only the one that corresponds to the selected language
425        configBlock
426            .querySelector(`.configuration-tabs li[data-language="${selectedLanguage}"]`)
427            .setAttribute('data-active', 'true');
428        configBlock.querySelector(`.configuration-codeblock[data-language="${selectedLanguage}"]`).style.display =
429            'block';
430    }
431
432    #copyTextToClipboard(textContent) {
433        // try to use the modern Clipboard API first
434        if (navigator.clipboard?.writeText) {
435            navigator.clipboard.writeText(textContent).catch(() => {
436                // fall back to the older method if Clipboard API fails
437                this.#fallbackCopyTextToClipboard(textContent);
438            });
439            return;
440        }
441
442        // fall back to the older method if Clipboard API is not available
443        this.#fallbackCopyTextToClipboard(textContent);
444    }
445
446    #fallbackCopyTextToClipboard(textContent) {
447        // create a single hidden element for copying text
448        const hiddenCopyElement = document.createElement('textarea');
449
450        // set non-editable to avoid focus and move outside of view
451        hiddenCopyElement.setAttribute('readonly', '');
452        hiddenCopyElement.style.position = 'absolute';
453        hiddenCopyElement.style.left = '-9999px';
454        hiddenCopyElement.value = textContent;
455
456        document.body.appendChild(hiddenCopyElement);
457
458        hiddenCopyElement.select();
459        document.execCommand('copy');
460
461        document.body.removeChild(hiddenCopyElement);
462    }
463}

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.