From 7cf56c2daa98af850274a8ce46a785984ec965b0 Mon Sep 17 00:00:00 2001 From: Benjamin Milde Date: Tue, 16 Feb 2021 21:32:27 +0100 Subject: [PATCH 1/2] Add escape and escape_once --- lib/solid/filter.ex | 33 ++++++++++++ lib/solid/html.ex | 104 ++++++++++++++++++++++++++++++++++++ test/cases/110/input.json | 1 + test/cases/110/input.liquid | 2 + test/cases/111/input.json | 1 + test/cases/111/input.liquid | 2 + 6 files changed, 143 insertions(+) create mode 100644 lib/solid/html.ex create mode 100644 test/cases/110/input.json create mode 100644 test/cases/110/input.liquid create mode 100644 test/cases/111/input.json create mode 100644 test/cases/111/input.liquid diff --git a/lib/solid/filter.ex b/lib/solid/filter.ex index 2172cb5..519ea47 100644 --- a/lib/solid/filter.ex +++ b/lib/solid/filter.ex @@ -774,4 +774,37 @@ defmodule Solid.Filter do |> IO.iodata_to_binary() |> URI.decode_www_form() end + + @doc """ + HTML encodes the string. + + Output + iex> Solid.Filter.escape("Have you read 'James & the Giant Peach'?") + "Have you read 'James & the Giant Peach'?" + """ + @spec escape(iodata()) :: String.t() + def escape(iodata) do + iodata + |> IO.iodata_to_binary() + |> Solid.HTML.html_escape() + end + + @doc """ + HTML encodes the string without encoding already encoded characters again. + + This mimics the regex based approach of the ruby library. + + Output + "1 < 2 & 3" + + iex> Solid.Filter.escape_once("1 < 2 & 3") + "1 < 2 & 3" + """ + @escape_once_regex ~r{["><']|&(?!([a-zA-Z]+|(#\d+));)} + @spec escape_once(iodata()) :: String.t() + def escape_once(iodata) do + iodata + |> IO.iodata_to_binary() + |> String.replace(@escape_once_regex, &Solid.HTML.replacements/1) + end end diff --git a/lib/solid/html.ex b/lib/solid/html.ex new file mode 100644 index 0000000..229992a --- /dev/null +++ b/lib/solid/html.ex @@ -0,0 +1,104 @@ +defmodule Solid.HTML do + # Vendored in from Plug.HTML + # + # + # Copyright (c) 2013 Plataformatec. + + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + + # http://www.apache.org/licenses/LICENSE-2.0 + + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + # + + @moduledoc """ + Conveniences for generating HTML. + """ + + @doc ~S""" + Escapes the given HTML to string. + + iex> Plug.HTML.html_escape("foo") + "foo" + + iex> Plug.HTML.html_escape("") + "<foo>" + + iex> Plug.HTML.html_escape("quotes: \" & \'") + "quotes: " & '" + """ + @spec html_escape(String.t()) :: String.t() + def html_escape(data) when is_binary(data) do + IO.iodata_to_binary(to_iodata(data, 0, data, [])) + end + + @doc ~S""" + Escapes the given HTML to iodata. + + iex> Plug.HTML.html_escape_to_iodata("foo") + "foo" + + iex> Plug.HTML.html_escape_to_iodata("") + [[[] | "<"], "foo" | ">"] + + iex> Plug.HTML.html_escape_to_iodata("quotes: \" & \'") + [[[[], "quotes: " | """], " " | "&"], " " | "'"] + + """ + @spec html_escape_to_iodata(String.t()) :: iodata + def html_escape_to_iodata(data) when is_binary(data) do + to_iodata(data, 0, data, []) + end + + escapes = [ + {?<, "<"}, + {?>, ">"}, + {?&, "&"}, + {?", """}, + {?', "'"} + ] + + for {match, insert} <- escapes do + defp to_iodata(<>, skip, original, acc) do + to_iodata(rest, skip + 1, original, [acc | unquote(insert)]) + end + end + + defp to_iodata(<<_char, rest::bits>>, skip, original, acc) do + to_iodata(rest, skip, original, acc, 1) + end + + defp to_iodata(<<>>, _skip, _original, acc) do + acc + end + + for {match, insert} <- escapes do + defp to_iodata(<>, skip, original, acc, len) do + part = binary_part(original, skip, len) + to_iodata(rest, skip + len + 1, original, [acc, part | unquote(insert)]) + end + end + + defp to_iodata(<<_char, rest::bits>>, skip, original, acc, len) do + to_iodata(rest, skip, original, acc, len + 1) + end + + defp to_iodata(<<>>, 0, original, _acc, _len) do + original + end + + defp to_iodata(<<>>, skip, original, acc, len) do + [acc | binary_part(original, skip, len)] + end + + # Addition + for {match, insert} <- escapes do + def replacements(<>), do: unquote(insert) + end +end diff --git a/test/cases/110/input.json b/test/cases/110/input.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/cases/110/input.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/cases/110/input.liquid b/test/cases/110/input.liquid new file mode 100644 index 0000000..e2c8db9 --- /dev/null +++ b/test/cases/110/input.liquid @@ -0,0 +1,2 @@ +{{ "Have you read 'James & the Giant Peach'?" | escape }} +{{ "Tetsuro Takara" | escape }} diff --git a/test/cases/111/input.json b/test/cases/111/input.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/test/cases/111/input.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/test/cases/111/input.liquid b/test/cases/111/input.liquid new file mode 100644 index 0000000..741176b --- /dev/null +++ b/test/cases/111/input.liquid @@ -0,0 +1,2 @@ +{{ "1 < 2 & 3" | escape_once }} +{{ "1 < 2 & 3" | escape_once }} From cddebb95b6f4d81e4e2fb68dec515d079cc200a4 Mon Sep 17 00:00:00 2001 From: Benjamin Milde Date: Tue, 16 Feb 2021 21:37:58 +0100 Subject: [PATCH 2/2] Fix documentation --- lib/solid/filter.ex | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/solid/filter.ex b/lib/solid/filter.ex index 519ea47..9018ff8 100644 --- a/lib/solid/filter.ex +++ b/lib/solid/filter.ex @@ -743,9 +743,7 @@ defmodule Solid.Filter do end @doc """ - Removes any HTML tags from a string. - - This mimics the regex based approach of the ruby library. + URL encodes the string. Output iex> Solid.Filter.url_encode("john@liquid.com") @@ -761,9 +759,7 @@ defmodule Solid.Filter do end @doc """ - Removes any HTML tags from a string. - - This mimics the regex based approach of the ruby library. + URL decodes the string. Output iex> Solid.Filter.url_decode("%27Stop%21%27+said+Fred")