Skip to content

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:

  1. (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).
  2. Implicitly concatenated strings with a comment between the pieces were joined onto one line, past the width, with the comments bunched at its end.
  3. A def whose signature did not fit broke the return annotation's brackets (-> tuple[ … ]:) instead of the parameters, which is what Black does and what every signature in src/ 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 import line are unchanged. The $props examples lose their semicolons (const { data } = $props()): the guide's rule is "omit semicolons", its examples carry them. A project that wants them kept writes semicolons = "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 a d attribute 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: off covers 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.