VMPrint Scripting API

This document formalizes the public API for VMPrint Scripting Series 1.

Series 1 is about one thing:

It is intentionally not about page scripting, animation, rich world interaction, or open-ended runtime state systems.

Core Model

Series 1 is built around two supporting structures:

Together they form the basic paradigm of the scripting system.

The split is intentional. Events describe system-level moments. Messages are how elements build more complex logic between themselves without needing a central coordinator. A well-written VMPrint script uses events to react to the world and messages to coordinate participants.

Series 1 is therefore not procedural-first. A scripter can still write procedural code inside a handler, but the public model is organized around:

Authoring Format

Script code lives in YAML front matter. The document body remains ordinary JSON.

---
TITLE: "Hello World"

methods:
  onLoad(): |
    setContent("greeting", "Hello, world!")
---
{
  "documentVersion": "1.1",
  "layout": {
    "pageSize": "LETTER",
    "margins": { "top": 72, "right": 72, "bottom": 72, "left": 72 },
    "fontFamily": "Times New Roman",
    "fontSize": 20,
    "lineHeight": 1.4
  },
  "styles": {},
  "elements": [
    {
      "type": "p",
      "name": "greeting",
      "content": "Waiting for script..."
    }
  ]
}

The engine parses this front matter itself. This is not delegated to the CLI.

Implied Scopes

Series 1 relies heavily on implied scope.

Document Scope

The top-level scope is the document scope.

Its representative object is:

Top-level handlers belong to the document by default. They do not need an explicit doc_ prefix.

Examples:

So this:

methods:
  onLoad(): |
    append({
      type: "p",
      content: "Loaded."
    })

means the handler belongs to the document and append(...) applies to the document.

Element Scope

Named element handlers are bound by convention:

Inside those handlers, self is that element.

So:

methods:
  summary_onMessage(from, msg): |
    append({
      type: "p",
      content: "Updated from message."
    })

means the new content is appended to summary, not to the document.

Variable Scope

Series 1 defines scope in scripting terms, not engine terms.

Top-Level Variables

Variables declared in YAML front matter outside function bodies belong to the document scope.

They are implicitly document variables.

They may be accessed:

Example:

---
TITLE: "Hello World"
ACCENT: "#8A5A2B"

methods:
  onLoad(): |
    append({
      type: "p",
      content: TITLE
    })
---

By convention, authors may use ALL_CAPS for these bindings, but that is only a convention.

Handler-Local Variables

Variables declared inside a handler are local to that handler call only.

Example:

onReady(): |
  const titles = elementsByType("h1")
  const count = titles.length

Not Supported In Series 1

Series 1 does not formally support:

That limitation is intentional. Series 1 is about dynamic content manipulation, not open-ended runtime state design.

Identity

Use top-level name as the public identity for elements.

Example:

{
  "type": "p",
  "name": "summary",
  "content": ""
}

Internally the engine may map this into its own identity system. That is not part of the public scripting language.

Handlers

Document Handlers

Series 1 document handlers:

These handlers are declared directly at top level. They are document handlers by implication, not by a doc_ naming prefix.

Lifecycle meaning:

This distinction is intentional. The public lifecycle follows user perception, not internal settlement terminology.

Element Handlers

Series 1 element handlers:

The current receiver is always available as:

Event parameters are only for event payload. The receiver itself is not passed positionally.

Public Objects

Series 1 currently exposes these primary objects:

doc

The document participant.

Current document members:

self

The current receiver.

Typical element-facing members:

For document handlers, self is the document.

Global Helpers

These helpers are available directly inside handlers.

Helper Meaning

replace(...) is receiver-oriented.

It means:

append(...) and prepend(...) are receiver-oriented.

They mean:

So:

Explicit targeted forms are also valid through the receiver object, for example:

Value Shape

append(...) and prepend(...) may accept:

The runtime is responsible for normalizing that input.

replace(...) accepts the same shapes.

This is the preferred structural mutation primitive for compound or ambiguous elements, where setContent(...) may not have a clear meaning.

Recipient / Target Resolution

A target or recipient can be:

Examples:

sendMessage("summary", { subject: "refresh" })
sendMessage(doc, { subject: "refreshAll" })

element("chapterTitle").append({
  type: "p",
  content: "Act I"
})

Queries

Series 1 keeps queries intentionally small and generic.

Supported now:

This is deliberate. Series 1 should not grow into a catalog of hard-coded document semantics.

Messages

Sending:

sendMessage("summary", {
  subject: "refresh",
  payload: {
    total: 3
  }
})

Receiving:

summary_onMessage(from, msg)

Document receiving:

onMessage(from, msg)

msg is the full message object, not just raw payload.

Current expected shape:

from is the sender reference.

When the document sends a message, from.name is doc.

Update Model

The public scripting model does not ask the author to think about replay.

Authors should think in terms of:

The engine is responsible for mapping those changes onto its native update model:

Manual refresh control is not part of the intended public Series 1 surface.

For the core Series 1 structural helpers, the runtime now prefers live participant composition over replay-oriented document mutation.

Examples

Minimal Hello World

---
methods:
  onLoad(): |
    setContent("greeting", "Hello, world!")
---
{
  "documentVersion": "1.1",
  "layout": {
    "pageSize": "LETTER",
    "margins": { "top": 72, "right": 72, "bottom": 72, "left": 72 },
    "fontFamily": "Times New Roman",
    "fontSize": 20,
    "lineHeight": 1.4
  },
  "styles": {},
  "elements": [
    {
      "type": "p",
      "name": "greeting",
      "content": "Waiting for script..."
    }
  ]
}

Message-Driven Element Growth

---
methods:
  greeter_onCreate(): |
    sendMessage("messageTarget", {
      subject: "greet",
      payload: {
        text: "Hello from another element!"
      }
    })

  messageTarget_onMessage(from, msg): |
    if (from.name !== "greeter") return
    if (msg.subject !== "greet") return

    append({
      type: "p",
      content: msg.payload.text
    })
---

What This Version Does Not Include

The following are intentional boundaries for this release, not gaps to work around.

Page scriptingpage is not part of the public scripting surface. Scripts operate on document elements, not on individual pages.

Semantic document helpers — there is no built-in getHeadings(), getFootnotes(), or similar catalog of document-type-specific queries. Use elementsByType(type) with the element types you author. This keeps the scripting layer generic and usable across any document structure.

Persistent inter-handler state — variables declared inside a handler live only for that handler call. Document-scoped bindings (TITLE, ACCENT, etc. declared in the YAML front matter) are available across all handlers and persist for the document’s lifetime. There is no mutable state bag beyond that.

Animation and ticking — scripting does not run on a continuous tick. It runs at defined lifecycle moments. Document elements do not animate.

User-managed refresh control — you do not instruct the engine when to re-render or re-settle. The engine classifies the effect of each change and responds at the minimum necessary cost. This is by design: scripts that reason about rendering internals are fragile; scripts that reason about content and structure are not.

Notes