The difference between `context f(..)` and `f(context {..})` in lists?

In the following code, I step the counter and retrieve its value in a loop.
I used three methods to call n.get(), but only the first method (a.) gives the correct result.
(The other two always give one, as if I only stepped once.)
Why?

#set enum(numbering: ..) // Use a fancy format for readability

#let n = counter("n")
#for _ in "repeat" {
  n.step()

  context enum.item[#n.get()] // a.
  enum.item[#context n.get()] // b.
  context enum.item[#context n.get()] // c.
}

Full code
#set page(height: auto, width: auto, margin: 1em)

#set enum(numbering: n => if calc.rem(n, 3) == 1 {
  numbering("1", calc.ceil(n / 3)) + "a."
} else {
  numbering("a.", calc.rem(n - 1, 3) + 1)
})

#let n = counter("n")
#for _ in "repeat" {
  n.step()

  context enum.item[#n.get()]
  enum.item[#context n.get()]
  context enum.item[#context n.get()]
}

My attempt

Replace enum.item with box or rect

Suddenly all methods work as expected.
图片

Wrap the methods

If I wrap the three methods in an enum({..}), then all n.get() are corrected too.

#set enum(numbering: "1a.")

#let n = counter("n")
#for _ in "repeat" {
  n.step()

  enum({
    context enum.item[#n.get()]
    enum.item[#context n.get()]
    context enum.item[#context n.get()]
  })
}

The same thing also happens to grid.

#let n = counter("n")
#for _ in "repeat" {
  n.step()

  grid(
    columns: 3,
    context enum.item[#n.get()],
    enum.item[#context n.get()],
    context enum.item[#context n.get()],
  )
}

However, if I wrap with enum.item({..}) instead, they all become one.

#set enum(numbering: "1a.")

#let n = counter("n")
#for _ in "repeat" {
  n.step()

  enum.item({
    context enum.item[#n.get()]
    enum.item[#context n.get()]
    context enum.item[#context n.get()]
  })
}

Final: #context n.get()

Wrapping with context enum.item({..}), enum.item(context {..}), context enum.item(context {..}), or similar variants of list.item makes no difference.

2 Likes

Looks like a bug with list items. A minimal example:

#let n = counter("n")

#n.step() #list.item(context n.get())
#n.step() #list.item(context n.get())

Or

#n.step()
- #context n.get()
#n.step() 
- #context n.get()

image

Maybe this has to do with the special treatment of list items, where consecutive items are implicitly gathered to form a list.

3 Likes

tl;dr: looks like a bug, but the workaround is to put the steps into your list items:

enum.item[#n.step()#context n.get()]

It seems to have to do with invisible content between list items. Compare:

#let n = counter("n")

#n.step()
- #context n.get()  // (1,)
#n.step() 
- #context n.get()  // (1,)

and

#let n = counter("n")

- #n.step()#context n.get()  // (1,)
- #n.step()#context n.get()  // (2,)

State works the same; here’s metadata:

#let meta-before() = query(selector(<a>).before(here()))

#metadata(none)<a>
- #context meta-before().len()  // 1
#metadata(none)<a>
- #context meta-before().len()  // 1

vs

#let meta-before() = query(selector(<a>).before(here()))


- #metadata(none)<a>#context meta-before().len()  // 1
- #metadata(none)<a>#context meta-before().len()  // 2

I think the first invisible content is simply before the list; the rest is pushed down after the list, and only affects subsequent content.

I’m not sure how putting a list item inside context makes this work correctly though.

3 Likes

Thank you!

I edit SillyFreak’s code and add here().position().y.

#let here-y() = here().position().y
#let meta-before() = {
  query(selector(metadata).before(here())).map(m => m.value)
  [ \+ ]
  here-y()
}
#metadata(1)
+ #context meta-before()           #h(1fr) #context here-y()
#metadata(2)
+ #context meta-before()           #h(1fr) #context here-y()
#metadata(3)
#context [+ #meta-before()         #h(1em) #here-y() / #context here-y()]
#metadata(4)
#context [+ #context meta-before() #h(1fr) #here-y() / #context here-y()]
#metadata(5)
+ #context meta-before()           #h(1fr) #context here-y()

Final: #context meta-before()
Full real code
#let here-y() = here().position().y
#let raw-repr(value) = raw(repr(value), lang: "typc")
#let meta-before() = {
  raw-repr(query(selector(metadata).before(here())).map(m => m.value))
  [ \+ ]
  raw-repr(here-y())
}

#set page(
  height: auto,
  width: auto,
  margin: (x: 1em, y: 20pt),
  background: align(top, {
    let stripe = block(
      width: 100%,
      height: 20pt,
      fill: green.transparentize(80%),
    )
    v(20pt)
    set par(spacing: 20pt)
    ((stripe,) * 3).join()
  }),
)
#set text(size: 10pt, top-edge: "baseline", bottom-edge: "baseline")
#set par(leading: 20pt, spacing: 20pt)

#metadata(1)
+ #context meta-before() #h(1fr) #context here-y()
#metadata(2)
+ #context meta-before() #h(1fr) #context here-y()
#metadata(3)
#context [+ #meta-before() #h(1em) #here-y() / #context here-y()]
#metadata(4)
#context [+ #context meta-before() #h(1fr) #here-y() / #context here-y()]
#metadata(5)
+ #context meta-before() #h(1fr) #context here-y()

Final: #context meta-before()

#set text(top-edge: "cap-height")
#figure(
  table(
    columns: 2,
    stroke: none,
    align: (center, start),
    table.header(
      ..("metadata", "locate(..).position().y").map(raw.with(lang: "typc"))
    ),
    table.hline(),
    ..range(1, 6)
      .map(v => {
        (
          raw-repr(v),
          context locate(metadata.where(value: v)).position().y,
        )
      })
      .flatten(),
  ),
)

Here’s the result. (The heights of all stripes are 20pt.)

Invisible metadata between list items is indeed pushed down because there is nowhere else to put it, given that the user receives the individual items as an array for use in a list show rule.

1 Like

Thank you for the explanation. I understand this part of the behavior, but I still don’t know how it interacts with context.

For example, why the two calls of here-y in #context [+ … #here-y() / #context here-y()] give different results?

Context blocks have tags, just like metadata and such. The cause is now that the invisible tags of the context block itself also get pushed past the list item generated by itself, so that in essence its interior comes before itself in Typst’s logical order.

Here that same behaviour kind of saves us because all of the updates and the context block tags get pushed down together while keeping their relative order.


In total, I agree that the behaviour is buggy, but I also don’t see a way to fix it currently. Pushing the tags down was the best I could come up with at the time.

2 Likes