Also available in Polish: README
A week of several people on one A4 sheet: lessons, after-school activities and lunch breaks, written as plain text and drawn to scale. Edit and print a plan in the browser at bsulkowski.pl/weekly-plan; this repository holds the code that reads and draws it.
Each day of the sheet is split into one lane per person, and the time runs down the page at a fixed scale, so you can see at a glance who is where, who finishes when, and who has to be picked up. The plan is text rather than boxes to drag: lessons are listed in order and get their times from the school bells, so when the timetable changes in September you edit a few lines.
bells: 8:00-8:45, 8:55-9:40, 9:50-10:35, 10:45-11:30
[Ada]
mon: Math r.12, Eng r.4, -, PE
mon 16:30-18:00 Piano school
[Ada+Leo]
fri 16:00-17:30 Chess club
| Line | What it does |
|---|---|
[Ada] |
The lines below belong to Ada. [Ada+Leo] is shared by both, [everyone] by all. |
mon: Math r.12, -, PE |
Lessons in order of the bells. - is a free period. Two identical lessons in a row become one double lesson. |
wed 7-8 Maths club |
A lesson given by its number, or a range of numbers. |
mon 16:30-18:00 Piano |
An activity at a set time. Several days at once: tue,wed,thu 11:30-11:50 lunch. |
Robotics ? |
A question mark at the end draws the block with a dashed border. |
bells: 8:00-8:45, … |
Lesson times. Under a person's header they apply to that person only. |
colors: lunch=#d5e8d4 |
A colour for a person, or for every block whose label starts with that word. |
people: Ada, Leo, Mila |
The order of columns. |
days: mon tue wed thu fri |
Days on the sheet. A day with only [everyone] blocks gets a narrow column. |
hours: 7:00-20:00 |
The time axis. Without it, the range fits the plan. |
title: Autumn 2026 |
A label in the top-left corner. |
format: 1 |
The version of the plan format; a plan without it is format 1. |
Days and keywords can be written in English or in Polish (pn: …, dzwonki:), on both pages.
The first word of a label is the subject and the rest goes to a second line. Shorter blocks
are drawn over longer ones, which is how lunch sits inside an after-school club. Lines starting
with # are comments.
Full example plans are in examples/, each with its drawing.
One TypeScript module, src/weekly-plan.ts, with no dependencies and no
DOM access. It runs in the browser and in Node ≥ 22.12 (with --experimental-strip-types).
import { parsePlan, renderPlan, withFormatLine, describePlan } from 'weekly-plan';
const plan = parsePlan(text, 'en'); // people, days, blocks with times; plan.issues lists lines it could not read
const svg = renderPlan(plan, 'en'); // <svg viewBox="0 0 1123 794"> — A4 landscape; 'pl' for Polish day names
withFormatLine(text); // the text with `format: 1` added, for saving to a file
describePlan(plan); // what the plan means, one fact per line — used by the testsThe language argument only chooses the day names on the sheet and the wording of issues;
the syntax is the same in both.
Install from GitHub: npm install github:bsulkowski/weekly-plan, or with #<commit> at the end
to pin a version. The package ships the TypeScript source, so a bundler has to compile it
(Vite does; in an Astro or Vite SSR build, add weekly-plan to ssr.noExternal).
Plans are kept in people's own text files and browsers, not on a server, so they cannot be migrated. A new version must read every old plan the same way. The drawing (layout, fonts, default colours) may change freely; the meaning of a saved plan may not.
- New features come only as new syntax: a new keyword, or a shape of line that used to be
an error. Never as a new meaning of ordinary label text. Two such meanings exist and no more
will be added:
?at the end of a label, and a first word that matchescolors:. - A keyword is never a day abbreviation (
pn,sob,mon,sat…), and a new day alias is never an existing keyword: keywords are checked before days. - Settled rules: the comma separates lessons (a label has no comma);
-is a free period; identical lessons in a row merge;bells:under a person's header applies to that person only, wherever it stands;mon 10 Swimmingis lesson 10, not 10 o'clock;#starts a comment at the start of a line or after a space (#f8ceccandRoom#5stay); text is normalised to NFC. To change one of them, add an option beside it and keep the default.
FORMAT_VERSION(now1) is the version of the plan format, a whole number. Saving addsformat: 1after the leading comments. A plan with a higher number gets a warning but is otherwise read as before. The number grows only if an old plan could no longer be read the same way, and then older formats keep being read by their number.TOOL_VERSION(now1.0, withversioninpackage.json): a new option → 1.1, a fix in the drawing → 1.0.1, and 2.0 only together withformat: 2.
| Version | |
|---|---|
| 1.0 | October 2026. The first version. |
npm test # every plan in test/plans/ is read as recorded in its .expected.txt
npm run test:update # rewrite the .expected.txt files — only after an intended change of meaning
npm run examples # redraw examples/The reference plans are frozen: they are not edited when the examples change. A new feature of
the syntax gets new lines in edge-cases.txt or a new plan file. After test:update, every
changed line in the diff is somebody's saved plan read differently.
MIT — Bartosz Sułkowski.