How to access the text of the current section (before the next heading)?

I am trying to get all text between two headings (including the first heading, but not the second).

= first section

some
text _and_ 
more

= second section

test

I would like to be able to creating something like:

#show section-selector: it =>[
  heading is: “#box(it.heading)” -- and content is “#box(it.content)”
]

Which would be called two times, the first one should print (with the orginal formatting)

heading is: “first section” – and content is “some text and more”

And the second

heading is: “second section” – and content is “test”


So far, the best I’ve done is to:

  • emulate “match all content” by “match all level 3 headings”
  • match the header + the content by using a #show on the heading, but of course the “content” (here the level 3 heading) is printed two times (1 in my function, and 1 “normally”
// It should apply on the heading + the content, not just the content
// Alternatively I could also add a second #show rule to hide the content
#show heading.where(level: 1): it => {
  let all-text = heading.where(level: 3); // I don’t know how to do this
  let content = all-text.after(here())

  let next-headings = query(heading.where(level: 1).after(here()))
  if next-headings.len() > 1 {
    content = content.before(next-headings.at(1).location())
  }
  
  [ #box[#it] --> content is :  >> #box[#query(content).join()] << ]
}

= first section

=== some
=== text _and_ 
=== more

= second section

=== test

Which prints:

1 Like

I can’t answer your original question, but I have an alternative way to process all texts in a section.

The definitions can be found at clreq/typ/packages/till-next.typ at 0ac02155bde21c8fd9e91badb772b5591c2afbc7 · typst-doc-cn/clreq · GitHub, and I’ll only introduce the usage below. I’m willing to provide further info if you find it applicable.

  1. Define a wrapper function that receives all texts in a section and returns the content to display.

  2. Add #show: mark-till-next to the start of the document. This line does nothing until the next step.

  3. In every section that you want to process, put a #till-next(wrapper) after the heading.

Example input:

#show: mark-till-next

= Heading
A

#till-next(wrapper)
// You can also `#let foo = till-next(wrapper)` and put `#foo` here.

B

C

= The next heading

D

Example result:

= Heading
A

#wrapper[
  B

  C
]

= The next heading

D

Usage in practice: I define a wrapper function putting contents in a collapsible block, and use it for deprecated sections. The result is like this:

Compared to the introspective way you suggested, this method requires more markup to write, but it converges immediately (no context required), and is easier to skip certain texts.

2 Likes

@Y.D.X First of all, thanks a lot, and secondly sorry to not have responded earlier.

You did nudge me in the right direction. Here is the solution to my original question:

#let is-heading(it) = {
  it.func() == heading
}

/// A show rule that wrap everything between two headings of the same level
///
/// Usage: ```
/// #show: wrapp-section.with(
///   wrapper: it => rect(it),
///   include-heading-in-wrapper: false, // optional
///   depth: 1, // optional
/// )
/// ```
///
/// Note: `#set` rules (like `#set text(lang: "fr")`) must be declared before calling `#show: wrapp-section`
#let wrapp-section(
  body,
  wrapper: none,
  depth: 1,
  include-heading-in-wrapper: false,
) = {
  // The wrapper function
  let fn = none
  // The fenced elements to be wrapped
  let fenced = ()

  for it in body.children {
    let x = it.func();
    
    if (is-heading(it) and it.fields().at("depth") < depth) {
    if fn != none {
      // Complete the last fence
      fn(fenced.join())
      fn = none
      fenced = ()
    }
      it
    } else if is-heading(it) and it.fields().at("depth") == depth {
    if fn != none {
      // Complete the last fence
      fn(fenced.join())
      fn = none
      fenced = ()
    }

      // Print the heading itself and start a new fence
      if include-heading-in-wrapper {
        fn = wrapper
        fenced.push(it)
      } else {
        it
        fn = wrapper
      }
    } else if fn != none {
      // Continue the fence
      fenced.push(it)
    } else {
      it // if not in any fence
    }
  }

  // Complete the last fence
  if fn != none {
    fn(fenced.join())
  }
}

And a usage example:

#set text(lang: "en")

#show: wrapp-section.with(
  wrapper: it => rect(it),
  include-heading-in-wrapper: true,
  depth: 2,
)

= heading 1 - a

== heading 2 - a

some
text _and_ 
more

=== heading 3 - a

stuff

== heading 2 - b
test

Which generates:


The only thing I would like to improve would be to factorize the repeated block:

if fn != none {
  // Complete the last fence
  fn(fenced.join())
  fn = none
  fenced = ()
}
2 Likes

Use typc for code mode blocks.

1 Like

A slightly better version, that pass both the heading and the content to a single function:

#let is-heading(it) = {
  it.func() == heading
}

/// A show rule that wrap everything between two headings of the same level
///
/// Usage: ```
/// #show: wrapp-section.with(
///   depth: 1, // optional
///   wrapper: (heading: none, section: none) => {
///    rect[
///      heading: #box[#rect[#heading]]
///      
///      section: #box[#rect[#section]]
///    ]
///  }
/// )
/// ```
///
/// Note: `#set` rules (like `#set text(lang: "fr")`) must be declared before calling `#show: wrapp-section`
#let wrapp-section(
  body,
  depth: 1,
  wrapper: none,
) = {
  // The heading of the current section
  let heading = none
  // The content of the current section
  let section = ()

  for it in body.children {
    let x = it.func();
    
    if (is-heading(it) and it.fields().at("depth") < depth) {
      if heading != none {
        // Complete the last section
        wrapper(heading: heading, section: section.join())
        heading = none
        section = ()
      }
      it
    } else if is-heading(it) and it.fields().at("depth") == depth {
      if heading != none {
        // Complete the last section
        wrapper(heading: heading, section: section.join())
        heading = none
        section = ()
      }

      heading = it
    } else if heading != none {
      // Continue the current section
      section.push(it)
    } else {
      it // if not in any section (before the first heading of the appropriate depth)
    }
  }

  // Complete the last section
  if heading != none {
    wrapper(heading: heading, section: section.join())
  }
}

The same example as above:

#set text(lang: "en")

#show: wrapp-section.with(
  depth: 2,
  wrapper: (heading: none, section: none) => {
    heading
    rect(section)
  }
)


= heading 1 - a

== heading 2 - a

some
text _and_ 
more

=== heading 3 - a

stuff

== heading 2 - b
test

And a more interesting example that demonstrate how manipulate both the heading and the content of the section level you want to format:

#set text(lang: "en")

#show: wrapp-section.with(
  depth: 2,
  wrapper: (heading: none, section: none) => {
    rect[
      heading: #box[#rect[#heading]]
      
      section: #box[#rect[#section]]
    ]
  }
)


= heading 1 - a

== heading 2 - a

some
text _and_ 
more

=== heading 3 - a

stuff

== heading 2 - b
test


= heading 1 - c

== heading 2 - c

some
text _and_ 
more

=== heading 3 - c

stuff

== heading 2 - d
test

2 Likes