torb docs site
torb docs site writes the website of TorbScript from docs/ as static files - three levels from the first lesson to the reference and a project area apart from them, in every language of translations/, with a search per language.
The website at torb.dev is one more output of the documentation tree, beside the skill and the bundle: torb docs site
reads the pages torb docs check reads and writes plain files that any static host can serve. No page is written by
hand in HTML, and no JavaScript is needed to read one. It leads a reader in at one of three levels - Start, Guide,
Reference - keeps the pages about the project itself apart from them, and is written in English and in every language below docs/translations/.
Synopsis#
torb docs site <root> --output <dir> [--version <v>] [--brand <dir>] [--playground <dir>] [--benchmarks <file>]
[--check]
--output <dir> Where the site goes
--version <v> The version the documentation is of, and its directory below docs/ (default: latest)
--brand <dir> Where the fonts, the icons and the logo are (default: <root>/../brand)
--playground <dir> The playground playground/build.sh built (default: <root>/../build/playground)
--benchmarks <file> The report the benchmarks page shows (default: <root>/../build/benchmarks.json, where it is)
--check Build the site and check its links, and write nothingWhat it does#
What it writes#
| Path | What is in it | From |
|---|---|---|
index.html | The front page: the line, the install commands, the doors, a few examples | site/home.md |
install.html, benchmarks.html, imprint.html, privacy.html, play.html | A plain page of the site, each only where its source exists | site/install.md, site/benchmarks.md, site/imprint.md, site/privacy.md, site/play.md |
docs/<version>/... | Every page and design document, as .html at the path of its .md | every other .md file |
docs/<version>/search-index.json | The search of this version | every page |
<language>/... | All of the above but the assets, in another language: de/index.html, de/docs/<version>/... | translations/<language>/ |
404.html | The page for a path that is not there | |
assets/site.css, assets/site.js | The one stylesheet and the one script | the compiler |
assets/registry.css, assets/registry.js, assets/registry-search.js | The rules and the scripts only the registry's pages load: the package page, the chips, the table of versions, the theme toggle and the copy button, and the search page's script | the compiler (site-registry.trb) |
assets/fonts/ | Chivo and Geist Mono as .woff2, with their licence | brand/fonts/ |
assets/playground.js, playground-worker.js, playground-editor.js, playground-editor.css, playground-codicon.ttf, playground.css, torb.js, torb.wasm, torb.wasm.gz, examples/ | The playground, only where it was built; examples/ only where it has a gallery | --playground |
favicon.svg, favicon.ico, apple-touch-icon.png, mask-icon.svg, icons/, manifest.webmanifest, social-card.png | The icons, the web manifest with a maskable icon, and the preview card | brand/ |
Every link between two pages is relative, so the output can be served from any directory. The page for a missing path links from the root of the host, because the server answers it at every depth.
Three levels#
Every page belongs to one level, by the folder it is in:
| Level | For | Folder |
|---|---|---|
| Start | somebody who has never programmed | start/ |
| Guide | somebody who programs already | guide/ |
| Reference | the full depth: the language, the standard library, the tools, the reasons | everything else |
| Project | somebody working on TorbScript rather than with it | project/, design/, internals/, contributing/, site/index.md, and the documents at the root whose names are in capitals |
The header has the doors of a reader - on a phone, in a row below it - and marks the one the page is behind:
| Door | Leads to | Marked on |
|---|---|---|
| Learn | guide/index.md | a page of Start or of the Guide |
| Reference | index.md | a page of the Reference outside the standard library |
| Std | standard-library/index.md | a page of the standard library |
| Cheat sheet | language/syntax/cheat-sheet.md | that page |
| Playground | play.html, where the playground was built |
The Project has no door. Its overview, project/index.md, is linked from the footer of every page; its pages have a
sidebar of their own and are not in the Reference's, the root index leaves them out, and the search finds them only
from a page of the Project (data-scope="project" on its body), so a reader's results are the language and its tools.
The level shows on the page: a label above the title, in the colour of its level. A page's sidebar lists only its own level, and its previous and next pages stay inside it; the last page of Start leads on to the Guide and the last page of the Guide to the Reference, each link saying which step it is. The writing rules that go with the levels are in Three levels, plain words.
A lesson of Start (kind: lesson) has no sidebar. Above its title stand its number and the number of lessons, a
line that fills as the course goes on, a menu of every lesson and the arrows to its neighbours; its body is cut into
rows at its ## headings, each row's prose on the left and its code on the right, so the editor is beside the text it
belongs to. What is folded - a hint, a solution - stands below the prose, and on a phone below the code. A large link
to the next lesson ends it. The list of lessons, start/index.md, shows its introduction and every lesson with its
number and summary, in place of the list docs index writes into it. Lessons are not carried by the skill or the
bundle, which state the rules the lessons teach in small steps.
A page#
A page of the Guide or the Reference has the navigation of its level beside it - the index tree, ordered by order
and then by title, as torb docs index orders them - with its own section open. Above the
title stands its level, and for a page of the Reference the way from the overview to its section; below the title the
summary, the body, a link that edits the page on git.torb.dev, and the previous and next pages; beside it on a wide
screen, its headings of level 2 and 3.
- A heading has the anchor
torb docs checkresolves links against: the slug of its text, or the{#anchor}it ends with. A link between pages becomes a link to the.htmlfile the page became, with its anchor; a link out ofdocs/-../CONCEPT.md, a source file - goes to the file on git.torb.dev. - A
trbblock is coloured when the site is built, by the compiler's lexer and the resolver behindtorb highlight, in the syntax colours of BRAND.md section 9. A name that can be changed is underlined. The marker of a block (run,check,fragment) is for the tools and is not shown; atrb errorblock carries the label that it does not compile. - A quote that starts with
**Planned.**or**Draft.**- the banner a page that is notstableopens with - is a callout in the colours of information or warning. - The written page of a package of
std/links to its generated reference belowdocs/<version>/reference/, whichtorb doc stdwrites; the link comes from the package name the page is titled with.
Runnable blocks and exercises#
A trb run block can be edited and run in the page, and a trb exercise block is a small task with an expected
output. Both stay ordinary Markdown that torb docs check verifies; the site turns them into mounts that the
playground (docs/design/RELEASE.md, "The playground") enhances, and without the playground they are highlighted code
with a copy button.
In Markdown. An exercise is its starting code, then a folded hint, then a folded solution. The solution is the first
trb run block after the exercise and before the next one, and the lines its // prints comments name are the
exercise's expected output, so the output a reader is asked for is the output docs check saw the solution print.
```trb exercise
const name = "World"
print "Hello, {name}!"
```
<details>
<summary>Hint</summary>
The text between the quotes is what the name holds.
</details>
<details>
<summary>Solution</summary>
```trb run
const name = "Ada"
print "Hello, {name}!"
// prints Hello, Ada!
```
</details>| Fence | What docs check asks of it |
|---|---|
trb run | Type checks, is built natively and run, and prints what its // prints comments say |
trb exercise | Parses, is in the canon and type checks: a whole program that does not do the task yet. Its output is not compared |
trb exercise incomplete | Lexes: starting code with a gap on purpose, which the reader fills in |
A trb exercise block without a trb run block after it, before the next exercise, is a problem. The blank lines
around the <details> and <summary> lines are what keep the Markdown between them Markdown.
In HTML. Every trb run block but a solution, and every trb exercise block, is written as a mount. The attribute
names are stable; the playground reads nothing else:
<div class="playground" data-playground="run" data-file="first-program.trb" data-source="...">
<div class="code-block"><pre class="code language-trb"><code>...highlighted...</code></pre></div>
<p class="playground-local">...torb run first-program.trb...</p>
</div>
<div class="playground playground-exercise" data-playground="exercise" data-file="first-program.trb"
data-source="..." data-expected-output="Hello, Ada!" data-solution="...">
...the same fallback, and the expected output as text...
</div>| Attribute | What it holds |
|---|---|
data-playground | run or exercise |
data-source | The code of the block, exactly as written in the Markdown, HTML-escaped |
data-file | The name a reader saves it as to run it locally: the page's file name with .trb |
data-expected-output | Exercise only: the expected standard output, its lines joined by a line feed, no final one |
data-solution | Exercise only: the code of the solution, for a button that shows it |
A solution is written as a plain code block inside its <details>, not as a mount. The line "run it locally" stands
only under the exercises of Start; everywhere else a mount looks like any other code block until the playground takes
it.
Loading. No page names playground.js statically, so the link check does not require it. When a page has a mount,
assets/site.js inserts <script src="<root>assets/playground.js?v=<hash>"> - the hash of the playground's files,
which every page carries as data-playground-version when the site was written with --playground - and when that
loads and defines window.TorbPlayground, it calls, for every mount in document order:
TorbPlayground.mount(element, {
source, // data-source
file, // data-file
expectedOutput, // data-expected-output, or undefined
solution, // data-solution, or undefined
labels, // { run, stop, reset, solution, output, expected, solved, unsolved, running, loading, stopped,
// exitCode, tooMuchOutput, crashed, unsupported, examples, replace, format, source }: the
// words playground.*, in the page's language
onResult // function ({ output, exitCode, passed }) - passed: the output equals expectedOutput
})From then on mount owns the element's children, and keeps a playground-local line below what it builds. It reads
the highlighted <pre> first: its code stays what the reader sees until the editor replaces it, with its colours and in
its type, size and spacing - the panels of the front page set larger code than the documentation - so the editor takes
its place without moving anything. playground.js does not mount anything by itself, and whatever it loads besides -
the toolchain as WebAssembly, the editor with its stylesheet and its font, the examples - it loads relative to its own
URL and with its own ?v=, which hashes the gallery's files as well. When the script is missing or fails, nothing
happens and the fallback stays. A page site/play.md is written as play.html, and the header links it as Playground
where it exists; the site's stylesheet gives that page the width of the screen, up to 1840 px, where the other plain
pages keep the measure of their text.
The benchmarks page#
site/benchmarks.md is written as benchmarks.html, and the footer links it as Benchmarks where it exists. Its
numbers are not in the Markdown: benchmarks/game.sh measures six programs of the Computer Language Benchmarks Game in
TorbScript, C, Python and JavaScript and writes a JSON report, the forge publishes the newest one after every nightly
(benchmarks.json of its release benchmarks), and the site's build fetches it into build/benchmarks.json
(tools/fetch-benchmarks.sh). The page holds placeholders, each an empty div on a line of its own, which the report
fills:
| Placeholder | What it becomes |
|---|---|
<div data-benchmark="summary"></div> | Two numbers, each the geometric mean over the programs: TorbScript's binary against C, the VM against Python |
<div data-benchmark="n-body"></div> | The chart of one program: horizontal bars of the time relative to C on the same input, TorbScript's binary in carmine, as inline SVG with a text for every bar |
<div data-benchmark="table"></div> | Every time - the median with the fastest and the slowest run - and every peak of memory |
<div data-benchmark="machine"></div> | The machine, the tools, the commit and the date |
Without a report the summary says that nothing is measured yet and the other placeholders are left out, so a site
built without the file - the gate's --check - is still whole; a file that cannot be read as a report is said on
standard error and treated the same. A report that is not the forge's ("origin": "local") is shown with a warning
that it was measured elsewhere. The words are the interface's (benchmarks.* of site-strings.trb), so a translation
of the page gets its numbers in its own language, with its own decimal separator.
The front page#
site/home.md below the hero, cut at its ## headings. The hero is fixed: the mark, the line, the page's
summary in plain words, the install commands and the one red button. A section whose body is one list, every item a
link and one sentence, is the row of doors, each labelled with the level its link leads to; every other section is a
panel, its prose beside its code. The page stays short: the hero and the doors fill about one screen, and at most three
panels follow, each a plain title, two sentences and a few lines of code.
Languages#
English is the source, in docs/. A translation mirrors the path of its original below
docs/translations/<language>/ - translations/de/start/lists.md is the German start/lists.md - and
translations/<language>/strings.json holds the words of the interface in that language: the navigation, the buttons,
the notices, the search and the labels of the playground, as a flat JSON object whose keys are the ones of
englishStrings in site-strings.trb. A key it leaves out is said in English. A language exists when its
strings.json does.
- Paths. English is at the root of the site; every other language has the same paths below its code:
/de/,/de/docs/0.2.0/start/lists.html. The assets are shared. - A page without a translation is written anyway, in English inside the other language's interface, with a notice
and a link to the English page. It names the English page as its canonical one and asks search engines not to index
it; the generated reference of
stdstays English. - Other versions. Every page names each language it really exists in with
hreflang, English as the default. - The switcher in the header is a menu of links to the same page in every language, each in its own name ("English", "Deutsch"), which works without a script. Picking one is remembered.
- The browser's language. A reader who arrives at an English page from elsewhere, has picked no language yet, and whose browser prefers an available language over English is sent to the same page in that language. Nobody is sent from another language, or when coming from a page of the site.
- An outdated translation. A translation says which version of its original it was made from,
translates:and the twelve hexadecimal digits of the original's SHA-256 thatdocs checknames. When the original changes, the site shows the translation with a notice that it may be outdated, anddocs checklists it on a line of its own that starts withoutdated:- a note, never a failure, so an English change does not wait for every language. - Checks. A translation is checked like any page: its front matter, its headings, its links, which are the links
of its original and resolve from the folder of the original, and its
trbblocks. Its sections are not held to the English names a kind requires. It is not part of the index, the skill or the bundle. - The search has one index per language, at
<language>/docs/<version>/search-index.json.
A separate repository for the website was considered and not taken: the generator, the pages and the versioned reference change together, and one commit keeps them in step. It stays an option if the site ever needs a life of its own.
Where the patterns come from#
The three levels and their pages follow what works on the sites of other languages, kept small:
- A door per way in on the front page: the Vue docs' "pick your learning path", react.dev's Learn and Reference.
- One idea per lesson, the editor beside the text, "N of M" with arrows and a menu: the Go Tour, the Gleam tour, the Svelte and Vue tutorials.
- An exercise with a folded hint and a folded solution, checked by its output: react.dev's challenges, Rust by Example's activities, Exercism's hints; the solution a playground can show is Svelte's "solve" and Vue's "show me".
- A recap at the end, and plain titles with a term linked where it is first shown: react.dev, the Rust Book.
- A label on code that does not compile: the Rust Book's marks.
- Languages as path prefixes, a switcher that keeps the page, English with a notice where no translation exists, and one table of words per language: Astro's Starlight.
- Not taken: forks per language on subdomains, accounts, unlock trees and quizzes, toggles that double every page, and several learning products side by side.
The search#
search-index.json holds the title, the summary, the keywords and the headings of every page, and a term index of
their bodies: every word of the prose, the lists and the tables, sorted, with the pages it occurs on, the page that uses
it most first. The script fetches it the first time somebody searches (/, or Ctrl+K) and ranks a match in a title
first, then a keyword, a heading, the summary, and the bodies. There is no search service.
Versions#
--version 0.2.0 writes the documentation below docs/0.2.0/. The version switcher of the navigation reads
docs/versions.json at the root of the site when one is there, and shows only the version of the page when none is:
{"latest": "0.2.0", "versions": ["nightly", "0.2.0", "0.1.0"]}Choosing another version opens the same page in it, or its overview when the page does not exist there.
The stable address#
A link to docs/0.2.0/guide/tour.html names one release. docs/latest/ is the address that outlives it: the server
makes it an alias of the version it serves (the image of tools/deploy/Dockerfile.site links docs/latest and
<language>/docs/latest to the version's directory, and its server answers /docs/ with /docs/latest/). The site
writes nothing there itself, but it names it:
- The canonical address of every page of the documentation, and the addresses of its translations, are below
docs/latest/, so a search engine keeps one address per page across releases. - The pages at the root of the site (the front page, the install page, the playground, the missing page) link the
documentation below
docs/latest/. - The search reads the index of the directory the page is in and links its results there, so a reader on
docs/latest/stays on it. A page at the root readsdocs/latest/search-index.json.
The check of the links reads a link into docs/latest/ as one into the version written.
The look#
One stylesheet: the tokens of brand/tokens.css, which the compiler carries and a test holds equal to the file, and
the rules written for the site, with a light and a dark theme that follow the system and a toggle that overrides it.
The fonts are served by the site itself, never by a font service. Every script only adds to a page that is complete
without it: the copy buttons, the theme toggle, the search and the version switcher.
What a page asks of a reader's browser: no cookie, and no request to another host - the fonts, the icons, the
search index and versions.json all come from the site. Three things can be stored on the device, each only in
localStorage, each only after the reader does something, and each read and written inside a try so that a browser
that refuses storage shows the same pages: the theme a reader picks with the toggle (key torb-theme), the language
picked with the switcher or with the link of a notice (key torb-language), and which exercises of Start were solved
in the playground (key torb-progress), so the lists of lessons can mark them. Opening a lesson stores nothing, and
none of it leaves the device. The logo of the front page
assembles when the reader arrives from elsewhere and not again on the way back from another page of the site, which
the page it came from says, so nothing is stored for it.
The registry's shell#
The registry at its own host wears the same brand and loads the same assets/, so torb docs site also writes the
files only the registry's pages need, assets/registry.css, assets/registry.js and assets/registry-search.js, and
site-registry.trb has the shell around them: registryPageHtml(title, body, searchAction, description, scripts) is
the whole page - the head, the header of the site with the label packages in Geist Mono after the lockup, the body,
the footer. searchAction is None where a page has no search page to send to and otherwise the path of its
<form method="get" role="search">, so the field works without a script; scripts names more files of /assets/ the
page loads after registry.js. A registry page carries text strangers wrote, and the registry host allows scripts of
its own host only, so the shell has no inline script, no style= and no handler: the theme toggle and the copy button
sit in registry.js and stay hidden without it, and the stylesheet part names every colour by token role, which a test
holds. The same part has the rules of the forms the account pages have: a labelled field (.field, .field-label,
.text-input), a row of fields and their button (.form-row), the list of identities, passkeys and recovery codes
(.record-list) and the table of tokens inside .table-wrap; the primary button is the ink one
(button button-primary), never the red one. registry-search.js belongs to the search page alone: it asks
/api/1/search of the same origin and renders every field as text, never as markup. The registry's writers of pages
(tools/registry/src/packages.trb, account.trb and searchpage.trb,
the registry) use the shell.
The check#
Every href and src of the written pages that is relative has to lead to a file of the site, or the command fails
and names it - except a link into docs/<version>/reference/, which torb doc writes. --check builds the site,
checks it and writes nothing.
Examples#
The site of the version being released, then the reference of the standard library into it:
$ torb docs site docs --output build/site --version 0.2.0
311 pages of 0.2.0 in 2 languages: wrote 644 files to build/site
$ torb doc std --output build/site/docs/0.2.0/reference
41 packages, 133 modules: wrote 180 files to build/site/docs/0.2.0/referenceThe same build as a check:
$ torb docs site docs --check
311 pages of latest in 2 languages, 644 files, no broken linkRelated#
- torb doc - the generated reference of a package, which the site links to and which shares its look.
- The docs commands -
docs check,index,skillandbundle, the gates of the same tree. - The website - the pages the site writes at its root.
- Learn to program - the course of Start.
- Three levels, plain words - how a page of each level is written.