How can I manipulate content values?

Currently, manipulating content is very involved. This is mainly caused by the content functions

  • taking different amount of positional arguments
  • fields() not distinguishing between positional and keyword arguments
  • not all content functions being accessible (e.g. sequence can as far as I know only obtained by hacks like [].func())

For example, I’d like to recursively update all strings in a content, which currently requires a lot of special casing and hard to know if you caught all edge cases. This leads to code like this

#let titlecase(data) = {
  let update-dict(dict) = {
    let new-dict = (:)
    for (k, v) in dict.pairs() {
      new-dict.insert(k, titlecase(v))
    }
    return new-dict
  }

  if type(data) == content {
    if data.has("children") {
      let new-fields = data.fields()
      new-fields.remove("children")
      let new-children = data.children.map(titlecase)
      return data.func()(new-children, ..update-dict(new-fields))
    }
    if data.has("body") {
      let new-body = titlecase(data.body)
      let new-fields = data.fields()
      new-fields.remove("body")
      return data.func()(new-body, ..update-dict(new-fields))
    }
    if data.has("text") {
      let new-text = titlecase(data.text)
      let new-fields = data.fields()
      new-fields.remove("text")
      return data.func()(new-text, ..update-dict(new-fields))
    }
    if data.has("base") {
      let new-base = titlecase(data.base)
      let new-fields = data.fields()
      new-fields.remove("base")
      return data.func()(new-base, ..update-dict(new-fields))
    }
    // styled
    if data.has("child") {
      let new-child = titlecase(data.child)
      let new-fields = data.fields()
      new-fields.remove("child")
      let _ = new-fields.remove("styles")
      return data.func()(new-child, data.styles, ..update-dict(new-fields))
    }
    let new-fields = data.fields()
    return data.func()(..update-dict(new-fields))
    return data
  }
  if type(data) == str {
    str(_plugin.titlecase(bytes(data)))
  } else {
    data
  }
}

Am I overlooking something that would make this easier? Otherwise, it would be already much more straightforward if content had an arguments() function, that returns an arguments object. That would at least allow an easier reconstruction of the content.

Content is not really meant to be manipulated, usually the better way is to only create content from data that has already been processed in any way necessary. This is often the right advice for e.g. tables – however, it is not really applicable to your use case because the data you have is fundamentally already formatted text.

The next best way, though, is not manipulating the full hierarchy of the content value you’re dealing with, but applying a rule. Your specific case can be handled by this code:

#show regex("\b\w+\b"): word => {
  let (first, ..rest) = word.text.clusters()
  first = upper(first)
  (first, ..rest).join()
  // or instead:
  // str(_plugin.titlecase(bytes(word.text)))
}

some *important* text

The general principle here is: access deeply nested data using Typst’s tools (show and set rules), not by inspecting values.

In cases where even that doesn’t work, the approach you have is the one you need to go with. If that wasn’t already where you got your inspiration, there’s this example in the Typst examples book. You can still make your life a bit easier compared to the code you wrote, here’s an excerpt:

let update-dict(dict) = {
  for (k, v) in dict { ((k): titlecase(v)) }
  // or this:
  // dict.pairs().map(((k, v)) => ((k): titlecase(v))).join()
}

// ...

  if data.has("children") {
    let (children, ..fields) = data.fields()
    children = children.map(titlecase)
    fields = update-dict(fields)
    return data.func()(children, ..fields)
  }
  if data.has("body") {
    let (body, ..fields) = data.fields()
    body = titlecase(body)
    fields = update-dict(fields)
    return data.func()(body, ..fields)
  }

One part that you probably missed is how to use variables as dictionary keys; see How can I instantiate a dictionary with a variable key name?

That helps a bit, especially the regex part, thank you! Also, the pattern deconstruction of dictionaries seems useful as well.

I still think that an arguments() function would be more logical than fields() for some use cases.

1 Like