Skip to content

Commit 3153443

Browse files
NetdocsCopilot
andcommitted
Add calculator plugin (interactive `calc forms in Markdown)
A new first-party plugin that turns a fenced `calc` block written in YAML into a client-side calculator: labelled inputs (number/range/select) plus outputs computed live from expressions referencing the input names. - Preprocessor (order 15) replaces each calc fence with a raw-HTML <form class="nd-calc"> that Markdig passes through. - A single vanilla-JS evaluator is injected once per page; it builds a Function from each sanitised expression with the inputs as named variables (JS Math is in scope) and recomputes on input. Re-binds on Material's document$ so it survives instant navigation. - Expressions are restricted to a safe character set (no quotes/semicolons/ brackets/backslashes); unsafe outputs are dropped with a warning. - Lightweight number formatting (e.g. ".00", "0.0 kWh"). - Theme CSS for the input grid and result cards (palette-aware). Registered as "calculator" (alias "calc"), enabled in the dogfood site with a live power-cost example. Adds plugins/calculator.md + nav entry and 10 unit tests (198 total). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 09598cc commit 3153443

6 files changed

Lines changed: 734 additions & 0 deletions

File tree

docs-site/appsettings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
{ "path": "plugins/arithmatex.md" },
7171
{ "path": "plugins/b64.md" },
7272
{ "path": "plugins/blog.md" },
73+
{ "path": "plugins/calculator.md" },
7374
{ "path": "plugins/file-filter.md" },
7475
{ "path": "plugins/git-revision-date.md" },
7576
{ "path": "plugins/glightbox.md" },
@@ -119,6 +120,7 @@
119120
{ "name": "tags", "options": { "export": true } },
120121
{ "name": "meta" },
121122
{ "name": "arithmatex" },
123+
{ "name": "calculator" },
122124
{ "name": "social" }
123125
],
124126

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
---
2+
title: calculator
3+
---
4+
5+
# calculator
6+
7+
Author an **interactive calculator** entirely in Markdown. A fenced <code>```calc</code>
8+
block written in YAML becomes a small client-side form: labelled inputs plus outputs that
9+
recompute live as you type. No server, no build-time math — just a tiny vanilla-JS evaluator.
10+
11+
It is a first-party Netdocs plugin (there is no MkDocs equivalent). Enable it in `plugins`:
12+
13+
```json title="appsettings.json"
14+
{ "name": "calculator" }
15+
```
16+
17+
## Example
18+
19+
````markdown
20+
```calc
21+
title: Power cost
22+
inputs:
23+
- name: watts
24+
label: Power draw (W)
25+
default: 100
26+
- name: hours
27+
label: Hours per day
28+
default: 24
29+
- name: rate
30+
label: Price ($/kWh)
31+
default: 0.12
32+
step: 0.01
33+
outputs:
34+
- label: Daily cost
35+
expr: (watts * hours / 1000) * rate
36+
format: "$0.00"
37+
- label: Monthly cost
38+
expr: (watts * hours / 1000) * rate * 30
39+
format: "$0.00"
40+
```
41+
````
42+
43+
Renders as:
44+
45+
```calc
46+
title: Power cost
47+
inputs:
48+
- name: watts
49+
label: Power draw (W)
50+
default: 100
51+
- name: hours
52+
label: Hours per day
53+
default: 24
54+
- name: rate
55+
label: Price ($/kWh)
56+
default: 0.12
57+
step: 0.01
58+
outputs:
59+
- label: Daily cost
60+
expr: (watts * hours / 1000) * rate
61+
format: "$0.00"
62+
- label: Monthly cost
63+
expr: (watts * hours / 1000) * rate * 30
64+
format: "$0.00"
65+
```
66+
67+
## How it works
68+
69+
The plugin runs as a Markdown preprocessor (order `15`) and replaces each `calc` fence with
70+
a raw-HTML `<form class="nd-calc">`. A single evaluator script is injected **once per page**;
71+
it collects each form's inputs into named variables, evaluates every output expression, and
72+
updates the results on every `input`/`change`. It also re-binds on Material's `document$`
73+
observable, so calculators keep working with instant navigation.
74+
75+
## Inputs
76+
77+
Each entry under `inputs`:
78+
79+
| Field | Type | Required | Description |
80+
|---|---|---|---|
81+
| `name` | string | yes | Variable name used in expressions. Must be a valid identifier (`[A-Za-z_][A-Za-z0-9_]*`). |
82+
| `label` | string | no | Field label (defaults to `name`). |
83+
| `type` | string | no | `number` (default), `range`, or `select`. Providing `options` implies `select`. |
84+
| `default` | number/string | no | Initial value. |
85+
| `min` / `max` / `step` | number | no | Passed through to the `<input>` (useful for `number`/`range`). |
86+
| `options` | array | no | For selects — a list of strings or `{ value, label }` objects. |
87+
88+
A `range` input also renders a small live readout of its current value.
89+
90+
## Outputs
91+
92+
Each entry under `outputs`:
93+
94+
| Field | Type | Required | Description |
95+
|---|---|---|---|
96+
| `label` | string | no | Result label (defaults to `Result`). |
97+
| `expr` | string | yes | Expression evaluated with the input names as variables. |
98+
| `format` | string | no | Number format (see below). |
99+
100+
### Expressions
101+
102+
Expressions are evaluated in the browser with the inputs available as variables and the
103+
JavaScript [`Math`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Math)
104+
object in scope, so `Math.round`, `Math.min`, `Math.pow`, ternaries (`a > b ? x : y`), etc.
105+
all work:
106+
107+
```yaml
108+
outputs:
109+
- label: Rounded
110+
expr: Math.round(watts * hours / 1000)
111+
```
112+
113+
!!! warning "Sanitised input"
114+
Expressions are restricted to a safe character set (digits, identifiers, arithmetic and
115+
comparison operators, parentheses, commas). Quotes, semicolons, brackets and backslashes
116+
are rejected, and such an output is dropped with a build warning — so a stray expression
117+
can't inject arbitrary script into the page.
118+
119+
### Formats
120+
121+
`format` is a lightweight pattern: the first `0` / `0.0…` token sets the number of decimal
122+
places, and any surrounding text is kept as a prefix/suffix.
123+
124+
| `format` | `1234.5` renders as |
125+
|---|---|
126+
| *(omitted)* | `1234.5` |
127+
| `0` | `1235` |
128+
| `0.00` | `1234.50` |
129+
| `$0.00` | `$1234.50` |
130+
| `0.0 kWh` | `1234.5 kWh` |

src/Netdocs.Cli/CliApp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ private static async Task<int> ImportAsync(string[] args)
233233
.Register<LinkNotesPlugin>("link-notes", "affiliate-links")
234234
.Register<Base64EmbedPlugin>("b64", "pymdownx.b64")
235235
.Register<ArithmatexPlugin>("arithmatex", "pymdownx.arithmatex")
236+
.Register<CalculatorPlugin>("calculator", "calc")
236237
.Register<MacrosPlugin>("macros");
237238

238239
private static string? ResolveConfigPath(string? provided)

0 commit comments

Comments
 (0)