String.replace
uses :binary.replace
which in turn uses binary:matches
with some recursion. By writing a custom version of :binary.replace
we can do it all in one go.
I should say that :binary.replace
almost works out of the box.
:binary.replace(body, ["&", "<", ">"], <<"&;">>, [:global, {:insert_replaced, 1}])
Unfortunately we need not just to wrap the replacement but substitute them with amp, lt and gt respectively.
Anyway. After a quick look at binary.replace
implementation you can do the same with:
def replace_char("&"), do: "&"
def replace_char(">"), do: ">"
def replace_char("<"), do: "<"
def escape_body(body) do
match_list = :binary.matches(body, ["&", ">", "<"])
IO.iodata_to_binary(do_replace(body, match_list, &replace_char/1, 0))
end
def do_replace(h, [], _, n) do
[ :binary.part(h, {n, byte_size(h) - n}) ]
end
def do_replace(h, [{a, b} | t ], replace_fun, n) do
[ :binary.part(h, {n, a - n}),
replace_fun.(:binary.part(h, {a, b}))
| do_replace(h, t, replace_fun, a + b)
]
end
This is basically the binary:replace/3
function with some options taken away and using a function to replace the matches rather than a plain substitution. I don’t know what the penalty of this is.
Obviously this is not more readable than your current example but if you generalize it you can likely have a function like this:
general_replace(body, [{"&", "&"}, {"<", "<"}, {">", ">"}])
EDIT
Here is a the general substitution function:
def substitute(data, subs) do
match_list = :binary.matches(data, Map.keys(subs))
IO.iodata_to_binary(do_replace(data, match_list, subs, 0))
end
def do_replace(h, [], _, n) do
[ :binary.part(h, {n, byte_size(h) - n}) ]
end
def do_replace(h, [{a, b} | t ], subs, n) do
[ :binary.part(h, {n, a - n}),
Map.get(subs, :binary.part(h, {a, b}))
| do_replace(h, t, subs, a + b)
]
end
It takes a body of text and a map where each key in the map will be replaced by its value.