diff --git a/README.md b/README.md index b848054..de3d1fb 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ Zero's core is the `num()` function, which provides flexible number formatting. decimal-separator: str = ".", product: content = sym.times, tight: bool = false, + breakable: bool | dict = false, math: bool = true, omit-unity-mantissa: bool = false, @@ -128,6 +129,7 @@ Zero's core is the `num()` function, which provides flexible number formatting. - `decimal-separator: str = "."` : Specifies the marker that is used for separating integer and decimal part. - `product: content = sym.times` : Specifies the multiplication symbol used for scientific notation. - `tight: bool = false` : If true, tight spacing is applied between operands (applies to $\times$ and $\pm$). +- `breakable: bool | dict` : Whether numbers and quantities can be broken across paragraph lines. Setting this to `true`/`false` entirely enables/disables breaking. For more fine-grained control, a dictionary with the keys `uncertainty`, `power`, and `unit` (all booleans) can be passed for specifying whether breaks are allowed after the ± symbol, the × symbol, or before the unit, respectively. - `math: bool = true` : If set to `false`, the parts of the number won't be wrapped in a `math.equation`. This makes it possible to use `num()` with non-math fonts. - `omit-unity-mantissa: bool = false` : Determines whether a mantissa of 1 is omitted in scientific notation, e.g., $10^4$ instead of $1\cdot 10^4$. - `positive-sign: bool = false` : If set to `true`, positive coefficients are shown with a $+$ sign. @@ -409,7 +411,6 @@ The appearance of units can be configured via `set-unit`: #set-unit( unit-separator: content = sym.space.thin, fraction: str = "power", - breakable: bool = false, prefix: auto | none = auto ) ``` @@ -418,7 +419,6 @@ The appearance of units can be configured via `set-unit`: - `"power"` : Units with negative exponents are shown as powers. - `"fraction"` : When units with negative exponents are present, a fraction is created and the concerned units are put in the denominator. - `"inline"` : An inline fraction is created. -- `breakable: bool` : Whether units and quantities can be broken across paragraph lines. - `prefix: auto | none` : When set to `auto` and `num.exponent` is set to `"eng"`, a metric prefix is displayed along with the unit, replacing the exponent, e.g., `zi.m[2e4]` will render as 20km. These options are also available when instancing a quantity, e.g., `#zi.m(fraction: "inline")[2.5]`. diff --git a/src/formatting.typ b/src/formatting.typ index 6a2dd3a..3f16b28 100644 --- a/src/formatting.typ +++ b/src/formatting.typ @@ -173,9 +173,19 @@ } +#let nonbreaking-binary-class(body, tight: false, breakable: true) = { + let space = if tight { 0pt } else { 2em/9 } + + if breakable { + math.class("binary", body) + } else { + // Recover spacing of binary relations but without breakability + h(space) + math.class("normal", body) + h(space) + } +} #let format-uncertainty = it => { - /// pm, digits, mode, concise, tight, math + /// pm, digits, mode, concise, tight, math, breakable let pm = it.pm if pm == none { return () } let is-symmetric = type(pm.first()) != array @@ -206,18 +216,26 @@ decimal-separator: it.decimal-separator, ))) if is-symmetric { - if it.concise { ("(", pm.first(), ")") } else if it.math { + if it.concise { + ("(", pm.first(), ")") + } else if it.math { ( math.class("normal", none), - math.class(if it.tight { "normal" } else { "binary" }, sym.plus.minus), + nonbreaking-binary-class( + sym.plus.minus, + tight: it.tight, + breakable: it.breakable.uncertainty + ), pm.first(), ) } else { - let space = if not it.tight { sym.space.thin } + let space = if it.tight { sym.space.hair } else { sym.space.thin } ( space, + sym.wj, sym.plus.minus, space, + if not it.breakable.uncertainty { sym.wj }, pm.first(), ) } @@ -231,6 +249,7 @@ ) } else { ( + sym.wj, non-math-attach( none, t: "+" + pm.at(0), @@ -243,7 +262,7 @@ #let format-power = it => { - /// x, base, product, positive-sign-exponent, tight, math + /// x, base, product, positive-sign-exponent, tight, math, breakable if it.exponent == none { return () } let (sign, integer, fractional) = decompose-signed-float-numeral(it.exponent) @@ -262,19 +281,24 @@ if it.product == none { (power,) } else { ( box(), - math.class(if it.tight { "normal" } else { "binary" }, it.product), + nonbreaking-binary-class( + it.product, + tight: it.tight, + breakable: it.breakable.power + ), power, ) } } else { let power = non-math-attach([#it.base], t: [#exponent]) if it.product == none { (power,) } else { - let space = if not it.tight { sym.space.thin } + let space = if it.tight { sym.space.hair } else { sym.space.thin } ( - box(), space, + sym.wj, it.product, space, + if not it.breakable.power { sym.wj }, power, ) } @@ -284,6 +308,7 @@ #let show-num-impl = it => { + let breakable = utility.process-breakable(it.breakable) /// sign, int, frac, e, pm, /// digits /// omit-unity-mantissa, uncertainty-mode, positive-sign @@ -321,6 +346,7 @@ math: it.math, mode: it.uncertainty-mode, decimal-separator: it.decimal-separator, + breakable: breakable ) @@ -332,6 +358,7 @@ tight: it.tight, math: it.math, decimal-separator: it.decimal-separator, + breakable: breakable ) let integer-part = ( @@ -349,6 +376,9 @@ ) let uncertainty-part = format-uncertainty(uncertainty) + if not breakable.uncertainty and uncertainty-part.len() != 0{ + // uncertainty-part = (std.box(equation-from-items(uncertainty-part)),) + } if concise-uncertainty { fractional-part += uncertainty-part diff --git a/src/state.typ b/src/state.typ index e0158e0..d3edcea 100644 --- a/src/state.typ +++ b/src/state.typ @@ -15,6 +15,7 @@ fixed: none, exponent: auto, trim-zeros: false, + breakable: false, group: ( size: 3, separator: sym.space.thin, @@ -30,7 +31,6 @@ unit: ( unit-separator: sym.space.thin, fraction: "power", - breakable: false, use-sqrt: true, prefix: auto, lowercase-liter: false, diff --git a/src/units.typ b/src/units.typ index c72c956..38ec908 100644 --- a/src/units.typ +++ b/src/units.typ @@ -184,8 +184,15 @@ /// Unprocessed arguments. ..args, ) = { + assert( + fraction in ("power", "fraction", "inline"), + message: "Invalid fraction: " + + fraction + + ". Expected \"power\", \"fraction\", or \"inline\"", + ) + let fold-units = fold-units.with( - unit-separator: unit-separator, + unit-separator: unit-separator + sym.wj, math: math, use-sqrt: use-sqrt, ) @@ -200,10 +207,10 @@ let denominator-content = fold-units(..denominator, denom-exp-multiplier) if fraction == "power" { - // Numerator may be empty! + // Numerator could be empty! let result = denominator-content if numerator.len() != 0 { - result = numerator-content + unit-separator + result + result = numerator-content + unit-separator + sym.wj + result } return if math { $result$ } else { result } } @@ -211,34 +218,21 @@ // For the two fractional modes, the numerator shall not be empty. if numerator.len() == 0 { numerator-content = $1$ } - if fraction == "fraction" { - if not math { - assert( - false, - "`math: false` cannot be used together with `fraction: \"fraction\"`", - ) + if math { + if denominator.len() > 1 and fraction == "inline" { + denominator-content = $(#denominator-content)$ } - return $#numerator-content/#denominator-content$ - } else if fraction == "inline" { + set std.math.frac(style: "horizontal") if fraction == "inline" + $#numerator-content/#denominator-content$ + } else { if denominator.len() > 1 { - denominator-content = "(" + denominator-content + ")" + denominator-content = [(#denominator-content)] } - - if math { - $#numerator-content#h(0pt)\/#h(0pt)#denominator-content$ - } else { - numerator-content + "/" + denominator-content - } - } else { - assert( - false, - message: "Invalid fraction: " - + fraction - + ". Expected \"power\", \"fraction\", or \"symbol\"", - ) + numerator-content + "/" + denominator-content } } + #let unit( unit, ..args, @@ -249,19 +243,12 @@ } let num-state = update-num-state(num-state.get(), args) - let result = show-unit( + let result = (show-unit( unit.numerator, unit.denominator, ..num-state.unit, math: num-state.math, - ) - if not num-state.unit.breakable { - if num-state.math { - result = $result$ - } else { - result = box(result) - } - } + )) result } @@ -319,26 +306,22 @@ unit.numerator.first().first() = prefix + unit.numerator.first().first() } } + let breakable = utility.process-breakable(num-state.breakable) let result = { num(value, state: num-state, force-parentheses-around-uncertainty: true) + sym.wj separator - show-unit( + if not breakable.unit { sym.wj } + box(show-unit( unit.numerator, unit.denominator, fraction: num-state.unit.fraction, unit-separator: num-state.unit.unit-separator, math: num-state.math, - ) + )) } - if not num-state.unit.breakable { - if num-state.math { - result = $result$ - } else { - result = box(result) - } - } result } diff --git a/src/utility.typ b/src/utility.typ index 9dfb6e8..b1bb636 100644 --- a/src/utility.typ +++ b/src/utility.typ @@ -43,3 +43,27 @@ #assert.eq(shift-decimal-left("123", "456", digits: -5), ("12345600", "")) #assert.eq(shift-decimal-left("0", "0012", digits: -4), ("12", "")) #assert.eq(shift-decimal-left("0", "0012", digits: -2), ("0", "12")) + + +#let process-breakable(breakable) = { + let default = ( + uncertainty: false, + power: false, + unit: false, + ) + + if breakable == false { + return default + } + if breakable == true { + return ( + uncertainty: true, + power: true, + unit: true, + ) + } + if type(breakable) == dictionary { + return default + breakable + } + assert(false) +} diff --git a/tests/breakable/.gitignore b/tests/breakable/.gitignore new file mode 100644 index 0000000..2e4c5d8 --- /dev/null +++ b/tests/breakable/.gitignore @@ -0,0 +1,5 @@ +# generated by tytanic, do not edit + +/diff/ +/out/ +/ref/ diff --git a/tests/breakable/ref.typ b/tests/breakable/ref.typ new file mode 100644 index 0000000..8472607 --- /dev/null +++ b/tests/breakable/ref.typ @@ -0,0 +1,113 @@ +#set page(width: auto, height: auto, margin: .5em) + +#set super(typographic: false) +#let th = sym.space.thin +#let hair = sym.space.hair + + +== Math mode +// Non-breakable +#block(stroke: gray, width: 15em)[ + The length is $(1plus.minus 2)×10^2th"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 10em)[ + The length is\ $(1plus.minus 2)×10^2th"m"th"s"^(-1)$. +] +// Single break points +#block(stroke: gray, width: 13em)[ + The length is $(1plus.minus 2)×10^2$\ $"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 12em)[ + The length is $(1plus.minus 2)×$\ $10^2th"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 10em)[ + The length is $(1plus.minus$\ $2)×10^2th"m"th"s"^(-1)$. +] + +#pagebreak() + + +== Text mode +// Non-breakable +#block(stroke: gray, width: 13em)[ + The length is (1#th±#th⁠2) ⁠× ⁠10#super[2]#th;m#th;s#super[−1]. +] +#block(stroke: gray, width: 10em)[ + The length is\ (1#th±#th⁠2) ⁠× ⁠10#super[2]#th;m#th;s#super[−1]. +] +// Single break points +#block(stroke: gray, width: 11em)[ + The length is (1#th±#th⁠2) ⁠× ⁠10#super[2]#th;\ m#th;s#super[−1]. +] +#block(stroke: gray, width: 10.5em)[ + The length is (1#th±#th⁠2) ⁠× \ ⁠10#super[2]#th;m#th;s#super[−1]. +] +#block(stroke: gray, width: 10em)[ + The length is (1#th±#th\ 2) ⁠× ⁠10#super[2]#th;m#th;s#super[−1]. +] + +#pagebreak() + + +== Tight mode +#block(stroke: gray, width: 14em)[ + The length is $(1#h(0pt)plus.minus 2)#h(0pt)×#h(0pt)10^2th"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 10em)[ + The length is\ $(1#h(0pt)plus.minus 2)#h(0pt)×#h(0pt)10^2th"m"th"s"^(-1)$. +] +// Single break points +#block(stroke: gray, width: 11em)[ + The length is $(1#h(0pt)plus.minus 2)#h(0pt)×#h(0pt)10^2$\ $"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 10.5em)[ + The length is $(1#h(0pt)plus.minus 2)×$\ $10^2th"m"th"s"^(-1)$. +] +#block(stroke: gray, width: 10em)[ + The length is $(1plus.minus$\ $2)#h(0pt)×#h(0pt)10^2th"m"th"s"^(-1)$. +] + + +#pagebreak() + + +== Tight text mode +#block(stroke: gray, width: 13em)[ + The length is (1#hair±#hair⁠2)#hair;×#hair;⁠10#super[2]#th;m#th;s#super[−1]. +] +#block(stroke: gray, width: 10em)[ + The length is\ (1#hair±#hair⁠2)#hair;×#hair;⁠10#super[2]#th;m#th;s#super[−1]. +] +// Single break points +#block(stroke: gray, width: 11em)[ + The length is (1#hair±#hair⁠2)#hair;×#hair;⁠10#super[2]#hair;\ m#th;s#super[−1]. +] +#block(stroke: gray, width: 10.5em)[ + The length is (1#hair±#hair⁠2)#hair;×#hair;\ ⁠10#super[2]#th;m#th;s#super[−1]. +] +#block(stroke: gray, width: 10em)[ + The length is (1#hair±#hair\ 2)#hair;⁠×#hair;⁠10#super[2]#th;m#th;s#super[−1]. +] + + +#pagebreak() + +== Don't break asym. uncertainties +#block(stroke: gray, width: 10em)[ + The length is\ + $\(1#none^(+2)_(-2))×10^2th"m"$. +] +#block(stroke: gray, width: 10em)[ + The length is \ + #import "/src/zero.typ": zi + #zi.meter("1+2-2e2", math: false). +] + +#pagebreak() + +#set page(width: 9em, height: auto, margin: .5em) + + +Unit of force is $"kg"th"m"th"stel"th"A"th"A"th"s"^(-2)th"mol"^(-1)$ + +Unit of force is \ #box(width: 10cm)[kg#th;m#th;stel#th;A#th;A#th;s#super[−2]#th;mol#super[−1]] \ \ No newline at end of file diff --git a/tests/breakable/test.typ b/tests/breakable/test.typ new file mode 100644 index 0000000..f25933c --- /dev/null +++ b/tests/breakable/test.typ @@ -0,0 +1,86 @@ +#import "/src/zero.typ": * + +// #set box(fill: red) +#set page(width: auto, height: auto, margin: .5em) + +#let test-clause(..args) = [ + #[The length is] #(zi.m-s("1+-2e2", ..args)). \ + // #hide[The length is] #(zi.meter("1+2-2e2", ..args)). +] + +== Math mode +// Non-breakable +#block(stroke: gray, width: 15em, test-clause()) +#block(stroke: gray, width: 10em, test-clause()) +// Single break points +#block(stroke: gray, width: 13em, test-clause(breakable: (unit: true))) +#block(stroke: gray, width: 12em, test-clause(breakable: (power: true))) +#block(stroke: gray, width: 10em, test-clause(breakable: (uncertainty: true))) + +#pagebreak() + + +== Text mode +#set-num(math: false) +#block(stroke: gray, width: 13em, test-clause()) +#block(stroke: gray, width: 10em, test-clause()) +// Single break points +#block(stroke: gray, width: 11em, test-clause(breakable: (unit: true))) +#block(stroke: gray, width: 10.5em, test-clause(breakable: (power: true))) +#block(stroke: gray, width: 10em, test-clause(breakable: (uncertainty: true))) + +#pagebreak() + + +== Tight mode +#set-num(tight: true, math: true) +#block(stroke: gray, width: 14em, test-clause()) +#block(stroke: gray, width: 10em, test-clause()) +// Single break points +#block(stroke: gray, width: 11em, test-clause(breakable: (unit: true))) +#block(stroke: gray, width: 10.5em, test-clause(breakable: (power: true))) +#block(stroke: gray, width: 10em, test-clause(breakable: (uncertainty: true))) + + +#pagebreak() + + +== Tight text mode +#set-num(tight: true, math: false) +#block(stroke: gray, width: 13em, test-clause()) +#block(stroke: gray, width: 10em, test-clause()) +// Single break points +#block(stroke: gray, width: 11em, test-clause(breakable: (unit: true))) +#block(stroke: gray, width: 10.5em, test-clause(breakable: (power: true))) +#block(stroke: gray, width: 10em, test-clause(breakable: (uncertainty: true))) + + +#pagebreak() + +== Don't break asym. uncertainties +#set-num(tight: false, math: true) +#block(stroke: gray, width: 10em)[ + The length is #(zi.meter("1+2-2e2", breakable: (uncertainty: true))). +] +#set-num(tight: false, math: false) +#block(stroke: gray, width: 10em)[ + The length is #(zi.meter("1+2-2e2", breakable: (uncertainty: true))). +] + +#pagebreak() + + + +#set page(width: 9em, height: auto, margin: .5em) + +#set-num(tight: false, math: true) + + +#let long-unit = zi.declare("kg m/s^2/mol/N stel A A") + + +#set-num(breakable: false) +Unit of force is #long-unit(). + +#set-num(math: false) +Unit of force is #long-unit(). diff --git a/tests/zi/advanced/ref/1.png b/tests/zi/advanced/ref/1.png deleted file mode 100644 index 598c07d..0000000 Binary files a/tests/zi/advanced/ref/1.png and /dev/null differ diff --git a/tests/zi/advanced/ref/2.png b/tests/zi/advanced/ref/2.png deleted file mode 100644 index 5ef027d..0000000 Binary files a/tests/zi/advanced/ref/2.png and /dev/null differ diff --git a/tests/zi/declare-advanced/.gitignore b/tests/zi/declare-advanced/.gitignore new file mode 100644 index 0000000..2e73a39 --- /dev/null +++ b/tests/zi/declare-advanced/.gitignore @@ -0,0 +1 @@ +ref/ \ No newline at end of file diff --git a/tests/zi/declare-advanced/ref.typ b/tests/zi/declare-advanced/ref.typ new file mode 100644 index 0000000..2a6e309 --- /dev/null +++ b/tests/zi/declare-advanced/ref.typ @@ -0,0 +1,8 @@ +#set page(width: auto, height: auto, margin: .5em) + +#let th = sym.space.thin + +$Pi^2$ \ +$"M"_dot.o\/("s"^2 th"β"^2)$ \ +$1\/("M"_dot.o th √"s"th"β"^"a")$ \ +$#sym.prime.double^2$ diff --git a/tests/zi/advanced/test.typ b/tests/zi/declare-advanced/test.typ similarity index 74% rename from tests/zi/advanced/test.typ rename to tests/zi/declare-advanced/test.typ index 863682f..dd8ece4 100644 --- a/tests/zi/advanced/test.typ +++ b/tests/zi/declare-advanced/test.typ @@ -7,13 +7,3 @@ #zi.declare($M_dot.o$, ("s", -2), ($β$, -2))() \ #zi.declare(($M_dot.o$, -1), ("s", -0.5), ($β$, "-a"))() \ #zi.declare((sym.prime.double, 2))() \ - - -#pagebreak() - - -#zi.liter() \ -#zi.mL() \ -#set-unit(lowercase-liter: true) -#zi.liter() \ -#zi.mL() \ diff --git a/tests/zi/fraction/.gitignore b/tests/zi/fraction/.gitignore new file mode 100644 index 0000000..2e4c5d8 --- /dev/null +++ b/tests/zi/fraction/.gitignore @@ -0,0 +1,5 @@ +# generated by tytanic, do not edit + +/diff/ +/out/ +/ref/ diff --git a/tests/zi/fraction/ref.typ b/tests/zi/fraction/ref.typ new file mode 100644 index 0000000..2d6adfd --- /dev/null +++ b/tests/zi/fraction/ref.typ @@ -0,0 +1,23 @@ +#set page(width: auto, height: auto, margin: .5em) + +#let th = sym.space.thin + +$"m"th "s"^(-1)$ \ +$"m"\/"s"$ \ +$"m"/"s"$ \ + +$"m"th "s"^(-1) th "N"^(-1)$ \ +$"m"\/("s"th"N")$ \ +$"m"/("s"th"N")$ \ + +#pagebreak() + +#set super(typographic: false) + +m#th;s#super[−1] \ +m/s \ +m/s \ + +m#th;s#super[−1]#th;N#super[−1] \ +m/(s#th;N) \ +m/(s#th;N) \ diff --git a/tests/zi/fraction/test.typ b/tests/zi/fraction/test.typ new file mode 100644 index 0000000..3cf7317 --- /dev/null +++ b/tests/zi/fraction/test.typ @@ -0,0 +1,26 @@ +#set page(width: auto, height: auto, margin: .5em) +#import "/src/zero.typ": set-unit, set-num, zi + + +#let m-s-N = zi.declare("m/s/N") + +#zi.m-s() \ +#zi.m-s(fraction: "inline") \ +#zi.m-s(fraction: "fraction") \ + +#m-s-N() \ +#m-s-N(fraction: "inline") \ +#m-s-N(fraction: "fraction") \ + + +#pagebreak() +#set-num(math: false) + +#zi.m-s() \ +#zi.m-s(fraction: "inline") \ +#zi.m-s(fraction: "fraction") \ + + +#m-s-N() \ +#m-s-N(fraction: "inline") \ +#m-s-N(fraction: "fraction") \ \ No newline at end of file diff --git a/tests/zi/lowercase-liter/.gitignore b/tests/zi/lowercase-liter/.gitignore new file mode 100644 index 0000000..2e73a39 --- /dev/null +++ b/tests/zi/lowercase-liter/.gitignore @@ -0,0 +1 @@ +ref/ \ No newline at end of file diff --git a/tests/zi/lowercase-liter/ref.typ b/tests/zi/lowercase-liter/ref.typ new file mode 100644 index 0000000..9fd46fe --- /dev/null +++ b/tests/zi/lowercase-liter/ref.typ @@ -0,0 +1,6 @@ +#set page(width: auto, height: auto, margin: .5em) + +$"L"$ \ +$"mL"$ \ +$"l"$ \ +$"ml"$ \ diff --git a/tests/zi/lowercase-liter/test.typ b/tests/zi/lowercase-liter/test.typ new file mode 100644 index 0000000..618e36f --- /dev/null +++ b/tests/zi/lowercase-liter/test.typ @@ -0,0 +1,8 @@ +#set page(width: auto, height: auto, margin: .5em) +#import "/src/zero.typ": set-unit, zi + +#zi.liter() \ +#zi.mL() \ +#set-unit(lowercase-liter: true) +#zi.liter() \ +#zi.mL() \