diff --git a/.formatter.exs b/.formatter.exs index 6aebc5f..cfeb452 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,3 +1,14 @@ +locals_without_parens = [ + defparsec: 2, + defparsec: 3, + defparsecp: 2, + defparsecp: 3, + defcombinator: 2, + defcombinator: 3, + defcombinatorp: 2, + defcombinatorp: 3 +] + [ force_do_end_blocks: true, inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], diff --git a/lib/earmark_parser/nimble_parsers.ex b/lib/earmark_parser/nimble_parsers.ex new file mode 100644 index 0000000..927cab7 --- /dev/null +++ b/lib/earmark_parser/nimble_parsers.ex @@ -0,0 +1,9 @@ +defmodule EarmarkParser.NimbleParsers do + @moduledoc ~S""" + coming soon + """ + + defdelegate parse_html_atts(input), to: __MODULE__.HtmlAttsParser +end + +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/earmark_parser/nimble_parsers/html_atts_parser.ex b/lib/earmark_parser/nimble_parsers/html_atts_parser.ex new file mode 100644 index 0000000..a48d121 --- /dev/null +++ b/lib/earmark_parser/nimble_parsers/html_atts_parser.ex @@ -0,0 +1,40 @@ +defmodule EarmarkParser.NimbleParsers.HtmlAttsParser do + @moduledoc ~S""" + Parses an HTML tag + """ + import NimbleParsec + alias EarmarkParser.NimbleParsers.StringParser + + html_att_name = + ascii_string([?a..?z, ?A..?z, ?-..?-], min: 1) + + html_att = + html_att_name + |> choice([ + "=" |> string() |> ignore() |> parsec({StringParser, :string_value}), + empty() + ]) + |> reduce(:reduce_att) + + html_att_end = + string(">") + + html_atts = + html_att + |> repeat(" " |> string() |> times(min: 1) |> ignore() |> concat(html_att)) + + defparsec(:parse_html_atts, html_atts |> optional() |> ignore(html_att_end)) + + @doc false + def reduce_att(att_ast) + + def reduce_att([name]) do + {name, true} + end + + def reduce_att([name | values]) do + {name, IO.chardata_to_string(values)} + end +end + +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/earmark_parser/nimble_parsers/html_tag_parser.ex b/lib/earmark_parser/nimble_parsers/html_tag_parser.ex new file mode 100644 index 0000000..2046ca1 --- /dev/null +++ b/lib/earmark_parser/nimble_parsers/html_tag_parser.ex @@ -0,0 +1,10 @@ +defmodule EarmarkParser.NimbleParsers.HtmlTagParser do + + @moduledoc ~S""" + Parses an HTML tag + """ + import NimbleParsec + alias EarmarkParser.NimbleParsers.HtmlAttsParser + +end +# SPDX-License-Identifier: Apache-2.0 diff --git a/lib/earmark_parser/nimble_parsers/string_parser.ex b/lib/earmark_parser/nimble_parsers/string_parser.ex new file mode 100644 index 0000000..58f63f5 --- /dev/null +++ b/lib/earmark_parser/nimble_parsers/string_parser.ex @@ -0,0 +1,38 @@ +defmodule EarmarkParser.NimbleParsers.StringParser do + @moduledoc ~S""" + String combinator + """ + + import NimbleParsec + + inner_string = fn combinator, ch -> + combinator + |> repeat( + lookahead_not(ascii_char([ch])) + |> choice([ + IO.chardata_to_string(["\\", ch]) + |> string() + |> replace(ch), + utf8_char([]) + ]) + ) + end + + quoted_string = fn ch -> + empty() + |> ascii_char([ch]) + |> ignore() + |> inner_string.(ch) + |> ignore(ascii_char([ch])) + end + + defcombinator( + :string_value, + choice([ + quoted_string.(?"), + quoted_string.(?') + ]) + ) +end + +# SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/earmark_parser/options.ex b/lib/earmark_parser/options.ex index 8242c7d..d8a2b91 100644 --- a/lib/earmark_parser/options.ex +++ b/lib/earmark_parser/options.ex @@ -104,8 +104,8 @@ defmodule EarmarkParser.Options do Use normalize before passing it into any API function iex(1)> options = normalize(annotations: "%%") - ...(1)> options.annotations - ~r{\A(.*)(%%.*)} + ...(1)> options.annotations.source + "\\A(.*)(%%.*)" """ @spec normalize(t() | keyword()) :: t() def normalize(options) diff --git a/mix.exs b/mix.exs index 2666494..1caec0f 100644 --- a/mix.exs +++ b/mix.exs @@ -1,10 +1,14 @@ defmodule EarmarkParser.MixProject do use Mix.Project - @version "1.4.44" + @version "1.4.45" @url "https://github.com/RobertDober/earmark_parser" @deps [ + # production environnement + {:nimble_parsec, "~> 1.4.2", runtime: false}, + + # dev and test environnements {:benchee, "~> 1.3.1", only: [:dev]}, # {:credo, "~> 1.7.5", only: [:dev]}, {:dialyxir, "~> 1.4.5", only: [:dev], runtime: false}, @@ -14,6 +18,11 @@ defmodule EarmarkParser.MixProject do {:floki, "~> 0.36", only: [:dev, :test]} ] + def cli do + [ + ] + end + def project do [ app: :earmark_parser, diff --git a/mix.lock b/mix.lock index 36e1702..fad7842 100644 --- a/mix.lock +++ b/mix.lock @@ -17,6 +17,7 @@ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, diff --git a/test/nimble_parsers/html_att_test.exs b/test/nimble_parsers/html_att_test.exs new file mode 100644 index 0000000..cadbcf5 --- /dev/null +++ b/test/nimble_parsers/html_att_test.exs @@ -0,0 +1,50 @@ +defmodule Test.NimbleParsers.HtmlAttTest do + use Support.NimbleTestCase + + describe "empty att list" do + test "returns an empty list if end char (>) is present" do + parse_html_atts(">") + |> parsed_ok([]) + end + + test "does not parse an empty string" do + parse_html_atts("") + |> parsed_error("expected string \">\"") + end + end + + describe "boolean attribute" do + test "just it's presence" do + parse_html_atts("hidden>") + |> parsed_ok([{"hidden", true}]) + end + + test "two boolean attributes" do + parse_html_atts("hidden and-visible>") + |> parsed_ok([{"hidden", true}, {"and-visible", true}]) + end + end + + describe "a string attribute" do + test "elixir, what else?" do + parse_html_atts(~S{lang="elixir">}) + |> parsed_ok([{"lang", "elixir"}]) + end + + test "escaped double quote" do + parse_html_atts(~S{lang="\"lua\"">}) + |> parsed_ok([{"lang", "\"lua\""}]) + end + + test "single quoted string too" do + parse_html_atts(~S{lang="\"pt-br\"" lang='fr-fr' lang='de-\'at\''>}) + |> parsed_ok([ + {"lang", "\"pt-br\""}, + {"lang", "fr-fr"}, + {"lang", "de-'at'"} + ]) + end + end +end + +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/nimble_parsers/html_oneline_tag_test.exs b/test/nimble_parsers/html_oneline_tag_test.exs new file mode 100644 index 0000000..df2e1cc --- /dev/null +++ b/test/nimble_parsers/html_oneline_tag_test.exs @@ -0,0 +1,13 @@ +defmodule Test.NimbleParsers.HtmlOnelineTagTest do + + use Support.NimbleTestCase + + describe "no attributes" do + test "br" do + # parse_html_tag("
") + # |> parsed_ok({"br", [], [], %{verbatim: true}}) + end + end + +end +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/support/nimble_test_case.ex b/test/support/nimble_test_case.ex new file mode 100644 index 0000000..8be15f8 --- /dev/null +++ b/test/support/nimble_test_case.ex @@ -0,0 +1,12 @@ +defmodule Support.NimbleTestCase do + defmacro __using__(_options) do + quote do + use ExUnit.Case, async: true + + import EarmarkParser.NimbleParsers + import Support.NimbleTests + end + end +end + +# SPDX-License-Identifier: Apache-2.0 diff --git a/test/support/nimble_tests.ex b/test/support/nimble_tests.ex new file mode 100644 index 0000000..7dd2edf --- /dev/null +++ b/test/support/nimble_tests.ex @@ -0,0 +1,22 @@ +defmodule Support.NimbleTests do + @moduledoc ~S""" + Makes asserting on NimbleParsec results simpler + """ + defmacro parsed_error(parsed, expected) do + quote do + # |> IO.inspect() + {:error, message, _, _, _, _} = unquote(parsed) + assert message == unquote(expected) + end + end + + defmacro parsed_ok(parsed, expected) do + quote do + # |> IO.inspect() + {:ok, result, _, _, _, _} = unquote(parsed) + assert result == unquote(expected) + end + end +end + +# SPDX-License-Identifier: Apache-2.0