Text

A paragraph is a node with content.type: "text". Headings, body copy, quotes, and code listings are the same kind of node — they differ by role. Appearance comes from that role in styles/theme.json, not from fields on the node.

Copy catalog fragments into content/root.json children. Do not replace root.json with a fragment.

Paragraphs and headings

You wantWrite
A paragraphOne text node (role usually body)
The next paragraphA sibling text node. Space between them is the parent layout.gap (see Layout)
A line break inside one paragraph\n in content.value
A headingA separate node with role h1h4

Copy ex_heading.json. It sets keep_with_next on the title so the heading does not sit alone at the bottom of a page.

A quotation is the same shape with role: "quote". A code listing is role: "code" with content.type: "text" — copy ex_code.json. Do not author content.type: "code_block".

There is no HTML <br> node and no per-node margin. preserve_whitespace is for code-like text, not ordinary body copy.

Inline style is modifiers on the text node, not bold / color on the node, and not Markdown **…** in the string.

Each modifier needs type, intent, and a UTF-8 byte range ([start, end) into content.value). \n is 1 byte; CJK and emoji are multi-byte. Closed type set: emphasis, link, underline, strikethrough, subscript, superscript, math, syntax_highlight.

intent is a key under theme.modifiers.styles[type] for most types (strong, default, keyword). For link, intent is the URL (look comes from link.default). For math, intent is the TeX; put U+FFFC in the string and mark that character.

Always compute range with scripts/modifier_range.py — do not hand-count:

python scripts/modifier_range.py --text "Do not sign." --find "Do not"
# [0, 6]

Copy ex_modifiers.json. Starter already defines the style keys that example uses. $…$ is Markdown import only — see Markdown conversion.

Lists

A list is sibling nodes with role: "list_item", a shared list_id, optional depth (nesting; default 0), and marker_type (bullet or number). Copy ex_list.json. Marker look lives on the list_item role in the theme.

Do not fake bullets with in a body string.

See also (catalog, not this page)

NeedFile
Display equationex_math.json (content.type: "math"; add Noto Sans Math and font_aliases)
Numbered equationex_math_numbered.json (not \\tag)
Fillable blank or checkboxex_form.json (form_field — never ____ or )

Allowed keys

Allowed keys and nodes.schema.json.

Common mistakes

  • Putting font_size, bold, or color on the node — those belong on the role
  • Using \n\n in one string to mean “new paragraph”
  • Hand-counting modifier ranges across \n or non-ASCII
  • Writing $…$ or ^22^ in author JSON — use math / superscript modifiers
  • Drawing a blank with ____ or — copy ex_form.json

Catalog sources

ex_heading.json
{
  "id": "ex.heading",
  "role": "section",
  "content": {
    "type": "container",
    "value": {
      "children": [
        {
          "id": "ex.heading.title",
          "role": "h1",
          "keep_with_next": true,
          "content": { "type": "text", "value": "Catalog: heading" }
        },
        {
          "id": "ex.heading.body",
          "role": "body",
          "content": {
            "type": "text",
            "value": "h1 with keep_with_next plus a body paragraph."
          }
        }
      ]
    }
  },
  "layout": {
    "type": "stack",
    "direction": "vertical",
    "gap": 8000
  }
}
ex_modifiers.json
{
  "id": "ex.modifiers",
  "role": "section",
  "content": {
    "type": "container",
    "value": {
      "children": [
        {
          "id": "ex.modifiers.text",
          "role": "body",
          "content": {
            "type": "text",
            "value": "Bold link E=\uFFFC underline strike H2O super kw"
          },
          "modifiers": [
            {
              "range": [0, 4],
              "type": "emphasis",
              "intent": "strong"
            },
            {
              "range": [5, 9],
              "type": "link",
              "intent": "https://example.com"
            },
            {
              "range": [12, 15],
              "type": "math",
              "intent": "mc^2"
            },
            {
              "range": [16, 25],
              "type": "underline",
              "intent": "default"
            },
            {
              "range": [26, 32],
              "type": "strikethrough",
              "intent": "default"
            },
            {
              "range": [34, 35],
              "type": "subscript",
              "intent": "default"
            },
            {
              "range": [37, 42],
              "type": "superscript",
              "intent": "default"
            },
            {
              "range": [43, 45],
              "type": "syntax_highlight",
              "intent": "keyword"
            }
          ]
        },
        {
          "id": "ex.modifiers.multiline",
          "role": "h2",
          "content": {
            "type": "text",
            "value": "Lead line\nthen bold"
          },
          "modifiers": [
            {
              "range": [15, 19],
              "type": "emphasis",
              "intent": "strong"
            }
          ]
        }
      ]
    }
  },
  "layout": { "type": "stack", "direction": "vertical", "gap": 8000 }
}
ex_list.json
{
  "id": "ex.list",
  "role": "section",
  "content": {
    "type": "container",
    "value": {
      "children": [
        {
          "id": "ex.list.i0",
          "role": "list_item",
          "list_id": "ex.list.bullet",
          "depth": 0,
          "marker_type": "bullet",
          "content": { "type": "text", "value": "First bullet item." }
        },
        {
          "id": "ex.list.i1",
          "role": "list_item",
          "list_id": "ex.list.bullet",
          "depth": 0,
          "marker_type": "bullet",
          "content": { "type": "text", "value": "Second bullet item." }
        },
        {
          "id": "ex.list.n0",
          "role": "list_item",
          "list_id": "ex.list.number",
          "depth": 0,
          "marker_type": "number",
          "content": { "type": "text", "value": "First numbered item." }
        },
        {
          "id": "ex.list.n1",
          "role": "list_item",
          "list_id": "ex.list.number",
          "depth": 0,
          "marker_type": "number",
          "content": { "type": "text", "value": "Second numbered item." }
        }
      ]
    }
  },
  "layout": {
    "type": "stack",
    "direction": "vertical",
    "gap": 4000
  }
}