skip to the page

Your first tree

A tree is the page, as data. This one is five nodes.

import { buildElement, buildText, createTree, sequentialIdFactory } from "@jam-overture/loom"

const ids = sequentialIdFactory("firsttree")

const tree = createTree(
  buildElement(ids, {
    type: "loom.page",
    props: {
      width: "readable",
      "loom:theme": {
        palette: "minimal",
        fontPack: "minimal-sans",
        stylePreset: "precise",
      },
    },
    children: [
      buildElement(ids, {
        type: "loom.heading",
        props: { level: 1 },
        children: [buildText(ids, "Hello from a tree")],
      }),
      buildElement(ids, {
        type: "loom.prose",
        children: [buildText(ids, "Nothing here was written as markup.")],
      }),
    ],
  }),
  ids
)

That is the whole of it. Rendered through the runtime, it is this:

The smallest tree that renderslive · rendered through the runtime

Hello from a tree

Nothing here was written as markup.

Propose a change:

Every one of these goes through the same pipeline a model’s answer would: interpreted, analysed, weighed, judged, applied, appended. The log is real and it is in this tab — reload the page and the example is back as it was.

show the tree
{
  "kind": "element",
  "id": "n_firsttree5",
  "type": "loom.page",
  "props": {
    "fills": true,
    "width": "readable",
    "loom:theme": {
      "palette": "minimal",
      "fontPack": "minimal-sans",
      "stylePreset": "precise"
    }
  },
  "children": [
    {
      "kind": "element",
      "id": "n_firsttree2",
      "type": "loom.heading",
      "props": {
        "level": 1
      },
      "children": [
        {
          "kind": "text",
          "id": "n_firsttree1",
          "value": "Hello from a tree"
        }
      ]
    },
    {
      "kind": "element",
      "id": "n_firsttree4",
      "type": "loom.prose",
      "props": {},
      "children": [
        {
          "kind": "text",
          "id": "n_firsttree3",
          "value": "Nothing here was written as markup."
        }
      ]
    }
  ]
}
A page with a heading and a sentence. Every node names a registered primitive and carries props that primitive declared.

Open show the tree under the frame. What you are reading is the exact value the render walked — not a transcription of it.

The one prop that is not the page's own is loom:theme: three registered ids that the root primitive resolves and mounts, which is why the example is styled at all. Rendering a tree covers it.

Three kinds of node

KindBuilt withWhat it is
elementbuildElementa named primitive, its props, and its children
textbuildTexta string. The only place user-visible copy lives
slotbuildSlota named region a parent primitive places itself

An element's type must name something the registry knows. Its props must satisfy that primitive's declared schema. Both are checked — the first when the tree is rendered, the second either at render or the moment a proposal touches it, which is the subject of Rendering a tree.

Ids, and why you pass a factory

Every node carries an id, and every downstream operation addresses nodes by it. A delta says "insert here", where here is an id.

buildElement does not invent one. It asks the IdFactory you passed, and which factory you pass is a real decision:

import { randomIdFactory, sequentialIdFactory } from "@jam-overture/loom"

sequentialIdFactory("demo") // n_demo1, n_demo2, … — the same tree every time
randomIdFactory //            n_8f3c…      — a new tree every time

Use a sequential factory for anything built more than once from the same code — a seed, a fixture, an example on this site. The tree comes out byte-identical on every instance that builds it, which is what makes a stored tree and a scripted change against it survive a deploy.

Use a random factory for nodes created at runtime, where two of them must never collide.

A tree is more than its root

tree.treeId //        t_firsttree1 — what a store keys it by
tree.revision //      0 — every applied change increments this
tree.schemaVersion // the shape of everything above
tree.root //          the element node

revision is how a proposal says which version of the page it was written against. A change interpreted against revision 4 and applied to revision 7 is a change nobody checked, so the runtime declines it rather than guessing.

Builders, or a parse

The builders above make it hard to produce an invalid tree. When the tree comes from somewhere else — a database row, a request body, a file — you do not have that guarantee, so there is a boundary parse instead:

import { parseTree } from "@jam-overture/loom"

const parsed = parseTree(await request.json())

if (!parsed.ok) return reject(parsed.error)

Everything past that point is a LoomTree and can be treated as one. That is the whole purpose of having a schema: exactly one place where unknown input becomes a known value.