381 lines
9.3 KiB
JavaScript
381 lines
9.3 KiB
JavaScript
/** 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) === '<!--') {
|
|
var cend = s.indexOf('-->', i + 4)
|
|
i = cend < 0 ? s.length : cend + 3
|
|
continue
|
|
}
|
|
if (/^<!doctype/i.test(s.slice(i, i + 10))) {
|
|
var gt = s.indexOf('>', 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, '>')
|
|
.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('<body>' + String(html || '') + '</body>', {})
|
|
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
|
|
}
|