Everything the format supports, nothing it doesn't. Version 0.1 — stable.
A Mere workbook is an HTML document saved with the .mp.html extension.
The .mp conveys semantic identity (Mere package); the .html conveys
technical format (HTML document). Both are required.
The <workbook> element is the root content. Everything lives inside it.
<workbook theme="classic-light">
<state> …state declarations… </state>
<computed>…computed values… </computed>
<actions> …named action blocks… </actions>
<screen name="home"> …elements… </screen>
<screen name="detail"> … </screen>
</workbook>
The theme attribute is optional (defaults to classic-light).
Sections may appear in any order. Only <screen> elements are rendered.
Sigils are attribute prefixes that wire elements to state and actions. They replace event handlers, data attributes, and binding frameworks.
| Sigil | Name | Usage | Description |
|---|---|---|---|
| @name | read | <heading @title></heading> |
One-way read binding. On a scalar value, element content updates when state changes. Also works inline in text: <heading>Hello, @name</heading>. On a list container (<card-list @items>), iterates the array — one child rendered per item. Per-item access via @item.field. |
| ~name | two-way | <field ~email></field> |
Bidirectional sync. Input value updates state; state changes update the input. |
| !name | action | <button !submit> |
Invokes a named action on click. Pass arguments with with: !delete with item.id. |
| ? | intent | <screen ?"Show a summary"> |
AI compositor annotation. Ignored at runtime; used for generation. |
Declared in the <state> block. Each value has a name, type, and optional default.
| Type | Empty value | Description |
|---|---|---|
text | "" | A string value. |
number | 0 | A numeric value. value= is parsed with Number(). |
boolean | false | A true/false value. value= is true only for the exact string "true". |
list | [] | An ordered collection of records. value= is parsed as JSON; malformed JSON falls back to an empty list. |
map | {} | A key/value record, read with dotted paths (@selected-message.subject). Parsed as JSON; malformed JSON falls back to an empty map. |
record-list | [] | A list with a declared field schema — <field name= type= default=> children. Enables add-to key validation (MPD-009) and per-field type coercion. |
A value's modifier decides where it lives — and whether it leaves this machine.
The default is transient, so data ships only where an author wrote travel
deliberately.
| Modifier | Leaves the machine? | Meaning |
|---|---|---|
| (none) | No | Transient. Lives for the session and is gone when the workbook closes. |
persist | No | Saved locally to OPFS (localStorage fallback). Origin-scoped, so it does not travel with the file — "remember this on this device". |
travel | Yes — ships with the file | Serialized into the workbook’s own <value> attributes when a save statement runs. This data IS the document and ships wherever the file is sent. |
<value name="draft" type="text" value=""></value> <!-- transient -->
<value name="theme" type="text" value="light" persist></value> <!-- this device only -->
<value name="tasks" type="list" value="[]" travel></value> <!-- ships with the file -->
Persisted data does not travel with the file.
OPFS is origin-scoped — the same workbook opened from file://, served from a
domain, or forwarded to a colleague sees different persisted state.
travel closes that gap. On save, every
travel value is written back into the workbook's own <value> attributes,
so the data is the document — readable in a text editor, diffable in git, and
present wherever the file is sent. Run mere check --travel to see exactly what
would leave the machine before sending it.
Declared in <computed>. Derived from state, lazy and memoised,
invalidated when a source changes. Read-only — two-way binding to a computed value
is an error (MPD-007).
<computed>
<value name="unread" from="messages" where="read = false" op="count"></value>
<value name="total" from="expenses" op="sum" field="amount"></value>
<value name="by-category" from="expenses" op="group-by" field="amount" by="category"></value>
</computed>
op= selects the aggregation. Omitting it with a
where= filter yields the filtered list itself. Missing a required attribute
is an error (MPD-013).
| Operator | Source | Requires | Optional | Description |
|---|---|---|---|---|
add | from="a,b" | — | — | a + b, where from="a,b" names two number state values. |
subtract | from="a,b" | — | — | a − b, where from="a,b" names two number state values. |
percent | from="a,b" | — | — | a ÷ b × 100, rounded to a whole number. Returns 0 when b is 0. |
percent-of | from="a,b" | — | — | a × b ÷ 100 — b percent of a. |
count | list | — | where | Number of items remaining after the where filter. |
sum | list | field | where | Adds field across every matching item. |
avg | list | field | where, window | Mean of field, rounded to a whole number. window="N" averages only the last N items. |
min | list | field | where, window | Smallest value of field. window="N" considers only the last N items. |
max | list | field | where, window | Largest value of field. window="N" considers only the last N items. |
sum-product | list | field, by | where | Sum of field × by across matching items — line-item totals in one declaration. |
group-by | list | field, by | — | Groups items by the by field, summing field within each group. Returns a list of { key, value } sorted by value descending — feeds <chart from="..." field="value" label="key">. |
streak | list | field | by, where | Counts consecutive items, from the most recent backwards, whose field is truthy. by= names a date field to sort by (descending) before counting. |
Named blocks of statements declared in <actions>. Invoked by ! sigil elements.
Action bodies are plain text — one statement per line inside the <action> element.
| Statement | Grammar | Description |
|---|---|---|
set | set <target> to <value> [where <condition>] | Assign a value. With where, updates the matching field on every matching record (set habits.done to "true" where id = hid), or selects a matching record into a map value. |
clear | clear <target> | Reset a state value to the empty value for its type. |
go-to | go-to <screen> [with <key> = <value> ...] | Navigate to a screen, optionally passing parameters. Parameters must be declared in the target screen’s takes= (MPD-010). |
add-to | add-to <list> <key> <value> [<key> <value> ...] | Append a record to a list. Keys are validated against the record-list schema when one is declared (MPD-009). |
remove-from | remove-from <list> where <condition> | Remove every record in the list matching the condition. |
increment | increment <target> [by <n>] | Add to a number state value. Step defaults to 1. |
decrement | decrement <target> [by <n>] | Subtract from a number state value. Step defaults to 1. |
save | save | Write every travel value back into the workbook file. Explicit by design — a workbook opened read-only from an attachment must not autosave. No effect if nothing is declared travel. |
<action name="save-note">
add-to notes body @draft date "today"
clear draft
go-to notes
</action>
<action name="open-message" takes="id">
set selected-message to messages where id = id
go-to message-detail
</action>
Use takes to declare parameters. Pass arguments with with: <button !open-message with item.id>.
40 semantic elements. No HTML passthrough. Every element has a specific role.
| Element | Sigils | Passthrough attrs | Description |
|---|---|---|---|
screen | ? | name | A full screen. Entry point for navigation. |
header | ? | — | Top zone of a screen or card. |
footer | ? | — | Bottom zone of a screen. |
form | ? | — | Structural grouping for inputs. No implicit submit. |
toolbar | ? | — | Flex row wrapper for a search-bar plus inline actions, with padding and gap. |
heading | @ ? | — | Primary text — title or name. |
subtitle | @ ? | — | Secondary text — description or metadata. |
paragraph | @ ? | — | Body text. Supports multiline content. |
timestamp | @ ? | — | Date/time display. Formatted relative to now. |
badge | @ ? | — | Numeric or short text indicator. Hidden when value is 0 or empty. |
avatar | @ ? | — | Circular image or initials. Renders image if value is a URL. |
icon | ? | — | Named icon glyph. |
tab-bar | ~ ? | — | Horizontal tab switcher. Binds to a text state value via ~. |
tab | ? | — | A single tab inside a tab-bar. First positional attr is its value. |
navigation-bar | ? | — | Bottom or top navigation bar. First positional attr is position. |
nav-item | ! ? | — | Navigation action. First positional attr is the target screen name. |
message-list | @ ? | — | Renders a list of messages from a list state value via @. |
card-list | @ ? | — | Renders a list of cards from a list state value via @. |
list | @ ? | — | Generic list. Renders items from a list state value via @. |
message-card | ! ? | — | Tappable message row. Use inside message-list. |
card | ! ? | — | Content container with border and padding. |
field | ~ ? | placeholder, type, required, min, max, pattern, autocomplete, name | Text input. Binds two-way to state via ~. |
button | ! ? | type | Action trigger. Invokes an action via !. |
toggle | ~ ? | — | Boolean switch. Binds two-way to a boolean state via ~. |
camera | ~ ? | facing, name | Photo capture. Opens the device camera via the OS picker (no live preview stream). Binds two-way to a map state value via ~ — writes { dataUrl, capturedAt }. facing=user|environment hints front vs back camera. |
kv | @ ? | label, format | Key/value row. label= sets the label, @ binds the value. format=currency|percent for numeric formatting. |
chart | @ ? | type, from, field, label, where | Inline SVG chart. type=bar|line|pie. from= binds to a list state, field= is the numeric value, label= is the category label. |
modal | ? | — | Full-screen overlay dialog. |
toast | ? | — | Transient notification. Text content only. |
banner | ? | — | Persistent inline notification strip. |
sidebar | ? | — | Left navigation rail for layout="full". Container for sidebar-brand and sidebar-section. |
sidebar-brand | ? | — | Sidebar header/logo text. |
sidebar-section | ? | label | Grouped sidebar nav items under an optional label=. |
data-table | @ ! ? | — | Table from a list state via @. column children define fields; as=status-badge|name-url|contact|currency|product sets a special cell renderer. Optional ! binds a row-click action. |
column | ? | field, label, as, by, editable | Column definition inside data-table or spreadsheet. Declarative only — not rendered directly. |
search-bar | ~ ? | — | Text filter input with a search icon. Binds two-way via ~. |
spreadsheet | @ ? | — | Editable grid from a list state via @. column children define fields; editable on a column allows inline edits. |
metric | @ ? | format | Single KPI value with label. format=currency|percent for numeric formatting. |
metric-group | ? | — | Layout container for multiple metric cards. |
bar | @ ? | label | Horizontal progress/comparison bar. label= sets the caption, @ binds a 0-100 value. |
Set with theme="..." on <workbook>.
Themes are personalities, not palette swaps — each changes layout rhythm, typography, borders, and density.
Neutral baseline. Clean cards, comfortable spacing. The default.
theme="classic-light"
Purple accent, underline tabs, 14px base type.
theme="proton-mail"
Zero radius, 3px black borders, inverted header, red accent.
theme="brutalist"
Parchment and ink, restrained indigo accent, generous radius — brutalist’s warmer sibling.
theme="warm-brutalist"
A workbook is an ordinary HTML document containing the Mere runtime. Opening one executes its JavaScript, exactly like any other HTML file.
The vocabulary is not a security boundary.
The element registry and the MPD diagnostics constrain what you author with this
toolchain. They constrain nothing about a file that arrives by other means: a hostile
workbook can embed a modified runtime, or arbitrary script, and will still open in a
browser. A .mp.html received from a third party carries exactly the trust
model of any HTML attachment from that source — the small vocabulary does not make it safe.
What the runtime does guarantee for the workbooks it renders:
@
binding is written through innerHTML.https:, http:, root- or dot-relative paths, and
data:image/*. Anything else renders as text instead.camera captures are re-encoded through a canvas before being stored,
which discards EXIF metadata — including GPS coordinates — and
downscales the image.mere validate confirms that a packed workbook still
matches the source embedded in it at pack time. That is a tamper-detection check, not an
authorship check.
Reported by mere check. All codes are stable — they will not be renumbered.
| Code | Category | Severity | Description |
|---|---|---|---|
MPD-001 | structural | error | Workbook root element missing, unreadable, or invalid. |
MPD-002 | unknown-element | error | Tag is not in the element registry. |
MPD-003 | unknown-identifier | error | A sigil references state, a computed value, or an action that is not declared. |
MPD-004 | syntax | error | Malformed sigil — @, ~, or ! with no identifier after it. |
MPD-005 | type-mismatch | error (reserved) | Binding to an incompatible state type. |
MPD-006 | structural | error (reserved) | Action invoked with the wrong number of arguments. |
MPD-007 | structural | error | Two-way binding (~) targets a computed value, which is read-only. |
MPD-008 | structural | error | Circular computed value dependency. |
MPD-009 | type-mismatch | error | add-to uses a key that is not declared in the record-list schema. |
MPD-010 | unknown-identifier | error | go-to passes a parameter the target screen does not declare in takes=. |
MPD-011 | type-mismatch | error | <chart from="..."> does not reference a list or record-list state value. |
MPD-012 | type-mismatch | warning | <chart field="..."> is not declared in the record-list schema (warning). |
MPD-013 | structural | error | A computed op is missing a field= or by= attribute it requires. |
MPD-014 | syntax | error | Self-closing tag — no Mere tag is an HTML void element, so /> silently nests what follows. |
MPD-015 | unknown-identifier | error | theme= names a theme that does not exist; the runtime would silently fall back to classic-light. |
MPD-016 | structural | error | A value is declared both persist and travel. They mean opposite things — local-only versus ships-with-the-file — so one of them is a mistake. |
MPD-017 | structural | warning | A save statement runs but no value is declared travel, so saving would write nothing (warning). |