# `Yarnspinnex`
[🔗](https://github.com/atavistock/yarnspinnex/blob/main/lib/yarnspinnex.ex#L1)

Parses Yarn Spinner dialogue into a `Yarnspinnex.Script` (plain data, safe to cache) and runs
it one event at a time: `step/3` returns the next line, command, or option group along with an
updated `Yarnspinnex.Cursor`, and `choose/4` answers an option group. Nothing blocks.

A script reads two collections of values. `vars` live in the cursor, are written by `<<set>>`,
and persist with it. `context` is live host state passed to each `step/3` call and never
persisted - it is where structs, repos, and anything a game system owns belong. A name found in
both resolves to context; declare the context names at compile time to make that a hard error
instead of a surprise. In both, `$player` in a script and `player:` in Elixir are one name.

# `compile_option`

```elixir
@type compile_option() ::
  {:functions, module()}
  | {:context, [atom() | String.t()]}
  | {:variables, [atom() | String.t()]}
  | {:strict, boolean()}
```

# `check_cursor`

```elixir
@spec check_cursor(Yarnspinnex.Script.t(), Yarnspinnex.Cursor.t()) ::
  :ok | {:error, Yarnspinnex.Interpreter.cursor_error()}
```

Whether `cursor` can be resumed against `script`: its node is unchanged since the cursor was
made and it points at a real position in it. Call this on a cursor loaded from storage to decide
between resuming and restarting; `step/3` performs the same check and raises instead.

# `choose`

```elixir
@spec choose(
  Yarnspinnex.Script.t(),
  Yarnspinnex.Cursor.t(),
  term(),
  Yarnspinnex.Vars.collection()
) ::
  {:ok, Yarnspinnex.Cursor.t()}
  | {:error, Yarnspinnex.Interpreter.choose_error()}
```

Answers the option group `step/3` returned by picking the option at `index`.

# `compile_string`

```elixir
@spec compile_string(String.t(), [compile_option()]) ::
  {:ok, Yarnspinnex.Script.t()} | {:error, Yarnspinnex.CompileError.t()}
```

Parses and validates Yarn source into a `Yarnspinnex.Script`.

Options:

  * `:functions` - a `Yarnspinnex.HostFunctions` module; every call in the script is checked
    against its `declared/0`, by name and arity.
  * `:context` - the names the host supplies as context at run time. Assigning to one is
    rejected here rather than silently losing to context later.
  * `:variables` - names the host provides as `vars` (set by other scripts, or persisted), for
    `:strict`.
  * `:strict` - when true, every `$name` the script reads must be assigned somewhere in it or
    be listed under `:context` or `:variables`, so a misspelled name is a compile error instead
    of a `nil`.

# `enter`

```elixir
@spec enter(Yarnspinnex.Script.t(), String.t(), Yarnspinnex.Vars.vars()) ::
  {:ok, Yarnspinnex.Cursor.t()} | {:error, :unknown_node}
```

A cursor at the node titled `title` holding `vars`.

# `node_titles`

```elixir
@spec node_titles(Yarnspinnex.Script.t()) :: [String.t()]
```

Titles of every node in `script`.

# `start`

```elixir
@spec start(Yarnspinnex.Script.t(), Yarnspinnex.Vars.vars()) :: Yarnspinnex.Cursor.t()
```

A cursor at the start node (`"Start"` if present, else the first node) holding `vars`.

# `step`

```elixir
@spec step(
  Yarnspinnex.Script.t(),
  Yarnspinnex.Cursor.t(),
  Yarnspinnex.Vars.collection()
) ::
  Yarnspinnex.Interpreter.event()
```

Advances to the next event: a line, a command, an option group, or the end of the dialogue.

`context` is host state the script can read but not write; it is handed to
`c:Yarnspinnex.HostFunctions.call/3` untouched. Raises `Yarnspinnex.RuntimeError`, located at
the offending statement, when a statement cannot run or the cursor does not fit the script.

---

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