use sup_xml_core::xpath::{parse_xpath_with, Expr, XPathOptions};
use sup_xml_tree::dom::{Document, Node, Attribute, NodeKind};
use crate::ast::*;
use crate::error::XsltError;
use crate::loader::Loader;
use crate::whitespace::is_xslt_whitespace_only;
use crate::XSLT_NS;
thread_local! {
static XSLT_2_0_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static XPATH_2_0_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static FORWARDS_COMPAT_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static PACKAGE_ID_COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
static STATIC_PARAMS: std::cell::RefCell<
std::collections::HashMap<String, sup_xml_core::xpath::eval::Value>>
= std::cell::RefCell::new(std::collections::HashMap::new());
}
thread_local! {
static PACKAGE_SOURCES: std::cell::RefCell<
std::collections::HashMap<String, (String, Option<String>)>>
= std::cell::RefCell::new(std::collections::HashMap::new());
static MODULE_BASE_URI: std::cell::RefCell<Option<String>>
= const { std::cell::RefCell::new(None) };
static MODULE_LINE_STARTS: std::cell::RefCell<Option<Vec<u32>>>
= const { std::cell::RefCell::new(None) };
}
fn line_starts(src: &[u8]) -> Vec<u32> {
let mut starts = Vec::with_capacity(src.len() / 40 + 1);
starts.push(0);
for (i, &b) in src.iter().enumerate() {
if b == b'\n' {
starts.push((i + 1).min(u32::MAX as usize) as u32);
}
}
starts
}
fn node_src_pos(node: &Node) -> Option<crate::ast::SrcPos> {
let offset = node.source_offset;
if offset == 0 {
return None;
}
MODULE_LINE_STARTS.with(|s| {
s.borrow().as_ref().map(|starts| {
let (line, col) = line_col_from_starts(starts, offset);
crate::ast::SrcPos { line, col, offset }
})
})
}
fn line_col_from_starts(starts: &[u32], offset: u32) -> (u32, u32) {
let line_idx = match starts.binary_search(&offset) {
Ok(i) => i,
Err(i) => i - 1,
};
(line_idx as u32 + 1, offset - starts[line_idx] + 1)
}
fn current_module_file() -> Option<std::sync::Arc<str>> {
MODULE_BASE_URI.with(|b| b.borrow().as_deref().map(std::sync::Arc::<str>::from))
}
fn body_of_one(node: &Node, instr: Instr) -> Body {
let mut body = Body::new();
body.set_file(current_module_file());
body.push(instr, node_src_pos(node));
body
}
fn get_package_source(name: &str) -> Option<(String, Option<String>)> {
PACKAGE_SOURCES.with(|m| m.borrow().get(name).cloned())
}
pub fn compile_with_packages(
text: &str,
loader: &dyn Loader,
base: Option<&str>,
packages: std::collections::HashMap<String, (String, Option<String>)>,
) -> Result<StylesheetAst, XsltError> {
PACKAGE_SOURCES.with(|m| *m.borrow_mut() = packages);
let mut counter = TOP_LEVEL_IMPORT_PRECEDENCE;
let r = compile_with_imports(text, loader, base, StylesheetAst::default(), &mut counter);
PACKAGE_SOURCES.with(|m| m.borrow_mut().clear());
r
}
pub(crate) fn validate_input_type_annotations(
ast: &StylesheetAst,
) -> Result<(), XsltError> {
let mut has_strip = false;
let mut has_preserve = false;
for v in &ast.input_type_annotations {
match v.as_str() {
"strip" => has_strip = true,
"preserve" => has_preserve = true,
_ => {}
}
}
if has_strip && has_preserve {
return Err(XsltError::InvalidStylesheet(
"stylesheet declares input-type-annotations='strip' in one \
module and 'preserve' in another (XTSE0265)".into()));
}
Ok(())
}
fn set_static_param(key: String, v: sup_xml_core::xpath::eval::Value) {
STATIC_PARAMS.with(|m| { m.borrow_mut().insert(key, v); });
}
fn get_static_param(name: &str) -> Option<sup_xml_core::xpath::eval::Value> {
STATIC_PARAMS.with(|m| m.borrow().get(name).cloned())
}
fn clear_static_params() {
STATIC_PARAMS.with(|m| m.borrow_mut().clear());
}
pub(crate) fn in_forwards_compat_mode() -> bool {
FORWARDS_COMPAT_MODE.with(|c| c.get())
}
pub(crate) fn is_xslt_2_0_compile() -> bool {
XSLT_2_0_MODE.with(|c| c.get())
}
pub(crate) fn is_xpath_2_0_compile() -> bool {
XPATH_2_0_MODE.with(|c| c.get())
}
struct XsltModeGuard { prev_xslt: bool, prev_xpath: bool, prev_fwd: bool }
impl XsltModeGuard {
fn enter_full(xslt_2_0: bool, xpath_2_0: bool, fwd: bool) -> Self {
let prev_xslt = XSLT_2_0_MODE.with(|c| c.replace(xslt_2_0));
let prev_xpath = XPATH_2_0_MODE.with(|c| c.replace(xpath_2_0));
let prev_fwd = FORWARDS_COMPAT_MODE.with(|c| c.replace(fwd));
Self { prev_xslt, prev_xpath, prev_fwd }
}
}
impl Drop for XsltModeGuard {
fn drop(&mut self) {
let (px, py, pf) = (self.prev_xslt, self.prev_xpath, self.prev_fwd);
XSLT_2_0_MODE.with(|c| c.set(px));
XPATH_2_0_MODE.with(|c| c.set(py));
FORWARDS_COMPAT_MODE.with(|c| c.set(pf));
}
}
fn strip_leading_xpath_comments_and_space(s: &str) -> &str {
let bytes = s.as_bytes();
let mut pos = 0;
loop {
while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos + 1 < bytes.len() && bytes[pos] == b'(' && bytes[pos + 1] == b':' {
pos += 2;
let mut depth: u32 = 1;
while pos + 1 < bytes.len() && depth > 0 {
if bytes[pos] == b'(' && bytes[pos + 1] == b':' {
depth += 1;
pos += 2;
} else if bytes[pos] == b':' && bytes[pos + 1] == b')' {
depth -= 1;
pos += 2;
} else {
pos += 1;
}
}
if depth != 0 {
return "";
}
continue;
}
break;
}
&s[pos..]
}
fn parse_xpath_at(node: &Node, src: &str) -> sup_xml_core::error::Result<Expr> {
let mut expr = parse_xpath(src)?;
resolve_xpath_variable_prefixes(&mut expr, node);
apply_xpath_default_namespace(&mut expr, node);
apply_static_base_uri(&mut expr, node);
apply_default_collation(&mut expr, node);
if let Some(uri) = effective_default_collation(node) {
if uri != "http://www.w3.org/2005/xpath-functions/collation/codepoint" {
expr = Expr::WithDefaultCollation(uri, Box::new(expr));
}
}
if ancestor_forces_backwards_compat(node) {
expr = Expr::BackwardsCompat(Box::new(expr));
}
Ok(expr)
}
fn apply_default_collation(expr: &mut Expr, node: &Node) {
let Some(uri) = effective_default_collation(node) else { return; };
if uri == "http://www.w3.org/2005/xpath-functions/collation/codepoint" {
return;
}
fn walk(e: &mut Expr, uri: &str) {
use sup_xml_core::xpath::ast::LocationPath;
if let Expr::FunctionCall(name, args) = e {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let need_pad = match local {
"compare" if args.len() == 2 => true,
"contains" if args.len() == 2 => true,
"starts-with" if args.len() == 2 => true,
"ends-with" if args.len() == 2 => true,
"substring-before" if args.len() == 2 => true,
"substring-after" if args.len() == 2 => true,
"index-of" if args.len() == 2 => true,
"distinct-values" if args.len() == 1 => true,
"deep-equal" if args.len() == 2 => true,
_ => false,
};
if need_pad {
args.push(Expr::Literal(uri.to_string()));
}
}
match e {
Expr::FunctionCall(_, args)
| Expr::Sequence(args) => for a in args { walk(a, uri); }
Expr::Or(l, r) | Expr::And(l, r)
| Expr::Eq(l, r) | Expr::Ne(l, r)
| Expr::Lt(l, r) | Expr::Gt(l, r)
| Expr::Le(l, r) | Expr::Ge(l, r)
| Expr::ValueEq(l, r) | Expr::ValueNe(l, r)
| Expr::ValueLt(l, r) | Expr::ValueGt(l, r)
| Expr::ValueLe(l, r) | Expr::ValueGe(l, r)
| Expr::Add(l, r) | Expr::Sub(l, r)
| Expr::Mul(l, r) | Expr::Div(l, r)
| Expr::Mod(l, r) | Expr::IDiv(l, r)
| Expr::Union(l, r) | Expr::Intersect(l, r)
| Expr::Except(l, r) | Expr::Range(l, r)
| Expr::SimpleMap(l, r) | Expr::NodeBefore(l, r)
| Expr::NodeAfter(l, r) | Expr::NodeIs(l, r) => { walk(l, uri); walk(r, uri); }
Expr::Neg(x) | Expr::InstanceOf(x, _)
| Expr::CastAs(x, _) | Expr::CastableAs(x, _)
| Expr::TreatAs(x, _) => walk(x, uri),
Expr::IfThenElse { cond, then_branch, else_branch } => {
walk(cond, uri); walk(then_branch, uri); walk(else_branch, uri);
}
Expr::For { bindings, body } => {
for (_, e) in bindings { walk(e, uri); }
walk(body, uri);
}
Expr::Quantified { bindings, test, .. } => {
for (_, e) in bindings { walk(e, uri); }
walk(test, uri);
}
Expr::FilterPath { primary, predicates, steps } => {
walk(primary, uri);
for p in predicates { walk(p, uri); }
for s in steps {
if let Some(f) = s.filter.as_mut() { walk(f, uri); }
for p in &mut s.predicates { walk(p, uri); }
}
}
Expr::Path(LocationPath::Absolute(steps))
| Expr::Path(LocationPath::Relative(steps)) => {
for s in steps {
if let Some(f) = s.filter.as_mut() { walk(f, uri); }
for p in &mut s.predicates { walk(p, uri); }
}
}
_ => {}
}
}
walk(expr, &uri);
}
fn effective_xml_base(node: &Node) -> Option<String> {
let mut bases: Vec<String> = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
if n.is_element() {
for a in n.attributes() {
if a.local_name() == "base"
&& a.namespace.get().and_then(|n| n.prefix()) == Some("xml") {
bases.push(a.value().to_string());
break;
}
}
}
cur = n.parent.get();
}
if bases.is_empty() { return None; }
bases.reverse();
let mut base = bases[0].clone();
for b in &bases[1..] {
base = sup_xml_core::xpath::eval::resolve_uri_against(&base, b);
}
Some(base)
}
fn apply_static_base_uri(expr: &mut Expr, node: &Node) {
let Some(base) = effective_xml_base(node) else { return; };
rewrite_base_uri_calls(expr, &base);
}
fn rewrite_base_uri_calls(expr: &mut Expr, base: &str) {
use sup_xml_core::xpath::ast::LocationPath;
match expr {
Expr::FunctionCall(name, args) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
if (name == "static-base-uri" || local == "static-base-uri" || name == "fn:static-base-uri")
&& args.is_empty()
{
use sup_xml_core::xpath::ast::{ItemType, Occurrence, SequenceType};
*expr = Expr::CastAs(
Box::new(Expr::Literal(base.to_string())),
SequenceType { item: ItemType::Atomic("anyURI".into()),
occurrence: Occurrence::One },
);
return;
}
if (name == "resolve-uri" || local == "resolve-uri" || name == "fn:resolve-uri")
&& args.len() == 1
{
args.push(Expr::Literal(base.to_string()));
}
for a in args { rewrite_base_uri_calls(a, base); }
}
Expr::Or(a, b) | Expr::And(a, b)
| Expr::Eq(a, b) | Expr::Ne(a, b)
| Expr::Lt(a, b) | Expr::Gt(a, b) | Expr::Le(a, b) | Expr::Ge(a, b)
| Expr::ValueEq(a, b) | Expr::ValueNe(a, b)
| Expr::ValueLt(a, b) | Expr::ValueGt(a, b)
| Expr::ValueLe(a, b) | Expr::ValueGe(a, b)
| Expr::Add(a, b) | Expr::Sub(a, b)
| Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Mod(a, b)
| Expr::Union(a, b)
| Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b)
| Expr::Range(a, b) | Expr::SimpleMap(a, b)
| Expr::NodeBefore(a, b) | Expr::NodeAfter(a, b) | Expr::NodeIs(a, b) => {
rewrite_base_uri_calls(a, base);
rewrite_base_uri_calls(b, base);
}
Expr::Neg(a)
| Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => rewrite_base_uri_calls(a, base),
Expr::Sequence(args) => {
for a in args { rewrite_base_uri_calls(a, base); }
}
Expr::IfThenElse { cond, then_branch, else_branch } => {
rewrite_base_uri_calls(cond, base);
rewrite_base_uri_calls(then_branch, base);
rewrite_base_uri_calls(else_branch, base);
}
Expr::For { bindings, body } | Expr::Let { bindings, body } | Expr::Quantified { bindings, test: body, .. } => {
for (_, e) in bindings { rewrite_base_uri_calls(e, base); }
rewrite_base_uri_calls(body, base);
}
Expr::FilterPath { primary, predicates, steps } => {
rewrite_base_uri_calls(primary, base);
for p in predicates { rewrite_base_uri_calls(p, base); }
for s in steps { for p in &mut s.predicates { rewrite_base_uri_calls(p, base); } }
}
Expr::Path(p) => match p {
LocationPath::Absolute(steps) | LocationPath::Relative(steps) => {
for s in steps { for p in &mut s.predicates { rewrite_base_uri_calls(p, base); } }
}
},
Expr::TryCatch { body, catches } => {
rewrite_base_uri_calls(body, base);
for c in catches { rewrite_base_uri_calls(&mut c.body, base); }
}
Expr::WithDefaultCollation(_, inner) => rewrite_base_uri_calls(inner, base),
Expr::BackwardsCompat(inner) => rewrite_base_uri_calls(inner, base),
Expr::MapConstructor(es) => for (k, v) in es {
rewrite_base_uri_calls(k, base); rewrite_base_uri_calls(v, base);
},
Expr::ArrayConstructor { members, .. } =>
for m in members { rewrite_base_uri_calls(m, base); },
Expr::Lookup(b, key) => {
rewrite_base_uri_calls(b, base);
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key {
rewrite_base_uri_calls(e, base);
}
}
Expr::UnaryLookup(key) =>
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key {
rewrite_base_uri_calls(e, base);
},
Expr::InlineFunction { body, .. } => rewrite_base_uri_calls(body, base),
Expr::DynamicCall { func, args } => {
rewrite_base_uri_calls(func, base);
for a in args { rewrite_base_uri_calls(a, base); }
}
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => {}
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) | Expr::Variable(_) => {}
}
}
fn effective_default_collation(node: &Node) -> Option<String> {
let mut cur = Some(node);
while let Some(n) = cur {
if n.is_element() {
let v = if is_xslt_element(n) {
read_attribute(n, "default-collation")
} else {
read_xsl_attribute(n, "default-collation")
};
if let Some(raw) = v {
for tok in raw.split_whitespace() {
if is_recognised_collation(tok) {
return Some(tok.to_string());
}
}
return None;
}
}
cur = n.parent.get();
}
None
}
fn xpath_default_namespace_for(node: &Node) -> Option<String> {
let mut cur = Some(node);
while let Some(n) = cur {
if n.is_element() {
let v = if is_xslt_element(n) {
read_attribute(n, "xpath-default-namespace")
} else {
read_xsl_attribute(n, "xpath-default-namespace")
};
if let Some(uri) = v {
let trimmed = uri.trim();
return if trimmed.is_empty() { None } else { Some(trimmed.to_string()) };
}
}
cur = n.parent.get();
}
None
}
fn apply_xpath_default_namespace(expr: &mut Expr, node: &Node) {
let Some(uri) = xpath_default_namespace_for(node) else { return; };
use sup_xml_core::xpath::ast::{Axis, LocationPath, NodeTest, Step};
fn rewrite_step(s: &mut Step, uri: &str) {
let on_element_axis = matches!(s.axis,
Axis::Child | Axis::Descendant | Axis::DescendantOrSelf
| Axis::Self_ | Axis::Parent | Axis::Ancestor
| Axis::AncestorOrSelf | Axis::FollowingSibling
| Axis::PrecedingSibling | Axis::Following | Axis::Preceding);
if on_element_axis {
if let NodeTest::LocalName(local) = &s.node_test {
s.node_test = NodeTest::DefaultNamespaceName {
uri: uri.to_string(),
local: local.clone(),
};
}
}
for p in &mut s.predicates { rewrite(p, uri); }
}
fn rewrite(e: &mut Expr, uri: &str) {
match e {
Expr::Or(a, b) | Expr::And(a, b)
| Expr::Eq(a, b) | Expr::Ne(a, b)
| Expr::Lt(a, b) | Expr::Gt(a, b) | Expr::Le(a, b) | Expr::Ge(a, b)
| Expr::ValueEq(a, b) | Expr::ValueNe(a, b)
| Expr::ValueLt(a, b) | Expr::ValueGt(a, b)
| Expr::ValueLe(a, b) | Expr::ValueGe(a, b)
| Expr::Add(a, b) | Expr::Sub(a, b)
| Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Mod(a, b)
| Expr::Union(a, b)
| Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b)
| Expr::Range(a, b) | Expr::SimpleMap(a, b)
| Expr::NodeBefore(a, b) | Expr::NodeAfter(a, b) | Expr::NodeIs(a, b) => {
rewrite(a, uri); rewrite(b, uri);
}
Expr::Neg(a)
| Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => rewrite(a, uri),
Expr::FunctionCall(_, args) | Expr::Sequence(args) => {
for a in args { rewrite(a, uri); }
}
Expr::IfThenElse { cond, then_branch, else_branch } => {
rewrite(cond, uri); rewrite(then_branch, uri); rewrite(else_branch, uri);
}
Expr::For { bindings, body } | Expr::Let { bindings, body } | Expr::Quantified { bindings, test: body, .. } => {
for (_, e) in bindings { rewrite(e, uri); }
rewrite(body, uri);
}
Expr::FilterPath { primary, predicates, steps } => {
rewrite(primary, uri);
for p in predicates { rewrite(p, uri); }
for s in steps { rewrite_step(s, uri); }
}
Expr::Path(p) => match p {
LocationPath::Absolute(steps) | LocationPath::Relative(steps) => {
for s in steps { rewrite_step(s, uri); }
}
},
Expr::TryCatch { body, catches } => {
rewrite(body, uri);
for c in catches { rewrite(&mut c.body, uri); }
}
Expr::WithDefaultCollation(_, inner) => rewrite(inner, uri),
Expr::BackwardsCompat(inner) => rewrite(inner, uri),
Expr::MapConstructor(es) => for (k, v) in es { rewrite(k, uri); rewrite(v, uri); },
Expr::ArrayConstructor { members, .. } => for m in members { rewrite(m, uri); },
Expr::Lookup(b, key) => {
rewrite(b, uri);
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key { rewrite(e, uri); }
}
Expr::UnaryLookup(key) =>
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key { rewrite(e, uri); },
Expr::InlineFunction { body, .. } => rewrite(body, uri),
Expr::DynamicCall { func, args } => {
rewrite(func, uri);
for a in args { rewrite(a, uri); }
}
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => {}
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) | Expr::Variable(_) => {}
}
}
rewrite(expr, &uri);
}
fn resolve_xpath_variable_prefixes(expr: &mut Expr, node: &Node) {
use sup_xml_core::xpath::ast::{Step, LocationPath};
fn lookup(node: &Node, prefix: &str) -> Option<String> {
let mut cur = Some(node);
while let Some(n) = cur {
for (p, uri) in n.ns_declarations() {
if p == Some(prefix) { return Some(uri.to_string()); }
}
cur = n.parent.get();
}
None
}
fn walk(e: &mut Expr, node: &Node) {
match e {
Expr::Variable(name) => {
if let Some((p, local)) = name.split_once(':') {
if let Some(uri) = lookup(node, p) {
*name = format!("{{{uri}}}{local}");
}
}
}
Expr::Or(a, b) | Expr::And(a, b)
| Expr::Eq(a, b) | Expr::Ne(a, b)
| Expr::Lt(a, b) | Expr::Gt(a, b) | Expr::Le(a, b) | Expr::Ge(a, b)
| Expr::ValueEq(a, b) | Expr::ValueNe(a, b)
| Expr::ValueLt(a, b) | Expr::ValueGt(a, b)
| Expr::ValueLe(a, b) | Expr::ValueGe(a, b)
| Expr::Add(a, b) | Expr::Sub(a, b)
| Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Mod(a, b)
| Expr::Union(a, b)
| Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b)
| Expr::Range(a, b) | Expr::SimpleMap(a, b) | Expr::NodeBefore(a, b) | Expr::NodeAfter(a, b) | Expr::NodeIs(a, b) => {
walk(a, node); walk(b, node);
}
Expr::Neg(a)
| Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => walk(a, node),
Expr::FunctionCall(_, args)
| Expr::Sequence(args) => {
for a in args { walk(a, node); }
}
Expr::IfThenElse { cond, then_branch, else_branch } => {
walk(cond, node); walk(then_branch, node); walk(else_branch, node);
}
Expr::For { bindings, body } | Expr::Let { bindings, body } | Expr::Quantified { bindings, test: body, .. } => {
for (_, e) in bindings { walk(e, node); }
walk(body, node);
}
Expr::FilterPath { primary, predicates, steps } => {
walk(primary, node);
for p in predicates { walk(p, node); }
for s in steps { walk_step(s, node); }
}
Expr::Path(p) => match p {
LocationPath::Absolute(steps) | LocationPath::Relative(steps) => {
for s in steps { walk_step(s, node); }
}
},
Expr::TryCatch { body, catches } => {
walk(body, node);
for c in catches { walk(&mut c.body, node); }
}
Expr::WithDefaultCollation(_, inner) => walk(inner, node),
Expr::BackwardsCompat(inner) => walk(inner, node),
Expr::MapConstructor(es) => for (k, v) in es { walk(k, node); walk(v, node); },
Expr::ArrayConstructor { members, .. } => for m in members { walk(m, node); },
Expr::Lookup(b, key) => {
walk(b, node);
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key { walk(e, node); }
}
Expr::UnaryLookup(key) =>
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key { walk(e, node); },
Expr::InlineFunction { body, .. } => walk(body, node),
Expr::DynamicCall { func, args } => {
walk(func, node);
for a in args { walk(a, node); }
}
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => {}
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) => {}
}
}
fn walk_step(s: &mut Step, node: &Node) {
for p in &mut s.predicates { walk(p, node); }
}
walk(expr, node);
}
fn parse_xpath(src: &str) -> sup_xml_core::error::Result<Expr> {
let mut opts = XPathOptions::default();
opts.xpath_2_0 = is_xpath_2_0_compile();
parse_xpath_with(src, &opts)
}
pub(crate) fn version_enables_2_0(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 >= 2)
}
fn is_xs_decimal_lexical(s: &str) -> bool {
let t = s.trim();
if t.is_empty() { return false; }
let body = t.strip_prefix(|c| c == '+' || c == '-').unwrap_or(t);
let (whole, frac) = match body.split_once('.') {
Some(pair) => pair,
None => (body, ""),
};
let digits = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit());
if whole.is_empty() && frac.is_empty() { return false; }
(whole.is_empty() || digits(whole)) && (frac.is_empty() || digits(frac))
}
fn version_is_greater_than(version: &str, threshold: &str) -> bool {
fn parse(s: &str) -> Option<(u32, u32)> {
let mut parts = s.trim().split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next().and_then(|m| m.parse().ok()).unwrap_or(0);
Some((major, minor))
}
match (parse(version), parse(threshold)) {
(Some(v), Some(t)) => v > t,
_ => false,
}
}
fn ancestor_enables_forwards_compat(node: &Node) -> bool {
let mut cur = Some(node);
while let Some(n) = cur {
if n.is_element() {
let v = if is_xslt_element(n) {
read_attribute(n, "version")
} else {
read_xsl_attribute(n, "version")
};
if let Some(v) = v {
if version_is_greater_than(v, "2.0") { return true; }
}
}
cur = n.parent.get();
}
false
}
fn ancestor_forces_backwards_compat(node: &Node) -> bool {
let mut cur = Some(node);
while let Some(n) = cur {
if n.is_element() {
let v = if is_xslt_element(n) {
read_attribute(n, "version")
} else {
read_xsl_attribute(n, "version")
};
if let Some(v) = v {
return version_is_greater_than("2.0", v);
}
}
cur = n.parent.get();
}
false
}
fn expand_text_in_scope(node: &Node) -> bool {
let mut cur = node.parent.get();
while let Some(n) = cur {
if n.is_element() {
let v = if is_xslt_element(n) {
read_attribute(n, "expand-text")
} else {
read_xsl_attribute(n, "expand-text")
};
if let Some(v) = v {
return matches!(v.trim(), "yes" | "true" | "1");
}
}
cur = n.parent.get();
}
false
}
pub fn compile(doc: &Document) -> Result<StylesheetAst, XsltError> {
let prev_src = MODULE_LINE_STARTS.with(|s|
std::mem::replace(&mut *s.borrow_mut(), doc.source().map(line_starts)));
struct SourceGuard(Option<Vec<u32>>);
impl Drop for SourceGuard {
fn drop(&mut self) {
MODULE_LINE_STARTS.with(|s| *s.borrow_mut() = self.0.take());
}
}
let _src_guard = SourceGuard(prev_src);
let root = doc.root();
if !root.is_element() {
return Err(XsltError::InvalidStylesheet(
"stylesheet document has no root element".into(),
));
}
if !is_xslt_element(root) {
if has_xsl_version_attr(root) {
return compile_simplified(root);
}
return Err(XsltError::InvalidStylesheet(format!(
"root element is not in the XSLT namespace (got '{}')",
root.name(),
)));
}
let local = root.local_name();
if local != "stylesheet" && local != "transform" && local != "package" {
return Err(XsltError::InvalidStylesheet(format!(
"root XSLT element must be xsl:stylesheet, xsl:transform or \
xsl:package, got xsl:{local}"
)));
}
let mut ast = StylesheetAst::default();
ast.version = match read_attribute(root, "version") {
Some(v) => v.to_string(),
None => return Err(XsltError::InvalidStylesheet(format!(
"xsl:{local} requires a version= attribute (XTSE0010)"
))),
};
if !is_xs_decimal_lexical(&ast.version) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{local} version='{}' is not a valid xs:decimal (XTSE0110)",
ast.version,
)));
}
const ALLOWED_ROOT_ATTRS: &[&str] = &[
"version", "id", "xpath-default-namespace",
"default-validation", "default-collation",
"exclude-result-prefixes", "extension-element-prefixes",
"use-when", "default-mode", "expand-text",
"input-type-annotations",
];
let in_forwards_compat = version_is_greater_than(&ast.version, "2.0");
if !in_forwards_compat {
for attr in root.attributes() {
let name = attr.name();
if attr.namespace.get().is_some() || name.starts_with("xmlns") || name.contains(':') { continue; }
if !ALLOWED_ROOT_ATTRS.contains(&name) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{local} has unrecognised attribute '{name}' (XTSE0090)"
)));
}
}
if let Some(c) = read_attribute(root, "default-collation") {
let any_known = c.split_whitespace()
.any(is_recognised_collation);
if !any_known {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{local} default-collation='{c}' is not recognised (XTSE0125)"
)));
}
}
for attr_name in &["exclude-result-prefixes", "extension-element-prefixes"] {
if let Some(raw) = read_attribute(root, attr_name) {
for tok in raw.split_whitespace() {
if matches!(tok, "#all" | "#default") { continue; }
let mut found = false;
for (p, _) in root.ns_declarations() {
if p == Some(tok) { found = true; break; }
}
if !found {
let code = if *attr_name == "exclude-result-prefixes" { "XTSE0808" } else { "XTSE1430" };
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{local} {attr_name}='{raw}' references undeclared \
prefix '{tok}' ({code})"
)));
}
}
}
}
}
for attr in root.attributes() {
let ns_href = attr.namespace.get().map(|n| n.href()).unwrap_or("");
let local = attr.name().rsplit_once(':').map(|(_, l)| l).unwrap_or(attr.name());
if local == "base" && ns_href == "http://www.w3.org/XML/1998/namespace" {
ast.xml_base = Some(attr.value().to_string());
break;
}
}
if let Some(v) = read_attribute(root, "input-type-annotations") {
let v = v.trim();
if !v.is_empty() {
ast.input_type_annotations.push(v.to_string());
}
}
let xslt_2_0_on = version_enables_2_0(&ast.version);
let in_fwd_compat = version_is_greater_than(&ast.version, "2.0");
let _xslt_mode = XsltModeGuard::enter_full(xslt_2_0_on, true, in_fwd_compat);
for (prefix, uri) in root.ns_declarations() {
match prefix {
Some(p) => { ast.namespaces.insert(p.to_string(), uri.to_string()); }
None => { ast.namespaces.insert(String::new(), uri.to_string()); }
}
}
collect_inner_ns(root, &mut ast.namespaces);
clear_static_params();
for child in root.children() {
if !child.is_element() || !is_xslt_element(child) { continue; }
let ln = child.local_name();
if ln != "param" && ln != "variable" { continue; }
let is_static = read_attribute(child, "static")
.map(|v| matches!(v, "yes" | "true" | "1")).unwrap_or(false);
if !is_static { continue; }
let name = required_qname_attr(child, "name", "xsl:variable")?;
let value = match read_attribute(child, "select") {
Some(sel) => {
let e = parse_xpath_at(child, sel).map_err(XsltError::from)?;
eval_static_expr_ast(&e, Some(child))?
}
None => sup_xml_core::xpath::eval::Value::String(String::new()),
};
set_static_param(qname_key(&name), value);
}
let mut pos: u32 = 0;
for child in root.children() {
if !child.is_element() {
if is_text_like(child) && !child.content().trim().is_empty()
&& !in_forwards_compat_mode()
{
return Err(XsltError::InvalidStylesheet(
"xsl:stylesheet may not contain text node children (XTSE0120)".into()
));
}
continue;
}
compile_top_level(child, &mut ast, pos)?;
pos += 1;
}
ast.documents_to_load = crate::walk::collect_static_document_uris(&ast);
{
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();
for v in &ast.global_variables {
let k = qname_key(&v.name);
if !seen.insert(k.clone()) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate global xsl:variable '{k}' (XTSE0630)"
)));
}
}
for p in &ast.global_params {
let k = qname_key(&p.name);
if !seen.insert(k.clone()) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate global xsl:param '{k}' or collision with \
a same-named xsl:variable (XTSE0630)"
)));
}
}
}
detect_global_variable_cycle(&ast)?;
Ok(ast)
}
fn detect_global_variable_cycle(ast: &StylesheetAst) -> Result<(), XsltError> {
use std::collections::{HashMap, HashSet};
let mut deps: HashMap<String, Vec<String>> = HashMap::new();
let mut all_names: HashSet<String> = HashSet::new();
for v in &ast.global_variables { all_names.insert(qname_key(&v.name)); }
for p in &ast.global_params { all_names.insert(qname_key(&p.name)); }
let collect_refs = |sel: &Option<sup_xml_core::xpath::ast::Expr>|
-> Vec<String>
{
let Some(e) = sel else { return Vec::new(); };
let mut refs = Vec::new();
collect_variable_refs(e, &mut refs);
refs.into_iter()
.filter(|r| all_names.contains(r))
.collect()
};
for v in &ast.global_variables {
deps.insert(qname_key(&v.name), collect_refs(&v.select));
}
for p in &ast.global_params {
deps.insert(qname_key(&p.name), collect_refs(&p.select));
}
fn dfs(
name: &str,
deps: &HashMap<String, Vec<String>>,
seen: &mut HashSet<String>,
stack: &mut HashSet<String>,
) -> Option<String> {
if stack.contains(name) { return Some(name.to_string()); }
if seen.contains(name) { return None; }
seen.insert(name.to_string());
stack.insert(name.to_string());
if let Some(refs) = deps.get(name) {
for r in refs {
if let Some(cyc) = dfs(r, deps, seen, stack) { return Some(cyc); }
}
}
stack.remove(name);
None
}
let mut seen = HashSet::new();
for k in deps.keys() {
let mut stack = HashSet::new();
if let Some(cyc) = dfs(k, &deps, &mut seen, &mut stack) {
return Err(XsltError::InvalidStylesheet(format!(
"circular reference among global variables/params \
involving '{cyc}' (XTDE0640)"
)));
}
}
Ok(())
}
fn evaluate_use_when_at(
expr_text: &str, host: Option<&Node>,
) -> Result<bool, XsltError> {
use sup_xml_core::xpath::eval::{eval_expr, EvalCtx, StaticContext, Value, XPathBindings};
let mut expr = match sup_xml_core::xpath::parse_xpath_with(
expr_text,
&sup_xml_core::xpath::XPathOptions {
xpath_2_0: true, libxml2_compatible: false,
..sup_xml_core::xpath::XPathOptions::default()
},
) {
Ok(e) => e,
Err(e) => return Err(XsltError::InvalidStylesheet(format!(
"use-when='{expr_text}' failed to parse (XPST0003): {e}"
))),
};
if let Some(base) = MODULE_BASE_URI.with(|b| b.borrow().clone()) {
rewrite_base_uri_calls(&mut expr, &base);
}
if use_when_uses_focus(&expr) {
return Err(XsltError::InvalidStylesheet(format!(
"use-when='{expr_text}' accesses the context item / focus, \
which is undefined in the static context (XPDY0002)"
)));
}
let mut ns_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
if let Some(n) = host {
let mut cur = Some(n);
while let Some(n) = cur {
for (p, uri) in n.ns_declarations() {
if let Some(p) = p {
ns_map.entry(p.to_string()).or_insert_with(|| uri.to_string());
}
}
cur = n.parent.get();
}
}
let bindings = UseWhenBindings { extra_ns: ns_map };
let idx = StaticEmptyIndex;
let static_ctx = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: false, current_node: None,
};
let ctx = EvalCtx {
context_node: 0, pos: 1, size: 1,
bindings: &bindings, static_ctx: &static_ctx,
};
match eval_expr(&expr, &ctx, &idx) {
Ok(Value::Boolean(b)) => Ok(b),
Ok(Value::Number(n)) => Ok(n.as_f64() != 0.0 && !n.as_f64().is_nan()),
Ok(Value::String(s)) => Ok(!s.is_empty()),
Ok(Value::NodeSet(ns)) => Ok(!ns.is_empty()),
Ok(_) => Ok(true),
Err(e) => Err(XsltError::InvalidStylesheet(format!(
"use-when='{expr_text}': {}", e.message,
))),
}
}
fn eval_static_expr_ast(expr: &Expr, host: Option<&Node>) -> Result<sup_xml_core::xpath::eval::Value, XsltError> {
use sup_xml_core::xpath::eval::{eval_expr, EvalCtx, StaticContext, XPathBindings};
let mut ns_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
if let Some(n) = host {
let mut cur = Some(n);
while let Some(n) = cur {
for (p, uri) in n.ns_declarations() {
if let Some(p) = p {
ns_map.entry(p.to_string()).or_insert_with(|| uri.to_string());
}
}
cur = n.parent.get();
}
}
let bindings = UseWhenBindings { extra_ns: ns_map };
let idx = StaticEmptyIndex;
let static_ctx = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: false, current_node: None,
};
let ctx = EvalCtx {
context_node: 0, pos: 1, size: 1,
bindings: &bindings, static_ctx: &static_ctx,
};
eval_expr(expr, &ctx, &idx).map_err(|e| XsltError::InvalidStylesheet(format!(
"static expression: {}", e.message)))
}
fn resolve_shadow_attr(node: &Node, name: &str) -> Result<Option<String>, XsltError> {
use sup_xml_core::xpath::eval::value_to_string;
let shadow = format!("_{name}");
let Some(raw) = read_attribute(node, &shadow) else { return Ok(None) };
let template = avt(node, raw)?;
let mut out = String::new();
for part in &template.parts {
match part {
crate::ast::AvtPart::Literal(s) => out.push_str(s),
crate::ast::AvtPart::Expr(e) => {
let v = eval_static_expr_ast(e, Some(node))?;
out.push_str(&value_to_string(&v, &StaticEmptyIndex));
}
}
}
Ok(Some(out))
}
fn read_attr_with_shadow(node: &Node, name: &str) -> Result<Option<String>, XsltError> {
if let Some(v) = read_attribute(node, name) {
return Ok(Some(v.to_string()));
}
resolve_shadow_attr(node, name)
}
fn use_when_uses_focus(expr: &Expr) -> bool {
use sup_xml_core::xpath::ast::{LocationPath, Step};
fn step_uses_focus(s: &Step) -> bool {
if !s.predicates.is_empty() {
for p in &s.predicates { if walk(p) { return true; } }
}
true
}
fn walk(e: &Expr) -> bool {
match e {
Expr::Path(LocationPath::Relative(steps)) => steps.iter().any(step_uses_focus),
Expr::Path(LocationPath::Absolute(_)) => false,
Expr::FilterPath { primary, predicates, steps } => {
if walk(primary) { return true; }
for p in predicates { if walk(p) { return true; } }
for s in steps {
for p in &s.predicates { if walk(p) { return true; } }
}
false
}
Expr::FunctionCall(name, args) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
if matches!(local,
"position" | "last" | "current" | "current-group"
| "current-grouping-key" | "regex-group"
| "generate-id" | "id" | "idref"
| "key" | "format-number" | "format-date"
| "format-time" | "format-dateTime"
| "base-uri" | "in-scope-prefixes"
| "namespace-uri-for-prefix"
| "doc" | "document" | "unparsed-text"
| "unparsed-text-available"
) {
return true;
}
args.iter().any(walk)
}
Expr::Or(a, b) | Expr::And(a, b)
| Expr::Eq(a, b) | Expr::Ne(a, b)
| Expr::Lt(a, b) | Expr::Gt(a, b) | Expr::Le(a, b) | Expr::Ge(a, b)
| Expr::ValueEq(a, b) | Expr::ValueNe(a, b)
| Expr::ValueLt(a, b) | Expr::ValueGt(a, b)
| Expr::ValueLe(a, b) | Expr::ValueGe(a, b)
| Expr::Add(a, b) | Expr::Sub(a, b) | Expr::Mul(a, b)
| Expr::Div(a, b) | Expr::Mod(a, b)
| Expr::Union(a, b) | Expr::Range(a, b)
| Expr::SimpleMap(a, b)
| Expr::NodeBefore(a, b) | Expr::NodeAfter(a, b) | Expr::NodeIs(a, b)
=> walk(a) || walk(b),
Expr::Neg(a) => walk(a),
Expr::IfThenElse { cond, then_branch, else_branch } =>
walk(cond) || walk(then_branch) || walk(else_branch),
Expr::For { bindings, body } => {
bindings.iter().any(|(_, e)| walk(e)) || walk(body)
}
Expr::Quantified { bindings, test, .. } => {
bindings.iter().any(|(_, e)| walk(e)) || walk(test)
}
Expr::Sequence(items) => items.iter().any(walk),
Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b) => walk(a) || walk(b),
Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => walk(a),
Expr::TryCatch { body, catches } => {
if walk(body) { return true; }
catches.iter().any(|c| walk(&c.body))
}
_ => false,
}
}
walk(expr)
}
struct StaticEmptyIndex;
impl sup_xml_core::xpath::DocIndexLike for StaticEmptyIndex {
fn parent(&self, _: sup_xml_core::xpath::NodeId) -> Option<sup_xml_core::xpath::NodeId> { None }
fn children(&self, _: sup_xml_core::xpath::NodeId) -> &[sup_xml_core::xpath::NodeId] { &[] }
fn attr_range(&self, _: sup_xml_core::xpath::NodeId) -> std::ops::Range<sup_xml_core::xpath::NodeId> { 0..0 }
fn kind(&self, _: sup_xml_core::xpath::NodeId) -> sup_xml_core::xpath::XPathNodeKind {
sup_xml_core::xpath::XPathNodeKind::Document
}
fn pi_target(&self, _: sup_xml_core::xpath::NodeId) -> &str { "" }
fn string_value(&self, _: sup_xml_core::xpath::NodeId) -> String { String::new() }
fn node_name(&self, _: sup_xml_core::xpath::NodeId) -> &str { "" }
fn local_name(&self, _: sup_xml_core::xpath::NodeId) -> &str { "" }
fn namespace_uri(&self, _: sup_xml_core::xpath::NodeId) -> &str { "" }
}
struct UseWhenBindings {
extra_ns: std::collections::HashMap<String, String>,
}
impl sup_xml_core::xpath::eval::XPathBindings for UseWhenBindings {
fn resolve_prefix(&self, prefix: &str) -> Option<String> {
if let Some(u) = self.extra_ns.get(prefix) { return Some(u.clone()); }
match prefix {
"xsl" => Some("http://www.w3.org/1999/XSL/Transform".into()),
"xs" => Some("http://www.w3.org/2001/XMLSchema".into()),
"fn" => Some("http://www.w3.org/2005/xpath-functions".into()),
_ => None,
}
}
fn variable(&self, name: &str) -> Option<sup_xml_core::xpath::eval::Value> {
get_static_param(name)
.or_else(|| name.rsplit(':').next().and_then(get_static_param))
}
fn foreign_string_value(
&self, _: sup_xml_core::xpath::eval::ForeignNodePtr,
) -> String { String::new() }
fn call_function(
&self, _ns_uri: &str, name: &str,
args: Vec<sup_xml_core::xpath::eval::Value>,
) -> Option<std::result::Result<sup_xml_core::xpath::eval::Value, sup_xml_core::error::XmlError>> {
use sup_xml_core::xpath::eval::{Value, value_to_string};
let dummy = StaticEmptyIndex;
match name {
"system-property" if args.len() == 1 => {
let raw = value_to_string(&args[0], &dummy);
let (prefix, local) = match raw.split_once(':') {
Some((p, l)) => (Some(p), l),
None => (None, raw.as_str()),
};
let xsl = match prefix {
Some(p) => self.resolve_prefix(p).as_deref()
== Some("http://www.w3.org/1999/XSL/Transform"),
None => false,
};
if !xsl { return Some(Ok(Value::String(String::new()))); }
let s = match local {
"version" => "2.0",
"vendor" => "sup-xml",
"vendor-url" => "https://github.com/super_source/sup_xml",
"product-name" => "sup-xml",
"product-version" => env!("CARGO_PKG_VERSION"),
"is-schema-aware" => "no",
"supports-serialization" => "yes",
"supports-backwards-compatibility" => "yes",
"supports-namespace-axis" => "yes",
"supports-streaming" => "no",
"supports-dynamic-evaluation" => "no",
"supports-higher-order-functions" => "no",
"xpath-version" => "2.0",
"xsd-version" => "1.0",
_ => "",
};
Some(Ok(Value::String(s.to_string())))
}
"doc-available" => Some(Ok(Value::Boolean(false))),
"function-available" if (1..=2).contains(&args.len()) => {
let name = value_to_string(&args[0], &dummy);
if !crate::functions::is_lexical_qname(&name) {
return Some(Err(sup_xml_core::xpath::eval::xpath_err(
format!("function-available(): '{name}' is not a valid \
QName (XTDE1400)")
)));
}
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(&name);
let prefix = name.rsplit_once(':').map(|(p, _)| p);
if let Some(p) = prefix {
let known = matches!(p, "xs" | "fn" | "xsl" | "xslt" | "xsd")
|| self.extra_ns.contains_key(p);
if !known {
return Some(Err(sup_xml_core::xpath::eval::xpath_err(
format!("function-available(): prefix '{p}' is not \
bound in scope (XTDE1400)")
)));
}
}
let unprefixed_builtin = prefix.is_none()
&& crate::functions::is_builtin_function(local);
let xs_constructor = matches!(prefix, Some("xs"))
&& sup_xml_core::xpath::eval::atomic_kind_static(local).is_some();
let fn_builtin = matches!(prefix, Some("fn"))
&& crate::functions::is_builtin_function(local);
Some(Ok(Value::Boolean(
unprefixed_builtin || xs_constructor || fn_builtin
)))
}
"element-available" if args.len() == 1 => {
let name = value_to_string(&args[0], &dummy);
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(&name);
let prefix = name.rsplit_once(':').map(|(p, _)| p);
let xslt_uri = "http://www.w3.org/1999/XSL/Transform";
let in_xslt = match prefix {
None => true,
Some(p) => self.extra_ns.get(p)
.map(|u| u == xslt_uri)
.unwrap_or(false),
};
Some(Ok(Value::Boolean(
in_xslt && crate::functions::is_builtin_xslt_instruction(local)
)))
}
"type-available" if args.len() == 1 => {
let name = value_to_string(&args[0], &dummy);
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(&name);
let prefix = name.rsplit_once(':').map(|(p, _)| p);
let xs_type = matches!(prefix, Some("xs") | Some("xsd"))
&& sup_xml_core::xpath::eval::atomic_kind_static(local).is_some();
Some(Ok(Value::Boolean(xs_type)))
}
_ => None,
}
}
}
fn compile_top_level(node: &Node, ast: &mut StylesheetAst, pos: u32) -> Result<(), XsltError> {
let forwards_compat = version_is_greater_than(ast.version.as_str(), "2.0");
let is_unknown_xslt = is_xslt_element(node)
&& !KNOWN_TOP_LEVEL_XSLT_ELEMENTS.contains(&node.local_name());
if forwards_compat && is_unknown_xslt {
return Ok(());
}
if let Some(uw) = read_attribute(node, "use-when") {
if !evaluate_use_when_at(uw, Some(node))? {
return Ok(());
}
}
if !is_xslt_element(node) {
let ns_uri = node.namespace.get().map(|ns| ns.href()).unwrap_or("");
if ns_uri.is_empty() && !in_forwards_compat_mode() {
return Err(XsltError::InvalidStylesheet(format!(
"top-level element <{}> has no namespace URI (XTSE0130)",
node.name(),
)));
}
return Ok(());
}
match node.local_name() {
"template" => {
let mut t = compile_template(node)?;
t.source_path = vec![pos];
ast.templates.push(t);
}
"variable" => ast.global_variables.push(compile_variable(node)?),
"param" => {
let p = compile_param(node)?;
if p.tunnel {
return Err(XsltError::InvalidStylesheet(
"top-level xsl:param cannot specify tunnel='yes' (XTSE0020)".into()
));
}
ast.global_params.push(p);
}
"key" => {
let k = compile_key(node)?;
let new_coll = k.collation.as_deref()
.filter(|s| !s.is_empty()
&& *s != "http://www.w3.org/2005/xpath-functions/collation/codepoint");
for prior in ast.keys.iter()
.filter(|p| qname_key(&p.name) == qname_key(&k.name))
{
let prior_coll = prior.collation.as_deref()
.filter(|s| !s.is_empty()
&& *s != "http://www.w3.org/2005/xpath-functions/collation/codepoint");
if prior_coll != new_coll {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:key '{}' declared with conflicting effective \
collations (XTSE1220)", qname_key(&k.name)
)));
}
}
ast.keys.push(k);
}
"attribute-set" => ast.attribute_sets.push(compile_attribute_set(node)?),
"mode" => ast.modes.push(compile_mode(node)?),
"accumulator" => ast.accumulators.push(compile_accumulator(node)?),
"import-schema" => {
let namespace = read_attribute(node, "namespace").map(|s| s.to_string());
if let Some(loc) = read_attribute(node, "schema-location") {
ast.schema_imports.push((namespace, loc.to_string()));
}
if let Some(inline) = node.children().find(|c|
matches!(c.kind, sup_xml_tree::dom::NodeKind::Element)
&& c.name().rsplit(':').next() == Some("schema"))
{
ast.inline_schemas.push(serialize_inline_schema(inline));
}
}
"use-package" => ast.use_packages.push(compile_use_package(node)?),
"expose" => ast.exposes.push(validate_expose(node)?),
"output" => ast.outputs.push(compile_output(node)?),
"strip-space" => collect_whitespace_rules(node, true, &mut ast.whitespace_rules)?,
"preserve-space" => collect_whitespace_rules(node, false, &mut ast.whitespace_rules)?,
"include" => {
validate_xslt_only_attributes(node, "xsl:include", &["href"])?;
validate_must_be_empty(node, "xsl:include")?;
let href = read_attribute(node, "href").ok_or_else(||
XsltError::InvalidStylesheet(
"xsl:include requires an href= attribute (XTSE0010)".into()))?;
ast.includes.push(href.to_string());
ast.include_positions.push(pos);
}
"import" => {
validate_xslt_only_attributes(node, "xsl:import", &["href"])?;
validate_must_be_empty(node, "xsl:import")?;
let href = read_attribute(node, "href").ok_or_else(||
XsltError::InvalidStylesheet(
"xsl:import requires an href= attribute (XTSE0010)".into()))?;
ast.imports.push(href.to_string());
}
"namespace-alias" => {
if let Some(pair) = compile_namespace_alias(node)? {
let (style_uri, result_uri, _) = &pair;
for (existing_style, existing_result, _) in &ast.namespace_aliases {
if existing_style == style_uri && existing_result != result_uri {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:namespace-alias source URI '{style_uri}' is \
aliased to both '{existing_result}' and \
'{result_uri}' at the same import precedence \
(XTSE0810)"
)));
}
}
ast.namespace_aliases.push(pair);
}
}
"decimal-format" => compile_decimal_format(node, ast)?,
"function" => {
let f = compile_function(node)?;
let arity = f.params.len();
let key = qname_key(&f.name);
if ast.functions.iter().any(|g|
qname_key(&g.name) == key && g.params.len() == arity)
{
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:function '{key}#{arity}' (XTSE0770)"
)));
}
ast.functions.push(f);
}
"character-map" => {
ast.character_maps.push(compile_character_map(node)?);
}
name if INSTRUCTION_NAMES_TOP_LEVEL_FORBIDDEN.contains(&name) => {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{name} is not allowed at top level (XTSE0010)"
)));
}
"stylesheet" | "transform" => {
let v = ast.version.as_str();
if !version_is_greater_than(v, "2.0") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{} may only appear as the stylesheet root \
(XTSE0010)", node.local_name()
)));
}
}
other if KNOWN_TOP_LEVEL_XSLT_ELEMENTS.contains(&other) => {}
other => {
let v = ast.version.as_str();
let in_forwards_compat = version_is_greater_than(v, "2.0");
if !in_forwards_compat {
return Err(XsltError::InvalidStylesheet(format!(
"unknown XSLT top-level element xsl:{other} \
(XTSE0010 — declared version {v} is not in \
forwards-compatible mode)",
)));
}
}
}
Ok(())
}
const INSTRUCTION_NAMES_TOP_LEVEL_FORBIDDEN: &[&str] = &[
"apply-imports", "apply-templates", "call-template", "choose",
"if", "for-each", "value-of", "text", "copy", "copy-of",
"element", "attribute", "comment", "processing-instruction",
"number", "message", "fallback", "next-match",
"analyze-string", "perform-sort", "document", "result-document",
"sequence", "for-each-group", "namespace", "map", "map-entry",
];
const KNOWN_TOP_LEVEL_XSLT_ELEMENTS: &[&str] = &[
"template", "variable", "param", "key", "attribute-set",
"output", "strip-space", "preserve-space",
"include", "import", "namespace-alias", "decimal-format",
"stylesheet", "transform",
"function", "character-map", "import-schema",
"result-document", "use-package", "package", "accumulator",
"mode", "global-context-item", "merge-source", "merge-key",
"merge-action", "context-item", "expose",
];
fn compile_function(node: &Node) -> Result<UserFunction, XsltError> {
validate_xslt_only_attributes(node, "xsl:function",
&["name", "as", "override", "visibility", "streamability", "cache", "new-each-time"])?;
if let Some(v) = read_attribute(node, "override") {
if !matches!(v.trim(), "yes" | "no") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:function override='{v}' must be 'yes' or 'no' (XTSE0020)"
)));
}
}
let name = read_attribute(node, "name").ok_or_else(||
XsltError::InvalidStylesheet("xsl:function requires a name= attribute".into())
)?;
let qname = parse_qname_on(node, name)?;
if qname.uri.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:function name must be in a namespace (prefixed)".into()));
}
reject_reserved_name(&qname, "xsl:function")?;
let mut params = Vec::new();
let mut body = Body::new();
let mut seen_non_param = false;
for child in node.children() {
if !child.is_element() { continue; }
if is_xslt_element(&child) && child.local_name() == "param" && !seen_non_param {
let p = compile_param(&child)?;
if p.tunnel {
return Err(XsltError::InvalidStylesheet(
"xsl:function parameter cannot specify tunnel='yes' (XTSE0020)".into()
));
}
if p.select.is_some() || !p.body.is_empty() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:function parameter '{}' cannot have a default \
value (XTSE0760)", qname_key(&p.name),
)));
}
if read_attribute(&child, "required").is_some() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:function parameter '{}' cannot specify \
required= (XTSE0020)", qname_key(&p.name),
)));
}
let key = qname_key(&p.name);
if params.iter().any(|q: &Param| qname_key(&q.name) == key) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:param '{key}' on xsl:function (XTSE0580)"
)));
}
params.push(p);
continue;
}
seen_non_param = true;
compile_instr_into_body(&child, &mut body)?;
}
reject_function_body_focus_use(&body)?;
if let Some(expr) = desugar_body_to_xpath(&body) {
body = body_of_one(node, Instr::Sequence { select: expr });
}
let as_type = read_attribute(node, "as").map(str::to_string);
let visibility = read_attribute(node, "visibility").map(str::to_string);
Ok(UserFunction { name: qname, params, body, as_type, visibility, package_id: 0 })
}
fn collect_variable_refs(e: &sup_xml_core::xpath::ast::Expr, out: &mut Vec<String>) {
use sup_xml_core::xpath::ast::{Expr, LocationPath};
match e {
Expr::Variable(name) => out.push(name.clone()),
Expr::Or(l, r) | Expr::And(l, r)
| Expr::Eq(l, r) | Expr::Ne(l, r)
| Expr::Lt(l, r) | Expr::Gt(l, r) | Expr::Le(l, r) | Expr::Ge(l, r)
| Expr::ValueEq(l, r) | Expr::ValueNe(l, r)
| Expr::ValueLt(l, r) | Expr::ValueGt(l, r)
| Expr::ValueLe(l, r) | Expr::ValueGe(l, r)
| Expr::Add(l, r) | Expr::Sub(l, r) | Expr::Mul(l, r)
| Expr::Div(l, r) | Expr::Mod(l, r)
| Expr::Union(l, r) | Expr::IDiv(l, r)
| Expr::Intersect(l, r) | Expr::Except(l, r)
| Expr::Range(l, r) | Expr::SimpleMap(l, r)
| Expr::NodeBefore(l, r) | Expr::NodeAfter(l, r) | Expr::NodeIs(l, r) => {
collect_variable_refs(l, out);
collect_variable_refs(r, out);
}
Expr::Neg(x) | Expr::InstanceOf(x, _)
| Expr::CastAs(x, _) | Expr::CastableAs(x, _)
| Expr::TreatAs(x, _) => collect_variable_refs(x, out),
Expr::IfThenElse { cond, then_branch, else_branch } => {
collect_variable_refs(cond, out);
collect_variable_refs(then_branch, out);
collect_variable_refs(else_branch, out);
}
Expr::For { bindings, body } | Expr::Let { bindings, body } | Expr::Quantified { bindings, test: body, .. } => {
for (_, e) in bindings { collect_variable_refs(e, out); }
collect_variable_refs(body, out);
}
Expr::FilterPath { primary, predicates, steps } => {
collect_variable_refs(primary, out);
for p in predicates { collect_variable_refs(p, out); }
for s in steps {
if let Some(f) = &s.filter { collect_variable_refs(f, out); }
for p in &s.predicates { collect_variable_refs(p, out); }
}
}
Expr::Path(p) => match p {
LocationPath::Absolute(steps) | LocationPath::Relative(steps) => {
for s in steps {
if let Some(f) = &s.filter { collect_variable_refs(f, out); }
for p in &s.predicates { collect_variable_refs(p, out); }
}
}
}
Expr::FunctionCall(_, args) | Expr::Sequence(args) =>
for a in args { collect_variable_refs(a, out); }
Expr::TryCatch { body, catches } => {
collect_variable_refs(body, out);
for c in catches { collect_variable_refs(&c.body, out); }
}
Expr::WithDefaultCollation(_, inner) => collect_variable_refs(inner, out),
Expr::BackwardsCompat(inner) => collect_variable_refs(inner, out),
Expr::MapConstructor(es) => for (k, v) in es {
collect_variable_refs(k, out); collect_variable_refs(v, out);
},
Expr::ArrayConstructor { members, .. } =>
for m in members { collect_variable_refs(m, out); },
Expr::Lookup(b, key) => {
collect_variable_refs(b, out);
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key {
collect_variable_refs(e, out);
}
}
Expr::UnaryLookup(key) =>
if let sup_xml_core::xpath::ast::LookupKey::Expr(e) = key {
collect_variable_refs(e, out);
},
Expr::InlineFunction { params, body, .. } => {
let mut inner = Vec::new();
collect_variable_refs(body, &mut inner);
for v in inner {
if !params.contains(&v) { out.push(v); }
}
}
Expr::DynamicCall { func, args } => {
collect_variable_refs(func, out);
for a in args { collect_variable_refs(a, out); }
}
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => {}
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) => {}
}
}
fn expr_uses_outer_focus(e: &sup_xml_core::xpath::ast::Expr) -> bool {
use sup_xml_core::xpath::ast::{Expr, LocationPath};
match e {
Expr::Path(LocationPath::Relative(_)) => true,
Expr::Path(LocationPath::Absolute(_)) => false,
Expr::FilterPath { primary, .. } => expr_uses_outer_focus(primary),
Expr::For { bindings, .. }
| Expr::Quantified { bindings, .. } => {
bindings.iter().any(|(_, x)| expr_uses_outer_focus(x))
}
Expr::Let { bindings, body } =>
bindings.iter().any(|(_, x)| expr_uses_outer_focus(x))
|| expr_uses_outer_focus(body),
Expr::SimpleMap(l, _) => expr_uses_outer_focus(l),
Expr::Or(l, r) | Expr::And(l, r)
| Expr::Eq(l, r) | Expr::Ne(l, r)
| Expr::Lt(l, r) | Expr::Gt(l, r) | Expr::Le(l, r) | Expr::Ge(l, r)
| Expr::ValueEq(l, r) | Expr::ValueNe(l, r)
| Expr::ValueLt(l, r) | Expr::ValueGt(l, r)
| Expr::ValueLe(l, r) | Expr::ValueGe(l, r)
| Expr::Add(l, r) | Expr::Sub(l, r) | Expr::Mul(l, r)
| Expr::Div(l, r) | Expr::Mod(l, r)
| Expr::Union(l, r) | Expr::IDiv(l, r)
| Expr::Intersect(l, r) | Expr::Except(l, r)
| Expr::Range(l, r)
| Expr::NodeBefore(l, r) | Expr::NodeAfter(l, r) | Expr::NodeIs(l, r) =>
expr_uses_outer_focus(l) || expr_uses_outer_focus(r),
Expr::Neg(x) | Expr::InstanceOf(x, _)
| Expr::CastAs(x, _) | Expr::CastableAs(x, _)
| Expr::TreatAs(x, _) => expr_uses_outer_focus(x),
Expr::IfThenElse { cond, then_branch, else_branch } =>
expr_uses_outer_focus(cond)
|| expr_uses_outer_focus(then_branch)
|| expr_uses_outer_focus(else_branch),
Expr::FunctionCall(name, args) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let focus_no_arg = matches!(local,
"position" | "last" | "current"
| "name" | "local-name" | "namespace-uri"
| "string" | "normalize-space" | "number"
| "node-name" | "base-uri" | "string-length"
| "has-children" | "root" | "data");
if focus_no_arg && args.is_empty() { return true; }
let needs_ctx_doc = matches!(local, "key" | "id" | "idref");
if needs_ctx_doc && (args.len() == 1 || args.len() == 2) { return true; }
args.iter().any(expr_uses_outer_focus)
}
Expr::Sequence(items) => items.iter().any(expr_uses_outer_focus),
Expr::TryCatch { body, catches } =>
expr_uses_outer_focus(body)
|| catches.iter().any(|c| expr_uses_outer_focus(&c.body)),
Expr::WithDefaultCollation(_, inner) => expr_uses_outer_focus(inner),
Expr::BackwardsCompat(inner) => expr_uses_outer_focus(inner),
Expr::MapConstructor(es) =>
es.iter().any(|(k, v)| expr_uses_outer_focus(k) || expr_uses_outer_focus(v)),
Expr::ArrayConstructor { members, .. } =>
members.iter().any(expr_uses_outer_focus),
Expr::Lookup(b, key) => expr_uses_outer_focus(b)
|| matches!(key, sup_xml_core::xpath::ast::LookupKey::Expr(e) if expr_uses_outer_focus(e)),
Expr::ContextItem | Expr::UnaryLookup(_) => true,
Expr::InlineFunction { .. } => false,
Expr::DynamicCall { func, args } =>
expr_uses_outer_focus(func) || args.iter().any(expr_uses_outer_focus),
Expr::NamedFunctionRef { .. } | Expr::Placeholder => false,
Expr::Variable(_) | Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) => false,
}
}
fn reject_function_body_focus_use(body: &[Instr]) -> Result<(), XsltError> {
use crate::ast::Instr;
let reject_focus_expr = |e: &sup_xml_core::xpath::ast::Expr, who: &str|
-> Result<(), XsltError>
{
if expr_uses_outer_focus(e) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} inside xsl:function body references the context item, \
which is undefined for stylesheet functions (XPDY0002)"
)));
}
Ok(())
};
for instr in body {
match instr {
Instr::Copy { .. } => return Err(XsltError::InvalidStylesheet(
"xsl:copy inside xsl:function body is XTTE0945 — the body \
has no context item to copy".into()
)),
Instr::Sequence { select } =>
reject_focus_expr(select, "xsl:sequence select=")?,
Instr::ValueOf { select, .. } =>
reject_focus_expr(select, "xsl:value-of select=")?,
Instr::CopyOf { select, .. } =>
reject_focus_expr(select, "xsl:copy-of select=")?,
Instr::If { test, body } => {
reject_focus_expr(test, "xsl:if test=")?;
reject_function_body_focus_use(body)?;
}
Instr::Choose { whens, otherwise } => {
for (test, b) in whens {
reject_focus_expr(test, "xsl:when test=")?;
reject_function_body_focus_use(b)?;
}
if let Some(b) = otherwise { reject_function_body_focus_use(b)?; }
}
Instr::Variable(v) => {
if let Some(sel) = &v.select {
reject_focus_expr(sel, "xsl:variable select=")?;
}
reject_function_body_focus_use(&v.body)?;
}
Instr::Document { body }
| Instr::Fallback { body }
| Instr::Message { body, .. } => reject_function_body_focus_use(body)?,
_ => {}
}
}
Ok(())
}
fn desugar_body_to_xpath(body: &[Instr]) -> Option<sup_xml_core::xpath::ast::Expr> {
use sup_xml_core::xpath::ast::Expr;
if body.is_empty() {
return Some(Expr::Sequence(Vec::new()));
}
desugar_tail(body)
}
fn desugar_tail(tail: &[Instr]) -> Option<sup_xml_core::xpath::ast::Expr> {
use sup_xml_core::xpath::ast::Expr;
if tail.is_empty() {
return Some(Expr::Sequence(Vec::new()));
}
if tail.len() == 1 {
return desugar_one(&tail[0]);
}
if matches!(&tail[0], Instr::Variable(_)) {
return None;
}
let mut items: Vec<Expr> = Vec::with_capacity(tail.len());
for ins in tail {
items.push(desugar_one(ins)?);
}
Some(Expr::Sequence(items))
}
fn desugar_one(ins: &Instr) -> Option<sup_xml_core::xpath::ast::Expr> {
use sup_xml_core::xpath::ast::Expr;
match ins {
Instr::Sequence { select } => Some(select.clone()),
Instr::ValueOf { select, separator, .. } if separator.is_none() =>
Some(Expr::FunctionCall("string".into(), vec![select.clone()])),
Instr::CopyOf { select, .. } => Some(select.clone()),
Instr::If { test, body } => {
let then_e = desugar_tail(body)?;
Some(Expr::IfThenElse {
cond: Box::new(test.clone()),
then_branch: Box::new(then_e),
else_branch: Box::new(Expr::Sequence(Vec::new())),
})
}
Instr::Choose { whens, otherwise } => {
let mut acc: Expr = match otherwise {
Some(o) => desugar_tail(o)?,
None => Expr::Sequence(Vec::new()),
};
for (test, body) in whens.iter().rev() {
let then_e = desugar_tail(body)?;
acc = Expr::IfThenElse {
cond: Box::new(test.clone()),
then_branch: Box::new(then_e),
else_branch: Box::new(acc),
};
}
Some(acc)
}
Instr::LiteralText { text, dose: _ } => {
if !text.is_empty() && text.chars().all(char::is_whitespace) {
Some(Expr::Sequence(Vec::new()))
} else {
Some(Expr::Literal(text.clone()))
}
}
Instr::Variable(v) if v.body.is_empty() && v.select.is_none() => {
Some(Expr::Sequence(Vec::new()))
}
_ => None,
}
}
const TOP_LEVEL_IMPORT_PRECEDENCE: i32 = 0;
thread_local! {
static IMPORT_CHAIN: std::cell::RefCell<std::collections::HashSet<String>>
= std::cell::RefCell::new(std::collections::HashSet::new());
}
pub fn compile_with_imports(
text: &str,
loader: &dyn Loader,
base: Option<&str>,
acc: StylesheetAst,
precedence_counter: &mut i32,
) -> Result<StylesheetAst, XsltError> {
PACKAGE_ID_COUNTER.with(|c| c.set(0));
#[cfg_attr(not(feature = "xsd"), allow(unused_mut))]
let mut ast = compile_with_imports_inner(text, loader, base, acc, precedence_counter)?;
finalize_decimal_format_conflicts(&ast)?;
#[cfg(feature = "xsd")]
{
struct LoaderResolver<'a> { loader: &'a dyn Loader, base: Option<&'a str> }
impl sup_xml_core::xsd::SchemaResolver for LoaderResolver<'_> {
fn resolve(&self, location: &str, _ns: Option<&str>)
-> std::result::Result<Option<Vec<u8>>, std::io::Error>
{
Ok(self.loader.load(location, self.base).ok().map(String::into_bytes))
}
}
let imports = ast.schema_imports.clone();
for (_ns, location) in &imports {
if let Ok(text) = loader.load(location, base) {
let resolver = LoaderResolver { loader, base };
if let Ok(schema) = sup_xml_core::xsd::Schema::compile_with(&text, resolver) {
ast.schemas.push(std::sync::Arc::new(schema));
}
}
}
let inline = ast.inline_schemas.clone();
for text in &inline {
let resolver = LoaderResolver { loader, base };
if let Ok(schema) = sup_xml_core::xsd::Schema::compile_with(text, resolver) {
ast.schemas.push(std::sync::Arc::new(schema));
}
}
}
Ok(ast)
}
fn compile_with_imports_inner(
text: &str,
loader: &dyn Loader,
base: Option<&str>,
mut acc: StylesheetAst,
precedence_counter: &mut i32,
) -> Result<StylesheetAst, XsltError> {
let prev_base = MODULE_BASE_URI.with(|b|
std::mem::replace(&mut *b.borrow_mut(), base.map(str::to_string)));
struct BaseGuard(Option<String>);
impl Drop for BaseGuard {
fn drop(&mut self) {
MODULE_BASE_URI.with(|b| *b.borrow_mut() = self.0.take());
}
}
let _base_guard = BaseGuard(prev_base);
let chain_key = base.unwrap_or("").to_string();
let inserted = IMPORT_CHAIN.with(|c|
if !chain_key.is_empty() && c.borrow().contains(&chain_key) {
false
} else {
c.borrow_mut().insert(chain_key.clone());
true
});
if !inserted {
return Err(XsltError::InvalidStylesheet(format!(
"stylesheet '{chain_key}' includes itself (directly or via a chain)"
)));
}
struct ChainGuard(String);
impl Drop for ChainGuard {
fn drop(&mut self) {
IMPORT_CHAIN.with(|c| { c.borrow_mut().remove(&self.0); });
}
}
let _guard = ChainGuard(chain_key);
let resolver: Option<std::sync::Arc<dyn sup_xml_core::EntityResolver>> = base
.and_then(|b| std::path::Path::new(b).parent().map(std::path::Path::to_path_buf))
.map(|dir| {
let r: std::sync::Arc<dyn sup_xml_core::EntityResolver> =
std::sync::Arc::new(sup_xml_core::FilesystemResolver::new(vec![dir]));
r
});
let opts = sup_xml_core::ParseOptions {
namespace_aware: true,
load_external_dtd: resolver.is_some(),
external_resolver: resolver,
base_url: base.map(str::to_string),
..Default::default()
};
let doc = sup_xml_core::parse_str(text, &opts).map_err(XsltError::from)?;
let root = doc.root();
if root.is_element() {
if is_xslt_element(root)
&& matches!(root.local_name(), "stylesheet" | "transform" | "package")
{
if let Some(v) = read_attribute(root, "version") {
if !is_xs_decimal_lexical(v) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{} version='{v}' is not a valid xs:decimal (XTSE0110)",
root.local_name()
)));
}
}
}
if let Some(uw) = read_attribute(root, "use-when") {
if !evaluate_use_when_at(uw, Some(root))? {
return Ok(acc);
}
}
}
let local = compile(&doc)?;
let pkg_refs = (!local.use_packages.is_empty()).then(|| capture_pkg_refs(&local));
let this_precedence = *precedence_counter;
let mut local_templates = local.templates;
for t in &mut local_templates {
t.import_precedence = this_precedence;
}
let mut local_attribute_sets = local.attribute_sets;
for s in &mut local_attribute_sets {
s.import_precedence = this_precedence;
}
let mut local_whitespace_rules = local.whitespace_rules;
for r in &mut local_whitespace_rules {
match r {
WhitespaceRule::Strip(_, p) | WhitespaceRule::Preserve(_, p) => {
*p = this_precedence;
}
}
}
for (p, u) in local.namespaces {
acc.namespaces.entry(p).or_insert(u);
}
acc.templates.extend(local_templates);
acc.global_variables.extend(local.global_variables);
acc.global_params.extend(local.global_params);
acc.keys.extend(local.keys);
acc.attribute_sets.extend(local_attribute_sets);
acc.modes.extend(local.modes);
acc.accumulators.extend(local.accumulators);
acc.schema_imports.extend(local.schema_imports);
acc.inline_schemas.extend(local.inline_schemas);
for (k, df) in &local.decimal_formats {
let mask = local.decimal_format_explicit.get(k).copied().unwrap_or(0);
let conflict = local.decimal_format_conflicts.get(k).copied().unwrap_or(0);
merge_decimal_format_into(&mut acc, k, df, mask, conflict, this_precedence);
}
acc.whitespace_rules.extend(local_whitespace_rules);
acc.outputs.extend(local.outputs);
acc.input_type_annotations.extend(local.input_type_annotations);
acc.includes.extend(local.includes.iter().cloned());
acc.imports.extend(local.imports.iter().cloned());
acc.namespace_aliases.extend(local.namespace_aliases);
acc.documents_to_load.extend(local.documents_to_load);
acc.functions.extend(local.functions);
acc.character_maps.extend(local.character_maps);
acc.exposes.extend(local.exposes);
if acc.version.is_empty() { acc.version = local.version; }
if acc.xml_base.is_none() { acc.xml_base = local.xml_base; }
for (href, &inc_pos) in local.includes.iter().zip(local.include_positions.iter()) {
let inc_text = loader.load(href, base)?;
let inc_base = loader.resolve(href, base).ok();
let prev = *precedence_counter;
*precedence_counter = this_precedence;
let before = acc.templates.len();
acc = compile_with_imports_inner(&inc_text, loader, inc_base.as_deref(), acc, precedence_counter)?;
for t in &mut acc.templates[before..] {
let mut p = vec![inc_pos];
p.append(&mut t.source_path);
t.source_path = p;
}
*precedence_counter = prev;
}
for href in local.imports.iter().rev() {
let imp_text = loader.load(href, base)?;
let imp_base = loader.resolve(href, base).ok();
*precedence_counter -= 1;
acc = compile_with_imports_inner(&imp_text, loader, imp_base.as_deref(), acc, precedence_counter)?;
}
for up in &local.use_packages {
merge_package_components(&mut acc, &up.overrides, this_precedence);
match get_package_source(&up.name) {
Some((src, pkg_base)) => {
let before_fns = acc.functions.len();
let before_vars = acc.global_variables.len();
let before_params = acc.global_params.len();
let before_templates = acc.templates.len();
let before_attr_sets = acc.attribute_sets.len();
let before_aliases = acc.namespace_aliases.len();
let pkg_id = PACKAGE_ID_COUNTER.with(|c| { let n = c.get() + 1; c.set(n); n });
*precedence_counter -= 1;
acc = compile_with_imports_inner(
&src, loader, pkg_base.as_deref(), acc, precedence_counter)?;
for t in &mut acc.templates[before_templates..] {
if t.package_id == 0 { t.package_id = pkg_id; }
}
for f in &mut acc.functions[before_fns..] {
if f.package_id == 0 { f.package_id = pkg_id; }
}
for v in &mut acc.global_variables[before_vars..] {
if v.package_id == 0 { v.package_id = pkg_id; }
}
if acc.namespace_aliases.len() > before_aliases {
let own: Vec<_> = acc.namespace_aliases.split_off(before_aliases);
acc.package_aliases.entry(pkg_id).or_default().extend(own);
}
let saved_ctr = PACKAGE_ID_COUNTER.with(|c| c.get());
let mut tmp_prec = TOP_LEVEL_IMPORT_PRECEDENCE;
if let Ok(pure) = compile_with_imports_inner(
&src, loader, pkg_base.as_deref(),
StylesheetAst::default(), &mut tmp_prec)
{
if !pure.decimal_formats.is_empty() {
acc.package_decimal_formats.entry(pkg_id)
.or_default().extend(pure.decimal_formats);
}
}
PACKAGE_ID_COUNTER.with(|c| c.set(saved_ctr));
check_override_homonyms(
&up.overrides,
&acc.templates[before_templates..],
&acc.functions[before_fns..],
&acc.global_variables[before_vars..],
&acc.global_params[before_params..],
&acc.attribute_sets[before_attr_sets..])?;
if let Some(refs) = &pkg_refs {
let used_fns = acc.functions[before_fns..].to_vec();
let used_vars = acc.global_variables[before_vars..].to_vec();
check_used_package_privates(refs, &used_fns, &used_vars)?;
}
}
None => return Err(XsltError::InvalidStylesheet(format!(
"xsl:use-package: no package named '{}' is available (XTSE3000)",
up.name))),
}
}
Ok(acc)
}
fn visible_to_user(vis: &Option<String>) -> bool {
matches!(vis.as_deref(), Some("public") | Some("final") | Some("abstract"))
}
fn expand_lexical(name: &str, ns: &std::collections::HashMap<String, String>) -> String {
match name.split_once(':') {
Some((p, l)) => match ns.get(p) {
Some(uri) if !uri.is_empty() => format!("{{{uri}}}{l}"),
_ => name.to_string(),
},
None => name.to_string(),
}
}
fn collect_expr_refs(e: &Expr, fns: &mut Vec<String>, vars: &mut Vec<String>) {
use sup_xml_core::xpath::ast::Expr::*;
use sup_xml_core::xpath::ast::LocationPath;
let two = |a: &Expr, b: &Expr, f: &mut Vec<String>, v: &mut Vec<String>| {
collect_expr_refs(a, f, v); collect_expr_refs(b, f, v);
};
match e {
FunctionCall(name, args) => {
fns.push(name.clone());
for a in args { collect_expr_refs(a, fns, vars); }
}
Variable(n) => vars.push(n.clone()),
Or(l, r) | And(l, r) | Eq(l, r) | Ne(l, r) | Lt(l, r) | Gt(l, r)
| Le(l, r) | Ge(l, r) | ValueEq(l, r) | ValueNe(l, r) | ValueLt(l, r)
| ValueGt(l, r) | ValueLe(l, r) | ValueGe(l, r) | Add(l, r) | Sub(l, r)
| Mul(l, r) | Div(l, r) | Mod(l, r) | Union(l, r) | IDiv(l, r)
| Intersect(l, r) | Except(l, r) | Range(l, r) | SimpleMap(l, r)
| NodeBefore(l, r) | NodeAfter(l, r) | NodeIs(l, r) => two(l, r, fns, vars),
Neg(x) | InstanceOf(x, _) | CastAs(x, _) | CastableAs(x, _)
| TreatAs(x, _) | WithDefaultCollation(_, x) | BackwardsCompat(x) =>
collect_expr_refs(x, fns, vars),
IfThenElse { cond, then_branch, else_branch } => {
collect_expr_refs(cond, fns, vars);
collect_expr_refs(then_branch, fns, vars);
collect_expr_refs(else_branch, fns, vars);
}
For { bindings, body } | Let { bindings, body }
| Quantified { bindings, test: body, .. } => {
for (_, ex) in bindings { collect_expr_refs(ex, fns, vars); }
collect_expr_refs(body, fns, vars);
}
Sequence(items) => for x in items { collect_expr_refs(x, fns, vars); },
FilterPath { primary, predicates, steps } => {
collect_expr_refs(primary, fns, vars);
for p in predicates { collect_expr_refs(p, fns, vars); }
for s in steps { for p in &s.predicates { collect_expr_refs(p, fns, vars); } }
}
Path(LocationPath::Absolute(steps)) | Path(LocationPath::Relative(steps)) => {
for s in steps { for p in &s.predicates { collect_expr_refs(p, fns, vars); } }
}
_ => {}
}
}
fn collect_body_refs(body: &[Instr], fns: &mut Vec<String>, vars: &mut Vec<String>) {
crate::walk::walk_body(body, &mut |e: &Expr| collect_expr_refs(e, fns, vars));
}
struct PkgRefs {
ref_fns: Vec<String>,
ref_vars: Vec<String>,
own_fns: std::collections::HashSet<String>,
own_vars: std::collections::HashSet<String>,
ns: std::collections::HashMap<String, String>,
}
fn capture_pkg_refs(local: &StylesheetAst) -> PkgRefs {
use std::collections::HashSet;
let mut ref_fns = Vec::new();
let mut ref_vars = Vec::new();
for t in &local.templates { collect_body_refs(&t.body, &mut ref_fns, &mut ref_vars); }
for f in &local.functions { collect_body_refs(&f.body, &mut ref_fns, &mut ref_vars); }
for v in &local.global_variables {
if let Some(s) = &v.select { collect_expr_refs(s, &mut ref_fns, &mut ref_vars); }
collect_body_refs(&v.body, &mut ref_fns, &mut ref_vars);
}
let mut own_fns: HashSet<String> = local.functions.iter().map(|f| qname_key(&f.name)).collect();
let mut own_vars: HashSet<String> = local.global_variables.iter().map(|v| qname_key(&v.name))
.chain(local.global_params.iter().map(|p| qname_key(&p.name))).collect();
for up in &local.use_packages {
for t in &up.overrides.templates { collect_body_refs(&t.body, &mut ref_fns, &mut ref_vars); }
for f in &up.overrides.functions {
collect_body_refs(&f.body, &mut ref_fns, &mut ref_vars);
own_fns.insert(qname_key(&f.name));
}
for v in &up.overrides.global_variables {
if let Some(s) = &v.select { collect_expr_refs(s, &mut ref_fns, &mut ref_vars); }
own_vars.insert(qname_key(&v.name));
}
}
PkgRefs { ref_fns, ref_vars, own_fns, own_vars, ns: local.namespaces.clone() }
}
fn check_used_package_privates(
refs: &PkgRefs,
used_functions: &[UserFunction],
used_variables: &[Variable],
) -> Result<(), XsltError> {
use std::collections::HashSet;
let priv_fns: HashSet<String> = used_functions.iter()
.filter(|f| !visible_to_user(&f.visibility)).map(|f| qname_key(&f.name)).collect();
let pub_fns: HashSet<String> = used_functions.iter()
.filter(|f| visible_to_user(&f.visibility)).map(|f| qname_key(&f.name)).collect();
let priv_vars: HashSet<String> = used_variables.iter()
.filter(|v| !visible_to_user(&v.visibility)).map(|v| qname_key(&v.name)).collect();
let pub_vars: HashSet<String> = used_variables.iter()
.filter(|v| visible_to_user(&v.visibility)).map(|v| qname_key(&v.name)).collect();
for r in &refs.ref_fns {
let key = expand_lexical(r, &refs.ns);
if priv_fns.contains(&key) && !pub_fns.contains(&key) && !refs.own_fns.contains(&key) {
return Err(XsltError::InvalidStylesheet(format!(
"reference to private function '{r}' of a used package (XPST0017)")));
}
}
for r in &refs.ref_vars {
let key = expand_lexical(r, &refs.ns);
if priv_vars.contains(&key) && !pub_vars.contains(&key) && !refs.own_vars.contains(&key) {
return Err(XsltError::InvalidStylesheet(format!(
"reference to private variable '${r}' of a used package (XPST0008)")));
}
}
Ok(())
}
fn check_override_homonyms(
overrides: &StylesheetAst,
used_templates: &[Template],
used_functions: &[UserFunction],
used_variables: &[Variable],
used_params: &[Param],
used_attr_sets: &[AttributeSet],
) -> Result<(), XsltError> {
use std::collections::HashSet;
let t_names: HashSet<String> = used_templates.iter()
.filter_map(|t| t.name.as_ref().map(qname_key)).collect();
let f_names: HashSet<String> = used_functions.iter().map(|f| qname_key(&f.name)).collect();
let v_names: HashSet<String> = used_variables.iter().map(|v| qname_key(&v.name))
.chain(used_params.iter().map(|p| qname_key(&p.name))).collect();
let a_names: HashSet<String> = used_attr_sets.iter().map(|s| qname_key(&s.name)).collect();
let err = |kind: &str, name: String| Err(XsltError::InvalidStylesheet(format!(
"xsl:override {kind} '{name}' does not override a component of the \
used package (XTSE3058)")));
for t in &overrides.templates {
if let Some(n) = &t.name {
if !t_names.contains(&qname_key(n)) { return err("template", qname_key(n)); }
}
}
for f in &overrides.functions {
if !f_names.contains(&qname_key(&f.name)) { return err("function", qname_key(&f.name)); }
}
for v in &overrides.global_variables {
if !v_names.contains(&qname_key(&v.name)) { return err("variable", qname_key(&v.name)); }
}
for s in &overrides.attribute_sets {
if !a_names.contains(&qname_key(&s.name)) { return err("attribute-set", qname_key(&s.name)); }
}
Ok(())
}
fn merge_package_components(acc: &mut StylesheetAst, sub: &StylesheetAst, prec: i32) {
for t in &sub.templates {
let mut t = t.clone();
t.import_precedence = prec;
acc.templates.push(t);
}
for s in &sub.attribute_sets {
let mut s = s.clone();
s.import_precedence = prec;
acc.attribute_sets.push(s);
}
acc.global_variables.extend(sub.global_variables.iter().cloned());
acc.global_params.extend(sub.global_params.iter().cloned());
acc.functions.extend(sub.functions.iter().cloned());
acc.accumulators.extend(sub.accumulators.iter().cloned());
acc.modes.extend(sub.modes.iter().cloned());
acc.keys.extend(sub.keys.iter().cloned());
for (k, df) in &sub.decimal_formats {
let mask = sub.decimal_format_explicit.get(k).copied().unwrap_or(0);
let conflict = sub.decimal_format_conflicts.get(k).copied().unwrap_or(0);
merge_decimal_format_into(acc, k, df, mask, conflict, prec);
}
acc.character_maps.extend(sub.character_maps.iter().cloned());
}
fn compile_template(node: &Node) -> Result<Template, XsltError> {
validate_xslt_only_attributes(node, "xsl:template",
&["match", "name", "priority", "mode", "as", "visibility"])?;
let match_pattern = match read_attribute(node, "match") {
Some(s) => {
let leading = strip_leading_xpath_comments_and_space(s.as_ref());
if leading.starts_with('(') {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:template match='{s}' uses a parenthesised \
top-level expression, which is not allowed in a \
pattern (XTSE0340)"
)));
}
let e = parse_xpath_at(node, s).map_err(XsltError::from)?;
reject_pattern_grouping_calls(&e, "xsl:template match=")?;
reject_invalid_pattern_axes(&e, "xsl:template match=")?;
reject_invalid_pattern_key_calls(&e, "xsl:template match=")?;
ensure_pattern_shape(&e, "xsl:template match=")?;
Some(e)
}
None => None,
};
let name = read_attribute(node, "name")
.map(|s| parse_qname_on(node, s))
.transpose()?;
if let Some(n) = &name { reject_reserved_name(n, "xsl:template")?; }
let mut modes: Vec<QName> = Vec::new();
let mut modes_match_all = false;
let mut seen_keys: std::collections::HashSet<String> = std::collections::HashSet::new();
if let Some(raw) = read_attribute(node, "mode") {
let mut tok_count = 0usize;
for tok in raw.split_whitespace() {
tok_count += 1;
let key = tok.to_string();
if !seen_keys.insert(key.clone()) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:template mode='{raw}' lists '{tok}' more than once (XTSE0550)"
)));
}
match tok {
"#all" => modes_match_all = true,
"#default" => modes.push(QName {
prefix: None, local: String::new(), uri: String::new(),
}),
"#current" => return Err(XsltError::InvalidStylesheet(
"xsl:template mode='#current' is not valid on the \
template declaration (XTSE0550)".into()
)),
qn => modes.push(parse_qname_on(node, qn)?),
}
}
if tok_count == 0 {
return Err(XsltError::InvalidStylesheet(
"xsl:template mode= attribute must list at least one token (XTSE0550)".into()
));
}
if modes_match_all && tok_count > 1 {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:template mode='{raw}' — '#all' must appear alone (XTSE0550)"
)));
}
}
let mode = modes.first().cloned().and_then(|m| {
if m.local.is_empty() && m.uri.is_empty() { None } else { Some(m) }
});
let priority = match read_attribute(node, "priority") {
Some(s) => {
if !is_xs_decimal_lexical(s) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:template priority='{s}' is not a valid xs:decimal \
(XTSE0530)"
)));
}
Some(s.trim().parse::<f64>().map_err(|_| XsltError::InvalidStylesheet(
format!("xsl:template priority must be a number, got '{s}'")))?)
}
None => None,
};
if match_pattern.is_none() && name.is_none() {
return Err(XsltError::InvalidStylesheet(
"xsl:template must have either match= or name= (XTSE0500)".into(),
));
}
if match_pattern.is_none() {
if mode.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:template with no match= cannot carry mode= (XTSE0500)".into(),
));
}
if priority.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:template with no match= cannot carry priority= (XTSE0500)".into(),
));
}
}
let mut params = Vec::new();
let mut body = Body::new();
let mut seen_non_param = false;
for child in node.children() {
if !child.is_element() && !is_significant_text(child) { continue; }
if !seen_non_param
&& child.is_element() && is_xslt_element(child)
&& child.local_name() == "param"
{
let p = compile_param(child)?;
let key = qname_key(&p.name);
if params.iter().any(|q: &Param| qname_key(&q.name) == key) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:param '{key}' on xsl:template (XTSE0580)"
)));
}
params.push(p);
continue;
}
seen_non_param = true;
compile_instr_into_body(child, &mut body)?;
}
let as_type = read_attribute(node, "as").map(str::to_string);
Ok(Template {
match_pattern, name, mode, modes, modes_match_all, priority,
import_precedence: TOP_LEVEL_IMPORT_PRECEDENCE,
source_path: Vec::new(),
params, body, as_type,
visibility: read_attribute(node, "visibility").map(str::to_string),
package_id: 0,
})
}
fn compile_variable(node: &Node) -> Result<Variable, XsltError> {
validate_xslt_only_attributes(node, "xsl:variable",
&["name", "select", "as", "static", "visibility"])?;
let name = required_qname_attr(node, "name", "xsl:variable")?;
reject_reserved_name(&name, "xsl:variable")?;
let (select, body) = split_select_and_body(node)?;
reject_select_with_body(node, &select, &body, "xsl:variable")?;
let as_type = read_attribute(node, "as").map(str::to_string);
let base_uri = effective_xml_base(node);
let visibility = read_attribute(node, "visibility").map(str::to_string);
Ok(Variable { name, select, body, as_type, base_uri, visibility, package_id: 0 })
}
fn compile_param(node: &Node) -> Result<Param, XsltError> {
validate_xslt_only_attributes(node, "xsl:param",
&["name", "select", "as", "required", "tunnel", "static"])?;
let name = required_qname_attr(node, "name", "xsl:param")?;
reject_reserved_name(&name, "xsl:param")?;
let (select, body) = split_select_and_body(node)?;
reject_select_with_body(node, &select, &body, "xsl:param")?;
let tunnel = if is_xslt_2_0_compile() {
match read_attribute(node, "tunnel") {
Some(v) => parse_yesno_strict(v, "xsl:param", "tunnel")?,
None => false,
}
} else { false };
let as_type = read_attribute(node, "as").map(str::to_string);
let required = if is_xslt_2_0_compile() {
match read_attribute(node, "required") {
Some(v) => parse_yesno_strict(v, "xsl:param", "required")?,
None => false,
}
} else { false };
if required && (select.is_some() || !body.is_empty()) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:param '{}' is required and cannot specify a default \
value (XTSE0010)", qname_key(&name),
)));
}
Ok(Param { name, select, body, tunnel, as_type, required })
}
fn reject_select_with_body(
_node: &Node, select: &Option<Expr>, body: &[Instr], who: &str,
) -> Result<(), XsltError> {
if in_forwards_compat_mode() { return Ok(()); }
let has_body = body.iter().any(|i| !matches!(i,
Instr::LiteralText { text, .. } if text.trim().is_empty()));
if select.is_some() && has_body {
return Err(XsltError::InvalidStylesheet(format!(
"{who} cannot have both a select= attribute and a non-empty \
body (XTSE0620)"
)));
}
Ok(())
}
fn split_select_and_body(node: &Node)
-> Result<(Option<Expr>, Body), XsltError>
{
let select = match read_attribute(node, "select") {
Some(s) => Some(parse_xpath_at(node, s).map_err(XsltError::from)?),
None => None,
};
let mut body = Body::new();
for child in node.children() {
if !child.is_element() && !is_significant_text(child) { continue; }
compile_instr_into_body(child, &mut body)?;
}
Ok((select, body))
}
fn select_or_body(node: &Node, who: &str) -> Result<Body, XsltError> {
let (select, body) = split_select_and_body(node)?;
reject_select_with_body(node, &select, &body, who)?;
Ok(match select {
Some(select) => body_of_one(node, Instr::Sequence { select }),
None => body,
})
}
fn compile_key(node: &Node) -> Result<Key, XsltError> {
validate_xslt_only_attributes(node, "xsl:key",
&["name", "match", "use", "collation"])?;
let name = required_qname_attr(node, "name", "xsl:key")?;
reject_reserved_name(&name, "xsl:key")?;
let collation = if let Some(c) = read_attribute(node, "collation") {
if !in_forwards_compat_mode() && !is_recognised_collation(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:key collation='{c}' is not recognised by the processor (XTSE1210)"
)));
}
Some(c.trim().to_string())
} else {
effective_default_collation(node)
};
let m = require_attr(node, "match", "xsl:key")?;
let matcher = parse_xpath_at(node, m).map_err(XsltError::from)?;
reject_invalid_pattern_axes(&matcher, "xsl:key match=")?;
ensure_pattern_shape(&matcher, "xsl:key match=")?;
let has_content = node.children().any(|c| match c.kind {
NodeKind::Element => true,
NodeKind::Text | NodeKind::CData => !is_xslt_whitespace_only(c.content()),
_ => false,
});
match (read_attribute(node, "use"), has_content) {
(Some(_), true) => Err(XsltError::InvalidStylesheet(
"xsl:key must not have both a use= attribute and a \
sequence constructor (XTSE1205)".into())),
(None, false) => Err(XsltError::InvalidStylesheet(
"xsl:key requires either a use= attribute or a sequence \
constructor (XTSE1205)".into())),
(Some(u), false) => Ok(Key {
name,
matcher,
use_: parse_xpath_at(node, u).map_err(XsltError::from)?,
body: Body::new(),
collation,
}),
(None, true) => Ok(Key {
name,
matcher,
use_: Expr::Sequence(Vec::new()),
body: compile_body(node)?,
collation,
}),
}
}
fn compile_mode(node: &Node) -> Result<ModeDecl, XsltError> {
validate_xslt_only_attributes(node, "xsl:mode", &[
"name", "streamable", "on-no-match", "on-multiple-match",
"warning-on-no-match", "warning-on-multiple-match", "typed", "visibility",
])?;
validate_must_be_empty(node, "xsl:mode")?;
let name = match read_attribute(node, "name") {
None | Some("#default") | Some("#unnamed") => None,
Some(qn) => Some(parse_qname_on(node, qn)?),
};
let on_no_match = match read_attribute(node, "on-no-match") {
None | Some("text-only-copy") => OnNoMatch::TextOnlyCopy,
Some("deep-copy") => OnNoMatch::DeepCopy,
Some("shallow-copy") => OnNoMatch::ShallowCopy,
Some("deep-skip") => OnNoMatch::DeepSkip,
Some("shallow-skip") => OnNoMatch::ShallowSkip,
Some("fail") => OnNoMatch::Fail,
Some(other) => return Err(XsltError::InvalidStylesheet(format!(
"xsl:mode on-no-match='{other}' is not a recognised value (XTSE0020)"))),
};
Ok(ModeDecl { name, on_no_match })
}
fn compile_use_package(node: &Node) -> Result<UsePackage, XsltError> {
let name = require_attr(node, "name", "xsl:use-package")?.to_string();
let version = read_attribute(node, "package-version").map(str::to_string);
let mut overrides = StylesheetAst::default();
let mut pos: u32 = 0;
for child in node.children() {
if !child.is_element() || !is_xslt_element(child) { continue; }
match child.local_name() {
"override" => {
for decl in child.children() {
if !decl.is_element() { continue; }
compile_top_level(decl, &mut overrides, pos)?;
pos += 1;
}
}
"accept" => {}
_ => {}
}
}
Ok(UsePackage { name, version, overrides: Box::new(overrides) })
}
fn validate_expose(node: &Node) -> Result<crate::ast::ExposeDecl, XsltError> {
if let Some(v) = read_attribute(node, "visibility") {
if !matches!(v.trim(),
"public" | "private" | "final" | "abstract" | "hidden")
{
return Err(XsltError::InvalidStylesheet(format!(
"xsl:expose visibility='{v}' is not a valid visibility (XTSE0020)")));
}
}
let component = read_attribute(node, "component").map(|c| c.trim().to_string());
if let Some(c) = &component {
if !matches!(c.as_str(),
"template" | "function" | "attribute-set" | "variable" | "mode" | "*")
{
return Err(XsltError::InvalidStylesheet(format!(
"xsl:expose component='{c}' is not a valid component type (XTSE0020)")));
}
}
if read_attribute(node, "visibility").map(str::trim) == Some("abstract") {
return Err(XsltError::InvalidStylesheet(
"xsl:expose cannot make a component abstract (XTSE3025)".into()));
}
if component.as_deref() == Some("*") {
if let Some(names) = read_attribute(node, "names") {
let all_wild = names.split_whitespace().all(|t|
t == "*" || t.starts_with("*:") || t.ends_with(":*"));
if !all_wild {
return Err(XsltError::InvalidStylesheet(
"xsl:expose component='*' requires a wildcard names= (XTSE3022)".into()));
}
}
}
let ns: std::collections::HashMap<String, String> =
collect_in_scope_namespaces(node).into_iter()
.filter_map(|(p, u)| p.map(|p| (p, u)))
.collect();
let mut names = Vec::new();
if let Some(attr) = read_attribute(node, "names") {
for tok in attr.split_whitespace() {
validate_component_token(tok, &ns, "xsl:expose")?;
names.push(tok.to_string());
}
}
Ok(ExposeDecl {
component: component.unwrap_or_default(),
visibility: read_attribute(node, "visibility")
.map(|v| v.trim().to_string()).unwrap_or_default(),
names,
namespaces: ns,
})
}
fn validate_component_token(
tok: &str,
ns: &std::collections::HashMap<String, String>,
who: &str,
) -> Result<(), XsltError> {
if tok.starts_with('#') {
return Err(XsltError::InvalidStylesheet(format!(
"{who} names token '{tok}' is not permitted (XTSE0020)")));
}
if tok == "*" || tok.starts_with("*:") || tok.starts_with("Q{") {
return Ok(());
}
if let Some((prefix, _)) = tok.split_once(':') {
if !ns.contains_key(prefix) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} names token '{tok}' uses undeclared prefix '{prefix}' (XTSE0020)")));
}
}
Ok(())
}
fn expand_component_token(tok: &str, ns: &std::collections::HashMap<String, String>) -> String {
let tok = tok.split('#').next().unwrap_or(tok);
if let Some(rest) = tok.strip_prefix("Q{") {
if let Some(end) = rest.find('}') {
return format!("{{{}}}{}", &rest[..end], &rest[end + 1..]);
}
}
expand_lexical(tok, ns)
}
pub(crate) fn validate_package_exposes(ast: &StylesheetAst) -> Result<(), XsltError> {
if ast.exposes.is_empty() { return Ok(()); }
use std::collections::HashSet;
let templates: HashSet<String> = ast.templates.iter()
.filter_map(|t| t.name.as_ref().map(qname_key)).collect();
let functions: HashSet<String> = ast.functions.iter().map(|f| qname_key(&f.name)).collect();
let variables: HashSet<String> = ast.global_variables.iter().map(|v| qname_key(&v.name))
.chain(ast.global_params.iter().map(|p| qname_key(&p.name))).collect();
let attr_sets: HashSet<String> = ast.attribute_sets.iter().map(|s| qname_key(&s.name)).collect();
let modes: HashSet<String> = ast.modes.iter()
.filter_map(|m| m.name.as_ref().map(qname_key)).collect();
let mut declared_vis: std::collections::HashMap<(&str, String), &str> =
std::collections::HashMap::new();
for t in &ast.templates {
if let (Some(n), Some(v)) = (&t.name, &t.visibility) {
declared_vis.insert(("template", qname_key(n)), v.as_str());
}
}
for f in &ast.functions {
if let Some(v) = &f.visibility {
declared_vis.insert(("function", qname_key(&f.name)), v.as_str());
}
}
for v in &ast.global_variables {
if let Some(vis) = &v.visibility {
declared_vis.insert(("variable", qname_key(&v.name)), vis.as_str());
}
}
for ex in &ast.exposes {
for tok in &ex.names {
if tok == "*" || tok.starts_with("*:") || tok.ends_with(":*") { continue; }
let key = expand_component_token(tok, &ex.namespaces);
if !ex.visibility.is_empty() {
if let Some(decl) = declared_vis.get(&(ex.component.as_str(), key.clone())) {
if *decl != ex.visibility {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:expose visibility='{}' is inconsistent with the \
declared visibility '{decl}' of component '{tok}' (XTSE3010)",
ex.visibility)));
}
}
}
let found = match ex.component.as_str() {
"template" => templates.contains(&key),
"function" => functions.contains(&key),
"variable" => variables.contains(&key),
"attribute-set" => attr_sets.contains(&key),
"mode" => modes.contains(&key),
"*" => [&templates, &functions, &variables, &attr_sets, &modes]
.iter().any(|s| s.contains(&key)),
_ => true,
};
if !found {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:expose names='{tok}' matches no {} component in the \
package (XTSE3020)", ex.component)));
}
}
}
Ok(())
}
fn compile_accumulator(node: &Node) -> Result<AccumulatorDecl, XsltError> {
validate_xslt_only_attributes(node, "xsl:accumulator",
&["name", "initial-value", "as", "streamable"])?;
let name = match read_attr_with_shadow(node, "name")? {
Some(s) => parse_qname_on(node, &s)?,
None => return Err(XsltError::InvalidStylesheet(
"xsl:accumulator requires a name= attribute (XTSE0010)".into())),
};
let initial_value = parse_xpath_at(node,
require_attr(node, "initial-value", "xsl:accumulator")?)
.map_err(XsltError::from)?;
let mut rules = Vec::new();
for child in node.children() {
if !child.is_element() {
if is_significant_text(child) {
return Err(XsltError::InvalidStylesheet(
"xsl:accumulator cannot contain text content (XTSE0010)".into()));
}
continue;
}
if !is_xslt_element(child) || child.local_name() != "accumulator-rule" {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:accumulator may only contain xsl:accumulator-rule, found <{}> \
(XTSE0010)", child.name())));
}
rules.push(compile_accumulator_rule(child)?);
}
Ok(AccumulatorDecl { name, initial_value, rules })
}
fn compile_accumulator_rule(node: &Node) -> Result<AccumulatorRule, XsltError> {
validate_xslt_only_attributes(node, "xsl:accumulator-rule",
&["match", "phase", "select"])?;
let match_pattern = {
let e = parse_xpath_at(node, require_attr(node, "match", "xsl:accumulator-rule")?)
.map_err(XsltError::from)?;
reject_invalid_pattern_axes(&e, "xsl:accumulator-rule match=")?;
e
};
let phase = match read_attribute(node, "phase") {
None | Some("start") => AccumulatorPhase::Start,
Some("end") => AccumulatorPhase::End,
Some(other) => return Err(XsltError::InvalidStylesheet(format!(
"xsl:accumulator-rule phase='{other}' must be 'start' or 'end' (XTSE0020)"))),
};
let select = read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s)).transpose().map_err(XsltError::from)?;
let mut body = Body::new();
if select.is_none() {
for child in node.children() {
if !child.is_element() && !is_significant_text(child) { continue; }
compile_instr_into_body(child, &mut body)?;
}
}
Ok(AccumulatorRule { match_pattern, phase, select, body })
}
fn compile_attribute_set(node: &Node) -> Result<AttributeSet, XsltError> {
validate_xslt_only_attributes(node, "xsl:attribute-set",
&["name", "use-attribute-sets"])?;
let name = required_qname_attr(node, "name", "xsl:attribute-set")?;
reject_reserved_name(&name, "xsl:attribute-set")?;
let use_attribute_sets = parse_qname_list(
node, read_attribute(node, "use-attribute-sets").unwrap_or(""),
)?;
let mut attributes = Body::new();
for child in node.children() {
if matches!(child.kind, NodeKind::Text | NodeKind::CData)
&& !is_xslt_whitespace_only(child.content())
{
return Err(XsltError::InvalidStylesheet(
"xsl:attribute-set cannot contain non-whitespace text \
content (XTSE0010 — only xsl:attribute allowed)".into(),
));
}
if !child.is_element() { continue; }
if !is_xslt_element(child) || child.local_name() != "attribute" {
return Err(XsltError::InvalidStylesheet(
"xsl:attribute-set children must be xsl:attribute".into(),
));
}
compile_instr_into_body(child, &mut attributes)?;
}
Ok(AttributeSet {
name, use_attribute_sets, attributes,
import_precedence: TOP_LEVEL_IMPORT_PRECEDENCE,
})
}
fn compile_output(node: &Node) -> Result<OutputSpec, XsltError> {
let mut out = OutputSpec::default();
out.name = match read_attribute(node, "name") {
Some(n) => Some(parse_qname_on(node, &n)?),
None => None,
};
let require_yesno = |n: &Node, attr: &str| -> Result<(), XsltError> {
if in_forwards_compat_mode() { return Ok(()); }
if let Some(v) = read_attribute(n, attr) {
if !matches!(v.trim(), "yes" | "no") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output {attr}='{v}' must be 'yes' or 'no' (XTSE0020)"
)));
}
}
Ok(())
};
require_yesno(node, "indent")?;
require_yesno(node, "omit-xml-declaration")?;
require_yesno(node, "byte-order-mark")?;
require_yesno(node, "escape-uri-attributes")?;
require_yesno(node, "include-content-type")?;
require_yesno(node, "undeclare-prefixes")?;
if !in_forwards_compat_mode() {
if let Some(v) = read_attribute(node, "standalone") {
if !matches!(v.trim(), "yes" | "no" | "omit") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output standalone='{v}' must be 'yes', 'no', or 'omit' (XTSE0020)"
)));
}
}
if let Some(v) = read_attribute(node, "normalization-form") {
if !matches!(v.trim(),
"NFC" | "NFD" | "NFKC" | "NFKD" | "fully-normalized" | "none")
&& !v.trim().starts_with("nmtoken")
{
if v.trim().is_empty() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output normalization-form='{v}' is invalid (XTSE0020)"
)));
}
}
}
}
out.method = read_attribute(node, "method").map(str::to_string);
if let Some(m) = &out.method {
match m.split_once(':') {
None => if !matches!(m.as_str(),
"xml" | "html" | "xhtml" | "text") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output method='{m}' is not one of \
xml / html / xhtml / text (XTSE1570)"
)));
},
Some(_) => {
parse_qname_on(node, m).map_err(|_| XsltError::InvalidStylesheet(
format!("xsl:output method='{m}' is not a valid EQName (XTSE1570)")
))?;
}
}
}
out.encoding = read_attribute(node, "encoding").map(str::to_string);
out.indent = read_attribute(node, "indent").map(parse_yesno);
out.omit_xml_declaration = read_attribute(node, "omit-xml-declaration").map(parse_yesno);
out.standalone = read_attribute(node, "standalone").map(|v| match v.trim() {
"yes" | "true" | "1" => crate::ast::Standalone::Yes,
"omit" => crate::ast::Standalone::Omit,
_ => crate::ast::Standalone::No,
});
out.escape_uri_attributes = read_attribute(node, "escape-uri-attributes").map(parse_yesno);
out.include_content_type = read_attribute(node, "include-content-type").map(parse_yesno);
out.byte_order_mark = read_attribute(node, "byte-order-mark").map(parse_yesno);
out.media_type = read_attribute(node, "media-type").map(str::to_string);
out.doctype_public = read_attribute(node, "doctype-public").map(str::to_string);
out.doctype_system = read_attribute(node, "doctype-system").map(str::to_string);
out.version = read_attribute(node, "version").map(str::to_string);
if let Some(s) = read_attribute(node, "cdata-section-elements") {
out.cdata_section_elements = parse_qname_list(node, s)?;
}
if let Some(s) = read_attribute(node, "use-character-maps") {
out.use_character_maps = parse_qname_list(node, s)?;
}
Ok(out)
}
fn compile_character_map(node: &Node) -> Result<CharacterMap, XsltError> {
let name = require_attr(node, "name", "xsl:character-map")?;
let qname = parse_qname_on(node, name)?;
let use_character_maps = match read_attribute(node, "use-character-maps") {
Some(s) => parse_qname_list(node, s)?,
None => Vec::new(),
};
let mut mappings: Vec<(char, String)> = Vec::new();
for child in node.children() {
if !child.is_element() { continue; }
if !is_xslt_element(&child) || child.local_name() != "output-character" {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:character-map: unexpected child <{}>; only <xsl:output-character> is allowed",
child.name()
)));
}
let ch_str = require_attr(&child, "character", "xsl:output-character")?;
let mut chs = ch_str.chars();
let ch = chs.next().ok_or_else(|| XsltError::InvalidStylesheet(
"xsl:output-character 'character' attribute is empty".into()))?;
if chs.next().is_some() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output-character 'character' must be a single character (got {ch_str:?})"
)));
}
let replacement = read_attribute(&child, "string").unwrap_or("").to_string();
mappings.push((ch, replacement));
}
Ok(CharacterMap { name: qname, use_character_maps, mappings })
}
type DfCopy = fn(&mut crate::format_number::DecimalFormat, &crate::format_number::DecimalFormat);
type DfNeq = fn(&crate::format_number::DecimalFormat, &crate::format_number::DecimalFormat) -> bool;
const DF_FIELDS: [(u16, DfCopy, DfNeq); 10] = [
(1 << 0, |d, s| d.decimal_separator = s.decimal_separator, |a, b| a.decimal_separator != b.decimal_separator),
(1 << 1, |d, s| d.grouping_separator = s.grouping_separator, |a, b| a.grouping_separator != b.grouping_separator),
(1 << 2, |d, s| d.infinity = s.infinity.clone(), |a, b| a.infinity != b.infinity),
(1 << 3, |d, s| d.minus_sign = s.minus_sign, |a, b| a.minus_sign != b.minus_sign),
(1 << 4, |d, s| d.nan = s.nan.clone(), |a, b| a.nan != b.nan),
(1 << 5, |d, s| d.percent = s.percent, |a, b| a.percent != b.percent),
(1 << 6, |d, s| d.per_mille = s.per_mille, |a, b| a.per_mille != b.per_mille),
(1 << 7, |d, s| d.zero_digit = s.zero_digit, |a, b| a.zero_digit != b.zero_digit),
(1 << 8, |d, s| d.digit = s.digit, |a, b| a.digit != b.digit),
(1 << 9, |d, s| d.pattern_separator = s.pattern_separator, |a, b| a.pattern_separator != b.pattern_separator),
];
fn merge_decimal_format_into(
acc: &mut StylesheetAst, key: &str,
df: &crate::format_number::DecimalFormat, mask: u16, src_conflict: u16, prec: i32,
) {
let entry = acc.decimal_formats.entry(key.to_string())
.or_insert_with(crate::format_number::DecimalFormat::default);
let precs = acc.decimal_format_attr_prec.entry(key.to_string())
.or_insert([i32::MIN; 10]);
let exp = acc.decimal_format_explicit.entry(key.to_string()).or_insert(0);
let conf = acc.decimal_format_conflicts.entry(key.to_string()).or_insert(0);
for (idx, (bit, copy, neq)) in DF_FIELDS.iter().enumerate() {
if mask & bit == 0 { continue; }
let inc_conflicted = src_conflict & bit != 0;
if prec > precs[idx] {
copy(entry, df);
precs[idx] = prec;
*exp |= bit;
if inc_conflicted { *conf |= bit; } else { *conf &= !bit; }
} else if prec == precs[idx] && (neq(entry, df) || inc_conflicted) {
*conf |= bit;
}
}
}
fn finalize_decimal_format_conflicts(ast: &StylesheetAst) -> Result<(), XsltError> {
for (key, &conflict) in &ast.decimal_format_conflicts {
if conflict != 0 {
let name = if key.is_empty() { "(default)".to_string() } else { key.clone() };
return Err(XsltError::InvalidStylesheet(format!(
"xsl:decimal-format {name} has conflicting declarations of \
equal import precedence (XTSE1290)"
)));
}
}
Ok(())
}
fn compile_decimal_format(node: &Node, ast: &mut StylesheetAst) -> Result<(), XsltError> {
use crate::format_number::DecimalFormat;
validate_xslt_only_attributes(node, "xsl:decimal-format",
&["name", "decimal-separator", "grouping-separator", "infinity",
"minus-sign", "NaN", "percent", "per-mille",
"zero-digit", "digit", "pattern-separator"])?;
validate_must_be_empty(node, "xsl:decimal-format")?;
let mut df = DecimalFormat::default();
let mut set_decimal_sep = None::<char>;
let mut set_grouping_sep = None::<char>;
let mut set_minus = None::<char>;
let mut set_percent = None::<char>;
let mut set_per_mille = None::<char>;
let mut set_zero_digit = None::<char>;
let mut set_digit = None::<char>;
let mut set_pattern_sep = None::<char>;
if let Some(v) = read_attribute(node, "decimal-separator")
{ let c = first_char(v, "decimal-separator")?; df.decimal_separator = c;
set_decimal_sep = Some(c); }
if let Some(v) = read_attribute(node, "grouping-separator")
{ let c = first_char(v, "grouping-separator")?; df.grouping_separator = c;
set_grouping_sep = Some(c); }
if let Some(v) = read_attribute(node, "infinity") { df.infinity = v.to_string(); }
if let Some(v) = read_attribute(node, "minus-sign")
{ let c = first_char(v, "minus-sign")?; df.minus_sign = c; set_minus = Some(c); }
if let Some(v) = read_attribute(node, "NaN") { df.nan = v.to_string(); }
if let Some(v) = read_attribute(node, "percent")
{ let c = first_char(v, "percent")?; df.percent = c; set_percent = Some(c); }
if let Some(v) = read_attribute(node, "per-mille")
{ let c = first_char(v, "per-mille")?; df.per_mille = c; set_per_mille = Some(c); }
if let Some(v) = read_attribute(node, "zero-digit")
{ let c = first_char(v, "zero-digit")?;
if !is_unicode_zero_digit(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:decimal-format zero-digit='{c}' is not a valid \
zero-digit (XTSE1295)"
)));
}
df.zero_digit = c; set_zero_digit = Some(c); }
if let Some(v) = read_attribute(node, "digit")
{ let c = first_char(v, "digit")?; df.digit = c; set_digit = Some(c); }
if let Some(v) = read_attribute(node, "pattern-separator")
{ let c = first_char(v, "pattern-separator")?; df.pattern_separator = c;
set_pattern_sep = Some(c); }
let role_chars: [(&str, Option<char>, char); 8] = [
("decimal-separator", set_decimal_sep, df.decimal_separator),
("grouping-separator", set_grouping_sep, df.grouping_separator),
("minus-sign", set_minus, df.minus_sign),
("percent", set_percent, df.percent),
("per-mille", set_per_mille, df.per_mille),
("zero-digit", set_zero_digit, df.zero_digit),
("digit", set_digit, df.digit),
("pattern-separator", set_pattern_sep, df.pattern_separator),
];
for (i, (n1, set1, c1)) in role_chars.iter().enumerate() {
for (n2, set2, c2) in &role_chars[i + 1..] {
if set1.is_none() || set2.is_none() { continue; }
if c1 == c2 {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:decimal-format: {n1} and {n2} both use {c1:?} (XTSE1300)"
)));
}
}
}
let key = match read_attribute(node, "name") {
Some(raw) => {
let qname = parse_qname_on(node, raw)?;
reject_reserved_name(&qname, "xsl:decimal-format")?;
decimal_format_key(node, raw)
}
None => String::new(),
};
let mut mask: u16 = 0;
if set_decimal_sep.is_some() { mask |= 1 << 0; }
if set_grouping_sep.is_some() { mask |= 1 << 1; }
if read_attribute(node, "infinity").is_some() { mask |= 1 << 2; }
if set_minus.is_some() { mask |= 1 << 3; }
if read_attribute(node, "NaN").is_some() { mask |= 1 << 4; }
if set_percent.is_some() { mask |= 1 << 5; }
if set_per_mille.is_some() { mask |= 1 << 6; }
if set_zero_digit.is_some() { mask |= 1 << 7; }
if set_digit.is_some() { mask |= 1 << 8; }
if set_pattern_sep.is_some() { mask |= 1 << 9; }
if let Some(existing) = ast.decimal_formats.get(&key) {
let prev_mask = ast.decimal_format_explicit.get(&key).copied().unwrap_or(0);
if !in_forwards_compat_mode() {
let overlap = mask & prev_mask;
let attrs: [(u16, &dyn Fn(&DecimalFormat, &DecimalFormat) -> bool); 10] = [
(1 << 0, &|a, b| a.decimal_separator != b.decimal_separator),
(1 << 1, &|a, b| a.grouping_separator != b.grouping_separator),
(1 << 2, &|a, b| a.infinity != b.infinity),
(1 << 3, &|a, b| a.minus_sign != b.minus_sign),
(1 << 4, &|a, b| a.nan != b.nan),
(1 << 5, &|a, b| a.percent != b.percent),
(1 << 6, &|a, b| a.per_mille != b.per_mille),
(1 << 7, &|a, b| a.zero_digit != b.zero_digit),
(1 << 8, &|a, b| a.digit != b.digit),
(1 << 9, &|a, b| a.pattern_separator != b.pattern_separator),
];
let mut conflict = 0u16;
for (bit, neq) in &attrs {
if overlap & bit != 0 && neq(existing, &df) { conflict |= bit; }
}
if conflict != 0 {
*ast.decimal_format_conflicts.entry(key.clone()).or_insert(0) |= conflict;
}
}
let mut merged = existing.clone();
if mask & (1 << 0) != 0 { merged.decimal_separator = df.decimal_separator; }
if mask & (1 << 1) != 0 { merged.grouping_separator = df.grouping_separator; }
if mask & (1 << 2) != 0 { merged.infinity = df.infinity.clone(); }
if mask & (1 << 3) != 0 { merged.minus_sign = df.minus_sign; }
if mask & (1 << 4) != 0 { merged.nan = df.nan.clone(); }
if mask & (1 << 5) != 0 { merged.percent = df.percent; }
if mask & (1 << 6) != 0 { merged.per_mille = df.per_mille; }
if mask & (1 << 7) != 0 { merged.zero_digit = df.zero_digit; }
if mask & (1 << 8) != 0 { merged.digit = df.digit; }
if mask & (1 << 9) != 0 { merged.pattern_separator = df.pattern_separator; }
ast.decimal_formats.insert(key.clone(), merged);
ast.decimal_format_explicit.insert(key, prev_mask | mask);
} else {
ast.decimal_formats.insert(key.clone(), df);
ast.decimal_format_explicit.insert(key, mask);
}
Ok(())
}
pub(crate) fn decimal_format_key(node: &Node, raw: &str) -> String {
let (prefix, local) = match raw.split_once(':') {
Some(pl) => pl,
None => return raw.to_string(),
};
let mut cur = Some(node);
while let Some(n) = cur {
for (p, href) in n.ns_declarations() {
if p == Some(prefix) {
return format!("{{{href}}}{local}");
}
}
cur = n.parent.get();
}
raw.to_string()
}
fn first_char(s: &str, attr_name: &str) -> Result<char, XsltError> {
let mut chars = s.chars();
let first = chars.next().ok_or_else(|| XsltError::InvalidStylesheet(format!(
"xsl:decimal-format '{attr_name}' attribute is empty"
)))?;
if chars.next().is_some() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:decimal-format '{attr_name}' must be a single character (got {s:?})"
)));
}
Ok(first)
}
fn compile_namespace_alias(
node: &Node,
) -> Result<Option<(String, String, Option<String>)>, XsltError> {
let style_prefix = require_attr(node, "stylesheet-prefix", "xsl:namespace-alias")?;
let result_prefix = require_attr(node, "result-prefix", "xsl:namespace-alias")?;
let style_uri = resolve_alias_prefix(node, style_prefix)?.unwrap_or_default();
let result_uri = resolve_alias_prefix(node, result_prefix)?.unwrap_or_default();
let result_prefix_owned: Option<String> = if result_prefix == "#default" {
None
} else {
Some(result_prefix.to_owned())
};
Ok(Some((style_uri, result_uri, result_prefix_owned)))
}
fn resolve_alias_prefix(node: &Node, prefix: &str) -> Result<Option<String>, XsltError> {
match prefix {
"xml" => return Ok(Some("http://www.w3.org/XML/1998/namespace".to_string())),
"xmlns" => return Ok(Some("http://www.w3.org/2000/xmlns/".to_string())),
_ => {}
}
let target = if prefix == "#default" { None } else { Some(prefix) };
let mut cur = Some(node);
while let Some(n) = cur {
for (p, href) in n.ns_declarations() {
if p == target {
return Ok(Some(href.to_string()));
}
}
cur = n.parent.get();
}
if target.is_some() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:namespace-alias references undeclared prefix '{prefix}'"
)));
}
Ok(None)
}
fn collect_whitespace_rules(
node: &Node, strip: bool, out: &mut Vec<WhitespaceRule>,
) -> Result<(), XsltError> {
let who = if strip { "xsl:strip-space" } else { "xsl:preserve-space" };
validate_must_be_empty(node, who)?;
let s = require_attr(node, "elements", who)?;
let default_uri = xpath_default_namespace_for(node);
for tok in s.split_whitespace() {
let mut q = parse_name_test_token(node, tok)?;
if q.prefix.is_none() && q.local != "*" && q.uri.is_empty() {
if let Some(uri) = &default_uri {
q.uri = uri.clone();
}
}
out.push(if strip {
WhitespaceRule::Strip(q, TOP_LEVEL_IMPORT_PRECEDENCE)
} else {
WhitespaceRule::Preserve(q, TOP_LEVEL_IMPORT_PRECEDENCE)
});
}
Ok(())
}
fn compile_instr_into_body(node: &Node, out: &mut Body) -> Result<(), XsltError> {
if out.file().is_none() {
out.set_file(current_module_file());
}
compile_raw_instr_into(node, out, node_src_pos(node))
}
fn compile_raw_instr_into(
node: &Node, out: &mut Body, pos: Option<crate::ast::SrcPos>,
) -> Result<(), XsltError> {
match node.kind {
NodeKind::Text | NodeKind::CData => {
let text = node.content();
if expand_text_in_scope(node) && (text.contains('{') || text.contains('}')) {
for part in avt(node, text)?.parts {
match part {
AvtPart::Literal(s) => {
if !s.is_empty() {
out.push(Instr::LiteralText { text: s, dose: false }, pos);
}
}
AvtPart::Expr(e) => out.push(Instr::ValueOf {
select: e,
dose: false,
separator: Some(Avt::literal(" ")),
}, pos),
}
}
return Ok(());
}
out.push(Instr::LiteralText {
text: text.to_string(),
dose: false,
}, pos);
return Ok(());
}
NodeKind::Comment | NodeKind::Pi => return Ok(()),
NodeKind::Element => {}
_ => return Ok(()),
}
let use_when_attr = if is_xslt_element(node) {
read_attribute(node, "use-when")
} else {
read_xsl_attribute(node, "use-when")
};
if let Some(uw) = use_when_attr {
if !evaluate_use_when_at(uw, Some(node))? {
return Ok(());
}
}
if !is_xslt_element(node) {
out.push(compile_literal_element(node)?, pos);
return Ok(());
}
let name = node.local_name();
let instr = match name {
"apply-templates" => compile_apply_templates(node)?,
"apply-imports" => {
validate_xslt_only_attributes(node, "xsl:apply-imports", &[])?;
for child in node.children() {
if !child.is_element() { continue; }
let allowed = is_xslt_2_0_compile()
&& is_xslt_element(child)
&& child.local_name() == "with-param";
if !allowed {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:apply-imports may not contain <{}> (XTSE0010)",
child.name(),
)));
}
}
let (_, with_params) = collect_sort_and_with_params(node)?;
Instr::ApplyImports { with_params }
}
"next-match" => {
let (_, with_params) = collect_sort_and_with_params(node)?;
Instr::NextMatch { with_params }
}
"call-template" => compile_call_template(node)?,
"choose" => compile_choose(node)?,
"if" => compile_if(node)?,
"for-each" => compile_for_each(node)?,
"value-of" => compile_value_of(node)?,
"copy" => compile_copy(node)?,
"copy-of" => compile_copy_of(node)?,
"element" => compile_element(node)?,
"attribute" => compile_attribute(node)?,
"text" => compile_text(node)?,
"comment" => {
reject_select_with_content(node, "xsl:comment", "XTSE0940", false)?;
Instr::Comment {
select: read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s).map_err(XsltError::from))
.transpose()?,
body: compile_body(node)?,
}
},
"processing-instruction" => {
reject_select_with_content(node, "xsl:processing-instruction", "XTSE0880", false)?;
Instr::ProcessingInstruction {
name: avt(node, require_attr(node, "name", "xsl:processing-instruction")?)?,
select: read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s).map_err(XsltError::from))
.transpose()?,
body: compile_body(node)?,
}
},
"number" => compile_number(node)?,
"variable" => Instr::Variable(compile_variable(node)?),
"message" => {
validate_xslt_only_attributes(node, "xsl:message",
&["select", "terminate"])?;
let term_attr = read_attribute(node, "terminate");
let term_avt = term_attr.map(|s| avt(node, s)).transpose()?;
if let Some(a) = &term_avt {
if a.is_literal() {
let mut lit = String::new();
for p in &a.parts {
if let crate::ast::AvtPart::Literal(s) = p { lit.push_str(s); }
}
if !matches!(lit.trim(), "yes" | "no") {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:message terminate='{lit}' must be 'yes' or 'no' \
(XTSE0020)"
)));
}
}
}
Instr::Message {
terminate: term_avt,
body: compile_body(node)?,
}
},
"fallback" => {
validate_xslt_only_attributes(node, "xsl:fallback", &[])?;
Instr::Fallback { body: compile_body(node)? }
}
"assert" => compile_assert(node)?,
"sequence" => {
validate_xslt_only_attributes(node, "xsl:sequence", &["select"])?;
match read_attribute(node, "select") {
Some(sel) => {
for child in node.children() {
if child.is_element() {
let is_fallback = is_xslt_element(child)
&& child.local_name() == "fallback";
if !is_fallback {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sequence with select= may not also contain <{}> \
(only xsl:fallback is allowed — XTSE0010)",
child.name()
)));
}
} else if is_text_like(child) && !child.content().trim().is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:sequence with select= may not also contain text content \
(XTSE0010)".into()));
}
}
Instr::Sequence { select: parse_xpath_at(node, sel).map_err(XsltError::from)? }
}
None => {
if !in_forwards_compat_mode() {
return Err(XsltError::InvalidStylesheet(
"xsl:sequence requires a select attribute; a contained \
sequence constructor is only permitted in XSLT 3.0 \
(XTSE0010)".into()));
}
Instr::If {
test: sup_xml_core::xpath::parse_xpath("true()")
.map_err(XsltError::from)?,
body: compile_body(node)?,
}
}
}
}
"map" => Instr::Map { body: compile_body(node)? },
"map-entry" => {
let key = require_attr(node, "key", "xsl:map-entry")?;
let key = parse_xpath_at(node, key).map_err(XsltError::from)?;
let select = match read_attribute(node, "select") {
Some(s) => Some(parse_xpath_at(node, &s).map_err(XsltError::from)?),
None => None,
};
let body = if select.is_some() { Body::new() } else { compile_body(node)? };
Instr::MapEntry { key, select, body }
}
"for-each-group" => compile_for_each_group(node)?,
"source-document" | "stream" => compile_source_document(node)?,
"fork" => Instr::Fork { body: compile_body(node)? },
"where-populated" => Instr::WherePopulated { body: compile_body(node)? },
"on-empty" => Instr::OnEmpty { body: select_or_body(node, "xsl:on-empty")? },
"on-non-empty" => Instr::OnNonEmpty { body: select_or_body(node, "xsl:on-non-empty")? },
"evaluate" => compile_evaluate(node)?,
"merge" => compile_merge(node)?,
"analyze-string" => compile_analyze_string(node)?,
"perform-sort" => compile_perform_sort(node)?,
"document" => Instr::Document { body: compile_body(node)? },
"namespace" => compile_namespace_instr(node)?,
"try" => compile_try(node)?,
"iterate" if in_forwards_compat_mode() => compile_iterate(node)?,
"next-iteration" if in_forwards_compat_mode() => compile_next_iteration(node)?,
"break" if in_forwards_compat_mode() => compile_break(node)?,
"result-document" => {
for a in ["standalone", "omit-xml-declaration", "indent",
"include-content-type", "undeclare-prefixes",
"escape-uri-attributes", "byte-order-mark"] {
if let Some(v) = read_attribute(node, a) {
parse_yesno_strict(v, "xsl:result-document", a)?;
}
}
if let Some(v) = read_attribute(node, "html-version") {
if v.trim().parse::<f64>().is_err() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:result-document html-version='{v}' must be numeric (XTSE0020)")));
}
}
let href = avt(node, read_attribute(node, "href").unwrap_or_default())?;
let format = read_attribute(node, "format")
.map(|s| avt(node, s))
.transpose()?;
let format_namespaces: Vec<(Option<String>, String)> = if format.is_some() {
collect_in_scope_namespaces(node)
} else {
Vec::new()
};
let output = Box::new(compile_output(node)?);
Instr::ResultDocument {
href, format, format_namespaces, output, body: compile_body(node)?,
}
}
"sort" | "with-param" | "param" | "when" | "otherwise" | "catch" => {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{name} is misplaced (must be a child of a specific parent)"
)));
}
"template" | "import" | "include" | "key" | "output"
| "strip-space" | "preserve-space" | "namespace-alias"
| "decimal-format" | "attribute-set" | "function"
| "character-map" | "import-schema" => {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{name} must appear at the top level of the stylesheet (XTSE0010)"
)));
}
other => {
if !in_forwards_compat_mode() && !ancestor_enables_forwards_compat(node) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:{other} is not a recognised XSLT instruction (XTSE0010)"
)));
}
let mut fallback = Body::new();
for child in node.children() {
if child.is_element()
&& is_xslt_element(child)
&& child.local_name() == "fallback"
{
fallback.append(compile_body(child)?);
}
}
Instr::Unsupported { name: other.to_string(), fallback }
}
};
out.push(instr, pos);
Ok(())
}
fn compile_body(node: &Node) -> Result<Body, XsltError> {
let children: Vec<&Node> = node.children().collect();
let keep_run = run_keep_mask(&children);
let mut out = Body::new();
for (i, child) in children.iter().enumerate() {
if child.is_element() {
compile_instr_into_body(child, &mut out)?;
} else if is_text_like(child) {
if keep_run[i] { compile_instr_into_body(child, &mut out)?; }
}
}
Ok(out)
}
fn run_keep_mask(children: &[&Node]) -> Vec<bool> {
let mut keep = vec![false; children.len()];
let mut i = 0;
while i < children.len() {
if !is_text_like(children[i]) { i += 1; continue; }
let start = i;
let mut last_text = i;
i += 1;
while i < children.len() {
if is_text_like(children[i]) {
last_text = i;
i += 1;
} else if matches!(children[i].kind, NodeKind::Comment | NodeKind::Pi) {
i += 1;
} else {
break;
}
}
let end = last_text + 1;
let run_significant = (start..end).any(|j| {
is_text_like(children[j])
&& !crate::whitespace::is_xslt_whitespace_only(children[j].content())
});
if run_significant {
for j in start..end {
if is_text_like(children[j]) { keep[j] = true; }
}
} else {
for j in start..end {
if is_text_like(children[j]) && is_significant_text(children[j]) {
keep[j] = true;
}
}
}
}
keep
}
fn is_text_like(n: &Node) -> bool {
matches!(n.kind, NodeKind::Text | NodeKind::CData)
}
fn has_any_child(node: &Node) -> bool {
node.children().any(|c| matches!(c.kind,
NodeKind::Element | NodeKind::Text | NodeKind::CData
| NodeKind::Pi | NodeKind::Comment
) && !(is_text_like(c) && c.content().chars().all(|ch| ch.is_whitespace())))
}
fn has_non_whitespace_child(node: &Node) -> bool {
node.children().any(|c| match c.kind {
NodeKind::Element | NodeKind::Pi | NodeKind::Comment => true,
NodeKind::Text | NodeKind::CData =>
!c.content().chars().all(|ch| ch.is_whitespace()),
_ => false,
})
}
fn compile_apply_templates(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:apply-templates",
&["select", "mode"])?;
let select = read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s)).transpose().map_err(XsltError::from)?;
let mode_attr = read_attribute(node, "mode").map(str::trim);
let (mode, mode_current) = match mode_attr {
None | Some("#default") => (None, false),
Some("#current") if is_xslt_2_0_compile() => (None, true),
Some(s) => (Some(parse_qname_on(node, s)?), false),
};
validate_xslt_only_children(node, "xsl:apply-templates", &["sort", "with-param"])?;
let (sort, with_params) = collect_sort_and_with_params(node)?;
Ok(Instr::ApplyTemplates { select, mode, sort, with_params, mode_current })
}
fn compile_call_template(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:call-template", &["name"])?;
let name = required_qname_attr(node, "name", "xsl:call-template")?;
validate_xslt_only_children(node, "xsl:call-template", &["with-param"])?;
let (_, with_params) = collect_sort_and_with_params(node)?;
Ok(Instr::CallTemplate { name, with_params })
}
fn validate_must_be_empty(node: &Node, who: &str) -> Result<(), XsltError> {
if in_forwards_compat_mode() { return Ok(()); }
let xml_space_preserve = node.attributes().any(|a|
a.local_name() == "space"
&& a.namespace.get().and_then(|n| n.prefix()) == Some("xml")
&& a.value() == "preserve");
for child in node.children() {
if child.is_element() {
return Err(XsltError::InvalidStylesheet(format!(
"{who} must be empty — found <{}> (XTSE0260)",
child.name(),
)));
}
if is_text_like(child)
&& (xml_space_preserve || !child.content().trim().is_empty())
{
return Err(XsltError::InvalidStylesheet(format!(
"{who} must be empty — found text content (XTSE0260)"
)));
}
}
Ok(())
}
fn validate_xslt_only_attributes(
node: &Node, who: &str, allowed: &[&str],
) -> Result<(), XsltError> {
if in_forwards_compat_mode() { return Ok(()); }
const GENERIC_XSLT_ATTRS: &[&str] = &[
"use-when", "default-collation", "xpath-default-namespace",
"extension-element-prefixes", "exclude-result-prefixes",
"version", "expand-text",
];
for attr in node.attributes() {
let n = attr.name();
if n.starts_with("xmlns") { continue; }
let attr_ns = attr.namespace.get().map(|ns| ns.href()).unwrap_or("");
if attr_ns == "http://www.w3.org/1999/XSL/Transform" {
return Err(XsltError::InvalidStylesheet(format!(
"{who}: attribute '{n}' in the XSLT namespace is not \
permitted on an XSLT element (XTSE0090)"
)));
}
if attr.namespace.get().is_some() || n.contains(':') { continue; }
if !allowed.iter().any(|a| *a == n)
&& !GENERIC_XSLT_ATTRS.iter().any(|a| *a == n)
{
return Err(XsltError::InvalidStylesheet(format!(
"{who}: unrecognised attribute '{n}' (XTSE0090)"
)));
}
}
Ok(())
}
fn validate_xslt_only_children(
node: &Node, who: &str, allowed: &[&str],
) -> Result<(), XsltError> {
for child in node.children() {
if !child.is_element() { continue; }
if !is_xslt_element(child) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} cannot contain literal result element \
<{}> (XTSE0010)", child.name(),
)));
}
let local = child.local_name();
if !allowed.iter().any(|a| *a == local) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} cannot contain xsl:{local} (XTSE0010 — \
allowed children: xsl:{})",
allowed.join(", xsl:"),
)));
}
}
Ok(())
}
fn collect_sort_and_with_params(node: &Node)
-> Result<(Vec<Sort>, Vec<WithParam>), XsltError>
{
let mut sort = Vec::new();
let mut params = Vec::new();
for child in node.children() {
if !child.is_element() || !is_xslt_element(child) { continue; }
if let Some(uw) = read_attribute(child, "use-when") {
if !evaluate_use_when_at(uw, Some(child))? { continue; }
}
match child.local_name() {
"sort" => {
if !sort.is_empty() && read_attribute(child, "stable").is_some() {
return Err(XsltError::InvalidStylesheet(
"stable= attribute is only allowed on the first \
xsl:sort in a sibling sequence (XTSE1017)".into()));
}
sort.push(compile_sort(child)?);
}
"with-param" => {
let p = compile_with_param(child)?;
let key = qname_key(&p.name);
if params.iter().any(|q: &WithParam| qname_key(&q.name) == key) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:with-param '{key}' (XTSE0670)"
)));
}
params.push(p);
}
_ => {}
}
}
Ok((sort, params))
}
pub(crate) fn is_valid_xml_lang(s: &str) -> bool {
if s.is_empty() { return true; }
let mut parts = s.split('-');
let Some(first) = parts.next() else { return false; };
if first.is_empty() || !first.chars().all(|c| c.is_ascii_alphabetic()) {
return false;
}
for sub in parts {
if sub.is_empty()
|| !sub.chars().all(|c| c.is_ascii_alphanumeric())
{
return false;
}
}
true
}
fn compile_sort(node: &Node) -> Result<Sort, XsltError> {
validate_xslt_only_attributes(node, "xsl:sort",
&["select", "lang", "data-type", "order", "case-order",
"collation", "stable", "as"])?;
reject_select_with_content(node, "xsl:sort", "XTSE1015", false)?;
let mut select = read_attribute(node, "select")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
if select.is_none() {
let body = compile_body(node)?;
if !body.is_empty() {
if let Some(e) = desugar_body_to_xpath(&body) {
select = Some(e);
}
}
}
if let Some(c) = read_attribute(node, "collation") {
if !in_forwards_compat_mode() && !is_recognised_collation(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort collation='{c}' is not recognised by the processor (XTDE1035)"
)));
}
}
if let Some(s) = read_attribute(node, "lang") {
if !s.contains('{') && !is_valid_xml_lang(s) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort lang='{s}' is not a valid xml:lang value (XTDE0030)"
)));
}
}
if let Some(s) = read_attribute(node, "stable") {
if !s.contains('{') {
parse_yesno_strict(s, "xsl:sort", "stable")?;
}
}
Ok(Sort {
select,
lang: read_attribute(node, "lang").map(|s| avt(node, s)).transpose()?,
data_type: read_attribute(node, "data-type").map(|s| avt(node, s)).transpose()?,
order: read_attribute(node, "order").map(|s| avt(node, s)).transpose()?,
case_order: read_attribute(node, "case-order").map(|s| avt(node, s)).transpose()?,
collation: read_attribute(node, "collation").map(|s| avt(node, s)).transpose()?,
})
}
fn compile_with_param(node: &Node) -> Result<WithParam, XsltError> {
validate_xslt_only_attributes(node, "xsl:with-param",
&["name", "select", "as", "tunnel"])?;
let name = required_qname_attr(node, "name", "xsl:with-param")?;
let (select, body) = split_select_and_body(node)?;
reject_select_with_body(node, &select, &body, "xsl:with-param")?;
let tunnel = if is_xslt_2_0_compile() {
match read_attribute(node, "tunnel") {
Some(v) => parse_yesno_strict(v, "xsl:with-param", "tunnel")?,
None => false,
}
} else { false };
let as_type = read_attribute(node, "as").map(str::to_string);
Ok(WithParam { name, select, body, tunnel, as_type })
}
fn compile_choose(node: &Node) -> Result<Instr, XsltError> {
let mut whens = Vec::new();
let mut otherwise = None;
for child in node.children() {
if matches!(child.kind, NodeKind::Text | NodeKind::CData)
&& !is_xslt_whitespace_only(child.content())
{
return Err(XsltError::InvalidStylesheet(
"xsl:choose cannot contain non-whitespace text content \
(XTSE0010 — only xsl:when / xsl:otherwise allowed)".into(),
));
}
if !child.is_element() { continue; }
if !is_xslt_element(child) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:choose cannot contain literal result element \
<{}> (XTSE0010 — only xsl:when / xsl:otherwise allowed)",
child.name(),
)));
}
match child.local_name() {
"when" => {
if otherwise.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:when must precede xsl:otherwise in \
xsl:choose (XTSE0010)".into(),
));
}
let test = require_attr(child, "test", "xsl:when")?;
whens.push((parse_xpath_at(node, test).map_err(XsltError::from)?,
compile_body(child)?));
}
"otherwise" => {
if otherwise.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:choose can have at most one xsl:otherwise \
(XTSE0010)".into(),
));
}
otherwise = Some(compile_body(child)?);
}
_ => return Err(XsltError::InvalidStylesheet(format!(
"xsl:choose children must be xsl:when or xsl:otherwise, got xsl:{}",
child.local_name(),
))),
}
}
if whens.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:choose requires at least one xsl:when".into(),
));
}
Ok(Instr::Choose { whens, otherwise })
}
fn compile_if(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:if", &["test"])?;
let test = require_attr(node, "test", "xsl:if")?;
Ok(Instr::If {
test: parse_xpath_at(node, test).map_err(XsltError::from)?,
body: compile_body(node)?,
})
}
fn compile_assert(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:assert",
&["test", "select", "error-code"])?;
let test = require_attr(node, "test", "xsl:assert")?;
let select = read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s).map_err(XsltError::from))
.transpose()?;
let error_code = read_attribute(node, "error-code")
.map(|s| avt(node, s)).transpose()?;
Ok(Instr::Assert {
test: parse_xpath_at(node, test).map_err(XsltError::from)?,
select,
body: compile_body(node)?,
error_code,
})
}
fn compile_for_each(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:for-each", &["select"])?;
let select = require_attr(node, "select", "xsl:for-each")?;
let mut seen_non_sort = false;
for child in node.children() {
if !child.is_element() { continue; }
let is_sort = is_xslt_element(child) && child.local_name() == "sort";
if is_sort {
if seen_non_sort {
return Err(XsltError::InvalidStylesheet(
"xsl:sort must precede sequence-constructor content \
inside xsl:for-each (XTSE0010)".into(),
));
}
} else {
seen_non_sort = true;
}
}
let (sort, _) = collect_sort_and_with_params(node)?;
let mut body = Body::new();
for child in node.children() {
if child.is_element() && is_xslt_element(child) && child.local_name() == "sort" {
continue;
}
if !child.is_element() && !is_significant_text(child) { continue; }
compile_instr_into_body(child, &mut body)?;
}
Ok(Instr::ForEach {
select: parse_xpath_at(node, select).map_err(XsltError::from)?,
sort,
body,
})
}
fn compile_for_each_group(node: &Node) -> Result<Instr, XsltError> {
let select = require_attr(node, "select", "xsl:for-each-group")?;
let (kind, key_raw) = match (
read_attribute(node, "group-by"),
read_attribute(node, "group-adjacent"),
read_attribute(node, "group-starting-with"),
read_attribute(node, "group-ending-with"),
) {
(Some(k), None, None, None) => (GroupingKind::By, k),
(None, Some(k), None, None) => (GroupingKind::Adjacent, k),
(None, None, Some(k), None) => (GroupingKind::StartingWith, k),
(None, None, None, Some(k)) => (GroupingKind::EndingWith, k),
(None, None, None, None) => return Err(XsltError::InvalidStylesheet(
"xsl:for-each-group requires one of group-by, group-adjacent, group-starting-with, group-ending-with".into()
)),
_ => return Err(XsltError::InvalidStylesheet(
"xsl:for-each-group accepts exactly one grouping criterion".into()
)),
};
let mut collation = read_attribute(node, "collation").map(|c| c.trim().to_string());
if let Some(c) = &collation {
if !in_forwards_compat_mode()
&& !matches!(kind, GroupingKind::By | GroupingKind::Adjacent)
{
return Err(XsltError::InvalidStylesheet(
"xsl:for-each-group collation= only applies to group-by or \
group-adjacent (XTSE1090)".into()
));
}
if !in_forwards_compat_mode() && !is_recognised_collation(c) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:for-each-group collation='{c}' is not recognised (XTSE1190)"
)));
}
}
if collation.is_none() {
if matches!(kind, GroupingKind::By | GroupingKind::Adjacent) {
collation = effective_default_collation(node);
}
}
let (sort, _) = collect_sort_and_with_params(node)?;
let mut body = Body::new();
for child in node.children() {
if child.is_element() && is_xslt_element(child) && child.local_name() == "sort" {
continue;
}
if !child.is_element() && !is_significant_text(child) { continue; }
compile_instr_into_body(child, &mut body)?;
}
Ok(Instr::ForEachGroup {
select: parse_xpath_at(node, select).map_err(XsltError::from)?,
kind,
key: parse_xpath_at(node, key_raw).map_err(XsltError::from)?,
sort,
body,
collation,
})
}
fn compile_source_document(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:source-document",
&["href", "streamable", "validation", "type", "use-accumulators"])?;
let href = avt(node, require_attr(node, "href", "xsl:source-document")?)?;
let body = compile_body(node)?;
Ok(Instr::SourceDocument { href, body })
}
fn compile_evaluate(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:evaluate", &[
"xpath", "context-item", "as", "base-uri", "namespace-context",
"schema-aware", "with-params",
])?;
let xpath = parse_xpath_at(node, require_attr(node, "xpath", "xsl:evaluate")?)
.map_err(XsltError::from)?;
let context_item = read_attribute(node, "context-item")
.map(|s| parse_xpath_at(node, s)).transpose().map_err(XsltError::from)?;
let mut with_params = Vec::new();
for child in node.children() {
if child.is_element() && is_xslt_element(child)
&& child.local_name() == "with-param"
{
with_params.push(compile_with_param(child)?);
}
}
let schema_aware = read_attribute(node, "schema-aware")
.map(|s| s.trim() == "yes" || s.trim() == "1" || s.trim() == "true")
.unwrap_or(false);
Ok(Instr::Evaluate { xpath, context_item, with_params, schema_aware })
}
fn compile_merge(node: &Node) -> Result<Instr, XsltError> {
let mut sources = Vec::new();
let mut action: Option<Body> = None;
for child in node.children() {
if !child.is_element() {
if is_significant_text(child) {
return Err(XsltError::InvalidStylesheet(
"xsl:merge cannot contain text content (XTSE0010)".into()));
}
continue;
}
if !is_xslt_element(child) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:merge cannot contain literal result element <{}> (XTSE0010)",
child.name())));
}
match child.local_name() {
"merge-source" => sources.push(compile_merge_source(child)?),
"merge-action" => {
let mut body = Body::new();
for gc in child.children() {
if !gc.is_element() && !is_significant_text(gc) { continue; }
compile_instr_into_body(gc, &mut body)?;
}
action = Some(body);
}
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:merge cannot contain xsl:{other} (XTSE0010)"))),
}
}
if sources.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:merge requires at least one xsl:merge-source (XTSE0010)".into()));
}
let action = action.ok_or_else(|| XsltError::InvalidStylesheet(
"xsl:merge requires an xsl:merge-action (XTSE0010)".into()))?;
Ok(Instr::Merge { sources, action })
}
fn compile_merge_source(node: &Node) -> Result<MergeSource, XsltError> {
let name = read_attribute(node, "name").map(|s| s.to_string());
let select = parse_xpath_at(node, require_attr(node, "select", "xsl:merge-source")?)
.map_err(XsltError::from)?;
let for_each_source = read_attribute(node, "for-each-source")
.or_else(|| read_attribute(node, "for-each-item"))
.map(|s| parse_xpath_at(node, s)).transpose().map_err(XsltError::from)?;
let mut keys = Vec::new();
for child in node.children() {
if child.is_element() && is_xslt_element(child) && child.local_name() == "merge-key" {
keys.push(compile_sort(child)?);
}
}
if keys.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:merge-source requires at least one xsl:merge-key (XTSE0010)".into()));
}
Ok(MergeSource { name, select, for_each_source, keys })
}
fn compile_analyze_string(node: &Node) -> Result<Instr, XsltError> {
let select = require_attr(node, "select", "xsl:analyze-string")?;
let regex = avt(node, require_attr(node, "regex", "xsl:analyze-string")?)?;
let flags = read_attribute(node, "flags").map(|s| avt(node, s)).transpose()?
.unwrap_or_default();
let mut matching: Option<Body> = None;
let mut non_matching: Option<Body> = None;
for child in node.children() {
if !child.is_element() { continue; }
if !is_xslt_element(child) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:analyze-string cannot contain literal result element \
<{}> (XTSE0010)", child.name(),
)));
}
match child.local_name() {
"matching-substring" => {
if matching.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:analyze-string can have at most one \
xsl:matching-substring (XTSE0010)".into()));
}
matching = Some(compile_body(child)?);
}
"non-matching-substring" => {
if non_matching.is_some() {
return Err(XsltError::InvalidStylesheet(
"xsl:analyze-string can have at most one \
xsl:non-matching-substring (XTSE0010)".into()));
}
non_matching = Some(compile_body(child)?);
}
"fallback" => {}
other => return Err(XsltError::InvalidStylesheet(format!(
"unexpected child of xsl:analyze-string: xsl:{other}"
))),
}
}
if matching.is_none() && non_matching.is_none() {
return Err(XsltError::InvalidStylesheet(
"xsl:analyze-string requires at least one of \
xsl:matching-substring or xsl:non-matching-substring \
(XTSE1130)".into()));
}
Ok(Instr::AnalyzeString {
select: parse_xpath_at(node, select).map_err(XsltError::from)?,
regex, flags,
matching: matching.unwrap_or_default(),
non_matching: non_matching.unwrap_or_default(),
})
}
fn compile_perform_sort(node: &Node) -> Result<Instr, XsltError> {
let select = read_attribute(node, "select")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
require_xsl_sort_first(node, "xsl:perform-sort")?;
if select.is_some() {
for child in node.children() {
if child.is_element() {
let xslt_local = is_xslt_element(child).then(|| child.local_name());
match xslt_local.as_deref() {
Some("sort") | Some("fallback") => {}
_ => return Err(XsltError::InvalidStylesheet(format!(
"xsl:perform-sort with select= may only contain \
xsl:sort and xsl:fallback (XTSE1040)"
))),
}
} else if is_text_like(child) && !child.content().trim().is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:perform-sort with select= may only contain \
xsl:sort and xsl:fallback (XTSE1040)".into()
));
}
}
}
let (sort, _) = collect_sort_and_with_params(node)?;
let body = if select.is_some() {
Body::new()
} else {
compile_body_skipping_xsl_sort(node)?
};
Ok(Instr::PerformSort { select, sort, body })
}
fn require_xsl_sort_first(parent: &Node, who: &str) -> Result<(), XsltError> {
let mut seen_other = false;
for child in parent.children() {
if child.is_element() {
let xslt_local = is_xslt_element(child).then(|| child.local_name());
match xslt_local.as_deref() {
Some("sort") => {
if seen_other {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:sort in {who} must precede any other \
content (XTSE0010)"
)));
}
}
Some("with-param") => {}
_ => seen_other = true,
}
} else if is_text_like(child) && !child.content().trim().is_empty() {
seen_other = true;
}
}
Ok(())
}
fn compile_body_skipping_xsl_sort(node: &Node) -> Result<Body, XsltError> {
let children: Vec<&Node> = node.children().collect();
let keep_run = run_keep_mask(&children);
let mut out = Body::new();
for (i, child) in children.iter().enumerate() {
if child.is_element() {
if is_xslt_element(child) && child.local_name() == "sort" {
continue;
}
compile_instr_into_body(child, &mut out)?;
} else if is_text_like(child) && keep_run[i] {
compile_instr_into_body(child, &mut out)?;
}
}
Ok(out)
}
fn compile_namespace_instr(node: &Node) -> Result<Instr, XsltError> {
reject_select_with_content(node, "xsl:namespace", "XTSE0910", true)?;
let name = avt(node, require_attr(node, "name", "xsl:namespace")?)?;
let select = read_attribute(node, "select")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
let body = if select.is_none() { compile_body(node)? } else { Body::new() };
Ok(Instr::Namespace { name, select, body })
}
fn compile_try(node: &Node) -> Result<Instr, XsltError> {
let mut body = Body::new();
let mut catches = Vec::new();
let mut seen_catch = false;
for child in node.children() {
if !child.is_element() {
continue;
}
if is_xslt_element(child) && child.local_name() == "fallback" {
continue;
}
let is_catch = is_xslt_element(child) && child.local_name() == "catch";
if is_catch {
seen_catch = true;
catches.push(compile_catch(child)?);
continue;
}
if seen_catch {
return Err(XsltError::InvalidStylesheet(
"xsl:try body must precede all xsl:catch handlers (XTSE0010)".into(),
));
}
compile_instr_into_body(child, &mut body)?;
}
if catches.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:try requires at least one xsl:catch handler (XTSE0010)".into(),
));
}
if let Some(sel) = read_attribute(node, "select") {
if !body.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:try select= and body are mutually exclusive (XTSE0010)".into(),
));
}
let expr = parse_xpath_at(node, sel).map_err(XsltError::from)?;
body = body_of_one(node, Instr::Sequence { select: expr });
}
Ok(Instr::Try { body, catches })
}
fn compile_iterate(node: &Node) -> Result<Instr, XsltError> {
let select = require_attr(node, "select", "xsl:iterate")?;
let select = parse_xpath_at(node, select).map_err(XsltError::from)?;
let mut params = Vec::new();
let mut on_completion = Body::new();
let mut body = Body::new();
let children: Vec<&Node> = node.children().collect();
let keep_run = run_keep_mask(&children);
for (i, child) in children.iter().enumerate() {
if child.is_element() {
if is_xslt_element(child) {
match child.local_name() {
"param" => { params.push(compile_param(child)?); continue; }
"on-completion" => { on_completion = compile_body(child)?; continue; }
_ => {}
}
}
compile_instr_into_body(child, &mut body)?;
} else if is_text_like(child) && keep_run[i] {
compile_instr_into_body(child, &mut body)?;
}
}
Ok(Instr::Iterate { select, params, on_completion, body })
}
fn compile_next_iteration(node: &Node) -> Result<Instr, XsltError> {
let (_, with_params) = collect_sort_and_with_params(node)?;
Ok(Instr::NextIteration { with_params })
}
fn compile_break(node: &Node) -> Result<Instr, XsltError> {
let select = read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s)).transpose().map_err(XsltError::from)?;
let body = compile_body(node)?;
if select.is_some() && !body.is_empty() {
return Err(XsltError::InvalidStylesheet(
"xsl:break: select= and a non-empty body are mutually exclusive (XTSE0010)".into(),
));
}
Ok(Instr::Break { select, body })
}
fn compile_catch(node: &Node) -> Result<crate::ast::TryCatch, XsltError> {
use crate::ast::CatchMatcher;
let errors_attr = read_attribute(node, "errors").unwrap_or("*").trim().to_string();
let mut errors = Vec::new();
for tok in errors_attr.split_ascii_whitespace() {
let m = if tok == "*" {
CatchMatcher::Any
} else if let Some(rest) = tok.strip_prefix("*:") {
CatchMatcher::LocalNameOnly(rest.to_string())
} else if let Some(prefix) = tok.strip_suffix(":*") {
CatchMatcher::PrefixWildcard(prefix.to_string())
} else {
CatchMatcher::QName(parse_qname_on(node, tok)?)
};
errors.push(m);
}
let body = if let Some(sel) = read_attribute(node, "select") {
if node.children().any(|c| c.is_element()) {
return Err(XsltError::InvalidStylesheet(
"xsl:catch select= and body are mutually exclusive (XTSE0010)".into(),
));
}
body_of_one(node, Instr::Sequence {
select: parse_xpath_at(node, sel).map_err(XsltError::from)?,
})
} else {
compile_body(node)?
};
Ok(crate::ast::TryCatch { errors, body })
}
fn compile_value_of(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:value-of",
&["select", "disable-output-escaping", "separator"])?;
let select_attr = read_attribute(node, "select");
let explicit_sep = read_attribute(node, "separator").map(|s| avt(node, s)).transpose()?;
let bc = ancestor_forces_backwards_compat(node);
let separator_for_select = if explicit_sep.is_some() {
explicit_sep.clone()
} else if is_xslt_2_0_compile() && !bc {
Some(Avt::literal(" "))
} else {
None
};
let separator_for_body = if explicit_sep.is_some() {
explicit_sep.clone()
} else if is_xslt_2_0_compile() && !bc {
Some(Avt::literal(""))
} else {
None
};
let select = match select_attr {
Some(s) => {
if is_xslt_2_0_compile() && has_non_whitespace_child(node) {
return Err(XsltError::InvalidStylesheet(
"xsl:value-of with select= must have an empty \
sequence-constructor body (XTSE0870)".into()
));
}
parse_xpath_at(node, s).map_err(XsltError::from)?
}
None if is_xslt_2_0_compile() => {
if !has_any_child(node) {
return Err(XsltError::InvalidStylesheet(
"xsl:value-of must specify select= or a non-empty \
sequence constructor (XTSE0870)".into()
));
}
return Ok(Instr::ValueOfBody {
body: compile_body(node)?,
dose: read_attribute(node, "disable-output-escaping")
.map(parse_yesno).unwrap_or(false),
separator: separator_for_body,
});
}
None => return Err(XsltError::InvalidStylesheet(
"xsl:value-of requires select= attribute".into())),
};
let separator = separator_for_select;
Ok(Instr::ValueOf {
select,
dose: read_attribute(node, "disable-output-escaping")
.map(parse_yesno).unwrap_or(false),
separator,
})
}
fn compile_copy(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:copy",
&["use-attribute-sets", "copy-namespaces",
"inherit-namespaces", "type", "validation"])?;
let copy_namespaces = match read_attribute(node, "copy-namespaces") {
Some(v) => parse_bool_attr(&v, "xsl:copy", "copy-namespaces")?,
None => true,
};
Ok(Instr::Copy {
use_attribute_sets: parse_qname_list(
node, read_attribute(node, "use-attribute-sets").unwrap_or(""),
)?,
body: compile_body(node)?,
copy_namespaces,
})
}
fn compile_copy_of(node: &Node) -> Result<Instr, XsltError> {
let two_oh = is_xslt_2_0_compile();
for attr in node.attributes() {
let n = attr.name();
if n.starts_with("xmlns") { continue; }
let local = n.rsplit_once(':').map(|(_, l)| l).unwrap_or(n);
let in_xsl_ns = attr.namespace.get()
.map(|ns| ns.href() == "http://www.w3.org/1999/XSL/Transform")
.unwrap_or(false);
if !in_xsl_ns && (attr.namespace.get().is_some() || n.contains(':')) { continue; }
let is_2_0_attr = two_oh && matches!(local,
"copy-namespaces" | "validation" | "type");
if local != "select" && !is_2_0_attr {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:copy-of: unrecognised attribute '{n}' (XTSE0090)"
)));
}
}
let select = require_attr(node, "select", "xsl:copy-of")?;
let copy_namespaces = match read_attribute(node, "copy-namespaces") {
Some(v) => parse_bool_attr(&v, "xsl:copy-of", "copy-namespaces")?,
None => true,
};
for child in node.children() {
match child.kind {
NodeKind::Element => {
if !(is_xslt_element(child) && child.local_name() == "fallback") {
return Err(XsltError::InvalidStylesheet(
"xsl:copy-of must have an empty content model \
(XTSE0260)".into()));
}
}
NodeKind::Text | NodeKind::CData => {
if !is_xslt_whitespace_only(child.content()) {
return Err(XsltError::InvalidStylesheet(
"xsl:copy-of must have an empty content model \
(XTSE0260)".into()));
}
}
_ => {}
}
}
Ok(Instr::CopyOf {
select: parse_xpath_at(node, select).map_err(XsltError::from)?,
copy_namespaces,
})
}
fn compile_element(node: &Node) -> Result<Instr, XsltError> {
validate_xslt_only_attributes(node, "xsl:element",
&["name", "namespace", "inherit-namespaces",
"use-attribute-sets", "type", "validation"])?;
Ok(Instr::Element {
name: avt(node, require_attr(node, "name", "xsl:element")?)?,
namespace: read_attribute(node, "namespace").map(|s| avt(node, s)).transpose()?,
use_attribute_sets: parse_qname_list(
node, read_attribute(node, "use-attribute-sets").unwrap_or(""),
)?,
body: compile_body(node)?,
in_scope_namespaces: collect_in_scope_namespaces(node),
})
}
fn has_constructor_content(node: &Node, allow_fallback: bool) -> bool {
node.children().any(|c| {
if c.is_element() {
!(allow_fallback && is_xslt_element(c) && c.local_name() == "fallback")
} else {
is_significant_text(c)
}
})
}
fn reject_select_with_content(
node: &Node, who: &str, code: &str, allow_fallback: bool,
) -> Result<(), XsltError> {
if read_attribute(node, "select").is_some() && has_constructor_content(node, allow_fallback) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} has a select attribute and non-empty content ({code})"
)));
}
Ok(())
}
fn compile_attribute(node: &Node) -> Result<Instr, XsltError> {
reject_select_with_content(node, "xsl:attribute", "XTSE0840", false)?;
let select = read_attribute(node, "select")
.map(|s| parse_xpath_at(node, s).map_err(XsltError::from))
.transpose()?;
Ok(Instr::Attribute {
name: avt(node, require_attr(node, "name", "xsl:attribute")?)?,
namespace: read_attribute(node, "namespace").map(|s| avt(node, s)).transpose()?,
select,
separator: read_attribute(node, "separator").map(|s| avt(node, s)).transpose()?,
body: compile_body(node)?,
in_scope_namespaces: collect_in_scope_namespaces(node),
schema_type: read_attribute(node, "type")
.and_then(|v| resolve_type_qname(node, v)),
})
}
fn collect_in_scope_namespaces(node: &Node) -> Vec<(Option<String>, String)> {
let mut seen: std::collections::HashSet<Option<String>>
= std::collections::HashSet::new();
let mut out: Vec<(Option<String>, String)> = Vec::new();
let mut cur = Some(node);
while let Some(n) = cur {
for (prefix, uri) in n.ns_declarations() {
let key: Option<String> = prefix.map(str::to_owned);
if seen.contains(&key) { continue; }
seen.insert(key.clone());
out.push((key, uri.to_owned()));
}
cur = n.parent.get();
}
out
}
fn compile_text(node: &Node) -> Result<Instr, XsltError> {
let mut text = String::new();
for child in node.children() {
match child.kind {
NodeKind::Text | NodeKind::CData => text.push_str(child.content()),
NodeKind::Element => {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:text content must be character data; found <{}> (XTSE0010)",
child.name(),
)));
}
_ => {}
}
}
let dose = read_attribute(node, "disable-output-escaping")
.map(parse_yesno).unwrap_or(false);
let expand = read_attribute(node, "expand-text")
.map(|v| matches!(v.trim(), "yes" | "true" | "1"))
.unwrap_or_else(|| expand_text_in_scope(node));
if expand && (text.contains('{') || text.contains('}')) {
let mut body = Body::new();
body.set_file(current_module_file());
let pos = node_src_pos(node);
for part in avt(node, &text)?.parts {
match part {
AvtPart::Literal(s) => if !s.is_empty() {
body.push(Instr::LiteralText { text: s, dose }, pos);
},
AvtPart::Expr(e) => body.push(Instr::ValueOf {
select: e, dose, separator: Some(Avt::literal(" ")),
}, pos),
}
}
return Ok(Instr::If {
test: sup_xml_core::xpath::parse_xpath("true()").map_err(XsltError::from)?,
body,
});
}
Ok(Instr::LiteralText { text, dose })
}
fn compile_number(node: &Node) -> Result<Instr, XsltError> {
let value = read_attribute(node, "value")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
if value.is_some() && !in_forwards_compat_mode() {
for attr in &["select", "level", "count", "from"] {
if read_attribute(node, attr).is_some() {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:number value='{}' cannot combine with {attr}= (XTSE0975)",
read_attribute(node, "value").unwrap_or(""),
)));
}
}
}
let level = match read_attribute(node, "level").unwrap_or("single") {
"single" => crate::ast::NumberLevel::Single,
"any" => crate::ast::NumberLevel::Any,
"multiple" => crate::ast::NumberLevel::Multiple,
other => return Err(XsltError::InvalidStylesheet(format!(
"xsl:number level= must be single|any|multiple (got {other:?})"
))),
};
let count = read_attribute(node, "count")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
let from = read_attribute(node, "from")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
let select = read_attribute(node, "select")
.map(|src| parse_xpath_at(node, src)).transpose().map_err(XsltError::from)?;
for (slot, attr) in [(&count, "count"), (&from, "from")] {
if let Some(e) = slot { reject_pattern_grouping_calls(e, attr)?; }
}
for (slot, attr) in [(&count, "xsl:number count="), (&from, "xsl:number from=")] {
if let Some(e) = slot { ensure_pattern_shape(e, attr)?; }
}
let format = avt(node, read_attribute(node, "format").unwrap_or("1"))?;
let grouping_separator = read_attribute(node, "grouping-separator")
.map(|s| avt(node, s)).transpose()?;
let grouping_size = read_attribute(node, "grouping-size")
.map(|s| avt(node, s)).transpose()?;
let ordinal = read_attribute(node, "ordinal").map(|s| avt(node, s)).transpose()?;
let lang = read_attribute(node, "lang").map(|s| avt(node, s)).transpose()?;
if let Some(s) = read_attribute(node, "lang") {
if !s.contains('{') && !is_valid_xml_lang(s) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:number lang='{s}' is not a valid xml:lang value (XTSE0020)"
)));
}
}
let letter_value = read_attribute(node, "letter-value").map(|s| avt(node, s)).transpose()?;
let start_at = read_attribute(node, "start-at").map(|s| avt(node, s)).transpose()?;
Ok(Instr::Number {
value, select, level, count, from, format,
grouping_separator, grouping_size,
ordinal, lang, letter_value, start_at,
})
}
fn compile_literal_element(node: &Node) -> Result<Instr, XsltError> {
let name = node_qname(node)?;
let ext_uris = collect_extension_element_uris(node);
if !ext_uris.is_empty() && ext_uris.contains(&name.uri) {
let mut fallback = Body::new();
for child in node.children() {
if child.is_element()
&& is_xslt_element(child)
&& child.local_name() == "fallback"
{
fallback.append(compile_body(child)?);
}
}
return Ok(Instr::Unsupported {
name: format!("{}{}", name.uri, name.local),
fallback,
});
}
let mut attributes = Vec::new();
let mut use_attribute_sets: Vec<QName> = Vec::new();
let mut schema_type: Option<(String, String)> = None;
for attr in node.attributes() {
let aname_lex = attr.name();
if aname_lex == "xmlns" || aname_lex.starts_with("xmlns:") { continue; }
let aname = attr_qname(node, attr)?;
if aname.uri == "http://www.w3.org/1999/XSL/Transform" {
const LRE_ALLOWED_XSL_ATTRS: &[&str] = &[
"version", "use-attribute-sets", "default-collation",
"exclude-result-prefixes", "extension-element-prefixes",
"xpath-default-namespace", "expand-text",
"type", "use-when", "validation", "inherit-namespaces",
];
if !LRE_ALLOWED_XSL_ATTRS.contains(&aname.local.as_str())
&& !in_forwards_compat_mode()
{
return Err(XsltError::InvalidStylesheet(format!(
"literal result element <{}>: attribute 'xsl:{}' is not \
permitted (XTSE0085)", node.name(), aname.local,
)));
}
if aname.local == "use-attribute-sets" {
use_attribute_sets = parse_qname_list(node, attr.value())?;
}
if aname.local == "type" {
schema_type = resolve_type_qname(node, attr.value());
}
continue;
}
attributes.push((aname, avt(node, attr.value())?));
}
let mut local_by_prefix: std::collections::HashMap<Option<String>, String>
= std::collections::HashMap::new();
let mut inherited_by_prefix: std::collections::HashMap<Option<String>, String>
= std::collections::HashMap::new();
let mut cur = Some(node);
let mut is_lre_itself = true;
while let Some(n) = cur {
for (prefix, uri) in n.ns_declarations() {
if is_lre_itself && prefix.is_none() && uri.is_empty() {
continue;
}
let prefix = prefix.map(str::to_owned);
if is_lre_itself {
local_by_prefix.entry(prefix).or_insert_with(|| uri.to_owned());
} else {
inherited_by_prefix.entry(prefix).or_insert_with(|| uri.to_owned());
}
}
is_lre_itself = false;
cur = n.parent.get();
}
let exclude_uris = compile_lre_exclusion_uris(node);
let exclude_prefixes = compile_lre_exclusion_set(node);
for p in &exclude_prefixes {
if p == "#all" || p == "#default" { continue; }
let resolved = local_by_prefix.get(&Some(p.clone()))
.or_else(|| inherited_by_prefix.get(&Some(p.clone())));
if resolved.is_none() {
return Err(XsltError::InvalidStylesheet(format!(
"exclude-result-prefixes references undeclared prefix '{p}' (XTSE0808)"
)));
}
}
if exclude_prefixes.contains("#default")
&& local_by_prefix.get(&None).is_none()
&& inherited_by_prefix.get(&None).is_none()
{
return Err(XsltError::InvalidStylesheet(
"exclude-result-prefixes='#default' but no default namespace \
is in scope (XTSE0809)".into()));
}
let mut by_prefix: std::collections::HashMap<Option<String>, String> = local_by_prefix;
for (prefix, uri) in inherited_by_prefix {
by_prefix.entry(prefix).or_insert(uri);
}
let mut namespaces: Vec<(Option<String>, String)> = by_prefix.into_iter()
.filter(|(_p, uri)| {
if uri == "http://www.w3.org/1999/XSL/Transform" { return false; }
!exclude_uris.contains(uri.as_str())
})
.collect();
namespaces.sort_by(|a, b| a.0.cmp(&b.0));
Ok(Instr::LiteralElement {
name,
attributes,
namespaces,
use_attribute_sets,
schema_type,
body: compile_body(node)?,
})
}
fn serialize_inline_schema(schema_node: &Node) -> String {
use sup_xml_core::serializer::{serialize_node_to_string, SerializeOptions};
let body = serialize_node_to_string(schema_node, &SerializeOptions::default());
let mut seen: std::collections::HashSet<Option<String>> =
schema_node.ns_declarations().map(|(p, _)| p.map(str::to_owned)).collect();
let mut decls = String::new();
let mut cur = schema_node.parent.get();
while let Some(n) = cur {
for (prefix, uri) in n.ns_declarations() {
let key = prefix.map(str::to_owned);
if !seen.insert(key) || uri.is_empty() { continue; }
match prefix {
Some(p) => decls.push_str(&format!(" xmlns:{p}=\"{uri}\"")),
None => decls.push_str(&format!(" xmlns=\"{uri}\"")),
}
}
cur = n.parent.get();
}
if decls.is_empty() { return body; }
match body.find([' ', '>', '/']) {
Some(pos) => format!("{}{}{}", &body[..pos], decls, &body[pos..]),
None => body,
}
}
fn resolve_type_qname(node: &Node, value: &str) -> Option<(String, String)> {
let value = value.trim();
let (prefix, local) = match value.split_once(':') {
Some((p, l)) => (Some(p), l),
None => (None, value),
};
if prefix == Some("xml") {
return Some(("http://www.w3.org/XML/1998/namespace".into(), local.into()));
}
let mut cur = Some(node);
while let Some(n) = cur {
for (p, uri) in n.ns_declarations() {
match (prefix, p) {
(Some(want), Some(got)) if want == got =>
return Some((uri.to_string(), local.to_string())),
(None, None) =>
return Some((uri.to_string(), local.to_string())),
_ => {}
}
}
cur = n.parent.get();
}
prefix.is_none().then(|| (String::new(), local.to_string()))
}
fn compile_lre_exclusion_set(node: &Node) -> std::collections::HashSet<String> {
let mut out: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut cur = Some(node);
while let Some(n) = cur {
for attr in n.attributes() {
let name = attr.name();
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let in_xsl_ns = attr.namespace.get()
.map(|ns| ns.href() == "http://www.w3.org/1999/XSL/Transform")
.unwrap_or(false);
let bare = attr.namespace.get().is_none() && !name.contains(':');
let is_exclude = local == "exclude-result-prefixes"
&& (bare || in_xsl_ns || name == "xsl:exclude-result-prefixes");
let is_extension = local == "extension-element-prefixes"
&& (bare || in_xsl_ns || name == "xsl:extension-element-prefixes");
if is_exclude || is_extension {
for tok in attr.value().split_whitespace() {
out.insert(tok.to_owned());
}
}
}
cur = n.parent.get();
}
out
}
fn compile_lre_exclusion_uris(node: &Node) -> std::collections::HashSet<String> {
let mut uris: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut cur = Some(node);
while let Some(n) = cur {
let mut tokens: Vec<String> = Vec::new();
for attr in n.attributes() {
let name = attr.name();
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let in_xsl_ns = attr.namespace.get()
.map(|ns| ns.href() == "http://www.w3.org/1999/XSL/Transform")
.unwrap_or(false);
let bare = attr.namespace.get().is_none() && !name.contains(':');
let is_exclude = local == "exclude-result-prefixes"
&& (bare || in_xsl_ns || name == "xsl:exclude-result-prefixes");
let is_extension = local == "extension-element-prefixes"
&& (bare || in_xsl_ns || name == "xsl:extension-element-prefixes");
if is_exclude || is_extension {
for tok in attr.value().split_whitespace() {
tokens.push(tok.to_owned());
}
}
}
if !tokens.is_empty() {
let mut ancestor_scope: std::collections::HashMap<Option<String>, String>
= std::collections::HashMap::new();
let mut walk = Some(n);
while let Some(w) = walk {
for (prefix, uri) in w.ns_declarations() {
let p = prefix.map(str::to_owned);
ancestor_scope.entry(p).or_insert_with(|| uri.to_owned());
}
walk = w.parent.get();
}
for tok in tokens {
match tok.as_str() {
"#all" => {
for (_p, uri) in &ancestor_scope {
if uri != "http://www.w3.org/1999/XSL/Transform" {
uris.insert(uri.clone());
}
}
}
"#default" => {
if let Some(uri) = ancestor_scope.get(&None) {
uris.insert(uri.clone());
}
}
name => {
if let Some(uri) = ancestor_scope.get(&Some(name.to_owned())) {
uris.insert(uri.clone());
}
}
}
}
}
cur = n.parent.get();
}
uris
}
pub fn validate_global_variable_uniqueness(ast: &StylesheetAst) -> Result<(), XsltError> {
use std::collections::HashMap;
let mut seen: HashMap<(String, i32), usize> = HashMap::new();
let mut max_prec: HashMap<String, i32> = HashMap::new();
let entries = ast.global_variables.iter().map(|v| (&v.name, TOP_LEVEL_IMPORT_PRECEDENCE))
.chain(ast.global_params.iter().map(|p| (&p.name, TOP_LEVEL_IMPORT_PRECEDENCE)));
for (n, prec) in entries {
let key = qname_key(n);
*seen.entry((key.clone(), prec)).or_insert(0) += 1;
let entry = max_prec.entry(key).or_insert(prec);
if prec > *entry { *entry = prec; }
}
for ((name, prec), count) in &seen {
if *count < 2 { continue; }
if max_prec.get(name).copied().unwrap_or(*prec) > *prec { continue; }
return Err(XsltError::InvalidStylesheet(format!(
"duplicate global xsl:variable / xsl:param '{name}' at the same \
import precedence ({prec}) (XTSE0630)"
)));
}
Ok(())
}
pub fn validate_output_declarations(ast: &StylesheetAst) -> Result<(), XsltError> {
use crate::ast::OutputSpec;
let declared: std::collections::HashSet<String> =
ast.character_maps.iter().map(|m| qname_key(&m.name)).collect();
for o in &ast.outputs {
for q in &o.use_character_maps {
let k = qname_key(q);
if !declared.contains(&k) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:output use-character-maps references undeclared map '{k}' \
(XTSE1590)"
)));
}
}
}
for m in &ast.character_maps {
for q in &m.use_character_maps {
let k = qname_key(q);
if !declared.contains(&k) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:character-map use-character-maps references undeclared map \
'{k}' (XTSE1590)"
)));
}
}
}
{
use std::collections::HashSet as HS;
let mut seen: HS<String> = HS::new();
for m in &ast.character_maps {
let k = qname_key(&m.name);
if !seen.insert(k.clone()) {
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:character-map name '{k}' at the same \
import precedence (XTSE1580)"
)));
}
}
}
use std::collections::{HashMap as HM, HashSet as HS};
let by_name: HM<String, &Vec<QName>> = ast.character_maps.iter()
.map(|m| (qname_key(&m.name), &m.use_character_maps))
.collect();
fn visit(
name: &str, on_stack: &mut HS<String>, done: &mut HS<String>,
by_name: &HM<String, &Vec<QName>>,
) -> Result<(), XsltError> {
if done.contains(name) { return Ok(()); }
if !on_stack.insert(name.to_string()) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:character-map '{name}' references itself \
transitively via use-character-maps (XTSE1600)"
)));
}
if let Some(used) = by_name.get(name) {
for q in *used {
visit(&qname_key(q), on_stack, done, by_name)?;
}
}
on_stack.remove(name);
done.insert(name.to_string());
Ok(())
}
let mut done = HS::new();
for name in by_name.keys() {
let mut on_stack = HS::new();
visit(name, &mut on_stack, &mut done, &by_name)?;
}
let outs: Vec<&OutputSpec> = ast.outputs.iter().filter(|o| o.name.is_none()).collect();
if outs.len() < 2 { return Ok(()); }
macro_rules! check {
($field:ident, $label:expr) => {
let mut prev: Option<&_> = None;
for o in &outs {
if let Some(v) = &o.$field {
if let Some(p) = prev {
if p != v {
return Err(XsltError::InvalidStylesheet(format!(
"conflicting xsl:output {}='{:?}' vs '{:?}' \
(XTSE1560)", $label, p, v,
)));
}
} else {
prev = Some(v);
}
}
}
};
}
macro_rules! check_bool {
($field:ident, $label:expr) => {
let mut prev: Option<bool> = None;
for o in &outs {
if let Some(v) = o.$field {
if let Some(p) = prev {
if p != v {
return Err(XsltError::InvalidStylesheet(format!(
"conflicting xsl:output {}='{}' vs '{}' \
(XTSE1560)", $label, p, v,
)));
}
} else {
prev = Some(v);
}
}
}
};
}
check!(method, "method");
check!(encoding, "encoding");
check_bool!(indent, "indent");
check_bool!(omit_xml_declaration, "omit-xml-declaration");
check!(standalone, "standalone");
check!(media_type, "media-type");
check!(doctype_public, "doctype-public");
check!(doctype_system, "doctype-system");
check!(version, "version");
Ok(())
}
pub fn validate_call_template_with_params(ast: &StylesheetAst) -> Result<(), XsltError> {
use crate::ast::Instr;
use std::collections::HashMap;
if !version_enables_2_0(&ast.version) {
return Ok(());
}
let mut by_name: HashMap<String, &crate::ast::Template> = HashMap::new();
for t in &ast.templates {
if let Some(n) = &t.name {
by_name.insert(qname_key(n), t);
}
}
fn walk_body<'a>(
body: &'a [Instr],
by_name: &'a HashMap<String, &'a crate::ast::Template>,
) -> Result<(), XsltError> {
for instr in body {
match instr {
Instr::CallTemplate { name, with_params } => {
let key = qname_key(name);
let Some(t) = by_name.get(&key) else { continue; };
for wp in with_params {
if wp.tunnel { continue; }
let wp_key = qname_key(&wp.name);
let matched = t.params.iter().any(|p|
qname_key(&p.name) == wp_key && !p.tunnel);
if !matched {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:call-template name='{key}' has \
xsl:with-param '{wp_key}' that doesn't \
match any non-tunnel xsl:param of the \
called template (XTSE0680)"
)));
}
}
}
Instr::If { body, .. } => walk_body(body, by_name)?,
Instr::ForEach { body, .. } => walk_body(body, by_name)?,
Instr::ForEachGroup { body, .. } => walk_body(body, by_name)?,
Instr::Merge { action, .. } => walk_body(action, by_name)?,
Instr::Choose { whens, otherwise } => {
for (_, b) in whens { walk_body(b, by_name)?; }
if let Some(b) = otherwise { walk_body(b, by_name)?; }
}
Instr::LiteralElement { body, .. }
| Instr::Element { body, .. }
| Instr::Attribute { body, .. }
| Instr::Copy { body, .. }
| Instr::Document { body, .. }
| Instr::ProcessingInstruction { body, .. }
| Instr::Comment { body, .. }
| Instr::Namespace { body, .. }
| Instr::ValueOfBody { body, .. } =>
walk_body(body, by_name)?,
Instr::Try { body, catches } => {
walk_body(body, by_name)?;
for c in catches { walk_body(&c.body, by_name)?; }
}
Instr::PerformSort { body, .. } => walk_body(body, by_name)?,
Instr::AnalyzeString { matching, non_matching, .. } => {
walk_body(matching, by_name)?;
walk_body(non_matching, by_name)?;
}
_ => {}
}
}
Ok(())
}
for t in &ast.templates {
walk_body(&t.body, &by_name)?;
}
for f in &ast.functions {
walk_body(&f.body, &by_name)?;
}
Ok(())
}
pub fn validate_iterate_constraints(ast: &StylesheetAst) -> Result<(), XsltError> {
for t in &ast.templates { check_iterate(&t.body, None)?; }
for v in &ast.global_variables { check_iterate(&v.body, None)?; }
for p in &ast.global_params { check_iterate(&p.body, None)?; }
for f in &ast.functions { check_iterate(&f.body, None)?; }
for s in &ast.attribute_sets { check_iterate(&s.attributes, None)?; }
Ok(())
}
fn check_iterate(
body: &[Instr],
params: Option<&std::collections::HashSet<String>>,
) -> Result<(), XsltError> {
use crate::ast::Instr::*;
let last = body.len().saturating_sub(1);
for (i, instr) in body.iter().enumerate() {
match instr {
Break { .. } | NextIteration { .. } => {
let Some(declared) = params else {
return Err(XsltError::InvalidStylesheet(
"xsl:break / xsl:next-iteration must appear within an \
xsl:iterate (only xsl:if / xsl:choose may intervene) (XTSE3120)".into(),
));
};
if i != last {
return Err(XsltError::InvalidStylesheet(
"xsl:break / xsl:next-iteration must be the last instruction \
in its sequence constructor (XTSE0010)".into(),
));
}
if let NextIteration { with_params } = instr {
for w in with_params {
if !declared.contains(&qname_key(&w.name)) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:next-iteration sets parameter '{}' not declared on \
the enclosing xsl:iterate (XTSE3130)", w.name.local,
)));
}
}
}
}
If { body, .. } => check_iterate(body, params)?,
Choose { whens, otherwise } => {
for (_, b) in whens { check_iterate(b, params)?; }
if let Some(b) = otherwise { check_iterate(b, params)?; }
}
Iterate { params: ip, on_completion, body, .. } => {
let mut names = std::collections::HashSet::new();
for p in ip {
if !names.insert(qname_key(&p.name)) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:iterate has two parameters named '{}' (XTSE0580)", p.name.local,
)));
}
check_iterate(&p.body, None)?;
}
check_iterate(on_completion, None)?;
check_iterate(body, Some(&names))?;
}
LiteralElement { body, .. } | Element { body, .. }
| Attribute { body, .. } | Copy { body, .. }
| Comment { body, .. } | ProcessingInstruction { body, .. }
| ForEach { body, .. } | Document { body }
| Namespace { body, .. } | Message { body, .. }
| Assert { body, .. }
| Fallback { body } | PerformSort { body, .. }
| ValueOfBody { body, .. } => check_iterate(body, None)?,
ForEachGroup { body, .. } => check_iterate(body, None)?,
Merge { action, .. } => check_iterate(action, None)?,
SourceDocument { body, .. } | ResultDocument { body, .. } => check_iterate(body, None)?,
Fork { body } => check_iterate(body, None)?,
WherePopulated { body } => check_iterate(body, None)?,
OnEmpty { body } | OnNonEmpty { body } => check_iterate(body, None)?,
Map { body } => check_iterate(body, None)?,
MapEntry { body, .. } => check_iterate(body, None)?,
AnalyzeString { matching, non_matching, .. } => {
check_iterate(matching, None)?;
check_iterate(non_matching, None)?;
}
Try { body, catches } => {
check_iterate(body, None)?;
for c in catches { check_iterate(&c.body, None)?; }
}
ApplyTemplates { with_params, .. } | CallTemplate { with_params, .. }
| NextMatch { with_params } | ApplyImports { with_params }
| Evaluate { with_params, .. } => {
for w in with_params { check_iterate(&w.body, None)?; }
}
Variable(v) => check_iterate(&v.body, None)?,
Unsupported { fallback, .. } => check_iterate(fallback, None)?,
LiteralText { .. } | ValueOf { .. } | CopyOf { .. }
| Number { .. } | Sequence { .. } => {}
}
}
Ok(())
}
pub fn validate_named_template_uniqueness(ast: &StylesheetAst) -> Result<(), XsltError> {
use std::collections::HashMap;
let mut seen: HashMap<(String, i32), usize> = HashMap::new();
for t in &ast.templates {
let Some(name) = &t.name else { continue; };
let key = qname_key(name);
*seen.entry((key, t.import_precedence)).or_insert(0) += 1;
}
let mut max_prec: HashMap<String, i32> = HashMap::new();
for t in &ast.templates {
if let Some(name) = &t.name {
let key = qname_key(name);
let entry = max_prec.entry(key).or_insert(t.import_precedence);
if t.import_precedence > *entry { *entry = t.import_precedence; }
}
}
for ((name, prec), count) in &seen {
if *count < 2 { continue; }
if max_prec.get(name).copied().unwrap_or(*prec) > *prec { continue; }
return Err(XsltError::InvalidStylesheet(format!(
"duplicate xsl:template name='{name}' at the same import \
precedence ({prec}) with no higher-precedence override \
(XTSE0660)"
)));
}
Ok(())
}
pub fn validate_attribute_set_refs(ast: &StylesheetAst) -> Result<(), XsltError> {
let known: std::collections::HashSet<String> = ast.attribute_sets.iter()
.map(|s| {
if s.name.uri.is_empty() { s.name.local.clone() }
else { format!("{{{}}}{}", s.name.uri, s.name.local) }
})
.collect();
let qkey = |q: &QName| -> String {
if q.uri.is_empty() { q.local.clone() }
else { format!("{{{}}}{}", q.uri, q.local) }
};
let check_list = |list: &[QName]| -> Result<(), XsltError> {
for q in list {
if !known.contains(&qkey(q)) {
return Err(XsltError::InvalidStylesheet(format!(
"xsl:use-attribute-sets references undeclared set '{}' (XTSE0710)",
q.local,
)));
}
}
Ok(())
};
for s in &ast.attribute_sets { check_list(&s.use_attribute_sets)?; }
for t in &ast.templates {
walk_attr_set_refs(&t.body, &check_list)?;
}
for v in &ast.global_variables { walk_attr_set_refs(&v.body, &check_list)?; }
for p in &ast.global_params { walk_attr_set_refs(&p.body, &check_list)?; }
Ok(())
}
fn walk_attr_set_refs<F>(body: &[Instr], check: &F) -> Result<(), XsltError>
where F: Fn(&[QName]) -> Result<(), XsltError>,
{
use Instr::*;
for instr in body {
match instr {
LiteralElement { use_attribute_sets, body, .. } => {
check(use_attribute_sets)?;
walk_attr_set_refs(body, check)?;
}
Element { use_attribute_sets, body, .. } => {
check(use_attribute_sets)?;
walk_attr_set_refs(body, check)?;
}
Copy { use_attribute_sets, body, .. } => {
check(use_attribute_sets)?;
walk_attr_set_refs(body, check)?;
}
Attribute { body, .. } | Comment { body, .. }
| Variable(crate::ast::Variable { body, .. })
| Message { body, .. } | Assert { body, .. }
| Fallback { body } => walk_attr_set_refs(body, check)?,
ApplyTemplates { with_params, .. } | CallTemplate { with_params, .. }
| Evaluate { with_params, .. } => {
for w in with_params { walk_attr_set_refs(&w.body, check)?; }
}
ForEach { body, .. } => walk_attr_set_refs(body, check)?,
If { body, .. } => walk_attr_set_refs(body, check)?,
Choose { whens, otherwise } => {
for (_, b) in whens { walk_attr_set_refs(b, check)?; }
if let Some(b) = otherwise { walk_attr_set_refs(b, check)?; }
}
ProcessingInstruction { body, .. } => walk_attr_set_refs(body, check)?,
Unsupported { fallback, .. } => walk_attr_set_refs(fallback, check)?,
ApplyImports { .. } | LiteralText { .. } | ValueOf { .. } | CopyOf { .. }
| Number { .. } | Sequence { .. } => {}
NextMatch { with_params } => {
for w in with_params { walk_attr_set_refs(&w.body, check)?; }
}
ForEachGroup { body, .. } => walk_attr_set_refs(body, check)?,
Merge { action, .. } => walk_attr_set_refs(action, check)?,
SourceDocument { body, .. } | ResultDocument { body, .. } => walk_attr_set_refs(body, check)?,
Fork { body } => walk_attr_set_refs(body, check)?,
WherePopulated { body } => walk_attr_set_refs(body, check)?,
OnEmpty { body } | OnNonEmpty { body } => walk_attr_set_refs(body, check)?,
Map { body } | MapEntry { body, .. } => walk_attr_set_refs(body, check)?,
AnalyzeString { matching, non_matching, .. } => {
walk_attr_set_refs(matching, check)?;
walk_attr_set_refs(non_matching, check)?;
}
PerformSort { body, .. } => walk_attr_set_refs(body, check)?,
Document { body } => walk_attr_set_refs(body, check)?,
Namespace { body, .. } => walk_attr_set_refs(body, check)?,
ValueOfBody { body, .. } => walk_attr_set_refs(body, check)?,
Try { body, catches } => {
walk_attr_set_refs(body, check)?;
for c in catches { walk_attr_set_refs(&c.body, check)?; }
}
Iterate { params, on_completion, body, .. } => {
for p in params { walk_attr_set_refs(&p.body, check)?; }
walk_attr_set_refs(on_completion, check)?;
walk_attr_set_refs(body, check)?;
}
NextIteration { with_params } => {
for w in with_params { walk_attr_set_refs(&w.body, check)?; }
}
Break { body, .. } => walk_attr_set_refs(body, check)?,
}
}
Ok(())
}
fn collect_extension_element_uris(node: &Node) -> std::collections::HashSet<String> {
let mut prefixes: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut cur = Some(node);
while let Some(n) = cur {
for attr in n.attributes() {
let name = attr.name();
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let in_xsl_ns = attr.namespace.get()
.map(|ns| ns.href() == "http://www.w3.org/1999/XSL/Transform")
.unwrap_or(false);
let bare = attr.namespace.get().is_none() && !name.contains(':');
if local == "extension-element-prefixes"
&& (bare || in_xsl_ns || name == "xsl:extension-element-prefixes")
{
for tok in attr.value().split_whitespace() {
prefixes.insert(tok.to_owned());
}
}
}
cur = n.parent.get();
}
if prefixes.is_empty() {
return std::collections::HashSet::new();
}
let mut uris: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut cur = Some(node);
let mut seen_prefix: std::collections::HashSet<String> = std::collections::HashSet::new();
while let Some(n) = cur {
for (prefix, uri) in n.ns_declarations() {
let key = prefix.map(str::to_owned).unwrap_or_else(|| "#default".to_string());
if seen_prefix.contains(&key) { continue; }
seen_prefix.insert(key.clone());
let bound = match prefix {
Some(p) => prefixes.contains(p),
None => prefixes.contains("#default"),
};
if bound && !uri.is_empty() {
uris.insert(uri.to_owned());
}
}
cur = n.parent.get();
}
uris
}
fn avt_is_empty_expr(s: &str) -> bool {
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
' ' | '\t' | '\n' | '\r' => {}
'(' if chars.peek() == Some(&':') => {
chars.next();
let mut depth = 1u32;
while let Some(cc) = chars.next() {
if cc == '(' && chars.peek() == Some(&':') {
chars.next(); depth += 1;
} else if cc == ':' && chars.peek() == Some(&')') {
chars.next(); depth -= 1;
if depth == 0 { break; }
}
}
if depth > 0 { return false; }
}
_ => return false,
}
}
true
}
fn avt(node: &Node, s: &str) -> Result<Avt, XsltError> {
let bc = ancestor_forces_backwards_compat(node);
let mut parts = Vec::new();
let mut buf = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'{' if chars.peek() == Some(&'{') => {
chars.next();
buf.push('{');
}
'}' if chars.peek() == Some(&'}') => {
chars.next();
buf.push('}');
}
'{' => {
if !buf.is_empty() {
parts.push(AvtPart::Literal(std::mem::take(&mut buf)));
}
let mut expr_text = String::new();
let mut in_apos = false;
let mut in_quot = false;
let mut paren_depth = 0u32;
let mut bracket_depth = 0u32;
let mut comment_depth = 0u32;
let mut closed = false;
while let Some(ec) = chars.next() {
if comment_depth > 0 {
if ec == '(' && chars.peek() == Some(&':') {
chars.next();
comment_depth += 1;
expr_text.push('(');
expr_text.push(':');
} else if ec == ':' && chars.peek() == Some(&')') {
chars.next();
comment_depth -= 1;
expr_text.push(':');
expr_text.push(')');
} else {
expr_text.push(ec);
}
continue;
}
match ec {
'\'' if !in_quot => { in_apos = !in_apos; expr_text.push(ec); }
'"' if !in_apos => { in_quot = !in_quot; expr_text.push(ec); }
'(' if !in_apos && !in_quot && chars.peek() == Some(&':') => {
chars.next();
comment_depth += 1;
expr_text.push('(');
expr_text.push(':');
}
'(' if !in_apos && !in_quot => { paren_depth += 1; expr_text.push(ec); }
')' if !in_apos && !in_quot && paren_depth > 0
=> { paren_depth -= 1; expr_text.push(ec); }
'[' if !in_apos && !in_quot => { bracket_depth += 1; expr_text.push(ec); }
']' if !in_apos && !in_quot && bracket_depth > 0
=> { bracket_depth -= 1; expr_text.push(ec); }
'}' if !in_apos && !in_quot && paren_depth == 0
&& bracket_depth == 0 => { closed = true; break; }
_ => expr_text.push(ec),
}
}
if !closed {
return Err(XsltError::InvalidStylesheet(format!(
"unterminated AVT expression in '{s}'"
)));
}
if avt_is_empty_expr(&expr_text) {
continue;
}
let mut expr = parse_xpath(&expr_text).map_err(XsltError::from)?;
if bc {
expr = Expr::BackwardsCompat(Box::new(expr));
}
parts.push(AvtPart::Expr(expr));
}
'}' => {
return Err(XsltError::InvalidStylesheet(format!(
"unmatched '}}' in AVT '{s}' (use '}}}}' for a literal '}}')"
)));
}
other => buf.push(other),
}
}
if !buf.is_empty() {
parts.push(AvtPart::Literal(buf));
}
Ok(Avt { parts })
}
#[allow(dead_code)]
fn avt_from_body_or_value(node: &Node) -> Result<Avt, XsltError> {
let mut buf = String::new();
for child in node.children() {
match child.kind {
NodeKind::Text | NodeKind::CData => buf.push_str(child.content()),
_ => {}
}
}
avt(node, &buf)
}
fn is_xslt_element(node: &Node) -> bool {
node.is_element()
&& node.namespace.get().map(|ns| ns.href()) == Some(XSLT_NS)
}
fn has_xsl_version_attr(node: &Node) -> bool {
node.attributes().any(|a| {
a.name().rsplit(':').next() == Some("version")
&& a.namespace.get().map(|ns| ns.href()) == Some(XSLT_NS)
})
}
fn compile_simplified(root: &Node) -> Result<StylesheetAst, XsltError> {
let mut ast = StylesheetAst::default();
ast.version = root
.attributes()
.find(|a| {
a.name().rsplit(':').next() == Some("version")
&& a.namespace.get().map(|ns| ns.href()) == Some(XSLT_NS)
})
.map(|a| a.value().to_string())
.unwrap_or_default();
let xslt_2_0_on = version_enables_2_0(&ast.version);
let in_fwd_compat = version_is_greater_than(&ast.version, "2.0");
let _xslt_mode = XsltModeGuard::enter_full(xslt_2_0_on, true, in_fwd_compat);
for (prefix, uri) in root.ns_declarations() {
match prefix {
Some(p) => { ast.namespaces.insert(p.to_string(), uri.to_string()); }
None => { ast.namespaces.insert(String::new(), uri.to_string()); }
}
}
let lre = compile_literal_element(root)?;
let template = crate::ast::Template {
match_pattern: Some(parse_xpath("/").map_err(XsltError::from)?),
name: None,
mode: None,
modes: Vec::new(),
modes_match_all: false,
priority: None,
import_precedence: TOP_LEVEL_IMPORT_PRECEDENCE,
source_path: vec![0],
params: Vec::new(),
body: body_of_one(root, lre),
as_type: None,
visibility: None,
package_id: 0,
};
ast.templates.push(template);
ast.documents_to_load = crate::walk::collect_static_document_uris(&ast);
Ok(ast)
}
fn node_qname(node: &Node) -> Result<QName, XsltError> {
let ns_href = node.namespace.get().map(|ns| ns.href().to_string()).unwrap_or_default();
let ns_pref = node.namespace.get().and_then(|ns| ns.prefix()).map(str::to_string);
Ok(QName {
prefix: ns_pref,
local: node.local_name().to_string(),
uri: ns_href,
})
}
fn attr_qname(_parent: &Node, attr: &Attribute) -> Result<QName, XsltError> {
let n = attr.name();
let (prefix, local) = match n.split_once(':') {
Some((p, l)) => (Some(p.to_string()), l.to_string()),
None => (None, n.to_string()),
};
let uri = attr.namespace.get().map(|ns| ns.href().to_string()).unwrap_or_default();
Ok(QName { prefix, local, uri })
}
fn collect_inner_ns(node: &Node, map: &mut std::collections::HashMap<String, String>) {
for child in node.children() {
if !child.is_element() { continue; }
for (prefix, uri) in child.ns_declarations() {
let key = match prefix {
Some(p) => p.to_string(),
None => String::new(),
};
map.entry(key).or_insert_with(|| uri.to_string());
}
collect_inner_ns(child, map);
}
}
fn read_attribute<'doc>(node: &'doc Node, local: &str) -> Option<&'doc str> {
for attr in node.attributes() {
let n = attr.name();
if attr.namespace.get().is_some() || n.contains(':') { continue; }
if n == local { return Some(attr.value()); }
}
None
}
fn read_xsl_attribute<'doc>(node: &'doc Node, local: &str) -> Option<&'doc str> {
for attr in node.attributes() {
let n = attr.name();
let attr_local = n.rsplit_once(':').map(|(_, l)| l).unwrap_or(n);
if attr_local != local { continue; }
let in_xsl_ns = attr.namespace.get()
.map(|ns| ns.href() == "http://www.w3.org/1999/XSL/Transform")
.unwrap_or(false);
if in_xsl_ns { return Some(attr.value()); }
}
None
}
fn require_attr<'doc>(
node: &'doc Node, name: &str, who: &str,
) -> Result<&'doc str, XsltError> {
read_attribute(node, name).ok_or_else(|| XsltError::InvalidStylesheet(
format!("{who} requires {name}= attribute"),
))
}
fn required_qname_attr(
node: &Node, name: &str, who: &str,
) -> Result<QName, XsltError> {
let raw = require_attr(node, name, who)?;
let qn = parse_qname_on(node, raw)?;
if !is_valid_ncname(&qn.local) {
return Err(XsltError::InvalidStylesheet(format!(
"{who}: '{raw}' is not a valid QName (XTSE0020)"
)));
}
if let Some(p) = &qn.prefix {
if !is_valid_ncname(p) {
return Err(XsltError::InvalidStylesheet(format!(
"{who}: '{raw}' has an invalid prefix '{p}' (XTSE0020)"
)));
}
}
Ok(qn)
}
fn parse_name_test_token(context_node: &Node, tok: &str) -> Result<QName, XsltError> {
if tok == "*" {
return Ok(QName { prefix: None, local: "*".into(), uri: String::new() });
}
if let Some(prefix) = tok.strip_suffix(":*") {
let mut uri = String::new();
let mut cur = Some(context_node);
while let Some(n) = cur {
for (p, href) in n.ns_declarations() {
if p == Some(prefix) { uri = href.to_string(); break; }
}
if !uri.is_empty() { break; }
cur = n.parent.get();
}
if uri.is_empty() {
if prefix == "xml" {
uri = "http://www.w3.org/XML/1998/namespace".to_string();
} else {
return Err(XsltError::InvalidStylesheet(format!(
"undeclared namespace prefix '{prefix}' in NameTest \
'{tok}' (XTSE0280)"
)));
}
}
return Ok(QName {
prefix: Some(prefix.to_string()),
local: "*".into(), uri,
});
}
if let Some(local) = tok.strip_prefix("*:") {
return Ok(QName {
prefix: Some("*".into()),
local: local.to_string(),
uri: String::new(),
});
}
parse_qname_on(context_node, tok)
}
fn parse_qname_on(context_node: &Node, s: &str) -> Result<QName, XsltError> {
let s = s.trim();
let (prefix, local) = match s.split_once(':') {
Some((p, l)) => (Some(p.to_string()), l.to_string()),
None => (None, s.to_string()),
};
if !is_valid_ncname(&local) {
return Err(XsltError::InvalidStylesheet(format!(
"qname '{s}' is not a valid QName (XTSE0020)"
)));
}
if let Some(p) = &prefix {
if !is_valid_ncname(p) {
return Err(XsltError::InvalidStylesheet(format!(
"qname '{s}' has invalid prefix (XTSE0020)"
)));
}
}
let uri = match &prefix {
None => String::new(),
Some(p) => {
let mut cur = Some(context_node);
let mut found = None;
while let Some(n) = cur {
for (pref, href) in n.ns_declarations() {
if pref == Some(p.as_str()) {
found = Some(href.to_string());
break;
}
}
if found.is_some() { break; }
cur = n.parent.get();
}
found.ok_or_else(|| XsltError::InvalidStylesheet(format!(
"qname '{s}' references undeclared prefix '{p}'"
)))?
}
};
Ok(QName { prefix, local, uri })
}
fn reject_invalid_pattern_axes(expr: &Expr, who: &str) -> Result<(), XsltError> {
use sup_xml_core::xpath::ast::{Axis, LocationPath};
fn walk_step(s: &sup_xml_core::xpath::ast::Step, who: &str) -> Result<(), XsltError> {
let ok = matches!(s.axis,
Axis::Child | Axis::Attribute | Axis::DescendantOrSelf | Axis::Self_);
if !ok {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern axis '{:?}' not permitted in a pattern (XTSE0340)",
s.axis,
)));
}
Ok(())
}
fn walk(e: &Expr, who: &str) -> Result<(), XsltError> {
match e {
Expr::Path(LocationPath::Absolute(steps))
| Expr::Path(LocationPath::Relative(steps)) => {
for s in steps { walk_step(s, who)?; }
}
Expr::FilterPath { primary, steps, .. } => {
match primary.as_ref() {
Expr::FunctionCall(name, _) => {
let local = name.rsplit_once(':')
.map(|(_, l)| l).unwrap_or(name);
if !matches!(local, "id" | "key") {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern primary '{local}(...)' is not \
id() or key() — only those function calls may \
head a pattern (XSLT 2.0 §5.5.2)"
)));
}
}
Expr::Variable(name) => return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern starts with a variable reference \
'${name}' — variables aren't permitted at the head \
of an XSLT 2.0 pattern (XPST0003)"
))),
_ => {}
}
for s in steps { walk_step(s, who)?; }
}
Expr::Variable(name) => return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern '${name}' — a bare variable reference is \
not a legal XSLT 2.0 pattern (XPST0003)"
))),
Expr::Union(a, b) => { walk(a, who)?; walk(b, who)?; }
_ => {}
}
Ok(())
}
walk(expr, who)
}
fn reject_invalid_pattern_key_calls(expr: &Expr, who: &str) -> Result<(), XsltError> {
fn walk(e: &Expr, who: &str) -> Result<(), XsltError> {
match e {
Expr::FunctionCall(name, args) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
if local == "id" {
if args.len() != 1 {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern call id(): must have exactly one \
argument — the 2-arg form is not permitted in \
an XSLT 2.0 pattern (XTSE0340)"
)));
}
if !matches!(args[0], Expr::Literal(_) | Expr::Variable(_)) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern call id(): argument must be a \
string literal or variable reference (XPST0017)"
)));
}
}
if local == "key" && args.len() >= 1 {
if !matches!(args[0], Expr::Literal(_)) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern call key(): first argument must be a \
string literal (XPST0017)"
)));
}
if args.len() >= 2 {
let ok = matches!(args[1],
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_)
| Expr::Variable(_)
| Expr::FilterPath { .. });
if !ok {
return Err(XsltError::InvalidStylesheet(format!(
"{who} pattern call key(): second argument must be a \
literal or variable reference (XPST0017)"
)));
}
}
}
for a in args { walk(a, who)?; }
Ok(())
}
Expr::FilterPath { primary, predicates, steps } => {
walk(primary, who)?;
for p in predicates { walk(p, who)?; }
for s in steps { for p in &s.predicates { walk(p, who)?; } }
Ok(())
}
Expr::Union(a, b) => { walk(a, who)?; walk(b, who)?; Ok(()) }
_ => Ok(()),
}
}
walk(expr, who)
}
fn ensure_pattern_shape(expr: &Expr, who: &str) -> Result<(), XsltError> {
use sup_xml_core::xpath::ast::{Expr as E, LocationPath};
let illegal = matches!(expr,
E::Add(_, _) | E::Sub(_, _) | E::Mul(_, _) | E::Div(_, _)
| E::Mod(_, _) | E::IDiv(_, _) | E::Neg(_)
| E::And(_, _) | E::Or(_, _)
| E::Eq(_, _) | E::Ne(_, _)
| E::Lt(_, _) | E::Gt(_, _) | E::Le(_, _) | E::Ge(_, _)
| E::ValueEq(_, _) | E::ValueNe(_, _)
| E::ValueLt(_, _) | E::ValueGt(_, _)
| E::ValueLe(_, _) | E::ValueGe(_, _)
| E::IfThenElse { .. } | E::For { .. } | E::Let { .. }
| E::Quantified { .. } | E::Range(_, _));
if illegal {
return Err(XsltError::InvalidStylesheet(format!(
"{who} is not a valid pattern (XTSE0340)"
)));
}
fn check_path_steps(
steps: &[sup_xml_core::xpath::ast::Step], absolute: bool,
who: &str,
) -> Result<(), XsltError> {
for (i, s) in steps.iter().enumerate() {
let Some(filter) = &s.filter else { continue; };
match filter.as_ref() {
Expr::FunctionCall(name, _) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
let is_id_key = matches!(local, "id" | "key");
let allowed = is_id_key && i == 0 && !absolute;
if !allowed {
return Err(XsltError::InvalidStylesheet(format!(
"{who} uses a function call '{local}()' in a position \
that the pattern grammar doesn't permit (XPST0017)"
)));
}
}
Expr::Sequence(_) | Expr::Union(_, _) | Expr::Range(_, _) => {
return Err(XsltError::InvalidStylesheet(format!(
"{who} contains a parenthesised expression in a path \
step that the pattern grammar doesn't permit (XPST0003)"
)));
}
_ => {}
}
}
Ok(())
}
if let E::Path(LocationPath::Relative(steps)) = expr {
check_path_steps(steps, false, who)?;
}
if let E::Path(LocationPath::Absolute(steps)) = expr {
check_path_steps(steps, true, who)?;
}
if let E::Union(l, r) = expr {
ensure_pattern_shape(l, who)?;
ensure_pattern_shape(r, who)?;
}
Ok(())
}
fn reject_pattern_grouping_calls(expr: &Expr, who: &str) -> Result<(), XsltError> {
fn walk(e: &Expr) -> Option<(&'static str, &'static str)> {
use sup_xml_core::xpath::ast::LocationPath;
match e {
Expr::FunctionCall(name, args) => {
let local = name.rsplit_once(':').map(|(_, l)| l).unwrap_or(name);
match local {
"current-group" => return Some(("current-group()", "XTSE1060")),
"current-grouping-key" => return Some(("current-grouping-key()","XTSE1070")),
"regex-group" => return Some(("regex-group()", "XTSE1140")),
_ => {}
}
for a in args { if let Some(r) = walk(a) { return Some(r); } }
None
}
Expr::Or(a, b) | Expr::And(a, b)
| Expr::Eq(a, b) | Expr::Ne(a, b)
| Expr::Lt(a, b) | Expr::Gt(a, b) | Expr::Le(a, b) | Expr::Ge(a, b)
| Expr::ValueEq(a, b) | Expr::ValueNe(a, b)
| Expr::ValueLt(a, b) | Expr::ValueGt(a, b)
| Expr::ValueLe(a, b) | Expr::ValueGe(a, b)
| Expr::Add(a, b) | Expr::Sub(a, b)
| Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Mod(a, b)
| Expr::Union(a, b)
| Expr::IDiv(a, b) | Expr::Intersect(a, b) | Expr::Except(a, b)
| Expr::Range(a, b) | Expr::SimpleMap(a, b)
| Expr::NodeBefore(a, b) | Expr::NodeAfter(a, b) | Expr::NodeIs(a, b) =>
walk(a).or_else(|| walk(b)),
Expr::Neg(a)
| Expr::InstanceOf(a, _) | Expr::CastAs(a, _)
| Expr::CastableAs(a, _) | Expr::TreatAs(a, _) => walk(a),
Expr::Sequence(args) => {
for a in args { if let Some(r) = walk(a) { return Some(r); } }
None
}
Expr::IfThenElse { cond, then_branch, else_branch } =>
walk(cond).or_else(|| walk(then_branch)).or_else(|| walk(else_branch)),
Expr::For { bindings, body } | Expr::Let { bindings, body } | Expr::Quantified { bindings, test: body, .. } => {
for (_, e) in bindings { if let Some(r) = walk(e) { return Some(r); } }
walk(body)
}
Expr::FilterPath { primary, predicates, steps } => {
if let Some(r) = walk(primary) { return Some(r); }
for p in predicates { if let Some(r) = walk(p) { return Some(r); } }
for s in steps { for p in &s.predicates { if let Some(r) = walk(p) { return Some(r); } } }
None
}
Expr::Path(p) => {
let steps = match p { LocationPath::Absolute(s) | LocationPath::Relative(s) => s };
for s in steps { for p in &s.predicates { if let Some(r) = walk(p) { return Some(r); } } }
None
}
Expr::TryCatch { body, catches } => {
if let Some(r) = walk(body) { return Some(r); }
for c in catches { if let Some(r) = walk(&c.body) { return Some(r); } }
None
}
Expr::WithDefaultCollation(_, inner) => walk(inner),
Expr::BackwardsCompat(inner) => walk(inner),
Expr::MapConstructor(es) => es.iter()
.find_map(|(k, v)| walk(k).or_else(|| walk(v))),
Expr::ArrayConstructor { members, .. } => members.iter().find_map(walk),
Expr::Lookup(b, key) => walk(b).or_else(||
match key {
sup_xml_core::xpath::ast::LookupKey::Expr(e) => walk(e),
_ => None,
}),
Expr::UnaryLookup(key) => match key {
sup_xml_core::xpath::ast::LookupKey::Expr(e) => walk(e),
_ => None,
},
Expr::InlineFunction { body, .. } => walk(body),
Expr::DynamicCall { func, args } =>
walk(func).or_else(|| args.iter().find_map(walk)),
Expr::NamedFunctionRef { .. } | Expr::Placeholder | Expr::ContextItem => None,
Expr::Literal(_) | Expr::Integer(_) | Expr::Decimal(_) | Expr::Double(_) | Expr::Variable(_) => None,
}
}
if let Some((fname, code)) = walk(expr) {
return Err(XsltError::InvalidStylesheet(format!(
"{fname} cannot be used in {who} (a pattern context — {code})"
)));
}
Ok(())
}
fn is_unicode_zero_digit(c: char) -> bool {
matches!(c as u32,
0x0030 | 0x0660 | 0x06F0 | 0x07C0 | 0x0966 | 0x09E6 | 0x0A66 | 0x0AE6 | 0x0B66 | 0x0BE6 | 0x0C66 | 0x0CE6 | 0x0D66 | 0x0DE6 | 0x0E50 | 0x0ED0 | 0x0F20 | 0x1040 | 0x1090 | 0x17E0 | 0x1810 | 0x1946 | 0x19D0 | 0x1A80 | 0x1A90 | 0x1B50 | 0x1BB0 | 0x1C40 | 0x1C50 | 0xA620 | 0xA8D0 | 0xA900 | 0xA9D0 | 0xA9F0 | 0xAA50 | 0xABF0 | 0xFF10 | 0x104A0 | 0x11066 | 0x110F0 | 0x11136 | 0x111D0 | 0x112F0 | 0x114D0 | 0x11650 | 0x116C0 | 0x11730 | 0x118E0 | 0x11C50 | 0x11D50 | 0x11DA0 | 0x16A60 | 0x16B50 | 0x1D7CE | 0x1D7D8 | 0x1D7E2 | 0x1D7EC | 0x1D7F6 |
0x1E950 )
}
pub(crate) fn is_recognised_collation(uri: &str) -> bool {
let t = uri.trim();
if t.contains('{') { return true; }
matches!(t,
"" | "http://www.w3.org/2005/xpath-functions/collation/codepoint"
| "http://www.w3.org/2005/xpath-functions/collation/html-ascii-case-insensitive")
}
fn qname_key(q: &QName) -> String {
if q.uri.is_empty() { q.local.clone() }
else { format!("{{{uri}}}{local}", uri = q.uri, local = q.local) }
}
const RESERVED_NAME_URIS: &[&str] = &[
"http://www.w3.org/1999/XSL/Transform",
"http://www.w3.org/2005/xpath-functions",
"http://www.w3.org/2005/xpath-functions/math",
"http://www.w3.org/2005/xpath-functions/map",
"http://www.w3.org/2005/xpath-functions/array",
"http://www.w3.org/2005/xquery-local-functions",
"http://www.w3.org/2001/XMLSchema",
"http://www.w3.org/2001/XMLSchema-instance",
];
fn reject_reserved_name(name: &QName, who: &str) -> Result<(), XsltError> {
let is_initial_template = name.uri == "http://www.w3.org/1999/XSL/Transform"
&& name.local == "initial-template";
if !is_initial_template && RESERVED_NAME_URIS.iter().any(|u| *u == name.uri) {
return Err(XsltError::InvalidStylesheet(format!(
"{who} name='{}:{}' uses a reserved namespace (XTSE0080)",
name.prefix.as_deref().unwrap_or(""), name.local,
)));
}
Ok(())
}
fn is_valid_ncname(s: &str) -> bool {
if s.is_empty() { return false; }
let mut chars = s.chars();
let first = chars.next().unwrap();
let is_name_start = |c: char|
c.is_alphabetic() || c == '_';
let is_name_char = |c: char|
c.is_alphanumeric() || c == '_' || c == '-' || c == '.';
if !is_name_start(first) { return false; }
chars.all(is_name_char)
}
fn parse_qname_list(
context_node: &Node, raw: &str,
) -> Result<Vec<QName>, XsltError> {
let mut out = Vec::new();
for tok in raw.split_whitespace() {
out.push(parse_qname_on(context_node, tok)?);
}
Ok(out)
}
fn parse_yesno(s: &str) -> bool {
matches!(s.trim(), "yes" | "true" | "1")
}
fn parse_yesno_strict(s: &str, who: &str, attr: &str) -> Result<bool, XsltError> {
match s.trim() {
"yes" | "true" | "1" => Ok(true),
"no" | "false" | "0" => Ok(false),
other => Err(XsltError::InvalidStylesheet(format!(
"{who} {attr}='{other}' must be 'yes' or 'no' (XTSE0020)"
))),
}
}
fn parse_bool_attr(s: &str, who: &str, attr: &str) -> Result<bool, XsltError> {
match s.trim() {
"yes" => Ok(true),
"no" => Ok(false),
"true" | "1" if in_forwards_compat_mode() => Ok(true),
"false" | "0" if in_forwards_compat_mode() => Ok(false),
other => Err(XsltError::InvalidStylesheet(format!(
"{who} {attr}='{other}' must be 'yes' or 'no' (XTSE0020)"
))),
}
}
fn is_significant_text(node: &Node) -> bool {
if !matches!(node.kind, NodeKind::Text | NodeKind::CData) {
return false;
}
if !is_xslt_whitespace_only(node.content()) {
return true;
}
let mut cur = node.parent.get();
while let Some(p) = cur {
for a in p.attributes() {
if a.local_name() == "space"
&& a.namespace.get().and_then(|n| n.prefix()) == Some("xml") {
return a.value() == "preserve";
}
}
cur = p.parent.get();
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::loader::InMemoryLoader;
fn parse(src: &str) -> Document {
let opts = sup_xml_core::ParseOptions {
namespace_aware: true,
..sup_xml_core::ParseOptions::default()
};
sup_xml_core::parse_str(src, &opts).expect("parse")
}
#[test]
fn line_col_from_starts_matches_compute_line_col() {
let src = "<a>\n <b/>\n\n <c x=\"é\"/>\r\n</a>".as_bytes();
let starts = line_starts(src);
for offset in 0..=src.len() + 4 {
let want = sup_xml_core::compute_line_col(src, offset);
let clamped = offset.min(src.len()) as u32;
let got = line_col_from_starts(&starts, clamped);
assert_eq!(got, want, "offset {offset}");
}
}
#[test]
fn line_starts_records_each_line_boundary() {
assert_eq!(line_starts(b""), vec![0]);
assert_eq!(line_starts(b"abc"), vec![0]);
assert_eq!(line_starts(b"a\nbc\n"), vec![0, 2, 5]);
assert_eq!(line_starts(b"\n\n"), vec![0, 1, 2]);
}
#[test]
fn root_not_in_xslt_namespace_errors() {
let doc = parse(r#"<bogus version="1.0"/>"#);
let r = compile(&doc);
assert!(r.is_err());
if let Err(XsltError::InvalidStylesheet(msg)) = r {
assert!(msg.contains("not in the XSLT namespace"), "got {msg}");
}
}
#[test]
fn root_xslt_but_wrong_local_errors() {
let doc = parse(r#"<xsl:not-a-stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"/>"#);
let r = compile(&doc);
assert!(r.is_err());
if let Err(XsltError::InvalidStylesheet(msg)) = r {
assert!(msg.contains("xsl:stylesheet"), "got {msg}");
}
}
#[test]
fn transform_root_is_accepted() {
let doc = parse(r#"<xsl:transform
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"/>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.version, "1.0");
}
#[test]
fn top_level_non_xslt_element_silently_ignored() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:rdf="http://example.com/rdf"
version="1.0">
<rdf:Description>extension data</rdf:Description>
<xsl:template match="/"><x/></xsl:template>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.templates.len(), 1);
}
#[test]
fn top_level_include_captures_href() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:include href="other.xsl"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.includes, vec!["other.xsl"]);
}
#[test]
fn top_level_import_captures_href() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:import href="base.xsl"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.imports, vec!["base.xsl"]);
}
#[test]
fn top_level_decimal_format_accepted() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:decimal-format name="custom"
decimal-separator=","
grouping-separator="."/>
</xsl:stylesheet>"#);
compile(&doc).unwrap();
}
#[test]
fn top_level_unknown_xslt_element_rejected_at_supported_version() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:something-from-the-future name="x"/>
</xsl:stylesheet>"#);
assert!(compile(&doc).is_err());
}
#[test]
fn top_level_unknown_xslt_element_accepted_in_forwards_compat() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="9.5">
<xsl:something-from-the-future name="x"/>
</xsl:stylesheet>"#);
compile(&doc).unwrap();
}
#[test]
fn template_with_only_name_attribute_compiles() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template name="my-template"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.templates.len(), 1);
assert!(ast.templates[0].name.is_some());
assert!(ast.templates[0].match_pattern.is_none());
}
#[test]
fn template_without_match_or_name_errors() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template/>
</xsl:stylesheet>"#);
let r = compile(&doc);
assert!(r.is_err());
if let Err(XsltError::InvalidStylesheet(msg)) = r {
assert!(msg.contains("match= or name="), "got {msg}");
}
}
#[test]
fn template_with_invalid_priority_errors() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/" priority="not-a-number"/>
</xsl:stylesheet>"#);
let r = compile(&doc);
assert!(r.is_err());
if let Err(XsltError::InvalidStylesheet(msg)) = r {
assert!(msg.contains("priority"), "got {msg}");
}
}
#[test]
fn template_with_valid_priority_compiles() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/" priority="2.5"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.templates[0].priority, Some(2.5));
}
#[test]
fn attribute_set_with_attribute_children_compiles() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:attribute-set name="my-set">
<xsl:attribute name="id">x</xsl:attribute>
<xsl:attribute name="class">y</xsl:attribute>
</xsl:attribute-set>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.attribute_sets.len(), 1);
assert_eq!(ast.attribute_sets[0].attributes.len(), 2);
}
#[test]
fn attribute_set_with_non_attribute_child_errors() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:attribute-set name="my-set">
<xsl:template name="bogus"/>
</xsl:attribute-set>
</xsl:stylesheet>"#);
let r = compile(&doc);
assert!(r.is_err());
if let Err(XsltError::InvalidStylesheet(msg)) = r {
assert!(msg.contains("xsl:attribute"), "got {msg}");
}
}
#[test]
fn attribute_set_with_use_attribute_sets() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:attribute-set name="base">
<xsl:attribute name="id">x</xsl:attribute>
</xsl:attribute-set>
<xsl:attribute-set name="derived" use-attribute-sets="base">
<xsl:attribute name="class">y</xsl:attribute>
</xsl:attribute-set>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.attribute_sets.len(), 2);
assert_eq!(ast.attribute_sets[1].use_attribute_sets.len(), 1);
}
#[test]
fn output_with_cdata_section_elements() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" cdata-section-elements="raw script"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
assert_eq!(ast.outputs.len(), 1);
assert_eq!(ast.outputs[0].cdata_section_elements.len(), 2);
}
#[test]
fn output_full_options() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"
encoding="UTF-8"
indent="yes"
omit-xml-declaration="yes"
standalone="no"
media-type="text/html"
doctype-public="-//W3C//DTD HTML 4.01//EN"
doctype-system="http://www.w3.org/TR/html4/strict.dtd"
version="4.0"/>
</xsl:stylesheet>"#);
let ast = compile(&doc).unwrap();
let o = &ast.outputs[0];
assert_eq!(o.method.as_deref(), Some("html"));
assert_eq!(o.encoding.as_deref(), Some("UTF-8"));
assert_eq!(o.indent, Some(true));
assert_eq!(o.omit_xml_declaration, Some(true));
assert_eq!(o.standalone, Some(crate::ast::Standalone::No));
assert_eq!(o.media_type.as_deref(), Some("text/html"));
assert!(o.doctype_public.is_some());
assert!(o.doctype_system.is_some());
assert_eq!(o.version.as_deref(), Some("4.0"));
}
#[test]
fn namespace_alias_with_undeclared_prefix_errors() {
let doc = parse(r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:namespace-alias stylesheet-prefix="nope" result-prefix="xsl"/>
</xsl:stylesheet>"#);
let r = compile(&doc);
assert!(r.is_err());
}
#[test]
fn namespace_alias_default_prefix_resolves_to_null_namespace() {
let doc = parse(r##"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:namespace-alias stylesheet-prefix="#default" result-prefix="xsl"/>
</xsl:stylesheet>"##);
let ast = compile(&doc).unwrap();
assert_eq!(ast.namespace_aliases.len(), 1);
let (style_uri, result_uri, result_prefix) = &ast.namespace_aliases[0];
assert_eq!(style_uri, "");
assert_eq!(result_uri, "http://www.w3.org/1999/XSL/Transform");
assert_eq!(result_prefix.as_deref(), Some("xsl"));
}
#[test]
fn compile_with_imports_via_loader() {
let main = r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:import href="imp.xsl"/>
<xsl:template match="/"><x/></xsl:template>
</xsl:stylesheet>"#;
let imp = r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="foo"><y/></xsl:template>
</xsl:stylesheet>"#;
let loader = InMemoryLoader::new().with("imp.xsl", imp);
let mut counter = 0;
let ast = compile_with_imports(main, &loader, None,
StylesheetAst::default(), &mut counter).unwrap();
assert_eq!(ast.templates.len(), 2);
let local = ast.templates.iter().find(|t| t.import_precedence == 0).unwrap();
let imported = ast.templates.iter().find(|t| t.import_precedence < 0).unwrap();
assert!(local.match_pattern.is_some());
assert!(imported.match_pattern.is_some());
}
#[test]
fn compile_with_imports_via_include() {
let main = r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:include href="inc.xsl"/>
<xsl:template match="/"><x/></xsl:template>
</xsl:stylesheet>"#;
let inc = r#"<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="foo"><y/></xsl:template>
</xsl:stylesheet>"#;
let loader = InMemoryLoader::new().with("inc.xsl", inc);
let mut counter = 0;
let ast = compile_with_imports(main, &loader, None,
StylesheetAst::default(), &mut counter).unwrap();
assert_eq!(ast.templates.len(), 2);
assert!(ast.templates.iter().all(|t| t.import_precedence == 0));
}
}