How to remove headings by regex pattern match

I want to remove all second-level headings that contain “foo” from the compiled output - as if those lines were commented out or never existed.

I understand how to style headings with #show rules, but I don’t understand how to:

  1. Remove/exclude content entirely (not just style it)
  2. Filter headings by both their level AND their body content

Said in another way, the extent of my knowledge is styling text but I don’t know how to style only headings that match my regex; and I don’t know how to make text not display. (This is different from hiding. I want the pdf compiled as if my matched header was commented out.)

// What I know
#show heading.where(level: 2): set text(red)
#show regex("foo"): set text(blue)

// Example of what I have
== Something
== foo
== Something else

//Desired output
== Something
== Something else

// The `== foo` heading should not appear at all in the compiled document and the rest of the content should move up to fill the space.

This will not work, IMHO.

While it may look like show rules operate on the raw source text of your document, they do not. Show rules only kick in after Typst has parsed the document. In other words, the headings are there and will not go away. You might be able to hide them, and I’m sure people will help you with that, but it will be very hard to delete the headings from within Typst.

Have you looked at sed?

Hello @bountonw, welcome to the Typst community!

A reminder when posting in the Questions category: the title of the post must be a question, which you would pose your friends or colleagues (see How to post in the Questions category). So for the future set it accordingly. I unfortunately don’t have enough permissions to change your title.

Additionally use the code blocks syntax when posting code, as it’s easier to read typst code/script when syntax highlighting is shown:

```typst
// your code here
```

Now to your question:

This is not possible. Show rules are applied, after the content has been placed. This means even if you can hide the heading somehow, “foo” exists in the documents outline and location!

There are options to use command line arguments and the sys module:

// since inputs are always string, we have to use a comparison
// (or `eval()`) to get a boolean (I think, am not the best Typst programmer...)
#let show-foo = sys.inputs.at("show-foo", default: "true") == "true" 

== Something

#if show-foo [
== foo
]

== Something else

In this case you would call typst compile /path/to/doc.typ --inputs show-foo=false to not show the foo headers.

Although I’m not exactly why you would want to do that. What kind of document do you want to create?

Thank you. For collaboration reasons, == foo is in the text, but for pdf and html, we do not want it. That is why sed woudn’t work. But I supposed that we could comment them out. (We are considering migrating *.md files to *.typ and I am studying what would be involved.)

To be honest, this describes comments to some degree. If you really, really want this:

  1. Use a variable to check if you want to show foo or not
  2. Create a new function, which checks if foo will be shown and render the content accordingly
  3. Map it to the --input arg of the CLI (not shown in the example code below)
#let foo-show = false

#let foo-heading(title, level: 1, content) = {
  if foo-show {
    heading(title, level: level)

    content
  }
}

= Something

#foo-heading[foo][]

Thank you for the useful link on how to ask questions and how to mark the typst code so that it shows up. I have made the edits in the original post.

I’m considering migrating several books that are in *.md files that we have scheduled to be typeset via LaTeX. There are some ## section headings that are there for reference and navigation purposes in the markdown that are not meant for production. In the LaTeX pipeline, these are stripped out in pre-processing. But since I am considering migrating to .typ, we wouldn’t have the same pre-processing pipeline. This is a real world example, although I can think of … (er) other-worldly examples examples that are not currently in my use case.

Thank you! I think this is what I am looking for. I don’t know how to works :sweat_smile:, but I will play with it. What would I look for in the documentation that would help me understand the code you posted?

Thank you again for the helpful code to get me started. Obviously I don’t know what I am doing. I can’t even seem to get the and logic working.

#let foo-show = true

#let foo-in-subheading(level: 2, content) = {
  if foo-show {
    heading(level) and content == regex("foo")
    content
  }
}

= Something

// What the current code is and what I would like it to remain if possible.
== foo

// What is necessary (so far) in the current approach to toggle `== foo` on and off.
#foo-in-subheading[== foo][]

I’ve simplified the function more:

#let show-foo = true

#let foo(content) = {
  if show-foo {
    content
  }
}

= Something

#foo[
  == Some reference which can be hidden or not
]

#let show-foo = true

The first line declares a variable named show-foo with the value true assigned. This variable is used to show/hide the reference/navigation elements.

#let foo(content) = {
  if show-foo {
    content
  }
}

Then a function called foo with 1 parameter is created. content is where you would put the foo.

The position of level: ... is not important, BUT the first “unnamed” parameter will always be title and the second one content.

= Something

#foo[
  == Some reference which can be hidden or not
]

Now if you want to hide all foo() just change the value of show-foo to false


In regards to your changes

#let foo-in-subheading(level: 2, content) = {
  if foo-show {
    heading(level) and content == regex("foo")
    content
  }
}

You don’t need and content == regex("foo") at all. If you set show-foo to false, all the elements which are created/rendered through #foo will not be rendered.

1 Like

Thank you. I was trying to figure out a way to flag it without wrapping it.

This can be reduced to just

#let foo(content) = if show-foo { content }

The example can also be visually solved by direct comparison:

#import "@preview/t4t:0.4.3": get
#show heading: it => if get.text(it.body).match(regex("fo{2,}")) == none { it }
#show heading.where(body: [foo]): none

== Heading
== foo

But it will not affect the PDF Document Outline. Similar to show table: it => block[text], show rules cannot fully remove original elements either from context/compilation or from output. It is something that could be possible, so the above example can work more than just visually.

1 Like

This is very helpful, but like you say, the ignored content still shows up with

#outline()

If there is a way to suppress the undesired content in the outline and TOC too, then we have a full solution.

No, I said it shows up in PDF’s Document Outline, not with Typst’s outline element. But both are actually fixable with a show-set rule:

#show heading.where(body: [foo]): set heading(outlined: false)
#show heading.where(body: [foo]): none
1 Like

Wow, that is neat.

Saving the pdf and opening the outline pain, I don’t see the hidden element in the outline. So I think this is the solution except the part of how to use a regex. The documentation seems sparse in this area. I can see the syntax for the actual regex, but not how to attach a regex into the current function

So, given your code we have

#show heading.where(body: [foo]): set heading(outlined: false)
#show heading.where(body: [foo]): none

And using my pseudocode, I want

#show heading.where(body: [regex("foo"]): set heading(outlined: false)
#show heading.where(body: [regex("foo"]): none

As my text is more complicated than “foo”.

You unfortunately can’t use a regex in the same way. That’s where you end up without a good solution (or into the solutions with replacing/hiding an existing element). You should be able to match using more complex content than just [foo] but it needs to be literally the same, not by pattern.

(Second attempt - just a little hacky)

Here’s a way to match with regex and hide the matching headings. In this case it’s using a regex match on the t4t.get.text result which I think is probably the best way to do it - much easier than using the native show regex(..) kind of matching - both methods have their drawbacks too.

#import "@preview/t4t:0.4.3"

#show: doc => context {
  // find matching headings
  let matching-headings = query(heading).filter(elt => regex("foo") in t4t.get.text(elt.body))
  let heading-fields = matching-headings.map(elt => (level: elt.level, body: elt.body))
  // get a selector for those headings
  let heading-selector = <_matches_nothing>
  if heading-fields.len() > 0 {
    heading-selector = selector.or(..heading-fields.map(f => heading.where(..f)))
  }
  // hide them - and exclude them from outline and numbering
  show heading-selector: none
  show heading-selector: set heading(outlined: false, numbering: none)
  doc
}

#set heading(numbering: "1.1")
#outline()
== Something
== foo
== Something else
= A
== B
=== C
= foo
== foobar
== footerism
== Soon done
= Last


(Below: First attempt - Very Hacky)

Here is a hacky way to do it, but the warning above applies: we end up with un-typsty solutions that have drawbacks and don’t compose at all with other functionality (templates, packages).

We can’t replace a heading element truly. Good solutions use show-set to change an existing heading’s presentation. But here I think we need to replace the heading and “forget about the old one” to properly make it work with the outline as well.

To do this it’s essentially doing

  1. Make headings unoutlined and unnumbered by default. (So that we can forget about headings we don’t want)
  2. Check if the heading body matches the regex
  3. If so, convert it to a new heading that is outlined and numbered.

Below I’ve checked that this works out with numbering and outlining in total. However it is known that it breaks at least these features:

  • Heading labelling and referencing
  • (more to come)
Code and output

// Use heading.outlined = false/true as a marker
// and this makes sure that the extra headings we create are not displayed
// Some severe drawbacks of this: headings must be non-numbered and non-outlined
// by default. Because the original headings will be "abandoned and hidden", not removed.

// required defaults
#set heading(outlined: false, numbering: none)
// then the rest will be numbered and outlined.
#let numbering = "1.1"
#let outlined-exceptions = ([Contents], [Bibliography])

#show heading: head => {
  // First: early return if already good
  if head.outlined or outlined-exceptions.contains(head.body) {
    return head
  }
  let matches-pattern = state("matches-pattern")
  {
    // HACK: Place hidden copy of the heading title to test it vs the regex
    matches-pattern.update(false)
    show regex("foo"): it => it + matches-pattern.update(true)
    place(hide(head.body))
  }
  context {
    if matches-pattern.get() {
      // forget about this heading
    } else {
      let (..fields, body) = head.fields()
      heading(..fields, outlined: true, numbering: numbering, body)
    }
  }
}


#outline()
== Something
== foobar
== Something else
= A
== B
=== C
= foo
== foobar
== footerism
== Soon done
= Last

3 Likes

This is neat. Thank you. Real Life deadlines have the next couple of days packed, but I will be studying this. To show my newbieness, query and .filter are new things to me.