use crate::tests::assert_dom::virtual_dom::VirtualNode;
fn deduplicate_nodes(nodes: Vec<&VirtualNode>) -> Vec<&VirtualNode> {
let mut seen: Vec<*const VirtualNode> = Vec::new();
let mut result = Vec::new();
for node in nodes {
let ptr = node as *const VirtualNode;
if !seen.contains(&ptr) {
seen.push(ptr);
result.push(node);
}
}
result
}
pub(crate) fn query_xpath<'a>(root: &'a VirtualNode, xpath: &str) -> Vec<&'a VirtualNode> {
let xpath = xpath.trim();
if xpath.starts_with('(') {
return query_parenthesized(root, xpath);
}
if let Some(rest) = xpath.strip_prefix("//") {
return query_descendant_or_self(root, rest);
}
if let Some(rest) = xpath.strip_prefix('/') {
return query_from_root(root, rest);
}
query_descendant_or_self(root, xpath)
}
fn find_matching_paren(s: &str) -> Option<usize> {
let mut depth = 0;
for (i, ch) in s.chars().enumerate() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
}
None
}
fn query_parenthesized<'a>(root: &'a VirtualNode, xpath: &str) -> Vec<&'a VirtualNode> {
let close_paren = find_matching_paren(xpath);
if let Some(close_paren) = close_paren {
let subquery = &xpath[1..close_paren]; let rest = &xpath[close_paren + 1..].trim_start();
let mut results = query_xpath(root, subquery);
if let Some(rest) = rest.strip_prefix('[') {
if let Some(bracket_end) = rest.find(']') {
let predicate = &rest[..bracket_end];
let remaining = &rest[bracket_end + 1..];
if let Ok(index) = predicate.trim().parse::<usize>() {
if index > 0 && index <= results.len() {
results = vec![results[index - 1]];
} else {
return vec![];
}
}
if !remaining.is_empty() {
if remaining.starts_with('[') {
let (predicates, path_after) = extract_predicates(remaining);
results.retain(|node| matches_predicate(node, predicates));
if !path_after.is_empty() {
let mut final_results = Vec::new();
for node in results {
if path_after.starts_with('/') && !path_after.starts_with("//") {
final_results.extend(query_from_root(node, &path_after[1..]));
} else if let Some(stripped) = path_after.strip_prefix("//") {
final_results.extend(query_descendant_or_self(node, stripped));
}
}
return final_results;
}
return results;
}
if let Some(axis_rest) = remaining.strip_prefix("/preceding-sibling::") {
let (axis_selector, continuation) =
if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for node in results {
let siblings = find_preceding_siblings(root, node, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results
.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return final_results;
}
if let Some(axis_rest) = remaining.strip_prefix("/following-sibling::") {
let (axis_selector, continuation) =
if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for node in results {
let siblings = find_following_siblings(root, node, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results
.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return final_results;
}
if let Some(axis_rest) = remaining.strip_prefix("/self::") {
let (selector, predicate, continuation) =
parse_selector_with_predicates(axis_rest);
let selector_with_pred = if let Some(pred) = predicate {
format!("{}{}", selector, pred)
} else {
selector.to_string()
};
results.retain(|node| matches_selector(node, &selector_with_pred));
if let Some(cont) = continuation {
let mut final_results = Vec::new();
for node in results {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(node, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(node, stripped));
}
}
return final_results;
}
return results;
}
let mut final_results = Vec::new();
for node in results {
if remaining.starts_with('/') && !remaining.starts_with("//") {
final_results.extend(query_from_root(node, &remaining[1..]));
} else if let Some(stripped) = remaining.strip_prefix("//") {
final_results.extend(query_descendant_or_self(node, stripped));
}
}
return final_results;
}
}
} else if !rest.is_empty() {
if let Some(axis_rest) = rest.strip_prefix("/preceding-sibling::") {
let (axis_selector, continuation) = if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for node in results {
let siblings = find_preceding_siblings(root, node, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return final_results;
}
if let Some(axis_rest) = rest.strip_prefix("/following-sibling::") {
let (axis_selector, continuation) = if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for node in results {
let siblings = find_following_siblings(root, node, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return final_results;
}
if let Some(axis_rest) = rest.strip_prefix("/self::") {
let (selector, predicate, continuation) = parse_selector_with_predicates(axis_rest);
let selector_with_pred = if let Some(pred) = predicate {
format!("{}{}", selector, pred)
} else {
selector.to_string()
};
results.retain(|node| matches_selector(node, &selector_with_pred));
if let Some(cont) = continuation {
let mut final_results = Vec::new();
for node in results {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(node, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(node, stripped));
}
}
return final_results;
}
return results;
}
let mut final_results = Vec::new();
for node in results {
if rest.starts_with('/') && !rest.starts_with("//") {
final_results.extend(query_from_root(node, &rest[1..]));
} else if let Some(stripped) = rest.strip_prefix("//") {
final_results.extend(query_descendant_or_self(node, stripped));
}
}
return final_results;
}
return results;
}
vec![]
}
fn parse_selector_with_predicates(pattern: &str) -> (&str, Option<&str>, Option<&str>) {
let mut base_end = 0;
let mut predicate_start: Option<usize> = None;
let mut predicate_end: Option<usize> = None;
let mut bracket_depth = 0;
let mut in_string = false;
let mut string_delim = '\0';
for (i, ch) in pattern.char_indices() {
match ch {
'[' if !in_string => {
if bracket_depth == 0 && predicate_start.is_none() {
predicate_start = Some(i);
base_end = i;
}
bracket_depth += 1;
}
']' if !in_string => {
bracket_depth -= 1;
if bracket_depth == 0 {
predicate_end = Some(i + 1);
}
}
'"' | '\'' if bracket_depth > 0 => {
if !in_string {
in_string = true;
string_delim = ch;
} else if ch == string_delim {
in_string = false;
}
}
'/' if bracket_depth == 0 && !in_string => {
let base = if base_end > 0 {
&pattern[..base_end]
} else {
&pattern[..i]
};
let pred = if let (Some(start), Some(end)) = (predicate_start, predicate_end) {
Some(&pattern[start..end])
} else {
None
};
return (base, pred, Some(&pattern[i..]));
}
_ => {}
}
}
if let (Some(start), Some(end)) = (predicate_start, predicate_end) {
(&pattern[..base_end], Some(&pattern[start..end]), None)
} else if base_end > 0 {
(&pattern[..base_end], None, None)
} else {
(pattern, None, None)
}
}
fn query_descendant_or_self<'a>(node: &'a VirtualNode, pattern: &str) -> Vec<&'a VirtualNode> {
let (base_selector, predicate_part, continuation) = parse_selector_with_predicates(pattern);
if let Some(cont) = continuation {
let first_str = if let Some(pred) = predicate_part {
format!("{}{}", base_selector.trim(), pred)
} else {
base_selector.trim().to_string()
};
let first = first_str.as_str();
let is_descendant_path = cont.starts_with("//");
let rest = cont.trim_start_matches('/');
if let Some(axis_rest) = rest.strip_prefix("preceding-sibling::") {
let mut results = Vec::new();
collect_descendants_matching(node, first, &mut results);
results = apply_numeric_predicate(results, first);
let (axis_selector, axis_predicate, continuation) =
parse_selector_with_predicates(axis_rest);
let axis_selector_with_pred = if let Some(pred) = axis_predicate {
format!("{}{}", axis_selector, pred)
} else {
axis_selector.to_string()
};
let mut final_results = Vec::new();
for matched_node in results {
let siblings =
find_preceding_siblings(node, matched_node, &axis_selector_with_pred);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return deduplicate_nodes(final_results);
}
if let Some(axis_rest) = rest.strip_prefix("following-sibling::") {
let mut results = Vec::new();
collect_descendants_matching(node, first, &mut results);
results = apply_numeric_predicate(results, first);
let (axis_selector, axis_predicate, continuation) =
parse_selector_with_predicates(axis_rest);
let axis_selector_with_pred = if let Some(pred) = axis_predicate {
format!("{}{}", axis_selector, pred)
} else {
axis_selector.to_string()
};
let mut final_results = Vec::new();
for matched_node in results {
let siblings =
find_following_siblings(node, matched_node, &axis_selector_with_pred);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
return deduplicate_nodes(final_results);
}
let mut results = Vec::new();
collect_descendants_matching(node, first, &mut results);
results = apply_numeric_predicate(results, first);
let mut final_results = Vec::new();
for matched_node in results {
if is_descendant_path {
let descendants = query_descendant_or_self(matched_node, rest);
final_results.extend(descendants);
} else {
let children_results = query_from_root(matched_node, rest);
final_results.extend(children_results);
}
}
deduplicate_nodes(final_results)
} else {
let pattern = pattern.trim();
let (base_selector, predicate_part, continuation) = parse_selector_with_predicates(pattern);
let mut results = Vec::new();
collect_descendants_matching(node, base_selector, &mut results);
if let Some(pred) = predicate_part {
results.retain(|n| matches_predicate(n, pred));
}
if let Some(cont) = continuation {
let mut final_results = Vec::new();
for matched_node in results {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(matched_node, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(matched_node, stripped));
}
}
return apply_numeric_predicate(final_results, pattern);
}
apply_numeric_predicate(results, pattern)
}
}
fn query_from_root<'a>(node: &'a VirtualNode, pattern: &str) -> Vec<&'a VirtualNode> {
if let Some(axis_rest) = pattern.strip_prefix("following-sibling::") {
return find_following_siblings(node, node, axis_rest.trim());
}
if let Some(axis_rest) = pattern.strip_prefix("preceding-sibling::") {
return find_preceding_siblings(node, node, axis_rest.trim());
}
if let Some(descendant_rest) = pattern.strip_prefix("//") {
return query_descendant_or_self(node, descendant_rest);
}
if let Some((first, rest)) = pattern.split_once('/') {
let first = first.trim();
let rest = rest.trim();
if let Some(axis_rest) = rest.strip_prefix("following-sibling::") {
let (axis_selector, continuation) = if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for child in &node.children {
if matches_selector(child, first) {
let siblings = find_following_siblings(node, child, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
}
return deduplicate_nodes(final_results);
}
if let Some(axis_rest) = rest.strip_prefix("preceding-sibling::") {
let (axis_selector, continuation) = if let Some(slash_pos) = axis_rest.find('/') {
(&axis_rest[..slash_pos], Some(&axis_rest[slash_pos..]))
} else {
(axis_rest.trim(), None)
};
let mut final_results = Vec::new();
for child in &node.children {
if matches_selector(child, first) {
let siblings = find_preceding_siblings(node, child, axis_selector);
if let Some(cont) = continuation {
for sibling in siblings {
if let Some(stripped) = cont.strip_prefix("//") {
final_results.extend(query_descendant_or_self(sibling, stripped));
} else if let Some(stripped) = cont.strip_prefix('/') {
final_results.extend(query_from_root(sibling, stripped));
}
}
} else {
final_results.extend(siblings);
}
}
}
return deduplicate_nodes(final_results);
}
let mut matching_children: Vec<&VirtualNode> = node
.children
.iter()
.filter(|child| matches_selector(child, first))
.collect();
matching_children = apply_numeric_predicate(matching_children, first.trim());
let mut results = Vec::new();
for child in matching_children {
if rest.is_empty() {
results.push(child);
} else if rest.starts_with('/') {
results.extend(query_descendant_or_self(
child,
rest.trim_start_matches('/'),
));
} else {
results.extend(query_from_root(child, rest));
}
}
results
} else {
let results: Vec<&VirtualNode> = node
.children
.iter()
.filter(|child| matches_selector(child, pattern.trim()))
.collect();
apply_numeric_predicate(results, pattern.trim())
}
}
fn collect_descendants_matching<'a>(
node: &'a VirtualNode,
selector: &str,
results: &mut Vec<&'a VirtualNode>,
) {
for child in &node.children {
if matches_selector(child, selector) {
results.push(child);
}
collect_descendants_matching(child, selector, results);
}
}
fn find_preceding_siblings<'a>(
root: &'a VirtualNode,
target: &'a VirtualNode,
selector: &str,
) -> Vec<&'a VirtualNode> {
fn find_parent(node: &VirtualNode, target: *const VirtualNode) -> Option<&VirtualNode> {
for child in &node.children {
if std::ptr::eq(child as *const _, target) {
return Some(node);
}
if let Some(parent) = find_parent(child, target) {
return Some(parent);
}
}
None
}
let target_ptr = target as *const VirtualNode;
let parent = match find_parent(root, target_ptr) {
Some(p) => p,
None => return vec![], };
let mut results = Vec::new();
for child in &parent.children {
if std::ptr::eq(child as *const _, target_ptr) {
break;
}
if matches_selector(child, selector) {
results.push(child);
}
}
results
}
fn apply_numeric_predicate<'a>(
results: Vec<&'a VirtualNode>,
selector: &str,
) -> Vec<&'a VirtualNode> {
if let Some(bracket_pos) = selector.find('[')
&& let Some(predicate) = selector[bracket_pos..]
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
{
if let Ok(index) = predicate.trim().parse::<usize>() {
if index > 0 && index <= results.len() {
return vec![results[index - 1]];
} else {
return vec![];
}
}
}
results
}
fn find_following_siblings<'a>(
root: &'a VirtualNode,
target: &'a VirtualNode,
selector: &str,
) -> Vec<&'a VirtualNode> {
fn find_parent(node: &VirtualNode, target: *const VirtualNode) -> Option<&VirtualNode> {
for child in &node.children {
if std::ptr::eq(child as *const _, target) {
return Some(node);
}
if let Some(parent) = find_parent(child, target) {
return Some(parent);
}
}
None
}
let target_ptr = target as *const VirtualNode;
let parent = match find_parent(root, target_ptr) {
Some(p) => p,
None => return vec![], };
let mut results = Vec::new();
let mut found_target = false;
for child in &parent.children {
if found_target {
if matches_selector(child, selector) {
results.push(child);
}
} else if std::ptr::eq(child as *const _, target_ptr) {
found_target = true;
}
}
results
}
fn matches_selector(node: &VirtualNode, selector: &str) -> bool {
let selector = selector.trim();
let (base_selector, predicate) = if let Some(bracket_pos) = selector.find('[') {
(&selector[..bracket_pos], Some(&selector[bracket_pos..]))
} else {
(selector, None)
};
if base_selector == "*" {
if node.tag == "text" {
return false;
}
if let Some(predicate) = predicate {
return matches_predicate(node, predicate);
}
return true;
}
if let Some(class_name) = base_selector.strip_prefix('.') {
return node.classes.iter().any(|c| c == class_name);
}
if let Some(id) = base_selector.strip_prefix('#') {
return node.id.as_deref() == Some(id);
}
if !base_selector.is_empty() && node.tag != base_selector {
return false;
}
if let Some(predicate) = predicate {
return matches_predicate(node, predicate);
}
true
}
fn extract_predicates(path: &str) -> (&str, &str) {
let path = path.trim();
let mut end = 0;
let mut in_predicate = false;
let mut bracket_depth = 0;
for (i, ch) in path.chars().enumerate() {
match ch {
'[' => {
in_predicate = true;
bracket_depth += 1;
}
']' => {
bracket_depth -= 1;
if bracket_depth == 0 {
end = i + 1;
in_predicate = false;
}
}
'/' if !in_predicate && bracket_depth == 0 => {
return (&path[..end], &path[end..]);
}
_ => {}
}
}
(&path[..end], &path[end..])
}
fn matches_predicate(node: &VirtualNode, predicate: &str) -> bool {
let mut predicate = predicate.trim();
while !predicate.is_empty() {
if let Some(bracket_start) = predicate.find('[') {
if let Some(bracket_end) =
find_closing_bracket(&predicate[bracket_start..]).map(|i| bracket_start + i)
{
let single_pred = &predicate[bracket_start + 1..bracket_end];
if !matches_single_predicate(node, single_pred) {
return false;
}
predicate = predicate[bracket_end + 1..].trim();
} else {
return false;
}
} else {
break;
}
}
true
}
fn find_closing_bracket(s: &str) -> Option<usize> {
let mut depth = 0;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut prev_char = '\0';
for (byte_pos, ch) in s.char_indices() {
if prev_char == '\\' {
prev_char = '\0'; continue;
}
match ch {
'"' if !in_single_quote => in_double_quote = !in_double_quote,
'\'' if !in_double_quote => in_single_quote = !in_single_quote,
'[' if !in_single_quote && !in_double_quote => depth += 1,
']' if !in_single_quote && !in_double_quote => {
depth -= 1;
if depth == 0 {
return Some(byte_pos);
}
}
_ => {}
}
prev_char = ch;
}
None
}
fn get_text_content(node: &VirtualNode) -> String {
if let Some(ref text) = node.text {
return text.clone();
}
let mut result = String::new();
for child in &node.children {
result.push_str(&get_text_content(child));
}
result
}
fn matches_single_predicate(node: &VirtualNode, predicate: &str) -> bool {
let predicate = predicate.trim();
if let Some(rest) = predicate.strip_prefix("normalize-space(text())") {
let rest = rest.trim();
if let Some(value_part) = rest.strip_prefix('=').map(|s| s.trim()) {
if let Some(value) = value_part.strip_prefix('\'') {
if let Some(value) = value.strip_suffix('\'') {
let unescaped = unescape_xpath_string(value);
return normalize_space(&get_text_content(node)) == unescaped;
}
}
else if let Some(value) = value_part.strip_prefix('"')
&& let Some(value) = value.strip_suffix('"')
{
let unescaped = unescape_xpath_string(value);
return normalize_space(&get_text_content(node)) == unescaped;
}
}
return false;
}
if let Some(rest) = predicate.strip_prefix("starts-with(") {
let rest = rest.trim();
if let Some(args) = rest.strip_prefix("text()") {
let args = args.trim();
if let Some(args) = args.strip_prefix(',') {
let args = args.trim();
if let Some(close_paren) = args.rfind(')') {
let value_part = args[..close_paren].trim();
if let Some(value) = value_part.strip_prefix('"')
&& let Some(value) = value.strip_suffix('"')
{
let unescaped = unescape_xpath_string(value);
return get_text_content(node).starts_with(&unescaped);
}
else if let Some(value) = value_part.strip_prefix('\'')
&& let Some(value) = value.strip_suffix('\'')
{
let unescaped = unescape_xpath_string(value);
return get_text_content(node).starts_with(&unescaped);
}
}
}
}
return false;
}
if let Some(rest) = predicate.strip_prefix("text()") {
let rest = rest.trim();
if let Some(value_part) = rest.strip_prefix('=').map(|s| s.trim()) {
if let Some(value) = value_part.strip_prefix('\'') {
if let Some(value) = value.strip_suffix('\'') {
let unescaped = unescape_xpath_string(value);
return get_text_content(node) == unescaped;
}
}
else if let Some(value) = value_part.strip_prefix('"')
&& let Some(value) = value.strip_suffix('"')
{
let unescaped = unescape_xpath_string(value);
return get_text_content(node) == unescaped;
}
}
return false;
}
if let Some(attr_part) = predicate.strip_prefix('@')
&& let Some((attr_name, value_part)) = attr_part.split_once('=')
{
let attr_name = attr_name.trim();
let value = value_part
.trim()
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or(value_part.trim());
match attr_name {
"class" => {
let required_classes: Vec<&str> = value.split_whitespace().collect();
return required_classes
.iter()
.all(|required| node.classes.iter().any(|c| c == *required));
}
"id" => return node.id.as_deref() == Some(value),
_ => {
return node.attributes.get(attr_name).map(|v| v.as_str()) == Some(value);
}
}
}
if let Some(attr_name) = predicate.strip_prefix('@')
&& !attr_name.contains('=')
{
let attr_name = attr_name.trim();
return match attr_name {
"class" => !node.classes.is_empty(),
"id" => node.id.is_some(),
_ => node.attributes.contains_key(attr_name),
};
}
true
}
fn normalize_space(s: &str) -> String {
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn unescape_xpath_string(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(next) = chars.next() {
match next {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'\\' => result.push('\\'),
'\'' => result.push('\''),
'"' => result.push('"'),
_ => {
result.push('\\');
result.push(next);
}
}
} else {
result.push('\\');
}
} else {
result.push(ch);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{assert_dom::virtual_dom::ToVirtualDom, prelude::*};
#[test]
fn query_all_paragraphs() {
let doc = Parser::default().parse("Para 1\n\nPara 2\n\nPara 3");
let vdom = doc.to_virtual_dom();
let paras = query_xpath(&vdom, "//p");
assert_eq!(paras.len(), 3);
}
#[test]
fn query_list_items() {
let doc = Parser::default().parse("* item 1\n* item 2\n* item 3");
let vdom = doc.to_virtual_dom();
let uls = query_xpath(&vdom, "//ul");
assert_eq!(uls.len(), 1);
let lis = query_xpath(&vdom, "//li");
assert_eq!(lis.len(), 3);
let ul_lis = query_xpath(&vdom, "//ul/li");
assert_eq!(ul_lis.len(), 3);
}
#[test]
fn query_with_class_selector() {
let doc = Parser::default().parse("* item 1\n* item 2");
let vdom = doc.to_virtual_dom();
let ulists = query_xpath(&vdom, "//.ulist");
assert_eq!(ulists.len(), 1);
assert_eq!(ulists[0].tag, "div"); }
#[test]
fn query_section_headings() {
let doc = Parser::default().parse("== Section 1\n\nPara\n\n== Section 2\n\nPara");
let vdom = doc.to_virtual_dom();
let h2s = query_xpath(&vdom, "//h2");
assert_eq!(h2s.len(), 2);
}
#[test]
fn query_nested_path() {
let doc = Parser::default().parse("== Section\n\n* item 1\n* item 2");
let vdom = doc.to_virtual_dom();
let uls = query_xpath(&vdom, "//ul");
assert_eq!(uls.len(), 1, "Should find one ul element");
let ul_lis = query_xpath(&vdom, "//ul/li");
assert_eq!(ul_lis.len(), 2, "Should find 2 li elements under ul");
let items = query_xpath(&vdom, "//div/ul/li");
assert_eq!(
items.len(),
2,
"Should find 2 li elements via //div/ul/li path"
);
}
#[test]
fn query_with_text_predicate() {
let doc = Parser::default().parse("Hello\n\nWorld");
let vdom = doc.to_virtual_dom();
let hello_para = query_xpath(&vdom, "//p[text()=\"Hello\"]");
assert_eq!(hello_para.len(), 1);
assert_eq!(hello_para[0].text.as_deref(), Some("Hello"));
}
#[test]
fn query_with_attribute_predicate() {
let doc = Parser::default().parse("* item");
let vdom = doc.to_virtual_dom();
let ulist_wrapper = query_xpath(&vdom, "//div[@class=\"ulist\"]");
assert_eq!(ulist_wrapper.len(), 1);
}
#[test]
fn query_with_numeric_predicate() {
let doc = Parser::default().parse("* item 1\n* item 2\n* item 3");
let vdom = doc.to_virtual_dom();
let first_li = query_xpath(&vdom, "//ul/li[1]");
assert_eq!(first_li.len(), 1);
let second_li = query_xpath(&vdom, "//ul/li[2]");
assert_eq!(second_li.len(), 1);
let third_li = query_xpath(&vdom, "//ul/li[3]");
assert_eq!(third_li.len(), 1);
let fourth_li = query_xpath(&vdom, "//ul/li[4]");
assert_eq!(fourth_li.len(), 0);
}
#[test]
fn query_with_wildcard() {
let doc = Parser::default().parse("* item 1\n* item 2");
let vdom = doc.to_virtual_dom();
let all_elements = query_xpath(&vdom, "//*");
assert!(!all_elements.is_empty());
let first_li_children = query_xpath(&vdom, "//ul/li[1]/*");
assert_eq!(first_li_children.len(), 1);
assert_eq!(first_li_children[0].tag, "p");
}
#[test]
fn query_numeric_predicate_with_wildcard() {
let doc = Parser::default().parse("* item 1\n* item 2\n* item 3");
let vdom = doc.to_virtual_dom();
let first_li_all_children = query_xpath(&vdom, "//ul/li[1]/*");
assert_eq!(first_li_all_children.len(), 1);
assert_eq!(first_li_all_children[0].tag, "p");
let second_li_all_children = query_xpath(&vdom, "//ul/li[2]/*");
assert_eq!(second_li_all_children.len(), 1);
assert_eq!(second_li_all_children[0].tag, "p");
}
#[test]
fn query_comprehensive_numeric_wildcard() {
let doc = Parser::default().parse("* item 1\n* item 2\n* item 3");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//ul/li[1]/*");
assert_eq!(result.len(), 1, "Should find exactly 1 child element");
assert_eq!(result[0].tag, "p", "The child should be a paragraph");
assert_eq!(
result[0].text.as_deref(),
Some("item 1"),
"The paragraph should contain 'item 1'"
);
}
#[test]
fn query_parenthesized_with_predicate() {
let doc = Parser::default().parse("- Foo\n- Boo\n\n//\n\n- Blech\n");
let vdom = doc.to_virtual_dom();
let all_uls = query_xpath(&vdom, "//ul");
assert_eq!(all_uls.len(), 2, "Should have 2 ul elements");
let first_ul = query_xpath(&vdom, "(//ul)[1]");
assert_eq!(first_ul.len(), 1, "Should find exactly 1 ul");
let second_ul = query_xpath(&vdom, "(//ul)[2]");
assert_eq!(second_ul.len(), 1, "Should find exactly 1 ul");
let first_ul_lis = query_xpath(&vdom, "(//ul)[1]/li");
assert_eq!(first_ul_lis.len(), 2, "First ul should have 2 li elements");
let second_ul_lis = query_xpath(&vdom, "(//ul)[2]/li");
assert_eq!(second_ul_lis.len(), 1, "Second ul should have 1 li element");
}
#[test]
fn query_parenthesized_complex() {
let doc = Parser::default().parse("- Foo\n- Boo\n\n.Also\n- Blech\n");
let vdom = doc.to_virtual_dom();
let all_uls = query_xpath(&vdom, "//ul");
assert_eq!(all_uls.len(), 2);
let result = query_xpath(&vdom, "(//ul)[1]/li[1]");
assert_eq!(result.len(), 1);
let result = query_xpath(&vdom, "(//ul)[2]/li");
assert_eq!(result.len(), 1);
let result = query_xpath(&vdom, "(//ul)[3]/li");
assert_eq!(result.len(), 0, "Out of bounds should return empty");
}
#[test]
fn query_parenthesized_complex_2() {
let doc = Parser::default().parse("List\n====\n\n- Foo\n- Boo\n\n//\n\n- Blech\n");
let vdom = doc.to_virtual_dom();
assert_eq!(query_xpath(&vdom, "//ul").len(), 2);
let first_ul_items = query_xpath(&vdom, "(//ul)[1]/li");
assert_eq!(first_ul_items.len(), 2);
let second_ul_items = query_xpath(&vdom, "(//ul)[2]/li");
assert_eq!(second_ul_items.len(), 1);
let all_items = query_xpath(&vdom, "//ul/li");
assert_eq!(all_items.len(), 3);
}
#[test]
fn query_text_predicate_with_newlines() {
let doc = Parser::default().parse("- Foo\nwrapped content\n");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//ul/li[1]/p[text() = 'Foo\nwrapped content']");
assert_eq!(
result.len(),
1,
"Should find paragraph with newline in text"
);
let para = query_xpath(&vdom, "//ul/li[1]/p");
assert_eq!(para.len(), 1);
assert_eq!(para[0].text.as_deref(), Some("Foo\nwrapped content"));
}
#[test]
fn query_text_predicate_with_escaped_quotes() {
let doc = Parser::default().parse("Para with 'quotes'\n");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//p[text() = 'Para with \\'quotes\\'']");
assert_eq!(result.len(), 1);
}
#[test]
fn query_text_predicate_exact_match_from_test_suite() {
let doc = Parser::default().parse("List\n====\n\n- Foo\nwrapped content\n- Boo\n- Blech\n");
let vdom = doc.to_virtual_dom();
assert_eq!(query_xpath(&vdom, "//ul").len(), 1);
assert_eq!(query_xpath(&vdom, "//ul/li[1]/*").len(), 1);
let result = query_xpath(&vdom, "//ul/li[1]/p[text() = 'Foo\nwrapped content']");
assert_eq!(result.len(), 1, "Should match text with newline");
}
#[test]
fn query_text_predicate_with_special_chars() {
let doc = Parser::default().parse("== List\n\n- Foo\n.wrapped content\n- Boo\n- Blech\n");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//ul/li[1]/p[text() = 'Foo\n.wrapped content']");
assert_eq!(result.len(), 1);
let doc2 = Parser::default().parse("== List\n\n- Foo\n:foo: bar\n- Boo\n- Blech\n");
let vdom2 = doc2.to_virtual_dom();
let result2 = query_xpath(&vdom2, "//ul/li[1]/p[text() = 'Foo\n:foo: bar']");
assert_eq!(result2.len(), 1);
}
#[test]
fn query_preceding_sibling_axis() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("First"));
root = root.with_child(VirtualNode::new("p").with_text("Second"));
root = root.with_child(VirtualNode::new("p").with_text("Third"));
let result = query_xpath(&root, "//p[3]/preceding-sibling::p");
assert_eq!(result.len(), 2, "Should find 2 preceding siblings");
assert_eq!(result[0].text.as_deref(), Some("First"));
assert_eq!(result[1].text.as_deref(), Some("Second"));
}
#[test]
fn query_preceding_sibling_with_wildcard() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("Para"));
root = root.with_child(
VirtualNode::new("div")
.with_class("title")
.with_text("Title"),
);
root = root.with_child(VirtualNode::new("ul"));
let result = query_xpath(&root, "//ul/preceding-sibling::*");
assert_eq!(result.len(), 2, "Should find 2 preceding siblings");
assert_eq!(result[0].tag, "p");
assert_eq!(result[1].tag, "div");
}
#[test]
fn query_preceding_sibling_with_predicate() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("div").with_class("ulist"));
root = root.with_child(
VirtualNode::new("div")
.with_class("title")
.with_text("Also"),
);
root = root.with_child(VirtualNode::new("div").with_class("ulist"));
let result = query_xpath(
&root,
"(//div[@class=\"ulist\"])[2]/preceding-sibling::*[@class=\"title\"][text()=\"Also\"]",
);
assert_eq!(result.len(), 1, "Should find the title element");
assert_eq!(result[0].text.as_deref(), Some("Also"));
}
#[test]
fn query_preceding_sibling_no_matches() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("Para"));
root = root.with_child(VirtualNode::new("ul"));
let result = query_xpath(&root, "//p/preceding-sibling::*");
assert_eq!(
result.len(),
0,
"First element should have no preceding siblings"
);
}
#[test]
fn query_following_sibling_axis() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("First"));
root = root.with_child(VirtualNode::new("p").with_text("Second"));
root = root.with_child(VirtualNode::new("p").with_text("Third"));
let result = query_xpath(&root, "//p[1]/following-sibling::p");
assert_eq!(result.len(), 2, "Should find 2 following siblings");
assert_eq!(result[0].text.as_deref(), Some("Second"));
assert_eq!(result[1].text.as_deref(), Some("Third"));
}
#[test]
fn query_following_sibling_with_wildcard() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("Para"));
root = root.with_child(
VirtualNode::new("div")
.with_class("title")
.with_text("Title"),
);
root = root.with_child(VirtualNode::new("ul"));
let result = query_xpath(&root, "//p/following-sibling::*");
assert_eq!(result.len(), 2, "Should find 2 following siblings");
assert_eq!(result[0].tag, "div");
assert_eq!(result[1].tag, "ul");
}
#[test]
fn query_following_sibling_with_predicate() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("Para"));
root = root.with_child(
VirtualNode::new("div")
.with_class("literalblock")
.with_text("Literal"),
);
root = root.with_child(VirtualNode::new("p").with_text("Another"));
let result = query_xpath(
&root,
"//p[1]/following-sibling::*[@class=\"literalblock\"]",
);
assert_eq!(result.len(), 1, "Should find the literalblock element");
assert_eq!(result[0].text.as_deref(), Some("Literal"));
}
#[test]
fn query_following_sibling_no_matches() {
let mut root = VirtualNode::new("div").with_class("document");
root = root.with_child(VirtualNode::new("p").with_text("Para"));
root = root.with_child(VirtualNode::new("ul"));
let result = query_xpath(&root, "//ul/following-sibling::*");
assert_eq!(
result.len(),
0,
"Last element should have no following siblings"
);
}
#[test]
fn query_with_arbitrary_attribute() {
let doc = Parser::default().parse("* Foo\n[start=2]\n. Boo\n* Blech\n");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//ol[@start=\"2\"]");
assert_eq!(result.len(), 1, "Should find one ol with start=2");
assert_eq!(result[0].attributes.get("start"), Some(&"2".to_string()));
}
#[test]
fn query_deeply_nested_descendants() {
let doc = Parser::default().parse(
"== Lists\n\n* Item one, paragraph one\n+\nItem one, paragraph two\n+\n* Item two\n",
);
let vdom = doc.to_virtual_dom();
let all_p = query_xpath(&vdom, "//p");
assert_eq!(
all_p.len(),
3,
"Should find 3 total p tags (2 in first li, 1 in second li)"
);
let direct_p = query_xpath(&vdom, "//ul/li[1]/p");
assert_eq!(direct_p.len(), 1, "Should find 1 direct p child");
let all_descendants_p = query_xpath(&vdom, "//ul/li[1]//p");
assert_eq!(
all_descendants_p.len(),
2,
"Should find 2 descendant p tags (direct + nested in div)"
);
}
#[test]
fn query_with_starts_with_predicate() {
let doc = Parser::default().parse("Hello World\n\nGoodbye Moon\n");
let vdom = doc.to_virtual_dom();
let result = query_xpath(&vdom, "//p[starts-with(text(),\"Hello\")]");
assert_eq!(
result.len(),
1,
"Should find one paragraph starting with Hello"
);
assert_eq!(result[0].text.as_deref(), Some("Hello World"));
let result = query_xpath(&vdom, "//p[starts-with(text(),\"Good\")]");
assert_eq!(
result.len(),
1,
"Should find one paragraph starting with Good"
);
assert_eq!(result[0].text.as_deref(), Some("Goodbye Moon"));
let result = query_xpath(&vdom, "//p[starts-with(text(),\"Foo\")]");
assert_eq!(
result.len(),
0,
"Should find no paragraphs starting with Foo"
);
}
#[test]
fn test_extract_predicates() {
use super::extract_predicates;
let (pred, path) = extract_predicates("[@id=\"beck\"]/div");
assert_eq!(pred, "[@id=\"beck\"]");
assert_eq!(path, "/div");
let (pred, path) = extract_predicates("[@id=\"beck\"][@class=\"title\"]/div/span");
assert_eq!(pred, "[@id=\"beck\"][@class=\"title\"]");
assert_eq!(path, "/div/span");
let (pred, path) = extract_predicates("[@id=\"test\"]");
assert_eq!(pred, "[@id=\"test\"]");
assert_eq!(path, "");
let (pred, path) = extract_predicates("/div/span");
assert_eq!(pred, "");
assert_eq!(path, "/div/span");
}
#[test]
fn query_parenthesized_with_predicates_and_child_path() {
let doc = Parser::default().parse("== Lists\n\n* Item one, paragraph one\n+\n:foo: bar\n[[beck]]\n.Read the following aloud to yourself\n[source, ruby]\n----\n5.times { print \"Odelay!\" }\n----\n\n* Item two\n");
let vdom = doc.to_virtual_dom();
let listing = query_xpath(&vdom, "//*[@id=\"beck\"]");
assert_eq!(listing.len(), 1, "Should find the beck listingblock");
let title = query_xpath(&vdom, "//*[@id=\"beck\"]/div[@class=\"title\"]");
assert_eq!(title.len(), 1, "Should find the title div");
let result = query_xpath(
&vdom,
"(//ul/li[1]/p/following-sibling::*)[1][@id=\"beck\"]/div[@class=\"title\"]",
);
assert_eq!(
result.len(),
1,
"Should find the title div via complex query"
);
assert!(result[0].text.as_deref().unwrap().starts_with("Read"));
}
}