wiki-plugin-pod 1.0.0 → 1.2.2
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/ReadMe.md +61 -36
- package/api/handlers.js +2 -2
- package/api/openapi.json +6 -18
- package/client/pod.js +9 -8
- package/client/pod.js.map +4 -4
- package/package.json +29 -4
- package/pages/about-pod-plugin +103 -5
- package/src/client/pod.js +132 -286
- package/src/pod/commands.js +49 -102
- package/src/pod/kinship.js +39 -0
- package/src/pod/roll.js +35 -66
- package/src/pod/vocabulary.js +20 -0
- package/server/package.json +0 -3
- package/server/server.js +0 -47
package/ReadMe.md
CHANGED
|
@@ -29,48 +29,57 @@ Example item text:
|
|
|
29
29
|
POD
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
### Mistyped commands
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
A misspelled command is not swallowed as data. `CHIKDREN` names nothing, so the
|
|
35
|
+
item would otherwise fall back to `SISTERS` and draw a pod nobody asked for.
|
|
36
|
+
Instead the panel says *CHIKDREN — did you mean CHILDREN?* above the gather. A
|
|
37
|
+
word that announces itself as a command (uppercase, the fedwiki convention) but
|
|
38
|
+
names none is always reported, with a suggestion where a real command is within
|
|
39
|
+
a typo or two. Ordinary lowercase prose stays data.
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
| `ROSTER` | draw the gather compactly, **mirroring a normal roster item** — a left-aligned "<commands> Rosters" title over flowing flags, instead of the wide table |
|
|
40
|
-
| `TWIN` | show only the members that hold a page with **this page's slug** |
|
|
41
|
-
| `WATCH` | show only the members whose copy of this page is *actually a fork* of it — i.e. who is watching (tracking) your page |
|
|
42
|
-
| `TITLE yes\|no` | whether the `ROSTER` title shows (default `yes`). `TITLE no` hides it, keeping even padding around the flags |
|
|
41
|
+
This is not pod's code: it is `@fortyfoxes/wiki-dsl`, so every plugin whose item
|
|
42
|
+
text is a small language gets the same report from the same lines.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
members' `img.remote` flags flow inline in the grey box, each linking to that
|
|
46
|
-
member's copy of the page, the (sub)domain shown on hover. `ROSTER` shows the
|
|
47
|
-
whole gather; `TWIN`/`WATCH` filter it:
|
|
44
|
+
## How it draws
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
merely shares the name but was authored independently is a twin, not a fork.
|
|
46
|
+
Every pod is drawn as a **roster**: a heading, then the members' flags. Pod does
|
|
47
|
+
not draw it — `wiki-plugin-roster` does, on every wiki there is, so pod builds
|
|
48
|
+
the roster text and hands it over. The text it builds is exactly what the ❄ icon
|
|
49
|
+
saves as a page, so the drawn thing and the saved thing are one string.
|
|
54
50
|
|
|
55
|
-
|
|
51
|
+
Each group's heading names the site the pod is relative to — *Sisters of
|
|
52
|
+
fedwiki.club*, *Children of ide.earth*, *The rest of the farm*, *Your
|
|
53
|
+
neighbourhood* — which is the one thing a reader landing cold cannot work out.
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
separate `FREEZE` command — the icon replaces it.)
|
|
55
|
+
| Command | Effect |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `TITLE no` | draw the flags with no headings |
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
## Retired commands
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
WATCH
|
|
67
|
-
|
|
61
|
+
`ROSTER`, `TWIN` and `WATCH` are kept in the table rather than deleted, because a
|
|
62
|
+
word removed from the table is a word the parser reports as a mistake — and 11
|
|
63
|
+
live items say `ROSTER`, 9 say `TWIN` or `WATCH`. They parse, gather nothing, and
|
|
64
|
+
say what became of them:
|
|
65
|
+
|
|
66
|
+
- **`ROSTER`** — the default now. Nothing to say.
|
|
67
|
+
- **`TWIN`** and **`WATCH`** — moved to
|
|
68
|
+
[wiki-plugin-twin](https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-plugin-twin),
|
|
69
|
+
which asks who holds *this page* rather than who is kin to *this site*.
|
|
68
70
|
|
|
69
|
-
|
|
71
|
+
## Saving a roster page
|
|
72
|
+
|
|
73
|
+
A subtle **❄ icon** in the corner saves the gathered pod as a **roster page**,
|
|
74
|
+
titled after the groups it holds (e.g. *Parent of ide.earth, Sisters of
|
|
75
|
+
ide.earth*). What it saves is the same roster text the item is drawing, so the
|
|
76
|
+
page is an ordinary roster item that any wiki can read with no pod installed.
|
|
77
|
+
(There is no `FREEZE` command — the icon replaces it.)
|
|
78
|
+
|
|
79
|
+
Example — the whole pod, flags only:
|
|
70
80
|
|
|
71
81
|
```
|
|
72
82
|
POD
|
|
73
|
-
ROSTER
|
|
74
83
|
TITLE no
|
|
75
84
|
```
|
|
76
85
|
|
|
@@ -99,8 +108,19 @@ a module of plain functions, and the Farm Plugin mounts them:
|
|
|
99
108
|
GET /system/api/pod/roll.json?kinds=children,sisters
|
|
100
109
|
|
|
101
110
|
The answer depends on the site it is asked at, so `origin` and `farmRoot` are
|
|
102
|
-
declared as context in the specification and supplied by the farm.
|
|
103
|
-
|
|
111
|
+
declared as context in the specification and supplied by the farm. Each group is
|
|
112
|
+
a list of site names.
|
|
113
|
+
|
|
114
|
+
**The browser reads that same mount.** There is no second route wrapping the
|
|
115
|
+
same handler for the client's benefit — the item fetches
|
|
116
|
+
`/system/api/pod/roll.json?kinds=…`, the address an agent uses. So this plugin
|
|
117
|
+
carries no server component, no express route and no CommonJS/ESM loader
|
|
118
|
+
hazard, and `wiki-plugin-farm` is declared in `fedwiki.requires`: without it the
|
|
119
|
+
disk-derived pods say so rather than failing quietly.
|
|
120
|
+
|
|
121
|
+
Kinship itself is arithmetic on names (`src/pod/kinship.js`), and the only thing
|
|
122
|
+
needing the disk is what sites exist — one directory read, no page counts, since
|
|
123
|
+
the client takes counts and freshness from each neighbour's own sitemap.
|
|
104
124
|
|
|
105
125
|
## One vocabulary
|
|
106
126
|
|
|
@@ -113,10 +133,15 @@ language is a superset of an interface:
|
|
|
113
133
|
|---|---|---|
|
|
114
134
|
| parameter | `SISTERS` `PARENT` `CHILDREN` `DESCENDANTS` `FARM` | yes — a value of `kinds` |
|
|
115
135
|
| macro | `POD` | via `PARENT` + `SISTERS` |
|
|
116
|
-
| presentation | `
|
|
117
|
-
| client-data | `NEIGHBOURHOOD` `SNAPSHOT`
|
|
136
|
+
| presentation | `TITLE` | no — a drawing choice |
|
|
137
|
+
| client-data | `NEIGHBOURHOOD` `SNAPSHOT` | no — only a browser holds it |
|
|
118
138
|
| alias | `NEIGHBORHOOD` | resolves to `NEIGHBOURHOOD` |
|
|
139
|
+
| retired | `ROSTER` `TWIN` `WATCH` | no — kept so pages that say them still parse |
|
|
119
140
|
|
|
120
141
|
The `kinds` parameter's allowed values are derived from that table rather than
|
|
121
|
-
written a second time
|
|
122
|
-
it
|
|
142
|
+
written a second time, and so are the group headings. The **parser** is not
|
|
143
|
+
written here either: it is [@fortyfoxes/wiki-dsl](https://github.com/Hitchhikers-Guide-to-the-Galaxy/wiki-dsl),
|
|
144
|
+
bundled at build time and shared with every other plugin whose item text is a
|
|
145
|
+
small language — including the mistyped-command report. `npm test` checks this
|
|
146
|
+
table; the library has its own tests; the farm checks the table against the
|
|
147
|
+
specification.
|
package/api/handlers.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* be told where it is being asked should fail loudly rather than guess.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { roll as
|
|
15
|
+
import { roll as gatherPods } from '../src/pod/roll.js'
|
|
16
16
|
|
|
17
17
|
export async function roll({ kinds = 'sisters', origin, farmRoot } = {}) {
|
|
18
18
|
if (!origin || !farmRoot) {
|
|
@@ -20,5 +20,5 @@ export async function roll({ kinds = 'sisters', origin, farmRoot } = {}) {
|
|
|
20
20
|
err.status = 500
|
|
21
21
|
throw err
|
|
22
22
|
}
|
|
23
|
-
return
|
|
23
|
+
return gatherPods({ kinds, origin, farmRoot })
|
|
24
24
|
}
|
package/api/openapi.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Pod",
|
|
5
|
-
"version": "
|
|
6
|
-
"description": "Report which sites of this farm stand as pod to the site being asked \u2014 children, sisters, parent, descendants
|
|
5
|
+
"version": "1.1.0",
|
|
6
|
+
"description": "Report which sites of this farm stand as pod to the site being asked \u2014 children, sisters, parent, descendants. Kinship is read from the name, so the answer depends on where the question is put."
|
|
7
7
|
},
|
|
8
8
|
"paths": {
|
|
9
9
|
"/roll.json": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
],
|
|
39
39
|
"responses": {
|
|
40
40
|
"200": {
|
|
41
|
-
"description": "One group per requested kind, each sorted by
|
|
41
|
+
"description": "One group per requested kind, each a list of site names sorted by name.",
|
|
42
42
|
"content": {
|
|
43
43
|
"application/json": {
|
|
44
44
|
"schema": {
|
|
@@ -67,28 +67,16 @@
|
|
|
67
67
|
},
|
|
68
68
|
"groups": {
|
|
69
69
|
"type": "object",
|
|
70
|
-
"description": "One key per requested and recognised kind.",
|
|
70
|
+
"description": "One key per requested and recognised kind; each value is that pod\u2019s site names, sorted.",
|
|
71
71
|
"additionalProperties": {
|
|
72
72
|
"type": "array",
|
|
73
73
|
"items": {
|
|
74
|
-
"
|
|
74
|
+
"type": "string",
|
|
75
|
+
"description": "A site's domain name."
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
|
-
},
|
|
80
|
-
"Member": {
|
|
81
|
-
"type": "object",
|
|
82
|
-
"properties": {
|
|
83
|
-
"site": {
|
|
84
|
-
"type": "string",
|
|
85
|
-
"description": "The site's domain name."
|
|
86
|
-
},
|
|
87
|
-
"pages": {
|
|
88
|
-
"type": "integer",
|
|
89
|
-
"description": "How many pages it holds."
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
80
|
}
|
|
93
81
|
}
|
|
94
82
|
}
|
package/client/pod.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
(()=>{var
|
|
2
|
-
`))
|
|
3
|
-
`).some(
|
|
4
|
-
`)){let
|
|
5
|
-
`)
|
|
6
|
-
`)},T=()=>{if(m){let n=g();t.find(".groups").html(n.join(" ")),t.find(".caption").first().text(n.length?"":d?"no pod watchers yet":"no pod twins yet");return}if(f){let n=K?`${k} <br> `:"";t.find(".groups").html(n+b());return}t.find(".groups").html(S()),t.find(".caption").first().text("just updated")};T();let Y=()=>{let n=()=>Math.floor(Math.random()*0xffffffffffff).toString(16).padStart(12,"0"),a=[{type:"markdown",id:n(),text:`# ${k}`}];for(let i of r){let u=y[i]||[];a.push({type:"roster",id:n(),text:`${B[i]} Wikis
|
|
1
|
+
(()=>{var L=["no","false","off","0","hide","none"],F=/^[A-Z][A-Z]+$/,D=(t,o)=>{let a=Array.from({length:o.length+1},(c,p)=>p);for(let c=1;c<=t.length;c++){let p=[c];for(let d=1;d<=o.length;d++)p[d]=Math.min(a[d]+1,p[d-1]+1,a[d-1]+(t[c-1]===o[d-1]?0:1));a=p}return a[o.length]},g=t=>String(t||"").trim().split(/\s+/)[0].replace(/:$/,"").toUpperCase(),S=(t,{fallback:o=[]}={})=>{let a=s=>{let e=t[s];return e&&e.kind==="alias"?t[e.alias]:e},c=s=>{let e=t[s];return e&&e.kind==="alias"?e.alias:s},p=s=>Object.values(t).filter(e=>e.kind==="parameter"&&e.parameter===s&&e.value).map(e=>e.value),d=s=>{let e={};for(let n of Object.values(t))n.value&&n[s]&&(e[n.value]=n[s]);return e},m=(s,{fallback:e=o}={})=>{let n=[],r=i=>{let l=a(i);if(l){if(l.kind==="macro")return l.expands.forEach(r);l.value&&(n.includes(l.value)||n.push(l.value))}};for(let i of String(s||"").split(`
|
|
2
|
+
`))i.trim()&&r(g(i));return n.length?n:[...e]},w=(s,e)=>String(s||"").split(`
|
|
3
|
+
`).some(n=>c(g(n))===e),u=(s,e)=>{for(let n of String(s||"").split(`
|
|
4
|
+
`)){if(c(g(n))!==e)continue;let r=(n.trim().split(/\s+/)[1]||"").toLowerCase().replace(/:$/,"");return!L.includes(r)}return t[e]?.default},f=s=>{let e=String(s||"").trim().replace(/:$/,"").toUpperCase();if(!e)return null;let n=null,r=1/0;for(let i of Object.keys(t)){let l=D(e,i);l<r&&(r=l,n=i)}return r<=(e.length<=4?1:2)?c(n):null};return{resolve:a,canonical:c,valuesFor:p,fieldByValue:d,parseKinds:m,hasCommand:w,argumentOf:u,suggest:f,problems:s=>{let e=[];for(let n of String(s||"").split(`
|
|
5
|
+
`)){let r=n.trim();if(!r||a(g(r)))continue;let i=r.split(/\s+/)[0].replace(/:$/,""),l=f(i),h=r.split(/\s+/).length===1;!F.test(i)&&!(h&&l)||e.push({word:i,suggestion:l})}return e}}};var R=["sisters"],k={SISTERS:{kind:"parameter",parameter:"kinds",value:"sisters",label:"Sisters",heading:"Sisters of {site}",description:"sibling sites sharing the parent domain"},PARENT:{kind:"parameter",parameter:"kinds",value:"parent",label:"Parent",heading:"Parent of {site}",description:"the parent-domain site itself"},CHILDREN:{kind:"parameter",parameter:"kinds",value:"children",label:"Children",heading:"Children of {site}",description:"direct sub-domains of this site"},DESCENDANTS:{kind:"parameter",parameter:"kinds",value:"descendants",label:"Descendants",heading:"Descendants of {site}",description:"all sub-domains, any depth"},FARM:{kind:"parameter",parameter:"kinds",value:"farm",label:"Farm",heading:"The rest of the farm",description:"every wiki in the farm"},POD:{kind:"macro",expands:["PARENT","SISTERS"],description:"PARENT and SISTERS together (shorthand)"},NEIGHBOURHOOD:{kind:"client-data",value:"neighbourhood",label:"Neighbourhood",heading:"Your neighbourhood",description:"the sites currently in your neighborhood",why:"reads the neighbourhood the browser has already assembled, which no server can see"},NEIGHBORHOOD:{kind:"alias",alias:"NEIGHBOURHOOD",description:"American spelling"},SNAPSHOT:{kind:"client-data",value:"snapshot",label:"Snapshot",heading:"Your neighbourhood",description:"freeze the current neighborhood",why:"same browser-only source as NEIGHBOURHOOD"},FORCE:{kind:"presentation",argument:"yes|no",default:!1,description:"register the gathered sites as neighbours when the page loads"},TITLE:{kind:"presentation",argument:"yes|no",default:!0,description:"whether each group shows its heading"},ROSTER:{kind:"presentation",retired:"the default \u2014 every pod is drawn as a roster of flags",description:"no longer needed: rosters are how a pod is drawn"},TWIN:{kind:"presentation",retired:"wiki-plugin-twin",description:"moved: which pod members hold a page with this page\u2019s slug"},WATCH:{kind:"presentation",retired:"wiki-plugin-twin",description:"moved: which pod members have forked this page"}};var{parseKinds:T,hasCommand:I,argumentOf:x,problems:A,valuesFor:H,fieldByValue:C,canonical:M,resolve:Q,suggest:X}=S(k,{fallback:R}),N=t=>k[t]?.retired;var y=H("kinds"),j=C("heading"),E=t=>(t||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"),K=t=>t.length?'<p class=pod-problem style="color:#a00;font-size:80%;text-align:center;margin:0 0 6px">'+t.map(o=>o.suggestion?`${E(o.word)} \u2014 did you mean <b>${o.suggestion}</b>?`:`${E(o.word)} \u2014 not a pod command`).join("<br>")+"</p>":"",U=t=>{let o=["TWIN","WATCH","ROSTER"].filter(a=>I(t,a)).map(a=>[a,N(a)]);return o.length?'<p class=pod-retired style="color:#666;font-size:80%;text-align:center;margin:0 0 6px">'+o.map(([a,c])=>`${a} \u2014 now ${E(c)}`).join("<br>")+"</p>":""},V=()=>[80,"80","",null].includes(location.port)?"":`:${location.port}`,Y=(t,o)=>{let a=V(),c=T(o.text),p=x(o.text,"TITLE"),d=x(o.text,"FORCE"),m=K(A(o.text))+U(o.text);if(t.closest(".page").hasClass("remote")){t.html(`<div class=pod style="background-color:#eee;padding:15px"><center><p class=caption>Pod is only available when viewed on a page's home wiki.</p></div>`);return}t.html(`${m}<div class=pod-gather><p class=caption>gathering\u2026</p></div>`);let w=(e,n)=>c.map(r=>{let i=e[r]||[];return i.length?(p?(j[r]||r).replace("{site}",n)+`
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
`)
|
|
7
|
+
`:"")+i.join(`
|
|
8
|
+
`):""}).filter(Boolean).join(`
|
|
9
|
+
|
|
10
|
+
`),u=(e,n)=>{if(d){for(let l of c)if(y.includes(l))for(let h of e[l]||[])wiki.neighborhoodObject.registerNeighbor(h+a)}let r=w(e,n),i=t.find(".pod-gather");if(i.empty(),!r)return i.html("<p class=caption><i>none</i></p>");wiki.getPlugin("roster",l=>{i.empty();let h={type:"roster",id:`${o.id}-roster`,text:r};l.emit(i,h),l.bind(i,h),i.off("dblclick");let v=$('<span class=pod-freeze-icon title="Freeze \u2014 save this gather as a roster page" style="position:absolute;top:6px;right:9px;cursor:pointer;font-size:14px;opacity:.55">\u2744</span>');v.on("click",()=>{let P=c.map(O=>(j[O]||O).replace("{site}",n)).join(", "),B=wiki.newPage({title:P,story:[{type:"roster",id:Math.floor(Math.random()*0xffffffffffff).toString(16).padStart(12,"0"),text:r}]});wiki.showResult(B,{$page:t.parents(".page")})}),t.css("position","relative").append(v)})},f=()=>Object.keys(wiki.neighborhood).map(e=>a&&e.endsWith(a)?e.slice(0,-a.length):e),b=()=>{let e={};for(let n of c)y.includes(n)||(e[n]=f());return e},s=c.filter(e=>y.includes(e));if(s.length===0){u(b(),location.hostname);return}fetch(`/system/api/pod/roll.json?kinds=${encodeURIComponent(s.join(","))}`).then(e=>{if(!e.ok){let n=new Error(`HTTP ${e.status}`);throw n.status=e.status,n}return e.json()}).then(e=>u({...e.groups||{},...b()},e.origin||location.hostname)).catch(e=>{let n=e&&e.status===404?"pod needs wiki-plugin-farm on this wiki to mount its API \u2014 install it and restart":"server error";t.find(".caption").first().text(n)})},_=(t,o)=>{t.dblclick(()=>wiki.textEditor(t,o))};typeof window<"u"&&(window.plugins=window.plugins||{},window.plugins.pod={emit:Y,bind:_});})();
|
|
10
11
|
//# sourceMappingURL=pod.js.map
|
package/client/pod.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/pod/commands.js", "../src/client/pod.js"],
|
|
4
|
-
"sourcesContent": ["// commands.js \u2014 the plugin's vocabulary, in one place.\n//\n// This module is the single source for what a pod item's text may say. The\n// client imports it to parse item text; the plugin's API declaration names it so\n// the farm reads the identical table. There is no second copy to drift, which is\n// the same move that put the plugin's thinking in a module and left only the\n// serving in its server.\n//\n// The item text convention is fedwiki-wide: UPPERCASE first word is a command,\n// lowercase is data, and a single trailing colon is optional. See the Fedwiki\n// DSL Skill.\n//\n// A DSL is a superset of an API, never a mirror of one \u2014 so every command\n// declares which KIND it is, and only some kinds cross to HTTP:\n//\n// parameter supplies an argument to the call \u2192 crosses\n// macro shorthand expanding to other commands \u2192 expands, then crosses\n// client-data the server cannot supply it at all \u2192 never crosses\n// presentation changes only how it is drawn \u2192 never crosses\n// alias another spelling of a command \u2192 resolves first\n//\n// Adding a command here is the whole change: the parser, the API's allowed\n// values, the rendered reference page and the plugin's own documentation all\n// read this table.\n\nexport const VOCABULARY_VERSION = 1\n\n/** The story item type these commands are written into. An agent needs this to author one. */\nexport const ITEM_TYPE = 'pod'\n\n/** What the item does when no command is recognised. */\nexport const FALLBACK = ['sisters']\n\nexport const COMMANDS = {\n // ---- parameter: these become values of the `kinds` parameter -------------\n SISTERS: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'sisters',\n label: 'Sisters',\n description: 'sibling sites sharing the parent domain',\n },\n PARENT: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'parent',\n label: 'Parent',\n description: 'the parent-domain site itself',\n },\n CHILDREN: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'children',\n label: 'Children',\n description: 'direct sub-domains of this site',\n },\n DESCENDANTS: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'descendants',\n label: 'Descendants',\n description: 'all sub-domains, any depth',\n },\n FARM: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'farm',\n label: 'Farm',\n description: 'every wiki in the farm',\n },\n\n // ---- macro ---------------------------------------------------------------\n POD: {\n kind: 'macro',\n expands: ['PARENT', 'SISTERS'],\n description: 'PARENT and SISTERS together (shorthand)',\n },\n\n // ---- client-data: gathered in the browser, invisible to any server -------\n NEIGHBOURHOOD: {\n kind: 'client-data',\n value: 'neighbourhood',\n label: 'Neighbourhood',\n description: 'the sites currently in your neighborhood',\n why: 'reads the neighbourhood the browser has already assembled, which no server can see',\n },\n NEIGHBORHOOD: { kind: 'alias', alias: 'NEIGHBOURHOOD', description: 'American spelling' },\n SNAPSHOT: {\n kind: 'client-data',\n value: 'snapshot',\n label: 'Snapshot',\n description: 'freeze the current neighborhood',\n why: 'same browser-only source as NEIGHBOURHOOD',\n },\n TWIN: {\n kind: 'client-data',\n description: \"show only members holding a page with this page's slug\",\n why: \"matches against each neighbour's own sitemap, fetched from the browser\",\n },\n WATCH: {\n kind: 'client-data',\n description: 'stricter TWIN \u2014 only members whose copy is a fork of this page',\n why: \"reads each candidate's page journal, one fetch per neighbour, from the browser\",\n },\n\n // ---- presentation: never leaves the client -------------------------------\n ROSTER: {\n kind: 'presentation',\n description: 'draw the gather compactly, mirroring a normal roster item',\n },\n TITLE: {\n kind: 'presentation',\n argument: 'yes|no',\n default: true,\n description: 'whether the ROSTER title shows',\n },\n}\n\n/** Values a trailing TITLE argument may take to mean \"no\". */\nconst FALSEY = ['no', 'false', 'off', '0', 'hide', 'none']\n\n/** Follow an alias to the command it spells. */\nexport const resolve = name => {\n const entry = COMMANDS[name]\n return entry && entry.kind === 'alias' ? COMMANDS[entry.alias] : entry\n}\n\n/** The canonical name a command resolves to, for reporting. */\nexport const canonical = name => {\n const entry = COMMANDS[name]\n return entry && entry.kind === 'alias' ? entry.alias : name\n}\n\n/**\n * The keyword a line begins with, normalised. UPPERCASE is canonical but input\n * case is forgiven, and one trailing colon is optional \u2014 never required.\n */\nexport const keywordOf = line =>\n String(line || '')\n .trim()\n .split(/\\s+/)[0]\n .replace(/:$/, '')\n .toUpperCase()\n\n/** Every value a parameter may take, in declaration order. The API's enum. */\nexport const valuesFor = parameter =>\n Object.values(COMMANDS)\n .filter(c => c.kind === 'parameter' && c.parameter === parameter && c.value)\n .map(c => c.value)\n\n/** Kinds gathered in the browser rather than asked of a server. */\nexport const clientValues = () =>\n Object.values(COMMANDS)\n .filter(c => c.kind === 'client-data' && c.value)\n .map(c => c.value)\n\n/** Display names, derived so a label is written once beside its command. */\nexport const labels = () => {\n const out = {}\n for (const c of Object.values(COMMANDS)) if (c.value && c.label) out[c.value] = c.label\n return out\n}\n\n/**\n * Item text to an ordered, de-duplicated list of gathered kinds.\n *\n * Macros expand in place, aliases resolve, unrecognised lines are ignored as\n * data rather than treated as an error. Default when nothing is recognised:\n * sisters.\n */\nexport const parseKinds = (text, { fallback = ['sisters'] } = {}) => {\n const kinds = []\n const take = name => {\n const entry = resolve(name)\n if (!entry) return\n if (entry.kind === 'macro') return entry.expands.forEach(take)\n if (!entry.value) return\n if (!kinds.includes(entry.value)) kinds.push(entry.value)\n }\n for (const line of String(text || '').split('\\n')) {\n if (!line.trim()) continue\n take(keywordOf(line))\n }\n return kinds.length ? kinds : [...fallback]\n}\n\n/** Whether a bare command appears on a line of its own. */\nexport const hasCommand = (text, name) =>\n String(text || '')\n .split('\\n')\n .some(line => canonical(keywordOf(line)) === name)\n\n/** TITLE yes|no \u2014 shown unless explicitly turned off. */\nexport const parseTitle = text => {\n for (const raw of String(text || '').split('\\n')) {\n const parts = raw.trim().split(/\\s+/)\n if (canonical(keywordOf(raw)) !== 'TITLE') continue\n return !FALSEY.includes((parts[1] || '').toLowerCase().replace(/:$/, ''))\n }\n return COMMANDS.TITLE.default\n}\n", "// wiki-plugin-pod \u2014 gather configurable pods of related wiki sites.\n//\n// Augments the Present plugin: instead of only listing sister sites, the item\n// text is a list of keywords choosing WHICH pods of sites to gather and\n// show. Each gathered site is registered as a neighbor so its pages join your\n// search and lineage. Reads the in-browser neighbourhood for the client-side\n// pods, and a small server route for the disk-derived ones.\n//\n// Recognised keywords (one per line, in item.text):\n// sisters sibling sites sharing the parent domain (server)\n// parent the parent-domain site itself (server)\n// children direct sub-domains of this site (server)\n// descendants all sub-domains, any depth (server)\n// farm every wiki in the farm (server)\n// neighbourhood the sites currently in your neighborhood (client)\n// snapshot freeze the current neighborhood (client, v0.1 == neighbourhood)\n//\n// Actions (not site-kinds):\n// ROSTER draw the gathered pod compactly, mirroring a normal roster\n// item \u2014 a left-aligned \"<commands> Rosters\" title over flowing\n// `img.remote` flags in the grey box, instead of the wide\n// tables. A \u2744 corner icon saves it as a roster ghost page.\n// TITLE yes|no whether the ROSTER title shows (default yes). TITLE no hides\n// it, leaving even padding around the flags.\n// TWIN show a roster of the pod members that hold a page with\n// THIS page's slug \u2014 existence-only, exactly like the\n// wiki-client Twins strip: a slug match in the neighbour's\n// sitemap, no journal or lineage check.\n// WATCH stricter TWIN: show only the members whose copy is actually a\n// fork of this page \u2014 its journal carries a `fork` event \u2014 i.e.\n// who is watching (tracking) your page. Fetches each candidate.\n//\n// TWIN and WATCH render like a roster item (flowing flags, the (sub)domain in the\n// hover tooltip); each flag links to that member's copy of the page. ROSTER is\n// the same compact style for the whole (unfiltered) gather. The wider table (no\n// display command) carries a small \u2744 icon that saves the roster page too, so\n// there is no separate FREEZE command.\n\n// The vocabulary is not defined here. It lives in ../pod/commands.js, which\n// the plugin's API declaration also names \u2014 so what an author may type and what\n// the mounted operation accepts come from one table and cannot drift apart.\nimport { valuesFor, clientValues, labels, parseKinds, hasCommand, parseTitle } from '../pod/commands.js'\n\nconst SERVER_KINDS = valuesFor('kinds')\nconst CLIENT_KINDS = clientValues()\nconst LABEL = labels()\n\nconst expand = text =>\n (text || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\\*(.+?)\\*/g, '<i>$1</i>')\n\nconst elapsed = ms => {\n const s = Math.floor(ms / 1000)\n const f = 1.9\n let out = 'a minute ago'\n if (s > f * 60) out = `${Math.round(s / 60)} minutes ago`\n if (s > f * 3600) out = `${Math.round(s / 3600)} hours ago`\n if (s > f * 86400) out = `${Math.round(s / 86400)} days ago`\n if (s > f * 604800) out = `${Math.round(s / 604800)} weeks ago`\n if (s > f * 2592000) out = `${Math.round(s / 2592000)} months ago`\n if (s > f * 31536000) out = `${Math.round(s / 31536000)} years ago`\n return out\n}\n\nconst freshness = sitemap => {\n if (!sitemap) return ''\n const dates = sitemap.map(p => p.date).filter(d => typeof d === 'number')\n return dates.length ? elapsed(Date.now() - Math.max(...dates)) : ''\n}\n\nconst portSuffix = () => ([80, '80', '', null].includes(location.port) ? '' : `:${location.port}`)\n\n// parseKinds, hasCommand and parseTitle come from the shared vocabulary above.\n// ROSTER, TWIN and WATCH are bare keywords on their own line, so they never\n// pollute the gathered kinds: ROSTER draws the whole gather compactly (with a\n// button to save it as a roster page); TWIN and WATCH switch the panel to a\n// roster of the pod members that hold this page (TWIN by slug, WATCH by an\n// actual fork event in the copy's journal).\nconst hasAction = hasCommand\n\n// Display name for a site row. Descendants of the viewing site keep their\n// whole relative name (david.pod.peoplepowered.money seen from\n// peoplepowered.money \u2192 \"david.pod\"); everything else shows its first label.\nconst shortName = site =>\n site.endsWith('.' + location.hostname)\n ? site.slice(0, -(location.hostname.length + 1))\n : site.split('.')[0]\n\nconst rowHtml = (site, pages, sitemap) => {\n const suffix = portSuffix()\n const short = shortName(site)\n const img =\n `<img class=remote title=\"${site}${suffix}\" src=\"//${site}${suffix}/favicon.png\" ` +\n `data-site=\"${site}${suffix}\" data-slug=welcome-visitors>`\n return (\n `<tr><td align=right>${short} ${img}` +\n `<td data-site=\"${site}${suffix}\">${pages} pages ${freshness(sitemap)}`\n )\n}\n\nconst groupHtml = (kind, rows) =>\n `<p class=caption><b>${LABEL[kind]}</b></p>` +\n `<table width=100% class=pod-${kind}>${rows.join('\\n')}</table>`\n\nexport const emit = (div, item) => {\n const suffix = portSuffix()\n const kinds = parseKinds(item.text)\n const watch = hasAction(item.text, 'WATCH')\n const twin = hasAction(item.text, 'TWIN')\n const roster = watch || twin // both render as a (filtered) roster of member flags\n const rosterCmd = hasAction(item.text, 'ROSTER') // compact display of the whole gather\n const showTitle = parseTitle(item.text) // TITLE yes/no \u2014 show the ROSTER title\n // Title for the ROSTER caption and the saved roster page \u2014 the commands\n // present, e.g. \"SISTERS Rosters\" / \"PARENT SISTERS Rosters\".\n const rostersTitle = `${kinds.map(k => k.toUpperCase()).join(' ')} Rosters`\n // slug of the page hosting this item \u2014 what TWIN/WATCH match against, the way\n // Twins matches the viewing page's slug across the neighbourhood. The page\n // div's id IS the slug (possibly with a _rev\u2026 suffix on a historical view);\n // .data('key') is the lineup key, NOT the slug, so don't use it here.\n const slug = (div.closest('.page').attr('id') || '').split('_rev')[0]\n // gathered pod, captured during render so saveRosters() can reuse it:\n // kind -> ordered list of full domain names\n const gathered = {}\n\n // TWIN test \u2014 existence-only: does this neighbour's already-loaded sitemap hold\n // a page with our slug? (Twins' inner test \u2014 no journal, no lineage.)\n const hasTwin = key => {\n const sm = wiki.neighborhood[key]?.sitemap\n return Array.isArray(sm) && sm.some(p => p.slug === slug)\n }\n\n // WATCH test \u2014 is this member watching us: their copy of this page carries a\n // `fork` event in its journal (fedwiki's term for a copy taken from ours).\n // Needs the page JSON, so results are fetched lazily and cached (true/false\n // once resolved, `in` even while the request is in flight). We only fetch where\n // the sitemap already shows the slug, so it costs one request per actual twin.\n const watchStatus = {}\n const ensureWatchChecked = (key, onDone) => {\n if (key in watchStatus || !hasTwin(key)) return\n watchStatus[key] = undefined // in-flight marker\n wiki.site(key).get(`${slug}.json`, (err, page) => {\n watchStatus[key] =\n !err && Array.isArray(page?.journal) && page.journal.some(a => a?.type === 'fork')\n onDone()\n })\n }\n const matches = key => (watch ? watchStatus[key] === true : hasTwin(key))\n\n if (div.closest('.page').hasClass('remote')) {\n div.html(\n `<div class=pod style=\"background-color:#eee;padding:15px\"><center>${expand(item.text)}` +\n `<p class=caption>Pod is only available when viewed on a page's home wiki.</p></div>`,\n )\n return\n }\n\n // The flag-display modes (ROSTER / TWIN / WATCH) carry their own caption, so\n // skip echoing the raw command text; the table keeps its echo. ROSTER also\n // drops the centred `.caption` line entirely \u2014 it renders its own left-aligned\n // title inside `.groups`, mirroring a roster item, so the box padding stays\n // even (important when TITLE is off).\n const echo = roster || rosterCmd ? '' : `<center>${expand(item.text)}`\n const status = rosterCmd ? '' : `<p class=caption>gathering\u2026</p>`\n div.html(\n `<div class=pod style=\"position:relative;background-color:#eee;padding:15px\">${echo}` +\n `${status}<div class=groups>${rosterCmd ? '<i>gathering\u2026</i>' : ''}</div></div>`,\n )\n\n const render = serverGroups => {\n // Candidate pod members per kind, as neighbourhood lookup keys (with the\n // port suffix). Server kinds are registered once here so their sitemaps load;\n // TWIN/WATCH later filter these candidates down to the matching members.\n const candidates = {}\n for (const kind of kinds) {\n if (SERVER_KINDS.includes(kind)) {\n const roll = serverGroups[kind] || []\n gathered[kind] = roll.map(r => r.site)\n candidates[kind] = roll.map(r => r.site + suffix)\n for (const key of candidates[kind]) wiki.neighborhoodObject.registerNeighbor(key)\n } else {\n // neighbourhood / snapshot \u2014 read the live in-browser neighbourhood\n const sites = Object.keys(wiki.neighborhood)\n gathered[kind] = sites\n candidates[kind] = sites\n }\n }\n\n // TWIN / WATCH render like a roster item: the matching members' flags flowing\n // inline, deduplicated across kinds, each linking to that member's copy of\n // THIS page. No titles are shown (every copy is the same page) \u2014 the\n // (sub)domain lives in the hover tooltip. Sitemaps (and, for WATCH, the\n // fetched page journals) arrive asynchronously, so this is re-run as they\n // resolve, which is when matches reveal themselves.\n const buildRoster = () => {\n const seen = new Set()\n const flags = []\n for (const kind of kinds) {\n for (const key of candidates[kind]) {\n if (watch) ensureWatchChecked(key, paint) // lazily confirm the fork event\n if (seen.has(key) || !matches(key)) continue\n seen.add(key)\n flags.push(\n `<img class=\"remote\" src=\"${wiki.site(key).flag()}\" ` +\n `title=\"${key}\" data-site=\"${key}\" data-slug=\"${slug}\">`,\n )\n }\n }\n return flags\n }\n\n // ROSTER \u2014 the whole gather as one compact roster: a single \"<commands>\n // Rosters\" caption (the panel's own .caption) over all members' flags. The\n // markup mirrors the wiki-plugin-roster item exactly: space-separated\n // `img.remote` flags in the panel's grey box \u2014 no extra wrapper \u2014 each\n // linking to that member's welcome page. The \u2744 icon saves it as a page.\n const buildRosterDisplay = () => {\n const seen = new Set()\n const flags = []\n for (const kind of kinds) {\n for (const key of candidates[kind]) {\n if (seen.has(key)) continue\n seen.add(key)\n flags.push(\n `<img class=\"remote\" src=\"${wiki.site(key).flag()}\" ` +\n `title=\"${key}\" data-site=\"${key}\" data-slug=\"welcome-visitors\">`,\n )\n }\n }\n return flags.join(' ') || '<i>none</i>'\n }\n\n // Non-roster: the full pod tables (favicon + name + page count + freshness).\n const buildGroups = () => {\n const html = []\n for (const kind of kinds) {\n const rows = candidates[kind].map(key => {\n const bare = suffix && key.endsWith(suffix) ? key.slice(0, -suffix.length) : key\n const sm = wiki.neighborhood[key]?.sitemap\n return rowHtml(bare, sm ? sm.length : 0, sm)\n })\n const empty = SERVER_KINDS.includes(kind) ? '<tr><td><i>none</i>' : '<tr><td><i>empty</i>'\n html.push(groupHtml(kind, rows.length ? rows : [empty]))\n }\n return html.join('\\n')\n }\n\n const paint = () => {\n if (roster) {\n // TWIN / WATCH \u2014 flat, filtered flags\n const flags = buildRoster()\n div.find('.groups').html(flags.join(' '))\n div.find('.caption').first().text(\n flags.length ? '' : watch ? 'no pod watchers yet' : 'no pod twins yet',\n )\n return\n }\n if (rosterCmd) {\n // Mirror a roster item: an optional left-aligned title, then a <br>, then\n // the flags \u2014 all in the panel's grey box. TITLE off \u2192 flags only, even\n // padding all round.\n const title = showTitle ? `${rostersTitle} <br> ` : ''\n div.find('.groups').html(title + buildRosterDisplay())\n return\n }\n div.find('.groups').html(buildGroups())\n div.find('.caption').first().text('just updated')\n }\n paint()\n\n // Save the gathered pod as a roster ghost page titled after the commands\n // (e.g. \"SISTERS Rosters\"), one roster item per kind. No FREEZE command any\n // more: it's reached from the \u2744 icon tucked into the corner of the ROSTER\n // and wide-table views.\n const saveRosters = () => {\n const hexId = () =>\n Math.floor(Math.random() * 0xffffffffffff).toString(16).padStart(12, '0')\n const story = [{ type: 'markdown', id: hexId(), text: `# ${rostersTitle}` }]\n for (const kind of kinds) {\n const domains = gathered[kind] || []\n story.push({\n type: 'roster',\n id: hexId(),\n text: `${LABEL[kind]} Wikis\\n\\n${domains.join('\\n') || '(none)'}`,\n })\n }\n const page = wiki.newPage({ title: rostersTitle, story })\n wiki.showResult(page, { $page: div.parents('.page') })\n }\n\n // Freeze affordance \u2014 a subtle \u2744 icon in the panel corner, on the ROSTER and\n // the wide-table views (TWIN / WATCH are monitoring views, so they get none).\n if (rosterCmd || !roster) {\n const icon = $(\n '<span class=pod-freeze-icon title=\"Display Rosters \u2014 save as a roster page\" ' +\n 'style=\"position:absolute;top:6px;right:9px;cursor:pointer;font-size:14px;opacity:.55\">\u2744</span>',\n )\n icon.on('click', saveRosters)\n div.find('.pod').append(icon)\n }\n\n // As each neighbour's sitemap loads: in TWIN/WATCH mode repaint, since a match\n // may have just appeared (a member whose sitemap now shows our slug \u2014 and for\n // WATCH that also kicks off the journal fetch); otherwise just backfill that\n // member's page-count / freshness cell.\n $('body').on('new-neighbor-done', (e, site) => {\n if (roster) return paint()\n const cell = div.find(`td[data-site=\"${site}\"]`)\n if (!cell.length) return\n const sm = wiki.neighborhood[site]?.sitemap\n if (sm) cell.text(`${sm.length} pages ${freshness(sm)}`)\n })\n }\n\n const serverKinds = kinds.filter(k => SERVER_KINDS.includes(k))\n if (serverKinds.length === 0) {\n render({})\n return\n }\n\n fetch(`/plugin/pod/roll?kinds=${encodeURIComponent(serverKinds.join(','))}`)\n .then(res => {\n if (!res.ok) {\n const err = new Error(`HTTP ${res.status}`)\n err.status = res.status\n throw err\n }\n return res.json()\n })\n .then(data => render(data.groups || {}))\n .catch(err => {\n // A 404 means the plugin's server component never registered its route \u2014\n // almost always an old wiki-server / Node combination that couldn't load\n // server/server.js (see this plugin's server notes). Say so, rather than a\n // generic error, so the operator knows where to look.\n const msg =\n err && err.status === 404\n ? 'pod server not loaded \u2014 the host wiki may need a restart or a newer wiki-server / Node'\n : 'server error'\n div.find('.caption').first().text(msg)\n })\n}\n\nexport const bind = (div, item) => {\n div.dblclick(() => wiki.textEditor(div, item))\n}\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.pod = { emit, bind }\n}\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["
|
|
3
|
+
"sources": ["../../wiki-dsl/dsl.js", "../src/pod/commands.js", "../src/pod/vocabulary.js", "../src/client/pod.js"],
|
|
4
|
+
"sourcesContent": ["// dsl.js \u2014 the parser every fedwiki plugin with a command table needs.\n//\n// The convention is fedwiki-wide: in an item's text, an UPPERCASE first word is\n// a command, lowercase is data, and a single trailing colon is optional. Each\n// plugin writes its own table of commands; none of them should write this again.\n//\n// A table is an object of WORD \u2192 entry, and every entry declares its `kind`:\n//\n// parameter supplies an argument to a call \u2192 crosses to HTTP\n// macro shorthand expanding to other commands \u2192 expands, then crosses\n// client-data no server can supply it \u2192 never crosses\n// presentation changes only how it is drawn \u2192 never crosses\n// alias another spelling of a command \u2192 resolves first\n//\n// Those are the kinds wiki-plugin-farm's vocabulary check accepts, so a table\n// this module can read is a table the farm can mount. What the farm cannot do is\n// read item TEXT \u2014 it has no DOM and no parser \u2014 which is why this lives in the\n// browser, beside the table, rather than in the farm.\n//\n// Usage, in a plugin's own commands module:\n//\n// import { dsl } from '@fortyfoxes/wiki-dsl'\n// export const COMMANDS = { \u2026 }\n// export const { parseKinds, hasCommand, argumentOf, problems } = dsl(COMMANDS)\n\n/** Values an argument may take to mean \"no\". */\nconst FALSEY = ['no', 'false', 'off', '0', 'hide', 'none']\n\n/** Announces itself as a command: uppercase, letters only, two or more. */\nconst COMMAND_LIKE = /^[A-Z][A-Z]+$/\n\n/** Levenshtein distance. Only ever run over single short words. */\nconst distance = (a, b) => {\n let prev = Array.from({ length: b.length + 1 }, (_, j) => j)\n for (let i = 1; i <= a.length; i++) {\n const row = [i]\n for (let j = 1; j <= b.length; j++) {\n row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1))\n }\n prev = row\n }\n return prev[b.length]\n}\n\n/**\n * The keyword a line begins with, normalised. UPPERCASE is canonical but input\n * case is forgiven, and one trailing colon is optional \u2014 never required.\n */\nexport const keywordOf = line =>\n String(line || '')\n .trim()\n .split(/\\s+/)[0]\n .replace(/:$/, '')\n .toUpperCase()\n\n/**\n * Bind the helpers to one plugin's table. `fallback` is what parseKinds answers\n * when the text names nothing \u2014 the plugin's own default.\n */\nexport const dsl = (COMMANDS, { fallback = [] } = {}) => {\n /** Follow an alias to the command it spells. */\n const resolve = name => {\n const entry = COMMANDS[name]\n return entry && entry.kind === 'alias' ? COMMANDS[entry.alias] : entry\n }\n\n /** The canonical name a command resolves to, for reporting. */\n const canonical = name => {\n const entry = COMMANDS[name]\n return entry && entry.kind === 'alias' ? entry.alias : name\n }\n\n /** Every value a parameter may take, in declaration order. The API's enum. */\n const valuesFor = parameter =>\n Object.values(COMMANDS)\n .filter(c => c.kind === 'parameter' && c.parameter === parameter && c.value)\n .map(c => c.value)\n\n /** One field of the table, keyed by value \u2014 labels(), headings('heading'), \u2026 */\n const fieldByValue = field => {\n const out = {}\n for (const c of Object.values(COMMANDS)) if (c.value && c[field]) out[c.value] = c[field]\n return out\n }\n\n /**\n * Item text to an ordered, de-duplicated list of values. Macros expand in\n * place, aliases resolve, unrecognised lines are ignored as data rather than\n * treated as an error.\n */\n const parseKinds = (text, { fallback: given = fallback } = {}) => {\n const kinds = []\n const take = name => {\n const entry = resolve(name)\n if (!entry) return\n if (entry.kind === 'macro') return entry.expands.forEach(take)\n if (!entry.value) return\n if (!kinds.includes(entry.value)) kinds.push(entry.value)\n }\n for (const line of String(text || '').split('\\n')) {\n if (line.trim()) take(keywordOf(line))\n }\n return kinds.length ? kinds : [...given]\n }\n\n /** Whether a bare command appears on a line of its own. */\n const hasCommand = (text, name) =>\n String(text || '')\n .split('\\n')\n .some(line => canonical(keywordOf(line)) === name)\n\n /**\n * The argument written after a command \u2014 `TITLE no` \u2014 as a boolean. Absent\n * means the command's declared default.\n */\n const argumentOf = (text, name) => {\n for (const raw of String(text || '').split('\\n')) {\n if (canonical(keywordOf(raw)) !== name) continue\n const arg = (raw.trim().split(/\\s+/)[1] || '').toLowerCase().replace(/:$/, '')\n return !FALSEY.includes(arg)\n }\n return COMMANDS[name]?.default\n }\n\n /** The command a mistyped word most likely meant, or null when nothing is close. */\n const suggest = word => {\n const w = String(word || '').trim().replace(/:$/, '').toUpperCase()\n if (!w) return null\n let best = null\n let bestDistance = Infinity\n for (const name of Object.keys(COMMANDS)) {\n const d = distance(w, name)\n if (d < bestDistance) {\n bestDistance = d\n best = name\n }\n }\n return bestDistance <= (w.length <= 4 ? 1 : 2) ? canonical(best) : null\n }\n\n /**\n * Lines that look like a command and are not one, in order \u2014 each with the\n * word as written and the command it probably meant (null when unguessable).\n *\n * A mistyped command is worse than an unknown one: CHIKDREN names nothing, so\n * the item falls back and draws something the author never asked for, with\n * nothing on it saying why. Parsing is unaffected \u2014 this only gives the plugin\n * something true to say. Uppercase announces \"command\" out loud; a lone word\n * one typo from a real command says it quietly; ordinary prose says neither\n * and stays data.\n */\n const problems = text => {\n const found = []\n for (const raw of String(text || '').split('\\n')) {\n const line = raw.trim()\n if (!line || resolve(keywordOf(line))) continue\n const word = line.split(/\\s+/)[0].replace(/:$/, '')\n const suggestion = suggest(word)\n const alone = line.split(/\\s+/).length === 1\n if (!COMMAND_LIKE.test(word) && !(alone && suggestion)) continue\n found.push({ word, suggestion })\n }\n return found\n }\n\n return { resolve, canonical, valuesFor, fieldByValue, parseKinds, hasCommand, argumentOf, suggest, problems }\n}\n", "// commands.js \u2014 the plugin's vocabulary, in one place.\n//\n// This module is the single source for what a pod item's text may say. The\n// client imports it to parse item text; the plugin's API declaration names it so\n// the farm reads the identical table. There is no second copy to drift.\n//\n// The PARSER is not here, and neither is an import of it. The FARM loads this\n// module server-side to read the table, so it must be data and nothing else \u2014\n// an import of @fortyfoxes/wiki-dsl here would need that package installed on\n// every farm, and a vocabulary that fails to load is dropped from the farm's\n// merged document entirely. The parser is bound in ./vocabulary.js, which only\n// the browser and the tests load.\n//\n// A DSL is a superset of an API, never a mirror of one \u2014 so every command\n// declares which KIND it is, and only some kinds cross to HTTP:\n//\n// parameter supplies an argument to the call \u2192 crosses\n// macro shorthand expanding to other commands \u2192 expands, then crosses\n// client-data the server cannot supply it at all \u2192 never crosses\n// presentation changes only how it is drawn \u2192 never crosses\n// alias another spelling of a command \u2192 resolves first\n//\n// Adding a command here is the whole change: the parser, the API's allowed\n// values, the rendered reference page and the plugin's own documentation all\n// read this table.\n\nexport const VOCABULARY_VERSION = 1\n\n/** The story item type these commands are written into. An agent needs this to author one. */\nexport const ITEM_TYPE = 'pod'\n\n/** What the item does when no command is recognised. */\nexport const FALLBACK = ['sisters']\n\nexport const COMMANDS = {\n // ---- parameter: these become values of the `kinds` parameter -------------\n //\n // `heading` is what the drawn group is called. `{site}` is the site the item\n // is on, because that is what a pod is relative to and the one thing a reader\n // landing cold cannot work out.\n SISTERS: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'sisters',\n label: 'Sisters',\n heading: 'Sisters of {site}',\n description: 'sibling sites sharing the parent domain',\n },\n PARENT: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'parent',\n label: 'Parent',\n heading: 'Parent of {site}',\n description: 'the parent-domain site itself',\n },\n CHILDREN: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'children',\n label: 'Children',\n heading: 'Children of {site}',\n description: 'direct sub-domains of this site',\n },\n DESCENDANTS: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'descendants',\n label: 'Descendants',\n heading: 'Descendants of {site}',\n description: 'all sub-domains, any depth',\n },\n FARM: {\n kind: 'parameter',\n parameter: 'kinds',\n value: 'farm',\n label: 'Farm',\n heading: 'The rest of the farm',\n description: 'every wiki in the farm',\n },\n\n // ---- macro ---------------------------------------------------------------\n POD: {\n kind: 'macro',\n expands: ['PARENT', 'SISTERS'],\n description: 'PARENT and SISTERS together (shorthand)',\n },\n\n // ---- client-data: gathered in the browser, invisible to any server -------\n NEIGHBOURHOOD: {\n kind: 'client-data',\n value: 'neighbourhood',\n label: 'Neighbourhood',\n heading: 'Your neighbourhood',\n description: 'the sites currently in your neighborhood',\n why: 'reads the neighbourhood the browser has already assembled, which no server can see',\n },\n NEIGHBORHOOD: { kind: 'alias', alias: 'NEIGHBOURHOOD', description: 'American spelling' },\n SNAPSHOT: {\n kind: 'client-data',\n value: 'snapshot',\n label: 'Snapshot',\n heading: 'Your neighbourhood',\n description: 'freeze the current neighborhood',\n why: 'same browser-only source as NEIGHBOURHOOD',\n },\n\n // ---- presentation: never leaves the client -------------------------------\n //\n // FORCE is a behaviour switch rather than a drawing choice, but `presentation`\n // is the closest of the kinds the farm's vocabulary check accepts, and the\n // thing that matters is true of it: it never crosses to a server.\n FORCE: {\n kind: 'presentation',\n argument: 'yes|no',\n default: false,\n description: 'register the gathered sites as neighbours when the page loads',\n },\n TITLE: {\n kind: 'presentation',\n argument: 'yes|no',\n default: true,\n description: 'whether each group shows its heading',\n },\n\n // ---- retired: kept so pages that say them still parse ---------------------\n //\n // A word removed from this table is a word the parser reports as a mistake, so\n // retiring one by deleting it would put a false \"not a pod command\" warning on\n // every page that still says it. These stay, as presentation (the kind the\n // farm's check allows for a word that never crosses), carrying what became of\n // them.\n ROSTER: {\n kind: 'presentation',\n retired: 'the default \u2014 every pod is drawn as a roster of flags',\n description: 'no longer needed: rosters are how a pod is drawn',\n },\n TWIN: {\n kind: 'presentation',\n retired: 'wiki-plugin-twin',\n description: 'moved: which pod members hold a page with this page\u2019s slug',\n },\n WATCH: {\n kind: 'presentation',\n retired: 'wiki-plugin-twin',\n description: 'moved: which pod members have forked this page',\n },\n}\n", "// vocabulary.js \u2014 the table, read.\n//\n// ./commands.js is data with no imports, because the farm loads it server-side\n// and a vocabulary that cannot be imported is dropped from the farm's document.\n// Everything that READS that table lives here instead: @fortyfoxes/wiki-dsl,\n// bundled into the client at build time and shared with every other plugin whose\n// item text is a small language.\n\nimport { dsl } from '@fortyfoxes/wiki-dsl'\n\nimport { COMMANDS, FALLBACK } from './commands.js'\n\nexport const { parseKinds, hasCommand, argumentOf, problems, valuesFor, fieldByValue, canonical, resolve, suggest } =\n dsl(COMMANDS, { fallback: FALLBACK })\n\n/** What a retired command became, or undefined for a live one. */\nexport const retirementOf = word => COMMANDS[word]?.retired\n\nexport { COMMANDS, FALLBACK }\nexport { VOCABULARY_VERSION, ITEM_TYPE } from './commands.js'\n", "// wiki-plugin-pod \u2014 gather configurable pods of related wiki sites.\n//\n// The item text chooses WHICH pods of sites to gather \u2014 sisters, parent,\n// children, descendants, the farm, the neighbourhood \u2014 and draws them as a\n// roster of flags. Say FORCE and the gathered sites also join your neighbourhood\n// on load, so their pages join your search and lineage; without it the roster's\n// own \u00BB does that when you ask, which is how any other roster behaves.\n//\n// The commands an author may write are NOT listed here. They live in\n// ../pod/commands.js, which the plugin's API declaration also names, so what an\n// author may type and what the mounted operation accepts come from one table.\n// Restating that table in a comment is how the two drift apart; read the table.\n//\n// Where each pod comes from: this plugin ships NO server. It declares a\n// specification and a module of plain functions (api/, src/pod/), and the Farm\n// Plugin mounts them \u2014 so the disk-derived pods are read from that mount,\n// /system/api/pod/roll.json, the same address an agent uses. NEIGHBOURHOOD and\n// SNAPSHOT never leave the browser at all, being the neighbourhood the client\n// has already assembled.\n//\n// How it draws: it doesn't. A gather IS a roster \u2014 a heading over the members'\n// flags \u2014 and wiki-plugin-roster already draws one, on every wiki there is. So\n// pod builds the roster text and hands it to that plugin. The text it builds is\n// also exactly what the \u2744 icon saves as a page, so the drawn thing and the saved\n// thing are one string rather than two code paths that must agree.\nimport { valuesFor, fieldByValue, parseKinds, problems, hasCommand, argumentOf, retirementOf } from '../pod/vocabulary.js'\n\nconst SERVER_KINDS = valuesFor('kinds')\nconst HEADING = fieldByValue('heading')\n\nconst escapeHtml = text =>\n (text || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n\n// A mistyped command (CHIKDREN for CHILDREN) is parsed as data, so the item\n// falls back to sisters and draws a pod nobody asked for. Say so above the\n// gather, since nothing else on the panel would show a trace of it.\nconst problemsHtml = found =>\n found.length\n ? `<p class=pod-problem style=\"color:#a00;font-size:80%;text-align:center;margin:0 0 6px\">` +\n found\n .map(p =>\n p.suggestion\n ? `${escapeHtml(p.word)} \u2014 did you mean <b>${p.suggestion}</b>?`\n : `${escapeHtml(p.word)} \u2014 not a pod command`,\n )\n .join('<br>') +\n `</p>`\n : ''\n\n// A command that has moved says where it went, rather than silently doing\n// nothing on a page that still asks for it.\nconst retiredHtml = text => {\n const moved = ['TWIN', 'WATCH', 'ROSTER']\n .filter(word => hasCommand(text, word))\n .map(word => [word, retirementOf(word)])\n return moved.length\n ? `<p class=pod-retired style=\"color:#666;font-size:80%;text-align:center;margin:0 0 6px\">` +\n moved.map(([word, became]) => `${word} \u2014 now ${escapeHtml(became)}`).join('<br>') +\n `</p>`\n : ''\n}\n\nconst portSuffix = () => ([80, '80', '', null].includes(location.port) ? '' : `:${location.port}`)\n\nexport const emit = (div, item) => {\n const suffix = portSuffix()\n const kinds = parseKinds(item.text)\n const showHeadings = argumentOf(item.text, 'TITLE')\n const force = argumentOf(item.text, 'FORCE')\n const notes = problemsHtml(problems(item.text)) + retiredHtml(item.text)\n\n if (div.closest('.page').hasClass('remote')) {\n div.html(\n `<div class=pod style=\"background-color:#eee;padding:15px\"><center>` +\n `<p class=caption>Pod is only available when viewed on a page's home wiki.</p></div>`,\n )\n return\n }\n\n div.html(`${notes}<div class=pod-gather><p class=caption>gathering\u2026</p></div>`)\n\n // The roster text: a heading, a BLANK LINE, then one domain per line, and a\n // blank line again between groups. That is wiki-plugin-roster's own markup \u2014\n // categories are the lines that are not domains, and a blank line starts a new\n // display line, which is what puts the heading on a line of its own with the\n // flags in a row beneath it. Without the blank line roster runs them together:\n // \"Sisters of demo.localhost [flag] [flag]\" on one line.\n //\n // This same string draws the item and, saved, becomes an ordinary roster item\n // on a page any wiki can read.\n const rosterText = (groups, origin) =>\n kinds\n .map(kind => {\n const sites = groups[kind] || []\n if (!sites.length) return ''\n const heading = showHeadings ? (HEADING[kind] || kind).replace('{site}', origin) + '\\n\\n' : ''\n return heading + sites.join('\\n')\n })\n .filter(Boolean)\n .join('\\n\\n')\n\n const render = (groups, origin) => {\n // FORCE \u2014 register the gathered sites as neighbours, so their pages join\n // your search and lineage. Off by default: a pod is a roster, and a roster\n // does not reach into your neighbourhood unasked. The roster's own \u00BB does it\n // on demand instead, and the flags are drawn either way \u2014 wiki.site().flag()\n // never consults the neighbourhood.\n if (force) {\n for (const kind of kinds) {\n if (!SERVER_KINDS.includes(kind)) continue\n for (const site of groups[kind] || []) wiki.neighborhoodObject.registerNeighbor(site + suffix)\n }\n }\n\n const text = rosterText(groups, origin)\n const target = div.find('.pod-gather')\n target.empty()\n if (!text) return target.html('<p class=caption><i>none</i></p>')\n\n wiki.getPlugin('roster', roster => {\n target.empty()\n const rosterItem = { type: 'roster', id: `${item.id}-roster`, text }\n roster.emit(target, rosterItem)\n // bind() too, or the \u00BB roster draws beside each row \u2014 \"add these N sites\n // to neighborhood\" \u2014 is inert markup. With FORCE off that link is the only\n // way to take the pod into your neighbourhood, so it has to work.\n //\n // Its other binding is a dblclick opening a text editor on this synthetic\n // roster item, which belongs to nothing and would edit a string we\n // generated. Drop that one; pod binds its own dblclick, on the item, to\n // edit the pod text an author actually wrote.\n roster.bind(target, rosterItem)\n target.off('dblclick')\n // Freeze \u2014 the same text, saved as a real roster item on a new page.\n const icon = $(\n '<span class=pod-freeze-icon title=\"Freeze \u2014 save this gather as a roster page\" ' +\n 'style=\"position:absolute;top:6px;right:9px;cursor:pointer;font-size:14px;opacity:.55\">\u2744</span>',\n )\n icon.on('click', () => {\n const title = kinds.map(k => (HEADING[k] || k).replace('{site}', origin)).join(', ')\n const page = wiki.newPage({\n title,\n story: [{ type: 'roster', id: Math.floor(Math.random() * 0xffffffffffff).toString(16).padStart(12, '0'), text }],\n })\n wiki.showResult(page, { $page: div.parents('.page') })\n })\n div.css('position', 'relative').append(icon)\n })\n }\n\n // Only the disk-derived pods need asking; NEIGHBOURHOOD and SNAPSHOT are\n // already in the browser.\n const neighbourhood = () => Object.keys(wiki.neighborhood).map(key => (suffix && key.endsWith(suffix) ? key.slice(0, -suffix.length) : key))\n const localGroups = () => {\n const groups = {}\n for (const kind of kinds) if (!SERVER_KINDS.includes(kind)) groups[kind] = neighbourhood()\n return groups\n }\n\n const serverKinds = kinds.filter(k => SERVER_KINDS.includes(k))\n if (serverKinds.length === 0) {\n render(localGroups(), location.hostname)\n return\n }\n\n fetch(`/system/api/pod/roll.json?kinds=${encodeURIComponent(serverKinds.join(','))}`)\n .then(res => {\n if (!res.ok) {\n const err = new Error(`HTTP ${res.status}`)\n err.status = res.status\n throw err\n }\n return res.json()\n })\n .then(data => render({ ...(data.groups || {}), ...localGroups() }, data.origin || location.hostname))\n .catch(err => {\n // A 404 means nothing is mounted at that address: this farm has no\n // wiki-plugin-farm, or it has not been restarted since pod was installed.\n // Name it, rather than saying \"server error\", so the operator knows what\n // to install.\n const msg =\n err && err.status === 404\n ? 'pod needs wiki-plugin-farm on this wiki to mount its API \u2014 install it and restart'\n : 'server error'\n div.find('.caption').first().text(msg)\n })\n}\n\nexport const bind = (div, item) => {\n div.dblclick(() => wiki.textEditor(div, item))\n}\n\nif (typeof window !== 'undefined') {\n window.plugins = window.plugins || {}\n window.plugins.pod = { emit, bind }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["FALSEY", "COMMAND_LIKE", "distance", "a", "b", "prev", "_", "j", "i", "row", "keywordOf", "line", "dsl", "COMMANDS", "fallback", "resolve", "name", "entry", "canonical", "valuesFor", "parameter", "c", "fieldByValue", "field", "out", "parseKinds", "text", "given", "kinds", "take", "hasCommand", "argumentOf", "raw", "arg", "suggest", "word", "w", "best", "bestDistance", "d", "found", "suggestion", "alone", "FALLBACK", "COMMANDS", "parseKinds", "hasCommand", "argumentOf", "problems", "valuesFor", "fieldByValue", "canonical", "resolve", "suggest", "dsl", "COMMANDS", "FALLBACK", "retirementOf", "word", "SERVER_KINDS", "valuesFor", "HEADING", "fieldByValue", "escapeHtml", "text", "problemsHtml", "found", "p", "retiredHtml", "moved", "word", "hasCommand", "retirementOf", "became", "portSuffix", "emit", "div", "item", "suffix", "kinds", "parseKinds", "showHeadings", "argumentOf", "force", "notes", "problems", "rosterText", "groups", "origin", "kind", "sites", "render", "site", "target", "roster", "rosterItem", "icon", "title", "k", "page", "neighbourhood", "key", "localGroups", "serverKinds", "res", "err", "data", "msg", "bind"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wiki-plugin-pod",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Federated Wiki - Pod Plugin — gather configurable pods of related wiki sites (sisters, parent, children, farm, neighbourhood) into your neighborhood",
|
|
3
|
+
"version": "1.2.2",
|
|
4
|
+
"description": "Federated Wiki - Pod Plugin — gather configurable pods of related wiki sites (sisters, parent, children, descendants, farm, neighbourhood) into your neighborhood, drawn as rosters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "client/pod.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"api",
|
|
9
9
|
"client",
|
|
10
10
|
"pages",
|
|
11
|
-
"server",
|
|
12
11
|
"src",
|
|
13
12
|
"factory.json"
|
|
14
13
|
],
|
|
15
14
|
"scripts": {
|
|
16
15
|
"build": "node scripts/build.js",
|
|
17
|
-
"test": "node test/vocabulary.mjs"
|
|
16
|
+
"test": "node test/vocabulary.mjs && node test/kinship.mjs"
|
|
18
17
|
},
|
|
19
18
|
"keywords": [
|
|
20
19
|
"pod",
|
|
@@ -27,6 +26,7 @@
|
|
|
27
26
|
"author": "David Bovill",
|
|
28
27
|
"license": "MIT",
|
|
29
28
|
"devDependencies": {
|
|
29
|
+
"@fortyfoxes/wiki-dsl": "^0.1.0",
|
|
30
30
|
"esbuild": "^0.25.0"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
@@ -43,6 +43,31 @@
|
|
|
43
43
|
"spec": "api/openapi.json",
|
|
44
44
|
"handlers": "api/handlers.js",
|
|
45
45
|
"vocabulary": "src/pod/commands.js"
|
|
46
|
+
},
|
|
47
|
+
"requires": {
|
|
48
|
+
"plugins": [
|
|
49
|
+
{
|
|
50
|
+
"package": "wiki-plugin-farm",
|
|
51
|
+
"seat": "wiki-plugin-farm",
|
|
52
|
+
"range": ">=0.8.0",
|
|
53
|
+
"level": "required",
|
|
54
|
+
"why": "pod ships no server of its own: the Farm Plugin mounts its specification and handlers at /system/api/pod/roll.json, which is where the item reads its disk-derived pods."
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"package": "wiki-plugin-roster",
|
|
58
|
+
"seat": "wiki-plugin-roster",
|
|
59
|
+
"range": ">=0.4.0",
|
|
60
|
+
"level": "required",
|
|
61
|
+
"why": "pod draws nothing itself: a gather is a roster, and the roster plugin already draws one on every wiki, so pod builds the roster text and hands it over."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"package": "wiki-plugin-twin",
|
|
65
|
+
"seat": "twin",
|
|
66
|
+
"range": "^0.1.0",
|
|
67
|
+
"level": "optional",
|
|
68
|
+
"why": "TWIN and WATCH were split out of pod at 1.2.0; without it those words still parse and say where they went, but nothing shows which sites hold or forked the page."
|
|
69
|
+
}
|
|
70
|
+
]
|
|
46
71
|
}
|
|
47
72
|
}
|
|
48
73
|
}
|