quillmark-typst 0.94.0

Typst backend for Quillmark
Documentation
// Auto-generated quillmark-helper package
// Version: {version}
//
// The Rust backend regenerates this file per render. Everything the document
// needs is *generated source text* — content fields as markup blocks, the
// document data as a Typst literal, the schema address tables as a literal —
// so the file carries no runtime data processing. The only hand-written logic
// is `form-field` / `signature-field` (widget geometry + metadata hand-off),
// `_qm-known-path` (path validation, reading the generated `_qm-meta`), and
// `plaintext` (a content field's string projection, reading `_qm-plaintext`).

/// Backend-generated schema address tables (`fields`, `card_fields`,
/// `array_fields`, `card_array_fields`) that validate `form-field` paths.
/// A generated literal — never parsed from document data.
#let _qm-meta = {meta_literal}

/// Whether `path` parses against the schema address tables: a top-level field
/// (`subject`), an array element (`refs.2`), or a card address
/// (`$cards.<kind>.<n>.<field>`, optional `.<i>` element suffix). An indexed
/// suffix (`field.N`) is only a real address for an array-typed field — any
/// array, matching the pdfform resolver's grammar — so it validates against
/// `array_fields`/`card_array_fields`, not the broader `fields`/`card_fields`
/// name tables, rejecting `subject.0` on a scalar `subject` even though the
/// name itself is known: a scalar has no element for the address to resolve
/// to. Distinct from present-with-empty tables (e.g. a body-disabled main with
/// no other fields and no cards), which validates against the empty set and so
/// rejects every address.
#let _qm-known-path(path) = {
  let fields = _qm-meta.at("fields", default: ())
  let card-fields = _qm-meta.at("card_fields", default: (:))
  let array-fields = _qm-meta.at("array_fields", default: ())
  let card-array-fields = _qm-meta.at("card_array_fields", default: (:))
  let is-index(s) = s.match(regex("^[0-9]+$")) != none
  let parts = path.split(".")
  if parts.at(0) == "$cards" {
    if parts.len() < 4 or parts.len() > 5 { return false }
    let kind = parts.at(1)
    if not (kind in card-fields) { return false }
    if not is-index(parts.at(2)) { return false }
    let field = parts.at(3)
    if parts.len() == 5 {
      return field in card-array-fields.at(kind, default: ()) and is-index(parts.at(4))
    }
    return field in card-fields.at(kind)
  }
  if parts.len() == 1 { return path in fields }
  if parts.len() == 2 { return parts.at(0) in array-fields and is-index(parts.at(1)) }
  false
}

/// Emit an unsigned form field. PDF: a clickable AcroForm widget here (a text
/// box, checkbox, dropdown, or signature). SVG/PNG: same invisible layout
/// space.
///
/// `name` must be unique and match `[A-Za-z0-9_.]+` ('.' allowed for
/// fully-qualified names). `type` is one of `"text"`, `"checkbox"`, `"choice"`,
/// `"signature"`. `width` / `height` must be absolute lengths (pt/mm/cm/in).
///
/// Value binding is the plate author's job: pass `value:` from your data, e.g.
/// `form-field("Agree", type: "checkbox", value: data.agree)`. There is no
/// resolver on the Typst side — the helper forwards the value verbatim and the
/// Rust adapter maps it to the AcroForm value:
///   - text:      `value` is a string (numbers stringify); blank ⇒ no `/V`.
///   - checkbox:  `value` is a bool; `true` ⇒ checked.
///   - choice:    `value` is a string; bound only if it matches an `options` entry.
///   - signature: `value` is ignored.
/// `options` is an array of display strings for `type: "choice"`. `multiline`
/// toggles the multi-line flag for `type: "text"`.
///
/// `field` is the schema-field path the region sidecar keys this widget on
/// (`session.regions()`), e.g. `signature-field("Signature", field:
/// "signature_block")` so a click routes to the schema field rather than the
/// AcroForm `/T`. It is region-only: the `/T` widget name stays `name`. Omitted
/// (`none`), the widget exposes **no** region — its `/T` name is not a schema
/// address, so there is nothing for a consumer to route to.
///
/// `<__qm_field__>` and `kind: "__qm_field__"` are reserved for this hand-off.
/// Note: this emits a `metadata` element, so plates/packages that read their
/// own config via an unfiltered `query(metadata)` (e.g. `.last()`) must query
/// by a dedicated label or filter on a key they own, or they will read this
/// field's metadata by accident.
#let form-field(
  name,
  type: "text",
  value: none,
  options: (),
  multiline: false,
  width: 200pt,
  height: 20pt,
  field: none,
) = {
  assert(std.type(name) == str,
    message: "form-field: name must be a string, got " + repr(std.type(name)))
  assert(name.match(regex("^[A-Za-z0-9_.]+$")) != none,
    message: "form-field: name must match [A-Za-z0-9_.]+, got " + repr(name))
  assert(type in ("text", "checkbox", "choice", "signature"),
    message: "form-field: type must be one of text/checkbox/choice/signature, got " + repr(type))
  assert(field == none or std.type(field) == str,
    message: "form-field: field must be a string or none, got " + repr(std.type(field)))
  assert(field == none or _qm-known-path(field),
    message: "form-field: " + repr(field) + " is not a schema field address; expected a field name, an array element like \"refs.2\", or a card path composed from the card's $path prefix")
  let abs-pt(field, l) = {
    let r = repr(l)
    assert(not (r.contains("em") or r.ends-with("%")),
      message: "form-field: " + field + " must be an absolute length, got " + r)
    l / 1pt
  }
  [#metadata((
    kind: "__qm_field__",
    name: name,
    field-type: type,
    value: value,
    options: options,
    multiline: multiline,
    width: abs-pt("width", width),
    height: abs-pt("height", height),
    field: field,
  )) <__qm_field__>]
  box(width: width, height: height, stroke: none, fill: none)
}

/// Emit an unsigned signature field. A thin wrapper over `form-field` with
/// `type: "signature"` (value-free). `field` forwards the region schema-field
/// binding (see `form-field`). Retained so existing plates keep working.
#let signature-field(name, width: 200pt, height: 50pt, field: none) = form-field(
  name,
  type: "signature",
  width: width,
  height: height,
  field: field,
)

// Per-field content, generated by the Rust backend: each content field,
// `richtext[]` element, and card content field present in the document data —
// keyed by name below in `data` — is bound here as a markup **block**
// (`#let _qm_cN = [ .. ]`). The file parser parses each block once; every
// glyph the block places carries its own syntax node's span (word/run
// granularity), which `session.regions()` reads placement geometry back from.
// The backend records each block's bracketed byte window at generation time.
{content_blocks}

/// Document data as a dictionary — a backend-generated Typst literal. Content
/// fields reference their `_qm_cN` block bindings; present `date`/`datetime`
/// fields reference a `_qm_dN` value-object block — `(value: datetime(..),
/// display: (..args) => text(..))`, blank ⇒ `none` — so the `datetime` lives in
/// the block, not this literal; `$cards` carry their per-kind-ordinal `$path`
/// prefix; everything else is a value literal Typst judges equal to the former
/// `json()` parse (date cells excepted). No runtime processing: the former
/// `__meta__` strip, content insertion, date parsing, and card loop are gone —
/// the backend did that work at generation time.
#let data = {data_literal}

/// Backend-generated plaintext table for every non-blank content field, keyed
/// by schema address (a field name `subject`, an array element `refs.2`, or a
/// card path `$cards.<kind>.<n>.<field>`). Each value is the richtext content
/// text with island slots (tables/images) stripped and all formatting marks
/// dropped — the same projection the pdfform backend lowers a richtext field
/// to. A generated literal; blank fields are absent (they default to `""`).
#let _qm-plaintext = {plaintext_literal}

/// A content field's **plaintext** projection as a `str`. Where `data.<field>`
/// carries Typst **content** (a `richtext` field lowers to a markup block),
/// this returns the field's plain text — use it when a plate or vendored
/// package needs a string for a content field: string ops (`.trim()`,
/// `.starts-with()`, `upper(..)`) or an `assert(type(item) == str)`-style
/// consumer (e.g. `create-auto-grid`). This is the sanctioned richtext→str
/// coercion — do NOT `repr`/stringify the content block. `field` is the schema
/// address (compose a card field from the card's `$path`:
/// `plaintext(card.at("$path") + "$body")`). Returns `""` for a blank field or
/// an address carrying no richtext content.
#let plaintext(field) = _qm-plaintext.at(field, default: "")