Files
bare-operating-system/packages/bare-os-seeder/kernel/bin/summon
T
Raven Scott dfb5638c95
Release rolling / release (push) Failing after 5m54s
Update summon browser
2026-08-13 00:42:27 -04:00

3417 lines
92 KiB
Plaintext

/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
function bareStdin(ctx) {
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
}
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
function bareFormatModeString(mode, type) {
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
const perm = mode & 0o777
const r = (bit) => (perm & bit ? 'r' : '-')
const w = (bit) => (perm & bit ? 'w' : '-')
const x = (bit) => (perm & bit ? 'x' : '-')
return (
typeChar +
r(0o400) +
w(0o200) +
x(0o100) +
r(0o040) +
w(0o020) +
x(0o010) +
r(0o004) +
w(0o002) +
x(0o001)
)
}
/** @param {number} mtimeMs @param {number} [nowMs] */
function bareFormatLsMtime(mtimeMs, nowMs) {
const now = nowMs != null ? nowMs : Date.now()
const d = new Date(mtimeMs)
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
const mon = months[d.getMonth()]
const day = String(d.getDate()).padStart(2, ' ')
const sixMo = 180 * 24 * 3600 * 1000
if (Math.abs(now - mtimeMs) > sixMo) {
const yr = String(d.getFullYear()).padStart(4, ' ')
return mon + ' ' + day + ' ' + yr
}
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return mon + ' ' + day + ' ' + hh + ':' + mm
}
/** @param {number} size */
function barePosixBlocks(size) {
return Math.ceil(Number(size) / 512) || 0
}
/**
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
* @param {Record<string, unknown>} ctx
* @param {string | Uint8Array} chunk
* @returns {boolean}
*/
function bareOsEmitRaw(ctx, chunk) {
if (typeof ctx.bareOsBinWrite === 'function') {
const b4 = ctx.b4a
const u8 =
typeof chunk === 'string'
? b4 && typeof b4.from === 'function'
? b4.from(chunk)
: new TextEncoder().encode(chunk)
: chunk
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
return true
}
const w = globalThis.process?.stdout?.write
if (typeof w === 'function') {
w.call(globalThis.process.stdout, chunk)
return true
}
return false
}
/** Session env map (`vfs.env`, then `ctx.env`). Never throws. */
function bareOsEnv(ctx) {
const v = ctx && ctx.vfs && ctx.vfs.env
if (v && typeof v === 'object') return v
const e = ctx && ctx.env
if (e && typeof e === 'object') return e
return {}
}
/**
* Strict POSIX-ish decimal integer (no octal, no exponent, no empty).
* @param {unknown} s
* @returns {number}
*/
function bareOsParseDecInt(s) {
const t = String(s == null ? '' : s).trim()
if (!/^[+-]?(?:0|[1-9][0-9]*)$/.test(t)) return NaN
const n = Number.parseInt(t, 10)
return Number.isSafeInteger(n) ? n : NaN
}
/** @param {unknown} s */
function bareOsParseNonNegInt(s) {
const n = bareOsParseDecInt(s)
return n >= 0 ? n : NaN
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} name
* @param {number} fallback
* @param {number} [min]
* @param {number} [max]
*/
function bareOsEnvInt(ctx, name, fallback, min, max) {
const raw = bareOsEnv(ctx)[name]
if (raw == null || raw === '') return fallback
const n = Number.parseInt(String(raw), 10)
if (!Number.isFinite(n)) return fallback
let v = n
if (min != null && v < min) v = min
if (max != null && v > max) v = max
return v
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} msg
* @param {number} [code]
*/
function bareOsFail(ctx, msg, code) {
if (msg) ctx.console.error(msg)
ctx.exitCode = code == null ? 1 : code
}
/** @param {unknown} e */
function bareOsIsNotFoundErr(e) {
const code = e && typeof e === 'object' ? e.code : ''
if (code === 'ENOENT') return true
const msg = String((e && e.message) || e || '')
return /ENOENT|No such file|not found/i.test(msg)
}
/**
* @param {Record<string, unknown>} ctx
* @param {unknown} buf
* @returns {Uint8Array}
*/
function bareOsToU8(ctx, buf) {
if (!buf) return new Uint8Array(0)
if (buf instanceof Uint8Array) return buf
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(buf)
return new Uint8Array(buf)
}
/** @param {string} dir @param {string} name */
function bareOsJoinPath(dir, name) {
const d = String(dir || '').replace(/\/+$/, '')
const n = String(name || '').replace(/^\/+/, '')
if (!d || d === '/') return '/' + n
return d + '/' + n
}
/** @param {string} p */
function bareOsBaseName(p) {
const t = String(p || '').replace(/\/+$/, '')
if (!t || t === '/') return t === '/' ? '/' : ''
const i = t.lastIndexOf('/')
return i < 0 ? t : t.slice(i + 1) || t
}
/** @param {string} p */
function bareOsParentDir(p) {
const t = String(p || '').replace(/\/+$/, '') || '/'
if (t === '/') return '/'
const i = t.lastIndexOf('/')
return i <= 0 ? '/' : t.slice(0, i) || '/'
}
/** @param {string} p */
function bareOsNormPath(p) {
return String(p || '').replace(/\/+$/, '') || '/'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} p
*/
function bareOsResolvePath(ctx, p) {
if (ctx && ctx.vfs && typeof ctx.vfs.resolveLogical === 'function') {
try {
return String(ctx.vfs.resolveLogical(p) || p)
} catch {
/* fall through */
}
}
return String(p || '')
}
/**
* True when dest is src or lives under src (self-copy / self-move).
* @param {Record<string, unknown>} ctx
* @param {string} src
* @param {string} dest
*/
function bareOsDestInsideSrc(ctx, src, dest) {
const s = bareOsNormPath(bareOsResolvePath(ctx, src))
const d = bareOsNormPath(bareOsResolvePath(ctx, dest))
if (s === d) return true
if (s === '/') return d !== '/'
return d === s || d.startsWith(s + '/')
}
const BARE_OS_B64_ALPH =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
/** @param {Uint8Array} u8 */
function bareOsB64Encode(u8) {
let out = ''
let i = 0
for (; i + 2 < u8.length; i += 3) {
const n = (u8[i] << 16) | (u8[i + 1] << 8) | u8[i + 2]
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
BARE_OS_B64_ALPH[n & 63]
}
const rest = u8.length - i
if (rest === 1) {
const n = u8[i] << 16
out += BARE_OS_B64_ALPH[(n >> 18) & 63] + BARE_OS_B64_ALPH[(n >> 12) & 63] + '=='
} else if (rest === 2) {
const n = (u8[i] << 16) | (u8[i + 1] << 8)
out +=
BARE_OS_B64_ALPH[(n >> 18) & 63] +
BARE_OS_B64_ALPH[(n >> 12) & 63] +
BARE_OS_B64_ALPH[(n >> 6) & 63] +
'='
}
return out
}
/**
* RFC 4648 Base64 decode (also accepts URL-safe alphabet). Rejects junk.
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsB64Decode(s) {
const t = String(s).replace(/\s+/g, '')
if (!t) return new Uint8Array(0)
if (t.length % 4 === 1) throw new Error('invalid base64 length')
let pad = 0
if (t.endsWith('==')) pad = 2
else if (t.endsWith('=')) pad = 1
const body = pad ? t.slice(0, t.length - pad) : t
const bytes = []
let buf = 0
let bits = 0
for (let i = 0; i < body.length; i++) {
const c = body[i]
let v = BARE_OS_B64_ALPH.indexOf(c)
if (v < 0) {
if (c === '-') v = 62
else if (c === '_') v = 63
else throw new Error('invalid base64 character')
}
buf = (buf << 6) | v
bits += 6
if (bits >= 8) {
bits -= 8
bytes.push((buf >> bits) & 255)
}
}
if (pad) {
const want = Math.floor((body.length * 6) / 8)
if (bytes.length > want) bytes.length = want
}
return new Uint8Array(bytes)
}
/**
* @param {string} s
* @returns {Uint8Array}
*/
function bareOsHexDecode(s) {
const t = String(s).replace(/\s+/g, '')
if (t.length % 2 !== 0) throw new Error('odd hex length')
const out = new Uint8Array(t.length / 2)
for (let i = 0; i < out.length; i++) {
const pair = t.slice(i * 2, i * 2 + 2)
if (!/^[0-9a-fA-F]{2}$/.test(pair)) throw new Error('invalid hex')
out[i] = Number.parseInt(pair, 16)
}
return out
}
/** @param {Uint8Array} u8 */
function bareOsHexEncode(u8) {
let s = ''
for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0')
return s
}
/** URL parse / join for summon (about:, file:, http:, https:). */
var BARE_SUMMON_SCHEMES = {
http: true,
https: true,
file: true,
about: true
}
function bareSummonTrim(s) {
return String(s == null ? '' : s).trim()
}
function bareSummonParseUrl(raw) {
var s = bareSummonTrim(raw)
if (!s) return null
if (s.indexOf('://') < 0 && s.indexOf(':') > 0) {
var sch0 = s.slice(0, s.indexOf(':')).toLowerCase()
if (sch0 === 'about') {
return {
href: 'about:' + s.slice(s.indexOf(':') + 1),
protocol: 'about:',
host: '',
hostname: '',
port: '',
pathname: s.slice(s.indexOf(':') + 1) || 'blank',
search: '',
hash: '',
origin: 'about:'
}
}
}
try {
var u = new URL(s)
return {
href: u.href,
protocol: u.protocol,
host: u.host,
hostname: u.hostname,
port: u.port,
pathname: u.pathname || '/',
search: u.search,
hash: u.hash,
origin: u.origin
}
} catch (e) {
return null
}
}
function bareSummonResolveUrl(ref, base) {
var r = bareSummonTrim(ref)
if (!r) return bareSummonParseUrl(base)
if (/^javascript:/i.test(r)) return null
if (r.charAt(0) === '#') {
var b0 = bareSummonParseUrl(base)
if (!b0) return null
b0.hash = r
b0.href = b0.href.split('#')[0] + r
return b0
}
if (/^about:/i.test(r)) return bareSummonParseUrl(r)
try {
var u = base ? new URL(r, String(base)) : new URL(r)
return bareSummonParseUrl(u.href)
} catch (e2) {
return null
}
}
function bareSummonUrlOk(u) {
if (!u || !u.protocol) return false
var p = u.protocol.replace(/:$/, '')
return !!BARE_SUMMON_SCHEMES[p]
}
function bareSummonIsHttp(u) {
return u && (u.protocol === 'http:' || u.protocol === 'https:')
}
/** RFC 6265-ish cookie jar (first-party default). */
function bareSummonCookieHostMatch(domain, host) {
var d = String(domain || '')
.replace(/^\./, '')
.toLowerCase()
var h = String(host || '').toLowerCase()
if (!d || !h) return false
if (h === d) return true
return h.length > d.length && h.slice(h.length - d.length - 1) === '.' + d
}
function bareSummonCookiePathMatch(path, reqPath) {
var p = path || '/'
var r = reqPath || '/'
if (r === p) return true
if (r.indexOf(p) === 0) {
if (p.charAt(p.length - 1) === '/') return true
return r.charAt(p.length) === '/'
}
return false
}
function bareSummonParseSetCookie(raw, pageUrl) {
var s = String(raw || '')
var parts = s.split(';')
var nv = parts[0] || ''
var eq = nv.indexOf('=')
if (eq < 0) return null
var name = nv.slice(0, eq).trim()
var value = nv.slice(eq + 1).trim()
if (!name) return null
var rec = {
name: name,
value: value,
domain: '',
path: '/',
secure: false,
httpOnly: false,
expires: 0
}
for (var i = 1; i < parts.length; i++) {
var bit = parts[i].trim()
var e2 = bit.indexOf('=')
var k = (e2 < 0 ? bit : bit.slice(0, e2)).trim().toLowerCase()
var v = e2 < 0 ? '' : bit.slice(e2 + 1).trim()
if (k === 'domain') rec.domain = v.replace(/^\./, '').toLowerCase()
else if (k === 'path') rec.path = v || '/'
else if (k === 'secure') rec.secure = true
else if (k === 'httponly') rec.httpOnly = true
else if (k === 'max-age') {
var n = parseInt(v, 10)
if (Number.isFinite(n)) rec.expires = Date.now() + n * 1000
}
}
var u = bareSummonParseUrl(pageUrl)
if (!rec.domain && u) rec.domain = u.hostname
if ((!rec.path || rec.path === '/') && u && u.pathname) {
var slash = u.pathname.lastIndexOf('/')
rec.path = slash > 0 ? u.pathname.slice(0, slash) : '/'
}
if (u && rec.secure && u.protocol !== 'https:') return null
return rec
}
function bareSummonCreateCookieJar() {
var list = []
return {
list: list,
put: function (setCookieLine, pageUrl, opts) {
var rec = bareSummonParseSetCookie(setCookieLine, pageUrl)
if (!rec) return false
var u = bareSummonParseUrl(pageUrl)
if (
u &&
rec.domain &&
!bareSummonCookieHostMatch(rec.domain, u.hostname)
) {
return false
}
var third = opts && opts.thirdParty
if (!third && u && rec.domain && rec.domain !== u.hostname) {
/* first-party only: allow subdomain of page host */
if (!bareSummonCookieHostMatch(rec.domain, u.hostname)) return false
}
for (var i = list.length - 1; i >= 0; i--) {
if (
list[i].name === rec.name &&
list[i].domain === rec.domain &&
list[i].path === rec.path
) {
list.splice(i, 1)
}
}
list.push(rec)
return true
},
headerFor: function (pageUrl) {
var u = bareSummonParseUrl(pageUrl)
if (!u) return ''
var now = Date.now()
var bits = []
for (var i = 0; i < list.length; i++) {
var c = list[i]
if (c.expires && c.expires < now) continue
if (!bareSummonCookieHostMatch(c.domain, u.hostname)) continue
if (!bareSummonCookiePathMatch(c.path, u.pathname)) continue
if (c.secure && u.protocol !== 'https:') continue
bits.push(c.name + '=' + c.value)
}
return bits.join('; ')
},
toJSON: function () {
return list.slice()
},
load: function (arr) {
list.length = 0
if (!Array.isArray(arr)) return
for (var i = 0; i < arr.length; i++) {
if (arr[i] && arr[i].name) list.push(arr[i])
}
}
}
}
/** HTTP session for summon: redirects, cookies, caps. Uses ctx.httpFetch. */
var BARE_SUMMON_UA = 'Summon/0.1 (Bare OS; text)'
var BARE_SUMMON_MAX_REDIRECTS = 10
var BARE_SUMMON_DEFAULT_TIMEOUT = 20000
var BARE_SUMMON_DEFAULT_MAX_BYTES = 4 * 1024 * 1024
function bareSummonResolveFetch(ctx) {
if (ctx && typeof ctx.httpFetch === 'function') return ctx.httpFetch
if (ctx && ctx.bare && typeof ctx.bare.fetch === 'function')
return ctx.bare.fetch
if (typeof fetch === 'function') return fetch
return null
}
function bareSummonHeadersToObject(h) {
var out = {}
if (!h) return out
if (typeof h.forEach === 'function') {
h.forEach(function (v, k) {
out[String(k).toLowerCase()] = String(v)
})
return out
}
var k
for (k in h) {
if (Object.prototype.hasOwnProperty.call(h, k))
out[String(k).toLowerCase()] = String(h[k])
}
return out
}
function bareSummonReadSetCookies(headers, res) {
var out = []
if (
res &&
typeof res.headers !== 'undefined' &&
res.headers &&
typeof res.headers.getSetCookie === 'function'
) {
try {
var g = res.headers.getSetCookie()
if (Array.isArray(g)) return g
} catch (e) {
/* ignore */
}
}
var raw = headers['set-cookie']
if (!raw) return out
if (Array.isArray(raw)) return raw
return [String(raw)]
}
async function bareSummonFetch(ctx, urlStr, opts) {
opts = opts || {}
var fn = bareSummonResolveFetch(ctx)
if (!fn) {
return { ok: false, error: 'summon: no HTTP client (ctx.httpFetch)' }
}
var jar = opts.jar || bareSummonCreateCookieJar()
var method = String(opts.method || 'GET').toUpperCase()
var body = opts.body
var maxRedir =
opts.maxRedirects != null ? opts.maxRedirects : BARE_SUMMON_MAX_REDIRECTS
var timeout =
opts.timeoutMs != null ? opts.timeoutMs : BARE_SUMMON_DEFAULT_TIMEOUT
var maxBytes =
opts.maxBytes != null ? opts.maxBytes : BARE_SUMMON_DEFAULT_MAX_BYTES
var current = urlStr
var hops = 0
var lastStatus = 0
var headersOut = {}
var text = ''
var finalUrl = urlStr
while (hops <= maxRedir) {
var parsed = bareSummonParseUrl(current)
if (!parsed || !bareSummonIsHttp(parsed)) {
return { ok: false, error: 'summon: only http(s) URLs', url: current }
}
var hdr = {
'user-agent': BARE_SUMMON_UA,
accept: 'text/html,application/xhtml+xml;q=0.9,text/plain;q=0.8,*/*;q=0.1'
}
var cookie = jar.headerFor(current)
if (cookie) hdr.cookie = cookie
if (opts.headers) {
var hk
for (hk in opts.headers) {
if (Object.prototype.hasOwnProperty.call(opts.headers, hk)) {
hdr[hk] = opts.headers[hk]
}
}
}
var ac = null
var timer = null
if (typeof AbortController === 'function' && timeout > 0) {
ac = new AbortController()
timer = setTimeout(function () {
try {
ac.abort()
} catch (e) {
/* ignore */
}
}, timeout)
}
var res
try {
res = await fn(current, {
method: method,
headers: hdr,
body: method === 'GET' || method === 'HEAD' ? undefined : body,
redirect: 'manual',
signal: ac ? ac.signal : undefined
})
} catch (err) {
if (timer) clearTimeout(timer)
return {
ok: false,
error:
'summon: request failed: ' + ((err && err.message) || String(err)),
url: current
}
}
if (timer) clearTimeout(timer)
lastStatus = res && res.status ? res.status : 0
headersOut = bareSummonHeadersToObject(res && res.headers)
var setc = bareSummonReadSetCookies(headersOut, res)
for (var i = 0; i < setc.length; i++) jar.put(setc[i], current, opts)
var loc = headersOut.location
if (loc && lastStatus >= 300 && lastStatus < 400 && hops < maxRedir) {
var next = bareSummonResolveUrl(loc, current)
if (!next) break
if (
lastStatus === 303 ||
((lastStatus === 301 || lastStatus === 302) && method === 'POST')
) {
method = 'GET'
body = undefined
}
current = next.href
hops++
continue
}
finalUrl = current
if (typeof res.text === 'function') {
text = await res.text()
} else if (typeof res.arrayBuffer === 'function') {
var ab = await res.arrayBuffer()
text = new TextDecoder('utf-8', { fatal: false }).decode(
new Uint8Array(ab)
)
}
if (text && text.length > maxBytes) text = text.slice(0, maxBytes)
break
}
return {
ok: lastStatus >= 200 && lastStatus < 400,
status: lastStatus,
url: finalUrl,
headers: headersOut,
body: text,
jar: jar,
redirects: hops
}
}
/** 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
}
/** Minimal CSS: tag / .class / #id / * plus a few properties. */
var BARE_SUMMON_UA_CSS = [
{ sel: 'a', decl: { color: 'blue', 'text-decoration': 'underline' } },
{ sel: 'b', decl: { 'font-weight': 'bold' } },
{ sel: 'strong', decl: { 'font-weight': 'bold' } },
{ sel: 'em', decl: { 'font-style': 'italic' } },
{ sel: 'i', decl: { 'font-style': 'italic' } },
{
sel: 'h1',
decl: {
'font-weight': 'bold',
'margin-top': '1em',
'margin-bottom': '0.5em'
}
},
{ sel: 'h2', decl: { 'font-weight': 'bold', 'margin-top': '1em' } },
{ sel: 'h3', decl: { 'font-weight': 'bold' } },
{ sel: 'pre', decl: { 'white-space': 'pre', 'font-family': 'monospace' } },
{ sel: 'code', decl: { 'font-family': 'monospace' } },
{ sel: 'script', decl: { display: 'none' } },
{ sel: 'style', decl: { display: 'none' } },
{ sel: 'head', decl: { display: 'none' } },
{ sel: 'title', decl: { display: 'none' } },
{ sel: 'meta', decl: { display: 'none' } },
{ sel: 'link', decl: { display: 'none' } },
{ sel: 'noscript', decl: { display: 'none' } }
]
function bareSummonParseDeclarations(block) {
var decl = {}
var parts = String(block || '').split(';')
for (var i = 0; i < parts.length; i++) {
var p = parts[i]
var c = p.indexOf(':')
if (c < 0) continue
var k = p.slice(0, c).trim().toLowerCase()
var v = p.slice(c + 1).trim()
if (k) decl[k] = v
}
return decl
}
function bareSummonStripAtRules(css) {
var s = String(css || '')
var out = ''
var i = 0
while (i < s.length) {
if (s.charAt(i) === '@') {
var brace = s.indexOf('{', i)
if (brace < 0) break
var depth = 0
var j = brace
for (; j < s.length; j++) {
if (s.charAt(j) === '{') depth++
else if (s.charAt(j) === '}') {
depth--
if (depth === 0) {
j++
break
}
}
}
i = j
continue
}
out += s.charAt(i)
i++
}
return out
}
function bareSummonParseStylesheet(css, maxRules) {
var s = bareSummonStripAtRules(
String(css || '').replace(/\/\*[\s\S]*?\*\//g, '')
)
var rules = []
var cap = maxRules > 0 ? maxRules : 800
var re = /([^{}]+)\{([^{}]*)\}/g
var m
while ((m = re.exec(s)) && rules.length < cap) {
var sels = m[1].split(',')
var decl = bareSummonParseDeclarations(m[2])
for (var i = 0; i < sels.length; i++) {
var sel = sels[i].trim()
if (sel) rules.push({ sel: sel, decl: decl })
}
}
return rules
}
function bareSummonHasClass(node, name) {
var cls = (node.attrs && node.attrs.class ? node.attrs.class : '').split(
/\s+/
)
return cls.indexOf(name) >= 0
}
function bareSummonMatchCompound(sel, node) {
if (!node || node.type !== 'element') return false
sel = String(sel || '').trim()
if (!sel || sel === '*') return true
if (sel.charAt(0) === ':') return false
var i = 0
var tag = ''
if (/^[a-zA-Z]/.test(sel)) {
while (i < sel.length && /[a-zA-Z0-9_-]/.test(sel.charAt(i))) i++
tag = sel.slice(0, i).toLowerCase()
if (node.name !== tag) return false
}
while (i < sel.length) {
var ch = sel.charAt(i)
if (ch === '.') {
i++
var c0 = i
while (i < sel.length && /[a-zA-Z0-9_-]/.test(sel.charAt(i))) i++
if (!bareSummonHasClass(node, sel.slice(c0, i))) return false
} else if (ch === '#') {
i++
var i0 = i
while (i < sel.length && /[a-zA-Z0-9_-]/.test(sel.charAt(i))) i++
if ((node.attrs.id || '') !== sel.slice(i0, i)) return false
} else if (ch === '[') {
var end = sel.indexOf(']', i)
if (end < 0) return false
var inside = sel.slice(i + 1, end)
var eq = inside.indexOf('=')
var an = (eq < 0 ? inside : inside.slice(0, eq)).trim().toLowerCase()
var av =
eq < 0
? null
: inside
.slice(eq + 1)
.trim()
.replace(/^["']|["']$/g, '')
var have = node.attrs ? node.attrs[an] : undefined
if (eq < 0) {
if (have == null) return false
} else if (String(have) !== av) return false
i = end + 1
} else if (ch === ':') {
i++
while (i < sel.length && /[a-zA-Z0-9_-]/.test(sel.charAt(i))) i++
if (sel.charAt(i) === '(') {
var depth = 1
i++
while (i < sel.length && depth) {
if (sel.charAt(i) === '(') depth++
else if (sel.charAt(i) === ')') depth--
i++
}
}
} else return false
}
return true
}
function bareSummonSplitCombinators(sel) {
var parts = []
var cur = ''
var comb = ' '
var i
var s = String(sel || '')
.replace(/\s+/g, ' ')
.trim()
for (i = 0; i < s.length; i++) {
var ch = s.charAt(i)
if (ch === '>' || ch === '+' || ch === '~') {
if (cur.trim()) parts.push({ sel: cur.trim(), comb: comb })
cur = ''
comb = ch
if (s.charAt(i + 1) === ' ') i++
} else if (ch === ' ') {
if (cur.trim()) {
parts.push({ sel: cur.trim(), comb: comb })
cur = ''
comb = ' '
}
} else cur += ch
}
if (cur.trim()) parts.push({ sel: cur.trim(), comb: comb })
return parts
}
function bareSummonPrevElement(node) {
if (!node || !node.parent) return null
var kids = node.parent.children || []
var i
var prev = null
for (i = 0; i < kids.length; i++) {
if (kids[i] === node) return prev
if (kids[i] && kids[i].type === 'element') prev = kids[i]
}
return prev
}
function bareSummonMatches(sel, node) {
if (!node || node.type !== 'element') return false
sel = String(sel || '').trim()
if (!sel) return false
var groups = sel.split(',')
var g
for (g = 0; g < groups.length; g++) {
if (bareSummonMatchesOne(groups[g].trim(), node)) return true
}
return false
}
function bareSummonMatchesOne(sel, node) {
var parts = bareSummonSplitCombinators(sel)
if (!parts.length) return false
var cur = node
var p = parts.length - 1
if (!bareSummonMatchCompound(parts[p].sel, cur)) return false
p--
while (p >= 0 && cur) {
var comb = parts[p + 1] ? parts[p + 1].comb : ' '
if (comb === '>') {
cur = cur.parent
if (!cur || !bareSummonMatchCompound(parts[p].sel, cur)) return false
p--
} else if (comb === '+') {
cur = bareSummonPrevElement(cur)
if (!cur || !bareSummonMatchCompound(parts[p].sel, cur)) return false
p--
} else if (comb === '~') {
var ok = false
var sib = bareSummonPrevElement(cur)
while (sib) {
if (bareSummonMatchCompound(parts[p].sel, sib)) {
ok = true
cur = sib
break
}
sib = bareSummonPrevElement(sib)
}
if (!ok) return false
p--
} else {
var hit = false
var anc = cur.parent
while (anc) {
if (bareSummonMatchCompound(parts[p].sel, anc)) {
hit = true
cur = anc
break
}
anc = anc.parent
}
if (!hit) return false
p--
}
}
return p < 0
}
function bareSummonMatchSimple(sel, node) {
return bareSummonMatches(sel, node)
}
function bareSummonQueryAll(root, sel, cap) {
var out = []
var max = cap > 0 ? cap : 2000
function walk(n) {
if (!n || out.length >= max) return
if (n.type === 'element' && bareSummonMatches(sel, n)) out.push(n)
var ch = n.children || []
var i
for (i = 0; i < ch.length; i++) walk(ch[i])
}
walk(root)
return out
}
function bareSummonComputedStyle(node, sheets) {
var out = {}
function apply(decl) {
var k
for (k in decl) {
if (Object.prototype.hasOwnProperty.call(decl, k)) out[k] = decl[k]
}
}
var i
for (i = 0; i < BARE_SUMMON_UA_CSS.length; i++) {
if (bareSummonMatches(BARE_SUMMON_UA_CSS[i].sel, node))
apply(BARE_SUMMON_UA_CSS[i].decl)
}
if (sheets) {
for (i = 0; i < sheets.length; i++) {
var rules = sheets[i]
for (var r = 0; r < rules.length; r++) {
if (bareSummonMatches(rules[r].sel, node)) apply(rules[r].decl)
}
}
}
if (node && node.attrs && node.attrs.style) {
apply(bareSummonParseDeclarations(node.attrs.style))
}
return out
}
function bareSummonDisplay(style) {
var d = style && style.display ? String(style.display).toLowerCase() : ''
if (d === 'none') return 'none'
if (style && String(style.visibility || '').toLowerCase() === 'hidden')
return 'none'
if (d === 'inline' || d === 'inline-block' || d === 'inline-flex')
return 'inline'
if (d === 'flex' || d === 'grid' || d === 'table' || d === 'list-item')
return 'block'
return ''
}
/** Flow HTML to wrapped lines + link/form hit targets. */
var BARE_SUMMON_BLOCK = {
address: 1,
article: 1,
aside: 1,
blockquote: 1,
body: 1,
div: 1,
dl: 1,
dt: 1,
dd: 1,
fieldset: 1,
figcaption: 1,
figure: 1,
footer: 1,
form: 1,
h1: 1,
h2: 1,
h3: 1,
h4: 1,
h5: 1,
h6: 1,
header: 1,
hr: 1,
li: 1,
main: 1,
nav: 1,
ol: 1,
p: 1,
pre: 1,
section: 1,
table: 1,
tr: 1,
ul: 1
}
function bareSummonIsBlock(name, style) {
var d = bareSummonDisplay(style)
if (d === 'none') return false
if (d === 'inline') return false
if (d === 'block') return true
return !!BARE_SUMMON_BLOCK[name]
}
function bareSummonLayout(doc, opts) {
opts = opts || {}
var cols = Math.max(20, opts.cols || 80)
var sheets = opts.sheets || []
var numbers = opts.numbers !== false
var lines = []
var links = []
var forms = []
var cur = ''
var curMarks = []
function flush() {
if (cur === '' && !curMarks.length) return
lines.push({ text: cur, marks: curMarks })
cur = ''
curMarks = []
}
function emitNl() {
flush()
}
function emitText(s, mark) {
s = String(s || '')
if (!s) return
var i = 0
while (i < s.length) {
var ch = s.charAt(i)
if (ch === '\n') {
emitNl()
i++
continue
}
if (ch === '\r') {
i++
continue
}
if (cur.length >= cols) emitNl()
if (mark) {
curMarks.push({
col: cur.length,
ch: ch,
link: mark.link,
form: mark.form,
bold: mark.bold,
dim: mark.dim
})
}
cur += ch
i++
}
}
function emitSpace() {
if (cur && cur.charAt(cur.length - 1) !== ' ') emitText(' ')
}
function walk(node, mark) {
if (!node) return
if (node.type === 'text') {
var t = node.text || ''
if (!mark || !mark.pre) t = t.replace(/\s+/g, ' ')
if (t === ' ' || t === '') {
if (t === ' ') emitSpace()
return
}
emitText(t, mark)
return
}
if (node.type !== 'element') return
var st = bareSummonComputedStyle(node, sheets)
if (bareSummonDisplay(st) === 'none') return
var name = node.name
var nextMark = {
link: mark && mark.link,
form: mark && mark.form,
bold: mark && mark.bold,
dim: mark && mark.dim,
pre: mark && mark.pre
}
if (
st['font-weight'] === 'bold' ||
name === 'b' ||
name === 'strong' ||
/^h[1-6]$/.test(name)
)
nextMark.bold = true
if (name === 'pre' || name === 'code' || st['font-family'] === 'monospace')
nextMark.dim = true
if (name === 'pre' || st['white-space'] === 'pre') nextMark.pre = true
if (name === 'a' && node.attrs.href) {
var id = links.length
links.push({
id: id,
href: node.attrs.href,
text: bareSummonTextContent(node).replace(/\s+/g, ' ').trim()
})
nextMark.link = id
if (numbers) emitText('[' + (id + 1) + ']', nextMark)
}
if (name === 'br') {
emitNl()
return
}
if (name === 'hr') {
emitNl()
emitText('\u2500'.repeat(Math.min(cols, 40)))
emitNl()
return
}
if (name === 'img') {
var alt = node.attrs.alt || node.attrs.src || 'img'
emitText('[IMG ' + alt + ']')
return
}
if (
name === 'input' ||
name === 'textarea' ||
name === 'select' ||
name === 'button'
) {
var fid = forms.length
forms.push({
id: fid,
name: node.attrs.name || '',
type: (node.attrs.type || name).toLowerCase(),
value: node.attrs.value || '',
node: node
})
nextMark.form = fid
emitText('[' + (node.attrs.type || name) + ']')
if (name === 'input' && BARE_SUMMON_VOID[name]) return
}
if (name === 'iframe' || name === 'video' || name === 'audio') {
emitText(
'[' +
name.toUpperCase() +
(node.attrs.src ? ' ' + node.attrs.src : '') +
']'
)
return
}
var block = bareSummonIsBlock(name, st)
if (block) emitNl()
if (name === 'li') emitText('\u2022 ')
var ch = node.children || []
for (var i = 0; i < ch.length; i++) walk(ch[i], nextMark)
if (block) emitNl()
}
walk(doc.body, {})
flush()
while (lines.length && !String(lines[0].text || '').trim()) lines.shift()
while (lines.length && !String(lines[lines.length - 1].text || '').trim())
lines.pop()
var plain = lines.map(function (l) {
return l.text
})
return {
cols: cols,
lines: lines,
plain: plain,
links: links,
forms: forms,
title: doc.title || ''
}
}
function bareSummonReaderText(doc) {
var bits = []
function keep(n) {
return (
n &&
n.type === 'element' &&
(n.name === 'p' ||
n.name === 'h1' ||
n.name === 'h2' ||
n.name === 'h3' ||
n.name === 'li' ||
n.name === 'pre' ||
n.name === 'blockquote' ||
n.name === 'article')
)
}
function walk(n) {
if (!n) return
if (keep(n)) {
var t = bareSummonTextContent(n).replace(/\s+/g, ' ').trim()
if (t) bits.push(t)
return
}
var ch = n.children || []
for (var i = 0; i < ch.length; i++) walk(ch[i])
}
walk(doc.body)
return bits.join('\n\n')
}
/** Form collect + application/x-www-form-urlencoded. */
function bareSummonEncodeForm(pairs) {
var out = []
for (var i = 0; i < pairs.length; i++) {
var n = pairs[i].name
var v = pairs[i].value
if (!n) continue
out.push(
encodeURIComponent(n) + '=' + encodeURIComponent(v == null ? '' : v)
)
}
return out.join('&')
}
function bareSummonCollectForm(formNode) {
var pairs = []
var method = (
(formNode.attrs && formNode.attrs.method) ||
'get'
).toLowerCase()
var action = (formNode.attrs && formNode.attrs.action) || ''
function walk(n) {
if (!n || n.type !== 'element') return
var name = n.name
var a = n.attrs || {}
if (
(name === 'input' || name === 'textarea' || name === 'select') &&
a.name
) {
var typ = (a.type || 'text').toLowerCase()
if (
typ === 'submit' ||
typ === 'button' ||
typ === 'image' ||
typ === 'file'
)
return
if (
(typ === 'checkbox' || typ === 'radio') &&
a.checked == null &&
a.value === undefined
) {
/* still include if checked attr present */
}
if (typ === 'checkbox' && a.checked === undefined && !('checked' in a))
return
if (typ === 'radio' && !('checked' in a)) return
var val = a.value
if (name === 'textarea') val = bareSummonTextContent(n)
if (name === 'select') {
val = a.value || ''
var ch = n.children || []
for (var i = 0; i < ch.length; i++) {
if (ch[i].name === 'option' && 'selected' in (ch[i].attrs || {})) {
val =
ch[i].attrs.value != null
? ch[i].attrs.value
: bareSummonTextContent(ch[i])
}
}
}
pairs.push({ name: a.name, value: val == null ? '' : String(val) })
}
var kids = n.children || []
for (var k = 0; k < kids.length; k++) walk(kids[k])
}
walk(formNode)
return {
method: method === 'post' ? 'POST' : 'GET',
action: action,
pairs: pairs
}
}
function bareSummonSubmitUrl(form, pageUrl) {
var spec =
typeof form.method === 'string' ? form : bareSummonCollectForm(form)
var action = bareSummonResolveUrl(spec.action || pageUrl, pageUrl)
if (!action) return null
var q = bareSummonEncodeForm(spec.pairs)
if (spec.method === 'GET') {
var href = action.href.split('#')[0].split('?')[0]
return {
method: 'GET',
url: href + (q ? '?' + q : ''),
body: null
}
}
return {
method: 'POST',
url: action.href,
body: q,
headers: { 'content-type': 'application/x-www-form-urlencoded' }
}
}
/** First-party DOM for summon page JS. Wraps the HTML tree; no host objects leak. */
function bareSummonClassList(node) {
return {
add: function () {
var set = (node.attrs.class || '').split(/\s+/).filter(Boolean)
var i
for (i = 0; i < arguments.length; i++) {
var n = String(arguments[i] || '')
if (n && set.indexOf(n) < 0) set.push(n)
}
node.attrs.class = set.join(' ')
},
remove: function () {
var set = (node.attrs.class || '').split(/\s+/).filter(Boolean)
var i
for (i = 0; i < arguments.length; i++) {
var n = String(arguments[i] || '')
var x = set.indexOf(n)
if (x >= 0) set.splice(x, 1)
}
node.attrs.class = set.join(' ')
},
contains: function (n) {
return (node.attrs.class || '').split(/\s+/).indexOf(String(n || '')) >= 0
},
toggle: function (n) {
n = String(n || '')
if (this.contains(n)) this.remove(n)
else this.add(n)
},
toString: function () {
return node.attrs.class || ''
}
}
}
function bareSummonWrapNode(node, env) {
if (!node) return null
if (node._el) return node._el
if (node.type === 'text') {
var tn = {
nodeType: 3,
nodeName: '#text',
get data() {
return node.text || ''
},
set data(v) {
node.text = String(v)
},
get textContent() {
return node.text || ''
},
set textContent(v) {
node.text = String(v)
}
}
node._el = tn
return tn
}
var el = {
nodeType: 1,
nodeName: String(node.name || '').toUpperCase(),
tagName: String(node.name || '').toUpperCase(),
_node: node,
get id() {
return node.attrs.id || ''
},
set id(v) {
node.attrs.id = String(v == null ? '' : v)
},
get className() {
return node.attrs.class || ''
},
set className(v) {
node.attrs.class = String(v == null ? '' : v)
},
get classList() {
return bareSummonClassList(node)
},
get href() {
return node.attrs.href || ''
},
set href(v) {
node.attrs.href = String(v == null ? '' : v)
},
get src() {
return node.attrs.src || ''
},
set src(v) {
node.attrs.src = String(v == null ? '' : v)
},
get value() {
return node.attrs.value != null ? String(node.attrs.value) : ''
},
set value(v) {
node.attrs.value = String(v == null ? '' : v)
},
get textContent() {
return bareSummonTextContent(node)
},
set textContent(v) {
node.children = [
{
type: 'text',
text: String(v == null ? '' : v),
parent: node,
children: []
}
]
},
get innerHTML() {
return bareSummonSerializeInner(node)
},
set innerHTML(v) {
var kids = bareSummonParseFragment(String(v == null ? '' : v))
var i
for (i = 0; i < kids.length; i++) kids[i].parent = node
node.children = kids
},
getAttribute: function (k) {
k = String(k || '').toLowerCase()
return node.attrs && node.attrs[k] != null ? String(node.attrs[k]) : null
},
setAttribute: function (k, v) {
node.attrs[String(k || '').toLowerCase()] = String(v == null ? '' : v)
},
removeAttribute: function (k) {
delete node.attrs[String(k || '').toLowerCase()]
},
hasAttribute: function (k) {
return (
node.attrs &&
Object.prototype.hasOwnProperty.call(
node.attrs,
String(k || '').toLowerCase()
)
)
},
appendChild: function (child) {
var cn = child && child._node ? child._node : child
if (!cn || !cn.type) return child
if (cn.parent && cn.parent.children) {
var ix = cn.parent.children.indexOf(cn)
if (ix >= 0) cn.parent.children.splice(ix, 1)
}
cn.parent = node
node.children.push(cn)
return bareSummonWrapNode(cn, env)
},
removeChild: function (child) {
var cn = child && child._node ? child._node : child
var ix = node.children.indexOf(cn)
if (ix >= 0) {
node.children.splice(ix, 1)
cn.parent = null
}
return child
},
insertBefore: function (child, ref) {
var cn = child && child._node ? child._node : child
var rn = ref && ref._node ? ref._node : ref
if (cn.parent && cn.parent.children) {
var px = cn.parent.children.indexOf(cn)
if (px >= 0) cn.parent.children.splice(px, 1)
}
cn.parent = node
var at = rn ? node.children.indexOf(rn) : -1
if (at < 0) node.children.push(cn)
else node.children.splice(at, 0, cn)
return bareSummonWrapNode(cn, env)
},
querySelector: function (sel) {
var hits = bareSummonQueryAll(node, sel, 1)
return hits[0] ? bareSummonWrapNode(hits[0], env) : null
},
querySelectorAll: function (sel) {
return bareSummonQueryAll(node, sel, 500).map(function (n) {
return bareSummonWrapNode(n, env)
})
},
getElementsByTagName: function (name) {
name = String(name || '').toLowerCase()
return bareSummonQueryAll(node, name === '*' ? '*' : name, 500).map(
function (n) {
return bareSummonWrapNode(n, env)
}
)
},
getElementsByClassName: function (name) {
return bareSummonQueryAll(node, '.' + String(name || ''), 500).map(
function (n) {
return bareSummonWrapNode(n, env)
}
)
},
addEventListener: function (type, fn) {
if (!node._listeners) node._listeners = {}
var t = String(type || '')
if (!node._listeners[t]) node._listeners[t] = []
node._listeners[t].push(fn)
},
removeEventListener: function (type, fn) {
var t = String(type || '')
var arr = node._listeners && node._listeners[t]
if (!arr) return
node._listeners[t] = arr.filter(function (x) {
return x !== fn
})
},
click: function () {
var arr = (node._listeners && node._listeners.click) || []
var i
for (i = 0; i < arr.length; i++) {
try {
arr[i].call(el)
} catch (e) {
if (env && env.console && env.console.error) env.console.error(e)
}
}
}
}
Object.defineProperty(el, 'style', {
get: function () {
if (!node._styleProxy) {
var decl = bareSummonParseDeclarations(node.attrs.style || '')
node._styleProxy = decl
}
return node._styleProxy
}
})
Object.defineProperty(el, 'children', {
get: function () {
return (node.children || [])
.filter(function (c) {
return c.type === 'element'
})
.map(function (c) {
return bareSummonWrapNode(c, env)
})
}
})
Object.defineProperty(el, 'childNodes', {
get: function () {
return (node.children || []).map(function (c) {
return bareSummonWrapNode(c, env)
})
}
})
Object.defineProperty(el, 'parentNode', {
get: function () {
return node.parent ? bareSummonWrapNode(node.parent, env) : null
}
})
Object.defineProperty(el, 'firstChild', {
get: function () {
var c = node.children && node.children[0]
return c ? bareSummonWrapNode(c, env) : null
}
})
node._el = el
return el
}
function bareSummonFlushInlineStyle(node) {
if (!node) return
if (node._styleProxy) {
var bits = []
var k
for (k in node._styleProxy) {
if (Object.prototype.hasOwnProperty.call(node._styleProxy, k)) {
bits.push(k + ':' + node._styleProxy[k])
}
}
node.attrs.style = bits.join(';')
}
var ch = node.children || []
var i
for (i = 0; i < ch.length; i++) bareSummonFlushInlineStyle(ch[i])
}
function bareSummonCreateDocumentApi(doc, env) {
env = env || {}
var api = {
nodeType: 9,
nodeName: '#document',
documentElement: bareSummonWrapNode(doc.html, env),
head: bareSummonWrapNode(doc.head, env),
body: bareSummonWrapNode(doc.body, env),
get title() {
return doc.title || ''
},
set title(v) {
doc.title = String(v == null ? '' : v)
},
getElementById: function (id) {
var hits = bareSummonQueryAll(doc.root, '#' + String(id || ''), 1)
return hits[0] ? bareSummonWrapNode(hits[0], env) : null
},
querySelector: function (sel) {
var hits = bareSummonQueryAll(doc.root, sel, 1)
return hits[0] ? bareSummonWrapNode(hits[0], env) : null
},
querySelectorAll: function (sel) {
return bareSummonQueryAll(doc.root, sel, 500).map(function (n) {
return bareSummonWrapNode(n, env)
})
},
getElementsByTagName: function (name) {
name = String(name || '').toLowerCase()
return bareSummonQueryAll(doc.root, name === '*' ? '*' : name, 500).map(
function (n) {
return bareSummonWrapNode(n, env)
}
)
},
getElementsByClassName: function (name) {
return bareSummonQueryAll(doc.root, '.' + String(name || ''), 500).map(
function (n) {
return bareSummonWrapNode(n, env)
}
)
},
createElement: function (name) {
return bareSummonWrapNode(
bareSummonCreateNode(
'element',
String(name || 'div').toLowerCase(),
{}
),
env
)
},
createTextNode: function (text) {
return bareSummonWrapNode(
{
type: 'text',
text: String(text == null ? '' : text),
parent: null,
children: []
},
env
)
},
createDocumentFragment: function () {
return bareSummonWrapNode(
bareSummonCreateNode('element', 'fragment', {}),
env
)
}
}
env.document = api
return api
}
/** Run page JS on Bare (ctx.bare.bareVm / bare-realm) or a strict Function sandbox. */
var BARE_SUMMON_JS_MAX_SCRIPTS = 16
var BARE_SUMMON_JS_MAX_SOURCE = 256 * 1024
var BARE_SUMMON_JS_MAX_TIMERS = 32
function bareSummonResolveVm(ctx) {
if (ctx && typeof ctx.bareOsSummonEval === 'function') {
return { kind: 'syscall', eval: ctx.bareOsSummonEval }
}
var b = ctx && ctx.bare
var vm = b && (b.bareVm || b.vm)
if (vm && vm.default) vm = vm.default
if (
vm &&
typeof vm.createContext === 'function' &&
typeof vm.runInContext === 'function'
) {
return { kind: 'vm', vm: vm }
}
return { kind: 'function' }
}
function bareSummonMakeConsole(sink) {
function push(level, args) {
var msg = []
var i
for (i = 0; i < args.length; i++) {
try {
msg.push(typeof args[i] === 'string' ? args[i] : String(args[i]))
} catch (e) {
msg.push('[unprintable]')
}
}
sink.push({ level: level, text: msg.join(' ') })
}
return {
log: function () {
push('log', arguments)
},
info: function () {
push('info', arguments)
},
warn: function () {
push('warn', arguments)
},
error: function () {
push('error', arguments)
},
debug: function () {
push('debug', arguments)
}
}
}
function bareSummonMakeLocation(href) {
var u = bareSummonParseUrl(href) || {
href: String(href || ''),
protocol: '',
host: '',
hostname: '',
port: '',
pathname: '/',
search: '',
hash: '',
origin: ''
}
return {
href: u.href,
protocol: u.protocol,
host: u.host,
hostname: u.hostname,
port: u.port,
pathname: u.pathname,
search: u.search,
hash: u.hash,
origin: u.origin,
toString: function () {
return this.href
}
}
}
function bareSummonEvalSource(engine, source, sandbox) {
if (engine.kind === 'syscall') {
return engine.eval(source, sandbox)
}
if (engine.kind === 'vm') {
var box = engine.vm.createContext()
var k
for (k in sandbox) {
if (Object.prototype.hasOwnProperty.call(sandbox, k)) box[k] = sandbox[k]
}
return engine.vm.runInContext(
'var window = this; var self = this; var globalThis = this;\n' + source,
box
)
}
var fn = new Function(
'window',
'"use strict";' +
'var self = window;' +
'var document = window.document;' +
'var console = window.console;' +
'var location = window.location;' +
'var navigator = window.navigator;' +
'var setTimeout = window.setTimeout;' +
'var clearTimeout = window.clearTimeout;' +
'var fetch = window.fetch;' +
source
)
return fn(sandbox.window || sandbox)
}
async function bareSummonCollectScriptJobs(session, doc, pageUrl) {
var jobs = []
var nodes = (doc && doc.scripts) || []
var i
for (
i = 0;
i < nodes.length && jobs.length < BARE_SUMMON_JS_MAX_SCRIPTS;
i++
) {
var n = nodes[i]
var src = n.attrs && n.attrs.src
var type = ((n.attrs && n.attrs.type) || 'text/javascript').toLowerCase()
if (
type &&
type !== 'text/javascript' &&
type !== 'application/javascript' &&
type !== 'module' &&
type !== ''
) {
if (type.indexOf('javascript') < 0 && type !== 'module') continue
}
if (src) {
var abs = bareSummonResolveUrl(src, pageUrl)
if (!abs || !bareSummonIsHttp(abs)) continue
jobs.push({ node: n, url: abs.href, source: '', external: true })
} else {
var body = bareSummonScriptSource(n)
if (body)
jobs.push({ node: n, url: pageUrl, source: body, external: false })
}
}
for (i = 0; i < jobs.length; i++) {
if (!jobs[i].external) continue
var res = await bareSummonFetch(session.ctx, jobs[i].url, {
jar: session.jar,
maxBytes: BARE_SUMMON_JS_MAX_SOURCE
})
jobs[i].source = (res && res.body) || ''
if (jobs[i].source.length > BARE_SUMMON_JS_MAX_SOURCE) {
jobs[i].source = jobs[i].source.slice(0, BARE_SUMMON_JS_MAX_SOURCE)
}
}
return jobs
}
async function bareSummonRunDocumentJs(session, doc, pageUrl) {
var logs = []
var errors = []
var ran = 0
var engine = bareSummonResolveVm(session && session.ctx)
var timers = []
var tid = 0
var env = { console: bareSummonMakeConsole(logs) }
var document = bareSummonCreateDocumentApi(doc, env)
var location = bareSummonMakeLocation(pageUrl)
var window = {
document: document,
console: env.console,
location: location,
navigator: {
userAgent: 'Summon/0.2 (Bare OS; text; bare-vm)',
platform: 'bare',
language: 'en'
},
innerWidth: session.cols || 80,
innerHeight: 24,
devicePixelRatio: 1,
setTimeout: function (fn, ms) {
if (timers.length >= BARE_SUMMON_JS_MAX_TIMERS) return 0
tid++
timers.push({ id: tid, fn: fn, ms: ms | 0 })
return tid
},
clearTimeout: function (id) {
timers = timers.filter(function (t) {
return t.id !== id
})
},
fetch: function () {
return Promise.reject(new Error('summon: page fetch is not enabled'))
}
}
window.window = window
window.self = window
env.document = document
var jobs = await bareSummonCollectScriptJobs(session, doc, pageUrl)
var i
for (i = 0; i < jobs.length; i++) {
var src = jobs[i].source
if (!src || src.length > BARE_SUMMON_JS_MAX_SOURCE) continue
try {
bareSummonEvalSource(engine, src, {
window: window,
document: document,
console: env.console,
location: location,
navigator: window.navigator
})
ran++
} catch (err) {
errors.push({
url: jobs[i].url,
message: (err && err.message) || String(err)
})
env.console.error((err && err.message) || String(err))
}
}
for (i = 0; i < timers.length; i++) {
if (typeof timers[i].fn === 'function') {
try {
timers[i].fn.call(window)
} catch (e2) {
errors.push({ url: pageUrl, message: (e2 && e2.message) || String(e2) })
}
}
}
bareSummonFlushInlineStyle(doc.body)
bareSummonFlushInlineStyle(doc.head)
return {
engine: engine.kind,
ran: ran,
blocked: Math.max(0, ((doc.scripts && doc.scripts.length) || 0) - ran),
logs: logs,
errors: errors
}
}
/** Tabs, history, bookmarks, about: pages. */
function bareSummonHome(ctx) {
var env = ctx && ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
if (env.HOME) return String(env.HOME).replace(/\/+$/, '')
return '/home/guest'
}
function bareSummonPaths(ctx) {
var h = bareSummonHome(ctx)
return {
dir: h + '/.summon',
config: h + '/.summon/config.json',
cookies: h + '/.summon/cookies.json',
bookmarks: h + '/.summon/bookmarks.json',
history: h + '/.summon/history.jsonl'
}
}
function bareSummonCreateTab(url) {
return {
url: url || 'about:summon',
title: '',
loading: false,
status: 0,
error: '',
doc: null,
layout: null,
source: '',
reader: false,
find: '',
history: { back: [], forward: [] },
scriptsBlocked: 0,
stylesBlocked: 0
}
}
function bareSummonCreateSession(ctx, opts) {
opts = opts || {}
var jar = bareSummonCreateCookieJar()
return {
ctx: ctx,
jar: jar,
tabs: [bareSummonCreateTab(opts.url || 'about:summon')],
current: 0,
bookmarks: Array.isArray(opts.bookmarks) ? opts.bookmarks.slice() : [],
config: opts.config || { javascript: true, images: 'alt', numbers: true },
cols: opts.cols || 80
}
}
function bareSummonCurrent(session) {
return session.tabs[session.current] || session.tabs[0]
}
function bareSummonAboutBody(kind, session) {
if (kind === 'blank') return '<html><body></body></html>'
if (kind === 'bookmarks') {
var items = (session.bookmarks || [])
.map(function (b) {
return (
'<li><a href="' +
String(b.url || '') +
'">' +
String(b.title || b.url || '') +
'</a></li>'
)
})
.join('')
return (
'<html><body><h1>Bookmarks</h1><ul>' +
(items || '<li>(none)</li>') +
'</ul></body></html>'
)
}
if (kind === 'history') {
var t = bareSummonCurrent(session)
var h = (t.history.back || [])
.slice(-20)
.map(function (u) {
return '<li><a href="' + u + '">' + u + '</a></li>'
})
.join('')
return (
'<html><body><h1>History</h1><ul>' +
(h || '<li>(empty)</li>') +
'</ul></body></html>'
)
}
if (kind === 'cookies') {
var rows = session.jar
.toJSON()
.map(function (c) {
return '<li>' + c.name + ' @ ' + c.domain + c.path + '</li>'
})
.join('')
return (
'<html><body><h1>Cookies</h1><ul>' +
(rows || '<li>(none)</li>') +
'</ul></body></html>'
)
}
if (kind === 'net') {
return '<html><body><h1>Net</h1><p>HTTP via ctx.httpFetch. Allow/deny: BARE_OS_HTTP_ALLOWLIST / DENYLIST.</p></body></html>'
}
return (
'<html><body><h1>summon</h1><p>Text browser for Bare OS. Page JS runs in a Bare VM (ctx.bare.bareVm) when present.</p>' +
'<p>Type a URL (g) or open a bookmark. Try <a href="about:bookmarks">about:bookmarks</a>. Toggle JS with J.</p></body></html>'
)
}
function bareSummonCollectInlineSheets(doc) {
var inline = []
function collectStyle(n) {
if (n && n.type === 'element' && n.name === 'style') {
inline.push(bareSummonParseStylesheet(bareSummonTextContent(n)))
}
var ch = (n && n.children) || []
var i
for (i = 0; i < ch.length; i++) collectStyle(ch[i])
}
if (doc) {
collectStyle(doc.head)
collectStyle(doc.body)
}
return inline
}
function bareSummonFinishLayout(session, tab, url) {
var sheets = (tab.inlineSheets || []).concat(tab.fetchedSheets || [])
tab.sheets = sheets
var layout = bareSummonLayout(tab.doc, {
cols: session.cols,
sheets: sheets,
numbers: session.config.numbers !== false
})
tab.layout = layout
tab.title = layout.title || (tab.doc && tab.doc.title) || url || tab.url
return tab
}
function bareSummonApplyDocument(session, tab, html, url) {
var doc = bareSummonParseHtml(html, {})
tab.doc = doc
tab.source = html
tab.url = url
tab.inlineSheets = bareSummonCollectInlineSheets(doc)
tab.fetchedSheets = tab.fetchedSheets || []
tab.scriptsBlocked = doc.scriptsBlocked || 0
tab.js = tab.js || { ran: 0, engine: 'off', errors: [], logs: [] }
bareSummonFinishLayout(session, tab, url)
}
async function bareSummonEnhanceDocument(session, tab, url) {
if (!tab || !tab.doc) return tab
var hrefs = tab.doc.stylesheets || []
var fetched = []
var i
for (i = 0; i < hrefs.length && i < 4; i++) {
var abs = bareSummonResolveUrl(hrefs[i], url)
if (!abs || !bareSummonIsHttp(abs)) continue
try {
var res = await bareSummonFetch(session.ctx, abs.href, {
jar: session.jar
})
if (res && res.body) fetched.push(bareSummonParseStylesheet(res.body))
else tab.stylesBlocked = (tab.stylesBlocked || 0) + 1
} catch (e) {
tab.stylesBlocked = (tab.stylesBlocked || 0) + 1
}
}
tab.fetchedSheets = fetched
if (session.config && session.config.javascript !== false) {
tab.js = await bareSummonRunDocumentJs(session, tab.doc, url)
tab.inlineSheets = bareSummonCollectInlineSheets(tab.doc)
tab.scriptsBlocked = tab.js.blocked
tab.title = (tab.doc && tab.doc.title) || tab.title
} else {
tab.js = {
ran: 0,
engine: 'off',
blocked:
(tab.doc.scripts && tab.doc.scripts.length) || tab.scriptsBlocked || 0,
errors: [],
logs: []
}
}
bareSummonFinishLayout(session, tab, url)
return tab
}
function bareSummonRelayout(session) {
var tab = bareSummonCurrent(session)
if (!tab || !tab.doc) return tab
tab.layout = bareSummonLayout(tab.doc, {
cols: session.cols,
sheets: tab.sheets || [],
numbers: session.config.numbers !== false
})
return tab
}
async function bareSummonNavigate(session, urlStr, opts) {
opts = opts || {}
var tab = bareSummonCurrent(session)
var resolved = bareSummonResolveUrl(urlStr, tab.url)
if (!resolved || !bareSummonUrlOk(resolved)) {
tab.error = 'bad URL'
tab.loading = false
return tab
}
if (!opts.replace && tab.url && tab.url !== resolved.href) {
tab.history.back.push(tab.url)
tab.history.forward = []
}
tab.loading = true
tab.error = ''
tab.reader = false
if (resolved.protocol === 'about:') {
var kind = resolved.pathname || 'summon'
if (kind.charAt(0) === ':') kind = kind.slice(1)
bareSummonApplyDocument(
session,
tab,
bareSummonAboutBody(kind, session),
resolved.href
)
await bareSummonEnhanceDocument(session, tab, resolved.href)
tab.status = 200
tab.loading = false
return tab
}
if (resolved.protocol === 'file:') {
var ctx = session.ctx
var path = resolved.pathname || '/'
try {
var buf =
ctx.vfs && ctx.vfs.readFile ? await ctx.vfs.readFile(path) : null
var text = ''
if (buf) {
text =
ctx.b4a && typeof ctx.b4a.toString === 'function'
? ctx.b4a.toString(buf)
: String(buf)
}
bareSummonApplyDocument(
session,
tab,
text || '<html><body>(empty file)</body></html>',
resolved.href
)
await bareSummonEnhanceDocument(session, tab, resolved.href)
tab.status = buf ? 200 : 404
} catch (e) {
tab.error = (e && e.message) || String(e)
tab.status = 0
}
tab.loading = false
return tab
}
var fetched = await bareSummonFetch(session.ctx, resolved.href, {
jar: session.jar,
method: opts.method || 'GET',
body: opts.body,
headers: opts.headers
})
if (!fetched.ok && !fetched.body) {
tab.error = fetched.error || 'fetch failed'
tab.status = fetched.status || 0
tab.loading = false
return tab
}
tab.status = fetched.status
var body = fetched.body || ''
var ct = (fetched.headers && fetched.headers['content-type']) || ''
if (!/html/i.test(ct) && body && body.charAt(0) !== '<') {
body =
'<html><body><pre>' +
body.replace(/&/g, '&amp;').replace(/</g, '&lt;') +
'</pre></body></html>'
}
bareSummonApplyDocument(session, tab, body, fetched.url || resolved.href)
await bareSummonEnhanceDocument(session, tab, fetched.url || resolved.href)
tab.loading = false
return tab
}
function bareSummonBack(session) {
var tab = bareSummonCurrent(session)
if (!tab.history.back.length) return Promise.resolve(tab)
tab.history.forward.unshift(tab.url)
var prev = tab.history.back.pop()
return bareSummonNavigate(session, prev, { replace: true })
}
function bareSummonForward(session) {
var tab = bareSummonCurrent(session)
if (!tab.history.forward.length) return Promise.resolve(tab)
var next = tab.history.forward.shift()
return bareSummonNavigate(session, next, { replace: true })
}
function bareSummonAddBookmark(session, url, title) {
session.bookmarks.push({ url: url, title: title || url })
}
function bareSummonReload(session) {
var tab = bareSummonCurrent(session)
return bareSummonNavigate(session, tab.url, { replace: true })
}
function bareSummonFollowLink(session, id) {
var tab = bareSummonCurrent(session)
var links = (tab.layout && tab.layout.links) || []
var hit = null
var i
for (i = 0; i < links.length; i++) {
if (links[i].id === id) hit = links[i]
}
if (!hit) return Promise.resolve(tab)
return bareSummonNavigate(session, hit.href)
}
function bareSummonNewTab(session, url) {
session.tabs.push(bareSummonCreateTab(url || 'about:summon'))
session.current = session.tabs.length - 1
return bareSummonCurrent(session)
}
function bareSummonCloseTab(session) {
if (session.tabs.length <= 1) {
session.tabs[0] = bareSummonCreateTab('about:summon')
session.current = 0
return session.tabs[0]
}
session.tabs.splice(session.current, 1)
if (session.current >= session.tabs.length)
session.current = session.tabs.length - 1
return bareSummonCurrent(session)
}
function bareSummonSelectTab(session, index) {
if (index < 0 || index >= session.tabs.length)
return bareSummonCurrent(session)
session.current = index
return bareSummonCurrent(session)
}
function bareSummonCycleTab(session, dir) {
var n = session.tabs.length
if (!n) return bareSummonCurrent(session)
session.current = (session.current + dir + n) % n
return bareSummonCurrent(session)
}
function bareSummonBufToString(ctx, buf) {
if (buf == null) return ''
if (typeof buf === 'string') return buf
if (ctx && ctx.b4a && typeof ctx.b4a.toString === 'function') {
return ctx.b4a.toString(buf)
}
return String(buf)
}
async function bareSummonReadJson(ctx, path) {
if (!ctx || !ctx.vfs || typeof ctx.vfs.readFile !== 'function') return null
try {
var buf = ctx.vfs.readFile(path)
if (buf && typeof buf.then === 'function') buf = await buf
var t = String(bareSummonBufToString(ctx, buf) || '').trim()
if (!t) return null
return JSON.parse(t)
} catch (e) {
return null
}
}
async function bareSummonWriteJson(ctx, path, value) {
if (!ctx || !ctx.vfs || typeof ctx.vfs.writeFile !== 'function') return false
try {
var dir = path.slice(0, path.lastIndexOf('/'))
if (dir && ctx.vfs.mkdir) {
try {
var mk = ctx.vfs.mkdir(dir, { recursive: true })
if (mk && typeof mk.then === 'function') await mk
} catch (e) {
/* ignore */
}
}
var body = JSON.stringify(value, null, 2)
var w = ctx.vfs.writeFile(path, body)
if (w && typeof w.then === 'function') await w
return true
} catch (e2) {
return false
}
}
async function bareSummonLoadPersisted(session) {
var ctx = session.ctx
var paths = bareSummonPaths(ctx)
var cfg = await bareSummonReadJson(ctx, paths.config)
if (cfg && typeof cfg === 'object') {
session.config = Object.assign({}, session.config, cfg)
}
var marks = await bareSummonReadJson(ctx, paths.bookmarks)
if (Array.isArray(marks)) session.bookmarks = marks
var cookies = await bareSummonReadJson(ctx, paths.cookies)
if (Array.isArray(cookies)) session.jar.load(cookies)
return session
}
async function bareSummonSavePersisted(session) {
var ctx = session.ctx
var paths = bareSummonPaths(ctx)
await bareSummonWriteJson(ctx, paths.bookmarks, session.bookmarks || [])
await bareSummonWriteJson(ctx, paths.cookies, session.jar.toJSON())
await bareSummonWriteJson(ctx, paths.config, session.config || {})
}
/** TEA summon TUI. Cell buffer; engine lives in summon-*.js. */
function bareSummonTuiRunOpts() {
return { buffer: 'cell' }
}
function bareSummonCreateTuiApp(ctx, session) {
var tui = ctx.tui
var size0 = tui && typeof tui.size === 'function' ? tui.size() : {}
return {
session: session,
width: size0.width || 80,
height: size0.height || 24,
mode: 'browse',
help: false,
quitConfirm: false,
status: '',
num: '',
linkFocus: 0,
input: tui.textinput.create({ prompt: '', focused: true, charLimit: 800 }),
viewport: tui.viewport.create({
width: size0.width || 80,
height: Math.max(6, (size0.height || 24) - 5)
}),
init: function () {
var self = this
self.session.cols = Math.max(20, (self.width || 80) - 2)
var tab = bareSummonCurrent(self.session)
if (tab && tab.url) self._go(tab.url, { replace: true })
else self._sync()
return null
},
_tab: function () {
return bareSummonCurrent(this.session)
},
_sync: function () {
var tab = this._tab()
var cols = Math.max(20, (this.width || 80) - 2)
this.session.cols = cols
if (tab && tab.doc && (!tab.layout || tab.layout.cols !== cols)) {
bareSummonRelayout(this.session)
tab = this._tab()
}
this.viewport.width = cols
this.viewport.height = Math.max(4, (this.height || 24) - 5)
var lines = []
if (this.mode === 'source') {
lines = String((tab && tab.source) || '').split('\n')
} else if (tab && tab.reader && tab.doc) {
lines = String(bareSummonReaderText(tab.doc) || '').split('\n')
} else if (tab && tab.layout && tab.layout.plain) {
lines = tab.layout.plain.slice()
}
if (tab && tab.error) lines.unshift('error: ' + tab.error)
if (tab && tab.loading) lines.unshift('loading…')
this.viewport.setContent(lines.join('\n'))
var nlinks =
tab && tab.layout && tab.layout.links ? tab.layout.links.length : 0
if (this.linkFocus >= nlinks) this.linkFocus = Math.max(0, nlinks - 1)
},
_go: function (url, opts) {
var self = this
var tab = this._tab()
if (tab) tab.loading = true
this.status = 'loading…'
this.mode = 'browse'
this.num = ''
bareSummonNavigate(this.session, url, opts || {})
.then(function () {
self.status = ''
self._sync()
if (tui && typeof tui.send === 'function') {
tui.send({ type: 'summon.loaded' })
}
})
.catch(function (err) {
var cur = self._tab()
if (cur) {
cur.error = (err && err.message) || String(err)
cur.loading = false
}
self.status = 'error'
self._sync()
if (tui && typeof tui.send === 'function') {
tui.send({ type: 'summon.loaded' })
}
})
},
_follow: function (id) {
var tab = this._tab()
var links = (tab && tab.layout && tab.layout.links) || []
var hit = null
var i
for (i = 0; i < links.length; i++) {
if (links[i].id === id) hit = links[i]
}
if (!hit) {
this.status = 'no link ' + (id + 1)
return
}
this._go(hit.href)
},
_promptSubmit: function () {
var line = String(this.input.value || '').trim()
this.input.reset()
var mode = this.mode
this.mode = 'browse'
if (!line) return
if (mode === 'goto') this._go(line)
else if (mode === 'find') {
var tab = this._tab()
if (tab) tab.find = line
this._find(1)
}
},
_find: function (dir) {
var tab = this._tab()
var q = tab && tab.find ? String(tab.find) : ''
if (!q) {
this.status = 'no find'
return
}
var lines = this.viewport.lines || []
var start = this.viewport.yOffset + (dir > 0 ? 1 : -1)
var i
if (dir > 0) {
for (i = start; i < lines.length; i++) {
if (
String(lines[i] || '')
.toLowerCase()
.indexOf(q.toLowerCase()) >= 0
) {
this.viewport.setYOffset(i)
this.status = 'find ' + q
return
}
}
} else {
for (i = start; i >= 0; i--) {
if (
String(lines[i] || '')
.toLowerCase()
.indexOf(q.toLowerCase()) >= 0
) {
this.viewport.setYOffset(i)
this.status = 'find ' + q
return
}
}
}
this.status = 'not found: ' + q
},
update: function (msg) {
if (msg && msg.type === 'resize') {
this.width = msg.width || this.width
this.height = msg.height || this.height
this._sync()
return [this, null]
}
if (msg && msg.type === 'summon.loaded') {
this._sync()
return [this, null]
}
if (this.help) {
if (msg && msg.type === 'key') this.help = false
return [this, null]
}
if (this.quitConfirm) {
if (tui.key.matches(msg, 'y', 'Y')) return [this, tui.quit]
if (msg && msg.type === 'key') this.quitConfirm = false
return [this, null]
}
if (this.mode === 'goto' || this.mode === 'find') {
if (tui.key.matches(msg, 'escape', 'ctrl+c')) {
this.mode = 'browse'
this.input.reset()
return [this, null]
}
if (tui.key.matches(msg, 'enter')) {
this._promptSubmit()
return [this, null]
}
var pair = this.input.update(msg)
this.input = pair[0]
return [this, pair[1]]
}
if (tui.key.matches(msg, 'ctrl+c', 'q')) {
this.quitConfirm = true
return [this, null]
}
if (tui.key.matches(msg, 'f10')) return [this, tui.quit]
if (tui.key.matches(msg, '?')) {
this.help = true
return [this, null]
}
if (tui.key.matches(msg, 'g')) {
this.mode = 'goto'
this.input.reset()
this.input.setValue && this.input.setValue('')
return [this, null]
}
if (tui.key.matches(msg, '/')) {
this.mode = 'find'
this.input.reset()
return [this, null]
}
if (tui.key.matches(msg, 'n')) {
this._find(1)
return [this, null]
}
if (tui.key.matches(msg, 'N')) {
this._find(-1)
return [this, null]
}
if (tui.key.matches(msg, 'r')) {
this._go(this._tab().url, { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 'R')) {
var tabR = this._tab()
tabR.reader = !tabR.reader
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'J')) {
this.session.config.javascript =
this.session.config.javascript === false
this.status =
this.session.config.javascript === false ? 'js off' : 'js on'
if (this._tab().url) this._go(this._tab().url, { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 's')) {
this.mode = this.mode === 'source' ? 'browse' : 'source'
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'H')) {
this._go('about:summon')
return [this, null]
}
if (tui.key.matches(msg, 'o')) {
this._go('about:bookmarks')
return [this, null]
}
if (tui.key.matches(msg, 'a')) {
var tabA = this._tab()
bareSummonAddBookmark(this.session, tabA.url, tabA.title)
this.status = 'bookmarked'
if (typeof bareSummonSavePersisted === 'function') {
bareSummonSavePersisted(this.session)
}
return [this, null]
}
if (tui.key.matches(msg, 'left', 'h')) {
var selfB = this
bareSummonBack(this.session).then(function () {
selfB._sync()
if (tui && typeof tui.send === 'function')
tui.send({ type: 'summon.loaded' })
})
return [this, null]
}
if (tui.key.matches(msg, 'u')) {
var selfF = this
bareSummonForward(this.session).then(function () {
selfF._sync()
if (tui && typeof tui.send === 'function')
tui.send({ type: 'summon.loaded' })
})
return [this, null]
}
if (tui.key.matches(msg, 't')) {
bareSummonNewTab(this.session, 'about:summon')
this._go('about:summon', { replace: true })
return [this, null]
}
if (tui.key.matches(msg, 'w')) {
bareSummonCloseTab(this.session)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, ']')) {
bareSummonCycleTab(this.session, 1)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, '[')) {
bareSummonCycleTab(this.session, -1)
this._sync()
return [this, null]
}
if (tui.key.matches(msg, 'tab')) {
var tabT = this._tab()
var n =
tabT && tabT.layout && tabT.layout.links
? tabT.layout.links.length
: 0
if (n) this.linkFocus = (this.linkFocus + 1) % n
return [this, null]
}
if (tui.key.matches(msg, 'enter')) {
if (this.num) {
var id = parseInt(this.num, 10) - 1
this.num = ''
if (id >= 0) this._follow(id)
return [this, null]
}
this._follow(this.linkFocus)
return [this, null]
}
if (
msg &&
msg.type === 'key' &&
msg.name &&
msg.name.length === 1 &&
msg.name >= '0' &&
msg.name <= '9'
) {
this.num += msg.name
this.status = '#' + this.num
return [this, null]
}
if (tui.key.matches(msg, 'j', 'down')) {
this.viewport.scrollDown(1)
return [this, null]
}
if (tui.key.matches(msg, 'k', 'up')) {
this.viewport.scrollUp(1)
return [this, null]
}
if (tui.key.matches(msg, ' ', 'pagedown')) {
this.viewport.scrollDown(this.viewport.height || 10)
return [this, null]
}
if (tui.key.matches(msg, 'pageup')) {
this.viewport.scrollUp(this.viewport.height || 10)
return [this, null]
}
if (tui.key.matches(msg, 'home')) {
this.viewport.gotoTop()
return [this, null]
}
if (tui.key.matches(msg, 'end')) {
this.viewport.gotoBottom()
return [this, null]
}
return [this, null]
},
view: function () {
var cols = Math.max(40, this.width || 80)
var rows = Math.max(12, this.height || 24)
var st = tui.style
var tab = this._tab()
var ntab = this.session.tabs.length
var idx = this.session.current + 1
var url = (tab && tab.url) || ''
var title = (tab && tab.title) || ''
var jsOn = this.session.config && this.session.config.javascript !== false
var scripts =
' js:' +
(jsOn ? 'on' : 'off') +
(tab && tab.js && tab.js.ran ? '+' + tab.js.ran : '') +
(tab && tab.scriptsBlocked ? '/' + tab.scriptsBlocked : '')
var titleRaw =
' summon [' +
idx +
'/' +
ntab +
'] ' +
(title || url) +
(tab && tab.reader ? ' reader' : '') +
(this.mode === 'source' ? ' source' : '') +
scripts +
' ' +
cols +
'x' +
rows +
' '
var bar = st
? st()
.foreground('brightwhite')
.background('blue')
.width(cols)
.render(titleRaw)
: titleRaw
var locRaw =
' ' + url + ' ' + (tab && tab.status ? tab.status : '') + ' '
var loc = st ? st().dim().width(cols).render(locRaw) : locRaw
var body = this.viewport.view()
var prompt = ''
if (this.mode === 'goto') {
prompt =
'Go: ' +
(this.input.view ? this.input.view() : this.input.value || '')
} else if (this.mode === 'find') {
prompt =
'Find: ' +
(this.input.view ? this.input.view() : this.input.value || '')
} else {
prompt =
(this.status || '') +
(this.num ? ' #' + this.num : '') +
(this.linkFocus >= 0 &&
tab &&
tab.layout &&
tab.layout.links[this.linkFocus]
? ' → ' + tab.layout.links[this.linkFocus].href
: '')
}
var footRaw =
'g go Enter follow 12+Enter link h back j/k /find a mark o marks q quit ?'
var foot = st ? st().dim().width(cols).render(footRaw) : footRaw
var promptLine = st
? st()
.width(cols)
.render(prompt || ' ')
: prompt || ' '
var lines = [bar, loc]
.concat(String(body).split('\n'))
.concat([promptLine, foot])
while (lines.length < rows) lines.push('')
var out = []
var i
for (i = 0; i < rows; i++) {
out.push(
st
? st.truncate(lines[i] || '', cols)
: String(lines[i] || '').slice(0, cols)
)
}
return out.join('\n')
},
overlay: function (size) {
if (!this.help && !this.quitConfirm) return null
var cols = (size && size.width) || this.width || 80
var rows = (size && size.height) || this.height || 24
var st = tui.style
var body = this.quitConfirm
? 'Leave summon?\n\nY yes any other key cancel'
: 'summon — text web browser\n\n' +
'g URL go Enter follow focused / numbered link\n' +
'h/left back u forward r reload R reader s source\n' +
'j/k scroll / find n/N next/prev a bookmark o marks\n' +
't new tab w close [ ] tabs H home digits+Enter link #\n' +
'J toggle page JS (Bare VM / sandbox) HTTP via ctx.httpFetch\n\n' +
'Press any key to close.'
var boxed = st
? st()
.border(st.borders.rounded)
.padding(1, 2)
.background('black')
.render(body)
: body
var h = st ? st.height(boxed) : boxed.split('\n').length
var w = st ? st.width(boxed) : 56
return {
row: Math.max(0, Math.floor((rows - h) / 2)),
col: Math.max(0, Math.floor((cols - w) / 2)),
text: boxed
}
}
}
}
/**
* Text web browser. TTY → ctx.tui; otherwise dump | links | get | bookmarks.
*/
function summonUsage(argv0) {
return (
'usage: ' +
(argv0 || 'summon') +
' [URL]\n' +
' ' +
(argv0 || 'summon') +
' dump|links|get URL\n' +
' ' +
(argv0 || 'summon') +
' bookmarks [add URL [TITLE]]\n' +
' ' +
(argv0 || 'summon') +
' about [summon|bookmarks|history|cookies|net|blank]\n' +
'Text browser. Page JS on (Bare VM). --no-js to disable.\n' +
'See man summon.'
)
}
function bareSummonParseFlags(argv) {
var out = {
help: false,
json: false,
reader: false,
js: null,
cols: 80,
rest: []
}
var i
for (i = 1; i < argv.length; i++) {
var a = argv[i]
if (a === '-h' || a === '--help') out.help = true
else if (a === '--json') out.json = true
else if (a === '--reader') out.reader = true
else if (a === '--js') out.js = true
else if (a === '--no-js') out.js = false
else if (a === '--cols' && argv[i + 1]) {
out.cols = parseInt(argv[++i], 10) || 80
} else if (a.slice(0, 7) === '--cols=') {
out.cols = parseInt(a.slice(7), 10) || 80
} else out.rest.push(a)
}
return out
}
async function bareSummonEnsureSession(ctx, flags, startUrl) {
var session = bareSummonCreateSession(ctx, {
url: startUrl || 'about:summon',
cols: flags.cols || 80
})
await bareSummonLoadPersisted(session)
if (flags.js != null) session.config.javascript = flags.js
if (startUrl) session.tabs[0].url = startUrl
return session
}
async function bareSummonPrintDump(ctx, session, url, flags) {
var tab = await bareSummonNavigate(session, url)
if (tab.error && !tab.layout) {
ctx.console.error('summon: ' + tab.error)
ctx.exitCode = 1
return
}
if (flags.json) {
ctx.console.log(
JSON.stringify(
{
url: tab.url,
title: tab.title,
status: tab.status,
error: tab.error || '',
scriptsBlocked: tab.scriptsBlocked,
links: (tab.layout && tab.layout.links) || [],
text:
flags.reader && tab.doc
? bareSummonReaderText(tab.doc)
: (tab.layout && tab.layout.plain) || []
},
null,
2
)
)
return
}
if (flags.reader && tab.doc) ctx.console.log(bareSummonReaderText(tab.doc))
else ctx.console.log(((tab.layout && tab.layout.plain) || []).join('\n'))
}
async function run(ctx, argv) {
var flags = bareSummonParseFlags(argv)
if (flags.help) {
ctx.console.log(summonUsage(argv[0]))
return
}
var sub = flags.rest[0] || ''
var arg1 = flags.rest[1] || ''
if (sub === 'bookmarks' && flags.rest[1] !== 'add') {
var sessionB = await bareSummonEnsureSession(ctx, flags)
if (flags.json) {
ctx.console.log(JSON.stringify(sessionB.bookmarks || [], null, 2))
} else {
var marks = sessionB.bookmarks || []
if (!marks.length) ctx.console.log('(no bookmarks)')
var bi
for (bi = 0; bi < marks.length; bi++) {
ctx.console.log(
(marks[bi].title || marks[bi].url) + '\t' + marks[bi].url
)
}
}
return
}
if (sub === 'bookmarks' && flags.rest[1] === 'add') {
var addUrl = flags.rest[2]
if (!addUrl) {
ctx.console.error('summon: bookmarks add URL [TITLE]')
ctx.exitCode = 1
return
}
var sessionA = await bareSummonEnsureSession(ctx, flags)
bareSummonAddBookmark(
sessionA,
addUrl,
flags.rest.slice(3).join(' ') || addUrl
)
await bareSummonSavePersisted(sessionA)
ctx.console.log('bookmarked ' + addUrl)
return
}
var start =
sub === 'dump' || sub === 'links' || sub === 'get' || sub === 'about'
? arg1
: sub
if (sub === 'about' && !arg1) start = 'summon'
if (sub === 'about') start = 'about:' + (arg1 || 'summon')
if (sub === 'dump' || sub === 'links' || sub === 'get') {
if (!start) {
ctx.console.error('summon: ' + sub + ' URL')
ctx.exitCode = 1
return
}
var session = await bareSummonEnsureSession(ctx, flags, start)
if (sub === 'get') {
var tabG = await bareSummonNavigate(session, start)
if (tabG.error && !tabG.source) {
ctx.console.error('summon: ' + tabG.error)
ctx.exitCode = 1
return
}
ctx.console.log(tabG.source || '')
return
}
if (sub === 'links') {
var tabL = await bareSummonNavigate(session, start)
var links = (tabL.layout && tabL.layout.links) || []
if (flags.json) {
ctx.console.log(JSON.stringify(links, null, 2))
return
}
var li
for (li = 0; li < links.length; li++) {
ctx.console.log(
links[li].id + 1 + '\t' + links[li].text + '\t' + links[li].href
)
}
return
}
await bareSummonPrintDump(ctx, session, start, flags)
return
}
if (sub === 'about') {
var sessionAb = await bareSummonEnsureSession(ctx, flags, start)
await bareSummonPrintDump(ctx, sessionAb, start, flags)
return
}
var stdin = ctx.replStdin
var isTTY = Boolean(stdin && stdin.isTTY)
var url = start || 'about:summon'
if (!isTTY || !ctx.tui || typeof ctx.tui.run !== 'function') {
if (!start) {
ctx.console.error(
'summon: TTY + ctx.tui required for full-screen mode; use: summon dump URL'
)
ctx.exitCode = 1
return
}
var sessionD = await bareSummonEnsureSession(ctx, flags, url)
await bareSummonPrintDump(ctx, sessionD, url, flags)
return
}
try {
var live = await bareSummonEnsureSession(ctx, flags, url)
var app = bareSummonCreateTuiApp(ctx, live)
await ctx.tui.run(app, bareSummonTuiRunOpts())
await bareSummonSavePersisted(live)
} catch (err) {
ctx.console.error('summon: ' + ((err && err.message) || String(err)))
ctx.exitCode = 1
}
}