interactive-markdown 0.1.1 → 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/LICENSE +1 -1
- package/README.md +122 -26
- package/examples/agent-loop.md +26 -0
- package/examples/amplitude.md +11 -14
- package/examples/circular-motion.md +10 -14
- package/examples/graph.md +18 -0
- package/examples/normal-density.md +19 -0
- package/index.html +8 -1
- package/package.json +13 -5
- package/src/app.js +85 -8
- package/src/document-editor.js +1062 -0
- package/src/document.js +771 -0
- package/src/expr.js +6 -0
- package/src/graph-editor.js +1117 -0
- package/src/index.js +1 -0
- package/src/markdown.js +10 -2
- package/src/parse-ivm.js +488 -67
- package/src/plot.js +401 -0
- package/src/render.js +116 -6
- package/src/sanitize-svg.js +240 -9
- package/styles/app.css +659 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# Interactive Markdown (IVM)
|
|
2
2
|
|
|
3
|
-
**Interactive Markdown** is a small browser **editor + renderer** for plain `.md` files that include live **vector graphics** driven by sliders, timers, and a safe expression language.
|
|
3
|
+
**Interactive Markdown** is a small browser **editor + renderer** for plain `.md` files that include live **vector graphics** driven by sliders, timers, and a safe expression language, plus **Cartesian XY plots** and a **graph editor** stored in the same fence.
|
|
4
4
|
|
|
5
|
-
Write Markdown as usual. Add an `ivm` fenced block when you want interactive SVG. The format stays AI-friendly: readable text an agent can generate like normal Markdown.
|
|
5
|
+
Write Markdown as usual. Add an `ivm` fenced block when you want interactive SVG or a directed graph you can edit in the preview. The format stays AI-friendly: readable text an agent can generate like normal Markdown.
|
|
6
6
|
|
|
7
7
|
License: **MIT** (see [LICENSE](LICENSE)).
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
11
11
|
- Split **editor** and **live preview**
|
|
12
|
+
- **Graph editor** tab beside the source: prose renders as formatted text, and sliders, figures, and graphs stay editable blocks
|
|
12
13
|
- Open / save `.md`, with a **localStorage** draft
|
|
13
14
|
- IVM widgets: `@slider`, `@timer` (Play/Pause), `@input`, `@toggle`
|
|
14
|
-
- Derived values with `@let`
|
|
15
|
+
- Derived values with `@let` (agents should emit `@let`, not `@calc`)
|
|
15
16
|
- SVG templates with `{{expr}}` bindings
|
|
17
|
+
- Slim `@svg WxH` wrapper for compact markup
|
|
18
|
+
- **XY plots**: `@plot`, `@axis`, and `@series` with live slider-driven curves
|
|
19
|
+
- Visual **graph editor**: `@graph`, `@node`, and `@edge` (drag, connect, rename; edits write back into the fence)
|
|
20
|
+
- Animated SVG: CSS `@keyframes`, SMIL, glow filters, and internal `<use>`
|
|
16
21
|
- Whitelist expression evaluator (no `eval`)
|
|
17
22
|
- SVG sanitizer (strips scripts, event handlers, external hrefs)
|
|
18
23
|
|
|
@@ -37,23 +42,24 @@ npx serve .
|
|
|
37
42
|
|
|
38
43
|
> ES modules need HTTP — opening `index.html` as a `file://` URL will not work.
|
|
39
44
|
|
|
45
|
+
## Graph editor tab
|
|
46
|
+
|
|
47
|
+
The left pane has two tabs. **Source** is the markdown. **Graph editor** shows the same document with prose rendered as formatted text. Sliders, timers, text fields, toggles, formulas, shapes, XY plots, and graphs stay as blocks. Select a block to edit its label, range, expression, or shape attributes. A shape attribute can be a number or `{{name}}`. The graph block is the canvas: drag, connect, and rename there. An animated graph marches dashes along each edge and sends a dot along the same path. Each change rewrites the source. The right pane is the rendered preview.
|
|
48
|
+
|
|
40
49
|
## IVM dialect (quick reference)
|
|
41
50
|
|
|
42
|
-
Fence name: **`ivm`**.
|
|
51
|
+
Fence name: **`ivm`**. Long control names are canonical; short aliases are accepted for compact AI generation.
|
|
52
|
+
|
|
53
|
+
### Canonical exemplar
|
|
43
54
|
|
|
44
55
|
````markdown
|
|
45
56
|
```ivm
|
|
46
|
-
@
|
|
47
|
-
@
|
|
48
|
-
@
|
|
49
|
-
@
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@calc a = amp / 2
|
|
53
|
-
@svg
|
|
54
|
-
<svg viewBox="0 0 200 80" xmlns="http://www.w3.org/2000/svg">
|
|
55
|
-
<circle cx="100" cy="{{round(y, 2)}}" r="{{amp}}" fill="#38bdf8"/>
|
|
56
|
-
</svg>
|
|
57
|
+
@slider amp: 5..55 = 28
|
|
58
|
+
@timer t: 0..100 = 0 speed=25
|
|
59
|
+
@let y = 40 - amp * sind(t * 3.6)
|
|
60
|
+
@svg 200x80
|
|
61
|
+
<rect width="200" height="80" fill="#0f172a"/>
|
|
62
|
+
<path d="M10 40 Q55 {{y}} 100 40 T190 40" fill="none" stroke="#38bdf8" stroke-width="3"/>
|
|
57
63
|
```
|
|
58
64
|
````
|
|
59
65
|
|
|
@@ -61,26 +67,108 @@ Fence name: **`ivm`**.
|
|
|
61
67
|
|
|
62
68
|
| Line | Meaning |
|
|
63
69
|
|------|---------|
|
|
64
|
-
| `@slider name: min..max = default step=S "Label"` | Range input |
|
|
65
|
-
| `@timer name: min..max = default speed=N loop` | Animated value + Play/Pause (`
|
|
66
|
-
| `@input name: "default" "Label"` | Text field |
|
|
67
|
-
| `@toggle name: true\|false "Label"` | Checkbox |
|
|
68
|
-
| `@let name = expression` | Computed value |
|
|
69
|
-
| `@calc name = expression` | Alias of `@let` |
|
|
70
|
+
| `@slider name: min..max = default [step=S] [deg\|rad] ["Label"]` | Range input (`@s` alias) |
|
|
71
|
+
| `@timer name: min..max = default [speed=N] [step=S] [loop\|noloop] [deg\|rad] ["Label"]` | Animated value + Play/Pause (`@t` alias) |
|
|
72
|
+
| `@input name: "default" ["Label"]` | Text field (`@i` alias) |
|
|
73
|
+
| `@toggle name: true\|false ["Label"]` | Checkbox (`@b` alias) |
|
|
74
|
+
| `@let name = expression` | Computed value (`@l` alias) |
|
|
75
|
+
| `@calc name = expression` | Alias of `@let` (compat only; prefer `@let`) |
|
|
70
76
|
| `@ns name` | Share variable state across IVM blocks with the same name |
|
|
71
|
-
| `@svg` | Everything after this line is SVG markup |
|
|
77
|
+
| `@svg` | Everything after this line is full SVG markup |
|
|
78
|
+
| `@svg WxH` | Same, but wrap non-root markup in `<svg viewBox="0 0 W H" xmlns=…>` |
|
|
79
|
+
| `@plot [WxH] [grid\|nogrid]` | Cartesian XY plot (mutually exclusive with `@svg` / `@graph`) |
|
|
80
|
+
| `@axis x\|y: min..max [step=S] ["Label"]` | Plot axis range and label |
|
|
81
|
+
| `@series [id] …` / `y = expr` | Plot series (free var `x`) |
|
|
82
|
+
| `@grid on\|off` | Plot grid toggle |
|
|
83
|
+
| `@graph` | Start an editable directed graph. This is the first directive in that fence |
|
|
84
|
+
| `@node id: "Label" x=N y=N kind=K` | Node. `kind` is `action` (default), `input`, `output`, or `decision` |
|
|
85
|
+
| `@edge from -> to` | Directed edge. Add `from=top\|right\|bottom\|left` and `to=top\|right\|bottom\|left` to choose the sides. Defaults are `from=right` and `to=left` |
|
|
86
|
+
| `@animate` | Play marching dashes and traveling dots on the graph canvas. Options: `path=ortho\|straight\|curve`, `quantity=N` (1–12), `direction=forward\|reverse`, `speed=seconds` |
|
|
87
|
+
|
|
88
|
+
### Defaults (omit when unchanged)
|
|
89
|
+
|
|
90
|
+
- `step` defaults to **1**
|
|
91
|
+
- timer `loop` defaults to **true** (write `noloop` only when needed)
|
|
92
|
+
- timer `speed` defaults to **30**
|
|
93
|
+
- if `"Label"` is omitted, the UI shows the variable **name**
|
|
94
|
+
|
|
95
|
+
### Short aliases (AI-compact)
|
|
96
|
+
|
|
97
|
+
| Short | Long |
|
|
98
|
+
|-------|------|
|
|
99
|
+
| `@s` | `@slider` |
|
|
100
|
+
| `@t` | `@timer` |
|
|
101
|
+
| `@i` | `@input` |
|
|
102
|
+
| `@b` | `@toggle` |
|
|
103
|
+
| `@l` | `@let` |
|
|
104
|
+
|
|
105
|
+
Humans and docs should use the long forms. Generators may emit short aliases; both parse.
|
|
106
|
+
|
|
107
|
+
### Graph
|
|
108
|
+
|
|
109
|
+
A fence that starts with `@graph` is a node-and-edge diagram. The preview is the editor: drag a node, drag a port on any side onto another node, double-click a label to rename, and press Delete to remove the selection. `@animate` runs the motion on that same canvas. Path, quantity, direction, and speed are edited there and written back onto the `@animate` line. Each committed edit replaces that fence in the source. Do not mix `@svg` or controls into the same fence.
|
|
110
|
+
|
|
111
|
+
````markdown
|
|
112
|
+
```ivm
|
|
113
|
+
@graph
|
|
114
|
+
@node start: "Start" x=40 y=48 kind=input
|
|
115
|
+
@node step: "Step" x=280 y=48
|
|
116
|
+
@edge start -> step
|
|
117
|
+
```
|
|
118
|
+
````
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### XY plot
|
|
122
|
+
|
|
123
|
+
A fence that starts with `@plot` draws a Cartesian chart. Axes, grid, and series are declared in the dialect; the renderer builds the SVG. Sliders and `@let` work the same as in figure blocks. Free variable `x` is sampled across the X axis when evaluating each series. Do not mix `@svg` or `@graph` into the same fence.
|
|
124
|
+
|
|
125
|
+
````markdown
|
|
126
|
+
```ivm
|
|
127
|
+
@plot
|
|
128
|
+
@axis x: -10..10 step=2 "x"
|
|
129
|
+
@axis y: 0..0.8 step=0.2 "density"
|
|
130
|
+
@slider mu: -5..5 = 0 step=0.1 "μ (mean)"
|
|
131
|
+
@slider sigma: 0.5..4 = 1.5 step=0.1 "σ (std dev)"
|
|
132
|
+
@series density stroke=#6d28d9 fill=#7c3aed fillOpacity=0.25
|
|
133
|
+
y = (1/(sigma*sqrt(2*pi))) * exp(-0.5*((x-mu)/sigma)^2)
|
|
134
|
+
```
|
|
135
|
+
````
|
|
136
|
+
|
|
137
|
+
| Line | Meaning |
|
|
138
|
+
|------|---------|
|
|
139
|
+
| `@plot [WxH] [grid\|nogrid]` | Start an XY plot (default size 480×320, grid on) |
|
|
140
|
+
| `@axis x\|y: min..max [step=S] ["Label"]` | Axis range, tick step, and title |
|
|
141
|
+
| `@series [id] [stroke=#…] [fill=#…] [fillOpacity=N] [width=N]` | Curve style; next line (or same line) `y = expr` |
|
|
142
|
+
| `@grid on\|off` | Toggle the background grid |
|
|
143
|
+
|
|
144
|
+
Defaults: grid **on**, fill opacity **0.25**, stroke `#6d28d9`, fill `#7c3aed`.
|
|
72
145
|
|
|
73
146
|
### Expressions (`{{…}}`)
|
|
74
147
|
|
|
75
|
-
Placeholders use **double braces
|
|
148
|
+
Placeholders use **double braces** only: `{{expr}}`.
|
|
76
149
|
|
|
77
150
|
Allowed operators include `+ - * / % ^ **`, comparisons, `&& || !`, and ternary `? :`.
|
|
78
151
|
|
|
79
|
-
Functions include: `abs`, `sqrt`, `pow`, `exp`, `ln`, `log`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `round`, `floor`, `ceil`, `trunc`, `sign`, `min`, `max`, `clamp`, `mod`, `lerp`, `deg`, `rad`.
|
|
152
|
+
Functions include: `abs`, `sqrt`, `pow`, `exp`, `ln`, `log`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sind`, `cosd`, `tand`, `asind`, `acosd`, `atand`, `round`, `floor`, `ceil`, `trunc`, `sign`, `min`, `max`, `clamp`, `mod`, `lerp`, `deg`, `rad`.
|
|
80
153
|
|
|
81
154
|
Constants: `pi`, `e`, `tau`.
|
|
82
155
|
|
|
83
|
-
|
|
156
|
+
- `sin` / `cos` / `tan` take **radians**
|
|
157
|
+
- `sind` / `cosd` / `tand` take **degrees** (prefer these when a control range is in degrees)
|
|
158
|
+
- Optional `deg` / `rad` flag on a range line is stored as metadata (`units`) for authors/tools; trig still uses the helpers above
|
|
159
|
+
|
|
160
|
+
### Animated SVG
|
|
161
|
+
|
|
162
|
+
Everything after `@svg` can be a self-contained animated figure:
|
|
163
|
+
|
|
164
|
+
- `<style>` with `@keyframes` (crossfades, pulses, marching dashes)
|
|
165
|
+
- SMIL (`<animate>`, `<animateTransform>`, `<animateMotion>`)
|
|
166
|
+
- Filters (`feGaussianBlur`, …) and paint servers such as `url(#id)`
|
|
167
|
+
- `<use href="#symbol-id">` for icons defined in the same SVG
|
|
168
|
+
|
|
169
|
+
Scripts, event handlers, and remote URLs are still removed. The figure is mounted in a shadow root so its CSS cannot restyle the editor. The SVG is rewritten only when the substituted markup changes, so those animations keep playing while sliders sit still.
|
|
170
|
+
|
|
171
|
+
A block with only `@svg` (no controls) renders as a full-width figure.
|
|
84
172
|
|
|
85
173
|
### Fallback
|
|
86
174
|
|
|
@@ -92,18 +180,26 @@ Load from the toolbar **Examples…** menu, or open the files directly:
|
|
|
92
180
|
|
|
93
181
|
- [`examples/amplitude.md`](examples/amplitude.md) — wave amplitude + phase timer
|
|
94
182
|
- [`examples/circular-motion.md`](examples/circular-motion.md) — point on a circle with spin animation
|
|
183
|
+
- [`examples/agent-loop.md`](examples/agent-loop.md) — agent loop drawn as an animated graph
|
|
184
|
+
- [`examples/graph.md`](examples/graph.md) — directed graph you can edit in the preview
|
|
185
|
+
- [`examples/normal-density.md`](examples/normal-density.md) — Gaussian density XY plot with μ/σ sliders
|
|
95
186
|
|
|
96
187
|
## Project layout
|
|
97
188
|
|
|
98
189
|
```
|
|
99
190
|
index.html Editor shell
|
|
100
191
|
src/
|
|
192
|
+
index.js Library entry
|
|
101
193
|
app.js UI wiring
|
|
194
|
+
document.js Block model (parse and serialize)
|
|
195
|
+
document-editor.js Preview block editor
|
|
102
196
|
markdown.js MD + IVM pipeline
|
|
103
197
|
parse-ivm.js Fence / control parser
|
|
104
198
|
expr.js Safe expression evaluator
|
|
105
199
|
sanitize-svg.js SVG sanitizer
|
|
106
200
|
render.js Controls + SVG binding
|
|
201
|
+
graph-editor.js Graph model and canvas editor
|
|
202
|
+
plot.js Cartesian XY plot SVG builder
|
|
107
203
|
styles/app.css
|
|
108
204
|
examples/
|
|
109
205
|
test/ Node smoke tests
|
|
@@ -115,7 +211,7 @@ test/ Node smoke tests
|
|
|
115
211
|
npm test
|
|
116
212
|
```
|
|
117
213
|
|
|
118
|
-
Smoke tests cover the expression engine, IVM parser, SVG sanitizer, renderer,
|
|
214
|
+
Smoke tests cover the expression engine, IVM parser, SVG sanitizer, renderer, Markdown pipeline, graph editor, XY plots, and preview document builder (`linkedom` is used where a DOM is required).
|
|
119
215
|
|
|
120
216
|
## License
|
|
121
217
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Agent loop
|
|
2
|
+
|
|
3
|
+
One pass through an agent: a trigger starts the loop, then think, act, and observe. Done either returns the output or sends the run back to think. Memory feeds think, and act can call tools. The dots travel those edges.
|
|
4
|
+
|
|
5
|
+
```ivm
|
|
6
|
+
@graph
|
|
7
|
+
@animate path=ortho quantity=3 direction=forward speed=2.2
|
|
8
|
+
@node trigger: "Trigger" x=16 y=16 kind=input
|
|
9
|
+
@node output: "Output" x=688 y=16 kind=output
|
|
10
|
+
@node think: "Think" x=16 y=148
|
|
11
|
+
@node act: "Act" x=248 y=148
|
|
12
|
+
@node observe: "Observe" x=480 y=148
|
|
13
|
+
@node done: "Done?" x=480 y=292 kind=decision
|
|
14
|
+
@node memory: "Memory" x=16 y=436
|
|
15
|
+
@node tools: "Tools" x=248 y=436
|
|
16
|
+
@edge trigger -> think from=bottom to=top
|
|
17
|
+
@edge think -> act
|
|
18
|
+
@edge act -> observe
|
|
19
|
+
@edge observe -> done from=bottom to=top
|
|
20
|
+
@edge done -> output from=right to=bottom
|
|
21
|
+
@edge done -> think from=bottom to=left
|
|
22
|
+
@edge memory -> think from=top to=bottom
|
|
23
|
+
@edge act -> tools from=bottom to=top
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Drag any side dot to reconnect a node. On the canvas, change the path, quantity, direction, or seconds and the source updates.
|
package/examples/amplitude.md
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
# Wave amplitude
|
|
2
2
|
|
|
3
|
-
Drag **
|
|
3
|
+
Drag **amp** or press **Play** on the phase timer. The path and marker update from `{{expr}}` bindings.
|
|
4
4
|
|
|
5
5
|
```ivm
|
|
6
|
-
@slider amp: 5..55 = 28
|
|
7
|
-
@timer t: 0..100 = 0 speed=25
|
|
8
|
-
@let y = 40 - amp *
|
|
9
|
-
@let y2 = 40 - (amp * 0.55) *
|
|
10
|
-
@svg
|
|
11
|
-
<
|
|
12
|
-
<rect x="0" y="0" width="200" height="80" rx="4" fill="#0f172a"/>
|
|
6
|
+
@slider amp: 5..55 = 28
|
|
7
|
+
@timer t: 0..100 = 0 speed=25
|
|
8
|
+
@let y = 40 - amp * sind(t * 3.6)
|
|
9
|
+
@let y2 = 40 - (amp * 0.55) * sind(t * 3.6 + 68.75)
|
|
10
|
+
@svg 200x80
|
|
11
|
+
<rect width="200" height="80" rx="4" fill="#0f172a"/>
|
|
13
12
|
<line x1="10" y1="40" x2="190" y2="40" stroke="#334155" stroke-width="1"/>
|
|
14
|
-
<path d="
|
|
15
|
-
|
|
16
|
-
<
|
|
17
|
-
fill="none" stroke="#a78bfa" stroke-width="2" opacity="0.85"/>
|
|
18
|
-
<circle cx="100" cy="{{round(y,2)}}" r="5" fill="#f472b6"/>
|
|
19
|
-
</svg>
|
|
13
|
+
<path d="M10 40 Q55 {{y}} 100 40 T190 40" fill="none" stroke="#38bdf8" stroke-width="3"/>
|
|
14
|
+
<path d="M10 40 Q55 {{y2}} 100 40 T190 40" fill="none" stroke="#a78bfa" stroke-width="2" opacity="0.85"/>
|
|
15
|
+
<circle cx="100" cy="{{y}}" r="5" fill="#f472b6"/>
|
|
20
16
|
```
|
|
21
17
|
|
|
22
18
|
## Tips
|
|
23
19
|
|
|
24
20
|
- `@timer` uses **Play / Pause** in the widget chrome.
|
|
25
21
|
- `@let` values are recomputed whenever a slider or timer changes.
|
|
22
|
+
- Prefer `sind` / `cosd` when angles are in degrees.
|
|
@@ -3,23 +3,19 @@
|
|
|
3
3
|
Adjust **radius** and **angle**, or animate with the timer. The moving point sits on the circle at angle `a`.
|
|
4
4
|
|
|
5
5
|
```ivm
|
|
6
|
-
@slider radius: 10..80 = 50
|
|
7
|
-
@slider angle: 0..360 = 45
|
|
8
|
-
@timer spin: 0..360 = 0 speed=40
|
|
9
|
-
@
|
|
10
|
-
@let x = round(100 + radius *
|
|
11
|
-
@let y = round(100 + radius *
|
|
12
|
-
@svg
|
|
13
|
-
<
|
|
14
|
-
<rect x="0" y="0" width="200" height="200" rx="6" fill="#0f172a"/>
|
|
6
|
+
@slider radius: 10..80 = 50
|
|
7
|
+
@slider angle: 0..360 = 45 deg
|
|
8
|
+
@timer spin: 0..360 = 0 speed=40 deg
|
|
9
|
+
@let a = angle + spin
|
|
10
|
+
@let x = round(100 + radius * cosd(a), 2)
|
|
11
|
+
@let y = round(100 + radius * sind(a), 2)
|
|
12
|
+
@svg 200x200
|
|
13
|
+
<rect width="200" height="200" rx="6" fill="#0f172a"/>
|
|
15
14
|
<circle cx="100" cy="100" r="{{radius}}" fill="none" stroke="#475569" stroke-width="2"/>
|
|
16
15
|
<line x1="100" y1="100" x2="{{x}}" y2="{{y}}" stroke="#38bdf8" stroke-width="2"/>
|
|
17
16
|
<circle cx="100" cy="100" r="3" fill="#94a3b8"/>
|
|
18
17
|
<circle cx="{{x}}" cy="{{y}}" r="7" fill="#f472b6"/>
|
|
19
|
-
<text x="8" y="18" fill="#94a3b8" font-size="10" font-family="ui-monospace, monospace">
|
|
20
|
-
a={{round(a,1)}}°
|
|
21
|
-
</text>
|
|
22
|
-
</svg>
|
|
18
|
+
<text x="8" y="18" fill="#94a3b8" font-size="10" font-family="ui-monospace, monospace">a={{round(a,1)}}°</text>
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
Press **Play** on **
|
|
21
|
+
Press **Play** on **spin** to sweep the point around the circle while keeping your base angle offset.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# A small graph
|
|
2
|
+
|
|
3
|
+
This fence is a directed graph. Drag a node to move it, and drag a dot on its top, right, bottom, or left onto another node to connect them. The source on the left updates when you let go.
|
|
4
|
+
|
|
5
|
+
```ivm
|
|
6
|
+
@graph
|
|
7
|
+
@node trigger: "Trigger" x=24 y=40 kind=input
|
|
8
|
+
@node think: "Think" x=220 y=40
|
|
9
|
+
@node act: "Act" x=220 y=188
|
|
10
|
+
@node done: "Done?" x=430 y=40 kind=decision
|
|
11
|
+
@node output: "Output" x=430 y=188 kind=output
|
|
12
|
+
@edge trigger -> think
|
|
13
|
+
@edge think -> done
|
|
14
|
+
@edge think -> act
|
|
15
|
+
@edge act -> output
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Double-click a label to rename it. Choose a kind, then add a node, or change the kind of the node you have selected. Delete removes the selected node or edge.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Normal density
|
|
2
|
+
|
|
3
|
+
A Cartesian **XY plot** of the Gaussian PDF. Drag **μ (mean)** and **σ (std dev)** — the curve recomputes live.
|
|
4
|
+
|
|
5
|
+
```ivm
|
|
6
|
+
@plot
|
|
7
|
+
@axis x: -10..10 step=2 "x"
|
|
8
|
+
@axis y: 0..0.8 step=0.2 "density"
|
|
9
|
+
@slider mu: -5..5 = 0 step=0.1 "μ (mean)"
|
|
10
|
+
@slider sigma: 0.5..4 = 1.5 step=0.1 "σ (std dev)"
|
|
11
|
+
@series density stroke=#6d28d9 fill=#7c3aed fillOpacity=0.25
|
|
12
|
+
y = (1/(sigma*sqrt(2*pi))) * exp(-0.5*((x-mu)/sigma)^2)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Tips
|
|
16
|
+
|
|
17
|
+
- `@plot` builds axes, grid, and series — no hand-written SVG required.
|
|
18
|
+
- Series expressions use free variable `x` plus any `@slider` / `@let` names.
|
|
19
|
+
- Omit `nogrid` to keep the light gray grid (default on).
|
package/index.html
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
<option value="">Examples…</option>
|
|
22
22
|
<option value="amplitude.md">amplitude.md</option>
|
|
23
23
|
<option value="circular-motion.md">circular-motion.md</option>
|
|
24
|
+
<option value="agent-loop.md">agent-loop.md</option>
|
|
25
|
+
<option value="graph.md">graph.md</option>
|
|
26
|
+
<option value="normal-density.md">normal-density.md</option>
|
|
24
27
|
</select>
|
|
25
28
|
<input id="file-input" type="file" accept=".md,text/markdown,text/plain" />
|
|
26
29
|
</div>
|
|
@@ -29,8 +32,12 @@
|
|
|
29
32
|
|
|
30
33
|
<main class="layout">
|
|
31
34
|
<section class="pane" aria-label="Editor">
|
|
32
|
-
<div class="pane-
|
|
35
|
+
<div class="pane-tabs" role="tablist" aria-label="Editor view">
|
|
36
|
+
<button type="button" id="tab-source" role="tab" aria-selected="true" aria-controls="editor">Source</button>
|
|
37
|
+
<button type="button" id="tab-graph" role="tab" aria-selected="false" aria-controls="graph-editor">Graph editor</button>
|
|
38
|
+
</div>
|
|
33
39
|
<textarea id="editor" spellcheck="false" aria-label="Markdown source"></textarea>
|
|
40
|
+
<div id="graph-editor" hidden></div>
|
|
34
41
|
</section>
|
|
35
42
|
<section class="pane" aria-label="Preview">
|
|
36
43
|
<div class="pane-title">Preview</div>
|
package/package.json
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "interactive-markdown",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Interactive Markdown (IVM): plain .md with live SVG
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Interactive Markdown (IVM): plain .md with live SVG, XY plots, a preview block builder, and an editable graph",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
|
+
"directories": {
|
|
7
|
+
"example": "examples",
|
|
8
|
+
"test": "test"
|
|
9
|
+
},
|
|
6
10
|
"scripts": {
|
|
7
11
|
"start": "serve .",
|
|
8
|
-
"test": "node test/smoke-expr.mjs && node test/smoke-parse-ivm.mjs && node test/smoke-sanitize-svg.mjs && node test/smoke-render.mjs && node test/smoke-markdown.mjs",
|
|
12
|
+
"test": "node test/smoke-expr.mjs && node test/smoke-parse-ivm.mjs && node test/smoke-sanitize-svg.mjs && node test/smoke-render.mjs && node test/smoke-markdown.mjs && node test/smoke-graph.mjs && node test/smoke-plot.mjs && node test/smoke-document.mjs",
|
|
9
13
|
"test:expr": "node test/smoke-expr.mjs",
|
|
10
14
|
"test:parse": "node test/smoke-parse-ivm.mjs",
|
|
11
15
|
"test:sanitize": "node test/smoke-sanitize-svg.mjs",
|
|
12
16
|
"test:render": "node test/smoke-render.mjs",
|
|
13
|
-
"test:markdown": "node test/smoke-markdown.mjs"
|
|
17
|
+
"test:markdown": "node test/smoke-markdown.mjs",
|
|
18
|
+
"test:graph": "node test/smoke-graph.mjs",
|
|
19
|
+
"test:document": "node test/smoke-document.mjs",
|
|
20
|
+
"test:plot": "node test/smoke-plot.mjs"
|
|
14
21
|
},
|
|
15
22
|
"repository": {
|
|
16
23
|
"type": "git",
|
|
@@ -43,7 +50,8 @@
|
|
|
43
50
|
"./parse-ivm": "./src/parse-ivm.js",
|
|
44
51
|
"./sanitize-svg": "./src/sanitize-svg.js",
|
|
45
52
|
"./render": "./src/render.js",
|
|
46
|
-
"./markdown": "./src/markdown.js"
|
|
53
|
+
"./markdown": "./src/markdown.js",
|
|
54
|
+
"./plot": "./src/plot.js"
|
|
47
55
|
},
|
|
48
56
|
"files": [
|
|
49
57
|
"src",
|
package/src/app.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* Interactive Markdown — editor + live preview shell.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { mountDocumentEditor } from "./document-editor.js";
|
|
5
6
|
import { renderDocument } from "./markdown.js";
|
|
7
|
+
import { replaceIvmFence } from "./parse-ivm.js";
|
|
6
8
|
|
|
7
9
|
const STORAGE_KEY = "interactive-markdown-draft-v1";
|
|
8
10
|
|
|
9
11
|
const editor = document.getElementById("editor");
|
|
12
|
+
const graphEditorEl = document.getElementById("graph-editor");
|
|
10
13
|
const preview = document.getElementById("preview");
|
|
11
14
|
const statusEl = document.getElementById("status");
|
|
12
15
|
const btnOpen = document.getElementById("btn-open");
|
|
@@ -14,9 +17,13 @@ const btnSave = document.getElementById("btn-save");
|
|
|
14
17
|
const btnClear = document.getElementById("btn-clear");
|
|
15
18
|
const fileInput = document.getElementById("file-input");
|
|
16
19
|
const exampleSelect = document.getElementById("example-select");
|
|
20
|
+
const tabSource = document.getElementById("tab-source");
|
|
21
|
+
const tabGraph = document.getElementById("tab-graph");
|
|
17
22
|
|
|
18
23
|
/** @type {{ destroy: () => void } | null} */
|
|
19
|
-
let
|
|
24
|
+
let previewMount = null;
|
|
25
|
+
/** @type {{ destroy: () => void } | null} */
|
|
26
|
+
let graphMount = null;
|
|
20
27
|
let debounceTimer = 0;
|
|
21
28
|
let dirty = false;
|
|
22
29
|
|
|
@@ -24,19 +31,85 @@ function setStatus(msg) {
|
|
|
24
31
|
if (statusEl) statusEl.textContent = msg;
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
function persistDraft() {
|
|
35
|
+
dirty = true;
|
|
36
|
+
try {
|
|
37
|
+
localStorage.setItem(STORAGE_KEY, editor.value);
|
|
38
|
+
} catch (_) {
|
|
39
|
+
/* ignore quota */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function showPreviewError(err) {
|
|
44
|
+
preview.innerHTML = `<div class="im-errors">${escapeHtml(err.message || String(err))}</div>`;
|
|
45
|
+
setStatus("Render error");
|
|
46
|
+
console.error(err);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function renderPreview() {
|
|
50
|
+
if (previewMount) {
|
|
51
|
+
previewMount.destroy();
|
|
52
|
+
previewMount = null;
|
|
53
|
+
}
|
|
54
|
+
previewMount = renderDocument(preview, editor.value, {
|
|
55
|
+
onGraphChange(fenceIndex, body) {
|
|
56
|
+
editor.value = replaceIvmFence(editor.value, fenceIndex, body);
|
|
57
|
+
persistDraft();
|
|
58
|
+
setStatus("Graph updated");
|
|
59
|
+
refreshGraphEditor();
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function refreshGraphEditor() {
|
|
65
|
+
if (!graphEditorEl) return;
|
|
66
|
+
if (graphMount) {
|
|
67
|
+
graphMount.destroy();
|
|
68
|
+
graphMount = null;
|
|
69
|
+
}
|
|
70
|
+
graphMount = mountDocumentEditor(graphEditorEl, editor.value, {
|
|
71
|
+
onChange(markdown) {
|
|
72
|
+
editor.value = markdown;
|
|
73
|
+
persistDraft();
|
|
74
|
+
setStatus("Document updated");
|
|
75
|
+
try {
|
|
76
|
+
renderPreview();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
showPreviewError(err);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onStatus(msg) {
|
|
82
|
+
setStatus(msg);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
27
87
|
function renderNow() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
88
|
+
let previewOk = false;
|
|
89
|
+
try {
|
|
90
|
+
renderPreview();
|
|
91
|
+
previewOk = true;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
showPreviewError(err);
|
|
31
94
|
}
|
|
32
95
|
try {
|
|
33
|
-
|
|
34
|
-
setStatus(dirty ? "Edited · preview updated" : "Preview updated");
|
|
96
|
+
refreshGraphEditor();
|
|
35
97
|
} catch (err) {
|
|
36
|
-
|
|
98
|
+
if (graphEditorEl) {
|
|
99
|
+
graphEditorEl.innerHTML = `<div class="im-errors">${escapeHtml(err.message || String(err))}</div>`;
|
|
100
|
+
}
|
|
37
101
|
setStatus("Render error");
|
|
38
102
|
console.error(err);
|
|
39
103
|
}
|
|
104
|
+
if (previewOk) setStatus(dirty ? "Edited · preview updated" : "Preview updated");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function showEditorTab(name) {
|
|
108
|
+
const source = name !== "graph";
|
|
109
|
+
if (editor) editor.hidden = !source;
|
|
110
|
+
if (graphEditorEl) graphEditorEl.hidden = source;
|
|
111
|
+
tabSource?.setAttribute("aria-selected", source ? "true" : "false");
|
|
112
|
+
tabGraph?.setAttribute("aria-selected", source ? "false" : "true");
|
|
40
113
|
}
|
|
41
114
|
|
|
42
115
|
function scheduleRender() {
|
|
@@ -90,7 +163,7 @@ async function loadExample(name) {
|
|
|
90
163
|
|
|
91
164
|
const DEFAULT_DOC = `# Interactive Markdown
|
|
92
165
|
|
|
93
|
-
Edit the
|
|
166
|
+
Edit the source, or open the Graph editor tab. The preview on the right updates as you go.
|
|
94
167
|
|
|
95
168
|
Use an \`\`\`ivm\`\`\` fence for sliders, timers, and live SVG.
|
|
96
169
|
|
|
@@ -113,6 +186,7 @@ Use an \`\`\`ivm\`\`\` fence for sliders, timers, and live SVG.
|
|
|
113
186
|
- \`@timer name: min..max = default speed=N loop\`
|
|
114
187
|
- \`@let name = expression\` (or \`@calc\`)
|
|
115
188
|
- \`@svg\` then SVG with \`{{expr}}\` placeholders
|
|
189
|
+
- \`@graph\` with \`@node\` and \`@edge\` for a visual graph editor
|
|
116
190
|
|
|
117
191
|
Open a \`.md\` file, or save your draft from the toolbar.
|
|
118
192
|
`;
|
|
@@ -131,6 +205,9 @@ function init() {
|
|
|
131
205
|
|
|
132
206
|
editor.value = start;
|
|
133
207
|
editor.addEventListener("input", scheduleRender);
|
|
208
|
+
tabSource?.addEventListener("click", () => showEditorTab("source"));
|
|
209
|
+
tabGraph?.addEventListener("click", () => showEditorTab("graph"));
|
|
210
|
+
showEditorTab("source");
|
|
134
211
|
|
|
135
212
|
btnOpen?.addEventListener("click", () => fileInput?.click());
|
|
136
213
|
fileInput?.addEventListener("change", async () => {
|