A Typst library that brings convenient string formatting and interpolation through the strfmt
function. Its syntax is taken directly from Rust's format!
syntax, so read its page for more information (https://doc.rust-lang.org/std/fmt/). Only a few things aren't supported from the Rust syntax, such as the p
(pointer) format type, or the .*
precision specifier.
I intend to add a few extras over time, though. The first "extra" I've added so far is the fmt-decimal-separator: "string"
parameter, which lets you customize the decimal separator for decimal numbers (floats) inserted into strings. E.g. strfmt("Result: {}", 5.8, fmt-decimal-separator: ",")
will return the string "Result: 5,8"
(comma instead of dot). See more below.
Compatible with: Typst v0.4.0
Download the typst-strfmt.typ
file either from Releases or directly from the repository. Then, move it to your project's folder, and write at the top of your typst file(s):
#import "typst-strfmt.typ": strfmt
That will give you access to the main function provided by this library (strfmt
), which accepts a format string, followed by zero or more replacements to insert in that string (according to {...}
formats inserted in that string), an optional fmt-decimal-separator
parameter, and returns the formatted string, as described below.
Its syntax is almost identical to Rust's format!
(as specified here: https://doc.rust-lang.org/std/fmt/). You can escape formats by duplicating braces ({{
and }}
become {
and }
). Here's an example (see more examples in the file tests/strfmt-tests.typ
):
let s = strfmt("I'm {}. I have {num} cars. I'm {0}. {} is {{cool}}.", "John", "Carl", num: 10)
assert.eq(s, "I'm John. I have 10 cars. I'm John. Carl is {cool}.")
Note that {}
extracts positional arguments after the string sequentially (the first {}
extracts the first one, the second {}
extracts the second one, and so on), while {0}
, {1}
, etc. will always extract the first, the second etc. positional arguments after the string. Additionally, {bananas}
will extract the named argument "bananas".
You can use {:spec}
to customize your output. See the Rust docs linked above for more info, but here's a summary:
- Adding a
?
at the end ofspec
(that is, writing e.g.{0:?}
) will callrepr()
to stringify your argument, instead ofstr()
. Note that this only has an effect if your argument is a string, an integer, a float or alabel()
/<label>
- for all other types (such as booleans or elements),repr()
is always called (asstr()
is unsupported for those).- For strings,
?
(and thusrepr()
) has the effect of printing them with double quotes. For floats, this ensures a.0
appears after it, even if it doesn't have decimal digits. For integers, this doesn't change anything. Finally, for labels, the<label>
(with?
) is printed as<label>
instead oflabel
.
- For strings,
- After the
:
, add e.g._<8
to align the string to the left, padding it with as many_
s as necessary for it to be at least8
characters long. Replace<
by>
for right alignment, or^
for center alignment. (If the_
is omitted, it defaults to ' ' (aligns with spaces).)- If you prefer to specify the minimum width (the
8
there) as a separate argument tostrfmt
instead, you can specifyargument$
in place of the width, which will extract it from the integer atargument
. For example,_^3$
will center align the output with_
s, where the minimum width desired is specified by the fourth positional argument (index3
), as an integer. This means that a call such asstrfmt("{:_^3$}", 1, 2, 3, 4)
would produce"__1__"
, as3$
would evaluate to4
(the value at the fourth positional argument/index3
). Similarly,named$
would take the width from the argument with namenamed
, if it is an integer (otherwise, error).
- If you prefer to specify the minimum width (the
- For numbers:
- Specify
+
after the:
to ensure zero or positive numbers are prefixed with+
before them (instead of having no sign).-
is also accepted but ignored (negative numbers always specify their sign anyways). - Use something like
:09
to add zeroes to the left of the number until it has at least 9 digits / characters.- The
9
here is also a width, so the same comment from before applies (you can add$
to take it from an argument to thestrfmt
function).
- The
- Use
:.5
to ensure your float is represented with 5 decimal digits of precision (zeroes are added to the right if needed; otherwise, it is rounded, not truncated).- Note that floating point inaccuracies can be sometimes observed here, which is an unfortunate current limitation.
- Similarly to
width
, the precision can also be specified via an argument with the$
syntax:.5$
will take the precision from the integer at argument number 5 (the sixth one), while.test$
will take it from the argument namedtest
.
- Integers only: Add
x
(lowercase hex) orX
(uppercase) at the end of thespec
to convert the number to hexadecimal. Also,b
will convert it to binary, whileo
will convert to octal.- Specify a hashtag, e.g.
#x
or#b
, to prepend the corresponding base prefix to the base-converted number, e.g.0xABC
instead ofABC
.
- Specify a hashtag, e.g.
- Add
e
orE
at the end of thespec
to ensure the number is represented in scientific notation (withe
orE
as the exponent separator, respectively). - For decimal numbers (floats), you can specify
fmt-decimal-separator: ","
tostrfmt
to have the decimal separator be a comma instead of a dot, for example.- To have this be the default, you can alias
strfmt
, such as using#let strfmt = strfmt.with(fmt-decimal-separator: ",")
.
- To have this be the default, you can alias
- Number spec arguments (such as
.5
) are ignored when the argument is not a number, but e.g. a string, even if it looks like a number (such as"5"
).
- Specify
- Note that all spec arguments above have to be specified in order - if you mix up the order, it won't work properly!
- Check the grammar below for the proper order, but, in summary: fill (character) with align (
<
,>
or^
) -> sign (+
or-
) ->#
->0
(for 0 left-padding of numbers) -> width (e.g.8
from08
or9
from-<9
) ->.precision
-> spec type (?
,x
,X
,b
,o
,e
,E
)).
- Check the grammar below for the proper order, but, in summary: fill (character) with align (
Some examples:
#let s1 = strfmt("{0:?}, {test:+012e}, {1:-<#8x}", "hi", -74, test: 569.4)
#assert.eq(s1, "\"hi\", +00005.694e2, -0x4a---")
#let s2 = strfmt("{:_>+11.5}", 59.4)
#assert.eq(s2, "__+59.40000")
#let s3 = strfmt("Dict: {:!<10?}", (a: 5))
#assert.eq(s3, "Dict: (a: 5)!!!!")
Here's the grammar specification for valid format spec
s (in {name:spec}
), which is basically Rust's format:
format_spec := [[fill]align][sign]['#']['0'][width]['.' precision]type
fill := character
align := '<' | '^' | '>'
sign := '+' | '-'
width := count
precision := count | '*'
type := '' | '?' | 'x?' | 'X?' | identifier
count := parameter | integer
parameter := argument '$'
Note, however, that precision of type .*
is not supported yet and will raise an error.
- Initial release, added
strfmt
.