wiki-plugin-mech 0.1.4 → 0.1.6

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.
Files changed (2) hide show
  1. package/client/mech.js +235 -6
  2. package/package.json +1 -1
package/client/mech.js CHANGED
@@ -1,6 +1,8 @@
1
1
 
2
2
  (function() {
3
3
 
4
+ const uniq = (value, index, self) => self.indexOf(value) === index
5
+
4
6
  function expand(text) {
5
7
  return text
6
8
  .replace(/&/g, '&')
@@ -115,12 +117,125 @@
115
117
  elem.innerHTML = command + `<br><font face=Arial size=32>${value}</font>`
116
118
  }
117
119
 
120
+ function source_emit ({elem,command,args,body,state}) {
121
+ if (!(args && args.length)) return trouble(elem,`Expected Source topic, like "markers" for Map markers.`)
122
+ const topic = args[0]
123
+ const sources = requestSourceData(state.$item, topic)
124
+ if(!sources.length) return trouble(elem,`Expected source for "${topic}" in the lineup.`)
125
+ const count = type => {
126
+ const count = sources
127
+ .filter(source => [...source.div.classList].includes(type))
128
+ .length
129
+ return count ? `${count} ${type}` : null}
130
+ const counts = [count('map'),count('image'),count('frame')]
131
+ .filter(count => count)
132
+ .join(", ")
133
+ if (state.debug) console.log({topic,sources})
134
+ elem.innerHTML = command + ' ⇒ ' + counts
135
+ state[topic] = sources
136
+ if (body) run(body,state)
137
+ }
138
+
139
+ function preview_emit ({elem,command,args,state}) {
140
+ const round = digits => (+digits).toFixed(7)
141
+ const story = []
142
+ for (const arg of args) {
143
+ switch (arg) {
144
+ case 'map':
145
+ if(!('marker' in state)) return trouble(elem,`"map" preview expects "marker" state, like from "SOURCE marker".`)
146
+ const text = state.marker
147
+ .map(marker => [marker.result])
148
+ .flat(2)
149
+ .map(latlon => `${round(latlon.lat)}, ${round(latlon.lon)} ${latlon.label||''}`)
150
+ .filter(uniq)
151
+ .join("\n")
152
+ story.push({type:'map',text})
153
+ break
154
+ case 'graph':
155
+ if(!('aspect' in state)) return trouble(elem,`"graph" preview expects "aspect" state, like from "SOURCE aspect".`)
156
+ for (const {div,result} of state.aspect) {
157
+ for (const {name,graph} of result) {
158
+ if(state.debug) console.log({div,result,name,graph})
159
+ story.push({type:'paragraph',text:name})
160
+ story.push({type:'graphviz',text:dotify(graph)})
161
+ }
162
+ story.push({type:'pagefold',text:'.'})
163
+ }
164
+ break
165
+ case 'synopsis':
166
+ {const text = `This page has been generated by the Mech plugin. We want to tell you where. That's coming soon.`
167
+ story.push({type:'paragraph',text})}
168
+ break
169
+ default:
170
+ return trouble(elem,`"${arg}" doesn't name an item we can preview`)
171
+ }
172
+ }
173
+ const title = "Mech Preview" + (state.tick ? ` ${state.tick}` : '')
174
+ const page = {title,story}
175
+ const options = {$page:$(elem.closest('.page'))}
176
+ wiki.showResult(wiki.newPage(page), options)
177
+ }
178
+
179
+ function neighbors_emit ({elem,command,args,state}) {
180
+ const want = args[0]
181
+ if(state.debug) console.log({neighborhoodObject:wiki.neighborhoodObject})
182
+ const have = Object.entries(wiki.neighborhoodObject.sites)
183
+ .filter(([domain,site]) => !site.sitemapRequestInflight && (!want || domain.includes(want)))
184
+ .map(([domain,site]) => (site.sitemap||[])
185
+ .map(info => Object.assign({domain},info)))
186
+ state.neighborhood = have.flat()
187
+ .sort((a,b) => b.date - a.date)
188
+ elem.innerHTML = command + ` ⇒ ${state.neighborhood.length} pages, ${have.length} sites`
189
+ }
190
+
191
+ function walk_emit ({elem,command,args,state}) {
192
+ const steps = Object.groupBy(walks(state.neighborhood),({graph})=>graph?'some':'none')
193
+ console.log({steps})
194
+ const nodes = steps.some.map(({graph}) => graph.nodes).flat()
195
+ elem.innerHTML = command + ` ⇒ ${steps.some.length} aspects, ${steps.none.length} empty, ${nodes.length} nodes`
196
+ const item = elem.closest('.item')
197
+ item.classList.add('aspect-source')
198
+ item.aspectData = () => steps.some
199
+ state.aspect = [{div:item,result:steps.some}]
200
+ }
201
+
202
+ function tick_emit ({elem,args,body,state}) {
203
+ if(elem.innerHTML.match(/button/)) return
204
+ if (!body?.length) return trouble(elem,'TICK expects indented blocks to follow.')
205
+ const count = args[0] || '1'
206
+ if (!count.match(/^[1-9][0-9]?$/)) return trouble(elem,"TICK expects a count from 1 to 99")
207
+ let clock = null
208
+ elem.innerHTML += '<button style="border-width:0;">◉</button>'
209
+ elem.querySelector('button').addEventListener('click',event => {
210
+ state.debug = event.shiftKey
211
+ if(clock){
212
+ clock = clearInterval(clock)
213
+ delete state.tick
214
+ } else {
215
+ state.tick = +count
216
+ run(body,state)
217
+ clock = setInterval(()=>{
218
+ if(state.debug) console.log({tick:state.tick})
219
+ if(--state.tick > 0)
220
+ run(body,state)
221
+ else
222
+ clock = clearInterval(clock)
223
+ },1000)
224
+ }
225
+ })
226
+ }
227
+
118
228
  const blocks = {
119
- CLICK: {emit:click_emit, bind:null},
120
- HELLO: {emit:hello_emit, bind:null},
121
- FROM: {emit:from_emit, bind:null},
122
- SENSOR: {emit:sensor_emit, bind:null},
123
- REPORT: {emit:report_emit, bind:null}
229
+ CLICK: {emit:click_emit},
230
+ HELLO: {emit:hello_emit},
231
+ FROM: {emit:from_emit},
232
+ SENSOR: {emit:sensor_emit},
233
+ REPORT: {emit:report_emit},
234
+ SOURCE: {emit:source_emit},
235
+ PREVIEW: {emit:preview_emit},
236
+ NEIGHBORS:{emit:neighbors_emit},
237
+ WALK: {emit:walk_emit},
238
+ TICK: {emit:tick_emit}
124
239
  }
125
240
 
126
241
  function run (nest,state={}) {
@@ -152,8 +267,9 @@
152
267
  const lines = item.text.split(/\n/)
153
268
  const nest = tree(lines,[],0)
154
269
  const html = format(nest)
270
+ const state = {$item} // deprecated. use elem.closest('.item')
155
271
  $item.append(`<div style="background-color:#eee;padding:15px;border-top:8px;">${html}</div>`)
156
- run(nest)
272
+ run(nest,state)
157
273
  }
158
274
 
159
275
  function bind($item, item) {
@@ -170,4 +286,117 @@
170
286
  module.exports = {expand}
171
287
  }
172
288
 
289
+
290
+ // library functions
291
+
292
+ // adapted from wiki-plugin-frame/client/frame.js
293
+ function requestSourceData($item, topic) {
294
+ let sources = []
295
+ for (let div of document.querySelectorAll(`.item`)) {
296
+ if (div.classList.contains(`${topic}-source`)) {
297
+ sources.unshift(div)
298
+ }
299
+ if (div === $item.get(0)) {
300
+ break
301
+ }
302
+ }
303
+
304
+ return sources.map(div => {
305
+ let getData = div[`${topic}Data`]
306
+ let result = getData ? getData() : null
307
+ return {div,result}
308
+ })
309
+ }
310
+
311
+ // adapted from super-collaborator/dotify.js
312
+ function dotify(graph) {
313
+ const tip = props => Object.entries(props).filter(e => e[1]).map(e => `${e[0]}: ${e[1]}`).join("\\n")
314
+ const nodes = graph.nodes.map((node,id) => {
315
+ const label = node.type ? `${node.type}\\n${node.props.name}` : node.props.name
316
+ return `${id} [label="${label}" ${(node.props.url||node.props.tick)?`URL="${node.props.url||'#'}" target="_blank"`:''} tooltip="${tip(node.props)}"]`
317
+ })
318
+ const edges = graph.rels.map(rel => {
319
+ return `${rel.from}->${rel.to} [label="${rel.type}" labeltooltip="${tip(rel.props)}"]`
320
+ })
321
+ return [
322
+ 'digraph {',
323
+ 'rankdir=LR',
324
+ 'node [shape=box style=filled fillcolor=palegreen]',
325
+ ...nodes,
326
+ ...edges,
327
+ '}'].join("\n")
328
+ }
329
+
330
+ // inspired by aspects-of-recent-changes/roster-graphs.html
331
+ function walks(neighborhood) {
332
+ const prob = n => Math.floor(n * Math.abs(Math.random()-Math.random()))
333
+ const rand = a => a[prob(a.length)]
334
+ const domains = neighborhood
335
+ .map(info => info.domain)
336
+ .filter(uniq)
337
+ return domains
338
+ .map(domain => {
339
+ const name = domain.split('.').slice(0,3).join('.')
340
+ const done = new Set()
341
+ const graph = new Graph()
342
+ let nid = 0
343
+ const here = neighborhood
344
+ .filter(info => info.domain==domain && ('links' in info))
345
+ if(!here.length) return {name,graph:null}
346
+ const find = slug => neighborhood.find(info => info.slug == slug)
347
+ const node = info => {
348
+ nid = graph.addNode('',{
349
+ name:info.title.replaceAll(/ /g,"\n"),
350
+ title:info.title,
351
+ site:domain,
352
+ links:Object.keys(info.links||{}).filter(slug => find(slug))})
353
+ return nid}
354
+ const rel = (here,there) => graph.addRel('',here,there)
355
+ const links = nid => graph.nodes[nid].props.links.filter(slug => !done.has(slug))
356
+ const start = rand(here)
357
+ done.add(start.slug)
358
+ node(start)
359
+ for (n=5;n>0;n--) {
360
+ try {
361
+ const slugs = links(nid)
362
+ const slug = rand(slugs)
363
+ done.add(slug)
364
+ const info = find(slug)
365
+ rel(nid,node(info))}
366
+ catch (e) {}
367
+ }
368
+ return {name,graph}
369
+ })
370
+ }
371
+
372
+ // adapted from graph/src/graph.js
373
+ class Graph {
374
+
375
+ constructor(nodes=[], rels=[]) {
376
+ this.nodes = nodes;
377
+ this.rels = rels;
378
+ }
379
+
380
+ addNode(type, props={}){
381
+ const obj = {type, in:[], out:[], props};
382
+ this.nodes.push(obj);
383
+ return this.nodes.length-1;
384
+ }
385
+
386
+ addRel(type, from, to, props={}) {
387
+ const obj = {type, from, to, props};
388
+ this.rels.push(obj);
389
+ const rid = this.rels.length-1;
390
+ this.nodes[from].out.push(rid)
391
+ this.nodes[to].in.push(rid);
392
+ return rid;
393
+ }
394
+
395
+ stringify(...args) {
396
+ const obj = { nodes: this.nodes, rels: this.rels }
397
+ return JSON.stringify(obj, ...args)
398
+ }
399
+
400
+ }
401
+
173
402
  }).call(this)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wiki-plugin-mech",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Federated Wiki - Mechanism Scripting Plugin",
5
5
  "keywords": [
6
6
  "mech",