How to distinguish between `table.cell.where(…)` and `grid.cell.where(…)` selectors?

Is there a way to inspect a selector?
To be specific, how to distinguish between table.cell.where(…) and grid.cell.where(…) selectors?

The selector type does not support .fields.
I’ve also tried repr, but table.cell and grid.cell give identical results.

#let t-1 = table.cell.where(x: 1)
#let g-1 = grid.cell.where(x: 1)
#let g-2 = grid.cell.where(x: 2)

#repr(t-1) == #repr(g-1)

#assert.ne(t-1, g-1)
#assert.eq(g-1, g-1)
#assert.ne(g-1, g-2)

图片

Is it even possible? Thanks in advance.

Background

I am developing a numbly-like typst package that simplies the use of selector.

  • Canonical Typst:

    #show (
      heading
        .where(level: 1)
        .or(heading.where(level: 2))
        .or(heading.where(level: 3))
    ): set align(center)
    
  • Canonical Typst + Programming:

    #show selector.or(
      ..(1, 2, 3).map(n => heading.where(level: n)),
    ): set align(center)
    
  • Typst + My package:

    #show sel(
      heading.where(level: any(1, 2, 3))
    ): set align(center)
    

I implement it by eval-regex-repr. I know this is a quadruple hack, but the scope is limited and we don’t put complicated things in where selectors, so I think it’s okay.

Prototype implementation

(will be MIT licensed)

#let any(..values) = {
  assert.eq(values.named(), (:))
  assert.ne(values.pos(), ())
  metadata(values.pos())
}

#let sel(target, debug: false) = {
  assert.eq(type(target), selector)
  let expr = repr(target)

  let matches = expr
    .matches(regex(`metadata\(value: \((.*?)\)\)`.text))
    .map(
      m => (
        any: m.text,
        values: m.captures.first().split(", "),
      ),
    )

  // Replace each `any` by any of its possible `values`
  let expanded = matches.fold(
    (expr,),
    (expanded, (any, values)) => values
      .map(v => expanded.map(e => e.replace(any, v, count: 1)))
      .flatten(),
  )

  if debug { expanded } else {
    selector.or(..expanded.map(eval))
  }
}

#sel(heading.where(level: any(1, 2, 3), numbering: any("1.1", "A.1")))

#show sel(heading.where(level: any(..range(1, 4)))): set text(red)
= 甲
== 乙
=== 丙
==== 丁

#show sel(figure.where(kind: any(table, "atom"))): set figure.caption(
  position: top,
)
#figure(table[Table], caption: [caption])
#figure(circle(), caption: [caption], kind: "atom", supplement: "Atom")
#figure(rect[Image], caption: [caption])

Update: Submitted in sela:0.1.0 by YDX-2147483647 · Pull Request #3167 · typst/packages · GitHub.

1 Like

You’ve already answered your specific question. Use explicit selectors. This is exactly the same thing I use to compare element functions. It’s explicit and don’t require hacking with unstable repr and .has() or whatever. Inspect…since there are no fields, you can’t. You can only rely on repr for that.

Regarding repr, it might be possible to change Typst to print table.cell instead of cell, same for lists.

I’m also very surprised you can pass any value to where clause, and it will just work, even if level can’t be a string or whatever. Might be a missing assert.

Thank you!
Considering that show grid.cell is rare and discouraged, I plan to interpret cell as table.cell by default, and provide an option to switch to grid.cell.

Typst will give a pleasant error message in that case, saying Expected integer, found string and pointing out where the string comes from.
I think just letting the typst compiler handle these errors is better.

It literally says nothing:

#show heading.where(level: "this"): set align(center)
= Heading

Oh sorry, I was testing "5" for range

table.cell.where(y: any(..range(1, "5")))

I’ll include this caveat in my package’s README.

While you’re at it, maybe make an issue about this as well. Although for your package to work, you’d rely on Typst not giving errors for invalid type in where clause.

I won’t create the issue because it’s hard to fix and fixing it might affect the performance (as in a related PR and issue).
At present (586b04948), where selectors are (Element, Option<SmallVec<[(u8, Value); 1]>>) and generated by a macro. They do not carry type information for the fields.

In fact that’s why I estimate the hack will survive in Typst v0.14 and plan to publish it as a package.

(I do not object if you or anyone create the issue, though)

Estimating how long the bug will live to have enough time to make a new package useful, is next level. Ability to pass any value type to type-limited field in where clause · Issue #6991 · typst/typst · GitHub

Update: The package is now available on the Typst Universe.

#import "@preview/sela:0.1.0": sel, any
#show sel(
  heading.where(level: any(1, 2, 3)),
): set align(center)
sel(
  table.cell.where(x: any(1, 2), y: any(..range(1, 5))),
)
sel(
  grid.cell.where(x: any(1, 2), y: any(..range(1, 5))),
  scope: (cell: grid.cell),
)
3 Likes