Conventions¶
House rules for writing a page here. Readers browsing lessons do not need this file; it is for whoever is about to add one.
The rule that comes before the others¶
A page never claims something a program has not printed — on both machines. CI runs every example on ubuntu-latest and macos-latest, each with the Perl and Python its image ships, and an answer key is only what both agree on. A sentence that no program here backs, such as a remark about how another language behaves, ends with (Not machine-checked here.) or links to the sibling library page that does check it.
The shape of a lesson¶
04_Regex/
anchors_match_every_line/
README.md the lesson
examples/
anchors_match_every_line_rb.rb the Ruby program
anchors_match_every_line_rb.out its recorded output (generated — do not hand-edit)
anchors_match_every_line_pl.pl the same point in Perl, when the comparison teaches something
anchors_match_every_line_py.py the same point in Python, likewise
One idea per folder. The folder name is the idea, in lower_snake_case, and it becomes a permanent URL — so name it for what it teaches, not for where it sits in the reading order. A page names an example by its bare stem, so stems must be unique across languages: the suffix is _rb, _pl, _py or _sh.
The page¶
Open with the title, a **Level:** line (101 / 201 / 301, then ·, then who it is for) and a **One line:** that states the claim rather than the topic. Put the output block early; explain it after. Do not hard-wrap paragraphs — one paragraph, one line.
Output is generated, never typed¶
Mark the spot and let the tool fill it:
tools/run_examples.py runs the example and pastes what it actually printed, with a provenance line above the fence. Inside the markers is generated; outside is yours. A second kind, <!-- source:stem -->, pastes the program itself.
python3 tools/run_examples.py # verify + refill
python3 tools/run_examples.py --update --only X # record X's output as its answer key
python3 tools/run_examples.py --check # write nothing, fail on drift (CI)
python3 tools/check_all.py --staged # every gate CI runs, on what you are about to commit
Always pass --only with --update, and read what it recorded before committing: --update accepts whatever the program printed, so it will happily enshrine a bug.
The programs¶
Ruby: 4.0 or later, run as ruby -E UTF-8 <file> from the example's folder, under LC_ALL=C. The tool finds the Ruby itself ($RUBY, PATH, Homebrew) and refuses an older one. Standard library and bundled gems only — csv and reline are bundled gems. -E UTF-8 gives every example what a reader's UTF-8 terminal gives them; a lesson whose subject is the locale or a command-line flag starts a child with RbConfig.ruby and writes the child's environment into the program, in view — the locale lesson is the pattern.
Perl: core modules only, nothing newer than 5.34, which is /usr/bin/perl on a Mac. Neither CI runner has a perl that old (see the table below), so run a new Perl example under /usr/bin/perl before committing it. A Perl program that prints text sets binmode STDOUT, ':encoding(UTF-8)' in view, and says use feature 'unicode_strings' when a comparison depends on it — the case lesson shows why that matters.
Python: standard library, run as python3 -I.
Shell: bash, in a mktemp -d directory, printing each command before running it — the say helper in the one-liner lesson does that, so a verified block reads like a terminal. ruby on the script's PATH is the Ruby the .rb examples ran under.
Write non-ASCII characters as escapes that brace or name them: "\u{e9}" in Ruby, "\x{E9}" in Perl, "\N{LATIN SMALL LETTER E WITH ACUTE}" in Python. A source file then never depends on an editor, a terminal or a tool keeping an invisible character intact. Avoid the four-digit backslash-u form: at least one tool that writes files here decodes it on the way to disk.
Deterministic, on both machines. No timings — at most a threshold no machine could miss by, as the Perl and Python programs in the backtracking lesson use. No object ids, no hash iteration order in Perl, no RUBY_VERSION beyond the major release, no path that includes a temporary directory's name.
An exception is a result. Rescue it and print its class and message; the runner stops on an example that exits non-zero.
Two machines¶
CI's Show toolchain step prints what each runner has. Measured differences between the two go here, with the date — add a row when CI finds one:
| Ubuntu runner | macOS runner | |
|---|---|---|
| CPU | x86-64 | arm64 |
| Ruby | 4.0.6, from ruby/setup-ruby |
4.0.6, from ruby/setup-ruby |
perl |
5.38.2 | 5.44.0 |
python3 |
3.12.3 | 3.14.7 |
bash |
5.2.21 | 3.2.57 |
The runner versions are from the first CI run, on 2026-09-13, and no example has yet printed differently on the two. The keys were recorded on an x86-64 Mac with Ruby 4.0.0 (Homebrew), Perl 5.42.0 (Homebrew) and Python 3.14, and the Perl examples were also run under /usr/bin/perl 5.34.1.
Bridges¶
Every lesson ends with If you are coming from another language. Perl and Python get an output block when there is a program behind the comparison. Perl, Java, Rust, C and the encodings background get a link to the sibling library's page, which checks its own claims; do not repeat that page here.
Links¶
- Link a folder by naming its
README.md—[label](some_folder/README.md), never[label](some_folder/). - A link that leaves the library ends its label with
↗; an internal link never does.python3 tools/check_link_style.py --fixadds and removes them; CI runs it without--fix. - A sibling library's page is linked as
https://masiarek.github.io/<library>/<chapter>/<lesson>/index.html.
Nav order¶
A new lesson folder gets a row in NAV_ORDER in mkdocs_hooks.py. Its sidebar label is its README's # H1 with the backticks dropped; give it an entry in LABEL_OVERRIDES only when that H1 is too long for a sidebar. tools/check_nav_chain.py fails on a row naming a folder that does not exist, so commit the folder and its row together.