defmodule Jido.Chat.Markdown.Node do
@moduledoc """
Canonical Markdown AST node used by `Jido.Chat.Markdown`.
"""
alias Jido.Chat.Wire
@node_types [
:root,
:paragraph,
:text,
:strong,
:emphasis,
:link,
:code,
:code_block,
:heading,
:list,
:list_item,
:blockquote,
:table,
:table_row,
:table_cell,
:divider
]
@schema Zoi.struct(
__MODULE__,
%{
type: Zoi.enum(@node_types),
text: Zoi.string() |> Zoi.nullish(),
url: Zoi.string() |> Zoi.nullish(),
language: Zoi.string() |> Zoi.nullish(),
level: Zoi.integer() |> Zoi.nullish(),
ordered: Zoi.boolean() |> Zoi.nullish(),
start: Zoi.integer() |> Zoi.nullish(),
align: Zoi.string() |> Zoi.nullish(),
children: Zoi.list() |> Zoi.default([]),
metadata: Zoi.map() |> Zoi.default(%{})
},
coerce: true
)
@type node_type ::
:root
| :paragraph
| :text
| :strong
| :emphasis
| :link
| :code
| :code_block
| :heading
| :list
| :list_item
| :blockquote
| :table
| :table_row
| :table_cell
| :divider
@type t :: unquote(Zoi.type_spec(@schema))
@enforce_keys Zoi.Struct.enforce_keys(@schema)
defstruct Zoi.Struct.struct_fields(@schema)
@doc "Returns the schema for a Markdown node."
def schema, do: @schema
@doc "Creates a canonical Markdown node."
@spec new(t() | map()) :: t()
def new(%__MODULE__{} = node), do: node
def new(attrs) when is_map(attrs) do
attrs
|> normalize_children()
|> then(&Jido.Chat.Schema.parse!(__MODULE__, @schema, &1))
end
@doc "Serializes a Markdown node into a plain map with a type marker."
@spec to_map(t()) :: map()
def to_map(%__MODULE__{} = node) do
node
|> Map.from_struct()
|> Map.update!(:children, &Enum.map(&1, fn child -> child |> normalize() |> to_map() end))
|> Wire.to_plain()
|> Map.put("__type__", "markdown_node")
end
@doc "Builds a Markdown node from serialized map data."
@spec from_map(map()) :: t()
def from_map(map) when is_map(map), do: map |> Map.drop(["__type__", :__type__]) |> new()
@doc "Normalizes a Markdown node input."
@spec normalize(t() | map() | String.t() | nil) :: t() | nil
def normalize(nil), do: nil
def normalize(%__MODULE__{} = node), do: node
def normalize(value) when is_binary(value), do: new(%{type: :text, text: value})
def normalize(value) when is_map(value), do: new(value)
defp normalize_children(attrs) do
children =
attrs[:children] || attrs["children"] || []
attrs
|> Map.delete("children")
|> Map.put(:children, Enum.map(children, &normalize/1))
end
end