Skip to content
Merged
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
66 changes: 51 additions & 15 deletions c_src/lazy_html.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -902,26 +902,62 @@ std::vector<int64_t> 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<std::string> &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<StackFrame> 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<char *>(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<char *>(text), size);
stack.pop_back();
}

if (stack.empty()) {
return;
}
}
}

std::string text(ErlNifEnv *env, ExLazyHTML ex_lazy_html,
std::optional<std::string> separator) {
auto content = std::string();

for (auto node : ex_lazy_html.resource->nodes) {
node_text(node, content, separator);
}

return content;
Expand Down
21 changes: 18 additions & 3 deletions lib/lazy_html.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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|<div><span>Hello</span> <span>world</span></div>|)
iex> LazyHTML.text(lazy_html)
"Hello world"

iex> lazy_html = LazyHTML.from_fragment(~S|<div><span>1</span><span>2</span><span>3</span></div>|)
iex> LazyHTML.text(lazy_html, separator: ", ")
"1, 2, 3"

iex> lazy_html = LazyHTML.from_fragment(~S|<div><span>1</span><span></span><span>2</span></div>|)
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`:

Expand All @@ -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'''
Expand Down
2 changes: 1 addition & 1 deletion lib/lazy_html/nif.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
Expand Down
Loading