use sup_xml_core::xpath::eval::{
DateKind, Value, date_value_to_utc_micros, value_to_number, value_to_string,
};
use sup_xml_core::xpath::{DocIndexLike, NodeId};
use crate::ast::Sort;
use crate::error::XsltError;
type Result<T> = std::result::Result<T, XsltError>;
#[derive(Clone, Debug)]
struct Key {
value: KeyValue,
descending: bool,
upper_first: Option<bool>, codepoint: bool,
ascii_ci: bool,
}
#[derive(Clone, Debug)]
enum KeyValue {
Text(String),
Number(f64),
Auto { text: String, num: Option<f64>, typed_key: Option<i128> },
}
pub fn sort_nodes<I, F>(
nodes: &[NodeId],
sorts: &[Sort],
idx: &I,
mut eval: F,
) -> Result<Vec<NodeId>>
where
I: DocIndexLike,
F: FnMut(&sup_xml_core::xpath::Expr, NodeId, usize, usize) -> Result<Value>,
{
if sorts.is_empty() {
return Ok(nodes.to_vec());
}
let size = nodes.len();
let mut indexed: Vec<(usize, NodeId, Vec<Key>)> = Vec::with_capacity(size);
for (i, &n) in nodes.iter().enumerate() {
let mut keys = Vec::with_capacity(sorts.len());
for s in sorts {
keys.push(make_key(s, n, i + 1, size, idx, &mut eval)?);
}
indexed.push((i, n, keys));
}
indexed.sort_by(|a, b| compare_key_lists(&a.2, &b.2).then(a.0.cmp(&b.0)));
Ok(indexed.into_iter().map(|(_, n, _)| n).collect())
}
pub fn sort_order_keyed<I, F>(
nodes: &[NodeId],
per_node_sorts: &[&[Sort]],
idx: &I,
mut eval: F,
) -> Result<Vec<usize>>
where
I: DocIndexLike,
F: FnMut(&sup_xml_core::xpath::Expr, NodeId, usize, usize) -> Result<Value>,
{
let size = nodes.len();
let mut indexed: Vec<(usize, Vec<Key>)> = Vec::with_capacity(size);
for (i, &n) in nodes.iter().enumerate() {
let mut keys = Vec::with_capacity(per_node_sorts[i].len());
for s in per_node_sorts[i] {
keys.push(make_key(s, n, i + 1, size, idx, &mut eval)?);
}
indexed.push((i, keys));
}
indexed.sort_by(|a, b| compare_key_lists(&a.1, &b.1).then(a.0.cmp(&b.0)));
Ok(indexed.into_iter().map(|(i, _)| i).collect())
}
pub fn sort_items<I, F>(
items: Vec<Value>,
sorts: &[Sort],
idx: &I,
mut eval: F,
) -> Result<Vec<Value>>
where
I: DocIndexLike,
F: FnMut(&sup_xml_core::xpath::Expr, &Value, usize, usize) -> Result<Value>,
{
if sorts.is_empty() { return Ok(items); }
let size = items.len();
let mut indexed: Vec<(usize, Value, Vec<Key>)> = Vec::with_capacity(size);
for (i, it) in items.into_iter().enumerate() {
let mut keys = Vec::with_capacity(sorts.len());
for s in sorts {
keys.push(make_key_for_item(s, &it, i + 1, size, idx, &mut eval)?);
}
indexed.push((i, it, keys));
}
indexed.sort_by(|a, b| compare_key_lists(&a.2, &b.2).then(a.0.cmp(&b.0)));
Ok(indexed.into_iter().map(|(_, v, _)| v).collect())
}
fn make_key_for_item<I, F>(
s: &Sort, item: &Value, pos: usize, size: usize, idx: &I, eval: &mut F,
) -> Result<Key>
where
I: DocIndexLike,
F: FnMut(&sup_xml_core::xpath::Expr, &Value, usize, usize) -> Result<Value>,
{
let resolve_avt = |a: Option<&crate::ast::Avt>, eval: &mut F| -> Result<Option<String>> {
let Some(avt) = a else { return Ok(None); };
if let Some(s) = resolve_literal_avt(Some(avt)) { return Ok(Some(s)); }
let mut s = String::new();
for p in &avt.parts {
match p {
crate::ast::AvtPart::Literal(lit) => s.push_str(lit),
crate::ast::AvtPart::Expr(e) => {
let v = eval(e, item, pos, size)?;
s.push_str(&value_to_string(&v, idx));
}
}
}
Ok(Some(s))
};
let order_str = resolve_avt(s.order.as_ref(), eval)?.unwrap_or_else(|| "ascending".into());
let case_str = resolve_avt(s.case_order.as_ref(), eval)?;
let dtype_str = resolve_avt(s.data_type.as_ref(), eval)?;
let coll_str = resolve_avt(s.collation.as_ref(), eval)?;
let descending = order_str == "descending";
let upper_first = match case_str.as_deref() {
Some("upper-first") => Some(true),
Some("lower-first") => Some(false),
_ => None,
};
if let Some(c) = coll_str.as_deref() {
if !crate::compiler::is_recognised_collation(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort collation '{c}' is not recognised by the processor (XTDE1035)"
)));
}
}
let codepoint = matches!(coll_str.as_deref(),
Some("http://www.w3.org/2005/xpath-functions/collation/codepoint"));
let ascii_ci = matches!(coll_str.as_deref(),
Some("http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive"));
let v = match &s.select {
Some(e) => eval(e, item, pos, size)?,
None => item.clone(),
};
let value = match dtype_str.as_deref() {
Some("number") => KeyValue::Number(value_to_number(&v, idx)),
Some(_) => KeyValue::Text(value_to_string(&v, idx)),
None if value_is_numeric_atomic(&v) =>
KeyValue::Number(value_to_number(&v, idx)),
None => auto_key(&v, idx),
};
Ok(Key { value, descending, upper_first, codepoint, ascii_ci })
}
fn make_key<I, F>(
s: &Sort, node: NodeId, pos: usize, size: usize, idx: &I, eval: &mut F,
) -> Result<Key>
where
I: DocIndexLike,
F: FnMut(&sup_xml_core::xpath::Expr, NodeId, usize, usize) -> Result<Value>,
{
let resolve_avt = |a: Option<&crate::ast::Avt>, eval: &mut F| -> Result<Option<String>> {
let Some(avt) = a else { return Ok(None); };
if let Some(s) = resolve_literal_avt(Some(avt)) { return Ok(Some(s)); }
let mut s = String::new();
for p in &avt.parts {
match p {
crate::ast::AvtPart::Literal(lit) => s.push_str(lit),
crate::ast::AvtPart::Expr(e) => {
let v = eval(e, node, pos, size)?;
s.push_str(&value_to_string(&v, idx));
}
}
}
Ok(Some(s))
};
if let Some(lang) = resolve_avt(s.lang.as_ref(), eval)? {
if !crate::compiler::is_valid_xml_lang(&lang) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort lang='{lang}' is not a valid xml:lang value (XTDE0030)"
)));
}
}
let order_str = resolve_avt(s.order.as_ref(), eval)?.unwrap_or_else(|| "ascending".into());
let case_str = resolve_avt(s.case_order.as_ref(), eval)?;
let dtype_str = resolve_avt(s.data_type.as_ref(), eval)?;
let descending = order_str == "descending";
let upper_first = match case_str.as_deref() {
Some("upper-first") => Some(true),
Some("lower-first") => Some(false),
_ => None,
};
let coll_str = resolve_avt(s.collation.as_ref(), eval)?;
if let Some(c) = coll_str.as_deref() {
if !crate::compiler::is_recognised_collation(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort collation '{c}' is not recognised by the processor (XTDE1035)"
)));
}
}
let codepoint = matches!(coll_str.as_deref(),
Some("http://www.w3.org/2005/xpath-functions/collation/codepoint"));
let ascii_ci = matches!(coll_str.as_deref(),
Some("http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive"));
let v = match &s.select {
Some(e) => eval(e, node, pos, size)?,
None => Value::String(idx.string_value(node)),
};
let multi_item = match &v {
Value::Sequence(items) => items.len() > 1,
Value::NodeSet(ns) => ns.len() > 1,
_ => false,
};
if multi_item && !sup_xml_core::xpath::eval::in_xpath_1_0_compat() {
return Err(XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(
"xsl:sort key value is a sequence of more than one item (XTTE1020)"
).with_xpath_code("XTTE1020")));
}
let value = match dtype_str.as_deref() {
Some("number") => KeyValue::Number(value_to_number(&v, idx)),
Some(_) => KeyValue::Text(value_to_string(&v, idx)),
None if value_is_numeric_atomic(&v) =>
KeyValue::Number(value_to_number(&v, idx)),
None => auto_key(&v, idx),
};
Ok(Key { value, descending, upper_first, codepoint, ascii_ci })
}
fn value_is_numeric_atomic(v: &Value) -> bool {
match v {
Value::Number(_) => true,
Value::Typed(t) => t.numeric.is_some(),
Value::Sequence(items) => items.first().map_or(false, value_is_numeric_atomic),
_ => false,
}
}
fn auto_key<I: DocIndexLike>(v: &Value, idx: &I) -> KeyValue {
let text = value_to_string(v, idx);
let typed_key = typed_sort_key(v);
let num = if typed_key.is_some() { None } else { parse_xs_double(&text) };
KeyValue::Auto { text, num, typed_key }
}
fn typed_sort_key(v: &Value) -> Option<i128> {
match v {
Value::Typed(t) => match t.kind {
"date" | "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay" =>
date_value_to_utc_micros(&t.lexical, DateKind::Date),
"dateTime" => date_value_to_utc_micros(&t.lexical, DateKind::DateTime),
"time" => date_value_to_utc_micros(&t.lexical, DateKind::Time),
"dayTimeDuration" => parse_day_time_secs(&t.lexical).map(|s| s as i128),
"yearMonthDuration" => parse_year_month_months(&t.lexical).map(|m| m as i128),
_ => None,
},
Value::Sequence(items) => items.first().and_then(typed_sort_key),
_ => None,
}
}
fn parse_day_time_secs(s: &str) -> Option<i64> {
let s = s.trim();
let (sign, body) = match s.strip_prefix('-') {
Some(rest) => (-1i64, rest),
None => (1i64, s),
};
let body = body.strip_prefix('P')?;
let (day_part, time_part) = match body.find('T') {
Some(i) => (&body[..i], &body[i + 1..]),
None => (body, ""),
};
let pull = |part: &str, marker: char| -> i64 {
let Some(i) = part.find(marker) else { return 0; };
let start = part[..i].rfind(|c: char| !c.is_ascii_digit() && c != '.')
.map(|n| n + 1).unwrap_or(0);
part[start..i].parse().unwrap_or(0)
};
let days = pull(day_part, 'D');
let hours = pull(time_part, 'H');
let mins = pull(time_part, 'M');
let secs = pull(time_part, 'S');
Some(sign * (days * 86_400 + hours * 3600 + mins * 60 + secs))
}
fn parse_year_month_months(s: &str) -> Option<i64> {
let s = s.trim();
let (sign, body) = match s.strip_prefix('-') {
Some(rest) => (-1i64, rest),
None => (1i64, s),
};
let body = body.strip_prefix('P')?;
let pull = |marker: char| -> i64 {
let Some(i) = body.find(marker) else { return 0; };
let start = body[..i].rfind(|c: char| !c.is_ascii_digit())
.map(|n| n + 1).unwrap_or(0);
body[start..i].parse().unwrap_or(0)
};
let years = pull('Y');
let months = pull('M');
Some(sign * (years * 12 + months))
}
fn parse_xs_double(s: &str) -> Option<f64> {
let t = s.trim();
if t.is_empty() { return None; }
match t {
"INF" => Some(f64::INFINITY),
"-INF" => Some(f64::NEG_INFINITY),
"+INF" => Some(f64::INFINITY),
"NaN" => Some(f64::NAN),
_ => t.parse::<f64>().ok(),
}
}
fn resolve_literal_avt(a: Option<&crate::ast::Avt>) -> Option<String> {
let avt = a?;
if !avt.is_literal() { return None; }
let mut s = String::new();
for p in &avt.parts {
if let crate::ast::AvtPart::Literal(lit) = p { s.push_str(lit); }
}
Some(s)
}
fn compare_key_lists(a: &[Key], b: &[Key]) -> std::cmp::Ordering {
use std::cmp::Ordering;
for (ka, kb) in a.iter().zip(b.iter()) {
let mut ord = compare_one_key(ka, kb);
if ka.descending {
ord = ord.reverse();
}
if ord != Ordering::Equal { return ord; }
}
Ordering::Equal
}
fn compare_one_key(a: &Key, b: &Key) -> std::cmp::Ordering {
use std::cmp::Ordering;
fn cmp_num(x: f64, y: f64) -> Ordering {
match (x.is_nan(), y.is_nan()) {
(true, true) => Ordering::Equal,
(true, false) => Ordering::Less,
(false, true) => Ordering::Greater,
_ => x.partial_cmp(&y).unwrap_or(Ordering::Equal),
}
}
match (&a.value, &b.value) {
(KeyValue::Number(x), KeyValue::Number(y)) => cmp_num(*x, *y),
(KeyValue::Text(x), KeyValue::Text(y)) => text_compare_opts(x, y, a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Number(n), KeyValue::Text(s)) =>
text_compare_opts(&format_num(*n), s, a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Text(s), KeyValue::Number(n)) =>
text_compare_opts(s, &format_num(*n), a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Auto { text: ta, num: na, typed_key: ka },
KeyValue::Auto { text: tb, num: nb, typed_key: kb }) => {
if a.codepoint || a.ascii_ci {
return text_compare_opts(ta, tb, a.upper_first, a.codepoint, a.ascii_ci);
}
match (ka, kb) {
(Some(x), Some(y)) => x.cmp(y),
_ => match (na, nb) {
(Some(x), Some(y)) => cmp_num(*x, *y),
_ => text_compare_opts(ta, tb, a.upper_first, a.codepoint, a.ascii_ci),
},
}
}
(KeyValue::Auto { num: Some(x), .. }, KeyValue::Number(y)) => cmp_num(*x, *y),
(KeyValue::Number(x), KeyValue::Auto { num: Some(y), .. }) => cmp_num(*x, *y),
(KeyValue::Auto { text, .. }, KeyValue::Number(n)) =>
text_compare_opts(text, &format_num(*n), a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Number(n), KeyValue::Auto { text, .. }) =>
text_compare_opts(&format_num(*n), text, a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Auto { text: ta, .. }, KeyValue::Text(tb)) =>
text_compare_opts(ta, tb, a.upper_first, a.codepoint, a.ascii_ci),
(KeyValue::Text(ta), KeyValue::Auto { text: tb, .. }) =>
text_compare_opts(ta, tb, a.upper_first, a.codepoint, a.ascii_ci),
}
}
#[cfg(test)]
fn text_compare(
a: &str, b: &str,
upper_first: Option<bool>, codepoint: bool,
) -> std::cmp::Ordering {
text_compare_opts(a, b, upper_first, codepoint, false)
}
fn text_compare_opts(
a: &str, b: &str,
upper_first: Option<bool>, codepoint: bool, ascii_ci: bool,
) -> std::cmp::Ordering {
use std::cmp::Ordering;
if ascii_ci {
let fold = |c: char| if c.is_ascii_uppercase() { c.to_ascii_lowercase() } else { c };
let ai = a.chars().map(fold);
let bi = b.chars().map(fold);
return ai.cmp(bi);
}
if codepoint {
return a.chars().cmp(b.chars());
}
let ai = a.chars().flat_map(|c| c.to_lowercase());
let bi = b.chars().flat_map(|c| c.to_lowercase());
let primary = ai.cmp(bi);
if primary != Ordering::Equal {
return primary;
}
let mut ai = a.chars();
let mut bi = b.chars();
loop {
match (ai.next(), bi.next()) {
(None, None) => return Ordering::Equal,
(None, Some(_)) => return Ordering::Less,
(Some(_), None) => return Ordering::Greater,
(Some(ca), Some(cb)) => {
if ca == cb { continue; }
let ca_is_upper = ca.is_uppercase();
let cb_is_upper = cb.is_uppercase();
match (upper_first, ca_is_upper, cb_is_upper) {
(Some(true), true, false) => return Ordering::Less,
(Some(true), false, true) => return Ordering::Greater,
(Some(false), true, false) => return Ordering::Greater,
(Some(false), false, true) => return Ordering::Less,
_ => return ca.cmp(&cb),
}
}
}
}
}
fn format_num(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 1e15 { format!("{}", n as i64) }
else { format!("{n}") }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{Avt, AvtPart, Sort};
use sup_xml_core::{parse_str, ParseOptions, XPathContext};
use sup_xml_core::xpath::eval::NoBindings;
use sup_xml_core::xpath::eval::eval_expr;
use sup_xml_core::xpath::eval::EvalCtx;
fn lit_avt(s: &str) -> Avt {
Avt { parts: vec![AvtPart::Literal(s.into())] }
}
fn doc_nodes(xml: &str, query: &str) -> (sup_xml_tree::dom::Document, XPathContext<'static>, Vec<NodeId>) {
let doc = parse_str(xml, &ParseOptions::default()).unwrap();
let leaked: &'static sup_xml_tree::dom::Document = Box::leak(Box::new(doc));
let ctx: XPathContext<'static> = XPathContext::new(leaked);
let nodes = match ctx.eval(query).unwrap() {
Value::NodeSet(ns) => ns,
_ => panic!(),
};
let fake = parse_str("<x/>", &ParseOptions::default()).unwrap();
(fake, ctx, nodes)
}
#[test]
fn sort_text_ascending_default() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>banana</i><i>apple</i><i>cherry</i></r>",
"/r/i",
);
let s = Sort {
select: None, lang: None, data_type: None,
order: None, case_order: None, collation: None,
};
let result = sort_nodes(&nodes, std::slice::from_ref(&s), &ctx.index, |e, n, _, _| {
let v = eval_expr(e, &EvalCtx { context_node: n, pos: 1, size: 1, bindings: &NoBindings, static_ctx: &sup_xml_core::xpath::eval::DEFAULT_STATIC_CTX }, &ctx.index)
.map_err(XsltError::from)?;
Ok(v)
}).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["apple", "banana", "cherry"]);
}
#[test]
fn sort_descending_reverses() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>a</i><i>c</i><i>b</i></r>", "/r/i",
);
let s = Sort {
select: None, lang: None, data_type: None,
order: Some(lit_avt("descending")), case_order: None, collation: None,
};
let result = sort_nodes(&nodes, std::slice::from_ref(&s), &ctx.index, |e, n, _, _| {
let v = eval_expr(e, &EvalCtx { context_node: n, pos: 1, size: 1, bindings: &NoBindings, static_ctx: &sup_xml_core::xpath::eval::DEFAULT_STATIC_CTX }, &ctx.index)
.map_err(XsltError::from)?;
Ok(v)
}).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["c", "b", "a"]);
}
#[test]
fn sort_number_uses_numeric_comparison() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>10</i><i>2</i><i>1</i></r>", "/r/i",
);
let s = Sort {
select: None, lang: None,
data_type: Some(lit_avt("number")),
order: None, case_order: None, collation: None,
};
let result = sort_nodes(&nodes, std::slice::from_ref(&s), &ctx.index, |e, n, _, _| {
let v = eval_expr(e, &EvalCtx { context_node: n, pos: 1, size: 1, bindings: &NoBindings, static_ctx: &sup_xml_core::xpath::eval::DEFAULT_STATIC_CTX }, &ctx.index)
.map_err(XsltError::from)?;
Ok(v)
}).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["1", "2", "10"]);
}
#[test]
fn no_sort_keys_returns_input_unchanged() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>c</i><i>a</i><i>b</i></r>", "/r/i",
);
let result = sort_nodes(&nodes, &[], &ctx.index, |_, _, _, _| panic!()).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["c", "a", "b"]);
}
#[test]
fn format_num_integers_stringify_without_decimals() {
assert_eq!(format_num(0.0), "0");
assert_eq!(format_num(42.0), "42");
assert_eq!(format_num(-7.0), "-7");
}
#[test]
fn format_num_fractions_use_default_format() {
assert_eq!(format_num(3.5), "3.5");
assert_eq!(format_num(-1.25), "-1.25");
}
#[test]
fn format_num_large_values_fall_back_to_float_format() {
let s = format_num(1e20);
assert!(!s.contains("9223") && !s.contains("-9223"), "got {s}");
}
#[test]
fn text_compare_prefix_vs_full() {
use std::cmp::Ordering;
assert_eq!(text_compare("ab", "abc", None, false), Ordering::Less);
assert_eq!(text_compare("abc", "ab", None, false), Ordering::Greater);
assert_eq!(text_compare("", "", None, false), Ordering::Equal);
assert_eq!(text_compare("xyz", "xyz", None, false), Ordering::Equal);
}
#[test]
fn text_compare_case_order_upper_first() {
use std::cmp::Ordering;
assert_eq!(text_compare("A", "a", Some(true), false), Ordering::Less);
assert_eq!(text_compare("a", "A", Some(true), false), Ordering::Greater);
assert_eq!(text_compare("A", "a", Some(false), false), Ordering::Greater);
assert_eq!(text_compare("a", "A", Some(false), false), Ordering::Less);
assert_eq!(text_compare("A", "a", None, false), Ordering::Less);
}
fn key_num(n: f64) -> Key {
Key { value: KeyValue::Number(n), descending: false, upper_first: None, codepoint: false, ascii_ci: false }
}
fn key_text(s: &str) -> Key {
Key { value: KeyValue::Text(s.into()), descending: false, upper_first: None, codepoint: false, ascii_ci: false }
}
#[test]
fn number_compare_nan_handling() {
use std::cmp::Ordering;
assert_eq!(compare_one_key(&key_num(f64::NAN), &key_num(f64::NAN)), Ordering::Equal);
assert_eq!(compare_one_key(&key_num(f64::NAN), &key_num(1.0)), Ordering::Less);
assert_eq!(compare_one_key(&key_num(1.0), &key_num(f64::NAN)), Ordering::Greater);
assert_eq!(compare_one_key(&key_num(2.0), &key_num(3.0)), Ordering::Less);
}
#[test]
fn mixed_number_text_comparison_stringifies_number() {
use std::cmp::Ordering;
assert_eq!(compare_one_key(&key_num(10.0), &key_text("10")), Ordering::Equal);
assert_eq!(compare_one_key(&key_text("10"), &key_num(10.0)), Ordering::Equal);
assert_eq!(compare_one_key(&key_num(5.0), &key_text("abc")), Ordering::Less);
assert_eq!(compare_one_key(&key_text("abc"), &key_num(5.0)), Ordering::Greater);
}
#[test]
fn all_keys_equal_returns_equal() {
use std::cmp::Ordering;
let a = vec![key_text("x"), key_num(1.0)];
let b = vec![key_text("x"), key_num(1.0)];
assert_eq!(compare_key_lists(&a, &b), Ordering::Equal);
}
#[test]
fn sort_case_order_upper_first() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>apple</i><i>Apple</i><i>BANANA</i></r>", "/r/i",
);
let s = Sort {
select: None, lang: None, data_type: None,
order: None, case_order: Some(lit_avt("upper-first")), collation: None,
};
let result = sort_nodes(&nodes, std::slice::from_ref(&s), &ctx.index, |e, n, _, _| {
let v = eval_expr(e, &EvalCtx { context_node: n, pos: 1, size: 1, bindings: &NoBindings, static_ctx: &sup_xml_core::xpath::eval::DEFAULT_STATIC_CTX }, &ctx.index)
.map_err(XsltError::from)?;
Ok(v)
}).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["Apple", "apple", "BANANA"]);
}
#[test]
fn sort_case_order_lower_first() {
let (_doc, ctx, nodes) = doc_nodes(
"<r><i>apple</i><i>Apple</i></r>", "/r/i",
);
let s = Sort {
select: None, lang: None, data_type: None,
order: None, case_order: Some(lit_avt("lower-first")), collation: None,
};
let result = sort_nodes(&nodes, std::slice::from_ref(&s), &ctx.index, |e, n, _, _| {
let v = eval_expr(e, &EvalCtx { context_node: n, pos: 1, size: 1, bindings: &NoBindings, static_ctx: &sup_xml_core::xpath::eval::DEFAULT_STATIC_CTX }, &ctx.index)
.map_err(XsltError::from)?;
Ok(v)
}).unwrap();
let strings: Vec<_> = result.iter().map(|&n| ctx.index.string_value(n)).collect();
assert_eq!(strings, vec!["apple", "Apple"]);
}
}