3417 lines
92 KiB
Plaintext
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
}
|
|
|
|
function bareSummonSerializeInner(node) {
|
|
if (!node) return ''
|
|
var s = ''
|
|
var ch = node.children || []
|
|
var i
|
|
for (i = 0; i < ch.length; i++) {
|
|
var c = ch[i]
|
|
if (c.type === 'text') {
|
|
s += bareSummonEscapeHtml(c.text)
|
|
continue
|
|
}
|
|
if (c.type !== 'element') continue
|
|
var attrs = c.attrs || {}
|
|
var bits = '<' + c.name
|
|
var k
|
|
for (k in attrs) {
|
|
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
|
|
bits += ' ' + k + '="' + bareSummonEscapeHtml(attrs[k]) + '"'
|
|
}
|
|
}
|
|
if (BARE_SUMMON_VOID[c.name]) {
|
|
s += bits + '>'
|
|
continue
|
|
}
|
|
s += bits + '>' + bareSummonSerializeInner(c) + '</' + c.name + '>'
|
|
}
|
|
return s
|
|
}
|
|
|
|
function bareSummonParseFragment(html) {
|
|
var doc = bareSummonParseHtml('<body>' + String(html || '') + '</body>', {})
|
|
return (doc.body && doc.body.children) || []
|
|
}
|
|
|
|
function bareSummonScriptSource(node) {
|
|
if (!node) return ''
|
|
var ch = node.children || []
|
|
var s = ''
|
|
var i
|
|
for (i = 0; i < ch.length; i++) {
|
|
if (ch[i].type === 'text') s += ch[i].text || ''
|
|
}
|
|
return s
|
|
}
|
|
|
|
/** 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, '&').replace(/</g, '<') +
|
|
'</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
|
|
}
|
|
}
|