/** HTML5-ish tokenizer + tree (capped). Not a full WHATWG impl. */ var BARE_SUMMON_VOID = { area: 1, base: 1, br: 1, col: 1, embed: 1, hr: 1, img: 1, input: 1, link: 1, meta: 1, param: 1, source: 1, track: 1, wbr: 1 } var BARE_SUMMON_AUTO_CLOSE = { p: { p: 1, div: 1, h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, table: 1, form: 1 }, li: { li: 1 }, dt: { dt: 1, dd: 1 }, dd: { dt: 1, dd: 1 }, td: { td: 1, th: 1, tr: 1 }, th: { td: 1, th: 1, tr: 1 }, tr: { tr: 1 }, option: { option: 1 }, thead: { tbody: 1, tfoot: 1 }, tbody: { tbody: 1, tfoot: 1 } } var BARE_SUMMON_ENTS = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: '\u00a0', copy: '\u00a9', mdash: '\u2014', ndash: '\u2013', hellip: '\u2026', rsquo: '\u2019', lsquo: '\u2018', rdquo: '\u201d', ldquo: '\u201c' } function bareSummonDecodeEntities(s) { return String(s || '').replace( /&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]+);/g, function (m, g) { if (g.charAt(0) === '#') { var n = g.charAt(1) === 'x' || g.charAt(1) === 'X' ? parseInt(g.slice(2), 16) : parseInt(g.slice(1), 10) if (Number.isFinite(n) && n > 0 && n < 0x110000) return String.fromCodePoint(n) return m } return BARE_SUMMON_ENTS[g] || m } ) } function bareSummonParseAttrs(raw) { var attrs = {} var s = String(raw || '') var re = /([^\s=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g var m while ((m = re.exec(s))) { var key = m[1].toLowerCase() if (key.charAt(0) === '/') continue var val = m[2] != null ? m[2] : m[3] != null ? m[3] : m[4] != null ? m[4] : '' attrs[key] = bareSummonDecodeEntities(val) } return attrs } function bareSummonCreateNode(type, name, attrs) { return { type: type, name: name || '', attrs: attrs || {}, children: [], parent: null } } function bareSummonTokenizeHtml(html, maxNodes) { var s = String(html || '') var i = 0 var tokens = [] var cap = maxNodes > 0 ? maxNodes : 8000 while (i < s.length && tokens.length < cap) { if (s.charAt(i) !== '<') { var j = s.indexOf('<', i) if (j < 0) j = s.length var text = s.slice(i, j) if (text) tokens.push({ kind: 'text', text: bareSummonDecodeEntities(text) }) i = j continue } if (s.slice(i, i + 4) === '', i + 4) i = cend < 0 ? s.length : cend + 3 continue } if (/^', i) i = gt < 0 ? s.length : gt + 1 continue } var close = s.charAt(i + 1) === '/' var end = s.indexOf('>', i) if (end < 0) break var inner = s.slice(i + (close ? 2 : 1), end) var self = false if (inner.charAt(inner.length - 1) === '/') { self = true inner = inner.slice(0, -1) } var sp = inner.search(/\s/) var name = (sp < 0 ? inner : inner.slice(0, sp)).toLowerCase() var attrRaw = sp < 0 ? '' : inner.slice(sp) if (!name) { i = end + 1 continue } if (name === 'script' || name === 'style') { var closeTag = '' + name var k = s.toLowerCase().indexOf(closeTag, end + 1) var body = k < 0 ? s.slice(end + 1) : s.slice(end + 1, k) tokens.push({ kind: 'open', name: name, attrs: bareSummonParseAttrs(attrRaw), self: false }) tokens.push({ kind: 'text', text: body, raw: true }) tokens.push({ kind: 'close', name: name }) if (k < 0) break var closeEnd = s.indexOf('>', k) i = closeEnd < 0 ? s.length : closeEnd + 1 continue } if (close) tokens.push({ kind: 'close', name: name }) else tokens.push({ kind: 'open', name: name, attrs: bareSummonParseAttrs(attrRaw), self: self || !!BARE_SUMMON_VOID[name] }) i = end + 1 } return tokens } function bareSummonParseHtml(html, opts) { opts = opts || {} var max = opts.maxNodes > 0 ? opts.maxNodes : 8000 var tokens = bareSummonTokenizeHtml(html, max) var root = bareSummonCreateNode('element', 'document', {}) var htmlEl = bareSummonCreateNode('element', 'html', {}) var head = bareSummonCreateNode('element', 'head', {}) var body = bareSummonCreateNode('element', 'body', {}) htmlEl.children.push(head) head.parent = htmlEl htmlEl.children.push(body) body.parent = htmlEl root.children.push(htmlEl) htmlEl.parent = root var stack = [body] var scripts = 0 var stylesheets = [] function current() { return stack[stack.length - 1] || body } function closeUntil(name) { while (stack.length > 1) { var top = stack[stack.length - 1] if (top.name === name) { stack.pop() return } stack.pop() } } for (var t = 0; t < tokens.length; t++) { var tok = tokens[t] if (tok.kind === 'text') { var par = current() if (tok.raw && par.name !== 'style' && par.name !== 'script') continue par.children.push({ type: 'text', text: tok.text, parent: par, children: [] }) continue } if (tok.kind === 'close') { if (tok.name === 'html' || tok.name === 'body' || tok.name === 'head') continue closeUntil(tok.name) continue } if (tok.name === 'html' || tok.name === 'body' || tok.name === 'head') { if (tok.name === 'html') Object.assign(htmlEl.attrs, tok.attrs) if (tok.name === 'body') Object.assign(body.attrs, tok.attrs) if (tok.name === 'head') Object.assign(head.attrs, tok.attrs) continue } if (tok.name === 'script') { scripts++ var snode = bareSummonCreateNode('element', 'script', tok.attrs) var sdest = current() sdest.children.push(snode) snode.parent = sdest if (!tok.self) stack.push(snode) continue } var auto = BARE_SUMMON_AUTO_CLOSE[current().name] if (auto && auto[tok.name]) stack.pop() var node = bareSummonCreateNode('element', tok.name, tok.attrs) if (tok.name === 'link' && /stylesheet/i.test(tok.attrs.rel || '')) { stylesheets.push(tok.attrs.href || '') } var dest = tok.name === 'title' || tok.name === 'meta' || tok.name === 'link' ? head : current() if ( tok.name === 'title' || tok.name === 'meta' || tok.name === 'style' || tok.name === 'link' ) { dest = head } dest.children.push(node) node.parent = dest if (!tok.self && !BARE_SUMMON_VOID[tok.name]) { stack.push(node) } } var title = '' function findTitle(n) { if (!n) return if (n.type === 'element' && n.name === 'title') { title = (n.children || []) .map(function (c) { return c.text || '' }) .join('') .trim() return } var ch = n.children || [] for (var i = 0; i < ch.length && !title; i++) findTitle(ch[i]) } findTitle(head) var scriptNodes = [] function collectScripts(n) { if (n && n.type === 'element' && n.name === 'script') scriptNodes.push(n) var kids = (n && n.children) || [] var si for (si = 0; si < kids.length; si++) collectScripts(kids[si]) } collectScripts(root) return { root: root, html: htmlEl, head: head, body: body, title: title, scriptsBlocked: scripts, scripts: scriptNodes, stylesheets: stylesheets } } function bareSummonWalk(node, fn) { if (!node) return fn(node) var ch = node.children || [] for (var i = 0; i < ch.length; i++) bareSummonWalk(ch[i], fn) } function bareSummonTextContent(node) { return bareSummonTextContentInner(node, true) } function bareSummonTextContentInner(node, root) { if (!node) return '' if (node.type === 'text') return node.text || '' if (!root && (node.name === 'script' || node.name === 'style')) return '' var s = '' var ch = node.children || [] for (var i = 0; i < ch.length; i++) s += bareSummonTextContentInner(ch[i], false) return s } function bareSummonEscapeHtml(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function bareSummonSerializeInner(node) { if (!node) return '' var s = '' var ch = node.children || [] var i for (i = 0; i < ch.length; i++) { var c = ch[i] if (c.type === 'text') { s += bareSummonEscapeHtml(c.text) continue } if (c.type !== 'element') continue var attrs = c.attrs || {} var bits = '<' + c.name var k for (k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k)) { bits += ' ' + k + '="' + bareSummonEscapeHtml(attrs[k]) + '"' } } if (BARE_SUMMON_VOID[c.name]) { s += bits + '>' continue } s += bits + '>' + bareSummonSerializeInner(c) + '' + c.name + '>' } return s } function bareSummonParseFragment(html) { var doc = bareSummonParseHtml('
' + String(html || '') + '', {}) return (doc.body && doc.body.children) || [] } function bareSummonScriptSource(node) { if (!node) return '' var ch = node.children || [] var s = '' var i for (i = 0; i < ch.length; i++) { if (ch[i].type === 'text') s += ch[i].text || '' } return s }