Coders have it easy. When they want to cast a variable name between cases there are a myriad of libraries for the purpose. Writers have it harder. Each language comes with its own challenges, and most have more than one style guide. In the case of English, even the style guides can leave much up to interpretation.
This demo is interesting not because different typesetting engines handle this either the same or differently, but because one library can be used in more than one engine. Specifically the decasify project implements various prose casing functions and can be used as a native package in either SILE or Typst, Glu uses a small wrapper function around it's command line interface, and Paged JS uses a JavaScript wrapper that makes the WASM module available to the typesetter.
This demonstrates all included engines effectively passing the content to the same casing library, with the caveat that Glu does not track the language attribute on specific text spans and pass them to the shaper. SILE, Typst, and Paged JS demonstrate the surrounding document context being maintained both into and out of the filter and hence into the final text shaping.
\begin[papersize=a6]{document}
\nofolios
\neverindent
\use[module=packages.decasify]
\begin{lua}
local examples = {
{ "first impulse", lang = "en" },
{ "FIRST IMPULSE", lang = "en" },
{ "ilk ışıltı", lang = "tr" },
{ "İLK IŞILTI", lang = "tr" },
}
for _, case in ipairs({ "title", "lower", "upper", "sentence" }) do
SILE.call("bigskip")
SILE.call("font", { size = "18pt", weight = 800 }, function ()
SILE.call("decasify", { case = "title" }, { case .. "case" })
end)
SILE.call("par")
for _, s in ipairs(examples) do
SILE.typesetter:typeset(("[%s] "):format(s.lang))
SILE.call("font", { language = s.lang }, function (_, _)
SILE.typesetter:typeset(("%s → "):format(s[1]))
SILE.call("decasify", { case = case }, s)
end)
SILE.call("par")
end
end
\end{lua}
\end{document}
$ sile -o title-case-sile.pdf title-case-sile.sil
#import "@preview/decasify:0.11.4": * #set page(paper: "a6") #let examples = ( (str: "first impulse", lang: "en"), (str: "FIRST IMPULSE", lang: "en"), (str: "ilk ışıltı", lang: "tr"), (str: "İLK IŞILTI", lang: "tr"), ) = Titlecase #for s in examples [ #set text(lang: s.lang) #s.str → #titlecase(s.str) \ ] = Lowercase #for s in examples [ #set text(lang: s.lang) #s.str → #lowercase(s.str) \ ] = Uppercase #for s in examples [ #set text(lang: s.lang) #s.str → #uppercase(s.str) \ ] = Sentencecase #for s in examples [ #set text(lang: s.lang) #s.str → #sentencecase(s.str) \ ]
$ typst compile title-case-typst.typ title-case-typst.pdf
---
papersize: a6
---
<style>
@page { margin: 1cm; }
h1 { font-size: 18pt; margin: 0; }
p { margin: 0 0 1em 0; }
</style>
```{lua}
-- Reuse decasify's locale-aware casing via its command line tool.
local function decasify(case, lang, s)
local cmd = ("decasify -c %s -l %s '%s'"):format(case, lang, s)
local pipe = io.popen(cmd)
local out = pipe:read("*a")
pipe:close()
return (out:gsub("%s+$", ""))
end
local examples = {
{ str = "first impulse", lang = "en" },
{ str = "FIRST IMPULSE", lang = "en" },
{ str = "ilk ışıltı", lang = "tr" },
{ str = "İLK IŞILTI", lang = "tr" },
}
local out = {}
for _, case in ipairs({ "title", "lower", "upper", "sentence" }) do
out[#out+1] = "# " .. decasify("title", "en", case .. "case") .. "\n"
local lines = {}
for _, e in ipairs(examples) do
lines[#lines+1] = ("[%s] %s -> %s"):format(e.lang, e.str, decasify(case, e.lang, e.str))
end
out[#out+1] = table.concat(lines, "\\\n") .. "\n"
end
return table.concat(out, "\n")
```
$ glu --css all-system-fonts.css -o title-case-glu.pdf title-case-glu.md
<html>
<head>
<style>
@page {
size: A6;
bleed: 0;
margin: 1cm;
}
h1 {
font-size: 18pt;
margin: 0;
}
p {
margin: 0;
}
p + h1 {
margin-top: 1em;
}
</style>
</head>
<body>
<script>
/* See caveat in src/build.js ... this inline script is some LLM generated
* monkey business to trigger our sample generator after an async WASM module
* is loaded and before Paged JS starts doing a layout pass.
* I'd love to see a contribution from somebody that does thiskkome saner way.
*/
window.externalLibs = window.externalLibs || {};
window.externalLibs.decasifyReady = new Promise(function (resolve) {
window.externalLibs._resolveDecasifyReady = resolve;
});
</script>
<script>
window.generateSample = function (container) {
var decasify = externalLibs.decasify;
var Locale = decasify.Locale;
var DefaultStyle = decasify.StyleGuide.LanguageDefault;
var examples = [
{ text: "first impulse", lang: "en", loc: Locale.EN },
{ text: "FIRST IMPULSE", lang: "en", loc: Locale.EN },
{ text: "ilk ışıltı", lang: "tr", loc: Locale.TR },
{ text: "İLK IŞILTI", lang: "tr", loc: Locale.TR },
];
var cases = [
{ name: "title", fn: function (text, locale) { return decasify.titlecase(text, locale, DefaultStyle); } },
{ name: "lower", fn: decasify.lowercase },
{ name: "upper", fn: decasify.uppercase },
{ name: "sentence", fn: decasify.sentencecase },
];
for (var ci = 0; ci < cases.length; ci++) {
var entry = cases[ci];
var heading = document.createElement("h1");
heading.textContent = decasify.titlecase(entry.name + "case", Locale.EN, DefaultStyle);
container.appendChild(heading);
for (var j = 0; j < examples.length; j++) {
var example = examples[j];
var p = document.createElement("p");
p.textContent = "[" + example.lang + "] " + example.text + " → " + entry.fn(example.text, example.loc);
container.appendChild(p);
}
}
};
</script>
</body>
</html>
$ pagedjs-cli --additional-script data/decasify.js -i title-case-pagedjs.html -o title-case-pagedjs.pdf