Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/bbcode/generator.ex b/lib/bbcode/generator.ex
index a878eae..8d21390 100644
--- a/lib/bbcode/generator.ex
+++ b/lib/bbcode/generator.ex
@@ -1,79 +1,79 @@
defmodule BBCode.Generator do
@moduledoc """
Generate HTML from BBCode fragments in AST form.
The BBCode syntax supported is described at [bbcode.org][bbcode].
[bbcode]: https://www.bbcode.org/reference.php
"""
defp start_tag(tagname), do: Enum.join(["<", tagname, ">"])
defp end_tag(tagname), do: Enum.join(["</", tagname, ">"])
defp simple_tag(tagname, subtree) do
{:ok, text} = reduce_subtree(subtree)
{:ok, [start_tag(tagname), text, end_tag(tagname)] |> Enum.join()}
end
defp link_tag(url), do: Enum.join(["<a href=\"", url, "\">"])
defp reduce_subtree({:b, subtree}), do: simple_tag("strong", subtree)
defp reduce_subtree({:i, subtree}), do: simple_tag("em", subtree)
defp reduce_subtree({:u, subtree}), do: simple_tag("u", subtree)
defp reduce_subtree({:s, subtree}), do: simple_tag("del", subtree)
defp reduce_subtree({:ul, subtree}), do: simple_tag("ul", subtree)
defp reduce_subtree({:ol, subtree}), do: simple_tag("ol", subtree)
defp reduce_subtree({:li, subtree}), do: simple_tag("li", subtree)
defp reduce_subtree({:code, subtree}), do: simple_tag("pre", subtree)
defp reduce_subtree({:quote, subtree}), do: simple_tag("blockquote", subtree)
defp reduce_subtree({:table, subtree}), do: simple_tag("table", subtree)
defp reduce_subtree({:tr, subtree}), do: simple_tag("tr", subtree)
defp reduce_subtree({:th, subtree}), do: simple_tag("th", subtree)
defp reduce_subtree({:td, subtree}), do: simple_tag("td", subtree)
defp reduce_subtree({:url, text}),
do: {:ok, [link_tag(text), text, end_tag("a")] |> Enum.join()}
defp reduce_subtree({:url, address, text}),
do: {:ok, [link_tag(address), text, end_tag("a")] |> Enum.join()}
defp reduce_subtree({:img, address}),
do: {:ok, "<img src=\"#{address}\">"}
defp reduce_subtree({:img, size, address}) do
[width, height] = String.split(size, "x")
{width, ""} = Integer.parse(width)
{height, ""} = Integer.parse(height)
{:ok, "<img src=\"#{address}\" width=\"#{width}\" height=\"#{height}\">"}
end
- defp reduce_subtree("\n"), do: {:ok, "<br>"}
+ defp reduce_subtree({:br}), do: {:ok, "<br>"}
defp reduce_subtree(text_node) when is_binary(text_node),
do: {:ok, text_node}
defp reduce_subtree(children) when is_list(children) do
with {:ok, new_tree} <-
Enum.reduce_while(children, {:ok, []}, fn x, {:ok, acc} ->
with {:ok, new_tree} <- reduce_subtree(x) do
{:cont, {:ok, acc ++ [new_tree]}}
else
{:error, e} ->
{:halt, {:error, e}}
end
end) do
{:ok, Enum.join(new_tree)}
else
{:error, e} ->
{:error, e}
end
end
defp reduce_subtree(tree), do: {:error, "unknown input #{inspect(tree)}"}
def to_html(tree) when is_list(tree), do: reduce_subtree(tree)
def to_html(_), do: {:error, "not a valid tree"}
end
diff --git a/lib/bbcode/parser.ex b/lib/bbcode/parser.ex
index 4c8524e..657470a 100644
--- a/lib/bbcode/parser.ex
+++ b/lib/bbcode/parser.ex
@@ -1,105 +1,171 @@
defmodule BBCode.Parser do
import NimbleParsec
@moduledoc """
Parse BBCode into an abstract tree.
"""
tag = utf8_string([?a..?z, ?A..?Z, ?0..?9], min: 1)
text = utf8_string([not: ?[, not: ?]], min: 1)
text_without_newline = utf8_string([not: ?[, not: ?\n], min: 1)
- start_tag =
- ignore(string("[")) |> concat(tag) |> ignore(string("]")) |> ignore(optional(string("\n")))
+ end_tag =
+ ignore(string("[/"))
+ |> concat(tag)
+ |> ignore(string("]"))
+
+ # block tags
+ quote_tag = string("quote")
+ ul_tag = string("ul")
+ ol_tag = string("ol")
+ li_tag = string("li")
+ code_tag = string("code")
+ table_tag = string("table")
+ tr_tag = string("tr")
+ th_tag = string("th")
+ td_tag = string("td")
+
+ # span tags
+ b_tag = string("b")
+ i_tag = string("i")
+ u_tag = string("u")
+ s_tag = string("s")
+ url_tag = string("url")
+ img_tag = string("img")
+
+ # special tags
+ star_tag = ignore(string("[*]"))
- end_tag = ignore(string("[/")) |> concat(tag) |> ignore(string("]"))
+ # newline
+ newline = utf8_char([?\r, ?\n])
- start_tag_with_property =
+ defcombinatorp(
+ :block_tag,
ignore(string("["))
- |> concat(tag)
+ |> choice([quote_tag, ul_tag, ol_tag, li_tag, code_tag, table_tag, tr_tag, th_tag, td_tag])
+ |> ignore(string("]"))
+ |> ignore(optional(utf8_string([?\n, ?\r], min: 1, max: 2)))
+ )
+
+ defcombinatorp(
+ :block_stanza,
+ parsec(:block_tag)
+ |> repeat(lookahead_not(string("[/")) |> choice([parsec(:child_stanza), text]))
+ |> wrap()
+ |> concat(end_tag)
+ |> ignore(optional(utf8_string([?\n, ?\r], min: 1, max: 2)))
+ |> post_traverse(:emit_tree_node)
+ )
+
+ defcombinatorp(
+ :span_tag,
+ ignore(string("["))
+ |> choice([url_tag, img_tag, b_tag, i_tag, u_tag, s_tag])
+ |> ignore(string("]"))
+ |> ignore(optional(utf8_string([?\n, ?\r], min: 1, max: 2)))
+ )
+
+ defcombinatorp(
+ :span_tag_with_property,
+ ignore(string("["))
+ |> choice([url_tag, img_tag])
|> ignore(string("="))
|> concat(text)
|> ignore(string("]"))
- |> ignore(optional(string("\n")))
-
- star_tag = ignore(string("[*]"))
+ |> ignore(optional(utf8_string([?\n, ?\r], min: 1, max: 2)))
+ )
defcombinatorp(
- :basic_stanza,
- start_tag
+ :span_stanza,
+ parsec(:span_tag)
|> repeat(lookahead_not(string("[/")) |> choice([parsec(:child_stanza), text]))
|> wrap()
|> concat(end_tag)
|> post_traverse(:emit_tree_node)
)
defcombinatorp(
:text_stanza,
text
|> wrap()
|> post_traverse(:emit_tree_node)
)
defcombinatorp(
:star_stanza,
star_tag
|> repeat(
lookahead_not(string("\n"))
|> choice([parsec(:child_stanza), text_without_newline])
)
|> wrap()
- |> concat(ignore(string("\n")))
+ |> concat(ignore(optional(utf8_string([?\n, ?\r], min: 1, max: 2))))
|> post_traverse(:emit_tree_node_star)
)
- defparsec(
- :prop_stanza,
- start_tag_with_property
+ defcombinatorp(
+ :span_stanza_with_property,
+ parsec(:span_tag_with_property)
|> repeat(lookahead_not(string("[/")) |> choice([parsec(:child_stanza), text]))
|> wrap()
|> concat(end_tag)
|> post_traverse(:emit_tree_node_property)
)
+ defcombinatorp(
+ :newline_stanza,
+ newline
+ |> post_traverse(:emit_tree_node_newline)
+ )
+
defcombinatorp(
:child_stanza,
- choice([parsec(:star_stanza), parsec(:prop_stanza), parsec(:basic_stanza)])
+ choice([
+ parsec(:newline_stanza),
+ parsec(:star_stanza),
+ parsec(:block_stanza),
+ parsec(:span_stanza_with_property),
+ parsec(:span_stanza)
+ ])
)
defcombinatorp(
:root_stanza,
choice([parsec(:child_stanza), parsec(:text_stanza)])
)
defparsecp(
:parse_tree,
repeat(lookahead_not(string("[/")) |> parsec(:root_stanza)) |> eos()
)
+ defp emit_tree_node_newline(_rest, _args, context, _line, _offset),
+ do: {[{:br}], context}
+
defp emit_tree_node_star(_rest, [nodes], context, _line, _offset),
do: {[{:li, nodes}], context}
defp emit_tree_node_property(_rest, [tag, [tag, property, inside]], context, _line, _offset),
do: {[{String.to_atom(tag), property, inside}], context}
defp emit_tree_node_property(_rest, [tag, [tag, property | nodes]], context, _line, _offset),
do: {[{String.to_atom(tag), property, nodes}], context}
defp emit_tree_node(_rest, [tag, [tag, inside]], context, _line, _offset),
do: {[{String.to_atom(tag), inside}], context}
defp emit_tree_node(_rest, [tag, [tag | nodes]], context, _line, _offset),
do: {[{String.to_atom(tag), nodes}], context}
defp emit_tree_node(_rest, [[text]], context, _line, _offset),
do: {[text], context}
def parse(text) do
with {:ok, nodes, _, _, _, _} <- parse_tree(text) do
{:ok, nodes}
else
{:error, e, _, _, _, _} ->
{:error, e}
end
end
end
diff --git a/test/bbcode/generator_test.exs b/test/bbcode/generator_test.exs
index bfc9fc3..5b63955 100644
--- a/test/bbcode/generator_test.exs
+++ b/test/bbcode/generator_test.exs
@@ -1,149 +1,157 @@
defmodule BBCode.Generator.Test do
use ExUnit.Case
describe "simple tags" do
test "[b] tags are translated to <strong>" do
assert {:ok, "<strong>testing</strong>"} = BBCode.to_html("[b]testing[/b]")
end
test "[i] tags are translated to <em>" do
assert {:ok, "<em>testing</em>"} = BBCode.to_html("[i]testing[/i]")
end
test "[u] tags are translated to <u>" do
assert {:ok, "<u>testing</u>"} = BBCode.to_html("[u]testing[/u]")
end
test "[s] tags are translated to <del>" do
assert {:ok, "<del>testing</del>"} = BBCode.to_html("[s]testing[/s]")
end
test "[code] tags are translated to <pre>" do
assert {:ok, "<pre>testing</pre>"} = BBCode.to_html("[code]testing[/code]")
end
test "[quote] tags are translated to <blockquote>" do
assert {:ok, "<blockquote>testing</blockquote>"} = BBCode.to_html("[quote]testing[/quote]")
end
test "compounding simple tags works as expected" do
assert {:ok, "<strong><em>testing</em></strong>"} = BBCode.to_html("[b][i]testing[/i][/b]")
end
end
describe "lists" do
test "[ul] lists are rendered properly" do
data = """
[ul]
[*]a
[*]b
[*]c
[/ul]
"""
- expected = """
- <ul>
- <li>a</li><li>b</li><li>c</li></ul>
- """
+ expected = "<ul><li>a</li><li>b</li><li>c</li></ul>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
test "[ol] lists are rendered properly" do
data = """
[ol]
[*]a
[*]b
[*]c
[/ol]
"""
- expected = """
- <ol>
- <li>a</li><li>b</li><li>c</li></ol>
- """
+ expected = "<ol><li>a</li><li>b</li><li>c</li></ol>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
end
describe "tables" do
test "[table] tables are rendered properly" do
data = """
[table]
[tr]
[th]header[/th]
[/tr]
[tr]
[td]cell[/td]
[/tr]
[/table]
"""
- expected = """
- <table>
- <tr>
- <th>header</th>
- </tr>
- <tr>
- <td>cell</td>
- </tr>
- </table>
- """
+ expected = "<table><tr><th>header</th></tr><tr><td>cell</td></tr></table>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
end
describe "links" do
test "bare [url] links are rendered properly" do
data = """
[url]http://example.com[/url]
"""
- expected = """
- <a href="http://example.com">http://example.com</a>
- """
+ expected = "<a href=\"http://example.com\">http://example.com</a><br>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
test "named [url] links are rendered properly" do
data = """
[url=http://example.com]Example[/url]
"""
- expected = """
- <a href="http://example.com">Example</a>
- """
+ expected = "<a href=\"http://example.com\">Example</a><br>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
end
describe "images" do
test "bare [img] links are rendered properly" do
data = """
[img]http://example.com/image.jpg[/img]
"""
- expected = """
- <img src="http://example.com/image.jpg">
- """
+ expected = "<img src=\"http://example.com/image.jpg\"><br>"
assert {:ok, ^expected} = BBCode.to_html(data)
end
test "sized [img] links are rendered properly" do
data = """
[img=32x32]http://example.com/image.jpg[/img]
"""
- expected = """
- <img src="http://example.com/image.jpg" width="32" height="32">
+ expected = "<img src=\"http://example.com/image.jpg\" width=\"32\" height=\"32\"><br>"
+
+ assert {:ok, ^expected} = BBCode.to_html(data)
+ end
+ end
+
+ describe "documents" do
+ test "it correctly renders a complex document" do
+ data = """
+ [quote]
+ A multiline quote.
+ This is the second line.
+ [/quote]
+
+ [ul]
+ [*]a
+ [*]b
+ [*]c
+ [/ul]
+
+ [b]bold[/b]
+ [i]italic[/i]
+ [u]underline[/u]
+ [s]strikethrough[/s]
+
+ [url=http://example.com]a link[/url]
+
+ @kaniini (a mention)
"""
+ expected =
+ "<blockquote>A multiline quote.\nThis is the second line.\n</blockquote><ul><li>a</li><li>b</li><li>c</li></ul><strong>bold</strong><br><em>italic</em><br><u>underline</u><br><del>strikethrough</del><br><br><a href=\"http://example.com\">a link</a><br><br>@kaniini (a mention)\n"
+
assert {:ok, ^expected} = BBCode.to_html(data)
end
end
end
diff --git a/test/bbcode/parser_test.exs b/test/bbcode/parser_test.exs
index 9bdd541..82e3683 100644
--- a/test/bbcode/parser_test.exs
+++ b/test/bbcode/parser_test.exs
@@ -1,90 +1,89 @@
defmodule BBCode.Parser.Test do
use ExUnit.Case
alias BBCode.Parser
describe "simple tags" do
test "it parses [b] tags correctly" do
assert {:ok, [b: "testing"]} = Parser.parse("[b]testing[/b]")
end
test "it parses [i] tags correctly" do
assert {:ok, [i: "testing"]} = Parser.parse("[i]testing[/i]")
end
test "it parses [u] tags correctly" do
assert {:ok, [u: "testing"]} = Parser.parse("[u]testing[/u]")
end
test "it parses [s] tags correctly" do
assert {:ok, [s: "testing"]} = Parser.parse("[s]testing[/s]")
end
test "it parses [code] tags correctly" do
assert {:ok, [code: "testing"]} = Parser.parse("[code]testing[/code]")
end
test "it parses [quote] tags correctly" do
assert {:ok, [quote: "testing"]} = Parser.parse("[quote]testing[/quote]")
end
end
describe "nested tags" do
test "it parses [ul] lists correctly" do
assert {:ok, [{:ul, [{:li, "a"}, {:li, "b"}]}]} =
Parser.parse("[ul][li]a[/li][li]b[/li][/ul]")
end
test "it parses [ol] lists correctly" do
assert {:ok, [{:ol, [{:li, "a"}, {:li, "b"}]}]} =
Parser.parse("[ol][li]a[/li][li]b[/li][/ol]")
end
end
describe "multiline" do
test "it parses a multiline [li] list" do
data = """
[ul]
[li]a[/li]
[li]b[/li]
[/ul]
"""
- assert {:ok, [{:ul, ["\n", {:li, "a"}, "\n", {:li, "b"}, "\n"]}, "\n"]} = Parser.parse(data)
+ assert {:ok, [{:ul, [{:li, "a"}, {:li, "b"}]}]} = Parser.parse(data)
end
test "it parses a multiline [*] list" do
data = """
[ul]
[*]a
[*]b
[/ul]
"""
- assert {:ok, [{:ul, ["\n", {:li, ["a"]}, {:li, ["b"]}]}, "\n"]} = Parser.parse(data)
+ assert {:ok, [{:ul, [{:li, ["a"]}, {:li, ["b"]}]}]} = Parser.parse(data)
end
test "it parses a multiline [*] list with children" do
data = """
[ul]
[*][url=http://example.com]Example[/url]
[/ul]
"""
- assert {:ok, [{:ul, ["\n", {:li, [{:url, "http://example.com", "Example"}]}]}, "\n"]} =
- Parser.parse(data)
+ assert {:ok, [{:ul, {:li, [{:url, "http://example.com", "Example"}]}}]} = Parser.parse(data)
end
end
describe "property tags" do
test "it parses [url=] tags correctly" do
assert {:ok, [{:url, "http://example.com", "Example"}]} =
Parser.parse("[url=http://example.com]Example[/url]")
end
end
describe "invalid input" do
test "it fails to parse unterminated input" do
{:error, "expected end of string"} = Parser.parse("hello [b]world")
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 12:58 PM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39796
Default Alt Text
(15 KB)

Event Timeline