-
Notifications
You must be signed in to change notification settings - Fork 2
/
line.js
165 lines (149 loc) · 6.41 KB
/
line.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import { open, post } from './stream.js'
export { lineup, plugins }
let origin = 'localhost'
const newpid = () => Math.floor(Math.random()*1000000)
const newpanel = (props) => ({pid:newpid(), stats:{}, ...props})
const purl = (site, slug) => site ? `http://${site}/${slug}.json` : `http://${origin}/${slug}.json`
let lineup = []
let plugins = {}
let t0 = Date.now()
line()
async function line() {
let next = open()
while (true) {
let event = await next()
switch (event.type) {
case 'reload':
origin = event.origin || origin
await reload(event.hash)
post({type:'reloaded'})
break
case 'reference':
await reference(event.site, event.slug, event.pid)
post({type:'referenced'})
break
case 'click':
await click(event.title, event.pid)
post({type:'clicked'})
break
}
}
}
function reload(hash) {
let fields = hash.replace(/(^[/#]+)|([/]+$)/g,'').split('/')
let flight = []
let start = Date.now()
for (let field of fields) {
let [slug,site] = field.split('@')
// console.log({origin,hash,slug,site})
site ||= origin
let panel = newpanel({site, slug, where:site})
lineup.push(panel)
flight.push(fetch(purl(site,slug)).then(res => res.json())
.then(json => {
panel.page = json
refresh(panel)
.then(() => {panel.stats.refresh = Date.now() - start})
}
))
}
return Promise.all(flight)
}
function refresh(panel) {
let flight = []
panel.dt = Date.now() - t0
panel.panes = []
for (let item of panel.page.story) {
let id = item.id
let type = item.type
let pane = {id, type, item, look:'blank', links:[]}
panel.panes.push(pane)
flight.push(render(pane,panel))
}
return Promise.all(flight)
}
let loading = {}
async function load(type) {
let plugin = plugins[type]
if (plugin) return plugin
let queue = loading[type]
if (queue) return new Promise(resolve => queue.push(resolve))
queue = loading[type] = []
post({type:'load', plugin:type})
let url = `../plugins/wiki-client-type-${type}.js`
plugins[type] = plugin = await import(url).catch(err=>({err, emit:(pane,item) => pane.look = `<p>HELP ${item.text}</p>`}))
post({type:'loaded', plugin:type, queued:queue?.length, err:plugin.err})
if (queue?.length) queue.map(resolve => resolve(plugin))
delete loading[type]
return plugin
}
async function render(pane,panel) {
let item = pane.item
switch (item.type) {
case 'paragraph':
let resolved = item.text
.replace(/\[\[(.+?)\]\]/g, internal)
.replace(/\[(.+?) (.+?)\]/g, external)
pane.dt = Date.now() - t0
return pane.look = `<p>${resolved}</p>`
default:
let handler = await load(item.type)
handler.emit(pane, item)
pane.dt = Date.now() - t0
}
function internal(link, title) {
pane.links.push(title)
return `<a href="#" data-pid=${panel.pid}>${title}</a>`
}
function external(link, url, words) {
return `<a href="${url}" target=_blank>${words} ${linkmark()}</a>`
}
}
async function click(title, pid) {
let start = Date.now()
let panel = await resolve(title, pid)
panel.stats.fetch = Date.now() - start
let hit = lineup.findIndex(panel => panel.pid == pid)
lineup.splice(hit+1,lineup.length, panel)
start = Date.now()
return refresh(panel).then(() => {panel.stats.refresh = Date.now() - start})
}
async function resolve(title, pid) {
const asSlug = (title) => title.replace(/\s/g, '-').replace(/[^A-Za-z0-9-]/g, '').toLowerCase()
const recent = (list, action) => {if (action.site && !list.includes(action.site)) list.push(action.site); return list}
let panel = lineup.find(panel => panel.pid == pid)
let path = (panel.page.journal||[]).reverse().reduce(recent,[origin, panel.where])
post({type:'progress', context: path })
let slug = asSlug(title)
let pages = await Promise.all(path.map(where => probe(where, slug)))
let hit = pages.findIndex(page => !page.err)
post({type:'progress', hit })
if (hit >= 0) {
let site = path[hit]
return newpanel({where:site, site, slug, page:pages[hit]})
} else {
let text = "We can't find this page in the expected locations."
let page = {title,story:[{type:'paragraph', text}], journal:[], err:'not found where expected'}
return newpanel({where:'ghost', slug, page})
}
}
async function reference(site, slug, pid) {
let start = Date.now()
let page = await probe(site, slug)
post({type:'progress',event:'reference', title:page.title, err:page.err})
let panel = newpanel({where:site, site, slug, page})
panel.stats.fetch = Date.now() - start
let hit = lineup.findIndex(panel => panel.pid == pid)
lineup.splice(hit+1,lineup.length, panel)
start = Date.now()
return refresh(panel).then(() => {panel.stats.refresh = Date.now() - start})
}
function probe(where, slug) {
let site = where == null ? origin : where
return fetch(`http://${site}/${slug}.json`)
.then(res => res.ok ? res.json() : ({title:'Error',story:[],journal:[],err:res.statusText||'unknown-1'}))
.catch(err => ({title:'Error',story:[],journal:[],err:err.message||'unknown-2'}))
}
function linkmark() {
return `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAC0WlDQ1BJQ0MgUHJvZmlsZQAAKJGNlM9LFGEYx7+zjRgoQWBme4ihQ0ioTBZlROWuv9i0bVl/lBLE7Oy7u5Ozs9PM7JoiEV46ZtE9Kg8e+gM8eOiUl8LALALpblFEgpeS7Xlnxt0R7ccLM/N5nx/f53nf4X2BGlkxTT0kAXnDsZJ9Uen66JhU+xEhHEEdwqhTVNuMJBIDoMFjsWtsvofAvyute/v/OurStpoHhP1A6Eea2Sqw7xfZC1lqBBC5XsOEYzrE9zhbnv0x55TH8659KNlFvEh8QDUtHv+auEPNKWmgRiRuyQZiUgHO60XV7+cgPfXMGB6k73Hq6S6ze3wWZtJKdz9xG/HnNOvu4ZrE8xmtN0bcTM9axuod9lg4oTmxIY9DI4YeH/C5yUjFr/qaoulEk9v6dmmwZ9t+S7mcIA4TJ8cL/TymkXI7p3JD1zwW9KlcV9znd1Yxyeseo5g5U3f/F/UWeoVR6GDQYNDbgIQk+hBFK0xYKCBDHo0iNLIyN8YitjG+Z6SORIAl8q9TzrqbcxtFyuZZI4jGMdNSUZDkD/JXeVV+Ks/JX2bDxeaqZ8a6qanLD76TLq+8ret7/Z48fZXqRsirI0vWfGVNdqDTQHcZYzZcVeI12P34ZmCVLFCpFSlXadytVHJ9Nr0jgWp/2j2KXZpebKrWWhUXbqzUL03v2KvCrlWxyqp2zqtxwXwmHhVPijGxQzwHSbwkdooXxW6anRcHKhnDpKJhwlWyoVCWgUnymjv+mRcL76y5o6GPGczSVImf/4RVyGg6CxzRf7j/c/B7xaOxIvDCBg6frto2ku4dIjQuV23OFeDCN7oP3lZtzXQeDj0BFs6oRavkSwvCG4pmdxw+6SqYk5aWzTlSuyyflSJ0JTEpZqhtLZKi65LrsiWL2cwqsXQb7Mypdk+lnnal5lO5vEHnr/YRsPWwXP75rFzeek49rAEv9d/AvP1FThgxSQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAKtJREFUGJVtkLERwjAMRZ+5UHmmNNpCrpMloMi5gCXcO1MkLWwBS6SCO1EQgkP4d2q+nr50cmZGqbZt18YsV4IxRqv2FcfD8XeYXWl0Xefutzsxxk1iFUJYrfLeU9f1BtwB5JzJOeO9R1UREcZxXCVX5R0l1Pc9AKfz6ZsIoKpcrpcFmqaJlJJ7Pp6klByqah8Nw2BN05iZ2ezzqWU1gIggIv/e+AZDCH+bpV442lpGxygDswAAAABJRU5ErkJggg==" alt="" />`
}