Can I use show rule only in content of enum but not numbering?

Like if I have + Something1 and a rule #show "1": "2", can I let it show like 1. Something2?

No, unless you use regex, which isn’t perfect either. You’d have to reimplement show rule and inside add the show rule that only affects required parts. And enum is not of the easiest to re-implement. Ideally, you would just copy something from Add default typst implementation code where possible · Issue #5095 · typst/typst · GitHub, but there isn’t much there now.

I added Add default typst implementation code where possible · Issue #5095 · typst/typst · GitHub, so you can do this, but only for non-nested lists. Or add one layer more with another state hack, and it should just work.

#let enum-implementation(body-style: x => x, is-numbering: none, doc) = {
  let numbering-kind-from-char(c) = {
    let numberings = (
      "1",
      "a",
      "A",
      "i",
      "I",
      "α",
      "Α",
      "*",
      "א",
      "一",
      "壹",
      "あ",
      "い",
      "ア",
      "イ",
      "ㄱ",
      "가",
      "\u{0661}",
      "\u{06F1}",
      "\u{0967}",
      "\u{09E7}",
      "\u{0995}",
      "①",
      "⓵",
    )
    if c in numberings { c }
  }

  let numbering-pattern-from-str(pattern) = {
    let pieces = ()
    let handled = 0

    for (i, c) in pattern.codepoints().enumerate() {
      let kind = numbering-kind-from-char(c)
      if kind == none { continue }
      let prefix = pattern.slice(handled, i)
      pieces.push((prefix, kind))
      handled = c.len() + i
    }

    let suffix = pattern.slice(handled)
    if pieces.len() == 0 {
      panic("invalid numbering pattern")
    }
    (pieces: pieces, suffix: suffix, trimmed: false)
  }

  let apply-numbering(numbering, ..numbers) = {
    if numbering == none { return }
    std.numbering(numbering, ..numbers)
  }

  let apply-numbering-kth(numbering, k, number) = {
    let fmt = ""
    let self = numbering-pattern-from-str(numbering)
    if self.pieces.len() > 0 {
      let (prefix, _) = self.pieces.first()
      fmt += prefix
      let (_, kind) = if k < self.pieces.len() {
        self.pieces.at(k)
      } else {
        self.pieces.last()
      }
      fmt += apply-numbering(kind, number)
    }
    fmt += self.suffix
    fmt
  }

  let saturating-sub(this, rhs) = {
    assert(type(this) == int)
    calc.max(0, this - rhs)
  }

  let saturating-add(this, rhs) = {
    assert(type(this) == int)
    calc.min(18446744073709551615, this + rhs) // Conversion to float.
  }

  let unwrap-or-else(this, f) = if this != auto { this } else { f() }
  let unwrap-or(this, field, value) = {
    if field in this.fields() { this.at(field) } else { value }
  }

  let immediately-preceded-by-par = state("attach-v", false)

  let parents-in = state("enum-parents", ())

  /// Layout the enumeration.
  let layout-enum(elem) = context {
    let numbering = elem.numbering
    let reversed = elem.reversed
    let indent = elem.indent
    let body-indent = elem.body-indent
    let tight = elem.tight
    let gutter = unwrap-or-else(elem.spacing, () => {
      if tight { par.leading } else { par.spacing }
    })

    let cells = ()
    let number = unwrap-or-else(elem.start, () => {
      if reversed { elem.children.len() } else { 1 }
    })
    let parents = parents-in.get()

    let full = elem.full

    // Horizontally align based on the given respective parameter.
    // Vertically align to the top to avoid inheriting `horizon` or `bottom`
    // alignment from the context and having the number be displaced in
    // relation to the item it refers to.
    let number-align = elem.number-align

    for item in elem.children {
      number = unwrap-or(item, "number", number)

      let resolved = if full {
        let content = apply-numbering(numbering, ..parents, number)
        content
      } else {
        if type(numbering) == str {
          text(apply-numbering-kth(
            numbering,
            parents.len(),
            number,
          ))
        } else {
          apply-numbering(numbering, number)
        }
      }

      // Disable overhang as a workaround to end-aligned dots glitching
      // and decreasing spacing between numbers and items.
      let resolved = {
        set text(overhang: false)
        if is-numbering != none { is-numbering.update(true) }
        align(number-align, resolved)
        if is-numbering != none { is-numbering.update(false) }
      }

      // Text in wide enums shall always turn into paragraphs.
      let body = item.body
      if not tight {
        body += parbreak()
      }

      cells.push([])
      cells.push(resolved)
      cells.push([])
      cells.push({
        parents-in.update(arr => arr + (number,))
        immediately-preceded-by-par.update(false)
        body-style(body)
        parents-in.update(arr => (_ = arr.pop()) + arr)
      })
      number = if reversed {
        saturating-sub(number, 1)
      } else {
        saturating-add(number, 1)
      }
    }


    let grid = grid(
      columns: (indent, auto, body-indent, auto),
      row-gutter: gutter,
      ..cells
    )
    grid
  }

  let target = dictionary(std).at("target", default: () => "paged")
  let sequence = [].func()
  let styled = text(red)[].func()

  let v-space-hack(doc) = {
    let previous
    for child in doc.children {
      if child.func() == styled {
        child = styled(v-space-hack(child.child), child.styles)
      }
      if child.func() == enum.item {
        if child.body.func() in (sequence, styled) {
          let fields = child.fields()
          let body = fields.remove("body")
          child = enum.item(v-space-hack(body), ..fields)
        }
        if previous != none and previous.func() == parbreak {
          child = {
            immediately-preceded-by-par.update(true)
            child
            immediately-preceded-by-par.update(false)
          }
        }
      }
      child
      previous = child
    }
  }

  show enum: self => {
    let tight = self.tight

    context if target() == "html" {
      let elem = html.elem.with("ol")
      let attrs = (:)
      if self.reversed(styles) {
        attrs += ("reversed": "reversed")
      }
      if self.start != auto {
        attrs += ("start": str(self.start))
      }
      let body = sequence(..self.children.map(item => {
        let li = html.elem.with("li")
        let attrs = (:)
        if "number" in item.fields() {
          attrs += ("value", str(item.number))
        }
        // Text in wide enums shall always turn into paragraphs.
        let body = item.body
        if not tight {
          body += parbreak()
        }
        elem(attrs: attrs, body)
      }))
      elem(attrs: attrs, body)
    }

    let realized = layout-enum(self)

    context if tight and not immediately-preceded-by-par.get() {
      let spacing = unwrap-or-else(self.spacing, () => par.leading)
      v(spacing, weak: true)
    }

    realized
  }

  show: v-space-hack

  doc
}

#let is-numbering = state("is-numbering", false)

#show: enum-implementation.with(is-numbering: is-numbering, body-style: it => {
  show "1": it => context {
    if is-numbering.get() { return it }
    text(red, "2")
  }
  it
})

#lorem(20)1
+ #lorem(20)1
+ #lorem(20)1
  + #lorem(20)
    + #lorem(20)1
    + #lorem(20)1
+ #lorem(20)1

You can replace the enum item with a new item that applies the desired show rule in its body:

#show enum.item: it => {
  if it.has("label") and it.label == <processed> {
    return it
  }
  let new = enum.item(it.number, {
    show "1": "2"
    it.body
  })
  [#new<processed>]
}

+ Something1
4 Likes

I have also run into this before, but with list() instead. I was trying to make a presentation where sub-bullets had a smaller font and weight, but implementing that using show rules is not really possible atm.

I hope the feature gets implemented.

It is a good way, but if the enum is a sub of another object, it won’t work

Indeed… (but please post source code that I can copy-paste, instead of a screenshot)

You can use a state to disable the show rule in between the end of an item body and the start of the next one:

#let s = state("replace-2-3", false)

#show enum.item: it => {
  if it.has("label") and it.label == <processed> {
    return it
  }
  let new = enum.item(it.number, {
    show "2": it => context if s.get() { "3" } else { it }
    s.update(true)
    it.body
    s.update(false)
  })
  [#new<processed>]
}

+ Something2
+ Something2
  + Something2
  + Something2

(In this case the show rule could be placed at the top level but I guess it’s more performant to add it only in the scope of the enum item.)

#let s = state("replace-2-3", false)

#show enum.item: it => {
  if it.has("label") and it.label == <processed> {
    return it
  }
  let new = enum.item(it.number, {
    show "2": it => context if s.get() { "3" } else { it }
    s.update(true)
    it.body
    s.update(false)
  })
  [#new<processed>]
}

#set enum(reversed: true)
// #set enum(numbering: "I.")

+ Something2
+ Something2
  + Something2
  + Something2

Since numbering fixes this, I guess the show rule still affects the numbering. Which might be a bug.

1 Like

Strange…

#let s =state("replace-2-3", false)
#let en2ch_point_dict = ("\.":"3", "2":"3", "a":"b")
#show enum.item: it => {
  if it.has("label") and it.label == <processed> {
    return it
  }
  let new = enum.item(it.number, {
    let body = it.body
    for (pat, replacement) in en2ch_point_dict {
      body = {
        show regex(pat): it => context if s.get() { replacement } else { it }
        body
      }
    }
    s.update(true)
    body
    s.update(false)
  })
  [#new<processed>]
}


#set enum(numbering: "11a")
+ Something2
+ Something2
  + Something2
  + Something2
    + some2

It’s a bug in my code: the sub-enum is inside an item body, and the first marker (before the first sub-enum body) still has state s true. There’s an easy fix: also set state s false when starting a new enum:

#let s = state("replace-2-3", false)
#show enum: it => s.update(false) + it  // this is the fix
#show enum.item: it => {
  if it.has("label") and it.label == <processed> {
    return it
  }
  let new = enum.item(it.number, {
    show "2": it => context if s.get() { "3" } else { it }
    s.update(true)
    it.body
    s.update(false)
  })
  [#new<processed>]
}

+ Something2
+ Something2
  2. Something2
  + Something2

(Here I reproduce the bug by starting the sub-enum at number 2.)