diff --git a/README.md b/README.md index d39c2e7..f13572b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# typst-tablex (v0.0.5) +# typst-tablex (v0.0.6) **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!)** @@ -17,6 +17,7 @@ * [Basic types and functions](#basic-types-and-functions) * [Gridx and Tablex](#gridx-and-tablex) * [Changelog](#changelog) + * [v0.0.6](#v006) * [v0.0.5](#v005) * [v0.0.4](#v004) * [v0.0.3](#v003) @@ -27,11 +28,11 @@ ## Usage -To use this library through the Typst package manager **(for Typst v0.6.0+)**, write for example `#import "@preview/tablex:0.0.5": 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.6": 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 and v0.6.0. +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 and v0.8.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!). Here's an example of what `tablex` can do: @@ -39,8 +40,8 @@ Here's an example of what `tablex` can do: ![image](https://github.com/PgBiel/typst-tablex/assets/9021226/355c527a-7296-4264-bac7-4ec991b15a18) Here's the code for that table: -```js -#import "@preview/tablex:0.0.5": tablex, rowspanx, colspanx +```typ +#import "@preview/tablex:0.0.6": tablex, rowspanx, colspanx #tablex( columns: 4, @@ -91,8 +92,8 @@ 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: -```js -#import "@preview/tablex:0.0.5": tablex +```typ +#import "@preview/tablex:0.0.6": tablex #tablex( columns: (auto, 1em, 1fr, 1fr), // 4 columns @@ -112,12 +113,14 @@ There are still a few oddities in the library (see [Known Issues](#known-issues) This is mostly a word of caution in case anything I haven't anticipated happens, but, based on my tests (and after tons of bug-fixing commits), the vast majority of tables (that don't face one of the listed known issues) should work just fine under the library. +**Note:** If your document is written in a right-to-left (RTL) script, you may wish to enable `rtl: true` for your tables so that the order of cells and lines properly follows your text direction (when combined with `set text(dir: rtl)`). This is necessary because tablex cannot detect that setting automatically at the moment (while the native Typst table can and flips itself horizontally automatically). See the tablex option reference for more information. + ### colspanx/rowspanx Your cells can now span more than one column and/or row at once, with `colspanx` / `rowspanx`: -```js -#import "@preview/tablex:0.0.5": tablex, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, colspanx, rowspanx #tablex( columns: 3, @@ -145,8 +148,8 @@ Also, note that, by default, the horizontal lines below the header are transport Example: -```js -#import "@preview/tablex:0.0.5": tablex, hlinex, vlinex, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, hlinex, vlinex, colspanx, rowspanx #pagebreak() #v(80%) @@ -192,8 +195,8 @@ Something similar occurs for `vlinex()`, which has `start`, `end` (first row and Here's some sample usage: -```js -#import "@preview/tablex:0.0.5": tablex, gridx, hlinex, vlinex, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, gridx, hlinex, vlinex, colspanx, rowspanx #tablex( columns: 4, @@ -236,8 +239,8 @@ 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: -```js -#import "@preview/tablex:0.0.5": tablex, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, colspanx, rowspanx #tablex( columns: 3, @@ -268,8 +271,8 @@ Additionally, instead of specifying content to the cell, you can specify a funct For example: -```js -#import "@preview/tablex:0.0.5": tablex, cellx, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, cellx, colspanx, rowspanx #tablex( columns: 3, @@ -300,8 +303,8 @@ To customize multiple cells at once, you have a few options: Example: -```js -#import "@preview/tablex:0.0.5": tablex, colspanx, rowspanx +```typ +#import "@preview/tablex:0.0.6": tablex, colspanx, rowspanx #tablex( columns: 4, @@ -342,7 +345,7 @@ Example: Another example (summing columns): -```js +```typ #gridx( columns: 3, rows: 6, @@ -378,6 +381,13 @@ Another example (summing columns): - 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. +- 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). + This is a [known issue](https://github.com/typst/typst/issues/528) with Typst (perhaps, in the future, `#rotate` [may](https://github.com/typst/typst/issues/528#issuecomment-1494123195) get a setting to affect layout). + As a workaround for the text hyphenation problem, the content can be boxed (and thus grouped together) with `#box` (e.g., `rowspanx(7, box(rotate(-90deg, [*donothyphenatethis*])))`), or hyphenation can be prevented by setting `#text(hyphenate: false, ...)` (e.g., `colspanx(2, text(hyphenate: false, rotate(-90deg, [*donothyphenatethis*])))`), as also discussed in https://github.com/PgBiel/typst-tablex/issues/59; + another alternative is to use `#place`, e.g. aligning to `center + horizon`: `cellx(place(center + horizon, rotate(-90deg, [*donothyphenatethis*])))`, which probably allows the most control over the in-cell layout, since it simply draws the rotated content without having it occupy any space (letting you define that by yourself, e.g. using `box(width: 1em, height: 2em, place(...))`). + - Alternatively, you may attempt to use the solution proposed at https://github.com/typst/typst/issues/528#issuecomment-1494318510 to define a `rotatex` function which produces a rotated element with the appropriate sizes, such that tablex may recognize its size accordingly and avoid visual glitches. + - `tablex` can potentially be slower and/or take longer to compile than the default `table` (especially when the table spans a lot of pages). **Please use the latest Typst version to reduce this problem** (each version has been bringing further improvements in this sense). Still, we are looking for ways to better optimize the library (see more discussion at https://github.com/PgBiel/typst-tablex/issues/5 - feel free to give some input!). However, re-compilation is usually fine thanks to Typst's built-in memoization. - The internals of the library still aren't very well documented; I plan on adding more info about this eventually. @@ -390,7 +400,7 @@ Another example (summing columns): 1. `cellx`: Represents a table cell, and is initialized as follows: - ```js + ```typ #let cellx(content, x: auto, y: auto, rowspan: 1, colspan: 1, @@ -422,7 +432,7 @@ Another example (summing columns): 2. `hlinex`: represents a horizontal line: - ```js + ```typ #let hlinex( start: 0, end: auto, y: auto, stroke: auto, @@ -459,7 +469,7 @@ Another example (summing columns): 3. `vlinex`: represents a vertical line: - ```js + ```typ #let vlinex( start: 0, end: auto, x: auto, stroke: auto, @@ -507,7 +517,7 @@ Another example (summing columns): 2. `tablex:` The main function for creating a table with this library: - ```js + ```typ #let tablex( columns: auto, rows: auto, inset: 5pt, @@ -535,17 +545,31 @@ Another example (summing columns): **Parameters:** - - `columns`: The sizes of each column. They work just like regular `table`'s columns, and can be: + - `columns`: The sizes (widths) of each column. They work just like regular `table`'s columns, and can be: - an array of lengths (`1pt`, `2em`, `100%`, ...), including fractional (`2fr`), to specify the width of each column - - `auto` may be specified to automatically resize the column based on the space available - - when specifying fractional columns, the available space is divided between them, weighted on the fraction value of each column + - For instance, `columns: (2pt, 3em)` will give you two columns: one with a width of `2pt` and another with the width of `3em` (3 times the font size). + - Note that percentages, such as `49%`, **are considered fixed widths** as they are **always multiplied by the full page width** (minus margins) for columns. Thus, a column with a size of `100%` would span your whole page (even if there are other columns). + - `auto` may be specified to automatically resize the column based on the largest width of its contents, if possible - **this is the most common column width choice,** as it just delegates the column sizing job to tablex! + - For example, if your `auto`-sized column contains two cells with `Hello world!` and `Bye!` as contents, tablex will try to make the column large enough for `Hello world!` (the cell with largest _potential_ width) to fit in a single line. + - However, note that often enough that's not possible, as increasing the column's size too much would result in the table going over the page's margin - perhaps even beyond the document's total width. Therefore, **tablex will automatically reduce the size of your `auto` columns** when they would otherwise cause the table to overrun the page's normal width (i.e. the width between the page's lateral margins). + - Fixed width columns (such as `2pt`, `3em` or `49%`) are not subject to this size reduction; thus, if you specify all columns' widths with fixed lengths, your table _could_ become larger than the page's width! (In such a case, **`auto` columns would be reduced to a size of zero,** as there would be no available space anymore!) + - when specifying fractional widths (`1fr`, `2fr`...) for columns, the available space (remaining page width, after calculating all other columns' sizes) is divided between them, weighted on the fraction value of each column. - For example, with `(1fr, 2fr)`, the available space will be divided by 3 (1 + 2), and the first column will have 1/3 of the space, while the second will have 2/3. + - `(1fr, 1fr)` would cause both columns to have equal length (1/2 and 1/2 of the available space). + - This is useful when you want some columns to just occupy all the remaining horizontal space in the page. + - **Note:** If only one column has a fractional width (e.g. a single column with `1fr`), it will occupy the entire available space. + - **Warning:** fractional columns in tablex (much like in Typst's default tables) **will not work properly in pages with `auto` width** (the columns will have width zero) - this is because those pages theoretically have infinite width (they can expand indefinitely), so having columns spanning the entire available width is then impossible! - a single length like above, to indicate the width of a single column (equivalent to just placing it inside a unit array) + - For instance, `columns: 2pt` is equivalent to `columns: (2pt,)`, which translates to a single column of width `2pt`. - an integer (such as `4`), as a shorthand for `(auto,) * 4` (that many `auto` columns) - - `rows`: The sizes of each row. They follow the exact same format as `columns`, except that the "available space" is infinite (auto rows can expand as much as is needed, as the table can add rows over multiple pages). + - Useful if you just want to quickly set the amount of columns without worrying about their sizes (`columns: 4` will give you four `auto` columns). + - `rows`: The sizes (heights) of each row. They follow the exact same format as `columns`, except that the "available space" is infinite (auto rows can expand as much as is needed, as the table can add rows over multiple pages). + - **Note:** For rows, percentages (such as `49%`) are fixed width lengths, like in `columns`; however, here, they are **multiplied by the page's full height** (minus margins), and not width. - **Note:** If more rows than specified are added, the height for the **last row** will be the one assigned to all extra rows. (If the last row is `auto`, the extra ones will also be `auto`, for example.) + - Your table can have more rows than expected by simply having more cells than `(# columns)` multipled by `(# rows)`. In this case, you will have an extra row for each `(# columns)` cells after the limit. In other words, **the amount of columns is always fixed** (determined by the amount of widths in the array given to `columns`), but the amount of rows can vary depending on your input of cells to the table. + - Adding a cell at an arbitrary `y` coordinate can also cause your table to have extra rows (enough rows to reach the cell at that coordinate). - **Warning:** support for fractional sizes for rows is still rudimentary - they only work properly on the table's first page; on the second page and onwards, they will not behave properly, differently from the default `#table`. - - `inset`: Inset/internal padding to give to each cell. Defaults to `5pt` (the `#table` default). + - `inset`: Inset/internal padding to give to each cell. Can be either a length (same inset from the top, bottom, left and right of the cell), or a dictionary (e.g. `(left: 5pt, right: 10pt, bottom: 2pt, top: 4pt)`, or even `(left: 5pt, rest: 10pt)` to apply the same value to the remaining sides). Defaults to `5pt` (the `#table` default). - `align`: How to align text in the cells. Defaults to `auto`, which inherits alignment from the outer context. Must be either `auto`, an `alignment` (such as `left` or `top`), a `2d alignment` (such as `left + top`), an `array` of alignment/2d alignment (one for each column in the table - if there are more columns than alignment values, they will alternate); or a function `(column, row) => alignment/2d alignment` (to customize for each individual cell). @@ -574,6 +598,11 @@ Another example (summing columns): Defaults to `true`. - For example, if your header has a blue hline under it, that blue hline will display on all pages it is repeated on if this option is `true`. If this option is `false`, the header will repeat, but the blue hline will not. + - `rtl`: if true, the table is horizontally flipped. That is, cells and lines are placed in the opposite order (starting from the right), and horizontal lines are flipped. + This is meant to simulate the behavior of default Typst tables when `set text(dir: rtl)` is used, + and is useful when writing in a language with a RTL (right-to-left) script. + Defaults to `false`. + - `auto-lines`: Shorthand to apply a boolean to both `auto-hlines` and `auto-vlines` at the same time (overridable by each). Defaults to `true`. - `auto-hlines`: If `true`, draw a horizontal line on every line where you did not manually draw one; if `false`, no hlines other than the ones you specify (via `hlinex`) are drawn. Defaults to `auto` (follows `auto-lines`, which in turn defaults to `true`). @@ -598,6 +627,18 @@ Another example (summing columns): ## Changelog +### v0.0.6 + +- Added support for RTL tables with `rtl: true` (https://github.com/PgBiel/typst-tablex/issues/58). + - Default Typst tables are automatically flipped horizontally when using `set text(dir: rtl)`, however we can't detect that setting from tablex at this moment (it isn't currently possible to fetch set rules in Typst). + - Therefore, as a way around that, you can now specify `#tablex(rtl: true, ...)` to flip your table horizontally if you're writing a document in RTL (right-to-left) script. (You can use e.g. `#let old-tablex = tablex` followed by `#let tablex(..args) = old-tablex(rtl: true, ..args)` to not have to repeat the `rtl` parameter every time.) +- Added support for `box`'s dictionary inset syntax on tablex (https://github.com/PgBiel/typst-tablex/issues/54). + - For instance, you can now do `#tablex(inset: (left: 5pt, top: 10pt, rest: 2pt), ...)`. +- Fixed errors when using floating point strokes or other more complex strokes (https://github.com/PgBiel/typst-tablex/issues/55). +- Added full compatibility with the new Typst 0.8.0 type system (https://github.com/PgBiel/typst-tablex/issues/69). +- Added info about `#rotate` problems to "Known Issues" in the README (https://github.com/PgBiel/typst-tablex/pull/60). +- Improved docs for tablex options `columns` and `rows` (https://github.com/PgBiel/typst-tablex/issues/53). + ### v0.0.5 - ⚠️ **Minimum Typst version raised to v0.2.0** diff --git a/tablex-test.pdf b/tablex-test.pdf index 3c9e18f..6ec19d7 100644 Binary files a/tablex-test.pdf and b/tablex-test.pdf differ diff --git a/tablex-test.typ b/tablex-test.typ index 321aa14..ea9705c 100644 --- a/tablex-test.typ +++ b/tablex-test.typ @@ -611,3 +611,153 @@ Test fractional columns in an auto-sized block: // stroke: s, // [C], [C] // ) + +*Stroke parsing regression from issue \#55:* + +Red stroke: + +#let s = rect(stroke: red).stroke +#tablex( + stroke: s, + [a] +) + +Thick stroke with a decimal point: + +#tablex(columns: 2, stroke: 5.1pt + black)[a][b] + +Combining em and pt: + +#tablex(columns: 2, stroke: (2.5pt + 0.75em) + black)[a][b] + +Combining em and pt (with a stroke object): + +#let s = rect(stroke: (2.5pt + 0.75em) + black).stroke +#tablex( + columns: 2, + stroke: s, + [a], [b] +) + +*Dictionary insets from issue \#54:* + +#tablex( + columns: 3, + inset: (left: 20pt, rest: 10pt), + [A], [B], [C] +) + +#tablex( + columns: 2, + inset: ( + left: 20pt, + right: 5pt, + top: 10pt, + bottom: 3pt, + ), + [A], + [B], +) + +#tablex( + columns: 2, + [a], [b], + [c], cellx(inset: (left: 2pt, right: 5pt, top: 10pt, bottom: 1pt))[d], + cellx(inset: (left: 5pt, rest: 10pt))[e], [f] +) + +*RTL tables from issue \#58:* + +#[ +- Simple +#let simple(rtl) = tablex( + columns: 3, + rtl: rtl, + [a], [b], [c], + [d], [e], [f], + [g], [h], [i] +) +#stack(dir: ltr, simple(false), 1em, $->$, 1em, simple(true)) + +- Colspan, rowspan +#let colspanrowspan(rtl) = tablex( + columns: 3, + rtl: rtl, + [a], colspanx(2)[d], (), + [d], [e], rowspanx(2)[f], + [g], [h], (), +) +#stack(dir: ltr, colspanrowspan(false), 1em, $->$, 1em, colspanrowspan(true)) + +- No vertical lines +#let novertlines(rtl) = tablex( + columns: 3, + rtl: rtl, + auto-vlines: false, + stroke: red, + [a], colspanx(2)[d], (), + [b], [b], [b], + [d], [e], rowspanx(2)[f], + [g], [h], (), +) +#stack(dir: ltr, novertlines(false), 1em, $->$, 1em, novertlines(true)) + +- Line customization +#let linecustom(rtl) = tablex( + columns: 3, + rtl: rtl, + auto-lines: false, + (), vlinex(end: 1, stroke: blue), + [a], colspanx(2)[d], (), + [b], [b], [b], + hlinex(end: 2, stroke: red), + [d], [e], rowspanx(2)[f], + [g], [h], (), +) +#stack(dir: ltr, linecustom(false), 1em, $->$, 1em, linecustom(true)) + +- Alignment and fill +#set text(dir: rtl) +#let alignfill(rtl) = tablex( + columns: 3, + rtl: rtl, + align: (end, start, end), + fill: (x, y) => (red, green, blue, yellow).at(y).darken(20% * x), + [aaaa], colspanx(2)[ddddd], (), + [b], [bdd], [bd], + [d], [e], rowspanx(2)[f], + [g], [h], (), +) +#stack(dir: ltr, alignfill(false), 1em, $->$, 1em, alignfill(true)) + +- Map cells, map rows, map cols +#let mapstuff(rtl) = tablex( + columns: 3, + rtl: rtl, + align: (end, start, end), + fill: (x, y) => (red, green, blue, yellow).at(y).darken(20% * x), + map-rows: (y, cells) => { + cells.map(cell => { + if cell == none { return none } + cell.content = [#cell.content | y = #y] + cell + }) + }, + map-cols: (x, cells) => { + cells.map(cell => { + if cell == none { return none } + cell.content = [#cell.content | x = #x] + cell + }) + }, + map-cells: cell => { + cell.content = [#cell.content | HI] + cell + }, + [aaaa], colspanx(2)[ddddd], (), + [b], [bdd], [bd], + [d], [e], rowspanx(2)[f], + [g], [h], (), +) +#stack(dir: ttb, mapstuff(false), 1em, $arrow.b$, 1em, mapstuff(true)) +] diff --git a/tablex.typ b/tablex.typ index 9516a65..661c17e 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.5 +// Version: v0.0.6 // -- table counter -- @@ -12,6 +12,26 @@ 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 _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) + // ------------ // -- types -- @@ -85,7 +105,7 @@ // Is this a valid dict created by this library? #let is-tablex-dict(x) = ( - type(x) == "dictionary" + type(x) == _dict_type and "tablex-dict-type" in x ) @@ -101,11 +121,11 @@ #let is-tablex-occupied(x) = is-tablex-dict-type(x, "occupied") #let table-item-convert(item, keep_empty: true) = { - if type(item) == "function" { // dynamic cell content + if type(item) == _function_type { // dynamic cell content cellx(item) } else if keep_empty and item == () { item - } else if type(item) != "dictionary" or "tablex-dict-type" not in item { + } else if type(item) != _dict_type or "tablex-dict-type" not in item { cellx[#item] } else { item @@ -144,7 +164,7 @@ .filter(c => c.y != auto) .fold(0, (acc, cell) => { if (is-tablex-cell(cell) - and type(cell.y) in ("integer", "float") + and type(cell.y) in (_int_type, _float_type) and cell.y > acc) { cell.y } else { @@ -156,7 +176,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" { + } else if type(item) == _content_type { len += 1 } } @@ -172,26 +192,26 @@ // Check if this length is infinite. #let is-infinite-len(len) = { - type(len) in ("ratio", "fraction", "relative length", "length") and "inf" in repr(len) + type(len) in (_ratio_type, _fraction_type, _rel_len_type, _length_type) and "inf" in repr(len) } #let validate-cols-rows(columns, rows, items: ()) = { - if type(columns) == "integer" { + if type(columns) == _int_type { assert(columns >= 0, message: "Error: Cannot have a negative amount of columns.") columns = (auto,) * columns } - if type(rows) == "integer" { + if type(rows) == _int_type { assert(rows >= 0, message: "Error: Cannot have a negative amount of rows.") rows = (auto,) * rows } - if type(columns) != "array" { + if type(columns) != _array_type { columns = (columns,) } - if type(rows) != "array" { + if type(rows) != _array_type { rows = (rows,) } @@ -207,7 +227,7 @@ let col_row_is_valid(col_row) = ( (not is-infinite-len(col_row)) and (col_row == auto or type(col_row) in ( - "fraction", "length", "relative length", "ratio" + _fraction_type, _length_type, _rel_len_type, _ratio_type )) ) @@ -318,7 +338,7 @@ // Backwards-compatible enumerate #let enumerate(arr) = { - if type(arr) != "array" { + if type(arr) != _array_type { return arr } @@ -361,7 +381,7 @@ if is-infinite-len(len) { 0pt // avoid the destruction of the universe - } else if type(len) == "length" { + } else if type(len) == _length_type { if "em" in repr(len) { if styles == none { panic("Cannot convert length to pt ('styles' not specified).") @@ -371,7 +391,7 @@ } else { len + 0pt // mm, in, pt } - } else if type(len) == "ratio" { + } else if type(len) == _ratio_type { if page_size == none { panic("Cannot convert ratio to pt ('page_size' not specified).") } @@ -381,7 +401,7 @@ } ((len / 1%) / 100) * page_size + 0pt // e.g. 100% / 1% = 100; / 100 = 1; 1 * page_size - } else if type(len) == "fraction" { + } else if type(len) == _fraction_type { if frac_amount == none { panic("Cannot convert fraction to pt ('frac_amount' not specified).") } @@ -397,7 +417,7 @@ let len_per_frac = frac_total / frac_amount (len_per_frac * (len / 1fr)) + 0pt - } else if type(len) == "relative length" { + } else if type(len) == _rel_len_type { if styles == none { panic("Cannot convert relative length to pt ('styles' not specified).") } @@ -414,7 +434,7 @@ // SAFETY: guaranteed to be a ratio by regex let ratio_part = eval(ratio) - assert(type(ratio_part) == "ratio", message: "Eval didn't return a ratio") + assert(type(ratio_part) == _ratio_type, message: "Eval didn't return a ratio") let other_part = len - ratio_part // get the (2em + 5pt) part @@ -438,38 +458,57 @@ #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" { + if type(stroke) == _length_type { convert-length-to-pt(stroke, styles: styles) - } else if type(stroke) in ("relative length", "ratio") { + } else if type(stroke) in (_rel_len_type, _ratio_type) { panic(no-ratio-error) - } else if type(stroke) == "color" { + } else if type(stroke) == _color_type { 1pt - } else if type(stroke) == "stroke" { // 2em + blue - let r = regex("^\\d+(?:em|pt|cm|in|%)") + } else if type(stroke) == _stroke_type { + // support: + // - 5 + // - 5.5 + let maybe-float-regex = "(?:\\d+(?:\\.\\d+)?)" + // 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 r = regex("^" + len-regex) let s = repr(stroke).find(r) if s == none { // for more complex strokes, built through dictionaries // => "thickness: 5pt" field // note: on typst v0.7.0 or later, can just use 's.thickness' - let r = regex("thickness: (\\d+(?:em|pt|cm|in|%))") - s = repr(stroke).match(r).captures.first() + let r = regex("thickness: (" + len-regex + ")") + s = repr(stroke).match(r) + if s != none { + s = s.captures.first(); // get the first match (the thickness) + } } if s == none { 1pt // okay it's probably just a color then } else { let len = eval(s) - if type(len) == "length" { + if type(len) == _length_type { convert-length-to-pt(len, styles: styles) - } else if type(len) in ("relative length", "ratio") { + } else if type(len) in (_rel_len_type, _ratio_type) { panic(no-ratio-error) } else { 1pt // should be unreachable } } - } else if type(stroke) == "dictionary" and "thickness" in stroke { - stroke.thickness + } else if type(stroke) == _dict_type and "thickness" in stroke { + let thickness = stroke.thickness + if type(thickness) == _length_type { + convert-length-to-pt(thickness, styles: styles) + } else if type(thickness) in (_rel_len_type, _ratio_type) { + panic(no-ratio-error) + } else { + 1pt + } } else { 1pt } @@ -633,7 +672,7 @@ let item = items.at(i) // allow specifying () to change vline position - if type(item) == "array" 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 } @@ -709,7 +748,7 @@ } let content = cell.content - let content = if type(content) == "function" { + let content = if type(content) == _function_type { let res = content(this_x, this_y) if is-tablex-cell(res) { cell = res @@ -727,7 +766,7 @@ panic("Error: Cell with function as content returned another cell with 'none' as x or y!") } - if type(this_x) != "integer" or type(this_y) != "integer" { + 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))) } @@ -837,13 +876,13 @@ align_default: left, fill_default: none) = { - let align_default = if type(align_default) == "function" { + 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" { + let fill_default = if type(fill_default) == _function_type { fill_default(cell.x, cell.y) // row, column } else { fill_default @@ -861,7 +900,7 @@ // same here for fill let cell_fill = default-if-auto(cell.fill, fill_default) - if type(cell_fill) == "array" { + if type(cell_fill) == _array_type { let fill_len = cell_fill.len() if fill_len == 0 { @@ -881,11 +920,11 @@ } } - if cell_fill != none and type(cell_fill) != "color" { + if cell_fill != none and type(cell_fill) != _color_type { 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" { + if type(cell_align) == _array_type { let align_len = cell_align.len() if align_len == 0 { @@ -905,7 +944,7 @@ } } - if cell_align != auto and type(cell_align) not in ("alignment", "2d alignment") { + 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').") } @@ -932,7 +971,7 @@ // (auto, 1fr, ...) is ignored. #let sum-fixed-size-tracks(tracks) = { tracks.fold(0pt, (acc, el) => { - if type(el) == "length" { + if type(el) == _length_type { acc + el } else { acc @@ -943,11 +982,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") + let frac-tracks = enumerate(tracks).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" { + if type(gutter) == _fraction_type { amount-frac += (gutter / 1fr) * (tracks.len() - 1) } @@ -957,7 +996,7 @@ 0pt } - if type(gutter) == "fraction" { + if type(gutter) == _fraction_type { gutter = frac-width * (gutter / 1fr) } @@ -1019,7 +1058,7 @@ let i = i_col.at(0) let col = i_col.at(1) - if type(col) == "length" { + if type(col) == _length_type { size += col } } @@ -1038,7 +1077,7 @@ let i = i_row.at(0) let row = i_row.at(1) - if type(row) == "length" { + if type(row) == _length_type { size += row } } @@ -1046,7 +1085,7 @@ } // 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) = { +#let determine-auto-columns(grid: (), styles: none, columns: none, inset: none, align: auto) = { assert(styles != none, message: "Cannot measure auto columns without styles") let total_auto_size = 0pt let auto_sizes = () @@ -1074,9 +1113,16 @@ // take extra inset as extra width or height on 'auto' let cell_inset = default-if-auto(pcell.inset, inset) - let cell_inset = convert-length-to-pt(cell_inset, styles: styles) + // simulate wrapping this cell in the final box, + // but with unlimited width and height available + // so we can measure its width. + let cell-box = make-cell-box( + pcell, + width: auto, height: auto, + inset: cell_inset, align_default: auto + ) - let width = measure(pcell.content, styles).width + 2*cell_inset + let width = measure(cell-box, styles).width// + 2*cell_inset // the box already considers inset // here, we are excluding from the width of this cell // at this column all width that was already covered by @@ -1141,9 +1187,9 @@ columns } -#let determine-column-sizes(grid: (), page_width: 0pt, styles: none, columns: none, inset: none, col-gutter: none) = { +#let determine-column-sizes(grid: (), page_width: 0pt, styles: none, columns: none, inset: none, align: auto, col-gutter: none) = { let columns = columns.map(c => { - if type(c) in ("length", "relative length", "ratio") { + 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 @@ -1154,7 +1200,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" { + let fixed-size-gutter = if type(col-gutter) == _length_type { col-gutter } else { 0pt @@ -1167,7 +1213,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) + let auto_cols_result = determine-auto-columns(grid: grid, styles: styles, columns: columns, inset: inset, align: align) let total_auto_size = auto_cols_result.total let auto_sizes = auto_cols_result.sizes columns = auto_cols_result.columns @@ -1193,7 +1239,7 @@ } columns = columns.map(c => { - if type(c) == "fraction" { + if type(c) == _fraction_type { 0pt // no space left to be divided } else { c @@ -1202,7 +1248,7 @@ } } else { columns = columns.map(c => { - if c == auto or type(c) == "fraction" { + if c == auto or type(c) == _fraction_type { 0pt // no space remaining! } else { c @@ -1251,8 +1297,6 @@ // take extra inset as extra width or height on 'auto' let cell_inset = default-if-auto(pcell.inset, inset) - let cell_inset = convert-length-to-pt(cell_inset, styles: styles) - let cell-box = make-cell-box( pcell, width: width, height: auto, @@ -1288,7 +1332,7 @@ #let determine-row-sizes(grid: (), page_height: 0pt, styles: none, columns: none, rows: none, align: auto, inset: none, row-gutter: none) = { let rows = rows.map(r => { - if type(r) in ("length", "relative length", "ratio") { + if type(r) in (_length_type, _rel_len_type, _ratio_type) { convert-length-to-pt(r, styles: styles, page_size: page_height) } else { r @@ -1304,7 +1348,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" { + let fixed-size-gutter = if type(row-gutter) == _length_type { row-gutter } else { 0pt @@ -1321,7 +1365,7 @@ } else { ( rows: rows.map(r => { - if type(r) == "fraction" { // no space remaining in this page or box + if type(r) == _fraction_type { // no space remaining in this page or box 0pt } else { r @@ -1345,12 +1389,11 @@ inset: none, gutter: none, align: auto, ) = { - let inset = convert-length-to-pt(inset, styles: styles) - let columns_res = determine-column-sizes( grid: grid, page_width: page_width, styles: styles, columns: columns, inset: inset, + align: align, col-gutter: gutter.col ) columns = columns_res.columns @@ -1561,9 +1604,9 @@ // -- drawing -- #let parse-stroke(stroke) = { - if type(stroke) == "color" { + if type(stroke) == _color_type { stroke + 1pt - } else if type(stroke) in ("length", "relative length", "ratio", "stroke", "dictionary") 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) + "'.") @@ -1578,7 +1621,7 @@ if line.expand in (none, (none, none), auto, (auto, auto)) { return (none, none) } - if type(line.expand) != "array" { + if type(line.expand) != _array_type { line.expand = (line.expand, line.expand) } @@ -1606,6 +1649,7 @@ hline, initial_x: 0, initial_y: 0, columns: (), rows: (), stroke: auto, vlines: (), gutter: none, pre-gutter: false, styles: none, + rightmost_x: 0, rtl: false, ) = { let start = hline.start let end = hline.end @@ -1637,6 +1681,12 @@ return // negative length } + if rtl { + // invert the line (start from the right instead of from the left) + start_x = rightmost_x - start_x + end_x = rightmost_x - end_x + } + let start = ( start_x, y @@ -1659,7 +1709,8 @@ vline, initial_x: 0, initial_y: 0, columns: (), rows: (), stroke: auto, gutter: none, hlines: (), pre-gutter: false, stop-before-row-gutter: false, - styles: none + styles: none, + rightmost_x: 0, rtl: false, ) = { let start = vline.start let end = vline.end @@ -1691,6 +1742,11 @@ return // negative length } + if rtl { + // invert the vertical line's x pos (start from the right instead of from the left) + x = rightmost_x - x + } + let start = ( x, start_y @@ -1776,6 +1832,7 @@ min-pos: none, max-pos: none, header-hlines-have-priority: true, + rtl: false, table-loc: none, total-width: none, global-hlines: (), @@ -1817,7 +1874,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) == "integer" and rel_page <= repeat-header) or (type(repeat-header) == "array" 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 @@ -1832,6 +1889,7 @@ let first_x = none let first_y = none + let rightmost_x = none let row_heights = 0pt @@ -1846,10 +1904,30 @@ let y = cell_box.cell.y first_x = default-if-none(first_x, x) first_y = default-if-none(first_y, y) + rightmost_x = default-if-none(rightmost_x, width-between(start: first_x, end: none)) + + // where to place the cell (horizontally) + let dx = width-between(start: first_x, end: x) + + // TODO: consider implementing RTL before the rendering + // stage (perhaps by inverting 'x' positions on cells + // and lines beforehand). + if rtl { + // invert cell's x position (start from the right) + dx = rightmost_x - dx + // assume the cell doesn't start at the very end + // (that would be weird) + // Here we have to move dx back a bit as, after + // inverting it, it'd be the right edge of the cell; + // we need to keep it as the left edge's x position, + // as #place works with the cell's left edge. + // To do that, we subtract the cell's width from dx. + dx -= width-between(start: x, end: x + cell_box.cell.colspan) + } // place the cell! place(top+left, - dx: width-between(start: first_x, end: x), + dx: dx, dy: height-between(start: first_y, end: y) + added_header_height, cell_box.box) @@ -1870,8 +1948,8 @@ hide(rect(width: total-width, height: row_group_height)) - let draw-hline = draw-hline.with(initial_x: first_x, initial_y: first_y) - let draw-vline = draw-vline.with(initial_x: first_x, initial_y: first_y) + let draw-hline = draw-hline.with(initial_x: first_x, initial_y: first_y, rightmost_x: rightmost_x, rtl: rtl) + let draw-vline = draw-vline.with(initial_x: first_x, initial_y: first_y, rightmost_x: rightmost_x, rtl: rtl) let header_last_y = if first-row-group != none { first-row-group.row_group.y_span.at(1) @@ -1989,6 +2067,7 @@ min-pos: none, max-pos: none, header-rows: 1, + rtl: false, table-loc: none, table-id: none, ) = { @@ -2087,6 +2166,7 @@ total-width: total_width, table-loc: table-loc, header-hlines-have-priority: header-hlines-have-priority, + rtl: rtl, min-pos: min-pos, max-pos: max-pos, styles: styles, @@ -2118,7 +2198,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" 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 { @@ -2129,7 +2209,7 @@ e } else { e = default-if-auto(e, 0pt) - if type(e) not in ("length", "relative length", "ratio") { + if type(e) not in (_length_type, _rel_len_type, _ratio_type) { panic("'expand' argument to lines must be a pair (length, length).") } @@ -2177,11 +2257,11 @@ col-gutter = default-if-auto(col-gutter, 0pt) row-gutter = default-if-auto(row-gutter, 0pt) - if type(col-gutter) in ("length", "relative length", "ratio") { + 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", "relative length", "ratio") { + 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) } @@ -2197,7 +2277,7 @@ } else { o => o // identity } - } else if type(map-func) != "function" { + } else if type(map-func) != _function_type { panic("Map parameters must be functions.") } else { map-func @@ -2234,7 +2314,7 @@ if is-tablex-occupied(c) { none } else { c } })) - if type(cells) != "array" { + if type(cells) != _array_type { panic("Tablex error: 'map-rows' returned something that isn't an array.") } @@ -2249,7 +2329,7 @@ let c = i_c.at(1) let x = c.x let y = c.y - type(x) != "integer" or type(y) != "integer" or x < 0 or y < 0 or x >= col_len or y >= row_len + 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.") } @@ -2282,7 +2362,7 @@ if is-tablex-occupied(c) { none } else { c } })) - if type(cells) != "array" { + if type(cells) != _array_type { panic("Tablex error: 'map-cols' returned something that isn't an array.") } @@ -2297,7 +2377,7 @@ let c = i_c.at(1) let x = c.x let y = c.y - type(x) != "integer" or type(y) != "integer" or x < 0 or y < 0 or x >= col_len or y >= row_len + 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.") } @@ -2329,7 +2409,7 @@ #let validate-header-rows(header-rows) = { header-rows = default-if-auto(default-if-none(header-rows, 0), 1) - if type(header-rows) != "integer" or header-rows < 0 { + if type(header-rows) != _int_type or header-rows < 0 { panic("Tablex error: 'header-rows' must be a (positive) integer.") } @@ -2343,9 +2423,9 @@ repeat-header = default-if-auto(default-if-none(repeat-header, false), false) - if type(repeat-header) not in ("boolean", "integer", "array") { + if type(repeat-header) not in ("boolean", _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" and repeat-header.any(i => type(i) != "integer") { + } 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!") } @@ -2407,6 +2487,14 @@ // If false, they draw their own horizontal lines. // Defaults to true. // +// rtl: if true, the table is horizontally flipped. +// That is, cells and lines are placed in the opposite order +// (starting from the right), and horizontal lines are flipped. +// This is meant to simulate the behavior of default Typst tables when +// 'set text(dir: rtl)' is used, and is useful when writing in a language +// with a RTL (right-to-left) script. +// Defaults to false. +// // auto-lines: true = applies true to both auto-hlines and // auto-vlines; false = applies false to both. // Their values override this one unless they are 'auto'. @@ -2454,6 +2542,7 @@ repeat-header: false, header-rows: 1, header-hlines-have-priority: true, + rtl: false, auto-lines: true, auto-hlines: auto, auto-vlines: auto, @@ -2588,6 +2677,7 @@ repeat-header: repeat-header, header-hlines-have-priority: header-hlines-have-priority, header-rows: header-rows, + rtl: rtl, min-pos: min_pos, max-pos: max_pos, table-loc: t_loc, diff --git a/typst.toml b/typst.toml index e8d1bc9..ffe3bd0 100644 --- a/typst.toml +++ b/typst.toml @@ -1,6 +1,6 @@ [package] name = "tablex" -version = "0.0.5" +version = "0.0.6" authors = ["PgBiel "] license = "MIT" description = "More powerful and customizable tables in Typst."