What is the easiest way to horizontally visually flatten nested enum list?

I recently needed to copy this:

image

Which is semantically a nested enum list, but definitely doesn’t look nested. So I grabbed my par-like implementation mentioned in How to make bullet list item bodies flow like paragraphs? - #3 by Andrew, and massaged it a little and got this:

#set enum(full: true, numbering: (..n) => {
  numbering(("1.", "A.").at(n.pos().len() - 1), n.pos().last())
})

#show enum: en => {
  set par(spacing: 0.65em)
  let start = if en.start == auto {
    if en.children.first().has("number") {
      if en.reversed { en.children.first().number } else { 1 }
    } else {
      if en.reversed { en.children.len() } else { 1 }
    }
  } else {
    en.start
  }
  let number = start
  for (i, it) in en.children.enumerate() {
    number = if it.has("number") { it.number } else { number }
    if en.reversed { number = start - i }
    let parents = state("enum-parents", ())
    let indent = context h((parents.get().len() + 1) * en.indent)
    let num = if en.full {
      context numbering(en.numbering, ..parents.get(), number)
    } else {
      numbering(en.numbering, number)
    }
    let max-num = if en.full {
      context numbering(en.numbering, ..parents.get(), en.children.len())
    } else {
      numbering(en.numbering, en.children.len())
    }
    num = context box(width: measure(max-num).width, align(right, text(
      overhang: false,
      num,
    )))
    let body = {
      parents.update(arr => arr + (number,))
      it.body
      linebreak()
      parents.update(arr => arr.slice(0, -1))
    }
    if not en.reversed { number += 1 }
    context {
      num
      h(en.body-indent)
      body
    }
  }
}

So then I can write:

+ What is the primary purpose of machine learning according to the text?
  + To replace human decision-making
  + To analyze and extract useful information from large volumes of data
  + To create complex computer networks
  + To develop new storage technologies

image

But it’s 100% way too much for a simple show enum.where(level: 1): pad.with(left: ...).

I was able to strip it from stuff that I don’t use:

#show enum: en => {
  set par(spacing: 0.65em)
  let parents = state("enum-parents", ())
  for (i, it) in en.children.enumerate() {
    let number = i + 1
    let max-num = context {
      numbering(en.numbering, ..parents.get(), en.children.len())
    }
    let num = context {
      let num = numbering(en.numbering, ..parents.get(), number)
      num = align(right, text(overhang: false, num))
      box(width: measure(max-num).width, num)
    }
    let body = {
      parents.update(arr => arr + (number,))
      it.body
      linebreak()
      parents.update(arr => arr.slice(0, -1))
    }
    context num + h(en.body-indent) + body
  }
}

But it is still a re-implementation of the full show rule, though not even a full implementation.

I wonder if there is a magic one-liner for this, or maybe we really need to add level to enum.where or enum.item.where (for show enum.where(level: 1): pad.with(left: ...)), though the problem is that shifting only known from max numbering width + body-indent.

I’ve also noticed that both levels are not aligned, not sure if it will look better if they are aligned. But it’s not critical.

1 Like

Am I missing something obvious? Why not just do

#set enum(indent: -1.2em)

If we accept the indent, maybe it can be done like this, with measure to always get the right size. It seems to produce the right spacing - it seems to be correct, but I’m not sure if it always is.

#import "@preview/numbly:0.1.0": numbly
#set enum(full: true, numbering: numbly(
  "{1:1}.",
  "{2:A}.",
))

#show enum: it => {
  context {
    let size = measure((it.numbering)(1))
    set enum(indent: -size.width - it.body-indent)
    it
  }
}

+ What is the primary purpose of machine learning according to the text?
  + To replace human decision-making
  + To analyze and extract useful information from large volumes of data
  + To create complex computer networks
  + To develop new storage technologies

bild

(And also - the question if it’s a bug when that set enum thing works - that came up recently)

1 Like

Because it shifts the first level too. Also, I don’t see where -1.2em comes from.

Ohh, you are taking advantage of the fact that set rule can’t be applied to the outer enum, nice.

Although, if you apply a bunch of different numberings and more levels, then you will still have the shift appear.
#import "@preview/numbly:0.1.0": numbly
#set enum(full: true, numbering: numbly("{1:1}.", "{2:あ}.", "{3:A}.", "{4:A}."))
#show enum: it => context {
  let size = measure((it.numbering)(1))
  set enum(indent: -size.width - it.body-indent)
  it
}

#lorem(10)
+ #lorem(10)
  + #lorem(10)
  + #lorem(10)
  + #lorem(10)
  + #lorem(10)
    + #lorem(10)
    + #lorem(10)
    + #lorem(10)
    + #lorem(10)
    + #lorem(10)
    + #lorem(10)
      + #lorem(10)
      + #lorem(10)
      + #lorem(10)
+ #lorem(10)

image

So, the missing part is that you need to provide the “actual” numbers:

#show enum: it => {
  let c = state("enum level", 1)
  c.update(n => n + 1)
  context {
    // For level 2+.
    let size = measure((it.numbering)(..range(c.get()).map(_ => 1)))
    set enum(indent: -size.width - it.body-indent)
    it
  }
  c.update(n => n - 1)
}
Output

image

And even then you have some issues left. The obvious one is that this ignores margins that are supposed to be unreachable. But this feels like something that you have to choose on a case by case situation: is perfect vertical alignment is more important than going over the page margins.

This can be fixed by replacing 1 with 0.
#import "@preview/numbly:0.1.0": numbly
#set enum(full: true, numbering: numbly("{1:1}.", "{2:A}."))
#show enum: it => {
  let c = state("enum level", 0)
  c.update(n => n + 1)
  context {
    // For level 2+.
    let size = measure((it.numbering)(..range(c.get()).map(_ => 1)))
    set enum(indent: -size.width - it.body-indent)
    it
  }
  c.update(n => n - 1)
}

#lorem(10)
+ #lorem(10)
  + #lorem(10)
  + #lorem(10)
    + #lorem(10)
    + #lorem(10)
+ #lorem(10)

image

The other problem is that it’s not the actual actual numbers (Get final counter value for a specific "parent" counter state · Issue #6230 · typst/typst · GitHub):

#set enum(full: true, numbering: numbly("{1:1}.", "{2:あ}.", "{3:A}.", "{4:I}."))
Output

image

If Get final counter value for a specific "parent" counter state · Issue #6230 · typst/typst · GitHub was a thing, then the reimplementation can be skipped, while effectively still having access to the enum numbers, though reverse and start should also be considered:

#let el = state("enum level", 1)
#let ec = counter("enum counter")
#show enum.item: it => context ec.step(level: el.get()) + it
#show enum: it => {
  el.update(n => n + 1)
  context {
    // For level 2+.
    let final = ec.final(for: ec.get().slice(0, -1))
    let size = measure((it.numbering)(..final))
    set enum(indent: -size.width - it.body-indent)
    it
  }
  el.update(n => n - 1)
}

Although, if first and final display counter is not the widest, then you would have to use calc.max(..range(final.last()).map(measure...)).

I’m not yet sure if it will be implemented, so without it, my initial solution looks like the only proper way. And this one gets bigger and bigger, so sacrificing a little bit of lines for a less robust solution is questionable. But a 10-line solution is perfect for where thinnest and widest numbering are very close in width (which is most of common ones).

For 2 levels, it can be stripped down to your solution + , 1:

#show enum: it => context {
  let size = measure((it.numbering)(1, 1))
  set enum(indent: -size.width - it.body-indent)
  it
}

But again, this can go over page margins. Ideally, I think, a solution would move all levels to the same point, so that they are vertically aligned, but don’t go over margin. And this… will probably require doing the full implementation all over again.