quillmark-typst 0.87.2

Typst backend for Quillmark
Documentation
// Auto-generated quillmark-helper package
// Version: {version}

/// Emit an unsigned signature field. PDF: a clickable AcroForm SigField
/// widget here. SVG/PNG: same invisible layout space.
/// `name` must be unique and match `[A-Za-z0-9_]+`. `width` / `height` must
/// be absolute lengths (pt/mm/cm/in).
/// `<__qm_sig__>` and `kind: "__qm_sig__"` 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 signature-field(name, width: 200pt, height: 50pt) = {
  assert(type(name) == str,
    message: "signature-field: name must be a string, got " + repr(type(name)))
  assert(name.match(regex("^[A-Za-z0-9_]+$")) != none,
    message: "signature-field: name must match [A-Za-z0-9_]+, got " + repr(name))
  let abs-pt(field, l) = {
    let r = repr(l)
    assert(not (r.contains("em") or r.ends-with("%")),
      message: "signature-field: " + field + " must be an absolute length, got " + r)
    l / 1pt
  }
  [#metadata((
    kind: "__qm_sig__",
    name: name,
    width: abs-pt("width", width),
    height: abs-pt("height", height),
  )) <__qm_sig__>]
  box(width: width, height: height, stroke: none, fill: none)
}

/// Parse an ISO 8601 date string (YYYY-MM-DD) to a Typst datetime
/// Handles both pure dates (2024-01-15) and datetime strings (2024-01-15T10:30:00)
/// Must be defined before `data` — Typst does not hoist `#let` bindings.
#let _parse-date(s) = {
  if s == none { return none }
  let date-str = str(s)
  // Handle datetime strings by extracting just the date part
  if date-str.contains("T") {
    date-str = date-str.split("T").at(0)
  }
  let parts = date-str.split("-")
  if parts.len() < 3 { return none }
  let year = int(parts.at(0))
  let month = int(parts.at(1))
  // Take only the first 2 characters in case there's extra content
  let day-str = parts.at(2)
  if day-str.len() > 2 { day-str = day-str.slice(0, 2) }
  let day = int(day-str)
  datetime(year: year, month: month, day: day)
}

/// Document data as a dictionary.
/// Markdown fields are automatically converted to Typst content objects.
/// The `__meta__` key (injected by the Rust backend) is consumed here and
/// never exposed to plate authors.
#let data = {
  let d = json(bytes("{escaped_json}"))

  // Read and consume __meta__ (injected by transform_fields)
  let meta = d.remove("__meta__", default: (
    content_fields: (),
    card_content_fields: (:),
    date_fields: (),
    card_date_fields: (:),
  ))

  // Eval the named content fields of `dict` into Typst content. A markdown
  // field is a string; a `markdown[]` field is an array whose string elements
  // are each eval'd. Used uniformly for the top-level data and for each card.
  let eval-content(dict, keys) = {
    for key in keys {
      if key in dict {
        let val = dict.at(key)
        if type(val) == str and val != "" {
          dict.insert(key, eval(val, mode: "markup"))
        } else if type(val) == array {
          dict.insert(key, val.map(v => if type(v) == str and v != "" { eval(v, mode: "markup") } else { v }))
        }
      }
    }
    dict
  }

  // Parse the named date fields of `dict` into Typst datetimes.
  let parse-dates(dict, keys) = {
    for key in keys {
      if key in dict {
        let val = dict.at(key)
        if val != none {
          dict.insert(key, _parse-date(val))
        }
      }
    }
    dict
  }

  d = eval-content(d, meta.content_fields)
  d = parse-dates(d, meta.date_fields)

  // Apply the same passes to each card under `$cards`, keyed by `$kind`.
  if "$cards" in d {
    let cards = ()
    for card in d.at("$cards") {
      let card-type = card.at("$kind", default: none)
      card = eval-content(card, meta.card_content_fields.at(card-type, default: ()))
      card = parse-dates(card, meta.card_date_fields.at(card-type, default: ()))
      cards.push(card)
    }
    d.insert("$cards", cards)
  }

  d
}