use std::collections::HashMap;
use sup_xml_core::xpath::eval::{
eval_expr, format_numeric_styled, value_equality_key, value_to_string_styled,
EvalCtx, Numeric, NumStyle, StaticContext, Value, XPathBindings,
};
use sup_xml_core::xpath::{DocIndex, DocIndexLike, NodeId, XPathNodeKind};
use sup_xml_core::xpath::context::INodeKind;
use sup_xml_tree::dom::Document;
use crate::ast::{
Avt, AvtPart, Instr, OnNoMatch, Param, QName, StylesheetAst, Template,
Variable, WithParam,
};
use crate::error::XsltError;
use crate::functions::{self, KeyIndex};
use crate::loader::{Loader, NullLoader};
use crate::pattern;
use crate::result_tree::{ResultBuilder, ResultNode, ResultTree};
type Result<T> = std::result::Result<T, XsltError>;
#[cfg(feature = "xsd")]
pub(crate) struct NodeType {
#[allow(dead_code)]
pub type_ref: sup_xml_core::xsd::TypeRef,
pub name: Option<(String, String)>,
}
#[cfg(feature = "xsd")]
#[derive(Default)]
pub(crate) struct SourceTypes {
by_node: HashMap<NodeId, NodeType>,
}
#[cfg(not(feature = "xsd"))]
#[derive(Default)]
pub(crate) struct SourceTypes;
#[cfg(feature = "xsd")]
fn build_source_types(
style: &StylesheetAst, source_doc: &Document, idx: &DocIndex,
) -> Option<SourceTypes> {
if style.schemas.is_empty()
|| style.input_type_annotations.iter().any(|v| v == "strip")
{
return None;
}
let mut by_node: HashMap<NodeId, NodeType> = HashMap::new();
for schema in &style.schemas {
let (_res, psvi) = schema.validate_doc_typed(source_doc);
if psvi.is_empty() { continue; }
for (id, inode) in idx.nodes.iter().enumerate() {
let INodeKind::Element(n) = &inode.kind else { continue };
let Some(ty) = psvi.governing_type(n) else { continue };
if let std::collections::hash_map::Entry::Vacant(e) = by_node.entry(id) {
e.insert(NodeType {
name: registered_type_name(ty, schema),
type_ref: ty.clone(),
});
}
if let sup_xml_core::xsd::TypeRef::Complex(ct) = ty {
for aid in inode.attr_start..inode.attr_end {
let alocal = idx.local_name(aid);
let auri = idx.namespace_uri(aid);
let Some(au) = ct.attributes.iter().find(|au|
au.decl.name.local.as_ref() == alocal
&& au.decl.name.namespace.as_deref().unwrap_or("") == auri)
else { continue };
let tref = resolve_unresolved_type(
sup_xml_core::xsd::TypeRef::Simple(au.decl.type_def.clone()), schema);
if let std::collections::hash_map::Entry::Vacant(e) = by_node.entry(aid) {
e.insert(NodeType {
name: registered_type_name(&tref, schema),
type_ref: tref,
});
}
}
}
}
}
(!by_node.is_empty()).then_some(SourceTypes { by_node })
}
#[cfg(feature = "xsd")]
fn resolve_unresolved_type(
tref: sup_xml_core::xsd::TypeRef, schema: &sup_xml_core::xsd::Schema,
) -> sup_xml_core::xsd::TypeRef {
use sup_xml_core::xsd::{QName as XQName, TypeRef};
let TypeRef::Simple(st) = &tref else { return tref };
let Some(rest) = st.name.as_deref().and_then(|n| n.strip_prefix("UNRESOLVED:"))
else { return tref };
let qn = match rest.strip_prefix('{').and_then(|r| r.split_once('}')) {
Some((ns, local)) => XQName::new((!ns.is_empty()).then_some(ns), local),
None => XQName::new(None, rest),
};
schema.type_def(&qn).cloned().unwrap_or(tref)
}
#[cfg(feature = "xsd")]
fn registered_type_name(
ty: &sup_xml_core::xsd::TypeRef, schema: &sup_xml_core::xsd::Schema,
) -> Option<(String, String)> {
use sup_xml_core::xsd::TypeRef;
schema.types().find_map(|(qn, registered)| {
let same = match (ty, registered) {
(TypeRef::Simple(a), TypeRef::Simple(b)) => std::sync::Arc::ptr_eq(a, b),
(TypeRef::Complex(a), TypeRef::Complex(b)) => std::sync::Arc::ptr_eq(a, b),
_ => false,
};
same.then(|| (
qn.namespace.as_deref().unwrap_or("").to_string(),
qn.local.to_string(),
))
})
}
#[cfg(not(feature = "xsd"))]
fn build_source_types(
_style: &StylesheetAst, _source_doc: &Document, _idx: &DocIndex,
) -> Option<SourceTypes> {
None
}
struct XsltBindings<'a, I: DocIndexLike> {
variables: &'a VariableScope,
namespaces: &'a NamespaceContext,
keys: Option<&'a KeyIndex>,
xslt_context_node: NodeId,
idx: &'a I,
style: &'a crate::ast::StylesheetAst,
documents: Option<&'a HashMap<String, NodeId>>,
decimal_formats: &'a HashMap<String, crate::format_number::DecimalFormat>,
unparsed_entities: &'a HashMap<String, sup_xml_tree::UnparsedEntity>,
user_exts: Option<&'a dyn crate::extensions::ExtensionFunctions>,
user_functions: Option<&'a [crate::ast::UserFunction]>,
current_group: Option<&'a [NodeId]>,
current_grouping_key: Option<&'a Value>,
accumulators: Option<&'a HashMap<String, AccumulatorData>>,
regex_groups: Option<&'a [String]>,
unparsed_texts: Option<&'a HashMap<String, String>>,
xslt_3_0: bool,
xslt_version: &'a str,
static_base_uri: Option<&'a str>,
loader: Option<&'a dyn crate::loader::Loader>,
loader_base: Option<&'a str>,
dyn_doc_cache: Option<&'a std::cell::RefCell<HashMap<String, NodeId>>>,
rtf_base_uris: &'a std::cell::RefCell<HashMap<NodeId, String>>,
#[cfg_attr(not(feature = "xsd"), allow(dead_code))]
source_types: Option<&'a SourceTypes>,
}
#[cfg(feature = "xsd")]
impl<'a, I: DocIndexLike> XsltBindings<'a, I> {
fn typed_value_for_named_type(&self, ns: &str, local: &str, lexical: &str)
-> Option<Value>
{
use sup_xml_core::xsd::{BuiltinType, SimpleType, QName as XQName, TypeRef};
const XSD_NS: &str = "http://www.w3.org/2001/XMLSchema";
if ns == XSD_NS {
let bt = BuiltinType::from_name(local)?;
let xv = SimpleType::of_builtin(bt).validate(lexical).ok()?;
return Some(match xsd_value_to_xpath(xv, lexical, None) {
Value::Typed(mut t) => { t.kind = bt.name(); Value::Typed(t) }
other => other,
});
}
let qn = XQName::new((!ns.is_empty()).then_some(ns), local);
for schema in &self.style.schemas {
if let Some(TypeRef::Simple(st)) = schema.type_def(&qn) {
let xv = st.validate(lexical).ok()?;
return Some(xsd_value_to_xpath(xv, lexical, Some((ns, local))));
}
}
None
}
}
fn xslt_version_3_or_more(version: &str) -> bool {
let v = version.trim();
let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
matches!(major, Some(n) if n >= 3)
}
fn grouping_key_items(v: &Value) -> Vec<Value> {
match v {
Value::NodeSet(ns) => ns.iter().map(|&id| Value::NodeSet(vec![id])).collect(),
Value::ForeignNodeSet(ns) => ns.iter().map(|&p| Value::ForeignNodeSet(vec![p])).collect(),
Value::Sequence(items) => items.clone(),
Value::IntRange { lo, hi } => (*lo..=*hi).map(|i| Value::Number(Numeric::Integer(i))).collect(),
other => vec![other.clone()],
}
}
fn static_ctx_for_version(version: &str) -> StaticContext {
let major = version.trim().split('.').next()
.and_then(|s| s.parse::<u32>().ok());
StaticContext {
xpath_2_0: matches!(major, Some(n) if n >= 2),
xpath_3_0: matches!(major, Some(n) if n >= 3),
libxml2_compatible: false,
current_node: None,
}
}
impl<'a, I: DocIndexLike> XsltBindings<'a, I> {
fn resolve_document_against_base(&self, mut args: Vec<Value>) -> Vec<Value> {
let base_node = match args.get(1) {
Some(Value::NodeSet(ns)) => ns.first().copied(),
_ => None,
};
let (Some(bn), Some(loader)) = (base_node, self.loader) else { return args; };
let mut root = bn;
while let Some(p) = self.idx.parent(root) { root = p; }
let Some(base) = self.node_base_uri(root).or_else(|| self.node_base_uri(bn))
else { return args; };
let resolve_one = |s: &str|
loader.resolve(s, Some(&base)).unwrap_or_else(|_| s.to_string());
args[0] = match &args[0] {
Value::String(s) => Value::String(resolve_one(s)),
Value::Typed(t) => Value::String(resolve_one(&t.lexical)),
Value::NodeSet(ns) if ns.len() == 1 =>
Value::String(resolve_one(&self.idx.string_value(ns[0]))),
other => other.clone(),
};
args
}
fn resolve_document_node_bases(&self, mut args: Vec<Value>) -> Vec<Value> {
let Some(loader) = self.loader else { return args; };
let resolve_node = |id: NodeId| -> String {
let raw = self.idx.string_value(id);
let mut root = id;
while let Some(p) = self.idx.parent(root) { root = p; }
match self.node_base_uri(root) {
Some(base) => loader.resolve(&raw, Some(&base)).unwrap_or(raw),
None => raw,
}
};
if let Value::NodeSet(ns) = &args[0] {
args[0] = Value::Sequence(
ns.iter().map(|&id| Value::String(resolve_node(id))).collect());
}
args
}
}
impl<'a, I: DocIndexLike> XPathBindings for XsltBindings<'a, I> {
fn static_base_uri(&self) -> Option<String> {
self.static_base_uri.map(|s| {
if s.starts_with('/') { format!("file://{s}") } else { s.to_string() }
})
}
fn node_base_uri(&self, id: NodeId) -> Option<String> {
self.rtf_base_uris.borrow().get(&id).cloned()
}
fn regex_dialect(&self) -> sup_xml_core::regex::Dialect {
if self.xslt_3_0 {
sup_xml_core::regex::Dialect::Xpath
} else {
sup_xml_core::regex::Dialect::Xpath20
}
}
fn load_dynamic_document(
&self, uri: &str,
) -> Option<std::result::Result<NodeId, sup_xml_core::error::XmlError>> {
let cache = self.dyn_doc_cache?;
if let Some(&id) = cache.borrow().get(uri) {
return Some(Ok(id));
}
let loader = self.loader?;
let doc = match loader.load_parsed(uri, self.loader_base) {
Ok(d) => d,
Err(_) => return None, };
let id = match self.idx.graft_dynamic_document(&doc) {
Some(id) => id,
None => return None,
};
cache.borrow_mut().insert(uri.to_string(), id);
if let Ok(abs) = loader.resolve(uri, self.loader_base) {
self.rtf_base_uris.borrow_mut().insert(id, abs);
}
Some(Ok(id))
}
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
if let Some(uri) = self.namespaces.resolve(prefix) {
return Some(uri);
}
if self.xslt_3_0 {
match prefix {
"fn" => return Some("http://www.w3.org/2005/xpath-functions".into()),
"math" => return Some("http://www.w3.org/2005/xpath-functions/math".into()),
"map" => return Some("http://www.w3.org/2005/xpath-functions/map".into()),
"array" => return Some("http://www.w3.org/2005/xpath-functions/array".into()),
"err" => return Some("http://www.w3.org/2005/xqt-errors".into()),
_ => {}
}
}
None
}
fn xpath_version_2_or_later(&self) -> bool {
let major = self.xslt_version.trim().split('.').next()
.and_then(|s| s.parse::<u32>().ok());
matches!(major, Some(n) if n >= 2)
}
fn variable(&self, name: &str) -> Option<Value> {
let key = if let Some((prefix, local)) = name.split_once(':') {
match self.resolve_prefix(prefix) {
Some(uri) => format!("{{{uri}}}{local}"),
None => name.to_string(),
}
} else {
name.to_string()
};
self.variables.get(&key)
.or_else(|| self.variables.get(name))
.cloned()
}
fn call_function(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
self.call_function_in(ns_uri, name, args, self.xslt_context_node)
}
fn function_available_in(&self, ns_uri: &str, name: &str, arity: usize) -> bool {
self.user_functions.unwrap_or(&[]).iter().any(|uf| {
uf.name.uri == ns_uri && uf.name.local == name && uf.params.len() == arity
})
}
fn function_signature_in(&self, ns_uri: &str, name: &str, arity: usize)
-> Option<sup_xml_core::xpath::FunctionSig> {
use sup_xml_core::xpath::{parse_sequence_type_str, FunctionSig, ItemType,
Occurrence, SequenceType};
let item_star = || SequenceType { item: ItemType::Any, occurrence: Occurrence::ZeroOrMore };
let uf = self.user_functions.unwrap_or(&[]).iter().find(|uf|
uf.name.uri == ns_uri && uf.name.local == name && uf.params.len() == arity)?;
let params = uf.params.iter().map(|p|
p.as_type.as_deref().and_then(parse_sequence_type_str).unwrap_or_else(item_star)
).collect();
let ret = uf.as_type.as_deref().and_then(parse_sequence_type_str).unwrap_or_else(item_star);
Some(FunctionSig { params, ret })
}
#[cfg(feature = "xsd")]
fn castable_as_user_type(
&self, ns_uri: &str, local: &str, value: &str, source_kind: Option<&str>,
) -> Option<bool> {
use sup_xml_core::xsd::{QName as XQName, TypeRef};
let qn = XQName::new((!ns_uri.is_empty()).then_some(ns_uri), local);
for schema in &self.style.schemas {
match schema.type_def(&qn) {
Some(TypeRef::Simple(st)) => return Some(simple_type_castable(st, value, source_kind)),
Some(TypeRef::Complex(_)) => return Some(false),
None => {}
}
}
None
}
#[cfg(feature = "xsd")]
fn instance_of_user_type(
&self, target_ns: &str, target_local: &str, value_type: Option<(&str, &str)>,
) -> Option<bool> {
use sup_xml_core::xsd::{types::Variety, QName as XQName, TypeRef};
if schema_suppressed() { return None; }
let (vns, vlocal) = value_type?;
let qn = XQName::new((!target_ns.is_empty()).then_some(target_ns), target_local);
for schema in &self.style.schemas {
let Some(TypeRef::Simple(target_st)) = schema.type_def(&qn) else { continue };
if vns == target_ns && vlocal == target_local { return Some(true); }
if let Variety::Union { members } = &target_st.variety {
let m = members.iter().any(|m| m.name.as_deref()
.map_or(false, |n| simple_type_name_is(n, vns, vlocal)));
if m { return Some(true); }
}
return None;
}
None
}
#[cfg(feature = "xsd")]
fn schema_type_exists(&self, ns: &str, local: &str) -> bool {
if schema_suppressed() { return false; }
use sup_xml_core::xsd::QName as XQName;
let qn = XQName::new((!ns.is_empty()).then_some(ns), local);
self.style.schemas.iter().any(|s| s.type_def(&qn).is_some())
}
#[cfg(feature = "xsd")]
fn node_schema_type(&self, node_id: NodeId) -> Option<(String, String)> {
self.source_types?.by_node.get(&node_id)?.name.clone()
}
#[cfg(feature = "xsd")]
fn node_typed_value(&self, node_id: NodeId, lexical: &str) -> Option<Value> {
use sup_xml_core::xsd::TypeRef;
if let Some((ns, local)) = self.idx.rtf_node_type(node_id) {
return self.typed_value_for_named_type(&ns, &local, lexical);
}
let nt = self.source_types?.by_node.get(&node_id)?;
let TypeRef::Simple(st) = &nt.type_ref else { return None };
let xv = st.validate(lexical).ok()?;
let user_type = nt.name.as_ref().map(|(n, l)| (n.as_str(), l.as_str()));
let v = xsd_value_to_xpath(xv, lexical, user_type);
if let Value::Typed(mut t) = v {
t.kind = st.name.as_deref()
.and_then(sup_xml_core::xpath::eval::atomic_kind_static)
.unwrap_or_else(|| st.builtin.name());
return Some(Value::Typed(t));
}
Some(v)
}
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
xpath_context_node: NodeId,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
if ns_uri.is_empty()
|| ns_uri == "http://www.w3.org/2005/xpath-functions"
{
let args = if name == "document" && args.len() == 2 {
self.resolve_document_against_base(args)
} else if name == "document" && args.len() == 1 {
self.resolve_document_node_bases(args)
} else { args };
let dyn_load = |uri: &str|
<Self as sup_xml_core::xpath::eval::XPathBindings>::load_dynamic_document(self, uri);
let dyn_load: Option<&dyn Fn(&str) -> Option<std::result::Result<NodeId, sup_xml_core::error::XmlError>>>
= if self.loader.is_some() { Some(&dyn_load) } else { None };
return functions::dispatch(
name, args, self.idx, self.xslt_context_node, xpath_context_node,
self.keys, &[], self.documents, dyn_load, self.decimal_formats,
self.namespaces, self.unparsed_entities,
self.current_group, self.current_grouping_key, self.regex_groups,
self.unparsed_texts,
self.user_functions.unwrap_or(&[]),
self.xslt_version,
self.accumulators,
);
}
if let Some(fs) = self.user_functions {
let uf = fs.iter().find(|f| f.name.uri == ns_uri
&& f.name.local == name
&& f.params.len() == args.len())
.or_else(|| fs.iter().find(|f| f.name.uri == ns_uri
&& f.name.local == name));
if let Some(uf) = uf {
if uf.visibility.as_deref() == Some("abstract") {
return Some(Err(sup_xml_core::xpath::eval::xpath_err(format!(
"cannot call abstract function {} — no implementation \
was supplied by the using package (XTDE3052)", name))));
}
return Some(call_user_function_pure_xpath(
uf, args, self.idx, xpath_context_node, self, self.style,
));
}
}
#[cfg(feature = "xsd")]
if args.len() == 1 && !ns_uri.is_empty() {
use sup_xml_core::xsd::{QName as XQName, TypeRef};
let qn = XQName::new(Some(ns_uri), name);
for schema in &self.style.schemas {
if let Some(TypeRef::Simple(st)) = schema.type_def(&qn) {
let s = sup_xml_core::xpath::eval::value_to_string(&args[0], self.idx);
return Some(match st.validate(&s) {
Ok(xv) => Ok(xsd_value_to_xpath(xv, &s, Some((ns_uri, name)))),
Err(_) => Err(sup_xml_core::xpath::eval::xpath_err(format!(
"cannot construct {{{ns_uri}}}{name} from '{s}' (FORG0001)"))
.with_xpath_code("FORG0001")),
});
}
}
}
self.user_exts.and_then(|exts| exts.call(ns_uri, name, args))
}
fn foreign_string_value(
&self,
p: sup_xml_core::xpath::eval::ForeignNodePtr,
) -> String {
sup_xml_tree::dom::Document::node_string_value_by_ptr(p)
}
}
#[cfg(feature = "xsd")]
fn simple_type_name_is(name: &str, ns: &str, local: &str) -> bool {
let name = name.strip_prefix("UNRESOLVED:").unwrap_or(name);
if let Some(rest) = name.strip_prefix('{') {
if let Some(end) = rest.find('}') {
return &rest[..end] == ns && &rest[end + 1..] == local;
}
}
name == local
}
#[cfg(feature = "xsd")]
fn simple_type_castable(
st: &std::sync::Arc<sup_xml_core::xsd::SimpleType>, value: &str, source_kind: Option<&str>,
) -> bool {
let Some(kind) = source_kind else { return st.validate(value).is_ok(); };
if matches!(kind, "string" | "untypedAtomic" | "anyAtomicType" | "normalizedString" | "token") {
return st.validate(value).is_ok();
}
let src = sup_xml_core::xsd::BuiltinType::from_name(kind)
.unwrap_or(sup_xml_core::xsd::BuiltinType::String);
castable_typed(st, value, src)
}
#[cfg(feature = "xsd")]
fn castable_typed(
st: &std::sync::Arc<sup_xml_core::xsd::SimpleType>, value: &str,
src: sup_xml_core::xsd::BuiltinType,
) -> bool {
use sup_xml_core::xsd::types::Variety;
match &st.variety {
Variety::Union { members } => members.iter().any(|m| castable_typed(m, value, src)),
Variety::List { .. } => false,
Variety::Atomic => cast_primitive_allowed(src, st.builtin) && st.validate(value).is_ok(),
}
}
#[cfg(feature = "xsd")]
fn cast_primitive_allowed(
src: sup_xml_core::xsd::BuiltinType, target: sup_xml_core::xsd::BuiltinType,
) -> bool {
use sup_xml_core::xsd::BuiltinType::*;
let s = src.primitive();
let t = target.primitive();
if matches!(s, String | AnySimpleType | AnyAtomicType) { return true; }
if matches!(t, String | AnySimpleType | AnyAtomicType) { return true; }
if s == t { return true; }
let numeric = |b| matches!(b, Decimal | Float | Double);
if (numeric(s) || s == Boolean) && (numeric(t) || t == Boolean) { return true; }
if matches!(s, Duration) && matches!(t, Duration) { return true; }
if matches!(s, HexBinary | Base64Binary) && matches!(t, HexBinary | Base64Binary) { return true; }
match s {
DateTime => matches!(t, DateTime | Date | Time | GYearMonth | GYear | GMonthDay | GMonth | GDay),
Date => matches!(t, Date | DateTime | GYearMonth | GYear | GMonthDay | GMonth | GDay),
_ => false,
}
}
#[cfg(feature = "xsd")]
fn xsd_value_to_xpath(v: sup_xml_core::xsd::Value, input: &str, user_type: Option<(&str, &str)>) -> Value {
use sup_xml_core::xsd::Value as X;
use sup_xml_core::xpath::eval::TypedAtomic;
use sup_xml_core::rust_decimal::prelude::ToPrimitive;
let typed = |kind: &'static str, lexical: String, numeric: Option<f64>, boolean: Option<bool>| {
let user_type = user_type.map(|(ns, l)| Box::new((ns.to_string(), l.to_string())));
Value::Typed(Box::new(TypedAtomic { kind, lexical, numeric, boolean, user_type }))
};
let lex = || input.split_whitespace().collect::<Vec<_>>().join(" ");
match v {
X::String(s) | X::Token(s) => typed("string", s, None, None),
X::Bool(b) => typed("boolean", if b { "true" } else { "false" }.to_string(), None, Some(b)),
X::Int(i) => typed("integer", i.to_string(), Some(i as f64), None),
X::BigInt(_) => typed("integer", lex(), None, None),
X::Decimal(d) => typed("decimal", d.to_string(), d.to_f64(), None),
X::Float(f) => typed("float", f.to_string(), Some(f as f64), None),
X::Double(d) => typed("double", d.to_string(), Some(d), None),
X::Bytes(_) => typed("string", lex(), None, None),
X::DateTime(_) => typed("dateTime", lex(), None, None),
X::Date(_) => typed("date", lex(), None, None),
X::Time(_) => typed("time", lex(), None, None),
X::Duration(_) => typed("duration", lex(), None, None),
X::GYearMonth(_) => typed("gYearMonth", lex(), None, None),
X::GYear(_) => typed("gYear", lex(), None, None),
X::GMonthDay(_) => typed("gMonthDay", lex(), None, None),
X::GDay(_) => typed("gDay", lex(), None, None),
X::GMonth(_) => typed("gMonth", lex(), None, None),
}
}
fn call_user_function_pure_xpath<I: DocIndexLike>(
uf: &crate::ast::UserFunction,
args: Vec<Value>,
idx: &I,
context_node: NodeId,
outer: &dyn XPathBindings,
style: &crate::ast::StylesheetAst,
) -> std::result::Result<Value, sup_xml_core::error::XmlError> {
use sup_xml_core::error::{ErrorDomain, ErrorLevel, XmlError};
let err = |m: &str| XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, m.to_string());
if args.len() != uf.params.len() {
return Err(err(&format!(
"xsl:function {}:{} expects {} argument(s), got {}",
uf.name.prefix.as_deref().unwrap_or(""),
uf.name.local, uf.params.len(), args.len(),
)));
}
let mut coerced: Vec<Value> = Vec::with_capacity(args.len());
for (p, v) in uf.params.iter().zip(args.into_iter()) {
let bound = if let Some(t) = &p.as_type {
match crate::eval::parse_as_atomic_type(t) {
Some(st) => crate::eval::coerce_to_atomic_sequence(v, &st, idx)
.map_err(|e| err(&format!(
"xsl:function: argument '{}' type mismatch: {e}",
p.name.local,
)))?,
None => v,
}
} else { v };
coerced.push(bound);
}
fn build_chain<'p>(
params: &'p [crate::ast::Param], args: Vec<Value>, parent: &'p dyn XPathBindings,
) -> Box<dyn XPathBindings + 'p> {
let mut cur: Box<dyn XPathBindings + 'p> = Box::new(PassthroughBindings { inner: parent });
for (p, v) in params.iter().zip(args.into_iter()) {
let local = p.name.local.clone();
cur = Box::new(NamedBinding { parent_owned: cur, name: local, value: v });
}
cur
}
let chain = build_chain(&uf.params, coerced, outer);
let static_ctx = static_ctx_for_version(&style.version);
let prev_ctx = CONTEXT_UNDEFINED.with(|c| c.replace(true));
let items = sup_xml_core::xpath::eval::with_focus_undefined(true, || {
eval_function_body(
&uf.body, idx, context_node, 1, 1, chain.as_ref(),
FnEnv { style, depth: 0 }, &static_ctx,
)
});
CONTEXT_UNDEFINED.with(|c| c.set(prev_ctx));
let items = items?;
let v = if items.len() == 1 { items.into_iter().next().unwrap() }
else { Value::Sequence(items) };
if let Some(t) = &uf.as_type {
if let Some(typed) = crate::eval::coerce_to_user_schema_type(&v, t, style, idx) {
return Ok(typed);
}
if let Some(st) = crate::eval::parse_as_atomic_type(t) {
return crate::eval::coerce_to_atomic_sequence(v, &st, idx)
.map_err(|e| err(&format!("xsl:function: {e}")));
}
}
Ok(v)
}
fn avt_static_string(a: &crate::ast::Avt) -> Option<String> { a.as_literal() }
#[derive(Clone, Copy)]
struct FnEnv<'a> {
style: &'a crate::ast::StylesheetAst,
depth: u32,
}
fn fn_seq_constructor_value<I: DocIndexLike>(
select: Option<&sup_xml_core::xpath::Expr>,
body: &[crate::ast::Instr],
as_type: Option<&str>,
idx: &I,
ctx_node: NodeId, pos: usize, size: usize,
bindings: &dyn XPathBindings,
static_ctx: &StaticContext,
) -> std::result::Result<Value, sup_xml_core::error::XmlError> {
use sup_xml_core::xpath::eval::{eval_expr, EvalCtx};
if let Some(sel) = select {
let ctx = EvalCtx {
context_node: ctx_node, pos, size, bindings,
static_ctx,
};
return eval_expr(sel, &ctx, idx);
}
if body.is_empty() {
return Ok(Value::String(String::new()));
}
let mut builder = ResultBuilder::new();
builder.no_text_merge = true;
build_function_subtree(body, bindings, idx, ctx_node, pos, size, &mut builder, static_ctx)?;
let nodes = builder.finish();
let want_nodes = as_type
.map(|t| as_is_sequence_typed(t) && !as_target_is_atomic(t))
.unwrap_or(false);
if want_nodes {
Ok(Value::NodeSet(rtf_children_into_index_generic(idx, &nodes)))
} else if as_type.is_none() {
Ok(Value::NodeSet(rtf_children_into_index_generic(idx, &nodes)))
} else {
Ok(Value::String(stringify(&nodes)))
}
}
fn fn_coerce_as<I: DocIndexLike>(
v: Value, as_type: Option<&str>, idx: &I,
) -> std::result::Result<Value, sup_xml_core::error::XmlError> {
use sup_xml_core::error::{ErrorDomain, ErrorLevel, XmlError};
match as_type.and_then(crate::eval::parse_as_atomic_type) {
Some(st) => crate::eval::coerce_to_atomic_sequence(v, &st, idx)
.map_err(|e| XmlError::new(
ErrorDomain::XPath, ErrorLevel::Error, format!("{e}"))),
None => Ok(v),
}
}
fn eval_function_body<I: DocIndexLike>(
body: &[crate::ast::Instr],
idx: &I,
ctx_node: NodeId,
pos: usize,
size: usize,
bindings: &dyn XPathBindings,
env: FnEnv,
static_ctx: &StaticContext,
) -> std::result::Result<Vec<Value>, sup_xml_core::error::XmlError> {
use sup_xml_core::error::{ErrorDomain, ErrorLevel, XmlError};
use sup_xml_core::xpath::eval::{eval_expr, value_to_bool, value_to_string_with, EvalCtx};
use crate::ast::Instr;
let err = |m: String| XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, m);
let mut out: Vec<Value> = Vec::new();
fn mk_ctx<'a>(
b: &'a dyn XPathBindings, sc: &'a StaticContext,
ctx_node: NodeId, pos: usize, size: usize,
) -> EvalCtx<'a> {
EvalCtx {
context_node: ctx_node, pos, size, bindings: b,
static_ctx: sc,
}
}
for (i, instr) in body.iter().enumerate() {
match instr {
Instr::Sequence { select } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
out.push(v);
}
Instr::Variable(v) => {
let val = if let Some(sel) = &v.select {
eval_expr(sel, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?
} else if v.body.is_empty() {
Value::String(String::new())
} else {
let items = eval_function_body(&v.body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
if items.len() == 1 { items.into_iter().next().unwrap() }
else { Value::Sequence(items) }
};
let layered = NamedBinding {
parent_owned: Box::new(PassthroughBindings { inner: bindings }),
name: v.name.local.clone(),
value: val,
};
let mut tail = eval_function_body(
&body[i + 1..], idx, ctx_node, pos, size, &layered, env, static_ctx)?;
out.append(&mut tail);
return Ok(out);
}
Instr::If { test, body: if_body } => {
let v = eval_expr(test, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
if value_to_bool(&v, idx) {
let mut sub = eval_function_body(if_body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
out.append(&mut sub);
}
}
Instr::Choose { whens, otherwise } => {
let mut matched = false;
for (test, when_body) in whens {
let v = eval_expr(test, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
if value_to_bool(&v, idx) {
let mut sub = eval_function_body(
when_body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
out.append(&mut sub);
matched = true;
break;
}
}
if !matched {
if let Some(else_body) = otherwise {
let mut sub = eval_function_body(
else_body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
out.append(&mut sub);
}
}
}
Instr::ForEach { select, body: fe_body, .. } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
let items: Vec<Value> = match v {
Value::NodeSet(ns) => ns.into_iter()
.map(|id| Value::NodeSet(vec![id])).collect(),
Value::Sequence(items) => items,
other => vec![other],
};
let total = items.len();
for (i, item) in items.into_iter().enumerate() {
let cx = match &item {
Value::NodeSet(ns) if ns.len() == 1 => ns[0],
_ => ctx_node,
};
let mut sub = eval_function_body(
fe_body, idx, cx, i + 1, total, bindings, env, static_ctx)?;
out.append(&mut sub);
}
}
Instr::ValueOf { select, separator, .. } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
let text = match separator {
None => value_to_string_with(&v, idx, bindings),
Some(sep_avt) => {
let sep = avt_static_string(sep_avt)
.unwrap_or_else(|| " ".to_string());
let items = sequence_string_items(
&v, idx,
NumStyle::from_context(false, bindings.xpath_version_2_or_later()),
);
if items.len() <= 1 {
value_to_string_with(&v, idx, bindings)
} else {
items.join(&sep)
}
}
};
out.push(Value::String(text));
}
Instr::ValueOfBody { body: vob_body, separator, .. } => {
let sep = separator.as_ref()
.and_then(avt_static_string).unwrap_or_default();
let sub = eval_function_body(
vob_body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
let pieces: Vec<String> = sub.into_iter()
.map(|v| value_to_string_with(&v, idx, bindings))
.filter(|s| !s.is_empty())
.collect();
out.push(Value::String(pieces.join(&sep)));
}
Instr::LiteralText { text, .. } => {
out.push(Value::String(text.clone()));
}
Instr::LiteralElement { .. }
| Instr::Element { .. }
| Instr::Attribute { .. }
| Instr::Comment { .. }
| Instr::ProcessingInstruction { .. }
| Instr::CopyOf { .. }
| Instr::Copy { .. } => {
let mut builder = ResultBuilder::new();
build_function_subtree(
std::slice::from_ref(instr), bindings, idx,
ctx_node, pos, size, &mut builder, static_ctx,
)?;
let nodes = builder.finish();
let ids = rtf_children_into_index_generic(idx, &nodes);
for id in ids {
out.push(Value::NodeSet(vec![id]));
}
}
Instr::Number { value, select, level, count, from, format,
grouping_separator, grouping_size, ordinal, lang, letter_value: _, start_at } => {
let format_str = avt_static_string(format)
.unwrap_or_else(|| "1".to_string());
let fmt = crate::number::parse_format(&format_str);
let ordinal_str = ordinal.as_ref()
.and_then(avt_static_string).unwrap_or_default();
let lang_str = lang.as_ref()
.and_then(avt_static_string).unwrap_or_default();
let opts = crate::number::FormatOptions {
ordinal: !ordinal_str.is_empty(),
lang: if lang_str.is_empty() { None } else { Some(lang_str) },
ordinal_scheme: if ordinal_str.is_empty() { None }
else { Some(ordinal_str) },
};
let grouping_sep_str = grouping_separator.as_ref()
.and_then(avt_static_string);
let grouping_size_n = grouping_size.as_ref()
.and_then(avt_static_string)
.and_then(|s| s.parse::<usize>().ok())
.filter(|n| *n > 0);
let group = match (grouping_sep_str, grouping_size_n) {
(Some(sep), Some(n)) => Some((sep, n)),
_ => None,
};
let numbers = if let Some(ve) = value {
let v = eval_expr(ve, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
match v {
Value::Sequence(items) => items.into_iter().filter_map(|it| {
let f = crate::eval::value_to_number_xpath(&it, idx);
if f.is_finite() { Some(f.round() as i64) } else { None }
}).collect(),
Value::IntRange { lo, hi } => (lo..=hi).collect(),
Value::Number(f) if f.as_f64().is_finite() => vec![f.as_f64().round() as i64],
Value::Number(_) => Vec::new(),
other => {
let f = crate::eval::value_to_number_xpath(&other, idx);
if f.is_finite() { vec![f.round() as i64] } else { Vec::new() }
}
}
} else {
let target_node = if let Some(sel) = select {
match eval_expr(sel, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)? {
Value::NodeSet(ns) if ns.len() == 1 => ns[0],
_ => return Err(err(
"xsl:number select must evaluate to a \
single node (XTTE1000)".into())),
}
} else {
if sup_xml_core::xpath::eval::focus_is_undefined()
|| in_atomic_for_each()
{
return Err(err(
"xsl:number with no select= called where \
the context item is not a node \
(XTTE0990)".into()));
}
ctx_node
};
compute_number_list_generic(
idx, bindings, *level,
count.as_ref(), from.as_ref(), target_node,
).map_err(|e| err(format!("xsl:number in function body: {e}")))?
};
let mut numbers = numbers;
let start_offsets: Vec<i64> = start_at.as_ref()
.and_then(avt_static_string)
.map(|s| s.split_whitespace()
.filter_map(|t| t.parse::<i64>().ok())
.map(|v| v - 1).collect())
.unwrap_or_default();
for (i, n) in numbers.iter_mut().enumerate() {
let off = if value.is_some() { start_offsets.first() }
else { start_offsets.get(i) };
if let Some(off) = off { *n += off; }
}
let mut s = crate::number::format_list_opts(&numbers, &fmt, &opts);
if let Some((sep, sz)) = &group {
s = crate::number::apply_grouping(&s, sep, *sz);
}
out.push(Value::String(s));
}
Instr::Message { terminate: _, body } => {
let _ = eval_function_body(body, idx, ctx_node, pos, size, bindings, env, static_ctx)?;
}
Instr::CallTemplate { name, with_params } => {
let key = qname_key(name);
let template = env.style.templates.iter()
.find(|t| t.name.as_ref().map(qname_key).as_deref()
== Some(key.as_str()))
.ok_or_else(|| err(format!(
"xsl:call-template in function body: no template named `{key}`")))?;
if env.depth >= MAX_TEMPLATE_CALL_DEPTH {
return Err(err(format!(
"xsl:call-template depth exceeds limit ({MAX_TEMPLATE_CALL_DEPTH}) \
— likely infinite recursion in template `{key}`")));
}
let mut supplied: Vec<(String, Value)> =
Vec::with_capacity(with_params.len());
for wp in with_params {
if wp.tunnel {
return Err(err(format!(
"xsl:with-param tunnel=\"yes\" (param `{}`) requires mutable \
XSLT state and isn't supported in xsl:function bodies yet",
wp.name.local)));
}
let raw = fn_seq_constructor_value(
wp.select.as_ref(), &wp.body, wp.as_type.as_deref(),
idx, ctx_node, pos, size, bindings, static_ctx)?;
supplied.push((
qname_key(&wp.name),
fn_coerce_as(raw, wp.as_type.as_deref(), idx)?,
));
}
let mut chain: Box<dyn XPathBindings> =
Box::new(PassthroughBindings { inner: bindings });
for p in &template.params {
let pkey = qname_key(&p.name);
let raw = if p.tunnel {
if p.required {
return Err(err(format!(
"required tunnel parameter `{pkey}` can't be supplied \
in an xsl:function body")));
}
fn_seq_constructor_value(
p.select.as_ref(), &p.body, p.as_type.as_deref(),
idx, ctx_node, pos, size, chain.as_ref(), static_ctx)?
} else if let Some((_, v)) = supplied.iter().find(|(k, _)| *k == pkey) {
v.clone()
} else if p.required {
return Err(err(format!("required parameter `{pkey}` not supplied")));
} else {
fn_seq_constructor_value(
p.select.as_ref(), &p.body, p.as_type.as_deref(),
idx, ctx_node, pos, size, chain.as_ref(), static_ctx)?
};
let value = fn_coerce_as(raw, p.as_type.as_deref(), idx)?;
chain = Box::new(NamedBinding {
parent_owned: chain, name: p.name.local.clone(), value,
});
}
let mut sub = eval_function_body(
&template.body, idx, ctx_node, pos, size, chain.as_ref(),
FnEnv { depth: env.depth + 1, ..env }, static_ctx)?;
out.append(&mut sub);
}
Instr::Fallback { .. } => {
}
Instr::PerformSort { select, sort, body: _ } => {
let select_expr = select.as_ref().ok_or_else(|| err(
"xsl:perform-sort body form is not supported in xsl:function bodies".into()
))?;
let v = eval_expr(select_expr, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
match v {
Value::NodeSet(nodes) => {
let sorted = crate::sort::sort_nodes(
&nodes, sort, idx, |e, n, p, s| {
eval_expr(e, &mk_ctx(bindings, static_ctx, n, p, s), idx)
.map_err(crate::error::XsltError::from)
},
).map_err(|e| match e {
crate::error::XsltError::Xpath(x) => x,
other => err(format!("{other}")),
})?;
out.push(Value::NodeSet(sorted));
}
other => {
let items: Vec<Value> = match other {
Value::Sequence(items) => items,
single => vec![single],
};
let sorted = crate::sort::sort_items(
items, sort, idx, |e, item, p, s| {
sup_xml_core::xpath::eval::with_atomic_context_item(
Some(item.clone()),
|| eval_expr(e, &mk_ctx(bindings, static_ctx, ctx_node, p, s), idx)
.map_err(crate::error::XsltError::from),
)
},
).map_err(|e| match e {
crate::error::XsltError::Xpath(x) => x,
other => err(format!("{other}")),
})?;
for it in sorted { out.push(it); }
}
}
}
other => return Err(err(format!(
"xsl:function body: instruction `{}` requires mutable XSLT state \
and isn't supported in user-function bodies yet",
instr_kind_name(other),
))),
}
}
Ok(out)
}
fn instr_kind_name(i: &crate::ast::Instr) -> &'static str {
use crate::ast::Instr;
match i {
Instr::Sequence { .. } => "xsl:sequence",
Instr::Map { .. } => "xsl:map",
Instr::MapEntry { .. } => "xsl:map-entry",
Instr::Variable(_) => "xsl:variable",
Instr::If { .. } => "xsl:if",
Instr::Choose { .. } => "xsl:choose",
Instr::ForEach { .. } => "xsl:for-each",
Instr::Iterate { .. } => "xsl:iterate",
Instr::NextIteration { .. } => "xsl:next-iteration",
Instr::Break { .. } => "xsl:break",
Instr::ForEachGroup { .. } => "xsl:for-each-group",
Instr::ApplyTemplates { .. } => "xsl:apply-templates",
Instr::ApplyImports { .. } => "xsl:apply-imports",
Instr::NextMatch { .. } => "xsl:next-match",
Instr::CallTemplate { .. } => "xsl:call-template",
Instr::ValueOf { .. } => "xsl:value-of",
Instr::ValueOfBody { .. } => "xsl:value-of",
Instr::LiteralElement { .. } => "literal result element",
Instr::LiteralText { .. } => "literal text",
Instr::Copy { .. } => "xsl:copy",
Instr::CopyOf { .. } => "xsl:copy-of",
Instr::Element { .. } => "xsl:element",
Instr::Attribute { .. } => "xsl:attribute",
Instr::Comment { .. } => "xsl:comment",
Instr::ProcessingInstruction { .. } => "xsl:processing-instruction",
Instr::Number { .. } => "xsl:number",
Instr::Message { .. } => "xsl:message",
Instr::Fallback { .. } => "xsl:fallback",
Instr::AnalyzeString { .. } => "xsl:analyze-string",
Instr::SourceDocument { .. } => "xsl:source-document",
Instr::Fork { .. } => "xsl:fork",
Instr::WherePopulated { .. } => "xsl:where-populated",
Instr::OnEmpty { .. } => "xsl:on-empty",
Instr::OnNonEmpty { .. } => "xsl:on-non-empty",
Instr::Evaluate { .. } => "xsl:evaluate",
Instr::Merge { .. } => "xsl:merge",
Instr::PerformSort { .. } => "xsl:perform-sort",
Instr::Document { .. } => "xsl:document",
Instr::ResultDocument { .. } => "xsl:result-document",
Instr::Namespace { .. } => "xsl:namespace",
Instr::Try { .. } => "xsl:try",
Instr::Unsupported { .. } => "unknown",
}
}
struct PassthroughBindings<'p> { inner: &'p dyn XPathBindings }
impl<'p> PassthroughBindings<'p> {
fn suppress_regex_group(
ns_uri: &str, name: &str,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
let is_builtin = name == "regex-group"
&& (ns_uri.is_empty()
|| ns_uri == "http://www.w3.org/2005/xpath-functions");
is_builtin.then(|| Ok(Value::String(String::new())))
}
}
impl<'p> XPathBindings for PassthroughBindings<'p> {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
self.inner.resolve_prefix(prefix)
}
fn xpath_version_2_or_later(&self) -> bool { self.inner.xpath_version_2_or_later() }
fn variable(&self, name: &str) -> Option<Value> { self.inner.variable(name) }
fn call_function(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
Self::suppress_regex_group(ns_uri, name)
.or_else(|| self.inner.call_function(ns_uri, name, args))
}
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>, ctx: NodeId,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
Self::suppress_regex_group(ns_uri, name)
.or_else(|| self.inner.call_function_in(ns_uri, name, args, ctx))
}
fn foreign_string_value(
&self, p: sup_xml_core::xpath::eval::ForeignNodePtr,
) -> String { self.inner.foreign_string_value(p) }
}
struct NamedBinding<'p> {
parent_owned: Box<dyn XPathBindings + 'p>,
name: String,
value: Value,
}
impl<'p> XPathBindings for NamedBinding<'p> {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
self.parent_owned.resolve_prefix(prefix)
}
fn xpath_version_2_or_later(&self) -> bool { self.parent_owned.xpath_version_2_or_later() }
fn variable(&self, name: &str) -> Option<Value> {
if name == self.name { Some(self.value.clone()) } else { self.parent_owned.variable(name) }
}
fn call_function(
&self, ns_uri: &str, name: &str, args: Vec<Value>,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
self.parent_owned.call_function(ns_uri, name, args)
}
fn call_function_in(
&self, ns_uri: &str, name: &str, args: Vec<Value>, ctx: NodeId,
) -> Option<std::result::Result<Value, sup_xml_core::error::XmlError>> {
self.parent_owned.call_function_in(ns_uri, name, args, ctx)
}
fn foreign_string_value(
&self, p: sup_xml_core::xpath::eval::ForeignNodePtr,
) -> String { self.parent_owned.foreign_string_value(p) }
}
pub(crate) struct AccumulatorData {
pub before: HashMap<NodeId, Value>,
pub after: HashMap<NodeId, Value>,
pub initial: Value,
}
#[derive(Default)]
struct VariableScope {
frames: Vec<HashMap<String, Value>>,
}
impl VariableScope {
fn enter(&mut self) { self.frames.push(HashMap::new()); }
fn leave(&mut self) { self.frames.pop(); }
fn bind(&mut self, name: String, value: Value) {
if self.frames.is_empty() { self.frames.push(HashMap::new()); }
self.frames.last_mut().unwrap().insert(name, value);
}
fn get(&self, name: &str) -> Option<&Value> {
for frame in self.frames.iter().rev() {
if let Some(v) = frame.get(name) { return Some(v); }
}
None
}
}
#[derive(Default, Debug)]
pub(crate) struct NamespaceContext {
map: HashMap<String, String>,
}
impl NamespaceContext {
pub(crate) fn resolve(&self, prefix: &str) -> Option<String> {
self.map.get(prefix).cloned()
}
fn from_stylesheet(style: &StylesheetAst) -> Self {
let mut map: HashMap<String, String> = style.namespaces.clone();
map.entry("math".into()) .or_insert("http://exslt.org/math".into());
map.entry("date".into()) .or_insert("http://exslt.org/dates-and-times".into());
map.entry("str".into()) .or_insert("http://exslt.org/strings".into());
map.entry("set".into()) .or_insert("http://exslt.org/sets".into());
map.entry("regexp".into()).or_insert("http://exslt.org/regular-expressions".into());
map.entry("exsl".into()) .or_insert("http://exslt.org/common".into());
map.entry("dyn".into()) .or_insert("http://exslt.org/dynamic".into());
NamespaceContext { map }
}
}
fn apply_builtin_template(
state: &mut EvalState,
node: NodeId,
mode: Option<&QName>,
) -> Result<()> {
apply_builtin_template_with_args(state, node, mode, &[])
}
fn apply_builtin_template_with_args(
state: &mut EvalState,
node: NodeId,
mode: Option<&QName>,
args: &[(QName, Value, Option<Vec<ResultNode>>)],
) -> Result<()> {
match mode_on_no_match(state.style, mode) {
OnNoMatch::DeepSkip => Ok(()),
OnNoMatch::Fail => Err(XsltError::InvalidStylesheet(
"no template rule matches and the mode's on-no-match is \
'fail' (XTDE0555)".into())),
OnNoMatch::DeepCopy => {
match state.idx.kind(node) {
XPathNodeKind::Document => {
for c in state.idx.children(node).to_vec() {
deep_copy_node(state, c, None, true)?;
}
}
_ => deep_copy_node(state, node, None, true)?,
}
Ok(())
}
OnNoMatch::ShallowCopy => builtin_shallow(state, node, mode, args, true),
OnNoMatch::ShallowSkip => builtin_shallow(state, node, mode, args, false),
OnNoMatch::TextOnlyCopy => {
match state.idx.kind(node) {
XPathNodeKind::Document | XPathNodeKind::Element => {
let style = state.style;
let idx = state.idx;
let children: Vec<NodeId> = state.idx.children(node).iter().copied()
.filter(|&n| !crate::whitespace::should_strip(style, n, idx))
.collect();
let total = children.len();
for (i, child) in children.iter().enumerate() {
apply_one_to_node_with_args(state, *child, mode, i + 1, total, args)?;
}
}
XPathNodeKind::Text | XPathNodeKind::CData => {
let s = state.idx.string_value(node);
state.builder.push_text(s, false);
}
XPathNodeKind::Attribute => {
let s = state.idx.string_value(node);
state.builder.push_text(s, false);
}
_ => {}
}
Ok(())
}
}
}
fn mode_on_no_match(style: &StylesheetAst, mode: Option<&QName>) -> OnNoMatch {
fn norm(q: Option<&QName>) -> Option<(&str, &str)> {
match q {
Some(q) if !q.local.is_empty() || !q.uri.is_empty() =>
Some((q.uri.as_str(), q.local.as_str())),
_ => None,
}
}
let want = norm(mode);
style.modes.iter()
.find(|m| norm(m.name.as_ref()) == want)
.map(|m| m.on_no_match)
.unwrap_or_default()
}
fn builtin_shallow(
state: &mut EvalState,
node: NodeId,
mode: Option<&QName>,
args: &[(QName, Value, Option<Vec<ResultNode>>)],
copy_self: bool,
) -> Result<()> {
let recurse = |state: &mut EvalState| -> Result<()> {
let attrs: Vec<NodeId> = state.idx.attr_range(node).collect();
let na = attrs.len();
for (i, a) in attrs.iter().enumerate() {
apply_one_to_node_with_args(state, *a, mode, i + 1, na, args)?;
}
let style = state.style;
let idx = state.idx;
let children: Vec<NodeId> = state.idx.children(node).iter().copied()
.filter(|&n| !crate::whitespace::should_strip(style, n, idx))
.collect();
let nc = children.len();
for (i, c) in children.iter().enumerate() {
apply_one_to_node_with_args(state, *c, mode, i + 1, nc, args)?;
}
Ok(())
};
match state.idx.kind(node) {
XPathNodeKind::Document => recurse(state),
XPathNodeKind::Element => {
if copy_self {
let q = element_qname(state, node);
state.builder.open_element(q);
for ns_id in state.idx.ns_range(node) {
let prefix = state.idx.local_name(ns_id);
if prefix == "xml" { continue; }
let uri = state.idx.string_value(ns_id);
let p = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
state.builder.push_namespace_decl(p, uri);
}
recurse(state)?;
state.builder.close_element();
Ok(())
} else {
recurse(state)
}
}
XPathNodeKind::Text | XPathNodeKind::CData => {
if copy_self { state.builder.push_text(state.idx.string_value(node), false); }
Ok(())
}
XPathNodeKind::Attribute => {
if copy_self {
let q = attribute_qname(state, node);
state.builder.push_attribute(q, state.idx.string_value(node));
}
Ok(())
}
XPathNodeKind::Comment => {
if copy_self { state.builder.push_comment(state.idx.string_value(node)); }
Ok(())
}
XPathNodeKind::PI => {
if copy_self {
state.builder.push_pi(
state.idx.pi_target(node).to_string(),
state.idx.string_value(node));
}
Ok(())
}
XPathNodeKind::Namespace => Ok(()),
}
}
struct EvalState<'a> {
style: &'a StylesheetAst,
idx: &'a DocIndex<'a>,
namespaces: &'a NamespaceContext,
keys: Option<&'a KeyIndex>,
documents: Option<&'a HashMap<String, NodeId>>,
xslt_current: NodeId,
variables: VariableScope,
rtfs: HashMap<String, Vec<ResultNode>>,
rtf_scopes: Vec<Vec<String>>,
builder: ResultBuilder,
principal_buf: Option<ResultBuilder>,
unparsed_entities: std::sync::Arc<HashMap<String, sup_xml_tree::UnparsedEntity>>,
source_doc: &'a Document,
apply_imports_ctx: Option<(NodeId, Option<QName>, i32, Option<usize>, f64, Option<usize>)>,
user_exts: Option<&'a dyn crate::extensions::ExtensionFunctions>,
sequence_sinks: Vec<Vec<Value>>,
template_call_depth: u32,
current_group: Vec<NodeId>,
current_grouping_key: Option<Value>,
accumulators: HashMap<String, AccumulatorData>,
regex_groups: Vec<String>,
tunnel_pool: HashMap<String, Value>,
unparsed_texts: Option<&'a HashMap<String, String>>,
static_base_uri: Option<String>,
loader: Option<&'a dyn crate::loader::Loader>,
loader_base: Option<&'a str>,
dyn_doc_cache: Option<&'a std::cell::RefCell<HashMap<String, NodeId>>>,
rtf_base_uris: &'a std::cell::RefCell<HashMap<NodeId, String>>,
static_ctx: StaticContext,
source_types: Option<&'a SourceTypes>,
}
const MAX_TEMPLATE_CALL_DEPTH: u32 = 1024;
impl<'a> EvalState<'a> {
fn sequence_sink_active(&self) -> bool { !self.sequence_sinks.is_empty() }
fn num_style(&self) -> NumStyle {
let v2 = self.style.version.trim().split('.').next()
.and_then(|m| m.parse::<u32>().ok())
.is_some_and(|m| m >= 2);
NumStyle::from_context(false, v2)
}
fn push_to_sequence_sink(&mut self, v: Value) {
if let Some(top) = self.sequence_sinks.last_mut() { top.push(v); }
}
fn bindings(&self) -> XsltBindings<'_, DocIndex<'a>> {
XsltBindings {
variables: &self.variables,
namespaces: self.namespaces,
keys: self.keys,
xslt_context_node: self.xslt_current,
idx: self.idx,
style: self.style,
documents: self.documents,
decimal_formats: &self.style.decimal_formats,
unparsed_entities: &self.unparsed_entities,
user_exts: self.user_exts,
current_group: if self.current_group.is_empty() {
None
} else {
Some(self.current_group.as_slice())
},
current_grouping_key: self.current_grouping_key.as_ref(),
accumulators: (!self.accumulators.is_empty()).then_some(&self.accumulators),
regex_groups: if self.regex_groups.is_empty() {
None
} else {
Some(self.regex_groups.as_slice())
},
user_functions: (!self.style.functions.is_empty())
.then_some(self.style.functions.as_slice()),
unparsed_texts: self.unparsed_texts,
xslt_3_0: xslt_version_3_or_more(&self.style.version),
xslt_version: self.style.version.as_str(),
static_base_uri: self.static_base_uri.as_deref(),
loader: self.loader,
loader_base: self.loader_base,
dyn_doc_cache: self.dyn_doc_cache,
rtf_base_uris: self.rtf_base_uris,
source_types: self.source_types,
}
}
fn xpath_eval(
&self, expr: &sup_xml_core::xpath::Expr,
context_node: NodeId, pos: usize, size: usize,
) -> Result<Value> {
sup_xml_core::xpath::eval::reset_eval_budget();
let sc = self.static_ctx;
let b = self.bindings();
sup_xml_core::xpath::eval::validate_prefixes(expr, &b)
.map_err(|e| e.or_xpath_code("XPST0081"))?;
let ctx = EvalCtx { context_node, pos, size, bindings: &b, static_ctx: &sc };
eval_expr(expr, &ctx, self.idx).map_err(XsltError::from)
}
}
pub fn apply_stylesheet(
style: &StylesheetAst,
source_doc: &Document,
) -> Result<ResultTree> {
apply_stylesheet_full(style, source_doc, &NullLoader, None, None)
}
pub fn apply_stylesheet_with_loader(
style: &StylesheetAst,
source_doc: &Document,
loader: &dyn Loader,
base: Option<&str>,
) -> Result<ResultTree> {
apply_stylesheet_full(style, source_doc, loader, base, None)
}
pub fn apply_stylesheet_full(
style: &StylesheetAst,
source_doc: &Document,
loader: &dyn Loader,
base: Option<&str>,
extensions: Option<&dyn crate::extensions::ExtensionFunctions>,
) -> Result<ResultTree> {
apply_stylesheet_full_with_params(style, source_doc, loader, base, extensions, &[])
}
pub fn apply_stylesheet_full_with_params(
style: &StylesheetAst,
source_doc: &Document,
loader: &dyn Loader,
base: Option<&str>,
extensions: Option<&dyn crate::extensions::ExtensionFunctions>,
top_level_params: &[(String, String)],
) -> Result<ResultTree> {
apply_stylesheet_full_with_params_and_initial(
style, source_doc, loader, base, extensions, top_level_params, None, None,
)
}
pub fn apply_stylesheet_full_with_params_and_initial(
style: &StylesheetAst,
source_doc: &Document,
loader: &dyn Loader,
base: Option<&str>,
extensions: Option<&dyn crate::extensions::ExtensionFunctions>,
top_level_params: &[(String, String)],
initial_template: Option<&str>,
initial_mode: Option<&str>,
) -> Result<ResultTree> {
sup_xml_core::xpath::eval::reset_eval_budget();
sup_xml_core::xpath::eval::refresh_stable_now();
let _ = take_iterate_control();
reset_secondary_docs();
let mut loaded_docs: Vec<Box<Document>> = Vec::with_capacity(style.documents_to_load.len());
for href in &style.documents_to_load {
let text = if href.is_empty() {
match base {
Some(b) => loader.load(b, None)?,
None => continue, }
} else {
loader.load(crate::functions::strip_uri_fragment(href), base)?
};
let opts = sup_xml_core::ParseOptions {
namespace_aware: true,
..Default::default()
};
let doc = sup_xml_core::parse_str(&text, &opts).map_err(XsltError::from)?;
loaded_docs.push(Box::new(doc));
}
let mut style_documents_dynamic: Vec<(String, Box<Document>)> = Vec::new();
if crate::walk::has_dynamic_document_call(style) {
let mut seen: std::collections::HashSet<String> = style.documents_to_load
.iter().cloned().collect();
let mut candidates: Vec<String> = Vec::new();
if crate::walk::has_document_node_set_call(style) {
collect_candidate_uris(source_doc, &mut candidates, &mut seen);
}
for lit in crate::walk::collect_all_string_literals(style) {
if seen.insert(lit.clone()) { candidates.push(lit); }
}
for cand in candidates {
if cand.is_empty() { continue; }
match loader.load(&cand, base) {
Ok(text) => {
let opts = sup_xml_core::ParseOptions {
namespace_aware: true, ..Default::default()
};
if let Ok(d) = sup_xml_core::parse_str(&text, &opts) {
style_documents_dynamic.push((cand, Box::new(d)));
}
}
Err(_) => { }
}
}
}
let mut unparsed_texts: HashMap<String, String> = HashMap::new();
for uri in crate::walk::collect_static_unparsed_text_uris(style) {
if uri.is_empty() { continue; }
if let Ok(text) = loader.load(&uri, base) {
unparsed_texts.insert(uri, text);
}
}
let mut idx = DocIndex::build(source_doc);
let mut documents: HashMap<String, NodeId> = HashMap::with_capacity(loaded_docs.len());
for (href, doc) in style.documents_to_load.iter().zip(loaded_docs.iter()) {
let id = idx.add_document(doc.as_ref());
documents.insert(crate::functions::strip_uri_fragment(href).to_string(), id);
}
for (uri, doc) in &style_documents_dynamic {
let id = idx.add_document(doc.as_ref());
documents.insert(uri.clone(), id);
}
apply_strip_space(style, &mut idx);
let source_types = build_source_types(style, source_doc, &idx);
let namespaces = NamespaceContext::from_stylesheet(style);
let unparsed_entities = {
let raw = source_doc.unparsed_entities();
match source_doc.base_url() {
Some(base) if !raw.is_empty() => {
let resolved = raw.iter().map(|(name, ent)| {
(name.clone(), sup_xml_tree::UnparsedEntity {
system_id: sup_xml_core::xpath::eval::resolve_uri_against(
base, &ent.system_id),
public_id: ent.public_id.clone(),
})
}).collect();
std::sync::Arc::new(resolved)
}
_ => raw.clone(),
}
};
let key_index: Option<KeyIndex>;
let dyn_doc_cache: std::cell::RefCell<HashMap<String, NodeId>>
= std::cell::RefCell::new(HashMap::new());
let rtf_base_uris: std::cell::RefCell<HashMap<NodeId, String>>
= std::cell::RefCell::new(HashMap::new());
{
let mut map = rtf_base_uris.borrow_mut();
if let Some(uri) = source_doc.base_url().or(base) {
map.insert(0, uri.to_string());
}
for (href, &id) in &documents {
map.insert(id, href.clone());
}
}
let mut state = EvalState {
style,
idx: &idx,
namespaces: &namespaces,
keys: None,
documents: Some(&documents),
xslt_current: 0,
variables: VariableScope::default(),
rtfs: HashMap::new(),
rtf_scopes: Vec::new(),
builder: {
let mut b = ResultBuilder::new();
b.is_principal_document = true;
b
},
principal_buf: None,
unparsed_entities: unparsed_entities.clone(),
source_doc,
apply_imports_ctx: None,
user_exts: extensions,
sequence_sinks: Vec::new(),
template_call_depth: 0,
current_group: Vec::new(),
regex_groups: Vec::new(),
tunnel_pool: HashMap::new(),
current_grouping_key: None,
accumulators: HashMap::new(),
unparsed_texts: if unparsed_texts.is_empty() { None } else { Some(&unparsed_texts) },
static_base_uri: style.xml_base.clone().or_else(|| base.map(str::to_string)),
loader: Some(loader),
loader_base: base,
dyn_doc_cache: Some(&dyn_doc_cache),
rtf_base_uris: &rtf_base_uris,
static_ctx: static_ctx_for_version(&style.version),
source_types: source_types.as_ref(),
};
state.variables.enter();
let mut seen_params: std::collections::HashSet<(Option<String>, String)> =
std::collections::HashSet::new();
let dedup_params: Vec<&_> = style.global_params.iter().filter(|p| {
seen_params.insert((p.name.prefix.clone(), p.name.local.clone()))
}).collect();
let mut seen_vars: std::collections::HashSet<(Option<String>, String)> =
std::collections::HashSet::new();
let dedup_vars: Vec<&_> = style.global_variables.iter().filter(|v| {
seen_vars.insert((v.name.prefix.clone(), v.name.local.clone()))
}).collect();
for p in &dedup_params {
state.variables.bind(qname_key(&p.name), Value::String(String::new()));
}
for v in &dedup_vars {
state.variables.bind(qname_key(&v.name), Value::String(String::new()));
}
for (name, value) in top_level_params {
for p in &dedup_params {
if p.name.local == *name {
state.variables.bind(qname_key(&p.name), Value::String(value.clone()));
break;
}
}
}
for p in &dedup_params {
if p.required && !top_level_params.iter().any(|(n, _)| *n == p.name.local) {
return Err(XsltError::InvalidStylesheet(format!(
"required global xsl:param '{}' was not supplied (XTDE0050)",
p.name.local,
)));
}
}
for _round in 0..16 {
for p in &dedup_params {
let overridden = top_level_params.iter().any(|(n, _)| *n == p.name.local);
if !overridden {
bind_variable(&mut state, &p.name, p.select.as_ref(), &p.body, p.as_type.as_deref(), None, 0, 1, 1)?;
}
}
for v in &dedup_vars {
bind_variable(&mut state, &v.name, v.select.as_ref(), &v.body, v.as_type.as_deref(), v.base_uri.as_deref(), 0, 1, 1)?;
}
}
if !style.keys.is_empty() {
let sc = state.static_ctx;
let (mut built, deferred) = {
let state_ref = &state;
KeyIndex::build(style, &idx, |expr, node| {
let mut bindings = state_ref.bindings();
bindings.xslt_context_node = node;
let ctx = EvalCtx { context_node: node, pos: 1, size: 1, bindings: &bindings, static_ctx: &sc };
eval_expr(expr, &ctx, &idx)
}).map_err(XsltError::from)?
};
for (ki, node_id) in deferred {
let value = eval_key_body_value(&mut state, &style.keys[ki].body, node_id)?;
built.add_value(&qname_key(&style.keys[ki].name), node_id, &value, &idx);
}
key_index = Some(built);
state.keys = key_index.as_ref();
for p in &dedup_params {
let overridden = top_level_params.iter().any(|(n, _)| *n == p.name.local);
if !overridden {
bind_variable(&mut state, &p.name, p.select.as_ref(), &p.body, p.as_type.as_deref(), None, 0, 1, 1)?;
}
}
for v in &dedup_vars {
bind_variable(&mut state, &v.name, v.select.as_ref(), &v.body, v.as_type.as_deref(), v.base_uri.as_deref(), 0, 1, 1)?;
}
}
let implicit_initial_template: Option<&str> =
if initial_template.is_none() && initial_mode.is_none() {
const XSL_INITIAL: &str = "{http://www.w3.org/1999/XSL/Transform}initial-template";
if state.style.templates.iter().any(|t|
t.name.as_ref().map(qname_key).as_deref() == Some(XSL_INITIAL))
{
Some(XSL_INITIAL)
} else { None }
} else { None };
let initial_template = initial_template.or(implicit_initial_template);
if !state.style.accumulators.is_empty() {
precompute_accumulators(&mut state, 0)?;
}
if let Some(name) = initial_template {
let key = if name.starts_with('{') {
name.to_string()
} else if let Some((p, l)) = name.split_once(':') {
match state.namespaces.resolve(p) {
Some(uri) => format!("{{{uri}}}{l}"),
None => name.to_string(),
}
} else {
name.to_string()
};
let tmpl = state.style.templates.iter()
.find(|t| t.name.as_ref().map(qname_key).as_deref() == Some(key.as_str()))
.ok_or_else(|| XsltError::UnresolvedReference(format!(
"no template named '{key}' for initial-template entry"
)))?;
run_template_body(&mut state, tmpl, 0, 1, 1, &[])?;
} else {
let mode_qname = initial_mode.map(|raw| {
match raw.split_once(':') {
Some((p, l)) => QName {
prefix: Some(p.to_string()),
local: l.to_string(),
uri: state.namespaces.resolve(p).unwrap_or_default(),
},
None => QName {
prefix: None,
local: raw.to_string(),
uri: String::new(),
},
}
});
apply_one_to_node(&mut state, 0, mode_qname.as_ref())?;
}
state.variables.leave();
if let Some(msg) = state.builder.deferred_error.take() {
return Err(XsltError::InvalidStylesheet(msg));
}
let children = state.builder.finish();
let mut output = crate::ast::OutputSpec::default();
for o in &style.outputs {
if o.method.is_some() { output.method = o.method.clone(); }
if o.encoding.is_some() { output.encoding = o.encoding.clone(); }
if o.indent.is_some() { output.indent = o.indent; }
if o.omit_xml_declaration.is_some() { output.omit_xml_declaration = o.omit_xml_declaration; }
if o.standalone.is_some() { output.standalone = o.standalone; }
if o.media_type.is_some() { output.media_type = o.media_type.clone(); }
if o.doctype_public.is_some() { output.doctype_public = o.doctype_public.clone(); }
if o.doctype_system.is_some() { output.doctype_system = o.doctype_system.clone(); }
if o.version.is_some() { output.version = o.version.clone(); }
output.cdata_section_elements.extend(o.cdata_section_elements.iter().cloned());
output.use_character_maps.extend(o.use_character_maps.iter().cloned());
}
let character_map = flatten_character_maps(
&output.use_character_maps, &style.character_maps);
let secondary = take_secondary_docs().into_iter()
.map(|(uri, nodes)| (uri, ResultTree {
children: nodes,
output: output.clone(),
character_map: character_map.clone(),
secondary: Vec::new(),
}))
.collect();
Ok(ResultTree { children, output, character_map, secondary })
}
fn flatten_character_maps(
refs: &[QName],
declared: &[crate::ast::CharacterMap],
) -> Vec<(char, String)> {
fn visit(
name: &QName,
declared: &[crate::ast::CharacterMap],
chain: &mut Vec<String>,
out: &mut Vec<(char, String)>,
) {
let key = qname_key(name);
if chain.iter().any(|c| c == &key) { return; }
let Some(map) = declared.iter().find(|m| qname_key(&m.name) == key) else {
return;
};
chain.push(key);
for referenced in &map.use_character_maps {
visit(referenced, declared, chain, out);
}
for (ch, repl) in &map.mappings {
if let Some(slot) = out.iter_mut().find(|(k, _)| k == ch) {
slot.1 = repl.clone();
} else {
out.push((*ch, repl.clone()));
}
}
chain.pop();
}
let mut out = Vec::new();
let mut chain = Vec::new();
for name in refs {
visit(name, declared, &mut chain, &mut out);
}
out
}
fn apply_one_to_node(state: &mut EvalState, node: NodeId, mode: Option<&QName>) -> Result<()> {
let mut bindings = state.bindings();
bindings.xslt_context_node = node;
let chosen = pattern::select_template(
state.style, node, mode, state.idx, &bindings,
).map_err(XsltError::from)?;
match chosen {
Some(sel) => {
state.template_call_depth += 1;
if state.template_call_depth > MAX_TEMPLATE_CALL_DEPTH {
state.template_call_depth -= 1;
return Err(XsltError::InvalidStylesheet(format!(
"xsl:apply-templates depth exceeds limit \
({MAX_TEMPLATE_CALL_DEPTH}) — possible infinite recursion"
)));
}
let prev = state.apply_imports_ctx.replace(
(node, mode.cloned(), sel.template.import_precedence,
template_index_of(state.style, sel.template),
sel.priority, sel.branch_idx),
);
let r = run_template_body(state, sel.template, node, 1, 1, &[]);
state.apply_imports_ctx = prev;
state.template_call_depth -= 1;
r?;
}
None => apply_builtin_template(state, node, mode)?,
}
Ok(())
}
const ERR_NS: &str = "http://www.w3.org/2005/xqt-errors";
fn run_try_instr<'a>(
state: &mut EvalState<'a>,
body: &[crate::ast::Instr],
catches: &[crate::ast::TryCatch],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<()> {
use crate::result_tree::ResultBuilder;
let prev = std::mem::replace(&mut state.builder, ResultBuilder::new());
let r = eval_body(state, body, ctx_node, pos, size);
let written = std::mem::replace(&mut state.builder, prev);
match r {
Ok(()) => {
for n in written.finish() {
copy_result_node(state, &n);
}
Ok(())
}
Err(e) => {
let (code_qname, message) = error_to_qname(&e);
for c in catches {
if !catch_matches(&c.errors, &code_qname, state) { continue; }
state.variables.enter();
for (local, value) in [
("code", Value::String(format_qname_for_err(&code_qname))),
("description", Value::String(message.clone())),
("value", Value::NodeSet(Vec::new())),
("module", Value::NodeSet(Vec::new())),
("line-number", Value::NodeSet(Vec::new())),
("column-number", Value::NodeSet(Vec::new())),
] {
let key = format!("{{{ERR_NS}}}{local}");
state.variables.bind(key, value);
}
let r = eval_body(state, &c.body, ctx_node, pos, size);
state.variables.leave();
return r;
}
Err(e)
}
}
}
fn error_to_qname(e: &XsltError) -> (QName, String) {
let local = match e {
XsltError::Xpath(xe) => xe.xpath_code.clone()
.unwrap_or_else(|| "FOER0000".to_string()),
_ => "FOER0000".to_string(),
};
let qn = QName {
prefix: Some("err".into()),
uri: ERR_NS.to_string(),
local,
};
(qn, e.to_string())
}
fn format_qname_for_err(q: &QName) -> String {
match &q.prefix {
Some(p) => format!("{p}:{}", q.local),
None => q.local.clone(),
}
}
fn catch_matches(
errors: &[crate::ast::CatchMatcher],
err_qname: &QName,
state: &EvalState,
) -> bool {
use crate::ast::CatchMatcher::*;
errors.iter().any(|m| match m {
Any => true,
LocalNameOnly(local) => err_qname.local == *local,
PrefixWildcard(prefix) => state.namespaces.resolve(prefix)
.as_deref() == Some(err_qname.uri.as_str()),
QName(q) => q.uri == err_qname.uri && q.local == err_qname.local,
})
}
fn copy_result_node(state: &mut EvalState, n: &crate::result_tree::ResultNode) {
use crate::result_tree::ResultNode;
match n {
ResultNode::Text { content, dose } => {
state.builder.push_text(content.clone(), *dose);
}
ResultNode::Element { name, namespaces, attributes, children, .. } => {
state.builder.open_element(name.clone());
for (p, u) in namespaces {
state.builder.push_namespace_decl(p.clone(), u.clone());
}
for (q, v) in attributes {
state.builder.push_attribute(q.clone(), v.clone());
}
for c in children { copy_result_node(state, c); }
state.builder.close_element();
}
ResultNode::Comment(s) => state.builder.push_comment(s.clone()),
ResultNode::ProcessingInstruction { target, data } => {
state.builder.push_pi(target.clone(), data.clone());
}
ResultNode::Attribute { name, value } => {
state.builder.push_attribute(name.clone(), value.clone());
}
}
}
fn run_template_body(
state: &mut EvalState,
template: &Template,
ctx_node: NodeId,
pos: usize,
size: usize,
args: &[(QName, Value, Option<Vec<ResultNode>>)],
) -> Result<()> {
state.variables.enter();
rtf_scope_enter(state);
let prev_current = state.xslt_current;
state.xslt_current = ctx_node;
for p in &template.params {
let key = qname_key(&p.name);
let raw = if p.tunnel {
match state.tunnel_pool.get(&key).cloned() {
Some(v) => v,
None if p.required => return Err(XsltError::InvalidStylesheet(format!(
"required tunnel param '{key}' not supplied"
))),
None => evaluate_param_default(state, p, ctx_node, pos, size)?,
}
} else if let Some((_, v, rtf)) = args.iter().find(|(n, _, _)| qname_key(n) == key) {
if let Some(nodes) = rtf {
store_rtf(state, &key, nodes.clone());
}
v.clone()
} else if p.required {
return Err(XsltError::InvalidStylesheet(format!(
"required parameter '{key}' not supplied"
)));
} else {
evaluate_param_default(state, p, ctx_node, pos, size)?
};
let value = if let Some(t) = &p.as_type {
if let Some(st) = parse_as_atomic_type(t) {
coerce_to_atomic_sequence(raw, &st, state.idx)?
} else { raw }
} else { raw };
state.variables.bind(key, value);
}
let declared = template.as_type.as_deref()
.and_then(parse_as_atomic_type)
.filter(|st| template_result_type_is_node_kind(st));
let result = if let Some(st) = declared {
use crate::result_tree::ResultBuilder;
let prev = std::mem::replace(&mut state.builder, ResultBuilder::new());
let r = eval_body(state, &template.body, ctx_node, pos, size);
let written = std::mem::replace(&mut state.builder, prev);
r.and_then(|()| {
let nodes = written.finish();
if template_result_violates_type(&nodes, &st) {
return Err(XsltError::InvalidStylesheet(format!(
"template result doesn't match the declared type {:?} \
(XTTE0505)", st.item
)));
}
for n in &nodes { copy_result_node(state, n); }
Ok(())
})
} else {
eval_body(state, &template.body, ctx_node, pos, size)
};
state.xslt_current = prev_current;
rtf_scope_leave(state);
state.variables.leave();
result
}
fn evaluate_param_default(
state: &mut EvalState, p: &Param,
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<Value> {
if let Some(sel) = &p.select {
return state.xpath_eval(sel, ctx_node, pos, size);
}
let key = qname_key(&p.name);
if p.as_type.as_deref().map(as_is_attribute_kind).unwrap_or(false) {
let nodes = build_rtf_nodes_no_merge(state, &p.body, ctx_node, pos, size)?;
let ids = rtf_children_into_index(state.idx, &nodes);
store_rtf(state, &key, nodes);
return Ok(Value::NodeSet(ids));
}
let nodes = build_rtf_nodes(state, &p.body, ctx_node, pos, size)?;
let root_id = rtf_into_index(state.idx, &nodes);
store_rtf(state, &key, nodes);
Ok(Value::NodeSet(vec![root_id]))
}
enum IterateControl {
Break,
Next(Vec<(QName, Value, Option<Vec<ResultNode>>)>),
}
thread_local! {
static ITERATE_CONTROL: std::cell::RefCell<Option<IterateControl>> =
const { std::cell::RefCell::new(None) };
static CONTEXT_UNDEFINED: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
static SCHEMA_SUPPRESSED: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
static ATOMIC_FOR_EACH_DEPTH: std::cell::Cell<u32> =
const { std::cell::Cell::new(0) };
static SECONDARY_DOCS: std::cell::RefCell<Vec<(String, Vec<ResultNode>)>> =
const { std::cell::RefCell::new(Vec::new()) };
static TEMP_OUTPUT_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
}
struct TempOutputGuard;
impl TempOutputGuard {
fn enter() -> Self {
TEMP_OUTPUT_DEPTH.with(|c| c.set(c.get() + 1));
TempOutputGuard
}
}
impl Drop for TempOutputGuard {
fn drop(&mut self) {
TEMP_OUTPUT_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
}
}
fn in_temporary_output() -> bool {
TEMP_OUTPUT_DEPTH.with(|c| c.get() > 0)
}
pub(crate) fn is_context_undefined() -> bool {
CONTEXT_UNDEFINED.with(|c| c.get())
}
#[cfg(feature = "xsd")]
fn schema_suppressed() -> bool {
SCHEMA_SUPPRESSED.with(|c| c.get())
}
pub(crate) fn in_atomic_for_each() -> bool {
ATOMIC_FOR_EACH_DEPTH.with(|c| c.get() > 0)
}
struct AtomicForEachGuard;
impl AtomicForEachGuard {
fn enter() -> Self {
ATOMIC_FOR_EACH_DEPTH.with(|c| c.set(c.get() + 1));
AtomicForEachGuard
}
}
impl Drop for AtomicForEachGuard {
fn drop(&mut self) {
ATOMIC_FOR_EACH_DEPTH.with(|c| c.set(c.get().saturating_sub(1)));
}
}
fn reset_secondary_docs() {
SECONDARY_DOCS.with(|d| d.borrow_mut().clear());
TEMP_OUTPUT_DEPTH.with(|c| c.set(0));
}
fn take_secondary_docs() -> Vec<(String, Vec<ResultNode>)> {
SECONDARY_DOCS.with(|d| std::mem::take(&mut *d.borrow_mut()))
}
fn iterate_control_active() -> bool {
ITERATE_CONTROL.with(|c| c.borrow().is_some())
}
fn take_iterate_control() -> Option<IterateControl> {
ITERATE_CONTROL.with(|c| c.borrow_mut().take())
}
fn set_iterate_control(ctrl: IterateControl) {
ITERATE_CONTROL.with(|c| *c.borrow_mut() = Some(ctrl));
}
fn iterate_select_nodes(state: &mut EvalState, v: Value) -> Result<Vec<NodeId>> {
Ok(match v {
Value::NodeSet(ns) => ns,
Value::String(s) => state.idx.allocate_rtf_text_nodes_inherent(vec![s]),
Value::Number(n) => state.idx.allocate_rtf_text_nodes_inherent(
vec![value_to_string_styled(&Value::Number(n), state.idx, state.num_style())]),
Value::Boolean(b) => state.idx.allocate_rtf_text_nodes_inherent(
vec![if b { "true".into() } else { "false".into() }]),
Value::Typed(t) => state.idx.allocate_rtf_text_nodes_inherent(vec![t.lexical]),
Value::IntRange { lo, hi } => state.idx.allocate_rtf_text_nodes_inherent(
(lo..=hi).map(|i| i.to_string()).collect()),
Value::Sequence(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
Value::IntRange { lo, hi } => {
let strings: Vec<String> = (lo..=hi).map(|i| i.to_string()).collect();
out.extend(state.idx.allocate_rtf_text_nodes_inherent(strings));
}
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
out.extend(state.idx.allocate_rtf_text_nodes_inherent(vec![s]));
}
}
}
out
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:iterate select= must yield a sequence (got {other:?})"))),
})
}
const ON_COND_NS: &str = "https://sup-xml.internal/on-conditional";
fn eval_body(
state: &mut EvalState,
body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<()> {
if body.iter().any(|i| matches!(i, Instr::OnEmpty { .. } | Instr::OnNonEmpty { .. })) {
return eval_body_conditional(state, body, ctx_node, pos, size);
}
for instr in body {
eval_instr(state, instr, ctx_node, pos, size)?;
if iterate_control_active() { break; }
}
Ok(())
}
fn eval_body_conditional(
state: &mut EvalState,
body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<()> {
let prev = std::mem::replace(&mut state.builder, ResultBuilder::new());
let r = capture_on_conditional_body(state, body, ctx_node, pos, size);
let captured = std::mem::replace(&mut state.builder, prev).finish();
r?;
let ordinary_significant = captured.iter().any(|n| {
on_cond_kind(n).is_none() && match n {
ResultNode::Text { content, .. } => !content.is_empty(),
_ => true,
}
});
for n in captured {
match on_cond_kind(&n) {
Some(is_on_empty) => {
let keep = if is_on_empty { !ordinary_significant } else { ordinary_significant };
if keep {
if let ResultNode::Element { children, attributes, .. } = n {
for (name, value) in attributes {
state.builder.push_built_node(ResultNode::Attribute { name, value });
}
for c in children { state.builder.push_built_node(c); }
}
}
}
None => state.builder.push_built_node(n),
}
}
Ok(())
}
fn capture_on_conditional_body(
state: &mut EvalState,
body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<()> {
for instr in body {
match instr {
Instr::OnEmpty { body } | Instr::OnNonEmpty { body } => {
let local = if matches!(instr, Instr::OnEmpty { .. })
{ "on-empty" } else { "on-non-empty" };
state.builder.open_element(QName {
prefix: None, local: local.to_string(), uri: ON_COND_NS.to_string() });
eval_body(state, body, ctx_node, pos, size)?;
state.builder.close_element();
}
other => eval_instr(state, other, ctx_node, pos, size)?,
}
if iterate_control_active() { break; }
}
Ok(())
}
fn on_cond_kind(n: &ResultNode) -> Option<bool> {
match n {
ResultNode::Element { name, .. } if name.uri == ON_COND_NS =>
Some(name.local == "on-empty"),
_ => None,
}
}
fn eval_instr(
state: &mut EvalState,
instr: &Instr,
ctx_node: NodeId,
pos: usize,
size: usize,
) -> Result<()> {
match instr {
Instr::LiteralElement { name, attributes, namespaces, use_attribute_sets, schema_type, body } => {
let element_name = apply_namespace_alias(state, name);
state.builder.open_element(element_name.clone());
if let Some(t) = schema_type {
state.builder.set_current_element_type(t.clone());
}
if !element_name.uri.is_empty() {
state.builder.push_namespace_decl(element_name.prefix.clone(), element_name.uri.clone());
}
for (prefix, uri) in namespaces {
let aliased = apply_namespace_alias(state, &QName {
prefix: prefix.clone(),
uri: uri.clone(),
local: String::new(),
});
state.builder.push_namespace_decl(aliased.prefix, aliased.uri);
}
apply_attribute_sets(state, use_attribute_sets, ctx_node, pos, size)?;
for (aname, avt) in attributes {
let aname = if aname.uri.is_empty() {
aname.clone()
} else {
apply_namespace_alias(state, aname)
};
let value = render_avt(state, avt, ctx_node, pos, size)?;
state.builder.push_attribute(aname.clone(), value);
if !aname.uri.is_empty() && aname.prefix.is_some() {
state.builder.push_namespace_decl(aname.prefix.clone(), aname.uri.clone());
}
}
state.variables.enter();
let r = eval_body(state, body, ctx_node, pos, size);
state.variables.leave();
r?;
state.builder.close_element();
}
Instr::LiteralText { text, dose } => {
state.builder.push_text(text.clone(), *dose);
}
Instr::ApplyTemplates { select, mode, sort, with_params, mode_current } => {
let nodes = if let Some(sel) = select {
match state.xpath_eval(sel, ctx_node, pos, size)? {
Value::NodeSet(ns) => ns,
Value::String(s) if s.is_empty() => Vec::new(),
Value::Sequence(items) => {
let mut out: Vec<NodeId> = Vec::with_capacity(items.len());
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
let ids = state.idx.allocate_rtf_text_nodes_inherent(vec![s]);
out.extend(ids);
}
}
}
out
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:apply-templates select= must yield a sequence (got {other:?})"
))),
}
} else {
if sup_xml_core::xpath::eval::focus_is_undefined() {
return Err(XsltError::InvalidStylesheet(
"xsl:apply-templates with no select= called where \
the context item is undefined (XTTE0510)".into()));
}
if in_atomic_for_each() {
return Err(XsltError::InvalidStylesheet(
"xsl:apply-templates with no select= called where \
the context item is not a node (XTTE0510)".into()));
}
let style = state.style;
let idx = state.idx;
state.idx.children(ctx_node).iter()
.copied()
.filter(|&n| !crate::whitespace::should_strip(style, n, idx))
.collect()
};
let nodes = sort_nodes_for_iter(state, &nodes, sort, ctx_node, pos, size)?;
let tunnel_save = state.tunnel_pool.clone();
let args = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
let inherited_mode: Option<QName> = if *mode_current {
state.apply_imports_ctx.as_ref()
.and_then(|(_, m, _, _, _, _)| m.clone())
} else {
None
};
let effective_mode = if *mode_current {
inherited_mode.as_ref()
} else {
mode.as_ref()
};
let total = nodes.len();
let r = (|| -> Result<()> {
for (i, child) in nodes.iter().enumerate() {
apply_one_to_node_with_args(state, *child, effective_mode, i + 1, total, &args)?;
}
Ok(())
})();
state.tunnel_pool = tunnel_save;
r?;
}
Instr::NextMatch { with_params } => {
let (node, mode, _cur_prec, cur_index, cur_prio, cur_branch) = state.apply_imports_ctx
.clone()
.ok_or_else(|| XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(
"xsl:next-match invoked when no current template rule is in scope (XTDE0560)"
).with_xpath_code("XTDE0560")
))?;
let tunnel_save = state.tunnel_pool.clone();
let args = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
let bindings = state.bindings();
let cur_index = cur_index.unwrap_or(usize::MAX);
let cur_tmpl = state.style.templates.get(cur_index);
let chosen = if let Some(t) = cur_tmpl {
let current = pattern::Selected {
template: t,
priority: cur_prio,
branch_idx: cur_branch,
};
pattern::select_template_next(
state.style, node, mode.as_ref(), state.idx, &bindings,
¤t, cur_index,
).map_err(XsltError::from)?
} else {
None
};
let inner_r: Result<()> = if let Some(sel) = chosen {
state.template_call_depth += 1;
if state.template_call_depth > MAX_TEMPLATE_CALL_DEPTH {
state.template_call_depth -= 1;
state.tunnel_pool = tunnel_save;
return Err(XsltError::InvalidStylesheet(format!(
"xsl:next-match depth exceeds limit ({MAX_TEMPLATE_CALL_DEPTH}) \
— possible infinite recursion"
)));
}
let prev = state.apply_imports_ctx.replace(
(node, mode.clone(), sel.template.import_precedence,
template_index_of(state.style, sel.template),
sel.priority, sel.branch_idx),
);
let r = run_template_body(state, sel.template, node, pos, size, &args);
state.apply_imports_ctx = prev;
state.template_call_depth -= 1;
r
} else {
apply_builtin_template_with_args(state, node, mode.as_ref(), &args)
};
state.tunnel_pool = tunnel_save;
inner_r?;
}
Instr::ApplyImports { with_params } => {
let (node, mode, cur_prec, _cur_index, _cur_prio, _cur_branch) = state.apply_imports_ctx
.clone()
.ok_or_else(|| XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(
"xsl:apply-imports invoked when no current template rule is in scope (XTDE0560)"
).with_xpath_code("XTDE0560")
))?;
let tunnel_save = state.tunnel_pool.clone();
let args = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
let bindings = state.bindings();
let chosen = pattern::select_template_max_precedence(
state.style, node, mode.as_ref(), state.idx, &bindings,
cur_prec - 1,
).map_err(XsltError::from)?;
let r: Result<()> = (|| {
if let Some(sel) = chosen {
let prev = state.apply_imports_ctx.replace(
(node, mode.clone(), sel.template.import_precedence,
template_index_of(state.style, sel.template),
sel.priority, sel.branch_idx),
);
let r = run_template_body(state, sel.template, node, pos, size, &args);
state.apply_imports_ctx = prev;
r
} else {
apply_builtin_template_with_args(state, node, mode.as_ref(), &args)
}
})();
state.tunnel_pool = tunnel_save;
r?;
}
Instr::CallTemplate { name, with_params } => {
let key = qname_key(name);
let template = state.style.templates.iter()
.find(|t| t.name.as_ref().map(qname_key).as_deref() == Some(key.as_str()))
.ok_or_else(|| XsltError::UnresolvedReference(format!(
"no template named '{key}'"
)))?;
let tunnel_save = state.tunnel_pool.clone();
let args = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
state.template_call_depth += 1;
if state.template_call_depth > MAX_TEMPLATE_CALL_DEPTH {
state.template_call_depth -= 1;
state.tunnel_pool = tunnel_save;
return Err(XsltError::InvalidStylesheet(format!(
"xsl:call-template depth exceeds limit ({MAX_TEMPLATE_CALL_DEPTH}) \
— likely infinite recursion in template '{key}'"
)));
}
let r = run_template_body(state, template, ctx_node, pos, size, &args);
state.template_call_depth -= 1;
state.tunnel_pool = tunnel_save;
r?;
}
Instr::Choose { whens, otherwise } => {
for (test, body) in whens {
let v = state.xpath_eval(test, ctx_node, pos, size)?;
if value_to_bool(&v) {
eval_body(state, body, ctx_node, pos, size)?;
return Ok(());
}
}
if let Some(body) = otherwise {
eval_body(state, body, ctx_node, pos, size)?;
}
}
Instr::If { test, body } => {
let v = state.xpath_eval(test, ctx_node, pos, size)?;
if value_to_bool(&v) {
eval_body(state, body, ctx_node, pos, size)?;
}
}
Instr::PerformSort { select, sort, body } => {
match select {
Some(e) => match state.xpath_eval(e, ctx_node, pos, size)? {
Value::NodeSet(ns) => {
let sorted = sort_nodes_for_iter(state, &ns, sort, ctx_node, pos, size)?;
copy_value_into(state, &Value::NodeSet(sorted), true)?;
}
Value::Sequence(items) => {
let sorted = sort_items_for_iter(state, items, sort, ctx_node)?;
for item in sorted {
copy_value_into(state, &item, true)?;
}
}
Value::IntRange { lo, hi } => {
let items: Vec<Value> = (lo..=hi)
.map(|i| Value::Number(Numeric::Integer(i)))
.collect();
let sorted = sort_items_for_iter(state, items, sort, ctx_node)?;
for item in sorted {
copy_value_into(state, &item, true)?;
}
}
other @ (Value::Number(_) | Value::String(_)
| Value::Boolean(_) | Value::Typed(_)) => {
copy_value_into(state, &other, true)?;
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:perform-sort select= must yield a sequence \
(got {other:?})"
))),
},
None => {
let result_nodes = build_rtf_nodes_no_merge(state, body, ctx_node, pos, size)?;
let nodes = rtf_children_into_index(state.idx, &result_nodes);
let sorted = sort_nodes_for_iter(state, &nodes, sort, ctx_node, pos, size)?;
copy_value_into(state, &Value::NodeSet(sorted), true)?;
}
}
}
Instr::Document { body } => {
if state.sequence_sink_active() {
let children = build_rtf_nodes_no_merge(
state, body, ctx_node, pos, size,
)?;
let doc_id = rtf_into_index(state.idx, &children);
state.push_to_sequence_sink(Value::NodeSet(vec![doc_id]));
} else {
let prev_principal = state.builder.is_principal_document;
state.builder.is_principal_document = true;
let r = eval_body(state, body, ctx_node, pos, size);
state.builder.is_principal_document = prev_principal;
r?;
}
}
Instr::ResultDocument { href, format, format_namespaces, body } => {
use crate::result_tree::ResultBuilder;
if let Some(fmt_avt) = format {
let fmt = render_avt(state, fmt_avt, ctx_node, pos, size)?;
let fmt = fmt.trim();
if let Some((prefix, _)) = fmt.split_once(':') {
let bound = format_namespaces.iter().any(|(p, _)|
p.as_deref() == Some(prefix));
if !bound {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:result-document format='{fmt}' references \
undeclared prefix '{prefix}' (XTDE1460)")));
}
}
}
if in_temporary_output() {
return Err(XsltError::InvalidStylesheet(
"xsl:result-document is not allowed while writing to a \
temporary output destination (XTDE1480)".into()));
}
let uri = render_avt(state, href, ctx_node, pos, size)?;
if uri.is_empty() {
if let Some(principal) = state.principal_buf.take() {
let secondary = std::mem::replace(&mut state.builder, principal);
if !state.builder.is_empty() {
state.principal_buf =
Some(std::mem::replace(&mut state.builder, secondary));
return Err(XsltError::InvalidStylesheet(
"xsl:result-document targets the principal output URI, \
which already has content (XTRE1495)".into()));
}
let r = eval_body(state, body, ctx_node, pos, size);
state.principal_buf =
Some(std::mem::replace(&mut state.builder, secondary));
return r;
}
if !state.builder.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:result-document targets the principal output URI, \
which already has content (XTRE1495)".into()));
}
eval_body(state, body, ctx_node, pos, size)?;
return Ok(());
}
if SECONDARY_DOCS.with(|d| d.borrow().iter().any(|(u, _)| *u == uri)) {
return Err(XsltError::InvalidStylesheet(format!(
"two xsl:result-document instructions write to the same \
URI '{uri}' (XTRE1495)")));
}
let parent_active = std::mem::replace(&mut state.builder, ResultBuilder::new());
let outer_secondary = if state.principal_buf.is_none() {
state.principal_buf = Some(parent_active);
None
} else {
Some(parent_active)
};
let r = eval_body(state, body, ctx_node, pos, size);
let restored = outer_secondary
.unwrap_or_else(|| state.principal_buf.take().expect("principal stashed"));
let written = std::mem::replace(&mut state.builder, restored);
r?;
SECONDARY_DOCS.with(|d| d.borrow_mut().push((uri, written.finish())));
}
Instr::Try { body, catches } => {
run_try_instr(state, body, catches, ctx_node, pos, size)?;
}
Instr::Namespace { name, select, body } => {
let prefix = render_avt(state, name, ctx_node, pos, size)?;
let uri = match select {
Some(e) => value_to_string_styled(
&state.xpath_eval(e, ctx_node, pos, size)?, state.idx, state.num_style()),
None => {
use crate::result_tree::ResultBuilder;
let prev = std::mem::replace(
&mut state.builder, ResultBuilder::new()
);
let r = eval_body(state, body, ctx_node, pos, size);
let nested = std::mem::replace(&mut state.builder, prev);
r?;
stringify(&nested.finish())
}
};
if prefix == "xmlns" || (!prefix.is_empty() && !is_ncname_str(&prefix)) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:namespace name='{prefix}' is not a valid namespace \
prefix (XTDE0920)")));
}
const XML_NS: &str = "http://www.w3.org/XML/1998/namespace";
if (prefix == "xml") != (uri == XML_NS) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:namespace binding of prefix '{prefix}' to '{uri}' \
conflicts with the reserved xml namespace (XTDE0925)")));
}
if prefix.is_empty() && !uri.is_empty()
&& state.builder.current_element_uri() == Some("") {
return Err(XsltError::InvalidStylesheet(
"xsl:namespace declares a default namespace on an element \
that is in no namespace (XTDE0440)".into()));
}
if !prefix.is_empty() && uri.is_empty() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:namespace name='{prefix}' bound to the empty URI \
(XTDE0930)"
)));
}
if uri == "http://www.w3.org/2000/xmlns/" {
return Err(XsltError::InvalidStylesheet(
"xsl:namespace URI 'http://www.w3.org/2000/xmlns/' \
is reserved and may not be declared (XTDE0905)".into()));
}
let prefix_opt = if prefix.is_empty() { None } else { Some(prefix) };
state.builder.push_namespace_decl_explicit(prefix_opt, uri);
}
Instr::AnalyzeString { select, regex, flags, matching, non_matching } => {
let input_v = state.xpath_eval(select, ctx_node, pos, size)?;
match &input_v {
Value::String(_) => {}
Value::Typed(t) if matches!(t.kind,
"string" | "untypedAtomic" | "anyURI"
| "normalizedString" | "token" | "Name" | "NCName"
| "language" | "ID" | "IDREF" | "ENTITY" | "NMTOKEN") => {}
Value::Sequence(items) if items.is_empty() => return Err(
XsltError::InvalidStylesheet(
"xsl:analyze-string select= must yield a single \
string (got empty sequence — XSLT 2.1+ relaxes this \
to '' but XSLT 2.0 is XPTY0004)".into())),
Value::Sequence(items) if items.len() == 1 => {}
Value::Sequence(items) => return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string select= must yield a single string \
(got {}-item sequence) (XPTY0004)", items.len()
))),
Value::NodeSet(ns) if ns.is_empty() => return Err(
XsltError::InvalidStylesheet(
"xsl:analyze-string select= must yield a single \
string (got empty node-set — XSLT 2.1+ relaxes this \
to '' but XSLT 2.0 is XPTY0004)".into())),
Value::NodeSet(ns) if ns.len() == 1 => {}
Value::NodeSet(ns) => return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string select= must yield a single string \
(got {}-node set) (XPTY0004)", ns.len()
))),
_ => return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string select= must yield a string \
(got {input_v:?}) (XPTY0004)"
))),
}
let input = value_to_string_styled(&input_v, state.idx, state.num_style());
let pattern = render_avt(state, regex, ctx_node, pos, size)?;
let flag_s = render_avt(state, flags, ctx_node, pos, size)?;
let allow_q = crate::functions::xslt_version_3_or_more(&state.style.version);
if !allow_q && pattern.contains("(?") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string regex='{pattern}' uses a `(?…)` \
construct not allowed in XSLT 2.0 (XTDE1145)"
)));
}
for ch in flag_s.chars() {
let ok = matches!(ch, 's' | 'm' | 'i' | 'x')
|| (allow_q && ch == 'q');
if !ok {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string flags='{flag_s}' contains \
unrecognised flag '{ch}' (XTDE1145)"
)));
}
}
if let Ok(probe) = sup_xml_core::regex::Pattern::compile_with(
&pattern, sup_xml_core::regex::Dialect::Xpath,
) {
if probe.is_match("") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string regex='{pattern}' matches the \
zero-length string (XTDE1150)"
)));
}
}
let uses_captures = matching_body_uses_regex_group(matching);
let has_reluctant = regex_has_reluctant_quantifier(&pattern);
let mut segments: Vec<(bool, String, Vec<String>)> = Vec::new();
if flag_s.is_empty() && !uses_captures && !has_reluctant {
if let Ok(p) = sup_xml_core::regex::Pattern::compile_with(
&pattern, sup_xml_core::regex::Dialect::Xpath,
) {
let matches = p.find_iter(&input);
let mut cursor = 0usize;
for (start, end) in matches {
if start > cursor {
segments.push((false, input[cursor..start].to_string(), Vec::new()));
}
if end > start {
let m_str = input[start..end].to_string();
let groups = vec![m_str.clone()];
segments.push((true, m_str, groups));
}
cursor = end;
}
if cursor < input.len() {
segments.push((false, input[cursor..].to_string(), Vec::new()));
}
return run_analyze_partition(
state, matching, non_matching, &segments, ctx_node);
}
}
let re = sup_xml_core::xpath::compile_xpath_2_0_regex(&pattern, &flag_s)
.map_err(XsltError::from)?;
let mut cursor = 0usize;
for cap in re.captures_iter(&input) {
let m = cap.get(0).unwrap();
if m.start() > cursor {
segments.push((false, input[cursor..m.start()].to_string(), Vec::new()));
}
let groups: Vec<String> = (0..cap.len())
.map(|i| cap.get(i).map(|m| m.as_str().to_string()).unwrap_or_default())
.collect();
segments.push((true, m.as_str().to_string(), groups));
cursor = m.end();
}
if cursor < input.len() {
segments.push((false, input[cursor..].to_string(), Vec::new()));
}
run_analyze_partition(state, matching, non_matching, &segments, ctx_node)?;
}
Instr::ForEachGroup { select, kind, key, sort, body, collation } => {
use crate::ast::GroupingKind;
let select_val = state.xpath_eval(select, ctx_node, pos, size)?;
let pattern_grouping = matches!(kind,
GroupingKind::StartingWith | GroupingKind::EndingWith);
if pattern_grouping {
let all_nodes = match &select_val {
Value::NodeSet(_) | Value::ForeignNodeSet(_) => true,
Value::Sequence(items) => items.iter().all(|v| matches!(v,
Value::NodeSet(_) | Value::ForeignNodeSet(_))),
_ => false,
};
if !all_nodes {
return Err(XsltError::InvalidStylesheet(
"xsl:for-each-group group-starting-with / \
group-ending-with requires every item to be a \
node (XTTE1120)".into()));
}
}
let nodes = match select_val {
Value::NodeSet(ns) => ns,
Value::Sequence(items) => {
let mut out: Vec<NodeId> = Vec::with_capacity(items.len());
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
let ids = state.idx
.allocate_rtf_text_nodes_inherent(vec![s]);
out.extend(ids);
}
}
}
out
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:for-each-group select= must yield a sequence \
(got {other:?})"
))),
};
let collation_fold = |s: &str| -> String {
let ci = matches!(collation.as_deref(),
Some("http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive"));
if ci {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii_uppercase() { out.push(c.to_ascii_lowercase()); }
else { out.push(c); }
}
out
} else { s.to_string() }
};
let groups: Vec<(Value, Vec<NodeId>)> = match kind {
GroupingKind::By => {
let mut key_order: Vec<String> = Vec::new();
let mut buckets: std::collections::HashMap<String, (Value, Vec<NodeId>)>
= std::collections::HashMap::new();
for (i, &n) in nodes.iter().enumerate() {
state.xslt_current = n;
let kv = state.xpath_eval(key, n, i + 1, nodes.len())?;
let mut seen = std::collections::HashSet::new();
for key_item in grouping_key_items(&kv) {
let k_str = value_equality_key(&key_item).unwrap_or_else(|| collation_fold(
&value_to_string_styled(&key_item, state.idx, state.num_style())));
if !seen.insert(k_str.clone()) { continue; }
if !buckets.contains_key(&k_str) {
key_order.push(k_str.clone());
}
buckets.entry(k_str)
.or_insert_with(|| (key_item, Vec::new()))
.1.push(n);
}
}
key_order.into_iter().map(|k| buckets.remove(&k).unwrap()).collect()
}
GroupingKind::Adjacent => {
let mut out: Vec<(Value, Vec<NodeId>)> = Vec::new();
let mut prev_key: Option<String> = None;
for (i, &n) in nodes.iter().enumerate() {
state.xslt_current = n;
let kv = state.xpath_eval(key, n, i + 1, nodes.len())?;
let item_count = match &kv {
Value::NodeSet(ns) => ns.len(),
Value::Sequence(items) => items.len(),
_ => 1,
};
if item_count != 1 {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:for-each-group group-adjacent= must yield \
exactly one item per source item (got {item_count} \
items) (XTTE1100)"
)));
}
let k_str = value_equality_key(&kv).unwrap_or_else(|| collation_fold(
&value_to_string_styled(&kv, state.idx, state.num_style())));
if Some(&k_str) == prev_key.as_ref() {
out.last_mut().unwrap().1.push(n);
} else {
out.push((kv, vec![n]));
prev_key = Some(k_str);
}
}
out
}
GroupingKind::StartingWith => {
let mut out: Vec<(Value, Vec<NodeId>)> = Vec::new();
let bindings = state.bindings();
for &n in nodes.iter() {
let starts = pattern::matches(
key, n, state.idx, &bindings,
).map_err(XsltError::from)?;
if starts || out.is_empty() {
out.push((Value::NodeSet(Vec::new()), vec![n]));
} else {
out.last_mut().unwrap().1.push(n);
}
}
out
}
GroupingKind::EndingWith => {
let mut out: Vec<(Value, Vec<NodeId>)> = Vec::new();
let mut current: Vec<NodeId> = Vec::new();
let bindings = state.bindings();
for &n in nodes.iter() {
current.push(n);
let ends = pattern::matches(
key, n, state.idx, &bindings,
).map_err(XsltError::from)?;
if ends {
out.push((Value::NodeSet(Vec::new()),
std::mem::take(&mut current)));
}
}
if !current.is_empty() {
out.push((Value::NodeSet(Vec::new()), current));
}
out
}
};
let group_order: Vec<usize> = sort_group_indices(state, &groups, sort)?;
state.variables.enter();
let prev_current = state.xslt_current;
let prev_group = std::mem::take(&mut state.current_group);
let prev_key = state.current_grouping_key.take();
let total = groups.len();
for (i, gi) in group_order.iter().enumerate() {
let (k, ns) = &groups[*gi];
state.current_group = ns.clone();
state.current_grouping_key = Some(k.clone());
let leader = *ns.first().unwrap_or(&ctx_node);
state.xslt_current = leader;
eval_body(state, body, leader, i + 1, total)?;
}
state.xslt_current = prev_current;
state.current_group = prev_group;
state.current_grouping_key = prev_key;
state.variables.leave();
}
Instr::OnEmpty { body } | Instr::OnNonEmpty { body } => {
eval_body(state, body, ctx_node, pos, size)?;
}
Instr::WherePopulated { body } => {
let nodes = build_rtf_nodes(state, body, ctx_node, pos, size)?;
if nodes.iter().any(result_node_is_significant) {
for n in nodes { state.builder.push_built_node(n); }
}
}
Instr::Fork { body } => {
eval_body(state, body, ctx_node, pos, size)?;
}
Instr::SourceDocument { href, body } => {
let uri = render_avt(state, href, ctx_node, pos, size)?;
let root = match state.documents.and_then(|d| d.get(&uri).copied()) {
Some(id) => id,
None => match state.bindings().load_dynamic_document(&uri) {
Some(Ok(id)) => id,
_ => return Err(XsltError::InvalidStylesheet(format!(
"xsl:source-document: cannot load {uri:?} (FODC0002)"))),
},
};
let prev_current = state.xslt_current;
state.xslt_current = root;
eval_body(state, body, root, 1, 1)?;
state.xslt_current = prev_current;
}
Instr::Evaluate { xpath, context_item, with_params, schema_aware } => {
let xpath_str = value_to_string_styled(
&state.xpath_eval(xpath, ctx_node, pos, size)?, state.idx, state.num_style());
let opts = sup_xml_core::xpath::XPathOptions {
xpath_2_0: true, libxml2_compatible: false,
..sup_xml_core::xpath::XPathOptions::default()
};
let expr = sup_xml_core::xpath::parse_xpath_with(&xpath_str, &opts)
.map_err(|e| XsltError::InvalidStylesheet(format!(
"xsl:evaluate: invalid XPath {xpath_str:?}: {} (XTDE3160)", e.message)))?;
let cnode = match context_item {
Some(ce) => match state.xpath_eval(ce, ctx_node, pos, size)? {
Value::NodeSet(ns) if !ns.is_empty() => ns[0],
_ => ctx_node,
},
None => ctx_node,
};
let bound = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
state.variables.enter();
for (name, value, _) in &bound {
state.variables.bind(qname_key(name), value.clone());
}
let prev_suppress = SCHEMA_SUPPRESSED.with(|c| c.replace(!schema_aware));
let result = state.xpath_eval(&expr, cnode, 1, 1);
SCHEMA_SUPPRESSED.with(|c| c.set(prev_suppress));
state.variables.leave();
let v = result?;
if state.sequence_sink_active() {
state.push_to_sequence_sink(v);
} else {
copy_value_into(state, &v, true)?;
}
}
Instr::Merge { sources, action } => {
let mut tagged: Vec<(usize, NodeId)> = Vec::new();
for (si, src) in sources.iter().enumerate() {
let contexts: Vec<NodeId> = match &src.for_each_source {
None => vec![ctx_node],
Some(fes) => {
let v = state.xpath_eval(fes, ctx_node, pos, size)?;
merge_materialize_nodes(state, v)
}
};
let nc = contexts.len();
for (ci, c) in contexts.into_iter().enumerate() {
let v = state.xpath_eval(&src.select, c, ci + 1, nc)?;
tagged.extend(merge_materialize_nodes(state, v).into_iter().map(|n| (si, n)));
}
}
let nodes: Vec<NodeId> = tagged.iter().map(|(_, n)| *n).collect();
let per_node: Vec<&[crate::ast::Sort]> =
tagged.iter().map(|(si, _)| sources[*si].keys.as_slice()).collect();
let order = merge_sort_order(state, &nodes, &per_node)?;
let sorted: Vec<(usize, NodeId)> = order.into_iter().map(|i| tagged[i]).collect();
let mut groups: Vec<(Value, Vec<NodeId>)> = Vec::new();
let mut prev_gk: Option<String> = None;
for (si, n) in &sorted {
let (si, n) = (*si, *n);
let vals = merge_key_values(state, &sources[si].keys, n)?;
let gk = merge_group_key_str(&vals, state);
let key_value = if vals.len() == 1 {
vals.into_iter().next().unwrap()
} else {
Value::Sequence(vals)
};
if prev_gk.as_ref() == Some(&gk) {
groups.last_mut().unwrap().1.push(n);
} else {
groups.push((key_value, vec![n]));
prev_gk = Some(gk);
}
}
state.variables.enter();
let prev_current = state.xslt_current;
let prev_group = std::mem::take(&mut state.current_group);
let prev_key = state.current_grouping_key.take();
let total = groups.len();
for (i, (k, ns)) in groups.iter().enumerate() {
state.current_group = ns.clone();
state.current_grouping_key = Some(k.clone());
let leader = *ns.first().unwrap_or(&ctx_node);
state.xslt_current = leader;
eval_body(state, action, leader, i + 1, total)?;
}
state.xslt_current = prev_current;
state.current_group = prev_group;
state.current_grouping_key = prev_key;
state.variables.leave();
}
Instr::ForEach { select, sort, body } => {
let v = state.xpath_eval(select, ctx_node, pos, size)?;
if !sort.is_empty() {
if let Value::Sequence(items) = &v {
if !items.is_empty()
&& items.iter().all(|it| matches!(it,
Value::Typed(_) | Value::String(_)
| Value::Number(_) | Value::Boolean(_)))
{
return run_for_each_typed_sequence(
state, items.clone(), sort, body,
ctx_node, pos, size);
}
}
}
let is_fn_item = |it: &Value| matches!(it,
Value::Function(_) | Value::Map(_) | Value::Array(_));
if is_fn_item(&v)
|| matches!(&v, Value::Sequence(items) if items.iter().any(is_fn_item))
{
let items: Vec<Value> = match v {
Value::Sequence(items) => items,
other => vec![other],
};
let items = sort_items_for_iter(state, items, sort, ctx_node)?;
state.variables.enter();
let prev_current = state.xslt_current;
let prev_apply_imports = state.apply_imports_ctx.take();
let total = items.len();
let mut result = Ok(());
for (i, item) in items.into_iter().enumerate() {
result = sup_xml_core::xpath::eval::with_atomic_context_item(
Some(item), || eval_body(state, body, ctx_node, i + 1, total));
if result.is_err() { break; }
}
state.xslt_current = prev_current;
state.apply_imports_ctx = prev_apply_imports;
state.variables.leave();
return result;
}
let select_is_atomic = !matches!(&v,
Value::NodeSet(_) | Value::ForeignNodeSet(_));
let nodes = match v {
Value::NodeSet(ns) => ns,
Value::String(s) => {
state.idx.allocate_rtf_text_nodes_inherent(vec![s])
}
Value::Number(n) => {
state.idx.allocate_rtf_text_nodes_inherent(
vec![value_to_string_styled(&Value::Number(n), state.idx, state.num_style())])
}
Value::Boolean(b) => {
state.idx.allocate_rtf_text_nodes_inherent(
vec![if b { "true".into() } else { "false".into() }])
}
Value::Typed(t) => {
state.idx.allocate_rtf_text_nodes_inherent(vec![t.lexical])
}
Value::Sequence(items) => {
let mut out: Vec<NodeId> = Vec::with_capacity(items.len());
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
Value::IntRange { lo, hi } => {
let strings: Vec<String> = (lo..=hi).map(|i| i.to_string()).collect();
let ids = state.idx.allocate_rtf_text_nodes_inherent(strings);
out.extend(ids);
}
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
let ids = state.idx.allocate_rtf_text_nodes_inherent(vec![s]);
out.extend(ids);
}
}
}
out
}
Value::IntRange { lo, hi } => {
let strings: Vec<String> = (lo..=hi).map(|i| i.to_string()).collect();
state.idx.allocate_rtf_text_nodes_inherent(strings)
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:for-each select= must yield a sequence (got {other:?})"
))),
};
let nodes = sort_nodes_for_iter(state, &nodes, sort, ctx_node, pos, size)?;
state.variables.enter();
let prev_current = state.xslt_current;
let prev_apply_imports = state.apply_imports_ctx.take();
let total = nodes.len();
let _atomic_guard = select_is_atomic.then(AtomicForEachGuard::enter);
for (i, child) in nodes.iter().enumerate() {
state.xslt_current = *child;
sup_xml_core::xpath::eval::with_atomic_context_item(
None, || eval_body(state, body, *child, i + 1, total))?;
}
state.xslt_current = prev_current;
state.apply_imports_ctx = prev_apply_imports;
state.variables.leave();
}
Instr::Iterate { select, params, on_completion, body } => {
let v = state.xpath_eval(select, ctx_node, pos, size)?;
let nodes = iterate_select_nodes(state, v)?;
let mut current: HashMap<String, Value> = HashMap::new();
for p in params {
let raw = evaluate_param_default(state, p, ctx_node, pos, size)?;
let val = match &p.as_type {
Some(t) => match parse_as_atomic_type(t) {
Some(st) => coerce_to_atomic_sequence(raw, &st, state.idx)?,
None => raw,
},
None => raw,
};
current.insert(qname_key(&p.name), val);
}
let prev_current = state.xslt_current;
let total = nodes.len();
let mut broke = false;
for (i, child) in nodes.iter().enumerate() {
state.variables.enter();
for p in params {
let key = qname_key(&p.name);
if let Some(v) = current.get(&key) {
state.variables.bind(key, v.clone());
}
}
state.xslt_current = *child;
let r = eval_body(state, body, *child, i + 1, total);
state.variables.leave();
let ctrl = take_iterate_control();
r?;
match ctrl {
Some(IterateControl::Break) => { broke = true; break; }
Some(IterateControl::Next(args)) => {
for (name, val, _rtf) in args {
let key = qname_key(&name);
let val = params.iter()
.find(|p| qname_key(&p.name) == key)
.and_then(|p| p.as_type.as_deref())
.and_then(parse_as_atomic_type)
.map(|st| coerce_to_atomic_sequence(val.clone(), &st, state.idx))
.transpose()?
.unwrap_or(val);
current.insert(key, val);
}
}
None => {} }
}
state.xslt_current = prev_current;
if !broke && !on_completion.is_empty() {
state.variables.enter();
for p in params {
let key = qname_key(&p.name);
if let Some(v) = current.get(&key) {
state.variables.bind(key, v.clone());
}
}
eval_body(state, on_completion, ctx_node, pos, size)?;
state.variables.leave();
}
}
Instr::NextIteration { with_params } => {
let args = evaluate_with_params(state, with_params, ctx_node, pos, size)?;
set_iterate_control(IterateControl::Next(args));
}
Instr::Break { select, body } => {
match select {
Some(sel) => {
let v = state.xpath_eval(sel, ctx_node, pos, size)?;
copy_value_into(state, &v, true)?;
}
None => eval_body(state, body, ctx_node, pos, size)?,
}
set_iterate_control(IterateControl::Break);
}
Instr::ValueOf { select, dose, separator } => {
let v = state.xpath_eval(select, ctx_node, pos, size)?;
let text = match separator {
Some(sep_avt) => {
let sep = render_avt(state, sep_avt, ctx_node, pos, size)?;
let pieces = sequence_string_items(&v, state.idx, state.num_style());
pieces.join(&sep)
}
None => value_to_string_styled(&v, state.idx, state.num_style()),
};
state.builder.push_text(text, *dose);
}
Instr::ValueOfBody { body, dose, separator } => {
let sep = match separator {
Some(avt) => render_avt(state, avt, ctx_node, pos, size)?,
None => String::new(),
};
let pieces = collect_value_of_body_pieces(state, body, ctx_node, pos, size)?;
let text = pieces.join(&sep);
state.builder.push_text(text, *dose);
}
Instr::Copy { use_attribute_sets, body, copy_namespaces } => {
match state.idx.kind(ctx_node) {
XPathNodeKind::Element => {
let q = element_qname(state, ctx_node);
state.builder.open_element(q.clone());
if *copy_namespaces {
for ns_id in state.idx.ns_range(ctx_node) {
let prefix = state.idx.local_name(ns_id);
let uri = state.idx.string_value(ns_id);
if prefix == "xml" { continue; }
let p_opt = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
state.builder.push_namespace_decl(p_opt, uri);
}
} else if !q.uri.is_empty() {
state.builder.push_namespace_decl(q.prefix.clone(), q.uri.clone());
}
apply_attribute_sets(state, use_attribute_sets, ctx_node, pos, size)?;
eval_body(state, body, ctx_node, pos, size)?;
state.builder.close_element();
}
XPathNodeKind::Text | XPathNodeKind::CData => {
state.builder.push_text(state.idx.string_value(ctx_node), false);
}
XPathNodeKind::Attribute => {
let q = attribute_qname(state, ctx_node);
state.builder.push_attribute(q, state.idx.string_value(ctx_node));
}
XPathNodeKind::Comment => {
state.builder.push_comment(state.idx.string_value(ctx_node));
}
XPathNodeKind::PI => {
state.builder.push_pi(
state.idx.pi_target(ctx_node).to_string(),
state.idx.string_value(ctx_node),
);
}
XPathNodeKind::Document => {
if state.sequence_sink_active() {
let children = build_rtf_nodes_no_merge(
state, body, ctx_node, pos, size,
)?;
check_document_node_content(&children)?;
let doc_id = rtf_into_index(state.idx, &children);
state.push_to_sequence_sink(Value::NodeSet(vec![doc_id]));
} else {
let children = build_rtf_nodes(
state, body, ctx_node, pos, size,
)?;
check_document_node_content(&children)?;
for child in children {
state.builder.push_built_node(child);
}
}
}
XPathNodeKind::Namespace => {
let prefix = state.idx.local_name(ctx_node);
if prefix != "xml" {
let uri = state.idx.string_value(ctx_node);
let p = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
state.builder.push_namespace_decl(p, uri);
}
}
}
}
Instr::CopyOf { select, copy_namespaces } => {
if let sup_xml_core::xpath::Expr::Variable(name) = select {
if let Some(nodes) = state.rtfs.get(name).cloned() {
for n in nodes { copy_result_node_into(state, &n); }
return Ok(());
}
}
let v = state.xpath_eval(select, ctx_node, pos, size)?;
if state.sequence_sink_active() {
if let Value::NodeSet(ns) = &v {
let all_docs = !ns.is_empty() && ns.iter().all(|&id|
matches!(state.idx.kind(id), XPathNodeKind::Document));
if all_docs {
for &id in ns {
state.push_to_sequence_sink(Value::NodeSet(vec![id]));
}
return Ok(());
}
}
if let Value::Sequence(items) = &v {
let all_doc_nodesets = !items.is_empty() && items.iter().all(|it| {
if let Value::NodeSet(ns) = it {
ns.len() == 1 && matches!(state.idx.kind(ns[0]),
XPathNodeKind::Document)
} else { false }
});
if all_doc_nodesets {
for it in items {
state.push_to_sequence_sink(it.clone());
}
return Ok(());
}
}
}
copy_value_into(state, &v, *copy_namespaces)?;
}
Instr::Element { name, namespace, body, use_attribute_sets, in_scope_namespaces } => {
let name_str = render_avt(state, name, ctx_node, pos, size)?;
if name_str.trim().is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:element name= must yield a non-empty QName (XTDE0820)".into()
));
}
if !is_lexical_qname_str(&name_str) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:element name='{name_str}' is not a valid QName (XTDE0820)"
)));
}
let explicit_namespace = namespace.is_some();
let ns_uri = match namespace {
Some(avt) => render_avt(state, avt, ctx_node, pos, size)?,
None => String::new(),
};
if explicit_namespace && ns_uri == "http://www.w3.org/2000/xmlns/" {
return Err(XsltError::InvalidStylesheet(
"xsl:element namespace='http://www.w3.org/2000/xmlns/' \
is reserved (XTDE0835)".into()));
}
let lookup_local = |target: Option<&str>| -> Option<String> {
in_scope_namespaces.iter()
.find(|(p, _)| p.as_deref() == target)
.map(|(_, u)| u.clone())
};
let (mut prefix, local) = split_qname(&name_str);
if explicit_namespace && ns_uri.is_empty() {
prefix = None;
}
let resolved_uri = if explicit_namespace {
ns_uri
} else if let Some((p, _)) = name_str.split_once(':') {
match lookup_local(Some(p)).or_else(|| state.namespaces.resolve(p)) {
Some(u) => u,
None => return Err(XsltError::InvalidStylesheet(format!(
"xsl:element name='{name_str}' uses prefix '{p}', which is \
not a declared namespace (XTDE0830)"))),
}
} else {
lookup_local(None).unwrap_or_default()
};
let q = QName { prefix, local, uri: resolved_uri };
state.builder.open_element(q.clone());
if !q.uri.is_empty() {
state.builder.push_namespace_decl(q.prefix.clone(), q.uri.clone());
}
apply_attribute_sets(state, use_attribute_sets, ctx_node, pos, size)?;
state.variables.enter();
let body_r = eval_body(state, body, ctx_node, pos, size);
state.variables.leave();
body_r?;
state.builder.close_element();
}
Instr::Attribute { name, namespace, select, separator, body, in_scope_namespaces, schema_type } => {
let name_str = render_avt(state, name, ctx_node, pos, size)?;
if name_str.trim().is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:attribute name= must yield a non-empty QName (XTDE0850)".into()
));
}
if !is_lexical_qname_str(&name_str) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:attribute name='{name_str}' is not a valid QName (XTDE0850)"
)));
}
let explicit_namespace = namespace.is_some();
if !explicit_namespace && name_str == "xmlns" {
return Err(XsltError::InvalidStylesheet(
"xsl:attribute with name='xmlns' and no namespace= \
(XTDE0855)".into()));
}
if explicit_namespace
&& namespace.is_some()
{
let ns_str = render_avt(state, namespace.as_ref().unwrap(),
ctx_node, pos, size)?;
if ns_str == "http://www.w3.org/2000/xmlns/" {
return Err(XsltError::InvalidStylesheet(
"xsl:attribute namespace='http://www.w3.org/2000/xmlns/' \
is reserved (XTDE0865)".into()));
}
}
let ns_uri = match namespace {
Some(avt) => render_avt(state, avt, ctx_node, pos, size)?,
None => String::new(),
};
let sep = match separator {
Some(avt) => render_avt(state, avt, ctx_node, pos, size)?,
None => if select.is_some() { " ".to_string() } else { String::new() },
};
let value = if let Some(sel) = select {
let v = state.xpath_eval(sel, ctx_node, pos, size)?;
let items = sequence_string_items(&v, state.idx, state.num_style());
if items.len() <= 1 {
value_to_string_styled(&v, state.idx, state.num_style())
} else {
items.join(&sep)
}
} else {
collect_value_of_body_pieces(state, body, ctx_node, pos, size)?
.join(&sep)
};
let (mut prefix, local) = split_qname(&name_str);
if explicit_namespace && prefix.as_deref() == Some("xmlns") {
prefix = None;
}
if explicit_namespace && ns_uri.is_empty() {
prefix = None;
}
let lookup_local = |target: Option<&str>| -> Option<String> {
in_scope_namespaces.iter()
.find(|(p, _)| p.as_deref() == target)
.map(|(_, u)| u.clone())
};
let resolved_uri = if explicit_namespace {
ns_uri
} else if let Some((p, _)) = name_str.split_once(':') {
lookup_local(Some(p))
.or_else(|| state.namespaces.resolve(p))
.or_else(|| (p == "xml")
.then(|| "http://www.w3.org/XML/1998/namespace".to_string()))
.ok_or_else(|| XsltError::InvalidStylesheet(format!(
"xsl:attribute name='{name_str}' uses undeclared \
prefix '{p}' (XTDE0860)"
)))?
} else {
String::new()
};
let aq = QName { prefix: prefix.clone(), local, uri: resolved_uri };
if !aq.uri.is_empty() && aq.prefix.is_some() {
state.builder.push_namespace_decl(aq.prefix.clone(), aq.uri.clone());
}
if let Some(t) = schema_type {
state.builder.set_current_attr_type(aq.clone(), t.clone());
}
state.builder.push_attribute(aq, value);
}
Instr::Comment { select, body } => {
let raw = match select {
Some(sel) => {
let v = state.xpath_eval(sel, ctx_node, pos, size)?;
let items = sequence_string_items(&v, state.idx, state.num_style());
if items.len() <= 1 { value_to_string_styled(&v, state.idx, state.num_style()) }
else { items.join(" ") }
}
None => collect_value_of_body_pieces(state, body, ctx_node, pos, size)?
.join(" "),
};
let mut s = String::with_capacity(raw.len());
let mut prev_hyphen = false;
for ch in raw.chars() {
if ch == '-' && prev_hyphen { s.push(' '); }
s.push(ch);
prev_hyphen = ch == '-';
}
if prev_hyphen { s.push(' '); }
state.builder.push_comment(s);
}
Instr::ProcessingInstruction { name, select, body } => {
let target = render_avt(state, name, ctx_node, pos, size)?;
let target_trim = target.trim();
if !is_ncname_str(target_trim) || target_trim.eq_ignore_ascii_case("xml") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:processing-instruction name='{target}' is not a valid PI target (XTDE0890)"
)));
}
let raw = match select {
Some(sel) => {
let v = state.xpath_eval(sel, ctx_node, pos, size)?;
let items = sequence_string_items(&v, state.idx, state.num_style());
if items.len() <= 1 { value_to_string_styled(&v, state.idx, state.num_style()) }
else { items.join(" ") }
}
None => collect_value_of_body_pieces(state, body, ctx_node, pos, size)?
.join(" "),
};
let data = raw.replace("?>", "? >");
state.builder.push_pi(target, data);
}
Instr::Number { value, select, level, count, from, format, grouping_separator, grouping_size, ordinal, lang, letter_value: _, start_at } => {
let format_str = render_avt(state, format, ctx_node, pos, size)?;
let fmt = crate::number::parse_format(&format_str);
let start_offsets: Vec<i64> = match start_at {
Some(a) => render_avt(state, a, ctx_node, pos, size)?
.split_whitespace()
.filter_map(|t| t.parse::<i64>().ok())
.map(|v| v - 1)
.collect(),
None => Vec::new(),
};
let apply_start = |nums: &mut [i64]| {
for (i, n) in nums.iter_mut().enumerate() {
let off = if value.is_some() { start_offsets.first() }
else { start_offsets.get(i) };
if let Some(off) = off { *n += off; }
}
};
let ordinal_str = match ordinal {
Some(a) => render_avt(state, a, ctx_node, pos, size)?,
None => String::new(),
};
let lang_str = match lang {
Some(a) => render_avt(state, a, ctx_node, pos, size)?,
None => String::new(),
};
if !lang_str.is_empty()
&& !crate::compiler::is_valid_xml_lang(&lang_str)
{
return Err(XsltError::InvalidStylesheet(format!(
"xsl:number lang='{lang_str}' is not a valid xml:lang \
value (XTTE0990)"
)));
}
let opts = crate::number::FormatOptions {
ordinal: !ordinal_str.is_empty(),
lang: if lang_str.is_empty() { None } else { Some(lang_str.clone()) },
ordinal_scheme: if ordinal_str.is_empty() { None }
else { Some(ordinal_str.clone()) },
};
let grouping_sep_str = match grouping_separator {
Some(a) => Some(render_avt(state, a, ctx_node, pos, size)?),
None => None,
};
let grouping_size_n = match grouping_size {
Some(a) => render_avt(state, a, ctx_node, pos, size)?
.parse::<usize>().ok()
.filter(|n| *n > 0),
None => None,
};
let group = match (grouping_sep_str, grouping_size_n) {
(Some(sep), Some(n)) => Some((sep, n)),
_ => None,
};
if let Some(e) = value {
let v = state.xpath_eval(e, ctx_node, pos, size)?;
if matches!(e, sup_xml_core::xpath::Expr::BackwardsCompat(_)) {
let f = crate::eval::value_to_number_xpath(&v, state.idx);
let s = if f.is_finite() {
let mut nums = [f.round() as i64];
apply_start(&mut nums);
let mut s = crate::number::format_list_opts(&nums, &fmt, &opts);
if let Some((sep, sz)) = &group {
s = crate::number::apply_grouping(&s, sep, *sz);
}
s
} else {
"NaN".to_string()
};
state.builder.push_text(s, false);
return Ok(());
}
let to_int = |v: &Value| -> Result<i64> {
let f = crate::eval::value_to_number_xpath(v, state.idx);
if !f.is_finite() {
return Err(XsltError::InvalidStylesheet(
"xsl:number value= item is not convertible \
to an integer (XTDE0980)".into()));
}
let n = f.round() as i64;
if n < 0 {
return Err(XsltError::InvalidStylesheet(
"xsl:number value= item is negative (XTDE0980)".into()));
}
Ok(n)
};
let nums: Vec<i64> = match v {
Value::Sequence(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items { out.push(to_int(&it)?); }
out
}
Value::IntRange { lo, hi } => {
if lo < 0 {
return Err(XsltError::InvalidStylesheet(
"xsl:number value= item is negative (XTDE0980)".into()));
}
(lo..=hi).collect()
}
Value::NodeSet(ref ns) if ns.is_empty() => Vec::new(),
Value::ForeignNodeSet(ref ns) if ns.is_empty() => Vec::new(),
other => vec![to_int(&other)?],
};
let mut nums = nums;
apply_start(&mut nums);
let mut s = crate::number::format_list_opts(&nums, &fmt, &opts);
if let Some((sep, sz)) = &group { s = crate::number::apply_grouping(&s, sep, *sz); }
state.builder.push_text(s, false);
return Ok(());
}
let target_node = if let Some(sel) = select {
match state.xpath_eval(sel, ctx_node, pos, size)? {
Value::NodeSet(ns) if ns.len() == 1 => ns[0],
_ => return Err(XsltError::InvalidStylesheet(
"xsl:number select must evaluate to a single node \
(XTTE1000)".into())),
}
} else {
if sup_xml_core::xpath::eval::focus_is_undefined() {
return Err(XsltError::InvalidStylesheet(
"xsl:number with no select= called where the \
context item is undefined (XTTE0990)".into()));
}
if in_atomic_for_each() {
return Err(XsltError::InvalidStylesheet(
"xsl:number with no select= called where the \
context item is not a node (XTTE0990)".into()));
}
ctx_node
};
let mut numbers = compute_number_list(state, *level, count.as_ref(), from.as_ref(), target_node)?;
apply_start(&mut numbers);
let mut s = crate::number::format_list_opts(&numbers, &fmt, &opts);
if let Some((sep, sz)) = &group { s = crate::number::apply_grouping(&s, sep, *sz); }
state.builder.push_text(s, false);
}
Instr::Variable(v) => {
let mut val = evaluate_variable_value(state, v, ctx_node, pos, size)?;
if let Some(t) = &v.as_type {
if let Some(typed) = coerce_to_user_schema_type(&val, t, state.style, state.idx) {
val = typed;
} else if let Some(st) = parse_as_atomic_type(t) {
val = coerce_to_atomic_sequence(val, &st, state.idx)?;
}
}
state.variables.bind(qname_key(&v.name), val);
}
Instr::Message { terminate, body } => {
let terminate_yes = match terminate {
Some(a) => {
let raw = render_avt(state, a, ctx_node, pos, size)?;
match raw.trim() {
"yes" => true,
"no" => false,
bad => return Err(XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(format!(
"xsl:message terminate='{bad}' must be 'yes' or 'no' (XTDE0030)"
)).with_xpath_code("XTDE0030"))),
}
}
None => false,
};
let s = stringify_into_string(state, body, ctx_node, pos, size)?;
eprintln!("xsl:message: {s}");
if terminate_yes {
return Err(XsltError::Terminated(s));
}
}
Instr::Fallback { .. } => {
}
Instr::Sequence { select } => {
let v = state.xpath_eval(select, ctx_node, pos, size)?;
if state.sequence_sink_active() {
state.push_to_sequence_sink(v);
} else {
copy_value_into(state, &v, true)?;
}
}
Instr::MapEntry { key, select, body } => {
let k = state.xpath_eval(key, ctx_node, pos, size)?;
let key_val = sup_xml_core::xpath::eval::first_atomic_key(&k, state.idx);
let val = match select {
Some(sel) => state.xpath_eval(sel, ctx_node, pos, size)?,
None => {
state.sequence_sinks.push(Vec::new());
let r = eval_body(state, body, ctx_node, pos, size);
let mut items = state.sequence_sinks.pop().unwrap_or_default();
r?;
if items.len() == 1 { items.pop().unwrap() }
else { Value::Sequence(items) }
}
};
let entry = Value::Map(Box::new(vec![(key_val, val)]));
if state.sequence_sink_active() {
state.push_to_sequence_sink(entry);
} else {
copy_value_into(state, &entry, true)?;
}
}
Instr::Map { body } => {
state.sequence_sinks.push(Vec::new());
let r = eval_body(state, body, ctx_node, pos, size);
let collected = state.sequence_sinks.pop().unwrap_or_default();
r?;
let mut entries: Vec<(Value, Value)> = Vec::new();
for v in collected {
if let Value::Map(m) = v {
for (k, val) in *m {
if let Some(slot) = entries.iter_mut().find(|(ek, _)|
sup_xml_core::xpath::eval::map_key_eq(ek, &k, state.idx))
{
slot.1 = val;
} else {
entries.push((k, val));
}
}
}
}
let map = Value::Map(Box::new(entries));
if state.sequence_sink_active() {
state.push_to_sequence_sink(map);
} else {
copy_value_into(state, &map, true)?;
}
}
Instr::Unsupported { name, fallback } => {
if !fallback.is_empty() {
eval_body(state, fallback, ctx_node, pos, size)?;
} else {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{name} is not implemented in this build"
)));
}
}
}
Ok(())
}
fn eval_key_body_value(
state: &mut EvalState, body: &[Instr], node: NodeId,
) -> Result<Value> {
let _temp = TempOutputGuard::enter();
if body_uses_sequence_or_call(body) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, body, node, 1, 1);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let mut flat: Vec<Value> = Vec::new();
for item in captured {
match item {
Value::Sequence(items) => flat.extend(items),
Value::NodeSet(ns) => flat.extend(
ns.into_iter().map(|id| Value::NodeSet(vec![id]))),
other => flat.push(other),
}
}
return Ok(if flat.len() == 1 { flat.pop().unwrap() }
else { Value::Sequence(flat) });
}
let nodes = build_rtf_nodes(state, body, node, 1, 1)?;
let root_id = rtf_into_index(state.idx, &nodes);
Ok(Value::NodeSet(vec![root_id]))
}
fn body_uses_sequence_or_call(body: &[Instr]) -> bool {
for i in body {
if matches!(i,
Instr::Sequence { .. }
| Instr::Map { .. } | Instr::MapEntry { .. }
| Instr::CallTemplate { .. }
| Instr::ApplyTemplates { .. })
{
return true;
}
let inner: Vec<&[Instr]> = match i {
Instr::If { body, .. } | Instr::ForEach { body, .. }
| Instr::ForEachGroup { body, .. } => vec![body.as_slice()],
Instr::Choose { whens, otherwise } => {
let mut v: Vec<&[Instr]> = whens.iter()
.map(|(_, b)| b.as_slice()).collect();
if let Some(o) = otherwise { v.push(o.as_slice()); }
v
}
_ => continue,
};
if inner.iter().any(|b| body_uses_sequence_or_call(b)) {
return true;
}
}
false
}
fn bind_variable(
state: &mut EvalState, name: &QName,
select: Option<&sup_xml_core::xpath::Expr>,
body: &[Instr],
as_type: Option<&str>,
base_uri: Option<&str>,
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<()> {
let key = qname_key(name);
let want_item_seq = as_type
.map(as_is_sequence_typed)
.unwrap_or(false);
let mut v = if let Some(sel) = select {
state.xpath_eval(sel, ctx_node, pos, size)?
} else if as_type.map(as_is_document_node_sequence).unwrap_or(false) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, body, ctx_node, pos, size);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let mut ids: Vec<sup_xml_core::xpath::NodeId> = Vec::new();
for item in captured {
match item {
Value::NodeSet(ns) => ids.extend(ns),
Value::Sequence(items) => {
for it in items {
if let Value::NodeSet(ns) = it { ids.extend(ns); }
}
}
_ => {}
}
}
Value::NodeSet(ids)
} else if want_item_seq && body_uses_sequence_or_call(body) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, body, ctx_node, pos, size);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let mut flat: Vec<Value> = Vec::new();
for item in captured {
match item {
Value::Sequence(items) => flat.extend(items),
Value::NodeSet(ns) => flat.extend(
ns.into_iter().map(|id| Value::NodeSet(vec![id]))),
other => flat.push(other),
}
}
if flat.len() == 1 { flat.pop().unwrap() }
else { Value::Sequence(flat) }
} else if body.is_empty() {
if as_type.is_some() {
Value::NodeSet(Vec::new())
} else {
let ids = state.idx.allocate_rtf_text_nodes_inherent(vec![String::new()]);
Value::NodeSet(ids)
}
} else if as_type.map(as_is_attribute_kind).unwrap_or(false) {
let nodes = build_rtf_nodes_no_merge(state, body, ctx_node, pos, size)?;
let ids = rtf_children_into_index(state.idx, &nodes);
if let Some(uri) = base_uri {
let mut map = state.rtf_base_uris.borrow_mut();
for &id in &ids { map.insert(id, uri.to_string()); }
}
store_rtf(state, &key, nodes);
Value::NodeSet(ids)
} else if as_type.map(|t|
as_is_sequence_typed(t) && !as_target_is_atomic(t)
).unwrap_or(false) {
let nodes = build_rtf_nodes_no_merge(state, body, ctx_node, pos, size)?;
let ids = rtf_children_into_index(state.idx, &nodes);
if let Some(uri) = base_uri {
let mut map = state.rtf_base_uris.borrow_mut();
for &id in &ids { map.insert(id, uri.to_string()); }
}
store_rtf(state, &key, nodes);
Value::NodeSet(ids)
} else if let Some(st) = as_type
.and_then(parse_as_atomic_type)
.filter(template_result_type_is_node_kind)
.filter(|st| !matches!(st.item,
sup_xml_core::xpath::ast::ItemType::Document))
{
let nodes = build_rtf_nodes_no_merge(state, body, ctx_node, pos, size)?;
let counted: Vec<&ResultNode> = nodes.iter().filter(|n| match n {
ResultNode::Text { content, .. } => !content.trim().is_empty(),
_ => true,
}).collect();
if node_kind_overflow_or_mismatch(&counted, &st) {
return Err(XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(format!(
"variable '{}' declared as='{}' but the body \
produced an incompatible sequence ({} items) (XTTE0570)",
qname_key(name), as_type.unwrap_or(""), counted.len(),
)).with_xpath_code("XTTE0570")));
}
let root_id = rtf_into_index(state.idx, &nodes);
state.idx.mark_synthetic_wrap(root_id);
if let Some(uri) = base_uri {
state.rtf_base_uris.borrow_mut().insert(root_id, uri.to_string());
}
store_rtf(state, &key, nodes);
Value::NodeSet(vec![root_id])
} else {
let nodes = build_rtf_nodes(state, body, ctx_node, pos, size)?;
if as_type.is_none() {
check_document_node_content(&nodes)?;
}
let root_id = rtf_into_index(state.idx, &nodes);
if let Some(uri) = base_uri {
state.rtf_base_uris.borrow_mut().insert(root_id, uri.to_string());
}
store_rtf(state, &key, nodes);
Value::NodeSet(vec![root_id])
};
if let Some(t) = as_type {
if let Some(typed) = coerce_to_user_schema_type(&v, t, state.style, state.idx) {
v = typed;
} else if let Some(st) = parse_as_atomic_type(t) {
v = coerce_to_atomic_sequence(v, &st, state.idx)?;
}
}
state.variables.bind(key, v);
Ok(())
}
pub(crate) fn parse_as_atomic_type(
src: &str,
) -> Option<sup_xml_core::xpath::ast::SequenceType> {
use sup_xml_core::xpath::ast::{SequenceType, ItemType, Occurrence};
let src = src.trim();
let (body, occ) = if let Some(b) = src.strip_suffix('*') {
(b.trim(), Occurrence::ZeroOrMore)
} else if let Some(b) = src.strip_suffix('+') {
(b.trim(), Occurrence::OneOrMore)
} else if let Some(b) = src.strip_suffix('?') {
(b.trim(), Occurrence::Optional)
} else {
(src, Occurrence::One)
};
if body.contains('(') {
let (bare, inside) = match body.split_once('(') {
Some((b, rest)) => (b.trim(), rest.trim_end_matches(')').trim()),
None => (body, ""),
};
let first_arg_local = |args: &str| -> Option<String> {
let head = args.split(',').next()?.trim();
if head.is_empty() || head == "*" { return None; }
let local = head.rsplit_once(':').map(|(_, l)| l).unwrap_or(head);
(!local.is_empty() && local != "*").then(|| local.to_string())
};
let item = match bare {
"node" => ItemType::AnyNode,
"element" => ItemType::Element(first_arg_local(inside)),
"attribute" => ItemType::Attribute(first_arg_local(inside)),
"schema-element" => ItemType::Element(None),
"schema-attribute" => ItemType::Attribute(None),
"document-node" => ItemType::Document,
"text" => ItemType::Text,
"comment" => ItemType::Comment,
"processing-instruction" => ItemType::PI(None),
_ => return None,
};
return Some(SequenceType { item, occurrence: occ });
}
let local = match body.rsplit_once(':') {
Some((_, l)) => l,
None => body,
};
Some(SequenceType {
item: ItemType::Atomic(local.to_string()),
occurrence: occ,
})
}
fn node_matches_kind_test<I: sup_xml_core::xpath::DocIndexLike>(
item: &sup_xml_core::xpath::ast::ItemType,
id: sup_xml_core::xpath::NodeId,
idx: &I,
) -> bool {
use sup_xml_core::xpath::ast::ItemType;
use sup_xml_core::xpath::XPathNodeKind as K;
let k = idx.kind(id);
match item {
ItemType::Any | ItemType::AnyNode => true,
ItemType::Element(name) => matches!(k, K::Element)
&& name.as_ref().map_or(true, |n| idx.local_name(id) == n),
ItemType::Attribute(name) => matches!(k, K::Attribute)
&& name.as_ref().map_or(true, |n| idx.local_name(id) == n),
ItemType::Text => matches!(k, K::Text | K::CData),
ItemType::Comment => matches!(k, K::Comment),
ItemType::PI(name) => matches!(k, K::PI)
&& name.as_ref().map_or(true, |n| idx.local_name(id) == n),
ItemType::Document => matches!(k, K::Document),
ItemType::Atomic(_) => false,
ItemType::Function(_) | ItemType::Map | ItemType::Array
| ItemType::EmptySequence => false,
}
}
fn template_result_type_is_node_kind(
st: &sup_xml_core::xpath::ast::SequenceType,
) -> bool {
use sup_xml_core::xpath::ast::ItemType;
matches!(&st.item,
ItemType::Element(_) | ItemType::Attribute(_) | ItemType::Text
| ItemType::Comment | ItemType::PI(_) | ItemType::AnyNode
| ItemType::Document)
}
fn template_result_violates_type(
nodes: &[crate::result_tree::ResultNode],
st: &sup_xml_core::xpath::ast::SequenceType,
) -> bool {
use sup_xml_core::xpath::ast::Occurrence;
let n = nodes.len();
let cardinality_bad = match st.occurrence {
Occurrence::One => n != 1,
Occurrence::Optional => n > 1,
Occurrence::OneOrMore => n < 1,
Occurrence::ZeroOrMore => false,
};
if cardinality_bad { return true; }
!nodes.iter().all(|node| result_node_matches_item(node, &st.item))
}
fn node_kind_overflow_or_mismatch(
nodes: &[&crate::result_tree::ResultNode],
st: &sup_xml_core::xpath::ast::SequenceType,
) -> bool {
use sup_xml_core::xpath::ast::Occurrence;
let too_many = matches!(st.occurrence, Occurrence::One | Occurrence::Optional)
&& nodes.len() > 1;
if too_many { return true; }
!nodes.iter().all(|node| result_node_matches_item(node, &st.item))
}
fn result_node_matches_item(
node: &crate::result_tree::ResultNode,
item: &sup_xml_core::xpath::ast::ItemType,
) -> bool {
use sup_xml_core::xpath::ast::ItemType;
use crate::result_tree::ResultNode as R;
match item {
ItemType::Any | ItemType::AnyNode => true,
ItemType::Element(name) => matches!(node,
R::Element { name: qn, .. }
if name.as_ref().map_or(true, |n| &qn.local == n)),
ItemType::Attribute(name) => matches!(node,
R::Attribute { name: qn, .. }
if name.as_ref().map_or(true, |n| &qn.local == n)),
ItemType::Text => matches!(node, R::Text { .. }),
ItemType::Comment => matches!(node, R::Comment(_)),
ItemType::PI(name) => matches!(node,
R::ProcessingInstruction { target, .. }
if name.as_ref().map_or(true, |n| target == n)),
ItemType::Document | ItemType::Atomic(_) | ItemType::Function(_)
| ItemType::Map | ItemType::Array | ItemType::EmptySequence => false,
}
}
#[cfg(feature = "xsd")]
pub(crate) fn coerce_to_user_schema_type<I: sup_xml_core::xpath::DocIndexLike>(
v: &Value, as_type: &str, style: &StylesheetAst, idx: &I,
) -> Option<Value> {
use sup_xml_core::xsd::{QName as XQName, TypeRef};
let body = as_type.trim().trim_end_matches(['*', '+', '?']).trim();
let (ns, local): (String, &str) = match body.split_once(':') {
Some((prefix, l)) => {
if prefix == "xs" || prefix == "xsd" { return None; }
(style.namespaces.get(prefix)?.clone(), l)
}
None => {
if sup_xml_core::xpath::eval::atomic_kind_static(body).is_some() { return None; }
(String::new(), body)
}
};
let qn = XQName::new((!ns.is_empty()).then_some(ns.as_str()), local);
for schema in &style.schemas {
let Some(TypeRef::Simple(st)) = schema.type_def(&qn) else { continue };
let s = sup_xml_core::xpath::eval::value_to_string(v, idx);
return st.validate(&s).ok()
.map(|xv| xsd_value_to_xpath(xv, &s, Some((ns.as_str(), local))));
}
None
}
#[cfg(not(feature = "xsd"))]
pub(crate) fn coerce_to_user_schema_type<I: sup_xml_core::xpath::DocIndexLike>(
_v: &Value, _as_type: &str, _style: &StylesheetAst, _idx: &I,
) -> Option<Value> {
None
}
pub(crate) fn coerce_to_atomic_sequence<I: sup_xml_core::xpath::DocIndexLike>(
v: Value,
st: &sup_xml_core::xpath::ast::SequenceType,
idx: &I,
) -> Result<Value> {
use sup_xml_core::xpath::ast::{Occurrence, ItemType};
if let Value::NodeSet(ref ns) = v {
if ns.len() == 1 {
let id = ns[0];
let needs_unwrap = matches!(&st.item,
ItemType::AnyNode
| ItemType::Element(_)
| ItemType::Attribute(_)
| ItemType::Text
| ItemType::Comment
| ItemType::PI(_));
if needs_unwrap
&& matches!(idx.kind(id),
sup_xml_core::xpath::XPathNodeKind::Document) {
let kids: Vec<sup_xml_core::xpath::NodeId> =
idx.children(id).to_vec();
for &k in &kids {
if !node_matches_kind_test(&st.item, k, idx) {
return Err(XsltError::InvalidStylesheet(format!(
"body produces a node whose kind doesn't match \
the declared type {:?} (XTTE0570)", st.item
)));
}
}
return Ok(Value::NodeSet(kids));
}
}
}
if let Value::NodeSet(ref ns) = v {
if ns.is_empty() && matches!(st.occurrence,
Occurrence::Optional | Occurrence::ZeroOrMore) {
return Ok(v);
}
if ns.is_empty() && matches!(st.occurrence,
Occurrence::One | Occurrence::OneOrMore)
{
return Err(XsltError::InvalidStylesheet(
"value's cardinality (empty) is incompatible with declared \
type (XTTE0570)".into()
));
}
if let ItemType::Atomic(_) = &st.item {
if ns.len() > 1
&& matches!(st.occurrence, Occurrence::One | Occurrence::Optional)
{
return Err(XsltError::InvalidStylesheet(format!(
"value of cardinality {} doesn't match the declared \
singleton type (XTTE0570)", ns.len(),
)));
}
let single = sup_xml_core::xpath::ast::SequenceType {
item: st.item.clone(), occurrence: Occurrence::One,
};
let mut out = Vec::with_capacity(ns.len());
for &id in ns.iter() {
let atom = Value::String(idx.string_value(id));
match sup_xml_core::xpath::eval::cast_value_to_atomic(&atom, &single, idx) {
Ok(c) => out.push(c),
Err(_) => return Err(XsltError::InvalidStylesheet(format!(
"value can't be atomised to the declared type {:?} \
(XTTE0570)", st.item,
))),
}
}
return Ok(if out.len() == 1 { out.pop().unwrap() }
else { Value::Sequence(out) });
}
if ns.len() > 1 {
if matches!(st.occurrence, Occurrence::One | Occurrence::Optional) {
return Err(XsltError::InvalidStylesheet(format!(
"value of cardinality {} doesn't match the declared \
singleton type (XTTE0570)", ns.len(),
)));
}
for &id in ns.iter() {
if !node_matches_kind_test(&st.item, id, idx) {
return Err(XsltError::InvalidStylesheet(format!(
"value's item kind doesn't match the declared \
type {:?} (XTTE0570)", st.item
)));
}
}
return Ok(v);
}
if ns.len() == 1 && !matches!(&st.item, ItemType::Atomic(_) | ItemType::Any) {
if !node_matches_kind_test(&st.item, ns[0], idx) {
return Err(XsltError::InvalidStylesheet(format!(
"value's item kind doesn't match the declared \
type {:?} (XTTE0570)", st.item
)));
}
}
}
if let Value::IntRange { .. } = v {
if matches!(&st.item, ItemType::Atomic(_)) {
return Ok(v);
}
}
if let ItemType::Atomic(target) = &st.item {
if target == "anyAtomicType" {
if matches!(v, Value::Sequence(_)) { return Ok(v); }
}
if let Value::Sequence(items) = v {
let single = sup_xml_core::xpath::ast::SequenceType {
item: st.item.clone(),
occurrence: Occurrence::One,
};
let mut out = Vec::with_capacity(items.len());
for it in items {
match sup_xml_core::xpath::eval::cast_value_to_atomic(&it, &single, idx) {
Ok(c) => out.push(c),
Err(_) => out.push(it),
}
}
return Ok(if out.len() == 1 { out.pop().unwrap() }
else { Value::Sequence(out) });
}
}
if let ItemType::Atomic(target_name) = &st.item {
if let Value::Typed(t) = &v {
if sup_xml_core::xpath::eval::xsd_is_subtype_of(t.kind, target_name) {
return Ok(v);
}
}
}
let original = v.clone();
match sup_xml_core::xpath::eval::cast_value_to_atomic(&v, st, idx) {
Ok(cast) => Ok(cast),
Err(e) => {
if as_is_strict_mismatch_with_idx(&original, &st.item, idx) {
return Err(XsltError::from(e));
}
Ok(original)
}
}
}
fn as_is_strict_mismatch_with_idx<I: sup_xml_core::xpath::DocIndexLike>(
v: &Value,
target: &sup_xml_core::xpath::ast::ItemType,
idx: &I,
) -> bool {
use sup_xml_core::xpath::ast::ItemType;
let ItemType::Atomic(target_name) = target else { return false; };
let numeric_target = matches!(target_name.as_str(),
"integer" | "long" | "int" | "short" | "byte"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "decimal" | "double" | "float" | "numeric");
if let Value::NodeSet(ns) = v {
if numeric_target && ns.len() == 1 {
let s = idx.string_value(ns[0]);
if s.trim().is_empty() {
return true;
}
}
}
let _ = idx;
if let Value::String(s) = v {
if numeric_target && s.trim().parse::<f64>().is_err() {
return true;
}
}
let src_kind: Option<&str> = match v {
Value::Boolean(_) => Some("boolean"),
Value::Number(_) => Some("double"),
Value::Typed(t) => Some(t.kind),
_ => None,
};
let Some(src) = src_kind else { return false; };
let numeric_target = matches!(target_name.as_str(),
"integer" | "long" | "int" | "short" | "byte"
| "unsignedLong" | "unsignedInt" | "unsignedShort" | "unsignedByte"
| "nonNegativeInteger" | "nonPositiveInteger"
| "positiveInteger" | "negativeInteger"
| "decimal" | "double" | "float" | "numeric");
let temporal_target = matches!(target_name.as_str(),
"date" | "dateTime" | "time"
| "duration" | "dayTimeDuration" | "yearMonthDuration"
| "gYear" | "gYearMonth" | "gMonth" | "gMonthDay" | "gDay");
if src == "boolean" && (numeric_target || temporal_target) {
return true;
}
if matches!(src, "date" | "dateTime" | "time"
| "duration" | "dayTimeDuration" | "yearMonthDuration")
&& target_name == "boolean"
{
return true;
}
if numeric_target && matches!(src,
"date" | "dateTime" | "time"
| "duration" | "dayTimeDuration" | "yearMonthDuration")
{
return true;
}
if temporal_target && matches!(src,
"double" | "float" | "decimal" | "integer" | "long" | "int"
| "short" | "byte" | "unsignedLong" | "unsignedInt"
| "unsignedShort" | "unsignedByte" | "nonNegativeInteger"
| "nonPositiveInteger" | "positiveInteger" | "negativeInteger")
{
return true;
}
false
}
fn evaluate_variable_value(
state: &mut EvalState, v: &Variable,
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<Value> {
if let Some(sel) = &v.select {
return state.xpath_eval(sel, ctx_node, pos, size);
}
if v.body.is_empty() {
return Ok(Value::String(String::new()));
}
let want_item_seq = v.as_type.as_deref()
.map(as_is_sequence_typed)
.unwrap_or(false);
if want_item_seq && body_uses_sequence_or_call(&v.body) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, &v.body, ctx_node, pos, size);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let mut flat: Vec<Value> = Vec::new();
for item in captured {
match item {
Value::Sequence(items) => flat.extend(items),
Value::NodeSet(ns) => flat.extend(
ns.into_iter().map(|id| Value::NodeSet(vec![id]))),
other => flat.push(other),
}
}
return Ok(if flat.len() == 1 { flat.pop().unwrap() }
else { Value::Sequence(flat) });
}
if let Some(t) = &v.as_type {
if as_is_document_node_sequence(t) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, &v.body, ctx_node, pos, size);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let mut ids: Vec<sup_xml_core::xpath::NodeId> = Vec::new();
for item in captured {
match item {
Value::NodeSet(ns) => ids.extend(ns),
Value::Sequence(items) => {
for it in items {
if let Value::NodeSet(ns) = it { ids.extend(ns); }
}
}
_ => {} }
}
return Ok(Value::NodeSet(ids));
}
if (as_is_sequence_typed(t) && !as_target_is_atomic(t)) || as_is_attribute_kind(t) {
let nodes = build_rtf_nodes_no_merge(state, &v.body, ctx_node, pos, size)?;
let key = qname_key(&v.name);
let child_ids = rtf_children_into_index(state.idx, &nodes);
if let Some(uri) = &v.base_uri {
let mut map = state.rtf_base_uris.borrow_mut();
for &id in &child_ids {
map.insert(id, uri.clone());
}
}
store_rtf(state, &key, nodes);
return Ok(Value::NodeSet(child_ids));
}
if as_is_sequence_typed(t) && as_target_is_atomic(t) {
let nodes = build_rtf_nodes_no_merge(state, &v.body, ctx_node, pos, size)?;
let key = qname_key(&v.name);
let child_ids = rtf_children_into_index(state.idx, &nodes);
if let Some(uri) = &v.base_uri {
let mut map = state.rtf_base_uris.borrow_mut();
for &id in &child_ids { map.insert(id, uri.clone()); }
}
store_rtf(state, &key, nodes);
return Ok(Value::NodeSet(child_ids));
}
}
let key = qname_key(&v.name);
if let Some(t) = &v.as_type {
if let Some(st) = parse_as_atomic_type(t)
.filter(template_result_type_is_node_kind)
.filter(|st| !matches!(st.item,
sup_xml_core::xpath::ast::ItemType::Document))
{
let nodes = build_rtf_nodes_no_merge(state, &v.body, ctx_node, pos, size)?;
let counted: Vec<&ResultNode> = nodes.iter().filter(|n| match n {
ResultNode::Text { content, .. } => !content.trim().is_empty(),
_ => true,
}).collect();
if node_kind_overflow_or_mismatch(&counted, &st) {
return Err(XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(format!(
"variable '{}' declared as='{t}' but the body \
produced an incompatible sequence ({} items) (XTTE0570)",
qname_key(&v.name), counted.len(),
)).with_xpath_code("XTTE0570")));
}
let root_id = rtf_into_index(state.idx, &nodes);
state.idx.mark_synthetic_wrap(root_id);
if let Some(uri) = &v.base_uri {
state.rtf_base_uris.borrow_mut().insert(root_id, uri.clone());
}
store_rtf(state, &key, nodes);
return Ok(Value::NodeSet(vec![root_id]));
}
}
let nodes = build_rtf_nodes(state, &v.body, ctx_node, pos, size)?;
if v.as_type.is_none() {
check_document_node_content(&nodes)?;
}
let root_id = rtf_into_index(state.idx, &nodes);
if let Some(uri) = &v.base_uri {
state.rtf_base_uris.borrow_mut().insert(root_id, uri.clone());
}
store_rtf(state, &key, nodes);
Ok(Value::NodeSet(vec![root_id]))
}
fn as_is_document_node_sequence(t: &str) -> bool {
let s = t.trim();
if !(s.ends_with('*') || s.ends_with('+')) { return false; }
let body = s.trim_end_matches(|c: char| c == '*' || c == '+').trim();
body == "document-node()" || body.starts_with("document-node(")
}
fn as_target_is_atomic(t: &str) -> bool {
let body = t.trim().trim_end_matches(|c: char| c == '*' || c == '+' || c == '?').trim();
!body.contains('(')
}
fn as_is_sequence_typed(t: &str) -> bool {
let s = t.trim();
if s.starts_with("document-node") { return false; }
s.ends_with('*') || s.ends_with('+')
|| as_is_nonnode_item_type(s)
}
fn as_is_nonnode_item_type(t: &str) -> bool {
let s = t.trim();
s.starts_with("map(") || s.starts_with("array(") || s.starts_with("function(")
}
fn as_is_attribute_kind(t: &str) -> bool {
matches!(
parse_as_atomic_type(t).map(|st| st.item),
Some(sup_xml_core::xpath::ast::ItemType::Attribute(_))
)
}
fn check_document_node_content(nodes: &[ResultNode]) -> Result<()> {
if let Some(ResultNode::Attribute { name, .. }) =
nodes.iter().find(|n| matches!(n, ResultNode::Attribute { .. }))
{
return Err(XsltError::Xpath(
sup_xml_core::xpath::eval::xpath_err(format!(
"the sequence used to construct the content of a document \
node contains attribute '{}' (XTDE0420)", name.local))
.with_xpath_code("XTDE0420")));
}
Ok(())
}
fn build_rtf_nodes_no_merge(
state: &mut EvalState, body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<Vec<ResultNode>> {
let mut tmp = EvalState {
style: state.style, idx: state.idx, namespaces: state.namespaces,
keys: state.keys,
documents: state.documents,
xslt_current: state.xslt_current,
rtf_base_uris: state.rtf_base_uris,
static_ctx: state.static_ctx,
variables: std::mem::take(&mut state.variables),
rtfs: std::mem::take(&mut state.rtfs),
rtf_scopes: std::mem::take(&mut state.rtf_scopes),
builder: {
let mut b = ResultBuilder::new();
b.no_text_merge = true;
b
},
principal_buf: None,
unparsed_entities: state.unparsed_entities.clone(),
source_doc: state.source_doc,
source_types: state.source_types,
apply_imports_ctx: state.apply_imports_ctx.clone(),
user_exts: state.user_exts,
sequence_sinks: std::mem::take(&mut state.sequence_sinks),
template_call_depth: state.template_call_depth,
current_group: std::mem::take(&mut state.current_group),
regex_groups: std::mem::take(&mut state.regex_groups),
tunnel_pool: std::mem::take(&mut state.tunnel_pool),
current_grouping_key: state.current_grouping_key.take(),
accumulators: std::mem::take(&mut state.accumulators),
unparsed_texts: state.unparsed_texts,
static_base_uri: state.static_base_uri.clone(),
loader: state.loader,
loader_base: state.loader_base,
dyn_doc_cache: state.dyn_doc_cache,
};
let r = {
let _tmp_out = TempOutputGuard::enter();
eval_body(&mut tmp, body, ctx_node, pos, size)
};
state.variables = tmp.variables;
state.rtfs = tmp.rtfs;
state.rtf_scopes = tmp.rtf_scopes;
state.sequence_sinks = tmp.sequence_sinks;
state.current_group = tmp.current_group;
state.regex_groups = tmp.regex_groups;
state.tunnel_pool = tmp.tunnel_pool;
state.current_grouping_key = tmp.current_grouping_key;
state.accumulators = tmp.accumulators;
r?;
Ok(tmp.builder.finish())
}
fn rtf_children_into_index<'a>(
idx: &'a sup_xml_core::xpath::DocIndex<'a>,
nodes: &[ResultNode],
) -> Vec<sup_xml_core::xpath::NodeId> {
let mut b = idx.start_rtf();
let doc_root = b.add_document();
let initial_scope: NsScope = vec![
(Some("xml".into()), "http://www.w3.org/XML/1998/namespace".into()),
];
let mut child_ids = Vec::with_capacity(nodes.len());
for n in nodes {
let id = add_result_node_and_return_id(&mut b, doc_root, n, &initial_scope);
if let Some(id) = id { child_ids.push(id); }
}
let _ = idx.finish_rtf(b);
idx.mark_synthetic_wrap(doc_root);
child_ids
}
fn build_function_subtree<I: DocIndexLike>(
body: &[Instr],
bindings: &dyn XPathBindings,
idx: &I,
ctx_node: NodeId, pos: usize, size: usize,
builder: &mut ResultBuilder,
static_ctx: &StaticContext,
) -> std::result::Result<(), sup_xml_core::error::XmlError> {
use sup_xml_core::error::{ErrorDomain, ErrorLevel, XmlError};
use sup_xml_core::xpath::eval::{eval_expr, value_to_bool, value_to_string_with, EvalCtx};
let err = |m: String| XmlError::new(ErrorDomain::XPath, ErrorLevel::Error, m);
fn mk_ctx<'a>(
b: &'a dyn XPathBindings, sc: &'a StaticContext,
ctx_node: NodeId, pos: usize, size: usize,
) -> EvalCtx<'a> {
EvalCtx { context_node: ctx_node, pos, size, bindings: b, static_ctx: sc }
}
for instr in body {
match instr {
Instr::LiteralElement { name, attributes, namespaces, body: lre_body, .. } => {
builder.open_element(name.clone());
if !name.uri.is_empty() {
builder.push_namespace_decl(name.prefix.clone(), name.uri.clone());
}
for (prefix, uri) in namespaces {
builder.push_namespace_decl(prefix.clone(), uri.clone());
}
for (aname, avt) in attributes {
let value = render_avt_static(avt, bindings, idx, ctx_node, pos, size, static_ctx)?;
builder.push_attribute(aname.clone(), value);
if !aname.uri.is_empty() && aname.prefix.is_some() {
builder.push_namespace_decl(aname.prefix.clone(), aname.uri.clone());
}
}
build_function_subtree(lre_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
builder.close_element();
}
Instr::LiteralText { text, dose } => {
if !text.is_empty() {
builder.push_text(text.clone(), *dose);
}
}
Instr::ValueOf { select, dose, separator } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
let text = match separator {
Some(sep_avt) => {
let sep = render_avt_static(sep_avt, bindings, idx, ctx_node, pos, size, static_ctx)?;
let pieces = sequence_string_items(&v, idx, NumStyle::from_context(false, bindings.xpath_version_2_or_later()));
pieces.join(&sep)
}
None => value_to_string_with(&v, idx, bindings),
};
if !text.is_empty() { builder.push_text(text, *dose); }
}
Instr::CopyOf { select, .. } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
copy_value_into_builder(
builder, idx, &v,
NumStyle::from_context(false, bindings.xpath_version_2_or_later()),
);
}
Instr::Copy { body: cpy_body, .. } => {
match idx.kind(ctx_node) {
sup_xml_core::xpath::XPathNodeKind::Element => {
let qname = QName {
prefix: idx.namespace_prefix(ctx_node).map(str::to_string),
local: idx.local_name(ctx_node).to_string(),
uri: idx.namespace_uri(ctx_node).to_string(),
};
builder.open_element(qname);
build_function_subtree(cpy_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
builder.close_element();
}
sup_xml_core::xpath::XPathNodeKind::Text
| sup_xml_core::xpath::XPathNodeKind::CData => {
builder.push_text(idx.string_value(ctx_node), false);
}
sup_xml_core::xpath::XPathNodeKind::Comment => {
builder.push_comment(idx.string_value(ctx_node));
}
sup_xml_core::xpath::XPathNodeKind::PI => {
builder.push_pi(idx.pi_target(ctx_node).to_string(),
idx.string_value(ctx_node));
}
sup_xml_core::xpath::XPathNodeKind::Document => {
build_function_subtree(cpy_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
}
_ => {}
}
}
Instr::Element { name, namespace, body: elt_body, in_scope_namespaces, .. } => {
let name_str = render_avt_static(name, bindings, idx, ctx_node, pos, size, static_ctx)?;
let explicit_ns = namespace.is_some();
let ns_uri = match namespace {
Some(avt) => render_avt_static(avt, bindings, idx, ctx_node, pos, size, static_ctx)?,
None => String::new(),
};
let (prefix, local) = split_qname(&name_str);
let lookup_local = |target: Option<&str>| -> Option<String> {
in_scope_namespaces.iter()
.find(|(p, _)| p.as_deref() == target)
.map(|(_, u)| u.clone())
};
let resolved_uri = if explicit_ns { ns_uri }
else if let Some((p, _)) = name_str.split_once(':') {
lookup_local(Some(p)).unwrap_or_default()
} else {
lookup_local(None).unwrap_or_default()
};
let q = QName { prefix, local, uri: resolved_uri };
builder.open_element(q.clone());
if !q.uri.is_empty() {
builder.push_namespace_decl(q.prefix.clone(), q.uri.clone());
}
build_function_subtree(elt_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
builder.close_element();
}
Instr::Attribute { name, namespace, select, separator, body: a_body, in_scope_namespaces, .. } => {
let name_str = render_avt_static(name, bindings, idx, ctx_node, pos, size, static_ctx)?;
let explicit_ns = namespace.is_some();
let ns_uri = match namespace {
Some(avt) => render_avt_static(avt, bindings, idx, ctx_node, pos, size, static_ctx)?,
None => String::new(),
};
let sep = match separator {
Some(avt) => render_avt_static(avt, bindings, idx, ctx_node, pos, size, static_ctx)?,
None => if select.is_some() { " ".to_string() } else { String::new() },
};
let value = if let Some(sel) = select {
let v = eval_expr(sel, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
let items = sequence_string_items(&v, idx, NumStyle::from_context(false, bindings.xpath_version_2_or_later()));
if items.len() <= 1 { value_to_string_with(&v, idx, bindings) }
else { items.join(&sep) }
} else {
let mut sub = ResultBuilder::new();
build_function_subtree(a_body, bindings, idx, ctx_node, pos, size, &mut sub, static_ctx)?;
stringify(&sub.finish())
};
let (prefix, local) = split_qname(&name_str);
let lookup_local = |target: Option<&str>| -> Option<String> {
in_scope_namespaces.iter()
.find(|(p, _)| p.as_deref() == target)
.map(|(_, u)| u.clone())
};
let resolved_uri = if explicit_ns { ns_uri }
else if let Some((p, _)) = name_str.split_once(':') {
lookup_local(Some(p)).unwrap_or_default()
} else {
String::new()
};
let aq = QName { prefix: prefix.clone(), local, uri: resolved_uri };
if !aq.uri.is_empty() && aq.prefix.is_some() {
builder.push_namespace_decl(aq.prefix.clone(), aq.uri.clone());
}
builder.push_attribute(aq, value);
}
Instr::Comment { select, body: c_body } => {
let raw = match select {
Some(sel) => {
let v = eval_expr(sel, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
value_to_string_with(&v, idx, bindings)
}
None => {
let mut sub = ResultBuilder::new();
build_function_subtree(c_body, bindings, idx, ctx_node, pos, size, &mut sub, static_ctx)?;
stringify(&sub.finish())
}
};
builder.push_comment(raw);
}
Instr::ProcessingInstruction { name, select, body: p_body } => {
let target = render_avt_static(name, bindings, idx, ctx_node, pos, size, static_ctx)?;
let data = match select {
Some(sel) => {
let v = eval_expr(sel, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
value_to_string_with(&v, idx, bindings)
}
None => {
let mut sub = ResultBuilder::new();
build_function_subtree(p_body, bindings, idx, ctx_node, pos, size, &mut sub, static_ctx)?;
stringify(&sub.finish())
}
};
builder.push_pi(target, data);
}
Instr::ForEach { select, body: fe_body, sort: _ } => {
let v = eval_expr(select, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
let items: Vec<Value> = match v {
Value::NodeSet(ns) => ns.into_iter()
.map(|id| Value::NodeSet(vec![id])).collect(),
Value::Sequence(items) => items,
other => vec![other],
};
let total = items.len();
for (i, item) in items.into_iter().enumerate() {
let cx = match &item {
Value::NodeSet(ns) if ns.len() == 1 => ns[0],
_ => ctx_node,
};
build_function_subtree(fe_body, bindings, idx, cx, i + 1, total, builder, static_ctx)?;
}
}
Instr::If { test, body: if_body } => {
let v = eval_expr(test, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
if value_to_bool(&v, idx) {
build_function_subtree(if_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
}
}
Instr::Choose { whens, otherwise } => {
let mut matched = false;
for (test, when_body) in whens {
let v = eval_expr(test, &mk_ctx(bindings, static_ctx, ctx_node, pos, size), idx)?;
if value_to_bool(&v, idx) {
build_function_subtree(when_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
matched = true;
break;
}
}
if !matched {
if let Some(else_body) = otherwise {
build_function_subtree(else_body, bindings, idx, ctx_node, pos, size, builder, static_ctx)?;
}
}
}
other => return Err(err(format!(
"xsl:function body: construction instruction `{}` is not yet supported",
instr_kind_name(other),
))),
}
}
Ok(())
}
fn render_avt_static<I: DocIndexLike>(
avt: &crate::ast::Avt,
bindings: &dyn XPathBindings,
idx: &I,
ctx_node: NodeId, pos: usize, size: usize,
static_ctx: &StaticContext,
) -> std::result::Result<String, sup_xml_core::error::XmlError> {
use sup_xml_core::xpath::eval::{eval_expr, value_to_string_with, EvalCtx};
if avt.is_literal() {
let mut s = String::new();
for part in &avt.parts {
if let AvtPart::Literal(lit) = part { s.push_str(lit); }
}
return Ok(s);
}
let ctx = EvalCtx { context_node: ctx_node, pos, size, bindings, static_ctx };
let mut out = String::new();
for part in &avt.parts {
match part {
AvtPart::Literal(s) => out.push_str(s),
AvtPart::Expr(e) => {
let v = eval_expr(e, &ctx, idx)?;
out.push_str(&value_to_string_with(&v, idx, bindings));
}
}
}
Ok(out)
}
fn copy_value_into_builder<I: DocIndexLike>(
builder: &mut ResultBuilder, idx: &I, v: &Value, style: NumStyle,
) {
match v {
Value::String(s) => builder.push_text(s.clone(), false),
Value::Boolean(b) => builder.push_text(if *b { "true".into() } else { "false".into() }, false),
Value::Number(n) => builder.push_text(format_numeric_styled(*n, style), false),
Value::Typed(t) => builder.push_text(t.lexical.clone(), false),
Value::NodeSet(ns) => {
let mut prev_was_atomic = false;
for &id in ns {
let is_atomic = sup_xml_core::xpath::is_synthetic_id(id);
if is_atomic && prev_was_atomic {
builder.push_text(" ".into(), false);
}
deep_copy_into_builder(builder, idx, id);
prev_was_atomic = is_atomic;
}
}
Value::ForeignNodeSet(_) => {}
Value::Sequence(items) => {
let mut prev_was_atomic = false;
for item in items {
let is_atomic = !matches!(item,
Value::NodeSet(_) | Value::ForeignNodeSet(_));
if is_atomic && prev_was_atomic {
builder.push_text(" ".into(), false);
}
copy_value_into_builder(builder, idx, item, style);
prev_was_atomic = is_atomic;
}
}
Value::IntRange { lo, hi } => {
let mut first = true;
for i in *lo..=*hi {
if !first { builder.push_text(" ".into(), false); }
builder.push_text(i.to_string(), false);
first = false;
}
}
Value::Map(_) | Value::Array(_) | Value::Function(_) => {}
}
}
fn deep_copy_into_builder<I: DocIndexLike>(
builder: &mut ResultBuilder, idx: &I, node: NodeId,
) {
use sup_xml_core::xpath::XPathNodeKind;
match idx.kind(node) {
XPathNodeKind::Element => {
let q = QName {
prefix: idx.namespace_prefix(node).map(str::to_string),
local: idx.local_name(node).to_string(),
uri: idx.namespace_uri(node).to_string(),
};
builder.open_element(q);
if let Some(t) = idx.rtf_node_type(node) {
builder.set_current_element_type(t);
}
for ns_id in idx.ns_range(node) {
let prefix = idx.local_name(ns_id);
if prefix == "xml" { continue; }
let p_opt = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
builder.push_namespace_decl(p_opt, idx.string_value(ns_id));
}
for attr in idx.attr_range(node) {
let aname = idx.node_name(attr);
if aname == "xmlns" || aname.starts_with("xmlns:") { continue; }
let aq = QName {
prefix: idx.namespace_prefix(attr).map(str::to_string),
local: idx.local_name(attr).to_string(),
uri: idx.namespace_uri(attr).to_string(),
};
if let Some(t) = idx.rtf_node_type(attr) {
builder.set_current_attr_type(aq.clone(), t);
}
builder.push_attribute(aq, idx.string_value(attr));
}
for &child in idx.children(node) {
deep_copy_into_builder(builder, idx, child);
}
builder.close_element();
}
XPathNodeKind::Text | XPathNodeKind::CData => {
builder.push_text(idx.string_value(node), false);
}
XPathNodeKind::Attribute => {
let aq = QName {
prefix: idx.namespace_prefix(node).map(str::to_string),
local: idx.local_name(node).to_string(),
uri: idx.namespace_uri(node).to_string(),
};
builder.push_attribute(aq, idx.string_value(node));
}
XPathNodeKind::Comment => builder.push_comment(idx.string_value(node)),
XPathNodeKind::PI => {
builder.push_pi(idx.pi_target(node).to_string(), idx.string_value(node));
}
XPathNodeKind::Document => {
for &c in idx.children(node) {
deep_copy_into_builder(builder, idx, c);
}
}
XPathNodeKind::Namespace => {}
}
}
pub(crate) fn rtf_children_into_index_generic<I: DocIndexLike>(
idx: &I,
nodes: &[ResultNode],
) -> Vec<sup_xml_core::xpath::NodeId> {
let Some(mut b) = idx.rtf_builder() else { return Vec::new(); };
let doc_root = b.add_document();
let initial_scope: NsScope = vec![
(Some("xml".into()), "http://www.w3.org/XML/1998/namespace".into()),
];
let mut child_ids = Vec::with_capacity(nodes.len());
for n in nodes {
let id = add_result_node_and_return_id(&mut b, doc_root, n, &initial_scope);
if let Some(id) = id { child_ids.push(id); }
}
let _ = idx.finish_rtf(b);
child_ids
}
fn evaluate_with_params(
state: &mut EvalState,
params: &[WithParam],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<Vec<(QName, Value, Option<Vec<ResultNode>>)>> {
let mut out = Vec::with_capacity(params.len());
for p in params {
if let Some(sel) = &p.select {
let mut v = state.xpath_eval(sel, ctx_node, pos, size)?;
if let Some(t) = &p.as_type {
if let Some(typed) = coerce_to_user_schema_type(&v, t, state.style, state.idx) {
v = typed;
} else if let Some(st) = parse_as_atomic_type(t) {
v = coerce_to_atomic_sequence(v, &st, state.idx)?;
}
}
if p.tunnel { state.tunnel_pool.insert(qname_key(&p.name), v); }
else { out.push((p.name.clone(), v, None)); }
} else if p.body.is_empty() {
let v = Value::String(String::new());
if p.tunnel { state.tunnel_pool.insert(qname_key(&p.name), v); }
else { out.push((p.name.clone(), v, None)); }
} else {
if p.as_type.as_deref().map(as_is_nonnode_item_type).unwrap_or(false) {
state.sequence_sinks.push(Vec::new());
let res = eval_body(state, &p.body, ctx_node, pos, size);
let captured = state.sequence_sinks.pop().unwrap_or_default();
res?;
let v = if captured.len() == 1 {
captured.into_iter().next().unwrap()
} else {
Value::Sequence(captured)
};
if p.tunnel { state.tunnel_pool.insert(qname_key(&p.name), v); }
else { out.push((p.name.clone(), v, None)); }
continue;
}
let want_no_merge = p.as_type.as_deref()
.map(|t| as_is_sequence_typed(t) && !as_target_is_atomic(t))
.unwrap_or(false);
if want_no_merge {
let nodes = build_rtf_nodes_no_merge(state, &p.body, ctx_node, pos, size)?;
let child_ids = rtf_children_into_index(state.idx, &nodes);
let value = Value::NodeSet(child_ids);
if p.tunnel {
state.tunnel_pool.insert(qname_key(&p.name), value);
} else {
out.push((p.name.clone(), value, Some(nodes)));
}
} else {
let nodes = build_rtf_nodes(state, &p.body, ctx_node, pos, size)?;
let s = stringify(&nodes);
let mut value = Value::String(s);
if let Some(t) = &p.as_type {
if as_target_is_atomic(t) {
if let Some(st) = parse_as_atomic_type(t) {
value = coerce_to_atomic_sequence(value, &st, state.idx)?;
}
}
}
if p.tunnel {
state.tunnel_pool.insert(qname_key(&p.name), value);
} else {
out.push((p.name.clone(), value, Some(nodes)));
}
}
};
}
Ok(out)
}
fn apply_one_to_node_with_args(
state: &mut EvalState,
node: NodeId,
mode: Option<&QName>,
pos: usize,
size: usize,
args: &[(QName, Value, Option<Vec<ResultNode>>)],
) -> Result<()> {
let mut bindings = state.bindings();
bindings.xslt_context_node = node;
let chosen = pattern::select_template(
state.style, node, mode, state.idx, &bindings,
).map_err(XsltError::from)?;
match chosen {
Some(sel) => {
state.template_call_depth += 1;
if state.template_call_depth > MAX_TEMPLATE_CALL_DEPTH {
state.template_call_depth -= 1;
return Err(XsltError::InvalidStylesheet(format!(
"xsl:apply-templates depth exceeds limit \
({MAX_TEMPLATE_CALL_DEPTH}) — possible infinite recursion"
)));
}
let prev = state.apply_imports_ctx.replace(
(node, mode.cloned(), sel.template.import_precedence,
template_index_of(state.style, sel.template),
sel.priority, sel.branch_idx),
);
let r = run_template_body(state, sel.template, node, pos, size, args);
state.apply_imports_ctx = prev;
state.template_call_depth -= 1;
r
}
None => apply_builtin_template_with_args(state, node, mode, args),
}
}
fn render_avt(
state: &mut EvalState, avt: &Avt,
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<String> {
if avt.is_literal() {
let mut s = String::new();
for part in &avt.parts {
if let AvtPart::Literal(lit) = part { s.push_str(lit); }
}
return Ok(s);
}
let mut out = String::new();
for part in &avt.parts {
match part {
AvtPart::Literal(s) => out.push_str(s),
AvtPart::Expr(e) => {
let bc = matches!(e, sup_xml_core::xpath::Expr::BackwardsCompat(_));
let v = state.xpath_eval(e, ctx_node, pos, size)?;
if bc {
out.push_str(&value_to_string_styled(&v, state.idx, state.num_style()));
continue;
}
let items = sequence_string_items(&v, state.idx, state.num_style());
if items.len() > 1 {
out.push_str(&items.join(" "));
} else {
out.push_str(&value_to_string_styled(&v, state.idx, state.num_style()));
}
}
}
}
Ok(out)
}
fn compute_number_list<'a>(
state: &mut EvalState<'a>,
level: crate::ast::NumberLevel,
count: Option<&sup_xml_core::xpath::Expr>,
from: Option<&sup_xml_core::xpath::Expr>,
ctx_node: NodeId,
) -> Result<Vec<i64>> {
let idx = state.idx;
let bindings_owned = state.bindings();
compute_number_list_generic(
idx, &bindings_owned, level, count, from, ctx_node,
)
}
fn compute_number_list_generic<I: DocIndexLike>(
idx: &I,
bindings: &dyn XPathBindings,
level: crate::ast::NumberLevel,
count: Option<&sup_xml_core::xpath::Expr>,
from: Option<&sup_xml_core::xpath::Expr>,
ctx_node: NodeId,
) -> Result<Vec<i64>> {
use crate::ast::NumberLevel as L;
use crate::number::{CountMatcher, count_level_any, count_level_multiple, resolve_from_root};
let mut eval_pat = |expr: &sup_xml_core::xpath::Expr, n: NodeId| -> bool {
pattern::matches(expr, n, idx, bindings).unwrap_or(false)
};
let matcher = CountMatcher::new(ctx_node, count, idx);
let from_root = resolve_from_root(ctx_node, from, idx, &mut eval_pat);
Ok(match level {
L::Single => {
let mut cur = Some(ctx_node);
let mut target: Option<NodeId> = None;
while let Some(n) = cur {
if matcher.matches(n, idx, &mut eval_pat) {
target = Some(n);
break;
}
if n == from_root { break; }
cur = idx.parent(n);
}
match target {
Some(n) => vec![sibling_position_generic(n, &matcher, idx, &mut eval_pat)],
None => Vec::new(),
}
}
L::Any => {
match count_level_any(ctx_node, from_root, &matcher, from, idx, &mut eval_pat) {
Some(0) | None => Vec::new(),
Some(n) => vec![n],
}
}
L::Multiple => count_level_multiple(ctx_node, from_root, &matcher, idx, &mut eval_pat),
})
}
fn sibling_position_generic<I: DocIndexLike, F>(
node: NodeId,
matcher: &crate::number::CountMatcher<'_>,
idx: &I,
eval: &mut F,
) -> i64
where F: FnMut(&sup_xml_core::xpath::Expr, NodeId) -> bool,
{
let Some(parent) = idx.parent(node) else { return 1; };
let mut pos: i64 = 0;
for &sib in idx.children(parent) {
if matcher.matches(sib, idx, eval) { pos += 1; }
if sib == node { return pos.max(1); }
}
1
}
fn apply_attribute_sets<'a>(
state: &mut EvalState<'a>,
names: &[QName],
ctx_node: NodeId,
pos: usize,
size: usize,
) -> Result<()> {
if names.is_empty() { return Ok(()); }
let mut visiting: Vec<String> = Vec::new();
for name in names {
apply_attribute_set_one(state, name, ctx_node, pos, size, &mut visiting)?;
}
Ok(())
}
fn apply_attribute_set_one<'a>(
state: &mut EvalState<'a>,
name: &QName,
ctx_node: NodeId,
pos: usize,
size: usize,
visiting: &mut Vec<String>,
) -> Result<()> {
let key = qname_key(name);
if visiting.iter().any(|k| k == &key) {
return Err(XsltError::InvalidStylesheet(format!(
"<xsl:attribute-set> cycle detected at '{key}'"
)));
}
let mut sets: Vec<_> = state.style.attribute_sets.iter()
.filter(|s| qname_key(&s.name) == key)
.cloned()
.collect();
sets.sort_by_key(|s| s.import_precedence);
if sets.is_empty() {
return Err(XsltError::UnresolvedReference(format!(
"no <xsl:attribute-set> named '{key}'"
)));
}
visiting.push(key);
let saved_frames = std::mem::take(&mut state.variables.frames);
if let Some(globals) = saved_frames.first().cloned() {
state.variables.frames.push(globals);
}
let result: Result<()> = (|| {
for set in &sets {
for inner in &set.use_attribute_sets {
apply_attribute_set_one(state, inner, ctx_node, pos, size, visiting)?;
}
for instr in &set.attributes {
eval_instr(state, instr, ctx_node, pos, size)?;
}
}
Ok(())
})();
state.variables.frames = saved_frames;
visiting.pop();
result
}
fn qname_key(q: &QName) -> String {
if q.uri.is_empty() { q.local.clone() }
else { format!("{{{uri}}}{local}", uri = q.uri, local = q.local) }
}
fn is_lexical_qname_str(s: &str) -> bool {
match s.split_once(':') {
Some((p, l)) => is_ncname_str(p) && is_ncname_str(l),
None => is_ncname_str(s),
}
}
fn is_ncname_str(s: &str) -> bool {
if s.is_empty() { return false; }
let mut cs = s.chars();
let first = cs.next().unwrap();
if !(first.is_alphabetic() || first == '_') { return false; }
cs.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
}
fn value_to_bool(v: &Value) -> bool {
match v {
Value::Boolean(b) => *b,
Value::Number(n) => n.as_f64() != 0.0 && !n.as_f64().is_nan(),
Value::String(s) => !s.is_empty(),
Value::NodeSet(n) => !n.is_empty(),
Value::ForeignNodeSet(n) => !n.is_empty(),
Value::Typed(t) => {
if let Some(b) = t.boolean { return b; }
if let Some(n) = t.numeric { return n != 0.0 && !n.is_nan(); }
!t.lexical.is_empty()
}
Value::Sequence(items) => match items.first() {
None => false,
Some(v) => value_to_bool(v),
}
Value::IntRange { lo, hi } if lo == hi => *lo != 0,
Value::IntRange { .. } => true,
Value::Map(_) | Value::Array(_) | Value::Function(_) => true,
}
}
pub(crate) fn value_to_number_xpath<I: DocIndexLike>(v: &Value, idx: &I) -> f64 {
sup_xml_core::xpath::eval::value_to_number(v, idx)
}
fn sequence_string_items<I: DocIndexLike>(v: &Value, idx: &I, style: NumStyle) -> Vec<String> {
match v {
Value::NodeSet(ns) => ns.iter().map(|&id| idx.string_value(id)).collect(),
Value::ForeignNodeSet(ns) => ns.iter()
.map(|&p| sup_xml_tree::dom::Document::node_string_value_by_ptr(p))
.collect(),
Value::String(s) => vec![s.clone()],
Value::Number(n) => vec![format_numeric_styled(*n, style)],
Value::Boolean(b) => vec![(if *b { "true" } else { "false" }).to_string()],
Value::Typed(t) => vec![t.lexical.clone()],
Value::Sequence(items) => items.iter()
.flat_map(|item| sequence_string_items(item, idx, style))
.collect(),
Value::IntRange { lo, hi } => (*lo..=*hi).map(|i| i.to_string()).collect(),
Value::Map(_) | Value::Array(_) | Value::Function(_) => Vec::new(),
}
}
fn result_node_is_significant(n: &ResultNode) -> bool {
match n {
ResultNode::Text { content, .. } => !content.is_empty(),
ResultNode::Element { attributes, children, .. } =>
!attributes.is_empty() || !children.is_empty(),
ResultNode::Attribute { .. }
| ResultNode::Comment(_)
| ResultNode::ProcessingInstruction { .. } => true,
}
}
fn build_rtf_nodes(
state: &mut EvalState, body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<Vec<ResultNode>> {
let mut tmp = EvalState {
style: state.style, idx: state.idx, namespaces: state.namespaces,
keys: state.keys,
documents: state.documents,
xslt_current: state.xslt_current,
rtf_base_uris: state.rtf_base_uris,
static_ctx: state.static_ctx,
variables: std::mem::take(&mut state.variables),
rtfs: std::mem::take(&mut state.rtfs),
rtf_scopes: std::mem::take(&mut state.rtf_scopes),
builder: ResultBuilder::new(),
principal_buf: None,
unparsed_entities: state.unparsed_entities.clone(),
source_doc: state.source_doc,
source_types: state.source_types,
apply_imports_ctx: state.apply_imports_ctx.clone(),
user_exts: state.user_exts,
sequence_sinks: std::mem::take(&mut state.sequence_sinks),
template_call_depth: state.template_call_depth,
current_group: std::mem::take(&mut state.current_group),
regex_groups: std::mem::take(&mut state.regex_groups),
tunnel_pool: std::mem::take(&mut state.tunnel_pool),
current_grouping_key: state.current_grouping_key.take(),
accumulators: std::mem::take(&mut state.accumulators),
unparsed_texts: state.unparsed_texts,
static_base_uri: state.static_base_uri.clone(),
loader: state.loader,
loader_base: state.loader_base,
dyn_doc_cache: state.dyn_doc_cache,
};
let r = {
let _tmp_out = TempOutputGuard::enter();
eval_body(&mut tmp, body, ctx_node, pos, size)
};
state.variables = tmp.variables;
state.rtfs = tmp.rtfs;
state.rtf_scopes = tmp.rtf_scopes;
state.sequence_sinks = tmp.sequence_sinks;
state.current_group = tmp.current_group;
state.regex_groups = tmp.regex_groups;
state.tunnel_pool = tmp.tunnel_pool;
state.current_grouping_key = tmp.current_grouping_key;
state.accumulators = tmp.accumulators;
r?;
Ok(tmp.builder.finish())
}
fn precompute_accumulators(state: &mut EvalState, root: NodeId) -> Result<()> {
let decls = state.style.accumulators.clone();
for decl in &decls {
let key = qname_key(&decl.name);
let initial = state.xpath_eval(&decl.initial_value, root, 1, 1)?;
let mut data = AccumulatorData {
before: HashMap::new(), after: HashMap::new(), initial: initial.clone(),
};
let mut value = initial;
accumulate_walk(state, decl, root, &mut value, &mut data)?;
state.accumulators.insert(key, data);
}
Ok(())
}
fn accumulate_walk(
state: &mut EvalState,
decl: &crate::ast::AccumulatorDecl,
node: NodeId,
value: &mut Value,
data: &mut AccumulatorData,
) -> Result<()> {
use crate::ast::AccumulatorPhase::{Start, End};
apply_accum_rule(state, decl, node, Start, value)?;
data.before.insert(node, value.clone());
for a in state.idx.attr_range(node).collect::<Vec<_>>() {
apply_accum_rule(state, decl, a, Start, value)?;
data.before.insert(a, value.clone());
apply_accum_rule(state, decl, a, End, value)?;
data.after.insert(a, value.clone());
}
for c in state.idx.children(node).to_vec() {
accumulate_walk(state, decl, c, value, data)?;
}
apply_accum_rule(state, decl, node, End, value)?;
data.after.insert(node, value.clone());
Ok(())
}
fn apply_accum_rule(
state: &mut EvalState,
decl: &crate::ast::AccumulatorDecl,
node: NodeId,
phase: crate::ast::AccumulatorPhase,
value: &mut Value,
) -> Result<()> {
let matched = {
let b = state.bindings();
let mut found = None;
for (i, rule) in decl.rules.iter().enumerate() {
if rule.phase != phase { continue; }
if pattern::matches(&rule.match_pattern, node, state.idx, &b)
.map_err(XsltError::from)?
{
found = Some(i);
break;
}
}
found
};
let Some(i) = matched else { return Ok(()) };
let rule = &decl.rules[i];
state.variables.enter();
state.variables.bind("value".to_string(), value.clone());
let nv = match &rule.select {
Some(e) => state.xpath_eval(e, node, 1, 1),
None => build_rtf_nodes(state, &rule.body, node, 1, 1)
.map(|ns| Value::NodeSet(rtf_children_into_index(state.idx, &ns))),
};
state.variables.leave();
*value = nv?;
Ok(())
}
fn rtf_scope_enter(state: &mut EvalState) {
state.rtf_scopes.push(Vec::new());
}
fn rtf_scope_leave(state: &mut EvalState) {
if let Some(keys) = state.rtf_scopes.pop() {
for k in keys { state.rtfs.remove(&k); }
}
}
fn rtf_into_index<'a>(
idx: &'a sup_xml_core::xpath::DocIndex<'a>,
nodes: &[ResultNode],
) -> sup_xml_core::xpath::NodeId {
let mut b = idx.start_rtf();
let doc_root = b.add_document();
let initial_scope: NsScope = vec![
(Some("xml".into()), "http://www.w3.org/XML/1998/namespace".into()),
];
for n in nodes {
add_result_node(&mut b, doc_root, n, &initial_scope);
}
idx.finish_rtf(b)
}
type NsScope = Vec<(Option<String>, String)>;
fn ns_scope_extend(
parent_scope: &NsScope,
locals: &[(Option<String>, String)],
) -> NsScope {
let mut scope = parent_scope.clone();
for (prefix, uri) in locals {
if let Some(existing) = scope.iter_mut().find(|(p, _)| p == prefix) {
existing.1 = uri.clone();
} else {
scope.push((prefix.clone(), uri.clone()));
}
}
scope
}
fn add_result_node_and_return_id(
b: &mut sup_xml_core::xpath::rtf::RtfBuilder,
parent: sup_xml_core::xpath::NodeId,
n: &ResultNode,
ns_scope: &NsScope,
) -> Option<sup_xml_core::xpath::NodeId> {
match n {
ResultNode::Text { content, .. } => Some(b.add_text(parent, content)),
ResultNode::Comment(s) => Some(b.add_comment(parent, s)),
ResultNode::ProcessingInstruction { target, data } =>
Some(b.add_pi(parent, target, data)),
ResultNode::Attribute { name, value } => {
let prefix = name.prefix.as_deref();
let qname = match prefix {
Some(p) if !p.is_empty() => format!("{p}:{}", name.local),
_ => name.local.clone(),
};
let owner = b.add_element(parent, "_attr-owner", "", None);
b.start_attrs(owner);
Some(b.add_attribute(owner, &qname, &name.uri, prefix, value))
}
ResultNode::Element { name, attributes, namespaces, children, schema_type, attr_types } => {
let prefix_str = name.prefix.as_deref();
let qname = match prefix_str {
Some(p) if !p.is_empty() => format!("{p}:{}", name.local),
_ => name.local.clone(),
};
let elem = b.add_element(parent, &qname, &name.uri, prefix_str);
if let Some(t) = schema_type { b.typed_nodes.push((elem, t.clone())); }
if !attributes.is_empty() {
b.start_attrs(elem);
for (aname, value) in attributes {
let aprefix = aname.prefix.as_deref();
let aq = match aprefix {
Some(p) if !p.is_empty() => format!("{p}:{}", aname.local),
_ => aname.local.clone(),
};
let aid = b.add_attribute(elem, &aq, &aname.uri, aprefix, value);
if let Some((_, t)) = attr_types.iter()
.find(|(n, _)| n.uri == aname.uri && n.local == aname.local)
{
b.typed_nodes.push((aid, t.clone()));
}
}
}
let mut element_scope = ns_scope_extend(ns_scope, namespaces);
if !name.uri.is_empty() {
let target_prefix = prefix_str.filter(|p| !p.is_empty()).map(str::to_string);
if !element_scope.iter().any(|(p, _)| p == &target_prefix) {
element_scope.push((target_prefix, name.uri.clone()));
}
}
let visible: Vec<&(Option<String>, String)> = element_scope.iter()
.filter(|(_, uri)| !uri.is_empty())
.collect();
if !visible.is_empty() {
b.start_ns(elem);
for (prefix, uri) in &visible {
b.add_namespace_node(elem, prefix.as_deref(), uri);
}
}
for c in children {
add_result_node(b, elem, c, &element_scope);
}
Some(elem)
}
}
}
fn add_result_node(
b: &mut sup_xml_core::xpath::rtf::RtfBuilder,
parent: sup_xml_core::xpath::NodeId,
n: &ResultNode,
ns_scope: &NsScope,
) {
match n {
ResultNode::Text { content, .. } => {
b.add_text(parent, content);
}
ResultNode::Comment(s) => {
b.add_comment(parent, s);
}
ResultNode::ProcessingInstruction { target, data } => {
b.add_pi(parent, target, data);
}
ResultNode::Attribute { name, value } => {
let prefix = name.prefix.as_deref();
let qname = match prefix {
Some(p) if !p.is_empty() => format!("{p}:{}", name.local),
_ => name.local.clone(),
};
b.add_attribute(parent, &qname, &name.uri, prefix, value);
}
ResultNode::Element { name, attributes, namespaces, children, schema_type, attr_types } => {
let prefix_str = name.prefix.as_deref();
let qname = match prefix_str {
Some(p) if !p.is_empty() => format!("{p}:{}", name.local),
_ => name.local.clone(),
};
let elem = b.add_element(parent, &qname, &name.uri, prefix_str);
if let Some(t) = schema_type { b.typed_nodes.push((elem, t.clone())); }
if !attributes.is_empty() {
b.start_attrs(elem);
for (aname, value) in attributes {
let aprefix = aname.prefix.as_deref();
let aq = match aprefix {
Some(p) if !p.is_empty() => format!("{p}:{}", aname.local),
_ => aname.local.clone(),
};
let aid = b.add_attribute(elem, &aq, &aname.uri, aprefix, value);
if let Some((_, t)) = attr_types.iter()
.find(|(n, _)| n.uri == aname.uri && n.local == aname.local)
{
b.typed_nodes.push((aid, t.clone()));
}
}
}
let element_scope = ns_scope_extend(ns_scope, namespaces);
let mut element_scope = element_scope;
if !name.uri.is_empty() {
let target_prefix = prefix_str.filter(|p| !p.is_empty()).map(str::to_string);
if !element_scope.iter().any(|(p, _)| p == &target_prefix) {
element_scope.push((target_prefix, name.uri.clone()));
}
}
let visible: Vec<&(Option<String>, String)> = element_scope.iter()
.filter(|(_, uri)| !uri.is_empty())
.collect();
if !visible.is_empty() {
b.start_ns(elem);
for (prefix, uri) in &visible {
b.add_namespace_node(elem, prefix.as_deref(), uri);
}
}
for c in children {
add_result_node(b, elem, c, &element_scope);
}
}
}
}
fn store_rtf(state: &mut EvalState, key: &str, nodes: Vec<ResultNode>) {
state.rtfs.insert(key.to_string(), nodes);
if let Some(scope) = state.rtf_scopes.last_mut() {
scope.push(key.to_string());
}
}
fn copy_result_node_into(state: &mut EvalState, node: &ResultNode) {
match node {
ResultNode::Element { name, namespaces, attributes, children, schema_type, attr_types } => {
state.builder.open_element(name.clone());
if let Some(t) = schema_type {
state.builder.set_current_element_type((**t).clone());
}
for (p, u) in namespaces {
state.builder.push_namespace_decl(p.clone(), u.clone());
}
for (an, v) in attributes {
if let Some((_, t)) = attr_types.iter()
.find(|(n, _)| n.uri == an.uri && n.local == an.local)
{
state.builder.set_current_attr_type(an.clone(), (**t).clone());
}
state.builder.push_attribute(an.clone(), v.clone());
}
for c in children { copy_result_node_into(state, c); }
state.builder.close_element();
}
ResultNode::Text { content, dose } => {
state.builder.push_text(content.clone(), *dose);
}
ResultNode::Comment(s) => {
state.builder.push_comment(s.clone());
}
ResultNode::ProcessingInstruction { target, data } => {
state.builder.push_pi(target.clone(), data.clone());
}
ResultNode::Attribute { name, value } => {
state.builder.push_attribute(name.clone(), value.clone());
}
}
}
fn stringify_into_string(
state: &mut EvalState, body: &[Instr],
ctx_node: NodeId, pos: usize, size: usize,
) -> Result<String> {
let mut tmp = EvalState {
style: state.style, idx: state.idx, namespaces: state.namespaces,
keys: state.keys,
documents: state.documents,
xslt_current: state.xslt_current,
rtf_base_uris: state.rtf_base_uris,
static_ctx: state.static_ctx,
variables: std::mem::take(&mut state.variables),
rtfs: std::mem::take(&mut state.rtfs),
rtf_scopes: std::mem::take(&mut state.rtf_scopes),
builder: ResultBuilder::new(),
principal_buf: None,
unparsed_entities: state.unparsed_entities.clone(),
source_doc: state.source_doc,
source_types: state.source_types,
apply_imports_ctx: state.apply_imports_ctx.clone(),
user_exts: state.user_exts,
sequence_sinks: std::mem::take(&mut state.sequence_sinks),
template_call_depth: state.template_call_depth,
current_group: std::mem::take(&mut state.current_group),
regex_groups: std::mem::take(&mut state.regex_groups),
tunnel_pool: std::mem::take(&mut state.tunnel_pool),
current_grouping_key: state.current_grouping_key.take(),
accumulators: std::mem::take(&mut state.accumulators),
unparsed_texts: state.unparsed_texts,
static_base_uri: state.static_base_uri.clone(),
loader: state.loader,
loader_base: state.loader_base,
dyn_doc_cache: state.dyn_doc_cache,
};
let r = {
let _tmp_out = TempOutputGuard::enter();
eval_body(&mut tmp, body, ctx_node, pos, size)
};
state.variables = tmp.variables;
state.rtfs = tmp.rtfs;
state.rtf_scopes = tmp.rtf_scopes;
state.sequence_sinks = tmp.sequence_sinks;
state.current_group = tmp.current_group;
state.regex_groups = tmp.regex_groups;
state.tunnel_pool = tmp.tunnel_pool;
state.current_grouping_key = tmp.current_grouping_key;
r?;
Ok(stringify(&tmp.builder.finish()))
}
fn stringify(nodes: &[ResultNode]) -> String {
let mut out = String::new();
for n in nodes { append_string_value(n, &mut out); }
out
}
fn collect_value_of_body_pieces(
state: &mut EvalState, body: &[Instr], ctx_node: NodeId,
pos: usize, size: usize,
) -> Result<Vec<String>> {
let mut pieces: Vec<String> = Vec::new();
let mut text_buf = String::new();
let flush = |pieces: &mut Vec<String>, text_buf: &mut String| {
if !text_buf.is_empty() {
pieces.push(std::mem::take(text_buf));
}
};
for instr in body {
state.sequence_sinks.push(Vec::new());
let nodes = build_rtf_nodes(state, std::slice::from_ref(instr),
ctx_node, pos, size)?;
let captured = state.sequence_sinks.pop().unwrap_or_default();
for n in &nodes {
match n {
ResultNode::Text { content, .. } => text_buf.push_str(content),
ResultNode::Element { .. } => {
flush(&mut pieces, &mut text_buf);
let mut s = String::new();
append_string_value(n, &mut s);
pieces.push(s);
}
ResultNode::Attribute { value, .. } => {
flush(&mut pieces, &mut text_buf);
pieces.push(value.clone());
}
ResultNode::Comment(_) | ResultNode::ProcessingInstruction { .. } => {}
}
}
if !captured.is_empty() {
flush(&mut pieces, &mut text_buf);
for v in captured {
let items = sequence_string_items(&v, state.idx, state.num_style());
pieces.extend(items.into_iter().filter(|s| !s.is_empty()));
}
}
}
flush(&mut pieces, &mut text_buf);
Ok(pieces.into_iter().filter(|s| !s.is_empty()).collect())
}
fn append_string_value(node: &ResultNode, out: &mut String) {
match node {
ResultNode::Text { content, .. } => out.push_str(content),
ResultNode::Element { children, .. } => {
for c in children { append_string_value(c, out); }
}
ResultNode::Attribute { value, .. } => out.push_str(value),
ResultNode::Comment(_) | ResultNode::ProcessingInstruction { .. } => {}
}
}
fn copy_value_into(state: &mut EvalState, v: &Value, copy_ns: bool) -> Result<()> {
match v {
Value::String(s) => { state.builder.push_atomic_text(s.clone()); }
Value::Boolean(b) => { state.builder.push_atomic_text(if *b { "true".into() } else { "false".into() }); }
Value::Number(n) => { state.builder.push_atomic_text(format_numeric_styled(*n, state.num_style())); }
Value::Typed(t) => { state.builder.push_atomic_text(t.lexical.clone()); }
Value::NodeSet(ns) => {
let mut prev_was_atomic = false;
for &id in ns {
let is_atomic = sup_xml_core::xpath::is_synthetic_id(id);
if is_atomic && prev_was_atomic {
state.builder.push_text(" ".into(), false);
}
deep_copy_node(state, id, None, copy_ns)?;
prev_was_atomic = is_atomic;
}
}
Value::ForeignNodeSet(_) => {}
Value::Sequence(items) => {
let mut prev_was_atomic = false;
for item in items {
let is_atomic = !matches!(item,
Value::NodeSet(_) | Value::ForeignNodeSet(_));
if is_atomic && prev_was_atomic {
state.builder.push_text(" ".into(), false);
}
copy_value_into(state, item, copy_ns)?;
prev_was_atomic = is_atomic;
}
}
Value::IntRange { lo, hi } => {
let mut first = true;
for i in *lo..=*hi {
if !first { state.builder.push_text(" ".into(), false); }
state.builder.push_text(i.to_string(), false);
first = false;
}
}
Value::Map(_) | Value::Array(_) | Value::Function(_) => {}
}
Ok(())
}
fn deep_copy_node(state: &mut EvalState, node: NodeId, mirrored_parent: Option<NodeId>, copy_ns: bool) -> Result<()> {
match state.idx.kind(node) {
XPathNodeKind::Element => {
let q = element_qname(state, node);
state.builder.open_element(q.clone());
let mut child_has_default = false;
if copy_ns {
for ns_id in state.idx.ns_range(node) {
let prefix = state.idx.local_name(ns_id);
let uri = state.idx.string_value(ns_id);
if prefix == "xml" { continue; }
if prefix.is_empty() { child_has_default = true; }
let p_opt = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
state.builder.push_namespace_decl(p_opt, uri);
}
} else {
if !q.uri.is_empty() {
state.builder.push_namespace_decl(q.prefix.clone(), q.uri.clone());
if q.prefix.is_none() { child_has_default = true; }
}
}
if let Some(parent_id) = mirrored_parent {
if !child_has_default {
let parent_had_default = state.idx.ns_range(parent_id)
.into_iter()
.any(|nid| state.idx.local_name(nid).is_empty()
&& !state.idx.string_value(nid).is_empty());
if parent_had_default {
state.builder.push_namespace_decl(None, String::new());
}
}
}
for attr in state.idx.attr_range(node) {
let aname = state.idx.node_name(attr);
if aname == "xmlns" || aname.starts_with("xmlns:") { continue; }
let aq = attribute_qname(state, attr);
if !copy_ns && !aq.uri.is_empty() && aq.prefix.is_some() {
state.builder.push_namespace_decl(aq.prefix.clone(), aq.uri.clone());
}
state.builder.push_attribute(aq, state.idx.string_value(attr));
}
for &child in state.idx.children(node) {
deep_copy_node(state, child, Some(node), copy_ns)?;
}
state.builder.close_element();
}
XPathNodeKind::Text | XPathNodeKind::CData => {
state.builder.push_text(state.idx.string_value(node), false);
}
XPathNodeKind::Attribute => {
let q = attribute_qname(state, node);
state.builder.push_attribute(q, state.idx.string_value(node));
}
XPathNodeKind::Comment => {
state.builder.push_comment(state.idx.string_value(node));
}
XPathNodeKind::PI => {
state.builder.push_pi(
state.idx.pi_target(node).to_string(),
state.idx.string_value(node),
);
}
XPathNodeKind::Document => {
for &c in state.idx.children(node) {
deep_copy_node(state, c, Some(node), copy_ns)?;
}
}
XPathNodeKind::Namespace => {
let prefix = state.idx.local_name(node);
if prefix != "xml" {
let uri = state.idx.string_value(node);
let p = if prefix.is_empty() { None } else { Some(prefix.to_string()) };
state.builder.push_namespace_decl(p, uri);
}
}
}
Ok(())
}
fn element_qname(state: &EvalState, node: NodeId) -> QName {
let local = state.idx.local_name(node).to_string();
let prefix = state.idx.namespace_prefix(node).map(str::to_string);
let uri = state.idx.namespace_uri(node).to_string();
QName { prefix, local, uri }
}
fn attribute_qname(state: &EvalState, node: NodeId) -> QName {
let local = state.idx.local_name(node).to_string();
let prefix = state.idx.namespace_prefix(node).map(str::to_string);
let uri = state.idx.namespace_uri(node).to_string();
QName { prefix, local, uri }
}
fn apply_namespace_alias(state: &EvalState, name: &QName) -> QName {
for (style_uri, result_uri, result_prefix) in &state.style.namespace_aliases {
if name.uri == *style_uri {
let new_prefix = result_prefix.clone().or_else(|| {
state.namespaces.map.iter()
.find(|(_, u)| u.as_str() == result_uri)
.and_then(|(p, _)| {
if p.is_empty() { None } else { Some(p.clone()) }
})
});
return QName {
prefix: new_prefix,
local: name.local.clone(),
uri: result_uri.clone(),
};
}
}
name.clone()
}
fn split_qname(s: &str) -> (Option<String>, String) {
match s.split_once(':') {
Some((p, l)) => (Some(p.to_string()), l.to_string()),
None => (None, s.to_string()),
}
}
#[allow(dead_code)]
fn _force_inode_kind_import(_: INodeKind) {}
#[allow(dead_code)] fn collect_candidate_uris(
doc: &Document,
out: &mut Vec<String>,
seen: &mut std::collections::HashSet<String>,
) {
fn walk(
node: &sup_xml_tree::dom::Node,
out: &mut Vec<String>,
seen: &mut std::collections::HashSet<String>,
) {
use sup_xml_tree::dom::NodeKind as K;
match node.kind {
K::Element => {
for attr in node.attributes() {
let v = attr.value().trim().to_string();
if !v.is_empty() && seen.insert(v.clone()) {
out.push(v);
}
}
for child in node.children() {
walk(child, out, seen);
}
}
K::Text | K::CData => {
let v = node.content().trim().to_string();
if !v.is_empty() && seen.insert(v.clone()) {
out.push(v);
}
}
_ => {}
}
}
walk(doc.root(), out, seen);
}
fn apply_strip_space(style: &StylesheetAst, idx: &mut DocIndex) {
if style.whitespace_rules.is_empty() { return; }
let nlen = idx.nodes.len();
for id in 0..nlen {
if !matches!(idx.nodes[id].kind, INodeKind::Element(_)) { continue; }
let mut filtered: Vec<NodeId> = Vec::with_capacity(idx.nodes[id].content_children.len());
let kept: Vec<NodeId> = idx.nodes[id].content_children.clone();
for child in kept {
if !crate::whitespace::should_strip(style, child, idx) {
filtered.push(child);
}
}
idx.nodes[id].content_children = filtered;
}
}
fn template_index_of(style: &StylesheetAst, t: &crate::ast::Template) -> Option<usize> {
style.templates.iter().position(|x| std::ptr::eq(x, t))
}
fn regex_has_reluctant_quantifier(src: &str) -> bool {
let bytes = src.as_bytes();
let mut in_class = false;
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
match b {
b'\\' => { i += 2; continue; } b'[' if !in_class => { in_class = true; }
b']' if in_class => { in_class = false; }
b'?' if !in_class && i > 0 => {
let prev = bytes[i - 1];
if matches!(prev, b'*' | b'+' | b'?' | b'}') {
return true;
}
}
_ => {}
}
i += 1;
}
false
}
fn matching_body_uses_regex_group(body: &[Instr]) -> bool {
if crate::walk::body_invokes_templates(body) { return true; }
let mut hit = false;
crate::walk::walk_body(body, &mut |e| {
use sup_xml_core::xpath::ast::Expr;
if let Expr::FunctionCall(name, _) = e {
if name == "regex-group" || name.ends_with(":regex-group") { hit = true; }
}
});
hit
}
fn run_analyze_partition(
state: &mut EvalState,
matching: &[Instr],
non_matching:&[Instr],
segments: &[(bool, String, Vec<String>)],
ctx_node: NodeId,
) -> Result<()> {
let total = segments.len();
for (i, (is_match, text, groups)) in segments.iter().enumerate() {
let body = if *is_match { matching } else { non_matching };
if body.is_empty() { continue; }
run_analyze_segment(state, body, text, groups, ctx_node, i + 1, total)?;
}
Ok(())
}
fn run_analyze_segment(
state: &mut EvalState,
body: &[Instr],
segment: &str,
groups: &[String],
_ctx_node: NodeId,
pos: usize, size: usize,
) -> Result<()> {
let synth = state.idx
.allocate_rtf_text_nodes_inherent(vec![segment.to_string()]);
let leader = *synth.first().unwrap_or(&_ctx_node);
let prev_groups = std::mem::take(&mut state.regex_groups);
state.regex_groups = groups.to_vec();
let prev_current = state.xslt_current;
state.xslt_current = leader;
let r = eval_body(state, body, leader, pos, size);
state.xslt_current = prev_current;
state.regex_groups = prev_groups;
r
}
fn run_for_each_typed_sequence(
state: &mut EvalState,
items: Vec<sup_xml_core::xpath::eval::Value>,
sorts: &[crate::ast::Sort],
body: &[Instr],
ctx_node: NodeId,
_pos: usize,
_size: usize,
) -> Result<()> {
let sorted = sort_items_for_iter(state, items, sorts, ctx_node)?;
state.variables.enter();
let total = sorted.len();
for (i, item) in sorted.into_iter().enumerate() {
let pos1 = i + 1;
let body_res = sup_xml_core::xpath::eval::with_atomic_context_item(
Some(item.clone()),
|| eval_body(state, body, ctx_node, pos1, total),
);
body_res?;
}
state.variables.leave();
Ok(())
}
fn sort_items_for_iter(
state: &mut EvalState,
items: Vec<sup_xml_core::xpath::eval::Value>,
sorts: &[crate::ast::Sort],
ctx_node: NodeId,
) -> Result<Vec<sup_xml_core::xpath::eval::Value>> {
if sorts.is_empty() { return Ok(items); }
let idx = state.idx;
let style = state.style;
let namespaces = state.namespaces;
let keys = state.keys;
let documents = state.documents;
let decimal_formats = &state.style.decimal_formats;
let unparsed_entities = &state.unparsed_entities;
let user_exts = state.user_exts;
let current_group = if state.current_group.is_empty() { None } else { Some(state.current_group.as_slice()) };
let current_grouping_key = state.current_grouping_key.as_ref();
let accumulators = (!state.accumulators.is_empty()).then_some(&state.accumulators);
let regex_groups = if state.regex_groups.is_empty() { None } else { Some(state.regex_groups.as_slice()) };
let user_functions = (!style.functions.is_empty()).then_some(style.functions.as_slice());
let variables = &state.variables;
let unparsed_texts = state.unparsed_texts;
let xslt_3_0 = xslt_version_3_or_more(&style.version);
let xslt_version = style.version.as_str();
let sc = state.static_ctx;
let static_base_uri = state.static_base_uri.as_deref();
let loader = state.loader;
let loader_base = state.loader_base;
let dyn_doc_cache = state.dyn_doc_cache;
let rtf_base_uris = state.rtf_base_uris;
let source_types = state.source_types;
crate::sort::sort_items(items, sorts, idx, |expr, item, p, s| {
let bindings = XsltBindings {
variables, namespaces, keys,
xslt_context_node: ctx_node,
idx, style, documents, decimal_formats, unparsed_entities,
user_exts, current_group, current_grouping_key, accumulators,
regex_groups, user_functions, unparsed_texts, xslt_3_0,
xslt_version, static_base_uri,
loader, loader_base, dyn_doc_cache, rtf_base_uris,
source_types,
};
let ctx = EvalCtx { context_node: ctx_node, pos: p, size: s, bindings: &bindings, static_ctx: &sc };
sup_xml_core::xpath::eval::with_atomic_context_item(
Some(item.clone()),
|| eval_expr(expr, &ctx, idx).map_err(XsltError::from),
)
})
}
fn sort_group_indices(
state: &EvalState,
groups: &[(Value, Vec<NodeId>)],
sorts: &[crate::ast::Sort],
) -> Result<Vec<usize>> {
if sorts.is_empty() { return Ok((0..groups.len()).collect()); }
let idx = state.idx;
let style = state.style;
let namespaces = state.namespaces;
let keys = state.keys;
let documents = state.documents;
let decimal_formats = &state.style.decimal_formats;
let unparsed_entities = &state.unparsed_entities;
let user_exts = state.user_exts;
let user_functions = (!style.functions.is_empty()).then_some(style.functions.as_slice());
let variables = &state.variables;
let unparsed_texts = state.unparsed_texts;
let xslt_3_0 = xslt_version_3_or_more(&style.version);
let xslt_version = style.version.as_str();
let sc = state.static_ctx;
let static_base_uri = state.static_base_uri.as_deref();
let loader = state.loader;
let loader_base = state.loader_base;
let dyn_doc_cache = state.dyn_doc_cache;
let rtf_base_uris = state.rtf_base_uris;
let source_types = state.source_types;
let first_nodes: Vec<NodeId> =
groups.iter().map(|(_, ns)| *ns.first().unwrap_or(&0)).collect();
let sorted = crate::sort::sort_nodes(&first_nodes, sorts, idx, |expr, n, p, s| {
let (gk, gns) = &groups[p - 1];
let bindings = XsltBindings {
variables, namespaces, keys,
xslt_context_node: n,
idx, style, documents, decimal_formats, unparsed_entities,
user_exts,
current_group: Some(gns.as_slice()),
current_grouping_key: Some(gk),
accumulators: None,
regex_groups: None,
user_functions, unparsed_texts, xslt_3_0,
xslt_version, static_base_uri,
loader, loader_base, dyn_doc_cache, rtf_base_uris,
source_types,
};
let ctx = EvalCtx { context_node: n, pos: p, size: s, bindings: &bindings, static_ctx: &sc };
eval_expr(expr, &ctx, idx).map_err(XsltError::from)
})?;
let mut by_node: HashMap<NodeId, Vec<usize>> = HashMap::new();
for (i, &n) in first_nodes.iter().enumerate() {
by_node.entry(n).or_default().push(i);
}
Ok(sorted.into_iter()
.filter_map(|n| by_node.get_mut(&n).and_then(|v| v.pop()))
.collect())
}
fn merge_materialize_nodes(state: &mut EvalState, v: Value) -> Vec<NodeId> {
match v {
Value::NodeSet(ns) => ns,
Value::Sequence(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
match item {
Value::NodeSet(ns) => out.extend(ns),
Value::ForeignNodeSet(_) => {}
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
out.extend(state.idx.allocate_rtf_text_nodes_inherent(vec![s]));
}
}
}
out
}
Value::ForeignNodeSet(_) => Vec::new(),
atomic => {
let s = value_to_string_styled(&atomic, state.idx, state.num_style());
state.idx.allocate_rtf_text_nodes_inherent(vec![s])
}
}
}
fn merge_key_values(state: &mut EvalState, keys: &[crate::ast::Sort], node: NodeId)
-> Result<Vec<Value>>
{
let mut out = Vec::with_capacity(keys.len());
for k in keys {
let v = match &k.select {
Some(e) => state.xpath_eval(e, node, 1, 1)?,
None => Value::String(state.idx.string_value(node)),
};
out.push(v);
}
Ok(out)
}
fn merge_group_key_str(vals: &[Value], state: &EvalState) -> String {
vals.iter()
.map(|v| value_equality_key(v).unwrap_or_else(||
value_to_string_styled(v, state.idx, state.num_style())))
.collect::<Vec<_>>()
.join("\u{1}")
}
fn sort_nodes_for_iter(
state: &mut EvalState,
nodes: &[NodeId],
sorts: &[crate::ast::Sort],
_ctx_node: NodeId, _pos: usize, _size: usize,
) -> Result<Vec<NodeId>> {
if sorts.is_empty() { return Ok(nodes.to_vec()); }
with_sort_key_eval(state, |idx, eval| crate::sort::sort_nodes(nodes, sorts, idx, eval))
}
fn merge_sort_order(
state: &mut EvalState,
nodes: &[NodeId],
per_node_sorts: &[&[crate::ast::Sort]],
) -> Result<Vec<usize>> {
with_sort_key_eval(state, |idx, eval|
crate::sort::sort_order_keyed(nodes, per_node_sorts, idx, eval))
}
fn with_sort_key_eval<R>(
state: &mut EvalState,
driver: impl FnOnce(
&DocIndex,
&mut dyn FnMut(&sup_xml_core::xpath::Expr, NodeId, usize, usize) -> Result<Value>,
) -> Result<R>,
) -> Result<R> {
let idx = state.idx;
let style = state.style;
let namespaces = state.namespaces;
let keys = state.keys;
let documents = state.documents;
let decimal_formats = &state.style.decimal_formats;
let unparsed_entities = &state.unparsed_entities;
let user_exts = state.user_exts;
let current_group = if state.current_group.is_empty() { None } else { Some(state.current_group.as_slice()) };
let current_grouping_key = state.current_grouping_key.as_ref();
let accumulators = (!state.accumulators.is_empty()).then_some(&state.accumulators);
let regex_groups = if state.regex_groups.is_empty() { None } else { Some(state.regex_groups.as_slice()) };
let user_functions = (!style.functions.is_empty()).then_some(style.functions.as_slice());
let variables = &state.variables;
let unparsed_texts = state.unparsed_texts;
let xslt_3_0 = xslt_version_3_or_more(&style.version);
let xslt_version = style.version.as_str();
let sc = state.static_ctx;
let static_base_uri = state.static_base_uri.as_deref();
let loader = state.loader;
let loader_base = state.loader_base;
let dyn_doc_cache = state.dyn_doc_cache;
let rtf_base_uris = state.rtf_base_uris;
let source_types = state.source_types;
let mut eval = |expr: &sup_xml_core::xpath::Expr, n, p, s| {
let bindings = XsltBindings {
variables, namespaces, keys,
xslt_context_node: n,
idx, style, documents, decimal_formats, unparsed_entities,
user_exts, current_group, current_grouping_key, accumulators,
regex_groups, user_functions, unparsed_texts, xslt_3_0,
xslt_version, static_base_uri,
loader, loader_base, dyn_doc_cache, rtf_base_uris,
source_types,
};
let ctx = EvalCtx { context_node: n, pos: p, size: s, bindings: &bindings, static_ctx: &sc };
eval_expr(expr, &ctx, idx).map_err(XsltError::from)
};
driver(idx, &mut eval)
}