Files
bare-operating-system/packages/bare-os-coreutils/lib/summon/summon-html.js
T
2026-08-18 18:11:28 -04:00

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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
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
}