diff --git a/src/col-row-size.typ b/src/col-row-size.typ index 565a68a..d46108f 100644 --- a/src/col-row-size.typ +++ b/src/col-row-size.typ @@ -335,7 +335,7 @@ #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_type, _rel_len_type, _ratio_type) { - convert-length-to-pt(c, styles: styles, page_size: page-width) + convert-length-to-pt(c, styles: styles, page-size: page-width) } else if c == none { 0pt } else { @@ -478,7 +478,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_type, _rel_len_type, _ratio_type) { - convert-length-to-pt(r, styles: styles, page_size: page-height) + convert-length-to-pt(r, styles: styles, page-size: page-height) } else { r } diff --git a/src/option-parsing.typ b/src/option-parsing.typ index 9dcb640..428ee38 100644 --- a/src/option-parsing.typ +++ b/src/option-parsing.typ @@ -34,7 +34,7 @@ panic("'expand' argument to lines must be a pair (length, length).") } - convert-length-to-pt(e, styles: styles, page_size: page-size) + convert-length-to-pt(e, styles: styles, page-size: page-size) } }) } @@ -79,11 +79,11 @@ row-gutter = default-if-auto(row-gutter, 0pt) 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) + 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) { - row-gutter = convert-length-to-pt(row-gutter, styles: styles, page_size: page-width) + row-gutter = convert-length-to-pt(row-gutter, styles: styles, page-size: page-width) } (col: col-gutter, row: row-gutter) diff --git a/src/type-validators.typ b/src/type-validators.typ index 0450ea7..dc66875 100644 --- a/src/type-validators.typ +++ b/src/type-validators.typ @@ -98,6 +98,11 @@ type(len) in (_ratio_type, _fraction_type, _rel_len_type, _length_type) and "inf" in repr(len) } +// 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) +} + #let validate-cols-rows(columns, rows, items: ()) = { if type(columns) == _int_type { assert(columns >= 0, message: "Error: Cannot have a negative amount of columns.") diff --git a/src/utilities.typ b/src/utilities.typ index d5e5c75..92621dd 100644 --- a/src/utilities.typ +++ b/src/utilities.typ @@ -6,6 +6,11 @@ #import "type-validators.typ": * // -- end imports -- +// 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+" + // Which positions does a cell occupy // (Usually just its own, but increases if colspan / rowspan // is greater than 1) @@ -114,88 +119,171 @@ previous } -// Convert a certain (non-relative) length to pt +// Measure a length in pt by drawing a line and using the measure() function. +// This function will work for negative lengths as well. +// +// Note that for ratios, the measurement will be 0pt due to limitations of +// the "draw and measure" technique (wrapping the line in a box still returns 0pt; +// not sure if there is any viable way to measure a ratio). This also affects +// relative lengths — this function will only be able to measure the length component. // // styles: from style() -// page_size: equivalent to 100% -// frac_amount: amount of 'fr' specified -// frac_total: total space shared by fractions -#let convert-length-to-pt( - len, - styles: none, page_size: none, frac_amount: none, frac_total: none -) = { - page_size = 0pt + page_size +#let measure-pt(len, styles) = { + let measured-pt = measure(line(length: len), styles).width - if is-infinite-len(len) { - 0pt // avoid the destruction of the universe - } else if type(len) == _length_type { - if "em" in repr(len) { - if styles == none { - panic("Cannot convert length to pt ('styles' not specified).") - } + // If the measured length is positive, `len` must have overall been positive. + // There's nothing else to be done, so return the measured length. + if measured-pt > 0pt { + return measured-pt + } - measure(line(length: len), styles).width + 0pt - } else { - len + 0pt // mm, in, pt - } - } else if type(len) == _ratio_type { - if page_size == none { - panic("Cannot convert ratio to pt ('page_size' not specified).") - } + // If we've reached this point, the previously measured length must have been `0pt` + // (drawing a line with a negative length will draw nothing, so measuring it will return `0pt`). + // Hence, `len` must either be `0pt` or negative. + // We multiply `len` by -1 to get a positive length, draw a line and measure it, then negate + // the measured length. This nicely handles the `0pt` case as well. + measured-pt = -measure(line(length: -len), styles).width + return measured-pt +} - if is-infinite-len(page_size) { - return 0pt // page has 'auto' size => % should return 0 - } +// Convert a length of type length to pt. +// +// 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) { + // No need to do any conversion because it must already be in pt. + return len + } - ((len / 1%) / 100) * page_size + 0pt // e.g. 100% / 1% = 100; / 100 = 1; 1 * page_size - } else if type(len) == _fraction_type { - if frac_amount == none { - panic("Cannot convert fraction to pt ('frac_amount' not specified).") - } + // At this point, we will need to draw a line for measurement, + // so we need the styles. + if styles == none { + panic("Cannot convert length to pt ('styles' not specified).") + } - if frac_total == none { - panic("Cannot convert fraction to pt ('frac_total' not specified).") - } + return measure-pt(len, styles) +} - if frac_amount <= 0 or is-infinite-len(frac_total) { - return 0pt - } +// Convert a ratio type length to pt +// +// page-size: equivalent to 100% +#let convert-ratio-type-to-pt(len, page-size) = { + assert( + is-purely-pt-len(page-size), + message: "'page-size' should be a purely pt length" + ) + + if page-size == none { + panic("Cannot convert ratio to pt ('page-size' not specified).") + } - let len_per_frac = frac_total / frac_amount + if is-infinite-len(page-size) { + return 0pt // page has 'auto' size => % should return 0 + } - (len_per_frac * (len / 1fr)) + 0pt - } else if type(len) == _rel_len_type { - if styles == none { - panic("Cannot convert relative length to pt ('styles' not specified).") - } + ((len / 1%) / 100) * page-size + 0pt // e.g. 100% / 1% = 100; / 100 = 1; 1 * page-size +} - let ratio_regex = regex("^\\d+%") - let ratio = repr(len).find(ratio_regex) +// Convert a fraction type length to pt +// +// frac-amount: amount of 'fr' specified +// frac-total: total space shared by fractions +#let convert-fraction-type-to-pt(len, frac-amount, frac-total) = { + assert( + is-purely-pt-len(frac-total), + message: "'frac-total' should be a purely pt length" + ) + + if frac-amount == none { + panic("Cannot convert fraction to pt ('frac-amount' not specified).") + } - if ratio == none { // 2em + 5pt (doesn't contain 100% or something) - measure(line(length: len), styles).width - } else { // 100% + 2em + 5pt --> extract the "100%" part - if page_size == none { - panic("Cannot convert relative length to pt ('page_size' not specified).") - } + if frac-total == none { + panic("Cannot convert fraction to pt ('frac-total' not specified).") + } - // SAFETY: guaranteed to be a ratio by regex - let ratio_part = eval(ratio) - assert(type(ratio_part) == _ratio_type, message: "Eval didn't return a ratio") + if frac-amount <= 0 or is-infinite-len(frac-total) { + return 0pt + } - let other_part = len - ratio_part // get the (2em + 5pt) part + let len-per-frac = frac-total / frac-amount - let ratio_part_pt = if is-infinite-len(page_size) { 0pt } else { ((ratio_part / 1%) / 100) * page_size } - let other_part_pt = 0pt + (len-per-frac * (len / 1fr)) + 0pt +} - if other_part < 0pt { - other_part_pt = -measure(line(length: -other_part), styles).width - } else { - other_part_pt = measure(line(length: other_part), styles).width - } +// Convert a relative type length to pt +// +// 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) = { + // 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).") + } - ratio_part_pt + other_part_pt + 0pt - } + // 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. + let em-regex = regex(NUMBER-REGEX-STRING + "em") + let em-part-repr = repr(len).find(em-regex) + + // Calculate the length minus its em component. + // E.g., 1% + 1pt + 1em -> 1% + 1pt + let (em-part, len-minus-em) = if em-part-repr == none { + (0em, len) + } else { + // SAFETY: guaranteed to be a purely em length by regex + let em-part = eval(em-part-repr) + (em-part, len - em-part) + } + + // This will give only the pt part of the length. + // E.g., 1% + 1pt -> 1pt + // See the documentation on measure-pt for more information. + let pt-part = measure-pt(len-minus-em, styles) + + // Since we have the values of the em and pt components, + // we can calculate the ratio part. + let ratio-part = len-minus-em - pt-part + let ratio-part-pt = if ratio-part == 0% { + // No point doing `convert-ratio-type-to-pt` if there's no ratio component. + 0pt + } else { + convert-ratio-type-to-pt(ratio-part, page-size) + } + + // 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. + let length-part-pt = convert-length-type-to-pt(pt-part + em-part, styles: styles) + + ratio-part-pt + length-part-pt +} + +// Convert a certain (non-relative) length to pt +// +// styles: from style() +// page-size: equivalent to 100% +// frac-amount: amount of 'fr' specified +// frac-total: total space shared by fractions +#let convert-length-to-pt( + len, + styles: none, page-size: none, frac-amount: none, frac-total: none +) = { + page-size = 0pt + page-size + + if is-infinite-len(len) { + 0pt // avoid the destruction of the universe + } else if type(len) == _length_type { + convert-length-type-to-pt(len, styles: styles) + } else if type(len) == _ratio_type { + convert-ratio-type-to-pt(len, page-size) + } else if type(len) == _fraction_type { + convert-fraction-type-to-pt(len, frac-amount, frac-total) + } 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.") } diff --git a/tablex-test.typ b/tablex-test.typ index ba448fe..eac0d86 100644 --- a/tablex-test.typ +++ b/tablex-test.typ @@ -1,5 +1,5 @@ -#import "src/common.typ": calc-mod -#import "src/utilities.typ": default-if-auto +#import "src/common.typ": calc-mod, _length_type +#import "src/utilities.typ": default-if-auto, convert-length-to-pt #import "tablex.typ": * *Test* @@ -783,3 +783,224 @@ Combining em and pt (with a stroke object): }), [E] ) + +*Length to pt conversion* + +#let convert-length-to-pt-test( + len, expected, + page-size: 100pt, // Set 1% to 1pt + frac-amount: 10, // Set 1fr to 1pt + frac-total: 10pt, +) = { + set text(size: 1pt) // Set 1em to 1pt + style(styles => { + let actual = convert-length-to-pt( + len, + styles: styles, + page-size: page-size, + frac-amount: frac-amount, + frac-total: frac-total, + ) + + assert(type(actual) == _length_type) + assert(expected == actual) + }) +} + +// `length` tests +#convert-length-to-pt-test(0pt, 0pt) +#convert-length-to-pt-test(1pt, 1pt) +#convert-length-to-pt-test(1em, 1pt) +#convert-length-to-pt-test(-1pt, -1pt) +#convert-length-to-pt-test(-1em, -1pt) +#convert-length-to-pt-test(0.005pt, 0.005pt) +#convert-length-to-pt-test(0.005em, 0.005pt) +#convert-length-to-pt-test(-0.005pt, -0.005pt) +#convert-length-to-pt-test(-0.005em, -0.005pt) +#convert-length-to-pt-test(0.005pt + 0.005em, 0.01pt) +#convert-length-to-pt-test(0.005pt - 0.005em, 0pt) +#convert-length-to-pt-test(-0.005pt + 0.005em, 0pt) +#convert-length-to-pt-test(-0.005pt - 0.005em, -0.01pt) + +// `ratio` tests +#convert-length-to-pt-test(1%, 1pt) +#convert-length-to-pt-test(-1%, -1pt) +#convert-length-to-pt-test(0.5%, 0.5pt) +#convert-length-to-pt-test(-0.5%, -0.5pt) + +// `fraction` tests +#convert-length-to-pt-test(1fr, 1pt) +#convert-length-to-pt-test(-1fr, -1pt) +#convert-length-to-pt-test(0.5fr, 0.5pt) +#convert-length-to-pt-test(-0.5fr, -0.5pt) + +// `relative` tests +#convert-length-to-pt-test(0% + 0pt + 0em, 0pt) +#convert-length-to-pt-test(0% + 0pt + 1em, 1pt) +#convert-length-to-pt-test(0% + 1pt + 0em, 1pt) +#convert-length-to-pt-test(0% + 1pt + 1em, 2pt) +#convert-length-to-pt-test(1% + 0pt + 0em, 1pt) +#convert-length-to-pt-test(1% + 0pt + 1em, 2pt) +#convert-length-to-pt-test(1% + 1pt + 0em, 2pt) +#convert-length-to-pt-test(1% + 1pt + 1em, 3pt) + +#convert-length-to-pt-test(0% + 0pt + 0.005em, 0.005pt) +#convert-length-to-pt-test(0% + 0.005pt + 0em, 0.005pt) +#convert-length-to-pt-test(0% + 0.005pt + 0.005em, 0.01pt) +#convert-length-to-pt-test(0.005% + 0pt + 0em, 0.005pt) +#convert-length-to-pt-test(0.005% + 0pt + 0.005em, 0.01pt) +#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) + +#convert-length-to-pt-test(0% + 0pt - 0.005em, -0.005pt) +#convert-length-to-pt-test(0% - 0.005pt + 0em, -0.005pt) +#convert-length-to-pt-test(0% - 0.005pt - 0.005em, -0.01pt) +#convert-length-to-pt-test(-0.005% + 0pt + 0em, -0.005pt) +#convert-length-to-pt-test(-0.005% + 0pt - 0.005em, -0.01pt) +#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) + +*Line expansion - issue \#74:* + +#let wrap-for-linex-expansion-test(tabx) = { + set text(size: 1pt) // Set 1em to 1pt + box( + width: 100pt, // Set 1% to 1pt + height: 100pt, + tabx + ) +} + +- Positive single-cell hlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 3pt, + auto-lines: false, + hlinex(), + [], + hlinex(expand: 3pt), + [], + hlinex(expand: 3em), + [], + hlinex(expand: 3%), + [], + hlinex(expand: 1% + 1pt + 1em), + ) +) + +- Positive multi-cell hlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: (1pt, 1pt, 1pt), + auto-lines: false, + hlinex(), + [], [], [], + hlinex(expand: 3pt), + [], [], [], + hlinex(expand: 3em), + [], [], [], + hlinex(expand: 3%), + [], [], [], + hlinex(expand: 1% + 1pt + 1em), + ) +) + +- Negative single-cell hlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 15pt, + auto-lines: false, + hlinex(), + [], + hlinex(expand: -6pt), + [], + hlinex(expand: -6em), + [], + hlinex(expand: -6%), + [], + hlinex(expand: -(2% + 2pt + 2em)), + ) +) + +// TODO: currently does not work as intended (https://github.com/PgBiel/typst-tablex/issues/85) +- Negative multi-cell hlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: (5pt, 5pt, 5pt), + auto-lines: false, + hlinex(), + [], [], [], + hlinex(expand: -6pt), + [], [], [], + hlinex(expand: -6em), + [], [], [], + hlinex(expand: -6%), + [], [], [], + hlinex(expand: -(2% + 2pt + 2em)), + ) +) + +- Positive single-cell vlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 5, + rows: 3pt, + auto-lines: false, + vlinex(), + vlinex(expand: 3pt), + vlinex(expand: 3em), + vlinex(expand: 3%), + vlinex(expand: 1% + 1pt + 1em), + ) +) + +- Positive multi-cell vlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 5, + rows: (1pt, 1pt, 1pt), + auto-lines: false, + vlinex(), + vlinex(expand: 3pt), + vlinex(expand: 3em), + vlinex(expand: 3%), + vlinex(expand: 1% + 1pt + 1em), + ) +) + +- Negative single-cell vlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 5, + rows: 15pt, + auto-lines: false, + vlinex(), + vlinex(expand: -6pt), + vlinex(expand: -6em), + vlinex(expand: -6%), + vlinex(expand: -(2% + 2pt + 2em)), + ) +) + +// TODO: currently does not work as intended (https://github.com/PgBiel/typst-tablex/issues/85) +- Negative multi-cell vlinex expansion + +#wrap-for-linex-expansion-test( + tablex( + columns: 5, + rows: (5pt, 5pt, 5pt), + auto-lines: false, + vlinex(), + vlinex(expand: -6pt), + vlinex(expand: -6em), + vlinex(expand: -6%), + vlinex(expand: -(2% + 2pt + 2em)), + ) +)