diff --git a/c_src/lazy_html.cpp b/c_src/lazy_html.cpp index 6bfda8f..2f5746f 100644 --- a/c_src/lazy_html.cpp +++ b/c_src/lazy_html.cpp @@ -902,26 +902,62 @@ std::vector nth_child(ErlNifEnv *env, ExLazyHTML ex_lazy_html) { } FINE_NIF(nth_child, ERL_NIF_DIRTY_JOB_CPU_BOUND); -std::string text(ErlNifEnv *env, ExLazyHTML ex_lazy_html) { - auto document = ex_lazy_html.resource->document_ref->document; +void node_text(lxb_dom_node_t *node, std::string &content, + std::optional &separator) { + // We use an explicit stack instead of recursion to avoid stack + // overflow on deeply nested trees. + struct StackFrame { + lxb_dom_node_t *next_child; + }; - auto content = std::string(); + std::vector stack; - for (auto node : ex_lazy_html.resource->nodes) { - if (node->type == LXB_DOM_NODE_TYPE_ELEMENT || - node->type == LXB_DOM_NODE_TYPE_TEXT) { - size_t size; - auto text = lxb_dom_node_text_content(node, &size); - if (text == NULL) { - throw std::runtime_error("failed to get element text content"); + auto current = node; + + while (true) { + if (current->type == LXB_DOM_NODE_TYPE_TEXT) { + auto character_data = lxb_dom_interface_character_data(current); + if (character_data->data.length > 0) { + if (separator.has_value() && !content.empty()) { + content.append(separator.value()); + } + content.append(reinterpret_cast(character_data->data.data), + character_data->data.length); + } + } else if (current->type == LXB_DOM_NODE_TYPE_ELEMENT) { + auto first_child = template_aware_first_child(current); + if (first_child != nullptr) { + stack.push_back({lxb_dom_node_next(first_child)}); + current = first_child; + continue; + } + } + + // Advance to the next sibling, or pop frames until we find one. + while (!stack.empty()) { + auto &frame = stack.back(); + + if (frame.next_child != nullptr) { + current = frame.next_child; + frame.next_child = lxb_dom_node_next(current); + break; } - auto text_guard = ScopeGuard([&]() { - lxb_dom_document_destroy_text(lxb_dom_interface_document(document), - text); - }); - content.append(reinterpret_cast(text), size); + stack.pop_back(); } + + if (stack.empty()) { + return; + } + } +} + +std::string text(ErlNifEnv *env, ExLazyHTML ex_lazy_html, + std::optional separator) { + auto content = std::string(); + + for (auto node : ex_lazy_html.resource->nodes) { + node_text(node, content, separator); } return content; diff --git a/lib/lazy_html.ex b/lib/lazy_html.ex index dfd3398..599c50b 100644 --- a/lib/lazy_html.ex +++ b/lib/lazy_html.ex @@ -402,12 +402,26 @@ defmodule LazyHTML do @doc """ Returns the text content of all nodes in `lazy_html`. + ## Options + + * `:separator` - a separator used to join the text content from + individual nodes. Note that the separator is only inserted + between non-empty nodes. Defaults to no separator. + ## Examples iex> lazy_html = LazyHTML.from_fragment(~S|
Hello world
|) iex> LazyHTML.text(lazy_html) "Hello world" + iex> lazy_html = LazyHTML.from_fragment(~S|
123
|) + iex> LazyHTML.text(lazy_html, separator: ", ") + "1, 2, 3" + + iex> lazy_html = LazyHTML.from_fragment(~S|
12
|) + iex> LazyHTML.text(lazy_html, separator: ", ") + "1, 2" + If you want to get the text for each root node separately, you can use `Enum.map/2`: @@ -424,9 +438,10 @@ defmodule LazyHTML do ["Hello", "world"] """ - @spec text(t()) :: String.t() - def text(%LazyHTML{} = lazy_html) do - LazyHTML.NIF.text(lazy_html) + @spec text(t(), keyword()) :: String.t() + def text(%LazyHTML{} = lazy_html, opts \\ []) do + opts = Keyword.validate!(opts, [:separator]) + LazyHTML.NIF.text(lazy_html, opts[:separator]) end @doc ~S''' diff --git a/lib/lazy_html/nif.ex b/lib/lazy_html/nif.ex index 1661af9..e1e2212 100644 --- a/lib/lazy_html/nif.ex +++ b/lib/lazy_html/nif.ex @@ -23,7 +23,7 @@ defmodule LazyHTML.NIF do def child_nodes(_lazy_html), do: err!() def parent_node(_lazy_html), do: err!() def nth_child(_lazy_html), do: err!() - def text(_lazy_html), do: err!() + def text(_lazy_html, _separator), do: err!() def attribute(_lazy_html, _name), do: err!() def attributes(_lazy_html), do: err!() def tag(_lazy_html), do: err!()