1(function () { 2 function getHighlightText() { 3 const params = new URLSearchParams(window.location.search); 4 const value = params.get('highlight'); 5 6 if (!value) { 7 return ''; 8 } 9 10 // URLSearchParams Ñже декодиÑÑÐµÑ Ð·Ð½Ð°Ñение, но оÑÑавлÑем fallback на ÑлÑÑай двойного кодиÑованиÑ. 11 try { 12 return decodeURIComponent(value).trim(); 13 } catch (error) { 14 return value.trim(); 15 } 16 } 17 18 function escapeRegExp(value) { 19 return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 20 } 21 22 function createTermPattern(term) { 23 const chars = Array.from(term); 24 const minPrefixLength = Math.min(chars.length, 4); 25 const prefixes = []; 26 27 for (let length = chars.length; length >= minPrefixLength; length -= 1) { 28 prefixes.push(`${escapeRegExp(chars.slice(0, length).join(''))}[\\p{L}\\p{M}\\p{N}_'âʼ-]*`); 29 } 30 31 return `(?:${prefixes.join('|')})`; 32 } 33 34 function createHighlightRegex(text) { 35 const normalized = text.trim(); 36 if (!normalized) { 37 return null; 38 } 39 40 const patterns = normalized 41 .split(/\s+/u) 42 .filter(Boolean) 43 .map((term) => createTermPattern(term)); 44 45 const phraseSeparator = '[\\s\\u00a0]+'; 46 const phrasePattern = patterns.join(phraseSeparator); 47 48 return phrasePattern ? new RegExp(phrasePattern, 'giu') : null; 49 } 50 51 function shouldSkipNode(node) { 52 const parent = node.parentElement; 53 if (!parent) { 54 return true; 55 } 56 57 return Boolean(parent.closest('script, style, noscript, textarea, mark[data-query-highlight="1"]')); 58 } 59 60 function collectTextNodes(root) { 61 const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { 62 acceptNode(node) { 63 if (!node.nodeValue || !node.nodeValue.trim()) { 64 return NodeFilter.FILTER_REJECT; 65 } 66 67 if (shouldSkipNode(node)) { 68 return NodeFilter.FILTER_REJECT; 69 } 70 71 return NodeFilter.FILTER_ACCEPT; 72 } 73 }); 74 75 const nodes = []; 76 let currentNode = walker.nextNode(); 77 while (currentNode) { 78 nodes.push(currentNode); 79 currentNode = walker.nextNode(); 80 } 81 82 return nodes; 83 } 84 85 function highlightTextNode(node, regex) { 86 const text = node.nodeValue; 87 if (!text) { 88 return 0; 89 } 90 91 regex.lastIndex = 0; 92 if (!regex.test(text)) { 93 return 0; 94 } 95 96 regex.lastIndex = 0; 97 const fragment = document.createDocumentFragment(); 98 let lastIndex = 0; 99 let count = 0; 100 let match; 101 102 while ((match = regex.exec(text)) !== null) { 103 const matchIndex = match.index; 104 const matchText = match[0]; 105 106 if (matchIndex > lastIndex) { 107 fragment.appendChild(document.createTextNode(text.slice(lastIndex, matchIndex))); 108 } 109 110 const mark = document.createElement('mark'); 111 mark.dataset.queryHighlight = '1'; 112 mark.style.backgroundColor = '#ffeb3b'; 113 mark.style.padding = '0'; 114 mark.textContent = matchText; 115 fragment.appendChild(mark); 116 117 lastIndex = matchIndex + matchText.length; 118 count += 1; 119 120 if (matchText.length === 0) { 121 regex.lastIndex += 1;
122 } 123 } 124 125 if (lastIndex < text.length) { 126 fragment.appendChild(document.createTextNode(text.slice(lastIndex))); 127 } 128 129 node.parentNode.replaceChild(fragment, node); 130 return count; 131 } 132 133 function highlightMatchingContainers() { 134 const marks = document.querySelectorAll('mark[data-query-highlight="1"]'); 135 const containers = new Set(); 136 const containerSelector = 'p, li, div, td, th, section, article, blockquote, h1, h2, h3, h4, h5, h6, tr'; 137 138 for (const mark of marks) { 139 const container = mark.closest(containerSelector); 140 if (container) { 141 containers.add(container); 142 } 143 } 144 145 for (const container of containers) { 146 if (container.dataset.queryHighlightContainer === '1') { 147 continue; 148 } 149 150 container.dataset.queryHighlightContainer = '1'; 151 container.style.backgroundColor = '#fffde7'; 152 } 153 } 154 155 function isCollapseExpanded(element) { 156 return element.classList.contains('in') || element.classList.contains('show'); 157 } 158 159 function getParentCollapses(element) { 160 const collapses = []; 161 let parent = element.parentElement; 162 163 while (parent) { 164 if (parent.classList && parent.classList.contains('collapse')) { 165 collapses.unshift(parent); 166 } 167 168 parent = parent.parentElement; 169 } 170 171 return collapses; 172 } 173 174 function isCollapseTriggerForElement(trigger, target) { 175 const href = trigger.getAttribute('href'); 176 const dataTarget = trigger.getAttribute('data-target'); 177 178 return href === target || dataTarget === target || Boolean(href && href.endsWith(target)); 179 } 180 181 function setCollapseTriggerExpanded(element) { 182 if (!element.id) { 183 return; 184 } 185 186 const target = `#${element.id}`; 187 const triggers = document.querySelectorAll('[data-toggle="collapse"]'); 188 189 for (const trigger of triggers) { 190 if (!isCollapseTriggerForElement(trigger, target)) { 191 continue; 192 } 193 194 trigger.classList.remove('collapsed'); 195 trigger.setAttribute('aria-expanded', 'true'); 196 197 const title = trigger.closest('.title'); 198 const icon = title ? title.querySelector('.fa-angle-down') : null; 199 200 if (icon) { 201 icon.classList.add('rotate-180'); 202 } 203 } 204 } 205 206 function getJQueryCollapseElement(element) { 207 if (!window.jQuery) { 208 return null; 209 } 210 211 const jqueryElement = window.jQuery(element); 212 if (typeof jqueryElement.collapse !== 'function') { 213 return null; 214 } 215 216 return jqueryElement; 217 } 218 219 function showCollapse(element) { 220 setCollapseTriggerExpanded(element); 221 222 if (isCollapseExpanded(element)) { 223 return Promise.resolve(); 224 } 225 226 return new Promise((resolve) => { 227 let isResolved = false; 228 let timeoutId = null; 229 const jqueryElement = getJQueryCollapseElement(element); 230 231 const resolveOnce = () => { 232 if (isResolved) { 233 return; 234 } 235 236 isResolved = true; 237 238 if (timeoutId) { 239 window.clearTimeout(timeoutId); 240 } 241 242 if (jqueryElement) { 243 jqueryElement.off('shown.bs.collapse', resolveOnce); 244 } 245 246 resolve(); 247 }; 248 249 if (jqueryElement) { 250 jqueryElement.one('shown.bs.collapse', resolveOnce); 251 jqueryElement.collapse('show'); 252 timeoutId = window.setTimeout(resolveOnce, 700); 253 return; 254 } 255 256 element.classList.add('in'); 257 element.classList.add('show'); 258 element.style.display = 'block'; 259 element.style.height = 'auto'; 260 261 resolveOnce(); 262 }); 263 } 264 265 async function openParentCollapses(element) { 266 const collapses = getParentCollapses(element); 267 268 for (const collapse of collapses) { 269 await showCollapse(collapse); 270 } 271 } 272 273 async function runHighlight() { 274 const query = getHighlightText(); 275 const regex = createHighlightRegex(query); 276 277 if (!regex) { 278 return; 279 } 280 281 const contentBlocks = document.querySelectorAll('.page-content-block'); 282 if (!contentBlocks.length) { 283 return; 284 } 285 286 const textNodes = []; 287 for (const block of contentBlocks) { 288 textNodes.push(...collectTextNodes(block)); 289 } 290 291 let highlightedCount = 0; 292 293 for (const node of textNodes) { 294 highlightedCount += highlightTextNode(node, regex); 295 } 296 297 if (highlightedCount > 0) { 298 highlightMatchingContainers(); 299 300 const firstMatch = document.querySelector('mark[data-query-highlight="1"]'); 301 if (firstMatch) { 302 await openParentCollapses(firstMatch); 303 firstMatch.scrollIntoView({behavior: 'smooth', block: 'center'}); 304 } 305 } 306 } 307 308 if (document.readyState === 'loading') { 309 document.addEventListener('DOMContentLoaded', runHighlight); 310 } else { 311 runHighlight(); 312 } 313 })();
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.