diff --git a/lib/feed_reader/parser.ex b/lib/feed_reader/parser.ex
index 6c873a5..d1e4cf3 100644
--- a/lib/feed_reader/parser.ex
+++ b/lib/feed_reader/parser.ex
@@ -88,6 +88,8 @@ defmodule FeedReader.Parser do
|> normalize_xml_prefix()
|> transcode_input(encoding)
|> normalize_encoding_declaration(encoding)
+ |> normalize_entities()
+ |> normalize_attribute_quotes()
end
defp normalize_xml_prefix(input) do
@@ -182,6 +184,168 @@ defmodule FeedReader.Parser do
defp flush_declaration({:prolog, prefix}), do: [prefix]
defp flush_declaration(:ready), do: []
+ defp normalize_entities(input) do
+ Stream.transform(
+ input,
+ fn -> {:normal, <<>>} end,
+ &normalize_entity_chunk/2,
+ &flush_entities/1
+ )
+ end
+
+ defp normalize_entity_chunk(chunk, {mode, pending}) do
+ {output, mode, pending} = scan_entities(pending <> chunk, mode, [])
+ emit_entity_chunk(output, {mode, pending})
+ end
+
+ defp scan_entities(data, :normal, output) do
+ case next_markup(data) do
+ :none ->
+ case partial_markup_prefix(data) do
+ :none ->
+ {append_output(output, data), :normal, <<>>}
+
+ length ->
+ split = byte_size(data) - length
+
+ {append_output(output, binary_part(data, 0, split)), :normal,
+ binary_part(data, split, length)}
+ end
+
+ {position, :cdata} ->
+ {prefix, rest} = :erlang.split_binary(data, position)
+
+ scan_entities(binary_part(rest, 9, byte_size(rest) - 9), :cdata, [
+ "
+ {prefix, rest} = :erlang.split_binary(data, position)
+
+ scan_entities(binary_part(rest, 4, byte_size(rest) - 4), :comment, [
+ "") do
+ :nomatch ->
+ keep = min(byte_size(data), 2)
+ split = byte_size(data) - keep
+
+ {append_output(output, binary_part(data, 0, split)), :comment,
+ binary_part(data, split, keep)}
+
+ {position, 3} ->
+ {prefix, rest} = :erlang.split_binary(data, position + 3)
+ scan_entities(rest, :normal, [prefix | output])
+ end
+ end
+
+ defp next_markup(data) do
+ [
+ {:cdata, :binary.match(data, " Enum.reduce(:none, &earliest_markup/2)
+ end
+
+ defp partial_markup_prefix(<<>>), do: :none
+
+ defp partial_markup_prefix(data) do
+ max_length = min(byte_size(data), 8)
+
+ Enum.find_value(max_length..1//-1, :none, fn length ->
+ suffix = binary_part(data, byte_size(data) - length, length)
+
+ if suffix in [binary_part("
+ entity = if valid_entity?(entity), do: entity, else: escape_entity(entity)
+ scan_entities(rest, :normal, [entity | output])
+
+ {:invalid, entity, rest} ->
+ scan_entities(rest, :normal, [escape_entity(entity) | output])
+
+ :incomplete ->
+ {output, :normal, "&" <> data}
+ end
+ end
+
+ defp consume_entity(<<>>, _entity), do: :incomplete
+ defp consume_entity(<<";", rest::binary>>, entity), do: {:complete, entity <> ";", rest}
+
+ defp consume_entity(<
A & B
" + end + + test "repairs literal quotes inside XML attributes" do + xml = + ~s(