Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions lib/xml_stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,45 @@ defmodule XmlStream do
options = Keyword.merge(@default_options, options)
printer = options[:printer]

flatten(nodes)
|> Stream.transform(printer.init(options), &printer.print/2)
Stream.resource(
fn -> {[{:enum, nodes}], printer.init(options)} end,
fn {stack, state} ->
case step(stack, state, printer) do
:done -> {:halt, {stack, state}}
{output, new_stack, new_state} -> {[output], {new_stack, new_state}}
end
end,
fn {stack, _state} ->
Enum.each(stack, fn
{:cont, cont} -> cont.({:halt, nil})
{:enum, _} -> :ok
end)
end
)
end

defp step([], _state, _printer), do: :done

defp step([{:enum, enum} | rest], state, printer) do
case Enumerable.reduce(enum, {:cont, nil}, fn item, _ -> {:suspend, item} end) do
{:suspended, item, cont} -> handle(item, [{:cont, cont} | rest], state, printer)
_ -> step(rest, state, printer)
end
end

defp step([{:cont, cont} | rest], state, printer) do
case cont.({:cont, nil}) do
{:suspended, item, cont2} -> handle(item, [{:cont, cont2} | rest], state, printer)
_ -> step(rest, state, printer)
end
end

defp handle(item, stack, state, printer) when is_tuple(item) do
{output, new_state} = printer.print(item, state)
{output, stack, new_state}
end

defp flatten(node) do
Stream.flat_map(node, fn
operation when is_tuple(operation) -> [operation]
operation -> flatten(operation)
end)
defp handle(item, stack, state, printer) do
step([{:enum, item} | stack], state, printer)
end
end
192 changes: 168 additions & 24 deletions lib/xml_stream/printer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,157 @@ defmodule XmlStream.Printer do
@callback init(term) :: term

@doc false
def attrs_to_string([]), do: []

def attrs_to_string(attrs) do
Enum.map(attrs, fn {key, value} ->
[" ", encode_name(key), ~s(="), escape_binary(to_string(value)), ~s(")]
Enum.reduce(attrs, <<>>, fn {key, value}, acc ->
acc <> " " <> encode_name(key) <> ~s(=") <> escape_binary(to_string(value)) <> ~s(")
end)
end

@doc false
def escape_binary(""), do: []
def escape_binary("&" <> rest), do: ["&amp;" | escape_binary(rest)]
def escape_binary("\"" <> rest), do: ["&quot;" | escape_binary(rest)]
def escape_binary("'" <> rest), do: ["&apos;" | escape_binary(rest)]
def escape_binary("<" <> rest), do: ["&lt;" | escape_binary(rest)]
def escape_binary(">" <> rest), do: ["&gt;" | escape_binary(rest)]
def escape_binary(<<char::utf8>> <> rest), do: [<<char::utf8>> | escape_binary(rest)]
def escape_binary(binary) when is_binary(binary), do: escape_binary(binary, binary)

defp escape_binary(<<>>, original), do: original

defp escape_binary(<<?&, rest::bits>>, original) do
pos = byte_size(original) - byte_size(rest) - 1
escape_binary(rest, original, pos + 1, 0, binary_part(original, 0, pos) <> "&amp;")
end

defp escape_binary(<<?", rest::bits>>, original) do
pos = byte_size(original) - byte_size(rest) - 1
escape_binary(rest, original, pos + 1, 0, binary_part(original, 0, pos) <> "&quot;")
end

defp escape_binary(<<?', rest::bits>>, original) do
pos = byte_size(original) - byte_size(rest) - 1
escape_binary(rest, original, pos + 1, 0, binary_part(original, 0, pos) <> "&apos;")
end

defp escape_binary(<<?<, rest::bits>>, original) do
pos = byte_size(original) - byte_size(rest) - 1
escape_binary(rest, original, pos + 1, 0, binary_part(original, 0, pos) <> "&lt;")
end

defp escape_binary(<<?>, rest::bits>>, original) do
pos = byte_size(original) - byte_size(rest) - 1
escape_binary(rest, original, pos + 1, 0, binary_part(original, 0, pos) <> "&gt;")
end

defp escape_binary(<<char, rest::bits>>, original) when char < 0x80,
do: escape_binary(rest, original)

defp escape_binary(<<_::utf8, rest::bits>>, original), do: escape_binary(rest, original)

defp escape_binary(<<>>, _original, _skip, 0, acc), do: acc
defp escape_binary(<<>>, original, skip, len, acc), do: acc <> binary_part(original, skip, len)

defp escape_binary(<<?&, rest::bits>>, original, skip, 0, acc),
do: escape_binary(rest, original, skip + 1, 0, acc <> "&amp;")

defp escape_binary(<<?&, rest::bits>>, original, skip, len, acc),
do:
escape_binary(
rest,
original,
skip + len + 1,
0,
acc <> binary_part(original, skip, len) <> "&amp;"
)

defp escape_binary(<<?", rest::bits>>, original, skip, 0, acc),
do: escape_binary(rest, original, skip + 1, 0, acc <> "&quot;")

defp escape_binary(<<?", rest::bits>>, original, skip, len, acc),
do:
escape_binary(
rest,
original,
skip + len + 1,
0,
acc <> binary_part(original, skip, len) <> "&quot;"
)

defp escape_binary(<<?', rest::bits>>, original, skip, 0, acc),
do: escape_binary(rest, original, skip + 1, 0, acc <> "&apos;")

defp escape_binary(<<?', rest::bits>>, original, skip, len, acc),
do:
escape_binary(
rest,
original,
skip + len + 1,
0,
acc <> binary_part(original, skip, len) <> "&apos;"
)

defp escape_binary(<<?<, rest::bits>>, original, skip, 0, acc),
do: escape_binary(rest, original, skip + 1, 0, acc <> "&lt;")

defp escape_binary(<<?<, rest::bits>>, original, skip, len, acc),
do:
escape_binary(
rest,
original,
skip + len + 1,
0,
acc <> binary_part(original, skip, len) <> "&lt;"
)

defp escape_binary(<<?>, rest::bits>>, original, skip, 0, acc),
do: escape_binary(rest, original, skip + 1, 0, acc <> "&gt;")

defp escape_binary(<<?>, rest::bits>>, original, skip, len, acc),
do:
escape_binary(
rest,
original,
skip + len + 1,
0,
acc <> binary_part(original, skip, len) <> "&gt;"
)

defp escape_binary(<<char, rest::bits>>, original, skip, len, acc) when char < 0x80,
do: escape_binary(rest, original, skip, len + 1, acc)

defp escape_binary(<<cp::utf8, rest::bits>>, original, skip, len, acc) when cp < 0x800,
do: escape_binary(rest, original, skip, len + 2, acc)

defp escape_binary(<<cp::utf8, rest::bits>>, original, skip, len, acc) when cp < 0x10000,
do: escape_binary(rest, original, skip, len + 3, acc)

defp escape_binary(<<_::utf8, rest::bits>>, original, skip, len, acc),
do: escape_binary(rest, original, skip, len + 4, acc)

@doc false
def escape_cdata(""), do: []
def escape_cdata("]]>" <> rest), do: ["]]]]><![CDATA[>" | escape_cdata(rest)]
def escape_cdata(<<char::utf8>> <> rest), do: [<<char::utf8>> | escape_cdata(rest)]
def escape_cdata(binary) when is_binary(binary) do
escape_cdata(binary, binary, 0, 0)
end

defp escape_cdata(<<>>, original, 0, _len), do: original
defp escape_cdata(<<>>, original, skip, len), do: [binary_part(original, skip, len)]

defp escape_cdata(<<"]]>", rest::bits>>, original, skip, 0),
do: ["]]]]><![CDATA[>" | escape_cdata(rest, original, skip + 3, 0)]

defp escape_cdata(<<"]]>", rest::bits>>, original, skip, len),
do: [
binary_part(original, skip, len),
"]]]]><![CDATA[>" | escape_cdata(rest, original, skip + len + 3, 0)
]

defp escape_cdata(<<char, rest::bits>>, original, skip, len) when char < 0x80,
do: escape_cdata(rest, original, skip, len + 1)

defp escape_cdata(<<cp::utf8, rest::bits>>, original, skip, len) when cp < 0x800,
do: escape_cdata(rest, original, skip, len + 2)

defp escape_cdata(<<cp::utf8, rest::bits>>, original, skip, len) when cp < 0x10000,
do: escape_cdata(rest, original, skip, len + 3)

defp escape_cdata(<<_::utf8, rest::bits>>, original, skip, len),
do: escape_cdata(rest, original, skip, len + 4)

@doc false
def encode_comment(text) do
Expand All @@ -40,23 +172,32 @@ defmodule XmlStream.Printer do
defp validate_comment!(<<_char::utf8>> <> rest), do: validate_comment!(rest)

@doc false
def encode_name(name) do
name = to_string(name)
def encode_name(name) when is_binary(name) do
validate_name!(name)
name
end

def encode_name(name) when is_atom(name) do
encode_name(Atom.to_string(name))
end

def encode_name(name), do: encode_name(to_string(name))

@doc false
def pi_target_name(name) do
if String.downcase(name) == "xml" do
raise EncodeError, message: "'xml' is a reserved name"
else
encode_name(name)
end
def pi_target_name(<<x, m, l>>)
when (x === ?x or x === ?X) and (m === ?m or m === ?M) and (l === ?l or l === ?L) do
raise EncodeError, message: "'xml' is a reserved name"
end

def pi_target_name(name), do: encode_name(name)

defp validate_name!(""), do: raise(EncodeError, message: "Invalid tag name")

defp validate_name!(<<char, rest::bits>>)
when char in ?A..?Z or char in ?a..?z or char === ?_ or char === ?: do
validate_name_rest!(rest)
end

defp validate_name!(<<char::utf8>> <> rest) do
validate_name_start!(char)
validate_name_rest!(rest)
Expand Down Expand Up @@ -88,11 +229,14 @@ defmodule XmlStream.Printer do

defp validate_name_rest!(""), do: :ok

defp validate_name_rest!(<<char, rest::bits>>)
when char in ?A..?Z or char in ?a..?z or char in ?0..?9 or
char === ?_ or char === ?: or char === ?- or char === ?. do
validate_name_rest!(rest)
end

defp validate_name_rest!(<<char::utf8>> <> rest)
when char in [?:, ?_, ?-, ?., 0xB7] or
char in ?0..?9 or
char in ?A..?Z or
char in ?a..?z or
when char === 0xB7 or
char in 0xC0..0xD6 or
char in 0xD8..0xF6 or
char in 0xF8..0x37D or
Expand Down
Loading