How to allow blocks to break only if their length is more than one page?

In my document, I want to avoid paragraphs that are both:

  1. Not starting at the top of a page, and
  2. Breaking across pages.

In other words, I want each paragraph to either:

  • Start at the top of a page and span as many pages as it needs, or
  • Appear anywhere on the page, but only if it fits entirely on that page without breaking.

Simply using show par: it => block(breakable: false)[#it], does not work because if the block takes more than one complete page, not the whole content will be displayed.

I tried the following:

#let should-break = state("should-break", false)
#let fit-on-page(content) = layout(size => {
  context {
    let current-y = here().position().y
    let remaining = size.height - current-y
    
    let measured = measure(content, width: size.width)
    should-break.update(x => measured.height > remaining) 
  }
})

#lorem(500)
#let a = lorem(350)
#fit-on-page(a)
#context {
  if should-break.get() {pagebreak()}
}
#a

But this:

  1. Did not always work, for some reason.
  2. Made me a warning: layout did not converge within 5 attempts

These are probably related. The warning means the compiler stopped after five times where the layout keeps changing, leading to you observing an “intermediate” result. It’s similar to the issue in this post:

In your case the problem is a bit more subtle; you don’t have get() → update() state dependence, but pagebreak() → remaining → update() dependence that cascades over multiple layout attempts.


I think I have something that works:

#show par: it => layout(size => {
  let height = measure(width: size.width, it).height
  let fitting = height < size.height
  it
  [#metadata(fitting)<fitting>]
})

#show par: it => context {
  let fitting = query(selector(<fitting>).after(here()))
    .at(0, default: none)
  if fitting == none {
    it
  } else if fitting.value {
    block(breakable: false, it)
  } else {
    pagebreak()
    it
  }
}

#set page(paper: "a7")

#lorem(30)

#lorem(30)

#lorem(30)

#lorem(50)

#lorem(90)

#lorem(30)

#lorem(30)

Basically instead of deciding for each paragraph at a specific position on the page whether it should break or not, I say:

  • if the paragraph is short enough to fit on a single page, it should be unbreakable
  • if it isn’t, only then I do a manual page break - that’s necessary whatever the position is - and allow breaking the paragraph across different pages.

Not having page breaks depend on the layout allows this to work without running into the dreaded “layout did not converge within 5 attempts” warning.

There are some strange details about this code:

  • I tried to do the same with state instead of metadata, but state.get() was off-by-one (it returned none for the first paragraph instead of the value that was set)
  • I also tried to avoid the state at all and do it in one show rule, but you can’t do pagebreak() inside layout(), it seems. but splitting it into two show rules works!
2 Likes

I got it working.

#show par: it => {
  let fitting = state("fitting")
  layout(size => {
    let par-height = measure(width: size.width, it).height
    fitting.update(par-height < size.height)
  })
  context {
    assert(type(fitting.get()) == bool)
    if fitting.get() {
      block(breakable: false, it)
    } else {
      pagebreak()
      it
    }
    // if not fitting.get() { pagebreak() }
    // place(dx: -2em, repr(fitting.get()))
    // if fitting.get() { block(breakable: false, it) } else { it }
  }
}

// #set page(paper: "a7", margin: 24.97pt)
#set page(paper: "a7")

#lorem(30)

#lorem(30)

#lorem(30)

#lorem(50)

#lorem(90)

#lorem(30)

#lorem(30)

I guess you can’t make this work, with this type of approach, without using context, so that’s that. But in a way it’s less hacky and more understandable?

Your show rule is wrapped in the context. The metadata that needs to be fetch is contained in the it. You haven’t placed it yet, but already requesting context. Hence, no metadata yet.

So it’s impossible to make it work in 2 parts, because you depend on metadata in it to put pagebreak before it, but to put it you need to first place it. Which is, I guess, fixed by using .after(here()).

pagebreak() can only be put in global scope, and is not allowed in containers (that are not page).

2 Likes

Ooh, I didn’t consider making the layout not the whole show rule… yes, it makes sense that this works and that it is different from two separate rules, where the second rule again contains the whole paragraph. Thanks!

1 Like

Amazing insights and ideas! Thank you everyone. It solved my problem.