@nervur-org/dock 0.3.0
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/GETTING_STARTED.md +128 -0
- package/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +37 -0
- package/api/quo-api.md +196 -0
- package/api/route.ts +227 -0
- package/beings/GLOSSARY.md +118 -0
- package/beings/WORLDS.md +567 -0
- package/beings/avatar.ts +101 -0
- package/beings/carry.ts +159 -0
- package/beings/clock.ts +552 -0
- package/beings/courier.ts +107 -0
- package/beings/desk.ts +128 -0
- package/beings/doorbell.ts +129 -0
- package/beings/envoy.ts +218 -0
- package/beings/index.ts +16 -0
- package/beings/join.ts +51 -0
- package/beings/link.ts +104 -0
- package/beings/look.ts +139 -0
- package/beings/porter.ts +43 -0
- package/beings/presence.ts +39 -0
- package/beings/quo-app-bells.md +280 -0
- package/beings/quo-dock.md +1942 -0
- package/beings/setup.ts +45 -0
- package/beings/side.ts +60 -0
- package/beings/user.ts +459 -0
- package/beings/way.ts +65 -0
- package/cli/bundles.ts +20 -0
- package/cli/client.ts +73 -0
- package/cli/daemon.ts +581 -0
- package/cli/estate/Caddyfile +33 -0
- package/cli/estate/quo.service +35 -0
- package/cli/estate/systems.quo.plist +44 -0
- package/cli/estate.ts +48 -0
- package/cli/folder.ts +27 -0
- package/cli/oauth.ts +32 -0
- package/cli/pilot.ts +38 -0
- package/cli/quo-dock.md +1942 -0
- package/cli/quo.ts +203 -0
- package/cli/stand.ts +160 -0
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.js +193 -0
- package/dist/beings/avatar.d.ts +32 -0
- package/dist/beings/avatar.js +100 -0
- package/dist/beings/carry.d.ts +12 -0
- package/dist/beings/carry.js +174 -0
- package/dist/beings/clock.d.ts +161 -0
- package/dist/beings/clock.js +528 -0
- package/dist/beings/courier.d.ts +50 -0
- package/dist/beings/courier.js +96 -0
- package/dist/beings/desk.d.ts +53 -0
- package/dist/beings/desk.js +135 -0
- package/dist/beings/doorbell.d.ts +78 -0
- package/dist/beings/doorbell.js +133 -0
- package/dist/beings/envoy.d.ts +54 -0
- package/dist/beings/envoy.js +211 -0
- package/dist/beings/index.d.ts +14 -0
- package/dist/beings/index.js +16 -0
- package/dist/beings/join.d.ts +15 -0
- package/dist/beings/join.js +42 -0
- package/dist/beings/link.d.ts +14 -0
- package/dist/beings/link.js +91 -0
- package/dist/beings/look.d.ts +30 -0
- package/dist/beings/look.js +104 -0
- package/dist/beings/porter.d.ts +14 -0
- package/dist/beings/porter.js +41 -0
- package/dist/beings/presence.d.ts +8 -0
- package/dist/beings/presence.js +21 -0
- package/dist/beings/setup.d.ts +3 -0
- package/dist/beings/setup.js +39 -0
- package/dist/beings/side.d.ts +32 -0
- package/dist/beings/side.js +37 -0
- package/dist/beings/user.d.ts +315 -0
- package/dist/beings/user.js +462 -0
- package/dist/beings/way.d.ts +19 -0
- package/dist/beings/way.js +28 -0
- package/dist/cli/bundles.d.ts +5 -0
- package/dist/cli/bundles.js +17 -0
- package/dist/cli/client.d.ts +15 -0
- package/dist/cli/client.js +58 -0
- package/dist/cli/daemon.d.ts +50 -0
- package/dist/cli/daemon.js +584 -0
- package/dist/cli/estate/Caddyfile +33 -0
- package/dist/cli/estate/quo.service +35 -0
- package/dist/cli/estate/systems.quo.plist +44 -0
- package/dist/cli/estate.d.ts +5 -0
- package/dist/cli/estate.js +50 -0
- package/dist/cli/folder.d.ts +2 -0
- package/dist/cli/folder.js +25 -0
- package/dist/cli/oauth.d.ts +2 -0
- package/dist/cli/oauth.js +32 -0
- package/dist/cli/pilot.d.ts +5 -0
- package/dist/cli/pilot.js +22 -0
- package/dist/cli/quo.d.ts +2 -0
- package/dist/cli/quo.js +201 -0
- package/dist/cli/stand.d.ts +37 -0
- package/dist/cli/stand.js +110 -0
- package/dist/dock/agents.d.ts +102 -0
- package/dist/dock/agents.js +105 -0
- package/dist/dock/apns.d.ts +27 -0
- package/dist/dock/apns.js +98 -0
- package/dist/dock/console.d.ts +6 -0
- package/dist/dock/console.js +20 -0
- package/dist/dock/dialers.d.ts +56 -0
- package/dist/dock/dialers.js +75 -0
- package/dist/dock/faculty.d.ts +24 -0
- package/dist/dock/faculty.js +142 -0
- package/dist/dock/fcm.d.ts +13 -0
- package/dist/dock/fcm.js +77 -0
- package/dist/dock/grants.d.ts +41 -0
- package/dist/dock/grants.js +99 -0
- package/dist/dock/http2.d.ts +4 -0
- package/dist/dock/http2.js +68 -0
- package/dist/dock/index.d.ts +41 -0
- package/dist/dock/index.js +113 -0
- package/dist/dock/local.d.ts +13 -0
- package/dist/dock/local.js +44 -0
- package/dist/dock/location.d.ts +19 -0
- package/dist/dock/location.js +11 -0
- package/dist/dock/notifications.d.ts +38 -0
- package/dist/dock/notifications.js +60 -0
- package/dist/dock/push.d.ts +79 -0
- package/dist/dock/push.js +132 -0
- package/dist/dock/service.d.ts +4 -0
- package/dist/dock/service.js +107 -0
- package/dist/dock/timer.d.ts +52 -0
- package/dist/dock/timer.js +149 -0
- package/dist/dock/webpush.d.ts +12 -0
- package/dist/dock/webpush.js +144 -0
- package/dist/harbor/browser.d.ts +15 -0
- package/dist/harbor/browser.js +82 -0
- package/dist/harbor/capacitor.d.ts +28 -0
- package/dist/harbor/capacitor.js +218 -0
- package/dist/harbor/disk.d.ts +27 -0
- package/dist/harbor/disk.js +249 -0
- package/dist/harbor/edge/alarm.d.ts +10 -0
- package/dist/harbor/edge/alarm.js +38 -0
- package/dist/harbor/edge/edge.d.ts +54 -0
- package/dist/harbor/edge/edge.js +370 -0
- package/dist/harbor/edge/exercise.d.ts +7 -0
- package/dist/harbor/edge/exercise.js +232 -0
- package/dist/harbor/edge/given.d.ts +26 -0
- package/dist/harbor/edge/given.js +42 -0
- package/dist/harbor/edge/platform.d.ts +39 -0
- package/dist/harbor/edge/storage.d.ts +30 -0
- package/dist/harbor/edge/storage.js +180 -0
- package/dist/harbor/edge/worker.d.ts +17 -0
- package/dist/harbor/edge/worker.js +67 -0
- package/dist/harbor/files.d.ts +15 -0
- package/dist/harbor/files.js +220 -0
- package/dist/harbor/http.d.ts +19 -0
- package/dist/harbor/http.js +177 -0
- package/dist/harbor/idb.d.ts +15 -0
- package/dist/harbor/idb.js +0 -0
- package/dist/harbor/quo.d.ts +10 -0
- package/dist/harbor/quo.js +42 -0
- package/dist/harbor/resolve.d.ts +16 -0
- package/dist/harbor/resolve.js +17 -0
- package/dist/harbor/seal.d.ts +3 -0
- package/dist/harbor/seal.js +25 -0
- package/dist/harbor/sealed.d.ts +35 -0
- package/dist/harbor/sealed.js +162 -0
- package/dist/harbor/tauri.d.ts +16 -0
- package/dist/harbor/tauri.js +59 -0
- package/dist/harbor/ward.d.ts +9 -0
- package/dist/harbor/ward.js +15 -0
- package/dist/human/app.d.ts +32 -0
- package/dist/human/app.js +527 -0
- package/dist/human/apps.d.ts +10 -0
- package/dist/human/apps.js +29 -0
- package/dist/human/dom.d.ts +2 -0
- package/dist/human/dom.js +15 -0
- package/dist/human/door.d.ts +19 -0
- package/dist/human/door.js +92 -0
- package/dist/human/guest.d.ts +3 -0
- package/dist/human/guest.js +33 -0
- package/dist/human/html.d.ts +55 -0
- package/dist/human/html.js +451 -0
- package/dist/human/local.d.ts +7 -0
- package/dist/human/local.js +10 -0
- package/dist/human/mark.d.ts +8 -0
- package/dist/human/mark.js +47 -0
- package/dist/human/move.d.ts +17 -0
- package/dist/human/move.js +90 -0
- package/dist/human/screen.d.ts +19 -0
- package/dist/human/screen.js +119 -0
- package/dist/human/style.d.ts +2 -0
- package/dist/human/style.js +60 -0
- package/dist/human/tab.bundle.txt +4775 -0
- package/dist/human/tab.d.ts +15 -0
- package/dist/human/tab.js +409 -0
- package/dist/human/tree.d.ts +50 -0
- package/dist/human/tree.js +157 -0
- package/dist/human/wake.d.ts +34 -0
- package/dist/human/wake.js +179 -0
- package/dist/human/web.d.ts +33 -0
- package/dist/human/web.js +184 -0
- package/dist/human/worker.bundle.txt +3868 -0
- package/dist/human/worker.d.ts +1 -0
- package/dist/human/worker.js +81 -0
- package/dist/human/worlds.d.ts +24 -0
- package/dist/human/worlds.js +99 -0
- package/dist/mcp/agent.d.ts +10 -0
- package/dist/mcp/agent.js +78 -0
- package/dist/mcp/allow.d.ts +20 -0
- package/dist/mcp/allow.js +88 -0
- package/dist/mcp/http.d.ts +27 -0
- package/dist/mcp/http.js +92 -0
- package/dist/mcp/oauth.d.ts +91 -0
- package/dist/mcp/oauth.js +241 -0
- package/dist/mcp/route.d.ts +15 -0
- package/dist/mcp/route.js +42 -0
- package/dist/mcp/runner.d.ts +45 -0
- package/dist/mcp/runner.js +99 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +102 -0
- package/dock/agents.ts +121 -0
- package/dock/apns.ts +138 -0
- package/dock/console.ts +23 -0
- package/dock/dialers.ts +85 -0
- package/dock/faculty.ts +148 -0
- package/dock/fcm.ts +103 -0
- package/dock/grants.ts +111 -0
- package/dock/http2.ts +70 -0
- package/dock/index.ts +133 -0
- package/dock/local.ts +48 -0
- package/dock/location.ts +22 -0
- package/dock/notifications.ts +67 -0
- package/dock/push.ts +161 -0
- package/dock/quo-app-bells.md +280 -0
- package/dock/quo-dock.md +1942 -0
- package/dock/service.ts +100 -0
- package/dock/timer.ts +174 -0
- package/dock/webpush.ts +182 -0
- package/harbor/browser.ts +97 -0
- package/harbor/capacitor.ts +223 -0
- package/harbor/disk.ts +237 -0
- package/harbor/edge/alarm.ts +48 -0
- package/harbor/edge/edge.ts +407 -0
- package/harbor/edge/exercise.ts +241 -0
- package/harbor/edge/given.ts +106 -0
- package/harbor/edge/platform.d.ts +39 -0
- package/harbor/edge/storage.ts +179 -0
- package/harbor/edge/text.d.ts +15 -0
- package/harbor/edge/worker.ts +70 -0
- package/harbor/edge/wrangler.toml +30 -0
- package/harbor/files.ts +221 -0
- package/harbor/http.ts +188 -0
- package/harbor/idb.ts +0 -0
- package/harbor/quo-harbor.md +850 -0
- package/harbor/quo.ts +44 -0
- package/harbor/resolve.ts +31 -0
- package/harbor/seal.ts +26 -0
- package/harbor/sealed.ts +183 -0
- package/harbor/tauri.ts +79 -0
- package/harbor/ward.ts +28 -0
- package/human/app.ts +580 -0
- package/human/apps.ts +47 -0
- package/human/dom.ts +21 -0
- package/human/door.ts +113 -0
- package/human/guest.ts +34 -0
- package/human/html.ts +490 -0
- package/human/local.ts +30 -0
- package/human/mark.ts +53 -0
- package/human/move.ts +122 -0
- package/human/quo-human.md +1042 -0
- package/human/screen.ts +136 -0
- package/human/style.ts +63 -0
- package/human/tab.bundle.txt +4775 -0
- package/human/tab.ts +446 -0
- package/human/tree.ts +217 -0
- package/human/wake.ts +191 -0
- package/human/web.ts +259 -0
- package/human/worker.bundle.txt +3868 -0
- package/human/worker.ts +124 -0
- package/human/worlds.ts +127 -0
- package/mcp/agent.ts +94 -0
- package/mcp/allow.ts +131 -0
- package/mcp/http.ts +92 -0
- package/mcp/oauth.ts +270 -0
- package/mcp/quo-mcp.md +343 -0
- package/mcp/route.ts +45 -0
- package/mcp/runner.ts +136 -0
- package/mcp/server.ts +101 -0
- package/package.json +121 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// The door page: what a stranger sees where nothing lets them in. No
|
|
3
|
+
// invitation in the fragment, no relation in this tab's harbor, and nobody
|
|
4
|
+
// at the ward's door; or a harbor's root with no world to list. It is
|
|
5
|
+
// Quo's page and not the estate's: it says what this is and that a world
|
|
6
|
+
// here is entered by a link someone sends you, and it asks for nothing,
|
|
7
|
+
// since there is nothing a stranger could type that would let them in. A
|
|
8
|
+
// world that wants a face of its own has a public being with a page and a
|
|
9
|
+
// look, or its own page on its own origin; this is the default and nothing
|
|
10
|
+
// more. It is the first page written in the grammar, `tree.ts`, painted
|
|
11
|
+
// with nobody behind it: no ask, no answer, no standing.
|
|
12
|
+
import { paint, nobody } from './tree.js';
|
|
13
|
+
import { escape } from './html.js';
|
|
14
|
+
import { MARK } from './mark.js';
|
|
15
|
+
export function doorTree(at) {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'stack',
|
|
18
|
+
as: 'hero',
|
|
19
|
+
of: [
|
|
20
|
+
{ kind: 'image', src: MARK, alt: '' },
|
|
21
|
+
{ kind: 'text', text: 'quo', role: 'title' },
|
|
22
|
+
{ kind: 'text', text: at.world ? `${at.world} at ${at.host}` : at.host, role: 'label' },
|
|
23
|
+
{ kind: 'text', text: at.world ? 'This world is entered by invitation.' : 'The worlds here are entered by invitation.', role: 'lead' },
|
|
24
|
+
{ kind: 'text', text: 'An invitation is a link someone sends you. Open it here, and you are in: no account, no password, nothing to type. What you can do inside is what the world shows you, and it is yours to keep on this device.' },
|
|
25
|
+
{ kind: 'text', text: 'Nothing on this page asks anything of you. If you were sent here without a link, ask the person who sent you for one.', role: 'quiet' },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function door(at) {
|
|
30
|
+
return `<main data-door>${paint(doorTree(at), nobody)}</main>`;
|
|
31
|
+
}
|
|
32
|
+
// The other page with nobody behind it: what a human sees where this
|
|
33
|
+
// browser already holds the harbor in another tab. A device is one harbor
|
|
34
|
+
// and the lock is what says so, since two harbors over one seed would be
|
|
35
|
+
// two devices claiming to be the same one, each writing the other's wards.
|
|
36
|
+
// So the second tab is a page and not a device, and the page says which of
|
|
37
|
+
// the two it is rather than saying `booting` until she gives up. Nothing
|
|
38
|
+
// here offers to take the lock: the tab that holds it may be the one she
|
|
39
|
+
// is working in.
|
|
40
|
+
export function heldTree(at) {
|
|
41
|
+
return {
|
|
42
|
+
kind: 'stack',
|
|
43
|
+
as: 'hero',
|
|
44
|
+
of: [
|
|
45
|
+
{ kind: 'image', src: MARK, alt: '' },
|
|
46
|
+
{ kind: 'text', text: 'Already open', role: 'title' },
|
|
47
|
+
{ kind: 'text', text: at.world ? `${at.world} at ${at.host}` : at.host, role: 'label' },
|
|
48
|
+
{ kind: 'text', text: 'This world is open in another tab of this browser, and one browser is one device here. That tab holds the device; this one is only a page.', role: 'lead' },
|
|
49
|
+
{ kind: 'text', text: 'Go back to the tab that has it, or close that tab and reload this one. Nothing is lost either way: what this device is in every world is kept in the browser, not in a tab.' },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function held(at) {
|
|
54
|
+
return `<main data-door>${paint(heldTree(at), nobody)}</main>`;
|
|
55
|
+
}
|
|
56
|
+
// The policy every page Quo writes itself is served under, wherever it is
|
|
57
|
+
// served from. These pages run no script and want none, so nothing but
|
|
58
|
+
// their own form is allowed anywhere, an image is the mark and therefore
|
|
59
|
+
// data, and no referer carries what brought a human here off the page.
|
|
60
|
+
export const POLICY = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
61
|
+
// A painter for Quo's own pages. It is `nobody` with links painted, and
|
|
62
|
+
// that is sound where the other painter's caution is not: the vouching rule
|
|
63
|
+
// exists because a far being names a place and the side cannot know where
|
|
64
|
+
// she is sending a human. On these pages the dock wrote the address itself,
|
|
65
|
+
// out of what the daemon was configured with, so there is nobody to vouch
|
|
66
|
+
// for. No ask, no answer and no standing, as before.
|
|
67
|
+
export const ours = {
|
|
68
|
+
...nobody,
|
|
69
|
+
link: (href, text) => `<a class="link" href="${escape(href)}">${escape(text)}</a>`,
|
|
70
|
+
};
|
|
71
|
+
export function landedTree(l) {
|
|
72
|
+
const of = [
|
|
73
|
+
{ kind: 'image', src: MARK, alt: '' },
|
|
74
|
+
{ kind: 'text', text: l.ok ? 'That is done' : 'That did not finish', role: 'title' },
|
|
75
|
+
{ kind: 'text', text: l.world ? `${l.world} at ${l.host}` : l.host, role: 'label' },
|
|
76
|
+
{
|
|
77
|
+
kind: 'text',
|
|
78
|
+
text: l.ok
|
|
79
|
+
? 'You came back from somewhere this world sent you, and it heard you. Nothing else is needed here.'
|
|
80
|
+
: 'You came back from somewhere this world sent you, and it could not finish. Nothing was taken from you here, and you can try again from the world itself.',
|
|
81
|
+
role: 'lead',
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
if (l.at)
|
|
85
|
+
of.push({ kind: 'link', href: l.at, text: l.world ? `back to ${l.world}` : 'back to this world' });
|
|
86
|
+
else
|
|
87
|
+
of.push({ kind: 'text', text: 'You can close this page.', role: 'quiet' });
|
|
88
|
+
return { kind: 'stack', as: 'hero', of };
|
|
89
|
+
}
|
|
90
|
+
export function landed(l) {
|
|
91
|
+
return `<main data-door>${paint(landedTree(l), ours)}</main>`;
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// A guest at a world's door: the world's public being, whatever class she
|
|
3
|
+
// is, as a subject the screen can render. Her describe for a stranger is the
|
|
4
|
+
// guest page; a form on it is a knock on the public invitation; and an
|
|
5
|
+
// answer that is an invitation is the way in, which the tab takes by
|
|
6
|
+
// joining. Nothing here knows the desk, and nothing needs to: the forms are
|
|
7
|
+
// drawn from whatever asks she has, and what makes one of them the way in
|
|
8
|
+
// is that its answer is an invitation. She may call it anything.
|
|
9
|
+
//
|
|
10
|
+
// The proof way in is the one place a name is needed, since a proof arrives
|
|
11
|
+
// in a link's fragment rather than typed, so there is no form to answer and
|
|
12
|
+
// the shell must knock a named ask. That name is `device`, and it is a
|
|
13
|
+
// convention of the dock's like `hello`, `look` and `page`: a public being
|
|
14
|
+
// of any class takes proofs by writing an ask under that name, and one that
|
|
15
|
+
// writes none has no proof way in and loses nothing else.
|
|
16
|
+
import { isSilence, isWord, wordOf } from '@nervur-org/nervur';
|
|
17
|
+
export function guest(avatar, ward) {
|
|
18
|
+
const at = { ward };
|
|
19
|
+
return {
|
|
20
|
+
tools: async () => {
|
|
21
|
+
const bp = await avatar.knock(at);
|
|
22
|
+
if (isSilence(bp))
|
|
23
|
+
return { error: 'silence' };
|
|
24
|
+
if (isWord(bp))
|
|
25
|
+
return { error: wordOf(bp) };
|
|
26
|
+
if (bp === null || typeof bp !== 'object' || Array.isArray(bp) || !Array.isArray(bp.asks))
|
|
27
|
+
return { error: 'nobody is home' };
|
|
28
|
+
return bp;
|
|
29
|
+
},
|
|
30
|
+
call: (name, args = {}, wanted) => avatar.knock(at, name, args, wanted),
|
|
31
|
+
ears: new Set(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Answer, Ask, Blueprint, Json, JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import { type Word } from '../beings/side.ts';
|
|
3
|
+
import { hintFor, type Look, type Hint } from '../beings/look.ts';
|
|
4
|
+
import { type Node } from './tree.ts';
|
|
5
|
+
export type Property = {
|
|
6
|
+
type?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
enum?: Json[];
|
|
9
|
+
format?: string;
|
|
10
|
+
default?: Json;
|
|
11
|
+
};
|
|
12
|
+
export type Schema = {
|
|
13
|
+
properties?: Record<string, Property>;
|
|
14
|
+
required?: string[];
|
|
15
|
+
};
|
|
16
|
+
export declare const escape: (s: string) => string;
|
|
17
|
+
export declare const MOMENT = "moment";
|
|
18
|
+
export declare const mine: (ask: Ask, at?: number) => JsonObject;
|
|
19
|
+
export declare const asks: (ask: Ask) => string[];
|
|
20
|
+
export declare function field(name: string, p: Property, must: boolean): string;
|
|
21
|
+
export declare function form(ask: Ask, h?: Hint): string;
|
|
22
|
+
export type Raw = Record<string, string>;
|
|
23
|
+
export declare function values(ask: Ask, raw: Raw): {
|
|
24
|
+
args: JsonObject;
|
|
25
|
+
} | {
|
|
26
|
+
error: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function view(value: Json, schema?: JsonObject): string;
|
|
29
|
+
export declare function face(w: Word, schema?: JsonObject): string;
|
|
30
|
+
export declare function people(value: Json): string;
|
|
31
|
+
export declare function feed(value: Json): string;
|
|
32
|
+
export type Model = {
|
|
33
|
+
blueprint: Blueprint | null;
|
|
34
|
+
look: Look;
|
|
35
|
+
tree: Node | null;
|
|
36
|
+
pages: Record<string, Node | null>;
|
|
37
|
+
focus?: string;
|
|
38
|
+
at?: string;
|
|
39
|
+
notice: string;
|
|
40
|
+
standing: Standing;
|
|
41
|
+
answers: Record<string, Word>;
|
|
42
|
+
pushes: JsonObject[];
|
|
43
|
+
};
|
|
44
|
+
export type Standing = 'in' | 'ended' | 'away' | 'error';
|
|
45
|
+
export declare function standing(error: string): Standing;
|
|
46
|
+
export declare function stands(error: string): string;
|
|
47
|
+
export declare function refused(answer: Answer): string;
|
|
48
|
+
export declare function title(bp: Blueprint | null, l?: Look): string;
|
|
49
|
+
export declare function look(l: Look | undefined): {
|
|
50
|
+
style: string;
|
|
51
|
+
head: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function ordered(all: Ask[], l: Look | undefined): Ask[];
|
|
54
|
+
export { hintFor };
|
|
55
|
+
export declare function page(m: Model): string;
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { SILENCE_TEXT, UNREACHED_TEXT, word, wordText } from '../beings/side.js';
|
|
2
|
+
import { hint, hintFor, sanitise, groups as grouped, presentation } from '../beings/look.js';
|
|
3
|
+
import { paint, titled } from './tree.js';
|
|
4
|
+
import { isInvitation } from '@nervur-org/nervur';
|
|
5
|
+
import { link } from '../beings/link.js';
|
|
6
|
+
export const escape = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ?? c);
|
|
7
|
+
const props = (schema) => Object.entries((schema?.properties ?? {}));
|
|
8
|
+
const required = (schema) => new Set((schema?.required ?? []));
|
|
9
|
+
// ---- what a side fills for itself
|
|
10
|
+
//
|
|
11
|
+
// An ask says what its arguments are, and one of them a side can answer
|
|
12
|
+
// better than the human can: the moment. A being may not read a clock, since
|
|
13
|
+
// a being who could would be a being who could not be moved, so she takes the
|
|
14
|
+
// moment as an argument and every side hands her its own. A screen is not a
|
|
15
|
+
// being; it runs on a device, and a device has a clock.
|
|
16
|
+
//
|
|
17
|
+
// So a property whose schema says `format: "moment"` is never drawn as a
|
|
18
|
+
// field and never typed by anybody. The screen fills it, and a read-only ask
|
|
19
|
+
// with nothing left to ask a human then runs on its own and shows a table
|
|
20
|
+
// where a form would have been. Nothing about this is presentation, which is
|
|
21
|
+
// why it is read off the input schema and not off a look: the ask says what
|
|
22
|
+
// the argument is, and each side decides for itself what to do with that.
|
|
23
|
+
export const MOMENT = 'moment';
|
|
24
|
+
export const mine = (ask, at = Date.now()) => Object.fromEntries(props(ask.input).filter(([, p]) => p.format === MOMENT).map(([name]) => [name, at]));
|
|
25
|
+
// What is left for a human once the side has filled its own.
|
|
26
|
+
export const asks = (ask) => [...required(ask.input)].filter((name) => !(name in mine(ask)));
|
|
27
|
+
// ---- a schema as a form
|
|
28
|
+
// A property is one field by its type. A string is a text input, and a
|
|
29
|
+
// format the browser knows, `password`, `date`, `email`, is the input's
|
|
30
|
+
// type. A number is a number input. A boolean is a checkbox. An enum is a
|
|
31
|
+
// select. An object or an array is a box for JSON, because a form has no
|
|
32
|
+
// better face for a value the schema left open. A description is the label.
|
|
33
|
+
export function field(name, p, must) {
|
|
34
|
+
const id = escape(name);
|
|
35
|
+
const label = `<label for="${id}">${id}${must ? ' <b>*</b>' : ''}</label>`;
|
|
36
|
+
const help = p.description ? `<p class="nv-hint">${escape(p.description)}</p>` : '';
|
|
37
|
+
const one = (control) => `<div class="nv-field">${label}${control}${help}</div>`;
|
|
38
|
+
const req = must ? ' required' : '';
|
|
39
|
+
if (Array.isArray(p.enum)) {
|
|
40
|
+
// An enum value is a scalar in every schema a being writes, but the
|
|
41
|
+
// schema comes from her and not from here: an object among the options
|
|
42
|
+
// is shown as what it is rather than as `[object Object]`.
|
|
43
|
+
const text = (v) => (v !== null && typeof v === 'object' ? JSON.stringify(v) : String(v));
|
|
44
|
+
const options = p.enum.map((v) => `<option value="${escape(text(v))}">${escape(text(v))}</option>`).join('');
|
|
45
|
+
return one(`<select id="${id}" name="${id}"${req}>${must ? '' : '<option value=""></option>'}${options}</select>`);
|
|
46
|
+
}
|
|
47
|
+
if (p.type === 'boolean')
|
|
48
|
+
return `<div class="nv-field" data-inline><input type="checkbox" id="${id}" name="${id}" value="true">${label}${help}</div>`;
|
|
49
|
+
if (p.type === 'number' || p.type === 'integer')
|
|
50
|
+
return one(`<input type="number" id="${id}" name="${id}"${p.type === 'integer' ? ' step="1"' : ' step="any"'}${req}>`);
|
|
51
|
+
if (p.type === 'string' || p.type === undefined) {
|
|
52
|
+
const kind = p.format === 'password' || p.format === 'date' || p.format === 'email' || p.format === 'time' ? p.format : 'text';
|
|
53
|
+
return one(`<input type="${kind}" id="${id}" name="${id}"${req}>`);
|
|
54
|
+
}
|
|
55
|
+
return one(`<textarea id="${id}" name="${id}" placeholder="JSON"${req}></textarea>`);
|
|
56
|
+
}
|
|
57
|
+
// The ask as a form: its name is the button, its description the legend's
|
|
58
|
+
// small print, and `data-ask` is how the surface says which ask was sent.
|
|
59
|
+
export function form(ask, h = {}) {
|
|
60
|
+
const must = required(ask.input);
|
|
61
|
+
const own = mine(ask);
|
|
62
|
+
const fields = props(ask.input)
|
|
63
|
+
.filter(([name]) => !(name in own)) // the side fills it; nobody types a moment
|
|
64
|
+
.map(([name, p]) => field(name, p, must.has(name)))
|
|
65
|
+
.join('');
|
|
66
|
+
const label = `${h.icon ? `${escape(h.icon)} ` : ''}${escape(h.title ?? ask.name)}`;
|
|
67
|
+
const care = h.destructive ? ' data-confirm="true"' : '';
|
|
68
|
+
const said = ask.description ? `<p class="nv-hint">${escape(ask.description)}</p>` : '';
|
|
69
|
+
return `<form class="nv-stack" data-ask="${escape(ask.name)}"${care}><fieldset><legend>${label}</legend>${said}${fields}<div class="nv-row" data-align="center"><button class="nv-button"${h.destructive ? ' data-variant="danger"' : ''}>${label}</button></div></fieldset></form>`;
|
|
70
|
+
}
|
|
71
|
+
// The strings typed by the schema. An empty field that is not required is
|
|
72
|
+
// left out, so the being sees what the human said and nothing else; an
|
|
73
|
+
// empty required field goes as the empty string, and the being answers.
|
|
74
|
+
// A JSON box that does not parse is an error the side shows before asking.
|
|
75
|
+
export function values(ask, raw) {
|
|
76
|
+
// The side's own first, so a form the human submits carries the moment it
|
|
77
|
+
// was submitted at rather than the one the page was drawn at.
|
|
78
|
+
const args = mine(ask);
|
|
79
|
+
const must = required(ask.input);
|
|
80
|
+
for (const [name, p] of props(ask.input)) {
|
|
81
|
+
if (name in args)
|
|
82
|
+
continue;
|
|
83
|
+
const s = raw[name];
|
|
84
|
+
if (p.type === 'boolean') {
|
|
85
|
+
if (s === 'true')
|
|
86
|
+
args[name] = true;
|
|
87
|
+
else if (must.has(name))
|
|
88
|
+
args[name] = false;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (s === undefined || (s === '' && !must.has(name)))
|
|
92
|
+
continue;
|
|
93
|
+
if (p.type === 'number' || p.type === 'integer') {
|
|
94
|
+
const n = Number(s);
|
|
95
|
+
if (s === '' || Number.isNaN(n))
|
|
96
|
+
return { error: `${name} is a number` };
|
|
97
|
+
args[name] = n;
|
|
98
|
+
}
|
|
99
|
+
else if (p.type === 'string' || p.type === undefined || Array.isArray(p.enum)) {
|
|
100
|
+
args[name] = s;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
try {
|
|
104
|
+
args[name] = JSON.parse(s);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { error: `${name} is not JSON` };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { args };
|
|
112
|
+
}
|
|
113
|
+
// ---- an answer as a view
|
|
114
|
+
// A value has one face by its shape. A primitive is text. An object is a
|
|
115
|
+
// table of its keys, in the output schema's order when she declared one and
|
|
116
|
+
// her own order otherwise, each value viewed in turn. A list of objects is
|
|
117
|
+
// one table with a column per key seen; any other list is a list. Null is
|
|
118
|
+
// the word nothing, so that an answer of null is never a blank.
|
|
119
|
+
export function view(value, schema) {
|
|
120
|
+
if (value === null)
|
|
121
|
+
return '<i>nothing</i>';
|
|
122
|
+
if (typeof value !== 'object')
|
|
123
|
+
return escape(String(value));
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
if (value.length && value.every((v) => v !== null && typeof v === 'object' && !Array.isArray(v))) {
|
|
126
|
+
const rows = value;
|
|
127
|
+
const keys = [...new Set(rows.flatMap((r) => Object.keys(r)))];
|
|
128
|
+
const head = keys.map((k) => `<th>${escape(k)}</th>`).join('');
|
|
129
|
+
const body = rows.map((r) => `<tr>${keys.map((k) => `<td>${k in r ? view(r[k]) : ''}</td>`).join('')}</tr>`).join('');
|
|
130
|
+
return `<div class="nv-table-scroll"><table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
|
|
131
|
+
}
|
|
132
|
+
return value.length ? `<ul class="nv-list" role="list">${value.map((v) => `<li class="nv-item">${view(v)}</li>`).join('')}</ul>` : '<p class="nv-empty">none</p>';
|
|
133
|
+
}
|
|
134
|
+
const declared = props(schema).map(([k]) => k);
|
|
135
|
+
const keys = [...declared.filter((k) => k in value), ...Object.keys(value).filter((k) => !declared.includes(k))];
|
|
136
|
+
if (!keys.length)
|
|
137
|
+
return '<i>empty</i>';
|
|
138
|
+
return `<div class="nv-table-scroll"><table>${keys.map((k) => `<tr><th>${escape(k)}</th><td>${view(value[k], props(schema).find(([n]) => n === k)?.[1])}</td></tr>`).join('')}</table></div>`;
|
|
139
|
+
}
|
|
140
|
+
// The three words for "no object", and the object, each with a face. An
|
|
141
|
+
// error object is the answer she gave, marked; silence and unreached are
|
|
142
|
+
// what the side says about them, since they carry nothing themselves. The
|
|
143
|
+
// word is on the answer for a design and a test to read, and the tone is
|
|
144
|
+
// what a reader sees: hers in red, the wire's in amber.
|
|
145
|
+
export function face(w, schema) {
|
|
146
|
+
if (w.word === 'object')
|
|
147
|
+
return `<div class="nv-answer" data-word="object">${view(w.value, schema)}</div>`;
|
|
148
|
+
if (w.word === 'error')
|
|
149
|
+
return `<div class="nv-answer" data-word="error" data-tone="danger">${view(w.value)}</div>`;
|
|
150
|
+
if (w.word === 'silence')
|
|
151
|
+
return `<div class="nv-answer" data-word="silence" data-tone="warning">${escape(SILENCE_TEXT)}</div>`;
|
|
152
|
+
if (w.word === 'quo')
|
|
153
|
+
return `<div class="nv-answer" data-word="quo" data-tone="warning">${escape(wordText(w.name))}</div>`;
|
|
154
|
+
return `<div class="nv-answer" data-word="unreached" data-tone="warning">${escape(UNREACHED_TEXT)}</div>`;
|
|
155
|
+
}
|
|
156
|
+
// ---- an answer of a named kind
|
|
157
|
+
// Rows of people: a name, a line under it, and a face when she has one, on
|
|
158
|
+
// one surface. A row that is not a person is shown by its own shape, so a
|
|
159
|
+
// page that said `people` of a list of strings still shows the strings.
|
|
160
|
+
const person = (v) => {
|
|
161
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v) || typeof v.name !== 'string')
|
|
162
|
+
return view(v);
|
|
163
|
+
const name = v.name;
|
|
164
|
+
const line = typeof v.line === 'string' ? `<p class="nv-t-quiet">${escape(v.line)}</p>` : '';
|
|
165
|
+
const pic = typeof v.picture === 'string' && /^data:image\//.test(v.picture) ? `<img alt="" src="${v.picture}">` : escape(name.trim().charAt(0).toUpperCase());
|
|
166
|
+
return `<div class="nv-person"><span class="nv-avatar">${pic}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p>${line}</div></div>`;
|
|
167
|
+
};
|
|
168
|
+
export function people(value) {
|
|
169
|
+
if (!Array.isArray(value))
|
|
170
|
+
return view(value);
|
|
171
|
+
if (!value.length)
|
|
172
|
+
return '<p class="nv-empty">nobody yet</p>';
|
|
173
|
+
return `<div class="nv-list">${value.map((v) => `<div class="nv-item">${person(v)}</div>`).join('')}</div>`;
|
|
174
|
+
}
|
|
175
|
+
// What happened, in order: a moment and the words, down a line.
|
|
176
|
+
const happened = (v) => {
|
|
177
|
+
if (v === null || typeof v !== 'object' || Array.isArray(v) || typeof v.text !== 'string')
|
|
178
|
+
return view(v);
|
|
179
|
+
const at = typeof v.at === 'string' || typeof v.at === 'number' ? `<p class="nv-t-label">${escape(String(v.at))}</p>` : '';
|
|
180
|
+
return `<div class="nv-stack">${at}<p class="nv-t-body">${escape(v.text)}</p></div>`;
|
|
181
|
+
};
|
|
182
|
+
export function feed(value) {
|
|
183
|
+
if (!Array.isArray(value))
|
|
184
|
+
return view(value);
|
|
185
|
+
if (!value.length)
|
|
186
|
+
return '<p class="nv-empty">nothing yet</p>';
|
|
187
|
+
return `<div class="nv-feed">${value.map(happened).join('')}</div>`;
|
|
188
|
+
}
|
|
189
|
+
const ENDED = new Set(['removed', 'dropped', 'not joined']);
|
|
190
|
+
const AWAY = new Set(['unreached', 'silence', 'late', 'absent']);
|
|
191
|
+
export function standing(error) {
|
|
192
|
+
return ENDED.has(error) ? 'ended' : AWAY.has(error) ? 'away' : 'error';
|
|
193
|
+
}
|
|
194
|
+
// What a human reads for one, in her own terms rather than the ward's word.
|
|
195
|
+
export function stands(error) {
|
|
196
|
+
switch (standing(error)) {
|
|
197
|
+
case 'ended':
|
|
198
|
+
return error === 'not joined' ? 'you are not in this world' : 'this world ended it: you are out, and nothing here reaches it';
|
|
199
|
+
case 'away':
|
|
200
|
+
return `this world cannot be reached right now, and nothing was delivered (${error})`;
|
|
201
|
+
default:
|
|
202
|
+
return `not in: ${error}`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// What a human reads when a way in did not let her in. A world refuses for
|
|
206
|
+
// several different reasons and a shell that said one sentence for all of
|
|
207
|
+
// them sent her looking for the wrong thing. One of them has an answer and
|
|
208
|
+
// is the reason this exists: `id taken` is a device of that name already in
|
|
209
|
+
// this world, which is exactly what a human meets on a new device after
|
|
210
|
+
// losing the one that held it, since a presence is a seed and the seed went
|
|
211
|
+
// with the device. The standing it left is still there, and hers to take
|
|
212
|
+
// out from any device she still holds.
|
|
213
|
+
//
|
|
214
|
+
// The three words it knows are the dock's own beings', the desk's and the
|
|
215
|
+
// user being's, and knowing them is allowed because they ship in this
|
|
216
|
+
// package beside this file: what the screen may not do is invent a word a
|
|
217
|
+
// being never said, and it invents none. Any other public being falls
|
|
218
|
+
// through and is quoted in her own words, so a world that answers something
|
|
219
|
+
// of its own is passed on rather than guessed at. That is the whole rule,
|
|
220
|
+
// and it is why there is a default branch and not a table.
|
|
221
|
+
export function refused(answer) {
|
|
222
|
+
const w = word(answer);
|
|
223
|
+
if (w.word === 'silence')
|
|
224
|
+
return SILENCE_TEXT;
|
|
225
|
+
if (w.word === 'unreached')
|
|
226
|
+
return UNREACHED_TEXT;
|
|
227
|
+
if (w.word === 'quo')
|
|
228
|
+
return wordText(w.name);
|
|
229
|
+
const said = w.word === 'error' && typeof w.value.error === 'string' ? w.value.error : '';
|
|
230
|
+
switch (said) {
|
|
231
|
+
case 'id taken':
|
|
232
|
+
return 'a device of that name is already in this world. If it is one you have lost, take it out of the world from a device you still hold, or come back under another name.';
|
|
233
|
+
case 'no such user':
|
|
234
|
+
return 'this world knows nobody by that way in.';
|
|
235
|
+
case 'refused':
|
|
236
|
+
return 'that way in was refused: it may be spent, or it may never have been one.';
|
|
237
|
+
default:
|
|
238
|
+
return said ? `that way in was refused: ${said}` : 'that way in was refused.';
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// The title is the one hint the notes may carry: a string named `name`.
|
|
242
|
+
// The rest of the notes is shown as a view and read as nothing else.
|
|
243
|
+
export function title(bp, l = {}) {
|
|
244
|
+
if (l.name)
|
|
245
|
+
return l.name;
|
|
246
|
+
const n = bp?.notes;
|
|
247
|
+
return n !== null && typeof n === 'object' && !Array.isArray(n) && typeof n.name === 'string' && n.name ? n.name : 'quo';
|
|
248
|
+
}
|
|
249
|
+
// ---- a look
|
|
250
|
+
// A look as the page paints it: the tokens become CSS variables on one
|
|
251
|
+
// section, and the name and the logo its heading. The shape of every token
|
|
252
|
+
// is the dock's, `beings/look.ts`; this only writes what survived it.
|
|
253
|
+
// Her tokens become the kit's slots on her section and nowhere else: her
|
|
254
|
+
// accent is the accent there, her ground the raised ground, her font the
|
|
255
|
+
// sans, her radius the large corner. The kit's derived slots follow from
|
|
256
|
+
// the accent in the stylesheet, so one colour of hers recolours her whole
|
|
257
|
+
// section and nothing outside it.
|
|
258
|
+
export function look(l) {
|
|
259
|
+
if (!l)
|
|
260
|
+
return { style: '', head: '' };
|
|
261
|
+
const vars = [];
|
|
262
|
+
if (l.accent)
|
|
263
|
+
vars.push(`--nv-color-accent:${l.accent}`, `--nv-color-accent-hover:${l.accent}`, `--nv-color-link:${l.accent}`);
|
|
264
|
+
if (l.background)
|
|
265
|
+
vars.push(`--nv-color-bg-raised:${l.background}`);
|
|
266
|
+
if (l.foreground)
|
|
267
|
+
vars.push(`--nv-color-fg:${l.foreground}`, `--nv-color-fg-body:${l.foreground}`);
|
|
268
|
+
if (l.font)
|
|
269
|
+
vars.push(`--nv-font-sans:${l.font}`, `--nv-font-display:${l.font}`);
|
|
270
|
+
if (l.radius !== undefined)
|
|
271
|
+
vars.push(`--nv-radius-lg:${l.radius}px`, `--nv-radius-xl:${l.radius}px`);
|
|
272
|
+
const logo = l.logo ? `<img class="logo" alt="" src="${l.logo}">` : '';
|
|
273
|
+
const name = l.name ? escape(l.name) : '';
|
|
274
|
+
return { style: vars.length ? ` style="${vars.join(';')}"` : '', head: logo || name ? `<h2>${logo}${name}</h2>` : '' };
|
|
275
|
+
}
|
|
276
|
+
// Her asks in the order her look asks for, the rest after in her own order.
|
|
277
|
+
export function ordered(all, l) {
|
|
278
|
+
const want = l?.order ?? [];
|
|
279
|
+
return [...want.map((n) => all.find((a) => a.name === n)).filter((a) => a !== undefined), ...all.filter((a) => !want.includes(a.name))];
|
|
280
|
+
}
|
|
281
|
+
export { hintFor };
|
|
282
|
+
export function page(m) {
|
|
283
|
+
const bp = m.blueprint;
|
|
284
|
+
const pres = presentation(bp);
|
|
285
|
+
// An answer that is an invitation is a way in, and the only thing to do
|
|
286
|
+
// with one is to open it: painted as the link it is, on this world's page
|
|
287
|
+
// address. A being does not know where her page lives and the side does,
|
|
288
|
+
// which is why the grammar carries no URL and this is not a node.
|
|
289
|
+
const shows = (w, schema) => {
|
|
290
|
+
if (w.word !== 'object' || m.at === undefined || !isInvitation(w.value))
|
|
291
|
+
return face(w, schema);
|
|
292
|
+
const url = link(m.at, w.value);
|
|
293
|
+
return `<div class="nv-answer" data-word="object" data-way><a class="nv-key" href="${escape(url)}">${escape(url)}</a></div>`;
|
|
294
|
+
};
|
|
295
|
+
const one = (l, prefix = '') => (a) => {
|
|
296
|
+
const w = m.answers[a.name];
|
|
297
|
+
const h = hint(l, a.name.startsWith(prefix) ? a.name.slice(prefix.length) : a.name);
|
|
298
|
+
return `<section class="nv-stack">${form(a, h)}${w ? shows(w, a.output) : ''}</section>`;
|
|
299
|
+
};
|
|
300
|
+
// an answer shown as the page asks: as text, as cards, as a list, as
|
|
301
|
+
// people, as a feed, or by its shape
|
|
302
|
+
const answer = (name, as) => {
|
|
303
|
+
const a = bp?.asks.find((x) => x.name === name);
|
|
304
|
+
const w = a ? m.answers[name] : undefined;
|
|
305
|
+
if (!a || !w)
|
|
306
|
+
return '';
|
|
307
|
+
if (w.word === 'object' && as === 'text')
|
|
308
|
+
return `<div class="nv-answer" data-word="object">${view(w.value === null || typeof w.value !== 'object' ? w.value : JSON.stringify(w.value))}</div>`;
|
|
309
|
+
if (w.word === 'object' && as === 'cards' && Array.isArray(w.value))
|
|
310
|
+
return `<div class="nv-cards">${w.value.map((v) => `<div class="nv-card">${view(v)}</div>`).join('')}</div>`;
|
|
311
|
+
if (w.word === 'object' && as === 'list' && Array.isArray(w.value))
|
|
312
|
+
return `<ul class="nv-list" role="list">${w.value.map((v) => `<li class="nv-item">${view(v)}</li>`).join('')}</ul>`;
|
|
313
|
+
if (w.word === 'object' && as === 'people')
|
|
314
|
+
return people(w.value);
|
|
315
|
+
if (w.word === 'object' && as === 'feed')
|
|
316
|
+
return feed(w.value);
|
|
317
|
+
return shows(w, a.output);
|
|
318
|
+
};
|
|
319
|
+
// ---- where a being may send a human
|
|
320
|
+
//
|
|
321
|
+
// A being names a place and the side decides whether it becomes an anchor.
|
|
322
|
+
// Two origins are vouched for and no third. The world's own, which is the
|
|
323
|
+
// origin serving this very page, so a being may move a human around inside
|
|
324
|
+
// the world she already chose. And the origins she declared in her look as
|
|
325
|
+
// the ones she stands in for, which is how an envoy speaking for a payment
|
|
326
|
+
// or a booking vendor sends a human to that vendor's own checkout.
|
|
327
|
+
//
|
|
328
|
+
// The declaration is what makes the second safe to allow. A look is asked
|
|
329
|
+
// once per digest and is the same for every answer she gives, so the
|
|
330
|
+
// places she stands in for are a standing claim rather than a destination
|
|
331
|
+
// that changes under one reply. That is what bounds the attack this rule
|
|
332
|
+
// is for: a vendor's response relayed through an envoy, carrying an
|
|
333
|
+
// address nobody here wrote. It does not bound a being who is hostile
|
|
334
|
+
// herself, and nothing on a page could; what bounds her is that a human
|
|
335
|
+
// chose to hold a standing at her.
|
|
336
|
+
//
|
|
337
|
+
// An address the side will not vouch for is painted as its own words. The
|
|
338
|
+
// human sees exactly where she was being sent, in full, and goes there by
|
|
339
|
+
// her own hand or not at all. Nothing is dropped and nothing is hidden,
|
|
340
|
+
// because a link silently deleted is a page that lies about what the being
|
|
341
|
+
// said.
|
|
342
|
+
const originOf = (s) => {
|
|
343
|
+
if (s === undefined)
|
|
344
|
+
return null;
|
|
345
|
+
try {
|
|
346
|
+
return new URL(s).origin;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
const here = originOf(m.at);
|
|
353
|
+
const linkedBy = (l) => (href, text) => {
|
|
354
|
+
const origin = originOf(href);
|
|
355
|
+
if (origin === null)
|
|
356
|
+
return escape(href);
|
|
357
|
+
const vouched = (here !== null && origin === here) || (l?.stands?.includes(origin) ?? false);
|
|
358
|
+
if (!vouched)
|
|
359
|
+
return `<span class="nv-key link away" title="${escape(href)}">${escape(href)}</span>`;
|
|
360
|
+
// `noopener` and `noreferrer` because a page opened from here is a page
|
|
361
|
+
// the world wrote, and it is handed no window and no referer of ours.
|
|
362
|
+
// The origin is on the anchor whether or not a design shows it, so what
|
|
363
|
+
// a human is about to leave for is in the page and not only in a
|
|
364
|
+
// status bar she may never see.
|
|
365
|
+
const out = origin === here ? '' : ' out';
|
|
366
|
+
return `<a class="link${out}" href="${escape(href)}" rel="noopener noreferrer" data-origin="${escape(origin)}">${escape(text)}</a>`;
|
|
367
|
+
};
|
|
368
|
+
const groups = grouped(bp);
|
|
369
|
+
const taken = new Set(Object.values(groups).flatMap((g) => g.asks ?? []));
|
|
370
|
+
// A standing's section. With her page, the far tree is painted inside it
|
|
371
|
+
// through a painter of her own: a name it uses is one of her asks, carried,
|
|
372
|
+
// and never one of the carrier's or another standing's, so a far page
|
|
373
|
+
// reaches nothing outside its section; her title is the page's, and the
|
|
374
|
+
// section keeps her look. Without one, her asks are forms under her name.
|
|
375
|
+
const section = (id, as) => {
|
|
376
|
+
const g = groups[id];
|
|
377
|
+
const inGroup = bp && g ? bp.asks.filter((a) => g.asks?.includes(a.name)) : [];
|
|
378
|
+
if (!inGroup.length)
|
|
379
|
+
return '';
|
|
380
|
+
const kept = sanitise(g.look);
|
|
381
|
+
const l = look(kept);
|
|
382
|
+
const prefix = `${id}-`;
|
|
383
|
+
const tree = m.pages[id];
|
|
384
|
+
// where her own page is, when the world's page is known and this is not it already
|
|
385
|
+
const own = m.at !== undefined && m.focus !== id ? `<p class="nv-t-quiet"><a class="open" href="${escape(`${m.at}/${encodeURIComponent(id)}`)}">open</a></p>` : '';
|
|
386
|
+
const whole = m.focus === id ? ' data-whole' : '';
|
|
387
|
+
// A door: her name and the way to her page, and nothing of hers here.
|
|
388
|
+
// Painted whole on her own page, as any standing is.
|
|
389
|
+
if (as === 'door' && m.focus !== id) {
|
|
390
|
+
const name = kept.name ?? id;
|
|
391
|
+
const count = inGroup.length;
|
|
392
|
+
return `<section class="nv-standing" data-standing="${escape(id)}" data-door${l.style}><div class="nv-person"><span class="nv-avatar">${escape(name.charAt(0).toUpperCase())}</span><div class="nv-stack"><p class="nv-t-body">${escape(name)}</p><p class="nv-t-quiet">${count} ${count === 1 ? 'thing' : 'things'} to do here, on its own page</p></div></div>${own}</section>`;
|
|
393
|
+
}
|
|
394
|
+
if (tree) {
|
|
395
|
+
const hers = {
|
|
396
|
+
form: (name) => {
|
|
397
|
+
const a = inGroup.find((x) => x.name === prefix + name);
|
|
398
|
+
return a ? one(kept, prefix)(a) : '';
|
|
399
|
+
},
|
|
400
|
+
answer: (name, as) => (inGroup.some((x) => x.name === prefix + name) ? answer(prefix + name, as) : ''),
|
|
401
|
+
link: linkedBy(kept), // a carried standing vouches by her own look and never her carrier's
|
|
402
|
+
standing: () => '',
|
|
403
|
+
standings: () => '',
|
|
404
|
+
};
|
|
405
|
+
return `<section class="nv-standing" data-standing="${escape(id)}"${whole}${l.style}>${paint(tree, hers)}${own}</section>`;
|
|
406
|
+
}
|
|
407
|
+
const want = (kept.order ?? []).map((n) => prefix + n);
|
|
408
|
+
const inOrder = ordered(inGroup, { order: want });
|
|
409
|
+
return `<section class="nv-standing" data-standing="${escape(id)}"${whole}${l.style}>${l.head || `<h2>${escape(id)}</h2>`}${inOrder.map(one(kept, prefix)).join('')}${own}</section>`;
|
|
410
|
+
};
|
|
411
|
+
const far = Object.keys(groups).map((id) => section(id)).join('');
|
|
412
|
+
// Her page, when she answered one: painted from her tree, each name it
|
|
413
|
+
// uses looked up in her describe, so a page shows nothing her gate hid.
|
|
414
|
+
const painter = {
|
|
415
|
+
form: (name) => {
|
|
416
|
+
const a = bp?.asks.find((x) => x.name === name && !taken.has(x.name) && !pres.has(x.name));
|
|
417
|
+
return a ? one(m.look)(a) : '';
|
|
418
|
+
},
|
|
419
|
+
answer: (name, as) => (pres.has(name) ? '' : answer(name, as)),
|
|
420
|
+
link: linkedBy(m.look),
|
|
421
|
+
standing: section,
|
|
422
|
+
standings: () => far,
|
|
423
|
+
};
|
|
424
|
+
const mineAsks = bp ? ordered(bp.asks.filter((a) => !taken.has(a.name) && !pres.has(a.name)), m.look).map(one(m.look)).join('') : '';
|
|
425
|
+
// A being's page: one standing she carries as the whole page, and nothing of hers around it. A key her
|
|
426
|
+
// describe does not carry paints as nothing, the way a name on a page does: the address adds no right.
|
|
427
|
+
const focused = m.focus !== undefined;
|
|
428
|
+
const body = focused ? section(m.focus) : m.tree ? paint(m.tree, painter) : `<div class="nv-stack">${mineAsks}${far}</div>`;
|
|
429
|
+
const shown = bp && bp.notes !== null && typeof bp.notes === 'object' && !Array.isArray(bp.notes) ? Object.fromEntries(Object.entries(bp.notes).filter(([k]) => k !== 'standings' && k !== 'name')) : bp?.notes;
|
|
430
|
+
// her notes are shown beside her forms when she has no page: a being with
|
|
431
|
+
// a page says everything she wants said, and a note is wiring
|
|
432
|
+
const notes = bp && !m.tree && shown !== null && shown !== undefined && !(typeof shown === 'object' && !Array.isArray(shown) && !Object.keys(shown).length) ? `<aside class="nv-answer notes">${view(shown)}</aside>` : '';
|
|
433
|
+
const pushes = m.pushes.length ? `<section class="nv-stack pushes"><p class="nv-t-label">pushes</p><div class="nv-feed">${m.pushes.map((p) => `<div>${view(p)}</div>`).join('')}</div></section>` : '';
|
|
434
|
+
const styled = look(m.look);
|
|
435
|
+
// A name is said once. A page that opens with a title of its own is the
|
|
436
|
+
// name, and the bar keeps only where the human stands; a being painted
|
|
437
|
+
// as forms has no page to say it, so the bar says her name; a focused
|
|
438
|
+
// page says the way back and the standing's own tree says the rest.
|
|
439
|
+
const back = focused && m.at !== undefined ? `<a class="back" href="${escape(m.at)}">${escape(title(bp, m.look))}</a>` : '';
|
|
440
|
+
const said = focused ? (m.pages[m.focus] ? titled(m.pages[m.focus]) : false) : titled(m.tree);
|
|
441
|
+
const head = focused ? back : m.tree && said ? '' : `${m.look.logo ? `<img class="logo" alt="" src="${m.look.logo}">` : ''}<h1>${escape(title(bp, m.look))}</h1>`;
|
|
442
|
+
// The notice carries where she stands, so a design and a reader can tell
|
|
443
|
+
// a relation that ended from a world that is merely away without parsing
|
|
444
|
+
// a sentence; the chip's tone is that word.
|
|
445
|
+
const tone = m.standing === 'ended' || m.standing === 'error' ? 'danger' : m.standing === 'away' ? 'warning' : 'success';
|
|
446
|
+
// `data-page` is the one hook that is not the design's: a class here would
|
|
447
|
+
// be a livery's name, and a livery is worn and swapped. The door marks
|
|
448
|
+
// itself `data-door` for the same reason, and between them anything
|
|
449
|
+
// outside can say which of the two it is looking at.
|
|
450
|
+
return `<header class="nv-bar"${styled.style}>${head}<p class="nv-chip notice" data-standing="${m.standing}" data-tone="${tone}">${escape(m.notice)}</p></header>${focused ? '' : notes}<main data-page${focused ? ' class="focus"' : ''}${styled.style}>${body}</main>${pushes}`;
|
|
451
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { JsonObject } from '@nervur-org/nervur';
|
|
2
|
+
import type { Subject } from '../beings/side.ts';
|
|
3
|
+
type Owner = {
|
|
4
|
+
ask(method?: string, args?: JsonObject): Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
export declare function local(ward: Owner, key: string): Subject;
|
|
7
|
+
export {};
|