Practical recipes for common tasks with Quillon.
Document Creation
Creating a Blog Post
def create_blog_post(title, author, content_paragraphs) do
Quillon.document([
Quillon.heading(1, title),
Quillon.paragraph([
Quillon.text("By "),
Quillon.text(author, [:italic])
]),
Quillon.divider(),
| Enum.map(content_paragraphs, &Quillon.paragraph/1)
])
endCreating a FAQ Section
def create_faq(questions_and_answers) do
items = Enum.flat_map(questions_and_answers, fn {question, answer} ->
[
Quillon.heading(3, question),
Quillon.paragraph(answer)
]
end)
Quillon.document([
Quillon.heading(2, "Frequently Asked Questions")
| items
])
endCreating a Code Tutorial
def create_tutorial(title, steps) do
content = Enum.flat_map(steps, fn {description, code, language} ->
[
Quillon.paragraph(description),
Quillon.code_block(code, language)
]
end)
Quillon.document([
Quillon.heading(1, title)
| content
])
endWorking with Lists
Creating a Nested List
def nested_list do
Quillon.bullet_list([
Quillon.list_item([
Quillon.paragraph("First level item"),
Quillon.bullet_list([
Quillon.list_item([Quillon.paragraph("Nested item 1")]),
Quillon.list_item([Quillon.paragraph("Nested item 2")])
])
]),
Quillon.list_item([Quillon.paragraph("Another first level")])
])
endConverting List Types
# Toggle between bullet and ordered list
list = Quillon.bullet_list([...])
ordered = Quillon.toggle_list_type(list) # Now an ordered list
bullet = Quillon.toggle_list_type(ordered) # Back to bulletExtracting List Items as Text
def list_to_strings({type, _attrs, items}) when type in [:bullet_list, :ordered_list] do
Enum.map(items, fn {:list_item, _, children} ->
children
|> Enum.flat_map(&extract_text/1)
|> Enum.join("")
end)
end
defp extract_text({:paragraph, _, children}), do: Enum.map(children, &extract_text/1)
defp extract_text({:text, %{text: text}, _}), do: [text]
defp extract_text(_), do: []Working with Tables
Creating a Data Table
def create_table(headers, rows) do
header_row = Quillon.table_row(
Enum.map(headers, fn h ->
Quillon.table_cell([Quillon.paragraph(h)])
end),
header: true
)
data_rows = Enum.map(rows, fn row ->
Quillon.table_row(
Enum.map(row, fn cell ->
Quillon.table_cell([Quillon.paragraph(to_string(cell))])
end)
)
end)
Quillon.table([header_row | data_rows])
end
# Usage
create_table(
["Name", "Age", "City"],
[
["Alice", 30, "New York"],
["Bob", 25, "London"]
]
)Adding a Row to an Existing Table
def append_row(table, cell_values) do
{:table, attrs, rows} = table
row_count = length(rows)
new_row = Quillon.table_row(
Enum.map(cell_values, fn value ->
Quillon.table_cell([Quillon.paragraph(to_string(value))])
end)
)
Quillon.add_row(table, row_count)
|> then(fn {:ok, t} ->
Quillon.update(t, [row_count], fn _ -> new_row end)
end)
endTree Traversal
Finding All Nodes of a Type
def find_all(node, type) do
find_all(node, type, [])
end
defp find_all({node_type, _attrs, children} = node, type, acc) do
acc = if node_type == type, do: [node | acc], else: acc
Enum.reduce(children, acc, fn child, acc ->
find_all(child, type, acc)
end)
endExtracting All Text
def extract_all_text(node) do
extract_all_text(node, [])
|> Enum.reverse()
|> Enum.join("")
end
defp extract_all_text({:text, %{text: text}, _}, acc), do: [text | acc]
defp extract_all_text({_type, _attrs, children}, acc) do
Enum.reduce(children, acc, &extract_all_text/2)
endCounting Words
def word_count(doc) do
doc
|> extract_all_text()
|> String.split(~r/\s+/, trim: true)
|> length()
endWalking the Tree with Path
def walk_with_path(node, fun, path \\ []) do
fun.(node, path)
{_type, _attrs, children} = node
children
|> Enum.with_index()
|> Enum.each(fn {child, index} ->
walk_with_path(child, fun, path ++ [index])
end)
end
# Usage: Print all nodes with their paths
walk_with_path(doc, fn node, path ->
IO.inspect({path, Quillon.type(node)})
end)Transformations
Converting Headings to a Table of Contents
def table_of_contents(doc) do
headings = find_all(doc, :heading)
items = Enum.map(headings, fn {:heading, %{level: level}, children} ->
text = children
|> Enum.map(fn {:text, %{text: t}, _} -> t end)
|> Enum.join("")
# Indent based on level
indent = String.duplicate(" ", level - 1)
Quillon.list_item([Quillon.paragraph("#{indent}#{text}")])
end)
Quillon.bullet_list(items)
endStripping All Formatting
def strip_formatting(doc) do
transform_tree(doc, fn
{:text, %{text: text}, []} ->
{:text, %{text: text, marks: []}, []}
node ->
node
end)
end
defp transform_tree({type, attrs, children}, fun) do
transformed_children = Enum.map(children, &transform_tree(&1, fun))
fun.({type, attrs, transformed_children})
endAdding IDs to All Blocks
def add_ids(doc) do
{doc, _counter} = add_ids(doc, 0)
doc
end
defp add_ids({type, attrs, children}, counter) do
{children, counter} = Enum.map_reduce(children, counter, &add_ids/2)
if Quillon.block?({type, attrs, children}) do
attrs = Map.put(attrs, :id, "block_#{counter}")
{{type, attrs, children}, counter + 1}
else
{{type, attrs, children}, counter}
end
endValidation Patterns
Validating Before Save
def save_document(doc) do
case Quillon.validate(doc) do
{:ok, valid_doc} ->
# Save to database
{:ok, persist(valid_doc)}
{:error, errors} ->
{:error, format_errors(errors)}
end
end
defp format_errors(errors) do
Enum.map(errors, fn {path, message} ->
"At #{inspect(path)}: #{message}"
end)
endCustom Validation Rules
def validate_max_heading_level(doc, max_level) do
headings = find_all(doc, :heading)
invalid = Enum.filter(headings, fn {:heading, %{level: level}, _} ->
level > max_level
end)
if invalid == [] do
:ok
else
{:error, "Found #{length(invalid)} headings exceeding level #{max_level}"}
end
endJSON Patterns
Round-trip Testing
def round_trip(doc) do
json = Quillon.to_json(doc)
{:ok, parsed} = Quillon.from_json(json)
# Verify equality (you might need custom comparison)
doc == parsed
endStoring in Ecto
# In your schema
schema "documents" do
field :content, :map # Stores JSON
timestamps()
end
# Converting
def changeset(doc, attrs) do
content = case attrs[:content] do
%{} = json -> json
tuple when is_tuple(tuple) -> Quillon.to_json(tuple)
_ -> nil
end
doc
|> cast(%{content: content}, [:content])
|> validate_quillon_doc(:content)
end
defp validate_quillon_doc(changeset, field) do
validate_change(changeset, field, fn _, value ->
case Quillon.from_json(value) do
{:ok, doc} ->
case Quillon.validate(doc) do
{:ok, _} -> []
{:error, errors} -> [{field, "invalid document: #{inspect(errors)}"}]
end
{:error, msg} ->
[{field, "invalid JSON: #{msg}"}]
end
end)
endPerformance Patterns
Lazy Document Building
# Instead of building large documents all at once,
# build incrementally with streams
def build_large_doc(items_stream) do
children = items_stream
|> Stream.map(&item_to_block/1)
|> Enum.to_list()
Quillon.document(children)
endCaching Computed Values
defmodule DocumentCache do
def get_word_count(doc) do
# Cache key could be a hash of the document
key = :erlang.phash2(doc)
case :persistent_term.get({__MODULE__, :word_count, key}, nil) do
nil ->
count = compute_word_count(doc)
:persistent_term.put({__MODULE__, :word_count, key}, count)
count
count ->
count
end
end
endIntegration Patterns
Phoenix LiveView Integration
# In your LiveView
def mount(_params, _session, socket) do
doc = Quillon.document([
Quillon.paragraph("Start typing...")
])
{:ok, assign(socket, doc: doc, json: Quillon.to_json(doc))}
end
def handle_event("update_doc", %{"content" => json}, socket) do
case Quillon.from_json(json) do
{:ok, doc} ->
{:noreply, assign(socket, doc: doc, json: json)}
{:error, _} ->
{:noreply, socket}
end
endAPI Response Formatting
def render("document.json", %{document: doc}) do
%{
data: Quillon.to_json(doc),
meta: %{
word_count: word_count(doc),
block_count: count_blocks(doc)
}
}
end