Skip to content

Support for Typst (as .typ and Quarto's typst-pdf)#2137

Open
christopherkenny wants to merge 33 commits intorstudio:masterfrom
christopherkenny:master
Open

Support for Typst (as .typ and Quarto's typst-pdf)#2137
christopherkenny wants to merge 33 commits intorstudio:masterfrom
christopherkenny:master

Conversation

@christopherkenny
Copy link
Copy Markdown
Contributor

Summary

This PR adds full support for Typst-based tables, for both direct .typ outputs and for use in Quarto. Each feature is carefully designed to mimic the code style and heavy function use throughout the existing export types, particularly LaTeX, as the closest export engine.

I had planned to make a relatively minor PR a few weeks back, but I realized that this is best as a complete PR rather than a partially working one. So, my apologies for the very large PR.

It covers: normal table semantics, coloring, complete text styling (from font to color to stretch), lifting strokes to the column- or row- or table-level, lifting fill colors, exporting labels, etc. Tables can be saved to .typ and directly rendered. gt_group() is supported. It is tested for both raw Typst outputs and in Quarto.

Several features of this PR required some decision making, so below are the most important ones:

  • Titles and subtitles get stacked to keep them together.
  • Typst has separate figure and table concepts. A caption, title, and other related text require a figure. as_typst() allows for these to be configured, auto-detects if not supplied, and warns if a table is selected for something that would require a figure.
  • Typst uses show rules to allow for breakable tables. These are configurable in as_typst(). These work natively for Typst and don't require the ever-problematic long tables. If breakable is not set, then no Typst show rule is emitted, defaulting to the existing rules.
    • Making these general requires upstream Quarto changes, but these default to non-breakable tables.
  • Quarto captions and labels are delegated to Quarto. If they are supplied, they are ignored, as this is the current workflow for other export formats.
  • Repeated calls to the same object (e.g., changing font, color, and size) get mapped to the same text() call, since these are unified in Typst. This introduces a few paste0(..., collapse = ', ') that are not strictly necessary, but gives much more readable outputs.

For the overall style of the tables, I aimed to match existing exports, rather than Typst's defaults. The default table styling in Typst is a white, blank table with aligned text. I match LaTeX-for-gt's minimalistic styling here, with simple lines at the top of the table, bottom of the table, and below the header.

The main feature not implemented here is support for frac-style widths. These are very convenient ways to mix explicit widths and remainder fractions, but aren't necessary (though useful) and require making policy decisions for working with px, pct, etc in types without a corresponding frac idea, like rtf. I'm happy to do a follow-up with what I would expect this to look like, but the whole PR was getting big enough and this is nice-to-have-but-not-necessary.

Example Tables

Simple

Code
feature_df <-
  tibble::tribble(
    ~item        , ~plain  , ~markdown              , ~amount , ~score ,
    'alpha item' , 'alpha' , '**bold** text'        ,    1250 , 0.12   ,
    'beta item'  , 'beta'  , '_italic_ text'        ,    2575 , 0.45   ,
    'gamma item' , 'gamma' , '`code` text'          ,    3800 , 0.79   ,
    'delta item' , 'delta' , 'mix of **md** + `id`' ,    4450 , 0.97
  )
  
feature_df |>
  gt(rowname_col = 'item') |>
  cols_label(
    plain = 'Plain text',
    markdown = md('Markdown / `code`'),
    amount = 'Amount',
    score = 'Score'
  ) |>
  fmt_markdown(columns = markdown) |>
  fmt_currency(columns = amount, decimals = 0) |>
  fmt_percent(columns = score, decimals = 0) |>
  tab_header(
    title = md('Typst **figure** semantics'),
    subtitle = md('Header, caption, notes, and markdown-rich content')
  ) |>
  tab_caption(md(
    'A **caption** with `inline code` and escaped symbols like $100.'
  )) |>
  tab_footnote(
    footnote = md('A **footnote** attached to a body cell.'),
    locations = cells_body(columns = amount, rows = 2)
  ) |>
  tab_footnote(
    footnote = md('A footnote on a **column label**.'),
    locations = cells_column_labels(columns = markdown)
  ) |>
  tab_source_note(md('A _source note_ with `inline code`.'))
  
image

Complex

Code
exibble[
  1:5,
  c('row', 'group', 'num', 'char', 'fctr', 'time', 'datetime', 'currency')
] |>
  gt(rowname_col = 'row', groupname_col = 'group') |>
  tab_header(
    title = md('Comprehensive **Typst** table'),
    subtitle = md('Spanners, groups, summaries, fills, notes, and borders')
  ) |>
  tab_caption(md('A **caption** for the everything bagel table.')) |>
  tab_stubhead(label = 'stubhead') |>
  tab_spanner(
    label = md('*Textual*'),
    id = 'textual',
    columns = c(char, fctr)
  ) |>
  tab_spanner(
    label = 'Measures',
    id = 'measures',
    columns = c(num, currency)
  ) |>
  summary_rows(
    groups = 'grp_a',
    columns = c(num, currency),
    fns = list(
      list(label = 'Total', fn = 'sum'),
      list(label = 'Average', fn = 'mean')
    ),
    fmt = ~ fmt_number(., decimals = 1)
  ) |>
  grand_summary_rows(
    columns = c(num, currency),
    fns = list(list(label = 'Grand total', fn = 'sum')),
    fmt = ~ fmt_number(., decimals = 1)
  ) |>
  fmt_markdown(columns = char) |>
  tab_footnote(
    footnote = md('A **body** footnote.'),
    locations = cells_body(columns = time, rows = 3)
  ) |>
  tab_footnote(
    footnote = 'A spanner-adjacent label footnote.',
    locations = cells_column_labels(columns = fctr)
  ) |>
  tab_source_note(md('NOTES: _Styled_ source note.')) |>
  tab_style(
    style = list(
      cell_fill(color = '#DCE6F2'),
      cell_text(color = '#102A43', weight = 'bold')
    ),
    locations = cells_title(groups = 'title')
  ) |>
  tab_style(
    style = list(
      cell_fill(color = '#a8e4ad'),
      cell_text(color = 'white', weight = 'bold')
    ),
    locations = cells_column_labels()
  ) |>
  tab_style(
    style = cell_text(style = 'italic', color = '#6A1B9A'),
    locations = cells_column_spanners(spanners = 'textual')
  ) |>
  tab_style(
    style = list(
      cell_fill(color = '#FCE4EC'),
      cell_text(decorate = 'overline')
    ),
    locations = cells_stubhead()
  ) |>
  tab_style(
    style = list(
      cell_fill(color = '#E8F5E9'),
      cell_text(weight = 'bold')
    ),
    locations = cells_row_groups()
  ) |>
  tab_style(
    style = cell_text(
      size = px(18),
      decorate = 'underline',
      color = '#B71C1C',
      align = 'center'
    ),
    locations = cells_body(columns = time, rows = c(3, 4))
  ) |>
  tab_style(
    style = cell_text(indent = px(16), weight = 'bold'),
    locations = cells_body(columns = char, rows = c(2, 4, 5))
  ) |>
  tab_style(
    style = cell_text(size = 'x-small', color = '#0D47A1'),
    locations = cells_body(columns = datetime)
  ) |>
  tab_style(
    style = list(
      cell_text(color = 'white'),
      cell_fill(color = '#AA0000')
    ),
    locations = cells_stub()
  ) |>
  tab_style(
    style = list(
      cell_fill(color = '#ECEFF1'),
      cell_text(align = 'center', indent = px(8), weight = 700)
    ),
    locations = cells_source_notes()
  ) |>
  tab_style(
    style = cell_text(weight = 'bold', color = '#E65100'),
    locations = cells_footnotes()
  ) |>
  tab_style(
    style = cell_borders(sides = 'right', color = 'red', weight = px(2)),
    locations = cells_body(columns = num, rows = 1)
  ) |>
  tab_style(
    style = cell_borders(sides = 'left', color = 'blue', weight = px(3)),
    locations = cells_body(columns = char, rows = 1)
  )
file6d9cc046ea0

Related GitHub Issues and PRs

Checklist

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant