Skip to content

Commit

Permalink
Documentation and exception testing
Browse files Browse the repository at this point in the history
  • Loading branch information
bcardarella committed Oct 25, 2024
1 parent 4099bdf commit 9dcca0c
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 41 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
44 changes: 44 additions & 0 deletions lib/live_view_native/template/parse_error.ex
Original file line number Diff line number Diff line change
@@ -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
84 changes: 82 additions & 2 deletions lib/live_view_native/template/parser.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,76 @@
defmodule LiveViewNative.Template.Parser do
@moduledoc ~S'''
Floki-compliant parser for LiveView Native template syntax
iex> """
...> <Group>
...> <Text class="bold">Hello</Text>
...> <Text>world!</Text>
...> </Group>
...> """
...> |> LiveViewNative.Template.Parser.parse_document()
{:ok, [{"Group", [], [{"Text", [{"class", "bold"}], ["Hello"]}, {"Text", [], ["world!"]}]}]}
You can pass this AST into Floki for querying:
iex> """
...> <Group>
...> <Text class="bold">Hello</Text>"
...> <Text>world!</Text>
...> </Group>
...> """
...> |> 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> """
...> <Group>
...> <Text class="bold">Hello</Text>"
...> <Text>world!</Text>
...> </Group>
...> """
...> |> 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("<Group><Text></Text><Text>hello</Text></Group>")
{:ok, [{"Group", [], [{"Text", [], []}, {"Text", [], ["hello"]}]}]}
iex> LiveViewNative.Template.Parser.parse_document(
...> ~S(<Group><Text></Text><Text class="main">hello</Text></Group>),
...> 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
Expand All @@ -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!("<Group><Text></Text><Text>hello</Text></Group>")
[{"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

Expand Down
110 changes: 71 additions & 39 deletions test/live_view_native/template/parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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} = """
<FooBar></FooBar>
Expand Down Expand Up @@ -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]} = "<!--"
|> parse_document()
doc = "<!--"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 1, column: 5]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "invalid tag name character" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar!></FooBar!>
"""
|> parse_document()
doc = "<FooBar!></FooBar!>"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 1, column: 8]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "eof during tag name parsing" do
{:error, _message, [start: start_pos, end: end_pos]} = "<FooBar"
|> parse_document()
doc = "<FooBar"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 1, column: 8]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "invalid attribute key name" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar
a-*b="123"></FooBar>
"""
|> parse_document()
doc = """
<FooBar
a-*b="123"></FooBar>
"""
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 2, column: 5]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "eof during attribute key parsing" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar a
"""
|> parse_document()
doc = "<FooBar a"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 2, column: 1]
assert start_pos == [line: 1, column: 10]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "eof during attribute value parsing" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar a="
"""
|> parse_document()
doc = "<FooBar a="
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 2, column: 1]
assert start_pos == [line: 1, column: 11]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "catches parsing errors with invalid value format for attribute" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar a=
"""
|> parse_document()
doc = "<FooBar a=></FooBar>"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 2, column: 1]
assert start_pos == [line: 1, column: 11]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "catches errors with not closing tag entity propery" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar <Baz/>
"""
|> parse_document()
doc = "<FooBar <Baz/>"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 1, column: 9]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "eof while parsing tag entity" do
{:error, _message, [start: start_pos, end: end_pos]} = "<FooBar a=\"123\""
|> parse_document()
doc = ~s(<FooBar a="123")
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 1, column: 16]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end

test "eof while parsing children" do
{:error, _message, [start: start_pos, end: end_pos]} = """
<FooBar>
"""
|> parse_document()
doc = "<FooBar>"
{:error, _message, [start: start_pos, end: end_pos]} = parse_document(doc)

assert start_pos == end_pos
assert start_pos == [line: 2, column: 1]
assert start_pos == [line: 1, column: 9]

assert_raise ParseError, fn ->
parse_document!(doc)
end
end
end
end

0 comments on commit 9dcca0c

Please sign in to comment.