# `Statifier.Parser.Handler`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/parser/handler.ex#L1)

The `Saxy.Handler` that builds a `Statifier.Parser.DOM` tree, attaching the
positions Saxy does not supply.

Six clauses, and not one of them mentions an element name. v1's handler
lowered elements into typed structs as it went, which needed a clause per
(element type x parent context) pair - 73 of them in one module
(`../statifier/lib/statifier/parser/scxml/state_stack.ex`). Building a
generic node instead makes `:end_element` a single clause: append a child
to the open frame, whatever either of them is called.

## The zip

Saxy gives semantics and no positions; `Statifier.Parser.Markup` gives
positions and no semantics. Both produce element boundaries in document
order, and that is the join key - the Nth start-tag record the scanner
found is the Nth `:start_element` Saxy emits - so this handler carries the
scanner's records as a queue and pops exactly one per element event.

The two passes disagreeing is the mechanism's one real risk, so it is
checked rather than assumed: every popped record's name is compared against
the name in the Saxy event and a mismatch aborts with
`{:location_desync, expected, got}` instead of attaching a wrong location.
Because Saxy does no namespace processing and no expansion touches names,
the two spellings are byte-identical whenever the scan is in sync.

Every pop advances `cursor` to the popped record's end, which is what makes
text spans computable: a run of character data covers everything between
the cursor and the start of the next queued record, raw source included.

## Attribute values

`build_attributes/3` normalizes each attribute's value per XML 1.0 3.3.3
(ADR-0043), walking the raw slice (`Location.slice(value_location, source)`)
against Saxy's expanded value with `Location.normalize_attribute_value/3` -
Saxy's value alone cannot tell a literal newline from an expanded `&#10;`,
so the raw text is what disambiguates. When the scanner has no record for an
attribute, or the record's `value_location` is `nil`, there is no raw slice
to walk and Saxy's value stands unnormalized.

## Character data

`add_text/2` folds each text run's `value` per XML 1.0 2.11 (ADR-0045),
walking the run's raw span (`text_span/1`) against an *unfolded*
accumulation of Saxy's characters with `Location.normalize_character_data/3`.
The accumulation, not `value` itself, is what gets re-walked on every event
of a coalescing run: `walk_units/3`'s raw-versus-expanded pairing requires
both sides to empty together, and a raw `\r` no longer pairs against an
already-folded `\n` the way it pairs against Saxy's still-unfolded `\r\n`.
State carries this accumulation in a `text` field, reset implicitly by the
coalescing check already in `add_text/2` - a fresh run starts from `""`
because its frame's head child is not a `DOM.Text`. When the raw slice and
the accumulation desync, `normalize_character_data/3` degrades to the
unfolded value, the same posture attribute normalization takes.

# `cursor`

```elixir
@type cursor() ::
  {offset :: non_neg_integer(), line :: pos_integer(), column :: pos_integer()}
```

# `frame`

```elixir
@type frame() :: %{
  record: Statifier.Parser.Markup.t() | nil,
  attributes: [Statifier.Parser.DOM.Attribute.t()],
  children: [Statifier.Parser.DOM.Element.child()]
}
```

# `t`

```elixir
@type t() :: %{
  source: binary(),
  markup: [Statifier.Parser.Markup.t()],
  cursor: cursor(),
  stack: [frame()],
  text: binary()
}
```

# `init`

```elixir
@spec init(source :: binary(), markup :: [Statifier.Parser.Markup.t()]) :: t()
```

The initial handler state for `source` and its scanned markup records.

The stack starts with one frame standing in for the document itself, so
`:end_element` never has to special-case the root: the root element is
appended to that frame like any other child, and `:end_document` reads it
back out.

`text` starts empty; it is the unfolded accumulation `add_text/2` grows for
whichever run is currently open (see the moduledoc's "Character data"
section).

---

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