Adding a lesson¶
The conventions are few, and most exist to keep one promise: a page never claims something a program has not actually printed.
The shape of a lesson¶
NN_Chapter/
topic_name/
README.md the lesson
examples/
topic_name_py.py a program that demonstrates it
topic_name_py.out its recorded output — the answer key
topic_name_pl.pl the same question, another engine
z_topic_name.prog.abap ABAP: source only, no key (see below)
A folder's overview page is named exactly README.md: GitHub renders it in the folder's tree view, and MkDocs makes it that section's landing page. The descriptive title goes in the page's # H1, not in the filename.
Stems are unique across the whole repo, because a Markdown block names a bare stem with no path and no extension. The convention is a language suffix: doubled_word_py, doubled_word_pl, doubled_word_rs_sh.
The one rule¶
Mark where output belongs and let the tool fill it:
Then run python3 tools/run_examples.py. It runs the program, compares it with the recorded .out, and rewrites the block. Inside the markers is generated; outside is yours. <!-- source:stem --> pastes the program itself the same way — use it instead of hand-copying code onto a page, because a hand-pasted copy is one that can quietly stop matching the program CI runs.
CI runs --check, which writes nothing and fails if code, key and page have drifted apart.
The languages¶
| Extension | How it runs |
|---|---|
.py |
python3 -I, standard library only |
.pl |
perl 5.36+ |
.js |
node |
.rb |
ruby 3.2+ (macOS's /usr/bin/ruby is 2.6 — the runner looks for a newer one) |
.go |
go build, Go 1.25+, standard library only |
.java |
java <file>.java, Java 25+ |
.c |
cc -std=c17, for POSIX <regex.h> |
.sh |
bash — for grep, sed, cargo, and anything about an exit status |
.abap |
never run here. See below. |
Rust has no regular expressions in its standard library, so a Rust example is a question about a crate. The crates live in one Cargo workspace at rust-demo/ with a bin per lesson, and a .sh example runs it:
--locked is not decoration. These examples print error messages, and a silent minor bump that rewords one is exactly the drift this library exists to catch.
Keyword and topic tables¶
A page on the Keywords or Topics shelf gets its table from one program, tools/kwprobe/run.py, driven by a .sh example that holds the rows:
python3 ../../../tools/kwprobe/run.py <<'ROWS'
label :: pattern :: subject [:: replace=TMPL | split | node=FLAGS | skip=a,b]
ROWS
Eight engines answer every row — Python 3.12 (CI's), Perl, Node, Ruby, Java, Go, regex and fancy-regex — and the cell is what each matched, no, or - for refused. The driver's docstring has the grammar and the cell meanings. Two rules for rows: keep every subject ASCII in the file (\u{HEX} decodes the same way in every probe), and skip= an engine whose answer would depend on a runtime version that differs between your machine and CI — that is what keeps a key byte-identical on both. Then write the prose against the table, not before it.
The ABAP column¶
Nothing in this repository can run ABAP, and neither can a GitHub runner. So:
- An
.abapfile is source only. It has no.outkey,run_examples.pyskips it, and a page shows it with asource:block. - Never write an output block for an ABAP example, and never hand-type what you think it would print. If you have run it on a real system, record the transcript with a provenance line saying which system and release — the way the ABAP learning library ↗ does — and label it as such.
- What can be shown is the engine underneath. ABAP's
PCREaddition is the PCRE2 library, and PCRE2 is on any machine withpcre2grep. Those go on a page as a dated Real run fence naming the version and the machine, not as a generated block. - Name ABAP files the abapGit way —
z_name.prog.abap,zcl_name.clas.abap. abaplint types an object from that infix and analyses nothing without it, and its way of saying so is the words0 file(s) analyzed, which look exactly like success.
Writing the prose¶
- One idea per page. Two H1-sized ideas are two pages.
- Lead with the shortest true statement — a
**One line:**summary the reader can carry away — then earn it. **Level:**tags the depth:101,201,301orreference, then·and the audience.- Say which trap you are describing. The valuable half of most lessons is the mistake, not the mechanism.
- Say which engine and which version. "Regular expressions support lookbehind" is not a true sentence.
(?<=a{1,3})bcompiles in Perl, Java and JavaScript, and is an error in Python and Ruby — that is a true sentence. - Put every engine's answer next to the others. This library exists for the comparison; a page about one language belongs in that language's own library.
- Don't hard-wrap paragraphs. One paragraph, one line.
Linking¶
- Link a folder by naming its README:
[label](some_folder/README.md), never[label](some_folder/). The bare form works on GitHub and on the built site but not in a plain Markdown viewer, and MkDocs ships it unrewritten so it 404s. - Every external link ends its label with
↗and no internal link does, so a reader can tell without hovering which links leave the library.tools/check_link_style.py --fixmaintains this; CI checks it.
Reading order in the sidebar¶
Set it in NAV_ORDER in mkdocs_hooks.py ↗ — never by renaming files. A filename is a permanent URL; inserting one lesson would otherwise move every page after it. Numeric prefixes on chapter folders are fine (the hook strips them from the label).
Before you commit¶
That runs every gate CI runs, against the tree your next commit would make: the examples, the link style, the nav chain, and mkdocs build --strict.