New:Socket for Asana Is Now Available.Learn more
Get Started

html-parse-stringify

Package Overview
Dependencies
Maintainers
4
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

html-parse-stringify - npm Package Compare versions

Comparing version
3.1.0
to
4.0.0
+420
dist/cjs/html-parse-stringify.js
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
// inlined from the former `void-elements` dependency, so the package has
// zero runtime dependencies. `!doctype`/`!DOCTYPE` are treated as void so a
// doctype parses as a childless node instead of swallowing the document.
const voidElements = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
'!doctype': true,
'!DOCTYPE': true,
};
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;
function parseTag(tag) {
const res = {
type: 'tag',
name: '',
voidElement: false,
attrs: {},
children: [],
};
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
if (tagMatch) {
res.name = tagMatch[1];
// void-element lookup stays case sensitive on purpose: react-i18next
// relies on `<Br>` NOT being treated as a void `<br>` (see 1df0f9d)
if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
res.voidElement = true;
}
// handle comment tag
if (res.name.startsWith('!--')) {
const endIndex = tag.indexOf('-->');
return {
type: 'comment',
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
}
}
}
const reg = new RegExp(attrRE);
let result = null;
for (;;) {
result = reg.exec(tag);
if (result === null) {
break
}
if (!result[0].trim()) {
continue
}
if (result[1]) {
const attr = result[1].trim();
// boolean attributes carry `null` so stringify can render them bare
// (`<input disabled/>` instead of `<input disabled=""/>`)
let arr = [attr, null];
const eq = attr.indexOf('=');
if (eq > -1) {
// split at the first `=` only, so `data-x=a=b` keeps its full value
arr = [attr.slice(0, eq), attr.slice(eq + 1)];
}
res.attrs[arr[0]] = arr[1];
reg.lastIndex--;
} else if (result[2]) {
res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
}
}
return res
}
// comments are matched as a whole (so `>` inside them is fine), everything
// else tag-shaped is matched by the second alternative
const tagRE = /<!--[\s\S]*?-->|<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
const tagNameRE = /<\/?([^\s]+?)[/\s>]/;
const whitespaceRE = /^\s*$/;
// tags whose content is raw text: nothing inside them is markup
const rawTextRE = /^(script|style)$/i;
// placeholder for `<` of tags rejected by options.allowedTags; restored to a
// literal `<` in text/attr/comment content after parsing (U+0000 cannot appear
// in sane input, and a collision would merely render as an extra `<`)
const sentinel = '\u0000';
// re-used obj for quick lookups of components
const empty = Object.create(null);
function restoreSentinels(nodes) {
nodes.forEach(function (node) {
if (node.type === 'text') {
node.content = node.content.split(sentinel).join('<');
return
}
if (node.type === 'comment') {
node.comment = node.comment.split(sentinel).join('<');
return
}
for (const key in node.attrs) {
const value = node.attrs[key];
if (typeof value === 'string' && value.indexOf(sentinel) > -1) {
node.attrs[key] = value.split(sentinel).join('<');
}
}
if (node.children.length) {
restoreSentinels(node.children);
}
});
}
function parse(html, options) {
const components = (options && options.components) || empty;
const allowedTags = options && options.allowedTags;
let restoreNeeded = false;
if (allowedTags) {
const isAllowed =
typeof allowedTags === 'function'
? allowedTags
: function (name) {
return allowedTags.indexOf(name) > -1
};
// neutralize tags whose name is not allowed, so they parse as text
html = html.replace(tagRE, function (tag) {
if (tag.startsWith('<!--')) return tag
const nameMatch = tag.match(tagNameRE);
if (nameMatch && isAllowed(nameMatch[1])) return tag
restoreNeeded = true;
return tag.split('<').join(sentinel)
});
}
const result = [];
const arr = [];
let current;
let level = -1;
let inComponent = false;
// while parsing raw-text content (script/style), tag-looking matches
// before this index belong to the content and must be skipped
let rawUntil = 0;
// lazily created lowercase copy for case-insensitive raw-text close-tag search
let htmlLower;
// handle text at top level
if (html.indexOf('<') !== 0) {
const end = html.indexOf('<');
result.push({
type: 'text',
content: end === -1 ? html : html.substring(0, end),
});
}
// collect matches with an exec loop instead of matchAll to keep ES5 API compat
const matches = [];
let m;
while ((m = tagRE.exec(html))) {
matches.push(m);
}
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
// comments match as a whole; their content must not trigger the
// mismatched-bracket split below
if (tag.startsWith('<!--')) return
// count brackets outside quoted attribute values, so `<` inside an
// attribute (e.g. title="1 < 2") can't trigger a bogus split
let lts = 0;
let gts = 0;
let secondLt = -1;
let quote = null;
for (let j = 0; j < tag.length; j++) {
const c = tag.charAt(j);
if (quote) {
if (c === quote) quote = null;
} else if (c === '"' || c === "'") {
quote = c;
} else if (c === '<') {
lts++;
if (lts === 2) secondLt = j;
} else if (c === '>') {
gts++;
}
}
// only split when the remainder is itself a valid tag start; otherwise
// a fragment like `< <!-->` desyncs the string-level isComment check
// from parseTag's name-based comment detection and crashes the walker
const validSplit =
secondLt > -1 && /[a-zA-Z0-9\-!/]/.test(tag.charAt(secondLt + 1));
if (lts > gts && validSplit) {
const firstPart = tag.substring(0, secondLt);
const secondPart = tag.substring(firstPart.length);
matches[i][0] = secondPart;
matches[i].index += firstPart.length;
}
});
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
const index = match.index;
if (index < rawUntil) return
if (inComponent) {
if (tag !== '</' + current.name + '>') {
return
} else {
inComponent = false;
}
}
const isOpen = tag.charAt(1) !== '/';
const isComment = tag.startsWith('<!--');
const start = index + tag.length;
const nextChar = html.charAt(start);
const nextMatch = matches[i + 1];
let isText;
if (nextChar === '<' && nextMatch) {
const nextTag = html.substring(start, nextMatch.index);
isText = nextTag.split('<').length > nextTag.split('>').length;
}
let parent;
if (isComment) {
const comment = parseTag(tag);
// if we're at root, push new base node
if (level < 0) {
result.push(comment);
return result
}
parent = arr[level];
parent.children.push(comment);
const text = html.slice(start, nextMatch ? nextMatch.index : undefined);
if (text.length > 0) {
parent.children.push({
type: 'text',
content: text,
});
}
return result
}
if (isOpen) {
level++;
current = parseTag(tag);
if (current.type === 'tag' && components[current.name]) {
current.type = 'component';
inComponent = true;
}
let isRawText = false;
if (
!inComponent &&
!current.voidElement &&
rawTextRE.test(current.name)
) {
// raw-text element: everything up to the matching close tag is one
// text child, regardless of what it looks like
isRawText = true;
htmlLower || (htmlLower = html.toLowerCase());
const closeIndex = htmlLower.indexOf(
'</' + current.name.toLowerCase() + '>',
start,
);
const contentEnd = closeIndex === -1 ? html.length : closeIndex;
const content = html.slice(start, contentEnd);
if (content) {
current.children.push({
type: 'text',
content,
});
}
rawUntil = contentEnd;
}
if (
!current.voidElement &&
!inComponent &&
!isRawText &&
nextChar &&
nextChar !== '<'
) {
// text content runs to the next actual tag match; stray `<`
// characters in between are part of the text
current.children.push({
type: 'text',
content: html.slice(start, nextMatch ? nextMatch.index : undefined),
});
}
// if we're at root, push new base node
if (level === 0) {
result.push(current);
}
parent = arr[level - 1];
if (parent) {
parent.children.push(current);
}
arr[level] = current;
}
if (!isOpen || current.voidElement) {
if (
level > -1 &&
(current.voidElement || current.name === tag.slice(2, -1))
) {
level--;
// move current up a level to match the end tag
current = level === -1 ? result : arr[level];
}
if (!inComponent && (nextChar !== '<' || isText) && nextChar) {
// trailing text node
// if we're at the root, push a base text node. otherwise add as
// a child to the current node.
parent = level === -1 ? result : arr[level].children;
// the text node runs to the next actual tag match; -1 means
// there's no tag after it (trailing text)
const end = nextMatch ? nextMatch.index : -1;
let content = html.slice(start, end === -1 ? undefined : end);
// if a node is nothing but whitespace, collapse it as the spec states:
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
if (whitespaceRE.test(content)) {
content = ' ';
}
// don't add whitespace-only text nodes if they would be trailing text nodes
// or if they would be leading whitespace-only text nodes:
// * end > -1 indicates this is not a trailing text node
// * leading node is when level is -1 and parent has length 0
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
parent.push({
type: 'text',
content,
});
}
}
}
});
if (restoreNeeded) {
restoreSentinels(result);
}
return result
}
function attrString(attrs) {
const buff = [];
for (const key in attrs) {
if (attrs[key] === null) {
// boolean attribute, render bare
buff.push(key);
} else {
// escape double quotes so values parsed from single-quoted or
// multiline attributes can't break out of the generated markup
buff.push(key + '="' + String(attrs[key]).replace(/"/g, '&quot;') + '"');
}
}
if (!buff.length) {
return ''
}
return ' ' + buff.join(' ')
}
function stringifyNode(buff, doc) {
switch (doc.type) {
case 'text':
return buff + doc.content
case 'tag': {
// a doctype is void but must not self-close: `<!DOCTYPE html>` not `<!DOCTYPE html/>`
const tagEnd =
doc.voidElement && doc.name.toLowerCase() !== '!doctype' ? '/>' : '>';
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + tagEnd;
if (doc.voidElement) {
return buff
}
return (
buff + doc.children.reduce(stringifyNode, '') + '</' + doc.name + '>'
)
}
case 'comment':
buff += '<!--' + doc.comment + '-->';
return buff
}
}
function stringify(doc) {
return doc.reduce(function (token, rootEl) {
return token + stringifyNode('', rootEl)
}, '')
}
var index = {
parse,
stringify,
};
exports.default = index;
exports.parse = parse;
exports.stringify = stringify;
// inlined from the former `void-elements` dependency, so the package has
// zero runtime dependencies. `!doctype`/`!DOCTYPE` are treated as void so a
// doctype parses as a childless node instead of swallowing the document.
const voidElements = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
'!doctype': true,
'!DOCTYPE': true,
};
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;
function parseTag(tag) {
const res = {
type: 'tag',
name: '',
voidElement: false,
attrs: {},
children: [],
};
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
if (tagMatch) {
res.name = tagMatch[1];
// void-element lookup stays case sensitive on purpose: react-i18next
// relies on `<Br>` NOT being treated as a void `<br>` (see 1df0f9d)
if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
res.voidElement = true;
}
// handle comment tag
if (res.name.startsWith('!--')) {
const endIndex = tag.indexOf('-->');
return {
type: 'comment',
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
}
}
}
const reg = new RegExp(attrRE);
let result = null;
for (;;) {
result = reg.exec(tag);
if (result === null) {
break
}
if (!result[0].trim()) {
continue
}
if (result[1]) {
const attr = result[1].trim();
// boolean attributes carry `null` so stringify can render them bare
// (`<input disabled/>` instead of `<input disabled=""/>`)
let arr = [attr, null];
const eq = attr.indexOf('=');
if (eq > -1) {
// split at the first `=` only, so `data-x=a=b` keeps its full value
arr = [attr.slice(0, eq), attr.slice(eq + 1)];
}
res.attrs[arr[0]] = arr[1];
reg.lastIndex--;
} else if (result[2]) {
res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
}
}
return res
}
// comments are matched as a whole (so `>` inside them is fine), everything
// else tag-shaped is matched by the second alternative
const tagRE = /<!--[\s\S]*?-->|<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
const tagNameRE = /<\/?([^\s]+?)[/\s>]/;
const whitespaceRE = /^\s*$/;
// tags whose content is raw text: nothing inside them is markup
const rawTextRE = /^(script|style)$/i;
// placeholder for `<` of tags rejected by options.allowedTags; restored to a
// literal `<` in text/attr/comment content after parsing (U+0000 cannot appear
// in sane input, and a collision would merely render as an extra `<`)
const sentinel = '\u0000';
// re-used obj for quick lookups of components
const empty = Object.create(null);
function restoreSentinels(nodes) {
nodes.forEach(function (node) {
if (node.type === 'text') {
node.content = node.content.split(sentinel).join('<');
return
}
if (node.type === 'comment') {
node.comment = node.comment.split(sentinel).join('<');
return
}
for (const key in node.attrs) {
const value = node.attrs[key];
if (typeof value === 'string' && value.indexOf(sentinel) > -1) {
node.attrs[key] = value.split(sentinel).join('<');
}
}
if (node.children.length) {
restoreSentinels(node.children);
}
});
}
function parse(html, options) {
const components = (options && options.components) || empty;
const allowedTags = options && options.allowedTags;
let restoreNeeded = false;
if (allowedTags) {
const isAllowed =
typeof allowedTags === 'function'
? allowedTags
: function (name) {
return allowedTags.indexOf(name) > -1
};
// neutralize tags whose name is not allowed, so they parse as text
html = html.replace(tagRE, function (tag) {
if (tag.startsWith('<!--')) return tag
const nameMatch = tag.match(tagNameRE);
if (nameMatch && isAllowed(nameMatch[1])) return tag
restoreNeeded = true;
return tag.split('<').join(sentinel)
});
}
const result = [];
const arr = [];
let current;
let level = -1;
let inComponent = false;
// while parsing raw-text content (script/style), tag-looking matches
// before this index belong to the content and must be skipped
let rawUntil = 0;
// lazily created lowercase copy for case-insensitive raw-text close-tag search
let htmlLower;
// handle text at top level
if (html.indexOf('<') !== 0) {
const end = html.indexOf('<');
result.push({
type: 'text',
content: end === -1 ? html : html.substring(0, end),
});
}
// collect matches with an exec loop instead of matchAll to keep ES5 API compat
const matches = [];
let m;
while ((m = tagRE.exec(html))) {
matches.push(m);
}
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
// comments match as a whole; their content must not trigger the
// mismatched-bracket split below
if (tag.startsWith('<!--')) return
// count brackets outside quoted attribute values, so `<` inside an
// attribute (e.g. title="1 < 2") can't trigger a bogus split
let lts = 0;
let gts = 0;
let secondLt = -1;
let quote = null;
for (let j = 0; j < tag.length; j++) {
const c = tag.charAt(j);
if (quote) {
if (c === quote) quote = null;
} else if (c === '"' || c === "'") {
quote = c;
} else if (c === '<') {
lts++;
if (lts === 2) secondLt = j;
} else if (c === '>') {
gts++;
}
}
// only split when the remainder is itself a valid tag start; otherwise
// a fragment like `< <!-->` desyncs the string-level isComment check
// from parseTag's name-based comment detection and crashes the walker
const validSplit =
secondLt > -1 && /[a-zA-Z0-9\-!/]/.test(tag.charAt(secondLt + 1));
if (lts > gts && validSplit) {
const firstPart = tag.substring(0, secondLt);
const secondPart = tag.substring(firstPart.length);
matches[i][0] = secondPart;
matches[i].index += firstPart.length;
}
});
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
const index = match.index;
if (index < rawUntil) return
if (inComponent) {
if (tag !== '</' + current.name + '>') {
return
} else {
inComponent = false;
}
}
const isOpen = tag.charAt(1) !== '/';
const isComment = tag.startsWith('<!--');
const start = index + tag.length;
const nextChar = html.charAt(start);
const nextMatch = matches[i + 1];
let isText;
if (nextChar === '<' && nextMatch) {
const nextTag = html.substring(start, nextMatch.index);
isText = nextTag.split('<').length > nextTag.split('>').length;
}
let parent;
if (isComment) {
const comment = parseTag(tag);
// if we're at root, push new base node
if (level < 0) {
result.push(comment);
return result
}
parent = arr[level];
parent.children.push(comment);
const text = html.slice(start, nextMatch ? nextMatch.index : undefined);
if (text.length > 0) {
parent.children.push({
type: 'text',
content: text,
});
}
return result
}
if (isOpen) {
level++;
current = parseTag(tag);
if (current.type === 'tag' && components[current.name]) {
current.type = 'component';
inComponent = true;
}
let isRawText = false;
if (
!inComponent &&
!current.voidElement &&
rawTextRE.test(current.name)
) {
// raw-text element: everything up to the matching close tag is one
// text child, regardless of what it looks like
isRawText = true;
htmlLower || (htmlLower = html.toLowerCase());
const closeIndex = htmlLower.indexOf(
'</' + current.name.toLowerCase() + '>',
start,
);
const contentEnd = closeIndex === -1 ? html.length : closeIndex;
const content = html.slice(start, contentEnd);
if (content) {
current.children.push({
type: 'text',
content,
});
}
rawUntil = contentEnd;
}
if (
!current.voidElement &&
!inComponent &&
!isRawText &&
nextChar &&
nextChar !== '<'
) {
// text content runs to the next actual tag match; stray `<`
// characters in between are part of the text
current.children.push({
type: 'text',
content: html.slice(start, nextMatch ? nextMatch.index : undefined),
});
}
// if we're at root, push new base node
if (level === 0) {
result.push(current);
}
parent = arr[level - 1];
if (parent) {
parent.children.push(current);
}
arr[level] = current;
}
if (!isOpen || current.voidElement) {
if (
level > -1 &&
(current.voidElement || current.name === tag.slice(2, -1))
) {
level--;
// move current up a level to match the end tag
current = level === -1 ? result : arr[level];
}
if (!inComponent && (nextChar !== '<' || isText) && nextChar) {
// trailing text node
// if we're at the root, push a base text node. otherwise add as
// a child to the current node.
parent = level === -1 ? result : arr[level].children;
// the text node runs to the next actual tag match; -1 means
// there's no tag after it (trailing text)
const end = nextMatch ? nextMatch.index : -1;
let content = html.slice(start, end === -1 ? undefined : end);
// if a node is nothing but whitespace, collapse it as the spec states:
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
if (whitespaceRE.test(content)) {
content = ' ';
}
// don't add whitespace-only text nodes if they would be trailing text nodes
// or if they would be leading whitespace-only text nodes:
// * end > -1 indicates this is not a trailing text node
// * leading node is when level is -1 and parent has length 0
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
parent.push({
type: 'text',
content,
});
}
}
}
});
if (restoreNeeded) {
restoreSentinels(result);
}
return result
}
function attrString(attrs) {
const buff = [];
for (const key in attrs) {
if (attrs[key] === null) {
// boolean attribute, render bare
buff.push(key);
} else {
// escape double quotes so values parsed from single-quoted or
// multiline attributes can't break out of the generated markup
buff.push(key + '="' + String(attrs[key]).replace(/"/g, '&quot;') + '"');
}
}
if (!buff.length) {
return ''
}
return ' ' + buff.join(' ')
}
function stringifyNode(buff, doc) {
switch (doc.type) {
case 'text':
return buff + doc.content
case 'tag': {
// a doctype is void but must not self-close: `<!DOCTYPE html>` not `<!DOCTYPE html/>`
const tagEnd =
doc.voidElement && doc.name.toLowerCase() !== '!doctype' ? '/>' : '>';
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + tagEnd;
if (doc.voidElement) {
return buff
}
return (
buff + doc.children.reduce(stringifyNode, '') + '</' + doc.name + '>'
)
}
case 'comment':
buff += '<!--' + doc.comment + '-->';
return buff
}
}
function stringify(doc) {
return doc.reduce(function (token, rootEl) {
return token + stringifyNode('', rootEl)
}, '')
}
var index = {
parse,
stringify,
};
export { index as default, parse, stringify };
{"type":"module"}
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.HTMLParseStringify = {}));
})(this, (function (exports) { 'use strict';
// inlined from the former `void-elements` dependency, so the package has
// zero runtime dependencies. `!doctype`/`!DOCTYPE` are treated as void so a
// doctype parses as a childless node instead of swallowing the document.
const voidElements = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
'!doctype': true,
'!DOCTYPE': true,
};
const attrRE = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;
function parseTag(tag) {
const res = {
type: 'tag',
name: '',
voidElement: false,
attrs: {},
children: [],
};
const tagMatch = tag.match(/<\/?([^\s]+?)[/\s>]/);
if (tagMatch) {
res.name = tagMatch[1];
// void-element lookup stays case sensitive on purpose: react-i18next
// relies on `<Br>` NOT being treated as a void `<br>` (see 1df0f9d)
if (voidElements[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {
res.voidElement = true;
}
// handle comment tag
if (res.name.startsWith('!--')) {
const endIndex = tag.indexOf('-->');
return {
type: 'comment',
comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',
}
}
}
const reg = new RegExp(attrRE);
let result = null;
for (;;) {
result = reg.exec(tag);
if (result === null) {
break
}
if (!result[0].trim()) {
continue
}
if (result[1]) {
const attr = result[1].trim();
// boolean attributes carry `null` so stringify can render them bare
// (`<input disabled/>` instead of `<input disabled=""/>`)
let arr = [attr, null];
const eq = attr.indexOf('=');
if (eq > -1) {
// split at the first `=` only, so `data-x=a=b` keeps its full value
arr = [attr.slice(0, eq), attr.slice(eq + 1)];
}
res.attrs[arr[0]] = arr[1];
reg.lastIndex--;
} else if (result[2]) {
res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1);
}
}
return res
}
// comments are matched as a whole (so `>` inside them is fine), everything
// else tag-shaped is matched by the second alternative
const tagRE = /<!--[\s\S]*?-->|<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
const tagNameRE = /<\/?([^\s]+?)[/\s>]/;
const whitespaceRE = /^\s*$/;
// tags whose content is raw text: nothing inside them is markup
const rawTextRE = /^(script|style)$/i;
// placeholder for `<` of tags rejected by options.allowedTags; restored to a
// literal `<` in text/attr/comment content after parsing (U+0000 cannot appear
// in sane input, and a collision would merely render as an extra `<`)
const sentinel = '\u0000';
// re-used obj for quick lookups of components
const empty = Object.create(null);
function restoreSentinels(nodes) {
nodes.forEach(function (node) {
if (node.type === 'text') {
node.content = node.content.split(sentinel).join('<');
return
}
if (node.type === 'comment') {
node.comment = node.comment.split(sentinel).join('<');
return
}
for (const key in node.attrs) {
const value = node.attrs[key];
if (typeof value === 'string' && value.indexOf(sentinel) > -1) {
node.attrs[key] = value.split(sentinel).join('<');
}
}
if (node.children.length) {
restoreSentinels(node.children);
}
});
}
function parse(html, options) {
const components = (options && options.components) || empty;
const allowedTags = options && options.allowedTags;
let restoreNeeded = false;
if (allowedTags) {
const isAllowed =
typeof allowedTags === 'function'
? allowedTags
: function (name) {
return allowedTags.indexOf(name) > -1
};
// neutralize tags whose name is not allowed, so they parse as text
html = html.replace(tagRE, function (tag) {
if (tag.startsWith('<!--')) return tag
const nameMatch = tag.match(tagNameRE);
if (nameMatch && isAllowed(nameMatch[1])) return tag
restoreNeeded = true;
return tag.split('<').join(sentinel)
});
}
const result = [];
const arr = [];
let current;
let level = -1;
let inComponent = false;
// while parsing raw-text content (script/style), tag-looking matches
// before this index belong to the content and must be skipped
let rawUntil = 0;
// lazily created lowercase copy for case-insensitive raw-text close-tag search
let htmlLower;
// handle text at top level
if (html.indexOf('<') !== 0) {
const end = html.indexOf('<');
result.push({
type: 'text',
content: end === -1 ? html : html.substring(0, end),
});
}
// collect matches with an exec loop instead of matchAll to keep ES5 API compat
const matches = [];
let m;
while ((m = tagRE.exec(html))) {
matches.push(m);
}
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
// comments match as a whole; their content must not trigger the
// mismatched-bracket split below
if (tag.startsWith('<!--')) return
// count brackets outside quoted attribute values, so `<` inside an
// attribute (e.g. title="1 < 2") can't trigger a bogus split
let lts = 0;
let gts = 0;
let secondLt = -1;
let quote = null;
for (let j = 0; j < tag.length; j++) {
const c = tag.charAt(j);
if (quote) {
if (c === quote) quote = null;
} else if (c === '"' || c === "'") {
quote = c;
} else if (c === '<') {
lts++;
if (lts === 2) secondLt = j;
} else if (c === '>') {
gts++;
}
}
// only split when the remainder is itself a valid tag start; otherwise
// a fragment like `< <!-->` desyncs the string-level isComment check
// from parseTag's name-based comment detection and crashes the walker
const validSplit =
secondLt > -1 && /[a-zA-Z0-9\-!/]/.test(tag.charAt(secondLt + 1));
if (lts > gts && validSplit) {
const firstPart = tag.substring(0, secondLt);
const secondPart = tag.substring(firstPart.length);
matches[i][0] = secondPart;
matches[i].index += firstPart.length;
}
});
matches.forEach(function (match, i) {
const tag = match[0];
if (!tag) return
const index = match.index;
if (index < rawUntil) return
if (inComponent) {
if (tag !== '</' + current.name + '>') {
return
} else {
inComponent = false;
}
}
const isOpen = tag.charAt(1) !== '/';
const isComment = tag.startsWith('<!--');
const start = index + tag.length;
const nextChar = html.charAt(start);
const nextMatch = matches[i + 1];
let isText;
if (nextChar === '<' && nextMatch) {
const nextTag = html.substring(start, nextMatch.index);
isText = nextTag.split('<').length > nextTag.split('>').length;
}
let parent;
if (isComment) {
const comment = parseTag(tag);
// if we're at root, push new base node
if (level < 0) {
result.push(comment);
return result
}
parent = arr[level];
parent.children.push(comment);
const text = html.slice(start, nextMatch ? nextMatch.index : undefined);
if (text.length > 0) {
parent.children.push({
type: 'text',
content: text,
});
}
return result
}
if (isOpen) {
level++;
current = parseTag(tag);
if (current.type === 'tag' && components[current.name]) {
current.type = 'component';
inComponent = true;
}
let isRawText = false;
if (
!inComponent &&
!current.voidElement &&
rawTextRE.test(current.name)
) {
// raw-text element: everything up to the matching close tag is one
// text child, regardless of what it looks like
isRawText = true;
htmlLower || (htmlLower = html.toLowerCase());
const closeIndex = htmlLower.indexOf(
'</' + current.name.toLowerCase() + '>',
start,
);
const contentEnd = closeIndex === -1 ? html.length : closeIndex;
const content = html.slice(start, contentEnd);
if (content) {
current.children.push({
type: 'text',
content,
});
}
rawUntil = contentEnd;
}
if (
!current.voidElement &&
!inComponent &&
!isRawText &&
nextChar &&
nextChar !== '<'
) {
// text content runs to the next actual tag match; stray `<`
// characters in between are part of the text
current.children.push({
type: 'text',
content: html.slice(start, nextMatch ? nextMatch.index : undefined),
});
}
// if we're at root, push new base node
if (level === 0) {
result.push(current);
}
parent = arr[level - 1];
if (parent) {
parent.children.push(current);
}
arr[level] = current;
}
if (!isOpen || current.voidElement) {
if (
level > -1 &&
(current.voidElement || current.name === tag.slice(2, -1))
) {
level--;
// move current up a level to match the end tag
current = level === -1 ? result : arr[level];
}
if (!inComponent && (nextChar !== '<' || isText) && nextChar) {
// trailing text node
// if we're at the root, push a base text node. otherwise add as
// a child to the current node.
parent = level === -1 ? result : arr[level].children;
// the text node runs to the next actual tag match; -1 means
// there's no tag after it (trailing text)
const end = nextMatch ? nextMatch.index : -1;
let content = html.slice(start, end === -1 ? undefined : end);
// if a node is nothing but whitespace, collapse it as the spec states:
// https://www.w3.org/TR/html4/struct/text.html#h-9.1
if (whitespaceRE.test(content)) {
content = ' ';
}
// don't add whitespace-only text nodes if they would be trailing text nodes
// or if they would be leading whitespace-only text nodes:
// * end > -1 indicates this is not a trailing text node
// * leading node is when level is -1 and parent has length 0
if ((end > -1 && level + parent.length >= 0) || content !== ' ') {
parent.push({
type: 'text',
content,
});
}
}
}
});
if (restoreNeeded) {
restoreSentinels(result);
}
return result
}
function attrString(attrs) {
const buff = [];
for (const key in attrs) {
if (attrs[key] === null) {
// boolean attribute, render bare
buff.push(key);
} else {
// escape double quotes so values parsed from single-quoted or
// multiline attributes can't break out of the generated markup
buff.push(key + '="' + String(attrs[key]).replace(/"/g, '&quot;') + '"');
}
}
if (!buff.length) {
return ''
}
return ' ' + buff.join(' ')
}
function stringifyNode(buff, doc) {
switch (doc.type) {
case 'text':
return buff + doc.content
case 'tag': {
// a doctype is void but must not self-close: `<!DOCTYPE html>` not `<!DOCTYPE html/>`
const tagEnd =
doc.voidElement && doc.name.toLowerCase() !== '!doctype' ? '/>' : '>';
buff += '<' + doc.name + (doc.attrs ? attrString(doc.attrs) : '') + tagEnd;
if (doc.voidElement) {
return buff
}
return (
buff + doc.children.reduce(stringifyNode, '') + '</' + doc.name + '>'
)
}
case 'comment':
buff += '<!--' + doc.comment + '-->';
return buff
}
}
function stringify(doc) {
return doc.reduce(function (token, rootEl) {
return token + stringifyNode('', rootEl)
}, '')
}
var index = {
parse,
stringify,
};
exports.default = index;
exports.parse = parse;
exports.stringify = stringify;
Object.defineProperty(exports, '__esModule', { value: true });
}));
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).HTMLParseStringify={})}(this,function(t){"use strict";const e={area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,"!doctype":!0,"!DOCTYPE":!0},n=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;function s(t){const s={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},i=t.match(/<\/?([^\s]+?)[/\s>]/);if(i&&(s.name=i[1],(e[i[1]]||"/"===t.charAt(t.length-2))&&(s.voidElement=!0),s.name.startsWith("!--"))){const e=t.indexOf("--\x3e");return{type:"comment",comment:-1!==e?t.slice(4,e):""}}const c=new RegExp(n);let o=null;for(;o=c.exec(t),null!==o;)if(o[0].trim())if(o[1]){const t=o[1].trim();let e=[t,null];const n=t.indexOf("=");n>-1&&(e=[t.slice(0,n),t.slice(n+1)]),s.attrs[e[0]]=e[1],c.lastIndex--}else o[2]&&(s.attrs[o[2]]=o[3].trim().substring(1,o[3].length-1));return s}const i=/<!--[\s\S]*?-->|<[a-zA-Z0-9\-!/](?:"[^"]*"|'[^']*'|[^'">])*>/g,c=/<\/?([^\s]+?)[/\s>]/,o=/^\s*$/,r=/^(script|style)$/i,l="\0",u=Object.create(null);function f(t){t.forEach(function(t){if("text"!==t.type)if("comment"!==t.type){for(const e in t.attrs){const n=t.attrs[e];"string"==typeof n&&n.indexOf(l)>-1&&(t.attrs[e]=n.split(l).join("<"))}t.children.length&&f(t.children)}else t.comment=t.comment.split(l).join("<");else t.content=t.content.split(l).join("<")})}function a(t,e){const n=e&&e.components||u,a=e&&e.allowedTags;let h=!1;if(a){const e="function"==typeof a?a:function(t){return a.indexOf(t)>-1};t=t.replace(i,function(t){if(t.startsWith("\x3c!--"))return t;const n=t.match(c);return n&&e(n[1])?t:(h=!0,t.split("<").join(l))})}const d=[],p=[];let m,g,x=-1,y=!1,b=0;if(0!==t.indexOf("<")){const e=t.indexOf("<");d.push({type:"text",content:-1===e?t:t.substring(0,e)})}const v=[];let E;for(;E=i.exec(t);)v.push(E);return v.forEach(function(t,e){const n=t[0];if(!n)return;if(n.startsWith("\x3c!--"))return;let s=0,i=0,c=-1,o=null;for(let t=0;t<n.length;t++){const e=n.charAt(t);o?e===o&&(o=null):'"'===e||"'"===e?o=e:"<"===e?(s++,2===s&&(c=t)):">"===e&&i++}const r=c>-1&&/[a-zA-Z0-9\-!/]/.test(n.charAt(c+1));if(s>i&&r){const t=n.substring(0,c),s=n.substring(t.length);v[e][0]=s,v[e].index+=t.length}}),v.forEach(function(e,i){const c=e[0];if(!c)return;const l=e.index;if(l<b)return;if(y){if(c!=="</"+m.name+">")return;y=!1}const u="/"!==c.charAt(1),f=c.startsWith("\x3c!--"),a=l+c.length,h=t.charAt(a),E=v[i+1];let O,j;if("<"===h&&E){const e=t.substring(a,E.index);O=e.split("<").length>e.split(">").length}if(f){const e=s(c);if(x<0)return d.push(e),d;j=p[x],j.children.push(e);const n=t.slice(a,E?E.index:void 0);return n.length>0&&j.children.push({type:"text",content:n}),d}if(u){x++,m=s(c),"tag"===m.type&&n[m.name]&&(m.type="component",y=!0);let e=!1;if(!y&&!m.voidElement&&r.test(m.name)){e=!0,g||(g=t.toLowerCase());const n=g.indexOf("</"+m.name.toLowerCase()+">",a),s=-1===n?t.length:n,i=t.slice(a,s);i&&m.children.push({type:"text",content:i}),b=s}m.voidElement||y||e||!h||"<"===h||m.children.push({type:"text",content:t.slice(a,E?E.index:void 0)}),0===x&&d.push(m),j=p[x-1],j&&j.children.push(m),p[x]=m}if((!u||m.voidElement)&&(x>-1&&(m.voidElement||m.name===c.slice(2,-1))&&(x--,m=-1===x?d:p[x]),!y&&("<"!==h||O)&&h)){j=-1===x?d:p[x].children;const e=E?E.index:-1;let n=t.slice(a,-1===e?void 0:e);o.test(n)&&(n=" "),(e>-1&&x+j.length>=0||" "!==n)&&j.push({type:"text",content:n})}}),h&&f(d),d}function h(t,e){switch(e.type){case"text":return t+e.content;case"tag":{const n=e.voidElement&&"!doctype"!==e.name.toLowerCase()?"/>":">";return t+="<"+e.name+(e.attrs?function(t){const e=[];for(const n in t)null===t[n]?e.push(n):e.push(n+'="'+String(t[n]).replace(/"/g,"&quot;")+'"');return e.length?" "+e.join(" "):""}(e.attrs):"")+n,e.voidElement?t:t+e.children.reduce(h,"")+"</"+e.name+">"}case"comment":return t+="\x3c!--"+e.comment+"--\x3e"}}function d(t){return t.reduce(function(t,e){return t+h("",e)},"")}var p={parse:a,stringify:d};t.default=p,t.parse=a,t.stringify=d,Object.defineProperty(t,"__esModule",{value:!0})});
export interface TagNode {
type: 'tag'
name: string
voidElement: boolean
/** boolean attributes (e.g. `disabled`) have the value `null` */
attrs: Record<string, string | null>
children: ASTNode[]
}
export interface TextNode {
type: 'text'
content: string
}
export interface CommentNode {
type: 'comment'
comment: string
}
export interface ComponentNode {
type: 'component'
name: string
voidElement: boolean
attrs: Record<string, string | null>
children: ASTNode[]
}
export type ASTNode = TagNode | TextNode | CommentNode | ComponentNode
/** @deprecated use ASTNode */
export type Node = ASTNode
export interface ParseOptions {
/** tag names that should be treated as components (children are not parsed) */
components?: Record<string, unknown>
/**
* When set, only tags with these names are parsed as markup; any other
* tag-shaped input is kept as literal text. Either an array of names or a
* predicate receiving the tag name. Comments are always parsed.
*/
allowedTags?: string[] | ((name: string) => boolean)
}
export declare function parse(html: string, options?: ParseOptions): ASTNode[]
export declare function stringify(doc: ASTNode[]): string
declare const HTML: {
parse: typeof parse
stringify: typeof stringify
}
export default HTML
export interface TagNode {
type: 'tag'
name: string
voidElement: boolean
/** boolean attributes (e.g. `disabled`) have the value `null` */
attrs: Record<string, string | null>
children: ASTNode[]
}
export interface TextNode {
type: 'text'
content: string
}
export interface CommentNode {
type: 'comment'
comment: string
}
export interface ComponentNode {
type: 'component'
name: string
voidElement: boolean
attrs: Record<string, string | null>
children: ASTNode[]
}
export type ASTNode = TagNode | TextNode | CommentNode | ComponentNode
/** @deprecated use ASTNode */
export type Node = ASTNode
export interface ParseOptions {
/** tag names that should be treated as components (children are not parsed) */
components?: Record<string, unknown>
/**
* When set, only tags with these names are parsed as markup; any other
* tag-shaped input is kept as literal text. Either an array of names or a
* predicate receiving the tag name. Comments are always parsed.
*/
allowedTags?: string[] | ((name: string) => boolean)
}
export declare function parse(html: string, options?: ParseOptions): ASTNode[]
export declare function stringify(doc: ASTNode[]): string
declare const HTML: {
parse: typeof parse
stringify: typeof stringify
}
export default HTML
+1
-0
MIT License
Copyright (c) 2025 Henrik Joreteg <henrik@joreteg.com>
Copyright (c) 2026-present i18next (https://github.com/i18next)

@@ -5,0 +6,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy

+52
-32
{
"name": "html-parse-stringify",
"description": "Parses well-formed HTML (meaning all tags closed) into an AST and back. quickly.",
"version": "3.1.0",
"version": "4.0.0",
"author": "Henrik Joreteg <henrik@joreteg.com>",
"contributors": [
"Adriano Raiano <adriano@raiano.ch>"
],
"license": "MIT",
"homepage": "https://github.com/i18next/html-parse-stringify",
"repository": {
"type": "git",
"url": "git+https://github.com/i18next/html-parse-stringify.git"
},
"bugs": {
"url": "https://github.com/henrikjoreteg/html-parse-stringify/issues"
"url": "https://github.com/i18next/html-parse-stringify/issues"
},
"dependencies": {
"void-elements": "3.1.0"
},
"devDependencies": {
"esm": "3.2.25",
"microbundle": "0.12.2",
"prettier": "2.0.5",
"tap-spec": "2.1.2",
"tape": "5.0.1"
},
"files": [
"dist",
"html-parse-stringify.d.ts"
],
"homepage": "https://github.com/henrikjoreteg/html-parse-stringify",
"funding": "https://locize.com",
"keywords": [

@@ -30,8 +25,35 @@ "ast",

],
"license": "MIT",
"main": "dist/html-parse-stringify.js",
"module": "dist/html-parse-stringify.module.js",
"main": "./dist/cjs/html-parse-stringify.js",
"module": "./dist/esm/html-parse-stringify.js",
"types": "./index.d.ts",
"unpkg": "./dist/umd/html-parse-stringify.min.js",
"source": "src/index.js",
"unpkg": "dist/html-parse-stringify.umd.js",
"types": "html-parse-stringify.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./index.d.mts",
"default": "./dist/esm/html-parse-stringify.js"
},
"require": {
"types": "./index.d.ts",
"default": "./dist/cjs/html-parse-stringify.js"
}
}
},
"files": [
"dist",
"index.d.ts",
"index.d.mts"
],
"scripts": {
"lint": "eslint src test",
"format": "prettier \"{src,test}/**/*.js\" \"*.{json,md,mjs}\" --check",
"format:fix": "prettier \"{src,test}/**/*.js\" \"*.{json,md,mjs}\" --write",
"test": "vitest --run",
"build": "rimraf dist && rollup -c && node -e \"require('fs').writeFileSync('dist/esm/package.json', JSON.stringify({ type: 'module' }))\"",
"prepublishOnly": "npm run lint && npm run test && npm run build",
"preversion": "npm run lint && npm run test && npm run build && git push",
"postversion": "git push && git push --tags"
},
"prettier": {

@@ -42,13 +64,11 @@ "arrowParens": "avoid",

},
"repository": {
"type": "git",
"url": "https://github.com/henrikjoreteg/html-parse-stringify"
},
"scripts": {
"build": "microbundle",
"format": "prettier --write .",
"prebuild": "rm -rf dist",
"prepublish": "npm run build",
"test": "tape -r esm test/* | tap-spec"
"devDependencies": {
"@rollup/plugin-terser": "1.0.0",
"eslint": "9.39.5",
"neostandard": "0.13.0",
"prettier": "3.9.6",
"rimraf": "6.1.3",
"rollup": "4.62.2",
"vitest": "4.1.10"
}
}
+43
-18
# html-parse-stringify
> **Note:** development of this package continues at [i18next/html-parse-stringify](https://github.com/i18next/html-parse-stringify), where version 4.x and later are maintained. 3.1.0 is the final release from this repository; the npm package name stays `html-parse-stringify`. See [issue #65](https://github.com/HenrikJoreteg/html-parse-stringify/issues/65) for the background.
[![CI](https://github.com/i18next/html-parse-stringify/actions/workflows/ci.yml/badge.svg)](https://github.com/i18next/html-parse-stringify/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/html-parse-stringify.svg)](https://www.npmjs.com/package/html-parse-stringify)
> **Maintenance home.** This is the maintained continuation of [HenrikJoreteg/html-parse-stringify](https://github.com/HenrikJoreteg/html-parse-stringify), created by [Henrik Joreteg](https://github.com/HenrikJoreteg). It is maintained by the [i18next](https://github.com/i18next) community (it powers `<Trans>` in react-i18next); see [the original repo's issue #65](https://github.com/HenrikJoreteg/html-parse-stringify/issues/65) for the history. The npm package name is unchanged: `html-parse-stringify`. See [CHANGELOG.md](CHANGELOG.md) for what changed in 4.0.0 and how to migrate.
This is an _experimental lightweight approach_ to enable quickly parsing HTML into an AST and stringify'ing it back to the original string.

@@ -35,2 +38,15 @@

## installation
```bash
npm install html-parse-stringify
```
```js
// ESM
import HTML, { parse, stringify } from 'html-parse-stringify'
// CommonJS
const HTML = require('html-parse-stringify')
```
It has two methods:

@@ -43,4 +59,13 @@

Takes a string of HTML and turns it into an AST, the only option you can currently pass is an object of registered `components` whose children will be ignored when generating the AST.
Takes a string of HTML and turns it into an AST. Two options can be passed in the second argument:
- `components`: an object of registered components; children of these components will be ignored when generating the AST.
- `allowedTags`: an array of tag names, or a predicate `(name) => boolean`. When set, only tags with these names are parsed as markup; any other tag-shaped input is kept as literal text (useful when parsing user-facing copy where `<div>` may just be text). Comments are always parsed.
```js
HTML.parse('Use <div> with <b>bold</b>', { allowedTags: ['b'] })
// -> [ { type: 'text', content: 'Use <div> with ' },
// { type: 'tag', name: 'b', ... children: [{ type: 'text', content: 'bold' }] } ]
```
## `.stringify(AST)`

@@ -55,9 +80,9 @@

```js
var HTML = require('html-parse-stringify')
import HTML from 'html-parse-stringify'
// this html:
var html = '<div class="oh"><p>hi</p></div>'
const html = '<div class="oh"><p>hi</p></div>'
// becomes this AST:
var ast = HTML.parse(html)
const ast = HTML.parse(html)

@@ -67,3 +92,3 @@ console.log(ast)

{
// can be `tag`, `text` or `component`
// can be `tag`, `text`, `comment` or `component`
type: 'tag',

@@ -117,3 +142,3 @@

- `name` - tag name, such as 'div'
- `attrs` - an object of key/value pairs. If an attribute has multiple space-separated items such as classes, they'll still be in a single string, for example: `class: "class1 class2"`
- `attrs` - an object of key/value pairs. If an attribute has multiple space-separated items such as classes, they'll still be in a single string, for example: `class: "class1 class2"`. Boolean attributes (like `disabled`) have the value `null`.
- `voidElement` - `true` or `false`. Whether this tag is a known void element as defined by [spec](http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements).

@@ -129,4 +154,11 @@ - `children` - array of child nodes. Note that any continuous string of text is a text node child, see below.

### 3. component
### 3. comment
properties:
- `type` - will always be `comment` for this type of node
- `comment` - the comment's content
### 4. component
If you pass an object of `components` as part of the `options` object passed as the second argument to `.parse()` then the AST won't keep parsing that branch of the DOM tree when it one of those registered components.

@@ -146,12 +178,3 @@

- `3.1.0` Maintenance release, merging long-standing community PRs: LICENSE file shipped in the npm package (#66 by @monholm, closes #61), text containing `<` no longer truncated (#64, closes #59), multi-line attribute values (#63 by @steffanhalv, closes #62), TypeScript declaration shipped and improved (#51/#52 by @jiangfengming, closes #56), text after comment nodes no longer discarded (#53 by @tohosaku). Development continues at [i18next/html-parse-stringify](https://github.com/i18next/html-parse-stringify).
- `3.0.1` Merged #47 which makes void elements check case insensitive. Thanks again, [@adrai](https://github.com/adrai) for this contribution!
- `3.0.0` Merged #46 which fixed an issue with handling of whitespace. Doing major version bump since this changes behavior if you have whitespace only nodes (see merged PR and #45 for more details). Thanks [@adrai](https://github.com/adrai) for this contribution!
- `2.1.1` Merged #41 which fixed an issue with tag nesting. Thanks [@ericponto](https://github.com/ericponto).
- `2.1.0` Merged support for numeric tags. This allows a use case described in [this PR](https://github.com/HenrikJoreteg/html-parse-stringify/pull/43). Thanks [@kachkaev](https://github.com/kachkaev).
- `2.0.3` Fixed failed publish. Accidentally published an empty package :sweat_smile:
- `2.0.2` Fixed incorrect attribution for vulnerability disclosure. The vulnerability was discovered by Yeting Li. Sam Sanoop was the one who reached out to me about it.
- `2.0.1` Addressing a reported regular expression denial of service issue found by [Yeting Li](https://github.com/yetingli) and reported to me by [Sam Sanoop](https://twitter.com/snoopysecurity) of [Snyk](https://snyk.io/) THANK YOU!. The issue was that sending certain input would cause one of the regular expressions we used to lock up and not finish, freezing the process. See the test that was added for details. To be clear, this lib wasn't meant for parsing non-well formed HTML. But, better safe than sorry! So we're fixing it.
- `2.0.0` updated to more modern dependencies/build system. Switched to prettier, etc. No big feature differences, just new build system/project structure. Added support for top level text nodes thanks to @jperl. Added support for comments thanks to @pconerly.
- `1.0.0 - 1.0.3` no big changes, bug fixes and speed improvements.
See [CHANGELOG.md](CHANGELOG.md).

@@ -162,4 +185,6 @@ ## credits

This package was created by [Henrik Joreteg](https://github.com/HenrikJoreteg) and is now maintained by the [i18next](https://github.com/i18next) community. Maintenance is sponsored by [Locize](https://www.locize.com?utm_source=html_parse_stringify_readme&utm_medium=github), the localization platform built by the team behind i18next.
## license
MIT
var e,t=(e=require("void-elements"))&&"object"==typeof e&&"default"in e?e.default:e,n=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;function r(e){var r={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},i=e.match(/<\/?([^\s]+?)[/\s>]/);if(i&&(r.name=i[1],(t[i[1]]||"/"===e.charAt(e.length-2))&&(r.voidElement=!0),r.name.startsWith("!--"))){var s=e.indexOf("--\x3e");return{type:"comment",comment:-1!==s?e.slice(4,s):""}}for(var a=new RegExp(n),c=null;null!==(c=a.exec(e));)if(c[0].trim())if(c[1]){var l=c[1].trim(),o=[l,""];l.indexOf("=")>-1&&(o=l.split("=")),r.attrs[o[0]]=o[1],a.lastIndex--}else c[2]&&(r.attrs[c[2]]=c[3].trim().substring(1,c[3].length-1));return r}var i=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,s=/^\s*$/,a=Object.create(null);function c(e,t){switch(t.type){case"text":return e+t.content;case"tag":return e+="<"+t.name+(t.attrs?function(e){var t=[];for(var n in e)t.push(n+'="'+e[n]+'"');return t.length?" "+t.join(" "):""}(t.attrs):"")+(t.voidElement?"/>":">"),t.voidElement?e:e+t.children.reduce(c,"")+"</"+t.name+">";case"comment":return e+"\x3c!--"+t.comment+"--\x3e"}}module.exports={parse:function(e,t){t||(t={}),t.components||(t.components=a);var n,c=[],l=[],o=-1,u=!1;if(0!==e.indexOf("<")){var h=e.indexOf("<");c.push({type:"text",content:-1===h?e:e.substring(0,h)})}for(var f,d=[];f=i.exec(e);)d.push(f);return d.forEach(function(e,t){var n=e[0];if(n&&!n.startsWith("\x3c!--")){for(var r=0,i=0,s=-1,a=null,c=0;c<n.length;c++){var l=n.charAt(c);a?l===a&&(a=null):'"'===l||"'"===l?a=l:"<"===l?2==++r&&(s=c):">"===l&&i++}var o=s>-1&&/[a-zA-Z0-9\-!/]/.test(n.charAt(s+1));if(r>i&&o){var u=n.substring(0,s),h=n.substring(u.length);d[t][0]=h,d[t].index+=u.length}}}),d.forEach(function(i,a){var h=i[0];if(h){var f=i.index;if(u){if(h!=="</"+n.name+">")return;u=!1}var v,m,p="/"!==h.charAt(1),x=h.startsWith("\x3c!--"),g=f+h.length,y=e.charAt(g),E=d[a+1];if("<"===y&&E){var b=e.substring(g,E.index);v=b.split("<").length>b.split(">").length}if(x){var A=r(h);if(o<0)return c.push(A),c;(m=l[o]).children.push(A);var O=e.slice(g,E?E.index:void 0);return O.length>0&&m.children.push({type:"text",content:O}),c}if(p&&(o++,"tag"===(n=r(h)).type&&t.components[n.name]&&(n.type="component",u=!0),n.voidElement||u||!y||"<"===y||n.children.push({type:"text",content:e.slice(g,E?E.index:void 0)}),0===o&&c.push(n),(m=l[o-1])&&m.children.push(n),l[o]=n),(!p||n.voidElement)&&(o>-1&&(n.voidElement||n.name===h.slice(2,-1))&&(o--,n=-1===o?c:l[o]),!u&&("<"!==y||v)&&y)){m=-1===o?c:l[o].children;var j=E?E.index:-1,W=e.slice(g,-1===j?void 0:j);s.test(W)&&(W=" "),(j>-1&&o+m.length>=0||" "!==W)&&m.push({type:"text",content:W})}}}),c},stringify:function(e){return e.reduce(function(e,t){return e+c("",t)},"")}};
//# sourceMappingURL=html-parse-stringify.js.map
{"version":3,"file":"html-parse-stringify.js","sources":["../src/parse-tag.js","../src/parse.js","../src/stringify.js","../src/index.js"],"sourcesContent":["import lookup from 'void-elements'\nconst attrRE = /\\s([^'\"/\\s><]+?)[\\s/>]|([^\\s=]+)=\\s?(\"[^\"]*\"|'[^']*')/g\n\nexport default function stringify(tag) {\n const res = {\n type: 'tag',\n name: '',\n voidElement: false,\n attrs: {},\n children: [],\n }\n\n const tagMatch = tag.match(/<\\/?([^\\s]+?)[/\\s>]/)\n if (tagMatch) {\n res.name = tagMatch[1]\n if (lookup[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {\n res.voidElement = true\n }\n\n // handle comment tag\n if (res.name.startsWith('!--')) {\n const endIndex = tag.indexOf('-->')\n return {\n type: 'comment',\n comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',\n }\n }\n }\n\n const reg = new RegExp(attrRE)\n let result = null\n for (;;) {\n result = reg.exec(tag)\n\n if (result === null) {\n break\n }\n\n if (!result[0].trim()) {\n continue\n }\n\n if (result[1]) {\n const attr = result[1].trim()\n let arr = [attr, '']\n\n if (attr.indexOf('=') > -1) {\n arr = attr.split('=')\n }\n\n res.attrs[arr[0]] = arr[1]\n reg.lastIndex--\n } else if (result[2]) {\n res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1)\n }\n }\n\n return res\n}\n","import parseTag from './parse-tag'\n\nconst tagRE = /<[a-zA-Z0-9\\-\\!\\/](?:\"[^\"]*\"|'[^']*'|[^'\">])*>/g\nconst whitespaceRE = /^\\s*$/\n\n// re-used obj for quick lookups of components\nconst empty = Object.create(null)\n\nexport default function parse(html, options) {\n options || (options = {})\n options.components || (options.components = empty)\n const result = []\n const arr = []\n let current\n let level = -1\n let inComponent = false\n\n // handle text at top level\n if (html.indexOf('<') !== 0) {\n var end = html.indexOf('<')\n result.push({\n type: 'text',\n content: end === -1 ? html : html.substring(0, end),\n })\n }\n\n // collect matches with an exec loop instead of matchAll to keep ES5 API compat\n const matches = []\n let m\n while ((m = tagRE.exec(html))) {\n matches.push(m)\n }\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n // comments are handled by parseTag as a whole\n if (tag.startsWith('<!--')) return\n // count brackets outside quoted attribute values, so `<` inside an\n // attribute (e.g. title=\"1 < 2\") can't trigger a bogus split\n let lts = 0\n let gts = 0\n let secondLt = -1\n let quote = null\n for (let j = 0; j < tag.length; j++) {\n const c = tag.charAt(j)\n if (quote) {\n if (c === quote) quote = null\n } else if (c === '\"' || c === \"'\") {\n quote = c\n } else if (c === '<') {\n lts++\n if (lts === 2) secondLt = j\n } else if (c === '>') {\n gts++\n }\n }\n // only split when the remainder is itself a valid tag start; otherwise\n // a fragment like `< <!-->` desyncs the string-level isComment check\n // from parseTag's name-based comment detection and crashes the walker\n const validSplit =\n secondLt > -1 && /[a-zA-Z0-9\\-!/]/.test(tag.charAt(secondLt + 1))\n if (lts > gts && validSplit) {\n const firstPart = tag.substring(0, secondLt)\n const secondPart = tag.substring(firstPart.length)\n matches[i][0] = secondPart\n matches[i].index += firstPart.length\n }\n })\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n const index = match.index\n if (inComponent) {\n if (tag !== '</' + current.name + '>') {\n return\n } else {\n inComponent = false\n }\n }\n const isOpen = tag.charAt(1) !== '/'\n const isComment = tag.startsWith('<!--')\n const start = index + tag.length\n const nextChar = html.charAt(start)\n const nextMatch = matches[i + 1]\n let isText\n if (nextChar === '<' && nextMatch) {\n const nextTag = html.substring(start, nextMatch.index)\n isText = nextTag.split('<').length > nextTag.split('>').length\n }\n\n let parent\n\n if (isComment) {\n const comment = parseTag(tag)\n\n // if we're at root, push new base node\n if (level < 0) {\n result.push(comment)\n return result\n }\n parent = arr[level]\n parent.children.push(comment)\n\n const text = html.slice(start, nextMatch ? nextMatch.index : undefined)\n if (text.length > 0) {\n parent.children.push({\n type: 'text',\n content: text,\n })\n }\n return result\n }\n\n if (isOpen) {\n level++\n\n current = parseTag(tag)\n if (current.type === 'tag' && options.components[current.name]) {\n current.type = 'component'\n inComponent = true\n }\n\n if (\n !current.voidElement &&\n !inComponent &&\n nextChar &&\n nextChar !== '<'\n ) {\n // text content runs to the next actual tag match; stray `<`\n // characters in between are part of the text\n current.children.push({\n type: 'text',\n content: html.slice(start, nextMatch ? nextMatch.index : undefined),\n })\n }\n\n // if we're at root, push new base node\n if (level === 0) {\n result.push(current)\n }\n\n parent = arr[level - 1]\n\n if (parent) {\n parent.children.push(current)\n }\n\n arr[level] = current\n }\n\n if (!isOpen || current.voidElement) {\n if (\n level > -1 &&\n (current.voidElement || current.name === tag.slice(2, -1))\n ) {\n level--\n // move current up a level to match the end tag\n current = level === -1 ? result : arr[level]\n }\n if (!inComponent && (nextChar !== '<' || isText) && nextChar) {\n // trailing text node\n // if we're at the root, push a base text node. otherwise add as\n // a child to the current node.\n parent = level === -1 ? result : arr[level].children\n\n // the text node runs to the next actual tag match; -1 means\n // there's no tag after it (trailing text)\n const end = nextMatch ? nextMatch.index : -1\n let content = html.slice(start, end === -1 ? undefined : end)\n // if a node is nothing but whitespace, collapse it as the spec states:\n // https://www.w3.org/TR/html4/struct/text.html#h-9.1\n if (whitespaceRE.test(content)) {\n content = ' '\n }\n // don't add whitespace-only text nodes if they would be trailing text nodes\n // or if they would be leading whitespace-only text nodes:\n // * end > -1 indicates this is not a trailing text node\n // * leading node is when level is -1 and parent has length 0\n if ((end > -1 && level + parent.length >= 0) || content !== ' ') {\n parent.push({\n type: 'text',\n content: content,\n })\n }\n }\n }\n })\n\n return result\n}\n","function attrString(attrs) {\n const buff = []\n for (let key in attrs) {\n buff.push(key + '=\"' + attrs[key] + '\"')\n }\n if (!buff.length) {\n return ''\n }\n return ' ' + buff.join(' ')\n}\n\nfunction stringify(buff, doc) {\n switch (doc.type) {\n case 'text':\n return buff + doc.content\n case 'tag':\n buff +=\n '<' +\n doc.name +\n (doc.attrs ? attrString(doc.attrs) : '') +\n (doc.voidElement ? '/>' : '>')\n if (doc.voidElement) {\n return buff\n }\n return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>'\n case 'comment':\n buff += '<!--' + doc.comment + '-->'\n return buff\n }\n}\n\nexport default function (doc) {\n return doc.reduce(function (token, rootEl) {\n return token + stringify('', rootEl)\n }, '')\n}\n","import parse from './parse'\nimport stringify from './stringify'\n\nexport default {\n parse,\n stringify,\n}\n"],"names":["attrRE","stringify","tag","res","type","name","voidElement","attrs","children","tagMatch","match","lookup","charAt","length","startsWith","endIndex","indexOf","comment","slice","reg","RegExp","result","exec","trim","attr","arr","split","lastIndex","substring","tagRE","whitespaceRE","empty","Object","create","buff","doc","content","key","push","join","attrString","reduce","parse","html","options","components","current","level","inComponent","end","m","matches","forEach","i","lts","gts","secondLt","quote","j","c","validSplit","test","firstPart","secondPart","index","isText","parent","isOpen","isComment","start","nextChar","nextMatch","nextTag","parseTag","text","undefined","token","rootEl"],"mappings":"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"}
import t from"void-elements";const e=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;function n(n){const s={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},i=n.match(/<\/?([^\s]+?)[/\s>]/);if(i&&(s.name=i[1],(t[i[1]]||"/"===n.charAt(n.length-2))&&(s.voidElement=!0),s.name.startsWith("!--"))){const t=n.indexOf("--\x3e");return{type:"comment",comment:-1!==t?n.slice(4,t):""}}const c=new RegExp(e);let r=null;for(;r=c.exec(n),null!==r;)if(r[0].trim())if(r[1]){const t=r[1].trim();let e=[t,""];t.indexOf("=")>-1&&(e=t.split("=")),s.attrs[e[0]]=e[1],c.lastIndex--}else r[2]&&(s.attrs[r[2]]=r[3].trim().substring(1,r[3].length-1));return s}const s=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,i=/^\s*$/,c=Object.create(null);function r(t,e){switch(e.type){case"text":return t+e.content;case"tag":return t+="<"+e.name+(e.attrs?function(t){const e=[];for(let n in t)e.push(n+'="'+t[n]+'"');return e.length?" "+e.join(" "):""}(e.attrs):"")+(e.voidElement?"/>":">"),e.voidElement?t:t+e.children.reduce(r,"")+"</"+e.name+">";case"comment":return t+"\x3c!--"+e.comment+"--\x3e"}}var o={parse:function(t,e){e||(e={}),e.components||(e.components=c);const r=[],o=[];let l,u=-1,h=!1;if(0!==t.indexOf("<")){var a=t.indexOf("<");r.push({type:"text",content:-1===a?t:t.substring(0,a)})}const f=[];let m;for(;m=s.exec(t);)f.push(m);return f.forEach(function(t,e){const n=t[0];if(!n)return;if(n.startsWith("\x3c!--"))return;let s=0,i=0,c=-1,r=null;for(let t=0;t<n.length;t++){const e=n.charAt(t);r?e===r&&(r=null):'"'===e||"'"===e?r=e:"<"===e?(s++,2===s&&(c=t)):">"===e&&i++}const o=c>-1&&/[a-zA-Z0-9\-!/]/.test(n.charAt(c+1));if(s>i&&o){const t=n.substring(0,c),s=n.substring(t.length);f[e][0]=s,f[e].index+=t.length}}),f.forEach(function(s,c){const a=s[0];if(!a)return;const m=s.index;if(h){if(a!=="</"+l.name+">")return;h=!1}const d="/"!==a.charAt(1),p=a.startsWith("\x3c!--"),x=m+a.length,g=t.charAt(x),v=f[c+1];let y,E;if("<"===g&&v){const e=t.substring(x,v.index);y=e.split("<").length>e.split(">").length}if(p){const e=n(a);if(u<0)return r.push(e),r;E=o[u],E.children.push(e);const s=t.slice(x,v?v.index:void 0);return s.length>0&&E.children.push({type:"text",content:s}),r}if(d&&(u++,l=n(a),"tag"===l.type&&e.components[l.name]&&(l.type="component",h=!0),l.voidElement||h||!g||"<"===g||l.children.push({type:"text",content:t.slice(x,v?v.index:void 0)}),0===u&&r.push(l),E=o[u-1],E&&E.children.push(l),o[u]=l),(!d||l.voidElement)&&(u>-1&&(l.voidElement||l.name===a.slice(2,-1))&&(u--,l=-1===u?r:o[u]),!h&&("<"!==g||y)&&g)){E=-1===u?r:o[u].children;const e=v?v.index:-1;let n=t.slice(x,-1===e?void 0:e);i.test(n)&&(n=" "),(e>-1&&u+E.length>=0||" "!==n)&&E.push({type:"text",content:n})}}),r},stringify:function(t){return t.reduce(function(t,e){return t+r("",e)},"")}};export default o;
//# sourceMappingURL=html-parse-stringify.modern.js.map
{"version":3,"file":"html-parse-stringify.modern.js","sources":["../src/parse-tag.js","../src/parse.js","../src/stringify.js","../src/index.js"],"sourcesContent":["import lookup from 'void-elements'\nconst attrRE = /\\s([^'\"/\\s><]+?)[\\s/>]|([^\\s=]+)=\\s?(\"[^\"]*\"|'[^']*')/g\n\nexport default function stringify(tag) {\n const res = {\n type: 'tag',\n name: '',\n voidElement: false,\n attrs: {},\n children: [],\n }\n\n const tagMatch = tag.match(/<\\/?([^\\s]+?)[/\\s>]/)\n if (tagMatch) {\n res.name = tagMatch[1]\n if (lookup[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {\n res.voidElement = true\n }\n\n // handle comment tag\n if (res.name.startsWith('!--')) {\n const endIndex = tag.indexOf('-->')\n return {\n type: 'comment',\n comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',\n }\n }\n }\n\n const reg = new RegExp(attrRE)\n let result = null\n for (;;) {\n result = reg.exec(tag)\n\n if (result === null) {\n break\n }\n\n if (!result[0].trim()) {\n continue\n }\n\n if (result[1]) {\n const attr = result[1].trim()\n let arr = [attr, '']\n\n if (attr.indexOf('=') > -1) {\n arr = attr.split('=')\n }\n\n res.attrs[arr[0]] = arr[1]\n reg.lastIndex--\n } else if (result[2]) {\n res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1)\n }\n }\n\n return res\n}\n","import parseTag from './parse-tag'\n\nconst tagRE = /<[a-zA-Z0-9\\-\\!\\/](?:\"[^\"]*\"|'[^']*'|[^'\">])*>/g\nconst whitespaceRE = /^\\s*$/\n\n// re-used obj for quick lookups of components\nconst empty = Object.create(null)\n\nexport default function parse(html, options) {\n options || (options = {})\n options.components || (options.components = empty)\n const result = []\n const arr = []\n let current\n let level = -1\n let inComponent = false\n\n // handle text at top level\n if (html.indexOf('<') !== 0) {\n var end = html.indexOf('<')\n result.push({\n type: 'text',\n content: end === -1 ? html : html.substring(0, end),\n })\n }\n\n // collect matches with an exec loop instead of matchAll to keep ES5 API compat\n const matches = []\n let m\n while ((m = tagRE.exec(html))) {\n matches.push(m)\n }\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n // comments are handled by parseTag as a whole\n if (tag.startsWith('<!--')) return\n // count brackets outside quoted attribute values, so `<` inside an\n // attribute (e.g. title=\"1 < 2\") can't trigger a bogus split\n let lts = 0\n let gts = 0\n let secondLt = -1\n let quote = null\n for (let j = 0; j < tag.length; j++) {\n const c = tag.charAt(j)\n if (quote) {\n if (c === quote) quote = null\n } else if (c === '\"' || c === \"'\") {\n quote = c\n } else if (c === '<') {\n lts++\n if (lts === 2) secondLt = j\n } else if (c === '>') {\n gts++\n }\n }\n // only split when the remainder is itself a valid tag start; otherwise\n // a fragment like `< <!-->` desyncs the string-level isComment check\n // from parseTag's name-based comment detection and crashes the walker\n const validSplit =\n secondLt > -1 && /[a-zA-Z0-9\\-!/]/.test(tag.charAt(secondLt + 1))\n if (lts > gts && validSplit) {\n const firstPart = tag.substring(0, secondLt)\n const secondPart = tag.substring(firstPart.length)\n matches[i][0] = secondPart\n matches[i].index += firstPart.length\n }\n })\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n const index = match.index\n if (inComponent) {\n if (tag !== '</' + current.name + '>') {\n return\n } else {\n inComponent = false\n }\n }\n const isOpen = tag.charAt(1) !== '/'\n const isComment = tag.startsWith('<!--')\n const start = index + tag.length\n const nextChar = html.charAt(start)\n const nextMatch = matches[i + 1]\n let isText\n if (nextChar === '<' && nextMatch) {\n const nextTag = html.substring(start, nextMatch.index)\n isText = nextTag.split('<').length > nextTag.split('>').length\n }\n\n let parent\n\n if (isComment) {\n const comment = parseTag(tag)\n\n // if we're at root, push new base node\n if (level < 0) {\n result.push(comment)\n return result\n }\n parent = arr[level]\n parent.children.push(comment)\n\n const text = html.slice(start, nextMatch ? nextMatch.index : undefined)\n if (text.length > 0) {\n parent.children.push({\n type: 'text',\n content: text,\n })\n }\n return result\n }\n\n if (isOpen) {\n level++\n\n current = parseTag(tag)\n if (current.type === 'tag' && options.components[current.name]) {\n current.type = 'component'\n inComponent = true\n }\n\n if (\n !current.voidElement &&\n !inComponent &&\n nextChar &&\n nextChar !== '<'\n ) {\n // text content runs to the next actual tag match; stray `<`\n // characters in between are part of the text\n current.children.push({\n type: 'text',\n content: html.slice(start, nextMatch ? nextMatch.index : undefined),\n })\n }\n\n // if we're at root, push new base node\n if (level === 0) {\n result.push(current)\n }\n\n parent = arr[level - 1]\n\n if (parent) {\n parent.children.push(current)\n }\n\n arr[level] = current\n }\n\n if (!isOpen || current.voidElement) {\n if (\n level > -1 &&\n (current.voidElement || current.name === tag.slice(2, -1))\n ) {\n level--\n // move current up a level to match the end tag\n current = level === -1 ? result : arr[level]\n }\n if (!inComponent && (nextChar !== '<' || isText) && nextChar) {\n // trailing text node\n // if we're at the root, push a base text node. otherwise add as\n // a child to the current node.\n parent = level === -1 ? result : arr[level].children\n\n // the text node runs to the next actual tag match; -1 means\n // there's no tag after it (trailing text)\n const end = nextMatch ? nextMatch.index : -1\n let content = html.slice(start, end === -1 ? undefined : end)\n // if a node is nothing but whitespace, collapse it as the spec states:\n // https://www.w3.org/TR/html4/struct/text.html#h-9.1\n if (whitespaceRE.test(content)) {\n content = ' '\n }\n // don't add whitespace-only text nodes if they would be trailing text nodes\n // or if they would be leading whitespace-only text nodes:\n // * end > -1 indicates this is not a trailing text node\n // * leading node is when level is -1 and parent has length 0\n if ((end > -1 && level + parent.length >= 0) || content !== ' ') {\n parent.push({\n type: 'text',\n content: content,\n })\n }\n }\n }\n })\n\n return result\n}\n","function attrString(attrs) {\n const buff = []\n for (let key in attrs) {\n buff.push(key + '=\"' + attrs[key] + '\"')\n }\n if (!buff.length) {\n return ''\n }\n return ' ' + buff.join(' ')\n}\n\nfunction stringify(buff, doc) {\n switch (doc.type) {\n case 'text':\n return buff + doc.content\n case 'tag':\n buff +=\n '<' +\n doc.name +\n (doc.attrs ? attrString(doc.attrs) : '') +\n (doc.voidElement ? '/>' : '>')\n if (doc.voidElement) {\n return buff\n }\n return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>'\n case 'comment':\n buff += '<!--' + doc.comment + '-->'\n return buff\n }\n}\n\nexport default function (doc) {\n return doc.reduce(function (token, rootEl) {\n return token + stringify('', rootEl)\n }, '')\n}\n","import parse from './parse'\nimport stringify from './stringify'\n\nexport default {\n parse,\n stringify,\n}\n"],"names":["attrRE","stringify","tag","res","type","name","voidElement","attrs","children","tagMatch","match","lookup","charAt","length","startsWith","endIndex","indexOf","comment","slice","reg","RegExp","result","exec","trim","attr","arr","split","lastIndex","substring","tagRE","whitespaceRE","empty","Object","create","buff","doc","content","key","push","join","attrString","reduce","parse","html","options","components","current","level","inComponent","end","matches","m","forEach","i","lts","gts","secondLt","quote","j","c","validSplit","test","firstPart","secondPart","index","isOpen","isComment","start","nextChar","nextMatch","isText","parent","nextTag","parseTag","text","undefined","token","rootEl"],"mappings":"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"}
import t from"void-elements";var e=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;function n(n){var r={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},i=n.match(/<\/?([^\s]+?)[/\s>]/);if(i&&(r.name=i[1],(t[i[1]]||"/"===n.charAt(n.length-2))&&(r.voidElement=!0),r.name.startsWith("!--"))){var s=n.indexOf("--\x3e");return{type:"comment",comment:-1!==s?n.slice(4,s):""}}for(var a=new RegExp(e),c=null;null!==(c=a.exec(n));)if(c[0].trim())if(c[1]){var l=c[1].trim(),o=[l,""];l.indexOf("=")>-1&&(o=l.split("=")),r.attrs[o[0]]=o[1],a.lastIndex--}else c[2]&&(r.attrs[c[2]]=c[3].trim().substring(1,c[3].length-1));return r}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,i=/^\s*$/,s=Object.create(null);function a(t,e){switch(e.type){case"text":return t+e.content;case"tag":return t+="<"+e.name+(e.attrs?function(t){var e=[];for(var n in t)e.push(n+'="'+t[n]+'"');return e.length?" "+e.join(" "):""}(e.attrs):"")+(e.voidElement?"/>":">"),e.voidElement?t:t+e.children.reduce(a,"")+"</"+e.name+">";case"comment":return t+"\x3c!--"+e.comment+"--\x3e"}}var c={parse:function(t,e){e||(e={}),e.components||(e.components=s);var a,c=[],l=[],o=-1,u=!1;if(0!==t.indexOf("<")){var h=t.indexOf("<");c.push({type:"text",content:-1===h?t:t.substring(0,h)})}for(var v,f=[];v=r.exec(t);)f.push(v);return f.forEach(function(t,e){var n=t[0];if(n&&!n.startsWith("\x3c!--")){for(var r=0,i=0,s=-1,a=null,c=0;c<n.length;c++){var l=n.charAt(c);a?l===a&&(a=null):'"'===l||"'"===l?a=l:"<"===l?2==++r&&(s=c):">"===l&&i++}var o=s>-1&&/[a-zA-Z0-9\-!/]/.test(n.charAt(s+1));if(r>i&&o){var u=n.substring(0,s),h=n.substring(u.length);f[e][0]=h,f[e].index+=u.length}}}),f.forEach(function(r,s){var h=r[0];if(h){var v=r.index;if(u){if(h!=="</"+a.name+">")return;u=!1}var m,d,p="/"!==h.charAt(1),x=h.startsWith("\x3c!--"),g=v+h.length,y=t.charAt(g),E=f[s+1];if("<"===y&&E){var A=t.substring(g,E.index);m=A.split("<").length>A.split(">").length}if(x){var b=n(h);if(o<0)return c.push(b),c;(d=l[o]).children.push(b);var O=t.slice(g,E?E.index:void 0);return O.length>0&&d.children.push({type:"text",content:O}),c}if(p&&(o++,"tag"===(a=n(h)).type&&e.components[a.name]&&(a.type="component",u=!0),a.voidElement||u||!y||"<"===y||a.children.push({type:"text",content:t.slice(g,E?E.index:void 0)}),0===o&&c.push(a),(d=l[o-1])&&d.children.push(a),l[o]=a),(!p||a.voidElement)&&(o>-1&&(a.voidElement||a.name===h.slice(2,-1))&&(o--,a=-1===o?c:l[o]),!u&&("<"!==y||m)&&y)){d=-1===o?c:l[o].children;var W=E?E.index:-1,j=t.slice(g,-1===W?void 0:W);i.test(j)&&(j=" "),(W>-1&&o+d.length>=0||" "!==j)&&d.push({type:"text",content:j})}}}),c},stringify:function(t){return t.reduce(function(t,e){return t+a("",e)},"")}};export default c;
//# sourceMappingURL=html-parse-stringify.module.js.map
{"version":3,"file":"html-parse-stringify.module.js","sources":["../src/parse-tag.js","../src/parse.js","../src/stringify.js","../src/index.js"],"sourcesContent":["import lookup from 'void-elements'\nconst attrRE = /\\s([^'\"/\\s><]+?)[\\s/>]|([^\\s=]+)=\\s?(\"[^\"]*\"|'[^']*')/g\n\nexport default function stringify(tag) {\n const res = {\n type: 'tag',\n name: '',\n voidElement: false,\n attrs: {},\n children: [],\n }\n\n const tagMatch = tag.match(/<\\/?([^\\s]+?)[/\\s>]/)\n if (tagMatch) {\n res.name = tagMatch[1]\n if (lookup[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {\n res.voidElement = true\n }\n\n // handle comment tag\n if (res.name.startsWith('!--')) {\n const endIndex = tag.indexOf('-->')\n return {\n type: 'comment',\n comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',\n }\n }\n }\n\n const reg = new RegExp(attrRE)\n let result = null\n for (;;) {\n result = reg.exec(tag)\n\n if (result === null) {\n break\n }\n\n if (!result[0].trim()) {\n continue\n }\n\n if (result[1]) {\n const attr = result[1].trim()\n let arr = [attr, '']\n\n if (attr.indexOf('=') > -1) {\n arr = attr.split('=')\n }\n\n res.attrs[arr[0]] = arr[1]\n reg.lastIndex--\n } else if (result[2]) {\n res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1)\n }\n }\n\n return res\n}\n","import parseTag from './parse-tag'\n\nconst tagRE = /<[a-zA-Z0-9\\-\\!\\/](?:\"[^\"]*\"|'[^']*'|[^'\">])*>/g\nconst whitespaceRE = /^\\s*$/\n\n// re-used obj for quick lookups of components\nconst empty = Object.create(null)\n\nexport default function parse(html, options) {\n options || (options = {})\n options.components || (options.components = empty)\n const result = []\n const arr = []\n let current\n let level = -1\n let inComponent = false\n\n // handle text at top level\n if (html.indexOf('<') !== 0) {\n var end = html.indexOf('<')\n result.push({\n type: 'text',\n content: end === -1 ? html : html.substring(0, end),\n })\n }\n\n // collect matches with an exec loop instead of matchAll to keep ES5 API compat\n const matches = []\n let m\n while ((m = tagRE.exec(html))) {\n matches.push(m)\n }\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n // comments are handled by parseTag as a whole\n if (tag.startsWith('<!--')) return\n // count brackets outside quoted attribute values, so `<` inside an\n // attribute (e.g. title=\"1 < 2\") can't trigger a bogus split\n let lts = 0\n let gts = 0\n let secondLt = -1\n let quote = null\n for (let j = 0; j < tag.length; j++) {\n const c = tag.charAt(j)\n if (quote) {\n if (c === quote) quote = null\n } else if (c === '\"' || c === \"'\") {\n quote = c\n } else if (c === '<') {\n lts++\n if (lts === 2) secondLt = j\n } else if (c === '>') {\n gts++\n }\n }\n // only split when the remainder is itself a valid tag start; otherwise\n // a fragment like `< <!-->` desyncs the string-level isComment check\n // from parseTag's name-based comment detection and crashes the walker\n const validSplit =\n secondLt > -1 && /[a-zA-Z0-9\\-!/]/.test(tag.charAt(secondLt + 1))\n if (lts > gts && validSplit) {\n const firstPart = tag.substring(0, secondLt)\n const secondPart = tag.substring(firstPart.length)\n matches[i][0] = secondPart\n matches[i].index += firstPart.length\n }\n })\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n const index = match.index\n if (inComponent) {\n if (tag !== '</' + current.name + '>') {\n return\n } else {\n inComponent = false\n }\n }\n const isOpen = tag.charAt(1) !== '/'\n const isComment = tag.startsWith('<!--')\n const start = index + tag.length\n const nextChar = html.charAt(start)\n const nextMatch = matches[i + 1]\n let isText\n if (nextChar === '<' && nextMatch) {\n const nextTag = html.substring(start, nextMatch.index)\n isText = nextTag.split('<').length > nextTag.split('>').length\n }\n\n let parent\n\n if (isComment) {\n const comment = parseTag(tag)\n\n // if we're at root, push new base node\n if (level < 0) {\n result.push(comment)\n return result\n }\n parent = arr[level]\n parent.children.push(comment)\n\n const text = html.slice(start, nextMatch ? nextMatch.index : undefined)\n if (text.length > 0) {\n parent.children.push({\n type: 'text',\n content: text,\n })\n }\n return result\n }\n\n if (isOpen) {\n level++\n\n current = parseTag(tag)\n if (current.type === 'tag' && options.components[current.name]) {\n current.type = 'component'\n inComponent = true\n }\n\n if (\n !current.voidElement &&\n !inComponent &&\n nextChar &&\n nextChar !== '<'\n ) {\n // text content runs to the next actual tag match; stray `<`\n // characters in between are part of the text\n current.children.push({\n type: 'text',\n content: html.slice(start, nextMatch ? nextMatch.index : undefined),\n })\n }\n\n // if we're at root, push new base node\n if (level === 0) {\n result.push(current)\n }\n\n parent = arr[level - 1]\n\n if (parent) {\n parent.children.push(current)\n }\n\n arr[level] = current\n }\n\n if (!isOpen || current.voidElement) {\n if (\n level > -1 &&\n (current.voidElement || current.name === tag.slice(2, -1))\n ) {\n level--\n // move current up a level to match the end tag\n current = level === -1 ? result : arr[level]\n }\n if (!inComponent && (nextChar !== '<' || isText) && nextChar) {\n // trailing text node\n // if we're at the root, push a base text node. otherwise add as\n // a child to the current node.\n parent = level === -1 ? result : arr[level].children\n\n // the text node runs to the next actual tag match; -1 means\n // there's no tag after it (trailing text)\n const end = nextMatch ? nextMatch.index : -1\n let content = html.slice(start, end === -1 ? undefined : end)\n // if a node is nothing but whitespace, collapse it as the spec states:\n // https://www.w3.org/TR/html4/struct/text.html#h-9.1\n if (whitespaceRE.test(content)) {\n content = ' '\n }\n // don't add whitespace-only text nodes if they would be trailing text nodes\n // or if they would be leading whitespace-only text nodes:\n // * end > -1 indicates this is not a trailing text node\n // * leading node is when level is -1 and parent has length 0\n if ((end > -1 && level + parent.length >= 0) || content !== ' ') {\n parent.push({\n type: 'text',\n content: content,\n })\n }\n }\n }\n })\n\n return result\n}\n","function attrString(attrs) {\n const buff = []\n for (let key in attrs) {\n buff.push(key + '=\"' + attrs[key] + '\"')\n }\n if (!buff.length) {\n return ''\n }\n return ' ' + buff.join(' ')\n}\n\nfunction stringify(buff, doc) {\n switch (doc.type) {\n case 'text':\n return buff + doc.content\n case 'tag':\n buff +=\n '<' +\n doc.name +\n (doc.attrs ? attrString(doc.attrs) : '') +\n (doc.voidElement ? '/>' : '>')\n if (doc.voidElement) {\n return buff\n }\n return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>'\n case 'comment':\n buff += '<!--' + doc.comment + '-->'\n return buff\n }\n}\n\nexport default function (doc) {\n return doc.reduce(function (token, rootEl) {\n return token + stringify('', rootEl)\n }, '')\n}\n","import parse from './parse'\nimport stringify from './stringify'\n\nexport default {\n parse,\n stringify,\n}\n"],"names":["attrRE","stringify","tag","res","type","name","voidElement","attrs","children","tagMatch","match","lookup","charAt","length","startsWith","endIndex","indexOf","comment","slice","reg","RegExp","result","exec","trim","attr","arr","split","lastIndex","substring","tagRE","whitespaceRE","empty","Object","create","buff","doc","content","key","push","join","attrString","reduce","parse","html","options","components","current","level","inComponent","end","m","matches","forEach","i","lts","gts","secondLt","quote","j","c","validSplit","test","firstPart","secondPart","index","isText","parent","isOpen","isComment","start","nextChar","nextMatch","nextTag","parseTag","text","undefined","token","rootEl"],"mappings":"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"}
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("void-elements")):"function"==typeof define&&define.amd?define(["void-elements"],t):(e=e||self).htmlParseStringify=t(e.voidElements)}(this,function(e){e=e&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e;var t=/\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?("[^"]*"|'[^']*')/g;function n(n){var r={type:"tag",name:"",voidElement:!1,attrs:{},children:[]},i=n.match(/<\/?([^\s]+?)[/\s>]/);if(i&&(r.name=i[1],(e[i[1]]||"/"===n.charAt(n.length-2))&&(r.voidElement=!0),r.name.startsWith("!--"))){var s=n.indexOf("--\x3e");return{type:"comment",comment:-1!==s?n.slice(4,s):""}}for(var a=new RegExp(t),c=null;null!==(c=a.exec(n));)if(c[0].trim())if(c[1]){var o=c[1].trim(),l=[o,""];o.indexOf("=")>-1&&(l=o.split("=")),r.attrs[l[0]]=l[1],a.lastIndex--}else c[2]&&(r.attrs[c[2]]=c[3].trim().substring(1,c[3].length-1));return r}var r=/<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g,i=/^\s*$/,s=Object.create(null);function a(e,t){switch(t.type){case"text":return e+t.content;case"tag":return e+="<"+t.name+(t.attrs?function(e){var t=[];for(var n in e)t.push(n+'="'+e[n]+'"');return t.length?" "+t.join(" "):""}(t.attrs):"")+(t.voidElement?"/>":">"),t.voidElement?e:e+t.children.reduce(a,"")+"</"+t.name+">";case"comment":return e+"\x3c!--"+t.comment+"--\x3e"}}return{parse:function(e,t){t||(t={}),t.components||(t.components=s);var a,c=[],o=[],l=-1,u=!1;if(0!==e.indexOf("<")){var f=e.indexOf("<");c.push({type:"text",content:-1===f?e:e.substring(0,f)})}for(var d,h=[];d=r.exec(e);)h.push(d);return h.forEach(function(e,t){var n=e[0];if(n&&!n.startsWith("\x3c!--")){for(var r=0,i=0,s=-1,a=null,c=0;c<n.length;c++){var o=n.charAt(c);a?o===a&&(a=null):'"'===o||"'"===o?a=o:"<"===o?2==++r&&(s=c):">"===o&&i++}var l=s>-1&&/[a-zA-Z0-9\-!/]/.test(n.charAt(s+1));if(r>i&&l){var u=n.substring(0,s),f=n.substring(u.length);h[t][0]=f,h[t].index+=u.length}}}),h.forEach(function(r,s){var f=r[0];if(f){var d=r.index;if(u){if(f!=="</"+a.name+">")return;u=!1}var m,p,v="/"!==f.charAt(1),x=f.startsWith("\x3c!--"),g=d+f.length,y=e.charAt(g),E=h[s+1];if("<"===y&&E){var b=e.substring(g,E.index);m=b.split("<").length>b.split(">").length}if(x){var A=n(f);if(l<0)return c.push(A),c;(p=o[l]).children.push(A);var O=e.slice(g,E?E.index:void 0);return O.length>0&&p.children.push({type:"text",content:O}),c}if(v&&(l++,"tag"===(a=n(f)).type&&t.components[a.name]&&(a.type="component",u=!0),a.voidElement||u||!y||"<"===y||a.children.push({type:"text",content:e.slice(g,E?E.index:void 0)}),0===l&&c.push(a),(p=o[l-1])&&p.children.push(a),o[l]=a),(!v||a.voidElement)&&(l>-1&&(a.voidElement||a.name===f.slice(2,-1))&&(l--,a=-1===l?c:o[l]),!u&&("<"!==y||m)&&y)){p=-1===l?c:o[l].children;var j=E?E.index:-1,w=e.slice(g,-1===j?void 0:j);i.test(w)&&(w=" "),(j>-1&&l+p.length>=0||" "!==w)&&p.push({type:"text",content:w})}}}),c},stringify:function(e){return e.reduce(function(e,t){return e+a("",t)},"")}}});
//# sourceMappingURL=html-parse-stringify.umd.js.map
{"version":3,"file":"html-parse-stringify.umd.js","sources":["../src/parse-tag.js","../src/parse.js","../src/stringify.js","../src/index.js"],"sourcesContent":["import lookup from 'void-elements'\nconst attrRE = /\\s([^'\"/\\s><]+?)[\\s/>]|([^\\s=]+)=\\s?(\"[^\"]*\"|'[^']*')/g\n\nexport default function stringify(tag) {\n const res = {\n type: 'tag',\n name: '',\n voidElement: false,\n attrs: {},\n children: [],\n }\n\n const tagMatch = tag.match(/<\\/?([^\\s]+?)[/\\s>]/)\n if (tagMatch) {\n res.name = tagMatch[1]\n if (lookup[tagMatch[1]] || tag.charAt(tag.length - 2) === '/') {\n res.voidElement = true\n }\n\n // handle comment tag\n if (res.name.startsWith('!--')) {\n const endIndex = tag.indexOf('-->')\n return {\n type: 'comment',\n comment: endIndex !== -1 ? tag.slice(4, endIndex) : '',\n }\n }\n }\n\n const reg = new RegExp(attrRE)\n let result = null\n for (;;) {\n result = reg.exec(tag)\n\n if (result === null) {\n break\n }\n\n if (!result[0].trim()) {\n continue\n }\n\n if (result[1]) {\n const attr = result[1].trim()\n let arr = [attr, '']\n\n if (attr.indexOf('=') > -1) {\n arr = attr.split('=')\n }\n\n res.attrs[arr[0]] = arr[1]\n reg.lastIndex--\n } else if (result[2]) {\n res.attrs[result[2]] = result[3].trim().substring(1, result[3].length - 1)\n }\n }\n\n return res\n}\n","import parseTag from './parse-tag'\n\nconst tagRE = /<[a-zA-Z0-9\\-\\!\\/](?:\"[^\"]*\"|'[^']*'|[^'\">])*>/g\nconst whitespaceRE = /^\\s*$/\n\n// re-used obj for quick lookups of components\nconst empty = Object.create(null)\n\nexport default function parse(html, options) {\n options || (options = {})\n options.components || (options.components = empty)\n const result = []\n const arr = []\n let current\n let level = -1\n let inComponent = false\n\n // handle text at top level\n if (html.indexOf('<') !== 0) {\n var end = html.indexOf('<')\n result.push({\n type: 'text',\n content: end === -1 ? html : html.substring(0, end),\n })\n }\n\n // collect matches with an exec loop instead of matchAll to keep ES5 API compat\n const matches = []\n let m\n while ((m = tagRE.exec(html))) {\n matches.push(m)\n }\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n // comments are handled by parseTag as a whole\n if (tag.startsWith('<!--')) return\n // count brackets outside quoted attribute values, so `<` inside an\n // attribute (e.g. title=\"1 < 2\") can't trigger a bogus split\n let lts = 0\n let gts = 0\n let secondLt = -1\n let quote = null\n for (let j = 0; j < tag.length; j++) {\n const c = tag.charAt(j)\n if (quote) {\n if (c === quote) quote = null\n } else if (c === '\"' || c === \"'\") {\n quote = c\n } else if (c === '<') {\n lts++\n if (lts === 2) secondLt = j\n } else if (c === '>') {\n gts++\n }\n }\n // only split when the remainder is itself a valid tag start; otherwise\n // a fragment like `< <!-->` desyncs the string-level isComment check\n // from parseTag's name-based comment detection and crashes the walker\n const validSplit =\n secondLt > -1 && /[a-zA-Z0-9\\-!/]/.test(tag.charAt(secondLt + 1))\n if (lts > gts && validSplit) {\n const firstPart = tag.substring(0, secondLt)\n const secondPart = tag.substring(firstPart.length)\n matches[i][0] = secondPart\n matches[i].index += firstPart.length\n }\n })\n matches.forEach(function (match, i) {\n const tag = match[0]\n if (!tag) return\n const index = match.index\n if (inComponent) {\n if (tag !== '</' + current.name + '>') {\n return\n } else {\n inComponent = false\n }\n }\n const isOpen = tag.charAt(1) !== '/'\n const isComment = tag.startsWith('<!--')\n const start = index + tag.length\n const nextChar = html.charAt(start)\n const nextMatch = matches[i + 1]\n let isText\n if (nextChar === '<' && nextMatch) {\n const nextTag = html.substring(start, nextMatch.index)\n isText = nextTag.split('<').length > nextTag.split('>').length\n }\n\n let parent\n\n if (isComment) {\n const comment = parseTag(tag)\n\n // if we're at root, push new base node\n if (level < 0) {\n result.push(comment)\n return result\n }\n parent = arr[level]\n parent.children.push(comment)\n\n const text = html.slice(start, nextMatch ? nextMatch.index : undefined)\n if (text.length > 0) {\n parent.children.push({\n type: 'text',\n content: text,\n })\n }\n return result\n }\n\n if (isOpen) {\n level++\n\n current = parseTag(tag)\n if (current.type === 'tag' && options.components[current.name]) {\n current.type = 'component'\n inComponent = true\n }\n\n if (\n !current.voidElement &&\n !inComponent &&\n nextChar &&\n nextChar !== '<'\n ) {\n // text content runs to the next actual tag match; stray `<`\n // characters in between are part of the text\n current.children.push({\n type: 'text',\n content: html.slice(start, nextMatch ? nextMatch.index : undefined),\n })\n }\n\n // if we're at root, push new base node\n if (level === 0) {\n result.push(current)\n }\n\n parent = arr[level - 1]\n\n if (parent) {\n parent.children.push(current)\n }\n\n arr[level] = current\n }\n\n if (!isOpen || current.voidElement) {\n if (\n level > -1 &&\n (current.voidElement || current.name === tag.slice(2, -1))\n ) {\n level--\n // move current up a level to match the end tag\n current = level === -1 ? result : arr[level]\n }\n if (!inComponent && (nextChar !== '<' || isText) && nextChar) {\n // trailing text node\n // if we're at the root, push a base text node. otherwise add as\n // a child to the current node.\n parent = level === -1 ? result : arr[level].children\n\n // the text node runs to the next actual tag match; -1 means\n // there's no tag after it (trailing text)\n const end = nextMatch ? nextMatch.index : -1\n let content = html.slice(start, end === -1 ? undefined : end)\n // if a node is nothing but whitespace, collapse it as the spec states:\n // https://www.w3.org/TR/html4/struct/text.html#h-9.1\n if (whitespaceRE.test(content)) {\n content = ' '\n }\n // don't add whitespace-only text nodes if they would be trailing text nodes\n // or if they would be leading whitespace-only text nodes:\n // * end > -1 indicates this is not a trailing text node\n // * leading node is when level is -1 and parent has length 0\n if ((end > -1 && level + parent.length >= 0) || content !== ' ') {\n parent.push({\n type: 'text',\n content: content,\n })\n }\n }\n }\n })\n\n return result\n}\n","function attrString(attrs) {\n const buff = []\n for (let key in attrs) {\n buff.push(key + '=\"' + attrs[key] + '\"')\n }\n if (!buff.length) {\n return ''\n }\n return ' ' + buff.join(' ')\n}\n\nfunction stringify(buff, doc) {\n switch (doc.type) {\n case 'text':\n return buff + doc.content\n case 'tag':\n buff +=\n '<' +\n doc.name +\n (doc.attrs ? attrString(doc.attrs) : '') +\n (doc.voidElement ? '/>' : '>')\n if (doc.voidElement) {\n return buff\n }\n return buff + doc.children.reduce(stringify, '') + '</' + doc.name + '>'\n case 'comment':\n buff += '<!--' + doc.comment + '-->'\n return buff\n }\n}\n\nexport default function (doc) {\n return doc.reduce(function (token, rootEl) {\n return token + stringify('', rootEl)\n }, '')\n}\n","import parse from './parse'\nimport stringify from './stringify'\n\nexport default {\n parse,\n stringify,\n}\n"],"names":["attrRE","stringify","tag","res","type","name","voidElement","attrs","children","tagMatch","match","lookup","charAt","length","startsWith","endIndex","indexOf","comment","slice","reg","RegExp","result","exec","trim","attr","arr","split","lastIndex","substring","tagRE","whitespaceRE","empty","Object","create","buff","doc","content","key","push","join","attrString","reduce","parse","html","options","components","current","level","inComponent","end","m","matches","forEach","i","lts","gts","secondLt","quote","j","c","validSplit","test","firstPart","secondPart","index","isText","parent","isOpen","isComment","start","nextChar","nextMatch","nextTag","parseTag","text","undefined","token","rootEl"],"mappings":"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"}
declare module 'html-parse-stringify' {
namespace HTML {
interface TagNode {
type: 'tag';
name: string;
voidElement: boolean;
attrs: Record<string, string | undefined>;
children: Node[];
}
interface TextNode {
type: 'text';
content: string;
}
interface CommentNode {
type: 'comment';
comment: string;
}
interface ComponentNode {
type: 'component';
name: string;
attrs: Record<string, string | undefined>;
voidElement: boolean;
children: [];
}
type Node = TagNode | TextNode | CommentNode | ComponentNode;
interface ParseOptions {
components?: Record<string, boolean>;
}
function parse(html: string, options?: ParseOptions): Node[];
function stringify(doc: Node[]): string;
}
// the CommonJS build assigns `module.exports = { parse, stringify }` with
// no `default` property, so `export =` is the only declaration shape that
// is correct both with and without esModuleInterop
export = HTML;
}