Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

utilities: fix issues related to conversion of lengths to pt #84

Merged
merged 13 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/col-row-size.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions src/option-parsing.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/type-validators.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
218 changes: 153 additions & 65 deletions src/utilities.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) = {
dixslyf marked this conversation as resolved.
Show resolved Hide resolved
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.")
}
Expand Down
Loading