How to query headings in current document?

I use the bundle feature to build a website. I want to include an outline on every page, that links to the different headers on the current document. Is there a way to do that?

This document:

#document("page1.html")[
  #outline()
  = Page 1, header 1
  = Page 1, header 2
]
#document("page2.html")[
  = Page 2, header 1
  = Page 2, header 2
]

Compiles to:

<nav>
  <h2>Contents</h2>
  <ol style="list-style-type: none">
    <li><a href="#loc-1">Page 1, header 1</a></li>
    <li><a href="#loc-2">Page 1, header 2</a></li>
    <li><a href="page2.html#loc-1">Page 2, header 1</a></li>
    <li><a href="page2.html#loc-2">Page 2, header 2</a></li>
  </ol>
</nav>
<h2 id="loc-1">Page 1, header 1</h2>
<h2 id="loc-2">Page 1, header 2</h2>

Where I would have wanted:

<nav>
  <h2>Contents</h2>
  <ol style="list-style-type: none">
    <li><a href="#loc-1">Page 1, header 1</a></li>
    <li><a href="#loc-2">Page 1, header 2</a></li>
  </ol>
</nav>
<h2 id="loc-1">Page 1, header 1</h2>
<h2 id="loc-2">Page 1, header 2</h2>

I would need something like

#context outline(target: selector(heading).within(selector(document).parent(here())))

But selectors don’t have anything like a parent() method.

A workaround is to wrap the whole document in a context, and use here() in the outline’s target selector. It is somewhat fragile though, because there cannot be another context as the outline’s parents.

#document("page1.html", context [
  #outline(target: selector(heading).within(here()))
= Page 1, header 1
= Page 1, header 2
]) // works
#document("page2.html", context [
  #context outline(target: selector(heading).within(here()))
= Page 2, header 1
= Page 2, header 2
]) // doesn't work (outline is empty)
1 Like

You could query the location of the current document:

#context outline(target: selector(heading).within(
  query(selector(document).before(here()))
    .last()
    .location()
))

Because locations are unique identifiers for elements, you may use it inside within(...) :slight_smile:

2 Likes

See also:

3 Likes

ha, that’s the same exact solution! thanks for the reference :smile:

1 Like

That works, thank you very much. I didn’t think before() would also select the current element, shouldn’t that be specified in the documentation? I guess that’s what inclusive is for, but that isn’t very clear.

Actually, inclusive doesn’t matter here (you can set inclusive to false, and it produces the same output). Since the layout of the document is something like

#document[
  #context(...)
  ...
]

The document occurs strictly before the context block even if the context block is nested inside the document. Meaning, when we call .before(here()), there is a document before the start of the context block, which gets matched.

The inclusive field would be relevant in a scenario like

#document("index.html")[
  #context query(selector(document).before(label("test"), inclusive: false))
]<test>

Since in this example, once we label “index.html” with the label <test>, the document occurs at the same location as the label. This is what the documentation means with

inclusive: Whether end itself should match or not. This is only relevant if both selectors match the same type of element. Defaults to true.

But I agree that it can be a little confusing. The fact that one needs to use .location() to go from an element to an identifier for that same element could also be elaborated on, since that is a “trick” that can be easily missed.

1 Like