diff --git a/xee-xslt-compiler/src/ast_ir.rs b/xee-xslt-compiler/src/ast_ir.rs index 3a028ff0..de70d5f5 100644 --- a/xee-xslt-compiler/src/ast_ir.rs +++ b/xee-xslt-compiler/src/ast_ir.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use ahash::HashSetExt; use xee_name::{Name, Namespaces, FN_NAMESPACE}; @@ -12,6 +14,9 @@ use crate::{default_declarations::text_only_copy_declarations, priority::default struct IrConverter<'a> { variables: Variables, static_context: &'a StaticContext, + // Named templates by name: the ordered xsl:param declarations, used to + // resolve an xsl:call-template's arguments regardless of declaration order. + template_signatures: HashMap>, } pub fn compile( @@ -56,6 +61,7 @@ impl<'a> IrConverter<'a> { IrConverter { variables: Variables::new(), static_context, + template_signatures: HashMap::new(), } } @@ -117,6 +123,7 @@ impl<'a> IrConverter<'a> { fn transform(&mut self, transform: &ast::Transform) -> error::SpannedResult { let main_sequence_constructor = self.main_sequence_constructor(); + self.collect_template_signatures(transform); let main = self.sequence_constructor_function(&main_sequence_constructor)?; let mut declarations = ir::Declarations::new(main); @@ -126,6 +133,19 @@ impl<'a> IrConverter<'a> { Ok(declarations) } + fn collect_template_signatures(&mut self, transform: &ast::Transform) { + for declaration in &transform.declarations { + if let ast::Declaration::Template(template) = declaration { + if template.match_.is_none() { + if let Some(name) = &template.name { + self.template_signatures + .insert(name.clone(), template.params.clone()); + } + } + } + } + } + fn declaration( &mut self, declarations: &mut ir::Declarations, @@ -186,14 +206,8 @@ impl<'a> IrConverter<'a> { "A template must have a match pattern or a name".to_string(), ) })?; - if !template.params.is_empty() { - return Err(error::Error::Unsupported( - "Template parameters are not supported yet".to_string(), - ) - .into()); - } let function_definition = - self.sequence_constructor_function(&template.sequence_constructor)?; + self.named_template_function(&template.params, &template.sequence_constructor)?; declarations.named_templates.push(ir::NamedTemplate { name, function_definition, @@ -360,6 +374,45 @@ impl<'a> IrConverter<'a> { }) } + // A named template is a function taking the context (item, position, last) + // followed by its declared parameters, so xsl:call-template can supply them. + fn named_template_function( + &mut self, + params: &[ast::Param], + sequence_constructor: &ast::SequenceConstructor, + ) -> error::SpannedResult { + let context_names = self.variables.push_context(); + let param_names: Vec = params + .iter() + .map(|param| self.variables.new_var_name(¶m.name)) + .collect(); + let bindings = self.sequence_constructor(sequence_constructor)?; + self.variables.pop_context(); + + let mut ir_params = vec![ + ir::Param { + name: context_names.item, + type_: None, + }, + ir::Param { + name: context_names.position, + type_: None, + }, + ir::Param { + name: context_names.last, + type_: None, + }, + ]; + for name in param_names { + ir_params.push(ir::Param { name, type_: None }); + } + Ok(ir::FunctionDefinition { + params: ir_params, + return_type: None, + body: Box::new(bindings.expr()), + }) + } + fn sequence_constructor( &mut self, sequence_constructor: &[ast::SequenceConstructorItem], @@ -582,24 +635,41 @@ impl<'a> IrConverter<'a> { &mut self, call_template: &ast::CallTemplate, ) -> error::SpannedResult { - if !call_template.with_params.is_empty() { - return Err(error::Error::Unsupported( - "Passing parameters with xsl:with-param is not supported yet".to_string(), - ) - .into()); - } - // A named template is a function taking the context (item, position, - // last); xsl:call-template keeps the caller's context, so forward it. + // The template runs with the caller's context (item, position, last), + // which xsl:call-template preserves, followed by its parameters. Each + // parameter takes the matching xsl:with-param, or the parameter's own + // default when the call omits it. let context = self.variables.current_context_names().ok_or_else(|| { error::Error::Unsupported("xsl:call-template outside a template context".to_string()) })?; - let args = vec![ + let signature = self + .template_signatures + .get(&call_template.name) + .cloned() + .unwrap_or_default(); + + let mut combined = Bindings::empty(); + let mut args = vec![ Spanned::new(ir::Atom::Variable(context.item), (0..0).into()), Spanned::new(ir::Atom::Variable(context.position), (0..0).into()), Spanned::new(ir::Atom::Variable(context.last), (0..0).into()), ]; - let bindings = Bindings::empty(); - Ok(bindings.bind_expr_no_span( + for param in &signature { + let supplied = call_template + .with_params + .iter() + .find(|with_param| with_param.name == param.name); + let (atom, bindings) = match supplied { + Some(with_param) => self + .select_or_sequence_constructor(with_param)? + .atom_bindings(), + None => self.select_or_sequence_constructor(param)?.atom_bindings(), + }; + combined = combined.concat(bindings); + args.push(atom); + } + + Ok(combined.bind_expr_no_span( &mut self.variables, ir::Expr::CallTemplate(ir::CallTemplate { name: call_template.name.clone(), diff --git a/xee-xslt-compiler/tests/test_xslt.rs b/xee-xslt-compiler/tests/test_xslt.rs index 5074d94e..ac9f41cc 100644 --- a/xee-xslt-compiler/tests/test_xslt.rs +++ b/xee-xslt-compiler/tests/test_xslt.rs @@ -1263,3 +1263,35 @@ fn test_call_template_keeps_caller_context() { .unwrap(); assert_eq!(xml(&xot, output), "doc"); } + +#[test] +fn test_call_template_with_param() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), "hi"); +} + +#[test] +fn test_call_template_param_default() { + let mut xot = Xot::new(); + let output = evaluate( + &mut xot, + "", + r#" + + + +"#, + ) + .unwrap(); + assert_eq!(xml(&xot, output), "default"); +}