How to add a custom array-like field to a custom theorion box?

Hello there everyone, I’m trying to learn the software and explore making & customizing things to learn, and I came across this use case:

I made a simple custom theorem from the provided template from theorion package.

I wanted to add some sort of marker I called it suffix (an optional tag for the theorem). Ideally expecting it to have a flexibility when dealing with specifying the arguments like in stroke, or radius for example.

The goal was to achieve the look in the attached photo.

which I did using the following code:

/// page setup
#set page(height: auto, width: 10cm)

#import "@preview/theorion:0.3.3": *
#import cosmos.clouds: *

// tag fnc to define the style
#let tag(fill: blue, body) = box(
            radius:0.20em,
            fill: fill.lighten(80%),
            inset: (x:0.25em),
            outset: (y:0.15em)
            )[
              #text(size:0.6em)[#body]
              ]

#show: show-theorion

#let (mytheo-counter, mytheo-box, mytheo, show-mytheo) = make-frame(
  "theorem",
  "Theorem",
  counter: theorem-counter,
  inherited-levels: 2,
  inherited-from: heading,
  render: (
    prefix: auto,
    title: "",
    full-title: auto,
    suffix: (text:auto,fill: blue), /* // need it to behave like `stroke`  or `radius` */
    body) => [
    #block(
      fill:blue.lighten(85%),
      inset: 0.5em,
      radius: 0.4em,
      )[
        *#full-title*
        #h(1fr)
        #{  
          if suffix.text != auto {
            tag(fill: suffix.fill)[#suffix.text]
            }
        }
        #linebreak()
        #body
      ]
    ]
)

#show: show-mytheo
    #mytheo(
      suffix:(text:"important", fill:red) //  both specified, works fine
      )[
      #lorem(5)
      ]

My attempt at implementing suffix was bad but I don’t know how to fix it, it worked in the example above but only because both suffix.text and suffix.fill are explicitly specified.

But whenever I attempt not specifying at least one of the keys, it no longer renders, and gives the following error as shown below:

#mytheo(
      suffix:(text:"important") //  error: "dictionary does not contain key "fill""
      )[
      #lorem(5)
      ]
  1. When I don’t specify the fill arg, I need it to fall back to the default.
    I expect suffix to handle its arguments/parameters with flexibility, like stroke for example, specifying stroke: 2pt + red is equivalent to stroke: (paint: red, thickness: 2pt). Any parameter (from the rest) that is not specified, doesn’t fail to return a value, it gives the defaults.

It doesn’t need to be the same level of flexibility, as this may be reserved to Typst’s native types, but any correction to my current understanding to how to implement this is most welcome.

Hi @MJ.0, try using an argument sink: Arguments Type – Typst Documentation

1 Like

Thank you @vmartel08 for the nudge in the right direction, I knew of sink arguments but couldn’t get it right the first time, so I did give it a second more careful try and ended up with a better result!

I replaced the suffix: (text:auto,fill: blue) with ...suffix I knew that from previous attempts,

yet I didn’t know how to set the defaults for the keys so I assumed I needed a lower level approach. But thanks to your insight, I realized that I missed the option default in definitions/methods like at() which allowed me to set keys with defaults, and now works as expected.

my latest attempt:

#let (mytheo-counter, mytheo-box, mytheo, show-mytheo) = make-frame(
  "theorem",
  "Theorem",
  counter: theorem-counter,
  inherited-levels: 2,
  inherited-from: heading,
  render: (
    prefix: auto,
    title: "",
    full-title: auto,
    ..suffix,
    body) => [
    #block(
      fill:blue.lighten(85%),
      inset: 0.5em,
      radius: 0.4em,
      )[
        *#full-title*
        #h(1fr)
        #{  
          if suffix.at("suffix", default: "") != "" {
            tag(fill: suffix.at("suffill", default: blue))[#suffix.at("suffix", default: "")]
            }
        }
        #linebreak()
        #body
      ]
    ]
)

#show: show-mytheo
    #mytheo(
      suffix:"important",
      suffill:red, //works as expected
      )[
      #lorem(5)
      ]

This for is more than acceptable, both the text are independent and fall back to defaults. But If you don’t mind me asking, what else do I need to utilize to approach the form suffix:(text:"important",) without triggering errors for not specifying the other key?

I don’t understand where the source of truth is for default values — in tag() or in render: () => {}. You can define default values inside the closure.

You can’t get default value automatically by not specifying arguments, because suffix is a named argument. The only way is to provide named arguments separately, like Codly does with codly().

Separate arguments approach
#import "@preview/theorion:0.3.3": make-frame, theorem-counter

/// Tag function to define the style.
#let tag(fill: blue, body, ..args) = box(
  radius: 0.20em,
  fill: fill.lighten(80%),
  inset: (x: 0.25em),
  outset: (y: 0.15em),
  text(size: 0.6em)[#body],
  ..args,
)

/// Tag function to define the style.
#let render(
  prefix: auto,
  title: "",
  full-title: auto,
  suffix-text: none,
  suffix-fill: blue,
  suffix-stroke: 2pt + red,
  suffix-radius: 0.2em,
  body,
) = {
  let content = {
    strong(full-title)
    h(1fr)
    if suffix-text != none {
      tag(
        fill: suffix-fill,
        stroke: suffix-stroke,
        radius: suffix-radius,
      )[#suffix-text]
    }
    linebreak()
    body
  }
  block(fill: blue.lighten(85%), inset: 0.5em, radius: 0.4em, content)
}

#let (mytheo-counter, mytheo-box, mytheo, show-mytheo) = make-frame(
  "theorem",
  "Theorem",
  counter: theorem-counter,
  inherited-levels: 2,
  inherited-from: heading,
  render: render,
)

#set page(height: auto, width: 10cm)
#show: show-mytheo

#mytheo(
  suffix-text: "important",
  // suffix-fill: red,
)[
  #lorem(5)
]

A less elegant way it to use dictionary (or arguments/sink) and probe for passed argument or specify the default value, which is what you do in How to add a custom array-like field to a custom theorion box? - #3 by MJ.0.

Dictionary approach
#import "@preview/theorion:0.3.3": make-frame, theorem-counter

/// Tag function to define the style.
#let tag(fill: blue, body, ..args) = box(
  radius: 0.20em,
  fill: fill.lighten(80%),
  inset: (x: 0.25em),
  outset: (y: 0.15em),
  text(size: 0.6em)[#body],
  ..args,
)

/// Tag function to define the style.
#let render(prefix: auto, title: "", full-title: auto, suffix: none, body) = {
  let content = {
    strong(full-title)
    h(1fr)
    if type(suffix) == dictionary and suffix.at("text", default: none) != none {
      let fill = suffix.at("fill", default: blue)
      let stroke = suffix.at("stroke", default: 2pt + red)
      let radius = suffix.at("radius", default: 0.2em)
      tag(fill: fill, stroke: stroke, radius: radius, suffix.text)
    }
    linebreak()
    body
  }
  block(fill: blue.lighten(85%), inset: 0.5em, radius: 0.4em, content)
}

#let (mytheo-counter, mytheo-box, mytheo, show-mytheo) = make-frame(
  "theorem",
  "Theorem",
  counter: theorem-counter,
  inherited-levels: 2,
  inherited-from: heading,
  render: render,
)

#set page(height: auto, width: 10cm)
#show: show-mytheo

#mytheo(suffix: (text: "important"))[
  #lorem(5)
]
1 Like

This was the comprehensive answer I was looking for. Thanks for sharing.
Especially the following approach is insightful to me:

Whether I opt for either method, I managed to learn to ways to do it.

1 Like