/* 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} 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} 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} 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} 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} 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} 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) === '', i + 4) i = cend < 0 ? s.length : cend + 3 continue } if (/^', i) i = gt < 0 ? s.length : gt + 1 continue } var close = s.charAt(i + 1) === '/' var end = s.indexOf('>', i) if (end < 0) break var inner = s.slice(i + (close ? 2 : 1), end) var self = false if (inner.charAt(inner.length - 1) === '/') { self = true inner = inner.slice(0, -1) } var sp = inner.search(/\s/) var name = (sp < 0 ? inner : inner.slice(0, sp)).toLowerCase() var attrRaw = sp < 0 ? '' : inner.slice(sp) if (!name) { i = end + 1 continue } if (name === 'script' || name === 'style') { var closeTag = '', k) i = closeEnd < 0 ? s.length : closeEnd + 1 continue } if (close) tokens.push({ kind: 'close', name: name }) else tokens.push({ kind: 'open', name: name, attrs: bareSummonParseAttrs(attrRaw), self: self || !!BARE_SUMMON_VOID[name] }) i = end + 1 } return tokens } function bareSummonParseHtml(html, opts) { opts = opts || {} var max = opts.maxNodes > 0 ? opts.maxNodes : 8000 var tokens = bareSummonTokenizeHtml(html, max) var root = bareSummonCreateNode('element', 'document', {}) var htmlEl = bareSummonCreateNode('element', 'html', {}) var head = bareSummonCreateNode('element', 'head', {}) var body = bareSummonCreateNode('element', 'body', {}) htmlEl.children.push(head) head.parent = htmlEl htmlEl.children.push(body) body.parent = htmlEl root.children.push(htmlEl) htmlEl.parent = root var stack = [body] var scripts = 0 var stylesheets = [] function current() { return stack[stack.length - 1] || body } function closeUntil(name) { while (stack.length > 1) { var top = stack[stack.length - 1] if (top.name === name) { stack.pop() return } stack.pop() } } for (var t = 0; t < tokens.length; t++) { var tok = tokens[t] if (tok.kind === 'text') { var par = current() if (tok.raw && par.name !== 'style' && par.name !== 'script') continue par.children.push({ type: 'text', text: tok.text, parent: par, children: [] }) continue } if (tok.kind === 'close') { if (tok.name === 'html' || tok.name === 'body' || tok.name === 'head') continue closeUntil(tok.name) continue } if (tok.name === 'html' || tok.name === 'body' || tok.name === 'head') { if (tok.name === 'html') Object.assign(htmlEl.attrs, tok.attrs) if (tok.name === 'body') Object.assign(body.attrs, tok.attrs) if (tok.name === 'head') Object.assign(head.attrs, tok.attrs) continue } if (tok.name === 'script') { scripts++ var snode = bareSummonCreateNode('element', 'script', tok.attrs) var sdest = current() sdest.children.push(snode) snode.parent = sdest if (!tok.self) stack.push(snode) continue } var auto = BARE_SUMMON_AUTO_CLOSE[current().name] if (auto && auto[tok.name]) stack.pop() var node = bareSummonCreateNode('element', tok.name, tok.attrs) if (tok.name === 'link' && /stylesheet/i.test(tok.attrs.rel || '')) { stylesheets.push(tok.attrs.href || '') } var dest = tok.name === 'title' || tok.name === 'meta' || tok.name === 'link' ? head : current() if ( tok.name === 'title' || tok.name === 'meta' || tok.name === 'style' || tok.name === 'link' ) { dest = head } dest.children.push(node) node.parent = dest if (!tok.self && !BARE_SUMMON_VOID[tok.name]) { stack.push(node) } } var title = '' function findTitle(n) { if (!n) return if (n.type === 'element' && n.name === 'title') { title = (n.children || []) .map(function (c) { return c.text || '' }) .join('') .trim() return } var ch = n.children || [] for (var i = 0; i < ch.length && !title; i++) findTitle(ch[i]) } findTitle(head) var scriptNodes = [] function collectScripts(n) { if (n && n.type === 'element' && n.name === 'script') scriptNodes.push(n) var kids = (n && n.children) || [] var si for (si = 0; si < kids.length; si++) collectScripts(kids[si]) } collectScripts(root) return { root: root, html: htmlEl, head: head, body: body, title: title, scriptsBlocked: scripts, scripts: scriptNodes, stylesheets: stylesheets } } function bareSummonWalk(node, fn) { if (!node) return fn(node) var ch = node.children || [] for (var i = 0; i < ch.length; i++) bareSummonWalk(ch[i], fn) } function bareSummonTextContent(node) { return bareSummonTextContentInner(node, true) } function bareSummonTextContentInner(node, root) { if (!node) return '' if (node.type === 'text') return node.text || '' if (!root && (node.name === 'script' || node.name === 'style')) return '' var s = '' var ch = node.children || [] for (var i = 0; i < ch.length; i++) s += bareSummonTextContentInner(ch[i], false) return s } function bareSummonEscapeHtml(s) { return String(s || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') } function bareSummonSerializeInner(node) { if (!node) return '' var s = '' var ch = node.children || [] var i for (i = 0; i < ch.length; i++) { var c = ch[i] if (c.type === 'text') { s += bareSummonEscapeHtml(c.text) continue } if (c.type !== 'element') continue var attrs = c.attrs || {} var bits = '<' + c.name var k for (k in attrs) { if (Object.prototype.hasOwnProperty.call(attrs, k)) { bits += ' ' + k + '="' + bareSummonEscapeHtml(attrs[k]) + '"' } } if (BARE_SUMMON_VOID[c.name]) { s += bits + '>' continue } s += bits + '>' + bareSummonSerializeInner(c) + '' } return s } function bareSummonParseFragment(html) { var doc = bareSummonParseHtml('' + String(html || '') + '', {}) return (doc.body && doc.body.children) || [] } function bareSummonScriptSource(node) { if (!node) return '' var ch = node.children || [] var s = '' var i for (i = 0; i < ch.length; i++) { if (ch[i].type === 'text') s += ch[i].text || '' } return s } /** 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 '' if (kind === 'bookmarks') { var items = (session.bookmarks || []) .map(function (b) { return ( '
  • ' + String(b.title || b.url || '') + '
  • ' ) }) .join('') return ( '

    Bookmarks

      ' + (items || '
    • (none)
    • ') + '
    ' ) } if (kind === 'history') { var t = bareSummonCurrent(session) var h = (t.history.back || []) .slice(-20) .map(function (u) { return '
  • ' + u + '
  • ' }) .join('') return ( '

    History

      ' + (h || '
    • (empty)
    • ') + '
    ' ) } if (kind === 'cookies') { var rows = session.jar .toJSON() .map(function (c) { return '
  • ' + c.name + ' @ ' + c.domain + c.path + '
  • ' }) .join('') return ( '

    Cookies

      ' + (rows || '
    • (none)
    • ') + '
    ' ) } if (kind === 'net') { return '

    Net

    HTTP via ctx.httpFetch. Allow/deny: BARE_OS_HTTP_ALLOWLIST / DENYLIST.

    ' } return ( '

    summon

    Text browser for Bare OS. Page JS runs in a Bare VM (ctx.bare.bareVm) when present.

    ' + '

    Type a URL (g) or open a bookmark. Try about:bookmarks. Toggle JS with J.

    ' ) } 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 || '(empty file)', 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 = '
    ' +
          body.replace(/&/g, '&').replace(/'
      }
      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
      }
    }