Overriding template parameters; missing social convention or Typst design flaw?

I don’t think it’s that hacky… unless maybe only declarative solutions are considered clean and imperative configuration is seen as a kind of hack. I hope you’ll forgive me for going a bit on a tangent here but I think it’s an interesting dichotomy:

Typst has both declarative rules (set xxx and show xxx: set ...) rules and imperative rules (show xxx: it => ...) which is great: declarative code is generally more readable, easier to analyze and optimize, it composes better, etc. so people should use show-set rules as much as possible. But they are limited to the control knobs that have been explicitly designed in the language… For anything else you need the power of imperative rules that give you a whole programming language to replace an element with anything you want.

The unequivocal-ams template uses an imperative rule to style the headers:

show heading: it => {
  // Create the heading numbering.
  let number = if it.numbering != none {
    counter(heading).display(it.numbering)
    h(7pt, weak: true)
  }

  // Level 1 headings are centered and smallcaps.
  // The other ones are run-in.
  set text(size: normal-size, weight: 400)
  set par(first-line-indent: 0em)
  if it.level == 1 {
    set align(center)
    set text(size: normal-size)
    smallcaps[
      #v(15pt, weak: true)
      #number
      #it.body
      #v(normal-size, weak: true)
    ]
    counter(figure.where(kind: "theorem")).update(0)
  } else {
    v(11pt, weak: true)
    number
    let styled = if it.level == 2 { strong } else { emph }
    styled(it.body + [. ])
    h(7pt, weak: true)
  }
}

Among many things this wraps every level 1 heading in a smallcaps element. You need an imperative rule for that, and it seems natural that an imperative rule is required to undo just this part of the code. I find it rather elegant that Typst lets you do it with a simple show smallcaps: it => it.body which basically reads as “remove the smallcaps wrapper”.

Maybe a future version will let us wrap/unwrap elements declaratively… And when Typst gets custom types, third-party packages/templates will be able to extend the range of things we can configure/override with set and show xxx: set .... But for users, declarative rules will still be limited to predefined settings so imperative rules will always have a place, not as hacks but as legitimate solutions.

This sounds similar to the idea of revokable show rules. If that gets implemented it would allow for a nice declarative solution, but again only if the template author thinks of making the smallcaps wrapper “configurable” by isolating it in its own show rule and labelling or exporting the rule.

(I think your idea with weak rules is bit different as it would not require labelling/exporting the rule, but then it would not be flexible enough: you might have 4 weak rules applied on an element and want to override only one of them…)

I’m not sure having sub-settings in options makes a big difference. In the end either the option is supported by the template or it is not (or has a different syntax), and you need to adapt your code when you switch templates.

But yeah having more template options is another way to offer declarative settings, and it’s great for things that are not easy to configure/override otherwise. Maybe it would make sense to have smallcap-h1 in there. But this can’t possibly be a general solution… Just looking at the heading show rule above, to cover all uses cases you’d need a gazillion of options:

  • center-h1
  • number-hspace
  • v-above-h1
  • v-below-h1
  • v-above
  • v-below
  • style-h2
  • reset-theorem-h1

Basically every line of imperative code would require a setting, which means a huge API to learn, a huge manual to write, and a more complex code base since these settings must be declared in code and every line replaced with if setting-xxx { ... } else { ... }.

So I think it shouldn’t be the goal to make everything configurable/overridable declaratively. Instead we should aim to

  1. make as many things as possible easy to override with standard Typst code (either declarative or imperative), and
  2. offer settings for things that are difficult to override with standard code.

A concrete example

How can I disable the centering of level 1 headings with unequivocal-ams? Should the template offer a center-h1 setting?

It sounds like something trivial that doesn’t need an explicit setting because a simple show-set rule will do, but unfortunately it’s not that simple (as far as I can see).

You can “override” the set align(center) with something like show heading.where(level: 1): block to make the align ineffective, but that will also affect the spacing (and probably other things) so it’s not a clean solution.

It touches on the main issue I encounter when trying to compose show rules: composability breaks down when a rule for element of type X doesn’t return an element of type X (see previous discussion here). Here the heading show rule returns

smallcaps[
	#v(15pt, weak: true)
	#number
	#it.body
	#v(normal-size, weak: true)
]

No heading is returned. By chance the returned content is an element (smallcaps) that I can select on, so I can do

#show heading.where(level: 1): it => {
  show smallcaps: set align(left)
  it
}

but that’s definitely a hack…

Ideally you’d like the show rule to return a heading with the smallcaps as body, but that introduces all kinds of issues with numbering and styling and recursion.

Maybe the solution will be user types: if the show rule returned an ams-heading I could simply do show ams-heading: set align(left).

It might even fix composability if ams-heading could be made a child type of heading or something like that, so that a show rule on heading placed before show: ams-article would still be applied even after its transformation into an ams-heading?

3 Likes