1 2(function() { 3 // Configuration 4 const CONFIG = { 5 attributePrefix: 'data-component', 6 includeContentAttribute: true, 7 maxContentLength: 1000, 8 includeLegacyAttributes: true, 9 includeElements: [], 10 excludeElements: [], 11 }; 12 13 const TAG_SHOULD_EXCLUDE = [ 14 "base", 15 "object", 16 "link", 17 "meta", 18 "noscript", 19 "script", 20 "style", 21 "title", 22 "animate", 23 "animateMotion", 24 "animateTransform", 25 "circle", 26 "clipPath", 27 "defs", 28 "desc", 29 "ellipse", 30 "feBlend", 31 "feColorMatrix", 32 "feComponentTransfer", 33 "feComposite", 34 "feConvolveMatrix", 35 "feDiffuseLighting", 36 "feDisplacementMap", 37 "feDistantLight", 38 "feDropShadow", 39 "feFlood", 40 "feFuncA", 41 "feFuncB", 42 "feFuncG", 43 "feFuncR", 44 "feGaussianBlur", 45 "feImage", 46 "feMerge", 47 "feMergeNode", 48 "feMorphology", 49 "feOffset", 50 "fePointLight", 51 "feSpecularLighting", 52 "feSpotLight", 53 "feTile", 54 "feTurbulence", 55 "filter", 56 "foreignObject", 57 "g", 58 "image", 59 "line", 60 "linearGradient", 61 "marker", 62 "mask", 63 "metadata", 64 "mpath", 65 "path", 66 "pattern", 67 "polygon", 68 "polyline", 69 "radialGradient", 70 "rect", 71 "set", 72 "stop", 73 "switch", 74 "symbol", 75 "text", 76 "textPath", 77 "tspan", 78 "use", 79 "view", 80 "body", 81 "param", 82] 83 84 // Map to track per-class indices for unique component IDs 85 const classIndexMap = new Map(); 86 87 /** 88 * Sanitizes attribute values to prevent XSS 89 */ 90 function sanitizeAttributeValue(value) { 91 return String(value) 92 .replace(/&/g, '&') 93 .replace(/"/g, '"') 94 .replace(/'/g, ''') 95 .replace(/</g, '<') 96 .replace(/>/g, '>'); 97 } 98 99 /** 100 * Creates a component ID based on class name and index 101 */ 102 function createComponentId(el) { 103 104 const path = []; 105 let current = el; 106 107 while (current && current.parentElement) { 108 const siblings = Array.from(current.parentElement.children) 109 .filter(child => child.tagName); 110 const index = siblings.indexOf(current); 111 path.unshift(index); 112 current = current.parentElement; 113 114 if (current.tagName === 'BODY') break; 115 } 116 117 const line = path.join('-') 118 119 return sanitizeAttributeValue(line); 120 } 121 122 /** 123 * Extracts text content from an element 124 */ 125 function extractTextContent(element) { 126 let text = ''; 127 for (const node of element.childNodes) { 128 if (node.nodeType === Node.TEXT_NODE) { 129 text += node.textContent.trim() + ' '; 130 } 131 } 132 return text.trim(); 133 } 134 135 /** 136 * Extracts attributes from an element 137 */ 138 function extractAttributes(element) { 139 const attributes = {}; 140 const defaultAttrs = [ 141 'class', 'id', 'src', 'alt', 'href', 'type', 'name', 'value' 142 ]; 143 144 // Extract default attributes 145 for (const attr of defaultAttrs) { 146 if (element.hasAttribute(attr)) { 147 if (attr === 'class') { 148 attributes['className'] = element.getAttribute(attr); 149 } else { 150 attributes[attr] = element.getAttribute(attr); 151 } 152 } 153 } 154 155 const textContent = extractTextContent(element); 156 if (textContent) { 157 attributes['textContent'] = textContent; 158 } 159 160 return attributes; 161 } 162 163 /** 164 * Determines if an element should be tagged 165 */ 166 function shouldTagElement(element) { 167 // Skip non-element nodes 168 if (element.nodeType !== Node.ELEMENT_NODE) { 169 return false; 170 } 171 172 const tagName = element.tagName.toLowerCase(); 173 174 if (CONFIG.includeElements.length > 0) { 175 return CONFIG.includeElements.includes(tagName); 176 } 177 178 if (CONFIG.excludeElements.includes(tagName) || TAG_SHOULD_EXCLUDE.includes(tagName)) { 179 return false; 180 } 181 182 return true; 183 } 184 185 /** 186 * Tags an element with component metadata 187 */ 188 function tagElement(element) { 189 // Skip if already tagged 190 if (element.hasAttribute(`${CONFIG.attributePrefix}-id`)) { 191 return; 192 } 193 194 // Get element name 195 const elementName = element.tagName.toLowerCase(); 196 197 // Create component ID 198 const componentId = createComponentId(element); 199 200 // Set ID attribute 201 element.setAttribute(`${CONFIG.attributePrefix}-id`, componentId); 202 203 // Add legacy attributes if enabled 204 if (CONFIG.includeLegacyAttributes) { 205 element.setAttribute(`${CONFIG.attributePrefix}-path`, window.location.pathname); 206 element.setAttribute(`${CONFIG.attributePrefix}-name`, elementName); 207 element.setAttribute(`${CONFIG.attributePrefix}-file`, window.location.pathname.split('/').pop()); 208 } 209 210 // Add content attribute if enabled 211 if (CONFIG.includeContentAttribute) { 212 const attributes = extractAttributes(element); 213 const content = { 214 elementName, 215 ...attributes 216 }; 217 218 let encodedContent = encodeURIComponent(JSON.stringify(content)); 219 220 // Truncate if needed 221 if (encodedContent.length > CONFIG.maxContentLength) { 222 encodedContent = encodedContent.substring(0, CONFIG.maxContentLength) + '...'; 223 } 224 225 element.setAttribute(`${CONFIG.attributePrefix}-content`, encodedContent); 226 } 227 } 228 229 /** 230 * Process all elements in the document 231 */ 232 function processAllElements() { 233 const elements = document.querySelectorAll('*'); 234 let taggedCount = 0; 235
236 elements.forEach((element, index) => { 237 if (shouldTagElement(element)) { 238 tagElement(element, index); 239 taggedCount++; 240 } 241 }); 242 243 } 244 245 /** 246 * Initialize the component tagger 247 */ 248 function initialize() { 249 // Wait for DOM to be fully loaded 250 if (document.readyState === 'loading') { 251 document.addEventListener('DOMContentLoaded', processAllElements); 252 } else { 253 processAllElements(); 254 } 255 256 // Also process on dynamic content changes 257 const observer = new MutationObserver((mutations) => { 258 259 for (const mutation of mutations) { 260 if (mutation.type === 'childList') { 261 for (const node of mutation.addedNodes) { 262 if (node.nodeType === Node.ELEMENT_NODE) { 263 if (shouldTagElement(node)) { 264 tagElement(node); 265 } 266 267 // Process child elements 268 const childElements = node.querySelectorAll('*'); 269 for (const child of childElements) { 270 if (shouldTagElement(child)) { 271 tagElement(child); 272 } 273 } 274 } 275 } 276 } 277 } 278 }); 279 280 observer.observe(document.body, { 281 childList: true, 282 subtree: true 283 }); 284 } 285 286 // Start the component tagger 287 initialize(); 288})();
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.