1import { r as react } from './common/index-fc424163.js'; 2import './common/_commonjsHelpers-8c19dec8.js'; 3 4/* eslint-disable no-bitwise, no-magic-numbers, sort-keys */ 5// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories 6 7const TYPE_FLOW = 1; 8const TYPE_SECTION = 1 << 1; 9const TYPE_HEADING = 1 << 2; 10const TYPE_PHRASING = 1 << 3; 11const TYPE_EMBEDDED = 1 << 4; 12const TYPE_INTERACTIVE = 1 << 5; 13const TYPE_PALPABLE = 1 << 6; // https://developer.mozilla.org/en-US/docs/Web/HTML/Element 14 15const tagConfigs = { 16 a: { 17 content: TYPE_FLOW | TYPE_PHRASING, 18 self: false, 19 type: TYPE_FLOW | TYPE_PHRASING | TYPE_INTERACTIVE | TYPE_PALPABLE 20 }, 21 address: { 22 invalid: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'address', 'article', 'aside', 'section', 'div', 'header', 'footer'], 23 self: false 24 }, 25 audio: { 26 children: ['track', 'source'] 27 }, 28 br: { 29 type: TYPE_FLOW | TYPE_PHRASING, 30 void: true 31 }, 32 body: { 33 content: TYPE_FLOW | TYPE_SECTION | TYPE_HEADING | TYPE_PHRASING | TYPE_EMBEDDED | TYPE_INTERACTIVE | TYPE_PALPABLE 34 }, 35 button: { 36 content: TYPE_PHRASING, 37 type: TYPE_FLOW | TYPE_PHRASING | TYPE_INTERACTIVE | TYPE_PALPABLE 38 }, 39 caption: { 40 content: TYPE_FLOW, 41 parent: ['table'] 42 }, 43 col: { 44 parent: ['colgroup'], 45 void: true 46 }, 47 colgroup: { 48 children: ['col'], 49 parent: ['table'] 50 }, 51 details: { 52 children: ['summary'], 53 type: TYPE_FLOW | TYPE_INTERACTIVE | TYPE_PALPABLE 54 }, 55 dd: { 56 content: TYPE_FLOW, 57 parent: ['dl'] 58 }, 59 dl: { 60 children: ['dt', 'dd'], 61 type: TYPE_FLOW 62 }, 63 dt: { 64 content: TYPE_FLOW, 65 invalid: ['footer', 'header'], 66 parent: ['dl'] 67 }, 68 figcaption: { 69 content: TYPE_FLOW, 70 parent: ['figure'] 71 }, 72 footer: { 73 invalid: ['footer', 'header'] 74 }, 75 header: { 76 invalid: ['footer', 'header'] 77 }, 78 hr: { 79 type: TYPE_FLOW, 80 void: true 81 }, 82 img: { 83 void: true 84 }, 85 li: { 86 content: TYPE_FLOW, 87 parent: ['ul', 'ol', 'menu'] 88 }, 89 main: { 90 self: false 91 }, 92 ol: { 93 children: ['li'], 94 type: TYPE_FLOW 95 }, 96 picture: { 97 children: ['source', 'img'], 98 type: TYPE_FLOW | TYPE_PHRASING | TYPE_EMBEDDED 99 }, 100 rb: { 101 parent: ['ruby', 'rtc'] 102 }, 103 rp: { 104 parent: ['ruby', 'rtc'] 105 }, 106 rt: { 107 content: TYPE_PHRASING, 108 parent: ['ruby', 'rtc'] 109 }, 110 rtc: { 111 content: TYPE_PHRASING, 112 parent: ['ruby'] 113 }, 114 ruby: { 115 children: ['rb', 'rp', 'rt', 'rtc'] 116 }, 117 source: { 118 parent: ['audio', 'video', 'picture'], 119 void: true 120 }, 121 summary: { 122 content: TYPE_PHRASING, 123 parent: ['details'] 124 }, 125 table: { 126 children: ['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr'], 127 type: TYPE_FLOW 128 }, 129 tbody: { 130 parent: ['table'], 131 children: ['tr'] 132 }, 133 td: { 134 content: TYPE_FLOW, 135 parent: ['tr'] 136 }, 137 tfoot: { 138 parent: ['table'], 139 children: ['tr'] 140 }, 141 th: { 142 content: TYPE_FLOW, 143 parent: ['tr'] 144 }, 145 thead: { 146 parent: ['table'], 147 children: ['tr'] 148 }, 149 tr: { 150 parent: ['table', 'tbody', 'thead', 'tfoot'], 151 children: ['th', 'td'] 152 }, 153 track: { 154 parent: ['audio', 'video'], 155 void: true 156 }, 157 ul: { 158 children: ['li'], 159 type: TYPE_FLOW 160 }, 161 video: { 162 children: ['track', 'source'] 163 }, 164 wbr: { 165 type: TYPE_FLOW | TYPE_PHRASING, 166 void: true 167 } 168}; 169 170function createConfigBuilder(config) { 171 return tagName => { 172 tagConfigs[tagName] = { ...config, 173 ...tagConfigs[tagName] 174 }; 175 }; 176} 177 178['address', 'main', 'div', 'figure', 'p', 'pre'].forEach(createConfigBuilder({ 179 content: TYPE_FLOW, 180 type: TYPE_FLOW | TYPE_PALPABLE 181})); 182['abbr', 'b', 'bdi', 'bdo', 'cite', 'code', 'data', 'dfn', 'em', 'i', 'kbd', 'mark', 'q', 'ruby', 'samp', 'strong', 'sub', 'sup', 'time', 'u', 'var'].forEach(createConfigBuilder({ 183 content: TYPE_PHRASING, 184 type: TYPE_FLOW | TYPE_PHRASING | TYPE_PALPABLE 185})); 186['p', 'pre'].forEach(createConfigBuilder({ 187 content: TYPE_PHRASING, 188 type: TYPE_FLOW | TYPE_PALPABLE 189})); 190['s', 'small', 'span', 'del', 'ins'].forEach(createConfigBuilder({ 191 content: TYPE_PHRASING, 192 type: TYPE_FLOW | TYPE_PHRASING 193})); 194['article', 'aside', 'footer', 'header', 'nav', 'section', 'blockquote'].forEach(createConfigBuilder({ 195 content: TYPE_FLOW, 196 type: TYPE_FLOW | TYPE_SECTION | TYPE_PALPABLE 197})); 198['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].forEach(createConfigBuilder({ 199 content: TYPE_PHRASING, 200 type: TYPE_FLOW | TYPE_HEADING | TYPE_PALPABLE 201})); 202['audio', 'canvas', 'iframe', 'img', 'video'].forEach(createConfigBuilder({ 203 type: TYPE_FLOW | TYPE_PHRASING | TYPE_EMBEDDED | TYPE_PALPABLE 204})); // Disable this map from being modified 205 206const TAGS = Object.freeze(tagConfigs); // Tags that should never be allowed, even if the allow list is disabled 207 208const BANNED_TAG_LIST = ['applet', 'base', 'body', 'command', 'embed', 'frame', 'frameset', 'head', 'html', 'link', 'meta', 'noscript', 'object', 'script', 'style', 'title']; 209const ALLOWED_TAG_LIST = Object.keys(TAGS).filter(tag => tag !== 'canvas' && tag !== 'iframe'); // Filters apply to HTML attributes 210 211const FILTER_ALLOW = 1; 212const FILTER_DENY = 2; 213const FILTER_CAST_NUMBER = 3; 214const FILTER_CAST_BOOL = 4; 215const FILTER_NO_CAST = 5; // Attributes not listed here will be denied 216// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes 217 218const ATTRIBUTES = Object.freeze({ 219 alt: FILTER_ALLOW, 220 cite: FILTER_ALLOW, 221 class: FILTER_ALLOW, 222 colspan: FILTER_CAST_NUMBER, 223 controls: FILTER_CAST_BOOL, 224 datetime: FILTER_ALLOW, 225 default: FILTER_CAST_BOOL, 226 disabled: FILTER_CAST_BOOL, 227 dir: FILTER_ALLOW, 228 height: FILTER_ALLOW, 229 href: FILTER_ALLOW, 230 id: FILTER_ALLOW, 231 kind: FILTER_ALLOW, 232 label: FILTER_ALLOW, 233 lang: FILTER_ALLOW, 234 loading: FILTER_ALLOW, 235 loop: FILTER_CAST_BOOL, 236 media: FILTER_ALLOW, 237 muted: FILTER_CAST_BOOL, 238 poster: FILTER_ALLOW, 239 rel: FILTER_ALLOW, 240 role: FILTER_ALLOW, 241 rowspan: FILTER_CAST_NUMBER, 242 scope: FILTER_ALLOW, 243 sizes: FILTER_ALLOW, 244 span: FILTER_CAST_NUMBER, 245 start: FILTER_CAST_NUMBER, 246 style: FILTER_NO_CAST, 247 src: FILTER_ALLOW, 248 srclang: FILTER_ALLOW, 249 srcset: FILTER_ALLOW, 250 target: FILTER_ALLOW, 251 title: FILTER_ALLOW, 252 type: FILTER_ALLOW, 253 width: FILTER_ALLOW 254}); // Attributes to camel case for React props 255 256const ATTRIBUTES_TO_PROPS = Object.freeze({ 257 class: 'className', 258 colspan: 'colSpan', 259 datetime: 'dateTime', 260 rowspan: 'rowSpan', 261 srclang: 'srcLang', 262 srcset: 'srcSet' 263}); 264 265function _extends() { 266 _extends = Object.assign || function (target) { 267 for (var i = 1; i < arguments.length; i++) { 268 var source = arguments[i]; 269 270 for (var key in source) { 271 if (Object.prototype.hasOwnProperty.call(source, key)) { 272 target[key] = source[key]; 273 } 274 } 275 } 276 277 return target; 278 }; 279 280 return _extends.apply(this, arguments); 281} 282 283function Element({ 284 attributes = {},
285 className, 286 children = null, 287 selfClose = false, 288 tagName 289}) { 290 const Tag = tagName; 291 return selfClose ? /*#__PURE__*/react.createElement(Tag, _extends({ 292 className: className 293 }, attributes)) : /*#__PURE__*/react.createElement(Tag, _extends({ 294 className: className 295 }, attributes), children); 296} 297 298class Filter { 299 /** 300 * Filter and clean an HTML attribute value. 301 */ 302 attribute(name, value) { 303 // eslint-disable-next-line @typescript-eslint/no-unsafe-return 304 return value; 305 } 306 /** 307 * Filter and clean an HTML node. 308 */ 309 310 311 node(name, node) { 312 return node; 313 } 314 315} 316 317/*! 318 * escape-html 319 * Copyright(c) 2012-2013 TJ Holowaychuk 320 * Copyright(c) 2015 Andreas Lubbe 321 * Copyright(c) 2015 Tiancheng "Timothy" Gu 322 * MIT Licensed 323 */ 324 325/** 326 * Module variables. 327 * @private 328 */ 329 330var matchHtmlRegExp = /["'&<>]/; 331 332/** 333 * Module exports. 334 * @public 335 */ 336 337var escapeHtml_1 = escapeHtml; 338 339/** 340 * Escape special characters in the given string of html. 341 * 342 * @param {string} string The string to escape for inserting into HTML 343 * @return {string} 344 * @public 345 */ 346 347function escapeHtml(string) { 348 var str = '' + string; 349 var match = matchHtmlRegExp.exec(str); 350 351 if (!match) { 352 return str; 353 } 354 355 var escape; 356 var html = ''; 357 var index = 0; 358 var lastIndex = 0; 359 360 for (index = match.index; index < str.length; index++) { 361 switch (str.charCodeAt(index)) { 362 case 34: // " 363 escape = '"'; 364 break; 365 case 38: // & 366 escape = '&'; 367 break; 368 case 39: // ' 369 escape = '''; 370 break; 371 case 60: // < 372 escape = '<'; 373 break; 374 case 62: // > 375 escape = '>'; 376 break; 377 default: 378 continue; 379 } 380 381 if (lastIndex !== index) { 382 html += str.substring(lastIndex, index); 383 } 384 385 lastIndex = index + 1; 386 html += escape; 387 } 388 389 return lastIndex !== index 390 ? html + str.substring(lastIndex, index) 391 : html; 392} 393 394function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 395const INVALID_STYLES = /(url|image|image-set)\(/i; 396 397class StyleFilter extends Filter { 398 attribute(name, value) { 399 if (name === 'style') { 400 Object.keys(value).forEach(key => { 401 if (String(value[key]).match(INVALID_STYLES)) { 402 // eslint-disable-next-line no-param-reassign 403 delete value[key]; 404 } 405 }); 406 } // eslint-disable-next-line @typescript-eslint/no-unsafe-return 407 408 409 return value; 410 } 411 412} 413/* eslint-disable no-bitwise, no-cond-assign, complexity, @typescript-eslint/no-unsafe-return */ 414 415 416const ELEMENT_NODE = 1; 417const TEXT_NODE = 3; 418const INVALID_ROOTS = /^<(!doctype|(html|head|body)(\s|>))/i; 419const ALLOWED_ATTRS = /^(aria-|data-|\w+:)/iu; 420const OPEN_TOKEN = /{{{(\w+)\/?}}}/; 421 422function createDocument() { 423 // Maybe SSR? Just do nothing instead of crashing! 424 if (typeof window === 'undefined' || typeof document === 'undefined') { 425 return undefined; 426 } 427 428 return document.implementation.createHTMLDocument('Interweave'); 429} 430 431class Parser { 432 constructor(markup, props = {}, matchers = [], filters = []) { 433 var _props$allowList; 434 435 _defineProperty(this, "allowed", void 0); 436 437 _defineProperty(this, "banned", void 0); 438 439 _defineProperty(this, "blocked", void 0); 440 441 _defineProperty(this, "container", void 0); 442 443 _defineProperty(this, "content", []); 444 445 _defineProperty(this, "props", void 0); 446 447 _defineProperty(this, "matchers", void 0); 448 449 _defineProperty(this, "filters", void 0); 450 451 _defineProperty(this, "keyIndex", void 0); 452 453 this.props = props; 454 this.matchers = matchers; 455 this.filters = [...filters, new StyleFilter()]; 456 this.keyIndex = -1; 457 this.container = this.createContainer(markup || ''); 458 this.allowed = new Set((_props$allowList = props.allowList) !== null && _props$allowList !== void 0 ? _props$allowList : ALLOWED_TAG_LIST); 459 this.banned = new Set(BANNED_TAG_LIST); 460 this.blocked = new Set(props.blockList); 461 } 462 /** 463 * Loop through and apply all registered attribute filters. 464 */ 465 466 467 applyAttributeFilters(name, value) { 468 return this.filters.reduce((nextValue, filter) => nextValue !== null && typeof filter.attribute === 'function' ? filter.attribute(name, nextValue) : nextValue, value); 469 } 470 /** 471 * Loop through and apply all registered node filters. 472 */ 473 474 475 applyNodeFilters(name, node) { 476 // Allow null to be returned 477 return this.filters.reduce((nextNode, filter) => nextNode !== null && typeof filter.node === 'function' ? filter.node(name, nextNode) : nextNode, node); 478 } 479 /** 480 * Loop through and apply all registered matchers to the string. 481 * If a match is found, create a React element, and build a new array. 482 * This array allows React to interpolate and render accordingly. 483 */ 484 485 486 applyMatchers(string, parentConfig) { 487 const elements = {}; 488 const { 489 props 490 } = this; 491 let matchedString = string; 492 let elementIndex = 0; 493 let parts = null;
494 this.matchers.forEach(matcher => { 495 const tagName = matcher.asTag().toLowerCase(); 496 const config = this.getTagConfig(tagName); // Skip matchers that have been disabled from props or are not supported 497 498 if (props[matcher.inverseName] || !this.isTagAllowed(tagName)) { 499 return; 500 } // Skip matchers in which the child cannot be rendered 501 502 503 if (!this.canRenderChild(parentConfig, config)) { 504 return; 505 } // Continuously trigger the matcher until no matches are found 506 507 508 let tokenizedString = ''; 509 510 while (matchedString && (parts = matcher.match(matchedString))) { 511 const { 512 index, 513 length, 514 match, 515 valid, 516 void: isVoid, 517 ...partProps 518 } = parts; 519 const tokenName = matcher.propName + String(elementIndex); // Piece together a new string with interpolated tokens 520 521 if (index > 0) { 522 tokenizedString += matchedString.slice(0, index); 523 } 524 525 if (valid) { 526 tokenizedString += isVoid ? `{{{${tokenName}/}}}` : `{{{${tokenName}}}}${match}{{{/${tokenName}}}}`; 527 this.keyIndex += 1; 528 elementIndex += 1; 529 elements[tokenName] = { 530 children: match, 531 matcher, 532 props: { ...props, 533 ...partProps, 534 key: this.keyIndex 535 } 536 }; 537 } else { 538 tokenizedString += match; 539 } // Reduce the string being matched against, 540 // otherwise we end up in an infinite loop! 541 542 543 if (matcher.greedy) { 544 matchedString = tokenizedString + matchedString.slice(index + length); 545 tokenizedString = ''; 546 } else { 547 // eslint-disable-next-line unicorn/explicit-length-check 548 matchedString = matchedString.slice(index + (length || match.length)); 549 } 550 } // Update the matched string with the tokenized string, 551 // so that the next matcher can apply to it. 552 553 554 if (!matcher.greedy) { 555 matchedString = tokenizedString + matchedString; 556 } 557 }); 558 559 if (elementIndex === 0) { 560 return string; 561 } 562 563 return this.replaceTokens(matchedString, elements); 564 } 565 /** 566 * Determine whether the child can be rendered within the parent. 567 */ 568 569 570 canRenderChild(parentConfig, childConfig) { 571 if (!parentConfig.tagName || !childConfig.tagName) { 572 return false; 573 } // No children 574 575 576 if (parentConfig.void) { 577 return false; 578 } // Valid children 579 580 581 if (parentConfig.children.length > 0) { 582 return parentConfig.children.includes(childConfig.tagName); 583 } 584 585 if (parentConfig.invalid.length > 0 && parentConfig.invalid.includes(childConfig.tagName)) { 586 return false; 587 } // Valid parent 588 589 590 if (childConfig.parent.length > 0) { 591 return childConfig.parent.includes(parentConfig.tagName); 592 } // Self nesting 593 594 595 if (!parentConfig.self && parentConfig.tagName === childConfig.tagName) { 596 return false; 597 } // Content category type 598 599 600 return Boolean(parentConfig && parentConfig.content & childConfig.type); 601 } 602 /** 603 * Convert line breaks in a string to HTML `<br/>` tags. 604 * If the string contains HTML, we should not convert anything, 605 * as line breaks should be handled by `<br/>`s in the markup itself. 606 */ 607 608 609 convertLineBreaks(markup) { 610 const { 611 noHtml, 612 disableLineBreaks 613 } = this.props; 614 615 if (noHtml || disableLineBreaks || markup.match(/<((?:\/[ a-z]+)|(?:[ a-z]+\/))>/gi)) { 616 return markup; 617 } // Replace carriage returns 618 619 620 let nextMarkup = markup.replace(/\r\n/g, '\n'); // Replace long line feeds 621 622 nextMarkup = nextMarkup.replace(/\n{3,}/g, '\n\n\n'); // Replace line feeds with `<br/>`s 623 624 nextMarkup = nextMarkup.replace(/\n/g, '<br/>'); 625 return nextMarkup; 626 } 627 /** 628 * Create a detached HTML document that allows for easy HTML 629 * parsing while not triggering scripts or loading external 630 * resources. 631 */ 632 633 634 createContainer(markup) { 635 var _this$props$container; 636 637 const factory = typeof global !== 'undefined' && global.INTERWEAVE_SSR_POLYFILL || createDocument; 638 const doc = factory(); 639 640 if (!doc) { 641 return undefined; 642 } 643 644 const tag = (_this$props$container = this.props.containerTagName) !== null && _this$props$container !== void 0 ? _this$props$container : 'body'; 645 const el = tag === 'body' || tag === 'fragment' ? doc.body : doc.createElement(tag); 646 647 if (markup.match(INVALID_ROOTS)) ; else { 648 el.innerHTML = this.convertLineBreaks(this.props.escapeHtml ? escapeHtml_1(markup) : markup); 649 } 650 651 return el; 652 } 653 /** 654 * Convert an elements attribute map to an object map. 655 * Returns null if no attributes are defined. 656 */ 657 658 659 extractAttributes(node) { 660 const { 661 allowAttributes 662 } = this.props; 663 const attributes = {};
664 let count = 0; 665 666 if (node.nodeType !== ELEMENT_NODE || !node.attributes) { 667 return null; 668 } // @ts-expect-error Cant type iterator 669 670 671 [...node.attributes].forEach(attr => { 672 const { 673 name, 674 value 675 } = attr; 676 const newName = name.toLowerCase(); 677 const filter = ATTRIBUTES[newName] || ATTRIBUTES[name]; // Verify the node is safe from attacks 678 679 if (!this.isSafe(node)) { 680 return; 681 } // Do not allow denied attributes, excluding ARIA attributes 682 // Do not allow events or XSS injections 683 684 685 if (!newName.match(ALLOWED_ATTRS) && (!allowAttributes && (!filter || filter === FILTER_DENY) || newName.startsWith('on') || value.replace(/(\s|\0|�([9AD]);)/, '').match(/(javascript|vbscript|livescript|xss):/i))) { 686 return; 687 } // Apply attribute filters 688 689 690 let newValue = newName === 'style' ? this.extractStyleAttribute(node) : value; // Cast to boolean 691 692 if (filter === FILTER_CAST_BOOL) { 693 newValue = true; // Cast to number 694 } else if (filter === FILTER_CAST_NUMBER) { 695 newValue = Number.parseFloat(String(newValue)); // Cast to string 696 } else if (filter !== FILTER_NO_CAST) { 697 newValue = String(newValue); 698 } 699 700 attributes[ATTRIBUTES_TO_PROPS[newName] || newName] = this.applyAttributeFilters(newName, newValue); 701 count += 1; 702 }); 703 704 if (count === 0) { 705 return null; 706 } 707 708 return attributes; 709 } 710 /** 711 * Extract the style attribute as an object and remove values that allow for attack vectors. 712 */ 713 714 715 extractStyleAttribute(node) { 716 const styles = {}; // eslint-disable-next-line unicorn/prefer-spread 717 718 Array.from(node.style).forEach(key => { 719 const value = node.style[key]; 720 721 if (typeof value === 'string' || typeof value === 'number') { 722 styles[key.replace(/-([a-z])/g, (match, letter) => String(letter).toUpperCase())] = value; 723 } 724 }); 725 return styles; 726 } 727 /** 728 * Return configuration for a specific tag. 729 */ 730 731 732 getTagConfig(tagName) { 733 const common = { 734 children: [], 735 content: 0, 736 invalid: [], 737 parent: [], 738 self: true, 739 tagName: '', 740 type: 0, 741 void: false 742 }; // Only spread when a tag config exists, 743 // otherwise we use the empty `tagName` 744 // for parent config inheritance. 745 746 if (TAGS[tagName]) { 747 return { ...common, 748 ...TAGS[tagName], 749 tagName 750 }; 751 } 752 753 return common; 754 } 755 /** 756 * Verify that a node is safe from XSS and injection attacks. 757 */ 758 759 760 isSafe(node) { 761 // URLs should only support HTTP, email and phone numbers 762 if (typeof HTMLAnchorElement !== 'undefined' && node instanceof HTMLAnchorElement) { 763 const href = node.getAttribute('href'); // Fragment protocols start with about: 764 // So let's just allow them 765 766 if (href !== null && href !== void 0 && href.startsWith('#')) { 767 return true; 768 } 769 770 const protocol = node.protocol.toLowerCase(); 771 return protocol === ':' || protocol === 'http:' || protocol === 'https:' || protocol === 'mailto:' || protocol === 'tel:'; 772 } 773 774 return true; 775 } 776 /** 777 * Verify that an HTML tag is allowed to render. 778 */ 779 780 781 isTagAllowed(tagName) { 782 if (this.banned.has(tagName) || this.blocked.has(tagName)) { 783 return false; 784 } // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 785 786 787 return this.props.allowElements || this.allowed.has(tagName); 788 } 789 /** 790 * Parse the markup by injecting it into a detached document, 791 * while looping over all child nodes and generating an 792 * array to interpolate into JSX. 793 */ 794 795 796 parse() { 797 if (!this.container) { 798 return []; 799 } 800 801 return this.parseNode(this.container, this.getTagConfig(this.container.nodeName.toLowerCase())); 802 } 803 /** 804 * Loop over the nodes children and generate a 805 * list of text nodes and React elements. 806 */ 807 808 809 parseNode(parentNode, parentConfig) { 810 const { 811 noHtml, 812 noHtmlExceptMatchers, 813 allowElements, 814 transform, 815 transformOnlyAllowList 816 } = this.props;
817 let content = []; 818 let mergedText = ''; // @ts-expect-error Cant type iterator 819 820 [...parentNode.childNodes].forEach(node => { 821 // Create React elements from HTML elements 822 if (node.nodeType === ELEMENT_NODE) { 823 const tagName = node.nodeName.toLowerCase(); 824 const config = this.getTagConfig(tagName); // Persist any previous text 825 826 if (mergedText) { 827 content.push(mergedText); 828 mergedText = ''; 829 } // Apply node filters first 830 831 832 const nextNode = this.applyNodeFilters(tagName, node); 833 834 if (!nextNode) { 835 return; 836 } // Apply transformation second 837 838 839 let children; 840 841 if (transform && !(transformOnlyAllowList && !this.isTagAllowed(tagName))) { 842 this.keyIndex += 1; 843 const key = this.keyIndex; // Must occur after key is set 844 845 children = this.parseNode(nextNode, config); 846 const transformed = transform(nextNode, children, config); 847 848 if (transformed === null) { 849 return; 850 } 851 852 if (typeof transformed !== 'undefined') { 853 content.push( /*#__PURE__*/react.cloneElement(transformed, { 854 key 855 })); 856 return; 857 } // Reset as we're not using the transformation 858 859 860 this.keyIndex = key - 1; 861 } // Never allow these tags (except via a transformer) 862 863 864 if (this.banned.has(tagName)) { 865 return; 866 } // Only render when the following criteria is met: 867 // - HTML has not been disabled 868 // - Tag is allowed 869 // - Child is valid within the parent 870 871 872 if (!(noHtml || noHtmlExceptMatchers && tagName !== 'br') && this.isTagAllowed(tagName) && (allowElements || this.canRenderChild(parentConfig, config))) { 873 var _children; 874 875 this.keyIndex += 1; // Build the props as it makes it easier to test 876 877 const attributes = this.extractAttributes(nextNode); 878 const elementProps = { 879 tagName 880 }; 881 882 if (attributes) { 883 elementProps.attributes = attributes; 884 } 885 886 if (config.void) { 887 elementProps.selfClose = config.void; 888 } 889 890 content.push( /*#__PURE__*/react.createElement(Element, { ...elementProps, 891 key: this.keyIndex 892 }, (_children = children) !== null && _children !== void 0 ? _children : this.parseNode(nextNode, config))); // Render the children of the current element only. 893 // Important: If the current element is not allowed, 894 // use the parent element for the next scope. 895 } else { 896 content = [...content, ...this.parseNode(nextNode, config.tagName ? config : parentConfig)]; 897 } // Apply matchers if a text node 898 899 } else if (node.nodeType === TEXT_NODE) { 900 const text = noHtml && !noHtmlExceptMatchers ? node.textContent : // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 901 this.applyMatchers(node.textContent || '', parentConfig); 902 903 if (Array.isArray(text)) { 904 content = [...content, ...text]; 905 } else { 906 mergedText += text; 907 } 908 } 909 }); 910 911 if (mergedText) { 912 content.push(mergedText); 913 } 914 915 return content; 916 } 917 /** 918 * Deconstruct the string into an array, by replacing custom tokens with React elements, 919 * so that React can render it correctly. 920 */ 921 922 923 replaceTokens(tokenizedString, elements) { 924 if (!tokenizedString.includes('{{{')) { 925 return tokenizedString; 926 } 927 928 const nodes = []; 929 let text = tokenizedString; 930 let open = null; // Find an open token tag 931 932 while (open = text.match(OPEN_TOKEN)) { 933 const [match, tokenName] = open; 934 const startIndex = open.index; 935 const isVoid = match.includes('/'); 936 937 938 if (startIndex > 0) { 939 nodes.push(text.slice(0, startIndex)); // Reduce text so that the closing tag will be found after the opening 940 941 text = text.slice(startIndex); 942 } 943 944 const { 945 children, 946 matcher, 947 props: elementProps 948 } = elements[tokenName]; 949 let endIndex; // Use tag as-is if void 950 951 if (isVoid) { 952 endIndex = match.length; 953 nodes.push(matcher.createElement(children, elementProps)); // Find the closing tag if not void 954 } else { 955 const close = text.match(new RegExp(`{{{/${tokenName}}}}`)); 956 957 endIndex = close.index + close[0].length; 958 nodes.push(matcher.createElement(this.replaceTokens(text.slice(match.length, close.index), elements), elementProps)); 959 } // Reduce text for the next interation 960 961 962 text = text.slice(endIndex); 963 } // Extra the remaining text 964 965 966 if (text.length > 0) { 967 nodes.push(text); 968 } // Reduce to a string if possible 969 970 971 if (nodes.length === 0) { 972 return ''; 973 } 974 975 if (nodes.length === 1 && typeof nodes[0] === 'string') { 976 return nodes[0]; 977 } 978 979 return nodes; 980 } 981 982} 983/* eslint-disable react/jsx-fragments */ 984 985 986function Markup(props) { 987 var _ref; 988 989 const { 990 attributes, 991 className, 992 containerTagName, 993 content, 994 emptyContent, 995 parsedContent, 996 tagName, 997 noWrap: baseNoWrap 998 } = props; 999 const tag = (_ref = containerTagName !== null && containerTagName !== void 0 ? containerTagName : tagName) !== null && _ref !== void 0 ? _ref : 'span'; 1000 const noWrap = tag === 'fragment' ? true : baseNoWrap; 1001 let mainContent; 1002 1003 if (parsedContent) { 1004 mainContent = parsedContent; 1005 } else { 1006 const markup = new Parser(content !== null && content !== void 0 ? content : '', props).parse(); 1007 1008 if (markup.length > 0) { 1009 mainContent = markup; 1010 } 1011 } 1012 1013 if (!mainContent) { 1014 mainContent = emptyContent; 1015 } 1016 1017 if (noWrap) { 1018 // eslint-disable-next-line react/jsx-no-useless-fragment 1019 return /*#__PURE__*/react.createElement(react.Fragment, null, mainContent); 1020 } 1021 1022 return /*#__PURE__*/react.createElement(Element, { 1023 attributes: attributes, 1024 className: className, 1025 tagName: tag 1026 }, mainContent); 1027} 1028/* eslint-disable promise/prefer-await-to-callbacks */ 1029 1030 1031function Interweave(props) { 1032 const { 1033 attributes, 1034 className, 1035 content = '', 1036 disableFilters = false, 1037 disableMatchers = false, 1038 emptyContent = null, 1039 filters = [], 1040 matchers = [], 1041 onAfterParse = null, 1042 onBeforeParse = null, 1043 tagName = 'span', 1044 noWrap = false, 1045 ...parserProps 1046 } = props; 1047 const allMatchers = disableMatchers ? [] : matchers; 1048 const allFilters = disableFilters ? [] : filters; 1049 const beforeCallbacks = onBeforeParse ? [onBeforeParse] : []; 1050 const afterCallbacks = onAfterParse ? [onAfterParse] : []; // Inherit callbacks from matchers 1051
1052 allMatchers.forEach(matcher => { 1053 if (matcher.onBeforeParse) { 1054 beforeCallbacks.push(matcher.onBeforeParse.bind(matcher)); 1055 } 1056 1057 if (matcher.onAfterParse) { 1058 afterCallbacks.push(matcher.onAfterParse.bind(matcher)); 1059 } 1060 }); // Trigger before callbacks 1061 1062 const markup = beforeCallbacks.reduce((string, callback) => { 1063 const nextString = callback(string, props); 1064 1065 return nextString; 1066 }, content !== null && content !== void 0 ? content : ''); // Parse the markup 1067 1068 const parser = new Parser(markup, parserProps, allMatchers, allFilters); // Trigger after callbacks 1069 1070 const nodes = afterCallbacks.reduce((parserNodes, callback) => { 1071 const nextNodes = callback(parserNodes, props); 1072 1073 return nextNodes; 1074 }, parser.parse()); 1075 return /*#__PURE__*/react.createElement(Markup, { 1076 attributes: attributes, 1077 className: className // eslint-disable-next-line react/destructuring-assignment 1078 , 1079 containerTagName: props.containerTagName, 1080 emptyContent: emptyContent, 1081 noWrap: noWrap, 1082 parsedContent: nodes.length === 0 ? undefined : nodes, 1083 tagName: tagName 1084 }); 1085} 1086 1087export { Interweave };
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.