whole_func_string: function sanitizeHtml(html, options, _recursing) {
    var result = '';
  
    function Frame(tag, attribs) {
      var that = this;
      this.tag = tag;
      this.attribs = attribs || {};
      this.tagPosition = result.length;
      this.text = ''; // Node inner text
  
      this.updateParentNodeText = function() {
        if (stack.length) {
            var parentFrame = stack[stack.length - 1];
            parentFrame.text += that.text;
        }
      };
    }
  
    if (!options) {
      options = sanitizeHtml.defaults;
      options.parser = htmlParserDefaults;
    } else {
      options = extend(sanitizeHtml.defaults, options);
      if (options.parser) {
        options.parser = extend(htmlParserDefaults, options.parser);
      } else {
        options.parser = htmlParserDefaults;
      }
    }
  
    // Tags that contain something other than HTML, or where discarding
    // the text when the tag is disallowed makes sense for other reasons.
    // If we are not allowing these tags, we should drop their content too.
    // For other tags you would drop the tag but keep its content.
    var nonTextTagsArray = options.nonTextTags || [ 'script', 'style', 'textarea' ];
    var allowedAttributesMap;
    var allowedAttributesGlobMap;
    if(options.allowedAttributes) {
      allowedAttributesMap = {};
      allowedAttributesGlobMap = {};
      each(options.allowedAttributes, function(attributes, tag) {
        allowedAttributesMap[tag] = [];
        var globRegex = [];
        attributes.forEach(function(obj) {
          if(isString(obj) && obj.indexOf('*') >= 0) {
            globRegex.push(quoteRegexp(obj).replace(/\\\*/g, '.*'));
          } else {
            allowedAttributesMap[tag].push(obj);
          }
        });
        allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
      });
    }
    var allowedClassesMap = {};
    each(options.allowedClasses, function(classes, tag) {
      // Implicitly allows the class attribute
      if(allowedAttributesMap) {
        if (!has(allowedAttributesMap, tag)) {
          allowedAttributesMap[tag] = [];
        }
        allowedAttributesMap[tag].push('class');
      }
  
      allowedClassesMap[tag] = classes;
    });
  
    var transformTagsMap = {};
    var transformTagsAll;
    each(options.transformTags, function(transform, tag) {
      var transFun;
      if (typeof transform === 'function') {
        transFun = transform;
      } else if (typeof transform === "string") {
        transFun = sanitizeHtml.simpleTransform(transform);
      }
      if (tag === '*') {
        transformTagsAll = transFun;
      } else {
        transformTagsMap[tag] = transFun;
      }
    });
  
    var depth = 0;
    var stack = [];
    var skipMap = {};
    var transformMap = {};
    var skipText = false;
    var skipTextDepth = 0;
  
    var parser = new htmlparser.Parser({
      onopentag: function(name, attribs) {
        if (skipText) {
          skipTextDepth++;
          return;
        }
        var frame = new Frame(name, attribs);
        stack.push(frame);
  
        var skip = false;
        var hasText = frame.text ? true : false;
        var transformedTag;
        if (has(transformTagsMap, name)) {
          transformedTag = transformTagsMap[name](name, attribs);
  
          frame.attribs = attribs = transformedTag.attribs;
  
          if (transformedTag.text !== undefined) {
            frame.innerText = transformedTag.text;
          }
  
          if (name !== transformedTag.tagName) {
            frame.name = name = transformedTag.tagName;
            transformMap[depth] = transformedTag.tagName;
          }
        }
        if (transformTagsAll) {
          transformedTag = transformTagsAll(name, attribs);
  
          frame.attribs = attribs = transformedTag.attribs;
          if (name !== transformedTag.tagName) {
            frame.name = name = transformedTag.tagName;
            transformMap[depth] = transformedTag.tagName;
          }
        }
  
        if (options.allowedTags && options.allowedTags.indexOf(name) === -1) {
          skip = true;
          if (nonTextTagsArray.indexOf(name) !== -1) {
            skipText = true;
            skipTextDepth = 1;
          }
          skipMap[depth] = true;
        }
        depth++;
        if (skip) {
          // We want the contents but not this tag
          return;
        }
        result += '<' + name;
        if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
          each(attribs, function(value, a) {
            if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) {
              // This prevents part of an attribute name in the output from being
              // interpreted as the end of an attribute, or end of a tag.
              delete frame.attribs[a];
              return;
            }
            var parsed;
            // check allowedAttributesMap for the element and attribute and modify the value
            // as necessary if there are specific values defined.
            var passedAllowedAttributesMapCheck = false;
            if (!allowedAttributesMap ||
              (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1 ) ||
              (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1 ) ||
              (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
              (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {    
                passedAllowedAttributesMapCheck = true;
            } else if (allowedAttributesMap && allowedAttributesMap[name]) {
              for (const o of allowedAttributesMap[name]) {
                if (isPlainObject(o) && o.name && (o.name === a)) {
                  passedAllowedAttributesMapCheck = true;
                  var newValue = '';
                  if (o.multiple === true) {
                    // verify the values that are allowed
                    const splitStrArray = value.split(' ');
                    for (const s of splitStrArray) {
                      if (o.values.indexOf(s) !== -1) {
                        if (newValue === '') {
                          newValue = s;
                        } else {
                          newValue += ' ' + s;
                        }
                      }
                    }
                  } else if (o.values.indexOf(value) >= 0) {
                    // verified an allowed value matches the entire attribute value
                    newValue = value;
                  }
                  value = newValue;
                }
              }
            }
            if (passedAllowedAttributesMapCheck) {
              if (options.allowedSchemesAppliedToAttributes.indexOf(a) !== -1) {
                if (naughtyHref(name, value)) {
                  delete frame.attribs[a];
                  return;
                }
              }
              if (name === 'iframe' && a === 'src') {
                var allowed = true;
                try {
                  // naughtyHref is in charge of whether protocol relative URLs
                  // are cool. We should just accept them
                  parsed = url.parse(value, false, true);
                  var isRelativeUrl = parsed && parsed.host === null && parsed.protocol === null;
                  if (isRelativeUrl) {
                    // default value of allowIframeRelativeUrls is true unless allowIframeHostnames specified
                    allowed = has(options, "allowIframeRelativeUrls") ? 
                      options.allowIframeRelativeUrls : !options.allowedIframeHostnames;
                  } else if (options.allowedIframeHostnames) {
                    allowed = options.allowedIframeHostnames.find(function (hostname) {
                      return hostname === parsed.hostname;
                    });
                  }
                } catch (e) {
                  // Unparseable iframe src
                  allowed = false;
                }
                if (!allowed) {
                  delete frame.attribs[a];
                  return;
                }
              } 
              if (a === 'srcset') {
                try {
                  parsed = srcset.parse(value);
                  each(parsed, function(value) {
                    if (naughtyHref('srcset', value.url)) {
                      value.evil = true;
                    }
                  });
                  parsed = filter(parsed, function(v) {
                    return !v.evil;
                  });
                  if (!parsed.length) {
                    delete frame.attribs[a];
                    return;
                  } else {
                    value = srcset.stringify(filter(parsed, function(v) {
                      return !v.evil;
                    }));
                    frame.attribs[a] = value;
                  }
                } catch (e) {
                  // Unparseable srcset
                  delete frame.attribs[a];
                  return;
                }
              }
              if (a === 'class') {
                value = filterClasses(value, allowedClassesMap[name]);
                if (!value.length) {
                  delete frame.attribs[a];
                  return;
                }
              }
              if (a === 'style') {
                try {
                  var abstractSyntaxTree = postcss.parse(name + " {" + value + "}");
                  var filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
  
                  value = stringifyStyleAttributes(filteredAST);
  
                  if(value.length === 0) {
                    delete frame.attribs[a];
                    return;
                  }
                } catch (e) {
                  delete frame.attribs[a];
                  return;
                }
              }
              result += ' ' + a;
              if (value.length) {
                result += '="' + escapeHtml(value, true) + '"';
              }
            } else {
              delete frame.attribs[a];
            }
          });
        }
        if (options.selfClosing.indexOf(name) !== -1) {
          result += " />";
        } else {
          result += ">";
          if (frame.innerText && !hasText && !options.textFilter) {
            result += frame.innerText;
          }
        }
      },
      ontext: function(text) {
        if (skipText) {
          return;
        }
        var lastFrame = stack[stack.length-1];
        var tag;
  
        if (lastFrame) {
          tag = lastFrame.tag;
          // If inner text was set by transform function then let's use it
          text = lastFrame.innerText !== undefined ? lastFrame.innerText : text;
        }
  
        if ((tag === 'script') || (tag === 'style')) {
          // htmlparser2 gives us these as-is. Escaping them ruins the content. Allowing
          // script tags is, by definition, game over for XSS protection, so if that's
          // your concern, don't allow them. The same is essentially true for style tags
          // which have their own collection of XSS vectors.
          result += text;
        } else {
          var escaped = escapeHtml(text, false);
          if (options.textFilter) {
            result += options.textFilter(escaped);
          } else {
            result += escaped;
          }
        }
        if (stack.length) {
             var frame = stack[stack.length - 1];
             frame.text += text;
        }
      },
      onclosetag: function(name) {
  
        if (skipText) {
          skipTextDepth--;
          if (!skipTextDepth) {
            skipText = false;
          } else {
            return;
          }
        }
  
        var frame = stack.pop();
        if (!frame) {
          // Do not crash on bad markup
          return;
        }
        skipText = false;
        depth--;
        if (skipMap[depth]) {
          delete skipMap[depth];
          frame.updateParentNodeText();
          return;
        }
  
        if (transformMap[depth]) {
          name = transformMap[depth];
          delete transformMap[depth];
        }
  
        if (options.exclusiveFilter && options.exclusiveFilter(frame)) {
           result = result.substr(0, frame.tagPosition);
           return;
        }
  
        frame.updateParentNodeText();
  
        if (options.selfClosing.indexOf(name) !== -1) {
           // Already output />
           return;
        }
  
        result += "</" + name + ">";
      }
    }, options.parser);
    parser.write(html);
    parser.end();
  
    return result;
  
    function escapeHtml(s, quote) {
      if (typeof(s) !== 'string') {
        s = s + '';
      }
      if (options.parser.decodeEntities) {
        s = s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\>/g, '&gt;');
        if (quote) {
          s = s.replace(/\"/g, '&quot;');
        }
      }
      // TODO: this is inadequate because it will pass `&0;`. This approach
      // will not work, each & must be considered with regard to whether it
      // is followed by a 100% syntactically valid entity or not, and escaped
      // if it is not. If this bothers you, don't set parser.decodeEntities
      // to false. (The default is true.)
      s = s.replace(/&(?![a-zA-Z0-9#]{1,20};)/g, '&amp;') // Match ampersands not part of existing HTML entity
        .replace(/</g, '&lt;')
        .replace(/\>/g, '&gt;');
      if (quote) {
        s = s.replace(/\"/g, '&quot;');
      }
      return s;
    }
  
    function naughtyHref(name, href) {
      // Browsers ignore character codes of 32 (space) and below in a surprising
      // number of situations. Start reading here:
      // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab
      href = href.replace(/[\x00-\x20]+/g, '');
      // Clobber any comments in URLs, which the browser might
      // interpret inside an XML data island, allowing
      // a javascript: URL to be snuck through
      href = href.replace(/<\!\-\-.*?\-\-\>/g, '');
      // Case insensitive so we don't get faked out by JAVASCRIPT #1
      var matches = href.match(/^([a-zA-Z]+)\:/);
      if (!matches) {
        // Protocol-relative URL starting with any combination of '/' and '\'
        if (href.match(/^[\/\\]{2}/)) {
          return !options.allowProtocolRelative;
        }
  
        // No scheme
        return false;
      }
      var scheme = matches[1].toLowerCase();
  
      if (has(options.allowedSchemesByTag, name)) {
        return options.allowedSchemesByTag[name].indexOf(scheme) === -1;
      }
  
      return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1;
    }
  
    /**
     * Filters user input css properties by whitelisted regex attributes.
     *
     * @param {object} abstractSyntaxTree  - Object representation of CSS attributes.
     * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
     * @param {object} allowedStyles       - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i).
     * @return {object}                    - Abstract Syntax Tree with filtered style attributes.
     */
    function filterCss(abstractSyntaxTree, allowedStyles) {
      if (!allowedStyles) {
        return abstractSyntaxTree;
      }
  
      var filteredAST = cloneDeep(abstractSyntaxTree);
      var astRules = abstractSyntaxTree.nodes[0];
      var selectedRule;
  
      // Merge global and tag-specific styles into new AST.
      if (allowedStyles[astRules.selector] && allowedStyles['*']) {
        selectedRule = mergeWith(
          cloneDeep(allowedStyles[astRules.selector]),
          allowedStyles['*'],
          function(objValue, srcValue) {
            if (Array.isArray(objValue)) {
              return objValue.concat(srcValue);
            }
          }
        );
      } else {
        selectedRule = allowedStyles[astRules.selector] || allowedStyles['*'];
      }
  
      if (selectedRule) {
        filteredAST.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []);
      }
  
      return filteredAST;
    }
  
    /**
     * Extracts the style attribues from an AbstractSyntaxTree and formats those
     * values in the inline style attribute format.
     *
     * @param  {AbstractSyntaxTree} filteredAST
     * @return {string}             - Example: "color:yellow;text-align:center;font-family:helvetica;"
     */
    function stringifyStyleAttributes(filteredAST) {
      return filteredAST.nodes[0].nodes
        .reduce(function(extractedAttributes, attributeObject) {
          extractedAttributes.push(
            attributeObject.prop + ':' + attributeObject.value
          );
          return extractedAttributes;
        }, [])
        .join(';');
    }
  
    /**
      * Filters the existing attributes for the given property. Discards any attributes
      * which don't match the whitelist.
      *
      * @param  {object} selectedRule             - Example: { color: red, font-family: helvetica }
      * @param  {array} allowedDeclarationsList   - List of declarations which pass whitelisting.
      * @param  {object} attributeObject          - Object representing the current css property.
      * @property {string} attributeObject.type   - Typically 'declaration'.
      * @property {string} attributeObject.prop   - The CSS property, i.e 'color'.
      * @property {string} attributeObject.value  - The corresponding value to the css property, i.e 'red'.
      * @return {function}                        - When used in Array.reduce, will return an array of Declaration objects
      */
    function filterDeclarations(selectedRule) {
      return function (allowedDeclarationsList, attributeObject) {
        // If this property is whitelisted...
        if (selectedRule.hasOwnProperty(attributeObject.prop)) {
          var matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
            return regularExpression.test(attributeObject.value);
          });
  
          if (matchesRegex) {
            allowedDeclarationsList.push(attributeObject);
          }
        }
        return allowedDeclarationsList;
      };
    }
  
    function filterClasses(classes, allowed) {
      if (!allowed) {
        // The class attribute is allowed without filtering on this tag
        return classes;
      }
      classes = classes.split(/\s+/);
      return classes.filter(function(clss) {
        return allowed.indexOf(clss) !== -1;
      }).join(' ');
    }
  }
func_code_string: function sanitizeHtml(html, options, _recursing) {
    var result = '';
  
    function Frame(tag, attribs) {
      var that = this;
      this.tag = tag;
      this.attribs = attribs || {};
      this.tagPosition = result.length;
      this.text = ''; // Node inner text
  
      this.updateParentNodeText = function() {
        if (stack.length) {
            var parentFrame = stack[stack.length - 1];
            parentFrame.text += that.text;
        }
      };
    }
  
    if (!options) {
      options = sanitizeHtml.defaults;
      options.parser = htmlParserDefaults;
    } else {
      options = extend(sanitizeHtml.defaults, options);
      if (options.parser) {
        options.parser = extend(htmlParserDefaults, options.parser);
      } else {
        options.parser = htmlParserDefaults;
      }
    }
  
    // Tags that contain something other than HTML, or where discarding
    // the text when the tag is disallowed makes sense for other reasons.
    // If we are not allowing these tags, we should drop their content too.
    // For other tags you would drop the tag but keep its content.
    var nonTextTagsArray = options.nonTextTags || [ 'script', 'style', 'textarea' ];
    var allowedAttributesMap;
    var allowedAttributesGlobMap;
    if(options.allowedAttributes) {
      allowedAttributesMap = {};
      allowedAttributesGlobMap = {};
      each(options.allowedAttributes, function(attributes, tag) {
        allowedAttributesMap[tag] = [];
        var globRegex = [];
        attributes.forEach(function(obj) {
          if(isString(obj) && obj.indexOf('*') >= 0) {
            globRegex.push(quoteRegexp(obj).replace(/\\\*/g, '.*'));
          } else {
            allowedAttributesMap[tag].push(obj);
          }
        });
        allowedAttributesGlobMap[tag] = new RegExp('^(' + globRegex.join('|') + ')$');
      });
    }
    var allowedClassesMap = {};
    each(options.allowedClasses, function(classes, tag) {
      // Implicitly allows the class attribute
      if(allowedAttributesMap) {
        if (!has(allowedAttributesMap, tag)) {
          allowedAttributesMap[tag] = [];
        }
        allowedAttributesMap[tag].push('class');
      }
  
      allowedClassesMap[tag] = classes;
    });
  
    var transformTagsMap = {};
    var transformTagsAll;
    each(options.transformTags, function(transform, tag) {
      var transFun;
      if (typeof transform === 'function') {
        transFun = transform;
      } else if (typeof transform === "string") {
        transFun = sanitizeHtml.simpleTransform(transform);
      }
      if (tag === '*') {
        transformTagsAll = transFun;
      } else {
        transformTagsMap[tag] = transFun;
      }
    });
  
    var depth = 0;
    var stack = [];
    var skipMap = {};
    var transformMap = {};
    var skipText = false;
    var skipTextDepth = 0;
  
    var parser = new htmlparser.Parser({
      onopentag: function(name, attribs) {
        if (skipText) {
          skipTextDepth++;
          return;
        }
        var frame = new Frame(name, attribs);
        stack.push(frame);
  
        var skip = false;
        var hasText = frame.text ? true : false;
        var transformedTag;
        if (has(transformTagsMap, name)) {
          transformedTag = transformTagsMap[name](name, attribs);
  
          frame.attribs = attribs = transformedTag.attribs;
  
          if (transformedTag.text !== undefined) {
            frame.innerText = transformedTag.text;
          }
  
          if (name !== transformedTag.tagName) {
            frame.name = name = transformedTag.tagName;
            transformMap[depth] = transformedTag.tagName;
          }
        }
        if (transformTagsAll) {
          transformedTag = transformTagsAll(name, attribs);
  
          frame.attribs = attribs = transformedTag.attribs;
          if (name !== transformedTag.tagName) {
            frame.name = name = transformedTag.tagName;
            transformMap[depth] = transformedTag.tagName;
          }
        }
  
        if (options.allowedTags && options.allowedTags.indexOf(name) === -1) {
          skip = true;
          if (nonTextTagsArray.indexOf(name) !== -1) {
            skipText = true;
            skipTextDepth = 1;
          }
          skipMap[depth] = true;
        }
        depth++;
        if (skip) {
          // We want the contents but not this tag
          return;
        }
        result += '<' + name;
        if (!allowedAttributesMap || has(allowedAttributesMap, name) || allowedAttributesMap['*']) {
          each(attribs, function(value, a) {
            if (!VALID_HTML_ATTRIBUTE_NAME.test(a)) {
              // This prevents part of an attribute name in the output from being
              // interpreted as the end of an attribute, or end of a tag.
              delete frame.attribs[a];
              return;
            }
            var parsed;
            // check allowedAttributesMap for the element and attribute and modify the value
            // as necessary if there are specific values defined.
            var passedAllowedAttributesMapCheck = false;
            if (!allowedAttributesMap ||
              (has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1 ) ||
              (allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1 ) ||
              (has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
              (allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {    
                passedAllowedAttributesMapCheck = true;
            } else if (allowedAttributesMap && allowedAttributesMap[name]) {
              for (const o of allowedAttributesMap[name]) {
                if (isPlainObject(o) && o.name && (o.name === a)) {
                  passedAllowedAttributesMapCheck = true;
                  var newValue = '';
                  if (o.multiple === true) {
                    // verify the values that are allowed
                    const splitStrArray = value.split(' ');
                    for (const s of splitStrArray) {
                      if (o.values.indexOf(s) !== -1) {
                        if (newValue === '') {
                          newValue = s;
                        } else {
                          newValue += ' ' + s;
                        }
                      }
                    }
                  } else if (o.values.indexOf(value) >= 0) {
                    // verified an allowed value matches the entire attribute value
                    newValue = value;
                  }
                  value = newValue;
                }
              }
            }
            if (passedAllowedAttributesMapCheck) {
              if (options.allowedSchemesAppliedToAttributes.indexOf(a) !== -1) {
                if (naughtyHref(name, value)) {
                  delete frame.attribs[a];
                  return;
                }
              }
              if (name === 'iframe' && a === 'src') {
                var allowed = true;
                try {
                  // naughtyHref is in charge of whether protocol relative URLs
                  // are cool. We should just accept them
                  parsed = url.parse(value, false, true);
                  var isRelativeUrl = parsed && parsed.host === null && parsed.protocol === null;
                  if (isRelativeUrl) {
                    // default value of allowIframeRelativeUrls is true unless allowIframeHostnames specified
                    allowed = has(options, "allowIframeRelativeUrls") ? 
                      options.allowIframeRelativeUrls : !options.allowedIframeHostnames;
                  } else if (options.allowedIframeHostnames) {
                    allowed = options.allowedIframeHostnames.find(function (hostname) {
                      return hostname === parsed.hostname;
                    });
                  }
                } catch (e) {
                  // Unparseable iframe src
                  allowed = false;
                }
                if (!allowed) {
                  delete frame.attribs[a];
                  return;
                }
              } 
              if (a === 'srcset') {
                try {
                  parsed = srcset.parse(value);
                  each(parsed, function(value) {
                    if (naughtyHref('srcset', value.url)) {
                      value.evil = true;
                    }
                  });
                  parsed = filter(parsed, function(v) {
                    return !v.evil;
                  });
                  if (!parsed.length) {
                    delete frame.attribs[a];
                    return;
                  } else {
                    value = srcset.stringify(filter(parsed, function(v) {
                      return !v.evil;
                    }));
                    frame.attribs[a] = value;
                  }
                } catch (e) {
                  // Unparseable srcset
                  delete frame.attribs[a];
                  return;
                }
              }
              if (a === 'class') {
                value = filterClasses(value, allowedClassesMap[name]);
                if (!value.length) {
                  delete frame.attribs[a];
                  return;
                }
              }
              if (a === 'style') {
                try {
                  var abstractSyntaxTree = postcss.parse(name + " {" + value + "}");
                  var filteredAST = filterCss(abstractSyntaxTree, options.allowedStyles);
  
                  value = stringifyStyleAttributes(filteredAST);
  
                  if(value.length === 0) {
                    delete frame.attribs[a];
                    return;
                  }
                } catch (e) {
                  delete frame.attribs[a];
                  return;
                }
              }
              result += ' ' + a;
              if (value.length) {
                result += '="' + escapeHtml(value, true) + '"';
              }
            } else {
              delete frame.attribs[a];
            }
          });
        }
        if (options.selfClosing.indexOf(name) !== -1) {
          result += " />";
        } else {
          result += ">";
          if (frame.innerText && !hasText && !options.textFilter) {
            result += frame.innerText;
          }
        }
      },
      ontext: function(text) {
        if (skipText) {
          return;
        }
        var lastFrame = stack[stack.length-1];
        var tag;
  
        if (lastFrame) {
          tag = lastFrame.tag;
          // If inner text was set by transform function then let's use it
          text = lastFrame.innerText !== undefined ? lastFrame.innerText : text;
        }
  
        if ((tag === 'script') || (tag === 'style')) {
          // htmlparser2 gives us these as-is. Escaping them ruins the content. Allowing
          // script tags is, by definition, game over for XSS protection, so if that's
          // your concern, don't allow them. The same is essentially true for style tags
          // which have their own collection of XSS vectors.
          result += text;
        } else {
          var escaped = escapeHtml(text, false);
          if (options.textFilter) {
            result += options.textFilter(escaped);
          } else {
            result += escaped;
          }
        }
        if (stack.length) {
             var frame = stack[stack.length - 1];
             frame.text += text;
        }
      },
      onclosetag: function(name) {
  
        if (skipText) {
          skipTextDepth--;
          if (!skipTextDepth) {
            skipText = false;
          } else {
            return;
          }
        }
  
        var frame = stack.pop();
        if (!frame) {
          // Do not crash on bad markup
          return;
        }
        skipText = false;
        depth--;
        if (skipMap[depth]) {
          delete skipMap[depth];
          frame.updateParentNodeText();
          return;
        }
  
        if (transformMap[depth]) {
          name = transformMap[depth];
          delete transformMap[depth];
        }
  
        if (options.exclusiveFilter && options.exclusiveFilter(frame)) {
           result = result.substr(0, frame.tagPosition);
           return;
        }
  
        frame.updateParentNodeText();
  
        if (options.selfClosing.indexOf(name) !== -1) {
           // Already output />
           return;
        }
  
        result += "</" + name + ">";
      }
    }, options.parser);
    parser.write(html);
    parser.end();
  
    return result;
  
    function escapeHtml(s, quote) {
      if (typeof(s) !== 'string') {
        s = s + '';
      }
      if (options.parser.decodeEntities) {
        s = s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\>/g, '&gt;');
        if (quote) {
          s = s.replace(/\"/g, '&quot;');
        }
      }
      // TODO: this is inadequate because it will pass `&0;`. This approach
      // will not work, each & must be considered with regard to whether it
      // is followed by a 100% syntactically valid entity or not, and escaped
      // if it is not. If this bothers you, don't set parser.decodeEntities
      // to false. (The default is true.)
      s = s.replace(/&(?![a-zA-Z0-9#]{1,20};)/g, '&amp;') // Match ampersands not part of existing HTML entity
        .replace(/</g, '&lt;')
        .replace(/\>/g, '&gt;');
      if (quote) {
        s = s.replace(/\"/g, '&quot;');
      }
      return s;
    }
  
    function naughtyHref(name, href) {
      // Browsers ignore character codes of 32 (space) and below in a surprising
      // number of situations. Start reading here:
      // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab
      href = href.replace(/[\x00-\x20]+/g, '');
      // Clobber any comments in URLs, which the browser might
      // interpret inside an XML data island, allowing
      // a javascript: URL to be snuck through
      href = href.replace(/<\!\-\-.*?\-\-\>/g, '');
      // Case insensitive so we don't get faked out by JAVASCRIPT #1
      var matches = href.match(/^([a-zA-Z]+)\:/);
      if (!matches) {
        // Protocol-relative URL starting with any combination of '/' and '\'
        if (href.match(/^[\/\\]{2}/)) {
          return !options.allowProtocolRelative;
        }
  
        // No scheme
        return false;
      }
      var scheme = matches[1].toLowerCase();
  
      if (has(options.allowedSchemesByTag, name)) {
        return options.allowedSchemesByTag[name].indexOf(scheme) === -1;
      }
  
      return !options.allowedSchemes || options.allowedSchemes.indexOf(scheme) === -1;
    }
  
    /**
     * Filters user input css properties by whitelisted regex attributes.
     *
     * @param {object} abstractSyntaxTree  - Object representation of CSS attributes.
     * @property {array[Declaration]} abstractSyntaxTree.nodes[0] - Each object cointains prop and value key, i.e { prop: 'color', value: 'red' }.
     * @param {object} allowedStyles       - Keys are properties (i.e color), value is list of permitted regex rules (i.e /green/i).
     * @return {object}                    - Abstract Syntax Tree with filtered style attributes.
     */
    function filterCss(abstractSyntaxTree, allowedStyles) {
      if (!allowedStyles) {
        return abstractSyntaxTree;
      }
  
      var filteredAST = cloneDeep(abstractSyntaxTree);
      var astRules = abstractSyntaxTree.nodes[0];
      var selectedRule;
  
      // Merge global and tag-specific styles into new AST.
      if (allowedStyles[astRules.selector] && allowedStyles['*']) {
        selectedRule = mergeWith(
          cloneDeep(allowedStyles[astRules.selector]),
          allowedStyles['*'],
          function(objValue, srcValue) {
            if (Array.isArray(objValue)) {
              return objValue.concat(srcValue);
            }
          }
        );
      } else {
        selectedRule = allowedStyles[astRules.selector] || allowedStyles['*'];
      }
  
      if (selectedRule) {
        filteredAST.nodes[0].nodes = astRules.nodes.reduce(filterDeclarations(selectedRule), []);
      }
  
      return filteredAST;
    }
  
    /**
     * Extracts the style attribues from an AbstractSyntaxTree and formats those
     * values in the inline style attribute format.
     *
     * @param  {AbstractSyntaxTree} filteredAST
     * @return {string}             - Example: "color:yellow;text-align:center;font-family:helvetica;"
     */
    function stringifyStyleAttributes(filteredAST) {
      return filteredAST.nodes[0].nodes
        .reduce(function(extractedAttributes, attributeObject) {
          extractedAttributes.push(
            attributeObject.prop + ':' + attributeObject.value
          );
          return extractedAttributes;
        }, [])
        .join(';');
    }
  
    /**
      * Filters the existing attributes for the given property. Discards any attributes
      * which don't match the whitelist.
      *
      * @param  {object} selectedRule             - Example: { color: red, font-family: helvetica }
      * @param  {array} allowedDeclarationsList   - List of declarations which pass whitelisting.
      * @param  {object} attributeObject          - Object representing the current css property.
      * @property {string} attributeObject.type   - Typically 'declaration'.
      * @property {string} attributeObject.prop   - The CSS property, i.e 'color'.
      * @property {string} attributeObject.value  - The corresponding value to the css property, i.e 'red'.
      * @return {function}                        - When used in Array.reduce, will return an array of Declaration objects
      */
    function filterDeclarations(selectedRule) {
      return function (allowedDeclarationsList, attributeObject) {
        // If this property is whitelisted...
        if (selectedRule.hasOwnProperty(attributeObject.prop)) {
          var matchesRegex = selectedRule[attributeObject.prop].some(function(regularExpression) {
            return regularExpression.test(attributeObject.value);
          });
  
          if (matchesRegex) {
            allowedDeclarationsList.push(attributeObject);
          }
        }
        return allowedDeclarationsList;
      };
    }
  
    function filterClasses(classes, allowed) {
      if (!allowed) {
        // The class attribute is allowed without filtering on this tag
        return classes;
      }
      classes = classes.split(/\s+/);
      return classes.filter(function(clss) {
        return allowed.indexOf(clss) !== -1;
      }).join(' ');
    }
  }
func_documentation_string: Ignore the _recursing flag; it's there for recursive invocation as a guard against this exploit: https://github.com/fb55/htmlparser2/issues/105