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
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: xml2
Title: Parse XML
Version: 1.3.2.9000
Version: 1.3.2.9001
Authors@R:
c(person(given = "Hadley",
family = "Wickham",
Expand Down Expand Up @@ -40,7 +40,7 @@ VignetteBuilder:
knitr
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.1.0
RoxygenNote: 7.1.1
SystemRequirements: libxml2: libxml2-dev (deb), libxml2-devel
(rpm)
Collate:
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# xml2 (development version)

* `xml_find_all.xml_nodeset()` gains a `flatten` argument to control whether to return a single nodeset or a list of nodesets (#311, @jakejh)

* `write_xml()` and `write_html()` now return NULL invisibly, as they did prior to version 1.3.0 (#307)

# xml2 1.3.2
Expand Down
39 changes: 26 additions & 13 deletions R/xml_find.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@

#' @param xpath A string containing a xpath (1.0) expression.
#' @inheritParams xml_name
#' @return `xml_find_all` always returns a nodeset: if there are no matches
#' the nodeset will be empty. The result will always be unique; repeated
#' nodes are automatically de-duplicated.
#' @param ... Further arguments passed to or from other methods.
#' @return `xml_find_all` returns a nodeset if applied to a node, and a nodeset
#' or a list of nodesets if applied to a nodeset. If there are no matches,
#' the nodeset(s) will be empty. Within each nodeset, the result will always
#' be unique; repeated nodes are automatically de-duplicated.
#'
#' `xml_find_first` returns a node if applied to a node, and a nodeset
#' if applied to a nodeset. The output is *always* the same size as
Expand Down Expand Up @@ -46,11 +48,16 @@
#' </body>")
#' para <- xml_find_all(x, ".//p")
#'
#' # If you apply xml_find_all to a nodeset, it finds all matches,
#' # de-duplicates them, and returns as a single list. This means you
#' # By default, if you apply xml_find_all to a nodeset, it finds all matches,
#' # de-duplicates them, and returns as a single nodeset. This means you
#' # never know how many results you'll get
#' xml_find_all(para, ".//b")
#'
#' # If you set flatten to FALSE, though, xml_find_all will return a list of
#' # nodesets, where each nodeset contains the matches for the corresponding
#' # node in the original nodeset.
#' xml_find_all(para, ".//b", flatten = FALSE)
#'
#' # xml_find_first only returns the first match per input node. If there are 0
#' # matches it will return a missing node
#' xml_find_first(para, ".//b")
Expand All @@ -67,31 +74,37 @@
#' ')
#' xml_find_all(x, ".//f:doc")
#' xml_find_all(x, ".//f:doc", xml_ns(x))
xml_find_all <- function(x, xpath, ns = xml_ns(x)) {
xml_find_all <- function(x, xpath, ns = xml_ns(x), ...) {
UseMethod("xml_find_all")
}

#' @export
xml_find_all.xml_missing <- function(x, xpath, ns = xml_ns(x)) {
xml_find_all.xml_missing <- function(x, xpath, ns = xml_ns(x), ...) {
xml_nodeset()
}

#' @export
xml_find_all.xml_node <- function(x, xpath, ns = xml_ns(x)) {
xml_find_all.xml_node <- function(x, xpath, ns = xml_ns(x), ...) {
nodes <- .Call(xpath_search, x$node, x$doc, xpath, ns, Inf)
xml_nodeset(nodes)
}

#' @param flatten A logical indicating whether to return a single, flattened
#' nodeset or a list of nodesets.
#' @export
xml_find_all.xml_nodeset <- function(x, xpath, ns = xml_ns(x)) {
#' @rdname xml_find_all
xml_find_all.xml_nodeset <- function(x, xpath, ns = xml_ns(x), flatten = TRUE, ...) {
if (length(x) == 0)
return(xml_nodeset())

nodes <- unlist(recursive = FALSE,
lapply(x, function(x)
.Call(xpath_search, x$node, x$doc, xpath, ns, Inf)))
res <- lapply(x, function(x) .Call(xpath_search, x$node, x$doc, xpath, ns, Inf))

xml_nodeset(nodes)
if (isTRUE(flatten)) {
return(xml_nodeset(unlist(recursive = FALSE, res)))
}

res[] <- lapply(res, xml_nodeset)
res
}

#' @export
Expand Down
26 changes: 20 additions & 6 deletions man/xml_find_all.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions tests/testthat/test-xml_find.R
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ test_that("no matches returns empty nodeset", {
expect_equal(length(xml_find_all(x, "//baz")), 0)
})

test_that("xml_find_all returns nodeset or list of nodesets based on flatten", {
x <- read_xml("<body><p>Some <b>text</b>.</p>
<p>Some <b>other</b> <b>text</b>.</p>
<p>No bold here!</p></body>")
y <- xml_find_all(x, './/p')
z <- xml_find_all(y, './/b', flatten = FALSE)
expect_s3_class(xml_find_all(y, './/b'), 'xml_nodeset')
expect_type(z, 'list')
expect_s3_class(z[[1L]], 'xml_nodeset')
})

# Find num ---------------------------------------------------------------------
test_that("xml_find_num errors with non numeric results", {
x <- read_xml("<x><y/><y/></x>")
Expand Down