Pure Elixir utilities for working with editor JSON content.
Documents are stored as a JSON tree of typed nodes with marks. This module provides server-side operations — HTML rendering, plain-text extraction, validation, and sanitization — so you can process editor content without JavaScript.
Editor JSON Format
%{
"type" => "doc",
"content" => [
%{
"type" => "paragraph",
"content" => [
%{"type" => "text", "text" => "Hello ", "marks" => [%{"type" => "bold"}]},
%{"type" => "text", "text" => "world"}
]
}
]
}Usage
alias PhiaUi.Components.Editor.EditorContent
# Render JSON to safe HTML
html = EditorContent.content_to_html(json)
# Extract plain text for search indexing
text = EditorContent.content_to_text(json)
# Validate against allowed schema
{:ok, json} = EditorContent.validate_content(json, allowed_nodes: ~w(paragraph heading))
# Count words
count = EditorContent.word_count(json)
Summary
Functions
Converts an editor JSON document to an HTML string.
Extracts plain text from an editor JSON document.
Returns the canonical empty document.
Strips disallowed nodes and marks from an editor JSON document.
Validates an editor JSON document against allowed node and mark types.
Counts words in an editor JSON document.
Functions
Converts an editor JSON document to an HTML string.
The output is not marked as safe — use Phoenix.HTML.raw/1 when rendering
in templates, after sanitizing user content.
Examples
iex> doc = %{"type" => "doc", "content" => [
...> %{"type" => "paragraph", "content" => [
...> %{"type" => "text", "text" => "Hello"}
...> ]}
...> ]}
iex> EditorContent.content_to_html(doc)
"<p>Hello</p>"
Extracts plain text from an editor JSON document.
Block-level nodes are separated by newlines. Useful for search indexing, word counting, and preview generation.
Examples
iex> doc = %{"type" => "doc", "content" => [
...> %{"type" => "paragraph", "content" => [
...> %{"type" => "text", "text" => "Hello world"}
...> ]},
...> %{"type" => "paragraph", "content" => [
...> %{"type" => "text", "text" => "Second paragraph"}
...> ]}
...> ]}
iex> EditorContent.content_to_text(doc)
"Hello world\nSecond paragraph"
@spec empty_document() :: map()
Returns the canonical empty document.
Example
iex> EditorContent.empty_document()
%{"type" => "doc", "content" => [%{"type" => "paragraph"}]}
Strips disallowed nodes and marks from an editor JSON document.
Nodes with disallowed types are removed entirely. Marks with disallowed types are stripped from text nodes (text is kept). Returns the cleaned document.
Options
:allowed_nodes— list of allowed node type strings:allowed_marks— list of allowed mark type strings
Examples
iex> doc = %{"type" => "doc", "content" => [
...> %{"type" => "paragraph", "content" => [%{"type" => "text", "text" => "ok"}]},
...> %{"type" => "script", "content" => []}
...> ]}
iex> EditorContent.sanitize_content(doc)
%{"type" => "doc", "content" => [
%{"type" => "paragraph", "content" => [%{"type" => "text", "text" => "ok"}]}
]}
Validates an editor JSON document against allowed node and mark types.
Returns {:ok, doc} if valid, or {:error, reasons} with a list of issues.
Options
:allowed_nodes— list of allowed node type strings (default: standard set):allowed_marks— list of allowed mark type strings (default: standard set)
Examples
iex> doc = %{"type" => "doc", "content" => [%{"type" => "paragraph"}]}
iex> EditorContent.validate_content(doc)
{:ok, doc}
iex> doc = %{"type" => "doc", "content" => [%{"type" => "script"}]}
iex> EditorContent.validate_content(doc)
{:error, ["disallowed node type: script"]}
@spec word_count(map() | nil) :: non_neg_integer()
Counts words in an editor JSON document.
Examples
iex> doc = %{"type" => "doc", "content" => [
...> %{"type" => "paragraph", "content" => [
...> %{"type" => "text", "text" => "Hello beautiful world"}
...> ]}
...> ]}
iex> EditorContent.word_count(doc)
3