Skip to content

Commit 01fc562

Browse files
Add :separator option to LazyHTML.text/1 (#33)
1 parent 4d946f2 commit 01fc562

3 files changed

Lines changed: 70 additions & 19 deletions

File tree

c_src/lazy_html.cpp

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -902,26 +902,62 @@ std::vector<int64_t> nth_child(ErlNifEnv *env, ExLazyHTML ex_lazy_html) {
902902
}
903903
FINE_NIF(nth_child, ERL_NIF_DIRTY_JOB_CPU_BOUND);
904904

905-
std::string text(ErlNifEnv *env, ExLazyHTML ex_lazy_html) {
906-
auto document = ex_lazy_html.resource->document_ref->document;
905+
void node_text(lxb_dom_node_t *node, std::string &content,
906+
std::optional<std::string> &separator) {
907+
// We use an explicit stack instead of recursion to avoid stack
908+
// overflow on deeply nested trees.
909+
struct StackFrame {
910+
lxb_dom_node_t *next_child;
911+
};
907912

908-
auto content = std::string();
913+
std::vector<StackFrame> stack;
909914

910-
for (auto node : ex_lazy_html.resource->nodes) {
911-
if (node->type == LXB_DOM_NODE_TYPE_ELEMENT ||
912-
node->type == LXB_DOM_NODE_TYPE_TEXT) {
913-
size_t size;
914-
auto text = lxb_dom_node_text_content(node, &size);
915-
if (text == NULL) {
916-
throw std::runtime_error("failed to get element text content");
915+
auto current = node;
916+
917+
while (true) {
918+
if (current->type == LXB_DOM_NODE_TYPE_TEXT) {
919+
auto character_data = lxb_dom_interface_character_data(current);
920+
if (character_data->data.length > 0) {
921+
if (separator.has_value() && !content.empty()) {
922+
content.append(separator.value());
923+
}
924+
content.append(reinterpret_cast<char *>(character_data->data.data),
925+
character_data->data.length);
926+
}
927+
} else if (current->type == LXB_DOM_NODE_TYPE_ELEMENT) {
928+
auto first_child = template_aware_first_child(current);
929+
if (first_child != nullptr) {
930+
stack.push_back({lxb_dom_node_next(first_child)});
931+
current = first_child;
932+
continue;
933+
}
934+
}
935+
936+
// Advance to the next sibling, or pop frames until we find one.
937+
while (!stack.empty()) {
938+
auto &frame = stack.back();
939+
940+
if (frame.next_child != nullptr) {
941+
current = frame.next_child;
942+
frame.next_child = lxb_dom_node_next(current);
943+
break;
917944
}
918-
auto text_guard = ScopeGuard([&]() {
919-
lxb_dom_document_destroy_text(lxb_dom_interface_document(document),
920-
text);
921-
});
922945

923-
content.append(reinterpret_cast<char *>(text), size);
946+
stack.pop_back();
924947
}
948+
949+
if (stack.empty()) {
950+
return;
951+
}
952+
}
953+
}
954+
955+
std::string text(ErlNifEnv *env, ExLazyHTML ex_lazy_html,
956+
std::optional<std::string> separator) {
957+
auto content = std::string();
958+
959+
for (auto node : ex_lazy_html.resource->nodes) {
960+
node_text(node, content, separator);
925961
}
926962

927963
return content;

lib/lazy_html.ex

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,26 @@ defmodule LazyHTML do
402402
@doc """
403403
Returns the text content of all nodes in `lazy_html`.
404404
405+
## Options
406+
407+
* `:separator` - a separator used to join the text content from
408+
individual nodes. Note that the separator is only inserted
409+
between non-empty nodes. Defaults to no separator.
410+
405411
## Examples
406412
407413
iex> lazy_html = LazyHTML.from_fragment(~S|<div><span>Hello</span> <span>world</span></div>|)
408414
iex> LazyHTML.text(lazy_html)
409415
"Hello world"
410416
417+
iex> lazy_html = LazyHTML.from_fragment(~S|<div><span>1</span><span>2</span><span>3</span></div>|)
418+
iex> LazyHTML.text(lazy_html, separator: ", ")
419+
"1, 2, 3"
420+
421+
iex> lazy_html = LazyHTML.from_fragment(~S|<div><span>1</span><span></span><span>2</span></div>|)
422+
iex> LazyHTML.text(lazy_html, separator: ", ")
423+
"1, 2"
424+
411425
If you want to get the text for each root node separately, you can
412426
use `Enum.map/2`:
413427
@@ -424,9 +438,10 @@ defmodule LazyHTML do
424438
["Hello", "world"]
425439
426440
"""
427-
@spec text(t()) :: String.t()
428-
def text(%LazyHTML{} = lazy_html) do
429-
LazyHTML.NIF.text(lazy_html)
441+
@spec text(t(), keyword()) :: String.t()
442+
def text(%LazyHTML{} = lazy_html, opts \\ []) do
443+
opts = Keyword.validate!(opts, [:separator])
444+
LazyHTML.NIF.text(lazy_html, opts[:separator])
430445
end
431446

432447
@doc ~S'''

lib/lazy_html/nif.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule LazyHTML.NIF do
2323
def child_nodes(_lazy_html), do: err!()
2424
def parent_node(_lazy_html), do: err!()
2525
def nth_child(_lazy_html), do: err!()
26-
def text(_lazy_html), do: err!()
26+
def text(_lazy_html, _separator), do: err!()
2727
def attribute(_lazy_html, _name), do: err!()
2828
def attributes(_lazy_html), do: err!()
2929
def tag(_lazy_html), do: err!()

0 commit comments

Comments
 (0)