How to make (justified) list items fill the entire width?

If I use justified text, I want text inside lists to be also jsutifed to the same width as the main text. However, sometimes Typst makes the list item narrower:

#set page(width: 8cm, margin: 1cm)
#set par(justify: true)
#place(right, line(angle: 90deg, stroke: 0.5pt + luma(60%), length: 10cm))
#lorem(20)
- #lorem(20)
But
- This doesn’t fill the line fully even though it has a linebreak.

If I add a #h(1fr) after linebreak., this fixes it:
image

But I don’t want to manually check each list. Is there a way to achieve this via a show or set rule?

(If you look closely, you can see that the first list item in my example is also too narrow, notice the small gap after the “et” and that the dashes protrude slightly less into the margin)

1 Like

It’s possible to do this with a show rule:

#show list.item: it => list.item(it.body + h(1fr))

But this will give the error “Maximum show rule depth exceeded”, which means it recurses infinitely.

To get around that, I have borrowed a function written by sijo in this thread: Here is a recursion blocking function - can we extend it to block by element identity?

Full code
#set page(width: 8cm, margin: 1cm)
#set par(justify: true)
#place(right, line(angle: 90deg, stroke: 0.5pt + luma(60%), length: 10cm))


#let append-to-marker(mark, body) = {
  let arr = ()
  if type(mark) == content and mark.func() == metadata {
    arr = mark.value
  }
  arr.push(body)
  metadata(arr)
}

#let contained-in-marker(mark, body) = {
  let arr = ()
  if type(mark) == content and mark.func() == metadata {
    arr = mark.value
  }
  body in arr
}

/// Apply a style function, like for a show rule, but block recursive application of it
///
/// - markerlabel (label): unique label for this style function
/// - func (function): the style function; signature function(content) -> content
/// - body (content): the style function argument
/// - allow-nesting (bool): allow nesting of the same show rule, after having
///   blocked repeated calls on the same element once. If you say false here,
///   then no recursion at all is allowed (only outermost table of nested
///   tables are affected by a table rule for example). If you say true here,
///   nested tables are affected in this example.
#let recursion-block(markerlabel, func, body, allow-nesting: false) = {
  let sentinel = [#str(markerlabel)]
  context {
    let orig-title = bibliography.title
    [#context {
      let ctx-title = bibliography.title
      set bibliography(title: orig-title)
      if ctx-title != sentinel and (not allow-nesting or not contained-in-marker(ctx-title, body)) {
        // set simple recursion marker, just the sentinel
        show markerlabel: set bibliography(title: sentinel)
        func(body)
      } else {
        // we arrived back at something we have seen before
        // store the whole element in the marker this time
        // this guards against global reentrancy (if we have multiple instances of the same rule)
        // note: we can store `body` because it comes directly from the document.
        // storing `func(body)` from above would match nothing we see again - the document element that
        // results from `func(body)` is not exactly equal to that.
        let marker = append-to-marker(ctx-title, body)
        show markerlabel: set bibliography(title: marker) if allow-nesting
        body
      }
    }#markerlabel]
  }
}

#show list.item: recursion-block.with(<__list_recursion>, it => {
  list.item(it.body + h(1fr))
})

#lorem(20)
- #lorem(20)
But
- This doesn’t fill the line fully even though it has a linebreak.

This is Paragraph justification sometimes doesn't use the full page width · Issue #5472 · typst/typst · GitHub. Add #h(1fr) to any list item. Although sometimes, it doesn’t help.

This is too overcomplicated for what it does. You can do this in 3 neat lines:

#show list.item: it => {
  let children = it.body.at("children", default: ())
  if children.at(-1, default: none) == h(1fr) { return it }
  list.item(it.body + h(1fr))
}

The it.body.at("children", default: ()) idea is from How to avoid numbering subfigure captions when using legal numbering?.

Or in 2 with yet another <processed> hack:

#show list.item: it => {
  if it.has("label") and it.label == <processed> { return it }
  [#list.item(it.body + h(1fr))<processed>]
}

Since you can’t reference list items by default, it’s not a big deal, especially if labels are not used for lists.

1 Like

Thanks, the first option works perfectly!

1 Like