diff --git a/.github/workflows/tests-compile-ci.yml b/.github/workflows/tests-compile-ci.yml index 65f4537..8d443e7 100644 --- a/.github/workflows/tests-compile-ci.yml +++ b/.github/workflows/tests-compile-ci.yml @@ -8,9 +8,9 @@ name: Tests Compile CI on: # Triggers the workflow on push or pull request events but only for the branches below push: - branches: [ "main", "0.1.0-dev" ] + branches: [ "main", "0.0.x", "0.1.0-dev" ] pull_request: - branches: [ "main", "0.1.0-dev" ] + branches: [ "main", "0.0.x", "0.1.0-dev" ] # Allows one to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/README.md b/README.md index d766919..abd9549 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,20 @@ -# typst-tablex (v0.0.7) +# typst-tablex (v0.0.8) **More powerful and customizable tables in Typst.** -**NOTE:** This library still has a few bugs, but most of them shouldn't be noticeable. **Please open an issue if you find a bug** and I'll get to it as soon as I can. **(Do not be afraid to open issues!! Also, PRs are welcome!)** +**NOTE: Please open an issue if you find a bug with tablex** and I'll get to it as soon as I can. **(PRs are also welcome!)** ## Sponsors ❤️ If you'd like to appear here, [consider sponsoring the project!](https://github.com/sponsors/PgBiel) + +
+ + + + ## Table of Contents * [Usage](#usage) @@ -21,6 +29,7 @@ If you'd like to appear here, [consider sponsoring the project!](https://github. * [Basic types and functions](#basic-types-and-functions) * [Gridx and Tablex](#gridx-and-tablex) * [Changelog](#changelog) + * [v0.0.8](#v008) * [v0.0.7](#v007) * [v0.0.6](#v006) * [v0.0.5](#v005) @@ -33,12 +42,12 @@ If you'd like to appear here, [consider sponsoring the project!](https://github. ## Usage -To use this library through the Typst package manager **(for Typst v0.6.0+)**, write for example `#import "@preview/tablex:0.0.7": tablex, cellx` at the top of your Typst file (you may also add whichever other functions you use from the library to that import list!). +To use this library through the Typst package manager **(for Typst v0.6.0+)**, write for example `#import "@preview/tablex:0.0.8": tablex, cellx` at the top of your Typst file (you may also add whichever other functions you use from the library to that import list!). For older Typst versions, download the file `tablex.typ` from the latest release (or directly from the main branch, for the 'bleeding edge') at the tablex repository (https://github.com/PgBiel/typst-tablex) and place it on the same folder as your own Typst file. Then, at the top of your file, write for example `#import "tablex.typ": tablex, cellx` (plus whichever other functions you use from the library). This library should be compatible with Typst v0.2.0, v0.3.0, v0.4.0, v0.5.0, v0.6.0, v0.7.0, v0.8.0, v0.9.0 and v0.10.0. -**Using the latest Typst version is recommended (v0.6.0+)**, as it fixes certain bugs which made it almost impossible to use references and citations from within tablex tables (and also brings the package manager, making using tablex even easier!). +**Using the latest Typst version is always recommended** in order to make use of the latest optimizations and features available. Here's an example of what `tablex` can do: @@ -46,7 +55,7 @@ Here's an example of what `tablex` can do: Here's the code for that table: ```typ -#import "@preview/tablex:0.0.7": tablex, rowspanx, colspanx +#import "@preview/tablex:0.0.8": tablex, rowspanx, colspanx #tablex( columns: 4, @@ -98,7 +107,7 @@ Here's the code for that table: In most cases, you should be able to replace `#table` with `#tablex` and be good to go for a start - it should look _very_ similar (if not identical). Indeed, the syntax is very similar for the basics: ```typ -#import "@preview/tablex:0.0.7": tablex +#import "@preview/tablex:0.0.8": tablex #tablex( columns: (auto, 1em, 1fr, 1fr), // 4 columns @@ -125,7 +134,7 @@ This is mostly a word of caution in case anything I haven't anticipated happens, Your cells can now span more than one column and/or row at once, with `colspanx` / `rowspanx`: ```typ -#import "@preview/tablex:0.0.7": tablex, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, colspanx, rowspanx #tablex( columns: 3, @@ -139,7 +148,9 @@ Your cells can now span more than one column and/or row at once, with `colspanx` Note that the empty parentheses there are just for organization, and are ignored (unless they come before the first cell - more on that later). They're useful to help us keep track of which cell positions are being used up by the spans, because, if we try to add an actual cell at these spots, it will just push the others forward, which might seem unexpected. -Use `colspanx(2)(rowspanx(2)[d])` to colspan and rowspan at the same time. Be careful not to attempt to overwrite other cells' spans, as you will get a nasty error. +Use `colspanx(2, rowspanx(2)[d])` to colspan and rowspan at the same time. Be careful not to attempt to overwrite other cells' spans, as you will get a nasty error. + +**Note (since tablex v0.0.8):** By default, colspans and rowspans can cause spanned `auto` columns and rows to expand to fit their contents (only the last spanned track - column or row - can expand). If you'd like colspans to not affect column sizes at all (and thus "fit" within their spanned columns), you may specify `fit-spans: (x: true)` to the table. Similarly, you can specify `fit-spans: (y: true)` to have rowspans not affect row sizes at all. To apply both effects, use either `fit-spans: true` or `fit-spans: (x: true, y: true)`. You can also apply this to a single colspan (for example) with `colspanx(2, fit-spans: (x: true))[a]`, as this option is available not only for the whole table but also for each cell. See the reference section for more information. ### Repeat header rows @@ -149,12 +160,12 @@ Note that you may wish to customize this. Use `repeat-header: 6` to repeat for 6 Also, note that, by default, the horizontal lines below the header are transported to other pages, which may be an annoyance if you customize lines too much (see below). Use `header-hlines-have-priority: false` to ensure that the first row in each page will dictate the appearance of the horizontal lines above it (and not the header). -**Note:** Please open a GitHub issue if you have any issues with this feature. Note that the table must be contained within pages of same dimensions and (top) margins for this to work properly (or, really, for most things in `tablex` to work properly). +**Note:** Depending on the size of your document, repeatable headers might not behave properly due to certain limitations in Typst's introspection system (as observed in https://github.com/PgBiel/typst-tablex/issues/43). Example: ```typ -#import "@preview/tablex:0.0.7": tablex, hlinex, vlinex, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, hlinex, vlinex, colspanx, rowspanx #pagebreak() #v(80%) @@ -201,7 +212,7 @@ Something similar occurs for `vlinex()`, which has `start`, `end` (first row and Here's some sample usage: ```typ -#import "@preview/tablex:0.0.7": tablex, gridx, hlinex, vlinex, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, gridx, hlinex, vlinex, colspanx, rowspanx #tablex( columns: 4, @@ -245,7 +256,7 @@ Here's some sample usage: You can also *bulk-customize lines* by specifying `map-hlines: h => new_hline` and `map-vlines: v => new_vline`. This includes any automatically generated lines. For example: ```typ -#import "@preview/tablex:0.0.7": tablex, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, colspanx, rowspanx #tablex( columns: 3, @@ -277,7 +288,7 @@ Additionally, instead of specifying content to the cell, you can specify a funct For example: ```typ -#import "@preview/tablex:0.0.7": tablex, cellx, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, cellx, colspanx, rowspanx #tablex( columns: 3, @@ -309,7 +320,7 @@ To customize multiple cells at once, you have a few options: Example: ```typ -#import "@preview/tablex:0.0.7": tablex, colspanx, rowspanx +#import "@preview/tablex:0.0.8": tablex, colspanx, rowspanx #tablex( columns: 4, @@ -354,7 +365,7 @@ Another example (summing columns): #gridx( columns: 3, rows: 6, - fill: (col, row) => (blue, red, green).at(calc.mod(row + col - 1, 3)), + fill: (col, row) => (blue, red, green).at(calc.rem(row + col - 1, 3)), map-cols: (col, cells) => { let last = cells.last() last.content = [ @@ -382,9 +393,11 @@ Another example (summing columns): - Table lines don't play very well with column and row gutter when a colspan or rowspan is used. They may be missing or be cut off by gutters. -- Rows with fractional height (such as `2fr`) have zero height if the table spans more than one page. This is because fractional row heights are calculated on the available height of the first page of the table, which is something that the default `#table` can circumvent using internal code. This won't be fixed for now. (Columns with fractional width work fine, provided all pages the table is in have the same width, **and the page width isn't `auto`** (which forces fractional columns to be 0pt, even in the default `#table`).) +- Repeatable table headers might not behave properly depending on the size of your document or other factors (https://github.com/PgBiel/typst-tablex/issues/43). -- By default, the table assumes that all pages containing it have the same width and height (dimensions). This is used for auto-sizing of columns/rows and for repeatable headers to work properly. It would be potentially costly to re-calculate page sizes on every page, so this was postponed. +- Using tablex (especially when using repeatable header rows) may cause a warning, "layout did not converge within 5 attempts", to appear on recent Typst versions (https://github.com/PgBiel/typst-tablex/issues/38). This warning is due to how tablex works internally **and is not your fault** (in principle), so don't worry too much about it (unless you're sure it's not tablex that is causing this). + +- Rows with fractional height (such as `2fr`) have zero height if the table spans more than one page. This is because fractional row heights are calculated on the available height of the first page of the table, which is something that the default `#table` can circumvent using internal code. This won't be fixed for now. (Columns with fractional width work fine, provided all pages the table is in have the same width, **and the page width isn't `auto`** (which forces fractional columns to be 0pt, even in the default `#table`).) - Rotation (via Typst's `#rotate`) of text only affects the visual appearance of the text on the page, but does not change its dimensions as they factor into the layout. This leads to certain visual issues, such as rotated text potentially overflowing the cell height without being hyphenated or, inversely, being hyphenated even though there is enough space vertically (https://github.com/PgBiel/typst-tablex/issues/59). @@ -410,7 +423,8 @@ Another example (summing columns): x: auto, y: auto, rowspan: 1, colspan: 1, fill: auto, align: auto, - inset: auto + inset: auto, + fit-spans: auto ) = ( tablex-dict-type: "cell", content: content, @@ -419,6 +433,7 @@ Another example (summing columns): align: align, fill: fill, inset: inset, + fit-spans: fit-spans, x: x, y: y, ) @@ -432,6 +447,7 @@ Another example (summing columns): - `align` is this cell's align override, such as "center" (default `auto` to follow the rest of the table) - `fill` is this cell's fill override, such as "blue" (default `auto` to follow the rest of the table) - `inset` is this cell's inset override, such as `5pt` (default `auto` to follow the rest of the table) + - `fit-spans` allows overriding the table-wide `fit-spans` setting for this specific cell (e.g. if this cell has a `colspan` greater than 1, `fit-spans: (x: true)` will cause it to not affect the sizes of `auto` columns). - `x` is the cell's column index (0..len-1) - `auto` indicates it wasn't assigned yet - `y` is the cell's row index (0..len-1) - `auto` indicates it wasn't assigned yet @@ -630,8 +646,28 @@ Another example (summing columns): the modified `cell_array`. Note that, with your function, they cannot be sent to another column. Also, please preserve the order of the cells. This is especially important given that cells may be `none` if they're actually a position taken by another cell with colspan/rowspan. Make sure the `none` values are in the same indexes when the array is returned. + - `fit-spans`: either a dictionary `(x: bool, y: bool)` or just `bool` (e.g. just `true` is converted to `(x: true, y: true)`). When given `(x: true)`, colspans won't affect the sizes of `auto` columns. When given `(y: true)`, rowspans won't affect the sizes of `auto` rows. By default, this is equal to `(x: false, y: false)` (equivalent to just `false`), which means that colspans will cause the last spanned `auto` column to expand (depending on the contents of the cell) and rowspans will cause the last spanned `auto` row to expand similarly. + - This is usually used as `(x: true)` to prevent unexpected expansion of `auto` columns after using a colspan, which can happen when a colspan spans both a fractional-size column (e.g. `1fr`) and an `auto`-sized column. Can be applied to rows too through `(y: true)` or `(x: true, y: true)`, if needed, however. + - The point of this option is to have colspans and rowspans not affect the size of the table at all, and just "fit" within the columns and rows they span. Therefore, this option does not have any effect upon colspans and rowspans which don't span columns or rows with automatic size. + ## Changelog +### v0.0.8 + +- Added `fit-spans` option to `tablex` and `cellx` (https://github.com/PgBiel/typst-tablex/pull/111) + - Accepts `(x: bool, y: bool)`. When set to `(x: true)`, colspans won't affect the sizes of `auto` columns. When set to `(y: true)`, rowspans won't affect the sizes of `auto` rows. + - Defaults to `false`, equivalent to `(x: false, y: false)`, that is, colspans and rowspans affect the sizes of `auto` tracks (columns and rows) by default (expanding the last spanned track if the colspan/rowspan is too large). + - Useful when you want merged cells (or a specific merged cell) to "fit" within their spanned columns and rows. May help when adding a colspan or rowspan causes an `auto`-sized track to inadvertently expand. +- `auto` column sizing received multiple improvements and bug fixes. Tables should now have more natural column widths. (https://github.com/PgBiel/typst-tablex/pull/109, https://github.com/PgBiel/typst-tablex/pull/116) + - Fixes some problems with overflowing cells (https://github.com/PgBiel/typst-tablex/issues/48, https://github.com/PgBiel/typst-tablex/issues/75) + - Fixes `auto` columns being needlessly expanded in some cases (https://github.com/PgBiel/typst-tablex/issues/56, https://github.com/PgBiel/typst-tablex/issues/78) + - For similar problems not fixed by this, please use the new `fit-spans` option as needed, or use fixed-size columns instead. +- Several performance optimizations and other internal code improvements were made (https://github.com/PgBiel/typst-tablex/pull/113, https://github.com/PgBiel/typst-tablex/pull/114, https://github.com/PgBiel/typst-tablex/pull/115). + - Documents with lots of `tablex` tables might now become **up to 20% faster** to cold compile. Give it a shot! +- Fixed extra fixed-height rows appearing to have `auto` height (https://github.com/PgBiel/typst-tablex/pull/108). +- Fixed rows without any visible cells being drawn with zero height (https://github.com/PgBiel/typst-tablex/pull/107). + - Fixes some rowspans causing cells to overlap (https://github.com/PgBiel/typst-tablex/issues/82, https://github.com/PgBiel/typst-tablex/issues/105). + ### v0.0.7 I have begun [work on bringing many tablex improvements to built-in Typst tables](https://github.com/PgBiel/typst-improv-tables-planning)! In that regard, [you can now sponsor my work on tablex and improving Typst tables via GitHub Sponsors! Consider taking a look :)](https://github.com/sponsors/PgBiel) diff --git a/tablex-test.pdf b/tablex-test.pdf index 98c09e9..2f2cb1e 100644 Binary files a/tablex-test.pdf and b/tablex-test.pdf differ diff --git a/tablex-test.typ b/tablex-test.typ index 9970cba..f2c5941 100644 --- a/tablex-test.typ +++ b/tablex-test.typ @@ -800,7 +800,7 @@ Combining em and pt (with a stroke object): frac-total: frac-total, ) - assert(type(actual) == _length_type) + assert(type(actual) == _length-type) assert(expected == actual) }) } @@ -858,6 +858,42 @@ Combining em and pt (with a stroke object): #convert-length-to-pt-test(-0.005% - 0.005pt + 0em, -0.01pt) #convert-length-to-pt-test(-0.005% - 0.005pt - 0.005em, -0.015pt) +// Stroke thickness calculation +#let stroke-thickness-test( + value, expected, + compare-repr: false, +) = { + set text(size: 1pt) // Set 1em to 1pt + style(styles => { + let actual = stroke-len( + value, + styles: styles, + ) + + assert(type(actual) == _length-type) + + // Re-assign so we can modify the variable + let expected = expected + if compare-repr { + expected = repr(expected) + actual = repr(actual) + } + assert(expected == actual, message: "Expected " + repr(expected) + ", found " + repr(actual)) + }) +} + +#stroke-thickness-test(2pt, 2pt) +#stroke-thickness-test(2pt + 1em, 3pt) +#stroke-thickness-test(2pt + red, 2pt) +#stroke-thickness-test(2pt + 2em + red, 4pt) +#stroke-thickness-test(2.2pt - 2.2em + red, 0pt) +#stroke-thickness-test(0.005em + black, 0.005pt) +#stroke-thickness-test(red, 1pt) +#stroke-thickness-test((does-not-specify-thickness: 5), 1pt) +#stroke-thickness-test((thickness: 5pt + 2em, what: 55%), 7pt) +#stroke-thickness-test((thickness: 5pt + 2.005em, what: 55%), 7.005pt) +#stroke-thickness-test(rect(stroke: 2.002pt - 3.003em + red).stroke, -1.001pt, compare-repr: true) + *Line expansion - issue \#74:* #let wrap-for-linex-expansion-test(tabx) = { @@ -1002,3 +1038,346 @@ Combining em and pt (with a stroke object): vlinex(expand: -(2% + 2pt + 2em)), ) ) + +*Full-width rowspans displayed with the wrong height (Issue \#105)* + +#tablex( + columns: (auto, auto, auto, auto), + colspanx(4, rowspanx(3)[ONE]), + [TWO], [THREE], [FOUR], [FIVE], +) + +#block(breakable: false)[ + a + + #tablex( + columns: 3, + colspanx(3, rowspanx(2)[a]) + ) + + b +] + +*More overlapping rowspans (Issue \#82)* + +#tablex( + auto-lines: false, + stroke: 1pt, + columns: (auto,auto,auto,auto), + align:center, + //hlinex(), + //vlinex(), vlinex(), vlinex(),vlinex(), + [Name], [He],[Rack],[Beschreibung], + hlinex(), + cellx(rowspan:2,align:center)["mt01"], cellx(fill: rgb("#b9edffff"), align: left,rowspan:2)[42], + cellx(rowspan:2,align:center)["WAT"], + //hlinex(), + cellx(rowspan:2,align:center)["Löschgasflasche"], + cellx(rowspan:2,align:center)["mt2"], cellx(fill: rgb("#b9edffff"), align: left,rowspan:2)[41], + cellx(rowspan:2,align:center)["WAT"],"test", + (""),"","","","", + cellx(rowspan:2,align:center)["mt3"], cellx(fill: rgb("#b9edffff"), align: left,rowspan:2)[40], + cellx(rowspan:2,align:center)["WAT"],"test", + "","","","","", + cellx(rowspan:2,align:center)["mt3"], cellx(fill: rgb("#b9edffff"), align: left,rowspan:2)[40], + cellx(rowspan:2,align:center)["WAT"],"test", + "","","","","", + +) + +*Extra rows should inherit the last row size (Issue \#97)* + +#tablex( + rows: 5pt, + cellx(x: 0, y: 1)[a\ a\ a\ a] +) +#v(4em) + +#[ + #tablex( + align: center + horizon, + rows: 5mm, + columns: (7mm, 10mm, 23mm, 15mm, 10mm, 70mm, 5mm, 5mm, 5mm, 5mm, 12mm, 18mm), + ..range(5), cellx(rowspan: 3, colspan: 7)[], + ..range(5), + ..range(5), + + ..range(5), rowspanx(5)[], colspanx(3)[], colspanx(2)[], [], + ..range(5), rowspanx(3)[], rowspanx(3)[], rowspanx(3)[], cellx(rowspan: 3, colspan: 2)[], rowspanx(3)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), colspanx(4)[], colspanx(2)[], + + colspanx(2)[], ..range(3), rowspanx(3)[], cellx(rowspan: 3, colspan: 6)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + ) + + #tablex( + align: center + horizon, + rows: 5mm, + columns: (7mm, 10mm, 23mm, 15mm, 10mm, 70mm, 5mm, 5mm, 5mm, 5mm, 12mm, 18mm), + ..range(5), cellx(x: 5, rowspan: 3, colspan: 7)[], + ..range(5), + ..range(5), + + ..range(5), rowspanx(5)[], colspanx(3)[], colspanx(2)[], [], + ..range(5), rowspanx(3)[], rowspanx(3)[], rowspanx(3)[], cellx(rowspan: 3, colspan: 2)[], rowspanx(3)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), colspanx(4)[], colspanx(2)[], + + colspanx(2)[], ..range(3), rowspanx(3)[], cellx(rowspan: 3, colspan: 6)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + ) + + #tablex( + align: center + horizon, + rows: 5mm, + columns: (7mm, 10mm, 23mm, 15mm, 10mm, 70mm, 5mm, 5mm, 5mm, 5mm, 12mm, 18mm), + cellx(x: 5, rowspan: 3, colspan: 7)[], + ..range(5), + ..range(5), + + ..range(5), rowspanx(5)[], colspanx(3)[], colspanx(2)[], [], + ..range(5), rowspanx(3)[], rowspanx(3)[], rowspanx(3)[], cellx(rowspan: 3, colspan: 2)[], rowspanx(3)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), colspanx(4)[], colspanx(2)[], + + colspanx(2)[], ..range(3), rowspanx(3)[], cellx(rowspan: 3, colspan: 6)[], + colspanx(2)[], ..range(3), + colspanx(2)[], ..range(3), + ) +] + +#set page("a4") + +*Overflowing cells (Issues \#48 and \#75)* + +#tablex( + columns: 3, + [a: #lorem(7)], [b: $T h i s I s A L o n g A n d R a n d o m M a t h E p r e s s i o n$], [c] +) + +#tablex(columns: (auto, auto, auto, auto), + [lorem_ipsum_dolor_sit_amet], [lorem], [lorem_ipsum_dolor_sit_amet_consectetur_adipisici], [lorem], +) + +*Rowspans spanning 1fr and auto with 'fit-spans'* + +#let unbreakable-tablex(..args) = block(breakable: false, tablex(..args)) + +- Normal sizes: + + #unbreakable-tablex( + columns: (auto, auto, 1fr, 1fr), + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E] + ) + +- With colspan over auto and 1fr (but not all fractional columns): + + #unbreakable-tablex( + columns: (auto, auto, 1fr, 1fr), + colspanx(3)[Hello world! Hello!], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E] + ) + +- Using `fit-spans`, column sizes should be identical to the first table (in all three below): + + #unbreakable-tablex( + columns: (auto, auto, 1fr, 1fr), + fit-spans: (x: true), + colspanx(3)[Hello world! Hello!], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E] + ) + + #unbreakable-tablex( + columns: (auto, auto, 1fr, 1fr), + fit-spans: true, + colspanx(3)[Hello world! Hello!], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E] + ) + + #unbreakable-tablex( + columns: (auto, auto, 1fr, 1fr), + colspanx(3, fit-spans: (x: true))[Hello world! Hello!], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E], + [A], [BC], [D], [E] + ) + +*Other `fit-spans` tests* + +1. Columns + + #unbreakable-tablex( + columns: 4, + [A], [B], [C], [D], + ) + + #unbreakable-tablex( + columns: 4, + colspanx(4, lorem(20)), + [A], [B], [C], [D], + ) + + #unbreakable-tablex( + columns: 4, + fit-spans: (x: true), + colspanx(4, lorem(20)), + [A], [B], [C], [D], + ) + + #unbreakable-tablex( + columns: 4, + fit-spans: true, + colspanx(4, lorem(20)), + [A], [B], [C], [D], + ) + +2. Rows + + #unbreakable-tablex( + columns: (auto, 4em), + [A], [B], + [C], [B], + [D], [E] + ) + + #unbreakable-tablex( + columns: (auto, 4em), + [A], rowspanx(2, line(start: (0pt, 0pt), end: (0pt, 6em))), + [C], (), + [D], [E] + ) + + #unbreakable-tablex( + columns: (auto, 4em), + fit-spans: (y: true), + [A], rowspanx(2, line(start: (0pt, 0pt), end: (0pt, 6em))), + [C], (), + [D], [E #v(2em)] + ) + + #unbreakable-tablex( + columns: (auto, 4em), + fit-spans: true, + [A], rowspanx(2, line(start: (0pt, 0pt), end: (0pt, 6em))), + [C], (), + [D], [E #v(2em)] + ) + +*Rowspans spanning all fractional columns and auto (Issues \#56 and \#78)* + +_For issue \#78_ + +- Columns should have the same size in all samples below: + + #unbreakable-tablex( + columns: (1fr, 1fr, auto, auto, auto), + [a], [b], [c], [d], [e], + cellx(colspan: 5)[#lorem(5)], + [a], [b], [c], [d], [e], + cellx(colspan: 2)[#lorem(10)], none, none, none, + [a], [b], [c], [d], [e], + ) + + #unbreakable-tablex( + columns: (1fr, 1fr, auto, auto, auto), + [a], [b], [c], [d], [e], + cellx(colspan: 5)[#lorem(5)], + [a], [b], [c], [d], [e], + cellx(colspan: 2)[#lorem(10)], none, none, none, + [a], [b], [c], [d], [e], + cellx(colspan: 3)[#lorem(15)], none, none, + ) + + #unbreakable-tablex( + columns: (1fr, 1fr, auto, auto, auto), + fit-spans: (x: true), + [a], [b], [c], [d], [e], + cellx(colspan: 5)[#lorem(5)], + [a], [b], [c], [d], [e], + cellx(colspan: 2)[#lorem(10)], none, none, none, + [a], [b], [c], [d], [e], + cellx(colspan: 3)[#lorem(15)], none, none, + ) + +_For issue \#56_ + +- Columns should have the same size in all samples below: + + #unbreakable-tablex( + columns: (auto, auto, 1fr), + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] + ) + + #unbreakable-tablex( + columns: (auto, auto, 1fr), + colspanx(3)[Hello world! Hello!], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] + ) + + #unbreakable-tablex( + columns: (auto, auto, 1fr), + fit-spans: (x: true), + colspanx(3)[Hello world! Hello!], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] + ) + + +#set page(height: auto, width: auto) + +Ensure the heuristic doesn't apply on auto width pages. +Second table should have a longer second column. + +#tablex( + columns: (auto, auto, 1fr), + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] +) + +#tablex( + columns: (auto, auto, 1fr), + colspanx(3)[Hello world! Hello!], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] +) + +#tablex( + columns: (auto, auto, 1fr), + fit-spans: (x: true), + colspanx(3)[Hello world! Hello!], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D], + [A], [BC], [D] +) diff --git a/tablex.typ b/tablex.typ index 99a1a75..dbcf6fc 100644 --- a/tablex.typ +++ b/tablex.typ @@ -1,6 +1,6 @@ // Welcome to tablex! // Feel free to contribute with any features you think are missing. -// Version: v0.0.7 +// Version: v0.0.8 // -- table counter -- @@ -8,30 +8,90 @@ // -- compat -- -#let calc-mod(a, b) = { - calc.floor(a) - calc.floor(b * calc.floor(a / b)) -} - // get the types of things so we can compare with them // (0.2.0-0.7.0: they're strings; 0.8.0+: they're proper types) -#let _array_type = type(()) -#let _dict_type = type((a: 5)) -#let _bool_type = type(true) -#let _str_type = type("") -#let _color_type = type(red) -#let _stroke_type = type(red + 5pt) -#let _length_type = type(5pt) -#let _rel_len_type = type(100% + 5pt) -#let _ratio_type = type(100%) -#let _int_type = type(5) -#let _float_type = type(5.0) -#let _fraction_type = type(5fr) -#let _function_type = type(x => x) -#let _content_type = type([]) +#let _array-type = type(()) +#let _dict-type = type((a: 5)) +#let _bool-type = type(true) +#let _str-type = type("") +#let _color-type = type(red) +#let _stroke-type = type(red + 5pt) +#let _length-type = type(5pt) +#let _rel-len-type = type(100% + 5pt) +#let _ratio-type = type(100%) +#let _int-type = type(5) +#let _float-type = type(5.0) +#let _fraction-type = type(5fr) +#let _function-type = type(x => x) +#let _content-type = type([]) // note: since 0.8.0, alignment and 2d alignment are the same // but keep it like this for pre-0.8.0 -#let _align_type = type(left) -#let _2d_align_type = type(top + left) +#let _align-type = type(left) +#let _2d-align-type = type(top + left) + +// If types aren't strings, this means we're using 0.8.0+. +#let using-typst-v080-or-later = str(type(_str-type)) == "type" + +// Attachments use "t" and "b" instead of "top" and "bottom" since v0.3.0. +#let using-typst-v030-or-later = using-typst-v080-or-later or $a^b$.body.has("t") + +// This is true if types have fields in the current Typst version. +// This means we can use stroke.thickness, length.em, and so on. +#let typst-fields-supported = using-typst-v080-or-later + +// This is true if calc.rem exists in the current Typst version. +// Otherwise, we use a polyfill. +#let typst-calc-rem-supported = using-typst-v030-or-later + +// Remainder operation. +#let calc-mod = if typst-calc-rem-supported { + calc.rem +} else { + (a, b) => calc.floor(a) - calc.floor(b * calc.floor(a / b)) +} + +// Returns the sign of the operand. +// -1 for negative, 1 for positive or zero. +#let calc-sign(x) = { + // For positive: true - false = 1 - 0 = 1 + // For zero: true - false = 1 - 0 = 1 + // For negative: false - true = 0 - 1 = -1 + int(0 <= x) - int(x < 0) +} + +// Polyfill for array sum (.sum() is Typst 0.3.0+). +#let array-sum(arr, zero: 0) = { + arr.fold(zero, (a, x) => a + x) +} + +// -- common validators -- + +// Converts the 'fit-spans' argument to a (x: bool, y: bool) dictionary. +// Optionally use a default dictionary to fill missing arguments with. +// This is in the common section as it is needed by the grid section as well. +#let validate-fit-spans(fit-spans, default: (x: false, y: false), error-prefix: none) = { + if type(error-prefix) == _str-type { + error-prefix = " " + error-prefix + } else { + error-prefix = "" + } + if type(fit-spans) == _bool-type { + fit-spans = (x: fit-spans, y: fit-spans) + } + if type(fit-spans) == _dict-type { + assert(fit-spans.len() > 0, message: "Tablex error:" + error-prefix + " 'fit-spans', if a dictionary, must not be empty.") + assert(fit-spans.keys().all(k => k in ("x", "y")), message: "Tablex error:" + error-prefix + " 'fit-spans', if a dictionary, must only have the keys x and y.") + assert(fit-spans.values().all(v => type(v) == _bool-type), message: "Tablex error:" + error-prefix + " keys 'x' and 'y' in the 'fit-spans' dictionary must be booleans (true/false).") + for key in ("x", "y") { + if key in default and key not in fit-spans { + fit-spans.insert(key, default.at(key)) + } + } + } else { + panic("Tablex error:" + error-prefix + " Expected 'fit-spans' to be either a boolean or dictionary, found '" + str(type(fit-spans)) + "'") + } + fit-spans +} // ------------ @@ -79,7 +139,8 @@ x: auto, y: auto, rowspan: 1, colspan: 1, fill: auto, align: auto, - inset: auto + inset: auto, + fit-spans: auto ) = ( tablex-dict-type: "cell", content: content, @@ -88,6 +149,7 @@ align: align, fill: fill, inset: inset, + fit-spans: fit-spans, x: x, y: y, ) @@ -106,7 +168,7 @@ // Is this a valid dict created by this library? #let is-tablex-dict(x) = ( - type(x) == _dict_type + type(x) == _dict-type and "tablex-dict-type" in x ) @@ -122,11 +184,11 @@ #let is-tablex-occupied(x) = is-tablex-dict-type(x, "occupied") #let table-item-convert(item, keep_empty: true) = { - if type(item) == _function_type { // dynamic cell content + if type(item) == _function-type { // dynamic cell content cellx(item) } else if keep_empty and item == () { item - } else if type(item) != _dict_type or "tablex-dict-type" not in item { + } else if type(item) != _dict-type or "tablex-dict-type" not in item { cellx[#item] } else { item @@ -165,7 +227,7 @@ .filter(c => c.y != auto) .fold(0, (acc, cell) => { if (is-tablex-cell(cell) - and type(cell.y) in (_int_type, _float_type) + and type(cell.y) in (_int-type, _float-type) and cell.y > acc) { cell.y } else { @@ -177,7 +239,7 @@ if is-tablex-cell(item) and item.x == auto and item.y == auto { // cell occupies (colspan * rowspan) spaces len += item.colspan * item.rowspan - } else if type(item) == _content_type { + } else if type(item) == _content-type { len += 1 } } @@ -193,31 +255,31 @@ // Check if this length is infinite. #let is-infinite-len(len) = { - type(len) in (_ratio_type, _fraction_type, _rel_len_type, _length_type) and "inf" in repr(len) + type(len) in (_ratio-type, _fraction-type, _rel-len-type, _length-type) and "inf" in repr(len) } // Check if this is a valid color (color, gradient or pattern). #let is-color(val) = { - type(val) == _color_type or str(type(val)) in ("gradient", "pattern") + type(val) == _color-type or str(type(val)) in ("gradient", "pattern") } #let validate-cols-rows(columns, rows, items: ()) = { - if type(columns) == _int_type { + if type(columns) == _int-type { assert(columns >= 0, message: "Error: Cannot have a negative amount of columns.") columns = (auto,) * columns } - if type(rows) == _int_type { + if type(rows) == _int-type { assert(rows >= 0, message: "Error: Cannot have a negative amount of rows.") rows = (auto,) * rows } - if type(columns) != _array_type { + if type(columns) != _array-type { columns = (columns,) } - if type(rows) != _array_type { + if type(rows) != _array-type { rows = (rows,) } @@ -233,7 +295,7 @@ let col_row_is_valid(col_row) = ( (not is-infinite-len(col_row)) and (col_row == auto or type(col_row) in ( - _fraction_type, _length_type, _rel_len_type, _ratio_type + _fraction-type, _length-type, _rel-len-type, _ratio-type )) ) @@ -342,24 +404,6 @@ calc.max(a, b) } -// Backwards-compatible enumerate -#let enumerate(arr) = { - if type(arr) != _array_type { - return arr - } - - let new-arr = () - let i = 0 - - for x in arr { - new-arr.push((i, x)) - - i += 1 - } - - new-arr -} - // Gets the topmost parent of a line. #let get-top-parent(line) = { let previous = none @@ -376,11 +420,11 @@ // Typst 0.9.0 uses a minus sign ("−"; U+2212 MINUS SIGN) for negative numbers. // Before that, it used a hyphen minus ("-"; U+002D HYPHEN MINUS), so we use // regex alternation to match either of those. -#let NUMBER-REGEX-STRING = "(−|-)?\\d*\\.?\\d+" +#let NUMBER-REGEX-STRING = "(?:−|-)?\\d*\\.?\\d+" -// Check if the given length has type '_length_type' and no 'em' component. +// Check if the given length has type '_length-type' and no 'em' component. #let is-purely-pt-len(len) = { - type(len) == _length_type and "em" not in repr(len) + type(len) == _length-type and "em" not in repr(len) } // Measure a length in pt by drawing a line and using the measure() function. @@ -393,6 +437,18 @@ // // styles: from style() #let measure-pt(len, styles) = { + if typst-fields-supported { + // We can use fields to separate em from pt. + let pt = len.abs + let em = len.em + // Measure with abs (and later multiply by the sign) so negative em works. + // Otherwise it would return 0pt, and we would need to measure again with abs. + let measured-em = calc-sign(em) * measure(box(width: calc.abs(em) * 1em), styles).width + + return pt + measured-em + } + + // Fields not supported, so we have to measure twice when em can be negative. let measured-pt = measure(box(width: len), styles).width // If the measured length is positive, `len` must have overall been positive. @@ -415,7 +471,7 @@ // styles: from style() #let convert-length-type-to-pt(len, styles: none) = { // repr examples: "1pt", "1em", "0.5pt", "0.5em", "1pt + 1em", "-0.5pt + -0.5em" - if "em" not in repr(len) { + if is-purely-pt-len(len) { // No need to do any conversion because it must already be in pt. return len } @@ -481,17 +537,18 @@ // styles: from style() // page-size: equivalent to 100% (optional because the length may not have a ratio component) #let convert-relative-type-to-pt(len, styles, page-size: none) = { + if typst-fields-supported or eval(repr(0.00005em)) != 0.00005em { + // em repr changed in 0.11.0 => need to use fields here + // or use fields if they're supported anyway + return convert-ratio-type-to-pt(len.ratio, page-size) + convert-length-type-to-pt(len.length, styles: styles) + } + // We will need to draw a line for measurement later, // so we need the styles. if styles == none { panic("Cannot convert relative length to pt ('styles' not specified).") } - if eval(repr(0.00005em)) != 0.00005em { - // em repr changed in 0.11.0 => can safely use fields here - return convert-ratio-type-to-pt(len.ratio, page-size) + convert-length-type-to-pt(len.length, styles: styles) - } - // Note on precision: the `repr` for em components is precise, unlike // other length components, which are rounded to a precision of 2. // This is true up to Typst 0.9.0 and possibly later versions. @@ -524,8 +581,8 @@ } // The length part is the pt part + em part. - // Note: we cannot use `len - ratio-part` as that returns a `_rel_len_type` value, - // not a `_length_type` value. + // Note: we cannot use `len - ratio-part` as that returns a `_rel-len-type` value, + // not a `_length-type` value. let length-part-pt = convert-length-type-to-pt(pt-part + em-part, styles: styles) ratio-part-pt + length-part-pt @@ -545,13 +602,13 @@ if is-infinite-len(len) { 0pt // avoid the destruction of the universe - } else if type(len) == _length_type { + } else if type(len) == _length-type { convert-length-type-to-pt(len, styles: styles) - } else if type(len) == _ratio_type { + } else if type(len) == _ratio-type { convert-ratio-type-to-pt(len, page-size) - } else if type(len) == _fraction_type { + } else if type(len) == _fraction-type { convert-fraction-type-to-pt(len, frac-amount, frac-total) - } else if type(len) == _rel_len_type { + } else if type(len) == _rel-len-type { convert-relative-type-to-pt(len, styles, page-size: page-size) } else { panic("Cannot convert '" + type(len) + "' to length.") @@ -562,22 +619,24 @@ #let stroke-len(stroke, stroke-auto: 1pt, styles: none) = { let no-ratio-error = "Tablex error: Stroke cannot be a ratio or relative length (i.e. have a percentage like '53%'). Try using the layout() function (or similar) to convert the percentage to 'pt' instead." let stroke = default-if-auto(stroke, stroke-auto) - if type(stroke) == _length_type { + if type(stroke) == _length-type { convert-length-to-pt(stroke, styles: styles) - } else if type(stroke) in (_rel_len_type, _ratio_type) { + } else if type(stroke) in (_rel-len-type, _ratio-type) { panic(no-ratio-error) } else if is-color(stroke) { 1pt - } else if type(stroke) == _stroke_type { - // support: - // - 5 - // - 5.5 - let maybe-float-regex = "(?:\\d+(?:\\.\\d+)?)" + } else if type(stroke) == _stroke-type { + if typst-fields-supported { + // No need for any repr() parsing, just use the thickness field. + let thickness = default-if-auto(stroke.thickness, 1pt) + return convert-length-to-pt(thickness, styles: styles) + } + // support: // - 2pt / 2em / 2cm / 2in + color // - 2.5pt / 2.5em / ... + color // - 2pt + 3em + color - let len-regex = "(?:" + maybe-float-regex + "(?:em|pt|cm|in|%)(?:\\s+\\+\\s+" + maybe-float-regex + "em)?)" + let len-regex = "(?:" + NUMBER-REGEX-STRING + "(?:em|pt|cm|in|%)(?:\\s+\\+\\s+" + NUMBER-REGEX-STRING + "em)?)" let r = regex("^" + len-regex) let s = repr(stroke).find(r) @@ -596,19 +655,19 @@ 1pt // okay it's probably just a color then } else { let len = eval(s) - if type(len) == _length_type { + if type(len) == _length-type { convert-length-to-pt(len, styles: styles) - } else if type(len) in (_rel_len_type, _ratio_type) { + } else if type(len) in (_rel-len-type, _ratio-type) { panic(no-ratio-error) } else { 1pt // should be unreachable } } - } else if type(stroke) == _dict_type and "thickness" in stroke { + } else if type(stroke) == _dict-type and "thickness" in stroke { let thickness = stroke.thickness - if type(thickness) == _length_type { + if type(thickness) == _length-type { convert-length-to-pt(thickness, styles: styles) - } else if type(thickness) in (_rel_len_type, _ratio_type) { + } else if type(thickness) in (_rel-len-type, _ratio-type) { panic(no-ratio-error) } else { 1pt @@ -669,7 +728,24 @@ // Fetches an entire row of cells (all positions with the given y). #let grid-get-row(grid, y) = { - range(grid.width).map(x => grid-at(grid, x, y)) + let len = grid.items.len() + // position of the first cell in that row. + let first-row-pos = grid-index-at(0, y, grid: grid) + if len <= first-row-pos { + // grid isn't large enough, so no row to return + (none,) * grid.width + } else { + // position right after the last cell in this row + let next-row-pos = first-row-pos + grid.width + let cell-row = grid.items.slice(first-row-pos, calc.min(len, next-row-pos)) + let cell-row-len = cell-row.len() + if cell-row-len < grid.width { + // the row isn't complete because the grid wasn't large enough. + let missing-cells = (none,) * (grid.width - cell-row-len) + cell-row += missing-cells + } + cell-row + } } // Fetches an entire column of cells (all positions with the given x). @@ -740,7 +816,7 @@ // Organize cells in a grid from the given items, // and also get all given lines -#let generate-grid(items, x_limit: 0, y_limit: 0, map-cells: c => c) = { +#let generate-grid(items, x_limit: 0, y_limit: 0, map-cells: none, fit-spans: none) = { // init grid as a matrix // y_limit x x_limit let grid = create-grid(x_limit, y_limit) @@ -776,7 +852,7 @@ let item = items.at(i) // allow specifying () to change vline position - if type(item) == _array_type and item.len() == 0 { + if type(item) == _array-type and item.len() == 0 { if x == 0 and y == 0 { // increment vline's secondary counter prev_x += 1 } @@ -843,7 +919,10 @@ cell.x = this_x cell.y = this_y - cell = table-item-convert(map-cells(cell)) + + if type(map-cells) == _function-type { + cell = table-item-convert(map-cells(cell)) + } assert(is-tablex-cell(cell), message: "Tablex error: 'map-cells' returned something that isn't a valid cell.") @@ -852,7 +931,7 @@ } let content = cell.content - let content = if type(content) == _function_type { + let content = if type(content) == _function-type { let res = content(this_x, this_y) if is-tablex-cell(res) { cell = res @@ -870,12 +949,19 @@ panic("Error: Cell with function as content returned another cell with 'none' as x or y!") } - if type(this_x) != _int_type or type(this_y) != _int_type { + if type(this_x) != _int-type or type(this_y) != _int-type { panic("Error: Cell coordinates must be integers. Invalid pair: " + repr((this_x, this_y))) } cell.content = content + // resolve 'fit-spans' option for this cell + if "fit-spans" not in cell { + cell.fit-spans = auto + } else if cell.fit-spans != auto { + cell.fit-spans = validate-fit-spans(cell.fit-spans, default: fit-spans, error-prefix: "At cell (" + str(this_x) + ", " + str(this_y) + "):") + } + // up to which 'y' does this cell go let max_x = this_x + cell.colspan - 1 let max_y = this_y + cell.rowspan - 1 @@ -891,8 +977,7 @@ let cell_positions = positions-spanned-by(cell, x: this_x, y: this_y, x_limit: x_limit, y_limit: none) for position in cell_positions { - let px = position.at(0) - let py = position.at(1) + let (px, py) = position let currently_there = grid-at(grid, px, py) if currently_there != none { @@ -942,9 +1027,7 @@ } // for missing cell positions: add empty cell - for index_item in enumerate(grid.items) { - let index = index_item.at(0) - let item = index_item.at(1) + for (index, item) in grid.items.enumerate() { if item == none { grid.items.at(index) = new_empty_cell(grid, index: index) } @@ -980,13 +1063,13 @@ align_default: left, fill_default: none) = { - let align_default = if type(align_default) == _function_type { + let align_default = if type(align_default) == _function-type { align_default(cell.x, cell.y) // column, row } else { align_default } - let fill_default = if type(fill_default) == _function_type { + let fill_default = if type(fill_default) == _function-type { fill_default(cell.x, cell.y) // row, column } else { fill_default @@ -1004,7 +1087,7 @@ // same here for fill let cell_fill = default-if-auto(cell.fill, fill_default) - if type(cell_fill) == _array_type { + if type(cell_fill) == _array-type { let fill_len = cell_fill.len() if fill_len == 0 { @@ -1028,7 +1111,7 @@ panic("Tablex error: Invalid fill specified (must be either a function (column, row) -> fill, a color, an array of valid fill values, or 'none').") } - if type(cell_align) == _array_type { + if type(cell_align) == _array-type { let align_len = cell_align.len() if align_len == 0 { @@ -1048,7 +1131,7 @@ } } - if cell_align != auto and type(cell_align) not in (_align_type, _2d_align_type) { + if cell_align != auto and type(cell_align) not in (_align-type, _2d-align-type) { panic("Tablex error: Invalid alignment specified (must be either a function (column, row) -> alignment, an alignment value - such as 'left' or 'center + top' -, an array of alignment values (one for each column), or 'auto').") } @@ -1075,7 +1158,7 @@ // (auto, 1fr, ...) is ignored. #let sum-fixed-size-tracks(tracks) = { tracks.fold(0pt, (acc, el) => { - if type(el) == _length_type { + if type(el) == _length-type { acc + el } else { acc @@ -1086,11 +1169,11 @@ // Calculate the size of fraction tracks (cols/rows) (1fr, 2fr, ...), // based on the remaining sizes (after fixed-size and auto columns) #let determine-frac-tracks(tracks, remaining: 0pt, gutter: none) = { - let frac-tracks = enumerate(tracks).filter(t => type(t.at(1)) == _fraction_type) + let frac-tracks = tracks.enumerate().filter(t => type(t.at(1)) == _fraction-type) let amount-frac = frac-tracks.fold(0, (acc, el) => acc + (el.at(1) / 1fr)) - if type(gutter) == _fraction_type { + if type(gutter) == _fraction-type { amount-frac += (gutter / 1fr) * (tracks.len() - 1) } @@ -1100,14 +1183,11 @@ 0pt } - if type(gutter) == _fraction_type { + if type(gutter) == _fraction-type { gutter = frac-width * (gutter / 1fr) } - for i_size in frac-tracks { - let i = i_size.at(0) - let size = i_size.at(1) - + for (i, size) in frac-tracks { tracks.at(i) = frac-width * (size / 1fr) } @@ -1117,14 +1197,11 @@ // Gets the last (rightmost) auto column a cell is inserted in, for // due expansion #let get-colspan-last-auto-col(cell, columns: none) = { - let cell_cols = range(cell.x, cell.x + cell.colspan) + let cell-cols = range(cell.x, cell.x + cell.colspan) let last_auto_col = none - for i_col in enumerate(columns).filter(i_col => i_col.at(0) in cell_cols) { - let i = i_col.at(0) - let col = i_col.at(1) - - if col == auto { + for (i, col) in columns.enumerate() { + if i in cell-cols and col == auto { last_auto_col = max-if-not-none(last_auto_col, i) } } @@ -1135,14 +1212,11 @@ // Gets the last (bottom-most) auto row a cell is inserted in, for // due expansion #let get-rowspan-last-auto-row(cell, rows: none) = { - let cell_rows = range(cell.y, cell.y + cell.rowspan) + let cell-rows = range(cell.y, cell.y + cell.rowspan) let last_auto_row = none - for i_row in enumerate(rows).filter(i_row => i_row.at(0) in cell_rows) { - let i = i_row.at(0) - let row = i_row.at(1) - - if row == auto { + for (i, row) in rows.enumerate() { + if i in cell-rows and row == auto { last_auto_row = max-if-not-none(last_auto_row, i) } } @@ -1155,14 +1229,11 @@ // Useful to subtract from the total width to find out how much more // should an auto column extend to have that cell fit in the table. #let get-colspan-fixed-size-covered(cell, columns: none) = { - let cell_cols = range(cell.x, cell.x + cell.colspan) + let cell-cols = range(cell.x, cell.x + cell.colspan) let size = 0pt - for i_col in enumerate(columns).filter(i_col => i_col.at(0) in cell_cols) { - let i = i_col.at(0) - let col = i_col.at(1) - - if type(col) == _length_type { + for (i, col) in columns.enumerate() { + if i in cell-cols and type(col) == _length-type { size += col } } @@ -1174,14 +1245,11 @@ // Useful to subtract from the total height to find out how much more // should an auto row extend to have that cell fit in the table. #let get-rowspan-fixed-size-covered(cell, rows: none) = { - let cell_rows = range(cell.y, cell.y + cell.rowspan) + let cell-rows = range(cell.y, cell.y + cell.rowspan) let size = 0pt - for i_row in enumerate(rows).filter(i_row => i_row.at(0) in cell_rows) { - let i = i_row.at(0) - let row = i_row.at(1) - - if type(row) == _length_type { + for (i, row) in rows.enumerate() { + if i in cell-rows and type(row) == _length-type { size += row } } @@ -1189,16 +1257,14 @@ } // calculate the size of auto columns (based on the max width of their cells) -#let determine-auto-columns(grid: (), styles: none, columns: none, inset: none, align: auto) = { +#let determine-auto-columns(grid: (), styles: none, columns: none, inset: none, align: auto, fit-spans: none, page-width: 0pt) = { assert(styles != none, message: "Cannot measure auto columns without styles") let total_auto_size = 0pt let auto_sizes = () let new_columns = columns - for i_col in enumerate(columns) { - let i = i_col.at(0) - let col = i_col.at(1) - + let all-frac-columns = columns.enumerate().filter(i-col => type(i-col.at(1)) == _fraction-type).map(i-col => i-col.at(0)) + for (i, col) in columns.enumerate() { if col == auto { // max cell width let col_size = grid-get-column(grid, i) @@ -1208,12 +1274,39 @@ } let pcell = get-parent-cell(cell, grid: grid) // in case this is a colspan - let last_auto_col = get-colspan-last-auto-col(pcell, columns: columns) + let last-auto-col = get-colspan-last-auto-col(pcell, columns: columns) + + let fit-this-span = if "fit-spans" in pcell and pcell.fit-spans != auto { + pcell.fit-spans.x + } else { + fit-spans.x + } + let this-cell-can-expand-columns = pcell.colspan == 1 or not fit-this-span // only expand the last auto column of a colspan, // and only the amount necessary that isn't already // covered by fixed size columns. - if last_auto_col == i { + // However, ignore this cell if it is a colspan with + // `fit-spans.x == true` (it requests to not expand + // columns). + if last-auto-col == i and this-cell-can-expand-columns { + let cell-spans-all-frac-columns = pcell.colspan > 1 and all-frac-columns.len() > 0 and all-frac-columns.all(i => pcell.x <= i and i < (pcell.x + pcell.colspan)) + if cell-spans-all-frac-columns and page-width != 0pt and not is-infinite-len(page-width) { + // HEURISTIC (only effective when the page width isn't 'auto' / infinite): + // If this cell can expand auto cols, but it already + // spans all fractional columns, then don't expand + // this auto column, as the cell would already have + // all remaining available space for itself anyway + // through the fractional columns spanned. + // Effectively, ignore this colspan - it will already + // have the max space possible, since, eventually, + // auto columns will be reduced to fit in the available + // size. + // For 'auto'-width pages, fractional columns will + // always have 0pt width, so this doesn't apply. + return max + } + // take extra inset as extra width or height on 'auto' let cell_inset = default-if-auto(pcell.inset, inset) @@ -1250,50 +1343,83 @@ (total: total_auto_size, sizes: auto_sizes, columns: new_columns) } -#let fit-auto-columns(available: 0pt, auto_cols: none, columns: none) = { +// Try to reduce the width of auto columns so that the table fits within the +// page width. +// Fair version of the algorithm, tries to shrink the minimum amount of columns +// possible. The same algorithm used by native tables. +// Auto columns that are too wide will receive equal amounts of the remaining +// width (the "fair-share"). +#let fit-auto-columns(available: 0pt, auto-cols: none, columns: none) = { if is-infinite-len(available) { // infinite space available => don't modify columns return columns } + // Remaining space to share between auto columns. + // Starts as all of the available space (excluding fixed-width columns). + // Will reduce as we exclude auto columns from being resized. let remaining = available - let auto_cols_remaining = auto_cols.len() + let auto-cols-to-resize = auto-cols.len() - if auto_cols_remaining <= 0 { + if auto-cols-to-resize <= 0 { return columns } - let fair_share = remaining / auto_cols_remaining - - for i_col in auto_cols { - let i = i_col.at(0) - let col = i_col.at(1) - - if auto_cols_remaining <= 0 { - return columns // no more to share + // The fair-share must be the largest possible (to ensure maximum fairness) + // such that we can shrink the minimum amount of columns possible and, at the + // same time, ensure that the table won't cross the page width. + // To do this, we will try to divide the space evenly between each auto column + // to be resized. + // If one or more auto columns are smaller than that, then they don't need to be + // resized, so we will increase the fair share and check other columns, until + // either none needs to be resized (all are smaller than the fair share) + // or all columns to be resized are larger than the fair share. + let last-share + let fair-share = none + let fair-share-should-change = true + + // 1. Rule out auto columns from resizing, and determine the final fair share + // (the largest possible such that no columns are smaller than it). + // One iteration of this 'while' runs for each attempt at a value for the fair + // share. Once no non-excluded columns are smaller than the fair share + // (which would otherwise lead to them being excluded from being resized, and the + // fair share would increase), the loop stops, and we can resize down all columns + // larger than the fair share. + // The loop also stops if all auto columns would be smaller than the fair share, + // and thus there is nothing to resize. + while fair-share-should-change and auto-cols-to-resize > 0 { + last-share = fair-share + fair-share = remaining / auto-cols-to-resize + fair-share-should-change = false + + for (_, col) in auto-cols { + // 1. If it is smaller than the fair share, + // then it can keep its size, and we should + // update the fair share. + // 2. If it is larger than the last fair share, + // then it wasn't already excluded in any previous + // iterations. + if col <= fair-share and (last-share == none or col > last-share) { + remaining -= col + auto-cols-to-resize -= 1 + fair-share-should-change = true + } } + } - // subtract AFTER the check!!! (Avoid off-by-one error) - auto_cols_remaining -= 1 - - if col < fair_share { // ok, keep your size, it's less than the limit - remaining -= col - - if auto_cols_remaining > 0 { - fair_share = remaining / auto_cols_remaining - } - } else { // you surpassed the limit!!! - remaining -= fair_share - columns.at(i) = fair_share + // 2. Resize any columns larger than the calculated fair share to the fair share. + for (i, col) in auto-cols { + if col > fair-share { + columns.at(i) = fair-share } } columns } -#let determine-column-sizes(grid: (), page_width: 0pt, styles: none, columns: none, inset: none, align: auto, col-gutter: none) = { +#let determine-column-sizes(grid: (), page_width: 0pt, styles: none, columns: none, inset: none, align: auto, col-gutter: none, fit-spans: none) = { let columns = columns.map(c => { - if type(c) in (_length_type, _rel_len_type, _ratio_type) { + if type(c) in (_length-type, _rel-len-type, _ratio-type) { convert-length-to-pt(c, styles: styles, page-size: page_width) } else if c == none { 0pt @@ -1304,7 +1430,7 @@ // what is the fixed size of the gutter? // (calculate it later if it's fractional) - let fixed-size-gutter = if type(col-gutter) == _length_type { + let fixed-size-gutter = if type(col-gutter) == _length-type { col-gutter } else { 0pt @@ -1317,7 +1443,7 @@ // page_width == 0pt => page width is 'auto' // so we don't have to restrict our table's size if available_size >= 0pt or page_width == 0pt { - let auto_cols_result = determine-auto-columns(grid: grid, styles: styles, columns: columns, inset: inset, align: align) + let auto_cols_result = determine-auto-columns(grid: grid, styles: styles, columns: columns, inset: inset, align: align, fit-spans: fit-spans, page-width: page_width) let total_auto_size = auto_cols_result.total let auto_sizes = auto_cols_result.sizes columns = auto_cols_result.columns @@ -1337,13 +1463,13 @@ if page_width != 0pt { columns = fit-auto-columns( available: available_size, - auto_cols: auto_sizes, + auto-cols: auto_sizes, columns: columns ) } columns = columns.map(c => { - if type(c) == _fraction_type { + if type(c) == _fraction-type { 0pt // no space left to be divided } else { c @@ -1352,7 +1478,7 @@ } } else { columns = columns.map(c => { - if c == auto or type(c) == _fraction_type { + if c == auto or type(c) == _fraction-type { 0pt // no space remaining! } else { c @@ -1371,16 +1497,13 @@ } // calculate the size of auto rows (based on the max height of their cells) -#let determine-auto-rows(grid: (), styles: none, columns: none, rows: none, align: auto, inset: none) = { +#let determine-auto-rows(grid: (), styles: none, columns: none, rows: none, align: auto, inset: none, fit-spans: none) = { assert(styles != none, message: "Cannot measure auto rows without styles") let total_auto_size = 0pt let auto_sizes = () let new_rows = rows - for i_row in enumerate(rows) { - let i = i_row.at(0) - let row = i_row.at(1) - + for (i, row) in rows.enumerate() { if row == auto { // max cell height let row_size = grid-get-row(grid, i) @@ -1390,12 +1513,22 @@ } let pcell = get-parent-cell(cell, grid: grid) // in case this is a rowspan - let last_auto_row = get-rowspan-last-auto-row(pcell, rows: rows) + let last-auto-row = get-rowspan-last-auto-row(pcell, rows: rows) + + let fit-this-span = if "fit-spans" in pcell and pcell.fit-spans != auto { + pcell.fit-spans.y + } else { + fit-spans.y + } + let this-cell-can-expand-rows = pcell.rowspan == 1 or not fit-this-span // only expand the last auto row of a rowspan, // and only the amount necessary that isn't already // covered by fixed size rows. - if last_auto_row == i { + // However, ignore this cell if it is a rowspan with + // `fit-spans.y == true` (it requests to not expand + // rows). + if last-auto-row == i and this-cell-can-expand-rows { let width = get-colspan-fixed-size-covered(pcell, columns: columns) // take extra inset as extra width or height on 'auto' @@ -1434,9 +1567,9 @@ (total: total_auto_size, sizes: auto_sizes, rows: new_rows) } -#let determine-row-sizes(grid: (), page_height: 0pt, styles: none, columns: none, rows: none, align: auto, inset: none, row-gutter: none) = { +#let determine-row-sizes(grid: (), page_height: 0pt, styles: none, columns: none, rows: none, align: auto, inset: none, row-gutter: none, fit-spans: none) = { let rows = rows.map(r => { - if type(r) in (_length_type, _rel_len_type, _ratio_type) { + if type(r) in (_length-type, _rel-len-type, _ratio-type) { convert-length-to-pt(r, styles: styles, page-size: page_height) } else { r @@ -1444,7 +1577,7 @@ }) let auto_rows_res = determine-auto-rows( - grid: grid, columns: columns, rows: rows, styles: styles, align: align, inset: inset + grid: grid, columns: columns, rows: rows, styles: styles, align: align, inset: inset, fit-spans: fit-spans ) let auto_size = auto_rows_res.total @@ -1452,7 +1585,7 @@ // what is the fixed size of the gutter? // (calculate it later if it's fractional) - let fixed-size-gutter = if type(row-gutter) == _length_type { + let fixed-size-gutter = if type(row-gutter) == _length-type { row-gutter } else { 0pt @@ -1469,7 +1602,7 @@ } else { ( rows: rows.map(r => { - if type(r) == _fraction_type { // no space remaining in this page or box + if type(r) == _fraction-type { // no space remaining in this page or box 0pt } else { r @@ -1492,13 +1625,15 @@ columns: none, rows: none, inset: none, gutter: none, align: auto, + fit-spans: none, ) = { let columns_res = determine-column-sizes( grid: grid, page_width: page_width, styles: styles, columns: columns, inset: inset, align: align, - col-gutter: gutter.col + col-gutter: gutter.col, + fit-spans: fit-spans ) columns = columns_res.columns gutter.col = columns_res.gutter @@ -1510,7 +1645,8 @@ rows: rows, inset: inset, align: align, - row-gutter: gutter.row + row-gutter: gutter.row, + fit-spans: fit-spans ) rows = rows_res.rows gutter.row = rows_res.gutter @@ -1710,7 +1846,7 @@ #let parse-stroke(stroke) = { if is-color(stroke) { stroke + 1pt - } else if type(stroke) in (_length_type, _rel_len_type, _ratio_type, _stroke_type, _dict_type) or stroke in (none, auto) { + } else if type(stroke) in (_length-type, _rel-len-type, _ratio-type, _stroke-type, _dict-type) or stroke in (none, auto) { stroke } else { panic("Invalid stroke '" + repr(stroke) + "'.") @@ -1725,7 +1861,7 @@ if line.expand in (none, (none, none), auto, (auto, auto)) { return (none, none) } - if type(line.expand) != _array_type { + if type(line.expand) != _array-type { line.expand = (line.expand, line.expand) } @@ -1950,8 +2086,7 @@ let group-rows = row-group.rows let hlines = row-group.hlines let vlines = row-group.vlines - let start-y = row-group.y_span.at(0) - let end-y = row-group.y_span.at(1) + let (start-y, end-y) = row-group.y_span locate(loc => { // let old_page = latest-page-state.at(loc) @@ -1978,7 +2113,7 @@ if page_turned and at_top and not is-header { if repeat-header != false { header-pages-state.update(l => l + (page,)) - if (repeat-header == true) or (type(repeat-header) == _int_type and rel_page <= repeat-header) or (type(repeat-header) == _array_type and rel_page in repeat-header) { + if (repeat-header == true) or (type(repeat-header) == _int-type and rel_page <= repeat-header) or (type(repeat-header) == _array-type and rel_page in repeat-header) { let measures = measure(first-row-group.content, styles) place(top+left, first-row-group.content) // add header added_header_height = measures.height @@ -1992,14 +2127,10 @@ let first_y = none let rightmost_x = none - let row_heights = 0pt + let row_heights = array-sum(rows.slice(start-y, end-y + 1), zero: 0pt) let first_row = true for row in group-rows { - if row.len() > 0 { - let first_cell = row.at(0) - row_heights += rows.at(first_cell.cell.y) - } for cell_box in row { let x = cell_box.cell.x let y = cell_box.cell.y @@ -2248,11 +2379,10 @@ if row_group_add_counter <= 0 and header_rows_count <= 0 { row_group_add_counter = 1 - let row_group = this_row_group + let row-group = this_row_group // get where the row starts and where it ends - let start_y = row_group.y_span.at(0) - let end_y = row_group.y_span.at(1) + let (start_y, end_y) = row-group.y_span let next_y = end_y + 1 @@ -2260,7 +2390,7 @@ let is_header = first_row_group == none let content = draw-row-group( - row_group, + row-group, is-header: is_header, header-pages-state: header_pages, first-row-group: first_row_group, @@ -2280,7 +2410,7 @@ ) if is_header { // this is now the header group. - first_row_group = (row_group: row_group, content: content) // 'content' to repeat later + first_row_group = (row_group: row-group, content: content) // 'content' to repeat later } (content,) @@ -2303,7 +2433,7 @@ let parse-func(line, page-size: none) = { line.stroke-expand = line.stroke-expand == true line.expand = default-if-auto(line.expand, none) - if type(line.expand) != _array_type and line.expand != none { + if type(line.expand) != _array-type and line.expand != none { line.expand = (line.expand, line.expand) } line.expand = if line.expand == none { @@ -2314,7 +2444,7 @@ e } else { e = default-if-auto(e, 0pt) - if type(e) not in (_length_type, _rel_len_type, _ratio_type) { + if type(e) not in (_length-type, _rel-len-type, _ratio-type) { panic("'expand' argument to lines must be a pair (length, length).") } @@ -2362,31 +2492,24 @@ col-gutter = default-if-auto(col-gutter, 0pt) row-gutter = default-if-auto(row-gutter, 0pt) - if type(col-gutter) in (_length_type, _rel_len_type, _ratio_type) { + if type(col-gutter) in (_length-type, _rel-len-type, _ratio-type) { col-gutter = convert-length-to-pt(col-gutter, styles: styles, page-size: page-width) } - if type(row-gutter) in (_length_type, _rel_len_type, _ratio_type) { + if type(row-gutter) in (_length-type, _rel-len-type, _ratio-type) { row-gutter = convert-length-to-pt(row-gutter, styles: styles, page-size: page-width) } (col: col-gutter, row: row-gutter) } -// Accepts a map-X param, and returns its default, or validates -// it. -#let parse-map-func(map-func, uses-second-param: false) = { - if map-func in (none, auto) { - if uses-second-param { - (a, b) => b // identity - } else { - o => o // identity - } - } else if type(map-func) != _function_type { - panic("Map parameters must be functions.") - } else { - map-func +// Accepts a map-X param, and verifies whether it's a function or none/auto. +#let validate-map-func(map-func) = { + if map-func not in (none, auto) and type(map-func) != _function-type { + panic("Tablex error: Map parameters, if specified (not 'none'), must be functions.") } + + map-func } #let apply-maps( @@ -2398,113 +2521,129 @@ map-rows: none, map-cols: none, ) = { - vlines = vlines.map(map-vlines) - if vlines.any(h => not is-tablex-vline(h)) { - panic("'map-vlines' function returned a non-vline.") + if type(map-vlines) == _function-type { + vlines = vlines.map(vline => { + let vline = map-vlines(vline) + if not is-tablex-vline(vline) { + panic("'map-vlines' function returned a non-vline.") + } + vline + }) } - hlines = hlines.map(map-hlines) - if hlines.any(h => not is-tablex-hline(h)) { - panic("'map-hlines' function returned a non-hline.") + if type(map-hlines) == _function-type { + hlines = hlines.map(hline => { + let hline = map-hlines(hline) + if not is-tablex-hline(hline) { + panic("'map-hlines' function returned a non-hline.") + } + hline + }) } - let col_len = grid.width - let row_len = grid-count-rows(grid) + let should-map-rows = type(map-rows) == _function-type + let should-map-cols = type(map-cols) == _function-type - for row in range(row_len) { - let original_cells = grid-get-row(grid, row) + if not should-map-rows and not should-map-cols { + return (grid: grid, hlines: hlines, vlines: vlines) + } - // occupied cells = none for the outer user - let cells = map-rows(row, original_cells.map(c => { - if is-tablex-occupied(c) { none } else { c } - })) + let col-len = grid.width + let row-len = grid-count-rows(grid) - if type(cells) != _array_type { - panic("Tablex error: 'map-rows' returned something that isn't an array.") - } + if should-map-rows { + for row in range(row-len) { + let original-cells = grid-get-row(grid, row) - // only modify non-occupied cells - let cells = enumerate(cells).filter(i_c => is-tablex-cell(original_cells.at(i_c.at(0)))) + // occupied cells = none for the outer user + let cells = map-rows(row, original-cells.map(c => { + if is-tablex-occupied(c) { none } else { c } + })) - if cells.any(i_c => not is-tablex-cell(i_c.at(1))) { - panic("Tablex error: 'map-rows' returned a non-cell.") - } + if type(cells) != _array-type { + panic("Tablex error: 'map-rows' returned something that isn't an array.") + } - if cells.any(i_c => { - let c = i_c.at(1) - let x = c.x - let y = c.y - type(x) != _int_type or type(y) != _int_type or x < 0 or y < 0 or x >= col_len or y >= row_len - }) { - panic("Tablex error: 'map-rows' returned a cell with invalid coordinates.") - } + if cells.len() != original-cells.len() { + panic("Tablex error: 'map-rows' returned " + str(cells.len()) + " cells, when it should have returned exactly " + str(original-cells.len()) + ".") + } - if cells.any(i_c => i_c.at(1).y != row) { - panic("Tablex error: 'map-rows' returned a cell in a different row (the 'y' must be kept the same).") - } + for (i, cell) in cells.enumerate() { + let orig-cell = original-cells.at(i) + if not is-tablex-cell(orig-cell) { + // only modify non-occupied cells + continue + } - if cells.any(i_c => { - let i = i_c.at(0) - let c = i_c.at(1) - let orig_c = original_cells.at(i) + if not is-tablex-cell(cell) { + panic("Tablex error: 'map-rows' returned a non-cell.") + } - c.colspan != orig_c.colspan or c.rowspan != orig_c.rowspan - }) { - panic("Tablex error: Please do not change the colspan or rowspan of a cell in 'map-rows'.") - } + let x = cell.x + let y = cell.y - for i_cell in cells { - let cell = i_cell.at(1) - grid.items.at(grid-index-at(cell.x, cell.y, grid: grid)) = cell - } - } + if type(x) != _int-type or type(y) != _int-type or x < 0 or y < 0 or x >= col-len or y >= row-len { + panic("Tablex error: 'map-rows' returned a cell with invalid coordinates.") + } - for column in range(col_len) { - let original_cells = grid-get-column(grid, column) + if y != row { + panic("Tablex error: 'map-rows' returned a cell in a different row (the 'y' must be kept the same).") + } - // occupied cells = none for the outer user - let cells = map-cols(column, original_cells.map(c => { - if is-tablex-occupied(c) { none } else { c } - })) + if cell.colspan != orig-cell.colspan or cell.rowspan != orig-cell.rowspan { + panic("Tablex error: Please do not change the colspan or rowspan of a cell in 'map-rows'.") + } - if type(cells) != _array_type { - panic("Tablex error: 'map-cols' returned something that isn't an array.") + cell.content = [#cell.content] + grid.items.at(grid-index-at(cell.x, cell.y, grid: grid)) = cell + } } + } - // only modify non-occupied cells - let cells = enumerate(cells).filter(i_c => is-tablex-cell(original_cells.at(i_c.at(0)))) + if should-map-cols { + for column in range(col-len) { + let original-cells = grid-get-column(grid, column) - if cells.any(i_c => not is-tablex-cell(i_c.at(1))) { - panic("Tablex error: 'map-cols' returned a non-cell.") - } + // occupied cells = none for the outer user + let cells = map-cols(column, original-cells.map(c => { + if is-tablex-occupied(c) { none } else { c } + })) - if cells.any(i_c => { - let c = i_c.at(1) - let x = c.x - let y = c.y - type(x) != _int_type or type(y) != _int_type or x < 0 or y < 0 or x >= col_len or y >= row_len - }) { - panic("Tablex error: 'map-cols' returned a cell with invalid coordinates.") - } + if type(cells) != _array-type { + panic("Tablex error: 'map-cols' returned something that isn't an array.") + } - if cells.any(i_c => i_c.at(1).x != column) { - panic("Tablex error: 'map-cols' returned a cell in a different column (the 'x' must be kept the same).") - } + if cells.len() != original-cells.len() { + panic("Tablex error: 'map-cols' returned " + str(cells.len()) + " cells, when it should have returned exactly " + str(original-cells.len()) + ".") + } - if cells.any(i_c => { - let i = i_c.at(0) - let c = i_c.at(1) - let orig_c = original_cells.at(i) + for (i, cell) in cells.enumerate() { + let orig-cell = original-cells.at(i) + if not is-tablex-cell(orig-cell) { + // only modify non-occupied cells + continue + } - c.colspan != orig_c.colspan or c.rowspan != orig_c.rowspan - }) { - panic("Tablex error: Please do not change the colspan or rowspan of a cell in 'map-cols'.") - } + if not is-tablex-cell(cell) { + panic("Tablex error: 'map-cols' returned a non-cell.") + } + + let x = cell.x + let y = cell.y + + if type(x) != _int-type or type(y) != _int-type or x < 0 or y < 0 or x >= col-len or y >= row-len { + panic("Tablex error: 'map-cols' returned a cell with invalid coordinates.") + } + if x != column { + panic("Tablex error: 'map-cols' returned a cell in a different column (the 'x' must be kept the same).") + } + if cell.colspan != orig-cell.colspan or cell.rowspan != orig-cell.rowspan { + panic("Tablex error: Please do not change the colspan or rowspan of a cell in 'map-cols'.") + } - for i_cell in cells { - let cell = i_cell.at(1) - cell.content = [#cell.content] - grid.items.at(grid-index-at(cell.x, cell.y, grid: grid)) = cell + cell.content = [#cell.content] + grid.items.at(grid-index-at(cell.x, cell.y, grid: grid)) = cell + } } } @@ -2514,7 +2653,7 @@ #let validate-header-rows(header-rows) = { header-rows = default-if-auto(default-if-none(header-rows, 0), 1) - if type(header-rows) != _int_type or header-rows < 0 { + if type(header-rows) != _int-type or header-rows < 0 { panic("Tablex error: 'header-rows' must be a (positive) integer.") } @@ -2528,9 +2667,9 @@ repeat-header = default-if-auto(default-if-none(repeat-header, false), false) - if type(repeat-header) not in (_bool_type, _int_type, _array_type) { + if type(repeat-header) not in (_bool-type, _int-type, _array-type) { panic("Tablex error: 'repeat-header' must be a boolean (true - always repeat the header, false - never), an integer (amount of pages for which to repeat the header), or an array of integers (relative pages in which the header should repeat).") - } else if type(repeat-header) == _array_type and repeat-header.any(i => type(i) != _int_type) { + } else if type(repeat-header) == _array-type and repeat-header.any(i => type(i) != _int-type) { panic("Tablex error: 'repeat-header' cannot be an array of anything other than integers!") } @@ -2542,13 +2681,15 @@ ) = { header-hlines-have-priority = default-if-auto(default-if-none(header-hlines-have-priority, true), true) - if type(header-hlines-have-priority) != _bool_type { + if type(header-hlines-have-priority) != _bool-type { panic("Tablex error: 'header-hlines-have-priority' option must be a boolean.") } header-hlines-have-priority } +// 'validate-fit-spans' is needed by grid, and is thus in the common section + // -- end: option parsing // Creates a table. @@ -2636,6 +2777,30 @@ // cannot be sent to another row. Also, cells may be // 'none' if they're a position taken by a cell in a // colspan/rowspan. +// +// fit-spans: Determine if rowspans and colspans should fit within their +// spanned 'auto'-sized tracks (columns and rows) instead of causing them to +// expand based on the rowspan/colspan cell's size. (Most users of tablex +// shouldn't have to change this option.) +// Must either be a dictionary '(x: true/false, y: true/false)' or a boolean +// true/false (which is converted to the (x: value, y: value) format with both +// 'x' and 'y' being set to the same value; for instance, 'true' becomes +// '(x: true, y: true)'). +// Setting 'x' to 'false' (the default) means that colspans will cause the last +// (rightmost) auto column they span to expand if the cell's contents are too +// long; setting 'x' to 'true' negates this, and auto columns will ignore the +// size of colspans. Similarly, setting 'y' to 'false' (the default) means that +// rowspans will cause the last (bottommost) auto row they span to expand if +// the cell's contents are too tall; setting 'y' to 'true' causes auto rows to +// ignore the size of rowspans. +// This setting is mostly useful when you have a colspan or a rowspan spanning +// tracks with fractional (1fr, 2fr, ...) size, which can cause the fractional +// track to have less or even zero size, compromising all other cells in it. +// If you're facing this problem, you may want experiment with setting this +// option to '(x: true)' (if this is affecting columns) or 'true' (for rows +// too, same as '(x: true, y: true)'). +// Note that this option can also be set in a per-cell basis through cellx(). +// See its reference for more information. #let tablex( columns: auto, rows: auto, inset: 5pt, @@ -2656,6 +2821,7 @@ map-vlines: none, map-rows: none, map-cols: none, + fit-spans: false, ..items ) = { _tablex-table-counter.step() @@ -2665,11 +2831,12 @@ let header-rows = validate-header-rows(header-rows) let repeat-header = validate-repeat-header(repeat-header, header-rows: header-rows) let header-hlines-have-priority = validate-header-hlines-priority(header-hlines-have-priority) - let map-cells = parse-map-func(map-cells) - let map-hlines = parse-map-func(map-hlines) - let map-vlines = parse-map-func(map-vlines) - let map-rows = parse-map-func(map-rows, uses-second-param: true) - let map-cols = parse-map-func(map-cols, uses-second-param: true) + let map-cells = validate-map-func(map-cells) + let map-hlines = validate-map-func(map-hlines) + let map-vlines = validate-map-func(map-vlines) + let map-rows = validate-map-func(map-rows) + let map-cols = validate-map-func(map-cols) + let fit-spans = validate-fit-spans(fit-spans, default: (x: false, y: false)) layout(size => locate(t_loc => style(styles => { let table_id = _tablex-table-counter.at(t_loc) @@ -2707,7 +2874,8 @@ let grid_info = generate-grid( items, x_limit: col_len, y_limit: row_len, - map-cells: map-cells + map-cells: map-cells, + fit-spans: fit-spans ) let table_grid = grid_info.grid @@ -2715,8 +2883,11 @@ let vlines = grid_info.vlines let items = grid_info.items + // When there are more rows than the user specified, we ensure they have + // the same size as the last specified row. + let last-row-size = if rows.len() == 0 { auto } else { rows.last() } for _ in range(grid_info.new_row_count - row_len) { - rows.push(auto) // add new rows (due to extra cells) + rows.push(last-row-size) // add new rows (due to extra cells) } let col_len = columns.len() @@ -2764,7 +2935,8 @@ styles: styles, columns: columns, rows: rows, inset: inset, align: align, - gutter: gutter + gutter: gutter, + fit-spans: fit-spans ) let columns = updated_cols_rows.columns diff --git a/typst.toml b/typst.toml index a6a84a4..6fde0d6 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] name = "tablex" -version = "0.0.7" +version = "0.0.8" authors = ["PgBiel