1const PhoneNumbers = new (function(){ 2 3 // How many levels up the DOM two phone numbers may share an ancestor and 4 // still count as adjacent. 5 const PROXIMITY_LEVELS = 3; 6 7 // Shortest number we treat as a phone number on display. Anything shorter is 8 // an icon only call link or stray digits in the surrounding copy. 9 const MIN_DIGITS = 7; 10 11 function removeElement(el) { 12 let parent = el.parentNode; 13 14 if (parent === null) { 15 return; 16 } 17 18 parent.removeChild(el); 19 20 // Climb while the removal left an empty container behind, stopping at body 21 // and at any node already detached from the document. Removing a container 22 // can strand numbers still queued for removal, and those have a parent that 23 // no longer has one of its own. 24 while (parent !== document.body && parent.parentNode !== null && parent.innerHTML.trim() === '') { 25 const child = parent; 26 27 parent = child.parentNode; 28 parent.removeChild(child); 29 } 30 } 31 32 function removeEmptyElement(el) { 33 if (el.innerHTML === '' && telDigits(el) === '') { 34 removeElement(el); 35 } 36 } 37 38 function normalizeDigits(value) { 39 const digits = (value || '').replace(/\D/g, ''); 40 41 // 1.800.344.6118, +18003446118 and 800.344.6118 are the same number. 42 return digits.length === 11 && digits.startsWith('1') ? digits.slice(1) : digits; 43 } 44 45 function phoneDigits(el) { 46 return normalizeDigits(el.textContent); 47 } 48 49 function telDigits(el) { 50 const href = el.getAttribute('href') || ''; 51 52 return href.startsWith('tel:') ? normalizeDigits(href) : ''; 53 } 54 55 // Two numbers that read alike but dial differently are not duplicates. 56 function telConflicts(a, b) { 57 const aTel = telDigits(a), bTel = telDigits(b); 58 59 return aTel !== '' && bTel !== '' && aTel !== bTel; 60 } 61 62 function ancestorChain(el, levels) { 63 const chain = []; 64 65 for (let node = el; node && chain.length <= levels; node = node.parentElement) { 66 chain.push(node); 67 } 68 69 return chain; 70 } 71 72 // The closest node both elements sit under, or null when they are too far apart. 73 function sharedAncestor(a, b, levels) { 74 const chain = ancestorChain(a, levels); 75 76 return ancestorChain(b, levels).find(node => chain.includes(node)) || null; 77 } 78 79 function isHiddenNode(el, cache) { 80 if (!cache.has(el)) { 81 const style = window.getComputedStyle(el); 82 83 cache.set(el, style.display === 'none' || style.visibility === 'hidden'); 84 } 85 86 return cache.get(el); 87 } 88 89 // Is the number hidden by something below the ancestor it shares with its 90 // match? Hiding is only measured up to the shared ancestor so numbers hidden 91 // together, such as a pair inside a closed dropdown, still count as on 92 // display. One hidden on its own is not, so a breakpoint showing only one of 93 // the two is left intact. 94 function isHiddenWithin(el, root, cache) { 95 for (let node = el; node && node !== root; node = node.parentElement) { 96 if (isHiddenNode(node, cache)) { 97 return true; 98 } 99 } 100 101 return false; 102 } 103 104 function removeDuplicates(selector, levels) { 105 const kept = [], hidden = new Map(); 106 107 document.querySelectorAll(selector).forEach(el => { 108 // This list was taken before the first removal, and removing a container 109 // takes everything inside it, so entries still to come may already be off 110 // the page. A detached node has no computed style, which reads as on 111 // display, and it would then stand in for a number that is not there. 112 if (!document.contains(el)) { 113 return; 114 } 115 116 const digits = phoneDigits(el); 117 118 // No number on display to repeat, e.g. an icon only call link. 119 if (digits.length < MIN_DIGITS) { 120 return; 121 } 122 123 const duplicate = other => { 124 if (other.digits !== digits || telConflicts(other.el, el)) { 125 return false;
126 } 127 128 const root = sharedAncestor(other.el, el, levels); 129 130 return root !== null 131 && !isHiddenWithin(el, root, hidden) 132 && !isHiddenWithin(other.el, root, hidden); 133 }; 134 135 if (kept.some(duplicate)) { 136 removeElement(el); 137 return; 138 } 139 140 kept.push({el, digits}); 141 }); 142 } 143 144 // The node containing the number on display. A call link wraps anything from a bare 145 // number to a staff photo and a paragraph of copy, so the number is only written 146 // into the element marked for replacement, or into the link itself when the link holds 147 // nothing else. A link with other markup and no marked element is left alone 148 // rather than flattened down to the number. 149 function displaySlot(el) { 150 const marked = el.querySelector('.al-call-main, .al-call-local'); 151 152 if (marked !== null) { 153 return marked; 154 } 155 156 return el.children.length === 0 ? el : null; 157 } 158 159 function updateElement(el, phone) { 160 if (el.hasAttribute('href')) { 161 el.href = phone.href; 162 } 163 164 const slot = displaySlot(el); 165 166 // No number on display to replace, e.g. an icon only call link. 167 if (slot === null || phoneDigits(slot).length < MIN_DIGITS) { 168 return; 169 } 170 171 slot.innerHTML = phone.html; 172 } 173 174 function update(phones) { 175 document.querySelectorAll('.al-call') 176 .forEach(el => { 177 const phone = el.classList.contains('al-call-local') ? phones.local : phones.main; 178 179 // A number the site has no value for is left out of the payload rather 180 // than sent empty, so there is nothing to write and whatever the 181 // server rendered stands. 182 if (!phone) { 183 return; 184 } 185 186 updateElement(el, phone); 187 }); 188 189 document.querySelectorAll('.al-call').forEach(removeEmptyElement); 190 191 removeDuplicates('.al-call', PROXIMITY_LEVELS); 192 } 193 194 /** 195 * write the numbers returned by dynamic content into the page 196 * 197 * elements left with nothing to display are dropped, as are numbers the update 198 * leaves repeated close to one another. 199 */ 200 this.update = function(phones) { 201 if (!phones) { 202 return; 203 } 204 205 update(phones); 206 }; 207 208 /** 209 * remove phone numbers repeated close to one another on the page 210 * 211 * the main and local numbers can resolve to the same value, leaving that 212 * number rendered twice within a single block. the first of each repeated 213 * number is kept and the rest are removed, along with any container they empty. 214 * 215 * a repeat must match on both the number on display and the tel: href, so a 216 * link that reads the same but dials a different number is left alone. 217 */ 218 this.removeDuplicates = function(selector, levels) { 219 removeDuplicates(selector || '.al-call', levels || PROXIMITY_LEVELS); 220 }; 221 222})();
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.