How to add an invisible Heading to an outline?

Basically I want to insert an invisible “heading” into the outline. The heading should not not effect the rest of the document. I am looking for some sort of heading-label that is only shown in the outline to mark a new section: the appendix.

The outline should look like this:

...
Appendix                             
<appendix 1>
...

Appendix should show the same page as <appendix 1>.

Do you have an idea how to do this?

You can use a scoped show rule

...

#{
  show heading: none
  heading(numbering: none)[Appendix]
}

== <appendix 1>

so that the heading is added to the document tree but not actually shown. As the outline queries for all headings that exist in the document tree, the fact that it’s not actually visible doesn’t matter for the outline entry.

Note that (if a numbering is set) the heading counter will still be stepped, though you probably want to reset the counter value and update the numbering anyway for the appendix.

1 Like

Why do you want something invisible to be inserted in the outline, which also shouldn’t affect the rest of the document? Do you still need the heading to be seen in the document? Otherwise, just don’t add the heading in the first place.

As a real example, when I had the same problem, it was because of a complex document structure (nested/bundled documents). Basically I need to insert the headings to then use them in a global-scope outline (and in the PDF outline), but the headings themselves shouldn’t be visible in the document (outside the outline), because they serve a semantic purpose instead of a literal one. I fixed it like illustrated here.

You can use show rules to customise the outline. In this case I inserted a block with the text “Appendix” right before “Heading 2”:

#show outline.entry.where(body: [Heading 2]): it => {
  block(spacing: 0pt)[
    #link(it.element.location())[Appendix]
  ]
  it
}

#outline(indent: 1em)

= Heading 1
== Subheading 1
== Subheading 2
= Heading 2
== Subheading 3
= Heading 3
== Subheading 4

If the heading name is not unique, you can use labels instead. I’m not sure how to write this one in a cleaner way:

#show outline.entry: it => context {
  if it.element.location() == locate(<appendix-heading>) {
    block(spacing: 0pt)[
      #link(<appendix-heading>)[Appendix]
    ]
    it
  }
  else {
    it
  }
}

#outline(indent: 1em)

= Heading 1
== Subheading 1
== Subheading 2
= Heading 2 <appendix-heading>
== Subheading 3
= Heading 3
== Subheading 4

It’s a bit verbose of a solution (and it’s not really a heading shown in the outline but an inserted block of text before an entry) but gives you more freedom to customise the appearance of your section marker.

1 Like

I think they mean that the heading should be visible in the outline but invisible in the rest of the document, as they mention it should be visible in the outline.

Oh, this would make much more sense.

A generic function would be nice to add random content to the outline … for example it would be easy if I could find out how to check it.body for strings (starts-with) (if it is a sequence)?

First of all, thanks for helping!

As @Blaz clarified I was looking for a “Heading” that is visible in the outline but invisible in the rest of the document. Also I didn’t want it to be effected by styling-rules. So basically a label I place somewhere in the document.

@Eric Unfortunately your solution wasn’t what I was looking for because it actually creates a heading.

@Blaz You lead me into the right direction. My solution is heavily inspired by your examples :)


This is what I came up with. It is kind of hacky but works pretty well:

#let APPENDIX = [
  = A1 (Appendix starts here)
  #lorem(25)
  = A2 
]

#let is_set = counter("is_set") 
#is_set.update(0)

#show outline.entry: it => context {
  if is_set.get().at(0) == 0 {
    let pos_it = it.element.location().position()
    let pos_appendix = locate(<appendix>).position()
    let page_diff = pos_it.page - pos_appendix.page
    
    if page_diff > 0 or (page_diff == 0 and pos_it.y > pos_appendix.y) {
      block(link(<appendix>, strong({
        [Appendix ]
        box(width: 1fr, repeat[.])
        counter(page).display()
      })))
      is_set.update(1) 
    }
  }
  it
}

#outline()\

= H1
#lorem(25)
= H2
#lorem(25)

<appendix>
#APPENDIX
Output

image


If you have any ideas on how to improve this, please let me know!

1 Like

By using some of the same optimizations from How to mutate variables in a show rule? - #2 by Andrew, here is my slightly improved version:

#let make-outline-entry(label, body) = {
  let filler = box(width: 1fr, repeat[.])
  let content = body + " " + filler + counter(page).display()
  block(link(label, strong(content)))
}

#show outline.entry: it => {
  let label = <appendix>
  let is-set = state("is-set", false)
  context if not is-set.get() {
    let found = query(selector(label).before(it.element.location()))
    let is-after-appendix = found.len() > 0
    if is-after-appendix {
      make-outline-entry(<appendix>)[Appendix]
      is-set.update(true)
    }
  }
  it
}
The rest
#outline()

= H1
#lorem(25)
= H2
#lorem(25)

<appendix>
= A1 (Appendix starts here)
#lorem(25)
= A2

I made it more readable/maintainable. Or you can slap everything together to make the code smaller:

#show outline.entry: it => {
  let label = <appendix>
  let is-set = state("is-set", false)
  context if not is-set.get() {
    let found = query(selector(label).before(it.element.location()))
    if found.len() > 0 {
      block(link(label, strong({
        "Appendix "
        // box(width: 1fr, repeat[.])
        box(width: 1fr, it.fill)
        counter(page).display()
      })))
      is-set.update(true)
    }
  }
  it
}

Some things (e.g., using "" instead of [] for the name) are pure preference, but most things are the recommended way of writing Typst code (or code in general):

  • using descriptive variable/function names;
  • using state() over counter() if you don’t need to count natural numbers;
  • btw, counter() is initially set to 0, so you don’t have to explicitly set it initially unless you need to reset it at some point;
  • using query(selector(pivot).before(location)).len() > 0 over manually comparing pages/y position;
    • there is also .after();
    • sometimes you need to use things like <element>.where() which is already a selector;
  • the recommended variable/function naming convention is to use kebab case (which is typically easier to type).

A little bit less “you should do it” things:

  • adding aliases for things that need to be used multiple times (i.e., label) to reduce chance of a “desync value” bug (and similar stuff);
  • making context scope as small as possible.
2 Likes

This is a wonderful addition! Thanks a lot!

I didn’t know about state - this is very helpful :)

1 Like