# `Ascii.Parts`
[🔗](https://github.com/houllette/ascii_art/blob/main/lib/ascii/parts.ex#L1)

The parts of a piece, coloured one by one: a file tree's folders, files and
branches, a boot log's `OK` and `WARN`, a chart's bars and axes.

A piece in one ink that knows its parts lists them in its meta
(`Ascii.Meta`, `parts`) and labels each frame's cells with them (the
optional `c:Ascii.Piece.parts/2`). Give colours for some of them with
the `:parts` option, and those cells are drawn in them:

    Ascii.new("file-tree", parts: %{"folder" => "#58a6ff", "file.md" => "#d29922"})
    Ascii.new("boot-log", parts: %{"status.ok" => "#3fb950", "status.warn" => "#d29922"})
    Ascii.new("boot-log", parts: true)

`true` takes the colours the piece suggests for its parts, for a dark
page, or with `paper: true` for a light one (`suggested/2`). A part it
suggests none for takes its parent's (a form's `frame.input` the
`frame`'s), or the ink if it has no parent with one. To change a few of
them, merge yours over the meta's:

    suggested = Ascii.Parts.suggested(Ascii.meta!("file-tree"))
    Ascii.new("file-tree", parts: Map.merge(suggested, %{"folder" => "#ffffff"}))

A colour can be a CSS variable, so parts follow a page's theme, light or
dark: `%{"status.ok" => "var(--color-success, #3fb950)"}` (see
`Ascii.Ink`).

## Names

A part's name is lowercase words joined by dots, the most general first,
as in tree-sitter's highlight names: `status`, `status.ok`, `file.ts`. A
colour for a name colours every part under it unless that part has its
own: `file` colours every file, `file.ts` only TypeScript files. A name
given `nil` takes the base ink, so `%{"file" => "#c9d1d9", "file.md" => nil}`
leaves Markdown files out.

Each piece's docs list its parts. The first word of every name you give
must be one of them; the rest can be anything (a file's extension, a
flight's remark), and simply never matches if the piece never uses it.

## Over the ink

Parts are laid over the `:ink`: cells in a part with a colour take it, all
others the ink (one colour, or a gradient), or the renderer's own ink if
there is none. In a frame, that last is a `nil` entry in the palette (see
`Ascii.Frame`). The picture itself is never changed: parts are read
off it after the piece has drawn it.

## Labelling a frame

For a piece of your own, `parts/2` returns each row of the frame as runs
of `{name, text}`, `name` being `nil` for cells in no part. The runs' text
must make up the row exactly. The helpers here build runs from a line.

# `colors`

```elixir
@type colors() :: %{optional(name()) =&gt; String.t() | nil}
```

Colours for parts by name, `nil` for the base ink, as `cast/2` makes them.

# `name`

```elixir
@type name() :: String.t()
```

A part's name: lowercase words joined by dots, as `status.ok`.

# `run`

```elixir
@type run() :: {name() | nil, String.t()}
```

Some of a row's text, and the part it is in (or `nil`).

# `t`

```elixir
@type t() :: [[run()]]
```

A frame's rows, each as runs of text that make up the row.

# `captures`

```elixir
@spec captures(line :: String.t(), regex :: Regex.t(), names :: [name() | nil]) ::
  [run()] | nil
```

A line labelled by a regex that matches all of it: each capture is in the
part named at its place in `names` (`nil` for none), and text no capture
holds in no part. Captures that matched nothing are left out. `nil` if the
regex does not match.

    iex> Ascii.Parts.captures("  [ OK ] done", ~r/^ *(\[ OK \]) (.*)$/, ["status.ok", "message"])
    [{nil, "  "}, {"status.ok", "[ OK ]"}, {nil, " "}, {"message", "done"}]

# `cast`

```elixir
@spec cast(parts :: term(), meta :: Ascii.Meta.t(), opts :: [{:paper, boolean()}]) ::
  {:ok, colors()} | Ascii.Options.error()
```

Colours for a piece's parts, from outside: a map or keyword list of names
(atoms or strings) to colours (`#rrggbb` or a CSS variable, as for `:ink`,
or `nil`), `true` for the colours the piece suggests (`suggested/2`), or
`nil` for none. With `paper: true`, the suggestions are those for a light
page. Never creates an atom.

# `cells`

```elixir
@spec cells(line :: String.t(), fun :: (char(), non_neg_integer() -&gt; name() | nil)) ::
  [run()]
```

A line labelled a cell at a time, by a function of each cell's code point
(an integer, as `?█`) and its column from 0: for pieces whose parts go by
where a cell is as well as what it holds. Neighbours in one part are one
run.

    iex> Ascii.Parts.cells("▁▁▁▁", fn ?▁, column -> if column < 2, do: "low", else: "high" end)
    [{"low", "▁▁"}, {"high", "▁▁"}]

# `chars`

```elixir
@spec chars(line :: String.t(), fun :: (String.t() -&gt; name() | nil)) :: [run()]
```

A line labelled a character at a time, by a function of each character
returning its part's name (or `nil`). Neighbours in one part are one run.

    iex> Ascii.Parts.chars("██░░", &if(&1 == "█", do: "done", else: "left"))
    [{"done", "██"}, {"left", "░░"}]

# `first`

```elixir
@spec first(line :: String.t(), patterns :: [{Regex.t(), [name() | nil]}]) :: [run()]
```

A line labelled by the first of `patterns`, each `{regex, names}` as for
`captures/3`, that matches all of it; in no part if none does.

    iex> Ascii.Parts.first("[WARN] low", [{~r/^(\[OK\]) (.*)$/, ["ok", "text"]}, {~r/^(\[WARN\]) (.*)$/, ["warn", "text"]}])
    [{"warn", "[WARN]"}, {nil, " "}, {"text", "low"}]

# `line`

```elixir
@spec line(line :: String.t(), name :: name() | nil) :: [run()]
```

A whole line in one part, or (`nil`) in none.

# `matches`

```elixir
@spec matches(
  line :: String.t(),
  regex :: Regex.t(),
  name :: name() | (String.t() -&gt; name() | nil)
) ::
  [run()]
```

A line labelled where a regex matches it: each match in the part `name`
gives (a name, or a function of the matched text returning one), and the
rest in no part.

    iex> Ascii.Parts.matches("a1 b22", ~r/\d+/, "digit")
    [{nil, "a"}, {"digit", "1"}, {nil, " b"}, {"digit", "22"}]

# `merge`

```elixir
@spec merge(runs :: [run()]) :: [run()]
```

Runs with neighbours in one part made one, and empty runs dropped. Runs
can be built a piece at a time, then tidied with this.

# `parent`

```elixir
@spec parent(name :: name()) :: name() | nil
```

The name a colour is looked up by next, if `name` has none: the name
without its last word, or `nil` for a name of one word.

    iex> Ascii.Parts.parent("status.ok")
    "status"
    iex> Ascii.Parts.parent("status")
    nil

# `refine`

```elixir
@spec refine(runs :: [run()], fun :: (String.t() -&gt; [run()])) :: [run()]
```

Runs labelled by `fun` where they are in no part yet, its runs taking
their place: for labelling in passes, the plain text left by one pass
given to the next.

# `run_at`

```elixir
@spec run_at(runs :: [run()] | nil, x :: non_neg_integer()) :: run() | nil
```

The run of a row (as `parts/2` gives it) that holds column `x`, from 0:
its part and its text. `nil` past the row's end.

    iex> Ascii.Parts.run_at([{nil, "  "}, {"status.ok", "[ OK ]"}, {nil, " up"}], 4)
    {"status.ok", "[ OK ]"}

# `suggested`

```elixir
@spec suggested(meta :: Ascii.Meta.t(), opts :: [{:paper, boolean()}]) :: colors()
```

The colours a piece suggests for its parts (its meta's `parts`, those it
gives one), as `parts: true` takes them: for a dark page, or with
`paper: true` for a light one. On paper, the shared colours become their
light-page pairs, and any other is darkened until it reads on white.

    iex> Ascii.Parts.suggested(Ascii.meta!("boot-log"))["status.ok"]
    "#3fb950"
    iex> Ascii.Parts.suggested(Ascii.meta!("boot-log"), paper: true)["status.ok"]
    "#1a7f37"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
