Packages
phoenix_kit
1.7.31
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/publishing/page_builder/parser.ex
defmodule PhoenixKit.Modules.Publishing.PageBuilder.Parser do
@moduledoc """
Parses .phk (PhoenixKit) XML-style markup into an AST.
Example input:
```xml
<Page slug="home">
<Hero variant="split-image">
<Headline>Welcome to PhoenixKit</Headline>
<Subheadline>Build faster with {{framework}}</Subheadline>
<CTA primary="true" action="/signup">Get Started</CTA>
</Hero>
</Page>
```
Output AST:
```elixir
%{
type: :page,
attributes: %{slug: "home"},
children: [
%{
type: :hero,
attributes: %{variant: "split-image"},
children: [
%{type: :headline, content: "Welcome to PhoenixKit"},
%{type: :subheadline, content: "Build faster with {{framework}}"},
%{type: :cta, attributes: %{primary: "true", action: "/signup"}, content: "Get Started"}
]
}
]
}
```
"""
@doc """
Parses .phk XML content into an AST.
"""
@spec parse(String.t()) :: {:ok, map()} | {:error, term()}
def parse(content) when is_binary(content) do
content = String.trim(content)
case Saxy.parse_string(
content,
PhoenixKit.Modules.Publishing.PageBuilder.SaxHandler,
[]
) do
{:ok, ast} -> {:ok, ast}
{:error, reason} -> {:error, {:parse_error, reason}}
end
rescue
e -> {:error, {:parse_exception, e}}
end
def parse(_), do: {:error, :invalid_content}
end
defmodule PhoenixKit.Modules.Publishing.PageBuilder.SaxHandler do
@moduledoc false
@behaviour Saxy.Handler
def handle_event(:start_document, _prolog, _state) do
{:ok, %{stack: [], result: nil}}
end
def handle_event(:end_document, _data, state) do
{:ok, state.result}
end
def handle_event(:start_element, {name, attributes}, state) do
node = %{
type: normalize_tag_name(name),
attributes: parse_attributes(attributes),
children: [],
content: nil
}
new_state = %{state | stack: [node | state.stack]}
{:ok, new_state}
end
def handle_event(:end_element, _name, %{stack: [current | rest]} = state) do
# Simplify node if it only has content and no children
simplified =
cond do
current.children == [] and is_binary(current.content) ->
%{
type: current.type,
attributes: current.attributes,
content: String.trim(current.content)
}
current.content == nil and current.children != [] ->
%{
type: current.type,
attributes: current.attributes,
children: Enum.reverse(current.children)
}
true ->
%{
type: current.type,
attributes: current.attributes,
children: Enum.reverse(current.children),
content: current.content && String.trim(current.content)
}
end
case rest do
[] ->
{:ok, %{state | stack: [], result: simplified}}
[parent | ancestors] ->
updated_parent = %{parent | children: [simplified | parent.children]}
{:ok, %{state | stack: [updated_parent | ancestors]}}
end
end
def handle_event(:characters, chars, %{stack: [current | rest]} = state) do
trimmed = String.trim(chars)
updated_current =
if trimmed != "" do
case current.content do
nil -> %{current | content: chars}
existing -> %{current | content: existing <> chars}
end
else
current
end
{:ok, %{state | stack: [updated_current | rest]}}
end
def handle_event(:characters, _chars, state) do
{:ok, state}
end
# Normalize tag names to atoms (Page -> :page, Hero -> :hero)
defp normalize_tag_name(name) do
name
|> String.downcase()
|> String.to_atom()
end
# Convert attribute list to map with string keys
defp parse_attributes(attrs) do
Enum.into(attrs, %{}, fn {key, value} ->
{String.downcase(key), value}
end)
end
end