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!()