1/** 2 * @file 3 * External links js file. 4 */ 5 6((Drupal, drupalSettings) => { 7 Drupal.extlink = Drupal.extlink || {}; 8 9 Drupal.extlink.attach = (context, drupalSettings) => { 10 if (typeof drupalSettings.data === 'undefined' || !drupalSettings.data.hasOwnProperty('extlink')) { 11 return; 12 } 13 14 // Define the method (either 'append' or 'prepend') of placing the 15 // icon, defaults to 'append'. 16 let extIconPlacement = 'append'; 17 if (drupalSettings.data.extlink.extIconPlacement && drupalSettings.data.extlink.extIconPlacement !== '0') { 18 extIconPlacement = drupalSettings.data.extlink.extIconPlacement; 19 } 20 21 // Strip the host name down, removing ports, subdomains, or www. 22 const pattern = /^(([^:]+?\.)*)([^.:]+)((\.[a-z0-9]{1,253})*)(:[0-9]{1,5})?$/; 23 const host = window.location.host.replace(pattern, '$2$3$6'); 24 const subdomain = window.location.host.replace(host, ''); 25 26 // Determine what subdomains are considered internal. 27 let subdomains; 28 if (drupalSettings.data.extlink.extSubdomains) { 29 subdomains = '([^/]*\\.)?'; 30 } else if (subdomain === 'www.' || subdomain === '') { 31 subdomains = '(www\\.)?'; 32 } else { 33 subdomains = subdomain.replace('.', '\\.'); 34 } 35 36 // Whitelisted domains. 37 let whitelistedDomains = false; 38 if (drupalSettings.data.extlink.whitelistedDomains) { 39 whitelistedDomains = []; 40 for (let i = 0; i < drupalSettings.data.extlink.whitelistedDomains.length; i++) { 41 whitelistedDomains.push(new RegExp(`^https?:\\/\\/${drupalSettings.data.extlink.whitelistedDomains[i].replace(/(\r\n|\n|\r)/gm, '')}.*$`, 'i')); 42 } 43 } 44 45 // Build regular expressions that define an internal link. 46 const internalLink = new RegExp(`^https?://([^@]*@)?${subdomains}${host}`, 'i'); 47 48 // Extra internal link matching. 49 let extInclude = false; 50 if (drupalSettings.data.extlink.extInclude) { 51 extInclude = new RegExp(drupalSettings.data.extlink.extInclude.replace(/\\/, '\\'), 'i'); 52 } 53 54 // Extra external link matching. 55 let extExclude = false; 56 if (drupalSettings.data.extlink.extExclude) { 57 extExclude = new RegExp(drupalSettings.data.extlink.extExclude.replace(/\\/, '\\'), 'i'); 58 } 59 60 // Extra external link matching for excluding noreferrer. 61 let extExcludeNoreferrer = false; 62 if (drupalSettings.data.extlink.extExcludeNoreferrer) { 63 extExcludeNoreferrer = new RegExp(drupalSettings.data.extlink.extExcludeNoreferrer.replace(/\\/, '\\'), 'i'); 64 } 65 66 // Extra external link CSS selector exclusion. 67 let extCssExclude = false; 68 if (drupalSettings.data.extlink.extCssExclude) { 69 extCssExclude = drupalSettings.data.extlink.extCssExclude; 70 } 71 72 // Extra external link CSS selector inclusion. 73 let extCssInclude = false; 74 if (drupalSettings.data.extlink.extCssInclude) { 75 extCssInclude = drupalSettings.data.extlink.extCssInclude; 76 } 77 78 // Extra external link CSS selector explicit. 79 let extCssExplicit = false; 80 if (drupalSettings.data.extlink.extCssExplicit) { 81 extCssExplicit = drupalSettings.data.extlink.extCssExplicit; 82 } 83 84 // Find all links which are NOT internal and begin with http as opposed 85 // to ftp://, javascript:, etc. other kinds of links. 86 // When operating on the 'this' variable, the host has been appended to 87 // all links by the browser, even local ones. 88 const externalLinks = []; 89 const mailtoLinks = []; 90 const telLinks = []; 91 const extlinks = context.querySelectorAll('a:not([data-extlink]), area:not([data-extlink])'); 92 extlinks.forEach((el) => { 93 try { 94 let url = ''; 95 if (typeof el.href === 'string') { 96 url = el.href.toLowerCase(); 97 } 98 // Handle SVG links (xlink:href). 99 else if (typeof el.href === 'object') { 100 url = el.href.baseVal; 101 } 102 const isExtCssIncluded = extCssInclude && (el.matches(extCssInclude) || el.closest(extCssInclude)); 103 if ( 104 url.indexOf('http') === 0 && 105 ((!internalLink.test(url) && !(extExclude && extExclude.test(url))) || (extInclude && extInclude.test(url)) || isExtCssIncluded) &&
106 !(extCssExclude && el.matches(extCssExclude)) && 107 !(extCssExclude && el.closest(extCssExclude)) && 108 !(extCssExplicit && !el.closest(extCssExplicit)) 109 ) { 110 let match = false; 111 if (!isExtCssIncluded && whitelistedDomains) { 112 for (let i = 0; i < whitelistedDomains.length; i++) { 113 if (whitelistedDomains[i].test(url)) { 114 match = true; 115 break; 116 } 117 } 118 } 119 if (!match) { 120 externalLinks.push(el); 121 } 122 } 123 // Do not include area tags with begin with mailto: (this prohibits 124 // icons from being added to image-maps). 125 else if (el.tagName !== 'AREA' && !(extCssExclude && el.closest(extCssExclude)) && !(extCssExplicit && !el.closest(extCssExplicit))) { 126 if (url.indexOf('mailto:') === 0) { 127 mailtoLinks.push(el); 128 } else if (url.indexOf('tel:') === 0) { 129 telLinks.push(el); 130 } 131 } 132 } catch (error) { 133 // IE7 throws errors often when dealing with irregular links, such as: 134 // <a href="node/10"></a> Empty tags. 135 // <a href="http://user:[email protected]">example</a> User:pass syntax. 136 return false; 137 } 138 }); 139 140 const hasExtIcon = drupalSettings.data.extlink.extClass !== '0' && drupalSettings.data.extlink.extClass !== ''; 141 const hasAdditionalExtClasses = drupalSettings.data.extlink.extAdditionalLinkClasses !== ''; 142 Drupal.extlink.applyClassAndSpan(externalLinks, 'ext', hasExtIcon ? extIconPlacement : null); 143 if (hasAdditionalExtClasses) { 144 Drupal.extlink.applyClassAndSpan(externalLinks, drupalSettings.data.extlink.extAdditionalLinkClasses, null); 145 } 146 147 const hasMailtoClass = drupalSettings.data.extlink.mailtoClass !== '0' && drupalSettings.data.extlink.mailtoClass !== ''; 148 const hasAdditionalMailtoClasses = drupalSettings.data.extlink.extAdditionalMailtoClasses !== ''; 149 if (hasMailtoClass) { 150 Drupal.extlink.applyClassAndSpan(mailtoLinks, drupalSettings.data.extlink.mailtoClass, extIconPlacement); 151 } 152 if (hasAdditionalMailtoClasses) { 153 Drupal.extlink.applyClassAndSpan(mailtoLinks, drupalSettings.data.extlink.extAdditionalMailtoClasses, null); 154 } 155 156 const hasTelClass = drupalSettings.data.extlink.telClass !== '0' && drupalSettings.data.extlink.telClass !== ''; 157 const hasAdditionalTelClasses = drupalSettings.data.extlink.extAdditionalTelClasses !== '0' && drupalSettings.data.extlink.extAdditionalTelClasses !== ''; 158 if (hasTelClass) { 159 Drupal.extlink.applyClassAndSpan(telLinks, drupalSettings.data.extlink.telClass, extIconPlacement); 160 } 161 if (hasAdditionalTelClasses) { 162 Drupal.extlink.applyClassAndSpan(mailtoLinks, drupalSettings.data.extlink.extAdditionalTelClasses, null); 163 } 164 165 if (drupalSettings.data.extlink.extTarget) { 166 // Add target attr to open link in a new tab if not set. 167 externalLinks.forEach((link, i) => { 168 if (!(drupalSettings.data.extlink.extTargetNoOverride && link.matches('a[target]'))) { 169 externalLinks[i].setAttribute('target', '_blank'); 170 } 171 }); 172 173 // Add noopener rel attribute to combat phishing. 174 externalLinks.forEach((link, i) => { 175 const val = link.getAttribute('rel'); 176 // If no rel attribute is present, create one with the value noopener. 177 if (val === null || typeof val === 'undefined') { 178 externalLinks[i].setAttribute('rel', 'noopener'); 179 return; 180 } 181 // Check to see if rel contains noopener. Add what doesn't exist. 182 if (val.indexOf('noopener') > -1) { 183 if (val.indexOf('noopener') === -1) { 184 // Add noopener. 185 externalLinks[i].setAttribute('rel', `${val} noopener`); 186 } else { 187 // Noopener exists. Nothing needs to be added. 188 } 189 } 190 // Else, append noopener to val. 191 else { 192 // Add noopener. 193 externalLinks[i].setAttribute('rel', `${val} noopener`); 194 } 195 }); 196 } 197 198 if (drupalSettings.data.extlink.extNofollow) { 199 externalLinks.forEach((link, i) => { 200 const val = link.getAttribute('rel'); 201 // When the link does not have a rel attribute set it to 'nofollow'. 202 if (val === null || typeof val === 'undefined') { 203 externalLinks[i].setAttribute('rel', 'nofollow'); 204 return; 205 } 206 let target = 'nofollow'; 207 // Change the target, if not overriding follow. 208 if (drupalSettings.data.extlink.extFollowNoOverride) { 209 target = 'follow'; 210 } 211 if (val.indexOf(target) === -1) { 212 // Add nofollow. 213 externalLinks[i].setAttribute('rel', `${val} nofollow`); 214 } 215 }); 216 } 217 218 if (drupalSettings.data.extlink.extTitleNoOverride === false) { 219 // Set the title attribute of all external links that opens in a new window. 220 externalLinks.forEach((link, i) => { 221 const oldTitle = link.getAttribute('title'); 222 223 // Determine new title based on drupalSettings extTarget configuration. 224 let newTitle = drupalSettings.data.extlink.extTarget ? drupalSettings.data.extlink.extTargetAppendNewWindowLabel : ''; 225 if (oldTitle !== null) { 226 if (Drupal.extlink.hasNewWindowText(oldTitle)) { 227 return; 228 } 229 newTitle = Drupal.extlink.combineLabels(oldTitle, newTitle); 230 } 231 if (newTitle) { 232 externalLinks[i].setAttribute('title', newTitle); 233 } 234 }); 235 } 236 237 if (drupalSettings.data.extlink.extNoreferrer) { 238 externalLinks.forEach((link, i) => { 239 // Don't add 'noreferrer' if exclude noreferrer option is set and the 240 // link matches the entered pattern to exclude 'noreferrer' tag. 241 if (drupalSettings.data.extlink.extExcludeNoreferrer && extExcludeNoreferrer.test(link.getAttribute('href'))) { 242 return; 243 } 244 245 const val = link.getAttribute('rel'); 246 // When the link does not have a rel attribute set it to 'noreferrer'. 247 if (val === null || typeof val === 'undefined') { 248 externalLinks[i].setAttribute('rel', 'noreferrer'); 249 return; 250 } 251 // Add noreferrer. 252 externalLinks[i].setAttribute('rel', `${val} noreferrer`); 253 }); 254 } 255 256 /* eslint:disable:no-empty */ 257 Drupal.extlink = Drupal.extlink || {}; 258 259 // Set up default click function for the external links popup. This should be 260 // overridden by modules wanting to alter the popup. 261 Drupal.extlink.popupClickHandler = 262 Drupal.extlink.popupClickHandler || 263 (() => { 264 if (drupalSettings.data.extlink.extAlert) { 265 // eslint-disable-next-line no-restricted-globals 266 return confirm(drupalSettings.data.extlink.extAlertText); 267 } 268 }); 269 270 const _that = this; 271 Drupal.extlink.handleClick = function (event) { 272 const shouldNavigate = Drupal.extlink.popupClickHandler.call(_that, event); 273 if (typeof shouldNavigate !== 'undefined' && !shouldNavigate) { 274 // Prevent navigation if the user clicks "Cancel". 275 event.preventDefault(); 276 } 277 }; 278 279 externalLinks.forEach((val, i) => { 280 externalLinks[i].removeEventListener('click', Drupal.extlink.handleClick); 281 externalLinks[i].addEventListener('click', Drupal.extlink.handleClick); 282 }); 283 }; 284 285 /** 286 * Check if the label already has 'new window' text. 287 * 288 * 289 * @returns boolean 290 * @param label 291 */ 292 Drupal.extlink.hasNewWindowText = function (label) { 293 return label.toLowerCase().indexOf(Drupal.t('new window')) !== -1; 294 }; 295 296 /** 297 * Combine two labels. 298 * 299 * Combine labels in a readable manner, taking into account if the label 300 * uses parenthesis or not. For examples, 301 * 302 * 1. "A" + "B" => "A, B" 303 * 2. "A" + "(B)" => "A (B)" 304 * 3. "(A)" + "B" => "B (A)" 305 * 4. "(A)" + "(B)" => "(A, B)" 306 * 307 * 308 * @returns string 309 * @param labelA 310 * @param labelB 311 */ 312 Drupal.extlink.combineLabels = function (labelA, labelB) { 313 labelA = labelA || ''; 314 labelB = labelB || ''; 315 const labelANoParens = labelA.trim().replace('(', '').replace(')', ''); 316 const labelBNoParens = labelB.trim().replace('(', '').replace(')', ''); 317 if (labelA === labelANoParens) { 318 if (labelB === labelBNoParens) { 319 // This is Example 1 above: "A" + "B" => "A, B" 320 return `${labelA}, ${labelB}`; 321 } 322 323 // This is Example 2 above: "A" + "(B)" => "A (B)" 324 return `${labelA} ${labelB}`; 325 } 326 327 if (labelB === labelBNoParens) { 328 // This is Example 3 above: "(A)" + "B" => "B (A)" 329 return `${labelB} ${labelA}`; 330 } 331 332 // This is Example 4 above: "(A)" + "(B)" => "(A, B)" 333 return `(${labelANoParens}, ${labelBNoParens})`; 334 }; 335 336 /** 337 * Apply a class and a trailing <span> to all links not containing images. 338 * 339 * @param {object[]} links 340 * An array of DOM elements representing the links. 341 * @param {string} className 342 * The class to apply to the links. 343 * @param {string} iconPlacement 344 * 'append' or 'prepend' the icon to the link. 345 */ 346 Drupal.extlink.applyClassAndSpan = (links, className, iconPlacement) => { 347 let linksToProcess; 348 if (drupalSettings.data.extlink.extImgClass) { 349 linksToProcess = links; 350 } else { 351 // Only text links. 352 linksToProcess = links.filter((link) => { 353 return link.querySelector('img, svg') === null; 354 }); 355 } 356 357 for (let i = 0; i < linksToProcess.length; i++) { 358 if (className !== '0') { 359 linksToProcess[i].classList.add(className); 360 } 361 362 // Additional classes: 363 if (className === drupalSettings.data.extlink.mailtoClass && drupalSettings.data.extlink.extAdditionalMailtoClasse
363s) { 364 // Is mail link: 365 linksToProcess[i].classList.add(drupalSettings.data.extlink.extAdditionalMailtoClasses); 366 } else if (className === drupalSettings.data.extlink.telClass && drupalSettings.data.extlink.extAdditionalTelClasses) { 367 // Is regular external link: 368 linksToProcess[i].classList.add(drupalSettings.data.extlink.extAdditionalTelClasses); 369 } else if (drupalSettings.data.extlink.extAdditionalLinkClasses) { 370 // Is regular external link: 371 linksToProcess[i].classList.add(drupalSettings.data.extlink.extAdditionalLinkClasses); 372 } 373 374 // Add data-extlink attribute. 375 linksToProcess[i].setAttribute('data-extlink', ''); 376 377 // Add icon. 378 if (iconPlacement) { 379 let link = linksToProcess[i]; 380 381 // Prevent appended icons from wrapping lines. 382 if (drupalSettings.data.extlink.extPreventOrphan && iconPlacement === 'append') { 383 // Find the last word in the link. 384 let lastTextNode = link.lastChild; 385 let trailingWhitespace = null; 386 let parentNode = link; 387 while (lastTextNode) { 388 if (lastTextNode.lastChild) { 389 // Last node was not text; step down into child node. 390 parentNode = lastTextNode; 391 lastTextNode = lastTextNode.lastChild; 392 } else if (lastTextNode.nodeName === '#text' && parentNode.lastElementChild && lastTextNode.textContent.trim().length === 0) { 393 // Last node was text, but it was whitespace. Step back into previous node. 394 trailingWhitespace = lastTextNode; 395 parentNode = parentNode.lastElementChild; 396 lastTextNode = parentNode.lastChild; 397 } else { 398 // Last node was null or valid text. 399 break; 400 } 401 } 402 if (lastTextNode && lastTextNode.nodeName === '#text' && lastTextNode.textContent.length > 0) { 403 const lastText = lastTextNode.textContent; 404 const lastWordRegex = new RegExp(/\S+\s*$/, 'g'); 405 const lastWord = lastText.match(lastWordRegex); 406 if (lastWord !== null) { 407 // Wrap the last word in a span. 408 const breakPreventer = document.createElement('span'); 409 breakPreventer.classList.add('extlink-nobreak'); 410 breakPreventer.textContent = lastWord[0]; 411 if (trailingWhitespace) { 412 trailingWhitespace.textContent = ''; 413 breakPreventer.append(trailingWhitespace.textContent); 414 } 415 lastTextNode.textContent = lastText.substring(0, lastText.length - lastWord[0].length); 416 lastTextNode.parentNode.append(breakPreventer); 417 // Insert the icon into the span rather than the link. 418 link = breakPreventer; 419 } 420 } 421 } 422 423 // Create an icon element. 424 let iconElement; 425 if (drupalSettings.data.extlink.extUseFontAwesome) { 426 iconElement = document.createElement('span'); 427 iconElement.setAttribute('class', `fa-${className} extlink`); 428 if (className === drupalSettings.data.extlink.mailtoClass) { 429 if (drupalSettings.data.extlink.mailtoLabel) { 430 link.ariaLabel = drupalSettings.data.extlink.mailtoLabel; 431 } 432 iconElement.innerHTML = Drupal.theme('extlink_fa_mailto', drupalSettings, iconPlacement); 433 } else if (className === drupalSettings.data.extlink.extClass) { 434 if (drupalSettings.data.extlink.extLabel) { 435 link.ariaLabel = drupalSettings.data.extlink.extLabel; 436 } 437 iconElement.innerHTML = Drupal.theme('extlink_fa_extlink', drupalSettings, iconPlacement); 438 } else if (className === drupalSettings.data.extlink.telClass) { 439 if (drupalSettings.data.extlink.telLabel) { 440 link.ariaLabel = drupalSettings.data.extlink.telLabel; 441 } 442 iconElement.innerHTML = Drupal.theme('extlink_fa_tel', drupalSettings, iconPlacement); 443 } 444 } else { 445 iconElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); 446 iconElement.setAttribute('focusable', 'false'); 447 iconElement.classList.add(className); 448 iconElement.setAttribute('data-extlink-placement', iconPlacement); 449 if (className === drupalSettings.data.extlink.mailtoClass) { 450 iconElement = Drupal.theme('extlink_mailto', iconElement, drupalSettings); 451 } else if (className === drupalSettings.data.extlink.extClass) { 452 iconElement = Drupal.theme('extlink_extlink', iconElement, drupalSettings); 453 } else if (className === drupalSettings.data.extlink.telClass) { 454 iconElement = Drupal.theme('extlink_tel', iconElement, drupalSettings); 455 } 456 } 457 iconElement.setAttribute('role', 'img'); 458 iconElement.setAttribute('aria-hidden', drupalSettings.data.extlink.extHideIcons); 459 link[iconPlacement](iconElement); 460 } 461 } 462 }; 463 464 /** 465 * Theme function for a Font Awesome mailto icon. 466 * 467 * @param {object} drupalSettings 468 * Settings object used to construct the markup. 469 * @param {string} iconPlacement 470 * The class to apply to a link. 471 * 472 * @return {string} 473 * HTML string of the Font AweSome mailto link icon. 474 */ 475 Drupal.theme.extlink_fa_mailto = function (drupalSettings, iconPlacement) { 476 return `<span class="${drupalSettings.data.extlink.extFaMailtoClasse
476s}" data-extlink-placement="${iconPlacement}"></span>`; 477 }; 478 479 /** 480 * Theme function for a Font Awesome external link icon. 481 * 482 * @param {object} drupalSettings 483 * Settings object used to construct the markup. 484 * @param {string} iconPlacement 485 * The class to apply to a link. 486 * 487 * @return {string} 488 * HTML string of the Font AweSome external link icon. 489 */ 490 Drupal.theme.extlink_fa_extlink = function (drupalSettings, iconPlacement) { 491 return `<span class="${drupalSettings.data.extlink.extFaLinkClasses}" data-extlink-placement="${iconPlacement}"></span>`; 492 }; 493 494 /** 495 * Theme function for a Font Awesome telephone icon. 496 * 497 * @param {object} drupalSettings 498 * Settings object used to construct the markup. 499 * @param {string} iconPlacement 500 * The class to apply to a link. 501 * 502 * @return {string} 503 * HTML string of the Font AweSome telephone icon. 504 */ 505 Drupal.theme.extlink_fa_tel = function (drupalSettings, iconPlacement) { 506 return `<span class="${drupalSettings.data.extlink.extFaLinkClasses}" data-extlink-placement="${iconPlacement}"></span>`; 507 }; 508 509 /** 510 * Theme function for a mailto icon. 511 * 512 * @param {object} iconElement 513 * The current iconElement being altered. 514 * @param {object} drupalSettings 515 * Settings object used to construct the markup. 516 * 517 * @return {object} 518 * The altered iconElement. 519 */ 520 Drupal.theme.extlink_mailto = function (iconElement, drupalSettings) { 521 iconElement.setAttribute('aria-label', drupalSettings.data.extlink.mailtoLabel); 522 iconElement.setAttribute('viewBox', '0 10 70 20'); 523 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.mailtoLabel}</title><path d="M56 14H8c-1.1 0-2 0.9-2 2v32c0 1.1 0.9 2 2 2h48c1.1 0 2-0.9 2-2V16C58 14.9 57.1 14 56 14zM50.5 18L32 33.4 13.5 18H50.5zM10 46V20.3l20.7 17.3C31.1 37.8 31.5 38 32 38s0.9-0.2 1.3-0.5L54 20.3V46H10z"/>`; 524 return iconElement; 525 }; 526 527 /** 528 * Theme function for an external link icon. 529 * 530 * @param {object} iconElement 531 * The current iconElement being altered. 532 * @param {object} drupalSettings 533 * Settings object used to construct the markup. 534 * 535 * @return {object} 536 * The altered iconElement. 537 */ 538 Drupal.theme.extlink_extlink = function (iconElement, drupalSettings) { 539 iconElement.setAttribute('aria-label', drupalSettings.data.extlink.extLabel); 540 iconElement.setAttribute('viewBox', '0 0 80 40'); 541 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.extLabel}</title><path d="M48 26c-1.1 0-2 0.9-2 2v26H10V18h26c1.1 0 2-0.9 2-2s-0.9-2-2-2H8c-1.1 0-2 0.9-2 2v40c0 1.1 0.9 2 2 2h40c1.1 0 2-0.9 2-2V28C50 26.9 49.1 26 48 26z"/><path d="M56 6H44c-1.1 0-2 0.9-2 2s0.9 2 2 2h7.2L30.6 30.6c-0.8 0.8-0.8 2 0 2.8C31 33.8 31.5 34 32 34s1-0.2 1.4-0.6L54 12.8V20c0 1.1 0.9 2 2 2s2-0.9 2-2V8C58 6.9 57.1 6 56 6z"/>`; 542 return iconElement; 543 }; 544 545 /** 546 * Theme function for an external telephone icon. 547 * 548 * @param {object} iconElement 549 * The current iconElement being altered. 550 * @param {object} drupalSettings 551 * Settings object used to construct the markup. 552 * 553 * @return {object} 554 * The altered iconElement. 555 */ 556 Drupal.theme.extlink_tel = function (iconElement, drupalSettings) { 557 iconElement.setAttribute('aria-label', drupalSettings.data.extlink.telLabel); 558 iconElement.setAttribute('viewBox', '0 0 181.352 181.352'); 559 iconElement.innerHTML = `<title>${drupalSettings.data.extlink.telLabel}</title><path xmlns="http://www.w3.org/2000/svg" d="M169.393,167.37l-14.919,9.848c-9.604,6.614-50.531,14.049-106.211-53.404C-5.415,58.873,9.934,22.86,17.134,14.555L29.523,1.678c2.921-2.491,7.328-2.198,9.839,0.811l32.583,38.543l0.02,0.02c2.384,2.824,2.306,7.22-0.83,9.868v0.029l-14.44,10.415c-5.716,5.667-0.733,14.587,5.11,23.204l27.786,32.808c12.926,12.477,20.009,18.241,26.194,14.118l12.008-13.395c2.941-2.472,7.328-2.169,9.839,0.821l32.603,38.543v0.02C172.607,160.316,172.519,164.703,169.393,167.37z"/>`; 560 return iconElement; 561 }; 562 563 Drupal.behaviors.extlink = Drupal.behaviors.extlink || {}; 564 Drupal.behaviors.extlink.attach = (context, drupalSettings) => { 565 // Backwards compatibility, for the benefit of modules overriding extlink 566 // functionality by defining an "extlinkAttach" global function. 567 if (typeof extlinkAttach === 'function') { 568 // eslint-disable-next-line no-undef 569 extlinkAttach(context); 570 } else { 571 Drupal.extlink.attach(context, drupalSettings); 572 } 573 }; 574})(Drupal, drupalSettings);
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.