How to avoid nested enum items breaking at the end of the page?

Hi all. I’m trying to create a simple multiple choice test. For the sake of readability, I want to have a question and all its choices in the same page, meaning that if a choice doesn’t fit in the current page the whole question is moved to the next.

To achieve it I’m using nested enums like this

Code
#import "@preview/numbly:0.1.0": numbly


#set page(height: 7cm)
#set enum(numbering: numbly("{1}.", "{2:A})"), full: true)
#show enum.item: set block(breakable: false)

+ Some question
  
  + First answer
  
  + Second answer
  
  + Third answer
  
  + Fourth answer


+ Another question
  
  + First answer
  
  + Second answer
  
  + Third answer
  
  + Fourth answer
Result

nested-enums-1
nested-enums-2

As you can see, the question stays in the same page, but the choices go to the next. I tried to wrap every enum.item in a block, but it breaks the enum counter and instead of fixing that I just prefer to call a pagebreak manually.

Any help would be appreciated!

You were already on the right track to try to #show enum.item: set block(breakable: false) – this would, if enum items were implicitly wrapped in a block, make that block non-breakable. This works e.g. for tables. Enum items are not implicitly blocks, however (at least it seems so).

Adding a block within the show rule doesn’t work because it resets numbering, as you have already noticed:

#show enum.item: block.with(breakable: false)

The trick is to not wrap the enum item in a block, but the enum item’s body. You can inspect your current content by wrapping it in repr() and you’ll see that an enum item looks e.g. like this:

item(
  body: sequence(
    [Some question],
    parbreak(),
    item(body: [First answer]),
    // etc.
  ),
)

So if the body was an unbreakable block, this would work. However, the naive way of doing that also leads to a problem:

#show enum.item: it => {
  let (number, body, ..fields) = it.fields()
  body = block(breakable: false, body)
  enum.item(number, body, ..fields)
}

maximum show rule depth exceeded

Each show rule application creates a new enum item, on which the show rule is applied again… With one extra line to return the original item if it’s already the result of this show rule, it works:

#show enum.item: it => {
  let (number, body, ..fields) = it.fields()
  if body.func() == block { return it }

  body = block(breakable: false, body)
  enum.item(number, body, ..fields)
}
2 Likes

Thank you! I thought about recreating the enum but believed it was going to be harder. This works as expected.