From 9a7c7beb05ced76ad1d8b22beac1e8f320dcd687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 10:09:37 +0100 Subject: [PATCH 01/35] wip --- .../twicpics_v2/key_value_parser.ex | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex new file mode 100644 index 0000000..068aeaa --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex @@ -0,0 +1,81 @@ +defmodule KeyValueParser do + def parse(input) do + parse_pairs(input, [], 0) + end + + def parse(_input), do: [] + + defp parse_pairs("", acc, _pos), do: Enum.reverse(acc) + + defp parse_pairs(<<"/"::binary, input::binary>>, acc, pos) do + parse_pairs(input, acc, pos + 1) + end + + defp parse_pairs(input, acc, key_pos) do + {key, rest, value_pos} = extract_key(input, key_pos) + {value, rest, after_value_pos} = extract_value(rest, value_pos) + next_acc = [{key, value, key_pos} | acc] + parse_pairs(rest, next_acc, after_value_pos) + end + + defp extract_key(input, pos) do + case String.split(input, "=", parts: 2) do + [key, rest] -> {key, rest, pos + String.length(key) + 1} + _ -> raise "Invalid input: Missing '=' in #{inspect(input)}" + end + end + + defp extract_value(input, pos) do + extract_until_slash_or_end(input, "", pos) + end + + defp extract_until_slash_or_end("", acc, pos), do: {acc, "", pos} + + defp extract_until_slash_or_end(<<"/"::binary, rest::binary>>, acc, pos) do + if balanced_parentheses?(acc) do + {acc, rest, pos + 1} + else + extract_until_slash_or_end(rest, acc <> "/", pos + 1) + end + end + + defp extract_until_slash_or_end(<>, acc, pos) do + extract_until_slash_or_end(rest, acc <> <>, pos + 1) + end + + def balanced_parentheses?(value) when is_binary(value) do + value + |> String.graphemes() + |> Enum.filter(&(&1 in ["(", ")"])) + |> balanced_parentheses?([]) + end + + # both items and stack exhausted, we're in balance! + defp balanced_parentheses?([], []), do: true + + # items is empty, but stack is not, so a paren has not been closed + defp balanced_parentheses?([], _stack), do: false + + # add "(" to stack + defp balanced_parentheses?(["(" = next | rest], stack), + do: balanced_parentheses?(rest, [next | stack]) + + # we found a ")", remove "(" from stack + defp balanced_parentheses?([")" | rest], ["(" | stack]), do: balanced_parentheses?(rest, stack) + + # and anything unhandled by now is a bogus closing paren + defp balanced_parentheses?(_items, _stack), do: false + + def print_and_parse(input) do + IO.puts(input <> ":") + KeyValueParser.parse(input) |> IO.inspect(syntax_colors: IO.ANSI.syntax_colors()) + IO.puts("") + end + + def example do + KeyValueParser.print_and_parse("k1=v1/k2=v2/k3=v3") + KeyValueParser.print_and_parse("k1=v1/k20=v20/k3=v3") + KeyValueParser.print_and_parse("k1=v1/k201=v201/k30=v30") + nil + end +end From 72bf1dab5f647f5c4ee24d38703a5302de55e53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 11:30:39 +0100 Subject: [PATCH 02/35] wip --- .../param_parser/twicpics_v2/formatters.ex | 8 ++ .../twicpics_v2/key_value_parser.ex | 82 +++++++++---------- .../twicpics_v2/kv_parser_test.exs | 37 +++++++++ 3 files changed, 84 insertions(+), 43 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/formatters.ex create mode 100644 test/param_parser/twicpics_v2/kv_parser_test.exs diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics_v2/formatters.ex new file mode 100644 index 0000000..eb3b33f --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/formatters.ex @@ -0,0 +1,8 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do + def format_error({err, opts}) do + error_offset = Keyword.get(opts, :pos, 0) + error_padding = String.duplicate(" ", error_offset) + IO.puts("#{error_padding}▲") + IO.puts("#{error_padding}└── #{err}") + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex index 068aeaa..463413e 100644 --- a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex @@ -1,39 +1,47 @@ -defmodule KeyValueParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do def parse(input) do - parse_pairs(input, [], 0) + case parse_pairs(input, [], 0) do + {:ok, result} -> {:ok, Enum.reverse(result)} + {:error, _reason} = error -> error + end end - def parse(_input), do: [] + defp parse_pairs("", acc, _pos), do: {:ok, acc} - defp parse_pairs("", acc, _pos), do: Enum.reverse(acc) + # pos + 1 because key is expected at the next char + defp parse_pairs("/", acc, pos), do: {:error, {:expected_key, pos: pos + 1}} - defp parse_pairs(<<"/"::binary, input::binary>>, acc, pos) do - parse_pairs(input, acc, pos + 1) - end + defp parse_pairs(<<"/"::binary, input::binary>>, acc, pos), + do: parse_pairs(input, acc, pos + 1) defp parse_pairs(input, acc, key_pos) do - {key, rest, value_pos} = extract_key(input, key_pos) - {value, rest, after_value_pos} = extract_value(rest, value_pos) - next_acc = [{key, value, key_pos} | acc] - parse_pairs(rest, next_acc, after_value_pos) + with {:ok, {key, rest, value_pos}} <- extract_key(input, key_pos), + {:ok, {value, rest, next_pos}} <- extract_value(rest, value_pos) do + parse_pairs(rest, [{key, value, key_pos} | acc], next_pos) + else + {:error, _reason} = error -> error + end end defp extract_key(input, pos) do case String.split(input, "=", parts: 2) do - [key, rest] -> {key, rest, pos + String.length(key) + 1} - _ -> raise "Invalid input: Missing '=' in #{inspect(input)}" + [key, rest] -> {:ok, {key, rest, pos + String.length(key) + 1}} + [rest] -> {:error, {:expected_eq, pos: pos + String.length(rest) + 1}} end end defp extract_value(input, pos) do - extract_until_slash_or_end(input, "", pos) + case extract_until_slash_or_end(input, "", pos) do + {"", rest, new_pos} -> {:error, {:expected_value, pos: pos}} + {value, rest, new_pos} -> {:ok, {value, rest, new_pos}} + end end defp extract_until_slash_or_end("", acc, pos), do: {acc, "", pos} defp extract_until_slash_or_end(<<"/"::binary, rest::binary>>, acc, pos) do - if balanced_parentheses?(acc) do - {acc, rest, pos + 1} + if balanced_parens?(acc) do + {acc, "/" <> rest, pos} else extract_until_slash_or_end(rest, acc <> "/", pos + 1) end @@ -43,39 +51,27 @@ defmodule KeyValueParser do extract_until_slash_or_end(rest, acc <> <>, pos + 1) end - def balanced_parentheses?(value) when is_binary(value) do - value - |> String.graphemes() - |> Enum.filter(&(&1 in ["(", ")"])) - |> balanced_parentheses?([]) + def balanced_parens?(value) when is_binary(value) do + balanced_parens?(value, []) end - # both items and stack exhausted, we're in balance! - defp balanced_parentheses?([], []), do: true + # both sting and stack exhausted, we're in balance! + defp balanced_parens?("", []), do: true - # items is empty, but stack is not, so a paren has not been closed - defp balanced_parentheses?([], _stack), do: false + # string is empty, but stack is not, so a paren has not been closed + defp balanced_parens?("", _stack), do: false # add "(" to stack - defp balanced_parentheses?(["(" = next | rest], stack), - do: balanced_parentheses?(rest, [next | stack]) - - # we found a ")", remove "(" from stack - defp balanced_parentheses?([")" | rest], ["(" | stack]), do: balanced_parentheses?(rest, stack) + defp balanced_parens?(<<"("::binary, rest::binary>>, stack), + do: balanced_parens?(rest, ["(" | stack]) - # and anything unhandled by now is a bogus closing paren - defp balanced_parentheses?(_items, _stack), do: false + # we found a ")", remove "(" from stack and continue + defp balanced_parens?(<<")"::binary, rest::binary>>, ["(" | stack]), + do: balanced_parens?(rest, stack) - def print_and_parse(input) do - IO.puts(input <> ":") - KeyValueParser.parse(input) |> IO.inspect(syntax_colors: IO.ANSI.syntax_colors()) - IO.puts("") - end + # we found a ")", but head of stack doesn't match + defp balanced_parens?(<<")"::binary, rest::binary>>, _stack), do: false - def example do - KeyValueParser.print_and_parse("k1=v1/k2=v2/k3=v3") - KeyValueParser.print_and_parse("k1=v1/k20=v20/k3=v3") - KeyValueParser.print_and_parse("k1=v1/k201=v201/k30=v30") - nil - end + # consume all other chars + defp balanced_parens?(<>, stack), do: balanced_parens?(rest, stack) end diff --git a/test/param_parser/twicpics_v2/kv_parser_test.exs b/test/param_parser/twicpics_v2/kv_parser_test.exs new file mode 100644 index 0000000..d6dc562 --- /dev/null +++ b/test/param_parser/twicpics_v2/kv_parser_test.exs @@ -0,0 +1,37 @@ +defmodule ImagePlug.Twicpics.KeyValueParserTest do + use ExUnit.Case, async: true + use ExUnitProperties + + alias ImagePlug.ParamParser.TwicpicsV2.KeyValueParser + + test "successful parse output returns correct key positions" do + assert KeyValueParser.parse("k1=v1/k2=v2/k3=v3") == + {:ok, + [ + {"k1", "v1", 0}, + {"k2", "v2", 6}, + {"k3", "v3", 12} + ]} + + assert KeyValueParser.parse("k1=v1/k20=v20/k300=v300/k4000=v4000") == + {:ok, + [ + {"k1", "v1", 0}, + {"k20", "v20", 6}, + {"k300", "v300", 14}, + {"k4000", "v4000", 24} + ]} + end + + test ":expected_eq error returns correct position" do + assert KeyValueParser.parse("k1=v1/k20=v20/k300") == {:error, {:expected_eq, pos: 19}} + end + + test ":expected_key error returns correct position" do + assert KeyValueParser.parse("k1=v1/k20=v20/") == {:error, {:expected_key, pos: 14}} + end + + test ":expected_value error returns correct position" do + assert KeyValueParser.parse("k1=v1/k20=") == {:error, {:expected_value, pos: 10}} + end +end From a1658c3ca684d4da9bee4f97fa6def6f8706eda9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 11:35:52 +0100 Subject: [PATCH 03/35] wip --- .../twicpics_v2/key_value_parser.ex | 32 +++---------------- .../param_parser/twicpics_v2/utils.ex | 25 +++++++++++++++ test/param_parser/twicpics_v2/utils_test.exs | 21 ++++++++++++ 3 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/utils.ex create mode 100644 test/param_parser/twicpics_v2/utils_test.exs diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex index 463413e..ef264c8 100644 --- a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex @@ -1,6 +1,8 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do - def parse(input) do - case parse_pairs(input, [], 0) do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case parse_pairs(input, [], pos_offset) do {:ok, result} -> {:ok, Enum.reverse(result)} {:error, _reason} = error -> error end @@ -40,7 +42,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do defp extract_until_slash_or_end("", acc, pos), do: {acc, "", pos} defp extract_until_slash_or_end(<<"/"::binary, rest::binary>>, acc, pos) do - if balanced_parens?(acc) do + if Utils.balanced_parens?(acc) do {acc, "/" <> rest, pos} else extract_until_slash_or_end(rest, acc <> "/", pos + 1) @@ -50,28 +52,4 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do defp extract_until_slash_or_end(<>, acc, pos) do extract_until_slash_or_end(rest, acc <> <>, pos + 1) end - - def balanced_parens?(value) when is_binary(value) do - balanced_parens?(value, []) - end - - # both sting and stack exhausted, we're in balance! - defp balanced_parens?("", []), do: true - - # string is empty, but stack is not, so a paren has not been closed - defp balanced_parens?("", _stack), do: false - - # add "(" to stack - defp balanced_parens?(<<"("::binary, rest::binary>>, stack), - do: balanced_parens?(rest, ["(" | stack]) - - # we found a ")", remove "(" from stack and continue - defp balanced_parens?(<<")"::binary, rest::binary>>, ["(" | stack]), - do: balanced_parens?(rest, stack) - - # we found a ")", but head of stack doesn't match - defp balanced_parens?(<<")"::binary, rest::binary>>, _stack), do: false - - # consume all other chars - defp balanced_parens?(<>, stack), do: balanced_parens?(rest, stack) end diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics_v2/utils.ex new file mode 100644 index 0000000..1bcb00d --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/utils.ex @@ -0,0 +1,25 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do + def balanced_parens?(value) when is_binary(value) do + balanced_parens?(value, []) + end + + # both sting and stack exhausted, we're in balance! + defp balanced_parens?("", []), do: true + + # string is empty, but stack is not, so a paren has not been closed + defp balanced_parens?("", _stack), do: false + + # add "(" to stack + defp balanced_parens?(<<"("::binary, rest::binary>>, stack), + do: balanced_parens?(rest, ["(" | stack]) + + # we found a ")", remove "(" from stack and continue + defp balanced_parens?(<<")"::binary, rest::binary>>, ["(" | stack]), + do: balanced_parens?(rest, stack) + + # we found a ")", but head of stack doesn't match + defp balanced_parens?(<<")"::binary, rest::binary>>, _stack), do: false + + # consume all other chars + defp balanced_parens?(<>, stack), do: balanced_parens?(rest, stack) +end diff --git a/test/param_parser/twicpics_v2/utils_test.exs b/test/param_parser/twicpics_v2/utils_test.exs new file mode 100644 index 0000000..999a6bf --- /dev/null +++ b/test/param_parser/twicpics_v2/utils_test.exs @@ -0,0 +1,21 @@ +defmodule ImagePlug.Twicpics.UtilsTest do + use ExUnit.Case, async: true + use ExUnitProperties + + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + test "balanced_parens?/1" do + assert Utils.balanced_parens?("(") == false + assert Utils.balanced_parens?(")") == false + assert Utils.balanced_parens?("(()") == false + assert Utils.balanced_parens?("())") == false + assert Utils.balanced_parens?("(((()))") == false + assert Utils.balanced_parens?("((())))") == false + + assert Utils.balanced_parens?("") == true + assert Utils.balanced_parens?("()") == true + assert Utils.balanced_parens?("(())") == true + assert Utils.balanced_parens?("((()))") == true + + end +end From fc570e39bdd7267cb2b58297bc283db2c7e27956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 14:28:03 +0100 Subject: [PATCH 04/35] wip --- .../param_parser/twicpics_v2/formatters.ex | 32 ++- .../param_parser/twicpics_v2/number_parser.ex | 219 ++++++++++++++++++ test/param_parser/twicpics_v2/utils_test.exs | 1 - 3 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/number_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics_v2/formatters.ex index eb3b33f..bc5e225 100644 --- a/lib/image_plug/param_parser/twicpics_v2/formatters.ex +++ b/lib/image_plug/param_parser/twicpics_v2/formatters.ex @@ -1,8 +1,36 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do - def format_error({err, opts}) do + defp format_char(:eoi), do: "end of input" + defp format_char(other), do: other + + defp join_chars([choice | tail]), do: join_chars(tail, ~s|"#{format_char(choice)}"|) + defp join_chars([], acc), do: acc + defp join_chars([last_choice], acc), do: ~s|#{acc} and "#{format_char(last_choice)}"| + + defp join_chars([choice | tail], acc), + do: join_chars(tail, ~s|#{acc}, "#{format_char(choice)}"|) + + defp format_msg({:unexpected_char, opts}) do + expected_chars = Keyword.get(opts, :expected) + found_char = Keyword.get(opts, :found) + ~s|Expected #{join_chars(expected_chars)} but "#{format_char(found_char)}" found.| + end + + defp format_msg({other, _}), do: to_string(other) + + def format_error({_, opts} = error) do + input = Keyword.get(opts, :input, "") error_offset = Keyword.get(opts, :pos, 0) error_padding = String.duplicate(" ", error_offset) + IO.puts(input) IO.puts("#{error_padding}▲") - IO.puts("#{error_padding}└── #{err}") + IO.puts("#{error_padding}└── #{format_msg(error)}") + end + + def print_result({:ok, result}) do + IO.puts(inspect(result, syntax_colors: IO.ANSI.syntax_colors())) + end + + def print_result({:error, err}) do + format_error(err) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex new file mode 100644 index 0000000..edb9f94 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -0,0 +1,219 @@ +defmodule NumberParser do + def parse(input, pos_offset \\ 0) do + case do_parse(input, [], 0, pos_offset) do + {:ok, tokens} -> + {:ok, + tokens + |> Enum.reverse() + |> Enum.map(fn + {:int, int} -> {:int, String.to_integer(int)} + {:float, int} -> {:float, String.to_float(int)} + other -> other + end)} + + {:error, {reason, opts}} = error -> + {:error, {reason, Keyword.put(opts, :input, input)}} + end + end + + # end of input + defp do_parse("", [{:float_open, _} | _], _paren_count, pos), + do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} + + defp do_parse("", acc, paren_count, pos) when paren_count > 0, + do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} + + defp do_parse("", [{:int, _} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [{:float, _} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [:right_paren | _] = acc, _paren_count, _pos), do: {:ok, acc} + + # first char in string + defp do_parse(<<"("::binary, rest::binary>>, [], paren_count, pos) do + # the only way to enter paren_count > 0 is through the first char + do_parse(rest, [:left_paren], paren_count + 1, pos + 1) + end + + defp do_parse(<>, [], paren_count, pos) do + cond do + char in ?0..?9 -> do_parse(rest, [{:int, <>}], paren_count, pos + 1) + end + end + + defp do_parse(<>, [], paren_count, pos) do + cond do + char in ?0..?9 -> do_parse(rest, [{:int, <>}], paren_count, pos + 1) + end + end + + # prev token: :left_paren + defp do_parse(<>, [:left_paren | _] = acc, paren_count, pos) do + cond do + char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], paren_count, pos + 1) + char == ?( -> do_parse(rest, [:left_paren | acc], paren_count + 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: char}} + end + end + + # prev token: :right_paren + defp do_parse(<>, [:right_paren | _] = acc, paren_count, pos) + when paren_count == 0 do + {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: char}} + end + + defp do_parse(<>, [:right_paren | _] = acc, paren_count, pos) + when paren_count > 0 do + cond do + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: char}} + end + end + + # prev token: {:int, n} + defp do_parse( + <>, + [{:int, cur_val} | acc_tail] = acc, + paren_count, + pos + ) + when paren_count == 0 do + # not in parens, so it's only a number literal, and no ops are allowed + cond do + char in ?0..?9 -> + do_parse(rest, [{:int, cur_val <> <>} | acc_tail], paren_count, pos + 1) + + char == ?. -> + do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], paren_count, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: char}} + end + end + + defp do_parse( + <>, + [{:int, cur_val} | acc_tail] = acc, + paren_count, + pos + ) + when paren_count > 0 do + cond do + char in ?0..?9 -> + do_parse(rest, [{:int, cur_val <> <>} | acc_tail], paren_count, pos + 1) + + char == ?. -> + do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], paren_count, pos + 1) + + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) + + true -> + {:error, + {:unexpected_char, + pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: char}} + end + end + + # prev token: {:float_open, n} - which means that it's not a valid float yet + defp do_parse( + <>, + [{:float_open, cur_val} | acc_tail] = acc, + paren_count, + pos + ) do + cond do + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + end + end + + # prev token: {:float, n} - at this point it's a valid float + defp do_parse( + <>, + [{:float, cur_val} | acc_tail] = acc, + paren_count, + pos + ) + when paren_count == 0 do + # not in parens, so it's only a number literal, and no ops are allowed + cond do + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + end + end + + defp do_parse( + <>, + [{:float, cur_val} | acc_tail] = acc, + paren_count, + pos + ) + when paren_count > 0 do + cond do + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) + + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) + + true -> + {:error, + {:unexpected_char, + pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: char}} + end + end + + # prev token: {:op, v} + defp do_parse(<>, [{:op, _} | _] = acc, paren_count, pos) + when paren_count > 0 do + cond do + char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], paren_count, pos + 1) + char == ?( -> do_parse(rest, [:left_paren | acc], paren_count + 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + end + end +end diff --git a/test/param_parser/twicpics_v2/utils_test.exs b/test/param_parser/twicpics_v2/utils_test.exs index 999a6bf..b62254f 100644 --- a/test/param_parser/twicpics_v2/utils_test.exs +++ b/test/param_parser/twicpics_v2/utils_test.exs @@ -16,6 +16,5 @@ defmodule ImagePlug.Twicpics.UtilsTest do assert Utils.balanced_parens?("()") == true assert Utils.balanced_parens?("(())") == true assert Utils.balanced_parens?("((()))") == true - end end From af179240628017d38d12e2478c5dcd5d2ad306f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 14:29:47 +0100 Subject: [PATCH 05/35] wip --- .../param_parser/twicpics_v2/number_parser.ex | 217 +++++------------- 1 file changed, 63 insertions(+), 154 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index edb9f94..f6e9c7d 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -2,14 +2,13 @@ defmodule NumberParser do def parse(input, pos_offset \\ 0) do case do_parse(input, [], 0, pos_offset) do {:ok, tokens} -> - {:ok, - tokens - |> Enum.reverse() - |> Enum.map(fn - {:int, int} -> {:int, String.to_integer(int)} - {:float, int} -> {:float, String.to_float(int)} - other -> other - end)} + {:ok, tokens + |> Enum.reverse() + |> Enum.map(fn + {:int, int} -> {:int, String.to_integer(int)} + {:float, int} -> {:float, String.to_float(int)} + other -> other + end)} {:error, {reason, opts}} = error -> {:error, {reason, Keyword.put(opts, :input, input)}} @@ -17,203 +16,113 @@ defmodule NumberParser do end # end of input - defp do_parse("", [{:float_open, _} | _], _paren_count, pos), - do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} - - defp do_parse("", acc, paren_count, pos) when paren_count > 0, - do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} - - defp do_parse("", [{:int, _} | _] = acc, _paren_count, _pos), do: {:ok, acc} - defp do_parse("", [{:float, _} | _] = acc, _paren_count, _pos), do: {:ok, acc} - defp do_parse("", [:right_paren | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [{:float_open, _} | _], _pcount, pos), do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} + defp do_parse("", acc, pcount, pos) when pcount > 0, do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} + defp do_parse("", [{:int, _} | _] = acc, _pcount, _pos), do: {:ok, acc} + defp do_parse("", [{:float, _} | _] = acc, _pcount, _pos), do: {:ok, acc} + defp do_parse("", [:right_paren | _] = acc, _pcount, _pos), do: {:ok, acc} # first char in string - defp do_parse(<<"("::binary, rest::binary>>, [], paren_count, pos) do - # the only way to enter paren_count > 0 is through the first char - do_parse(rest, [:left_paren], paren_count + 1, pos + 1) + defp do_parse(<<"("::binary, rest::binary>>, [], pcount, pos) do + # the only way to enter pcount > 0 is through the first char + do_parse(rest, [:left_paren], pcount + 1, pos + 1) end - defp do_parse(<>, [], paren_count, pos) do + defp do_parse(<>, [], pcount, pos) do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>}], paren_count, pos + 1) + char in ?0..?9 -> do_parse(rest, [{:int, <>}], pcount, pos + 1) end end - defp do_parse(<>, [], paren_count, pos) do + defp do_parse(<>, [], pcount, pos) do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>}], paren_count, pos + 1) + char in ?0..?9 -> do_parse(rest, [{:int, <>}], pcount, pos + 1) end end # prev token: :left_paren - defp do_parse(<>, [:left_paren | _] = acc, paren_count, pos) do + defp do_parse(<>, [:left_paren | _] = acc, pcount, pos) do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], paren_count, pos + 1) - char == ?( -> do_parse(rest, [:left_paren | acc], paren_count + 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + char == ?( -> do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} end end # prev token: :right_paren - defp do_parse(<>, [:right_paren | _] = acc, paren_count, pos) - when paren_count == 0 do - {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: char}} + defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) when pcount == 0 do + {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: <>}} end - defp do_parse(<>, [:right_paren | _] = acc, paren_count, pos) - when paren_count > 0 do + defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) when pcount > 0 do cond do - char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) - - char == ?) -> - do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) - - true -> - {:error, {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: char}} + char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:int, n} - defp do_parse( - <>, - [{:int, cur_val} | acc_tail] = acc, - paren_count, - pos - ) - when paren_count == 0 do + defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) when pcount == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>} | acc_tail], paren_count, pos + 1) - - char == ?. -> - do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], paren_count, pos + 1) - - true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: char}} + char in ?0..?9 -> do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + char == ?. -> do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: <>}} end end - defp do_parse( - <>, - [{:int, cur_val} | acc_tail] = acc, - paren_count, - pos - ) - when paren_count > 0 do + defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) when pcount > 0 do cond do - char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>} | acc_tail], paren_count, pos + 1) - - char == ?. -> - do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], paren_count, pos + 1) - - char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) - - char == ?) -> - do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) - - true -> - {:error, - {:unexpected_char, - pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + char == ?. -> do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:float_open, n} - which means that it's not a valid float yet - defp do_parse( - <>, - [{:float_open, cur_val} | acc_tail] = acc, - paren_count, - pos - ) do + defp do_parse(<>, [{:float_open, cur_val} | acc_tail] = acc, pcount, pos) do cond do - char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) - - true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end # prev token: {:float, n} - at this point it's a valid float - defp do_parse( - <>, - [{:float, cur_val} | acc_tail] = acc, - paren_count, - pos - ) - when paren_count == 0 do + defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) when pcount == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) - - true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end - defp do_parse( - <>, - [{:float, cur_val} | acc_tail] = acc, - paren_count, - pos - ) - when paren_count > 0 do + defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) when pcount > 0 do cond do - char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], paren_count, pos + 1) - - char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-"} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*"} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], paren_count, pos + 1) - - char == ?) -> - do_parse(rest, [:right_paren | acc], paren_count - 1, pos + 1) - - true -> - {:error, - {:unexpected_char, - pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:op, v} - defp do_parse(<>, [{:op, _} | _] = acc, paren_count, pos) - when paren_count > 0 do + defp do_parse(<>, [{:op, _} | _] = acc, pcount, pos) when pcount > 0 do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], paren_count, pos + 1) - char == ?( -> do_parse(rest, [:left_paren | acc], paren_count + 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: char}} + char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + char == ?( -> do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end end From c7906ab86ae215f445d1010584355ede5ba776da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 14:34:41 +0100 Subject: [PATCH 06/35] wip --- lib/image_plug/param_parser/twicpics_v2/formatters.ex | 2 +- lib/image_plug/param_parser/twicpics_v2/number_parser.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics_v2/formatters.ex index bc5e225..fa57bfe 100644 --- a/lib/image_plug/param_parser/twicpics_v2/formatters.ex +++ b/lib/image_plug/param_parser/twicpics_v2/formatters.ex @@ -4,7 +4,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do defp join_chars([choice | tail]), do: join_chars(tail, ~s|"#{format_char(choice)}"|) defp join_chars([], acc), do: acc - defp join_chars([last_choice], acc), do: ~s|#{acc} and "#{format_char(last_choice)}"| + defp join_chars([last_choice], acc), do: ~s|#{acc} or "#{format_char(last_choice)}"| defp join_chars([choice | tail], acc), do: join_chars(tail, ~s|#{acc}, "#{format_char(choice)}"|) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index f6e9c7d..65bc726 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -122,7 +122,7 @@ defmodule NumberParser do cond do char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) char == ?( -> do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} + true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "("], found: <>}} end end end From f5a710afa2d55583aa232841361f9f263e49aebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 15:22:17 +0100 Subject: [PATCH 07/35] wip --- .../param_parser/twicpics_v2/length_parser.ex | 58 +++++ .../param_parser/twicpics_v2/number_parser.ex | 210 ++++++++++++------ .../param_parser/twicpics_v2/utils.ex | 4 + 3 files changed, 209 insertions(+), 63 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/length_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex new file mode 100644 index 0000000..9eca794 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex @@ -0,0 +1,58 @@ +defmodule LengthParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + {type, num_str} = + case String.reverse(input) do + "p" <> num_str -> {:percent, String.reverse(num_str)} + "s" <> num_str -> {:scale, String.reverse(num_str)} + num_str -> {:pixels, String.reverse(num_str)} + end + + case NumberParser.parse(num_str, pos_offset) do + {:ok, parsed} -> + {:ok, {type, parsed}} + + {:error, {reason, opts}} = error -> + Utils.update_error_input(error, input) + end + end +end + +defmodule SizeParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, "x", parts: 2) do + ["-", "-"] -> + {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} + + ["-", height_str] -> + case LengthParser.parse(height_str, pos_offset + 2) do + {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, "-"] -> + case LengthParser.parse(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, height_str] -> + with {:ok, parsed_width} <- LengthParser.parse(width_str, pos_offset), + {:ok, parsed_height} <- + LengthParser.parse(height_str, pos_offset + String.length(width_str) + 1) do + {:ok, [width: parsed_width, height: parsed_height]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str] -> + case LengthParser.parse(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 65bc726..d674c2a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -1,128 +1,212 @@ defmodule NumberParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + def parse(input, pos_offset \\ 0) do case do_parse(input, [], 0, pos_offset) do {:ok, tokens} -> - {:ok, tokens - |> Enum.reverse() - |> Enum.map(fn - {:int, int} -> {:int, String.to_integer(int)} - {:float, int} -> {:float, String.to_float(int)} - other -> other - end)} + {:ok, + tokens + |> Enum.reverse() + |> Enum.map(fn + {:int, int} -> {:int, String.to_integer(int)} + {:float, int} -> {:float, String.to_float(int)} + other -> other + end)} {:error, {reason, opts}} = error -> - {:error, {reason, Keyword.put(opts, :input, input)}} + Utils.update_error_input(error, input) end end # end of input - defp do_parse("", [{:float_open, _} | _], _pcount, pos), do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} - defp do_parse("", acc, pcount, pos) when pcount > 0, do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} + defp do_parse("", [], pcount, pos) when pcount == 0, + do: {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: :eoi}} + + defp do_parse("", [{:float_open, _} | _], _pcount, pos), + do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} + + defp do_parse("", acc, pcount, pos) when pcount > 0, + do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} + defp do_parse("", [{:int, _} | _] = acc, _pcount, _pos), do: {:ok, acc} defp do_parse("", [{:float, _} | _] = acc, _pcount, _pos), do: {:ok, acc} defp do_parse("", [:right_paren | _] = acc, _pcount, _pos), do: {:ok, acc} # first char in string - defp do_parse(<<"("::binary, rest::binary>>, [], pcount, pos) do - # the only way to enter pcount > 0 is through the first char - do_parse(rest, [:left_paren], pcount + 1, pos + 1) - end - defp do_parse(<>, [], pcount, pos) do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>}], pcount, pos + 1) - end - end + char in ?0..?9 -> + do_parse(rest, [{:int, <>}], pcount, pos + 1) - defp do_parse(<>, [], pcount, pos) do - cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>}], pcount, pos + 1) + # the only way to enter pcount > 0 is through the first char + char == ?( -> + do_parse(rest, [:left_paren], pcount + 1, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} end end # prev token: :left_paren defp do_parse(<>, [:left_paren | _] = acc, pcount, pos) do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) - char == ?( -> do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + + char == ?( -> + do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} end end # prev token: :right_paren - defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) when pcount == 0 do + defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) + when pcount == 0 do {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: <>}} end - defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) when pcount > 0 do + defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) + when pcount > 0 do cond do - char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) - char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) - char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) - char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) - char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: <>}} + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + + true -> + {:error, + {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:int, n} - defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) when pcount == 0 do + defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) + when pcount == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) - char == ?. -> do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + + char == ?. -> + do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: <>}} end end - defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) when pcount > 0 do + defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) + when pcount > 0 do cond do - char in ?0..?9 -> do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) - char == ?. -> do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) - char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) - char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) - char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) - char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) - char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + + char == ?. -> + do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + + true -> + {:error, + {:unexpected_char, + pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:float_open, n} - which means that it's not a valid float yet - defp do_parse(<>, [{:float_open, cur_val} | acc_tail] = acc, pcount, pos) do + defp do_parse( + <>, + [{:float_open, cur_val} | acc_tail] = acc, + pcount, + pos + ) do cond do - char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end # prev token: {:float, n} - at this point it's a valid float - defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) when pcount == 0 do + defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) + when pcount == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end - defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) when pcount > 0 do + defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) + when pcount > 0 do cond do - char in ?0..?9 -> do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) - char == ?+ -> do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) - char == ?- -> do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) - char == ?* -> do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) - char == ?/ -> do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) - char == ?) -> do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + + char == ?+ -> + do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + + char == ?- -> + do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + + char == ?* -> + do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + + char == ?/ -> + do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + + char == ?) -> + do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + + true -> + {:error, + {:unexpected_char, + pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} end end # prev token: {:op, v} - defp do_parse(<>, [{:op, _} | _] = acc, pcount, pos) when pcount > 0 do + defp do_parse(<>, [{:op, _} | _] = acc, pcount, pos) + when pcount > 0 do cond do - char in ?0..?9 -> do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) - char == ?( -> do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) - true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "("], found: <>}} + char in ?0..?9 -> + do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + + char == ?( -> + do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + + true -> + {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "("], found: <>}} end end end diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics_v2/utils.ex index 1bcb00d..3e34466 100644 --- a/lib/image_plug/param_parser/twicpics_v2/utils.ex +++ b/lib/image_plug/param_parser/twicpics_v2/utils.ex @@ -22,4 +22,8 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do # consume all other chars defp balanced_parens?(<>, stack), do: balanced_parens?(rest, stack) + + def update_error_input({:error, {reason, opts}}, input) do + {:error, {reason, Keyword.put(opts, :input, input)}} + end end From 66bb07b2fd30086c66a636caf51a1315e4c88f4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 21:15:03 +0100 Subject: [PATCH 08/35] keep ahold of token position so errors can be linked to tokens later --- .../param_parser/twicpics_v2/arithmetic.ex | 111 +++++++++++++++++ .../param_parser/twicpics_v2/length_parser.ex | 12 +- .../param_parser/twicpics_v2/number_parser.ex | 117 ++++++++++-------- 3 files changed, 179 insertions(+), 61 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/arithmetic.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex new file mode 100644 index 0000000..4450d25 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex @@ -0,0 +1,111 @@ +defmodule Arithmetic do + @type token :: {:int, integer} | {:float, float} | {:op, binary} | :left_paren | :right_paren + @type expr :: {:int, integer} | {:float, float} | {:op, binary, expr(), expr()} + + @spec evaluate(String.t()) :: {:ok, } | {:error, atom()} + def parse_and_evaluate(input) do + case parse(input) do + {:ok, expr} -> evaluate(expr) + {:error, _} = error -> error + end + end + + @spec parse(String.t()) :: {:ok, expr()} | {:error, atom()} + def parse(tokens) do + case parse_expression(tokens, 0) do + {:ok, expr, []} -> + {:ok, expr} + {:ok, expr, [token | _]} -> + {start_pos, _end_pos} = NumberParser.pos_for_token(token) + {:error, {:unexpected_token, pos: start_pos}} + {:error, _} = error -> error + end + end + + def parse_expression(tokens, min_prec) do + case parse_primary(tokens) do + {:ok, lhs, rest} -> parse_binary_op(lhs, rest, min_prec) + {:error, _} = error -> error + end + end + + defp parse_primary([{:int, n, pos_b, pos_e} | rest]), do: {:ok, {:int, n, pos_b, pos_e}, rest} + defp parse_primary([{:float, n, pos_b, pos_e} | rest]), do: {:ok, {:float, n, pos_b, pos_e}, rest} + + defp parse_primary([{:left_paren, pos} | rest]) do + case parse_expression(rest, 0) do + {:ok, expr, [{:right_paren, _pos} | rest2]} -> {:ok, expr, rest2} + {:ok, _, _} -> {:error, {:mismatched_paren, pos: pos}} + {:error, _} = error -> error + end + end + + defp parse_primary([token | _]) do + {start_pos, _end_pos} = NumberParser.pos_for_token(token) + {:error, {:unexpected_token, pos: start_pos}} + end + + defp parse_binary_op(lhs, tokens, min_prec) do + case tokens do + [{:op, op, _} | rest] -> + prec = precedence(op) + + if prec < min_prec do + {:ok, lhs, tokens} + else + case parse_expression(rest, prec + 1) do + {:ok, rhs, rest2} -> + new_lhs = {:op, op, lhs, rhs} + parse_binary_op(new_lhs, rest2, min_prec) + + {:error, _} = error -> + error + end + end + + _ -> + {:ok, lhs, tokens} + end + end + + defp precedence("+"), do: 1 + defp precedence("-"), do: 1 + defp precedence("*"), do: 2 + defp precedence("/"), do: 2 + + @spec evaluate(expr()) :: {:ok, number} | {:error, String.t()} + def evaluate({:int, n, _pos_b, _pos_e}), do: {:ok, n} + def evaluate({:float, n, _pos_b, _pos_e}), do: {:ok, n} + + def evaluate({:op, "+", lhs, rhs}) do + with {:ok, lval} <- evaluate(lhs), + {:ok, rval} <- evaluate(rhs) do + {:ok, lval + rval} + end + end + + def evaluate({:op, "-", lhs, rhs}) do + with {:ok, lval} <- evaluate(lhs), + {:ok, rval} <- evaluate(rhs) do + {:ok, lval - rval} + end + end + + def evaluate({:op, "*", lhs, rhs}) do + with {:ok, lval} <- evaluate(lhs), + {:ok, rval} <- evaluate(rhs) do + {:ok, lval * rval} + end + end + + def evaluate({:op, "/", lhs, rhs}) do + with {:ok, lval} <- evaluate(lhs), + {:ok, rval} <- evaluate(rhs) do + if rval == 0 do + {:error, :division_by_zero} + else + {:ok, lval / rval} + end + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex index 9eca794..a6c1653 100644 --- a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex @@ -1,5 +1,6 @@ defmodule LengthParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils + alias Arithmetic def parse(input, pos_offset \\ 0) do {type, num_str} = @@ -9,12 +10,11 @@ defmodule LengthParser do num_str -> {:pixels, String.reverse(num_str)} end - case NumberParser.parse(num_str, pos_offset) do - {:ok, parsed} -> - {:ok, {type, parsed}} - - {:error, {reason, opts}} = error -> - Utils.update_error_input(error, input) + with {:ok, tokens} <- NumberParser.parse(num_str, pos_offset), + {:ok, evaluated} <- Arithmetic.parse_and_evaluate(tokens) do + {:ok, {type, evaluated}} + else + {:error, {reason, opts}} = error -> Utils.update_error_input(error, input) end end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index d674c2a..1a9daab 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -8,8 +8,8 @@ defmodule NumberParser do tokens |> Enum.reverse() |> Enum.map(fn - {:int, int} -> {:int, String.to_integer(int)} - {:float, int} -> {:float, String.to_float(int)} + {:int, int, pos_b, pos_e} -> {:int, String.to_integer(int), pos_b, pos_e} + {:float, int, pos_b, pos_e} -> {:float, String.to_float(int), pos_b, pos_e} other -> other end)} @@ -19,28 +19,28 @@ defmodule NumberParser do end # end of input - defp do_parse("", [], pcount, pos) when pcount == 0, + defp do_parse("", [], paren_count, pos) when paren_count == 0, do: {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: :eoi}} - defp do_parse("", [{:float_open, _} | _], _pcount, pos), + defp do_parse("", [{:float_open, _, _} | _], _paren_count, pos), do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} - defp do_parse("", acc, pcount, pos) when pcount > 0, + defp do_parse("", acc, paren_count, pos) when paren_count > 0, do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} - defp do_parse("", [{:int, _} | _] = acc, _pcount, _pos), do: {:ok, acc} - defp do_parse("", [{:float, _} | _] = acc, _pcount, _pos), do: {:ok, acc} - defp do_parse("", [:right_paren | _] = acc, _pcount, _pos), do: {:ok, acc} + defp do_parse("", [{:int, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [{:float, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [{:right_paren, _t_pos} | _] = acc, _paren_count, _pos), do: {:ok, acc} # first char in string - defp do_parse(<>, [], pcount, pos) do + defp do_parse(<>, [], paren_count, pos) do cond do char in ?0..?9 -> - do_parse(rest, [{:int, <>}], pcount, pos + 1) + do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) - # the only way to enter pcount > 0 is through the first char + # the only way to enter paren_count > 0 is through the first char char == ?( -> - do_parse(rest, [:left_paren], pcount + 1, pos + 1) + do_parse(rest, [{:left_paren, pos}], paren_count + 1, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} @@ -48,13 +48,13 @@ defmodule NumberParser do end # prev token: :left_paren - defp do_parse(<>, [:left_paren | _] = acc, pcount, pos) do + defp do_parse(<>, [{:left_paren, _t_pos} | _] = acc, paren_count, pos) do cond do char in ?0..?9 -> - do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) char == ?( -> - do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} @@ -62,28 +62,28 @@ defmodule NumberParser do end # prev token: :right_paren - defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) - when pcount == 0 do + defp do_parse(<>, [{:right_paren, _t_pos} | _] = acc, paren_count, pos) + when paren_count == 0 do {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: <>}} end - defp do_parse(<>, [:right_paren | _] = acc, pcount, pos) - when pcount > 0 do + defp do_parse(<>, [{:right_paren, _t_pos} | _] = acc, paren_count, pos) + when paren_count > 0 do cond do char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) char == ?- -> - do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) char == ?* -> - do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) char == ?) -> - do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> {:error, @@ -92,44 +92,44 @@ defmodule NumberParser do end # prev token: {:int, n} - defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) - when pcount == 0 do + defp do_parse(<>, [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + when paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) char == ?. -> - do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b} | acc_tail], paren_count, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: <>}} end end - defp do_parse(<>, [{:int, cur_val} | acc_tail] = acc, pcount, pos) - when pcount > 0 do + defp do_parse(<>, [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + when paren_count > 0 do cond do char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) char == ?. -> - do_parse(rest, [{:float_open, cur_val <> "."} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b, pos} | acc_tail], paren_count, pos + 1) char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) char == ?- -> - do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) char == ?* -> - do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) char == ?) -> - do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> {:error, @@ -141,13 +141,13 @@ defmodule NumberParser do # prev token: {:float_open, n} - which means that it's not a valid float yet defp do_parse( <>, - [{:float_open, cur_val} | acc_tail] = acc, - pcount, + [{:float_open, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, + paren_count, pos ) do cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} @@ -155,38 +155,38 @@ defmodule NumberParser do end # prev token: {:float, n} - at this point it's a valid float - defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) - when pcount == 0 do + defp do_parse(<>, [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + when paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end - defp do_parse(<>, [{:float, cur_val} | acc_tail] = acc, pcount, pos) - when pcount > 0 do + defp do_parse(<>, [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + when paren_count > 0 do cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>} | acc_tail], pcount, pos + 1) + do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) char == ?+ -> - do_parse(rest, [{:op, "+"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) char == ?- -> - do_parse(rest, [{:op, "-"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) char == ?* -> - do_parse(rest, [{:op, "*"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) char == ?/ -> - do_parse(rest, [{:op, "/"} | acc], pcount, pos + 1) + do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) char == ?) -> - do_parse(rest, [:right_paren | acc], pcount - 1, pos + 1) + do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> {:error, @@ -196,17 +196,24 @@ defmodule NumberParser do end # prev token: {:op, v} - defp do_parse(<>, [{:op, _} | _] = acc, pcount, pos) - when pcount > 0 do + defp do_parse(<>, [{:op, _optype, _t_pos} | _] = acc, paren_count, pos) + when paren_count > 0 do cond do char in ?0..?9 -> - do_parse(rest, [{:int, <>} | acc], pcount, pos + 1) + do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) char == ?( -> - do_parse(rest, [:left_paren | acc], pcount + 1, pos + 1) + do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "("], found: <>}} end end + + def pos_for_token({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos_for_token({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos_for_token({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos_for_token({:left_paren, pos}), do: {pos, pos} + def pos_for_token({:right_paren, pos}), do: {pos, pos} + def pos_for_token({:op, _optype, pos}), do: {pos, pos} end From 463a326c1936c09e655ed8f143b0b9e04018ee3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 21:18:36 +0100 Subject: [PATCH 09/35] naming --- .../param_parser/twicpics_v2/arithmetic.ex | 4 ++-- .../param_parser/twicpics_v2/number_parser.ex | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex index 4450d25..8c88b0a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex +++ b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex @@ -16,7 +16,7 @@ defmodule Arithmetic do {:ok, expr, []} -> {:ok, expr} {:ok, expr, [token | _]} -> - {start_pos, _end_pos} = NumberParser.pos_for_token(token) + {start_pos, _end_pos} = NumberParser.pos(token) {:error, {:unexpected_token, pos: start_pos}} {:error, _} = error -> error end @@ -41,7 +41,7 @@ defmodule Arithmetic do end defp parse_primary([token | _]) do - {start_pos, _end_pos} = NumberParser.pos_for_token(token) + {start_pos, _end_pos} = NumberParser.pos(token) {:error, {:unexpected_token, pos: start_pos}} end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 1a9daab..b1fb336 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -210,10 +210,10 @@ defmodule NumberParser do end end - def pos_for_token({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos_for_token({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos_for_token({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos_for_token({:left_paren, pos}), do: {pos, pos} - def pos_for_token({:right_paren, pos}), do: {pos, pos} - def pos_for_token({:op, _optype, pos}), do: {pos, pos} + def pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:left_paren, pos}), do: {pos, pos} + def pos({:right_paren, pos}), do: {pos, pos} + def pos({:op, _optype, pos}), do: {pos, pos} end From d4e9c81a902d879bf89fe735ef4126841d59352a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 22:01:41 +0100 Subject: [PATCH 10/35] wip --- .../param_parser/twicpics_v2/arithmetic.ex | 11 +- .../param_parser/twicpics_v2/formatters.ex | 5 + .../param_parser/twicpics_v2/length_parser.ex | 100 +++++++++++------ .../param_parser/twicpics_v2/number_parser.ex | 101 +++++++++++++++--- 4 files changed, 168 insertions(+), 49 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex index 8c88b0a..68ae225 100644 --- a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex +++ b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex @@ -2,7 +2,7 @@ defmodule Arithmetic do @type token :: {:int, integer} | {:float, float} | {:op, binary} | :left_paren | :right_paren @type expr :: {:int, integer} | {:float, float} | {:op, binary, expr(), expr()} - @spec evaluate(String.t()) :: {:ok, } | {:error, atom()} + @spec evaluate(String.t()) :: {:ok} | {:error, atom()} def parse_and_evaluate(input) do case parse(input) do {:ok, expr} -> evaluate(expr) @@ -15,10 +15,13 @@ defmodule Arithmetic do case parse_expression(tokens, 0) do {:ok, expr, []} -> {:ok, expr} + {:ok, expr, [token | _]} -> {start_pos, _end_pos} = NumberParser.pos(token) {:error, {:unexpected_token, pos: start_pos}} - {:error, _} = error -> error + + {:error, _} = error -> + error end end @@ -30,7 +33,9 @@ defmodule Arithmetic do end defp parse_primary([{:int, n, pos_b, pos_e} | rest]), do: {:ok, {:int, n, pos_b, pos_e}, rest} - defp parse_primary([{:float, n, pos_b, pos_e} | rest]), do: {:ok, {:float, n, pos_b, pos_e}, rest} + + defp parse_primary([{:float, n, pos_b, pos_e} | rest]), + do: {:ok, {:float, n, pos_b, pos_e}, rest} defp parse_primary([{:left_paren, pos} | rest]) do case parse_expression(rest, 0) do diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics_v2/formatters.ex index fa57bfe..6a9be7c 100644 --- a/lib/image_plug/param_parser/twicpics_v2/formatters.ex +++ b/lib/image_plug/param_parser/twicpics_v2/formatters.ex @@ -15,6 +15,11 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do ~s|Expected #{join_chars(expected_chars)} but "#{format_char(found_char)}" found.| end + defp format_msg({:strictly_positive_number_required, opts}) do + found_number = Keyword.get(opts, :found) + ~s|Strictly positive number expected, found #{format_char(found_number)} instead.| + end + defp format_msg({other, _}), do: to_string(other) def format_error({_, opts} = error) do diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex index a6c1653..559292a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex @@ -23,36 +23,76 @@ defmodule SizeParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils def parse(input, pos_offset \\ 0) do - case String.split(input, "x", parts: 2) do - ["-", "-"] -> - {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} - - ["-", height_str] -> - case LengthParser.parse(height_str, pos_offset + 2) do - {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, "-"] -> - case LengthParser.parse(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, height_str] -> - with {:ok, parsed_width} <- LengthParser.parse(width_str, pos_offset), - {:ok, parsed_height} <- - LengthParser.parse(height_str, pos_offset + String.length(width_str) + 1) do - {:ok, [width: parsed_width, height: parsed_height]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str] -> - case LengthParser.parse(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end + result = + case String.split(input, "x", parts: 2) do + ["-", "-"] -> + {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} + + ["-", height_str] -> + case parse_and_validate(height_str, pos_offset + 2) do + {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, "-"] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, height_str] -> + with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), + {:ok, parsed_height} <- + parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do + {:ok, [width: parsed_width, height: parsed_height]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end + end + + defp parse_and_validate(length_str, offset) do + case LengthParser.parse(length_str, offset) do + {:ok, {_type, number} = parsed_length} when number > 0 -> + {:ok, parsed_length} + + {:ok, {_type, number}} -> + {:error, {:strictly_positive_number_required, pos: offset, found: number}} + + {:error, _reason} = error -> + error + end + end +end + +defmodule CoordinatesParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + result = + case String.split(input, "x", parts: 2) do + [left_str, top_str] -> + with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), + {:ok, parsed_top} <- + parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do + {:ok, [left: parsed_left, top: parsed_top]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end + end + + defp parse_and_validate(length_str, offset) do + case LengthParser.parse(length_str, offset) do + {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} + {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} + {:error, _reason} = error -> error end end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index b1fb336..bd155ad 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -28,8 +28,12 @@ defmodule NumberParser do defp do_parse("", acc, paren_count, pos) when paren_count > 0, do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} - defp do_parse("", [{:int, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), do: {:ok, acc} - defp do_parse("", [{:float, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse("", [{:int, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), + do: {:ok, acc} + + defp do_parse("", [{:float, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), + do: {:ok, acc} + defp do_parse("", [{:right_paren, _t_pos} | _] = acc, _paren_count, _pos), do: {:ok, acc} # first char in string @@ -38,7 +42,7 @@ defmodule NumberParser do char in ?0..?9 -> do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) - # the only way to enter paren_count > 0 is through the first char + # the only way to enter paren_count > 0 is through the first char char == ?( -> do_parse(rest, [{:left_paren, pos}], paren_count + 1, pos + 1) @@ -62,12 +66,22 @@ defmodule NumberParser do end # prev token: :right_paren - defp do_parse(<>, [{:right_paren, _t_pos} | _] = acc, paren_count, pos) + defp do_parse( + <>, + [{:right_paren, _t_pos} | _] = acc, + paren_count, + pos + ) when paren_count == 0 do {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: <>}} end - defp do_parse(<>, [{:right_paren, _t_pos} | _] = acc, paren_count, pos) + defp do_parse( + <>, + [{:right_paren, _t_pos} | _] = acc, + paren_count, + pos + ) when paren_count > 0 do cond do char == ?+ -> @@ -92,12 +106,22 @@ defmodule NumberParser do end # prev token: {:int, n} - defp do_parse(<>, [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + defp do_parse( + <>, + [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, + paren_count, + pos + ) when paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) char == ?. -> do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b} | acc_tail], paren_count, pos + 1) @@ -107,14 +131,29 @@ defmodule NumberParser do end end - defp do_parse(<>, [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + defp do_parse( + <>, + [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, + paren_count, + pos + ) when paren_count > 0 do cond do char in ?0..?9 -> - do_parse(rest, [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) char == ?. -> - do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:float_open, cur_val <> ".", t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) char == ?+ -> do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) @@ -147,7 +186,12 @@ defmodule NumberParser do ) do cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} @@ -155,23 +199,43 @@ defmodule NumberParser do end # prev token: {:float, n} - at this point it's a valid float - defp do_parse(<>, [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + defp do_parse( + <>, + [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, + paren_count, + pos + ) when paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) true -> {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} end end - defp do_parse(<>, [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, paren_count, pos) + defp do_parse( + <>, + [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, + paren_count, + pos + ) when paren_count > 0 do cond do char in ?0..?9 -> - do_parse(rest, [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], paren_count, pos + 1) + do_parse( + rest, + [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], + paren_count, + pos + 1 + ) char == ?+ -> do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) @@ -196,7 +260,12 @@ defmodule NumberParser do end # prev token: {:op, v} - defp do_parse(<>, [{:op, _optype, _t_pos} | _] = acc, paren_count, pos) + defp do_parse( + <>, + [{:op, _optype, _t_pos} | _] = acc, + paren_count, + pos + ) when paren_count > 0 do cond do char in ?0..?9 -> From 88245d129b1f2b3362080cf6646b49d252f4e9b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 22:15:05 +0100 Subject: [PATCH 11/35] wip --- .../param_parser/twicpics_v2/length_parser.ex | 121 +++++++++++------- 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex index 559292a..3368d9b 100644 --- a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex @@ -23,38 +23,37 @@ defmodule SizeParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils def parse(input, pos_offset \\ 0) do - result = - case String.split(input, "x", parts: 2) do - ["-", "-"] -> - {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} - - ["-", height_str] -> - case parse_and_validate(height_str, pos_offset + 2) do - {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, "-"] -> - case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, height_str] -> - with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), - {:ok, parsed_height} <- - parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do - {:ok, [width: parsed_width, height: parsed_height]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str] -> - case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - end + case String.split(input, "x", parts: 2) do + ["-", "-"] -> + {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} + + ["-", height_str] -> + case parse_and_validate(height_str, pos_offset + 2) do + {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, "-"] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, height_str] -> + with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), + {:ok, parsed_height} <- + parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do + {:ok, [width: parsed_width, height: parsed_height]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end end defp parse_and_validate(length_str, offset) do @@ -75,17 +74,53 @@ defmodule CoordinatesParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils def parse(input, pos_offset \\ 0) do - result = - case String.split(input, "x", parts: 2) do - [left_str, top_str] -> - with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), - {:ok, parsed_top} <- - parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do - {:ok, [left: parsed_left, top: parsed_top]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - end + case String.split(input, "x", parts: 2) do + [left_str, top_str] -> + with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), + {:ok, parsed_top} <- + parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do + {:ok, [left: parsed_left, top: parsed_top]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [""] -> + {:error, {:unexpected_eoi, pos: pos_offset}} + + _ -> + {:error, {:unexpected_token, pos: pos_offset}} + end + end + + defp parse_and_validate(length_str, offset) do + case LengthParser.parse(length_str, offset) do + {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} + {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} + {:error, _reason} = error -> error + end + end +end + +defmodule CropParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, "@", parts: 2) do + [size_str, coordinates_str] -> + with {:ok, parsed_size} <- SizeParser.parse(size_str, pos_offset), + {:ok, parsed_coordinates} <- + CoordinatesParser.parse(coordinates_str, pos_offset + String.length(size_str) + 1) do + {:ok, [crop: parsed_size, crop_from: parsed_coordinates]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [size_str] -> + case SizeParser.parse(size_str, pos_offset) do + {:ok, parsed_size} -> {:ok, [crop: parsed_size, crop_from: :focus]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end end defp parse_and_validate(length_str, offset) do From b413e999f8bf847a3193fe7ea935b10dda483992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 22:42:58 +0100 Subject: [PATCH 12/35] wip --- lib/image_plug/param_parser/twicpics_v2/formatters.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics_v2/formatters.ex index 6a9be7c..a4c8f5f 100644 --- a/lib/image_plug/param_parser/twicpics_v2/formatters.ex +++ b/lib/image_plug/param_parser/twicpics_v2/formatters.ex @@ -20,6 +20,11 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do ~s|Strictly positive number expected, found #{format_char(found_number)} instead.| end + defp format_msg({:positive_number_required, opts}) do + found_number = Keyword.get(opts, :found) + ~s|Positive number expected, found #{format_char(found_number)} instead.| + end + defp format_msg({other, _}), do: to_string(other) def format_error({_, opts} = error) do From 67ee1bd41655ea42576437f39fd6c785d5b2d678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 22:43:07 +0100 Subject: [PATCH 13/35] negative number support --- lib/image_plug/param_parser/twicpics_v2/number_parser.ex | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index bd155ad..22b1a30 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -42,6 +42,9 @@ defmodule NumberParser do char in ?0..?9 -> do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) + char == ?- -> + do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) + # the only way to enter paren_count > 0 is through the first char char == ?( -> do_parse(rest, [{:left_paren, pos}], paren_count + 1, pos + 1) @@ -57,6 +60,9 @@ defmodule NumberParser do char in ?0..?9 -> do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char == ?- -> + do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char == ?( -> do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) @@ -271,6 +277,9 @@ defmodule NumberParser do char in ?0..?9 -> do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char == ?- -> + do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char == ?( -> do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) From 97cb71634e2a2a3f490fc2c12eda7eb9cf6221b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Fri, 6 Dec 2024 23:20:45 +0100 Subject: [PATCH 14/35] add basic number parser test --- .../twicpics_v2/number_parser_test.exs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 test/param_parser/twicpics_v2/number_parser_test.exs diff --git a/test/param_parser/twicpics_v2/number_parser_test.exs b/test/param_parser/twicpics_v2/number_parser_test.exs new file mode 100644 index 0000000..913c3ec --- /dev/null +++ b/test/param_parser/twicpics_v2/number_parser_test.exs @@ -0,0 +1,38 @@ +defmodule ImagePlug.Twicpics.NumberParserTest do + use ExUnit.Case, async: true + use ExUnitProperties + + test "successful parse output returns correct key positions" do + assert NumberParser.parse("10") == {:ok, [{:int, 10, 0, 1}]} + + assert NumberParser.parse("(10+10)") == + {:ok, + [ + {:left_paren, 0}, + {:int, 10, 1, 2}, + {:op, "+", 3}, + {:int, 10, 4, 5}, + {:right_paren, 6} + ]} + + assert NumberParser.parse("(10/20*(4+5)+5*-1)") == + {:ok, + [ + {:left_paren, 0}, + {:int, 10, 1, 2}, + {:op, "/", 3}, + {:int, 20, 4, 5}, + {:op, "*", 6}, + {:left_paren, 7}, + {:int, 4, 8, 8}, + {:op, "+", 9}, + {:int, 5, 10, 10}, + {:right_paren, 11}, + {:op, "+", 12}, + {:int, 5, 13, 13}, + {:op, "*", 14}, + {:int, -1, 15, 16}, + {:right_paren, 17} + ]} + end +end From 2cefa92cf67e3ba5e07ff7c1f74974ada0d6ab61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 20:15:14 +0100 Subject: [PATCH 15/35] refactor op token creation --- .../param_parser/twicpics_v2/number_parser.ex | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 22b1a30..b0d1246 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -1,6 +1,8 @@ defmodule NumberParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils + @op_tokens ~c"+-*/" + def parse(input, pos_offset \\ 0) do case do_parse(input, [], 0, pos_offset) do {:ok, tokens} -> @@ -90,17 +92,8 @@ defmodule NumberParser do ) when paren_count > 0 do cond do - char == ?+ -> - do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) + char in @op_tokens -> + do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) char == ?) -> do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) @@ -161,17 +154,8 @@ defmodule NumberParser do pos + 1 ) - char == ?+ -> - do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) + char in @op_tokens -> + do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) char == ?) -> do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) @@ -243,17 +227,8 @@ defmodule NumberParser do pos + 1 ) - char == ?+ -> - do_parse(rest, [{:op, "+", pos} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:op, "-", pos} | acc], paren_count, pos + 1) - - char == ?* -> - do_parse(rest, [{:op, "*", pos} | acc], paren_count, pos + 1) - - char == ?/ -> - do_parse(rest, [{:op, "/", pos} | acc], paren_count, pos + 1) + char in @op_tokens -> + do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) char == ?) -> do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) From f238bae63ef3ef5527aaf4f29eb64a13eae3a3fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 20:19:59 +0100 Subject: [PATCH 16/35] extract unexpected_char error to function --- .../param_parser/twicpics_v2/number_parser.ex | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index b0d1246..a68f0b3 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -52,7 +52,7 @@ defmodule NumberParser do do_parse(rest, [{:left_paren, pos}], paren_count + 1, pos + 1) true -> - {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} + unexpected_char_error(pos, ["(", "[0-9]"], <>) end end @@ -69,7 +69,7 @@ defmodule NumberParser do do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) true -> - {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: <>}} + unexpected_char_error(pos, ["(", "[0-9]"], <>) end end @@ -81,7 +81,7 @@ defmodule NumberParser do pos ) when paren_count == 0 do - {:error, {:unexpected_char, pos: pos, expected: [:eoi], found: <>}} + unexpected_char_error(pos, [:eoi], <>) end defp do_parse( @@ -99,8 +99,7 @@ defmodule NumberParser do do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> - {:error, - {:unexpected_char, pos: pos, expected: ["+", "-", "*", "/", ")"], found: <>}} + unexpected_char_error(pos, ["+", "-", "*", "/", ")"], <>) end end @@ -126,7 +125,7 @@ defmodule NumberParser do do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b} | acc_tail], paren_count, pos + 1) true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "."], found: <>}} + unexpected_char_error(pos, ["[0-9]", "."], <>) end end @@ -161,9 +160,7 @@ defmodule NumberParser do do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> - {:error, - {:unexpected_char, - pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} + unexpected_char_error(pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -184,7 +181,7 @@ defmodule NumberParser do ) true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} + unexpected_char_error(pos, ["[0-9]"], <>) end end @@ -207,7 +204,7 @@ defmodule NumberParser do ) true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: <>}} + unexpected_char_error(pos, ["[0-9]"], <>) end end @@ -234,9 +231,7 @@ defmodule NumberParser do do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) true -> - {:error, - {:unexpected_char, - pos: pos, expected: ["[0-9]", ".", "+", "-", "*", "/", ")"], found: <>}} + unexpected_char_error(pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -259,10 +254,14 @@ defmodule NumberParser do do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) true -> - {:error, {:unexpected_char, pos: pos, expected: ["[0-9]", "("], found: <>}} + unexpected_char_error(pos, ["[0-9]", "("], <>) end end + defp unexpected_char_error(pos, expected, found) do + {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} + end + def pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} def pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} def pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} From 70ae6f86d4e7ed3ebe6eee02e0b604c436615e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 21:19:29 +0100 Subject: [PATCH 17/35] use state struct instead of args --- .../param_parser/twicpics_v2/number_parser.ex | 299 ++++++++++-------- 1 file changed, 173 insertions(+), 126 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index a68f0b3..746cbd9 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -3,8 +3,27 @@ defmodule NumberParser do @op_tokens ~c"+-*/" + defmodule State do + defstruct input: "", tokens: [], pos: 0, paren_count: 0 + end + + defp add_token(%State{tokens: tokens} = state, token), + do: %State{state | tokens: [token | tokens]} + + defp replace_token(%State{tokens: [_head | tail]} = state, token), + do: %State{state | tokens: [token | tail]} + + defp inc_paren_count(%State{paren_count: paren_count} = state), + do: %State{state | paren_count: paren_count + 1} + + defp dec_paren_count(%State{paren_count: paren_count} = state), + do: %State{state | paren_count: paren_count - 1} + + defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), + do: %State{state | input: rest, pos: pos + 1} + def parse(input, pos_offset \\ 0) do - case do_parse(input, [], 0, pos_offset) do + case do_parse(%State{input: input, pos: pos_offset}) do {:ok, tokens} -> {:ok, tokens @@ -21,240 +40,268 @@ defmodule NumberParser do end # end of input - defp do_parse("", [], paren_count, pos) when paren_count == 0, - do: {:error, {:unexpected_char, pos: pos, expected: ["(", "[0-9]"], found: :eoi}} + defp do_parse(%State{input: "", tokens: []} = state) when state.paren_count == 0, + do: unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) - defp do_parse("", [{:float_open, _, _} | _], _paren_count, pos), - do: {:error, {:unexpected_char, pos: pos, expected: ["[0-9]"], found: :eoi}} + defp do_parse(%State{input: "", tokens: [{:float_open, _, _} | _]} = state), + do: unexpected_char_error(state.pos, ["[0-9]"], found: :eoi) - defp do_parse("", acc, paren_count, pos) when paren_count > 0, - do: {:error, {:unexpected_char, pos: pos, expected: [")"], found: :eoi}} + defp do_parse(%State{input: ""} = state) when state.paren_count > 0, + do: unexpected_char_error(state.pos, [")"], found: :eoi) - defp do_parse("", [{:int, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), - do: {:ok, acc} + defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _] = tokens}), + do: {:ok, tokens} - defp do_parse("", [{:float, _value, _t_pos_s, _t_pos_e} | _] = acc, _paren_count, _pos), - do: {:ok, acc} + defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _] = tokens}), + do: {:ok, tokens} - defp do_parse("", [{:right_paren, _t_pos} | _] = acc, _paren_count, _pos), do: {:ok, acc} + defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _] = tokens}), + do: {:ok, tokens} # first char in string - defp do_parse(<>, [], paren_count, pos) do + defp do_parse(%State{input: <>, tokens: []} = state) do cond do - char in ?0..?9 -> - do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:int, <>, pos, pos}], paren_count, pos + 1) + char in ?0..?9 or char == ?- -> + state + |> add_token({:int, <>, state.pos, state.pos}) + |> consume_char() + |> do_parse() # the only way to enter paren_count > 0 is through the first char char == ?( -> - do_parse(rest, [{:left_paren, pos}], paren_count + 1, pos + 1) + state + |> add_token({:left_paren, state.pos}) + |> inc_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["(", "[0-9]"], <>) + unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end # prev token: :left_paren - defp do_parse(<>, [{:left_paren, _t_pos} | _] = acc, paren_count, pos) do + defp do_parse( + %State{ + input: <>, + tokens: [{:left_paren, _} | _] + } = state + ) do cond do - char in ?0..?9 -> - do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char in ?0..?9 or char == ?- -> + state + |> add_token({:int, <>, state.pos, state.pos}) + |> consume_char() + |> do_parse() char == ?( -> - do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) + state + |> add_token({:left_paren, state.pos}) + |> inc_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["(", "[0-9]"], <>) + unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end # prev token: :right_paren defp do_parse( - <>, - [{:right_paren, _t_pos} | _] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:right_paren, _} | _] + } = state ) - when paren_count == 0 do - unexpected_char_error(pos, [:eoi], <>) - end + when state.paren_count == 0, + do: unexpected_char_error(state.pos, [:eoi], <>) defp do_parse( - <>, - [{:right_paren, _t_pos} | _] = acc, - paren_count, - pos - ) - when paren_count > 0 do + %State{ + input: <>, + tokens: [{:right_paren, _} | _] + } = state + ) do cond do char in @op_tokens -> - do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) + state + |> add_token({:op, <>, state.pos}) + |> consume_char() + |> do_parse() char == ?) -> - do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) + state + |> add_token({:right_paren, state.pos}) + |> dec_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["+", "-", "*", "/", ")"], <>) + unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) end end # prev token: {:int, n} defp do_parse( - <>, - [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:int, cur_val, t_pos_b, _} | _] + } = state ) - when paren_count == 0 do + when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse( - rest, - [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:int, cur_val <> <>, t_pos_b, state.pos}) + |> consume_char() + |> do_parse() char == ?. -> - do_parse(rest, [{:float_open, cur_val <> ".", t_pos_b} | acc_tail], paren_count, pos + 1) + state + |> replace_token({:float_open, cur_val <> <>, t_pos_b}) + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]", "."], <>) + unexpected_char_error(state.pos, ["[0-9]", "."], <>) end end defp do_parse( - <>, - [{:int, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:int, cur_val, t_pos_b, _} | _] + } = state ) - when paren_count > 0 do + when state.paren_count > 0 do cond do char in ?0..?9 -> - do_parse( - rest, - [{:int, cur_val <> <>, t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:int, cur_val <> <>, t_pos_b, state.pos}) + |> consume_char() + |> do_parse() char == ?. -> - do_parse( - rest, - [{:float_open, cur_val <> ".", t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:float_open, cur_val <> <>, t_pos_b}) + |> consume_char() + |> do_parse() char in @op_tokens -> - do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) + state + |> add_token({:op, <>, state.pos}) + |> consume_char() + |> do_parse() char == ?) -> - do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) + state + |> add_token({:right_paren, state.pos}) + |> dec_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end # prev token: {:float_open, n} - which means that it's not a valid float yet defp do_parse( - <>, - [{:float_open, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:float_open, cur_val, t_pos_b, _} | _] + } = state ) do cond do char in ?0..?9 -> - do_parse( - rest, - [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]"], <>) + unexpected_char_error(state.pos, ["[0-9]"], <>) end end # prev token: {:float, n} - at this point it's a valid float defp do_parse( - <>, - [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:float, cur_val, t_pos_b, _} | _] + } = state ) - when paren_count == 0 do + when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - do_parse( - rest, - [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]"], <>) + unexpected_char_error(state.pos, ["[0-9]"], <>) end end defp do_parse( - <>, - [{:float, cur_val, t_pos_b, _t_pos_e} | acc_tail] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:float, cur_val, t_pos_b, _} | _] + } = state ) - when paren_count > 0 do + when state.paren_count > 0 do cond do char in ?0..?9 -> - do_parse( - rest, - [{:float, cur_val <> <>, t_pos_b, pos} | acc_tail], - paren_count, - pos + 1 - ) + state + |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) + |> consume_char() + |> do_parse() char in @op_tokens -> - do_parse(rest, [{:op, <>, pos} | acc], paren_count, pos + 1) + state + |> add_token({:op, <>, state.pos}) + |> consume_char() + |> do_parse() char == ?) -> - do_parse(rest, [{:right_paren, pos} | acc], paren_count - 1, pos + 1) + state + |> add_token({:right_paren, state.pos}) + |> dec_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end # prev token: {:op, v} defp do_parse( - <>, - [{:op, _optype, _t_pos} | _] = acc, - paren_count, - pos + %State{ + input: <>, + tokens: [{:op, _, _} | _] + } = state ) - when paren_count > 0 do + when state.paren_count > 0 do cond do - char in ?0..?9 -> - do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) - - char == ?- -> - do_parse(rest, [{:int, <>, pos, pos} | acc], paren_count, pos + 1) + char in ?0..?9 or char == ?- -> + state + |> add_token({:int, <>, state.pos, state.pos}) + |> consume_char() + |> do_parse() char == ?( -> - do_parse(rest, [{:left_paren, pos} | acc], paren_count + 1, pos + 1) + state + |> add_token({:left_paren, state.pos}) + |> inc_paren_count() + |> consume_char() + |> do_parse() true -> - unexpected_char_error(pos, ["[0-9]", "("], <>) + unexpected_char_error(state.pos, ["[0-9]", "("], <>) end end From 31eb795fbd709ba6cde985c9a82e64b9d136ad6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 22:04:23 +0100 Subject: [PATCH 18/35] more refactoring --- .../param_parser/twicpics_v2/number_parser.ex | 186 ++++-------------- 1 file changed, 42 insertions(+), 144 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 746cbd9..db0570e 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -7,12 +7,6 @@ defmodule NumberParser do defstruct input: "", tokens: [], pos: 0, paren_count: 0 end - defp add_token(%State{tokens: tokens} = state, token), - do: %State{state | tokens: [token | tokens]} - - defp replace_token(%State{tokens: [_head | tail]} = state, token), - do: %State{state | tokens: [token | tail]} - defp inc_paren_count(%State{paren_count: paren_count} = state), do: %State{state | paren_count: paren_count + 1} @@ -22,6 +16,20 @@ defmodule NumberParser do defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), do: %State{state | input: rest, pos: pos + 1} + defp add_token(%State{tokens: tokens} = state, token), + do: %State{state | tokens: [token | tokens]} |> consume_char() |> do_parse() + + defp replace_token(%State{tokens: [_head | tail]} = state, token), + do: %State{state | tokens: [token | tail]} |> consume_char() |> do_parse() + + defp add_left_paren(%State{} = state) do + state |> inc_paren_count() |> add_token({:left_paren, state.pos}) + end + + defp add_right_paren(%State{} = state) do + state |> dec_paren_count() |> add_token({:right_paren, state.pos}) + end + def parse(input, pos_offset \\ 0) do case do_parse(%State{input: input, pos: pos_offset}) do {:ok, tokens} -> @@ -61,22 +69,10 @@ defmodule NumberParser do # first char in string defp do_parse(%State{input: <>, tokens: []} = state) do cond do - char in ?0..?9 or char == ?- -> - state - |> add_token({:int, <>, state.pos, state.pos}) - |> consume_char() - |> do_parse() - + char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) # the only way to enter paren_count > 0 is through the first char - char == ?( -> - state - |> add_token({:left_paren, state.pos}) - |> inc_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + char == ?( -> add_left_paren(state) + true -> unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end @@ -88,21 +84,9 @@ defmodule NumberParser do } = state ) do cond do - char in ?0..?9 or char == ?- -> - state - |> add_token({:int, <>, state.pos, state.pos}) - |> consume_char() - |> do_parse() - - char == ?( -> - state - |> add_token({:left_paren, state.pos}) - |> inc_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) + char == ?( -> add_left_paren(state) + true -> unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end @@ -123,21 +107,9 @@ defmodule NumberParser do } = state ) do cond do - char in @op_tokens -> - state - |> add_token({:op, <>, state.pos}) - |> consume_char() - |> do_parse() - - char == ?) -> - state - |> add_token({:right_paren, state.pos}) - |> dec_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) + char in @op_tokens -> add_token(state, {:op, <>, state.pos}) + char == ?) -> add_right_paren(state) + true -> unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) end end @@ -151,20 +123,9 @@ defmodule NumberParser do when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> - state - |> replace_token({:int, cur_val <> <>, t_pos_b, state.pos}) - |> consume_char() - |> do_parse() - - char == ?. -> - state - |> replace_token({:float_open, cur_val <> <>, t_pos_b}) - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]", "."], <>) + char in ?0..?9 -> replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + char == ?. -> replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + true -> unexpected_char_error(state.pos, ["[0-9]", "."], <>) end end @@ -176,33 +137,11 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 -> - state - |> replace_token({:int, cur_val <> <>, t_pos_b, state.pos}) - |> consume_char() - |> do_parse() - - char == ?. -> - state - |> replace_token({:float_open, cur_val <> <>, t_pos_b}) - |> consume_char() - |> do_parse() - - char in @op_tokens -> - state - |> add_token({:op, <>, state.pos}) - |> consume_char() - |> do_parse() - - char == ?) -> - state - |> add_token({:right_paren, state.pos}) - |> dec_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + char in ?0..?9 -> replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + char == ?. -> replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + char in @op_tokens -> add_token(state, {:op, <>, state.pos}) + char == ?) -> add_right_paren(state) + true -> unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -214,14 +153,8 @@ defmodule NumberParser do } = state ) do cond do - char in ?0..?9 -> - state - |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]"], <>) + char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + true -> unexpected_char_error(state.pos, ["[0-9]"], <>) end end @@ -235,14 +168,8 @@ defmodule NumberParser do when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> - state - |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]"], <>) + char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + true -> unexpected_char_error(state.pos, ["[0-9]"], <>) end end @@ -254,27 +181,10 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 -> - state - |> replace_token({:float, cur_val <> <>, t_pos_b, state.pos}) - |> consume_char() - |> do_parse() - - char in @op_tokens -> - state - |> add_token({:op, <>, state.pos}) - |> consume_char() - |> do_parse() - - char == ?) -> - state - |> add_token({:right_paren, state.pos}) - |> dec_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + char in @op_tokens -> add_token(state, {:op, <>, state.pos}) + char == ?) -> add_right_paren(state) + true -> unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -287,21 +197,9 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 or char == ?- -> - state - |> add_token({:int, <>, state.pos, state.pos}) - |> consume_char() - |> do_parse() - - char == ?( -> - state - |> add_token({:left_paren, state.pos}) - |> inc_paren_count() - |> consume_char() - |> do_parse() - - true -> - unexpected_char_error(state.pos, ["[0-9]", "("], <>) + char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) + char == ?( -> add_left_paren(state) + true -> unexpected_char_error(state.pos, ["[0-9]", "("], <>) end end From ce7e1dd05f0f549957e561b52d573d6255f645d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 22:05:47 +0100 Subject: [PATCH 19/35] more refactoring --- .../param_parser/twicpics_v2/number_parser.ex | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index db0570e..97fb684 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -7,12 +7,6 @@ defmodule NumberParser do defstruct input: "", tokens: [], pos: 0, paren_count: 0 end - defp inc_paren_count(%State{paren_count: paren_count} = state), - do: %State{state | paren_count: paren_count + 1} - - defp dec_paren_count(%State{paren_count: paren_count} = state), - do: %State{state | paren_count: paren_count - 1} - defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), do: %State{state | input: rest, pos: pos + 1} @@ -23,11 +17,13 @@ defmodule NumberParser do do: %State{state | tokens: [token | tail]} |> consume_char() |> do_parse() defp add_left_paren(%State{} = state) do - state |> inc_paren_count() |> add_token({:left_paren, state.pos}) + %State{state | paren_count: state.paren_count + 1} + |> add_token({:left_paren, state.pos}) end defp add_right_paren(%State{} = state) do - state |> dec_paren_count() |> add_token({:right_paren, state.pos}) + %State{state | paren_count: state.paren_count - 1} + |> add_token({:right_paren, state.pos}) end def parse(input, pos_offset \\ 0) do From 00c894812ff7be86698cc87f8293f31b00e2c4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 22:06:28 +0100 Subject: [PATCH 20/35] mix format --- .../param_parser/twicpics_v2/number_parser.ex | 92 ++++++++++++++----- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 97fb684..1bfa7d0 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -65,10 +65,15 @@ defmodule NumberParser do # first char in string defp do_parse(%State{input: <>, tokens: []} = state) do cond do - char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) + char in ?0..?9 or char == ?- -> + add_token(state, {:int, <>, state.pos, state.pos}) + # the only way to enter paren_count > 0 is through the first char - char == ?( -> add_left_paren(state) - true -> unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + char == ?( -> + add_left_paren(state) + + true -> + unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end @@ -80,9 +85,14 @@ defmodule NumberParser do } = state ) do cond do - char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) - char == ?( -> add_left_paren(state) - true -> unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + char in ?0..?9 or char == ?- -> + add_token(state, {:int, <>, state.pos, state.pos}) + + char == ?( -> + add_left_paren(state) + + true -> + unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end @@ -119,9 +129,14 @@ defmodule NumberParser do when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) - char == ?. -> replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) - true -> unexpected_char_error(state.pos, ["[0-9]", "."], <>) + char in ?0..?9 -> + replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + + char == ?. -> + replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + + true -> + unexpected_char_error(state.pos, ["[0-9]", "."], <>) end end @@ -133,11 +148,20 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 -> replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) - char == ?. -> replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) - char in @op_tokens -> add_token(state, {:op, <>, state.pos}) - char == ?) -> add_right_paren(state) - true -> unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + char in ?0..?9 -> + replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + + char == ?. -> + replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + + char in @op_tokens -> + add_token(state, {:op, <>, state.pos}) + + char == ?) -> + add_right_paren(state) + + true -> + unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -149,8 +173,11 @@ defmodule NumberParser do } = state ) do cond do - char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) - true -> unexpected_char_error(state.pos, ["[0-9]"], <>) + char in ?0..?9 -> + replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + + true -> + unexpected_char_error(state.pos, ["[0-9]"], <>) end end @@ -164,8 +191,11 @@ defmodule NumberParser do when state.paren_count == 0 do # not in parens, so it's only a number literal, and no ops are allowed cond do - char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) - true -> unexpected_char_error(state.pos, ["[0-9]"], <>) + char in ?0..?9 -> + replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + + true -> + unexpected_char_error(state.pos, ["[0-9]"], <>) end end @@ -177,10 +207,17 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 -> replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) - char in @op_tokens -> add_token(state, {:op, <>, state.pos}) - char == ?) -> add_right_paren(state) - true -> unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + char in ?0..?9 -> + replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + + char in @op_tokens -> + add_token(state, {:op, <>, state.pos}) + + char == ?) -> + add_right_paren(state) + + true -> + unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) end end @@ -193,9 +230,14 @@ defmodule NumberParser do ) when state.paren_count > 0 do cond do - char in ?0..?9 or char == ?- -> add_token(state, {:int, <>, state.pos, state.pos}) - char == ?( -> add_left_paren(state) - true -> unexpected_char_error(state.pos, ["[0-9]", "("], <>) + char in ?0..?9 or char == ?- -> + add_token(state, {:int, <>, state.pos, state.pos}) + + char == ?( -> + add_left_paren(state) + + true -> + unexpected_char_error(state.pos, ["[0-9]", "("], <>) end end From 1c3ba43bc8abb881c960ef1c3d202dbdeca117ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 22:10:46 +0100 Subject: [PATCH 21/35] fix float parsing --- lib/image_plug/param_parser/twicpics_v2/number_parser.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 1bfa7d0..8f58913 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -47,7 +47,7 @@ defmodule NumberParser do defp do_parse(%State{input: "", tokens: []} = state) when state.paren_count == 0, do: unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) - defp do_parse(%State{input: "", tokens: [{:float_open, _, _} | _]} = state), + defp do_parse(%State{input: "", tokens: [{:float_open, _, _, _} | _]} = state), do: unexpected_char_error(state.pos, ["[0-9]"], found: :eoi) defp do_parse(%State{input: ""} = state) when state.paren_count > 0, @@ -133,7 +133,7 @@ defmodule NumberParser do replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) char == ?. -> - replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + replace_token(state, {:float_open, cur_val <> <>, t_pos_b, state.pos}) true -> unexpected_char_error(state.pos, ["[0-9]", "."], <>) @@ -152,7 +152,7 @@ defmodule NumberParser do replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) char == ?. -> - replace_token(state, {:float_open, cur_val <> <>, t_pos_b}) + replace_token(state, {:float_open, cur_val <> <>, t_pos_b, state.pos}) char in @op_tokens -> add_token(state, {:op, <>, state.pos}) From e78bbaa861d4a89860b636ab74b032805ac43549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 23:14:07 +0100 Subject: [PATCH 22/35] parser fixes + tests --- .../param_parser/twicpics_v2/number_parser.ex | 161 ++++++++---- .../twicpics_v2/number_parser_test.exs | 241 +++++++++++++++--- 2 files changed, 326 insertions(+), 76 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 8f58913..dc2ddc4 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -7,6 +7,17 @@ defmodule NumberParser do defstruct input: "", tokens: [], pos: 0, paren_count: 0 end + defp unexpected_char_error(pos, expected, found) do + {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} + end + + def pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def pos({:left_paren, pos}), do: {pos, pos} + def pos({:right_paren, pos}), do: {pos, pos} + def pos({:op, _optype, pos}), do: {pos, pos} + defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), do: %State{state | input: rest, pos: pos + 1} @@ -26,6 +37,18 @@ defmodule NumberParser do |> add_token({:right_paren, state.pos}) end + defp mk_int(value, left_pos, right_pos), + do: {:int, value, left_pos, right_pos} + + defp mk_float_open(value, left_pos, right_pos), + do: {:float_open, value, left_pos, right_pos} + + defp mk_float(value, left_pos, right_pos), + do: {:float, value, left_pos, right_pos} + + defp mk_op(type, pos), + do: {:op, type, pos} + def parse(input, pos_offset \\ 0) do case do_parse(%State{input: input, pos: pos_offset}) do {:ok, tokens} -> @@ -33,8 +56,8 @@ defmodule NumberParser do tokens |> Enum.reverse() |> Enum.map(fn - {:int, int, pos_b, pos_e} -> {:int, String.to_integer(int), pos_b, pos_e} - {:float, int, pos_b, pos_e} -> {:float, String.to_float(int), pos_b, pos_e} + {:int, int, pos_b, pos_e} -> mk_int(String.to_integer(int), pos_b, pos_e) + {:float, int, pos_b, pos_e} -> mk_float(String.to_float(int), pos_b, pos_e) other -> other end)} @@ -43,30 +66,36 @@ defmodule NumberParser do end end - # end of input + # we hit end of input, but no tokens have been processed defp do_parse(%State{input: "", tokens: []} = state) when state.paren_count == 0, do: unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) - defp do_parse(%State{input: "", tokens: [{:float_open, _, _, _} | _]} = state), - do: unexpected_char_error(state.pos, ["[0-9]"], found: :eoi) - - defp do_parse(%State{input: ""} = state) when state.paren_count > 0, - do: unexpected_char_error(state.pos, [")"], found: :eoi) + # just consume space characters + defp do_parse(%State{input: <>} = state) when char in ~c[ ] do + state |> consume_char() |> do_parse() + end - defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _] = tokens}), - do: {:ok, tokens} + # + # the following states are legal end of input locations as long as + # we're not inside a parentheses: :int, :float and :right_paren + # + defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _] = tokens} = state) + when state.paren_count == 0, + do: {:ok, tokens} - defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _] = tokens}), - do: {:ok, tokens} + defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _] = tokens} = state) + when state.paren_count == 0, + do: {:ok, tokens} - defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _] = tokens}), - do: {:ok, tokens} + defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _] = tokens} = state) + when state.paren_count == 0, + do: {:ok, tokens} # first char in string defp do_parse(%State{input: <>, tokens: []} = state) do cond do char in ?0..?9 or char == ?- -> - add_token(state, {:int, <>, state.pos, state.pos}) + add_token(state, mk_int(<>, state.pos, state.pos)) # the only way to enter paren_count > 0 is through the first char char == ?( -> @@ -77,7 +106,10 @@ defmodule NumberParser do end end + # # prev token: :left_paren + # + defp do_parse( %State{ input: <>, @@ -86,17 +118,27 @@ defmodule NumberParser do ) do cond do char in ?0..?9 or char == ?- -> - add_token(state, {:int, <>, state.pos, state.pos}) + add_token(state, mk_int(<>, state.pos, state.pos)) char == ?( -> add_left_paren(state) true -> - unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + unexpected_char_error(state.pos, ["(", "[0-9]", "-"], <>) end end + # we hit end of input while the previous token was a :left_paren + defp do_parse(%State{input: "", tokens: [{:left_paren, _} | _]} = state) do + unexpected_char_error(state.pos, ["(", "[0-9]", "-"], :eoi) + end + + # # prev token: :right_paren + # + + # if last :right_paren has been closed, the expression is completed, + # so no more characters are allowed defp do_parse( %State{ input: <>, @@ -111,15 +153,25 @@ defmodule NumberParser do input: <>, tokens: [{:right_paren, _} | _] } = state - ) do + ) + when state.paren_count > 0 do cond do - char in @op_tokens -> add_token(state, {:op, <>, state.pos}) + char in @op_tokens -> add_token(state, mk_op(<>, state.pos)) char == ?) -> add_right_paren(state) true -> unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) end end - # prev token: {:int, n} + # we hit end of input while the previous token was a :right_paren, but we're still inside a paren + defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _]} = state) + when state.paren_count > 0 do + unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], :eoi) + end + + # + # prev token: integer + # + defp do_parse( %State{ input: <>, @@ -130,10 +182,10 @@ defmodule NumberParser do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_int(cur_val <> <>, t_pos_b, state.pos)) char == ?. -> - replace_token(state, {:float_open, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) true -> unexpected_char_error(state.pos, ["[0-9]", "."], <>) @@ -149,13 +201,13 @@ defmodule NumberParser do when state.paren_count > 0 do cond do char in ?0..?9 -> - replace_token(state, {:int, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_int(cur_val <> <>, t_pos_b, state.pos)) char == ?. -> - replace_token(state, {:float_open, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) char in @op_tokens -> - add_token(state, {:op, <>, state.pos}) + add_token(state, mk_op(<>, state.pos)) char == ?) -> add_right_paren(state) @@ -165,7 +217,17 @@ defmodule NumberParser do end end - # prev token: {:float_open, n} - which means that it's not a valid float yet + # we hit eoi while on an :int token, and we're in a parentheses + defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _]} = state) + when state.paren_count > 0 do + unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], :eoi) + end + + # + # prev token: :float_open + # - it's not a valid float yet + # + defp do_parse( %State{ input: <>, @@ -174,14 +236,22 @@ defmodule NumberParser do ) do cond do char in ?0..?9 -> - replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> unexpected_char_error(state.pos, ["[0-9]"], <>) end end - # prev token: {:float, n} - at this point it's a valid float + # we hit end of input while in a :float_open + defp do_parse(%State{input: "", tokens: [{:float_open, _, _, _} | _]} = state), + do: unexpected_char_error(state.pos, ["[0-9]"], :eoi) + + # + # prev token: :float + # - at this point it's a valid float + # + defp do_parse( %State{ input: <>, @@ -192,7 +262,7 @@ defmodule NumberParser do # not in parens, so it's only a number literal, and no ops are allowed cond do char in ?0..?9 -> - replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> unexpected_char_error(state.pos, ["[0-9]"], <>) @@ -208,20 +278,29 @@ defmodule NumberParser do when state.paren_count > 0 do cond do char in ?0..?9 -> - replace_token(state, {:float, cur_val <> <>, t_pos_b, state.pos}) + replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) char in @op_tokens -> - add_token(state, {:op, <>, state.pos}) + add_token(state, mk_op(<>, state.pos)) char == ?) -> add_right_paren(state) true -> - unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], <>) end end - # prev token: {:op, v} + # we hit eoi while on an :int token, and we're in a parentheses + defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _]} = state) + when state.paren_count > 0 do + unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) + end + + # + # prev token: :op + # + defp do_parse( %State{ input: <>, @@ -231,24 +310,18 @@ defmodule NumberParser do when state.paren_count > 0 do cond do char in ?0..?9 or char == ?- -> - add_token(state, {:int, <>, state.pos, state.pos}) + add_token(state, mk_int(<>, state.pos, state.pos)) char == ?( -> add_left_paren(state) true -> - unexpected_char_error(state.pos, ["[0-9]", "("], <>) + unexpected_char_error(state.pos, ["[0-9]", "-", "("], <>) end end - defp unexpected_char_error(pos, expected, found) do - {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} + # we hit eoi while on an :op token + defp do_parse(%State{input: "", tokens: [{:op, _, _} | _]} = state) do + unexpected_char_error(state.pos, ["[0-9]", "-", "("], :eoi) end - - def pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:left_paren, pos}), do: {pos, pos} - def pos({:right_paren, pos}), do: {pos, pos} - def pos({:op, _optype, pos}), do: {pos, pos} end diff --git a/test/param_parser/twicpics_v2/number_parser_test.exs b/test/param_parser/twicpics_v2/number_parser_test.exs index 913c3ec..2efb030 100644 --- a/test/param_parser/twicpics_v2/number_parser_test.exs +++ b/test/param_parser/twicpics_v2/number_parser_test.exs @@ -2,37 +2,214 @@ defmodule ImagePlug.Twicpics.NumberParserTest do use ExUnit.Case, async: true use ExUnitProperties - test "successful parse output returns correct key positions" do - assert NumberParser.parse("10") == {:ok, [{:int, 10, 0, 1}]} - - assert NumberParser.parse("(10+10)") == - {:ok, - [ - {:left_paren, 0}, - {:int, 10, 1, 2}, - {:op, "+", 3}, - {:int, 10, 4, 5}, - {:right_paren, 6} - ]} - - assert NumberParser.parse("(10/20*(4+5)+5*-1)") == - {:ok, - [ - {:left_paren, 0}, - {:int, 10, 1, 2}, - {:op, "/", 3}, - {:int, 20, 4, 5}, - {:op, "*", 6}, - {:left_paren, 7}, - {:int, 4, 8, 8}, - {:op, "+", 9}, - {:int, 5, 10, 10}, - {:right_paren, 11}, - {:op, "+", 12}, - {:int, 5, 13, 13}, - {:op, "*", 14}, - {:int, -1, 15, 16}, - {:right_paren, 17} - ]} + describe "successful parsing" do + test "parses a single integer" do + input = "123" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [{:int, 123, 0, 2}] + end + + test "parses a single negative integer" do + input = "-123" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [{:int, -123, 0, 3}] + end + + test "parses a single floating-point number" do + input = "123.45" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [{:float, 123.45, 0, 5}] + end + + test "parses a single negative floating-point number" do + input = "-123.45" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [{:float, -123.45, 0, 6}] + end + + test "parses a simple addition expression with parentheses" do + input = "(123+456)" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [ + {:left_paren, 0}, + {:int, 123, 1, 3}, + {:op, "+", 4}, + {:int, 456, 5, 7}, + {:right_paren, 8} + ] + end + + test "parses a complex nested expression" do + input = "((123+456)*789)" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [ + {:left_paren, 0}, + {:left_paren, 1}, + {:int, 123, 2, 4}, + {:op, "+", 5}, + {:int, 456, 6, 8}, + {:right_paren, 9}, + {:op, "*", 10}, + {:int, 789, 11, 13}, + {:right_paren, 14} + ] + end + + test "parses an expression with mixed operators" do + input = "(123+456*789)" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [ + {:left_paren, 0}, + {:int, 123, 1, 3}, + {:op, "+", 4}, + {:int, 456, 5, 7}, + {:op, "*", 8}, + {:int, 789, 9, 11}, + {:right_paren, 12} + ] + end + + test "parses an expression with multiple levels of nesting" do + input = "(((123+456)-789)/2)" + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [ + {:left_paren, 0}, + {:left_paren, 1}, + {:left_paren, 2}, + {:int, 123, 3, 5}, + {:op, "+", 6}, + {:int, 456, 7, 9}, + {:right_paren, 10}, + {:op, "-", 11}, + {:int, 789, 12, 14}, + {:right_paren, 15}, + {:op, "/", 16}, + {:int, 2, 17, 17}, + {:right_paren, 18} + ] + end + + test "parses a complex statement with all operators" do + assert NumberParser.parse("(10/20*(4+5)-5*-1)") == + {:ok, + [ + {:left_paren, 0}, + {:int, 10, 1, 2}, + {:op, "/", 3}, + {:int, 20, 4, 5}, + {:op, "*", 6}, + {:left_paren, 7}, + {:int, 4, 8, 8}, + {:op, "+", 9}, + {:int, 5, 10, 10}, + {:right_paren, 11}, + {:op, "-", 12}, + {:int, 5, 13, 13}, + {:op, "*", 14}, + {:int, -1, 15, 16}, + {:right_paren, 17} + ]} + end + + test "parses an expression with whitespace" do + input = " ( 123 + 456 * ( 789 - 10 ) ) " + {:ok, tokens} = NumberParser.parse(input) + + assert tokens == [ + {:left_paren, 1}, + {:int, 123, 5, 7}, + {:op, "+", 9}, + {:int, 456, 11, 13}, + {:op, "*", 15}, + {:left_paren, 17}, + {:int, 789, 19, 21}, + {:op, "-", 23}, + {:int, 10, 28, 29}, + {:right_paren, 31}, + {:right_paren, 36} + ] + end + end + + describe "unexpected_char_error/3" do + test "invalid character at the start of input" do + input = "x123" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 0 + assert Keyword.get(opts, :expected) == ["(", "[0-9]"] + assert Keyword.get(opts, :found) == "x" + end + + test "invalid character after a valid integer" do + input = "123x" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 3 + assert Keyword.get(opts, :expected) == ["[0-9]", "."] + assert Keyword.get(opts, :found) == "x" + end + + test "invalid character after a valid float" do + input = "12.3x" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 4 + assert Keyword.get(opts, :expected) == ["[0-9]"] + assert Keyword.get(opts, :found) == "x" + end + + test "mismatched parentheses" do + input = "(123" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 4 + assert Keyword.get(opts, :expected) == ["[0-9]", ".", "+", "-", "*", "/", ")"] + assert Keyword.get(opts, :found) == :eoi + end + + test "operators outside parentheses" do + input = "123+456" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 3 + assert Keyword.get(opts, :expected) == ["[0-9]", "."] + assert Keyword.get(opts, :found) == "+" + end + + test "unexpected character after a valid expression" do + input = "(123+456)x" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 9 + assert Keyword.get(opts, :expected) == [:eoi] + assert Keyword.get(opts, :found) == "x" + end + + test "unclosed float at the end of input" do + input = "123." + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 4 + assert Keyword.get(opts, :expected) == ["[0-9]"] + assert Keyword.get(opts, :found) == :eoi + end + + test "unexpected end of input after opening parenthesis" do + input = "(" + {:error, {:unexpected_char, opts}} = NumberParser.parse(input) + + assert Keyword.get(opts, :pos) == 1 + assert Keyword.get(opts, :expected) == ["(", "[0-9]", "-"] + assert Keyword.get(opts, :found) == :eoi + end end end From bb320fe01f55d47dd6c9f4a40ca86e663111ffd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 23:37:10 +0100 Subject: [PATCH 23/35] wip refactor --- .../param_parser/twicpics_v2/arithmetic.ex | 10 +- .../twicpics_v2/coordinates_parser.ex | 31 +++++ .../param_parser/twicpics_v2/crop_parser.ex | 24 ++++ .../twicpics_v2/key_value_parser.ex | 4 +- .../param_parser/twicpics_v2/length_parser.ex | 122 +----------------- .../param_parser/twicpics_v2/number_parser.ex | 11 +- .../param_parser/twicpics_v2/size_parser.ex | 51 ++++++++ .../param_parser/twicpics_v2/utils.ex | 11 +- .../twicpics_v2/kv_parser_test.exs | 2 +- .../twicpics_v2/number_parser_test.exs | 4 +- test/param_parser/twicpics_v2/utils_test.exs | 2 +- 11 files changed, 135 insertions(+), 137 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics_v2/crop_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics_v2/size_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex index 68ae225..782a760 100644 --- a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex +++ b/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex @@ -1,4 +1,6 @@ -defmodule Arithmetic do +defmodule ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser do + alias ImagePlug.ParamParser.TwicpicsV2.Utils + @type token :: {:int, integer} | {:float, float} | {:op, binary} | :left_paren | :right_paren @type expr :: {:int, integer} | {:float, float} | {:op, binary, expr(), expr()} @@ -16,8 +18,8 @@ defmodule Arithmetic do {:ok, expr, []} -> {:ok, expr} - {:ok, expr, [token | _]} -> - {start_pos, _end_pos} = NumberParser.pos(token) + {:ok, _expr, [token | _]} -> + {start_pos, _end_pos} = Utils.token_pos(token) {:error, {:unexpected_token, pos: start_pos}} {:error, _} = error -> @@ -46,7 +48,7 @@ defmodule Arithmetic do end defp parse_primary([token | _]) do - {start_pos, _end_pos} = NumberParser.pos(token) + {start_pos, _end_pos} = Utils.token_pos(token) {:error, {:unexpected_token, pos: start_pos}} end diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex new file mode 100644 index 0000000..59e5b06 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex @@ -0,0 +1,31 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do + alias ImagePlug.ParamParser.TwicpicsV2.LengthParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, "x", parts: 2) do + [left_str, top_str] -> + with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), + {:ok, parsed_top} <- + parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do + {:ok, [left: parsed_left, top: parsed_top]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [""] -> + {:error, {:unexpected_eoi, pos: pos_offset}} + + _ -> + {:error, {:unexpected_token, pos: pos_offset}} + end + end + + defp parse_and_validate(length_str, offset) do + case LengthParser.parse(length_str, offset) do + {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} + {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} + {:error, _reason} = error -> error + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/crop_parser.ex b/lib/image_plug/param_parser/twicpics_v2/crop_parser.ex new file mode 100644 index 0000000..bfd417d --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/crop_parser.ex @@ -0,0 +1,24 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.CropParser do + alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser + alias ImagePlug.ParamParser.TwicpicsV2.SizeParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, "@", parts: 2) do + [size_str, coordinates_str] -> + with {:ok, parsed_size} <- SizeParser.parse(size_str, pos_offset), + {:ok, parsed_coordinates} <- + CoordinatesParser.parse(coordinates_str, pos_offset + String.length(size_str) + 1) do + {:ok, [crop: parsed_size, crop_from: parsed_coordinates]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [size_str] -> + case SizeParser.parse(size_str, pos_offset) do + {:ok, parsed_size} -> {:ok, [crop: parsed_size, crop_from: :focus]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex index ef264c8..c4c6afb 100644 --- a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex @@ -11,7 +11,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do defp parse_pairs("", acc, _pos), do: {:ok, acc} # pos + 1 because key is expected at the next char - defp parse_pairs("/", acc, pos), do: {:error, {:expected_key, pos: pos + 1}} + defp parse_pairs("/", _acc, pos), do: {:error, {:expected_key, pos: pos + 1}} defp parse_pairs(<<"/"::binary, input::binary>>, acc, pos), do: parse_pairs(input, acc, pos + 1) @@ -34,7 +34,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do defp extract_value(input, pos) do case extract_until_slash_or_end(input, "", pos) do - {"", rest, new_pos} -> {:error, {:expected_value, pos: pos}} + {"", _rest, new_pos} -> {:error, {:expected_value, pos: new_pos}} {value, rest, new_pos} -> {:ok, {value, rest, new_pos}} end end diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex index 3368d9b..c097d2a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/length_parser.ex @@ -1,6 +1,7 @@ -defmodule LengthParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.LengthParser do + alias ImagePlug.ParamParser.TwicpicsV2.NumberParser + alias ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser alias ImagePlug.ParamParser.TwicpicsV2.Utils - alias Arithmetic def parse(input, pos_offset \\ 0) do {type, num_str} = @@ -11,123 +12,10 @@ defmodule LengthParser do end with {:ok, tokens} <- NumberParser.parse(num_str, pos_offset), - {:ok, evaluated} <- Arithmetic.parse_and_evaluate(tokens) do + {:ok, evaluated} <- ArithmeticParser.parse_and_evaluate(tokens) do {:ok, {type, evaluated}} else - {:error, {reason, opts}} = error -> Utils.update_error_input(error, input) - end - end -end - -defmodule SizeParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils - - def parse(input, pos_offset \\ 0) do - case String.split(input, "x", parts: 2) do - ["-", "-"] -> - {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} - - ["-", height_str] -> - case parse_and_validate(height_str, pos_offset + 2) do - {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, "-"] -> - case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str, height_str] -> - with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), - {:ok, parsed_height} <- - parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do - {:ok, [width: parsed_width, height: parsed_height]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [width_str] -> - case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - end - end - - defp parse_and_validate(length_str, offset) do - case LengthParser.parse(length_str, offset) do - {:ok, {_type, number} = parsed_length} when number > 0 -> - {:ok, parsed_length} - - {:ok, {_type, number}} -> - {:error, {:strictly_positive_number_required, pos: offset, found: number}} - - {:error, _reason} = error -> - error - end - end -end - -defmodule CoordinatesParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils - - def parse(input, pos_offset \\ 0) do - case String.split(input, "x", parts: 2) do - [left_str, top_str] -> - with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), - {:ok, parsed_top} <- - parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do - {:ok, [left: parsed_left, top: parsed_top]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [""] -> - {:error, {:unexpected_eoi, pos: pos_offset}} - - _ -> - {:error, {:unexpected_token, pos: pos_offset}} - end - end - - defp parse_and_validate(length_str, offset) do - case LengthParser.parse(length_str, offset) do - {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} - {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} - {:error, _reason} = error -> error - end - end -end - -defmodule CropParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils - - def parse(input, pos_offset \\ 0) do - case String.split(input, "@", parts: 2) do - [size_str, coordinates_str] -> - with {:ok, parsed_size} <- SizeParser.parse(size_str, pos_offset), - {:ok, parsed_coordinates} <- - CoordinatesParser.parse(coordinates_str, pos_offset + String.length(size_str) + 1) do - {:ok, [crop: parsed_size, crop_from: parsed_coordinates]} - else - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - - [size_str] -> - case SizeParser.parse(size_str, pos_offset) do - {:ok, parsed_size} -> {:ok, [crop: parsed_size, crop_from: :focus]} - {:error, _reason} = error -> Utils.update_error_input(error, input) - end - end - end - - defp parse_and_validate(length_str, offset) do - case LengthParser.parse(length_str, offset) do - {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} - {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} - {:error, _reason} = error -> error + {:error, {_reason, _opts}} = error -> Utils.update_error_input(error, input) end end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index dc2ddc4..764b555 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -1,4 +1,4 @@ -defmodule NumberParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils @op_tokens ~c"+-*/" @@ -11,13 +11,6 @@ defmodule NumberParser do {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} end - def pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} - def pos({:left_paren, pos}), do: {pos, pos} - def pos({:right_paren, pos}), do: {pos, pos} - def pos({:op, _optype, pos}), do: {pos, pos} - defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), do: %State{state | input: rest, pos: pos + 1} @@ -61,7 +54,7 @@ defmodule NumberParser do other -> other end)} - {:error, {reason, opts}} = error -> + {:error, {_reason, _opts}} = error -> Utils.update_error_input(error, input) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/size_parser.ex b/lib/image_plug/param_parser/twicpics_v2/size_parser.ex new file mode 100644 index 0000000..c4c7cc3 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/size_parser.ex @@ -0,0 +1,51 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.SizeParser do + alias ImagePlug.ParamParser.TwicpicsV2.LengthParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, "x", parts: 2) do + ["-", "-"] -> + {:error, {:unexpected_char, pos: pos_offset + 2, expected: ["(", "[0-9]", found: "-"]}} + + ["-", height_str] -> + case parse_and_validate(height_str, pos_offset + 2) do + {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, "-"] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str, height_str] -> + with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), + {:ok, parsed_height} <- + parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do + {:ok, [width: parsed_width, height: parsed_height]} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str] -> + case parse_and_validate(width_str, pos_offset) do + {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + end + end + + defp parse_and_validate(length_str, offset) do + case LengthParser.parse(length_str, offset) do + {:ok, {_type, number} = parsed_length} when number > 0 -> + {:ok, parsed_length} + + {:ok, {_type, number}} -> + {:error, {:strictly_positive_number_required, pos: offset, found: number}} + + {:error, _reason} = error -> + error + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics_v2/utils.ex index 3e34466..7b0891a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/utils.ex +++ b/lib/image_plug/param_parser/twicpics_v2/utils.ex @@ -18,12 +18,19 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do do: balanced_parens?(rest, stack) # we found a ")", but head of stack doesn't match - defp balanced_parens?(<<")"::binary, rest::binary>>, _stack), do: false + defp balanced_parens?(<<")"::binary, _rest::binary>>, _stack), do: false # consume all other chars - defp balanced_parens?(<>, stack), do: balanced_parens?(rest, stack) + defp balanced_parens?(<<_char::utf8, rest::binary>>, stack), do: balanced_parens?(rest, stack) def update_error_input({:error, {reason, opts}}, input) do {:error, {reason, Keyword.put(opts, :input, input)}} end + + def token_pos({:int, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def token_pos({:float_open, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def token_pos({:float, _value, pos_b, pos_e}), do: {pos_b, pos_e} + def token_pos({:left_paren, pos}), do: {pos, pos} + def token_pos({:right_paren, pos}), do: {pos, pos} + def token_pos({:op, _optype, pos}), do: {pos, pos} end diff --git a/test/param_parser/twicpics_v2/kv_parser_test.exs b/test/param_parser/twicpics_v2/kv_parser_test.exs index d6dc562..63047aa 100644 --- a/test/param_parser/twicpics_v2/kv_parser_test.exs +++ b/test/param_parser/twicpics_v2/kv_parser_test.exs @@ -1,4 +1,4 @@ -defmodule ImagePlug.Twicpics.KeyValueParserTest do +defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParserTest do use ExUnit.Case, async: true use ExUnitProperties diff --git a/test/param_parser/twicpics_v2/number_parser_test.exs b/test/param_parser/twicpics_v2/number_parser_test.exs index 2efb030..609c0a1 100644 --- a/test/param_parser/twicpics_v2/number_parser_test.exs +++ b/test/param_parser/twicpics_v2/number_parser_test.exs @@ -1,7 +1,9 @@ -defmodule ImagePlug.Twicpics.NumberParserTest do +defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParserTest do use ExUnit.Case, async: true use ExUnitProperties + alias ImagePlug.ParamParser.TwicpicsV2.NumberParser + describe "successful parsing" do test "parses a single integer" do input = "123" diff --git a/test/param_parser/twicpics_v2/utils_test.exs b/test/param_parser/twicpics_v2/utils_test.exs index b62254f..6f2cd40 100644 --- a/test/param_parser/twicpics_v2/utils_test.exs +++ b/test/param_parser/twicpics_v2/utils_test.exs @@ -1,4 +1,4 @@ -defmodule ImagePlug.Twicpics.UtilsTest do +defmodule ImagePlug.ParamParser.TwicpicsV2.UtilsTest do use ExUnit.Case, async: true use ExUnitProperties From 2494e4d045a994ff2866c3a971039ac3c694cf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 23:53:58 +0100 Subject: [PATCH 24/35] handle missing 'x' in CoordinatesParser --- .../twicpics_v2/coordinates_parser.ex | 29 +++++++++---- .../param_parser/twicpics_v2/number_parser.ex | 42 +++++++++---------- .../param_parser/twicpics_v2/utils.ex | 4 ++ 3 files changed, 45 insertions(+), 30 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex index 59e5b06..8591b78 100644 --- a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex @@ -13,19 +13,30 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do {:error, _reason} = error -> Utils.update_error_input(error, input) end - [""] -> - {:error, {:unexpected_eoi, pos: pos_offset}} + [str] -> + # attempt to parse string to get error messages. if it suceeds, + # complain about the second dimension that's missing + case parse_and_validate(str, pos_offset) do + {:ok, _} -> + Utils.unexpected_char_error(pos_offset + String.length(str), ["x"], :eoi) + |> Utils.update_error_input(input) - _ -> - {:error, {:unexpected_token, pos: pos_offset}} + {:error, _} = error -> + Utils.update_error_input(error, input) + end end end - defp parse_and_validate(length_str, offset) do - case LengthParser.parse(length_str, offset) do - {:ok, {_type, number} = parsed_length} when number >= 0 -> {:ok, parsed_length} - {:ok, {_type, number}} -> {:error, {:positive_number_required, pos: offset, found: number}} - {:error, _reason} = error -> error + defp parse_and_validate(length_str, pos_offset) do + case LengthParser.parse(length_str, pos_offset) do + {:ok, {_type, number} = parsed_length} when number >= 0 -> + {:ok, parsed_length} + + {:ok, {_type, number}} -> + {:error, {:positive_number_required, pos: pos_offset, found: number}} + + {:error, _reason} = error -> + error end end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 764b555..3de19be 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -7,10 +7,6 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do defstruct input: "", tokens: [], pos: 0, paren_count: 0 end - defp unexpected_char_error(pos, expected, found) do - {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} - end - defp consume_char(%State{input: <<_char::utf8, rest::binary>>, pos: pos} = state), do: %State{state | input: rest, pos: pos + 1} @@ -61,7 +57,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do # we hit end of input, but no tokens have been processed defp do_parse(%State{input: "", tokens: []} = state) when state.paren_count == 0, - do: unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) + do: Utils.unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) # just consume space characters defp do_parse(%State{input: <>} = state) when char in ~c[ ] do @@ -95,7 +91,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + Utils.unexpected_char_error(state.pos, ["(", "[0-9]"], <>) end end @@ -117,13 +113,13 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - unexpected_char_error(state.pos, ["(", "[0-9]", "-"], <>) + Utils.unexpected_char_error(state.pos, ["(", "[0-9]", "-"], <>) end end # we hit end of input while the previous token was a :left_paren defp do_parse(%State{input: "", tokens: [{:left_paren, _} | _]} = state) do - unexpected_char_error(state.pos, ["(", "[0-9]", "-"], :eoi) + Utils.unexpected_char_error(state.pos, ["(", "[0-9]", "-"], :eoi) end # @@ -139,7 +135,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do } = state ) when state.paren_count == 0, - do: unexpected_char_error(state.pos, [:eoi], <>) + do: Utils.unexpected_char_error(state.pos, [:eoi], <>) defp do_parse( %State{ @@ -151,14 +147,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do cond do char in @op_tokens -> add_token(state, mk_op(<>, state.pos)) char == ?) -> add_right_paren(state) - true -> unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) + true -> Utils.unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) end end # we hit end of input while the previous token was a :right_paren, but we're still inside a paren defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _]} = state) when state.paren_count > 0 do - unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], :eoi) end # @@ -181,7 +177,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) true -> - unexpected_char_error(state.pos, ["[0-9]", "."], <>) + Utils.unexpected_char_error(state.pos, ["[0-9]", "."], <>) end end @@ -206,14 +202,18 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_right_paren(state) true -> - unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <>) + Utils.unexpected_char_error( + state.pos, + ["[0-9]", ".", "+", "-", "*", "/", ")"], + <> + ) end end # we hit eoi while on an :int token, and we're in a parentheses defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _]} = state) when state.paren_count > 0 do - unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], :eoi) end # @@ -232,13 +232,13 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> - unexpected_char_error(state.pos, ["[0-9]"], <>) + Utils.unexpected_char_error(state.pos, ["[0-9]"], <>) end end # we hit end of input while in a :float_open defp do_parse(%State{input: "", tokens: [{:float_open, _, _, _} | _]} = state), - do: unexpected_char_error(state.pos, ["[0-9]"], :eoi) + do: Utils.unexpected_char_error(state.pos, ["[0-9]"], :eoi) # # prev token: :float @@ -258,7 +258,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> - unexpected_char_error(state.pos, ["[0-9]"], <>) + Utils.unexpected_char_error(state.pos, ["[0-9]"], <>) end end @@ -280,14 +280,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_right_paren(state) true -> - unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], <>) + Utils.unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], <>) end end # we hit eoi while on an :int token, and we're in a parentheses defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _]} = state) when state.paren_count > 0 do - unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) end # @@ -309,12 +309,12 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - unexpected_char_error(state.pos, ["[0-9]", "-", "("], <>) + Utils.unexpected_char_error(state.pos, ["[0-9]", "-", "("], <>) end end # we hit eoi while on an :op token defp do_parse(%State{input: "", tokens: [{:op, _, _} | _]} = state) do - unexpected_char_error(state.pos, ["[0-9]", "-", "("], :eoi) + Utils.unexpected_char_error(state.pos, ["[0-9]", "-", "("], :eoi) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics_v2/utils.ex index 7b0891a..7630160 100644 --- a/lib/image_plug/param_parser/twicpics_v2/utils.ex +++ b/lib/image_plug/param_parser/twicpics_v2/utils.ex @@ -33,4 +33,8 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do def token_pos({:left_paren, pos}), do: {pos, pos} def token_pos({:right_paren, pos}), do: {pos, pos} def token_pos({:op, _optype, pos}), do: {pos, pos} + + def unexpected_char_error(pos, expected, found) do + {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} + end end From f058005fa1231c2233ef4eb952a56c192e0df98b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sat, 7 Dec 2024 23:57:43 +0100 Subject: [PATCH 25/35] add comment --- .../param_parser/twicpics_v2/coordinates_parser.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex index 8591b78..1b6a989 100644 --- a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex @@ -13,12 +13,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do {:error, _reason} = error -> Utils.update_error_input(error, input) end - [str] -> - # attempt to parse string to get error messages. if it suceeds, - # complain about the second dimension that's missing - case parse_and_validate(str, pos_offset) do + [left_str] -> + # this is an invalid coordinate! + # + # attempt to parse string to get error messages for number parsing. + # if it suceeds, complain that the second dimension that's missing + case parse_and_validate(left_str, pos_offset) do {:ok, _} -> - Utils.unexpected_char_error(pos_offset + String.length(str), ["x"], :eoi) + Utils.unexpected_char_error(pos_offset + String.length(left_str), ["x"], :eoi) |> Utils.update_error_input(input) {:error, _} = error -> From 111ed596df76bd2739134b1b56e35315fee7ee9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 00:07:23 +0100 Subject: [PATCH 26/35] better errors in kv parser + validate keys --- .../twicpics_v2/coordinates_parser.ex | 2 +- .../twicpics_v2/key_value_parser.ex | 7 +++- .../param_parser/twicpics_v2/number_parser.ex | 38 ++++++++++--------- .../param_parser/twicpics_v2/utils.ex | 2 +- .../twicpics_v2/number_parser_test.exs | 2 +- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex index 1b6a989..1ebfea6 100644 --- a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex @@ -20,7 +20,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do # if it suceeds, complain that the second dimension that's missing case parse_and_validate(left_str, pos_offset) do {:ok, _} -> - Utils.unexpected_char_error(pos_offset + String.length(left_str), ["x"], :eoi) + Utils.unexpected_value_error(pos_offset + String.length(left_str), ["x"], :eoi) |> Utils.update_error_input(input) {:error, _} = error -> diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex index c4c6afb..2d7c468 100644 --- a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex @@ -1,6 +1,8 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils + @valid_keys ~w(crop resize contain focus output) + def parse(input, pos_offset \\ 0) do case parse_pairs(input, [], pos_offset) do {:ok, result} -> {:ok, Enum.reverse(result)} @@ -27,8 +29,9 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do defp extract_key(input, pos) do case String.split(input, "=", parts: 2) do - [key, rest] -> {:ok, {key, rest, pos + String.length(key) + 1}} - [rest] -> {:error, {:expected_eq, pos: pos + String.length(rest) + 1}} + [key, rest] when key in @valid_keys -> {:ok, {key, rest, pos + String.length(key) + 1}} + [key, rest] -> Utils.unexpected_value_error(pos, @valid_keys, key) + [rest] -> Utils.unexpected_value_error(pos + String.length(rest), ["="], :eoi) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 3de19be..663feb1 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -57,7 +57,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do # we hit end of input, but no tokens have been processed defp do_parse(%State{input: "", tokens: []} = state) when state.paren_count == 0, - do: Utils.unexpected_char_error(state.pos, ["(", "[0-9]"], found: :eoi) + do: Utils.unexpected_value_error(state.pos, ["(", "[0-9]"], found: :eoi) # just consume space characters defp do_parse(%State{input: <>} = state) when char in ~c[ ] do @@ -91,7 +91,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - Utils.unexpected_char_error(state.pos, ["(", "[0-9]"], <>) + Utils.unexpected_value_error(state.pos, ["(", "[0-9]"], <>) end end @@ -113,13 +113,13 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - Utils.unexpected_char_error(state.pos, ["(", "[0-9]", "-"], <>) + Utils.unexpected_value_error(state.pos, ["(", "[0-9]", "-"], <>) end end # we hit end of input while the previous token was a :left_paren defp do_parse(%State{input: "", tokens: [{:left_paren, _} | _]} = state) do - Utils.unexpected_char_error(state.pos, ["(", "[0-9]", "-"], :eoi) + Utils.unexpected_value_error(state.pos, ["(", "[0-9]", "-"], :eoi) end # @@ -135,7 +135,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do } = state ) when state.paren_count == 0, - do: Utils.unexpected_char_error(state.pos, [:eoi], <>) + do: Utils.unexpected_value_error(state.pos, [:eoi], <>) defp do_parse( %State{ @@ -147,14 +147,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do cond do char in @op_tokens -> add_token(state, mk_op(<>, state.pos)) char == ?) -> add_right_paren(state) - true -> Utils.unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], <>) + true -> Utils.unexpected_value_error(state.pos, ["+", "-", "*", "/", ")"], <>) end end # we hit end of input while the previous token was a :right_paren, but we're still inside a paren defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _]} = state) when state.paren_count > 0 do - Utils.unexpected_char_error(state.pos, ["+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_value_error(state.pos, ["+", "-", "*", "/", ")"], :eoi) end # @@ -177,7 +177,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) true -> - Utils.unexpected_char_error(state.pos, ["[0-9]", "."], <>) + Utils.unexpected_value_error(state.pos, ["[0-9]", "."], <>) end end @@ -202,7 +202,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_right_paren(state) true -> - Utils.unexpected_char_error( + Utils.unexpected_value_error( state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], <> @@ -213,7 +213,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do # we hit eoi while on an :int token, and we're in a parentheses defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _]} = state) when state.paren_count > 0 do - Utils.unexpected_char_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_value_error(state.pos, ["[0-9]", ".", "+", "-", "*", "/", ")"], :eoi) end # @@ -232,13 +232,13 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> - Utils.unexpected_char_error(state.pos, ["[0-9]"], <>) + Utils.unexpected_value_error(state.pos, ["[0-9]"], <>) end end # we hit end of input while in a :float_open defp do_parse(%State{input: "", tokens: [{:float_open, _, _, _} | _]} = state), - do: Utils.unexpected_char_error(state.pos, ["[0-9]"], :eoi) + do: Utils.unexpected_value_error(state.pos, ["[0-9]"], :eoi) # # prev token: :float @@ -258,7 +258,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) true -> - Utils.unexpected_char_error(state.pos, ["[0-9]"], <>) + Utils.unexpected_value_error(state.pos, ["[0-9]"], <>) end end @@ -280,14 +280,18 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_right_paren(state) true -> - Utils.unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], <>) + Utils.unexpected_value_error( + state.pos, + ["[0-9]", "+", "-", "*", "/", ")"], + <> + ) end end # we hit eoi while on an :int token, and we're in a parentheses defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _]} = state) when state.paren_count > 0 do - Utils.unexpected_char_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) + Utils.unexpected_value_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) end # @@ -309,12 +313,12 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do add_left_paren(state) true -> - Utils.unexpected_char_error(state.pos, ["[0-9]", "-", "("], <>) + Utils.unexpected_value_error(state.pos, ["[0-9]", "-", "("], <>) end end # we hit eoi while on an :op token defp do_parse(%State{input: "", tokens: [{:op, _, _} | _]} = state) do - Utils.unexpected_char_error(state.pos, ["[0-9]", "-", "("], :eoi) + Utils.unexpected_value_error(state.pos, ["[0-9]", "-", "("], :eoi) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics_v2/utils.ex index 7630160..1657bc1 100644 --- a/lib/image_plug/param_parser/twicpics_v2/utils.ex +++ b/lib/image_plug/param_parser/twicpics_v2/utils.ex @@ -34,7 +34,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do def token_pos({:right_paren, pos}), do: {pos, pos} def token_pos({:op, _optype, pos}), do: {pos, pos} - def unexpected_char_error(pos, expected, found) do + def unexpected_value_error(pos, expected, found) do {:error, {:unexpected_char, pos: pos, expected: expected, found: found}} end end diff --git a/test/param_parser/twicpics_v2/number_parser_test.exs b/test/param_parser/twicpics_v2/number_parser_test.exs index 609c0a1..4cc2093 100644 --- a/test/param_parser/twicpics_v2/number_parser_test.exs +++ b/test/param_parser/twicpics_v2/number_parser_test.exs @@ -141,7 +141,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParserTest do end end - describe "unexpected_char_error/3" do + describe "unexpected_value_error/3" do test "invalid character at the start of input" do input = "x123" {:error, {:unexpected_char, opts}} = NumberParser.parse(input) From a536b0c8ea8165fbc7386721eea9c6aee0028e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 00:18:51 +0100 Subject: [PATCH 27/35] move stuff around --- .../twicpics_v2/coordinates_parser.ex | 2 +- .../{key_value_parser.ex => kv_parser.ex} | 2 +- .../param_parser/twicpics_v2/size_parser.ex | 8 +++--- .../{ => transforms}/crop_parser.ex | 25 ++++++++++++++++--- 4 files changed, 27 insertions(+), 10 deletions(-) rename lib/image_plug/param_parser/twicpics_v2/{key_value_parser.ex => kv_parser.ex} (96%) rename lib/image_plug/param_parser/twicpics_v2/{ => transforms}/crop_parser.ex (51%) diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex index 1ebfea6..f008b1c 100644 --- a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex @@ -8,7 +8,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do with {:ok, parsed_left} <- parse_and_validate(left_str, pos_offset), {:ok, parsed_top} <- parse_and_validate(top_str, pos_offset + String.length(left_str) + 1) do - {:ok, [left: parsed_left, top: parsed_top]} + {:ok, %{left: parsed_left, top: parsed_top}} else {:error, _reason} = error -> Utils.update_error_input(error, input) end diff --git a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex b/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex similarity index 96% rename from lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex rename to lib/image_plug/param_parser/twicpics_v2/kv_parser.ex index 2d7c468..01c6895 100644 --- a/lib/image_plug/param_parser/twicpics_v2/key_value_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex @@ -1,4 +1,4 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.KVParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils @valid_keys ~w(crop resize contain focus output) diff --git a/lib/image_plug/param_parser/twicpics_v2/size_parser.ex b/lib/image_plug/param_parser/twicpics_v2/size_parser.ex index c4c7cc3..3f43945 100644 --- a/lib/image_plug/param_parser/twicpics_v2/size_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/size_parser.ex @@ -9,13 +9,13 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.SizeParser do ["-", height_str] -> case parse_and_validate(height_str, pos_offset + 2) do - {:ok, parsed_height} -> {:ok, [width: :auto, height: parsed_height]} + {:ok, parsed_height} -> {:ok, %{width: :auto, height: parsed_height}} {:error, _reason} = error -> Utils.update_error_input(error, input) end [width_str, "-"] -> case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:ok, parsed_width} -> {:ok, %{width: parsed_width, height: :auto}} {:error, _reason} = error -> Utils.update_error_input(error, input) end @@ -23,14 +23,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.SizeParser do with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), {:ok, parsed_height} <- parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do - {:ok, [width: parsed_width, height: parsed_height]} + {:ok, %{width: parsed_width, height: parsed_height}} else {:error, _reason} = error -> Utils.update_error_input(error, input) end [width_str] -> case parse_and_validate(width_str, pos_offset) do - {:ok, parsed_width} -> {:ok, [width: parsed_width, height: :auto]} + {:ok, parsed_width} -> {:ok, %{width: parsed_width, height: :auto}} {:error, _reason} = error -> Utils.update_error_input(error, input) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/crop_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex similarity index 51% rename from lib/image_plug/param_parser/twicpics_v2/crop_parser.ex rename to lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex index bfd417d..63f8f78 100644 --- a/lib/image_plug/param_parser/twicpics_v2/crop_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex @@ -1,7 +1,8 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.CropParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.Transforms.CropParser do alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser alias ImagePlug.ParamParser.TwicpicsV2.SizeParser alias ImagePlug.ParamParser.TwicpicsV2.Utils + alias ImagePlug.Transform.Crop.CropParams def parse(input, pos_offset \\ 0) do case String.split(input, "@", parts: 2) do @@ -9,15 +10,31 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.CropParser do with {:ok, parsed_size} <- SizeParser.parse(size_str, pos_offset), {:ok, parsed_coordinates} <- CoordinatesParser.parse(coordinates_str, pos_offset + String.length(size_str) + 1) do - {:ok, [crop: parsed_size, crop_from: parsed_coordinates]} + {:ok, + %CropParams{ + width: parsed_size.width, + height: parsed_size.height, + crop_from: %{ + left: parsed_coordinates.left, + top: parsed_coordinates.top + } + }} else {:error, _reason} = error -> Utils.update_error_input(error, input) end [size_str] -> case SizeParser.parse(size_str, pos_offset) do - {:ok, parsed_size} -> {:ok, [crop: parsed_size, crop_from: :focus]} - {:error, _reason} = error -> Utils.update_error_input(error, input) + {:ok, parsed_size} -> + {:ok, + %CropParams{ + width: parsed_size.width, + height: parsed_size.height, + crop_from: :focus + }} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) end end end From 0593248a093973d1b55df91231eff6c9994bc626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 00:38:53 +0100 Subject: [PATCH 28/35] wip --- lib/image_plug/param_parser/twicpics_v2.ex | 58 +++++++++++++++++++ .../{arithmetic.ex => arithmetic_parser.ex} | 0 .../param_parser/twicpics_v2/ratio_parser.ex | 45 ++++++++++++++ .../{transforms => transform}/crop_parser.ex | 2 +- .../twicpics_v2/transform/scale_parser.ex | 34 +++++++++++ 5 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2.ex rename lib/image_plug/param_parser/twicpics_v2/{arithmetic.ex => arithmetic_parser.ex} (100%) create mode 100644 lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex rename lib/image_plug/param_parser/twicpics_v2/{transforms => transform}/crop_parser.ex (95%) create mode 100644 lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2.ex b/lib/image_plug/param_parser/twicpics_v2.ex new file mode 100644 index 0000000..cc9d768 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2.ex @@ -0,0 +1,58 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2 do + @behaviour ImagePlug.ParamParser + + alias ImagePlug.ParamParser.Twicpics.TwicpicsV2 + + @transforms %{ + "crop" => ImagePlug.Transform.Crop + } + + @parsers %{ + ImagePlug.Transform.Crop => TwicpicsV2.Transform.CropParser + } + + @impl ImagePlug.ParamParser + def parse(%Plug.Conn{} = conn) do + conn = Plug.Conn.fetch_query_params(conn) + + case conn.params do + %{"twic" => input} -> parse_string(input) + _ -> {:ok, []} + end + end + + def parse_string(input) do + case input do + "v1/" <> chain -> parse_chain(chain) + _ -> {:ok, []} + end + end + + # a `key=value` string followed by either a slash and a + # new key=value string or the end of the string using lookahead + @params_regex ~r/\/?([a-z]+)=(.+?(?=\/[a-z]+=|$))/ + + def parse_chain(chain_str) do + Regex.scan(@params_regex, chain_str, capture: :all_but_first) + |> Enum.reduce_while({:ok, []}, fn + [transform_name, params_str], {:ok, transforms_acc} + when is_map_key(@transforms, transform_name) -> + module = Map.get(@transforms, transform_name) + + case @parsers[module].parse(params_str) do + {:ok, parsed_params} -> + {:cont, {:ok, [{module, parsed_params} | transforms_acc]}} + + {:error, {:parameter_parse_error, input}} -> + {:halt, {:error, {:invalid_params, {module, "invalid input: #{input}"}}}} + end + + [transform_name, _params_str], acc -> + {:cont, [{:error, {:invalid_transform, transform_name}} | acc]} + end) + |> case do + {:ok, transforms} -> {:ok, Enum.reverse(transforms)} + other -> other + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic.ex b/lib/image_plug/param_parser/twicpics_v2/arithmetic_parser.ex similarity index 100% rename from lib/image_plug/param_parser/twicpics_v2/arithmetic.ex rename to lib/image_plug/param_parser/twicpics_v2/arithmetic_parser.ex diff --git a/lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex b/lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex new file mode 100644 index 0000000..22404ec --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex @@ -0,0 +1,45 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.RatioParser do + alias ImagePlug.ParamParser.TwicpicsV2.NumberParser + alias ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + def parse(input, pos_offset \\ 0) do + case String.split(input, ":", parts: 2) do + [width_str, height_str] -> + with {:ok, parsed_width} <- parse_and_validate(width_str, pos_offset), + {:ok, parsed_height} <- + parse_and_validate(height_str, pos_offset + String.length(width_str) + 1) do + {:ok, %{width: parsed_width, height: parsed_height}} + else + {:error, _reason} = error -> Utils.update_error_input(error, input) + end + + [width_str] -> + # this is an invalid ratio! + # + # attempt to parse string to get error messages for number parsing. + # if it suceeds, complain that the second component that's missing + case parse_and_validate(width_str, pos_offset) do + {:ok, _} -> + Utils.unexpected_value_error(pos_offset + String.length(width_str), [":"], :eoi) + |> Utils.update_error_input(input) + + {:error, _} = error -> + Utils.update_error_input(error, input) + end + end + end + + defp parse_and_validate(num_str, pos_offset) do + with {:ok, tokens} <- NumberParser.parse(num_str, pos_offset), + {:ok, evaluated} <- ArithmeticParser.parse_and_evaluate(tokens) do + if evaluated > 0 do + {:ok, evaluated} + else + {:error, {:positive_number_required, pos: pos_offset, found: evaluated}} + end + else + {:error, {_reason, _opts}} = error -> error + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex similarity index 95% rename from lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex rename to lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex index 63f8f78..e59898a 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transforms/crop_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex @@ -1,4 +1,4 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transforms.CropParser do +defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.CropParser do alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser alias ImagePlug.ParamParser.TwicpicsV2.SizeParser alias ImagePlug.ParamParser.TwicpicsV2.Utils diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex new file mode 100644 index 0000000..fa4e77b --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex @@ -0,0 +1,34 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser do + alias ImagePlug.ParamParser.TwicpicsV2.SizeParser + alias ImagePlug.ParamParser.TwicpicsV2.RatioParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + alias ImagePlug.Transform.Scale.ScaleParams + alias ImagePlug.Transform.Scale.ScaleParams.Dimensions + alias ImagePlug.Transform.Scale.ScaleParams.AspectRatio + + def parse(input, pos_offset \\ 0) do + if String.contains?(input, ":"), + do: parse_ratio(input, pos_offset), + else: parse_size(input, pos_offset) + end + + defp parse_ratio(input, pos_offset) do + case RatioParser.parse(input, pos_offset) do + {:ok, %{width: width, height: height}} -> + %ScaleParams{method: %AspectRatio{aspect_ratio: {:ratio, width, height}}} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end + + defp parse_size(input, pos_offset) do + case SizeParser.parse(input, pos_offset) do + {:ok, %{width: width, height: height}} -> + %ScaleParams{method: %Dimensions{width: width, height: height}} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end +end From e13383b25d6a545b069c40a25fe5005beb814499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 13:11:03 +0100 Subject: [PATCH 29/35] end to end working with new parsers --- lib/image_plug.ex | 22 +++--- lib/image_plug/param_parser/twicpics_v2.ex | 69 +++++++++++-------- .../param_parser/twicpics_v2/kv_parser.ex | 32 +++++---- .../twicpics_v2/transform/contain_parser.ex | 16 +++++ .../twicpics_v2/transform/focus_parser.ex | 45 ++++++++++++ .../twicpics_v2/transform/output_parser.ex | 26 +++++++ .../twicpics_v2/transform/scale_parser.ex | 4 +- lib/image_plug/transform.ex | 31 +++------ lib/simple_server.ex | 2 +- 9 files changed, 165 insertions(+), 82 deletions(-) create mode 100644 lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex create mode 100644 lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex diff --git a/lib/image_plug.ex b/lib/image_plug.ex index b5eb1a9..583dbfc 100644 --- a/lib/image_plug.ex +++ b/lib/image_plug.ex @@ -1,15 +1,6 @@ defmodule ImagePlug do @behaviour Plug - @type imgp_int() :: {:int, integer()} - @type imgp_float() :: {:float, float()} - @type imgp_expr() :: {:expr, float()} - @type imgp_number() :: imgp_int() | imgp_float() | imgp_expr() - @type imgp_pct() :: {:float, imgp_number()} - @type imgp_scale() :: {:float, imgp_number(), imgp_number()} - @type imgp_length() :: imgp_number() | imgp_pct() | imgp_scale() - @type imgp_ratio() :: {:ratio, imgp_number(), imgp_number()} - import Plug.Conn require Logger @@ -17,6 +8,16 @@ defmodule ImagePlug do alias ImagePlug.TransformState alias ImagePlug.TransformChain + @type imgp_number() :: integer() | float() + @type imgp_pixels() :: {:pixels, imgp_number()} + @type imgp_pct() :: {:percent, imgp_number()} + @type imgp_scale() :: {:scale, imgp_number(), imgp_number()} + @type imgp_ratio() :: {:ratio, imgp_number(), imgp_number()} + @type imgp_length() :: imgp_pixels() | imgp_pct() | imgp_scale() + + @alpha_format_priority ~w(image/avif image/webp image/png) + @no_alpha_format_priority ~w(image/avif image/webp image/jpeg) + def init(opts), do: opts def call(%Plug.Conn{} = conn, opts) do @@ -42,9 +43,6 @@ defmodule ImagePlug do end end - @alpha_format_priority ~w(image/avif image/webp image/png) - @no_alpha_format_priority ~w(image/avif image/webp image/jpeg) - defp accepted_formats(%Plug.Conn{} = conn) do from_accept_header = get_req_header(conn, "accept") diff --git a/lib/image_plug/param_parser/twicpics_v2.ex b/lib/image_plug/param_parser/twicpics_v2.ex index cc9d768..3fc405f 100644 --- a/lib/image_plug/param_parser/twicpics_v2.ex +++ b/lib/image_plug/param_parser/twicpics_v2.ex @@ -1,55 +1,66 @@ defmodule ImagePlug.ParamParser.TwicpicsV2 do @behaviour ImagePlug.ParamParser - alias ImagePlug.ParamParser.Twicpics.TwicpicsV2 + alias ImagePlug.ParamParser.TwicpicsV2 @transforms %{ - "crop" => ImagePlug.Transform.Crop + "crop" => {ImagePlug.Transform.Crop, TwicpicsV2.Transform.CropParser}, + "scale" => {ImagePlug.Transform.Scale, TwicpicsV2.Transform.ScaleParser}, + "focus" => {ImagePlug.Transform.Focus, TwicpicsV2.Transform.FocusParser}, + "contain" => {ImagePlug.Transform.Contain, TwicpicsV2.Transform.ContainParser}, + "output" => {ImagePlug.Transform.Output, TwicpicsV2.Transform.OutputParser}, } - @parsers %{ - ImagePlug.Transform.Crop => TwicpicsV2.Transform.CropParser - } + @transform_keys Map.keys(@transforms) + @query_param "twic" + @query_param_prefix "v1/" @impl ImagePlug.ParamParser def parse(%Plug.Conn{} = conn) do conn = Plug.Conn.fetch_query_params(conn) case conn.params do - %{"twic" => input} -> parse_string(input) - _ -> {:ok, []} + %{@query_param => input} -> + # start position count from where the request_path starts. + # used for parser error messages. + pos_offset = String.length(conn.request_path <> "?" <> @query_param <> "=") + parse_string(input, pos_offset) + + _ -> + {:ok, []} end end - def parse_string(input) do + def parse_string(input, pos_offset) do case input do - "v1/" <> chain -> parse_chain(chain) - _ -> {:ok, []} + @query_param_prefix <> chain -> + pos_offset = pos_offset + String.length(@query_param_prefix) + parse_chain(chain, pos_offset) + + _ -> + {:ok, []} end end - # a `key=value` string followed by either a slash and a - # new key=value string or the end of the string using lookahead - @params_regex ~r/\/?([a-z]+)=(.+?(?=\/[a-z]+=|$))/ + def parse_chain(chain_str, pos_offset) do + case TwicpicsV2.KVParser.parse(chain_str, @transform_keys, pos_offset) do + {:ok, kv_params} -> + Enum.reduce_while(kv_params, {:ok, []}, fn + {transform_name, params_str, pos}, {:ok, transforms_acc} -> + {transform_mod, parser_mod} = Map.get(@transforms, transform_name) - def parse_chain(chain_str) do - Regex.scan(@params_regex, chain_str, capture: :all_but_first) - |> Enum.reduce_while({:ok, []}, fn - [transform_name, params_str], {:ok, transforms_acc} - when is_map_key(@transforms, transform_name) -> - module = Map.get(@transforms, transform_name) + case parser_mod.parse(params_str, pos) do + {:ok, parsed_params} -> + {:cont, {:ok, [{transform_mod, parsed_params} | transforms_acc]}} - case @parsers[module].parse(params_str) do - {:ok, parsed_params} -> - {:cont, {:ok, [{module, parsed_params} | transforms_acc]}} + {:error, _reason} = error -> + {:halt, error} + end + end) - {:error, {:parameter_parse_error, input}} -> - {:halt, {:error, {:invalid_params, {module, "invalid input: #{input}"}}}} - end - - [transform_name, _params_str], acc -> - {:cont, [{:error, {:invalid_transform, transform_name}} | acc]} - end) + {:error, _reason} = error -> + error + end |> case do {:ok, transforms} -> {:ok, Enum.reverse(transforms)} other -> other diff --git a/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex b/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex index 01c6895..e30945d 100644 --- a/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex @@ -1,37 +1,39 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KVParser do alias ImagePlug.ParamParser.TwicpicsV2.Utils - @valid_keys ~w(crop resize contain focus output) - - def parse(input, pos_offset \\ 0) do - case parse_pairs(input, [], pos_offset) do + def parse(input, valid_keys, pos_offset \\ 0) do + case parse_pairs(input, [], valid_keys, pos_offset) do {:ok, result} -> {:ok, Enum.reverse(result)} {:error, _reason} = error -> error end end - defp parse_pairs("", acc, _pos), do: {:ok, acc} + defp parse_pairs("", acc, _valid_keys, _pos), do: {:ok, acc} # pos + 1 because key is expected at the next char - defp parse_pairs("/", _acc, pos), do: {:error, {:expected_key, pos: pos + 1}} + defp parse_pairs("/", _acc, _valid_keys, pos), do: {:error, {:expected_key, pos: pos + 1}} - defp parse_pairs(<<"/"::binary, input::binary>>, acc, pos), - do: parse_pairs(input, acc, pos + 1) + defp parse_pairs(<<"/"::binary, input::binary>>, acc, valid_keys, pos), + do: parse_pairs(input, acc, valid_keys, pos + 1) - defp parse_pairs(input, acc, key_pos) do - with {:ok, {key, rest, value_pos}} <- extract_key(input, key_pos), + defp parse_pairs(input, acc, valid_keys, key_pos) do + with {:ok, {key, rest, value_pos}} <- extract_key(input, valid_keys, key_pos), {:ok, {value, rest, next_pos}} <- extract_value(rest, value_pos) do - parse_pairs(rest, [{key, value, key_pos} | acc], next_pos) + parse_pairs(rest, [{key, value, key_pos} | acc], valid_keys, next_pos) else {:error, _reason} = error -> error end end - defp extract_key(input, pos) do + defp extract_key(input, valid_keys, pos) do case String.split(input, "=", parts: 2) do - [key, rest] when key in @valid_keys -> {:ok, {key, rest, pos + String.length(key) + 1}} - [key, rest] -> Utils.unexpected_value_error(pos, @valid_keys, key) - [rest] -> Utils.unexpected_value_error(pos + String.length(rest), ["="], :eoi) + [key, rest] -> + if key in valid_keys, + do: {:ok, {key, rest, pos + String.length(key) + 1}}, + else: Utils.unexpected_value_error(pos, valid_keys, key) + + [rest] -> + Utils.unexpected_value_error(pos + String.length(rest), ["="], :eoi) end end diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex new file mode 100644 index 0000000..0c9bd7c --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex @@ -0,0 +1,16 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ContainParser do + alias ImagePlug.ParamParser.TwicpicsV2.SizeParser + alias ImagePlug.ParamParser.TwicpicsV2.RatioParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + alias ImagePlug.Transform.Contain.ContainParams + + def parse(input, pos_offset \\ 0) do + case SizeParser.parse(input, pos_offset) do + {:ok, %{width: width, height: height}} -> + {:ok, %ContainParams{width: width, height: height}} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex new file mode 100644 index 0000000..a630dd8 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex @@ -0,0 +1,45 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.FocusParser do + alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + alias ImagePlug.Transform.Focus.FocusParams + + @anchors %{ + "center" => {:anchor, :center, :center}, + "bottom" => {:anchor, :center, :bottom}, + "bottom-left" => {:anchor, :left, :bottom}, + "bottom-right" => {:anchor, :right, :bottom}, + "left" => {:anchor, :left, :center}, + "top" => {:anchor, :center, :top}, + "top-left" => {:anchor, :left, :top}, + "top-right" => {:anchor, :right, :top}, + "right" => {:anchor, :right, :center} + } + + def parse(input, pos_offset \\ 0) do + if String.contains?(input, "x"), + do: parse_coordinates(input, pos_offset), + else: parse_anchor_string(input, pos_offset) + end + + defp parse_coordinates(input, pos_offset) do + case CoordinatesParser.parse(input, pos_offset) do + {:ok, %{left: left, top: top}} -> + {:ok, %FocusParams{type: {:coordinate, left, top}}} + + {:error, _reason} = error -> + Utils.update_error_input(error, input) + end + end + + defp parse_anchor_string(input, pos_offset) do + case Map.get(@anchors, input) do + {:anchor, _, _} = anchor -> + {:ok, %FocusParams{type: anchor}} + + _ -> + Utils.unexpected_value_error(pos_offset, Map.keys(@anchors), input) + |> Utils.update_error_input(input) + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex new file mode 100644 index 0000000..d68faa1 --- /dev/null +++ b/lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex @@ -0,0 +1,26 @@ +defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.OutputParser do + alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser + alias ImagePlug.ParamParser.TwicpicsV2.Utils + + alias ImagePlug.Transform.Output.OutputParams + + @formats %{ + "auto" => :auto, + "avif" => :avif, + "webp" => :webp, + "jpeg" => :jpeg, + "png" => :png, + "blurhash" => :blurhash + } + + def parse(input, pos_offset \\ 0) do + case Map.get(@formats, input) do + format when is_atom(format) -> + {:ok, %OutputParams{format: format}} + + _ -> + Utils.unexpected_value_error(pos_offset, Map.keys(@formats), input) + |> Utils.update_error_input(input) + end + end +end diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex b/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex index fa4e77b..d2021f5 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex @@ -15,7 +15,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser do defp parse_ratio(input, pos_offset) do case RatioParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> - %ScaleParams{method: %AspectRatio{aspect_ratio: {:ratio, width, height}}} + {:ok, %ScaleParams{method: %AspectRatio{aspect_ratio: {:ratio, width, height}}}} {:error, _reason} = error -> Utils.update_error_input(error, input) @@ -25,7 +25,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser do defp parse_size(input, pos_offset) do case SizeParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> - %ScaleParams{method: %Dimensions{width: width, height: height}} + {:ok, %ScaleParams{method: %Dimensions{width: width, height: height}}} {:error, _reason} = error -> Utils.update_error_input(error, input) diff --git a/lib/image_plug/transform.ex b/lib/image_plug/transform.ex index 01bce56..464b4b0 100644 --- a/lib/image_plug/transform.ex +++ b/lib/image_plug/transform.ex @@ -4,37 +4,22 @@ defmodule ImagePlug.Transform do @callback execute(TransformState.t(), String.t()) :: TransformState.t() - def eval_number({:int, int}), do: {:ok, int} - def eval_number({:float, float}), do: {:ok, float} - def eval_number({:expr, expr}), do: ArithmeticParser.parse_and_evaluate(expr) - def image_dim(%TransformState{image: image}, :width), do: Image.width(image) def image_dim(%TransformState{image: image}, :height), do: Image.height(image) @spec to_pixels(TransformState.t(), :width | :height, ImagePlug.imgp_length()) :: {:ok, integer()} | {:error, atom()} def to_pixels(state, dimension, length) - def to_pixels(_state, _dimension, {:int, int}), do: {:ok, int} - def to_pixels(_state, _dimension, {:float, float}), do: {:ok, round(float)} - def to_pixels(_state, _dimension, {:expr, _} = expr), do: eval_number(expr) - def to_pixels(state, dimension, {:scale, numerator_num, denominator_num}) do - with {:ok, numerator} <- eval_number(numerator_num), - {:ok, denominator} <- eval_number(denominator_num) do - if denominator_num != 0 do - {:ok, round(image_dim(state, dimension) * numerator / denominator)} - else - {:error, :division_by_zero} - end - else - {:error, _} = error -> error - end + def to_pixels(_state, _dimension, {:pixels, num}) do + {:ok, round(num)} + end + + def to_pixels(state, dimension, {:scale, numerator, denominator}) do + {:ok, round(image_dim(state, dimension) * numerator / denominator)} end - def to_pixels(state, dimension, {:pct, num}) do - case eval_number(num) do - {:ok, result} -> {:ok, round(result / 100 * image_dim(state, dimension))} - {:error, _} = error -> error - end + def to_pixels(state, dimension, {:percent, num}) do + {:ok, round(num / 100 * image_dim(state, dimension))} end end diff --git a/lib/simple_server.ex b/lib/simple_server.ex index 9a1b8c5..9489915 100644 --- a/lib/simple_server.ex +++ b/lib/simple_server.ex @@ -14,7 +14,7 @@ defmodule ImagePlug.SimpleServer do to: ImagePlug, init_opts: [ root_url: "http://localhost:4000", - param_parser: ImagePlug.ParamParser.Twicpics + param_parser: ImagePlug.ParamParser.TwicpicsV2 ] match _ do From b558ac416181e68916d51772b8ed426a98f53c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 13:37:07 +0100 Subject: [PATCH 30/35] wip --- lib/image_plug/param_parser/twicpics.ex | 66 ------------- .../param_parser/twicpics/common.ex | 69 -------------- .../param_parser/twicpics/contain_parser.ex | 34 ------- .../param_parser/twicpics/crop_parser.ex | 45 --------- .../param_parser/twicpics/focus_parser.ex | 67 ------------- .../param_parser/twicpics/output_parser.ex | 25 ----- .../param_parser/twicpics/scale_parser.ex | 83 ----------------- lib/image_plug/param_parser/twicpics_v2.ex | 4 +- test/param_parser/twicpics_parser_test.exs | 93 +++++++++++-------- test/{ => param_parser}/twicpics_test.exs | 4 +- .../twicpics_v2/kv_parser_test.exs | 16 ++-- test/support/image_plug_test_support.ex | 10 +- 12 files changed, 74 insertions(+), 442 deletions(-) delete mode 100644 lib/image_plug/param_parser/twicpics.ex delete mode 100644 lib/image_plug/param_parser/twicpics/common.ex delete mode 100644 lib/image_plug/param_parser/twicpics/contain_parser.ex delete mode 100644 lib/image_plug/param_parser/twicpics/crop_parser.ex delete mode 100644 lib/image_plug/param_parser/twicpics/focus_parser.ex delete mode 100644 lib/image_plug/param_parser/twicpics/output_parser.ex delete mode 100644 lib/image_plug/param_parser/twicpics/scale_parser.ex rename test/{ => param_parser}/twicpics_test.exs (93%) diff --git a/lib/image_plug/param_parser/twicpics.ex b/lib/image_plug/param_parser/twicpics.ex deleted file mode 100644 index 36ce47e..0000000 --- a/lib/image_plug/param_parser/twicpics.ex +++ /dev/null @@ -1,66 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics do - @behaviour ImagePlug.ParamParser - - alias ImagePlug.ParamParser.Twicpics - - @transforms %{ - "crop" => ImagePlug.Transform.Crop, - "resize" => ImagePlug.Transform.Scale, - "focus" => ImagePlug.Transform.Focus, - "contain" => ImagePlug.Transform.Contain, - "output" => ImagePlug.Transform.Output - } - - @parsers %{ - ImagePlug.Transform.Crop => Twicpics.CropParser, - ImagePlug.Transform.Scale => Twicpics.ScaleParser, - ImagePlug.Transform.Focus => Twicpics.FocusParser, - ImagePlug.Transform.Contain => Twicpics.ContainParser, - ImagePlug.Transform.Output => Twicpics.OutputParser - } - - @impl ImagePlug.ParamParser - def parse(%Plug.Conn{} = conn) do - conn = Plug.Conn.fetch_query_params(conn) - - case conn.params do - %{"twic" => input} -> parse_string(input) - _ -> {:ok, []} - end - end - - def parse_string(input) do - case input do - "v1/" <> chain -> parse_chain(chain) - _ -> {:ok, []} - end - end - - # a `key=value` string followed by either a slash and a - # new key=value string or the end of the string using lookahead - @params_regex ~r/\/?([a-z]+)=(.+?(?=\/[a-z]+=|$))/ - - def parse_chain(chain_str) do - Regex.scan(@params_regex, chain_str, capture: :all_but_first) - |> Enum.reduce_while({:ok, []}, fn - [transform_name, params_str], {:ok, transforms_acc} - when is_map_key(@transforms, transform_name) -> - module = Map.get(@transforms, transform_name) - - case @parsers[module].parse(params_str) do - {:ok, parsed_params} -> - {:cont, {:ok, [{module, parsed_params} | transforms_acc]}} - - {:error, {:parameter_parse_error, input}} -> - {:halt, {:error, {:invalid_params, {module, "invalid input: #{input}"}}}} - end - - [transform_name, _params_str], acc -> - {:cont, [{:error, {:invalid_transform, transform_name}} | acc]} - end) - |> case do - {:ok, transforms} -> {:ok, Enum.reverse(transforms)} - other -> other - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/common.ex b/lib/image_plug/param_parser/twicpics/common.ex deleted file mode 100644 index bb16786..0000000 --- a/lib/image_plug/param_parser/twicpics/common.ex +++ /dev/null @@ -1,69 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.Common do - def with_parsed_units(units, fun) do - case parse_all_units(units) do - {:error, _} = error -> error - {:ok, units} -> fun.(units) - end - end - - def parse_all_units(units) do - reduced = - Enum.reduce_while(units, [], fn unit, acc -> - case parse_unit(unit) do - {:ok, parsed} -> {:cont, [parsed | acc]} - {:error, _} = error -> {:halt, error} - end - end) - - case reduced do - parsed when is_list(parsed) -> - {:ok, Enum.reverse(parsed)} - - {:error, _} = error -> - error - end - end - - def parse_unit(input) do - cond do - Regex.match?(~r/^\((.+)\/(.+)\)s$/, input) -> - [_, num1, num2] = Regex.run(~r/^\((.+)\/(.+)\)s$/, input) - - with {:ok, parsed_num1} <- parse_number(num1), - {:ok, parsed_num2} <- parse_number(num2) do - {:ok, {:scale, parsed_num1, parsed_num2}} - else - {:error, _} = error -> error - end - - Regex.match?(~r/^(.+)p$/, input) -> - [_, num] = Regex.run(~r/^(.+)p$/, input) - - case parse_number(num) do - {:ok, parsed} -> {:ok, {:pct, parsed}} - {:error, _} = error -> error - end - - true -> - parse_number(input) - end - end - - def parse_number(input) do - cond do - Regex.match?(~r/^\d+(\.(\d+|0e\d+))?$/, input) -> - if String.contains?(input, ".") do - {:ok, {:float, String.to_float(input)}} - else - {:ok, {:int, String.to_integer(input)}} - end - - Regex.match?(~r/^\((.+)\)$/, input) -> - [_, expr] = Regex.run(~r/^\((.+)\)$/, input) - {:ok, {:expr, expr}} - - true -> - {:error, {:invalid_number, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/contain_parser.ex b/lib/image_plug/param_parser/twicpics/contain_parser.ex deleted file mode 100644 index f4205ce..0000000 --- a/lib/image_plug/param_parser/twicpics/contain_parser.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.ContainParser do - import ImagePlug.ParamParser.Twicpics.Common - - alias ImagePlug.Transform.Contain.ContainParams - - @doc """ - Parses a string into a `ImagePlug.Transform.Contain.ContainParams` struct. - - Returns a `ImagePlug.Transform.Contain.ContainParams` struct. - - ## Syntax - - ``` - contain= - ``` - - ## Examples - - iex> ImagePlug.ParamParser.Twicpics.ContainParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Contain.ContainParams{width: {:int, 250}, height: {:float, 25.5}}} - """ - def parse(input) do - cond do - Regex.match?(~r/^(.+)x(.+)$/, input) -> - Regex.run(~r/^(.+)x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width, height] -> - {:ok, %ContainParams{width: width, height: height}} - end) - - true -> - {:error, {:parameter_parse_error, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/crop_parser.ex b/lib/image_plug/param_parser/twicpics/crop_parser.ex deleted file mode 100644 index 095119f..0000000 --- a/lib/image_plug/param_parser/twicpics/crop_parser.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.CropParser do - import ImagePlug.ParamParser.Twicpics.Common - - alias ImagePlug.Transform.Crop.CropParams - - @doc """ - Parses a string into a `ImagePlug.Transform.Crop.CropParams` struct. - - Returns a `ImagePlug.Transform.Crop.CropParams` struct. - - ## Format - - ``` - [@] - ``` - - ## Examples - - iex> ImagePlug.ParamParser.Twicpics.CropParser.parse("250x25p") - {:ok, %ImagePlug.Transform.Crop.CropParams{width: {:int, 250}, height: {:pct, {:int, 25}}, crop_from: :focus}} - - iex> ImagePlug.ParamParser.Twicpics.CropParser.parse("20px25@10x50.1p") - {:ok, %ImagePlug.Transform.Crop.CropParams{width: {:pct, {:int, 20}}, height: {:int, 25}, crop_from: %{left: {:int, 10}, top: {:pct, {:float, 50.1}}}}} - """ - def parse(input) do - cond do - Regex.match?(~r/^(.+)x(.+)@(.+)x(.+)$/, input) -> - Regex.run(~r/^(.+)x(.+)@(.+)x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width, height, left, top] -> - # TODO: Validate that width/height is strictly positive (> 0) - {:ok, %CropParams{width: width, height: height, crop_from: %{left: left, top: top}}} - end) - - Regex.match?(~r/^(.+)x(.+)$/, input) -> - Regex.run(~r/^(.+)x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width, height] -> - # TODO: Validate that width/height is strictly positive (> 0) - {:ok, %CropParams{width: width, height: height, crop_from: :focus}} - end) - - true -> - {:error, {:parameter_parse_error, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/focus_parser.ex b/lib/image_plug/param_parser/twicpics/focus_parser.ex deleted file mode 100644 index 8b106c1..0000000 --- a/lib/image_plug/param_parser/twicpics/focus_parser.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.FocusParser do - import ImagePlug.ParamParser.Twicpics.Common - - alias ImagePlug.Transform.Focus.FocusParams - - @doc """ - Parses a string into a `ImagePlug.Transform.Focus.FocusParams` struct. - - Returns a `ImagePlug.Transform.Focus.FocusParams` struct. - - ## Format - - ``` - focus= - focus= - ``` - - Note: `auto` is not supported at the moment. - - ## Examples - - iex> ImagePlug.ParamParser.Twicpics.FocusParser.parse("250x25.5") - {:ok, %ImagePlug.Transform.Focus.FocusParams{type: {:coordinate, {:int, 250}, {:float, 25.5}}}} - - iex> ImagePlug.ParamParser.Twicpics.FocusParser.parse("bottom-right") - {:ok, %ImagePlug.Transform.Focus.FocusParams{type: {:anchor, :right, :bottom}}} - """ - def parse(input) do - cond do - Regex.match?(~r/^(.+)x(.+)$/, input) -> - Regex.run(~r/^(.+)x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [left, top] -> - {:ok, %FocusParams{type: {:coordinate, left, top}}} - end) - - input == "center" -> - {:ok, %FocusParams{type: {:anchor, :center, :center}}} - - input == "bottom" -> - {:ok, %FocusParams{type: {:anchor, :center, :bottom}}} - - input == "bottom-left" -> - {:ok, %FocusParams{type: {:anchor, :left, :bottom}}} - - input == "bottom-right" -> - {:ok, %FocusParams{type: {:anchor, :right, :bottom}}} - - input == "left" -> - {:ok, %FocusParams{type: {:anchor, :left, :center}}} - - input == "top" -> - {:ok, %FocusParams{type: {:anchor, :center, :top}}} - - input == "top-left" -> - {:ok, %FocusParams{type: {:anchor, :left, :top}}} - - input == "top-right" -> - {:ok, %FocusParams{type: {:anchor, :right, :top}}} - - input == "right" -> - {:ok, %FocusParams{type: {:anchor, :right, :center}}} - - true -> - {:error, {:parameter_parse_error, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/output_parser.ex b/lib/image_plug/param_parser/twicpics/output_parser.ex deleted file mode 100644 index aa185bc..0000000 --- a/lib/image_plug/param_parser/twicpics/output_parser.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.OutputParser do - alias ImagePlug.Transform.Output.OutputParams - - @doc """ - Parses a string into a `ImagePlug.Transform.Output.OutputParams` struct. - - Returns a `ImagePlug.Transform.Output.OutputParams` struct. - - ## Examples - - iex> ImagePlug.ParamParser.Twicpics.OutputParser.parse("avif") - {:ok, %ImagePlug.Transform.Output.OutputParams{format: :avif}} - """ - def parse(input) do - case input do - "auto" -> {:ok, %OutputParams{format: :auto}} - "avif" -> {:ok, %OutputParams{format: :avif}} - "webp" -> {:ok, %OutputParams{format: :webp}} - "jpeg" -> {:ok, %OutputParams{format: :jpeg}} - "png" -> {:ok, %OutputParams{format: :png}} - "blurhash" -> {:ok, %OutputParams{format: :blurhash}} - _ -> {:error, {:parameter_parse_error, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics/scale_parser.ex b/lib/image_plug/param_parser/twicpics/scale_parser.ex deleted file mode 100644 index 53f8cff..0000000 --- a/lib/image_plug/param_parser/twicpics/scale_parser.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule ImagePlug.ParamParser.Twicpics.ScaleParser do - import ImagePlug.ParamParser.Twicpics.Common - - alias ImagePlug.Transform.Scale.ScaleParams - - @doc """ - Parses a string into a `ImagePlug.Transform.Scale.ScaleParams` struct. - - Returns a `ImagePlug.Transform.Scale.ScaleParams` struct. - - ## Format - - ``` - [x] - ``` - - ## Units - - Type | Format - --------- | ------------ - `pixel` | `` - `percent` | `p` - `auto` | `-` - - Only one of the dimensions can be set to `auto`. - - ## Examples - - iex> ImagePlug.ParamParser.Twicpics.ScaleParser.parse("250x25p") - {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:int, 250}, height: {:pct, {:int, 25}}}}} - - iex> ImagePlug.ParamParser.Twicpics.ScaleParser.parse("-x25p") - {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: :auto, height: {:pct, {:int, 25}}}}} - - iex> ImagePlug.ParamParser.Twicpics.ScaleParser.parse("50.5px-") - {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:pct, {:float, 50.5}}, height: :auto}}} - - iex> ImagePlug.ParamParser.Twicpics.ScaleParser.parse("50.5") - {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:float, 50.5}, height: :auto}}} - - iex> ImagePlug.ParamParser.Twicpics.ScaleParser.parse("50p") - {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:pct, {:int, 50}}, height: :auto}}} - """ - def parse(input) do - cond do - Regex.match?(~r/^(.+)x-$/, input) -> - Regex.run(~r/^(.+)x-$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width] -> - {:ok, %ScaleParams{method: %ScaleParams.Dimensions{width: width, height: :auto}}} - end) - - Regex.match?(~r/^-x(.+)$/, input) -> - Regex.run(~r/^-x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [height] -> - {:ok, %ScaleParams{method: %ScaleParams.Dimensions{width: :auto, height: height}}} - end) - - Regex.match?(~r/^(.+)x(.+)$/, input) -> - Regex.run(~r/^(.+)x(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width, height] -> - {:ok, %ScaleParams{method: %ScaleParams.Dimensions{width: width, height: height}}} - end) - - Regex.match?(~r/^(.+):(.+)$/, input) -> - Regex.run(~r/^(.+):(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [ar_width, ar_height] -> - {:ok, - %ScaleParams{ - method: %ScaleParams.AspectRatio{aspect_ratio: {:ratio, ar_width, ar_height}} - }} - end) - - Regex.match?(~r/^(.+)$/, input) -> - Regex.run(~r/^(.+)$/, input, capture: :all_but_first) - |> with_parsed_units(fn [width] -> - {:ok, %ScaleParams{method: %ScaleParams.Dimensions{width: width, height: :auto}}} - end) - - true -> - {:error, {:parameter_parse_error, input}} - end - end -end diff --git a/lib/image_plug/param_parser/twicpics_v2.ex b/lib/image_plug/param_parser/twicpics_v2.ex index 3fc405f..b37ecc8 100644 --- a/lib/image_plug/param_parser/twicpics_v2.ex +++ b/lib/image_plug/param_parser/twicpics_v2.ex @@ -8,7 +8,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2 do "scale" => {ImagePlug.Transform.Scale, TwicpicsV2.Transform.ScaleParser}, "focus" => {ImagePlug.Transform.Focus, TwicpicsV2.Transform.FocusParser}, "contain" => {ImagePlug.Transform.Contain, TwicpicsV2.Transform.ContainParser}, - "output" => {ImagePlug.Transform.Output, TwicpicsV2.Transform.OutputParser}, + "output" => {ImagePlug.Transform.Output, TwicpicsV2.Transform.OutputParser} } @transform_keys Map.keys(@transforms) @@ -31,7 +31,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2 do end end - def parse_string(input, pos_offset) do + def parse_string(input, pos_offset \\ 0) do case input do @query_param_prefix <> chain -> pos_offset = pos_offset + String.length(@query_param_prefix) diff --git a/test/param_parser/twicpics_parser_test.exs b/test/param_parser/twicpics_parser_test.exs index 7dbc285..92a0527 100644 --- a/test/param_parser/twicpics_parser_test.exs +++ b/test/param_parser/twicpics_parser_test.exs @@ -4,36 +4,39 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do import ImagePlug.TestSupport - alias ImagePlug.ParamParser.Twicpics + alias ImagePlug.ParamParser.TwicpicsV2 alias ImagePlug.Transform.Crop alias ImagePlug.Transform.Scale alias ImagePlug.Transform.Focus alias ImagePlug.Transform.Contain - doctest ImagePlug.ParamParser.Twicpics.CropParser - doctest ImagePlug.ParamParser.Twicpics.ScaleParser - doctest ImagePlug.ParamParser.Twicpics.FocusParser - doctest ImagePlug.ParamParser.Twicpics.ContainParser - doctest ImagePlug.ParamParser.Twicpics.OutputParser + doctest ImagePlug.ParamParser.TwicpicsV2.Transform.CropParser + doctest ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser + doctest ImagePlug.ParamParser.TwicpicsV2.Transform.FocusParser + doctest ImagePlug.ParamParser.TwicpicsV2.Transform.ContainParser + doctest ImagePlug.ParamParser.TwicpicsV2.Transform.OutputParser - defp unit_str({:int, v}), do: "#{v}" - defp unit_str({:float, v}), do: "#{v}" - defp unit_str({:scale, unit_a, unit_b}), do: "(#{unit_str(unit_a)}/#{unit_str(unit_b)})s" - defp unit_str({:pct, unit}), do: "#{unit_str(unit)}p" + defp length_str({:pixels, unit}), do: "#{unit}" + defp length_str({:scale, unit_a, unit_b}), do: "(#{unit_a}/#{unit_b})s" + defp length_str({:percent, unit}), do: "#{unit}p" + + defp to_result({:pixels, unit}), do: {:pixels, unit} + defp to_result({:scale, unit_a, unit_b}), do: {:scale, unit_a / unit_b} + defp to_result({:percent, unit}), do: {:percent, unit} test "crop params parser" do check all width <- random_root_unit(), height <- random_root_unit(), crop_from <- crop_from() do - str_params = "#{unit_str(width)}x#{unit_str(height)}" + str_params = "#{length_str(width)}x#{length_str(height)}" str_params = case crop_from do :focus -> str_params - %{left: left, top: top} -> "#{str_params}@#{unit_str(left)}x#{unit_str(top)}" + %{left: left, top: top} -> "#{str_params}@#{length_str(left)}x#{length_str(top)}" end - parsed = Twicpics.CropParser.parse(str_params) + parsed = TwicpicsV2.Transform.CropParser.parse(str_params) assert {:ok, %Crop.CropParams{ @@ -42,7 +45,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do crop_from: case crop_from do :focus -> :focus - %{left: left, top: top} -> %{left: left, top: top} + %{left: left, top: top} -> %{left: to_result(left), top: to_result(top)} end }} == parsed @@ -63,23 +66,32 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do check all focus_type <- focus_type() do str_params = case focus_type do - {:coordinate, left, top} -> "#{unit_str(left)}x#{unit_str(top)}" + {:coordinate, left, top} -> "#{length_str(left)}x#{length_str(top)}" {:anchor, _, _} = anchor -> anchor_to_str(anchor) end - {:ok, parsed} = Twicpics.FocusParser.parse(str_params) - assert %Focus.FocusParams{type: focus_type} == parsed + {:ok, parsed} = TwicpicsV2.Transform.FocusParser.parse(str_params) + + case focus_type do + {:coordinate, left, top} -> + assert %Focus.FocusParams{type: {:coordinate, to_result(left), to_result(top)}} == + parsed + + {:anchor, _, _} -> + assert %Focus.FocusParams{type: focus_type} == parsed + end end end test "scale params parser" do check all {type, params} <- one_of([ - tuple({constant(:auto_width), tuple({random_root_unit()})}), - tuple({constant(:auto_height), tuple({random_root_unit()})}), - tuple({constant(:simple), tuple({random_root_unit()})}), + tuple({constant(:auto_width), tuple({random_root_unit(min: 1)})}), + tuple({constant(:auto_height), tuple({random_root_unit(min: 1)})}), + tuple({constant(:simple), tuple({random_root_unit(min: 1)})}), tuple( - {constant(:width_and_height), tuple({random_root_unit(), random_root_unit()})} + {constant(:width_and_height), + tuple({random_root_unit(min: 1), random_root_unit(min: 1)})} ), tuple( {constant(:aspect_ratio), tuple({random_root_unit(), random_root_unit()})} @@ -88,48 +100,55 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do {str_params, expected} = case {type, params} do {:auto_width, {height}} -> - {"-x#{unit_str(height)}", + {"-x#{length_str(height)}", %Scale.ScaleParams{ - method: %Scale.ScaleParams.Dimensions{width: :auto, height: height} + method: %Scale.ScaleParams.Dimensions{width: :auto, height: to_result(height)} }} {:auto_height, {width}} -> - {"#{unit_str(width)}x-", + {"#{length_str(width)}x-", %Scale.ScaleParams{ - method: %Scale.ScaleParams.Dimensions{width: width, height: :auto} + method: %Scale.ScaleParams.Dimensions{width: to_result(width), height: :auto} }} {:simple, {width}} -> - {"#{unit_str(width)}", + {"#{length_str(width)}", %Scale.ScaleParams{ - method: %Scale.ScaleParams.Dimensions{width: width, height: :auto} + method: %Scale.ScaleParams.Dimensions{width: to_result(width), height: :auto} }} {:width_and_height, {width, height}} -> - {"#{unit_str(width)}x#{unit_str(height)}", + {"#{length_str(width)}x#{length_str(height)}", %Scale.ScaleParams{ - method: %Scale.ScaleParams.Dimensions{width: width, height: height} + method: %Scale.ScaleParams.Dimensions{ + width: to_result(width), + height: to_result(height) + } }} {:aspect_ratio, {ar_w, ar_h}} -> - {"#{unit_str(ar_w)}:#{unit_str(ar_h)}", + {"#{length_str(ar_w)}:#{length_str(ar_h)}", %Scale.ScaleParams{ - method: %Scale.ScaleParams.AspectRatio{aspect_ratio: {:ratio, ar_w, ar_h}} + method: %Scale.ScaleParams.AspectRatio{ + aspect_ratio: {:ratio, to_result(ar_w), to_result(ar_h)} + } }} end - {:ok, parsed} = Twicpics.ScaleParser.parse(str_params) + {:ok, parsed} = TwicpicsV2.Transform.ScaleParser.parse(str_params) assert parsed == expected end end test "contain params parser" do - check all width <- random_root_unit(), - height <- random_root_unit() do - str_params = "#{unit_str(width)}x#{unit_str(height)}" - parsed = Twicpics.ContainParser.parse(str_params) - assert {:ok, %Contain.ContainParams{width: width, height: height}} == parsed + check all width <- random_root_unit(min: 1), + height <- random_root_unit(min: 1) do + str_params = "#{length_str(width)}x#{length_str(height)}" + parsed = TwicpicsV2.Transform.ContainParser.parse(str_params) + + assert {:ok, %Contain.ContainParams{width: to_result(width), height: to_result(height)}} == + parsed end end end diff --git a/test/twicpics_test.exs b/test/param_parser/twicpics_test.exs similarity index 93% rename from test/twicpics_test.exs rename to test/param_parser/twicpics_test.exs index c6b9904..1f1d3cb 100644 --- a/test/twicpics_test.exs +++ b/test/param_parser/twicpics_test.exs @@ -7,10 +7,10 @@ defmodule ImagePlug.TwicpicsTest do alias ImagePlug.Transform alias ImagePlug.TransformChain alias ImagePlug.TransformState - alias ImagePlug.ParamParser.Twicpics + alias ImagePlug.ParamParser.TwicpicsV2 test "parse from string" do - result = Twicpics.parse_string("v1/focus=(1/2)sx(2/3)s/crop=100x100/resize=200/output=avif") + result = TwicpicsV2.parse_string("v1/focus=(1/2)sx(2/3)s/crop=100x100/resize=200/output=avif") assert result == {:ok, diff --git a/test/param_parser/twicpics_v2/kv_parser_test.exs b/test/param_parser/twicpics_v2/kv_parser_test.exs index 63047aa..fa0f4aa 100644 --- a/test/param_parser/twicpics_v2/kv_parser_test.exs +++ b/test/param_parser/twicpics_v2/kv_parser_test.exs @@ -1,11 +1,13 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParserTest do +defmodule ImagePlug.ParamParser.TwicpicsV2.KVParserTest do use ExUnit.Case, async: true use ExUnitProperties - alias ImagePlug.ParamParser.TwicpicsV2.KeyValueParser + alias ImagePlug.ParamParser.TwicpicsV2.KVParser + + @keys ~w(k1 k2 k3 k1 k20 k300 k4000) test "successful parse output returns correct key positions" do - assert KeyValueParser.parse("k1=v1/k2=v2/k3=v3") == + assert KVParser.parse("k1=v1/k2=v2/k3=v3", @keys) == {:ok, [ {"k1", "v1", 0}, @@ -13,7 +15,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParserTest do {"k3", "v3", 12} ]} - assert KeyValueParser.parse("k1=v1/k20=v20/k300=v300/k4000=v4000") == + assert KVParser.parse("k1=v1/k20=v20/k300=v300/k4000=v4000", @keys) == {:ok, [ {"k1", "v1", 0}, @@ -24,14 +26,14 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KeyValueParserTest do end test ":expected_eq error returns correct position" do - assert KeyValueParser.parse("k1=v1/k20=v20/k300") == {:error, {:expected_eq, pos: 19}} + assert KVParser.parse("k1=v1/k20=v20/k300", @keys) == {:error, {:expected_eq, pos: 19}} end test ":expected_key error returns correct position" do - assert KeyValueParser.parse("k1=v1/k20=v20/") == {:error, {:expected_key, pos: 14}} + assert KVParser.parse("k1=v1/k20=v20/", @keys) == {:error, {:expected_key, pos: 14}} end test ":expected_value error returns correct position" do - assert KeyValueParser.parse("k1=v1/k20=") == {:error, {:expected_value, pos: 10}} + assert KVParser.parse("k1=v1/k20=", @keys) == {:error, {:expected_value, pos: 10}} end end diff --git a/test/support/image_plug_test_support.ex b/test/support/image_plug_test_support.ex index 2573813..9cbe975 100644 --- a/test/support/image_plug_test_support.ex +++ b/test/support/image_plug_test_support.ex @@ -6,8 +6,8 @@ defmodule ImagePlug.TestSupport do max = Keyword.get(opts, :max, 9999) one_of([ - tuple({constant(:int), integer(min..max)}), - tuple({constant(:float), float(min: min, max: max)}) + integer(min..max), + float(min: min, max: max) ]) end @@ -24,13 +24,13 @@ defmodule ImagePlug.TestSupport do denominator_max = Keyword.get(opts, :denominator_min, 9999) one_of([ - tuple({constant(:int), integer(int_min..int_max)}), - tuple({constant(:float), float(min: float_min, max: float_max)}), + tuple({constant(:pixels), integer(int_min..int_max)}), + tuple({constant(:pixels), float(min: float_min, max: float_max)}), tuple( {constant(:scale), random_base_unit(min: numerator_min, max: numerator_max), random_base_unit(min: 1, max: denominator_max)} ), - tuple({constant(:pct), random_base_unit(min: pct_min, max: pct_max)}) + tuple({constant(:percent), random_base_unit(min: pct_min, max: pct_max)}) ]) end From b58275aee066e0cf2f35e6c7cb17730d82a8d575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 20:16:56 +0100 Subject: [PATCH 31/35] parse e notation numbers --- .../param_parser/twicpics_v2/number_parser.ex | 97 ++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex index 663feb1..26a6356 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics_v2/number_parser.ex @@ -32,12 +32,25 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do defp mk_float_open(value, left_pos, right_pos), do: {:float_open, value, left_pos, right_pos} + defp mk_exp_open(value, left_pos, right_pos), + do: {:exp_open, value, left_pos, right_pos} + + defp mk_exp(value, left_pos, right_pos), + do: {:exp, value, left_pos, right_pos} + defp mk_float(value, left_pos, right_pos), do: {:float, value, left_pos, right_pos} defp mk_op(type, pos), do: {:op, type, pos} + defp sci_to_num(sci) do + [base_str, exponent_str] = String.split(sci, ~r/e/i) + {base, ""} = Float.parse(base_str) + {exponent, ""} = Integer.parse(exponent_str) + base * :math.pow(10, exponent) + end + def parse(input, pos_offset \\ 0) do case do_parse(%State{input: input, pos: pos_offset}) do {:ok, tokens} -> @@ -47,6 +60,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do |> Enum.map(fn {:int, int, pos_b, pos_e} -> mk_int(String.to_integer(int), pos_b, pos_e) {:float, int, pos_b, pos_e} -> mk_float(String.to_float(int), pos_b, pos_e) + {:exp, exp_num, pos_b, pos_e} -> mk_float(sci_to_num(exp_num), pos_b, pos_e) other -> other end)} @@ -66,7 +80,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do # # the following states are legal end of input locations as long as - # we're not inside a parentheses: :int, :float and :right_paren + # we're not inside a parentheses: :int, :float, :exp and :right_paren # defp do_parse(%State{input: "", tokens: [{:int, _, _, _} | _] = tokens} = state) when state.paren_count == 0, @@ -76,6 +90,10 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do when state.paren_count == 0, do: {:ok, tokens} + defp do_parse(%State{input: "", tokens: [{:exp, _, _, _} | _] = tokens} = state) + when state.paren_count == 0, + do: {:ok, tokens} + defp do_parse(%State{input: "", tokens: [{:right_paren, _} | _] = tokens} = state) when state.paren_count == 0, do: {:ok, tokens} @@ -176,6 +194,9 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do char == ?. -> replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) + char == ?e or char == ?E -> + replace_token(state, mk_exp_open(cur_val <> <>, t_pos_b, state.pos)) + true -> Utils.unexpected_value_error(state.pos, ["[0-9]", "."], <>) end @@ -195,6 +216,9 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do char == ?. -> replace_token(state, mk_float_open(cur_val <> <>, t_pos_b, state.pos)) + char == ?e or char == ?E -> + replace_token(state, mk_exp_open(cur_val <> <>, t_pos_b, state.pos)) + char in @op_tokens -> add_token(state, mk_op(<>, state.pos)) @@ -257,6 +281,9 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do char in ?0..?9 -> replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) + char == ?e or char == ?E -> + replace_token(state, mk_exp_open(cur_val <> <>, t_pos_b, state.pos)) + true -> Utils.unexpected_value_error(state.pos, ["[0-9]"], <>) end @@ -273,6 +300,9 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do char in ?0..?9 -> replace_token(state, mk_float(cur_val <> <>, t_pos_b, state.pos)) + char == ?e or char == ?E -> + replace_token(state, mk_exp_open(cur_val <> <>, t_pos_b, state.pos)) + char in @op_tokens -> add_token(state, mk_op(<>, state.pos)) @@ -288,12 +318,75 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do end end - # we hit eoi while on an :int token, and we're in a parentheses + # we hit eoi while on an :float token, and we're in a parentheses defp do_parse(%State{input: "", tokens: [{:float, _, _, _} | _]} = state) when state.paren_count > 0 do Utils.unexpected_value_error(state.pos, ["[0-9]", "+", "-", "*", "/", ")"], :eoi) end + # + # prev token: :exp_open + # - at this point it's a not a valid exponential number + # + + defp do_parse( + %State{ + input: <>, + tokens: [{:exp_open, cur_val, t_pos_b, _} | _] + } = state + ) do + cond do + char in ?0..?9 or char == ?- -> + replace_token(state, mk_exp(cur_val <> <>, t_pos_b, state.pos)) + + true -> + Utils.unexpected_value_error(state.pos, ["[0-9]", "-"], <>) + end + end + + # + # prev token: :exp + # - we have a valid number in exponential notation + # + + defp do_parse( + %State{ + input: <>, + tokens: [{:exp, cur_val, t_pos_b, _} | _] + } = state + ) + when state.paren_count == 0 do + cond do + char in ?0..?9 -> + replace_token(state, mk_exp(cur_val <> <>, t_pos_b, state.pos)) + + true -> + Utils.unexpected_value_error(state.pos, ["[0-9]"], <>) + end + end + + defp do_parse( + %State{ + input: <>, + tokens: [{:exp, cur_val, t_pos_b, _} | _] + } = state + ) + when state.paren_count > 0 do + cond do + char in ?0..?9 -> + replace_token(state, mk_exp(cur_val <> <>, t_pos_b, state.pos)) + + char in @op_tokens -> + add_token(state, mk_op(<>, state.pos)) + + char == ?) -> + add_right_paren(state) + + true -> + Utils.unexpected_value_error(state.pos, ["[0-9]", "-"], <>) + end + end + # # prev token: :op # From fb31d7eb3502af6a3f9870c017cd173747f2ae8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 22:26:26 +0100 Subject: [PATCH 32/35] fix tests --- lib/image_plug/param_parser/twicpics_v2.ex | 2 +- lib/image_plug/transform_chain.ex | 4 ++-- test/param_parser/twicpics_parser_test.exs | 14 +++++++------- test/param_parser/twicpics_test.exs | 10 +++++----- test/param_parser/twicpics_v2/kv_parser_test.exs | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/image_plug/param_parser/twicpics_v2.ex b/lib/image_plug/param_parser/twicpics_v2.ex index b37ecc8..f267950 100644 --- a/lib/image_plug/param_parser/twicpics_v2.ex +++ b/lib/image_plug/param_parser/twicpics_v2.ex @@ -5,7 +5,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2 do @transforms %{ "crop" => {ImagePlug.Transform.Crop, TwicpicsV2.Transform.CropParser}, - "scale" => {ImagePlug.Transform.Scale, TwicpicsV2.Transform.ScaleParser}, + "resize" => {ImagePlug.Transform.Scale, TwicpicsV2.Transform.ScaleParser}, "focus" => {ImagePlug.Transform.Focus, TwicpicsV2.Transform.FocusParser}, "contain" => {ImagePlug.Transform.Contain, TwicpicsV2.Transform.ContainParser}, "output" => {ImagePlug.Transform.Output, TwicpicsV2.Transform.OutputParser} diff --git a/lib/image_plug/transform_chain.ex b/lib/image_plug/transform_chain.ex index d13b20e..a307289 100644 --- a/lib/image_plug/transform_chain.ex +++ b/lib/image_plug/transform_chain.ex @@ -10,8 +10,8 @@ defmodule ImagePlug.TransformChain do ## Examples iex> chain = [ - ...> {ImagePlug.Transform.Focus, %ImagePlug.Transform.Focus.FocusParams{type: {:coordinate, {:int, 20}, {:int, 30}}}}, - ...> {ImagePlug.Transform.Crop, %ImagePlug.Transform.Crop.CropParams{width: {:int, 100}, height: {:int, 150}, crop_from: :focus}} + ...> {ImagePlug.Transform.Focus, %ImagePlug.Transform.Focus.FocusParams{type: {:coordinate, {:pixels, 20}, {:pixels, 30}}}}, + ...> {ImagePlug.Transform.Crop, %ImagePlug.Transform.Crop.CropParams{width: {:pixels, 100}, height: {:pixels, 150}, crop_from: :focus}} ...> ] ...> {:ok, empty_image} = Image.new(500, 500) ...> initial_state = %ImagePlug.TransformState{image: empty_image} diff --git a/test/param_parser/twicpics_parser_test.exs b/test/param_parser/twicpics_parser_test.exs index 92a0527..b188a3a 100644 --- a/test/param_parser/twicpics_parser_test.exs +++ b/test/param_parser/twicpics_parser_test.exs @@ -25,8 +25,8 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do defp to_result({:percent, unit}), do: {:percent, unit} test "crop params parser" do - check all width <- random_root_unit(), - height <- random_root_unit(), + check all width <- random_root_unit(min: 1), + height <- random_root_unit(min: 1), crop_from <- crop_from() do str_params = "#{length_str(width)}x#{length_str(height)}" @@ -40,8 +40,8 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do assert {:ok, %Crop.CropParams{ - width: width, - height: height, + width: to_result(width), + height: to_result(height), crop_from: case crop_from do :focus -> :focus @@ -94,7 +94,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do tuple({random_root_unit(min: 1), random_root_unit(min: 1)})} ), tuple( - {constant(:aspect_ratio), tuple({random_root_unit(), random_root_unit()})} + {constant(:aspect_ratio), tuple({random_base_unit(min: 1), random_base_unit(min: 1)})} ) ]) do {str_params, expected} = @@ -127,10 +127,10 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do }} {:aspect_ratio, {ar_w, ar_h}} -> - {"#{length_str(ar_w)}:#{length_str(ar_h)}", + {"#{ar_w}:#{ar_h}", %Scale.ScaleParams{ method: %Scale.ScaleParams.AspectRatio{ - aspect_ratio: {:ratio, to_result(ar_w), to_result(ar_h)} + aspect_ratio: {:ratio, ar_w, ar_h} } }} end diff --git a/test/param_parser/twicpics_test.exs b/test/param_parser/twicpics_test.exs index 1f1d3cb..859fd59 100644 --- a/test/param_parser/twicpics_test.exs +++ b/test/param_parser/twicpics_test.exs @@ -19,20 +19,20 @@ defmodule ImagePlug.TwicpicsTest do %Transform.Focus.FocusParams{ type: { :coordinate, - {:scale, {:int, 1}, {:int, 2}}, - {:scale, {:int, 2}, {:int, 3}} + {:scale, 1/2}, + {:scale, 2/3} } }}, {Transform.Crop, %Transform.Crop.CropParams{ - width: {:int, 100}, - height: {:int, 100}, + width: {:pixels, 100}, + height: {:pixels, 100}, crop_from: :focus }}, {Transform.Scale, %Transform.Scale.ScaleParams{ method: %Transform.Scale.ScaleParams.Dimensions{ - width: {:int, 200}, + width: {:pixels, 200}, height: :auto } }}, diff --git a/test/param_parser/twicpics_v2/kv_parser_test.exs b/test/param_parser/twicpics_v2/kv_parser_test.exs index fa0f4aa..e33ab66 100644 --- a/test/param_parser/twicpics_v2/kv_parser_test.exs +++ b/test/param_parser/twicpics_v2/kv_parser_test.exs @@ -25,15 +25,15 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KVParserTest do ]} end - test ":expected_eq error returns correct position" do - assert KVParser.parse("k1=v1/k20=v20/k300", @keys) == {:error, {:expected_eq, pos: 19}} + test "error returns correct position when missing =" do + assert KVParser.parse("k1=v1/k20=v20/k300", @keys) == {:error, {:unexpected_char, [{:pos, 18}, {:expected, ["="]}, {:found, :eoi}]}} end - test ":expected_key error returns correct position" do + test "expected key error returns correct position" do assert KVParser.parse("k1=v1/k20=v20/", @keys) == {:error, {:expected_key, pos: 14}} end - test ":expected_value error returns correct position" do + test "expected value error returns correct position" do assert KVParser.parse("k1=v1/k20=", @keys) == {:error, {:expected_value, pos: 10}} end end From 7a792611dee8f1fabd64d63c176482140f8280a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 22:27:30 +0100 Subject: [PATCH 33/35] rename TwicpicsV2 -> Twicpics --- .../{twicpics_v2.ex => twicpics.ex} | 16 ++++++------- .../arithmetic_parser.ex | 4 ++-- .../coordinates_parser.ex | 6 ++--- .../{twicpics_v2 => twicpics}/formatters.ex | 2 +- .../{twicpics_v2 => twicpics}/kv_parser.ex | 4 ++-- .../length_parser.ex | 8 +++---- .../number_parser.ex | 4 ++-- .../{twicpics_v2 => twicpics}/ratio_parser.ex | 8 +++---- .../{twicpics_v2 => twicpics}/size_parser.ex | 6 ++--- .../transform/contain_parser.ex | 8 +++---- .../transform/crop_parser.ex | 8 +++---- .../transform/focus_parser.ex | 6 ++--- .../transform/output_parser.ex | 6 ++--- .../transform/scale_parser.ex | 8 +++---- .../{twicpics_v2 => twicpics}/utils.ex | 2 +- lib/simple_server.ex | 2 +- test/param_parser/twicpics_parser_test.exs | 23 ++++++++++--------- test/param_parser/twicpics_test.exs | 8 +++---- .../twicpics_v2/kv_parser_test.exs | 7 +++--- .../twicpics_v2/number_parser_test.exs | 4 ++-- test/param_parser/twicpics_v2/utils_test.exs | 4 ++-- 21 files changed, 73 insertions(+), 71 deletions(-) rename lib/image_plug/param_parser/{twicpics_v2.ex => twicpics.ex} (73%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/arithmetic_parser.ex (96%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/coordinates_parser.ex (89%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/formatters.ex (96%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/kv_parser.ex (95%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/length_parser.ex (72%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/number_parser.ex (99%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/ratio_parser.ex (87%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/size_parser.ex (91%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/transform/contain_parser.ex (60%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/transform/crop_parser.ex (83%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/transform/focus_parser.ex (87%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/transform/output_parser.ex (74%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/transform/scale_parser.ex (81%) rename lib/image_plug/param_parser/{twicpics_v2 => twicpics}/utils.ex (96%) diff --git a/lib/image_plug/param_parser/twicpics_v2.ex b/lib/image_plug/param_parser/twicpics.ex similarity index 73% rename from lib/image_plug/param_parser/twicpics_v2.ex rename to lib/image_plug/param_parser/twicpics.ex index f267950..20a339b 100644 --- a/lib/image_plug/param_parser/twicpics_v2.ex +++ b/lib/image_plug/param_parser/twicpics.ex @@ -1,14 +1,14 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2 do +defmodule ImagePlug.ParamParser.Twicpics do @behaviour ImagePlug.ParamParser - alias ImagePlug.ParamParser.TwicpicsV2 + alias ImagePlug.ParamParser.Twicpics @transforms %{ - "crop" => {ImagePlug.Transform.Crop, TwicpicsV2.Transform.CropParser}, - "resize" => {ImagePlug.Transform.Scale, TwicpicsV2.Transform.ScaleParser}, - "focus" => {ImagePlug.Transform.Focus, TwicpicsV2.Transform.FocusParser}, - "contain" => {ImagePlug.Transform.Contain, TwicpicsV2.Transform.ContainParser}, - "output" => {ImagePlug.Transform.Output, TwicpicsV2.Transform.OutputParser} + "crop" => {ImagePlug.Transform.Crop, Twicpics.Transform.CropParser}, + "resize" => {ImagePlug.Transform.Scale, Twicpics.Transform.ScaleParser}, + "focus" => {ImagePlug.Transform.Focus, Twicpics.Transform.FocusParser}, + "contain" => {ImagePlug.Transform.Contain, Twicpics.Transform.ContainParser}, + "output" => {ImagePlug.Transform.Output, Twicpics.Transform.OutputParser} } @transform_keys Map.keys(@transforms) @@ -43,7 +43,7 @@ defmodule ImagePlug.ParamParser.TwicpicsV2 do end def parse_chain(chain_str, pos_offset) do - case TwicpicsV2.KVParser.parse(chain_str, @transform_keys, pos_offset) do + case Twicpics.KVParser.parse(chain_str, @transform_keys, pos_offset) do {:ok, kv_params} -> Enum.reduce_while(kv_params, {:ok, []}, fn {transform_name, params_str, pos}, {:ok, transforms_acc} -> diff --git a/lib/image_plug/param_parser/twicpics_v2/arithmetic_parser.ex b/lib/image_plug/param_parser/twicpics/arithmetic_parser.ex similarity index 96% rename from lib/image_plug/param_parser/twicpics_v2/arithmetic_parser.ex rename to lib/image_plug/param_parser/twicpics/arithmetic_parser.ex index 782a760..d621f70 100644 --- a/lib/image_plug/param_parser/twicpics_v2/arithmetic_parser.ex +++ b/lib/image_plug/param_parser/twicpics/arithmetic_parser.ex @@ -1,5 +1,5 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.ArithmeticParser do + alias ImagePlug.ParamParser.Twicpics.Utils @type token :: {:int, integer} | {:float, float} | {:op, binary} | :left_paren | :right_paren @type expr :: {:int, integer} | {:float, float} | {:op, binary, expr(), expr()} diff --git a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex b/lib/image_plug/param_parser/twicpics/coordinates_parser.ex similarity index 89% rename from lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex rename to lib/image_plug/param_parser/twicpics/coordinates_parser.ex index f008b1c..a998c1d 100644 --- a/lib/image_plug/param_parser/twicpics_v2/coordinates_parser.ex +++ b/lib/image_plug/param_parser/twicpics/coordinates_parser.ex @@ -1,6 +1,6 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser do - alias ImagePlug.ParamParser.TwicpicsV2.LengthParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.CoordinatesParser do + alias ImagePlug.ParamParser.Twicpics.LengthParser + alias ImagePlug.ParamParser.Twicpics.Utils def parse(input, pos_offset \\ 0) do case String.split(input, "x", parts: 2) do diff --git a/lib/image_plug/param_parser/twicpics_v2/formatters.ex b/lib/image_plug/param_parser/twicpics/formatters.ex similarity index 96% rename from lib/image_plug/param_parser/twicpics_v2/formatters.ex rename to lib/image_plug/param_parser/twicpics/formatters.ex index a4c8f5f..1a8f50d 100644 --- a/lib/image_plug/param_parser/twicpics_v2/formatters.ex +++ b/lib/image_plug/param_parser/twicpics/formatters.ex @@ -1,4 +1,4 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Formatters do +defmodule ImagePlug.ParamParser.Twicpics.Formatters do defp format_char(:eoi), do: "end of input" defp format_char(other), do: other diff --git a/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex b/lib/image_plug/param_parser/twicpics/kv_parser.ex similarity index 95% rename from lib/image_plug/param_parser/twicpics_v2/kv_parser.ex rename to lib/image_plug/param_parser/twicpics/kv_parser.ex index e30945d..4c822c1 100644 --- a/lib/image_plug/param_parser/twicpics_v2/kv_parser.ex +++ b/lib/image_plug/param_parser/twicpics/kv_parser.ex @@ -1,5 +1,5 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.KVParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.KVParser do + alias ImagePlug.ParamParser.Twicpics.Utils def parse(input, valid_keys, pos_offset \\ 0) do case parse_pairs(input, [], valid_keys, pos_offset) do diff --git a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex b/lib/image_plug/param_parser/twicpics/length_parser.ex similarity index 72% rename from lib/image_plug/param_parser/twicpics_v2/length_parser.ex rename to lib/image_plug/param_parser/twicpics/length_parser.ex index c097d2a..50701f3 100644 --- a/lib/image_plug/param_parser/twicpics_v2/length_parser.ex +++ b/lib/image_plug/param_parser/twicpics/length_parser.ex @@ -1,7 +1,7 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.LengthParser do - alias ImagePlug.ParamParser.TwicpicsV2.NumberParser - alias ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.LengthParser do + alias ImagePlug.ParamParser.Twicpics.NumberParser + alias ImagePlug.ParamParser.Twicpics.ArithmeticParser + alias ImagePlug.ParamParser.Twicpics.Utils def parse(input, pos_offset \\ 0) do {type, num_str} = diff --git a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex b/lib/image_plug/param_parser/twicpics/number_parser.ex similarity index 99% rename from lib/image_plug/param_parser/twicpics_v2/number_parser.ex rename to lib/image_plug/param_parser/twicpics/number_parser.ex index 26a6356..566cc70 100644 --- a/lib/image_plug/param_parser/twicpics_v2/number_parser.ex +++ b/lib/image_plug/param_parser/twicpics/number_parser.ex @@ -1,5 +1,5 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParser do - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.NumberParser do + alias ImagePlug.ParamParser.Twicpics.Utils @op_tokens ~c"+-*/" diff --git a/lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex b/lib/image_plug/param_parser/twicpics/ratio_parser.ex similarity index 87% rename from lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex rename to lib/image_plug/param_parser/twicpics/ratio_parser.ex index 22404ec..a61e2a9 100644 --- a/lib/image_plug/param_parser/twicpics_v2/ratio_parser.ex +++ b/lib/image_plug/param_parser/twicpics/ratio_parser.ex @@ -1,7 +1,7 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.RatioParser do - alias ImagePlug.ParamParser.TwicpicsV2.NumberParser - alias ImagePlug.ParamParser.TwicpicsV2.ArithmeticParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.RatioParser do + alias ImagePlug.ParamParser.Twicpics.NumberParser + alias ImagePlug.ParamParser.Twicpics.ArithmeticParser + alias ImagePlug.ParamParser.Twicpics.Utils def parse(input, pos_offset \\ 0) do case String.split(input, ":", parts: 2) do diff --git a/lib/image_plug/param_parser/twicpics_v2/size_parser.ex b/lib/image_plug/param_parser/twicpics/size_parser.ex similarity index 91% rename from lib/image_plug/param_parser/twicpics_v2/size_parser.ex rename to lib/image_plug/param_parser/twicpics/size_parser.ex index 3f43945..2a16401 100644 --- a/lib/image_plug/param_parser/twicpics_v2/size_parser.ex +++ b/lib/image_plug/param_parser/twicpics/size_parser.ex @@ -1,6 +1,6 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.SizeParser do - alias ImagePlug.ParamParser.TwicpicsV2.LengthParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.SizeParser do + alias ImagePlug.ParamParser.Twicpics.LengthParser + alias ImagePlug.ParamParser.Twicpics.Utils def parse(input, pos_offset \\ 0) do case String.split(input, "x", parts: 2) do diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex similarity index 60% rename from lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex rename to lib/image_plug/param_parser/twicpics/transform/contain_parser.ex index 0c9bd7c..73176d4 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/contain_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex @@ -1,7 +1,7 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ContainParser do - alias ImagePlug.ParamParser.TwicpicsV2.SizeParser - alias ImagePlug.ParamParser.TwicpicsV2.RatioParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.Transform.ContainParser do + alias ImagePlug.ParamParser.Twicpics.SizeParser + alias ImagePlug.ParamParser.Twicpics.RatioParser + alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Contain.ContainParams def parse(input, pos_offset \\ 0) do diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex b/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex similarity index 83% rename from lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex rename to lib/image_plug/param_parser/twicpics/transform/crop_parser.ex index e59898a..642855e 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/crop_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex @@ -1,7 +1,7 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.CropParser do - alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser - alias ImagePlug.ParamParser.TwicpicsV2.SizeParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.Transform.CropParser do + alias ImagePlug.ParamParser.Twicpics.CoordinatesParser + alias ImagePlug.ParamParser.Twicpics.SizeParser + alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Crop.CropParams def parse(input, pos_offset \\ 0) do diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex b/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex similarity index 87% rename from lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex rename to lib/image_plug/param_parser/twicpics/transform/focus_parser.ex index a630dd8..bf03f56 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/focus_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex @@ -1,6 +1,6 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.FocusParser do - alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.Transform.FocusParser do + alias ImagePlug.ParamParser.Twicpics.CoordinatesParser + alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Focus.FocusParams diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex b/lib/image_plug/param_parser/twicpics/transform/output_parser.ex similarity index 74% rename from lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex rename to lib/image_plug/param_parser/twicpics/transform/output_parser.ex index d68faa1..e933584 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/output_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/output_parser.ex @@ -1,6 +1,6 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.OutputParser do - alias ImagePlug.ParamParser.TwicpicsV2.CoordinatesParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.Transform.OutputParser do + alias ImagePlug.ParamParser.Twicpics.CoordinatesParser + alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Output.OutputParams diff --git a/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex b/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex similarity index 81% rename from lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex rename to lib/image_plug/param_parser/twicpics/transform/scale_parser.ex index d2021f5..8057169 100644 --- a/lib/image_plug/param_parser/twicpics_v2/transform/scale_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex @@ -1,7 +1,7 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser do - alias ImagePlug.ParamParser.TwicpicsV2.SizeParser - alias ImagePlug.ParamParser.TwicpicsV2.RatioParser - alias ImagePlug.ParamParser.TwicpicsV2.Utils +defmodule ImagePlug.ParamParser.Twicpics.Transform.ScaleParser do + alias ImagePlug.ParamParser.Twicpics.SizeParser + alias ImagePlug.ParamParser.Twicpics.RatioParser + alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Scale.ScaleParams alias ImagePlug.Transform.Scale.ScaleParams.Dimensions alias ImagePlug.Transform.Scale.ScaleParams.AspectRatio diff --git a/lib/image_plug/param_parser/twicpics_v2/utils.ex b/lib/image_plug/param_parser/twicpics/utils.ex similarity index 96% rename from lib/image_plug/param_parser/twicpics_v2/utils.ex rename to lib/image_plug/param_parser/twicpics/utils.ex index 1657bc1..8781f45 100644 --- a/lib/image_plug/param_parser/twicpics_v2/utils.ex +++ b/lib/image_plug/param_parser/twicpics/utils.ex @@ -1,4 +1,4 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.Utils do +defmodule ImagePlug.ParamParser.Twicpics.Utils do def balanced_parens?(value) when is_binary(value) do balanced_parens?(value, []) end diff --git a/lib/simple_server.ex b/lib/simple_server.ex index 9489915..9a1b8c5 100644 --- a/lib/simple_server.ex +++ b/lib/simple_server.ex @@ -14,7 +14,7 @@ defmodule ImagePlug.SimpleServer do to: ImagePlug, init_opts: [ root_url: "http://localhost:4000", - param_parser: ImagePlug.ParamParser.TwicpicsV2 + param_parser: ImagePlug.ParamParser.Twicpics ] match _ do diff --git a/test/param_parser/twicpics_parser_test.exs b/test/param_parser/twicpics_parser_test.exs index b188a3a..a354ba6 100644 --- a/test/param_parser/twicpics_parser_test.exs +++ b/test/param_parser/twicpics_parser_test.exs @@ -4,17 +4,17 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do import ImagePlug.TestSupport - alias ImagePlug.ParamParser.TwicpicsV2 + alias ImagePlug.ParamParser.Twicpics alias ImagePlug.Transform.Crop alias ImagePlug.Transform.Scale alias ImagePlug.Transform.Focus alias ImagePlug.Transform.Contain - doctest ImagePlug.ParamParser.TwicpicsV2.Transform.CropParser - doctest ImagePlug.ParamParser.TwicpicsV2.Transform.ScaleParser - doctest ImagePlug.ParamParser.TwicpicsV2.Transform.FocusParser - doctest ImagePlug.ParamParser.TwicpicsV2.Transform.ContainParser - doctest ImagePlug.ParamParser.TwicpicsV2.Transform.OutputParser + doctest ImagePlug.ParamParser.Twicpics.Transform.CropParser + doctest ImagePlug.ParamParser.Twicpics.Transform.ScaleParser + doctest ImagePlug.ParamParser.Twicpics.Transform.FocusParser + doctest ImagePlug.ParamParser.Twicpics.Transform.ContainParser + doctest ImagePlug.ParamParser.Twicpics.Transform.OutputParser defp length_str({:pixels, unit}), do: "#{unit}" defp length_str({:scale, unit_a, unit_b}), do: "(#{unit_a}/#{unit_b})s" @@ -36,7 +36,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do %{left: left, top: top} -> "#{str_params}@#{length_str(left)}x#{length_str(top)}" end - parsed = TwicpicsV2.Transform.CropParser.parse(str_params) + parsed = Twicpics.Transform.CropParser.parse(str_params) assert {:ok, %Crop.CropParams{ @@ -70,7 +70,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do {:anchor, _, _} = anchor -> anchor_to_str(anchor) end - {:ok, parsed} = TwicpicsV2.Transform.FocusParser.parse(str_params) + {:ok, parsed} = Twicpics.Transform.FocusParser.parse(str_params) case focus_type do {:coordinate, left, top} -> @@ -94,7 +94,8 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do tuple({random_root_unit(min: 1), random_root_unit(min: 1)})} ), tuple( - {constant(:aspect_ratio), tuple({random_base_unit(min: 1), random_base_unit(min: 1)})} + {constant(:aspect_ratio), + tuple({random_base_unit(min: 1), random_base_unit(min: 1)})} ) ]) do {str_params, expected} = @@ -135,7 +136,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do }} end - {:ok, parsed} = TwicpicsV2.Transform.ScaleParser.parse(str_params) + {:ok, parsed} = Twicpics.Transform.ScaleParser.parse(str_params) assert parsed == expected end @@ -145,7 +146,7 @@ defmodule ImagePlug.ParamParser.TwicpicsParserTest do check all width <- random_root_unit(min: 1), height <- random_root_unit(min: 1) do str_params = "#{length_str(width)}x#{length_str(height)}" - parsed = TwicpicsV2.Transform.ContainParser.parse(str_params) + parsed = Twicpics.Transform.ContainParser.parse(str_params) assert {:ok, %Contain.ContainParams{width: to_result(width), height: to_result(height)}} == parsed diff --git a/test/param_parser/twicpics_test.exs b/test/param_parser/twicpics_test.exs index 859fd59..699cc88 100644 --- a/test/param_parser/twicpics_test.exs +++ b/test/param_parser/twicpics_test.exs @@ -7,10 +7,10 @@ defmodule ImagePlug.TwicpicsTest do alias ImagePlug.Transform alias ImagePlug.TransformChain alias ImagePlug.TransformState - alias ImagePlug.ParamParser.TwicpicsV2 + alias ImagePlug.ParamParser.Twicpics test "parse from string" do - result = TwicpicsV2.parse_string("v1/focus=(1/2)sx(2/3)s/crop=100x100/resize=200/output=avif") + result = Twicpics.parse_string("v1/focus=(1/2)sx(2/3)s/crop=100x100/resize=200/output=avif") assert result == {:ok, @@ -19,8 +19,8 @@ defmodule ImagePlug.TwicpicsTest do %Transform.Focus.FocusParams{ type: { :coordinate, - {:scale, 1/2}, - {:scale, 2/3} + {:scale, 1 / 2}, + {:scale, 2 / 3} } }}, {Transform.Crop, diff --git a/test/param_parser/twicpics_v2/kv_parser_test.exs b/test/param_parser/twicpics_v2/kv_parser_test.exs index e33ab66..a0d6ee5 100644 --- a/test/param_parser/twicpics_v2/kv_parser_test.exs +++ b/test/param_parser/twicpics_v2/kv_parser_test.exs @@ -1,8 +1,8 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.KVParserTest do +defmodule ImagePlug.ParamParser.Twicpics.KVParserTest do use ExUnit.Case, async: true use ExUnitProperties - alias ImagePlug.ParamParser.TwicpicsV2.KVParser + alias ImagePlug.ParamParser.Twicpics.KVParser @keys ~w(k1 k2 k3 k1 k20 k300 k4000) @@ -26,7 +26,8 @@ defmodule ImagePlug.ParamParser.TwicpicsV2.KVParserTest do end test "error returns correct position when missing =" do - assert KVParser.parse("k1=v1/k20=v20/k300", @keys) == {:error, {:unexpected_char, [{:pos, 18}, {:expected, ["="]}, {:found, :eoi}]}} + assert KVParser.parse("k1=v1/k20=v20/k300", @keys) == + {:error, {:unexpected_char, [{:pos, 18}, {:expected, ["="]}, {:found, :eoi}]}} end test "expected key error returns correct position" do diff --git a/test/param_parser/twicpics_v2/number_parser_test.exs b/test/param_parser/twicpics_v2/number_parser_test.exs index 4cc2093..5ee1c07 100644 --- a/test/param_parser/twicpics_v2/number_parser_test.exs +++ b/test/param_parser/twicpics_v2/number_parser_test.exs @@ -1,8 +1,8 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.NumberParserTest do +defmodule ImagePlug.ParamParser.Twicpics.NumberParserTest do use ExUnit.Case, async: true use ExUnitProperties - alias ImagePlug.ParamParser.TwicpicsV2.NumberParser + alias ImagePlug.ParamParser.Twicpics.NumberParser describe "successful parsing" do test "parses a single integer" do diff --git a/test/param_parser/twicpics_v2/utils_test.exs b/test/param_parser/twicpics_v2/utils_test.exs index 6f2cd40..4abf5a8 100644 --- a/test/param_parser/twicpics_v2/utils_test.exs +++ b/test/param_parser/twicpics_v2/utils_test.exs @@ -1,8 +1,8 @@ -defmodule ImagePlug.ParamParser.TwicpicsV2.UtilsTest do +defmodule ImagePlug.ParamParser.Twicpics.UtilsTest do use ExUnit.Case, async: true use ExUnitProperties - alias ImagePlug.ParamParser.TwicpicsV2.Utils + alias ImagePlug.ParamParser.Twicpics.Utils test "balanced_parens?/1" do assert Utils.balanced_parens?("(") == false From 726883457af2da4d2898d9f52fa6f22a863f44fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 23:05:18 +0100 Subject: [PATCH 34/35] add docs/doctests --- .../twicpics/transform/contain_parser.ex | 11 +++++++ .../twicpics/transform/crop_parser.ex | 14 +++++++++ .../twicpics/transform/focus_parser.ex | 16 ++++++++++ .../twicpics/transform/output_parser.ex | 14 +++++++++ .../twicpics/transform/scale_parser.ex | 29 +++++++++++++++++++ 5 files changed, 84 insertions(+) diff --git a/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex index 73176d4..a29bac0 100644 --- a/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/contain_parser.ex @@ -4,6 +4,17 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.ContainParser do alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Contain.ContainParams + @doc """ + Parses a string into a `ImagePlug.Transform.Contain.ContainParams` struct. + + Syntax: + * `contain=` + + ## Examples + iex> ImagePlug.ParamParser.Twicpics.Transform.ContainParser.parse("250x25.5") + {:ok, %ImagePlug.Transform.Contain.ContainParams{width: {:pixels, 250}, height: {:pixels, 25.5}}} + """ + def parse(input, pos_offset \\ 0) do case SizeParser.parse(input, pos_offset) do {:ok, %{width: width, height: height}} -> diff --git a/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex b/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex index 642855e..14ba0c6 100644 --- a/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/crop_parser.ex @@ -4,6 +4,20 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.CropParser do alias ImagePlug.ParamParser.Twicpics.Utils alias ImagePlug.Transform.Crop.CropParams + @doc """ + Parses a string into a `ImagePlug.Transform.Crop.CropParams` struct. + + Syntax: + * `crop=` + * `crop=@` + + ## Examples + iex> ImagePlug.ParamParser.Twicpics.Transform.CropParser.parse("250x25p") + {:ok, %ImagePlug.Transform.Crop.CropParams{width: {:pixels, 250}, height: {:percent, 25}, crop_from: :focus}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.CropParser.parse("20px25@10x50.1p") + {:ok, %ImagePlug.Transform.Crop.CropParams{width: {:percent, 20}, height: {:pixels, 25}, crop_from: %{left: {:pixels, 10}, top: {:percent, 50.1}}}} + """ def parse(input, pos_offset \\ 0) do case String.split(input, "@", parts: 2) do [size_str, coordinates_str] -> diff --git a/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex b/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex index bf03f56..de4df6d 100644 --- a/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/focus_parser.ex @@ -16,6 +16,22 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.FocusParser do "right" => {:anchor, :right, :center} } + @doc """ + Parses a string into a `ImagePlug.Transform.Focus.FocusParams` struct. + + Syntax: + * `focus=` + * `focus=` + * ~~`focus=auto`~~ + + ## Examples + iex> ImagePlug.ParamParser.Twicpics.Transform.FocusParser.parse("(500/2)x25.5") + {:ok, %ImagePlug.Transform.Focus.FocusParams{type: {:coordinate, {:pixels, 250.0}, {:pixels, 25.5}}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.FocusParser.parse("bottom-right") + {:ok, %ImagePlug.Transform.Focus.FocusParams{type: {:anchor, :right, :bottom}}} + """ + def parse(input, pos_offset \\ 0) do if String.contains?(input, "x"), do: parse_coordinates(input, pos_offset), diff --git a/lib/image_plug/param_parser/twicpics/transform/output_parser.ex b/lib/image_plug/param_parser/twicpics/transform/output_parser.ex index e933584..5961a32 100644 --- a/lib/image_plug/param_parser/twicpics/transform/output_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/output_parser.ex @@ -13,6 +13,20 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.OutputParser do "blurhash" => :blurhash } + @doc """ + Parses a string into a `ImagePlug.Transform.Output.OutputParams` struct. + + Syntax: + * `output=` + * `output=` + + ## Examples + iex> ImagePlug.ParamParser.Twicpics.Transform.OutputParser.parse("avif") + {:ok, %ImagePlug.Transform.Output.OutputParams{format: :avif}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.OutputParser.parse("blurhash") + {:ok, %ImagePlug.Transform.Output.OutputParams{format: :blurhash}} + """ def parse(input, pos_offset \\ 0) do case Map.get(@formats, input) do format when is_atom(format) -> diff --git a/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex b/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex index 8057169..6dde875 100644 --- a/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex +++ b/lib/image_plug/param_parser/twicpics/transform/scale_parser.ex @@ -6,6 +6,35 @@ defmodule ImagePlug.ParamParser.Twicpics.Transform.ScaleParser do alias ImagePlug.Transform.Scale.ScaleParams.Dimensions alias ImagePlug.Transform.Scale.ScaleParams.AspectRatio + @doc """ + Parses a string into a `ImagePlug.Transform.Scale.ScaleParams` struct. + + Syntax + * `resize=` + * `resize=` + + ## Examples + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("250x25p") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:pixels, 250}, height: {:percent, 25}}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("-x25p") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: :auto, height: {:percent, 25}}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("50.5px-") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:percent, 50.5}, height: :auto}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("50.5") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:pixels, 50.5}, height: :auto}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("50p") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:percent, 50}, height: :auto}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("(25*10)x(1/2)s") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.Dimensions{width: {:pixels, 250}, height: {:scale, 0.5}}}} + + iex> ImagePlug.ParamParser.Twicpics.Transform.ScaleParser.parse("16:9") + {:ok, %ImagePlug.Transform.Scale.ScaleParams{method: %ImagePlug.Transform.Scale.ScaleParams.AspectRatio{aspect_ratio: {:ratio, 16, 9}}}} + """ def parse(input, pos_offset \\ 0) do if String.contains?(input, ":"), do: parse_ratio(input, pos_offset), From c1c8b61d96400ff4b293818bbda1ea4009b971dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Lindset?= Date: Sun, 8 Dec 2024 23:07:45 +0100 Subject: [PATCH 35/35] remove unused --- lib/image_plug/arithmetic_parser.ex | 145 ---------------------------- 1 file changed, 145 deletions(-) delete mode 100644 lib/image_plug/arithmetic_parser.ex diff --git a/lib/image_plug/arithmetic_parser.ex b/lib/image_plug/arithmetic_parser.ex deleted file mode 100644 index 9a5db83..0000000 --- a/lib/image_plug/arithmetic_parser.ex +++ /dev/null @@ -1,145 +0,0 @@ -defmodule ImagePlug.ArithmeticParser do - @type token :: {:int, integer} | {:float, float} | {:op, binary} | :left_paren | :right_paren - @type expr :: {:int, integer} | {:float, float} | {:op, binary, expr(), expr()} - - @spec parse(String.t()) :: {:ok, expr} | {:error, atom()} - def parse(input) do - case tokenize(input) do - {:ok, tokens} -> - case parse_expression(tokens, 0) do - {:ok, expr, []} -> {:ok, expr} - {:ok, _, _} -> {:error, :unexpected_token_after_expr} - {:error, _} = error -> error - end - - {:error, _} = error -> - error - end - end - - @spec evaluate(String.t()) :: {:ok, number} | {:error, atom()} - def parse_and_evaluate(input) do - case parse(input) do - {:ok, expr} -> evaluate(expr) - {:error, _} = error -> error - end - end - - defp tokenize(input) do - input - |> String.replace(~r/\s+/, "") - |> String.graphemes() - |> do_tokenize([]) - end - - defp do_tokenize([], acc), do: {:ok, Enum.reverse(acc)} - - defp do_tokenize([h | t], acc) when h in ~w(+ - * /) do - do_tokenize(t, [{:op, h} | acc]) - end - - defp do_tokenize(["(" | t], acc), do: do_tokenize(t, [:left_paren | acc]) - defp do_tokenize([")" | t], acc), do: do_tokenize(t, [:right_paren | acc]) - - defp do_tokenize([h | t], acc) when h in ~w(0 1 2 3 4 5 6 7 8 9 .) do - {number, rest} = consume_number([h | t]) - - token = - if String.contains?(number, "."), - do: {:float, String.to_float(number)}, - else: {:int, String.to_integer(number)} - - do_tokenize(rest, [token | acc]) - end - - defp do_tokenize(_, _), do: {:error, :invalid_character} - - defp consume_number(chars) do - {number, rest} = Enum.split_while(chars, &(&1 in ~w(0 1 2 3 4 5 6 7 8 9 .))) - {Enum.join(number), rest} - end - - defp parse_expression(tokens, min_prec) do - case parse_primary(tokens) do - {:ok, lhs, rest} -> parse_binary_op(lhs, rest, min_prec) - {:error, _} = error -> error - end - end - - defp parse_primary([{:int, n} | rest]), do: {:ok, {:int, n}, rest} - defp parse_primary([{:float, n} | rest]), do: {:ok, {:float, n}, rest} - - defp parse_primary([:left_paren | rest]) do - case parse_expression(rest, 0) do - {:ok, expr, [:right_paren | rest2]} -> {:ok, expr, rest2} - {:ok, _, _} -> {:error, :mismatched_paren} - {:error, _} = error -> error - end - end - - defp parse_primary(_), do: {:error, :expected_primary_expression} - - defp parse_binary_op(lhs, tokens, min_prec) do - case tokens do - [{:op, op} | rest] -> - prec = precedence(op) - - if prec < min_prec do - {:ok, lhs, tokens} - else - case parse_expression(rest, prec + 1) do - {:ok, rhs, rest2} -> - new_lhs = {:op, op, lhs, rhs} - parse_binary_op(new_lhs, rest2, min_prec) - - {:error, _} = error -> - error - end - end - - _ -> - {:ok, lhs, tokens} - end - end - - defp precedence("+"), do: 1 - defp precedence("-"), do: 1 - defp precedence("*"), do: 2 - defp precedence("/"), do: 2 - - @spec evaluate(expr()) :: {:ok, number} | {:error, String.t()} - defp evaluate({:int, n}), do: {:ok, n} - defp evaluate({:float, n}), do: {:ok, n} - - defp evaluate({:op, "+", lhs, rhs}) do - with {:ok, lval} <- evaluate(lhs), - {:ok, rval} <- evaluate(rhs) do - {:ok, lval + rval} - end - end - - defp evaluate({:op, "-", lhs, rhs}) do - with {:ok, lval} <- evaluate(lhs), - {:ok, rval} <- evaluate(rhs) do - {:ok, lval - rval} - end - end - - defp evaluate({:op, "*", lhs, rhs}) do - with {:ok, lval} <- evaluate(lhs), - {:ok, rval} <- evaluate(rhs) do - {:ok, lval * rval} - end - end - - defp evaluate({:op, "/", lhs, rhs}) do - with {:ok, lval} <- evaluate(lhs), - {:ok, rval} <- evaluate(rhs) do - if rval == 0 do - {:error, :division_by_zero} - else - {:ok, lval / rval} - end - end - end -end