Packages
nldoc_conversion_reader_tiptap
2.0.1
2.1.1
2.1.0
2.0.1
1.2.40
1.2.39
1.2.38
1.2.37
1.2.36
1.2.35
1.2.34
1.2.33
1.2.32
1.2.31
1.2.30
1.2.29
1.2.28
1.2.27
1.2.26
1.2.25
1.2.24
1.2.23
1.2.22
1.2.21
1.2.20
1.2.19
1.2.18
1.2.17
1.2.16
1.2.15
1.2.14
1.2.13
1.2.12
1.2.11
1.2.10
1.2.9
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
Logic for reading Tiptap format to NLdoc Spec.
Current section
Files
Jump to
Current section
Files
lib/nldoc/conversion/reader/tiptap.ex
defmodule NLdoc.Conversion.Reader.Tiptap do
@moduledoc """
Reader for convert_elementing Tiptap elements to Spec Resources.
"""
use TypedStruct
defmodule State do
@moduledoc """
Format of state modified during converting.
"""
typedstruct enforce: true do
field(:assets, [NLdoc.Spec.Asset.t()])
end
end
defmodule Context do
@moduledoc """
Context for conversion that is only relevant to children and not to be passed back.
"""
typedstruct enforce: true do
field(:strict?, boolean())
end
end
@type opt() :: {:strict, boolean()}
@type error() :: {:error, term()}
@type input(resource) :: {resource, state :: State.t(), ctx :: Context.t()}
@type output(value) :: {:ok, {value, state :: State.t()}} | error()
@type tiptap_element() :: %{required(:type) => String.t()}
@spec convert(doc :: tiptap_element(), opts: [opt()]) ::
{:ok, NLdoc.Spec.Document.t()} | error()
def convert(doc = %{type: "doc"}, opts \\ []) do
state = %State{assets: []}
ctx = %Context{strict?: Keyword.get(opts, :strict, false)}
case convert_element({doc, state, ctx}) do
{:ok, {[resource = %NLdoc.Spec.Document{}], _state = %State{}}} ->
{:ok, reverse(resource)}
end
end
@conversions [
{NLdoc.Spec.Document, "doc"},
{NLdoc.Spec.Paragraph, "paragraph"},
{NLdoc.Spec.Heading, "heading"},
{NLdoc.Spec.Table, "table"},
{NLdoc.Spec.TableRow, "tableRow"},
{NLdoc.Spec.TableCell, "tableCell"},
{NLdoc.Spec.TableHeader, "tableHeader"},
{NLdoc.Spec.DefinitionList, "definitionList"},
{NLdoc.Spec.DefinitionTerm, "definitionTerm"},
{NLdoc.Spec.DefinitionDetails, "definitionDetails"},
{NLdoc.Spec.OrderedList, "orderedList"},
{NLdoc.Spec.UnorderedList, "bulletList"},
{NLdoc.Spec.ListItem, "listItem"},
{NLdoc.Spec.Preformatted, "codeBlock"},
{NLdoc.Spec.BlockQuotation, "blockquote"}
]
for {module, element_name} <- @conversions do
@spec convert_element(input(map())) :: output([unquote(module).t()])
defp convert_element(
{element = %{type: unquote(element_name)}, state = %State{}, ctx = %Context{}}
) do
with {:ok, {resource = %unquote(module){}, state = %State{}}} <-
apply_attributes({%unquote(module){}, state, ctx}, element),
{:ok, {children, state = %State{}}} <-
convert_children({Map.get(element, :content, []), state, ctx}, &convert_element/1) do
{:ok, {[%unquote(module){} = %{resource | children: children}], state}}
end
end
end
@spec convert_element(input(map())) :: output([NLdoc.Spec.Image.t()])
defp convert_element({element = %{type: "image"}, state = %State{}, ctx = %Context{}}) do
with {:ok, {resource = %NLdoc.Spec.Image{}, state = %State{}}} <-
apply_attributes({%NLdoc.Spec.Image{}, state, ctx}, element) do
{:ok, {[resource], state}}
end
end
@spec convert_element(input(map())) :: output([NLdoc.Spec.Text.t()])
@spec convert_element(input(map())) :: output([NLdoc.Spec.Link.t()])
defp convert_element({element = %{type: "text"}, state = %State{}, _ctx = %Context{}}) do
marks = Map.get(element, :marks, [])
link_mark = Enum.find(marks, &match?(%{type: "link"}, &1))
meta_attrs =
marks
|> Enum.find(%{}, &match?(%{type: "meta"}, &1))
|> Map.get(:attrs, %{})
resource =
if link_mark do
link_attrs = Map.get(link_mark, :attrs, %{})
%NLdoc.Spec.Link{
id: meta_attrs |> Map.get(:id) |> uuid_if_nil(),
text: Map.get(element, :text, ""),
uri: Map.get(link_attrs, :href),
purpose: Map.get(link_attrs, :purpose),
styling: marks_to_styling(marks)
}
else
%NLdoc.Spec.Text{
id: meta_attrs |> Map.get(:id) |> uuid_if_nil(),
text: Map.get(element, :text, ""),
styling: marks_to_styling(marks)
}
end
{:ok, {[resource], state}}
end
defp convert_element({_unsupported_resource, state = %State{}, _ctx = %Context{}}),
do: {:ok, {[], state}}
@spec convert_children(input([child]), convert_fn :: (input(child) -> output([result]))) ::
output([result])
when child: var, result: var
defp convert_children({children, state = %State{}, ctx = %Context{}}, convert_fn),
do:
Enum.reduce(
children,
{:ok, {[], state}},
fn
child, {:ok, {contents, state}} ->
with {:ok, {content, state}} <- convert_fn.({child, state, ctx}) do
{:ok, {content ++ contents, state}}
end
_, {:error, reason} ->
{:error, reason}
end
)
@attr_conversions [
{NLdoc.Spec.Document,
[
id: {:id, :uuid_if_nil},
assets: {:assets, :modify_assets}
]},
{NLdoc.Spec.Paragraph,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.Heading,
[
id: {:id, :uuid_if_nil},
level: {:level, nil}
]},
{NLdoc.Spec.Table,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.TableRow,
[
id: {:id, :uuid_if_nil},
colspan: {:colspan, nil},
rowspan: {:rowspan, nil}
]},
{NLdoc.Spec.TableCell,
[
id: {:id, :uuid_if_nil},
colspan: {:colspan, nil},
rowspan: {:rowspan, nil}
]},
{NLdoc.Spec.TableHeader,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.DefinitionList,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.DefinitionTerm,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.DefinitionDetails,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.OrderedList,
[
id: {:id, :uuid_if_nil},
start: {:start, nil}
]},
{NLdoc.Spec.UnorderedList,
[
id: {:id, :uuid_if_nil},
style_type: {:style_type, nil}
]},
{NLdoc.Spec.ListItem,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.Preformatted,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.BlockQuotation,
[
id: {:id, :uuid_if_nil}
]},
{NLdoc.Spec.Image,
[
id: {:id, :uuid_if_nil},
decorative: {:decorative, nil},
alternative_text: {:alt, nil},
source: {:assetId, :normalize_asset_source}
]}
]
for {module, mapping} <- @attr_conversions do
attrs_var = Macro.var(:attrs, nil)
resource_var = Macro.var(:resource, nil)
pipeline_ast =
Enum.reduce(mapping, quote(do: unquote(resource_var)), fn {field, {tiptap_attr, modifier}},
acc ->
base_val_ast =
quote do
Map.get(unquote(attrs_var), unquote(tiptap_attr), nil)
end
val_ast =
case modifier do
nil ->
base_val_ast
fun_name when is_atom(fun_name) ->
quote do
unquote(fun_name)(unquote(base_val_ast))
end
end
quote do
val = unquote(val_ast)
if is_nil(val) do
unquote(acc)
else
struct(unquote(acc), %{unquote(field) => val})
end
end
end)
@spec apply_attributes(input(%unquote(module){}), map()) :: output(%unquote(module){})
defp apply_attributes(
{resource = %unquote(module){}, state = %State{}, _ctx = %Context{}},
tiptap_element = %{}
) do
attrs = Map.get(tiptap_element, :attrs, %{})
{:ok, {unquote(pipeline_ast), state}}
end
end
@styling_mapping [
strikethrough: "strike",
code: "code",
bold: "bold",
italic: "italic",
underline: "underline",
subscript: "subscript",
superscript: "superscript"
]
@spec marks_to_styling([term()]) :: [NLdoc.Spec.text_style()]
for {spec, tiptap} <- @styling_mapping do
defp marks_to_styling([%{type: unquote(tiptap)} | xs]),
do: [unquote(spec) | marks_to_styling(xs)]
end
defp marks_to_styling([_ | xs]),
do: marks_to_styling(xs)
defp marks_to_styling([]),
do: []
@spec uuid_if_nil(nil) :: String.t()
defp uuid_if_nil(nil),
do: Ecto.UUID.generate()
@spec uuid_if_nil(value) :: value when value: term()
defp uuid_if_nil(value),
do: value
@spec normalize_asset_source(value :: String.t()) :: String.t()
defp normalize_asset_source(value) when is_binary(value),
do: "#" <> value
@spec normalize_asset_source(value) :: value when value: term()
defp normalize_asset_source(value),
do: value
@spec modify_assets(assets :: term()) :: [NLdoc.Spec.Asset.t()] | nil
defp modify_assets(assets) when is_map(assets) do
Enum.reduce(
assets,
[],
fn {id, base64}, acc ->
[NLdoc.Spec.Asset.from_base64(Atom.to_string(id), base64) | acc]
end
)
end
defp modify_assets(_assets),
do: nil
@spec reverse(content) :: content when content: term()
defp reverse(content) when is_list(content),
do: content |> Enum.map(&reverse/1) |> Enum.reverse()
defp reverse(resource = %{children: children}) when is_list(children),
do: Map.put(resource, :children, reverse(children))
defp reverse(other),
do: other
end