# swatch 🎨
[](https://hex.pm/packages/swatch)
[](https://hex.pm/packages/swatch)
[](https://hexdocs.pm/swatch/)
[](https://github.com/scott-ray-wilson/swatch/actions/workflows/test.yml)
[](https://github.com/scott-ray-wilson/swatch/blob/main/LICENSE)
A CSS syntax highlighter for Gleam. Renders directly to HTML or
ANSI-colored terminal output, or hands back a classified token stream for
custom rendering.
## Install
```sh
gleam add swatch@1
```
## Quick start
```gleam
import gleam/io
import swatch
pub fn main() {
let source =
"@media (min-width: 600px) {
.btn {
--brand: #f00;
color: var(--brand);
padding: 8px 12px;
}
.btn:hover {
color: rgb(255 0 0 / 0.8) !important;
}
}"
// ANSI colors for the terminal
swatch.to_ansi(source) |> io.println
// HTML with `<span>` wrappers per token kind
let html = swatch.to_html(source)
io.println("<pre><code>" <> html <> "</code></pre>")
// Raw tokens for custom rendering or analysis
let _tokens = swatch.to_tokens(source)
}
```
Further documentation can be found at <https://hexdocs.pm/swatch>.
## HTML output
`swatch.to_html` wraps each token in a `<span class="hl-…">` describing its
kind. Whitespace passes through unwrapped. Content is HTML-escaped.
| Token | Class | Token | Class |
| ---------------- | -------------------- | ----------- | ---------------- |
| `Comment` | `hl-comment` | `String` | `hl-string` |
| `Selector` | `hl-selector` | `Number` | `hl-number` |
| `ClassSelector` | `hl-class` | `Unit` | `hl-unit` |
| `IdSelector` | `hl-id` | `HexColor` | `hl-hex` |
| `PseudoSelector` | `hl-pseudo` | `Function` | `hl-function` |
| `AttributeName` | `hl-attribute` | `Keyword` | `hl-keyword` |
| `AttributeValue` | `hl-attribute-value` | `Important` | `hl-important` |
| `AttributeFlag` | `hl-attribute-flag` | `Operator` | `hl-operator` |
| `AtRule` | `hl-at-rule` | `Punctuation` | `hl-punctuation` |
| `Property` | `hl-property` | `Other` | `hl-other` |
| `Variable` | `hl-variable` | | |
A starter stylesheet:
```css
pre code .hl-comment { color: #6a737d; font-style: italic }
pre code .hl-selector,
pre code .hl-at-rule,
pre code .hl-operator,
pre code .hl-important { color: #d73a49 }
pre code .hl-important { font-weight: bold }
pre code .hl-class,
pre code .hl-id,
pre code .hl-pseudo,
pre code .hl-attribute,
pre code .hl-attribute-flag,
pre code .hl-function { color: #6f42c1 }
pre code .hl-string,
pre code .hl-attribute-value { color: #032f62 }
pre code .hl-property,
pre code .hl-number,
pre code .hl-unit,
pre code .hl-hex { color: #005cc5 }
pre code .hl-variable { color: #e36209 }
pre code .hl-keyword { color: #22863a }
```
If you already have a token list, `swatch.tokens_to_html` renders it without re-tokenizing.
## ANSI output
`swatch.to_ansi` renders for the terminal using
[gleam_community_ansi](https://hex.pm/packages/gleam_community_ansi).
| Color | Tokens |
| ----------- | ------------------------------------------------------------------------------------- |
| yellow | selectors (element, class, id, pseudo, attribute, flag), keywords |
| cyan | properties, custom properties |
| green | strings, numbers, units, hex colors, unquoted attribute values |
| blue | function names |
| magenta | at-rules, operators |
| bold red | `!important` |
| italic gray | comments |
| reset | whitespace, punctuation, fallback |
Structural tokens use `ansi.reset` so an unclosed attribute from upstream text can't bleed into characters like `{` and `}`.
If you already have a token list, `swatch.tokens_to_ansi` renders it without re-tokenizing.
## Tokens
`swatch.to_tokens` returns a list of `swatch.Token`. Every variant carries a
single `String`, so concatenation
reproduces the input.
The full list: `Whitespace`, `Comment`, `Selector`, `ClassSelector`,
`IdSelector`, `PseudoSelector`, `AttributeName`, `AttributeValue`,
`AttributeFlag`, `AtRule`, `Property`, `Variable`, `String`, `Number`,
`Unit`, `HexColor`, `Function`, `Keyword`, `Important`, `Operator`,
`Punctuation`, `Other`.
Tokens are **round-trip safe**; concatenating each token's string payload
reproduces the original source byte-for-byte, including whitespace, comments,
escapes, and formatting.
Malformed input (unmatched brackets, invalid escapes, trailing identifiers
past the attribute-flag slot) surfaces as `Other` rather than being dropped,
so round-trip holds even on broken CSS.
## CSS coverage
- [CSS Syntax Level 3][syntax] — escapes, hex escapes with trailing
whitespace, `<urange>`, `<url-token>`, `<bad-string-token>` recovery,
CDO/CDC.
- [Selectors Level 4][sel4] — pseudo-elements (`::`), all attribute
matchers, case-sensitivity flags, namespace separator (`ns|attr`,
`*|attr`), and the `||` column combinator.
- [CSS Nesting][nest] — bounded lookahead promotes property-position runs
to selectors when they begin a nested rule.
- [Media Queries Level 4][mq4] range comparisons (`<`, `<=`, `>=`).
- [`@supports selector(…)`][cond4],
[`@container style(…)` / `scroll-state(…)`][cont3],
and [`@scope`][scope] — all switch their argument into the right context.
## Development
```sh
gleam build # Compile the project
gleam test # Run the tests
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full workflow.
[syntax]: https://www.w3.org/TR/css-syntax-3/
[sel4]: https://www.w3.org/TR/selectors-4/
[nest]: https://www.w3.org/TR/css-nesting-1/
[mq4]: https://www.w3.org/TR/mediaqueries-4/
[cond4]: https://www.w3.org/TR/css-conditional-4/
[cont3]: https://www.w3.org/TR/css-contain-3/
[scope]: https://www.w3.org/TR/css-cascade-6/
Inspired by [contour](https://hex.pm/packages/contour) and
[just](https://hex.pm/packages/just).