JSONHTL - JSON Hypertext Language

Version 0.4 — Draft

JSONHTL is a minimal document format encoded as JSON. It is designed to be trivially parseable by LLMs and straightforwardly convertible to HTML or other presentation formats by simple renderers.

JSONHTL defines only primitive building blocks. Higher-level conventions (document metadata, node hierarchies, runnable documents, etc.) are defined in separate convention documents linked from the root node of a given system.

Design Model — a Semantic Document Tree

JSONHTL is best understood as a document content tree in the same spirit as a web page's DOM: a typed tree of block elements, each block containing inline elements, resolved and rendered by a simple renderer. The analogy is deliberate and is the intended mental model. In particular, an inline element (such as link) is valid wherever inline content appears — inside a para, a list item, or a table cell — exactly as an <a> is valid in any inline context in HTML. As a rule of thumb: a container that can hold text should be able to hold inline elements.

How JSONHTL deliberately differs from the DOM, by purpose

This model is a direction to converge on, not yet fully realised. Some elements and renderers have historically treated certain containers (notably table cells) as plain strings rather than inline containers; such cases should migrate toward the uniform "container of inline content" model. New element types and renderer fixes should follow it, and existing divergences should be reconciled over time rather than entrenched.

Document Structure

A JSONHTL document is a JSON object. The only required key is content.

{
  "title": "Example Document",
  "content": [...]
}

title is optional but conventional. Any other top-level keys are permitted; their meaning is defined by convention documents, not this spec. Renderers and consumers should ignore keys they do not recognise.

Content

content is either a string (shorthand for a single paragraph of plain text) or a list of block elements. When it is a list, each block is a JSON object with a single identifying key.

A string value:

{"title": "Quick note", "content": "Remember to update the config."}

is equivalent to:

{"title": "Quick note", "content": [{"para": ["Remember to update the config."]}]}

Block Elements

para

A paragraph. Contains a list of inline elements.

{"para": ["This is plain text with a ", {"link": {"href": "other-node", "text": "link"}}, " in it."]}

heading

A section heading. Contains level (integer, 1–6) and text (string).

{"heading": {"level": 1, "text": "Introduction"}}

codeblock

A block of code or preformatted text. Contains lang (string, may be empty) and body (string).

{"codeblock": {"lang": "python", "body": "print('hello')"}}

Additional keys on a codeblock (e.g. name, exec) are not defined by this spec but may be defined by convention documents.

image

A raster (non-vector) image, e.g. a matplotlib plot. Contains format (string, e.g. "png"), data (string, base64-encoded image bytes — raster data isn't text, unlike svg, so unlike svg.body this cannot avoid base64), and optionally alt and caption (same meaning as on svg).

{"image": {"format": "png", "data": "iVBORw0KGgo...", "alt": "A speed-over-time plot", "caption": "Fig. 1"}}

Added 2026-07-17, alongside details, to support "fixing" a runnable note's output (see notes-browser/fixed-notes) into a static note. Rendered as <img src="data:image/{format};base64,..."> on the web; decoded straight to a wx.Image on the desktop app (no rasterization needed, unlike svg).

details

A collapsible section, collapsed by default. Contains summary (string, the always-visible clickable label) and content (a list of nested JSONHTL blocks, hidden until expanded).

{"details": {"summary": "Cell: fetch (code)", "content": [{"codeblock": {"lang": "python", "body": "..."}}]}}

Added 2026-07-17 for the same "fixed note" feature as image — the original code of a fixed cell is kept, but tucked away by default so the outputs (the actual point of a fixed note) aren't buried under code on every view. Rendered as native <details>/<summary> on the web. Per-item toggle is genuinely zero-JS (browser-native). "Expand all" is not: a pure-CSS version (checkbox + :checked + general-sibling selector, forcing display: block !important on collapsed content) was tried first and looked plausible, but confirmed empirically not to work — browsers suppress collapsed <details> content via native rendering suppression tied to the open DOM attribute itself, not an overridable UA-stylesheet display rule, so no amount of author !important reaches it. "Expand all" therefore uses a small inline onclick handler (not a <script> block) that sets d.open = true on every details element — the one deliberate exception to "no JavaScript" in this renderer, and only because CSS alone was proven incapable of it, not for convenience. wx.html.HtmlWindow (the desktop app) has no equivalent — it doesn't recognise <details> at all and just renders the content as permanently visible, un-collapsible text. A real fix needs a native wx.CollapsiblePane widget, which needs the desktop note-viewing architecture to change from one HTML blob per page to a sizer of mixed widgets (same class of change discussed for svg, but for the whole page rather than one block). Not done — see notes-browser/todo.

svg

An inline vector diagram. Contains body (string, raw SVG XML — stored as text, not base64, so it stays small and diffable), and optionally alt (string, accessibility/fallback text) and caption (string, shown below the diagram).

{"svg": {"body": "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='40'><rect width='100' height='40' fill='steelblue'/></svg>", "alt": "A blue rectangle", "caption": "Figure 1"}}

Added 2026-07-17. Rendered as a data-URI <img> on the public web renderer (never inlined as raw <svg> markup — that would be an XSS vector on a public page). The wx desktop browsers (which cannot render SVG at all natively) rasterize it at render time via cairosvg instead of wxPython's bundled SVG support, which was found to silently drop <text> elements entirely. See notes-browser/plotting for the related show(fig) image-rendering implementation this reuses infrastructure from, and notes-browser/svg-diagrams for full implementation detail.

section

A nested section container: a heading together with the content beneath it, held as a real subtree — unlike the flat heading block, whose hierarchy is only implied by ordering. Contains title (string, the heading text), content (a list of nested JSONHTL blocks, which may themselves be sections), and optional level (integer 1–6; defaults to nesting depth). This is the JSONHTL analogue of HTML's <section> and the form the Design Model above converges on: because the tree is real, a trailing paragraph can belong unambiguously to the outer section, after its sub-sections — something the flat heading model cannot express. Full editing model in proposals/section-editing. Coexists with the legacy heading block during migration; both renderers accept both, and a normaliser converts flat → nested.

{"section": {"title": "Discussion", "content": [
  {"para": ["Lead paragraph, unambiguously under Discussion."]},
  {"section": {"title": "Causes", "content": [{"para": ["..."]}]}},
  {"para": ["Concluding paragraph — still under Discussion, after its sub-section."]}
]}}

Inline Elements

Inline elements appear inside para lists. An inline element is either a string (plain text) or an object with a single identifying key.

Plain text

A bare JSON string.

"This is just text."

link

A hypertext reference. Contains href (string) and text (string).

{"link": {"href": "getting-started", "text": "Getting Started"}}

href resolution:

• Starts with http:// or https:// — external web link.

• Anything else — a key in the local data store. The storage layer defines how keys map to documents; JSONHTL does not impose hierarchy or path semantics on keys.

Note: the behaviour when following an external link (e.g. whether the target is another JSONHTL store, a web page, or something else) is not defined by this version of the spec and is reserved for future work.

code

Inline code. Contains a string.

{"code": "gdata_server.py"}

General Rules

1. Lists or scalars. Where this spec defines a value as a list, a bare scalar (string, integer) is also acceptable as shorthand for a single-element list. Parsers should normalise to list form internally. For example, {"para": "just text"} is equivalent to {"para": ["just text"]}.

2. Unknown keys are ignored. Blocks, inline elements, and top-level keys that a consumer does not recognise should be silently skipped. This allows convention documents to extend the format without breaking basic renderers.

3. Inline markup in text strings. Plain text strings inside para lists may contain markdown-style inline markup: bold, `code`, and italic. Renderers should interpret these patterns when displaying text. This provides a lightweight way to add emphasis without requiring every formatted phrase to be a separate JSONHTL inline object. The structured JSONHTL elements (code, link) remain the preferred form and take precedence where they overlap.

4. Convention documents over spec changes. New element types, metadata schemas, and structural conventions should be defined in convention documents stored within the system and linked from the root node — not by extending this spec.

Root Node Convention

When JSONHTL documents are stored in a key-value system, the root node (typically the empty-string key "") serves as a bootstrap. It should:

• Use only base JSONHTL elements (as defined in this spec) so any consumer can read it without prior knowledge.

• Briefly describe the link/navigation model.

• Link to convention documents that define any extended keys, metadata schemas, or organisational structures used in this particular system.

An LLM or tool encountering the system for the first time reads the root node, follows only the links it needs, and stops. This keeps context window usage minimal.

Example Document

{
  "title": "Project Overview",
  "content": [
    {"heading": {"level": 1, "text": "gdata-server"}},
    {"para": [
      "A FastAPI-based HTTP API for GDBM databases. See ",
      {"link": {"href": "gdata-server/api", "text": "API documentation"}},
      " for endpoint details."
    ]},
    {"para": [
      "Configuration is handled via ",
      {"code": ".gdata_server.yaml"},
      " or environment variables."
    ]},
    {"codeblock": {"lang": "bash", "body": "uvicorn gdata_server:app --host 127.0.0.1 --port 8020"}}
  ]
}
version 0.4  ·  updated 2026-08-04