How to override styles?

I was messing about with typst styling rules while I was trying to get a specific style. What I had in mind was to apply a certain style to all the text before the first header and a different style to all the text after the first header. Here is an example I set up with my own crude implementation.

#let color-state = state("color", false)

#show heading: it => {
  set text(fill: primary-red)
}
#show par: it => {
  context {
    let is-action = color-state.get()
    let txt-color = if is-action { black } else { red }
    set text(fill: txt-color)


    block(
      text(it.body)
    )
  }
}

#show heading.where(level: 4): it => block[
  #text(it.body, size: 1.3em, fill: red, weight: "light")
  #v(-0.5em)
  #line(length: 100%, stroke: 1pt + red)

  #color-state.update(true)
]

This is some text before the first header, which needs to be red and is red

==== This is the first header that needs to be red and is red

This is the text after the first header, which needs to be black and is black

==== This is the second header that also needs to be red but is black

It all works good and well, maybe even too good. I want the headings themselves to be a different style (in this case just red) but I couldn’t get the show rule for headings to override the show rule for paragraphs. I tried putting it before, after and inside to no avail.

The problem arises because you are stripping away the heading element when you use #text(it.body, ...). This makes the show rule return a par element, which matches with the other show rule. Simply keeping the entire heading, i.e. #text(it, ...) fixes the issue.

Also, you should prefer using show-set rules whenever possible, as this allows for more customisation later (see: Is it possible to call set multiple times in a single show rule? - #8 by ensko)

In your example, this would be

#show heading.where(level: 4): set text(size: 1.3em, fill: red, weight: "light")
#show heading.where(level: 4): set block(below: 0.7em)
#show heading.where(level: 4): it => {
  show divider: set line(length: 100%, stroke: 1pt + red)
  show divider: box
  it + divider() + color-state.update(true)
}

So that later in the document, if you wanted to e.g. change the line to asterix’s you could just write

#show heading.where(level: 4): it => {
  show divider: set align(center)
  show divider: block[\* \* \*]
  show block: set text(fill: red)
  it
}

Instead of re-writing the whole show rule.

1 Like

Changing it.body to just it causes the whole text to disappear for some reason. So having:

#let color-state = state("color", false)

#show heading: it => {
  set text(fill: red)
}

#show par: it => {
  context {
    let is-action = color-state.get()
    let txt-color = if is-action { black } else { red }
    set text(fill: txt-color)


    block(
      text(it.body)
    )
  }
}

#show heading.where(level: 4): it => block[
  #text(it, size: 1.3em, fill: red, weight: "light")
  #v(-0.5em)
  #line(length: 100%, stroke: 1pt + red)

  #color-state.update(true)
]

This is some text before the first header, which needs to be red and is red

==== This is the first header that needs to be red and is red

This is the text after the first header, which needs to be black and is black

==== This is the second header that also needs to be red but is black

Results in this:

I will try to remember your advice about prioritizing show set rules, but first I need to figure this out.

There is a discussion going on about resetting/revoking styles (which then can be used to override existing styles), but seems to be a rather complicated topic, as the goal is to get a general solution.

Resetting `show` and `set` to its default / Revoke rules · Issue #420 · typst/typst · GitHub and a talk one of the devs gave https://www.youtube.com/watch?v=PElGKQj0Ryc

You need to return it in your transforming show rule for headings.

#show heading: it => {
  set text(fill: red)
  it
}
1 Like

I did some more screwing around and with a combination of your suggestions I managed to get the test to work, but when I apply it to my real use case I have another issue. I have another show rule at the top level of the document which looks like this:

#show heading.where(level: 4): it => block[
  #v(0.5em)

  #set text(size: 1.4em, weight: "bold")
  #it

  #v(0.3em)
]

I want only a section of the document to be effected by the rules I made (which is determined by cmarker and html tags). So I have something like this:

#let stat-block = (attrs, body) => {
  let beige = rgb("#BDB1B1")
  let gray = rgb("#555555")
  
  block(
    stroke: 2pt + beige,
    radius: 10pt,
    inset: 0.5em,
    breakable: false
  )[
    #let color-state = state("color", false)

    #show par: it => {
      context {
        let is-action = color-state.at(it.location())
        let txt-color = if is-action { black } else { red }

        set text(fill: txt-color)
        
        it
      }
    }

    #show heading.where(level: 3): it => block[
      #color-state.update(false)
      
      #set text(size: 1.9em, fill: red)
      #it.body
      #v(-0.8em)
      #line(length: 100%, stroke: 1pt + red)
    ]


    #show heading.where(level: 4): it => block[
      #text(it, size: 1.3em, fill: red, weight: "light")
      #v(-0.5em)
      #line(length: 100%, stroke: 1pt + red)

      #color-state.update(true)
    ]

    #body
  ]
}

#import "@preview/cmarker:0.1.10"

#let html-dict = (
  statblock: ("normal", (attrs, body) => stat-block(attrs, body))
)

#let text = read("input.md")

#cmarker.render(
  text,
  html: html-dict,
)

I have two options now, when I put it.body in the #text, the black rule is applied and when I put it the top rule is applied and I can’t change the size or weight (interestingly I can change the color still). I have no idea in what order the rules are applied or how I can change it, any help would be appreciated.

If I understood correctly, the problem is caused exactly by what I was describing with prioritising show-set rules over “raw” show rules, precisely because of this thing with more customisation later on. So re-writing the first show rule to be

#show heading.where(level: 4): set block(inset: (
  top: 0.5em,
  bottom: 0.3em
))
#show heading.where(level: 4): set text(size: 1.4em, weight: "bold")

Allows you to override them later:

Code example
#show heading.where(level: 4): set block(inset: (
  top: 0.5em,
  bottom: 0.3em
))
#show heading.where(level: 4): set text(size: 1.4em, weight: "bold")


==== I am a level 4 heading, and bold!

#let color-state = state("color", false)

#show heading.where(level: 4): set text(size: 1.3em, fill: red, weight: "light")
#show heading.where(level: 4): set block(below: 0.7em)
#show heading.where(level: 4): it => {
  show divider: set line(length: 100%, stroke: 1pt + red)
  show divider: box.with(inset: (top: -1em))
  it + divider() + color-state.update(true)
}

==== I am also a level 4 heading, and because we used show-set rules I am red and not bold!
2 Likes