From 9c5f66c55cbb0099488ede3eaeec94e65f843c0f Mon Sep 17 00:00:00 2001 From: EinMaulwurf Date: Wed, 8 Jul 2026 14:45:12 +0000 Subject: [PATCH] fix(typst): deduplicate cell-level line entries in hlines/vlines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typst_hlines() and typst_vlines() emitted fragmented chains of table.hline()/table.vline() segments covering the same span whenever @style_lines contained duplicate (i, j, line, color, width) tuples. These duplicates arise routinely: theme_tinytable() adds style_tt(i = 0, line = "b", ...) and so does the user; the lazy style system records both calls and expands them to identical per-cell entries in @style_lines. typst_split_chunks() then saw phantom breaks in diff(x) != 1 and produced one chunk per duplicated j value. Minimal repro before the fix: tt(mtcars[1:5, 1:5]) |> style_tt(i = 0, line = "b", line_color = "black", line_width = 0.05) |> save_tt("typst") emitted 6 fragmented table.hline(y: 1, ...) segments covering sub-ranges of [0, 5). After the fix, it emits a single table.hline(y: 1, start: 0, end: 5, ...). Also visible in test-typst.R's typst-issue592.typ snapshot, where the x:0 vline was previously split into 5 overlapping fragments (0-1, 0-2, 1-7, 6-12, 11-16). The snapshot is regenerated to reflect the correct single-segment output. Fix: deduplicate identical rows of k inside the lapply() bodies of typst_hlines() and typst_vlines(), after the split() grouping. Other backends (html, latex/tabularray, markdown/grid) are unaffected — they either aggregate per cell (html) or build ranges that absorb duplicates (tabularray). Tests: - Regenerate inst/tinytest/_tinysnapshot/typst-issue592.typ - Add regression tests in inst/tinytest/test-bugfix.R covering both "user re-declares theme line" and "user adds the same line twice" --- R/typst_style.R | 10 ++++++ .../tinytest/_tinysnapshot/typst-issue592.typ | 8 ++--- inst/tinytest/test-bugfix.R | 33 +++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/R/typst_style.R b/R/typst_style.R index 53bcf98f0..0310396d6 100644 --- a/R/typst_style.R +++ b/R/typst_style.R @@ -240,6 +240,14 @@ typst_hlines <- function(x, lin) { tmp <- split(lin, list(lin$i, lin$line, lin$line_color_mapped, lin$line_width)) tmp <- Filter(function(x) nrow(x) > 0, tmp) tmp <- lapply(tmp, function(k) { + # Drop duplicate cell-level entries. These arise routinely whenever + # theme_tinytable() and a user style_tt() declare the same line (same + # i, j, line, color, width, trim). Without deduplication, + # typst_split_chunks() sees phantom breaks in diff(x) != 1 and emits a + # chain of fragmented table.hline()/table.vline() segments that cover + # the same span as a single line. See test-typst-line-dedup.R. + k <- k[!duplicated(k), , drop = FALSE] + # Split chunks based on consecutive columns AND line_trim boundaries # line_trim marks group boundaries - split chunks at these points chunks <- typst_split_chunks(k$j) @@ -462,6 +470,8 @@ typst_vlines <- function(x, lin) { lin <- split(lin, list(lin$j, lin$line, lin$line_color_mapped, lin$line_width)) lin <- Filter(function(x) nrow(x) > 0, lin) lin <- lapply(lin, function(k) { + # Drop duplicate cell-level entries; see typst_hlines() for rationale. + k <- k[!duplicated(k), , drop = FALSE] ymin <- typst_split_chunks(k$i)$min ymax <- typst_split_chunks(k$i)$max xmin <- k$j[1] diff --git a/inst/tinytest/_tinysnapshot/typst-issue592.typ b/inst/tinytest/_tinysnapshot/typst-issue592.typ index ba40cf261..b4d19daec 100644 --- a/inst/tinytest/_tinysnapshot/typst-issue592.typ +++ b/inst/tinytest/_tinysnapshot/typst-issue592.typ @@ -78,11 +78,7 @@ block[ // start block table.hline(y: 14, start: 1, end: 2, stroke: 0.1em + black), table.hline(y: 14, start: 3, end: 7, stroke: 0.1em + black), table.hline(y: 15, start: 1, end: 2, stroke: 0.1em + black), table.hline(y: 15, start: 3, end: 7, stroke: 0.1em + black), table.hline(y: 16, start: 2, end: 3, stroke: 0.1em + black), - table.vline(x: 0, start: 0, end: 1, stroke: 0.1em + black), - table.vline(x: 0, start: 0, end: 2, stroke: 0.1em + black), - table.vline(x: 0, start: 1, end: 7, stroke: 0.1em + black), - table.vline(x: 0, start: 6, end: 12, stroke: 0.1em + black), - table.vline(x: 0, start: 11, end: 16, stroke: 0.1em + black), + table.vline(x: 0, start: 0, end: 16, stroke: 0.1em + black), table.vline(x: 1, start: 0, end: 16, stroke: 0.1em + black), table.vline(x: 2, start: 0, end: 1, stroke: 0.1em + black), table.vline(x: 3, start: 0, end: 1, stroke: 0.1em + black), @@ -129,4 +125,4 @@ table.cell(rowspan: 3)[3], [x], [80.9%], [78.4%], [ ], [55.6%], // tinytable align-figure after ] // end block -) // end figure +) // end figure \ No newline at end of file diff --git a/inst/tinytest/test-bugfix.R b/inst/tinytest/test-bugfix.R index 7478049b3..907556306 100644 --- a/inst/tinytest/test-bugfix.R +++ b/inst/tinytest/test-bugfix.R @@ -58,3 +58,36 @@ local({ expect_true(grepl("1,23", typst, fixed = TRUE)) expect_true(grepl("1,23", tabulator, fixed = TRUE)) }) + + +# Regression test: typst_hlines/typst_vlines used to emit fragmented chains of +# table.hline()/table.vline() segments covering the same span whenever +# theme_tinytable() and the user declared the same line (or any line was +# declared twice with identical properties). This produced redundant, ugly +# output and slowed down rendering. The fix deduplicates cell-level entries +# inside the typst line processors. + +# Case 1: user re-declares the theme's middle "below colnames" line. +# Before the fix this produced 6 fragmented hline segments at y=1. +tab <- tt(mtcars[1:5, 1:5]) |> + style_tt(i = 0, line = "b", line_color = "black", line_width = 0.05) +out <- save_tt(tab, "typst") +hlines <- strsplit(out, "\n", fixed = TRUE)[[1]] +hlines <- hlines[grepl("table.hline", hlines, fixed = TRUE)] +middle <- hlines[grepl("y: 1,", hlines, fixed = TRUE)] +expect_true(length(middle) >= 1) +# The whole row should contain exactly one table.hline(y: 1, ...) segment +n_middle_segments <- lengths(regmatches(middle, gregexpr("table[.]hline", middle))) +expect_true(n_middle_segments == 1) + +# Case 2: user adds the same line twice -> should still produce a single segment +tab <- tt(mtcars[1:5, 1:5]) |> + style_tt(i = 2, line = "b", line_color = "pink") |> + style_tt(i = 2, line = "b", line_color = "pink") +out <- save_tt(tab, "typst") +hlines <- strsplit(out, "\n", fixed = TRUE)[[1]] +hlines <- hlines[grepl("table.hline", hlines, fixed = TRUE)] +dup <- hlines[grepl("y: 3,", hlines, fixed = TRUE)] +expect_true(length(dup) >= 1) +n_dup_segments <- lengths(regmatches(dup, gregexpr("table[.]hline", dup))) +expect_true(n_dup_segments == 1)