Is it possible to have a function in set rule that are inside a show rule?

I try to automatically add a horizontal line after the table header if a header is present, so I write the following code:

#show table: it => {
	let contains-header = it.fields().children.first().func() == table.header
	set table(
		stroke: (x, y) => (
			top: if y == 0 or (contains-header and y == 1) { 1pt + red } else { none }
		)
	)
	it
}

This doesn’t have any effect. It’s weird because a lot of other similar rules work:

// OK
#show table: set table(stroke: 1pt + red)

// OK
#show table: set table(stroke: (x, y) => 1pt + red)

// OK
#show table: it => {
	set table(stroke: 1pt + red)
	it
}

// No effect
#show table: it => {
	set table(stroke: (x, y) => 1pt + red)
	it
}

Am I missing something?

Hello! Can you show us the result of your table on dummy data? I cannot reproduce your issue. The last show rule has an effect on my side.

Hello,
I created a minimal reproducible example in the web app, but I get the same result with Typst CLI version 0.13.1 (ed2106e2) as well:

https://typst.app/project/rGeZxbARvWHZ7USiNCY8Sq

Thank you! Taking a look at this, it looks like using a show-set rule for table does not update the style when the stroke parameter is given a function for some reason…
EDIT: specifically when the show rule is a function itself.
I think this should be considered a bug, see for creating one at issues · typst/typst

Another puzzling one is that set table(stroke: none) in the show rule does not have any effect, while stroke: 0pt has.

Thank you!
I created an issue: Set rule with function inside show-it is not applied · Issue #6218 · typst/typst · GitHub

This is exactly why (and not only this) show-set rule is preferred over show rules. They also have different semantics, so the actual issue here is why #show table: it => { set table(stroke: red); it } does work.

Basically, show-set rule is a filter-apply rule, but show rule is a filter-override/wrap rule.

#show heading: set heading(numbering: "1.") will apply heading rule for all headings, but #show heading: it => { set heading(numbering: "1."); it } will wrap all headings by adding set heading(numbering: "1.") to headings, but since you are already in a show rule (where heading is/can be constructed), it will not apply, because it’s too late for adding more styling for what you already have (it, which is immutable).

This is why you sometimes have to fiddle around with wrapping stuff in a specific way or reconstructing elements with additional behavior, dodging infinite show rule recursion.

So, in general, it is not possible to have an applied element set rule inside a show rule for the same element.

Hi Andrew, that makes sense.
My initial attempt to style the headers was meant to be a workaround until issue Unable to style `table.header` and `table.footer` · Issue #3640 · typst/typst · GitHub is resolved. So the answer that using a set rule inside a show rule isn’t valid is sufficient. Nevertheless, is there any solution how to style table headers from template without using something like my-table()?

How exactly do you need to style the table header? Have you looked at all the solutions in that issue?

I just need to apply fill, stroke, and strong to the header cells for all tables in the document that have a header. While it’s possible to achieve this with a custom function, it would require an additional import and adjustments to all existing tables, so I was looking for a solution to avoid this. Especially if it means that I have to revert all changes once issue #6218 is resolved.

Are all headers horizontal and use 1 row of cells?

By using the already available solution for strong, you can stretch it to cell fill and stroke:

#show table.cell.where(y: 0): strong
#show table.cell.where(y: 0): set block(fill: red, stroke: 2pt)

#table(columns: 2, table.header[a][b], ..range(10).map(str))

#table(columns: 3, table.header[a][b][c], ..range(10).map(str))

It already was closed.

All headers are horizontal, but some tables have multiple header rows and some tables have no header at all. That’s why I wanted the it in the show rule. Never mind, I just use custom my-table()

Outch, I mean #3640, not #6218. Sorry!

Why do you need a custom table function if all you need is a styled header?

Because I cannot use #show table.cell.where(y: 0): set ... for headers with multiple rows or for tables that don’t have a header at all.

The show rule is created with an assumption that it’s used for a specific table or that all tables have header as the first row.

#let table-header(..cells) = table.header(
  ..cells.pos().map(strong).map(table.cell.with(fill: red, stroke: 2pt)),
)

#table(columns: 2, table-header[a][b], ..range(10).map(str))

#table(columns: 3, table-header[a][b][c], ..range(10).map(str))

Here is a hack solution

  • We can create a new table in the show rule, from the old table
    • If we just do this, it recurses. So we mark the new table with a label so that it’s not done again
  • You can now apply custom styles to tables with headers. But it overwrites any even manually specified styles unfortunately

#show table: it => {
  let contains-header = it.fields().children.first().func() == table.header
  if not contains-header or (it.has("label") and it.label == <table-style-done>) {
    it
  } else {
    let new-style = (stroke: (x, y) => (
			top: if y == 0 or y == 1 { 1pt + red } else { none })
    )
    let fields = it.fields()
    let chld = fields.remove("children")
    // note, this overwrites any previous stroke configuration
    [#table(..fields, ..new-style, ..chld)<table-style-done>]
  }
}

#table(columns: 2, table.header[a][b], ..range(10).map(str))
#table(columns: 3, table.header[a][b][c], ..range(10).map(str))
#table(columns: 3, ..range(10).map(str))
picture

1 Like

Thanks bluss,
that’s a really nasty solution, but as workaround it’s exactly what I need.