From abf63c876d437fad93bd9f4ba780c7a61379dc2b Mon Sep 17 00:00:00 2001 From: Patrick Gundlach Date: Thu, 20 Aug 2026 09:21:01 +0200 Subject: [PATCH] Add glu typesetter with samples for all eleven demos glu renders Markdown, HTML or Lua straight to PDF on top of the boxes and glue typesetting library (OpenType shaping, Knuth-Plass line breaking, an OpenType MATH engine and CSS Paged Media support). - Makefile pattern rules for glu.md / glu.html / glu.lua inputs and a .fonts download target for the Libertinus faces the samples use - a buildGoModule package for glu in the Nix dev shell (glu is not in nixpkgs yet); unzip joins the shell inputs for the font target - samples for all eleven demos --- .gitignore | 1 + Makefile | 23 ++++++++- content/dropcaps.md | 2 +- content/hello-world.md | 2 +- content/hieroglyphs.md | 2 +- content/integral-alignment.md | 2 +- content/italic-correction.md | 2 +- content/page-geometry.md | 2 +- content/prime-symbol.md | 2 +- content/superscript.md | 2 +- content/title-case.md | 2 +- content/varlimits.md | 2 +- content/willans.md | 2 +- data/dropcaps/glu.html | 34 ++++++++++++++ data/hello-world/glu.md | 5 ++ data/hieroglyphs/glu.html | 34 ++++++++++++++ data/integral-alignment/glu.md | 18 +++++++ data/italic-correction/glu.html | 34 ++++++++++++++ data/page-geometry/glu.html | 12 +++++ data/prime-symbol/glu.md | 32 +++++++++++++ data/superscript/glu.html | 21 +++++++++ data/title-case/glu.md | 83 +++++++++++++++++++++++++++++++++ data/varlimits/glu.html | 70 +++++++++++++++++++++++++++ data/willans/glu.md | 19 ++++++++ flake.nix | 22 +++++++++ templates/menu.html | 1 + 26 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 data/dropcaps/glu.html create mode 100644 data/hello-world/glu.md create mode 100644 data/hieroglyphs/glu.html create mode 100644 data/integral-alignment/glu.md create mode 100644 data/italic-correction/glu.html create mode 100644 data/page-geometry/glu.html create mode 100644 data/prime-symbol/glu.md create mode 100644 data/superscript/glu.html create mode 100644 data/title-case/glu.md create mode 100644 data/varlimits/glu.html create mode 100644 data/willans/glu.md diff --git a/.gitignore b/.gitignore index 2faa404..8181d14 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ data/*.toml data/*.aux data/*/*-aux data/*.log +data/*-aux.json .fonts static public diff --git a/Makefile b/Makefile index 4c8b783..06b2dcb 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ AWK ?= awk CMP ?= cmp CURL ?= curl GIT ?= git +GLU ?= glu GROFF ?= groff LUAROCKS ?= luarocks MAGICK ?= magick @@ -33,6 +34,8 @@ ZOLA ?= zola BASE_URL = / +GLU_ARGS = -o $@ $< + GROFF_ARGS = -T pdf $< > $@ PAGEDJS_ARGS = -i $< -o $@ @@ -98,7 +101,7 @@ $(LUAMODLOCK): $(LUAROCKSMANIFEST) $(LUAMODSPEC) $(genrockslock) > $@ .PHONY: fonts -fonts: .fonts/EgyptianOpenType.ttf +fonts: .fonts/EgyptianOpenType.ttf .fonts/LibertinusSerif-Regular.otf .fonts: mkdir -p $@ @@ -107,8 +110,26 @@ fonts: .fonts/EgyptianOpenType.ttf $(CURL) -fsSL https://github.com/microsoft/font-tools/raw/main/EgyptianOpenType/font/eot.ttf -o $@ touch $@ +.fonts/LibertinusSerif-Regular.otf: | .fonts + $(CURL) -fsSL https://github.com/alerque/libertinus/releases/download/v7.051/Libertinus-7.051.zip -o .fonts/libertinus.zip + cd .fonts && unzip -o -j -q libertinus.zip '*/static/OTF/LibertinusSerif-Regular.otf' '*/static/OTF/LibertinusSerif-Italic.otf' '*/static/OTF/LibertinusSerif-Bold.otf' '*/static/OTF/LibertinusMath-Regular.otf' + rm .fonts/libertinus.zip + touch $@ + %-groff.pdf %-groff.toml: TYPESETTER_ARGS = $(call get_typesetter_args,content/$(notdir $(basename $*)).md,$(notdir $(basename $<))) +%-glu.pdf %-glu.toml: %/glu.md + local args="$(call get_typesetter_args,content/$(notdir $(basename $*)).md,$(notdir $(basename $<)))" + $(call make_manifest,$(GLU) $(TYPESETTER_ARGS) $(GLU_ARGS)) + +%-glu.pdf %-glu.toml: %/glu.html + local args="$(call get_typesetter_args,content/$(notdir $(basename $*)).md,$(notdir $(basename $<)))" + $(call make_manifest,$(GLU) $(TYPESETTER_ARGS) $(GLU_ARGS)) + +%-glu.pdf %-glu.toml: %/glu.lua + local args="$(call get_typesetter_args,content/$(notdir $(basename $*)).md,$(notdir $(basename $<)))" + $(call make_manifest,$(GLU) $(TYPESETTER_ARGS) $(GLU_ARGS)) + %-groff.pdf %-groff.toml: %/groff.groff $(call make_manifest,$(GROFF) $(TYPESETTER_ARGS) $(GROFF_ARGS)) diff --git a/content/dropcaps.md b/content/dropcaps.md index 2713fa6..9622ef6 100644 --- a/content/dropcaps.md +++ b/content/dropcaps.md @@ -1,7 +1,7 @@ +++ title = "Dropcaps" description = "For that bold leading character" -extra.typesetters = [ "sile", "typst", "xelatex", "groff" ] +extra.typesetters = [ "sile", "typst", "xelatex", "groff", "glu" ] +++ Once upon a time in a far away land all chapters began with these. diff --git a/content/hello-world.md b/content/hello-world.md index 2e6921f..db32886 100644 --- a/content/hello-world.md +++ b/content/hello-world.md @@ -1,7 +1,7 @@ +++ title = "Hello World!" description = "Your most basic greeting." -extra.typesetters = [ "sile", "typst", "xelatex", "groff", "satysfi", "pagedjs", "weasyprint" ] +extra.typesetters = [ "sile", "typst", "xelatex", "groff", "satysfi", "pagedjs", "weasyprint", "glu" ] +++ Just the simplest way to get a phrase onto a numbered page. diff --git a/content/hieroglyphs.md b/content/hieroglyphs.md index 9d39d9d..24b0579 100644 --- a/content/hieroglyphs.md +++ b/content/hieroglyphs.md @@ -1,7 +1,7 @@ +++ title = "Hieroglyphs" description = "Language of the Pharaohs" -extra.typesetters = [ "sile", "typst" ] +extra.typesetters = [ "sile", "typst", "glu" ] +++ In ancient times pictographs –wielded in the hands of the Pharaohs– ruled the world . diff --git a/content/integral-alignment.md b/content/integral-alignment.md index d3d3122..f76eb10 100644 --- a/content/integral-alignment.md +++ b/content/integral-alignment.md @@ -1,7 +1,7 @@ +++ title = "Alignment of Integrals" description = "Compare the baseline alignment of integrals." -extra.typesetters = [ "typst", "sile", "xelatex" ] +extra.typesetters = [ "typst", "sile", "xelatex", "glu" ] +++ Compare the baseline alignment of integrals. diff --git a/content/italic-correction.md b/content/italic-correction.md index 5cc9e59..5d235da 100644 --- a/content/italic-correction.md +++ b/content/italic-correction.md @@ -1,7 +1,7 @@ +++ title = "Automated Italic Correction" description = "Switching between italic and roman, and reciprocally" -extra.typesetters = [ "sile", "xelatex", "typst" ] +extra.typesetters = [ "sile", "xelatex", "typst", "glu" ] +++ When an italicized word is followed or preceded by non-italicized text, the spacing may need to be adjusted, or “corrected”, so that the characters do not overlap. diff --git a/content/page-geometry.md b/content/page-geometry.md index 47f569f..907a034 100644 --- a/content/page-geometry.md +++ b/content/page-geometry.md @@ -1,7 +1,7 @@ +++ title = "Page Geometry 101" description = "Pick a paper and some margins" -extra.typesetters = [ "sile", "typst", "xelatex", "groff", "pagedjs" ] +extra.typesetters = [ "sile", "typst", "xelatex", "groff", "pagedjs", "glu" ] extra.typesetter_args = { groff = "-P-pA7" } +++ diff --git a/content/prime-symbol.md b/content/prime-symbol.md index 363dd8c..c42eac0 100644 --- a/content/prime-symbol.md +++ b/content/prime-symbol.md @@ -1,7 +1,7 @@ +++ title = "The Prime Symbol" description = "In math mode, prime-time is always a good time." -extra.typesetters = [ "typst", "sile", "xelatex", "groff" ] +extra.typesetters = [ "typst", "sile", "xelatex", "groff", "glu" ] extra.typesetter_args = { groff = "-e -P-pA7" } +++ diff --git a/content/superscript.md b/content/superscript.md index 465543e..4b3ec93 100644 --- a/content/superscript.md +++ b/content/superscript.md @@ -1,7 +1,7 @@ +++ title = "Superscript" description = "Those superior characters are not as simple as they seem" -extra.typesetters = [ "sile", "xelatex", "typst" ] +extra.typesetters = [ "sile", "xelatex", "typst", "glu" ] +++ Superscripts are sometimes needed for numbers (e.g., in footnote calls), but also for letters (e.g., in French, for centuries, issue numbers; likewise in English —depending on authors— for centuries, etc.) diff --git a/content/title-case.md b/content/title-case.md index df45a64..9ba0660 100644 --- a/content/title-case.md +++ b/content/title-case.md @@ -1,7 +1,7 @@ +++ title = "Title Case" description = "Title casing prose is more complex than munging some glyphs." -extra.typesetters = [ "sile", "typst" ] +extra.typesetters = [ "sile", "typst", "glu" ] +++ Coders have it easy. diff --git a/content/varlimits.md b/content/varlimits.md index a93a96d..9b48f01 100644 --- a/content/varlimits.md +++ b/content/varlimits.md @@ -1,7 +1,7 @@ +++ title = "Variant limits" description = "Those limits with a little extra flair" -extra.typesetters = [ "sile", "xelatex", "typst" ] +extra.typesetters = [ "sile", "xelatex", "typst", "glu" ] +++ Variants for superior, inferior, projective, and injective limits are an interesting challenge for typesetting engines with mathematical capabilities. diff --git a/content/willans.md b/content/willans.md index b26b27e..d4db1bd 100644 --- a/content/willans.md +++ b/content/willans.md @@ -1,7 +1,7 @@ +++ title = "Willan’s Formula" description = "An exact prime formula which is however quite insane and fairly useless." -extra.typesetters = [ "typst", "sile", "xelatex" ] +extra.typesetters = [ "typst", "sile", "xelatex", "glu" ] +++ Willan's formula for the nth prime circa 1964; a.k.a. an exact prime formula which is quite insane and fairly useless but a nice typesetting stress test. diff --git a/data/dropcaps/glu.html b/data/dropcaps/glu.html new file mode 100644 index 0000000..458c469 --- /dev/null +++ b/data/dropcaps/glu.html @@ -0,0 +1,34 @@ + + + + + + +

This paragraph has a pretty plain initial or drop cap. +It uses the default document font. You didn't really expect more +detail with such a generic font choice, right? This may be exactly +what you want, especially with modern typesetting styles which tend +towards the minimalist.

+ +

“Never say never,” the +saying goes. Someday your dropcap may include leading punctuation. +No worries. The quotation mark hangs together with the letter.

+ +

Another paragraph shows off a different line count. +This helps highlight the fact that the initial letter belongs to the +first word. The first line of text will be flush against the drop +cap, and so are the following lines that the initial spans.

+ + + diff --git a/data/hello-world/glu.md b/data/hello-world/glu.md new file mode 100644 index 0000000..6d603bf --- /dev/null +++ b/data/hello-world/glu.md @@ -0,0 +1,5 @@ + + +Hello World diff --git a/data/hieroglyphs/glu.html b/data/hieroglyphs/glu.html new file mode 100644 index 0000000..42d9e24 --- /dev/null +++ b/data/hieroglyphs/glu.html @@ -0,0 +1,34 @@ + + + + + +

mdw.t-km.t ‘Egyptian language’

+

𓌃𓂧𓐰𓏏𓐱𓏯𓀁𓏪𓆎𓅓𓊖

+ +

nfr ‘fine, beautiful’

+

𓄤𓆑𓐰𓂋

+ +

ḫprj ‘Khepri’

+

𓆣𓐰𓂋𓇋𓁛

+ + diff --git a/data/integral-alignment/glu.md b/data/integral-alignment/glu.md new file mode 100644 index 0000000..2608b0d --- /dev/null +++ b/data/integral-alignment/glu.md @@ -0,0 +1,18 @@ +--- +math: true +--- + + + +$$\sum_{k=0}^{\infty} \int_0^{\infty} e^{-kx^2} \mathrm{d}x$$ diff --git a/data/italic-correction/glu.html b/data/italic-correction/glu.html new file mode 100644 index 0000000..e5486f4 --- /dev/null +++ b/data/italic-correction/glu.html @@ -0,0 +1,34 @@ + + + + + + +

No italic correction:

+

(fluff)

+

¡fanciful proof!

+

[fluff]

+ +

With -bag-italic-correction: auto, a +heuristic kern from the boundary glyph's ink overhang:

+

(fluff)

+

¡fanciful proof!

+

[fluff]

+ + + diff --git a/data/page-geometry/glu.html b/data/page-geometry/glu.html new file mode 100644 index 0000000..851af4f --- /dev/null +++ b/data/page-geometry/glu.html @@ -0,0 +1,12 @@ + + + + + +

An A7 page with 1cm margins and 1cm paragraph indentation.

+ + diff --git a/data/prime-symbol/glu.md b/data/prime-symbol/glu.md new file mode 100644 index 0000000..8c99e4f --- /dev/null +++ b/data/prime-symbol/glu.md @@ -0,0 +1,32 @@ +--- +papersize: a6 +math: true +--- + + + +Math mode manners: + +$$f(x) = a' + b'' + c'''$$ + +$$f'(x) = x^2 + 1$$ + +Prose poses problems: + +60\*10'16"N 24\*55'52"E (plain) + +60°10′16″N 24°55′52″E (unicode) + +Markdown never turns straight quotes into smart quotes, so +feet, inches, minutes and seconds stay as typed. diff --git a/data/superscript/glu.html b/data/superscript/glu.html new file mode 100644 index 0000000..b340b47 --- /dev/null +++ b/data/superscript/glu.html @@ -0,0 +1,21 @@ + + + + + +

ther (not superscript)

+

Xᵗʰᵉʳ (unicode superscript)

+

Xther (font feature)

+

Xther (scaled and raised)

+ + diff --git a/data/title-case/glu.md b/data/title-case/glu.md new file mode 100644 index 0000000..c364ff1 --- /dev/null +++ b/data/title-case/glu.md @@ -0,0 +1,83 @@ +--- +papersize: a6 +--- + + + +```{lua} +-- Prose casing scripted right inside the document: no package needed. +-- Turkish has its own case folding: i <-> İ and ı <-> I. +local tr_to_lower = { ["İ"]="i", ["I"]="ı", ["Ş"]="ş", ["Ğ"]="ğ", ["Ü"]="ü", ["Ö"]="ö", ["Ç"]="ç" } +local tr_to_upper = { ["i"]="İ", ["ı"]="I", ["ş"]="Ş", ["ğ"]="Ğ", ["ü"]="Ü", ["ö"]="Ö", ["ç"]="Ç" } + +local function lower(s, lang) + if lang == "tr" then + for from, to in pairs(tr_to_lower) do s = s:gsub(from, to) end + end + return s:lower() +end + +local function upper(s, lang) + if lang == "tr" then + for from, to in pairs(tr_to_upper) do s = s:gsub(from, to) end + end + return s:upper() +end + +-- Split the first UTF-8 character off a word. +local function split_first(word) + local n, b = 1, word:byte(1) + if b >= 0xF0 then n = 4 elseif b >= 0xE0 then n = 3 elseif b >= 0xC0 then n = 2 end + return word:sub(1, n), word:sub(n + 1) +end + +local small = { + a=true, an=true, ["and"]=true, as=true, at=true, but=true, by=true, + ["for"]=true, ["in"]=true, nor=true, of=true, on=true, ["or"]=true, + per=true, so=true, the=true, to=true, via=true, yet=true, +} + +local function titlecase(s, lang) + local words, n = {}, 0 + for w in lower(s, lang):gmatch("%S+") do n = n + 1; words[n] = w end + for k, w in ipairs(words) do + if lang == "tr" or k == 1 or k == n or not small[w] then + local first, rest = split_first(w) + words[k] = upper(first, lang) .. rest + end + end + return table.concat(words, " ") +end + +local function sentencecase(s, lang) + local first, rest = split_first(lower(s, lang)) + return upper(first, lang) .. rest +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 = {} +local function section(title, fn) + out[#out+1] = "## " .. title .. "\n" + local lines = {} + for _, e in ipairs(examples) do + lines[#lines+1] = ("[%s] %s -> %s"):format(e.lang, e.str, fn(e.str, e.lang)) + end + out[#out+1] = table.concat(lines, "\\\n") .. "\n" +end + +section("Titlecase", titlecase) +section("Lowercase", lower) +section("Uppercase", upper) +section("Sentencecase", sentencecase) +return table.concat(out, "\n") +``` diff --git a/data/varlimits/glu.html b/data/varlimits/glu.html new file mode 100644 index 0000000..964c589 --- /dev/null +++ b/data/varlimits/glu.html @@ -0,0 +1,70 @@ + + + + + + +

+ proj limiI + Ai + = + + lim + iI + + Ai +

+ +

Inline: + proj limiI + Ai + = + + lim + iI + + Ai +, the arrow stays under the limit while the +expression moves to the side.

+ +

+ inj limiI + Ai + = + + lim + iI + + Ai +

+ +

+ lim infn + an + = + + lim̲ + n + + an +

+ + + + + diff --git a/data/willans/glu.md b/data/willans/glu.md new file mode 100644 index 0000000..a494ede --- /dev/null +++ b/data/willans/glu.md @@ -0,0 +1,19 @@ +--- +papersize: a7 landscape +math: true +--- + + + +$$p_n = 1 + \sum_{i=1}^{2^n} \left\lfloor \left( \frac{n}{\sum_{j=1}^{i} \left\lfloor \cos^2\left( \pi \frac{(j-1)!+1}{j} \right) \right\rfloor} \right)^{1/n} \right\rfloor$$ diff --git a/flake.nix b/flake.nix index 7944463..c2053d6 100644 --- a/flake.nix +++ b/flake.nix @@ -39,6 +39,26 @@ ]; }; inherit (gitignore.lib) gitignoreSource; + glu = pkgs.buildGoModule rec { + pname = "glu"; + version = "0.0.28"; + src = pkgs.fetchFromGitHub { + owner = "boxesandglue"; + repo = "glu"; + rev = "v${version}"; + hash = "sha256-piOQXtSkxTd8QPq1Xb+6gLsFfPtqO3rzab9f+m6RPk8="; + }; + vendorHash = "sha256-/AMehjNrw9lmiy95pDsZkvgv9hCc+Mpe2Yc2tk796jU="; + subPackages = [ "glu" ]; + # The golden tests compare rendered PDFs and need fonts + # that are not available inside the build sandbox. + doCheck = false; + ldflags = [ + "-s" + "-w" + "-X main.Version=${version}" + ]; + }; polytype = rec { }; @@ -54,6 +74,7 @@ gentium ghostscript git + glu gnumake groff groff.perl @@ -70,6 +91,7 @@ teracli.defaultPackage.${system} texlive.combined.scheme-full typst + unzip python313Packages.weasyprint xdg-utils yq diff --git a/templates/menu.html b/templates/menu.html index 8ffd107..67b550b 100644 --- a/templates/menu.html +++ b/templates/menu.html @@ -22,6 +22,7 @@