wiki-plugin-mech 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/mech.js CHANGED
@@ -1,1290 +1,24 @@
1
-
2
- (function() {
3
- "use strict"
4
- const uniq = (value, index, self) => self.indexOf(value) === index
5
- const delay = time => new Promise(res => setTimeout(res,time))
6
- const asSlug = (title) => title.replace(/\s/g, '-').replace(/[^A-Za-z0-9-]/g, '').toLowerCase()
7
-
8
- function expand(text) {
9
- return text
10
- .replace(/&/g, '&')
11
- .replace(/</g, '&lt;')
12
- .replace(/>/g, '&gt;')
13
- }
14
-
15
-
16
- // I N T E R P R E T E R
17
-
18
- // https://github.com/dobbs/wiki-plugin-graphviz/blob/main/client/graphviz.js#L86-L103
19
- function tree(lines, here, indent) {
20
- while (lines.length) {
21
- let m = lines[0].match(/( *)(.*)/)
22
- let spaces = m[1].length
23
- let command = m[2]
24
- if (spaces == indent) {
25
- here.push({command})
26
- lines.shift()
27
- } else if (spaces > indent) {
28
- var more = []
29
- here.push(more)
30
- tree(lines, more, spaces)
31
- } else {
32
- return here
33
- }
34
- }
35
- return here
36
- }
37
-
38
- function format(nest) {
39
- const unique = Math.floor(Math.random()*1000000)
40
- const block = (more,path) => {
41
- const html = []
42
- for (const part of more) {
43
- const key = `${unique}.${path.join('.')}`
44
- part.key = key
45
- if('command' in part)
46
- html.push(`<font color=gray size=small></font><span style="display: block;" id=${key}>${expand(part.command)}</span>`)
47
- else
48
- html.push(`<div id=${key} style="padding-left:15px">${block(part,[...path,0])}</div>`)
49
- path[path.length-1]++
50
- }
51
- return html.join("\n")
52
- }
53
- return block(nest,[0])
54
- }
55
-
56
- function trouble(elem,message) {
57
- if(elem.innerText.match(/✖︎/)) return
58
- elem.innerHTML += `<button style="border-width:0;color:red;">✖︎</button>`
59
- elem.querySelector('button').addEventListener('click',event => {
60
- elem.outerHTML += `<span style="width:80%;color:gray;">${message}</span>` })
61
- }
62
-
63
- function inspect(elem,key,state) {
64
- const tap = elem.previousElementSibling
65
- if(state.debug) {
66
- const value = state[key]
67
- tap.innerHTML = `${key} ⇒ `
68
- tap.addEventListener('click',event => {
69
- console.log({key,value})
70
- let look = tap.previousElementSibling
71
- if (!(look?.classList.contains('look'))) {
72
- const div = document.createElement('div')
73
- div.classList.add('look')
74
- tap.insertAdjacentElement('beforebegin',div)
75
- look = tap.previousElementSibling
76
- }
77
- let text = JSON.stringify(value,null,1)
78
- if(text.length>300) text = text.substring(0,400)+'...'
79
- const css = `border:1px solid black; background-color:#f8f8f8; padding:8px; color:gray; word-break: break-all;`
80
- look.innerHTML = `<div style="${css}">${text}</div>`
81
- })
82
- }
83
- else {
84
- tap.innerHTML = ''
85
- }
86
- }
87
-
88
- async function run (nest,state={},mock) {
89
- const scope = nest.slice()
90
- while (scope.length) {
91
- const code = scope.shift()
92
- if ('command' in code) {
93
- const command = code.command
94
- const elem = mock || document.getElementById(code.key)
95
- const [op, ...args] = code.command.split(/ +/)
96
- const next = scope[0]
97
- const body = next && ('command' in next) ? null : scope.shift()
98
- const stuff = {command,op,args,body,elem,state}
99
- if(state.debug) console.log(stuff)
100
- if (blocks[op])
101
- await blocks[op].emit.apply(null,[stuff])
102
- else
103
- if (op.match(/^[A-Z]+$/))
104
- trouble(elem,`${op} doesn't name a block we know.`)
105
- else if (code.command.match(/\S/))
106
- trouble(elem, `Expected line to begin with all-caps keyword.`)
107
- } else if(typeof code == 'array') {
108
- console.warn(`this can't happen.`)
109
- run(code,state) // when does this even happen?
110
- }
111
- }
112
- }
113
-
114
-
115
- // B L O C K S
116
-
117
- function click_emit ({elem,body,state}) {
118
- if(elem.innerHTML.match(/button/)) return
119
- if (!body?.length) return trouble(elem,`CLICK expects indented blocks to follow.`)
120
- elem.innerHTML += '<button style="border-width:0;">▶</button>'
121
- elem.querySelector('button').addEventListener('click',event => {
122
- state.debug = event.shiftKey
123
- run(body,state)
124
- })
125
- }
126
-
127
- function hello_emit ({elem,args,state}) {
128
- const world = args[0] == 'world' ? ' 🌎' : ' 😀'
129
- for (const key of Object.keys(state))
130
- inspect(elem,key,state)
131
- elem.innerHTML += world
132
- }
133
-
134
- function from_emit ({elem,args,body,state}) {
135
- const line = elem.innerHTML
136
- const url = args[0]
137
- elem.innerHTML = line + ' ⏳'
138
- fetch(`//${url}.json`)
139
- .then(res => res.json())
140
- .then(page => {
141
- state.page = page
142
- elem.innerHTML = line + ' ⌛'
143
- run(body,state)
144
- })
145
- }
146
-
147
- function sensor_emit ({elem,args,body,state}) {
148
- const line = elem.innerHTML.replaceAll(/ ⌛/g,'')
149
- if(!('page' in state)) return trouble(elem,`Expect "page" as with FROM.`)
150
- inspect(elem,'page',state)
151
- const datalog = state.page.story.find(item => item.type == 'datalog')
152
- if(!datalog) return trouble(elem, `Expect Datalog plugin in the page.`)
153
- const device = args[0]
154
- if(!device) return trouble(elem, `SENSOR needs a sensor name.`)
155
- const sensor = datalog.text.split(/\n/)
156
- .map(line => line.split(/ +/))
157
- .filter(fields => fields[0] == 'SENSOR')
158
- .find(fields => fields[1] == device)
159
- if(!sensor) return trouble(elem, `Expect to find "${device}" in Datalog.`)
160
- const url = sensor[2]
161
-
162
- const f = c => 9/5*(c/16)+32
163
- const avg = a => a.reduce((s,e)=>s+e,0)/a.length
164
- elem.innerHTML = line + ' ⏳'
165
- fetch(url)
166
- .then (res => res.json())
167
- .then (data => {
168
- if(state.debug) console.log({sensor,data})
169
- elem.innerHTML = line + ' ⌛'
170
- const value = f(avg(Object.values(data)))
171
- state.temperature = `${value.toFixed(2)}°F`
172
- run(body,state)
173
- })
174
- }
175
-
176
- function report_emit ({elem,command,state}) {
177
- const value = state?.temperature
178
- if (!value) return trouble(elem,`Expect data, as from SENSOR.`)
179
- inspect(elem,'temperature',state)
180
- elem.innerHTML = command + `<br><font face=Arial size=32>${value}</font>`
181
- }
182
-
183
- function source_emit ({elem,command,args,body,state}) {
184
- if (!(args && args.length)) return trouble(elem,`Expected Source topic, like "markers" for Map markers.`)
185
- const topic = args[0]
186
- const item = elem.closest('.item')
187
- const sources = requestSourceData(item, topic)
188
- if(!sources.length) return trouble(elem,`Expected source for "${topic}" in the lineup.`)
189
- const count = type => {
190
- const count = sources
191
- .filter(source => [...source.div.classList].includes(type))
192
- .length
193
- return count ? `${count} ${type}` : null}
194
- const counts = [count('map'),count('image'),count('frame'),count('assets')]
195
- .filter(count => count)
196
- .join(", ")
197
- if (state.debug) console.log({topic,sources})
198
- elem.innerHTML = command + ' ⇒ ' + counts
199
- // state.assets = ?
200
- // state.aspect = ?
201
- // state.region = ?
202
- // state.marker = ?
203
- state[topic] = sources.map(({div,result}) => ({id:div.dataset.id, result}))
204
- if (body) run(body,state)
205
- }
206
-
207
- function preview_emit ({elem,command,args,state}) {
208
- const round = digits => (+digits).toFixed(7)
209
- const story = []
210
- const types = args
211
- for (const type of types) {
212
- switch (type) {
213
- case 'map':
214
- if(!('marker' in state)) return trouble(elem,`"map" preview expects "marker" state, like from "SOURCE marker".`)
215
- inspect(elem,'marker',state)
216
- const text = state.marker
217
- .map(marker => [marker.result])
218
- .flat(2)
219
- .map(latlon => `${round(latlon.lat)}, ${round(latlon.lon)} ${latlon.label||''}`)
220
- .filter(uniq)
221
- .join("\n")
222
- story.push({type:'map',text})
223
- break
224
- case 'graph':
225
- if(!('aspect' in state)) return trouble(elem,`"graph" preview expects "aspect" state, like from "SOURCE aspect".`)
226
- inspect(elem,'aspect',state)
227
- for (const {div,result} of state.aspect) {
228
- for (const {name,graph} of result) {
229
- if(state.debug) console.log({div,result,name,graph})
230
- story.push({type:'paragraph',text:name})
231
- story.push({type:'graphviz',text:dotify(graph)})
232
- }
233
- story.push({type:'pagefold',text:'.'})
234
- }
235
- break
236
- case 'items':
237
- if(!('items' in state)) return trouble(elem,`"graph" preview expects "items" state, like from "KWIC".`)
238
- inspect(elem,'items',state)
239
- story.push(...state.items)
240
- break
241
- case 'page':
242
- if(!('page' in state)) return trouble(elem,`"page" preview expects "page" state, like from "FROM".`)
243
- inspect(elem,'page',state)
244
- story.push(...state.page.story)
245
- break
246
- case 'synopsis':
247
- {const text = `This page created with Mech command: "${command}". See [[${state.context.title}]].`
248
- story.push({type:'paragraph',text,id:state.context.itemId})}
249
- break
250
- default:
251
- return trouble(elem,`"${type}" doesn't name an item we can preview`)
252
- }
253
- }
254
- const title = "Mech Preview" + (state.tick ? ` ${state.tick}` : '')
255
- const page = {title,story}
256
- for (const item of page.story) item.id ||= (Math.random()*10**20).toFixed(0)
257
- const item = JSON.parse(JSON.stringify(page))
258
- const date = Date.now()
259
- page.journal = [{type:'create', date, item}]
260
- const options = {$page:$(elem.closest('.page'))}
261
- wiki.showResult(wiki.newPage(page), options)
262
- }
263
-
264
- async function neighbors_emit ({elem,command,args,body,state}) {
265
- const belem = probe => document.getElementById(probe.key)
266
- const want = args[0]
267
- if(state.debug) console.log({neighborhoodObject:wiki.neighborhoodObject})
268
- const have = Object.entries(wiki.neighborhoodObject.sites)
269
- .filter(([domain,site]) => !site.sitemapRequestInflight && (!want || domain.includes(want)))
270
- .map(([domain,site]) => (site.sitemap||[])
271
- .map(info => Object.assign({domain},info)))
272
- for (const probe of (body||[])) {
273
- if(!probe.command.endsWith(' Survey')) {
274
- trouble(belem(probe),`NEIGHBORS expects a Site Survey title, like Pattern Link Survey`)
275
- continue
276
- }
277
- const todos = have.filter(sitemap =>
278
- sitemap.find(info => info.title == probe.command))
279
- belem(probe).innerHTML = `${probe.command} ⇒ ${todos.length} sites`
280
- for (const todo of todos) {
281
- const url = `//${todo[0].domain}/${asSlug(probe.command)}.json`
282
- const page = await fetch(url).then(res => res.json())
283
- const survey = page.story.find(item => item.type == 'frame')?.survey
284
- for (const info of todo) {
285
- const extra = Object.assign({},survey.find(inf => inf.slug == info.slug),info)
286
- Object.assign(info,extra)
287
- }
288
- console.log({url,page,survey,todo})
289
- }
290
- }
291
- state.neighborhood = have.flat()
292
- .sort((a,b) => b.date - a.date)
293
- elem.innerHTML = command + ` ⇒ ${state.neighborhood.length} pages, ${have.length} sites`
294
- }
295
-
296
- function walk_emit ({elem,command,args,state}) {
297
- if(!('neighborhood' in state)) return trouble(elem,`WALK expects state.neighborhood, like from NEIGHBORS.`)
298
- inspect(elem,'neighborhood',state)
299
- const [,count,way] = command.match(/\b(\d+)? *(steps|days|weeks|months|hubs|lineup|references)\b/) || []
300
- if(!way && command != 'WALK') return trouble(elem, `WALK can't understand rest of this block.`)
301
- const scope = {
302
- lineup(){
303
- const items = [...document.querySelectorAll('.page')]
304
- const index = items.indexOf(elem.closest('.page'))
305
- return items.slice(0,index)
306
- },
307
- references(){
308
- const div = elem.closest('.page')
309
- const pageObject = wiki.lineup.atKey(div.dataset.key)
310
- const story = pageObject.getRawPage().story
311
- console.log({div,pageObject,story})
312
- return story.filter(item => item.type == 'reference')
313
- }
314
- }
315
- const steps = walks(count,way,state.neighborhood,scope)
316
- const aspects = steps.filter(({graph})=>graph)
317
- if(state.debug) console.log({steps})
318
- elem.innerHTML = command
319
- const nodes = aspects.map(({graph}) => graph.nodes).flat()
320
- elem.innerHTML += ` ⇒ ${aspects.length} aspects, ${nodes.length} nodes`
321
- if(steps.find(({graph}) => !graph)) trouble(elem,`WALK skipped sites with no links in sitemaps`)
322
- const item = elem.closest('.item')
323
- if (aspects.length) {
324
- state.aspect = state.aspect || []
325
- const obj = state.aspect.find(obj => obj.id == elem.id)
326
- if(obj) obj.result = aspects
327
- else state.aspect.push({id:elem.id, result:aspects, source:command})
328
- item.classList.add('aspect-source')
329
- item.aspectData = () => state.aspect.map(obj => obj.result).flat()
330
- if(state.debug) console.log({command,state:state.aspect,item:item.aspectData()})
331
-
332
- }
333
- }
334
-
335
- function tick_emit ({elem,command,args,body,state}) {
336
- if(elem.innerHTML.match(/button/)) return
337
- if (!body?.length) return trouble(elem,`TICK expects indented blocks to follow.`)
338
- const count = args[0] || '1'
339
- if (!count.match(/^[1-9][0-9]?$/)) return trouble(elem,`TICK expects a count from 1 to 99`)
340
- let clock = null
341
- ready()
342
- function ready () {
343
- elem.innerHTML = command+'<button style="border-width:0;">▶</button>'
344
- elem.querySelector('button').addEventListener('click',start)
345
- }
346
- function start (event) {
347
- state.debug = event.shiftKey
348
- const status = ticks => {elem.innerHTML = command + ` ⇒ ${ticks} remaining`}
349
- if(clock){
350
- clock = clearInterval(clock)
351
- delete state.tick
352
- } else {
353
- let working
354
- state.tick = +count
355
- status(state.tick)
356
- working = true; run(body,state).then(() => working = false)
357
- clock = setInterval(()=>{
358
- if(working) return
359
- if(state.debug) console.log({tick:state.tick})
360
- if(('tick' in state) && --state.tick > 0) {
361
- status(state.tick)
362
- working = true; run(body,state).then(() => working = false)
363
- }
364
- else {
365
- clock = clearInterval(clock)
366
- ready()
367
- }
368
- },1000)
369
- }
370
- }
371
- }
372
-
373
- function until_emit ({elem,command,args,body,state}) {
374
- if(!args.length) return trouble(elem,`UNTIL expects an argument, a word to stop running.`)
375
- if(!state.tick) return trouble(elem,`UNTIL expects to indented below an iterator, like TICKS.`)
376
- if(!state.aspect) return trouble(elem,`UNTIL expects "aspect", like from WALK.`)
377
- inspect(elem,'aspect',state)
378
- elem.innerHTML = command + ` ⇒ ${state.tick}`
379
- const word = args[0]
380
- for(const {div,result} of state.aspect)
381
- for(const {name,graph} of result)
382
- for(const node of graph.nodes)
383
- if(node.type.includes(word) || node.props.name.includes(word)) {
384
- if(state.debug) console.log({div,result,name,graph,node})
385
- delete state.tick
386
- elem.innerHTML += ' done'
387
- if (body) run(body,state)
388
- return
389
- }
390
- }
391
-
392
- function forward_emit ({elem,command,args,state}) {
393
- if(args.length < 1) return trouble(elem,`FORWARD expects an argument, the number of steps to move a "turtle".`)
394
- if(!('turtle' in state)) state.turtle = new Turtle(elem)
395
- const steps = args[0]
396
- const position = state.turtle.forward(+steps)
397
- elem.innerHTML = command + ` ⇒ ${position.map(n => (n-200).toFixed(1)).join(', ')}`
398
- }
399
-
400
- function turn_emit ({elem,command,args,state}) {
401
- if(args.length < 1) return trouble(elem,`TURN expects an argument, the number of degrees to turn a "turtle".`)
402
- if(!('turtle' in state)) state.turtle = new Turtle(elem)
403
- const degrees = args[0]
404
- const direction = state.turtle.turn(+degrees)
405
- elem.innerHTML = command + ` ⇒ ${direction}°`
406
- }
407
-
408
- function file_emit ({elem,command,args,body,state}) {
409
- if(!('assets' in state)) return trouble(elem,`FILE expects state.assets, like from SOURCE assets.`)
410
- inspect(elem,'assets',state)
411
-
412
- // [ { "id": "b2d5831168b4706b", "result":
413
- // { "pages/testing-file-mech":
414
- // { "//ward.dojo.fed.wiki/assets":
415
- // [ "KWIC-list+axe-files.txt", "KWIC-list-axe-files.tsv" ] } } } ]
416
-
417
- const origin = '//'+window.location.host
418
- const assets = state.assets.map(({id,result}) =>
419
- Object.entries(result).map(([dir,paths]) =>
420
- Object.entries(paths).map(([path,files]) =>
421
- files.map(file => {
422
- const assets = path.startsWith("//") ? path : `${origin}${path}`
423
- const host = assets.replace(/\/assets$/,'')
424
- const url = `${assets}/${dir}/${file}`
425
- return {id,dir,path,host,file,url}
426
- })))).flat(3)
427
- if(state.debug) console.log({assets})
428
-
429
- if(args.length < 1) return trouble(elem,`FILE expects an argument, the dot suffix for desired files.`)
430
- if (!body?.length) return trouble(elem,'FILE expects indented blocks to follow.')
431
- const suffix = args[0]
432
- const choices = assets.filter(asset => asset.file.endsWith(suffix))
433
- const flag = choice => `<img width=12 src=${choices[choice].host+'/favicon.png'}>`
434
- if(!choices) return trouble(elem,`FILE expects to find an asset with "${suffix}" suffix.`)
435
- elem.innerHTML = command +
436
- `<br><div class=choices style="border:1px solid black; background-color:#f8f8f8; padding:8px;" >${choices
437
- .map((choice,i) =>
438
- `<span data-choice=${i} style="cursor:pointer;">
439
- ${flag(i)}
440
- ${choice.file} ▶
441
- </span>`)
442
- .join("<br>\n")
443
- }</div>`
444
- elem.querySelector('.choices').addEventListener('click',event => {
445
- if (!('choice' in event.target.dataset)) return
446
- const url = choices[event.target.dataset.choice].url
447
- // console.log(event.target)
448
- // console.log(event.target.dataset.file)
449
- // const url = 'http://ward.dojo.fed.wiki/assets/pages/testing-file-mech/KWIC-list-axe-files.tsv'
450
- fetch(url)
451
- .then(res => res.text())
452
- .then(text => {
453
- elem.innerHTML = command + ` ⇒ ${text.length} bytes`
454
- state.tsv = text
455
- console.log({text})
456
- run(body,state)
457
- })
458
- })
459
- }
460
-
461
- function kwic_emit ({elem,command,args,body,state}) {
462
- const template = body && body[0]?.command
463
- if(template && !template.match(/\$[KW]/)) return trouble(elem,`KWIK expects $K or $W in link prototype.`)
464
- if(!('tsv' in state)) return trouble(elem,`KWIC expects a .tsv file, like from ASSETS .tsv.`)
465
- inspect(elem,'tsv',state)
466
- const prefix = args[0] || 1
467
- const lines = state.tsv.trim().split(/\n/)
468
-
469
- const stop = new Set(['of','and','in','at'])
470
- const page = $(elem.closest('.page')).data('data')
471
- const start = page.story.findIndex(item => item.type=='pagefold' && item.text=='stop')
472
- if(start >= 0) {
473
- const finish = page.story.findIndex((item,i) => i>start && item.type=='pagefold')
474
- page.story.slice(start+1,finish)
475
- .map(item => item.text.trim().split(/\s+/))
476
- .flat()
477
- .forEach(word => stop.add(word))
478
- }
479
-
480
- const groups = kwic(prefix,lines,stop)
481
- elem.innerHTML = command + ` ⇒ ${lines.length} lines, ${groups.length} groups`
482
- const link = quote => {
483
- let line = quote.line
484
- if(template) {
485
- const substitute = template
486
- .replaceAll(/\$K\+/g,quote.key.replaceAll(/ /g,'+'))
487
- .replaceAll(/\$K/g,quote.key)
488
- .replaceAll(/\$W/g,quote.word)
489
- const target = template.match(/\$W/) ? quote.word : quote.key
490
- line = line.replace(target,substitute)
491
- }
492
- return line
493
- }
494
-
495
- state.items = groups.map(group => {
496
- text = `# ${group.group}\n\n${group.quotes
497
- .map(quote=>link(quote))
498
- .join("\n")}`
499
- return {type:'markdown',text}})
500
- }
501
-
502
- function show_emit({elem,command,args,state}) {
503
- elem.innerHTML = command
504
- let site,slug
505
- if(args.length < 1) {
506
- if(state.info) {
507
- inspect(elem,'info',state)
508
- site = state.info.domain
509
- slug = state.info.slug
510
- elem.innerHTML = command + ` ⇒ ${state.info.title}`
511
- } else {
512
- return trouble(elem,`SHOW expects a slug or site/slug to open in the lineup.`)
513
- }
514
- } else {
515
- const info = args[0];
516
- [site,slug] = info.includes('/')
517
- ? info.split(/\//)
518
- : [null,info]
519
- }
520
- const lineup = [...document.querySelectorAll('.page')].map(e => e.id)
521
- if(lineup.includes(slug)) return trouble(elem,`SHOW expects a page not already in the lineup.`)
522
- const page = elem.closest('.page')
523
- wiki.doInternalLink(slug,page,site)
524
- }
525
-
526
- function random_emit({elem,command,state}) {
527
- if(!state.neighborhood) return trouble(elem,`RANDOM expected a neighborhood, like from NEIGHBORS.`)
528
- inspect(elem,'neighborhood',state)
529
- const infos = state.neighborhood
530
- const many = infos.length
531
- const one = Math.floor(Math.random()*many)
532
- elem.innerHTML = command + ` ⇒ ${one} of ${many}`
533
- state.info = infos[one]
534
- }
535
-
536
- function sleep_emit({elem,command,args,body,state}) {
537
- let count = args[0] || '1'
538
- if (!count.match(/^[1-9][0-9]?$/)) return trouble(elem,`SLEEP expects seconds from 1 to 99`)
539
- return new Promise(resolve => {
540
- if(body)
541
- run(body,state)
542
- .then(result => {if(state.debug) console.log(command,'children', result)})
543
- elem.innerHTML = command + ` ⇒ ${count} remain`
544
- let clock = setInterval(()=> {
545
- if(--count > 0)
546
- elem.innerHTML = command + ` ⇒ ${count} remain`
547
- else {
548
- clearInterval(clock)
549
- elem.innerHTML = command + ` ⇒ done`
550
- if(state.debug) console.log(command, 'done')
551
- resolve()
552
- }
553
- }, 1000)
554
- })
555
- }
556
-
557
- function together_emit({elem,command,args,body,state}) {
558
- if (!body) return trouble(elem,`TOGETHER expects indented commands to run together.`)
559
- const children = body
560
- .map(child => run([child],state))
561
- return Promise.all(children)
562
- }
563
-
564
- // http://localhost:3000/plugin/mech/run/testing-mechs-synchronization/5e269010fc81aebe?args=WyJoZWxsbyIsIndvcmxkIl0
565
- async function get_emit({elem,command,args,body,state}) {
566
- if (!body) return trouble(elem,`GET expects indented commands to run on the server.`)
567
- let share = {}
568
- let where = state.context.site
569
- if (args.length) {
570
- for(const arg of args) {
571
- if (arg in state) {
572
- inspect(elem,arg,state)
573
- share[arg] = state[arg]}
574
- else if (arg.match(/\./)) where=arg
575
- else {return trouble(elem,`GET expected "${arg}" to name state or site.`)}
576
- }
577
- }
578
- // const site = state.context.site
579
- const slug = state.context.slug
580
- const itemId = state.context.itemId
581
- const query = `mech=${btoa(JSON.stringify(body))}&state=${btoa(JSON.stringify(share))}`
582
- const url = `//${where}/plugin/mech/run/${slug}/${itemId}?${query}`
583
- elem.innerHTML = command + ` ⇒ in progress`
584
- const start = Date.now()
585
- let result
586
- try {
587
- result = await fetch(url).then(res => res.ok ? res.json() : res.status)
588
- if('err' in result) return trouble(elem,`RUN received error "${result.err}"`)
589
- } catch(err) {
590
- return trouble(elem,`RUN failed with "${err.message}"`)
591
- }
592
- state.result = result
593
- for(const arg of result.mech.flat(9)){
594
- const elem = document.getElementById(arg.key)
595
- if('status' in arg) elem.innerHTML = arg.command + ` ⇒ ${arg.status}`
596
- if('trouble' in arg) trouble(elem,arg.trouble)
597
- }
598
- if('debug' in result.state) delete result.state.debug
599
- Object.assign(state,result.state)
600
- const elapsed = ((Date.now() - start)/1000).toFixed(3)
601
- elem.innerHTML = command + ` ⇒ ${elapsed} seconds`
602
- }
603
-
604
- function delta_emit({elem,command,args,body,state}) {
605
- const copy = obj => JSON.parse(JSON.stringify(obj))
606
- const size = obj => JSON.stringify(obj).length
607
- if (args.length < 1) return trouble(elem,`DELTA expects argument, "have" or "apply" on client.`)
608
- if (body) return trouble(elem,`DELTA doesn't expect indented input.`)
609
- switch (args[0]) {
610
- case 'have':
611
- const edits = state.context.page.journal
612
- .filter(item => item.type != 'fork')
613
- state.recent = edits[edits.length-1].date
614
- elem.innerHTML = command + ` ⇒ ${new Date(state.recent).toLocaleString()}`
615
- break
616
- case 'apply':
617
- if(!('actions' in state)) return trouble(elem,`DELTA apply expect "actions" as input.`)
618
- inspect(elem,'actions',state)
619
- const page = copy(state.context.page)
620
- const before = size(page)
621
- for (const action of state.actions)
622
- apply(page,action)
623
- state.page = page
624
- const after = size(page)
625
- elem.innerHTML = command + ` ⇒ ∆ ${((after-before)/before*100).toFixed(1)}%`
626
- break
627
- default:
628
- trouble(elem,`DELTA doesn't know "${args[0]}".`)
629
- }
630
- }
631
-
632
- function roster_emit({elem,command,state}) {
633
- if(!state.neighborhood) return trouble(elem,`ROSTER expected a neighborhood, like from NEIGHBORS.`)
634
- inspect(elem,'neighborhood',state)
635
- const infos = state.neighborhood
636
- const sites = infos
637
- .map(info => info.domain)
638
- .filter(uniq)
639
- const any = array => array[Math.floor(Math.random()*array.length)]
640
- if(state.debug) console.log(infos)
641
- const items = [
642
- {type:'roster', text:"Mech\n"+sites.join("\n")},
643
- {type:'activity', text:`ROSTER Mech\nSINCE 30 days`}]
644
- elem.innerHTML = command + ` ⇒ ${sites.length} sites`
645
- state.items = items
646
- }
647
-
648
- function lineup_emit({elem,command,state}) {
649
- const items = [...document.querySelectorAll('.page')]
650
- .map(div => {
651
- const $page = $(div)
652
- const page = $page.data('data')
653
- const site = $page.data('site') || location.host
654
- const slug = $page.attr('id').split('_')[0]
655
- const title = page.title || 'Empty'
656
- const text = page.story[0]?.text||'empty'
657
- return {type:'reference',site,slug,title,text}
658
- })
659
- elem.innerHTML = command + ` ⇒ ${items.length} pages`
660
- state.items = items
661
- }
662
-
663
- function listen_emit({elem,command,args,state}) {
664
- if (args.length < 1) return trouble(elem,`LISTEN expects argument, an action.`)
665
- const topic = args[0]
666
- let recent = Date.now()
667
- let count = 0
668
- const handler=listen
669
- handler.action = 'publishSourceData'
670
- handler.id = elem.id
671
- window.addEventListener("message", listen)
672
- $(".main").on('thumb', (evt, thumb) => console.log('jquery',{evt, thumb}))
673
- elem.innerHTML = command + ` ⇒ ready`
674
-
675
- // window.listeners = (action=null) => {
676
- // return getEventListeners(window).message
677
- // .map(t => t.listener)
678
- // .filter(f => f.name == 'listen')
679
- // .map(f => ({action:f.action,elem:document.getElementById(f.id),count:f.count}))
680
- // }
681
-
682
-
683
-
684
- function listen(event) {
685
- console.log({event})
686
- const {data} = event
687
- if (data.action == 'publishSourceData' && (data.name == topic || data.topic == topic)) {
688
- count++
689
- handler.count = count
690
- if(state.debug) console.log({count,data})
691
- if(count<=100){
692
- const now = Date.now()
693
- const elapsed = now-recent
694
- recent = now
695
- elem.innerHTML = command + ` ⇒ ${count} events, ${elapsed} ms`
696
- } else {
697
- window.removeEventListener("message", listen)
698
- }
699
- }
700
- }
701
- }
702
-
703
- function message_emit({elem,command,args,state}) {
704
- if (args.length < 1) return trouble(elem,`MESSAGE expects argument, an action.`)
705
- const topic = args[0]
706
- const message = {
707
- action: "publishSourceData",
708
- topic,
709
- name: topic,}
710
- window.postMessage(message,"*")
711
- elem.innerHTML = command + ` ⇒ sent`
712
- }
713
-
714
- async function solo_emit({elem,command,state}) {
715
- if(!('aspect' in state)) return trouble(elem,`"SOLO" expects "aspect" state, like from "WALK".`)
716
- inspect(elem,'aspect',state)
717
- elem.innerHTML = command
718
- const todo = state.aspect.map(each => ({
719
- source:each.source || each.id,
720
- aspects:each.result
721
- }))
722
- const aspects = todo.reduce((sum,each) => sum+each.aspects.length, 0)
723
- elem.innerHTML += ` ⇒ ${todo.length} sources, ${aspects} aspects`
724
-
725
- // from Solo plugin, client/solo.js
726
- const pageKey = elem.closest('.page').dataset.key
727
- const doing = {type:'batch', sources:todo, pageKey}
728
- console.log({pageKey,doing})
729
-
730
- if (typeof window.soloListener == "undefined" || window.soloListener == null) {
731
- console.log('**** Adding solo listener')
732
- window.soloListener = soloListener
733
- window.addEventListener("message", soloListener)
734
- }
735
-
736
- await delay(750)
737
- const popup = window.open('/plugins/solo/dialog/#','solo','popup,height=720,width=1280')
738
- if (popup.location.pathname != '/plugins/solo/dialog/'){
739
- console.log('launching new dialog')
740
- popup.addEventListener('load', event => {
741
- console.log('launched and loaded')
742
- popup.postMessage(doing, window.origin)
743
- })
744
- }
745
- else {
746
- console.log('reusing existing dialog')
747
- popup.postMessage(doing, window.origin)
748
- }
749
- }
750
-
751
-
752
- // C A T A L O G
753
-
754
- const blocks = {
755
- CLICK: {emit:click_emit},
756
- HELLO: {emit:hello_emit},
757
- FROM: {emit:from_emit},
758
- SENSOR: {emit:sensor_emit},
759
- REPORT: {emit:report_emit},
760
- SOURCE: {emit:source_emit},
761
- PREVIEW: {emit:preview_emit},
762
- NEIGHBORS:{emit:neighbors_emit},
763
- WALK: {emit:walk_emit},
764
- TICK: {emit:tick_emit},
765
- UNTIL: {emit:until_emit},
766
- FORWARD: {emit:forward_emit},
767
- TURN: {emit:turn_emit},
768
- FILE: {emit:file_emit},
769
- KWIC: {emit:kwic_emit},
770
- SHOW: {emit:show_emit},
771
- RANDOM: {emit:random_emit},
772
- SLEEP: {emit:sleep_emit},
773
- TOGETHER:{emit:together_emit},
774
- GET: {emit:get_emit},
775
- DELTA: {emit:delta_emit},
776
- ROSTER: {emit:roster_emit},
777
- LINEUP: {emit:lineup_emit},
778
- LISTEN: {emit:listen_emit},
779
- MESSAGE: {emit:message_emit},
780
- SOLO: {emit:solo_emit}
781
- }
782
-
783
-
784
- // P L U G I N
785
-
786
- function emit($item, item) {
787
- const lines = item.text.split(/\n/)
788
- const nest = tree(lines,[],0)
789
- const html = format(nest)
790
- const $page = $item.parents('.page')
791
- const pageKey = $page.data("key")
792
- const context = {
793
- item,
794
- itemId: item.id,
795
- pageKey,
796
- page: wiki.lineup.atKey(pageKey).getRawPage(),
797
- origin: window.origin,
798
- site: $page.data("site") || window.location.host,
799
- slug: $page.attr("id"),
800
- title: $page.data("data").title,
801
- }
802
- const state = {context}
803
- $item.append(`<div style="background-color:#eee;padding:15px;border-top:8px;">${html}</div>`)
804
- run(nest,state)
805
- }
806
-
807
- function bind($item, item) {
808
- return $item.dblclick(() => {
809
- return wiki.textEditor($item, item);
810
- })
811
- }
812
-
813
- if (typeof window !== "undefined" && window !== null) {
814
- window.plugins.mech = {emit, bind}
815
- }
816
-
817
- if (typeof module !== "undefined" && module !== null) {
818
- module.exports = {expand,tree,format,run}
819
- }
820
-
821
-
822
- // L I B R A R Y
823
-
824
- // adapted from wiki-plugin-frame/client/frame.js
825
- function requestSourceData(item, topic) {
826
- let sources = []
827
- for (let div of document.querySelectorAll(`.item`)) {
828
- if (div.classList.contains(`${topic}-source`)) {
829
- sources.unshift(div)
830
- }
831
- if (div === item) {
832
- break
833
- }
834
- }
835
-
836
- return sources.map(div => {
837
- let getData = div[`${topic}Data`]
838
- let result = getData ? getData() : null
839
- return {div,result}
840
- })
841
- }
842
-
843
- // adapted from super-collaborator/dotify.js
844
- function dotify(graph) {
845
- const tip = props => Object.entries(props).filter(e => e[1]).map(e => `${e[0]}: ${e[1]}`).join("\\n")
846
- const nodes = graph.nodes.map((node,id) => {
847
- const label = node.type ? `${node.type}\\n${node.props.name}` : node.props.name
848
- return `${id} [label="${label}" ${(node.props.url||node.props.tick)?`URL="${node.props.url||'#'}" target="_blank"`:''} tooltip="${tip(node.props)}"]`
849
- })
850
- const edges = graph.rels.map(rel => {
851
- return `${rel.from}->${rel.to} [label="${rel.type}" labeltooltip="${tip(rel.props)}"]`
852
- })
853
- return [
854
- 'digraph {',
855
- 'rankdir=LR',
856
- 'node [shape=box style=filled fillcolor=palegreen]',
857
- ...nodes,
858
- ...edges,
859
- '}'].join("\n")
860
- }
861
-
862
- // inspired by aspects-of-recent-changes/roster-graphs.html
863
- function walks(count,way='steps',neighborhood,scope={}) {
864
- const find = (slug,site) => neighborhood.find(info => info.slug == slug && (!site || info.domain == site))
865
- const finds = (slugs) => slugs ? slugs.map(slug => find(slug)) : null
866
- const prob = n => Math.floor(n * Math.abs(Math.random()-Math.random()))
867
- const rand = a => a[prob(a.length)]
868
- const good = info => info.links && Object.keys(info.links).length < 10
869
- const back = slug => neighborhood.filter(info => good(info) && slug in info.links)
870
- const newr = infos => infos.toSorted((a,b)=>b.date-a.date).slice(0,3)
871
- const domains = neighborhood
872
- .map(info => info.domain)
873
- .filter(uniq)
874
-
875
- function blanket(info) {
876
-
877
- // hub[0] => slug
878
- // find(slug) => info
879
- // node(info) => nid
880
- // back(slug) => infos
881
- // newr(infos) => infos
882
-
883
- const graph = new Graph()
884
- const node = info => {
885
- return graph.addUniqNode('',{
886
- name:info.title.replaceAll(/ /g,"\n"),
887
- title:info.title,
888
- site:info.domain
889
- })
890
- }
891
- const up = info => finds(info?.patterns?.up) ?? newr(back(info.slug))
892
- const down = info => info?.patterns?.down ?? Object.keys(info.links||{})
893
-
894
- // hub
895
- const nid = node(info)
896
-
897
- // parents of hub
898
- for(const parent of up(info)) {
899
- graph.addRel('',node(parent),nid)
900
- }
901
-
902
- // children of hub
903
- for(const link of down(info)) {
904
- const child = find(link)
905
- if(child) {
906
- const cid = node(child)
907
- graph.addRel('',nid,cid)
908
-
909
- // parents of children of hub
910
- for(const parent of up(child)) {
911
- graph.addRel('',node(parent),cid)
912
- }
913
- }
914
- }
915
- return graph
916
- }
917
-
918
- switch(way) {
919
- case 'steps': return steps(count)
920
- case 'days': return periods(way,1,count)
921
- case 'weeks': return periods(way,7,count)
922
- case 'months': return periods(way,30,count)
923
- case 'hubs': return hubs(count)
924
- case 'references': return references()
925
- case 'lineup': return lineup()
926
- }
927
-
928
- function steps(count=5) {
929
- return domains.map(domain => {
930
- const name = domain.split('.').slice(0,3).join('.')
931
- const done = new Set()
932
- const graph = new Graph()
933
- let nid = 0
934
- const here = neighborhood
935
- .filter(info => info.domain==domain && ('links' in info))
936
- if(!here.length) return {name,graph:null}
937
- const node = info => {
938
- nid = graph.addNode('',{
939
- name:info.title.replaceAll(/ /g,"\n"),
940
- title:info.title,
941
- site:domain,
942
- links:Object.keys(info.links||{}).filter(slug => find(slug))})
943
- return nid}
944
- const rel = (here,there) => graph.addRel('',here,there)
945
- const links = nid => graph.nodes[nid].props.links.filter(slug => !done.has(slug))
946
- const start = rand(here)
947
- // const start = find('welcome-visitors')
948
- done.add(start.slug)
949
- node(start)
950
- for (let n=5;n>0;n--) {
951
- try {
952
- const slugs = links(nid)
953
- const slug = rand(slugs)
954
- done.add(slug)
955
- const info = find(slug)
956
- rel(nid,node(info))}
957
- catch (e) {}
958
- }
959
- return {name,graph}
960
- })
961
- }
962
-
963
- function periods(way,days,count=12) {
964
- const interval = days*24*60*60*1000
965
- const iota = [...Array(Number(count)).keys()]
966
- const dates = iota.map(n => Date.now()-n*interval)
967
- const aspects = []
968
- for(const stop of dates) {
969
- const start = stop-interval
970
- const name = `${way.replace(/s$/,'')} ${new Date(start).toLocaleDateString()}`
971
- const here = neighborhood
972
- .filter(info => info.date < stop && info.date >= start)
973
- .filter(info => !(info.links && Object.keys(info.links).length > 5))
974
- if(here.length) {
975
- const domains = here.reduce((set,info) => {set.add(info.domain); return set}, new Set())
976
- for (const domain of domains) {
977
- const graph = new Graph()
978
- const node = info => {
979
- return graph.addUniqNode('',{
980
- name:info.title.replaceAll(/ /g,"\n"),
981
- title:info.title,
982
- site:info.domain,
983
- date:info.date
984
- })
985
- }
986
- const author = domain.split(/\.|\:/)[0]
987
- for (const info of here.filter(info => info.domain == domain)) {
988
- const nid = node(info)
989
- for (const link in (info.links||{})) {
990
- const linked = find(link)
991
- if(linked)
992
- graph.addRel('',nid,node(linked))
993
- }
994
- }
995
- aspects.push({name:`${name} ${author}`,graph})
996
- }
997
- }
998
- }
999
- return aspects
1000
- }
1001
-
1002
- function hubs(count=12) {
1003
- const aspects = []
1004
- const ignored = new Set()
1005
- const hits = {}
1006
- for (const info of neighborhood)
1007
- if(info.links)
1008
- if(Object.keys(info.links).length <= 15) {
1009
- for(const link in info.links)
1010
- if(find(link))
1011
- hits[link] = (hits[link]||0) + 1
1012
- } else {
1013
- ignored.add(info.slug)
1014
- }
1015
- if(ignored.size > 0)
1016
- console.log('hub links ignored for large pages:',[...ignored])
1017
- const hubs = Object.entries(hits)
1018
- .sort((a,b) => b[1]-a[1])
1019
- .slice(0,count)
1020
- console.log({hits,hubs})
1021
-
1022
- for(const hub of hubs) {
1023
- const name = `hub ${hub[1]} ${hub[0]}`
1024
- const graph = blanket(find(hub[0]))
1025
- aspects.push({name,graph})
1026
- }
1027
- return aspects
1028
- }
1029
-
1030
- function lineup() {
1031
- const aspects = []
1032
- const lineup = scope.lineup()
1033
- console.log({lineup})
1034
- for(const div of lineup){
1035
- const pageObject = wiki.lineup.atKey(div.dataset.key)
1036
- const slug = pageObject.getSlug()
1037
- const site = pageObject.getRemoteSite(location.host)
1038
- const info = find(slug,site)
1039
- console.log({div,pageObject,site,slug,info})
1040
- aspects.push({name:pageObject.getTitle(), graph:blanket(info)})
1041
- }
1042
- return aspects
1043
- }
1044
-
1045
- function references() {
1046
- const aspects = []
1047
- const items = scope.references()
1048
- console.log({items})
1049
- for(const item of items){
1050
- const {title,site,slug} = item
1051
- const info = find(slug,site)
1052
- console.log({site,slug,info})
1053
- aspects.push({name:title, graph:blanket(info)})
1054
- }
1055
- console.log({aspects})
1056
- return aspects
1057
- }
1058
- }
1059
-
1060
-
1061
- // adapted from testing-file-mech/testing-kwic.html
1062
- function kwic(prefix,lines,stop) {
1063
- const quotes = lines
1064
- .filter(line => line.match(/\t/))
1065
- .map(quote)
1066
- .flat()
1067
- .sort((a,b) => a.word<b.word ? -1 : 1)
1068
- let current = 'zzz'.slice(0,prefix)
1069
- const groups = []
1070
- for (const quote of quotes) {
1071
- const group = quote.word.toLowerCase().slice(0,prefix)
1072
- if (group != current) {
1073
- groups.push({group,quotes:[]})
1074
- current = group}
1075
- groups[groups.length-1].quotes.push(quote)
1076
- }
1077
- return groups
1078
-
1079
- function quote(line) {
1080
- const [key,text] = line.split(/\t/)
1081
- const words = text
1082
- .replaceAll(/'t\b/g,'t')
1083
- .replaceAll(/'s\b/g,'s')
1084
- .split(/[^a-zA-Z]+/)
1085
- .filter(word => word.length>3 && !stop.has(word.toLowerCase()))
1086
- return words
1087
- .map(word => ({word,line,key}))
1088
- }
1089
- }
1090
-
1091
-
1092
-
1093
- // adapted from graph/src/graph.js
1094
- class Graph {
1095
-
1096
- constructor(nodes=[], rels=[]) {
1097
- this.nodes = nodes;
1098
- this.rels = rels;
1099
- }
1100
-
1101
- addNode(type, props={}){
1102
- const obj = {type, in:[], out:[], props};
1103
- this.nodes.push(obj);
1104
- return this.nodes.length-1;
1105
- }
1106
-
1107
- addUniqNode(type, props={}) {
1108
- const nid = this.nodes.findIndex(node => node.type == type && node.props?.name == props?.name)
1109
- return nid >= 0 ? nid : this.addNode(type, props)
1110
- }
1111
-
1112
- addRel(type, from, to, props={}) {
1113
- const obj = {type, from, to, props};
1114
- this.rels.push(obj);
1115
- const rid = this.rels.length-1;
1116
- this.nodes[from].out.push(rid)
1117
- this.nodes[to].in.push(rid);
1118
- return rid;
1119
- }
1120
-
1121
- stringify(...args) {
1122
- const obj = { nodes: this.nodes, rels: this.rels }
1123
- return JSON.stringify(obj, ...args)
1124
- }
1125
-
1126
- }
1127
-
1128
- class Turtle {
1129
- constructor(elem) {
1130
- const size = elem
1131
- const div = document.createElement('div')
1132
- elem.closest('.item').firstElementChild.prepend(div)
1133
- div.outerHTML = `
1
+ /* wiki-plugin-mech - 0.1.31 - Tue, 15 Jul 2025 18:23:04 GMT */
2
+ (()=>{function B(e,o){let i=[];for(let n of document.querySelectorAll(".item"))if(n.classList.contains(`${o}-source`)&&i.unshift(n),n===e)break;return i.map(n=>{let t=n[`${o}Data`],s=t?t():null;return{div:n,result:s}})}function z(e){let o=t=>Object.entries(t).filter(s=>s[1]).map(s=>`${s[0]}: ${s[1]}`).join("\\n"),i=e.nodes.map((t,s)=>{let r=t.type?`${t.type}\\n${t.props.name}`:t.props.name;return`${s} [label="${r}" ${t.props.url||t.props.tick?`URL="${t.props.url||"#"}" target="_blank"`:""} tooltip="${o(t.props)}"]`}),n=e.rels.map(t=>`${t.from}->${t.to} [label="${t.type}" labeltooltip="${o(t.props)}"]`);return["digraph {","rankdir=LR","node [shape=box style=filled fillcolor=palegreen]",...i,...n,"}"].join(`
3
+ `)}function J(e,o="steps",i,n={}){let t=(f,k)=>i.find(y=>y.slug==f&&(!k||y.domain==k)),s=f=>f?f.map(k=>t(k)):null,r=f=>Math.floor(f*Math.abs(Math.random()-Math.random())),c=f=>f[r(f.length)],l=f=>f.links&&Object.keys(f.links).length<10,p=f=>i.filter(k=>l(k)&&f in k.links),a=(f,k,y)=>y.findIndex(x=>x.slug==f.slug)===k,u=f=>f.toSorted((k,y)=>y.date-k.date).filter(a).slice(0,3),g=i.map(f=>f.domain).filter(_);function m(f){let k=new K,y=h=>k.addUniqNode("",{name:h.title.replaceAll(/ /g,`
4
+ `),title:h.title,site:h.domain}),x=h=>s(h?.patterns?.up)??u(p(h.slug)),E=h=>h?.patterns?.down??Object.keys(h.links||{}),w=y(f);for(let h of x(f))k.addRel("",y(h),w);for(let h of E(f)){let v=t(h);if(v){let A=y(v);k.addRel("",w,A);for(let D of x(v))k.addRel("",y(D),A)}}return k}switch(o){case"steps":return d(e);case"days":return S(o,1,e);case"weeks":return S(o,7,e);case"months":return S(o,30,e);case"hubs":return O(e);case"references":return C();case"lineup":return I()}function d(f=5){return g.map(k=>{let y=k.split(".").slice(0,3).join("."),x=new Set,E=new K,w=0,h=i.filter(L=>L.domain==k&&"links"in L);if(!h.length)return{name:y,graph:null};let v=L=>(w=E.addNode("",{name:L.title.replaceAll(/ /g,`
5
+ `),title:L.title,site:k,links:Object.keys(L.links||{}).filter(T=>t(T))}),w),A=(L,T)=>E.addRel("",L,T),D=L=>E.nodes[L].props.links.filter(T=>!x.has(T)),N=c(h);x.add(N.slug),v(N);for(let L=5;L>0;L--)try{let T=D(w),j=c(T);x.add(j);let W=t(j);A(w,v(W))}catch{}return{name:y,graph:E}})}function S(f,k,y=12){let x=k*24*60*60*1e3,w=[...Array(Number(y)).keys()].map(v=>Date.now()-v*x),h=[];for(let v of w){let A=v-x,D=`${f.replace(/s$/,"")} ${new Date(A).toLocaleDateString()}`,N=i.filter(L=>L.date<v&&L.date>=A).filter(L=>!(L.links&&Object.keys(L.links).length>5));if(N.length){let L=N.reduce((T,j)=>(T.add(j.domain),T),new Set);for(let T of L){let j=new K,W=H=>j.addUniqNode("",{name:H.title.replaceAll(/ /g,`
6
+ `),title:H.title,site:H.domain,date:H.date}),oe=T.split(/\.|\:/)[0];for(let H of N.filter(F=>F.domain==T)){let F=W(H);for(let ie in H.links||{}){let U=t(ie);U&&j.addRel("",F,W(U))}}h.push({name:`${D} ${oe}`,graph:j})}}}return h}function O(f=12){let k=[],y=new Set,x={};for(let w of i)if(w.links)if(Object.keys(w.links).length<=15)for(let h in w.links)t(h)&&(x[h]=(x[h]||0)+1);else y.add(w.slug);y.size>0&&console.log("hub links ignored for large pages:",[...y]);let E=Object.entries(x).sort((w,h)=>h[1]-w[1]).slice(0,f);console.log({hits:x,hubs:E});for(let w of E){let h=`hub ${w[1]} ${w[0]}`,v=m(t(w[0]));k.push({name:h,graph:v})}return k}function I(){let f=[],k=n.lineup();console.log({lineup:k});for(let y of k){let x=wiki.lineup.atKey(y.dataset.key),E=x.getSlug(),w=x.getRemoteSite(location.host),h=t(E,w);console.log({div:y,pageObject:x,site:w,slug:E,info:h}),f.push({name:x.getTitle(),graph:m(h)})}return f}function C(){let f=[],k=n.references();console.log({items:k});for(let y of k){let{title:x,site:E,slug:w}=y,h=t(w,E);console.log({site:E,slug:w,info:h}),f.push({name:x,graph:m(h)})}return console.log({aspects:f}),f}}function V(e,o,i){let n=o.filter(c=>c.match(/\t/)).map(r).flat().sort((c,l)=>c.word<l.word?-1:1),t="zzz".slice(0,e),s=[];for(let c of n){let l=c.word.toLowerCase().slice(0,e);l!=t&&(s.push({group:l,quotes:[]}),t=l),s[s.length-1].quotes.push(c)}return s;function r(c){let[l,p]=c.split(/\t/);return p.replaceAll(/'t\b/g,"t").replaceAll(/'s\b/g,"s").split(/[^a-zA-Z]+/).filter(u=>u.length>3&&!i.has(u.toLowerCase())).map(u=>({word:u,line:c,key:l}))}}var K=class{constructor(o=[],i=[]){this.nodes=o,this.rels=i}addNode(o,i={}){let n={type:o,in:[],out:[],props:i};return this.nodes.push(n),this.nodes.length-1}addUniqNode(o,i={}){let n=this.nodes.findIndex(t=>t.type==o&&t.props?.name==i?.name);return n>=0?n:this.addNode(o,i)}addRel(o,i,n,t={}){let s={type:o,from:i,to:n,props:t};this.rels.push(s);let r=this.rels.length-1;return this.nodes[i].out.push(r),this.nodes[n].in.push(r),r}stringify(...o){let i={nodes:this.nodes,rels:this.rels};return JSON.stringify(i,...o)}};function Z(e,o){let i=()=>(e.story||[]).map(s=>s?.id),n=(s,r)=>{let c=i().indexOf(s)+1;e.story.splice(c,0,r)},t=()=>{let s=i().indexOf(o.id);s!==-1&&e.story.splice(s,1)};switch(e.story=e.story||[],o.type){case"create":o.item&&(o.item.title!=null&&(e.title=o.item.title),o.item.story!=null&&(e.story=o.item.story.slice()));break;case"add":n(o.after,o.item);break;case"edit":let s=i().indexOf(o.id);s!==-1?e.story.splice(s,1,o.item):e.story.push(o.item);break;case"move":let r=o.order.indexOf(o.id),c=o.order[r-1],l=e.story[i().indexOf(o.id)];t(),n(c,l);break;case"remove":t();break}e.journal=e.journal||[],o.fork&&e.journal.push({type:"fork",site:o.fork,date:o.date-1}),e.journal.push(o)}function q(e){if(!e.data)return;let{data:o}=e;if(o?.action=="publishSourceData"&&o?.name=="aspect"){wiki.debug&&console.log("soloListener - source update",{event:e,data:o});return}if(!e.source.opener||e.source.location.pathname!=="/plugins/solo/dialog/"){wiki.debug&&console.log("soloListener - not for us",{event:e});return}wiki.debug&&console.log("soloListener - ours",{event:e});let{action:i,keepLineup:n=!1,pageKey:t=null,title:s=null,context:r=null,page:c=null}=o,l=null;switch(t!=null&&(l=n?null:$(".page").filter((p,a)=>$(a).data("key")==t)),i){case"doInternalLink":wiki.pageHandler.context=r,wiki.doInternalLink(s,l);break;case"showResult":let p=n?{}:{$page:l};wiki.showResult(wiki.newPage(c),p);break;default:console.error({where:"soloListener",message:"unknown action",data:o})}}var se=Number.MAX_SAFE_INTEGER;function G(e,o=1e3,i=se){let n=!1,t=Date.now(),s={stop:c,remainingTicks:i,minMS:o,ticksSoFar:0,timeSinceLastTick:0},r=l();return r.api=s,r;function c(){n=!0}async function l(){if(n||s.remainingTicks<1)return;s.remainingTicks-=1,s.ticksSoFar+=1,s.timeSinceLastTick=Date.now()-t,t=Date.now();let p=new Promise(a=>setTimeout(a,s.minMS));return await e(s),await p,l()}}var Q={trouble:b,inspect:R,response:Y,button:re,element:ce,jfetch:le,status:ae,sourceData:ue,showResult:de,neighborhood:fe,publishSourceData:pe,newSVG:ge,SVGline:he,ticker:G};function b(e,o){e.innerText.match(/✖︎/)||(e.innerHTML+='<button style="border-width:0;color:red;">\u2716\uFE0E</button>',e.querySelector("button").addEventListener("click",i=>{e.outerHTML+=`<span style="width:80%;color:gray;">${o}</span>`}))}function R(e,o,i){let n=e.previousElementSibling;if(i.debug){let t=i[o];n.innerHTML=`${o} \u21D2 `,n.addEventListener("click",s=>{console.log({key:o,value:t});let r=n.previousElementSibling;if(!r?.classList.contains("look")){let p=document.createElement("div");p.classList.add("look"),n.insertAdjacentElement("beforebegin",p),r=n.previousElementSibling}let c=JSON.stringify(t,null,1);c.length>300&&(c=c.substring(0,400)+"...");let l="border:1px solid black; background-color:#f8f8f8; padding:8px; color:gray; word-break: break-all;";r.innerHTML=`<div style="${l}">${c}</div>`})}else n.innerHTML=""}function Y(e,o){e.innerHTML+=o}function re(e,o,i){e.innerHTML.match(/button/)||(Y(e,`<button style="border-width:0;">${o}</button>`),e.querySelector("button").addEventListener("click",i))}function ce(e){return document.getElementById(e)}async function le(e){return fetch(e).then(o=>o.ok?o.json():null)}function ae(e,o,i){e.innerHTML=o+i}function ue(e,o){let i=e.closest(".item"),n=B(i,o).map(({div:t,result:s})=>({classList:[...t.classList],id:t.dataset.id,result:s}));return n.length?n:(b(e,`Expected source for "${o}" in the lineup.`),null)}function pe(e,o,i){let n=e.closest(".item");n.classList.add(`${o}-source`),n[`${o}Data`]=()=>i}function de(e,o){let i={$page:$(e.closest(".page"))};wiki.showResult(wiki.newPage(o),i)}function fe(e){return Object.entries(wiki.neighborhoodObject.sites).filter(([o,i])=>!i.sitemapRequestInflight&&(!e||o.includes(e))).map(([o,i])=>(i.sitemap||[]).map(n=>Object.assign({domain:o},n)))}function ge(e){let o=document.createElement("div");return e.closest(".item").firstElementChild.prepend(o),o.outerHTML=`
1134
7
  <div style="border:1px solid black; background-color:#f8f8f8; margin-bottom:16px;">
1135
8
  <svg viewBox="0 0 400 400" width=100% height=400>
1136
9
  <circle id=dot r=5 cx=200 cy=200 stroke="#ccc"></circle>
1137
10
  </svg>
1138
- </div>`
1139
- this.svg = elem.closest('.item').getElementsByTagName('svg')[0]
1140
- this.position = [200,200]
1141
- this.direction = 0
1142
- }
1143
-
1144
- forward(steps) {
1145
- const theta = this.direction*2*Math.PI/360
1146
- const [x1,y1] = this.position
1147
- const [x2,y2] = [x1+steps*Math.sin(theta), y1+steps*Math.cos(theta)]
1148
- const line = document.createElementNS("http://www.w3.org/2000/svg", 'line')
1149
- const set = (k,v) => line.setAttribute(k,Math.round(v))
1150
- set("x1",x1); set("y1",400-y1)
1151
- set("x2",x2); set("y2",400-y2)
1152
- line.style.stroke = "black"
1153
- line.style.strokeWidth = "2px"
1154
- this.svg.appendChild(line)
1155
- const dot = this.svg.getElementById('dot')
1156
- dot.setAttribute('cx',Math.round(x2))
1157
- dot.setAttribute('cy',Math.round(400-y2))
1158
- this.position = [x2,y2]
1159
- return this.position
1160
- }
1161
-
1162
- turn(degrees) {
1163
- this.direction += degrees
1164
- return this.direction}
1165
- }
1166
-
1167
- // adapted from wiki-client/lib/revision.coffee
1168
-
1169
- // This module interprets journal actions in order to update
1170
- // a story or even regenerate a complete story from some or
1171
- // all of a journal.
1172
-
1173
- function apply(page, action) {
1174
- const order = () => {
1175
- return (page.story || []).map(item => item?.id);
1176
- };
1177
-
1178
- const add = (after, item) => {
1179
- const index = order().indexOf(after) + 1;
1180
- page.story.splice(index, 0, item);
1181
- };
1182
-
1183
- const remove = () => {
1184
- const index = order().indexOf(action.id);
1185
- if (index !== -1) {
1186
- page.story.splice(index, 1);
1187
- }
1188
- };
1189
-
1190
- page.story = page.story || [];
1191
-
1192
- switch (action.type) {
1193
- case 'create':
1194
- if (action.item) {
1195
- if (action.item.title != null) {
1196
- page.title = action.item.title;
1197
- }
1198
- if (action.item.story != null) {
1199
- page.story = action.item.story.slice();
1200
- }
1201
- }
1202
- break;
1203
- case 'add':
1204
- add(action.after, action.item);
1205
- break;
1206
- case 'edit':
1207
- const index = order().indexOf(action.id);
1208
- if (index !== -1) {
1209
- page.story.splice(index, 1, action.item);
1210
- } else {
1211
- page.story.push(action.item);
1212
- }
1213
- break;
1214
- case 'move':
1215
- // construct relative addresses from absolute order
1216
- const moveIndex = action.order.indexOf(action.id);
1217
- const after = action.order[moveIndex - 1];
1218
- const item = page.story[order().indexOf(action.id)];
1219
- remove();
1220
- add(after, item);
1221
- break;
1222
- case 'remove':
1223
- remove();
1224
- break;
1225
- }
1226
-
1227
- page.journal = page.journal || [];
1228
- if (action.fork) {
1229
- // implicit fork
1230
- page.journal.push({ type: 'fork', site: action.fork, date: action.date - 1 });
1231
- }
1232
- page.journal.push(action);
1233
- }
1234
-
1235
-
1236
- // adapted from Solo client
1237
-
1238
- function soloListener(event) {
1239
-
1240
- if (!event.data) return
1241
- const { data } = event
1242
- if (data?.action == "publishSourceData" && data?.name == "aspect") {
1243
- if (wiki.debug) console.log('soloListener - source update', {event,data})
1244
- return
1245
- }
1246
-
1247
- // only continue if event is from a solo popup.
1248
- // events from a popup window will have an opener
1249
- // ensure that the popup window is one of ours
1250
-
1251
- if (!event.source.opener || event.source.location.pathname !== '/plugins/solo/dialog/') {
1252
- if (wiki.debug) {console.log('soloListener - not for us', {event})}
1253
- return
1254
- }
1255
- if (wiki.debug) {console.log('soloListener - ours', {event})}
1256
-
1257
- const { action, keepLineup=false, pageKey=null, title=null, context=null, page=null} = data;
1258
-
1259
- let $page = null
1260
- if (pageKey != null) {
1261
- $page = keepLineup ? null : $('.page').filter((i, el) => $(el).data('key') == pageKey)
1262
- }
1263
-
1264
- switch (action) {
1265
- case 'doInternalLink':
1266
- wiki.pageHandler.context = context
1267
- wiki.doInternalLink(title, $page)
1268
- break
1269
- case 'showResult':
1270
- const options = keepLineup ? {} : {$page}
1271
- wiki.showResult(wiki.newPage(page), options)
1272
- break
1273
- default:
1274
- console.error({ where:'soloListener', message: "unknown action", data })
1275
- }
1276
- }
1277
-
1278
-
1279
- function create(revIndex, data) {
1280
- revIndex = +revIndex;
1281
- const revJournal = data.journal.slice(0, revIndex + 1);
1282
- const revPage = { title: data.title, story: [] };
1283
- for (const action of revJournal) {
1284
- apply(revPage, action || {});
1285
- }
1286
- return revPage;
1287
- }
1288
-
1289
-
1290
- }).call(this)
11
+ </div>`,e.closest(".item").getElementsByTagName("svg")[0]}function he(e,[o,i],[n,t]){let s=document.createElementNS("http://www.w3.org/2000/svg","line"),r=(l,p)=>s.setAttribute(l,Math.round(p));r("x1",o),r("y1",400-i),r("x2",n),r("y2",400-t),s.style.stroke="black",s.style.strokeWidth="2px",e.appendChild(s);let c=e.getElementById("dot");c.setAttribute("cx",Math.round(n)),c.setAttribute("cy",Math.round(400-t))}async function M(e,o){let i=e.slice();for(;i.length;){let n=i.shift();if("command"in n){let t=n.command,s=o.api?o.api.element(n.key):document.getElementById(n.key),[r,...c]=n.command.split(/ +/),l=i[0],p=l&&"command"in l?null:i.shift(),a={command:t,op:r,args:c,body:p,elem:s,state:o};o.debug&&console.log(a),X[r]?await X[r].emit.apply(null,[a]):r.match(/^[A-Z]+$/)?o.api.trouble(s,`${r} doesn't name a block we know.`):n.command.match(/\S/)&&o.api.trouble(s,"Expected line to begin with all-caps keyword.")}}}function me({elem:e,body:o,state:i}){if(!o?.length)return i.api.trouble(e,"CLICK expects indented blocks to follow.");i.api.button(e,"\u25B6",n=>{i.debug=n.shiftKey,M(o,i)})}function ke({elem:e,args:o,state:i}){let n=o[0]=="world"?" \u{1F30E}":" \u{1F600}";for(let t of Object.keys(i))i.api.inspect(e,t,i);i.api.response(e,n)}async function be({elem:e,args:o,body:i,state:n}){if(!i?.length)return n.api.trouble(e,"FROM expects indented blocks to follow.");let t=o[0];n.api.response(e," \u23F3"),n.page=await n.api.jfetch(`//${t}.json`),n.api.response(e," \u231B"),M(i,n)}function we({elem:e,command:o,args:i,body:n,state:t}){if(t.api.status(e,o,""),!("page"in t))return t.api.trouble(e,'Expect "page" as with FROM.');t.api.inspect(e,"page",t);let s=t.page.story.find(u=>u.type=="datalog");if(!s)return t.api.trouble(e,"Expect Datalog plugin in the page.");let r=i[0];if(!r)return t.api.trouble(e,"SENSOR needs a sensor name.");let c=s.text.split(/\n/).map(u=>u.split(/ +/)).filter(u=>u[0]=="SENSOR").find(u=>u[1]==r);if(!c)return t.api.trouble(e,`Expect to find "${r}" in Datalog.`);let l=c[2],p=u=>9/5*(u/16)+32,a=u=>u.reduce((g,m)=>g+m,0)/u.length;t.api.status(e,o," \u23F3"),t.api.jfetch(l).then(u=>{t.debug&&console.log({sensor:c,data:u}),t.api.status(e,o," \u231B");let g=p(a(Object.values(u)));t.temperature=`${g.toFixed(2)}\xB0F`,M(n,t)})}function ye({elem:e,command:o,state:i}){let n=i?.temperature;if(!n)return i.api.trouble(e,"Expect data, as from SENSOR.");i.api.inspect(e,"temperature",i),i.api.response(e,`<br><font face=Arial size=32>${n}</font>`)}function xe({elem:e,command:o,args:i,body:n,state:t}){if(!(i&&i.length))return t.api.trouble(e,'Expected Source topic, like "markers" for Map markers.');let s=i[0],r=t.api.sourceData(e,s);if(!r)return;t.debug&&console.log({topic:s,sources:r});let c=p=>{let a=r.filter(u=>u.classList.includes(p)).length;return a?`${a} ${p}`:null},l=[c("map"),c("image"),c("frame"),c("assets")].filter(p=>p).join(", ");t.api.status(e,o," \u21D2 "+l),t[s]=r.map(({id:p,result:a})=>({id:p,result:a})),n&&M(n,t)}function $e({elem:e,command:o,args:i,state:n}){let t=u=>(+u).toFixed(7),s=[],r=i;for(let u of r)switch(u){case"map":if(!("marker"in n))return n.api.trouble(e,'"map" preview expects "marker" state, like from "SOURCE marker".');n.api.inspect(e,"marker",n);let g=n.marker.map(d=>[d.result]).flat(2).map(d=>`${t(d.lat)}, ${t(d.lon)} ${d.label||""}`).filter(_).join(`
12
+ `);s.push({type:"map",text:g});break;case"graph":if(!("aspect"in n))return n.api.trouble(e,'"graph" preview expects "aspect" state, like from "SOURCE aspect".');n.api.inspect(e,"aspect",n);for(let{div:d,result:S}of n.aspect){for(let{name:O,graph:I}of S)n.debug&&console.log({div:d,result:S,name:O,graph:I}),s.push({type:"paragraph",text:O}),s.push({type:"graphviz",text:z(I)});s.push({type:"pagefold",text:"."})}break;case"items":if(!("items"in n))return n.api.trouble(e,'"graph" preview expects "items" state, like from "KWIC".');n.api.inspect(e,"items",n),s.push(...n.items);break;case"page":if(!("page"in n))return n.api.trouble(e,'"page" preview expects "page" state, like from "FROM".');n.api.inspect(e,"page",n),s.push(...n.page.story);break;case"synopsis":let m=`This page created with Mech command: "${o}". See [[${n.context.title}]].`;s.push({type:"paragraph",text:m,id:n.context.itemId});break;default:return n.api.trouble(e,`"${u}" doesn't name an item we can preview`)}let l={title:"Mech Preview"+(n.tick?` ${n.tick}`:""),story:s};for(let u of l.story)u.id||=(Math.random()*10**20).toFixed(0);let p=JSON.parse(JSON.stringify(l)),a=Date.now();l.journal=[{type:"create",date:a,item:p}],n.api.showResult(e,l)}async function Le({elem:e,command:o,args:i,body:n,state:t}){let s=l=>t.api.element(l.key),r=i[0],c=t.api.neighborhood(r);for(let l of n||[]){if(!l.command.endsWith(" Survey")){t.api.trouble(s(l),"NEIGHBORS expects a Site Survey title, like Pattern Link Survey");continue}let p=c.filter(a=>a.find(u=>u.title==l.command));t.api.status(s(l),l.command,`\u21D2 ${p.length} sites`);for(let a of p){let u=`//${a[0].domain}/${te(l.command)}.json`,g=await t.api.jfetch(u);if(!g)continue;let m=g.story.find(d=>d.type=="frame")?.survey;if(m){for(let d of a){let S=Object.assign({},m.find(O=>O.slug==d.slug),d);Object.assign(d,S)}console.log({url:u,page:g,survey:m,todo:a})}}}t.neighborhood=c.flat().sort((l,p)=>p.date-l.date),t.api.status(e,o,`\u21D2 ${t.neighborhood.length} pages, ${c.length} sites`)}function Se({elem:e,command:o,args:i,state:n}){if(!("neighborhood"in n))return n.api.trouble(e,"WALK expects state.neighborhood, like from NEIGHBORS.");n.api.inspect(e,"neighborhood",n);let[,t,s]=o.match(/\b(\d+)? *(steps|days|weeks|months|hubs|lineup|references)\b/)||[];if(!s&&o!="WALK")return tate.api.trouble(e,"WALK can't understand rest of this block.");let r={lineup(){let a=[...document.querySelectorAll(".page")],u=a.indexOf(e.closest(".page"));return a.slice(0,u)},references(){let a=e.closest(".page"),u=wiki.lineup.atKey(a.dataset.key),g=u.getRawPage().story;return console.log({div:a,pageObject:u,story:g}),g.filter(m=>m.type=="reference")}},c=J(t,s,n.neighborhood,r),l=c.filter(({graph:a})=>a);n.debug&&console.log({steps:c});let p=l.map(({graph:a})=>a.nodes).flat();if(n.api.status(e,o,` \u21D2 ${l.length} aspects, ${p.length} nodes`),c.find(({graph:a})=>!a)&&n.api.trouble(e,"WALK skipped sites with no links in sitemaps"),l.length){n.aspect=n.aspect||[];let a=n.aspect.find(u=>u.id==e.id);a?a.result=l:n.aspect.push({id:e.id,result:l,source:o}),n.api.publishSourceData(e,"aspect",n.aspect.map(u=>u.result).flat()),n.debug&&console.log({command:o,state:n.aspect,item:item.aspectData()})}}function Ee({elem:e,command:o,args:i,body:n,state:t}){if(console.log({command:o,args:i,body:n,state:t}),!n?.length)return t.api.trouble(e,"TICK expects indented blocks to follow.");let s=i[0]||"1";if(!s.match(/^[1-9][0-9]?$/))return t.api.trouble(e,"TICK expects a count from 1 to 99");let r,c;if(t.tick!=null)return c=t.tick,a({shiftKey:t.debug}),r;l();function l(){t.api.button(e,"\u25B6",a)}function p(u){t.api.status(e,o,` \u21D2 ${u} remaining`)}function a(u){t.debug=u.shiftKey,t.tick=+s,p(t.tick),r=t.api.ticker(async()=>{t.debug&&console.log({tick:t.tick,count:s}),"tick"in t&&--t.tick>=0?(p(t.tick),await M(n,t)):(r=r.api.stop(),t.tick=c,t.api.status(e,o,""),l())})}}function Te({elem:e,command:o,args:i,body:n,state:t}){if(!i.length)return b(e,"UNTIL expects an argument, a word to stop running.");if(!t.tick)return b(e,"UNTIL expects to indented below an iterator, like TICKS.");if(!t.aspect)return b(e,'UNTIL expects "aspect", like from WALK.');R(e,"aspect",t),e.innerHTML=o+` \u21D2 ${t.tick}`;let s=i[0];for(let{div:r,result:c}of t.aspect)for(let{name:l,graph:p}of c)for(let a of p.nodes)if(a.type.includes(s)||a.props.name.includes(s)){t.debug&&console.log({div:r,result:c,name:l,graph:p,node:a}),delete t.tick,e.innerHTML+=" done",n&&M(n,t);return}}function ve({elem:e,command:o,args:i,state:n}){if(i.length<1)return n.api.trouble(e,'FORWARD expects an argument, the number of steps to move a "turtle".');n.turtle??={svg:n.api.newSVG(e),position:[200,200],direction:0};let t=i[0],s=n.turtle.direction*2*Math.PI/360,[r,c]=n.turtle.position;n.turtle.position=[r+t*Math.sin(s),c+t*Math.cos(s)],n.api.SVGline(n.turtle.svg,[r,c],n.turtle.position),n.api.status(e,o,` \u21D2 ${n.turtle.position.map(l=>(l-200).toFixed(1)).join(", ")}`)}function Oe({elem:e,command:o,args:i,state:n}){if(i.length<1)return n.api.trouble(e,'TURN expects an argument, the number of degrees to turn a "turtle".');n.turtle??={svg:n.api.newSVG(e),position:[200,200],direction:0};let t=+i[0];n.turtle.direction+=t,n.api.status(e,o,` \u21D2 ${n.turtle.direction}\xB0`)}function Me({elem:e,command:o,args:i,body:n,state:t}){if(!("assets"in t))return b(e,"FILE expects state.assets, like from SOURCE assets.");R(e,"assets",t);let s="//"+window.location.host,r=t.assets.map(({id:a,result:u})=>Object.entries(u).map(([g,m])=>Object.entries(m).map(([d,S])=>S.map(O=>{let I=d.startsWith("//")?d:`${s}${d}`,C=I.replace(/\/assets$/,""),f=`${I}/${g}/${O}`;return{id:a,dir:g,path:d,host:C,file:O,url:f}})))).flat(3);if(t.debug&&console.log({assets:r}),i.length<1)return b(e,"FILE expects an argument, the dot suffix for desired files.");if(!n?.length)return b(e,"FILE expects indented blocks to follow.");let c=i[0],l=r.filter(a=>a.file.endsWith(c)),p=a=>`<img width=12 src=${l[a].host+"/favicon.png"}>`;if(!l)return b(e,`FILE expects to find an asset with "${c}" suffix.`);e.innerHTML=o+`<br><div class=choices style="border:1px solid black; background-color:#f8f8f8; padding:8px;" >${l.map((a,u)=>`<span data-choice=${u} style="cursor:pointer;">
13
+ ${p(u)}
14
+ ${a.file} \u25B6
15
+ </span>`).join(`<br>
16
+ `)}</div>`,e.querySelector(".choices").addEventListener("click",a=>{if(!("choice"in a.target.dataset))return;let u=l[a.target.dataset.choice].url;fetch(u).then(g=>g.text()).then(g=>{e.innerHTML=o+` \u21D2 ${g.length} bytes`,t.tsv=g,console.log({text:g}),M(n,t)})})}function Re({elem:e,command:o,args:i,body:n,state:t}){let s=n&&n[0]?.command;if(s&&!s.match(/\$[KW]/))return b(e,"KWIK expects $K or $W in link prototype.");if(!("tsv"in t))return b(e,"KWIC expects a .tsv file, like from ASSETS .tsv.");R(e,"tsv",t);let r=i[0]||1,c=t.tsv.trim().split(/\n/),l=new Set(["of","and","in","at"]),p=$(e.closest(".page")).data("data"),a=p.story.findIndex(m=>m.type=="pagefold"&&m.text=="stop");if(a>=0){let m=p.story.findIndex((d,S)=>S>a&&d.type=="pagefold");p.story.slice(a+1,m).map(d=>d.text.trim().split(/\s+/)).flat().forEach(d=>l.add(d))}let u=V(r,c,l);e.innerHTML=o+` \u21D2 ${c.length} lines, ${u.length} groups`;let g=m=>{let d=m.line;if(s){let S=s.replaceAll(/\$K\+/g,m.key.replaceAll(/ /g,"+")).replaceAll(/\$K/g,m.key).replaceAll(/\$W/g,m.word),O=s.match(/\$W/)?m.word:m.key;d=d.replace(O,S)}return d};t.items=u.map(m=>({type:"markdown",text:`# ${m.group}
17
+
18
+ ${m.quotes.map(S=>g(S)).join(`
19
+ `)}`}))}function je({elem:e,command:o,args:i,state:n}){e.innerHTML=o;let t,s;if(i.length<1)if(n.info)R(e,"info",n),t=n.info.domain,s=n.info.slug,e.innerHTML=o+` \u21D2 ${n.info.title}`;else return b(e,"SHOW expects a slug or site/slug to open in the lineup.");else{let l=i[0];[t,s]=l.includes("/")?l.split(/\//):[null,l]}if([...document.querySelectorAll(".page")].map(l=>l.id).includes(s))return b(e,"SHOW expects a page not already in the lineup.");let c=e.closest(".page");wiki.doInternalLink(s,c,t)}function Ie({elem:e,command:o,state:i}){if(!i.neighborhood)return b(e,"RANDOM expected a neighborhood, like from NEIGHBORS.");R(e,"neighborhood",i);let n=i.neighborhood,t=n.length,s=Math.floor(Math.random()*t);e.innerHTML=o+` \u21D2 ${s} of ${t}`,i.info=n[s]}function Ae({elem:e,command:o,args:i,body:n,state:t}){let s=i[0]||"1";return s.match(/^[1-9][0-9]?$/)?new Promise(r=>{n&&M(n,t).then(l=>{t.debug&&console.log(o,"children",l)}),e.innerHTML=o+` \u21D2 ${s} remain`;let c=setInterval(()=>{--s>0?e.innerHTML=o+` \u21D2 ${s} remain`:(clearInterval(c),e.innerHTML=o+" \u21D2 done",t.debug&&console.log(o,"done"),r())},1e3)}):b(e,"SLEEP expects seconds from 1 to 99")}function He({elem:e,command:o,args:i,body:n,state:t}){if(!n)return b(e,"TOGETHER expects indented commands to run together.");let s=n.map(r=>M([r],t));return Promise.all(s)}async function Ne({elem:e,command:o,args:i,body:n,state:t}){if(!n)return b(e,"GET expects indented commands to run on the server.");let s={},r=t.context.site;if(i.length)for(let d of i)if(d in t)R(e,d,t),s[d]=t[d];else if(d.match(/\./))r=d;else return b(e,`GET expected "${d}" to name state or site.`);let c=t.context.slug,l=t.context.itemId,p=`mech=${btoa(JSON.stringify(n))}&state=${btoa(JSON.stringify(s))}`,a=`//${r}/plugin/mech/run/${c}/${l}?${p}`;e.innerHTML=o+" \u21D2 in progress";let u=Date.now(),g;try{if(g=await fetch(a).then(d=>d.ok?d.json():d.status),"err"in g)return b(e,`RUN received error "${g.err}"`)}catch(d){return b(e,`RUN failed with "${d.message}"`)}t.result=g;for(let d of g.mech.flat(9)){let S=document.getElementById(d.key);"status"in d&&(S.innerHTML=d.command+` \u21D2 ${d.status}`),"trouble"in d&&b(S,d.trouble)}"debug"in g.state&&delete g.state.debug,Object.assign(t,g.state);let m=((Date.now()-u)/1e3).toFixed(3);e.innerHTML=o+` \u21D2 ${m} seconds`}function De({elem:e,command:o,args:i,body:n,state:t}){let s=c=>JSON.parse(JSON.stringify(c)),r=c=>JSON.stringify(c).length;if(i.length<1)return b(e,'DELTA expects argument, "have" or "apply" on client.');if(n)return b(e,"DELTA doesn't expect indented input.");switch(i[0]){case"have":let c=t.context.page.journal.filter(u=>u.type!="fork");t.recent=c[c.length-1].date,e.innerHTML=o+` \u21D2 ${new Date(t.recent).toLocaleString()}`;break;case"apply":if(!("actions"in t))return b(e,'DELTA apply expect "actions" as input.');R(e,"actions",t);let l=s(t.context.page),p=r(l);for(let u of t.actions)Z(l,u);t.page=l;let a=r(l);e.innerHTML=o+` \u21D2 \u2206 ${((a-p)/p*100).toFixed(1)}%`;break;default:b(e,`DELTA doesn't know "${i[0]}".`)}}function Ke({elem:e,command:o,state:i}){if(!i.neighborhood)return b(e,"ROSTER expected a neighborhood, like from NEIGHBORS.");R(e,"neighborhood",i);let n=i.neighborhood,t=n.map(c=>c.domain).filter(_),s=c=>c[Math.floor(Math.random()*c.length)];i.debug&&console.log(n);let r=[{type:"roster",text:`Mech
20
+ `+t.join(`
21
+ `)},{type:"activity",text:`ROSTER Mech
22
+ SINCE 30 days`}];e.innerHTML=o+` \u21D2 ${t.length} sites`,i.items=r}function _e({elem:e,command:o,state:i}){let n=[...document.querySelectorAll(".page")].map(t=>{let s=$(t),r=s.data("data"),c=s.data("site")||location.host,l=s.attr("id").split("_")[0],p=r.title||"Empty",a=r.story[0]?.text||"empty";return{type:"reference",site:c,slug:l,title:p,text:a}});e.innerHTML=o+` \u21D2 ${n.length} pages`,i.items=n}function We({elem:e,command:o,args:i,state:n}){if(i.length<1)return b(e,"LISTEN expects argument, an action.");let t=i[0],s=Date.now(),r=0,c=l;c.action="publishSourceData",c.id=e.id,window.addEventListener("message",l),$(".main").on("thumb",(p,a)=>console.log("jquery",{evt:p,thumb:a})),e.innerHTML=o+" \u21D2 ready";function l(p){console.log({event:p});let{data:a}=p;if(a.action=="publishSourceData"&&(a.name==t||a.topic==t))if(r++,c.count=r,n.debug&&console.log({count:r,data:a}),r<=100){let u=Date.now(),g=u-s;s=u,e.innerHTML=o+` \u21D2 ${r} events, ${g} ms`}else window.removeEventListener("message",l)}}function Ce({elem:e,command:o,args:i,state:n}){if(i.length<1)return b(e,"MESSAGE expects argument, an action.");let t=i[0],s={action:"publishSourceData",topic:t,name:t};window.postMessage(s,"*"),e.innerHTML=o+" \u21D2 sent"}async function Fe({elem:e,command:o,state:i}){if(!("aspect"in i))return b(e,'"SOLO" expects "aspect" state, like from "WALK".');R(e,"aspect",i),e.innerHTML=o;let n=i.aspect.map(l=>({source:l.source||l.id,aspects:l.result})),t=n.reduce((l,p)=>l+p.aspects.length,0);e.innerHTML+=` \u21D2 ${n.length} sources, ${t} aspects`;let s=e.closest(".page").dataset.key,r={type:"batch",sources:n,pageKey:s};console.log({pageKey:s,doing:r}),(typeof window.soloListener>"u"||window.soloListener==null)&&(console.log("**** Adding solo listener"),window.soloListener=q,window.addEventListener("message",q)),await ee(750);let c=window.open("/plugins/solo/dialog/#","solo","popup,height=720,width=1280");c.location.pathname!="/plugins/solo/dialog/"?(console.log("launching new dialog"),c.addEventListener("load",l=>{console.log("launched and loaded"),c.postMessage(r,window.origin)})):(console.log("reusing existing dialog"),c.postMessage(r,window.origin))}var X={CLICK:{emit:me},HELLO:{emit:ke},FROM:{emit:be},SENSOR:{emit:we},REPORT:{emit:ye},SOURCE:{emit:xe},PREVIEW:{emit:$e},NEIGHBORS:{emit:Le},WALK:{emit:Se},TICK:{emit:Ee},UNTIL:{emit:Te},FORWARD:{emit:ve},TURN:{emit:Oe},FILE:{emit:Me},KWIC:{emit:Re},SHOW:{emit:je},RANDOM:{emit:Ie},SLEEP:{emit:Ae},TOGETHER:{emit:He},GET:{emit:Ne},DELTA:{emit:De},ROSTER:{emit:Ke},LINEUP:{emit:_e},LISTEN:{emit:We},MESSAGE:{emit:Ce},SOLO:{emit:Fe}};function qe(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function P(e,o,i){for(;e.length;){let t=e[0].match(/( *)(.*)/),s=t[1].length,r=t[2];if(s==i)o.push({command:r}),e.shift();else if(s>i){var n=[];o.push(n),P(e,n,s)}else return o}return o}function ne(e){let o=Math.floor(Math.random()*1e6),i=(n,t)=>{let s=[];for(let r of n){let c=`${o}.${t.join(".")}`;r.key=c,"command"in r?s.push(`<font color=gray size=small></font><span style="display: block;" id=${c}>${qe(r.command)}</span>`):s.push(`<div id=${c} style="padding-left:15px">${i(r,[...t,0])}</div>`),t[t.length-1]++}return s.join(`
23
+ `)};return i(e,[0])}var _=(e,o,i)=>i.indexOf(e)===o,ee=e=>new Promise(o=>setTimeout(o,e)),te=e=>e.replace(/\s/g,"-").replace(/[^A-Za-z0-9-]/g,"").toLowerCase();function lt(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Ge(e,o){let i=o.text.split(/\n/),n=P(i,[],0),t=ne(n),s=e.parents(".page"),r=s.data("key"),l={context:{item:o,itemId:o.id,pageKey:r,page:wiki.lineup.atKey(r).getRawPage(),origin:window.origin,site:s.data("site")||window.location.host,slug:s.attr("id"),title:s.data("data").title},api:Q};e.append(`<div style="background-color:#eee;padding:15px;border-top:8px;">${t}</div>`),M(n,l)}function Pe(e,o){return e.dblclick(()=>wiki.textEditor(e,o))}typeof window<"u"&&window!==null&&(window.plugins.mech={emit:Ge,bind:Pe});})();
24
+ //# sourceMappingURL=mech.js.map