Skip to main content

README.md

# swatch 🎨

[![Package Version](https://img.shields.io/hexpm/v/swatch)](https://hex.pm/packages/swatch)
[![Downloads](https://img.shields.io/hexpm/dt/swatch)](https://hex.pm/packages/swatch)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/swatch/)
[![Test](https://github.com/scott-ray-wilson/swatch/actions/workflows/test.yml/badge.svg)](https://github.com/scott-ray-wilson/swatch/actions/workflows/test.yml)
[![License](https://img.shields.io/hexpm/l/swatch)](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).