How to implement a style guide¶
STYLEGUIDE.md is this repository's own style
guide: a page of rules for Python, TypeScript, JavaScript, Svelte, HTML,
CSS and SVG. This page turns it into a rainbow-fmt configuration,
rule by rule, and then checks the result against the repository's
sources. The outcome is rainbow.toml next to this page,
and a list of what the guide asks for that a formatter cannot (or
rainbow-fmt cannot yet) do.
Sorting the rules¶
Not every sentence in a style guide is a formatting rule. The first pass over the guide sorts each rule into one of four bins:
flowchart TD
rule["a rule in the guide"] --> q1{"Is it about layout<br/>(whitespace, line breaks, brackets)?"}
q1 -->|no| lint["linter / review<br/>(names, length, docstrings)"]
q1 -->|yes| q2{"Does an option<br/>cover it?"}
q2 -->|yes| cfg["rainbow.toml"]
q2 -->|no| q3{"Is it 'leave this<br/>as written'?"}
q3 -->|yes| directive["default behaviour<br/>or a rainbow: directive"]
q3 -->|no| gap["follow-up: an option<br/>or rule to write"]
Rules that are not about layout (naming, function and file length, docstring contents, "use the relevant MCP servers") are for a linter or a reviewer; rainbow-fmt never renames anything or moves code between files. Of the layout rules, most map to an option; a few are "keep what was written", which rainbow-fmt does by default for everything it has no rule for; and a few are gaps.
The mapping¶
General formatting¶
| The guide says | Configuration | Notes |
|---|---|---|
| 4 spaces for indentation, no tabs | core.indent_style = "space", core.indent_size = 4 |
the defaults |
| Lines up to 79, preferably at most 100; HTML may be longer | core.max_width = 99 and an [[override]] with 120 for *.html and *.svelte |
99 is what ruff enforces in this repository; the override shows how one width per file kind works |
| Blank lines separate thoughts inside functions | language.python.statement_blank_lines = "separate", core.max_blank_lines = 1 |
a blank line before and after a for/while loop, after an if statement, and before an if that follows another if or a loop, at the top level of a function; blank lines the author wrote are kept (at most one in a row). The reference is parse_directive and _region in src/rainbow_fmt/core/directives.py |
| Imports at the top, grouped | — | not a formatting concern: rainbow-fmt never reorders statements (ruff's isort rules do this) |
Quotes¶
| The guide says | Configuration | Notes |
|---|---|---|
| Prefer single quotes unless the string contains one | gap | no pack normalizes quotes yet (a follow-up in TODO.md for Python and JavaScript); a project adopting the guide would keep writing single quotes by hand |
| Don't change existing quotes | default | every pack prints strings as written |
Data structures¶
| The guide says | Configuration | Notes |
|---|---|---|
| A list of values one per line, with a trailing comma; short lists on one line | shared.trailing_comma = "multiline", language.python.bracket_wrap = "magic_trailing_comma" |
a list that fits stays on one line; one written broken (with its trailing comma) stays broken, one per line; a list that does not fit is broken with a trailing comma |
A list of dicts as [{ … }, { … }] |
language.python.bracket_hug = true (and language.javascript.bracket_hug) |
a list whose items are all non-empty dicts hugs them when it does not fit on one line; lists of lists and mixed lists break one item per line as before |
| A dict of lists, a dict of dicts, as shown | default | the examples are exactly what the pack produces |
TypeScript, JavaScript and Svelte¶
| The guide says | Configuration | Notes |
|---|---|---|
| Omit semicolons, except where needed | language.javascript.semicolons = "as_needed" (and the same under typescript) |
the default; a ; is kept only where the next line would otherwise join the statement |
| Don't add semicolons to code that omits them | same option | |
Semicolons for return statements that are not the last statement in a block |
language.javascript.return_semicolons = "unless_last" |
if (v === value) return v; followed by more statements gets one, the final return value does not; the same under [language.typescript] |
import { A, B } from '…', multi-line when too long |
language.javascript.bracket_spacing = true |
the default; the list breaks one name per line when it does not fit |
const { data } = $props(), multi-line when several |
default | an object pattern that fits stays on one line; a long one breaks one per line |
| Keep repeated patterns similar even past the line length | // rainbow: off … // rainbow: on around the block, or <!-- rainbow: off --> in markup |
a formatter cannot tell "repetition" from "three unrelated calls"; the directive keeps a block as written |
Prefer condensed markup (an <input> with its attributes on one line) |
language.html.bracket_same_line is not it; the [[override]] width of 120 |
a tag breaks one attribute per line only when it does not fit the width; with 120 columns the guide's examples fit |
No closing solidus on void elements (<br>, not <br/>) |
language.html.void_elements = "no_slash" (the default for HTML and Svelte; spelled out) |
<br/>, <br />, <input … /> become <br>, <input …>; only the void elements, where the two spellings parse the same |
SVG¶
| The guide says | Configuration | Notes |
|---|---|---|
Keep inline SVG condensed; <path>s on one line |
default (whitespace_sensitivity = "css") |
svg and path are inline elements: they are filled to the width, and a </path><path written without whitespace between stays joined. Each d="…" is as written, but a tag whose attributes do not fit the width breaks them one per line: a <path> with a 300-column d is kept condensed only by <!-- rainbow: off -->. |
| Unless the SVG is already manually formatted | <!-- rainbow: off --> before it |
or <!-- prettier-ignore -->, which rainbow-fmt honours in HTML and Svelte |
Python¶
| The guide says | Configuration | Notes |
|---|---|---|
| PEP 8 | the [language.python] block |
two blank lines around top-level definitions, one inside classes, two spaces before an inline comment, breaks before binary operators |
Docstrings as shown: the closing """ on its own line, lines after the summary aligned under its first letter |
language.python.docstrings = "aligned" |
the summary stays on the opening line, continuation lines keep their relative indentation three columns in from the quotes, the closing quotes get a line of their own; applies to module, class and function docstrings |
Checking it against the repository¶
The same configuration ships as the preset rainbow:styleguide
(preset = "rainbow:styleguide"; the HTML/Svelte width override becomes
[language.html] max_width = 120, since a preset holds no [[override]]).
The configuration in rainbow.toml is what this
repository already uses in effect ([tool.rainbow] in
pyproject.toml sets max_width = 99; everything else is a default),
so the check is:
$ rainbow-fmt check src tests
would reformat tests\fixtures\css\01_rule\input.css
… (180 fixture inputs, which are deliberately unformatted)
The options this page added (statement_blank_lines, docstrings,
bracket_hug, return_semicolons, void_elements) were written against
the guide's examples; src/rainbow_fmt/core/directives.py, with the blank
lines its author placed by hand, is stable under
statement_blank_lines = "separate": the option adds exactly those and
no others in parse_directive and _region.
Before this page was written, the same command also listed six source files and three test files, all written by people and already formatted by ruff. Reading the diffs found three bugs in the Python pack rather than disagreements with the guide, which is the point of running a formatter over code its authors are happy with:
(path,) = case.path.glob("input.*")was exploded over three lines: the comma of a one-element tuple pattern was taken for a magic trailing comma (a one-tuple value was already handled).- Implicitly concatenated strings with a comment between the pieces were joined onto one line, past the width, with the comments bunched at its end.
- A
defwhose signature did not fit broke the return annotation's brackets (-> tuple[…]:) instead of the parameters, which is what Black does and what every signature insrc/looks like.
All three are fixed (fixtures 30 and 31 of the Python pack), and the command now reports only the fixtures.
The guide's own examples, run through the same configuration (the markup at the override's width of 120), come out as the guide shows them with four exceptions, each of which says something about the guide or about rainbow-fmt:
- The data-structure examples and the
importline are unchanged. The$propsexamples lose their semicolons (const { data } = $props()): the guide's rule is "omit semicolons", its examples carry them. A project that wants them kept writessemicolons = "preserve". - In the
<form>example the two<input>lines (115 and 118 columns) stay as written at width 120, which is the condensed form the guide prefers; the<button>, whose content fits, is joined onto one line (<button …> Opprett </button>). The guide does not say which it wants there. - The SVG example's
<path>tags carry adattribute of 300 columns, so each tag is broken with the attribute on a line of its own; the guide's condensed form (<path d="…"></path><path d="…"></path>on one line) needs<!-- rainbow: off -->, or<!-- prettier-ignore -->, in front of the<svg>— which the guide allows for manually formatted SVG. - Quote preference is the one gap left: no pack normalizes quotes yet.
One conflict was outside rainbow-fmt's control: ruff's formatter joins
"""Text.
""" back into """Text.""", so a project that wants the aligned
docstring style cannot run both. This repository therefore dropped
ruff format --check from CI in favour of rainbow-fmt check . (ruff's
linter stays) and formats itself with the rainbow:styleguide preset
([tool.rainbow] in pyproject.toml, with [files] exclude keeping the
test fixtures as they are).
What to take from this¶
- Most of a style guide is either the default or one option; write the configuration with every value spelled out, so the guide can be read from it.
- Rules about keeping something are free: as-written is the default for
anything without a rule, and
rainbow: offcovers the rest. - A formatter run over code that its authors already like is a test of the formatter. Expect to find bugs, and fix them before adjusting the guide.