diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46de98f..d24e8ea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
+* LiveViewNative.Template.Parser - a new Elixir parser for the LVN template syntax
+
### Changed
* `LiveViewNative.Component` no longer imports `Phoenix.Component.to_form/2`
diff --git a/lib/live_view_native/template/parse_error.ex b/lib/live_view_native/template/parse_error.ex
new file mode 100644
index 0000000..2b6ea61
--- /dev/null
+++ b/lib/live_view_native/template/parse_error.ex
@@ -0,0 +1,44 @@
+defmodule LiveViewNative.Template.ParseError do
+ @moduledoc """
+
+
+ """
+
+ defexception [:message]
+
+ @impl true
+ def exception({document, message, [start: cursor, end: cursor]}) do
+ msg = """
+ #{message}
+
+ #{document_line(document, cursor)}
+ """
+
+ %__MODULE__{message: msg}
+ end
+
+ def exception({document, message, [start: start_cursor, end: end_cursor]}) do
+ msg = """
+ #{message}
+
+ Start:
+ #{document_line(document, start_cursor)}
+
+ End:
+ #{document_line(document, end_cursor)}
+ """
+
+ %__MODULE__{message: msg}
+ end
+
+ defp document_line(document, [line: line, column: column]) do
+ doc_line =
+ document
+ |> String.split("\n")
+ |> Enum.at(line - 1)
+
+ loc = "#{line}: "
+
+ ~s|#{loc}#{doc_line}\n#{String.duplicate(" ", String.length(loc))}#{String.pad_leading("^", column, "-")}|
+ end
+end
diff --git a/lib/live_view_native/template/parser.ex b/lib/live_view_native/template/parser.ex
index 1e74968..7baa5bd 100644
--- a/lib/live_view_native/template/parser.ex
+++ b/lib/live_view_native/template/parser.ex
@@ -1,8 +1,76 @@
defmodule LiveViewNative.Template.Parser do
+ @moduledoc ~S'''
+ Floki-compliant parser for LiveView Native template syntax
+
+ iex> """
+ ...>
+ ...> Hello
+ ...> world!
+ ...>
+ ...> """
+ ...> |> LiveViewNative.Template.Parser.parse_document()
+ {:ok, [{"Group", [], [{"Text", [{"class", "bold"}], ["Hello"]}, {"Text", [], ["world!"]}]}]}
+
+ You can pass this AST into Floki for querying:
+
+ iex> """
+ ...>
+ ...> Hello"
+ ...> world!
+ ...>
+ ...> """
+ ...> |> LiveViewNative.Template.Parser.parse_document!()
+ ...> |> Floki.find("Text.bold")
+ [{"Text", [{"class", "bold"}], ["Hello"]}]
+
+ ## Floki Integration
+
+ Floki support passing parser in by option, this parser is compliant with that API:
+
+ iex> """
+ ...>
+ ...> Hello"
+ ...> world!
+ ...>
+ ...> """
+ ...> |> Floki.parse_document!(html_parser: LiveViewNative.Template.Parser)
+ ...> |> Floki.find("Text.bold")
+ [{"Text", [{"class", "bold"}], ["Hello"]}]
+
+ Or you can configure as the default:
+
+ ```elixir
+ config :floki, :html_parser, LiveViewNative.Tempalte.Parser
+ ```
+ '''
+
+ alias LiveViewNative.Template.ParseError
+
@first_chars ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@chars ~c"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"
@whitespace ~c"\s\t\n\r"
+ @doc """
+ Parses an LVN document from a string.
+
+ This is the main function to get a tree from a LVN string.
+
+ ## Options
+
+ * `:attributes_as_maps` - Change the behaviour of the parser to return the attributes
+ as maps, instead of a list of `{"key", "value"}`. Default to `false`.
+
+ ## Examples
+
+ iex> LiveViewNative.Template.Parser.parse_document("hello")
+ {:ok, [{"Group", [], [{"Text", [], []}, {"Text", [], ["hello"]}]}]}
+
+ iex> LiveViewNative.Template.Parser.parse_document(
+ ...> ~S(hello),
+ ...> attributes_as_maps: true
+ ...>)
+ {:ok, [{"Group", %{}, [{"Text", %{}, []}, {"Text", %{"class" => "main"}, ["hello"]}]}]}
+ """
def parse_document(document, args \\ []) do
parse(document, [line: 1, column: 1], [], args)
|> case do
@@ -11,10 +79,22 @@ defmodule LiveViewNative.Template.Parser do
end
end
+ @doc """
+ Parses a LVN Document from a string.
+
+ Similar to `parse_document/1`, but raises `LiveViewNative.Template.ParseError` if there was an
+ error parsing the document.
+
+ ## Example
+
+ iex> LiveViewNative.Template.Parser.parse_document!("hello")
+ [{"Group", [], [{"Text", [], []}, {"Text", [], ["hello"]}]}]
+
+ """
def parse_document!(document, args \\ []) do
case parse_document(document, args) do
- {:ok, {nodes, _cursor}} -> nodes
- {:error, message, _range} -> raise message
+ {:ok, nodes} -> nodes
+ {:error, message, range} -> raise ParseError, {document, message, range}
end
end
diff --git a/test/live_view_native/template/parser_test.exs b/test/live_view_native/template/parser_test.exs
index 7b9ae3e..05df169 100644
--- a/test/live_view_native/template/parser_test.exs
+++ b/test/live_view_native/template/parser_test.exs
@@ -2,6 +2,10 @@ defmodule LiveViewNative.Template.ParserTest do
use ExUnit.Case, async: false
import LiveViewNative.Template.Parser
+ alias LiveViewNative.Template.ParseError
+
+ doctest LiveViewNative.Template.Parser
+
test "will parse a tag" do
{:ok, nodes} = """
@@ -119,98 +123,126 @@ defmodule LiveViewNative.Template.ParserTest do
describe "parsing errors" do
test "eof within a comment" do
- {:error, _message, [start: start_pos, end: end_pos]} = "