420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
/** 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 || {})
|
|
}
|