defmodule TruncateHtml do import TruncateHtml.HtmlToken @regex ~r/(?:.*<\/script>)+|<\/?[^>]+>|[[:alpha:]0-9\|`~!@#\$%^&*\(\)\-_\+=\[\]{}:;'²³§",\.\/?]+|\s+|[[:punct:]]/ def truncate_html(html, limit, opts \\ []) do omission = opts[:omission] || "..." break_word = opts[:break_word] || false Regex.scan(@regex, html) |> List.flatten |> Enum.map(&(String.replace(&1, ~r/\n|(\s+)/, " "))) |> process_tokens([], "", 0, limit, omission, break_word) end defp process_tokens([token | rest], open_tags, result, length, limit, omission, break_word) when length < limit do case token_spec(token) do {:tag, true} -> process_tokens(rest, [token] ++ open_tags, result <> token, length, limit, omission, break_word) {:tag, false} -> process_tokens(rest, without_open_tag(open_tags, token), result <> token, length, limit, omission, break_word) {:text} -> process_tokens(rest, open_tags, result <> token, length + String.length(token), limit, omission, break_word) {:comment} -> process_tokens(rest, open_tags, result <> token, length, limit, omission, break_word) end end defp process_tokens(_, open_tags, result, length, limit, omission, break_word) when length >= limit do case break_word do true -> String.slice(result, 0..(String.length(result) - (length - limit) - 1)) false -> result end <> omission |> with_closed_tags(open_tags) end defp process_tokens([], _, result, _, _, _, _) do result end defp with_closed_tags(html, [tag | rest]) do with_closed_tags(html <> matching_close_tag(tag), rest) end defp with_closed_tags(html, []) do html end defp without_open_tag(tags, close_tag) do List.delete(tags, Enum.find(tags, &(matching_close_tag(&1) == close_tag))) end end