How to make an inline title that's sticky when the body starts with a block?

I’m trying to write a function that normally generates a title + body in the same paragraph. But the body is received as argument and can be a block element. In that case the title won’t be inline anymore (that’s fine) but it can become an orphan:

#set page(width: 6cm, height: 3cm, margin: 1cm)
#let section(body) = [
  *Section:* #body
]

#section[Body]

#section(block[Body])

image

How can I make the “Section:” title sticky when the following content is a block?

I can fiddle with the internals of body to try and guess if it’s a block, but that wouldn’t be a robust solution. For example it would fail for #section(context block[Body]).

You can’t make an inline title if you use block. The only way to do this is to make the inline title not be inline, and instead be a sticky block, then you can do this. Or wrap the body in box and hope it will stay together “inline”.

#set page(width: 6cm, height: 3cm, margin: 1cm)

#show block: it => it + [#metadata(none)<block>]

#let section(body) = {
  context [#metadata(here())<start>]
  let title = [*Section:*]
  context {
    let start = query(selector(<start>).before(here())).last().value
    let end = query(selector(<end>).after(here())).first().value
    let blocks = query(selector(<block>).after(start).before(end))
    if blocks.len() == 0 { title } else { block(sticky: true, title) }
  }
  body
  context [#metadata(here())<end>]
}

#section[Body]

#section(block[Body])

#section(block[Body])

Or this kind of “inline”:

#set page(width: 6cm, height: 3cm, margin: 1cm, fill: gray)

#show block: it => it + [#metadata(none)<block>]

#let section(body) = {
  context [#metadata(here())<start>]
  let title = [*Section:*]
  context {
    let start = query(selector(<start>).before(here())).last().value
    let end = query(selector(<end>).after(here())).first().value
    let blocks = query(selector(<block>).after(start).before(end))
    if blocks.len() == 0 { title } else {
      let size = measure(title)
      block(sticky: true, below: 0pt, place(dx: -size.width - 0.3em, title))
    }
  }
  body
  context [#metadata(here())<end>]
}

#section[Body]

#section(block[Body])

#section(block[Body])

image

1 Like

I guess my question wasn’t clear, I’ve updated it: indeed the title is no longer inline if the next thing is a new block but that’s fine, I just want to avoid orphans.

Regarding your solution: nice idea! We can also restrict the block show rule to the section content and simplify the code a bit:

#set page(width: 6cm, height: 3cm, margin: 1cm)

 #let section(body) = {
   show block: it => it + [#metadata(none)<block>]
   [#metadata(none)<start>]
   let title = [*Section:*]
   context {
     let start = query(selector(<start>).before(here())).last().location()
     let end = query(selector(<end>).after(here())).first().location()
     let blocks = query(selector(<block>).after(start).before(end))
     if blocks.len() == 0 { title + " " } else { block(sticky: true, title) }
   }
   body
   [#metadata(none)<end>]
 }

but this doesn’t work if the body starts with inline content with a block coming later: in that case it’s wrongly treated like a block:

#section[Body #block[More body]]

image

Why you can’t use different section functions to remove the guessing? Or pass an argument like block: true?

The only other sane solution is to simplify guessing by using a helper function:

#set page(width: 6cm, height: 3cm, margin: 1cm, fill: gray)

#let inline(body) = (body,)
#let section(body, title: [*Section:*]) = {
  if type(body) == array { return title + h(1em) + body.first() }
  block(sticky: true, title)
  body
}

#section(inline[Body])

#section(block[Body])

#section(block[Body])

#section(inline[Body #block[More body]])

I’m writing a package that handles exercises (among other things). I don’t want users to have to deal with this kind of technical issues. They should be able to write #exercise[Body...] and have things just work.