wiki-plugin-hitchhiker 0.4.0 → 0.5.1

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/wiki-ux.js CHANGED
@@ -1,1800 +1,39 @@
1
- /*
2
- * wiki-ux.js — the hitchhiker house UX layer for the Federated Wiki client.
3
- *
4
- * Loaded globally on every page by the wiki-security-hitchhiker bootstrap
5
- * (its client/security.js injects a <script> tag for this file). It runs in the
6
- * browser AFTER the wiki client bundle, and reaches the client only through the
7
- * public `window.wiki` / `window.plugins` surface — it never patches the bundle.
8
- *
9
- * It installs three overrides:
10
- * 1. Factory chooser — a friendlier, curated, searchable plugin picker
11
- * (keeps the built-in factory `bind`, so every existing
12
- * drag-and-drop behaviour via drop.js is preserved).
13
- * 2. Extra drops — a capture-phase drop handler that recognises a few
14
- * media URLs the built-in factory would otherwise punt.
15
- * 3. Page-flag click — opens an in-lineup "Tools" page for the clicked page
16
- * instead of reopening the origin site directly (the
17
- * original behaviour is preserved as the first tool).
18
- *
19
- * First pass / pilot: private farm only. Interactive, JS-backed tools on the
20
- * Tools page are intentionally left for a follow-up (a small toolkit item type
21
- * on wiki-plugin-hitchhiker); for now the Tools page offers links + wikilinks.
22
- */
23
- ;(function () {
24
- 'use strict'
1
+ /* wiki-plugin-hitchhiker 0.5.1 */
2
+ (()=>{(function(){"use strict";var P=!1,S=0,j=setInterval(function(){S++;var e=window.wiki&&window.plugins&&window.plugins.factory&&window.plugins.factory.bind&&window.$&&typeof window.wiki.pageHandler<"u";if(e&&!P){P=!0,clearInterval(j);try{_()}catch(t){console.error("[wiki-ux] install failed",t)}}else S>100&&clearInterval(j)},200),p="font-size:1.05em;min-height:44px;padding:10px 18px;cursor:pointer;",z='<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" style="vertical-align:-0.15em;margin-right:.4em" aria-hidden="true"><path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg>',k=!1,dt={"*":{recommend:["paragraph","markdown","image","html","reference"],hide:[]}};function _(){st(),B(),X(),M(),U(),G(),console.log("[wiki-ux] installed")}function F(e){if(e.hasClass("hearth-page")||e.find(".item.hearth").length)return!0;try{var t=window.wiki.lineup.atKey(e.data("key")),a=t&&t.getRawPage&&t.getRawPage().story;return!!(a&&a.some(function(i){return i.type==="hearth"}))}catch{return!1}}function H(e,t){var a=/^https?:/.test(e)?e:location.protocol+"//"+e;return fetch(a+"/"+t+".json").then(function(i){return i.ok?i.json():null}).then(function(i){return!!(i&&i.story&&i.story.some(function(r){return r.type==="hearth"}))}).catch(function(){return!1})}function A(e){window.wiki&&window.wiki.showResult;var t=document.createElement("div");t.textContent="\u{1F3E0} "+e,t.style.cssText="position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:99999;background:rgba(200,120,40,0.95);color:#fff;padding:10px 16px;border-radius:8px;font:14px/1.4 -apple-system,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:80vw;",document.body.appendChild(t),setTimeout(function(){t.remove()},4200)}function U(){document.addEventListener("dragstart",function(e){var t=e.target&&e.target.closest?e.target.closest(".page h1 a"):null;t&&F(window.$(t).closest(".page"))&&(e.preventDefault(),e.stopImmediatePropagation(),A("This page keeps to its hearth \u2014 it will not be dragged out."))},!0)}function B(){document.addEventListener("click",function(e){var t=e.target,a=t&&t.closest?t.closest(".page h1 a"):null;if(a&&(e.preventDefault(),e.stopImmediatePropagation(),!a.closest(".page.wiki-ux-tools"))){if(e.shiftKey){try{W(a)}catch{R(a)}return}try{ot(a)}catch(i){console.error("[wiki-ux] flag page failed, opening origin site instead",i),R(a)}}},!0)}function C(e,t,a){var i=window.wiki,r=t==="welcome-visitors"?"view/welcome-visitors":"view/welcome-visitors/view/"+t;if(e.hasClass("remote")){var n=a;if(!n)try{var o=i.lineup.atKey(e.data("key"));n=o&&o.getRemoteSite&&o.getRemoteSite()}catch{}if(n)return i.site(n).getDirectURL(r)}return"/"+r}function W(e){var t=window.$(e).closest(".page"),a=t.attr("id");window.location.assign(C(t,a,h(t)))}function h(e){if(!e.hasClass("remote"))return"";try{var t=window.wiki.lineup.atKey(e.data("key"));return t&&t.getRemoteSite&&t.getRemoteSite()||e.data("site")||""}catch{return e.data("site")||""}}function m(e,t,a){a=a||{};var i=window.$,r=window.wiki,n=e.prevAll(".page").length;r.showResult(t);var o=i(".main > .page").last();o.insertAfter(e);try{r.lineup.changePageIndex(o.data("key"),n+1)}catch{}o.addClass("wiki-ux-tools"),a.ghost||o.removeClass("ghost");try{o[0].scrollIntoView({behavior:"smooth",inline:"center",block:"nearest"})}catch{}return o}function M(){document.addEventListener("click",function(e){var t=e.target;if(!(!t||!t.closest)){var a=t.closest(".wiki-ux-tool-fork")?"fork":t.closest(".wiki-ux-tool-sim")?"sim":t.closest(".wiki-ux-tool-trash")?"trash":t.closest(".wiki-ux-tool-clean")?"clean":t.closest(".wiki-ux-tool-flag")?"flag":t.closest(".wiki-ux-tool-toolpage")?"toolpage":null;if(a){e.preventDefault();var i=window.$,r=t.closest(".wiki-ux-tool-"+a),n=i(r).closest(".page"),o=n.prev(".page");try{a==="fork"?N(o,n):a==="sim"?q(o,n):a==="trash"?V(o,n):a==="clean"?Z(o,n):a==="flag"?window.plugins.fishPicker&&window.plugins.fishPicker.open(o,n):a==="toolpage"&&K(o,n,r.getAttribute("data-slug"))}catch(s){console.error('[wiki-ux] tool "'+a+'" failed',s)}}}},!0)}var D="demo.localhost";function K(e,t,a){if(a){var i=function(r){return fetch("//"+r+"/"+a+".json").then(function(n){if(!n.ok)throw new Error(""+n.status);return n.json()})};i(location.host).catch(function(){return i(D)}).then(function(r){m(e,window.wiki.newPage(r),{ghost:!0}),t.remove()}).catch(function(){window.alert('Tool page "'+a+'" not found on '+location.host+" or "+D+".")})}}function N(e,t){var a=e.find(".fork-page");if(a.length){a.trigger("click"),t.remove();return}if(window.isAuthenticated!==!0||/_rev\d+$/.test(e.attr("id")||"")){console.warn("[wiki-ux] fork: no .fork-page control on the source page");return}var i={type:"fork"};if(e.hasClass("local"))e.removeClass("local");else{var r=h(e);r&&(i.site=r)}e.removeClass("ghost"),window.wiki.pageHandler.put(e,i),t.remove()}function V(e,t){var a=window.wiki,i=e.data("data")||{},r=i.title||e.attr("id")||"this page";if(window.confirm('Trash "'+r+`"?
25
3
 
26
- // ── trusted authors ─────────────────────────────────────────────────────────
27
- // Which remote origin-sites may OFFER to run a script on THIS viewer's local
28
- // pty. wiki-plugin-terminal reads window.trustedAuthors: a page federated in
29
- // from a matching site (e.g. bot.pi5) shows a single explicit "run · from
30
- // <site>" button — never auto-run — that runs on the viewer's own loopback
31
- // shell. A per-browser list, editable without redeploy (localStorage). Vouch
32
- // for / revoke a site from the console: window.trustAuthor('bot.pi5').
33
- try {
34
- var TRUST_KEY = 'wiki-trusted-authors'
35
- var loadTrust = function () {
36
- try { return JSON.parse(localStorage.getItem(TRUST_KEY)) || [] } catch (e) { return [] }
37
- }
38
- window.trustedAuthors = loadTrust()
39
- window.trustAuthor = function (site) {
40
- var l = loadTrust()
41
- if (site && l.indexOf(site) === -1) l.push(site)
42
- localStorage.setItem(TRUST_KEY, JSON.stringify(l))
43
- return (window.trustedAuthors = l)
44
- }
45
- window.untrustAuthor = function (site) {
46
- var l = loadTrust().filter(function (s) { return s !== site })
47
- localStorage.setItem(TRUST_KEY, JSON.stringify(l))
48
- return (window.trustedAuthors = l)
49
- }
50
- } catch (e) { window.trustedAuthors = window.trustedAuthors || [] }
4
+ It moves to the recycler and can be recovered from Recycled Pages.`)){var n;try{n=a.lineup.atKey(e.data("key"))}catch{}if(!n){window.alert("Could not identify the page to trash.");return}a.pageHandler.delete(n,e,function(o){if(o){window.alert("Trash failed: "+o);return}t.remove(),e.remove()})}}function Z(e,t){var a=window.wiki,i=e.data("data")||{},r=i.title||e.attr("id")||"this page",n=i.story||[],o={title:r,story:n,journal:[{type:"create",item:{title:r,story:n},date:Date.now(),certificate:"from marvin"}]};a.showResult(a.newPage(o),{$page:t})}function G(){fetch("/system/factories.json").then(function(e){return e.ok?e.json():[]}).then(function(e){window.catalog||(window.catalog=e),k=(e||[]).some(function(t){return t&&t.name==="Similarity"})}).catch(function(){})}function q(e,t){var a=e.data("data")||{},i=a.title||e.attr("id")||"this page",r=(e.attr("id")||"").split("_rev")[0],n=h(e)||location.hostname,o=t.find(".wiki-ux-tool-sim");o.prop("disabled",!0).text("\u{1F50D} Searching\u2026");var s=[i].concat((a.story||[]).filter(function(u){return u.type==="markdown"||u.type==="paragraph"}).map(function(u){return(u.text||"").trim()}).filter(Boolean)).join(`
5
+ `).slice(0,2e3);fetch(location.origin+"/system/search-report.json",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({query:i,seed:{site:n,slug:r},excludePage:{site:n,slug:r},text:s,domains:["*"],limit:12})}).then(function(u){if(!u.ok)throw new Error("search-report failed: "+u.status);return u.json()}).then(function(u){m(t,window.wiki.newPage(u),{ghost:!0}),o.prop("disabled",!1).text("\u{1F50D} Similarity search")}).catch(function(u){console.error("[wiki-ux] similarity failed",u),o.prop("disabled",!1).text("\u{1F50D} Similarity search"),window.alert("Similarity search failed: "+u.message)})}function Y(e){var t=window.wiki,a=t.lineup.crumbs(e.data("key"),location.host),i=a[0],r=t.site(i).getDirectURL("").split("/")[0];return r===""&&(r=window.location.protocol),{url:r+"//"+a.join("/"),target:i}}function R(e){var t=window.$,a=t(e).closest(".page"),i=Y(a);window.open(i.url,i.target).focus()}var g="wiki-ux-drop-target",J=["text/uri-list","text/x-moz-url","text/plain","text/html","Files"];function X(){var e=function(i){return i&&i.closest?i.closest(".page h1 a"):null},t=function(i){if(!i||!i.types)return!1;for(var r=0;r<i.types.length;r++)if(J.indexOf(i.types[r])!==-1)return!0;return!1},a=function(){for(var i=document.querySelectorAll("."+g),r=0;r<i.length;r++)i[r].classList.remove(g)};document.addEventListener("dragover",function(i){var r=e(i.target);if(!(!r||!t(i.dataTransfer))){i.preventDefault();try{i.dataTransfer.dropEffect="copy"}catch{}r.classList.contains(g)||(a(),r.classList.add(g))}},!0),document.addEventListener("dragleave",function(i){var r=e(i.target);r&&(!i.relatedTarget||!r.contains(i.relatedTarget))&&r.classList.remove(g)},!0),document.addEventListener("drop",function(i){var r=e(i.target);if(a(),!!r){i.preventDefault(),i.stopPropagation();try{var n=window.$(r).closest(".page"),o=$(i.dataTransfer);if(o.kind==="page"&&o.site&&o.slug){H(o.site,o.slug).then(function(s){s?A("That page keeps to its hearth \u2014 it declines to be copied here."):L(n,o)});return}L(n,o)}catch(s){console.error("[wiki-ux] drop failed",s)}}},!0),document.addEventListener("dragend",a,!0)}function Q(e){var t=e.match(/^https?:\/\/([a-zA-Z0-9:.-]+)(\/([a-zA-Z0-9:.-]+)\/([a-z0-9-]+(_rev\d+)?))+$/);if(!t)return null;var a=t[1],i=t[3],r=t[4];return["view","local","origin"].indexOf(i)!==-1&&(i=a),{site:i,slug:r}}function $(e){var t={kind:"unknown",url:"",site:"",slug:"",text:"",html:"",files:[],types:[]};if(!e)return t;if(t.types=Array.prototype.slice.call(e.types||[]),e.files&&e.files.length){for(var a=0;a<e.files.length;a++){var i=e.files[a];t.files.push({name:i.name,type:i.type,size:i.size})}t.kind="file"}try{t.text=e.getData("text/plain")||""}catch{}try{t.html=e.getData("text/html")||""}catch{}var r="";try{r=e.getData("text/uri-list")||e.getData("URL")||e.getData("text/x-moz-url")||""}catch{}if(r&&(r=r.split(`
6
+ `)[0].trim()),!r&&/^https?:\/\//i.test(t.text)&&(r=t.text.trim()),r){t.url=r;var n=Q(r);n?(t.kind="page",t.site=n.site,t.slug=n.slug):t.kind="link"}else if(t.kind==="unknown"){var o=(t.text||"").trim();o&&/^[a-z0-9.-]+\.[a-z]{2,}$/i.test(o)?(t.kind="site",t.site=o):o&&(t.kind="text")}return t}var v=null;function tt(){return v||(v=fetch(location.origin+"/system/outposts.json").then(function(e){return e.ok?e.json():{}}).then(function(e){return(e&&Array.isArray(e.unfurl)?e.unfurl:[]).filter(Boolean)}).catch(function(){return[]}),v)}function et(e){for(var t=e.slice(),a=t.length-1;a>0;a--){var i=Math.floor(Math.random()*(a+1)),r=t[a];t[a]=t[i],t[i]=r}return t}function O(e){return tt().then(function(t){var a=et(t),i=function(r){if(r>=a.length)return null;var n=typeof AbortController=="function"?new AbortController:null,o=n&&setTimeout(function(){n.abort()},2e3);return fetch(a[r]+"/system/unfurl.json?url="+encodeURIComponent(e),n?{signal:n.signal}:{}).then(function(s){return s.ok?s.json():null}).catch(function(){return null}).then(function(s){return o&&clearTimeout(o),s&&typeof s=="object"&&"title"in s?(s.outpost=a[r],s):i(r+1)})};return i(0)})}function it(e){var t=e.image?'<img src="'+e.image+'" style="width:100%;max-height:240px;object-fit:cover;display:block">':"",a=e.provider?'<div style="font-size:12px;opacity:.7">'+c(e.provider)+"</div>":"",i=e.description?'<div style="font-size:13px;opacity:.85;margin-top:4px">'+c(e.description)+"</div>":"";return'<div style="border:1px solid rgba(128,128,128,.35);border-radius:8px;overflow:hidden;max-width:480px">'+t+'<div style="padding:8px 10px">'+a+'<div style="font-weight:600;font-size:15px"><a href="'+c(e.url)+'" target="_blank" rel="noopener">'+c(e.title||e.url)+"</a></div>"+i+'<div style="font-size:11px;opacity:.55;margin-top:6px;word-break:break-all">'+c(e.url)+"</div></div></div>"}var w=null;function at(){return w?Promise.resolve(w):fetch(location.origin+"/system/farm-domains.json").then(function(e){return e.ok?e.json():{local:[],public:[]}}).then(function(e){return w=e,e}).catch(function(){return w={local:[],public:[]},w})}function rt(e,t){return t&&t.local&&t.local.indexOf(e)!==-1?"local":t&&t.public&&t.public.indexOf(e)!==-1?"public":"federation"}function E(e){return Array.isArray(e)?e:e&&Array.isArray(e.data)?e.data:null}function nt(e,t){return new Promise(function(a){var i=function(n){if(!n)return a({isWiki:!1,exists:!1,title:""});for(var o=null,s=0;s<n.length;s++)if(n[s]&&n[s].slug===t){o=n[s];break}a({isWiki:!0,exists:!!o,title:o?o.title||t:""})};try{var r=window.wiki&&window.wiki.neighborhood&&window.wiki.neighborhood[e];if(r&&r.sitemap)return i(E(r.sitemap));window.wiki.site(e).get("system/sitemap.json",function(n,o){i(n?null:E(o))})}catch{a({isWiki:!1,exists:!1,title:""})}})}function L(e,t){t.kind==="page"?Promise.all([nt(t.site,t.slug),at()]).then(function(a){var i=a[0];i.isWiki?(t.wikiTitle=i.title||t.slug,t.pageExists=i.exists,t.farm=rt(t.site,a[1]),t.kind=t.farm==="federation"?"federation-page":"farm-page",y(e,t,null)):(t.kind="link",O(t.url).then(function(r){y(e,t,r)}))}):t.url?O(t.url).then(function(a){y(e,t,a)}):y(e,t,null)}function y(e,t,a){var i=window.wiki,r=e.data("data")||{},n=r.title||e.attr("id")||"this page",o={"farm-page":"a page on our farm","federation-page":"a page in the wider federation",site:"a wiki site",link:"a link / URL",text:"plain text",file:"a file"}[t.kind]||"an unrecognised drop",s=function(d){return(d||"").slice(0,2e3)},u=[{type:"markdown",id:l(),text:"# \u{1F4E5} Dropped onto "+n+`
51
7
 
52
- var RECENT_KEY = 'wiki-ux-recent-plugins'
53
- var RECENT_MAX = 6
8
+ You dropped **`+o+"** onto its flag."}];if(t.kind==="farm-page"||t.kind==="federation-page"){var x=t.farm==="local"?"our **local** farm":t.farm==="public"?"our **public** farm":"the **wider federation**";u.push({type:"markdown",id:l(),text:"# "+(t.wikiTitle||t.slug)+`
54
9
 
55
- // ≥44px tap target — the Tools page must work on a phone (same standard as
56
- // the hitchhiker item's big fork button in hitchhiker.js).
57
- var BTN = 'font-size:1.05em;min-height:44px;padding:10px 18px;cursor:pointer;'
58
- // GitHub "repo-forked" glyph — the standard fork icon (not the ⚑ flag).
59
- var FORK_SVG =
60
- '<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor" ' +
61
- 'style="vertical-align:-0.15em;margin-right:.4em" aria-hidden="true">' +
62
- '<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75v-.878a2.25 2.25 0 1 1 1.5 0v.878a2.25 ' +
63
- '2.25 0 0 1-2.25 2.25h-1.5v2.128a2.251 2.251 0 1 1-1.5 0V8.5h-1.5A2.25 2.25 0 0 1 3.5 6.25v-.878a2.25 2.25 0 1 1 ' +
64
- '1.5 0ZM5 3.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Zm6.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm-3 ' +
65
- '8.75a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0Z"></path></svg>'
10
+ A wiki page on `+x+" \u2014 `"+c(t.site)+"`."+(t.pageExists?"":`
66
11
 
67
- // Set by detectSimilarity(): is wiki-plugin-similarity installed on this farm?
68
- var simInstalled = false
12
+ _(not listed in the site sitemap \u2014 may be new or removed)_`)}),u.push({type:"reference",id:l(),site:t.site,slug:t.slug,title:t.wikiTitle||t.slug,text:t.farm==="federation"?"federated page":"our page"})}a&&(a.title||a.image)&&u.push({type:"html",id:l(),text:it(a)}),u.push({type:"markdown",id:l(),text:"# Details"}),t.kind==="farm-page"||t.kind==="federation-page"?u.push({type:"markdown",id:l(),text:`# Wiki page
69
13
 
70
- // Per-host curation. Default: show everything. `hide` removes plugins from the
71
- // chooser for a host; `recommend` floats them to the top. Host '*' applies to all.
72
- var CURATION = {
73
- '*': { recommend: ['paragraph', 'markdown', 'image', 'html', 'reference'], hide: [] },
74
- }
14
+ - site: \``+c(t.site)+"`\n- slug: `"+c(t.slug)+"`\n- farm: `"+c(t.farm)+"`\n- in sitemap: `"+(t.pageExists?"yes":"no")+"`"}):t.kind==="site"&&u.push({type:"markdown",id:l(),text:`# Wiki site
75
15
 
76
- // ---- boot: wait for the client surface, then install once -----------------
77
- var installed = false
78
- var tries = 0
79
- var timer = setInterval(function () {
80
- tries++
81
- var ready =
82
- window.wiki &&
83
- window.plugins &&
84
- window.plugins.factory &&
85
- window.plugins.factory.bind &&
86
- window.$ &&
87
- typeof window.wiki.pageHandler !== 'undefined'
88
- if (ready && !installed) {
89
- installed = true
90
- clearInterval(timer)
91
- try {
92
- install()
93
- } catch (e) {
94
- console.error('[wiki-ux] install failed', e)
95
- }
96
- } else if (tries > 100) {
97
- clearInterval(timer) // ~20s; give up quietly
98
- }
99
- }, 200)
16
+ \``+c(t.site)+"` \u2014 [welcome](//"+c(t.site)+"/view/welcome-visitors)"}),t.url&&u.push({type:"markdown",id:l(),text:`# URL
100
17
 
101
- function install() {
102
- installFactoryChooser()
103
- installExtraDrops()
104
- installStyles()
105
- installFlagToolPage()
106
- installDropTarget()
107
- installToolButtons()
108
- installFishFlag()
109
- installHearthGuard()
110
- detectSimilarity()
111
- reEmitOpenFactories()
112
- console.log('[wiki-ux] installed: chooser + extra-drops + flag tool page + fish picker + hearth guard')
113
- }
18
+ \``+t.url+"`"}),t.text&&t.text!==t.url&&u.push({type:"markdown",id:l(),text:"# Text\n\n```\n"+s(t.text)+"\n```"}),t.html&&u.push({type:"markdown",id:l(),text:"# HTML\n\n```\n"+s(c(t.html))+"\n```"}),t.files.length&&u.push({type:"markdown",id:l(),text:`# Files
114
19
 
115
- // ===========================================================================
116
- // Hearth guard — a page carrying a `hearth` item keeps to its home farm.
117
- // The mark (halo + notice) is rendered by wiki-plugin-hearth; the teeth are
118
- // here, farm-wide: block dragging its flag out, and refuse to import a
119
- // hearth-marked page dropped in. Enforcement is total only where our client
120
- // runs — outside, the item's own text is the ask. See hearth-pages spec.
121
- // ===========================================================================
122
- function pageIsHearth($page) {
123
- // rendered marker (hearth item emit adds the class) or raw story fallback
124
- if ($page.hasClass('hearth-page') || $page.find('.item.hearth').length) return true
125
- try {
126
- var po = window.wiki.lineup.atKey($page.data('key'))
127
- var story = po && po.getRawPage && po.getRawPage().story
128
- return !!(story && story.some(function (i) { return i.type === 'hearth' }))
129
- } catch (e) {
130
- return false
131
- }
132
- }
20
+ `+t.files.map(function(d){return"- "+c(d.name)+" ("+c(d.type||"?")+", "+d.size+" bytes)"}).join(`
21
+ `)}),u.push({type:"markdown",id:l(),text:`# Raw dataTransfer types
133
22
 
134
- // Does the page JSON at site/slug carry a hearth item? Used for inbound drops.
135
- function fetchIsHearth(site, slug) {
136
- var url = /^https?:/.test(site) ? site : location.protocol + '//' + site
137
- return fetch(url + '/' + slug + '.json')
138
- .then(function (r) { return r.ok ? r.json() : null })
139
- .then(function (p) {
140
- return !!(p && p.story && p.story.some(function (i) { return i.type === 'hearth' }))
141
- })
142
- .catch(function () { return false })
143
- }
23
+ \``+c((t.types||[]).join(", ")||"(none)")+"`"}),m(e,i.newPage({title:n+" \u{1F4E5}",story:u,journal:[]}),{ghost:!0})}function ot(e){var t=window.$,a=window.wiki,i=t(e).closest(".page"),r=i.data("data")||{},n=r.title||i.attr("id")||"this page",o=i.attr("id"),s=location.host,u=window.isAuthenticated===!0,x=C(i,o,h(i)),d=[{type:"markdown",id:l(),text:`# Simplify
144
24
 
145
- function hearthToast(msg) {
146
- if (window.wiki && window.wiki.showResult) {
147
- // reuse the client's own flash if present; else a minimal toast
148
- }
149
- var t = document.createElement('div')
150
- t.textContent = '\u{1F3E0} ' + msg
151
- t.style.cssText =
152
- 'position:fixed;left:50%;bottom:28px;transform:translateX(-50%);z-index:99999;' +
153
- 'background:rgba(200,120,40,0.95);color:#fff;padding:10px 16px;border-radius:8px;' +
154
- 'font:14px/1.4 -apple-system,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.3);max-width:80vw;'
155
- document.body.appendChild(t)
156
- setTimeout(function () { t.remove() }, 4200)
157
- }
25
+ Collapse the lineup down to just **Welcome Visitors** and **`+n+"**, clearing away the trail of pages you clicked through to get here."},{type:"html",id:l(),text:'<p><a class="wiki-ux-tool-simplify" href="'+c(x)+'"><button style="'+p+'">\u2302 Simplify to Welcome Visitors + '+c(n)+"</button></a></p>"}],I=k?[{type:"markdown",id:l(),text:`# \u{1F527} Tool pages
158
26
 
159
- function installHearthGuard() {
160
- // outbound: block the drag of a hearth page's flag before it can leave
161
- document.addEventListener(
162
- 'dragstart',
163
- function (e) {
164
- var a = e.target && e.target.closest ? e.target.closest('.page h1 a') : null
165
- if (!a) return
166
- if (pageIsHearth(window.$(a).closest('.page'))) {
167
- e.preventDefault()
168
- e.stopImmediatePropagation()
169
- hearthToast('This page keeps to its hearth — it will not be dragged out.')
170
- }
171
- },
172
- true,
173
- )
174
- }
27
+ Open a tool page beside **`+n+"** \u2014 its searches take this page as their subject."},{type:"html",id:l(),text:'<p><button class="wiki-ux-tool-toolpage" data-slug="similar-page-tool" style="'+p+'">\u{1F50D} Similar Page Tool</button> <button class="wiki-ux-tool-toolpage" data-slug="search-related-page" style="'+p+'">\u{1F5C2}\uFE0F Search Related Page</button></p>'}]:[],f,b;if(u){b=n+" \u{1F6E0}\uFE0F";var T=i.hasClass("remote"),lt=i.hasClass("local"),ut=T||lt,ct=h(i);if(f=[{type:"paragraph",id:l(),text:"Tools for **"+n+"**."}],ut){var ft=T?"This page lives on **"+(ct||"another wiki")+"** \u2014 the blue halo. **Forking** copies it onto **"+s+"**, so you get your own editable version here and it joins your wiki. The original stays where it is; forking never changes it.":"This page has **unsaved local changes** \u2014 the yellow halo of death (it only exists in this browser). **Forking** writes it to the server here on **"+s+"**, making it a real, saved, shareable page.";f.push({type:"markdown",id:l(),text:`# Fork this page
175
28
 
176
- // ===========================================================================
177
- // 1. Factory chooser — replace emit only, keep the original bind
178
- // ===========================================================================
179
- var origFactoryEmit = window.plugins.factory.emit
180
- var origFactoryBind = window.plugins.factory.bind
29
+ `+ft}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-fork" style="'+p+'">'+z+"Fork this page to "+c(s)+"</button></p>"})}k&&(f.push({type:"markdown",id:l(),text:`# \u{1F50D} Similarity search
181
30
 
182
- // Factory display style is OWNED BY wiki-plugin-new (the 'wiki-new'
183
- // localStorage record, set from a `new` item's FACTORY buttons/DSL).
184
- // 'classic' — the default, and the fallback on any error — is the untouched
185
- // built-in factory menu. 'list' (né 'chooser') is the searchable list with
186
- // per-plugin summaries; 'rollover' is tabs + pills with a hover description.
187
- function factoryStyle() {
188
- try {
189
- var d = JSON.parse(localStorage.getItem('wiki-new') || '{}')
190
- if (d.factory === 'list' || d.factory === 'chooser') return 'list'
191
- if (d.factory === 'rollover') return 'rollover'
192
- return 'classic'
193
- } catch (e) {
194
- return 'classic'
195
- }
196
- }
31
+ Find pages across the whole farm that are semantically similar to **`+n+"** \u2014 seeded by this page itself; opens a ranked ghost report you can read and fork."}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-sim" style="'+p+'">\u{1F50D} Similarity search</button></p>'})),f=f.concat(I),f.push({type:"markdown",id:l(),text:`# Trash this page
197
32
 
198
- function installFactoryChooser() {
199
- window.plugins.factory.emit = function ($item, item) {
200
- // mirror the wrapper's marker class, then render the selected style
201
- $item.addClass('emit')
202
- var style = factoryStyle()
203
- if (style === 'classic') {
204
- return origFactoryEmit.call(this, $item, item)
205
- }
206
- try {
207
- if (style === 'rollover') rolloverEmit($item, item)
208
- else chooserEmit($item, item)
209
- } catch (e) {
210
- console.error('[wiki-ux] factory skin "' + style + '" failed, falling back', e)
211
- return origFactoryEmit.call(this, $item, item)
212
- }
213
- }
214
- }
33
+ Move **`+n+"** to the recycler. It disappears from the site but is not destroyed \u2014 recover it any time from [[Recycled Pages]]."}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-trash" style="'+p+'">\u{1F5D1}&#65039; Trash this page</button></p>'}),f.push({type:"markdown",id:l(),text:`# Clean the journal
215
34
 
216
- // ── Plugin summaries — the first paragraph of each About page ─────────────
217
- // The 2-3 line "what does this plugin do" text shown by the list and
218
- // rollover skins. Source of truth is the plugin's own about page
219
- // (about-<type>-plugin, served same-origin from the plugin package);
220
- // factory.json's one-line title is the instant fallback while it loads.
221
- // Cached in localStorage for a day, truncated at a word boundary.
35
+ Open a fresh **ghost** copy of this page whose journal is stripped back to a single "create" entry \u2014 as if written today, with no fork trail or remote attribution. Fork that ghost to keep the cleaned version.`}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-clean" style="'+p+'">\u{1F9F9} Clean the journal</button></p>'}),window.isOwner===!0&&!T&&(f.push({type:"markdown",id:l(),text:`# \u{1F41F} Site flag
222
36
 
223
- var SUMMARY_KEY = 'wiki-ux-plugin-summaries'
224
- var SUMMARY_TTL = 24 * 3600 * 1000
225
- var SUMMARY_MAX = 240
226
-
227
- function readSummaries() {
228
- try {
229
- return JSON.parse(localStorage.getItem(SUMMARY_KEY) || '{}')
230
- } catch (e) {
231
- return {}
232
- }
233
- }
234
- function writeSummaries(map) {
235
- try {
236
- localStorage.setItem(SUMMARY_KEY, JSON.stringify(map))
237
- } catch (e) {}
238
- }
239
-
240
- // Flatten fedwiki markdown to plain prose and cap it at ~3 small-print lines.
241
- function summarize(text) {
242
- var t = (text || '')
243
- .replace(/\[\[([^\]]+)\]\]/g, '$1') // [[Page]] → Page
244
- .replace(/\[(?:https?:\/\/\S+)\s+([^\]]+)\]/g, '$1') // [url label] → label
245
- .replace(/[*_`#>]/g, '') // emphasis / heading marks
246
- .replace(/\s+/g, ' ')
247
- .trim()
248
- if (t.length <= SUMMARY_MAX) return t
249
- var cut = t.slice(0, SUMMARY_MAX)
250
- return cut.slice(0, cut.lastIndexOf(' ')) + ' …'
251
- }
252
-
253
- // cb(text) fires at once with the best text to hand; fires again with the
254
- // about-page paragraph when a fresh fetch lands.
255
- function pluginSummary(entry, cb) {
256
- var key = entry.name.toLowerCase()
257
- var cache = readSummaries()
258
- var hit = cache[key]
259
- if (hit && Date.now() - hit.at < SUMMARY_TTL) {
260
- cb(hit.text)
261
- return
262
- }
263
- cb(summarize(entry.title || ''))
264
- var slug = 'about-' + key.replace(/\s+/g, '-') + '-plugin'
265
- fetch('/' + slug + '.json')
266
- .then(function (r) {
267
- return r.ok ? r.json() : null
268
- })
269
- .then(function (page) {
270
- var first =
271
- page &&
272
- (page.story || []).find(function (i) {
273
- return (i.type === 'markdown' || i.type === 'paragraph') && (i.text || '').trim()
274
- })
275
- var text = first ? summarize(first.text) : summarize(entry.title || '')
276
- if (!text) return
277
- var c = readSummaries()
278
- c[key] = { text: text, at: Date.now() }
279
- writeSummaries(c)
280
- cb(text)
281
- })
282
- .catch(function () {})
283
- }
284
-
285
- function curationFor(host) {
286
- var base = CURATION['*'] || { recommend: [], hide: [] }
287
- var site = CURATION[host] || {}
288
- return {
289
- recommend: (site.recommend || base.recommend || []).slice(),
290
- hide: (base.hide || []).concat(site.hide || []),
291
- }
292
- }
293
-
294
- function getRecent() {
295
- try {
296
- return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]')
297
- } catch (e) {
298
- return []
299
- }
300
- }
301
- function pushRecent(type) {
302
- try {
303
- var r = getRecent().filter(function (t) {
304
- return t !== type
305
- })
306
- r.unshift(type)
307
- localStorage.setItem(RECENT_KEY, JSON.stringify(r.slice(0, RECENT_MAX)))
308
- } catch (e) {}
309
- }
310
-
311
- // Shared stylesheet for the list and rollover factory skins.
312
- var FACTORY_CSS =
313
- '<style id="wiki-ux-factory-styles">' +
314
- '.wiki-ux-chooser, .wiki-ux-rollover { font-family: sans-serif; }' +
315
- '.wiki-ux-hint { font-size: 11.5px; color: #999; margin: 0 0 4px; }' +
316
- '.wiki-ux-group h4 { margin: 10px 0 3px; font-size: .78em; letter-spacing: .06em;' +
317
- ' text-transform: uppercase; color: #999; font-weight: normal; }' +
318
- '.wiki-ux-group ul { list-style: none; margin: 0; padding: 0; }' +
319
- '.wiki-ux-row { margin: 0 0 7px; }' +
320
- '.wiki-ux-pick { font-size: 14px; color: #3465a4; text-decoration: none; font-weight: normal; }' +
321
- '.wiki-ux-pick:hover { text-decoration: underline; }' +
322
- '.wiki-ux-sum { font-size: 11.5px; line-height: 1.35; color: #888; margin-top: 1px;' +
323
- ' display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }' +
324
- '.wiki-ux-rollover { padding: .5em .8em; background: #f4f4f8; border-left: 3px solid #888; }' +
325
- '.wiki-ux-tabs { display: flex; gap: 4px; margin-bottom: .5em; }' +
326
- '.wiki-ux-tab { padding: .2em .7em; border: 1px solid #bbb; border-radius: 3px;' +
327
- ' background: #fff; cursor: pointer; font-size: .85em; color: #444; }' +
328
- '.wiki-ux-tab:hover { background: #e8e8f0; }' +
329
- '.wiki-ux-tab.wiki-ux-active { background: #5b6fd9; color: #fff; border-color: #5b6fd9; }' +
330
- '.wiki-ux-pills { min-height: 2em; }' +
331
- '.wiki-ux-pillbtn { display: inline-block; padding: .15em .5em; margin: 2px 1px;' +
332
- ' background: #dde; border: 1px solid transparent; border-radius: 3px;' +
333
- ' font-size: .8em; color: #334; cursor: pointer; }' +
334
- '.wiki-ux-pillbtn:hover { background: #5b6fd9; color: #fff; }' +
335
- '.wiki-ux-rollover-info { margin-top: .6em; padding-top: .5em; border-top: 1px solid #ddd;' +
336
- ' font-size: 11.5px; line-height: 1.4; color: #777; min-height: 3.2em; }' +
337
- '.wiki-ux-rollover-info .wiki-ux-info-name { color: #444; }' +
338
- '</style>'
339
-
340
- function injectFactoryCss() {
341
- if (!document.getElementById('wiki-ux-factory-styles'))
342
- document.head.insertAdjacentHTML('beforeend', FACTORY_CSS)
343
- }
344
-
345
- function chooserEmit($item, item) {
346
- var $ = window.$
347
- var wiki = window.wiki
348
- injectFactoryCss()
349
-
350
- // A labelled factory (item.prompt) keeps the built-in prompt behaviour.
351
- if (item.prompt) {
352
- return origFactoryEmit.call(window.plugins.factory, $item, item)
353
- }
354
-
355
- var render = function (catalog) {
356
- var cur = curationFor(location.host)
357
- var recent = getRecent()
358
- var entries = (catalog || []).filter(function (info) {
359
- return info && info.name && info.category && cur.hide.indexOf(info.name.toLowerCase()) === -1
360
- })
361
-
362
- $item.html(
363
- '' +
364
- '<div class="wiki-ux-chooser">' +
365
- '<p class="wiki-ux-hint">Double-click to edit · drop text, an image or a link · or choose a plugin</p>' +
366
- '<input class="wiki-ux-search" type="text" placeholder="Search plugins…" ' +
367
- 'style="width:100%;box-sizing:border-box;margin:4px 0;padding:4px 6px;">' +
368
- '<div class="wiki-ux-groups"></div>' +
369
- '</div>',
370
- )
371
-
372
- var $groups = $item.find('.wiki-ux-groups')
373
-
374
- var byName = {}
375
- entries.forEach(function (e) {
376
- byName[e.name.toLowerCase()] = e
377
- })
378
- var recommended = cur.recommend
379
- .concat(recent)
380
- .map(function (t) {
381
- return byName[t]
382
- })
383
- .filter(Boolean)
384
- // de-dupe, preserve order
385
- var seen = {}
386
- recommended = recommended.filter(function (e) {
387
- var k = e.name.toLowerCase()
388
- if (seen[k]) return false
389
- seen[k] = 1
390
- return true
391
- })
392
-
393
- var drawList = function (filter) {
394
- var f = (filter || '').trim().toLowerCase()
395
- var match = function (e) {
396
- if (!f) return true
397
- return (e.name + ' ' + (e.title || '')).toLowerCase().indexOf(f) !== -1
398
- }
399
- var sections = []
400
- if (!f && recommended.length) {
401
- sections.push({ label: 'Suggested', list: recommended.filter(match) })
402
- }
403
- var cats = { format: [], data: [], other: [] }
404
- entries.forEach(function (e) {
405
- if (!match(e)) return
406
- var c = e.category === 'format' || e.category === 'data' ? e.category : 'other'
407
- cats[c].push(e)
408
- })
409
- sections.push({ label: 'Format', list: cats.format })
410
- sections.push({ label: 'Data', list: cats.data })
411
- sections.push({ label: 'Other', list: cats.other })
412
-
413
- var html = sections
414
- .filter(function (s) {
415
- return s.list.length
416
- })
417
- .map(function (s) {
418
- var rows = s.list
419
- .map(function (e) {
420
- // Plain link, no bold; the 2-3 line summary sits under it in
421
- // small print (about-page first paragraph, async-filled).
422
- return (
423
- '<li class="wiki-ux-row"><a class="wiki-ux-pick" href="#" ' +
424
- 'data-type="' + e.name.toLowerCase() + '" ' +
425
- 'data-name="' + esc(e.name) + '">' + esc(e.name) + '</a>' +
426
- '<div class="wiki-ux-sum" data-sum-for="' + esc(e.name) + '">' +
427
- esc(summarize(e.title || '')) + '</div></li>'
428
- )
429
- })
430
- .join('')
431
- return (
432
- '<div class="wiki-ux-group"><h4>' + s.label + '</h4><ul>' + rows + '</ul></div>'
433
- )
434
- })
435
- .join('')
436
- $groups.html(html || '<p style="opacity:.6;">No matching plugins.</p>')
437
-
438
- // Upgrade each row's summary to the about-page first paragraph.
439
- var listed = {}
440
- sections.forEach(function (s) {
441
- s.list.forEach(function (e) {
442
- listed[e.name] = e
443
- })
444
- })
445
- Object.keys(listed).forEach(function (name) {
446
- pluginSummary(listed[name], function (text) {
447
- $groups.find('.wiki-ux-sum[data-sum-for="' + name + '"]').text(text)
448
- })
449
- })
450
- }
451
-
452
- drawList('')
453
-
454
- $item.find('.wiki-ux-search').on('input', function () {
455
- drawList(this.value)
456
- })
457
-
458
- $item.on('click', 'a.wiki-ux-pick', function (evt) {
459
- evt.preventDefault()
460
- var type = this.getAttribute('data-type')
461
- var name = this.getAttribute('data-name')
462
- choose($item, item, type, name, catalog)
463
- })
464
- }
465
-
466
- if (window.catalog) {
467
- render(window.catalog)
468
- } else {
469
- wiki.origin.get('system/factories.json', function (err, data) {
470
- window.catalog = data || []
471
- render(window.catalog)
472
- })
473
- }
474
- }
475
-
476
- // Rollover skin — the wiki-plugin-new look: tabbed category sections, pill
477
- // buttons, and a small-print display at the foot that fills with a plugin's
478
- // 2-3 line summary (its about page's first paragraph) as you roll over the
479
- // pills. Click a pill to choose.
480
- function rolloverEmit($item, item) {
481
- var wiki = window.wiki
482
- injectFactoryCss()
483
-
484
- // A labelled factory (item.prompt) keeps the built-in prompt behaviour.
485
- if (item.prompt) {
486
- return origFactoryEmit.call(window.plugins.factory, $item, item)
487
- }
488
-
489
- var render = function (catalog) {
490
- var cur = curationFor(location.host)
491
- var recent = getRecent()
492
- var entries = (catalog || []).filter(function (info) {
493
- return info && info.name && info.category && cur.hide.indexOf(info.name.toLowerCase()) === -1
494
- })
495
-
496
- var byName = {}
497
- entries.forEach(function (e) {
498
- byName[e.name.toLowerCase()] = e
499
- })
500
- var suggested = cur.recommend
501
- .concat(recent)
502
- .map(function (t) {
503
- return byName[t]
504
- })
505
- .filter(Boolean)
506
- var seen = {}
507
- suggested = suggested.filter(function (e) {
508
- var k = e.name.toLowerCase()
509
- if (seen[k]) return false
510
- seen[k] = 1
511
- return true
512
- })
513
-
514
- var cats = { format: [], data: [], other: [] }
515
- entries.forEach(function (e) {
516
- var c = e.category === 'format' || e.category === 'data' ? e.category : 'other'
517
- cats[c].push(e)
518
- })
519
- var tabs = []
520
- if (suggested.length) tabs.push({ key: 'suggested', label: 'Suggested', list: suggested })
521
- tabs.push({ key: 'format', label: 'Format', list: cats.format })
522
- tabs.push({ key: 'data', label: 'Data', list: cats.data })
523
- tabs.push({ key: 'other', label: 'Other', list: cats.other })
524
- tabs = tabs.filter(function (t) {
525
- return t.list.length
526
- })
527
-
528
- var DEFAULT_INFO = 'Roll over a plugin to see what it does — click to choose. ' +
529
- 'Double-click the box to write plain text; drop text, an image or a link.'
530
-
531
- $item.html(
532
- '<div class="wiki-ux-rollover">' +
533
- '<div class="wiki-ux-tabs">' +
534
- tabs
535
- .map(function (t, i) {
536
- return (
537
- '<button class="wiki-ux-tab' + (i === 0 ? ' wiki-ux-active' : '') +
538
- '" data-tab="' + t.key + '">' + t.label + '</button>'
539
- )
540
- })
541
- .join('') +
542
- '</div>' +
543
- '<div class="wiki-ux-pills"></div>' +
544
- '<div class="wiki-ux-rollover-info">' + DEFAULT_INFO + '</div>' +
545
- '</div>',
546
- )
547
-
548
- var $pills = $item.find('.wiki-ux-pills')
549
- var $info = $item.find('.wiki-ux-rollover-info')
550
-
551
- var drawPills = function (key) {
552
- var tab = tabs.find(function (t) {
553
- return t.key === key
554
- })
555
- $pills.html(
556
- (tab ? tab.list : [])
557
- .map(function (e) {
558
- return (
559
- '<button class="wiki-ux-pillbtn" data-type="' + e.name.toLowerCase() +
560
- '" data-name="' + esc(e.name) + '">' + esc(e.name) + '</button>'
561
- )
562
- })
563
- .join(' '),
564
- )
565
- }
566
- drawPills(tabs.length ? tabs[0].key : null)
567
-
568
- $item.on('click', '.wiki-ux-tab', function () {
569
- $item.find('.wiki-ux-tab').removeClass('wiki-ux-active')
570
- this.classList.add('wiki-ux-active')
571
- drawPills(this.getAttribute('data-tab'))
572
- $info.html(DEFAULT_INFO)
573
- })
574
-
575
- $item.on('mouseenter', '.wiki-ux-pillbtn', function () {
576
- var name = this.getAttribute('data-name')
577
- var entry = byName[name.toLowerCase()]
578
- if (!entry) return
579
- pluginSummary(entry, function (text) {
580
- $info.html('<span class="wiki-ux-info-name">' + esc(name) + '</span> — ' + esc(text))
581
- })
582
- })
583
-
584
- $item.on('click', '.wiki-ux-pillbtn', function (evt) {
585
- evt.preventDefault()
586
- choose($item, item, this.getAttribute('data-type'), this.getAttribute('data-name'), catalog)
587
- })
588
- }
589
-
590
- if (window.catalog) {
591
- render(window.catalog)
592
- } else {
593
- wiki.origin.get('system/factories.json', function (err, data) {
594
- window.catalog = data || []
595
- render(window.catalog)
596
- })
597
- }
598
- }
599
-
600
- function choose($item, item, type, name, catalog) {
601
- var wiki = window.wiki
602
- pushRecent(type)
603
- $item.removeClass('factory').addClass((item.type = type))
604
- $item.off()
605
- var entry = (catalog || []).find(function (e) {
606
- return e.name === name
607
- })
608
- // load the plugin, then hand off to its editor (or the plain text editor)
609
- wiki.getPlugin(type, function () {
610
- try {
611
- if (entry && entry.editor && window.plugins[type] && window.plugins[type].editor) {
612
- window.plugins[type].editor($item, item)
613
- } else {
614
- wiki.textEditor($item, item)
615
- }
616
- } catch (e) {
617
- console.log('[wiki-ux] editor failed, falling back to textEditor:', e)
618
- wiki.textEditor($item, item)
619
- }
620
- })
621
- }
622
-
623
- // ===========================================================================
624
- // 2. Extra drops — wrap bind, add a capture-phase handler for media URLs
625
- // ===========================================================================
626
- function installExtraDrops() {
627
- window.plugins.factory.bind = function ($item, item) {
628
- var ret = origFactoryBind.call(this, $item, item)
629
- try {
630
- attachExtraDrop($item, item)
631
- } catch (e) {
632
- console.error('[wiki-ux] extra-drop attach failed', e)
633
- }
634
- return ret
635
- }
636
- }
637
-
638
- function attachExtraDrop($item, item) {
639
- var el = $item[0]
640
- if (!el) return
641
- el.addEventListener(
642
- 'drop',
643
- function (e) {
644
- // never touch file/image drops or internal page-flag drags — let the
645
- // original factory bind (drop.js) handle those.
646
- if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files.length) return
647
- var uri =
648
- (e.dataTransfer && (e.dataTransfer.getData('text/uri-list') || e.dataTransfer.getData('text'))) || ''
649
- uri = uri.trim()
650
- if (!/^https?:\/\//i.test(uri)) return
651
- var recognised = classifyUrl(uri)
652
- if (!recognised) return // fall through to the original handler
653
- e.preventDefault()
654
- e.stopImmediatePropagation()
655
- becomeType($item, item, recognised.type, recognised.fields)
656
- },
657
- true, // capture — runs before the jQuery bubble-phase handler
658
- )
659
- }
660
-
661
- function classifyUrl(url) {
662
- if (/(?:youtube\.com\/watch|youtu\.be\/|vimeo\.com\/\d)/i.test(url)) {
663
- return { type: 'video', fields: { text: url + '\n(double-click to edit caption)\n' } }
664
- }
665
- if (/\.(mp3|ogg|wav|m4a|flac)(\?|#|$)/i.test(url)) {
666
- return { type: 'audio', fields: { text: url } }
667
- }
668
- if (/\.pdf(\?|#|$)/i.test(url)) {
669
- return { type: 'reference', fields: { site: location.host, text: 'PDF: ' + url } }
670
- }
671
- return null
672
- }
673
-
674
- // Replicates factory.js syncEditAction using only the public wiki surface.
675
- function becomeType($item, item, type, fields) {
676
- var wiki = window.wiki
677
- var $ = window.$
678
- Object.keys(fields || {}).forEach(function (k) {
679
- item[k] = fields[k]
680
- })
681
- item.type = type
682
- var $page = $item.parents('.page:first')
683
- $item.empty().off().removeClass('factory').addClass(type)
684
- $item.data('pageElement', $page)
685
- $item.data('item', item)
686
- wiki.getPlugin(type, function (plugin) {
687
- if (plugin && plugin.emit) plugin.emit($item, item)
688
- if (plugin && plugin.bind) plugin.bind($item, item)
689
- })
690
- wiki.pageHandler.put($page, { type: 'edit', id: item.id, item: item })
691
- }
692
-
693
- // ===========================================================================
694
- // 3. Page-flag click → in-lineup Tools page
695
- // ===========================================================================
696
- function installFlagToolPage() {
697
- document.addEventListener(
698
- 'click',
699
- function (e) {
700
- var t = e.target
701
- var a = t && t.closest ? t.closest('.page h1 a') : null
702
- if (!a) return
703
- e.preventDefault()
704
- e.stopImmediatePropagation() // pre-empt the built-in handleHeaderClick
705
- // A Tools page's own flag is a no-op (don't recurse into Tools: Tools:…).
706
- if (a.closest('.page.wiki-ux-tools')) return
707
- // Shift-click is a shortcut straight to Simplify (skip the Tools page).
708
- if (e.shiftKey) {
709
- try {
710
- doSimplify(a)
711
- } catch (err) {
712
- openOriginSite(a)
713
- }
714
- return
715
- }
716
- try {
717
- openFlagPage(a)
718
- } catch (err) {
719
- console.error('[wiki-ux] flag page failed, opening origin site instead', err)
720
- openOriginSite(a)
721
- }
722
- },
723
- true,
724
- )
725
- }
726
-
727
- // "Simplify" target: the lineup collapsed to welcome-visitors + this page.
728
- // Local pages resolve on this host; remote pages resolve on their origin site.
729
- function simplifyUrl($page, slug, remoteSite) {
730
- var wiki = window.wiki
731
- var path = slug === 'welcome-visitors' ? 'view/welcome-visitors' : 'view/welcome-visitors/view/' + slug
732
- if ($page.hasClass('remote')) {
733
- var site = remoteSite
734
- if (!site) {
735
- try {
736
- var po = wiki.lineup.atKey($page.data('key'))
737
- site = po && po.getRemoteSite && po.getRemoteSite()
738
- } catch (e) {}
739
- }
740
- if (site) return wiki.site(site).getDirectURL(path)
741
- }
742
- return '/' + path
743
- }
744
-
745
- // Shift-click shortcut on a page flag: navigate straight to the Simplify
746
- // lineup (Welcome Visitors + the clicked page), skipping the Tools page.
747
- function doSimplify(a) {
748
- var $src = window.$(a).closest('.page')
749
- var slug = $src.attr('id')
750
- window.location.assign(simplifyUrl($src, slug, remoteSiteOf($src)))
751
- }
752
-
753
- function remoteSiteOf($page) {
754
- if (!$page.hasClass('remote')) return ''
755
- try {
756
- var po = window.wiki.lineup.atKey($page.data('key'))
757
- return (po && po.getRemoteSite && po.getRemoteSite()) || $page.data('site') || ''
758
- } catch (e) {
759
- return $page.data('site') || ''
760
- }
761
- }
762
-
763
- // Insert a generated page into the lineup immediately to the right of $src,
764
- // preserving any pages already to its right (unlike showResult's wipe). The
765
- // lineup key order mirrors DOM order, so we position by index. Marks the page
766
- // green (plugin) and tags it so its own flag click is a no-op.
767
- function insertPageAfter($src, pageObject, opts) {
768
- opts = opts || {}
769
- var $ = window.$
770
- var wiki = window.wiki
771
- var srcIndex = $src.prevAll('.page').length // lineup index == DOM index
772
- wiki.showResult(pageObject) // no {$page} → append at end, wipe nothing
773
- var $new = $('.main > .page').last()
774
- $new.insertAfter($src) // move into position in the DOM
775
- try {
776
- wiki.lineup.changePageIndex($new.data('key'), srcIndex + 1) // and in the lineup
777
- } catch (e) {}
778
- $new.addClass('wiki-ux-tools') // its own flag click is a no-op
779
- if (!opts.ghost) $new.removeClass('ghost') // Tools/Navigate: crisp; report: stay ghost
780
- try {
781
- $new[0].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
782
- } catch (e) {}
783
- return $new
784
- }
785
-
786
- // One delegated handler for every Tools-page action button. Each button lives
787
- // in the Tools page; the page it acts on is the Tools page's previous sibling
788
- // in the lineup (showResult appends the Tools page right after its source).
789
- function installToolButtons() {
790
- document.addEventListener(
791
- 'click',
792
- function (e) {
793
- var el = e.target
794
- if (!el || !el.closest) return
795
- var kind = el.closest('.wiki-ux-tool-fork')
796
- ? 'fork'
797
- : el.closest('.wiki-ux-tool-sim')
798
- ? 'sim'
799
- : el.closest('.wiki-ux-tool-trash')
800
- ? 'trash'
801
- : el.closest('.wiki-ux-tool-clean')
802
- ? 'clean'
803
- : el.closest('.wiki-ux-tool-flag')
804
- ? 'flag'
805
- : el.closest('.wiki-ux-tool-toolpage')
806
- ? 'toolpage'
807
- : null
808
- if (!kind) return
809
- e.preventDefault()
810
- var $ = window.$
811
- var btn = el.closest('.wiki-ux-tool-' + kind)
812
- var $tools = $(btn).closest('.page')
813
- var $src = $tools.prev('.page')
814
- try {
815
- if (kind === 'fork') doFork($src, $tools)
816
- else if (kind === 'sim') doSimilarity($src, $tools)
817
- else if (kind === 'trash') doTrash($src, $tools)
818
- else if (kind === 'clean') doClean($src, $tools)
819
- else if (kind === 'flag') openFishPicker($src, $tools)
820
- else if (kind === 'toolpage') openToolPage($src, $tools, btn.getAttribute('data-slug'))
821
- } catch (err) {
822
- console.error('[wiki-ux] tool "' + kind + '" failed', err)
823
- }
824
- },
825
- true,
826
- )
827
- }
828
-
829
- // Tool pages: real, forkable wiki pages (Similar Page Tool, Search Related
830
- // Page) whose SUBJECT-mode similarity items act on the page BEFORE them in
831
- // the lineup. Fetched from this site first, then the canonical home, and
832
- // inserted directly after the HOST page — so the tool's previous sibling is
833
- // its subject. The Tools page closes; the tool has the context it needs.
834
- var TOOL_PAGE_HOME = 'demo.localhost'
835
- function openToolPage($src, $tools, slug) {
836
- if (!slug) return
837
- var fetchPage = function (site) {
838
- return fetch('//' + site + '/' + slug + '.json').then(function (r) {
839
- if (!r.ok) throw new Error('' + r.status)
840
- return r.json()
841
- })
842
- }
843
- fetchPage(location.host)
844
- .catch(function () { return fetchPage(TOOL_PAGE_HOME) })
845
- .then(function (page) {
846
- insertPageAfter($src, window.wiki.newPage(page), { ghost: true })
847
- $tools.remove()
848
- })
849
- .catch(function () {
850
- window.alert('Tool page "' + slug + '" not found on ' + location.host + ' or ' + TOOL_PAGE_HOME + '.')
851
- })
852
- }
853
-
854
- // Fork: reuse the source page's own native .fork-page control (still in the
855
- // lineup) — it already handles the local / remote / recycler cases. The
856
- // hitchhiker FORK item (client/hitchhiker.js) forks its own page the same
857
- // way; keep the two in step.
858
- function doFork($src, $tools) {
859
- var $forkBtn = $src.find('.fork-page')
860
- if ($forkBtn.length) {
861
- $forkBtn.trigger('click')
862
- $tools.remove() // close the Tools page; the forked source page shows
863
- return
864
- }
865
- // Logged in but the "wiki" edit toggle is off — the flag isn't rendered.
866
- // Fork directly with the same pageHandler call the flag makes; the
867
- // server authorizes by session, not by the toggle. (Same fallback as the
868
- // hitchhiker FORK item in hitchhiker.js — keep the two in step.)
869
- if (window.isAuthenticated !== true || /_rev\d+$/.test($src.attr('id') || '')) {
870
- console.warn('[wiki-ux] fork: no .fork-page control on the source page')
871
- return
872
- }
873
- var action = { type: 'fork' }
874
- if ($src.hasClass('local')) {
875
- $src.removeClass('local')
876
- } else {
877
- var site = remoteSiteOf($src)
878
- if (site) action.site = site
879
- }
880
- $src.removeClass('ghost')
881
- window.wiki.pageHandler.put($src, action)
882
- $tools.remove()
883
- }
884
-
885
- // Trash: move the page to the recycler via the exposed pageHandler.delete.
886
- // Recoverable from Recycled Pages — not a permanent delete. Confirm first.
887
- function doTrash($src, $tools) {
888
- var wiki = window.wiki
889
- var raw = $src.data('data') || {}
890
- var title = raw.title || $src.attr('id') || 'this page'
891
- if (!window.confirm('Trash "' + title + '"?\n\nIt moves to the recycler and can be recovered from Recycled Pages.')) return
892
- var pageObject
893
- try {
894
- pageObject = wiki.lineup.atKey($src.data('key'))
895
- } catch (e) {}
896
- if (!pageObject) {
897
- window.alert('Could not identify the page to trash.')
898
- return
899
- }
900
- wiki.pageHandler.delete(pageObject, $src, function (err) {
901
- if (err) {
902
- window.alert('Trash failed: ' + err)
903
- return
904
- }
905
- $tools.remove()
906
- $src.remove()
907
- })
908
- }
909
-
910
- // Clean the journal: open a GHOST copy whose journal is one clean create entry
911
- // (the fedwiki-journal convention). Same slug — fork the ghost to keep it.
912
- function doClean($src, $tools) {
913
- var wiki = window.wiki
914
- var raw = $src.data('data') || {}
915
- var title = raw.title || $src.attr('id') || 'this page'
916
- var story = raw.story || []
917
- var cleaned = {
918
- title: title,
919
- story: story,
920
- journal: [{ type: 'create', item: { title: title, story: story }, date: Date.now(), certificate: 'from marvin' }],
921
- }
922
- wiki.showResult(wiki.newPage(cleaned), { $page: $tools }) // ghost, opens after Tools
923
- }
924
-
925
- // Detect wiki-plugin-similarity via the aggregated factory catalog (this also
926
- // warms window.catalog for the chooser). Sets simInstalled for the Tools page.
927
- function detectSimilarity() {
928
- fetch('/system/factories.json')
929
- .then(function (r) {
930
- return r.ok ? r.json() : []
931
- })
932
- .then(function (cat) {
933
- if (!window.catalog) window.catalog = cat
934
- simInstalled = (cat || []).some(function (e) {
935
- return e && e.name === 'Similarity'
936
- })
937
- })
938
- .catch(function () {})
939
- }
940
-
941
- // Similarity power tool: pages like the source page, FARM-WIDE, opened as a
942
- // GHOST report. One POST to the similarity plugin's report route, seeded by
943
- // the page itself: the server prefers the page's stored vector (seed), falls
944
- // back to embedding the supplied text (title + opening prose) for remote or
945
- // not-yet-indexed pages, and excludes the page and its fork family from the
946
- // results. Replaces the old current-domain-only client-side scan, whose GET
947
- // embed fallback died with 431 on long pages.
948
- function doSimilarity($src, $tools) {
949
- var raw = $src.data('data') || {}
950
- var title = raw.title || $src.attr('id') || 'this page'
951
- var slug = ($src.attr('id') || '').split('_rev')[0]
952
- var site = remoteSiteOf($src) || location.hostname
953
- var $btn = $tools.find('.wiki-ux-tool-sim')
954
- $btn.prop('disabled', true).text('🔍 Searching…')
955
-
956
- // Embed fallback text: title + opening prose. Capped — the BGE-small
957
- // embedder truncates around 512 tokens anyway.
958
- var text = [title]
959
- .concat(
960
- (raw.story || [])
961
- .filter(function (i) { return i.type === 'markdown' || i.type === 'paragraph' })
962
- .map(function (i) { return (i.text || '').trim() })
963
- .filter(Boolean),
964
- )
965
- .join('\n')
966
- .slice(0, 2000)
967
-
968
- fetch(location.origin + '/system/search-report.json', {
969
- method: 'POST',
970
- headers: { 'Content-Type': 'application/json' },
971
- body: JSON.stringify({
972
- query: title,
973
- seed: { site: site, slug: slug },
974
- excludePage: { site: site, slug: slug },
975
- text: text,
976
- domains: ['*'],
977
- limit: 12,
978
- }),
979
- })
980
- .then(function (r) {
981
- if (!r.ok) throw new Error('search-report failed: ' + r.status)
982
- return r.json()
983
- })
984
- .then(function (page) {
985
- // Server-built GHOST report (stub-filtered, fork-bundled), after Tools.
986
- insertPageAfter($tools, window.wiki.newPage(page), { ghost: true })
987
- $btn.prop('disabled', false).text('🔍 Similarity search')
988
- })
989
- .catch(function (e) {
990
- console.error('[wiki-ux] similarity failed', e)
991
- $btn.prop('disabled', false).text('🔍 Similarity search')
992
- window.alert('Similarity search failed: ' + e.message)
993
- })
994
- }
995
-
996
- function originUrl($page) {
997
- var wiki = window.wiki
998
- var crumbs = wiki.lineup.crumbs($page.data('key'), location.host)
999
- var target = crumbs[0]
1000
- var prefix = wiki.site(target).getDirectURL('').split('/')[0]
1001
- if (prefix === '') prefix = window.location.protocol
1002
- return { url: prefix + '//' + crumbs.join('/'), target: target }
1003
- }
1004
-
1005
- function openOriginSite(a) {
1006
- var $ = window.$
1007
- var $page = $(a).closest('.page')
1008
- var o = originUrl($page)
1009
- window.open(o.url, o.target).focus()
1010
- }
1011
-
1012
- // ===========================================================================
1013
- // Drop TARGET: drop a URL / link / page-flag ONTO a page flag → ghost report.
1014
- // ===========================================================================
1015
- var DROP_CLASS = 'wiki-ux-drop-target'
1016
- var DROPPABLE_TYPES = ['text/uri-list', 'text/x-moz-url', 'text/plain', 'text/html', 'Files']
1017
-
1018
- function installDropTarget() {
1019
- var flagOf = function (t) {
1020
- return t && t.closest ? t.closest('.page h1 a') : null
1021
- }
1022
- var hasDroppable = function (dt) {
1023
- if (!dt || !dt.types) return false
1024
- for (var i = 0; i < dt.types.length; i++) if (DROPPABLE_TYPES.indexOf(dt.types[i]) !== -1) return true
1025
- return false
1026
- }
1027
- var clearHighlights = function () {
1028
- var els = document.querySelectorAll('.' + DROP_CLASS)
1029
- for (var i = 0; i < els.length; i++) els[i].classList.remove(DROP_CLASS)
1030
- }
1031
-
1032
- // dragover must preventDefault to allow a drop; getData is unreadable here,
1033
- // so we decide purely from dataTransfer.types.
1034
- document.addEventListener(
1035
- 'dragover',
1036
- function (e) {
1037
- var flag = flagOf(e.target)
1038
- if (!flag || !hasDroppable(e.dataTransfer)) return
1039
- e.preventDefault()
1040
- try {
1041
- e.dataTransfer.dropEffect = 'copy'
1042
- } catch (_) {}
1043
- if (!flag.classList.contains(DROP_CLASS)) {
1044
- clearHighlights()
1045
- flag.classList.add(DROP_CLASS)
1046
- }
1047
- },
1048
- true,
1049
- )
1050
- document.addEventListener(
1051
- 'dragleave',
1052
- function (e) {
1053
- var flag = flagOf(e.target)
1054
- if (flag && (!e.relatedTarget || !flag.contains(e.relatedTarget))) flag.classList.remove(DROP_CLASS)
1055
- },
1056
- true,
1057
- )
1058
- document.addEventListener(
1059
- 'drop',
1060
- function (e) {
1061
- var flag = flagOf(e.target)
1062
- clearHighlights()
1063
- if (!flag) return
1064
- e.preventDefault()
1065
- e.stopPropagation()
1066
- try {
1067
- var $target = window.$(flag).closest('.page')
1068
- var drop = decodeDrop(e.dataTransfer)
1069
- // inbound hearth guard: refuse to import a page that keeps to its hearth
1070
- if (drop.kind === 'page' && drop.site && drop.slug) {
1071
- fetchIsHearth(drop.site, drop.slug).then(function (isHearth) {
1072
- if (isHearth) {
1073
- hearthToast('That page keeps to its hearth — it declines to be copied here.')
1074
- } else {
1075
- buildDropReport($target, drop)
1076
- }
1077
- })
1078
- return
1079
- }
1080
- buildDropReport($target, drop)
1081
- } catch (err) {
1082
- console.error('[wiki-ux] drop failed', err)
1083
- }
1084
- },
1085
- true,
1086
- )
1087
- document.addEventListener('dragend', clearHighlights, true)
1088
- }
1089
-
1090
- // Mirrors wiki-client/lib/drop.js isUrl/isPage (that module is bundled/private).
1091
- function isPageUrl(url) {
1092
- var m = url.match(/^https?:\/\/([a-zA-Z0-9:.-]+)(\/([a-zA-Z0-9:.-]+)\/([a-z0-9-]+(_rev\d+)?))+$/)
1093
- if (!m) return null
1094
- var origin = m[1],
1095
- site = m[3],
1096
- slug = m[4]
1097
- if (['view', 'local', 'origin'].indexOf(site) !== -1) site = origin
1098
- return { site: site, slug: slug }
1099
- }
1100
-
1101
- function decodeDrop(dt) {
1102
- var out = { kind: 'unknown', url: '', site: '', slug: '', text: '', html: '', files: [], types: [] }
1103
- if (!dt) return out
1104
- out.types = Array.prototype.slice.call(dt.types || [])
1105
- if (dt.files && dt.files.length) {
1106
- for (var i = 0; i < dt.files.length; i++) {
1107
- var f = dt.files[i]
1108
- out.files.push({ name: f.name, type: f.type, size: f.size })
1109
- }
1110
- out.kind = 'file'
1111
- }
1112
- try {
1113
- out.text = dt.getData('text/plain') || ''
1114
- } catch (_) {}
1115
- try {
1116
- out.html = dt.getData('text/html') || ''
1117
- } catch (_) {}
1118
- var url = ''
1119
- try {
1120
- url = dt.getData('text/uri-list') || dt.getData('URL') || dt.getData('text/x-moz-url') || ''
1121
- } catch (_) {}
1122
- if (url) url = url.split('\n')[0].trim() // x-moz-url is "url\ntitle"
1123
- if (!url && /^https?:\/\//i.test(out.text)) url = out.text.trim()
1124
- if (url) {
1125
- out.url = url
1126
- var page = isPageUrl(url)
1127
- if (page) {
1128
- out.kind = 'page'
1129
- out.site = page.site
1130
- out.slug = page.slug
1131
- } else {
1132
- out.kind = 'link'
1133
- }
1134
- } else if (out.kind === 'unknown') {
1135
- var t = (out.text || '').trim()
1136
- if (t && /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(t)) {
1137
- out.kind = 'site' // bare hostname = a neighborhood/site flag
1138
- out.site = t
1139
- } else if (t) {
1140
- out.kind = 'text'
1141
- }
1142
- }
1143
- return out
1144
- }
1145
-
1146
- // Ask our own server to unfurl a URL into a static preview card (no third-party
1147
- // calls from the browser; the thumbnail comes back inlined as a data URI).
1148
- // Link previews come from an Outpost — a host running wiki-plugin-unfurl —
1149
- // called straight from the browser. This farm only tells us which ones exist
1150
- // (GET /system/outposts.json, a list). We shuffle and try each in turn with a
1151
- // short timeout; the first card wins; every failure is swallowed. No Outpost,
1152
- // or none answering, means null — and renderDropReport already draws a plain
1153
- // report for a null card. Absence is normal, not an error.
1154
- var _outposts = null
1155
- function outposts() {
1156
- if (_outposts) return _outposts
1157
- _outposts = fetch(location.origin + '/system/outposts.json')
1158
- .then(function (r) { return r.ok ? r.json() : {} })
1159
- .then(function (j) { return (j && Array.isArray(j.unfurl) ? j.unfurl : []).filter(Boolean) })
1160
- .catch(function () { return [] })
1161
- return _outposts
1162
- }
1163
- function shuffle(a) {
1164
- var b = a.slice()
1165
- for (var i = b.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = b[i]; b[i] = b[j]; b[j] = t }
1166
- return b
1167
- }
1168
- function unfurl(url) {
1169
- return outposts().then(function (list) {
1170
- var order = shuffle(list)
1171
- var tryNext = function (i) {
1172
- if (i >= order.length) return null
1173
- var ctl = typeof AbortController === 'function' ? new AbortController() : null
1174
- var timer = ctl && setTimeout(function () { ctl.abort() }, 2000)
1175
- return fetch(order[i] + '/system/unfurl.json?url=' + encodeURIComponent(url), ctl ? { signal: ctl.signal } : {})
1176
- .then(function (r) { return r.ok ? r.json() : null })
1177
- .catch(function () { return null })
1178
- .then(function (card) {
1179
- if (timer) clearTimeout(timer)
1180
- if (card && typeof card === 'object' && 'title' in card) { card.outpost = order[i]; return card }
1181
- return tryNext(i + 1)
1182
- })
1183
- }
1184
- return tryNext(0)
1185
- })
1186
- }
1187
-
1188
- function cardHtml(card) {
1189
- var img = card.image
1190
- ? '<img src="' + card.image + '" style="width:100%;max-height:240px;object-fit:cover;display:block">'
1191
- : ''
1192
- var provider = card.provider ? '<div style="font-size:12px;opacity:.7">' + esc(card.provider) + '</div>' : ''
1193
- var desc = card.description ? '<div style="font-size:13px;opacity:.85;margin-top:4px">' + esc(card.description) + '</div>' : ''
1194
- return (
1195
- '<div style="border:1px solid rgba(128,128,128,.35);border-radius:8px;overflow:hidden;max-width:480px">' +
1196
- img +
1197
- '<div style="padding:8px 10px">' +
1198
- provider +
1199
- '<div style="font-weight:600;font-size:15px"><a href="' + esc(card.url) + '" target="_blank" rel="noopener">' +
1200
- esc(card.title || card.url) +
1201
- '</a></div>' +
1202
- desc +
1203
- '<div style="font-size:11px;opacity:.55;margin-top:6px;word-break:break-all">' + esc(card.url) + '</div>' +
1204
- '</div></div>'
1205
- )
1206
- }
1207
-
1208
- // "is this host ours?" — cached list from our own server.
1209
- var _farmDomains = null
1210
- function farmDomains() {
1211
- if (_farmDomains) return Promise.resolve(_farmDomains)
1212
- return fetch(location.origin + '/system/farm-domains.json')
1213
- .then(function (r) {
1214
- return r.ok ? r.json() : { local: [], public: [] }
1215
- })
1216
- .then(function (f) {
1217
- _farmDomains = f
1218
- return f
1219
- })
1220
- .catch(function () {
1221
- _farmDomains = { local: [], public: [] }
1222
- return _farmDomains
1223
- })
1224
- }
1225
- function farmClass(site, farm) {
1226
- if (farm && farm.local && farm.local.indexOf(site) !== -1) return 'local'
1227
- if (farm && farm.public && farm.public.indexOf(site) !== -1) return 'public'
1228
- return 'federation'
1229
- }
1230
-
1231
- // wiki.site().get returns the sitemap as {data,lastModified} (or, cached in the
1232
- // neighborhood, as the raw array) — normalize both.
1233
- function sitemapArray(g) {
1234
- if (Array.isArray(g)) return g
1235
- if (g && Array.isArray(g.data)) return g.data
1236
- return null
1237
- }
1238
- // Confirm a candidate {site,slug} is really a wiki page → {isWiki, exists, title}.
1239
- function probeWiki(site, slug) {
1240
- return new Promise(function (resolve) {
1241
- var done = function (arr) {
1242
- if (!arr) return resolve({ isWiki: false, exists: false, title: '' })
1243
- var entry = null
1244
- for (var i = 0; i < arr.length; i++)
1245
- if (arr[i] && arr[i].slug === slug) {
1246
- entry = arr[i]
1247
- break
1248
- }
1249
- resolve({ isWiki: true, exists: !!entry, title: entry ? entry.title || slug : '' })
1250
- }
1251
- try {
1252
- var n = window.wiki && window.wiki.neighborhood && window.wiki.neighborhood[site]
1253
- if (n && n.sitemap) return done(sitemapArray(n.sitemap)) // instant if cached
1254
- window.wiki.site(site).get('system/sitemap.json', function (err, got) {
1255
- done(err ? null : sitemapArray(got))
1256
- })
1257
- } catch (e) {
1258
- resolve({ isWiki: false, exists: false, title: '' })
1259
- }
1260
- })
1261
- }
1262
-
1263
- // Orchestrate: confirm wiki pages, else fetch an unfurl card, then render.
1264
- function buildDropReport($target, d) {
1265
- if (d.kind === 'page') {
1266
- Promise.all([probeWiki(d.site, d.slug), farmDomains()]).then(function (res) {
1267
- var p = res[0]
1268
- if (p.isWiki) {
1269
- d.wikiTitle = p.title || d.slug
1270
- d.pageExists = p.exists
1271
- d.farm = farmClass(d.site, res[1]) // local | public | federation
1272
- d.kind = d.farm === 'federation' ? 'federation-page' : 'farm-page'
1273
- renderDropReport($target, d, null)
1274
- } else {
1275
- // URL shaped like a lineup, but the host isn't a wiki → treat as a link.
1276
- d.kind = 'link'
1277
- unfurl(d.url).then(function (card) {
1278
- renderDropReport($target, d, card)
1279
- })
1280
- }
1281
- })
1282
- } else if (d.url) {
1283
- unfurl(d.url).then(function (card) {
1284
- renderDropReport($target, d, card)
1285
- })
1286
- } else {
1287
- renderDropReport($target, d, null)
1288
- }
1289
- }
1290
-
1291
- function renderDropReport($target, d, card) {
1292
- var wiki = window.wiki
1293
- var raw = $target.data('data') || {}
1294
- var targetTitle = raw.title || $target.attr('id') || 'this page'
1295
- var kindLabel =
1296
- {
1297
- 'farm-page': 'a page on our farm',
1298
- 'federation-page': 'a page in the wider federation',
1299
- site: 'a wiki site',
1300
- link: 'a link / URL',
1301
- text: 'plain text',
1302
- file: 'a file',
1303
- }[d.kind] || 'an unrecognised drop'
1304
- var cap = function (s) {
1305
- return (s || '').slice(0, 2000)
1306
- }
1307
-
1308
- var story = [
1309
- {
1310
- type: 'markdown',
1311
- id: rand(),
1312
- text: '# 📥 Dropped onto ' + targetTitle + '\n\nYou dropped **' + kindLabel + '** onto its flag.',
1313
- },
1314
- ]
1315
- // Lead: confirmed wiki page → title + provenance + a live reference.
1316
- if (d.kind === 'farm-page' || d.kind === 'federation-page') {
1317
- var where =
1318
- d.farm === 'local' ? 'our **local** farm' : d.farm === 'public' ? 'our **public** farm' : 'the **wider federation**'
1319
- story.push({
1320
- type: 'markdown',
1321
- id: rand(),
1322
- text:
1323
- '# ' +
1324
- (d.wikiTitle || d.slug) +
1325
- '\n\nA wiki page on ' +
1326
- where +
1327
- ' — `' +
1328
- esc(d.site) +
1329
- '`.' +
1330
- (d.pageExists ? '' : '\n\n_(not listed in the site sitemap — may be new or removed)_'),
1331
- })
1332
- story.push({
1333
- type: 'reference',
1334
- id: rand(),
1335
- site: d.site,
1336
- slug: d.slug,
1337
- title: d.wikiTitle || d.slug,
1338
- text: d.farm === 'federation' ? 'federated page' : 'our page',
1339
- })
1340
- }
1341
- // Lead with the rich preview card when we have one.
1342
- if (card && (card.title || card.image)) story.push({ type: 'html', id: rand(), text: cardHtml(card) })
1343
-
1344
- story.push({ type: 'markdown', id: rand(), text: '# Details' })
1345
- if (d.kind === 'farm-page' || d.kind === 'federation-page') {
1346
- story.push({
1347
- type: 'markdown',
1348
- id: rand(),
1349
- text:
1350
- '# Wiki page\n\n- site: `' + esc(d.site) + '`\n- slug: `' + esc(d.slug) + '`\n- farm: `' + esc(d.farm) +
1351
- '`\n- in sitemap: `' + (d.pageExists ? 'yes' : 'no') + '`',
1352
- })
1353
- } else if (d.kind === 'site') {
1354
- story.push({ type: 'markdown', id: rand(), text: '# Wiki site\n\n`' + esc(d.site) + '` — [welcome](//' + esc(d.site) + '/view/welcome-visitors)' })
1355
- }
1356
- if (d.url) story.push({ type: 'markdown', id: rand(), text: '# URL\n\n`' + d.url + '`' })
1357
- if (d.text && d.text !== d.url) story.push({ type: 'markdown', id: rand(), text: '# Text\n\n```\n' + cap(d.text) + '\n```' })
1358
- if (d.html) story.push({ type: 'markdown', id: rand(), text: '# HTML\n\n```\n' + cap(esc(d.html)) + '\n```' })
1359
- if (d.files.length)
1360
- story.push({
1361
- type: 'markdown',
1362
- id: rand(),
1363
- text: '# Files\n\n' + d.files.map(function (f) {
1364
- return '- ' + esc(f.name) + ' (' + esc(f.type || '?') + ', ' + f.size + ' bytes)'
1365
- }).join('\n'),
1366
- })
1367
- story.push({ type: 'markdown', id: rand(), text: '# Raw dataTransfer types\n\n`' + esc((d.types || []).join(', ') || '(none)') + '`' })
1368
-
1369
- insertPageAfter($target, wiki.newPage({ title: targetTitle + ' 📥', story: story, journal: [] }), { ghost: true })
1370
- }
1371
-
1372
- // Clicking a page flag opens, inserted to the right of that page:
1373
- // • logged in → a Tools page (Fork / Trash / Clean journal / Simplify)
1374
- // • logged out → a Navigate page (Simplify + this site's recent Changes)
1375
- function openFlagPage(a) {
1376
- var $ = window.$
1377
- var wiki = window.wiki
1378
- var $src = $(a).closest('.page')
1379
- var raw = $src.data('data') || {}
1380
- var pageTitle = raw.title || $src.attr('id') || 'this page'
1381
- var slug = $src.attr('id')
1382
- var host = location.host
1383
- var loggedIn = window.isAuthenticated === true
1384
-
1385
- // Simplify — shared by both pages (a plain nav link; needs no login).
1386
- var surl = simplifyUrl($src, slug, remoteSiteOf($src))
1387
- var simplifySection = [
1388
- {
1389
- type: 'markdown',
1390
- id: rand(),
1391
- text:
1392
- '# Simplify\n\nCollapse the lineup down to just **Welcome Visitors** and **' +
1393
- pageTitle +
1394
- '**, clearing away the trail of pages you clicked through to get here.',
1395
- },
1396
- {
1397
- type: 'html',
1398
- id: rand(),
1399
- text:
1400
- '<p><a class="wiki-ux-tool-simplify" href="' +
1401
- esc(surl) +
1402
- '"><button style="' + BTN + '">⌂ Simplify to Welcome Visitors + ' +
1403
- esc(pageTitle) +
1404
- '</button></a></p>',
1405
- },
1406
- ]
1407
-
1408
- // Tool pages — real, forkable wiki pages that act on this page via the
1409
- // similarity DSL's SUBJECT keyword. Shown to visitors and owners alike:
1410
- // the searches are read-only. Gated on the similarity plugin being here.
1411
- var toolPagesSection = simInstalled
1412
- ? [
1413
- {
1414
- type: 'markdown',
1415
- id: rand(),
1416
- text:
1417
- '# 🔧 Tool pages\n\nOpen a tool page beside **' +
1418
- pageTitle +
1419
- '** — its searches take this page as their subject.',
1420
- },
1421
- {
1422
- type: 'html',
1423
- id: rand(),
1424
- text:
1425
- '<p><button class="wiki-ux-tool-toolpage" data-slug="similar-page-tool" style="' + BTN + '">🔍 Similar Page Tool</button> ' +
1426
- '<button class="wiki-ux-tool-toolpage" data-slug="search-related-page" style="' + BTN + '">🗂️ Search Related Page</button></p>',
1427
- },
1428
- ]
1429
- : []
1430
-
1431
- var story, title
1432
-
1433
- if (loggedIn) {
1434
- // ===== Tools page: editing actions (owner only) =====
1435
- title = pageTitle + ' 🛠️'
1436
- var isRemote = $src.hasClass('remote') // blue halo — from another server
1437
- var isLocal = $src.hasClass('local') // yellow halo of death — unsaved
1438
- var showFork = isRemote || isLocal
1439
- var remoteSite = remoteSiteOf($src)
1440
-
1441
- story = [{ type: 'paragraph', id: rand(), text: 'Tools for **' + pageTitle + '**.' }]
1442
-
1443
- if (showFork) {
1444
- var why = isRemote
1445
- ? 'This page lives on **' +
1446
- (remoteSite || 'another wiki') +
1447
- '** — the blue halo. **Forking** copies it onto **' +
1448
- host +
1449
- '**, so you get your own editable version here and it joins your wiki. The original stays where it is; forking never changes it.'
1450
- : 'This page has **unsaved local changes** — the yellow halo of death (it only exists in this browser). **Forking** writes it to the server here on **' +
1451
- host +
1452
- '**, making it a real, saved, shareable page.'
1453
- story.push({ type: 'markdown', id: rand(), text: '# Fork this page\n\n' + why })
1454
- story.push({
1455
- type: 'html',
1456
- id: rand(),
1457
- text: '<p><button class="wiki-ux-tool-fork" style="' + BTN + '">' + FORK_SVG + 'Fork this page to ' + esc(host) + '</button></p>',
1458
- })
1459
- }
1460
-
1461
- // Similarity power tool — only when wiki-plugin-similarity is installed.
1462
- // Sits directly below Fork, or first when there is no Fork.
1463
- if (simInstalled) {
1464
- story.push({
1465
- type: 'markdown',
1466
- id: rand(),
1467
- text:
1468
- '# 🔍 Similarity search\n\nFind pages across the whole farm that are semantically similar to **' +
1469
- pageTitle +
1470
- '** — seeded by this page itself; opens a ranked ghost report you can read and fork.',
1471
- })
1472
- story.push({
1473
- type: 'html',
1474
- id: rand(),
1475
- text: '<p><button class="wiki-ux-tool-sim" style="' + BTN + '">🔍 Similarity search</button></p>',
1476
- })
1477
- }
1478
- story = story.concat(toolPagesSection)
1479
-
1480
- story.push({
1481
- type: 'markdown',
1482
- id: rand(),
1483
- text:
1484
- '# Trash this page\n\nMove **' +
1485
- pageTitle +
1486
- '** to the recycler. It disappears from the site but is not destroyed — recover it any time from [[Recycled Pages]].',
1487
- })
1488
- story.push({
1489
- type: 'html',
1490
- id: rand(),
1491
- text: '<p><button class="wiki-ux-tool-trash" style="' + BTN + '">🗑&#65039; Trash this page</button></p>',
1492
- })
1493
-
1494
- story.push({
1495
- type: 'markdown',
1496
- id: rand(),
1497
- text:
1498
- '# Clean the journal\n\nOpen a fresh **ghost** copy of this page whose journal is stripped back to a single "create" entry — as if written today, with no fork trail or remote attribution. Fork that ghost to keep the cleaned version.',
1499
- })
1500
- story.push({
1501
- type: 'html',
1502
- id: rand(),
1503
- text: '<p><button class="wiki-ux-tool-clean" style="' + BTN + '">🧹 Clean the journal</button></p>',
1504
- })
1505
-
1506
- // Fish Picker — only for the OWNER, on their OWN site (not a remote page).
1507
- if (window.isOwner === true && !isRemote) {
1508
- story.push({
1509
- type: 'markdown',
1510
- id: rand(),
1511
- text:
1512
- '# 🐟 Site flag\n\nDesign this site’s identity flag (favicon) with the **Fish Picker** — species = you, colour = this site’s topic school, a Mouse (or robo-mouse) for human/AI.',
1513
- })
1514
- story.push({
1515
- type: 'html',
1516
- id: rand(),
1517
- text: '<p><button class="wiki-ux-tool-flag" style="' + BTN + '">🐟 Fish Picker</button></p>',
1518
- })
1519
- }
1520
-
1521
- story = story.concat(simplifySection)
1522
- } else {
1523
- // ===== Navigate page: for a logged-out visitor =====
1524
- title = pageTitle + ' 🧭'
1525
- story = [{ type: 'paragraph', id: rand(), text: 'Ways to get around from **' + pageTitle + '**.' }]
1526
- .concat(toolPagesSection)
1527
- .concat(simplifySection)
1528
- .concat([
1529
- { type: 'markdown', id: rand(), text: '# Changes to this Site' },
1530
- { type: 'paragraph', id: rand(), text: 'Below we list recent changes to pages on [[this site]].' },
1531
- { type: 'activity', id: rand(), text: 'NEIGHBORHOOD no\nNARRATIVE ' },
1532
- ])
1533
- }
1534
-
1535
- // Title = original + a tools/compass emoji (reads as the same page, tagged).
1536
- // asSlug strips the emoji but the trailing space leaves a hyphen, so the slug
1537
- // stays distinct from the source. `plugin` → the green plugin-page halo.
1538
- var pageObject = wiki.newPage({ title: title, story: story, journal: [], plugin: 'hitchhiker' })
1539
- insertPageAfter($src, pageObject)
1540
- }
1541
-
1542
- // ===========================================================================
1543
- // 5. Fish Picker — the site identity flag editor (owner only)
1544
- // ===========================================================================
1545
- function installFishFlag() {
1546
- if (document.getElementById('fish-picker-gen')) return
1547
- var s = document.createElement('script')
1548
- s.id = 'fish-picker-gen'
1549
- s.src = '/plugins/hitchhiker/fish.js' // sets window.FishPicker
1550
- document.head.appendChild(s)
1551
- // Bridge: an embedded (sandboxed, session-less) Fish Picker frame asks us —
1552
- // the authenticated page — to persist its favicon. The POST is owner-gated
1553
- // server-side, so relaying is safe; we just carry the session cookie.
1554
- window.addEventListener('message', function (e) {
1555
- var d = e.data
1556
- if (!d || d.type !== 'fishpicker:save' || typeof d.image !== 'string' || d.image.slice(0, 10) !== 'data:image') return
1557
- var reply = function (ok, status) {
1558
- try { e.source && e.source.postMessage({ type: 'fishpicker:saved', ok: ok, status: status }, '*') } catch (_) {}
1559
- }
1560
- fetch('/favicon.png', {
1561
- method: 'POST',
1562
- credentials: 'include',
1563
- headers: { 'Content-Type': 'application/json' },
1564
- body: JSON.stringify({ image: d.image }),
1565
- })
1566
- .then(function (r) { if (r.ok) bustFavicon(); reply(r.ok, r.status) })
1567
- .catch(function () { reply(false, 0) })
1568
- })
1569
- }
1570
-
1571
- // Open the Fish Picker for THIS site's flag. Seeds from /status/identity.json
1572
- // if present, else from the {person}.{topic} domain.
1573
- function openFishPicker($src, $tools) {
1574
- var FP = window.FishPicker
1575
- if (!FP) return window.alert('Fish Picker still loading — try again in a moment.')
1576
- var host = location.hostname
1577
- var labels = host.split('.').filter(Boolean)
1578
- var st = { id: labels[0] || 'owner', name: labels[0] || '', type: 'human', topic: labels[1] || 'fish', species: FP.speciesFor(labels[0] || ''), variant: 3 }
1579
- var SPECIES = ['koi', 'pufferfish', 'pike']
1580
- var opt = function (arr, sel) {
1581
- return arr.map(function (x) { return '<option' + (x === sel ? ' selected' : '') + '>' + x + '</option>' }).join('')
1582
- }
1583
- var identity = function () {
1584
- return { name: st.name, id: st.type === 'agent' ? st.id + '-agent' : st.id, type: st.type, topicGroup: st.topic, species: st.species, variant: st.variant, publicKey: null }
1585
- }
1586
- var dlg = document.createElement('dialog')
1587
- dlg.id = 'fish-picker-dialog'
1588
- dlg.style.cssText = 'max-width:520px;border:1px solid #ccc;border-radius:10px;padding:0'
1589
- dlg.innerHTML =
1590
- '<form method="dialog" style="margin:0;padding:16px 18px;font:14px system-ui,sans-serif">' +
1591
- '<h2 style="margin:0 0 8px;font-size:16px">🐟 Site flag — ' + esc(host) + '</h2>' +
1592
- '<div id="fp-preview" style="display:flex;gap:10px;align-items:flex-end;min-height:132px;margin:6px 0"></div>' +
1593
- '<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">' +
1594
- '<label>Species <select id="fp-species">' + opt(SPECIES, st.species) + '</select></label>' +
1595
- '<label>School <select id="fp-topic">' + opt(Object.keys(FP.SCHOOLS), st.topic) + '</select></label>' +
1596
- '<label><input type="checkbox" id="fp-agent"> Agent (AI)</label>' +
1597
- '<label>Variant <button type="button" id="fp-vd">−</button> <span id="fp-v">' + st.variant + '</span> <button type="button" id="fp-vu">+</button></label>' +
1598
- '</div>' +
1599
- '<pre id="fp-json" style="background:#f6f5f2;border-radius:6px;padding:8px;font-size:11px;max-height:120px;overflow:auto;margin:8px 0"></pre>' +
1600
- '<menu style="display:flex;gap:8px;justify-content:flex-end;padding:0;margin:6px 0 0">' +
1601
- '<button value="cancel">Cancel</button><button value="save" style="' + BTN + '">Save flag</button></menu>' +
1602
- '</form>'
1603
- document.body.appendChild(dlg)
1604
- var render = function () {
1605
- dlg.querySelector('#fp-preview').innerHTML = FP.fishSVG(st, 32) + FP.fishSVG(st, 64) + FP.fishSVG(st, 128)
1606
- dlg.querySelector('#fp-json').textContent = JSON.stringify(identity(), null, 1)
1607
- }
1608
- dlg.querySelector('#fp-species').onchange = function () { st.species = this.value; render() }
1609
- dlg.querySelector('#fp-topic').onchange = function () { st.topic = this.value; render() }
1610
- dlg.querySelector('#fp-agent').onchange = function () { st.type = this.checked ? 'agent' : 'human'; render() }
1611
- dlg.querySelector('#fp-vu').onclick = function () { st.variant++; dlg.querySelector('#fp-v').textContent = st.variant; render() }
1612
- dlg.querySelector('#fp-vd').onclick = function () { st.variant = Math.max(0, st.variant - 1); dlg.querySelector('#fp-v').textContent = st.variant; render() }
1613
- dlg.addEventListener('close', function () {
1614
- if (dlg.returnValue === 'save') saveFishFlag(st)
1615
- dlg.remove()
1616
- if ($tools) $tools.remove()
1617
- })
1618
- var seed = function (id) {
1619
- if (id) {
1620
- st.id = (id.id || st.id).replace(/-agent$/, '') // stored agent id is base+"-agent"
1621
- st.name = id.name || st.name
1622
- st.type = id.type || st.type
1623
- st.topic = id.topicGroup || st.topic
1624
- st.species = id.species || st.species
1625
- if (id.variant != null) st.variant = id.variant
1626
- dlg.querySelector('#fp-species').value = st.species
1627
- dlg.querySelector('#fp-topic').value = st.topic
1628
- dlg.querySelector('#fp-agent').checked = st.type === 'agent'
1629
- dlg.querySelector('#fp-v').textContent = st.variant
1630
- }
1631
- render()
1632
- }
1633
- // seed from the current favicon's embedded identity, else /status/identity.json, else domain
1634
- fetch('/favicon.png?t=' + Date.now())
1635
- .then(function (r) { return r.ok ? r.arrayBuffer() : null })
1636
- .then(function (ab) {
1637
- if (ab) {
1638
- var txt = pngReadText(new Uint8Array(ab), 'fishpicker')
1639
- if (txt) {
1640
- try {
1641
- return JSON.parse(decodeURIComponent(txt))
1642
- } catch (e) {}
1643
- }
1644
- }
1645
- return fetch('/status/identity.json')
1646
- .then(function (r) { return r.ok ? r.json() : null })
1647
- .catch(function () { return null })
1648
- })
1649
- .then(seed)
1650
- .catch(function () { seed(null) })
1651
- render()
1652
- dlg.showModal()
1653
- }
1654
-
1655
- // v1.5b: persist the flag by POSTing the favicon to fedwiki's built-in,
1656
- // owner-gated POST /favicon.png — reusing the farm's own security
1657
- // (securityhandler.isAuthorized), so NO new route and NO new auth. The identity
1658
- // record rides along in a PNG tEXt chunk ("fishpicker"), so the favicon is
1659
- // self-describing and the picker re-seeds from it — no identity.json file needed.
1660
- function saveFishFlag(st) {
1661
- var FP = window.FishPicker
1662
- var identity = { name: st.name, id: st.type === 'agent' ? st.id + '-agent' : st.id, type: st.type, topicGroup: st.topic, species: st.species, variant: st.variant, publicKey: null }
1663
- var svg = FP.fishSVG(st, 128)
1664
- var img = new Image()
1665
- img.onload = function () {
1666
- var cv = document.createElement('canvas')
1667
- cv.width = cv.height = 64
1668
- cv.getContext('2d').drawImage(img, 0, 0, 64, 64)
1669
- cv.toBlob(function (blob) {
1670
- blob.arrayBuffer().then(function (ab) {
1671
- var png = pngInsertText(new Uint8Array(ab), 'fishpicker', encodeURIComponent(JSON.stringify(identity)))
1672
- fetch('/favicon.png', {
1673
- method: 'POST',
1674
- credentials: 'include',
1675
- headers: { 'Content-Type': 'application/json' },
1676
- body: JSON.stringify({ image: 'data:image/png;base64,' + b64(png) }),
1677
- })
1678
- .then(function (r) {
1679
- if (r.ok) {
1680
- bustFavicon()
1681
- window.alert('🐟 Flag saved to ' + location.hostname + '.')
1682
- } else if (r.status === 403) {
1683
- window.alert('Not saved — you must be signed in as the owner of this site.')
1684
- } else {
1685
- window.alert('Save failed (HTTP ' + r.status + ').')
1686
- }
1687
- })
1688
- .catch(function (e) { window.alert('Save failed: ' + (e && e.message)) })
1689
- })
1690
- }, 'image/png')
1691
- }
1692
- img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg)))
1693
- }
1694
-
1695
- // Refresh the favicon in every page header (cache-bust) after a save.
1696
- function bustFavicon() {
1697
- try {
1698
- var imgs = document.querySelectorAll('img.favicon')
1699
- for (var i = 0; i < imgs.length; i++) imgs[i].src = '/favicon.png?t=' + Date.now()
1700
- } catch (e) {}
1701
- }
1702
-
1703
- // ---- PNG tEXt chunk helpers — insert/read a keyword=text metadata chunk ----
1704
- var CRC_TABLE = (function () {
1705
- var t = [], c, n, k
1706
- for (n = 0; n < 256; n++) { c = n; for (k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0 }
1707
- return t
1708
- })()
1709
- function crc32(bytes) {
1710
- var c = 0xffffffff
1711
- for (var i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8)
1712
- return (c ^ 0xffffffff) >>> 0
1713
- }
1714
- function u32(n) { return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255] }
1715
- function pngInsertText(png, keyword, text) {
1716
- var data = []
1717
- for (var i = 0; i < keyword.length; i++) data.push(keyword.charCodeAt(i) & 255)
1718
- data.push(0)
1719
- for (var j = 0; j < text.length; j++) data.push(text.charCodeAt(j) & 255)
1720
- var body = [0x74, 0x45, 0x58, 0x74].concat(data) // 'tEXt' + data
1721
- var chunk = u32(data.length).concat(body).concat(u32(crc32(new Uint8Array(body))))
1722
- var iend = png.length - 12 // IEND is the final 12 bytes; insert before it
1723
- var out = new Uint8Array(png.length + chunk.length)
1724
- out.set(png.subarray(0, iend), 0)
1725
- out.set(new Uint8Array(chunk), iend)
1726
- out.set(png.subarray(iend), iend + chunk.length)
1727
- return out
1728
- }
1729
- function pngReadText(png, keyword) {
1730
- var p = 8
1731
- while (p + 8 <= png.length) {
1732
- var len = (png[p] << 24) | (png[p + 1] << 16) | (png[p + 2] << 8) | png[p + 3]
1733
- var type = String.fromCharCode(png[p + 4], png[p + 5], png[p + 6], png[p + 7])
1734
- if (type === 'tEXt') {
1735
- var s = ''
1736
- for (var i = 0; i < len; i++) s += String.fromCharCode(png[p + 8 + i])
1737
- var z = s.indexOf('\0')
1738
- if (z !== -1 && s.slice(0, z) === keyword) return s.slice(z + 1)
1739
- }
1740
- if (type === 'IEND') break
1741
- p += 12 + len
1742
- }
1743
- return null
1744
- }
1745
- function b64(bytes) {
1746
- var s = ''
1747
- for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
1748
- return btoa(s)
1749
- }
1750
-
1751
- // ===========================================================================
1752
- // helpers
1753
- // ===========================================================================
1754
- function reEmitOpenFactories() {
1755
- var $ = window.$
1756
- $('.item.factory').each(function (_i, el) {
1757
- var $i = $(el)
1758
- var it = $i.data('item')
1759
- if (it && it.type === 'factory') {
1760
- $i.empty()
1761
- try {
1762
- window.plugins.factory.emit($i, it)
1763
- } catch (e) {}
1764
- }
1765
- })
1766
- }
1767
-
1768
- // Scoped stylesheet for wiki-ux-generated pages.
1769
- function installStyles() {
1770
- if (document.getElementById('wiki-ux-style')) return
1771
- var s = document.createElement('style')
1772
- s.id = 'wiki-ux-style'
1773
- // Generated pages have no sister sites, so their twins band's <p> is empty
1774
- // and self-collapses, pulling the title ~1em higher than normal pages. Add
1775
- // the missing 1em back so the title aligns with about/normal page titles.
1776
- s.textContent = [
1777
- '.page.wiki-ux-tools .twins{min-height:2em}',
1778
- // drop-target highlight — deliberately loud: enlarged, pulsing favicon + a label.
1779
- '@keyframes wikiUxPulse{0%{box-shadow:0 0 0 0 rgba(0,190,190,.7)}70%{box-shadow:0 0 0 14px rgba(0,190,190,0)}100%{box-shadow:0 0 0 0 rgba(0,190,190,0)}}',
1780
- '.page h1 a.wiki-ux-drop-target{position:relative}',
1781
- '.page h1 a.wiki-ux-drop-target img.favicon{transform:scale(1.5);border-radius:4px;outline:3px solid #0bb;animation:wikiUxPulse 1s infinite}',
1782
- ".page h1 a.wiki-ux-drop-target::after{content:'\\2935 drop to preview';position:absolute;left:0;top:100%;margin-top:5px;white-space:nowrap;font-size:11px;font-weight:600;color:#0bb;background:rgba(0,190,190,.14);padding:1px 6px;border-radius:3px;z-index:5}",
1783
- '.page:has(h1 a.wiki-ux-drop-target) .header{background:rgba(0,190,190,.08);border-radius:4px}',
1784
- ].join('')
1785
- document.head.appendChild(s)
1786
- }
1787
-
1788
- function esc(s) {
1789
- return ('' + (s == null ? '' : s))
1790
- .replace(/&/g, '&amp;')
1791
- .replace(/</g, '&lt;')
1792
- .replace(/>/g, '&gt;')
1793
- .replace(/"/g, '&quot;')
1794
- }
1795
- function rand() {
1796
- var h = ''
1797
- for (var i = 0; i < 16; i++) h += ((Math.random() * 16) | 0).toString(16)
1798
- return h
1799
- }
1800
- })()
37
+ Design this site\u2019s identity flag (favicon) with the **Fish Picker** \u2014 species = you, colour = this site\u2019s topic school, a Mouse (or robo-mouse) for human/AI.`}),f.push({type:"html",id:l(),text:'<p><button class="wiki-ux-tool-flag" style="'+p+'">\u{1F41F} Fish Picker</button></p>'})),f=f.concat(d)}else b=n+" \u{1F9ED}",f=[{type:"paragraph",id:l(),text:"Ways to get around from **"+n+"**."}].concat(I).concat(d).concat([{type:"markdown",id:l(),text:"# Changes to this Site"},{type:"paragraph",id:l(),text:"Below we list recent changes to pages on [[this site]]."},{type:"activity",id:l(),text:`NEIGHBORHOOD no
38
+ NARRATIVE `}]);var pt=a.newPage({title:b,story:f,journal:[],plugin:"hitchhiker"});m(i,pt)}function c(e){return(""+(e??"")).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function l(){for(var e="",t=0;t<16;t++)e+=(Math.random()*16|0).toString(16);return e}function st(){if(!document.getElementById("wiki-ux-style")){var e=document.createElement("style");e.id="wiki-ux-style",e.textContent=[".page.wiki-ux-tools .twins{min-height:2em}","@keyframes wikiUxPulse{0%{box-shadow:0 0 0 0 rgba(0,190,190,.7)}70%{box-shadow:0 0 0 14px rgba(0,190,190,0)}100%{box-shadow:0 0 0 0 rgba(0,190,190,0)}}",".page h1 a.wiki-ux-drop-target{position:relative}",".page h1 a.wiki-ux-drop-target img.favicon{transform:scale(1.5);border-radius:4px;outline:3px solid #0bb;animation:wikiUxPulse 1s infinite}",".page h1 a.wiki-ux-drop-target::after{content:'\\2935 drop to preview';position:absolute;left:0;top:100%;margin-top:5px;white-space:nowrap;font-size:11px;font-weight:600;color:#0bb;background:rgba(0,190,190,.14);padding:1px 6px;border-radius:3px;z-index:5}",".page:has(h1 a.wiki-ux-drop-target) .header{background:rgba(0,190,190,.08);border-radius:4px}"].join(""),document.head.appendChild(e)}}})();})();
39
+ //# sourceMappingURL=wiki-ux.js.map