Check the built pages' typography with JoliTypo
`make typo` (nix run .#typo-check) runs JoliTypo 2.0's phar over every language in ./result and prints what it would change, exiting 1 if that is anything. It reports rather than fixes, so the fix lands in the markdown or the templates.
549059e parent: b148e7c modified
Makefile +7 -1 | @@ -21,7 +21,7 @@ PROD_PORT ?= 8788 | ||
| 21 | 21 | # deploy-% targets aren't listed here: giving an instantiated pattern target |
| 22 | 22 | # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which |
| 23 | 23 | # then blocks the deploy-% pattern rule below from ever supplying a recipe. |
| 24 | -.PHONY: help dev build check-tracked serve prod-server infra-plan infra-apply deploy clean | |
| 24 | +.PHONY: help dev build check-tracked serve prod-server typo infra-plan infra-apply deploy clean | |
| 25 | 25 | |
| 26 | 26 | help: |
| 27 | 27 | @grep -E '^[a-zA-Z_.-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' |
| @@ -71,6 +71,12 @@ serve: ## Run the Hugo dev server with live reload | ||
| 71 | 71 | prod-server: build ## Serve the built site exactly as deployed, e.g. `make prod-server SITE_LANG=fr PROD_PORT=8080` |
| 72 | 72 | nix run .#prod-server -- $(SITE_LANG) $(PROD_PORT) |
| 73 | 73 | |
| 74 | +# Read-only: prints what JoliTypo would change in ./result, e.g. `make typo | |
| 75 | +# TYPO_LANGS=fr`. Exits 1 if there is anything, so it can gate CI later. | |
| 76 | +TYPO_LANGS ?= $(LANGS) | |
| 77 | +typo: build ## Show typography fixes JoliTypo would make in the built pages | |
| 78 | + nix run .#typo-check -- $(TYPO_LANGS) | |
| 79 | + | |
| 74 | 80 | infra-plan: ## Show what infra-apply would change on Cloudflare |
| 75 | 81 | nix run .#infra -- plan |
| 76 | 82 | |
| @@ -21,7 +21,7 @@ PROD_PORT ?= 8788 | |||
| 21 | # deploy-% targets aren't listed here: giving an instantiated pattern target | 21 | # deploy-% targets aren't listed here: giving an instantiated pattern target |
| 22 | # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which | 22 | # (e.g. deploy-en) to .PHONY creates an explicit no-recipe rule for it, which |
| 23 | # then blocks the deploy-% pattern rule below from ever supplying a recipe. | 23 | # then blocks the deploy-% pattern rule below from ever supplying a recipe. |
| 24 | -.PHONY: help dev build check-tracked serve prod-server infra-plan infra-apply deploy clean | 24 | +.PHONY: help dev build check-tracked serve prod-server typo infra-plan infra-apply deploy clean |
| 25 | 25 | ||
| 26 | help: | 26 | help: |
| 27 | @grep -E '^[a-zA-Z_.-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' | 27 | @grep -E '^[a-zA-Z_.-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' |
| @@ -71,6 +71,12 @@ serve: ## Run the Hugo dev server with live reload | |||
| 71 | prod-server: build ## Serve the built site exactly as deployed, e.g. `make prod-server SITE_LANG=fr PROD_PORT=8080` | 71 | prod-server: build ## Serve the built site exactly as deployed, e.g. `make prod-server SITE_LANG=fr PROD_PORT=8080` |
| 72 | nix run .#prod-server -- $(SITE_LANG) $(PROD_PORT) | 72 | nix run .#prod-server -- $(SITE_LANG) $(PROD_PORT) |
| 73 | 73 | ||
| 74 | +# Read-only: prints what JoliTypo would change in ./result, e.g. `make typo | ||
| 75 | +# TYPO_LANGS=fr`. Exits 1 if there is anything, so it can gate CI later. | ||
| 76 | +TYPO_LANGS ?= $(LANGS) | ||
| 77 | +typo: build ## Show typography fixes JoliTypo would make in the built pages | ||
| 78 | + nix run .#typo-check -- $(TYPO_LANGS) | ||
| 79 | + | ||
| 74 | infra-plan: ## Show what infra-apply would change on Cloudflare | 80 | infra-plan: ## Show what infra-apply would change on Cloudflare |
| 75 | nix run .#infra -- plan | 81 | nix run .#infra -- plan |
| 76 | 82 | ||
modified
flake.nix +70 -0 | @@ -25,6 +25,11 @@ | ||
| 25 | 25 | # once Newsreader/JetBrains Mono (self-hosted, already covering every |
| 26 | 26 | # other language here) come up short on a codepoint. |
| 27 | 27 | ipaexfont = pin.version "ipaexfont" "004.01"; |
| 28 | + # 2.0.0 is the first release whose phar actually contains the library. | |
| 29 | + jolitypo = pkgs.fetchurl { | |
| 30 | + url = "https://github.com/jolicode/JoliTypo/releases/download/v2.0.0/jolitypo.phar"; | |
| 31 | + hash = "sha256-d6SZko7GvHwc1cDpRsPHLb+owM0E5za5/fBHhW+dIYE="; | |
| 32 | + }; | |
| 28 | 33 | in |
| 29 | 34 | { |
| 30 | 35 | packages.${system}.default = pkgs.stdenvNoCC.mkDerivation { |
| @@ -267,6 +272,71 @@ FONTCONF | ||
| 267 | 272 | ''; |
| 268 | 273 | }}/bin/mykiwi-blog-prod-server"; |
| 269 | 274 | }; |
| 275 | + | |
| 276 | + # `nix run .#typo-check -- [lang...]` - runs JoliTypo over the built | |
| 277 | + # `./result/<lang>` pages (every language in languages.txt by default) | |
| 278 | + # and prints what it would change, exiting 1 if that is anything. It | |
| 279 | + # fixes nothing: the answer belongs in the markdown or the templates, | |
| 280 | + # not in a post-processing step. | |
| 281 | + # | |
| 282 | + # The CLI rewrites files in place and re-serialises the whole page | |
| 283 | + # (adds the doctype, quotes attributes, expands `<rect />`), so a plain | |
| 284 | + # diff against `result/` is all noise. Instead each page is copied | |
| 285 | + # twice and put through two passes: UnicodeNormalization alone, a no-op | |
| 286 | + # on composed text that only gives the baseline the same | |
| 287 | + # serialisation, and the real rules. The diff between those two is the | |
| 288 | + # typography. | |
| 289 | + # | |
| 290 | + # It has to be given single files, not a directory: given one, the CLI | |
| 291 | + # also rewrites index.xml, feed.js and feed.xsl, and then crashes. | |
| 292 | + # | |
| 293 | + # Hyphen is left out on purpose: soft hyphens are spelled out letter by | |
| 294 | + # letter by screen readers, and it has no dictionary for es or ja. | |
| 295 | + # | |
| 296 | + # ja skips four more. JoliTypo has no Japanese quotation style, so | |
| 297 | + # SmartQuotes would push "…" towards “…” where 「…」 is the norm, and | |
| 298 | + # Dash, SpaceBeforePunctuation and NoSpaceBeforeComma bind or strip | |
| 299 | + # spaces by Latin-script rules, in a script that puts no spaces | |
| 300 | + # between words at all. | |
| 301 | + typo-check = { | |
| 302 | + type = "app"; | |
| 303 | + program = "${pkgs.writeShellApplication { | |
| 304 | + name = "mykiwi-blog-typo-check"; | |
| 305 | + runtimeInputs = [ pkgs.php pkgs.git pkgs.coreutils pkgs.diffutils pkgs.findutils pkgs.gnused ]; | |
| 306 | + text = '' | |
| 307 | + root="$(git rev-parse --show-toplevel)" | |
| 308 | + # shellcheck disable=SC2046 # one language per word, on purpose | |
| 309 | + [ $# -gt 0 ] || set -- $(cat "$root/languages.txt") | |
| 310 | + work="$(mktemp -d)" | |
| 311 | + trap 'rm -rf "$work"' EXIT | |
| 312 | + | |
| 313 | + for lang in "$@"; do | |
| 314 | + src="$root/result/$lang" | |
| 315 | + if [ ! -d "$src" ]; then | |
| 316 | + echo "$src does not exist - run 'make build' first." >&2 | |
| 317 | + exit 1 | |
| 318 | + fi | |
| 319 | + # The language code is enough of a locale: JoliTypo only reads | |
| 320 | + # its language part, and fr/fr_FR, en/en_GB... fix alike. | |
| 321 | + rules=(UnicodeNormalization Ellipsis Dimension Unit CurlyQuote Trademark) | |
| 322 | + [ "$lang" = ja ] || rules+=(Dash SmartQuotes SpaceBeforePunctuation NoSpaceBeforeComma) | |
| 323 | + while IFS= read -r -d "" page; do | |
| 324 | + install -Dm644 "$src/$page" "$work/base/$lang/$page" | |
| 325 | + install -Dm644 "$src/$page" "$work/fixed/$lang/$page" | |
| 326 | + php ${jolitypo} --silent "$lang" "$work/base/$lang/$page" --rule=UnicodeNormalization | |
| 327 | + php ${jolitypo} --silent "$lang" "$work/fixed/$lang/$page" "''${rules[@]/#/--rule=}" | |
| 328 | + done < <(cd "$src" && find . -name '*.html' -print0) | |
| 329 | + done | |
| 330 | + | |
| 331 | + # One element per line and visible spaces, or a minified page is | |
| 332 | + # a single unreadable line. The serialiser already writes U+00A0 | |
| 333 | + # as , but leaves U+202F raw. | |
| 334 | + find "$work" -name '*.html' -exec sed -i -e 's/></>\n</g' -e 's/\xe2\x80\xaf/[nnbsp]/g' {} + | |
| 335 | + cd "$work" | |
| 336 | + diff -ru base fixed | |
| 337 | + ''; | |
| 338 | + }}/bin/mykiwi-blog-typo-check"; | |
| 339 | + }; | |
| 270 | 340 | }; |
| 271 | 341 | }; |
| 272 | 342 | } |
| @@ -25,6 +25,11 @@ | |||
| 25 | # once Newsreader/JetBrains Mono (self-hosted, already covering every | 25 | # once Newsreader/JetBrains Mono (self-hosted, already covering every |
| 26 | # other language here) come up short on a codepoint. | 26 | # other language here) come up short on a codepoint. |
| 27 | ipaexfont = pin.version "ipaexfont" "004.01"; | 27 | ipaexfont = pin.version "ipaexfont" "004.01"; |
| 28 | + # 2.0.0 is the first release whose phar actually contains the library. | ||
| 29 | + jolitypo = pkgs.fetchurl { | ||
| 30 | + url = "https://github.com/jolicode/JoliTypo/releases/download/v2.0.0/jolitypo.phar"; | ||
| 31 | + hash = "sha256-d6SZko7GvHwc1cDpRsPHLb+owM0E5za5/fBHhW+dIYE="; | ||
| 32 | + }; | ||
| 28 | in | 33 | in |
| 29 | { | 34 | { |
| 30 | packages.${system}.default = pkgs.stdenvNoCC.mkDerivation { | 35 | packages.${system}.default = pkgs.stdenvNoCC.mkDerivation { |
| @@ -267,6 +272,71 @@ FONTCONF | |||
| 267 | ''; | 272 | ''; |
| 268 | }}/bin/mykiwi-blog-prod-server"; | 273 | }}/bin/mykiwi-blog-prod-server"; |
| 269 | }; | 274 | }; |
| 275 | + | ||
| 276 | + # `nix run .#typo-check -- [lang...]` - runs JoliTypo over the built | ||
| 277 | + # `./result/<lang>` pages (every language in languages.txt by default) | ||
| 278 | + # and prints what it would change, exiting 1 if that is anything. It | ||
| 279 | + # fixes nothing: the answer belongs in the markdown or the templates, | ||
| 280 | + # not in a post-processing step. | ||
| 281 | + # | ||
| 282 | + # The CLI rewrites files in place and re-serialises the whole page | ||
| 283 | + # (adds the doctype, quotes attributes, expands `<rect />`), so a plain | ||
| 284 | + # diff against `result/` is all noise. Instead each page is copied | ||
| 285 | + # twice and put through two passes: UnicodeNormalization alone, a no-op | ||
| 286 | + # on composed text that only gives the baseline the same | ||
| 287 | + # serialisation, and the real rules. The diff between those two is the | ||
| 288 | + # typography. | ||
| 289 | + # | ||
| 290 | + # It has to be given single files, not a directory: given one, the CLI | ||
| 291 | + # also rewrites index.xml, feed.js and feed.xsl, and then crashes. | ||
| 292 | + # | ||
| 293 | + # Hyphen is left out on purpose: soft hyphens are spelled out letter by | ||
| 294 | + # letter by screen readers, and it has no dictionary for es or ja. | ||
| 295 | + # | ||
| 296 | + # ja skips four more. JoliTypo has no Japanese quotation style, so | ||
| 297 | + # SmartQuotes would push "…" towards “…” where 「…」 is the norm, and | ||
| 298 | + # Dash, SpaceBeforePunctuation and NoSpaceBeforeComma bind or strip | ||
| 299 | + # spaces by Latin-script rules, in a script that puts no spaces | ||
| 300 | + # between words at all. | ||
| 301 | + typo-check = { | ||
| 302 | + type = "app"; | ||
| 303 | + program = "${pkgs.writeShellApplication { | ||
| 304 | + name = "mykiwi-blog-typo-check"; | ||
| 305 | + runtimeInputs = [ pkgs.php pkgs.git pkgs.coreutils pkgs.diffutils pkgs.findutils pkgs.gnused ]; | ||
| 306 | + text = '' | ||
| 307 | + root="$(git rev-parse --show-toplevel)" | ||
| 308 | + # shellcheck disable=SC2046 # one language per word, on purpose | ||
| 309 | + [ $# -gt 0 ] || set -- $(cat "$root/languages.txt") | ||
| 310 | + work="$(mktemp -d)" | ||
| 311 | + trap 'rm -rf "$work"' EXIT | ||
| 312 | + | ||
| 313 | + for lang in "$@"; do | ||
| 314 | + src="$root/result/$lang" | ||
| 315 | + if [ ! -d "$src" ]; then | ||
| 316 | + echo "$src does not exist - run 'make build' first." >&2 | ||
| 317 | + exit 1 | ||
| 318 | + fi | ||
| 319 | + # The language code is enough of a locale: JoliTypo only reads | ||
| 320 | + # its language part, and fr/fr_FR, en/en_GB... fix alike. | ||
| 321 | + rules=(UnicodeNormalization Ellipsis Dimension Unit CurlyQuote Trademark) | ||
| 322 | + [ "$lang" = ja ] || rules+=(Dash SmartQuotes SpaceBeforePunctuation NoSpaceBeforeComma) | ||
| 323 | + while IFS= read -r -d "" page; do | ||
| 324 | + install -Dm644 "$src/$page" "$work/base/$lang/$page" | ||
| 325 | + install -Dm644 "$src/$page" "$work/fixed/$lang/$page" | ||
| 326 | + php ${jolitypo} --silent "$lang" "$work/base/$lang/$page" --rule=UnicodeNormalization | ||
| 327 | + php ${jolitypo} --silent "$lang" "$work/fixed/$lang/$page" "''${rules[@]/#/--rule=}" | ||
| 328 | + done < <(cd "$src" && find . -name '*.html' -print0) | ||
| 329 | + done | ||
| 330 | + | ||
| 331 | + # One element per line and visible spaces, or a minified page is | ||
| 332 | + # a single unreadable line. The serialiser already writes U+00A0 | ||
| 333 | + # as , but leaves U+202F raw. | ||
| 334 | + find "$work" -name '*.html' -exec sed -i -e 's/></>\n</g' -e 's/\xe2\x80\xaf/[nnbsp]/g' {} + | ||
| 335 | + cd "$work" | ||
| 336 | + diff -ru base fixed | ||
| 337 | + ''; | ||
| 338 | + }}/bin/mykiwi-blog-typo-check"; | ||
| 339 | + }; | ||
| 270 | }; | 340 | }; |
| 271 | }; | 341 | }; |
| 272 | } | 342 | } |