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
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.
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.
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.)