Typst smartquote algorithm implemented in Typst
Typst contains a “smartquote” algorithm, to automatically balance non-fancy quotes to fancy quotes. It also handles apostrophes and primes. While I don’t think the algorithm is particularly good, it’s probably as correct as any such algorithm can be without having a registry of special (I’ve, ’em, …) character sequences.
- https://typst.app/docs/reference/text/smartquote/
- https://docs.rs/typst/latest/typst/text/struct.SmartQuoter.html
- https://docs.rs/typst-library/0.15.1/src/typst_library/text/smartquote.rs.html#114-155
Currently, the algorithm is only available to the layout engine, with no direct programmatic access possible. Here, I’ve reimplemented the algorithm in pure Typst, so it can be used to balance strings in addition to content:
Reveal code
let balance-quotes(input) = {
if input == "" { return "" }
let graphemes = input.clusters()
let windows = ((" ", graphemes.at(0)),) + graphemes.windows(2)
let quote-rule(
prev, char, nesting-stack,
rules: ("\"": ("“", "”"), "'": ("‘", "’"))
) = {
if (char not in rules) {
return (char, nesting-stack)
}
let (opening, closing) = rules.at(char)
let opened = nesting-stack.last(default: none)
if (
opened != char
and prev.contains(regex("\d"))
) {
let prime = ("\"": "″", "'": "′").at(char)
(prime, nesting-stack)
} else if (
char == "'"
and opened != char
and prev.contains(regex("[\w\u{FFFC}]"))
) {
("’", nesting-stack)
} else if (
char == opened
and not prev.contains(regex("[\s\n(\[{]"))
) {
(closing, nesting-stack.slice(0, -1))
} else {
(opening, nesting-stack + (char,))
}
}
let (balanced, _) = windows.fold(
("", ()),
((acc, nesting-stack), (prev, char)) => {
let (char, nesting-stack) = quote-rule(prev, char, nesting-stack)
(acc + char, nesting-stack)
}
)
balanced
}