undercity 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -0
- package/library/AGENTS.md +492 -0
- package/library/display/category.json +7 -0
- package/library/display/clear/action.json +11 -0
- package/library/display/clear/library.js +17 -0
- package/library/display/index.js +23 -0
- package/library/display/markdown/action.json +12 -0
- package/library/display/markdown/library.js +63 -0
- package/library/display/rawHtml/action.json +12 -0
- package/library/display/rawHtml/library.js +17 -0
- package/library/display/safeHtml/action.json +12 -0
- package/library/display/safeHtml/library.js +42 -0
- package/library/display/text/action.json +12 -0
- package/library/display/text/library.js +17 -0
- package/library/display/value/action.json +12 -0
- package/library/display/value/library.js +23 -0
- package/library/example-category/category.json +7 -0
- package/{actions/forms/getRange → library/example-category/example-action}/action.json +2 -5
- package/library/example-category/example-action/library.js +0 -0
- package/library/index.js +13 -0
- package/library/index.json +7 -0
- package/library/input/category.json +7 -0
- package/library/input/checkbox/action.json +13 -0
- package/library/input/checkbox/library.js +54 -0
- package/library/input/color/action.json +13 -0
- package/library/input/color/library.js +63 -0
- package/library/input/date/action.json +15 -0
- package/library/input/date/library.js +54 -0
- package/library/input/datetimeLocal/action.json +15 -0
- package/library/input/datetimeLocal/library.js +54 -0
- package/library/input/email/action.json +14 -0
- package/library/input/email/library.js +53 -0
- package/library/input/file/action.json +15 -0
- package/library/input/file/library.js +53 -0
- package/library/input/image/action.json +14 -0
- package/library/input/image/library.js +59 -0
- package/library/input/index.js +47 -0
- package/library/input/longText/action.json +16 -0
- package/library/input/longText/library.js +54 -0
- package/library/input/month/action.json +13 -0
- package/library/input/month/library.js +52 -0
- package/library/input/number/action.json +17 -0
- package/library/input/number/library.js +58 -0
- package/library/input/password/action.json +15 -0
- package/library/input/password/library.js +68 -0
- package/library/input/radio/action.json +14 -0
- package/library/input/radio/library.js +73 -0
- package/library/input/range/action.json +16 -0
- package/library/input/range/library.js +72 -0
- package/library/input/tel/action.json +15 -0
- package/library/input/tel/library.js +54 -0
- package/library/input/text/action.json +16 -0
- package/library/input/text/library.js +57 -0
- package/library/input/time/action.json +15 -0
- package/library/input/time/library.js +54 -0
- package/library/input/url/action.json +14 -0
- package/library/input/url/library.js +53 -0
- package/library/input/week/action.json +13 -0
- package/library/input/week/library.js +52 -0
- package/library/new-action.js +298 -0
- package/library/room/category.json +7 -0
- package/library/room/emit/action.json +12 -0
- package/library/room/emit/library.js +15 -0
- package/library/room/index.js +17 -0
- package/library/room/showNav/action.json +12 -0
- package/library/room/showNav/library.js +35 -0
- package/library/room/take/action.json +11 -0
- package/library/room/take/library.js +11 -0
- package/package.json +3 -2
- package/projects/test-a/project.json +47 -25
- package/src/generator/index.js +1 -1
- package/src/generator/page.js +10 -5
- package/src/generator/runtime.js +20 -22
- package/src/ide/app.js +62 -4
- package/src/ide/css/ide.css +21 -0
- package/src/ide/savant.js +51 -10
- package/src/ide/thing-library.js +14 -0
- package/src/server/index.js +1 -1
- package/src/server/routes/actions.js +18 -66
- package/things/form/thing.json +15 -0
- package/actions/AGENTS.md +0 -41
- package/actions/_shared/container.js +0 -16
- package/actions/auth/ask-login/action.json +0 -15
- package/actions/auth/ask-signup/action.json +0 -14
- package/actions/display/clear/action.json +0 -18
- package/actions/display/clear/action.test.js +0 -32
- package/actions/display/markdown/action.json +0 -12
- package/actions/display/markdown/action.test.js +0 -32
- package/actions/display/rawHtml/action.json +0 -24
- package/actions/display/rawHtml/action.test.js +0 -32
- package/actions/display/safeHtml/action.json +0 -24
- package/actions/display/safeHtml/action.test.js +0 -32
- package/actions/display/text/action.js +0 -9
- package/actions/display/text/action.json +0 -12
- package/actions/display/text/action.test.js +0 -40
- package/actions/display/value/action.json +0 -24
- package/actions/display/value/action.test.js +0 -32
- package/actions/dom/addClass/action.json +0 -23
- package/actions/dom/addClass/action.test.js +0 -32
- package/actions/dom/focus/action.json +0 -17
- package/actions/dom/focus/action.test.js +0 -32
- package/actions/dom/hide/action.json +0 -18
- package/actions/dom/hide/action.test.js +0 -32
- package/actions/dom/removeClass/action.json +0 -22
- package/actions/dom/removeClass/action.test.js +0 -32
- package/actions/dom/scroll/action.json +0 -29
- package/actions/dom/scroll/action.test.js +0 -32
- package/actions/dom/setAttr/action.json +0 -29
- package/actions/dom/setAttr/action.test.js +0 -32
- package/actions/dom/setHtml/action.json +0 -22
- package/actions/dom/setHtml/action.test.js +0 -32
- package/actions/dom/setStyle/action.json +0 -29
- package/actions/dom/setStyle/action.test.js +0 -32
- package/actions/dom/setText/action.json +0 -24
- package/actions/dom/setText/action.test.js +0 -32
- package/actions/dom/show/action.json +0 -18
- package/actions/dom/show/action.test.js +0 -32
- package/actions/dom/toggle/action.json +0 -17
- package/actions/dom/toggle/action.test.js +0 -32
- package/actions/dom/toggleClass/action.json +0 -22
- package/actions/dom/toggleClass/action.test.js +0 -32
- package/actions/event/emit/action.json +0 -24
- package/actions/event/emit/action.test.js +0 -32
- package/actions/event/on/action.json +0 -23
- package/actions/event/on/action.test.js +0 -32
- package/actions/event/waitFor/action.json +0 -28
- package/actions/event/waitFor/action.test.js +0 -32
- package/actions/forms/bindField/action.js +0 -27
- package/actions/forms/bindField/action.json +0 -24
- package/actions/forms/bindField/action.test.js +0 -20
- package/actions/forms/check/action.json +0 -22
- package/actions/forms/check/action.test.js +0 -32
- package/actions/forms/clearErrors/action.json +0 -11
- package/actions/forms/clearErrors/action.test.js +0 -35
- package/actions/forms/clearField/action.json +0 -17
- package/actions/forms/clearField/action.test.js +0 -32
- package/actions/forms/getField/action.json +0 -24
- package/actions/forms/getField/action.test.js +0 -32
- package/actions/forms/getRange/action.test.js +0 -32
- package/actions/forms/getSelect/action.json +0 -22
- package/actions/forms/getSelect/action.test.js +0 -32
- package/actions/forms/index.js +0 -140
- package/actions/forms/serialize/action.json +0 -24
- package/actions/forms/serialize/action.test.js +0 -32
- package/actions/forms/setCheck/action.json +0 -23
- package/actions/forms/setCheck/action.test.js +0 -32
- package/actions/forms/setError/action.json +0 -22
- package/actions/forms/setError/action.test.js +0 -32
- package/actions/forms/setField/action.js +0 -14
- package/actions/forms/setField/action.json +0 -24
- package/actions/forms/setField/action.test.js +0 -32
- package/actions/forms/submit/action.json +0 -18
- package/actions/forms/submit/action.test.js +0 -32
- package/actions/forms/validate/action.json +0 -24
- package/actions/forms/validate/action.test.js +0 -32
- package/actions/http/delete/action.json +0 -22
- package/actions/http/delete/action.test.js +0 -32
- package/actions/http/get/action.json +0 -24
- package/actions/http/get/action.test.js +0 -32
- package/actions/http/post/action.json +0 -30
- package/actions/http/post/action.test.js +0 -32
- package/actions/http/put/action.json +0 -27
- package/actions/http/put/action.test.js +0 -32
- package/actions/http/upload/action.json +0 -35
- package/actions/http/upload/action.test.js +0 -32
- package/actions/index.js +0 -306
- package/actions/index.json +0 -5
- package/actions/input/askChoice/action.json +0 -29
- package/actions/input/askChoice/action.test.js +0 -32
- package/actions/input/askConfirm/action.json +0 -24
- package/actions/input/askConfirm/action.test.js +0 -32
- package/actions/input/askDate/action.json +0 -24
- package/actions/input/askDate/action.test.js +0 -32
- package/actions/input/askEmail/action.json +0 -24
- package/actions/input/askEmail/action.test.js +0 -32
- package/actions/input/askNumber/action.json +0 -35
- package/actions/input/askNumber/action.test.js +0 -32
- package/actions/input/askPassword/action.json +0 -24
- package/actions/input/askPassword/action.test.js +0 -32
- package/actions/input/askText/action.json +0 -36
- package/actions/input/askText/action.test.js +0 -32
- package/actions/logic/delay/action.json +0 -18
- package/actions/logic/delay/action.test.js +0 -32
- package/actions/logic/if/action.json +0 -28
- package/actions/logic/if/action.test.js +0 -32
- package/actions/logic/log/action.json +0 -18
- package/actions/logic/log/action.test.js +0 -32
- package/actions/logic/random/action.json +0 -36
- package/actions/logic/random/action.test.js +0 -32
- package/actions/logic/transform/action.json +0 -24
- package/actions/logic/transform/action.test.js +0 -32
- package/actions/media/askAudioUpload/action.json +0 -30
- package/actions/media/askAudioUpload/action.test.js +0 -32
- package/actions/media/askFileUpload/action.json +0 -36
- package/actions/media/askFileUpload/action.test.js +0 -32
- package/actions/media/askImageUpload/action.json +0 -37
- package/actions/media/askImageUpload/action.test.js +0 -32
- package/actions/media/askVideoUpload/action.js +0 -74
- package/actions/media/askVideoUpload/action.json +0 -44
- package/actions/media/askVideoUpload/action.test.js +0 -51
- package/actions/media/captureWebcam/action.json +0 -24
- package/actions/media/captureWebcam/action.test.js +0 -32
- package/actions/nav/back/action.json +0 -11
- package/actions/nav/back/action.test.js +0 -35
- package/actions/nav/goto/action.json +0 -18
- package/actions/nav/goto/action.test.js +0 -32
- package/actions/nav/redirect/action.json +0 -28
- package/actions/nav/redirect/action.test.js +0 -32
- package/actions/nav/reload/action.json +0 -11
- package/actions/nav/reload/action.test.js +0 -35
- package/actions/nav/reset/action.json +0 -11
- package/actions/nav/reset/action.test.js +0 -35
- package/actions/render/alert/action.js +0 -12
- package/actions/render/alert/action.json +0 -36
- package/actions/render/alert/action.test.js +0 -32
- package/actions/render/button/action.js +0 -15
- package/actions/render/button/action.json +0 -44
- package/actions/render/button/action.test.js +0 -37
- package/actions/render/clear/action.js +0 -7
- package/actions/render/clear/action.json +0 -11
- package/actions/render/clear/action.test.js +0 -35
- package/actions/render/divider/action.js +0 -8
- package/actions/render/divider/action.json +0 -11
- package/actions/render/divider/action.test.js +0 -35
- package/actions/render/field/action.js +0 -17
- package/actions/render/field/action.json +0 -59
- package/actions/render/field/action.test.js +0 -57
- package/actions/render/link/action.js +0 -20
- package/actions/render/link/action.json +0 -30
- package/actions/render/link/action.test.js +0 -32
- package/actions/render/markdown/action.json +0 -18
- package/actions/render/markdown/action.test.js +0 -32
- package/actions/render/paragraph/action.js +0 -9
- package/actions/render/paragraph/action.json +0 -32
- package/actions/render/paragraph/action.test.js +0 -32
- package/actions/render/section/action.js +0 -10
- package/actions/render/section/action.json +0 -18
- package/actions/render/section/action.test.js +0 -32
- package/actions/render/subtitle/action.js +0 -9
- package/actions/render/subtitle/action.json +0 -18
- package/actions/render/subtitle/action.test.js +0 -32
- package/actions/render/title/action.js +0 -9
- package/actions/render/title/action.json +0 -30
- package/actions/render/title/action.test.js +0 -44
- package/actions/session/clear/action.json +0 -11
- package/actions/session/clear/action.test.js +0 -35
- package/actions/session/load/action.json +0 -22
- package/actions/session/load/action.test.js +0 -32
- package/actions/session/local/action.json +0 -22
- package/actions/session/local/action.test.js +0 -32
- package/actions/session/save/action.json +0 -22
- package/actions/session/save/action.test.js +0 -32
- package/actions/ui/accordion/action.json +0 -23
- package/actions/ui/accordion/action.test.js +0 -32
- package/actions/ui/badge/action.json +0 -22
- package/actions/ui/badge/action.test.js +0 -32
- package/actions/ui/collapse/action.json +0 -23
- package/actions/ui/collapse/action.test.js +0 -32
- package/actions/ui/hideModal/action.json +0 -17
- package/actions/ui/hideModal/action.test.js +0 -32
- package/actions/ui/loading/action.json +0 -18
- package/actions/ui/loading/action.test.js +0 -32
- package/actions/ui/modal/action.json +0 -18
- package/actions/ui/modal/action.test.js +0 -32
- package/actions/ui/progress/action.json +0 -24
- package/actions/ui/progress/action.test.js +0 -32
- package/actions/ui/toast/action.json +0 -29
- package/actions/ui/toast/action.test.js +0 -32
- package/actions/ui/tooltip/action.json +0 -17
- package/actions/ui/tooltip/action.test.js +0 -32
- package/actions/user/carry/action.json +0 -25
- package/actions/user/carry/action.test.js +0 -32
- package/actions/user/check/action.json +0 -24
- package/actions/user/check/action.test.js +0 -32
- package/actions/user/clear/action.json +0 -11
- package/actions/user/clear/action.test.js +0 -35
- package/actions/user/delete/action.json +0 -17
- package/actions/user/delete/action.test.js +0 -32
- package/actions/user/dump/action.json +0 -11
- package/actions/user/dump/action.test.js +0 -35
- package/actions/user/get/action.json +0 -24
- package/actions/user/get/action.test.js +0 -32
- package/actions/user/merge/action.json +0 -18
- package/actions/user/merge/action.test.js +0 -32
- package/actions/user/set/action.json +0 -24
- package/actions/user/set/action.test.js +0 -32
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Undercity — AGENTS.md
|
|
2
2
|
|
|
3
|
+
## DO NOT INSTALL NPM DEPENDENCIES
|
|
4
|
+
|
|
5
|
+
npm packages are unsafe and should not be installed
|
|
6
|
+
|
|
7
|
+
## PUSH ARCHITECTURE
|
|
8
|
+
|
|
9
|
+
use Signal, combineLatest, Scope, Events, Disposable, CompositeDisposable, Repeater and similar
|
|
10
|
+
|
|
11
|
+
See src/lib/signal.js src/lib/scope.js
|
|
12
|
+
|
|
13
|
+
## USE WEB COMPONENTS
|
|
14
|
+
|
|
15
|
+
Use web-components/custom-elements to create reusable and protable code
|
|
16
|
+
|
|
3
17
|
## The MUD Agent Model
|
|
4
18
|
|
|
5
19
|
Undercity's architecture is grounded in the **Multi-User Dungeon (MUD) metaphor**:
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# library/ — Action Library Specification
|
|
2
|
+
|
|
3
|
+
**Agents: read this entire file before touching anything in `library/`.**
|
|
4
|
+
|
|
5
|
+
This is the authoritative specification for Undercity's action library.
|
|
6
|
+
`./library/` replaces `./actions/`. Do not modify `./actions/`.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Directory layout
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
library/
|
|
14
|
+
AGENTS.md ← this file (do not edit)
|
|
15
|
+
new-action.js ← scaffolding CLI — the ONLY way to create actions
|
|
16
|
+
index.js ← single entry point export default library
|
|
17
|
+
index.json ← registry: which categories and actions exist
|
|
18
|
+
<category>/
|
|
19
|
+
category.json ← category metadata
|
|
20
|
+
<actionName>/
|
|
21
|
+
action.json ← IDE metadata: id, label, desc, params, icon, color
|
|
22
|
+
library.js ← runtime implementation (Emitter API — see below)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Every file except `AGENTS.md` and `new-action.js` is either generated by
|
|
26
|
+
`new-action.js` or maintained according to the schemas in this document.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Categories
|
|
31
|
+
|
|
32
|
+
Three categories are defined. Category id = directory name = UI label slug.
|
|
33
|
+
|
|
34
|
+
| Directory | id | UI Label | Icon | Color |
|
|
35
|
+
|------------|-----------|-----------|-------------------|--------------------|
|
|
36
|
+
| `input/` | `input` | Input | `pencil-square` | `var(--sol-blue)` |
|
|
37
|
+
| `room/` | `room` | Room | `broadcast` | `var(--sol-orange)`|
|
|
38
|
+
| `display/` | `display` | Display | `type` | `var(--sol-cyan)` |
|
|
39
|
+
|
|
40
|
+
- **input** — 18 smart inline form inputs. Each renders directly onto the page
|
|
41
|
+
and reacts to Inventory in real time (two-way binding). Use `room.take` to
|
|
42
|
+
package them up.
|
|
43
|
+
- **room** — Core room behaviours: emit events, show nav buttons, take a form.
|
|
44
|
+
Pinned to top in the UI because room wiring is foundational.
|
|
45
|
+
- **display** — Read inventory values and display them on the page as text,
|
|
46
|
+
markdown, sanitised HTML, or raw HTML.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Import map — no relative paths in `library.js`
|
|
51
|
+
|
|
52
|
+
All `library.js` files import via bare specifiers. Never use `../../../` paths.
|
|
53
|
+
|
|
54
|
+
| Bare specifier | Resolved file | What it exports |
|
|
55
|
+
|-----------------|-----------------------------|-------------------------------------------------------------|
|
|
56
|
+
| `framework` | `src/lib/signal.js` | `Signal`, `Emitter`, `Disposable`, `CompositeDisposable`, `Repeater`, `on` |
|
|
57
|
+
| `scope` | `src/lib/scope.js` | `Scope` |
|
|
58
|
+
|
|
59
|
+
The importmap is already declared in `public/index.html` for the IDE. The
|
|
60
|
+
generator (`src/generator/page.js`) must inject an equivalent importmap into
|
|
61
|
+
every generated page's `<head>`, pointing to the copied files in `./js/`:
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<script type="importmap">
|
|
65
|
+
{
|
|
66
|
+
"imports": {
|
|
67
|
+
"framework": "./js/signal.js",
|
|
68
|
+
"scope": "./js/scope.js"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The generator must also copy `src/lib/signal.js` and `src/lib/scope.js` into
|
|
75
|
+
the `js/` output directory alongside `runtime.js` and `af-icons.js`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Scaffolding: `new-action.js` is mandatory
|
|
80
|
+
|
|
81
|
+
**Agents MUST run `new-action.js` to create every new action. Never create
|
|
82
|
+
`action.json` or `library.js` by hand.** The tool ensures consistent structure,
|
|
83
|
+
updates `index.json`, and keeps the category `index.js` in sync.
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
node library/new-action.js --category <id> --name <camelCaseName>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
```
|
|
91
|
+
node library/new-action.js --category input --name text
|
|
92
|
+
node library/new-action.js --category room --name emit
|
|
93
|
+
node library/new-action.js --category display --name value
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The tool:
|
|
97
|
+
1. Creates `library/<category>/<name>/` with `action.json` and `library.js`.
|
|
98
|
+
2. Registers the action in `library/<category>/index.js`.
|
|
99
|
+
3. Registers the category in `library/index.json` if not already present.
|
|
100
|
+
4. Prints a checklist of fields to fill in.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## `index.json` schema
|
|
105
|
+
|
|
106
|
+
Tracks which categories are active. Maintained by `new-action.js`.
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"categories": ["input", "room", "display"]
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## `category.json` schema
|
|
117
|
+
|
|
118
|
+
One per category directory. Created by `new-action.js` on first action in the
|
|
119
|
+
category.
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"id": "input",
|
|
124
|
+
"name": "Input",
|
|
125
|
+
"icon": "pencil-square",
|
|
126
|
+
"color": "var(--sol-blue)",
|
|
127
|
+
"description": "Smart inline form inputs, each two-way bound to Inventory."
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rules:
|
|
132
|
+
- `id` must match the directory name exactly.
|
|
133
|
+
- `name` is the human-readable UI label shown in the Savant sidebar.
|
|
134
|
+
- `icon` is a Bootstrap Icons name (no `.svg` extension).
|
|
135
|
+
- `color` is a CSS variable from the Solarized palette.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## `action.json` schema
|
|
140
|
+
|
|
141
|
+
One per action directory. Describes the action to the IDE — no runtime code.
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"id": "input.text",
|
|
146
|
+
"icon": "input-cursor-text",
|
|
147
|
+
"color": "var(--sol-blue)",
|
|
148
|
+
"label": "Ask For Text",
|
|
149
|
+
"desc": "Render a single-line text field on the page. Two-way bound to an inventory key — as the user types, inventory updates instantly.",
|
|
150
|
+
"version": "1.0.0",
|
|
151
|
+
"params": [
|
|
152
|
+
{ "name": "key", "label": "Inventory key", "type": "text", "placeholder": "firstName" },
|
|
153
|
+
{ "name": "label", "label": "Label", "type": "text", "placeholder": "First Name" },
|
|
154
|
+
{ "name": "placeholder", "label": "Placeholder", "type": "text", "placeholder": "Type here…" },
|
|
155
|
+
{ "name": "required", "label": "Required", "type": "boolean", "default": false }
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Rules:
|
|
161
|
+
- `id` format: `<categoryId>.<camelCaseName>` — must match directory path exactly.
|
|
162
|
+
- `desc` states WHEN the action runs and WHERE results go.
|
|
163
|
+
- `params[].type` is one of: `text`, `code`, `textarea`, `json`, `boolean`,
|
|
164
|
+
`number`, `select`, `inventory-key`, `room`.
|
|
165
|
+
- `params[].default` is optional; omit if there is no sensible default.
|
|
166
|
+
- `params[].placeholder` is optional; include for text/code/textarea types.
|
|
167
|
+
- `params[].options` is required when `type` is `select`.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## `library.js` — Emitter API (runtime implementation)
|
|
172
|
+
|
|
173
|
+
Every action's runtime lives in `library.js`. This is the browser-side module
|
|
174
|
+
loaded at page runtime. All actions share the same API contract.
|
|
175
|
+
|
|
176
|
+
### API contract
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
// library/<category>/<actionName>/library.js
|
|
180
|
+
import { Emitter } from 'framework';
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* run(params, ctx) — execute this action.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} params — resolved param values from action.json (user config)
|
|
186
|
+
* @param {object} ctx — runtime context (see below)
|
|
187
|
+
* @returns {Emitter} — lifecycle emitter (callers listen on this)
|
|
188
|
+
*/
|
|
189
|
+
export function run(params, ctx) {
|
|
190
|
+
const emitter = new Emitter();
|
|
191
|
+
|
|
192
|
+
// --- action logic here ---
|
|
193
|
+
// Use ctx.container, ctx.inventory, ctx.room, ctx.scope (see Context below)
|
|
194
|
+
// When done: emitter.emit('done', optionalResult)
|
|
195
|
+
// On error: emitter.emit('error', new Error('reason'))
|
|
196
|
+
|
|
197
|
+
return emitter; // return BEFORE async work completes
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Standard emitted events
|
|
202
|
+
|
|
203
|
+
| Event | Payload | Meaning |
|
|
204
|
+
|----------|---------------------|----------------------------------------------|
|
|
205
|
+
| `render` | `HTMLElement` | Action produced a DOM element to display |
|
|
206
|
+
| `done` | `any` (optional) | Action completed successfully |
|
|
207
|
+
| `error` | `Error` | Action failed; include a descriptive message |
|
|
208
|
+
|
|
209
|
+
Callers subscribe like:
|
|
210
|
+
```js
|
|
211
|
+
const em = run(params, ctx);
|
|
212
|
+
em.on('render', el => ctx.container.appendChild(el));
|
|
213
|
+
em.on('done', result => next());
|
|
214
|
+
em.on('error', err => showError(err.message));
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Context object (`ctx`)
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
ctx.container // HTMLElement — the room's content area; append rendered UI here
|
|
221
|
+
ctx.inventory // Signal<object> — read/write user inventory
|
|
222
|
+
// read: ctx.inventory.value.myKey
|
|
223
|
+
// write: ctx.inventory.value = { ...ctx.inventory.value, myKey: v }
|
|
224
|
+
ctx.room // Emitter — room event bus
|
|
225
|
+
// emit: ctx.room.emit('myEvent', data)
|
|
226
|
+
// listen: ctx.room.on('myEvent', fn) → returns Disposable
|
|
227
|
+
ctx.scope // Scope — action-instance lifetime (from 'scope')
|
|
228
|
+
// add subscriptions: ctx.scope.add(disposable)
|
|
229
|
+
// disposed automatically when the room tears down
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Two-way binding pattern (used by all input actions)
|
|
233
|
+
|
|
234
|
+
Input actions are **Web Components** so the DOM owns its own cleanup via
|
|
235
|
+
`disconnectedCallback`. The component uses a `Scope` for subscriptions and
|
|
236
|
+
disposes them when removed. `Signal.combineLatest` handles multi-signal reads.
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
import { Emitter, Signal, on } from 'framework';
|
|
240
|
+
import { Scope } from 'scope';
|
|
241
|
+
|
|
242
|
+
const TAG = 'uc-input-text';
|
|
243
|
+
|
|
244
|
+
if (!customElements.get(TAG)) {
|
|
245
|
+
customElements.define(TAG, class extends HTMLElement {
|
|
246
|
+
#scope = new Scope();
|
|
247
|
+
|
|
248
|
+
connectedCallback() {
|
|
249
|
+
const { params, ctx } = this;
|
|
250
|
+
|
|
251
|
+
// Build DOM
|
|
252
|
+
const label = document.createElement('label');
|
|
253
|
+
label.textContent = params.label ?? '';
|
|
254
|
+
const input = document.createElement('input');
|
|
255
|
+
input.type = 'text';
|
|
256
|
+
input.name = params.key;
|
|
257
|
+
input.placeholder = params.placeholder ?? '';
|
|
258
|
+
input.required = params.required ?? false;
|
|
259
|
+
this.append(label, input);
|
|
260
|
+
|
|
261
|
+
// Push: Inventory → DOM (subscribe fires immediately with current value)
|
|
262
|
+
this.#scope.add(
|
|
263
|
+
ctx.inventory.subscribe(inv => {
|
|
264
|
+
const v = String(inv[params.key] ?? '');
|
|
265
|
+
if (input.value !== v) input.value = v;
|
|
266
|
+
})
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Push: DOM → Inventory
|
|
270
|
+
this.#scope.add(
|
|
271
|
+
on(input, 'input', () => {
|
|
272
|
+
ctx.inventory.value = { ...ctx.inventory.value, [params.key]: input.value };
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
disconnectedCallback() {
|
|
278
|
+
this.#scope.dispose();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function run(params, ctx) {
|
|
284
|
+
const emitter = new Emitter();
|
|
285
|
+
const el = Object.assign(document.createElement(TAG), { params, ctx });
|
|
286
|
+
emitter.emit('render', el);
|
|
287
|
+
emitter.emit('done');
|
|
288
|
+
return emitter;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
#### `combineLatest` — when an action depends on multiple signals
|
|
293
|
+
|
|
294
|
+
Use `Signal.combineLatest` when the component needs to react to two or more
|
|
295
|
+
signals simultaneously, e.g. inventory + a separate config signal:
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
connectedCallback() {
|
|
299
|
+
const combined = Signal.combineLatest([ctx.inventory, ctx.config]);
|
|
300
|
+
this.#scope.add(combined); // disposes the combiner's internal subs
|
|
301
|
+
this.#scope.add(
|
|
302
|
+
combined.subscribe(([inv, cfg]) => {
|
|
303
|
+
// update DOM from both signals at once
|
|
304
|
+
})
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Always `scope.add(combined)` before `scope.add(combined.subscribe(...))` so the
|
|
310
|
+
combiner is torn down before its subscriber.
|
|
311
|
+
|
|
312
|
+
### Rules for `library.js`
|
|
313
|
+
|
|
314
|
+
- Import only from `'framework'`, `'scope'`, or browser built-ins. No relative paths.
|
|
315
|
+
- No shared helpers between actions — each `library.js` is fully self-contained.
|
|
316
|
+
Three similar lines of code is better than a premature abstraction.
|
|
317
|
+
- Input actions MUST be Web Components (one `customElements.define` per action,
|
|
318
|
+
guarded with `if (!customElements.get(TAG))`).
|
|
319
|
+
- Web Components manage their own cleanup in `disconnectedCallback` via `Scope`.
|
|
320
|
+
- Always `return emitter` before any async operations begin.
|
|
321
|
+
- Always emit either `done` or `error` — never leave callers hanging.
|
|
322
|
+
- Render actions must emit `render` before `done`.
|
|
323
|
+
- Rely on push architecture — subscribe to signals, do not poll.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Category `index.js` — plugin entry point
|
|
328
|
+
|
|
329
|
+
Each category directory has an `index.js` that aggregates all actions in that
|
|
330
|
+
category and exports a plugin object compatible with `App.use()`.
|
|
331
|
+
|
|
332
|
+
`new-action.js` generates and maintains this file. Do not edit by hand.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
// library/input/index.js (generated — maintained by new-action.js)
|
|
336
|
+
import { run as textRun } from './text/library.js';
|
|
337
|
+
import textMeta from './text/action.json' assert { type: 'json' };
|
|
338
|
+
// ... one pair per action
|
|
339
|
+
|
|
340
|
+
import categoryMeta from './category.json' assert { type: 'json' };
|
|
341
|
+
|
|
342
|
+
export const inputCategory = {
|
|
343
|
+
name: `library/${categoryMeta.id}`,
|
|
344
|
+
install(app) {
|
|
345
|
+
app.registerCategory(categoryMeta, {
|
|
346
|
+
[textMeta.id]: { ...textMeta, run: textRun },
|
|
347
|
+
// ...
|
|
348
|
+
});
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## `library/index.js` — single entry point
|
|
356
|
+
|
|
357
|
+
**Do not touch this file.** It is the only file the application imports. To add
|
|
358
|
+
a new category: run `new-action.js` — it keeps `index.json` up to date and
|
|
359
|
+
`index.js` is regenerated from `index.json` automatically.
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
// library/index.js
|
|
363
|
+
import { inputCategory } from './input/index.js';
|
|
364
|
+
import { roomCategory } from './room/index.js';
|
|
365
|
+
import { displayCategory } from './display/index.js';
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
name: 'undercity/library',
|
|
369
|
+
install(app) {
|
|
370
|
+
app.use(roomCategory); // pinned first — foundational
|
|
371
|
+
app.use(inputCategory);
|
|
372
|
+
app.use(displayCategory);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Application usage:
|
|
378
|
+
```js
|
|
379
|
+
import library from './library/index.js';
|
|
380
|
+
app.use(library);
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
To switch to a hypothetical `library-v2`, change only this one import path in
|
|
384
|
+
the application.
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Naming rules
|
|
389
|
+
|
|
390
|
+
| Thing | Convention | Example |
|
|
391
|
+
|---------------------|---------------------------------------------|----------------------|
|
|
392
|
+
| Category id | lowercase, no spaces, no hyphens | `input` |
|
|
393
|
+
| Category directory | same as id | `input/` |
|
|
394
|
+
| Action id | `<categoryId>.<camelCaseName>` | `input.text` |
|
|
395
|
+
| Action directory | camelCase name only (no category prefix) | `text/` |
|
|
396
|
+
| Web Component tag | `uc-<category>-<name>` (kebab-case) | `uc-input-text` |
|
|
397
|
+
| `library.js` export | named export `run` | `export function run`|
|
|
398
|
+
|
|
399
|
+
---
|
|
400
|
+
|
|
401
|
+
## Actions to port (reference)
|
|
402
|
+
|
|
403
|
+
### `input/` — 18 actions (from `actions/index.js` → `InputPlugin`)
|
|
404
|
+
|
|
405
|
+
| Directory | action.json `id` | Label |
|
|
406
|
+
|-------------------|--------------------------|-------------------------|
|
|
407
|
+
| `text/` | `input.text` | Ask For Text |
|
|
408
|
+
| `longText/` | `input.longText` | Ask For Long Text |
|
|
409
|
+
| `email/` | `input.email` | Ask For Email Address |
|
|
410
|
+
| `password/` | `input.password` | Ask For Password |
|
|
411
|
+
| `tel/` | `input.tel` | Ask For Phone Number |
|
|
412
|
+
| `url/` | `input.url` | Ask For Web Address |
|
|
413
|
+
| `number/` | `input.number` | Ask For Number |
|
|
414
|
+
| `range/` | `input.range` | Ask For Numeric Range |
|
|
415
|
+
| `date/` | `input.date` | Ask For Date |
|
|
416
|
+
| `datetimeLocal/` | `input.datetimeLocal` | Ask For Date & Time |
|
|
417
|
+
| `time/` | `input.time` | Ask For Time |
|
|
418
|
+
| `month/` | `input.month` | Ask For Month |
|
|
419
|
+
| `week/` | `input.week` | Ask For Week |
|
|
420
|
+
| `color/` | `input.color` | Ask For Color |
|
|
421
|
+
| `checkbox/` | `input.checkbox` | Ask With Checkbox |
|
|
422
|
+
| `radio/` | `input.radio` | Ask With Radio Buttons |
|
|
423
|
+
| `file/` | `input.file` | Ask For File |
|
|
424
|
+
| `image/` | `input.image` | Ask For Image |
|
|
425
|
+
|
|
426
|
+
Params for each are defined in `actions/index.js` → `INPUT_CATEGORY`. Copy
|
|
427
|
+
them verbatim into `action.json`, then implement `library.js` using the
|
|
428
|
+
two-way binding / Web Component pattern above.
|
|
429
|
+
|
|
430
|
+
### `room/` — 3 actions (from `actions/index.js` → `RoomPlugin`)
|
|
431
|
+
|
|
432
|
+
| Directory | action.json `id` | Label |
|
|
433
|
+
|-------------|-------------------|--------------------------|
|
|
434
|
+
| `emit/` | `room.emit` | Emit Room Event |
|
|
435
|
+
| `showNav/` | `room.showNav` | Show Navigation Buttons |
|
|
436
|
+
| `take/` | `room.take` | Take Form |
|
|
437
|
+
|
|
438
|
+
Params from `actions/index.js` → `ROOM_CATEGORY`.
|
|
439
|
+
|
|
440
|
+
### `display/` — 6 actions (from `src/ide/action-library.js` → `display`)
|
|
441
|
+
|
|
442
|
+
| Directory | action.json `id` | Label |
|
|
443
|
+
|-------------|---------------------|------------------------|
|
|
444
|
+
| `text/` | `display.text` | Print Text |
|
|
445
|
+
| `markdown/` | `display.markdown` | Print Markdown |
|
|
446
|
+
| `safeHtml/` | `display.safeHtml` | Print Safe HTML |
|
|
447
|
+
| `rawHtml/` | `display.rawHtml` | Print Raw HTML |
|
|
448
|
+
| `clear/` | `display.clear` | Clear Content |
|
|
449
|
+
| `value/` | `display.value` | Show Inventory Value |
|
|
450
|
+
|
|
451
|
+
Params from `src/ide/action-library.js` → `ACTION_LIBRARY.display`.
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Example: complete action
|
|
456
|
+
|
|
457
|
+
`library/display/text/action.json`:
|
|
458
|
+
```json
|
|
459
|
+
{
|
|
460
|
+
"id": "display.text",
|
|
461
|
+
"icon": "body-text",
|
|
462
|
+
"color": "var(--sol-cyan)",
|
|
463
|
+
"label": "Print Text",
|
|
464
|
+
"desc": "Set plain text content inside a DOM selector. Safe — no HTML interpretation.",
|
|
465
|
+
"version": "1.0.0",
|
|
466
|
+
"params": [
|
|
467
|
+
{ "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-label" },
|
|
468
|
+
{ "name": "text", "label": "Text", "type": "code", "placeholder": "\"Hello \" + inventory.firstName" }
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
`library/display/text/library.js`:
|
|
474
|
+
```js
|
|
475
|
+
import { Emitter } from 'framework';
|
|
476
|
+
|
|
477
|
+
export function run(params, ctx) {
|
|
478
|
+
const emitter = new Emitter();
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
const el = document.querySelector(params.selector);
|
|
482
|
+
if (!el) throw new Error(`display.text: selector not found: ${params.selector}`);
|
|
483
|
+
// params.text is already evaluated by the runtime before run() is called
|
|
484
|
+
el.textContent = params.text;
|
|
485
|
+
emitter.emit('done');
|
|
486
|
+
} catch (err) {
|
|
487
|
+
emitter.emit('error', err);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return emitter;
|
|
491
|
+
}
|
|
492
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "display.clear",
|
|
3
|
+
"icon": "eraser",
|
|
4
|
+
"color": "var(--sol-cyan)",
|
|
5
|
+
"label": "Clear Content",
|
|
6
|
+
"desc": "Empty the innerHTML of a selector.",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"params": [
|
|
9
|
+
{ "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// library/display/clear/library.js
|
|
2
|
+
import { Emitter } from 'framework';
|
|
3
|
+
|
|
4
|
+
export function run(params, ctx) {
|
|
5
|
+
const emitter = new Emitter();
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
const el = document.querySelector(params.selector);
|
|
9
|
+
if (!el) throw new Error(`display.clear: selector not found: ${params.selector}`);
|
|
10
|
+
el.innerHTML = '';
|
|
11
|
+
emitter.emit('done');
|
|
12
|
+
} catch (err) {
|
|
13
|
+
emitter.emit('error', err);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return emitter;
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// library/display/index.js — generated by new-action.js, do not edit by hand
|
|
2
|
+
import { run as clearRun } from './clear/library.js';
|
|
3
|
+
import { run as markdownRun } from './markdown/library.js';
|
|
4
|
+
import { run as rawHtmlRun } from './rawHtml/library.js';
|
|
5
|
+
import { run as safeHtmlRun } from './safeHtml/library.js';
|
|
6
|
+
import { run as textRun } from './text/library.js';
|
|
7
|
+
import { run as valueRun } from './value/library.js';
|
|
8
|
+
|
|
9
|
+
const categoryMeta = {"id":"display","name":"Display","icon":"type","color":"var(--sol-cyan)","description":"Display inventory values as text, markdown, or HTML."};
|
|
10
|
+
|
|
11
|
+
export const displayCategory = {
|
|
12
|
+
name: `library/${categoryMeta.id}`,
|
|
13
|
+
install(app) {
|
|
14
|
+
app.registerCategory(categoryMeta, {
|
|
15
|
+
"display.clear": { ...{"id":"display.clear","icon":"eraser","color":"var(--sol-cyan)","label":"Clear Content","desc":"Empty the innerHTML of a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"}]}, run: clearRun },
|
|
16
|
+
"display.markdown": { ...{"id":"display.markdown","icon":"markdown","color":"var(--sol-cyan)","label":"Print Markdown","desc":"Render a Markdown string as HTML inside a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"content","label":"Markdown content","type":"textarea","placeholder":"**Bold** and _italic_"}]}, run: markdownRun },
|
|
17
|
+
"display.rawHtml": { ...{"id":"display.rawHtml","icon":"code-slash","color":"var(--sol-cyan)","label":"Print Raw HTML","desc":"Inject trusted HTML directly (no sanitization). Use only with controlled content.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"html","label":"HTML","type":"textarea","placeholder":"<b>Trusted HTML</b>"}]}, run: rawHtmlRun },
|
|
18
|
+
"display.safeHtml": { ...{"id":"display.safeHtml","icon":"shield-check","color":"var(--sol-cyan)","label":"Print Safe HTML","desc":"Sanitize and inject HTML — script/event attributes stripped.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-area"},{"name":"html","label":"HTML","type":"textarea","placeholder":"<b>Bold</b>"}]}, run: safeHtmlRun },
|
|
19
|
+
"display.text": { ...{"id":"display.text","icon":"body-text","color":"var(--sol-cyan)","label":"Print Text","desc":"Set plain text content inside a selector. Safe — no HTML interpretation.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#my-label"},{"name":"text","label":"Text","type":"code","placeholder":"\"Hello \" + inventory.firstName"}]}, run: textRun },
|
|
20
|
+
"display.value": { ...{"id":"display.value","icon":"database","color":"var(--sol-cyan)","label":"Show Inventory Value","desc":"Print an inventory key's value as text into a selector.","version":"1.0.0","params":[{"name":"selector","label":"Target selector","type":"text","placeholder":"#name-label"},{"name":"key","label":"Inventory key","type":"text","placeholder":"firstName"}]}, run: valueRun },
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "display.markdown",
|
|
3
|
+
"icon": "markdown",
|
|
4
|
+
"color": "var(--sol-cyan)",
|
|
5
|
+
"label": "Print Markdown",
|
|
6
|
+
"desc": "Render a Markdown string as HTML inside a selector.",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"params": [
|
|
9
|
+
{ "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" },
|
|
10
|
+
{ "name": "content", "label": "Markdown content","type": "textarea", "placeholder": "**Bold** and _italic_" }
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// library/display/markdown/library.js
|
|
2
|
+
import { Emitter } from 'framework';
|
|
3
|
+
|
|
4
|
+
function markdownToHtml(md) {
|
|
5
|
+
// Process line by line for headings, then handle inline elements.
|
|
6
|
+
const lines = md.split('\n');
|
|
7
|
+
const output = [];
|
|
8
|
+
let inPara = false;
|
|
9
|
+
|
|
10
|
+
const flushPara = () => {
|
|
11
|
+
if (inPara) { output.push('</p>'); inPara = false; }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const inline = str =>
|
|
15
|
+
str
|
|
16
|
+
// Bold: **text**
|
|
17
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
18
|
+
// Italic: _text_ (not preceded by word char to avoid mid-word matches)
|
|
19
|
+
.replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>')
|
|
20
|
+
// Inline code: `code`
|
|
21
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
22
|
+
// Links: [text](url)
|
|
23
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
// Blank line
|
|
27
|
+
if (line.trim() === '') {
|
|
28
|
+
flushPara();
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Headings
|
|
33
|
+
const h = line.match(/^(#{1,6})\s+(.*)/);
|
|
34
|
+
if (h) {
|
|
35
|
+
flushPara();
|
|
36
|
+
const level = h[1].length;
|
|
37
|
+
output.push(`<h${level}>${inline(h[2])}</h${level}>`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Normal text — accumulate into paragraph
|
|
42
|
+
if (!inPara) { output.push('<p>'); inPara = true; }
|
|
43
|
+
output.push(inline(line));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
flushPara();
|
|
47
|
+
return output.join('');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function run(params, ctx) {
|
|
51
|
+
const emitter = new Emitter();
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const el = document.querySelector(params.selector);
|
|
55
|
+
if (!el) throw new Error(`display.markdown: selector not found: ${params.selector}`);
|
|
56
|
+
el.innerHTML = markdownToHtml(params.content ?? '');
|
|
57
|
+
emitter.emit('done');
|
|
58
|
+
} catch (err) {
|
|
59
|
+
emitter.emit('error', err);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return emitter;
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "display.rawHtml",
|
|
3
|
+
"icon": "code-slash",
|
|
4
|
+
"color": "var(--sol-cyan)",
|
|
5
|
+
"label": "Print Raw HTML",
|
|
6
|
+
"desc": "Inject trusted HTML directly (no sanitization). Use only with controlled content.",
|
|
7
|
+
"version": "1.0.0",
|
|
8
|
+
"params": [
|
|
9
|
+
{ "name": "selector", "label": "Target selector", "type": "text", "placeholder": "#my-area" },
|
|
10
|
+
{ "name": "html", "label": "HTML", "type": "textarea", "placeholder": "<b>Trusted HTML</b>" }
|
|
11
|
+
]
|
|
12
|
+
}
|