Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 39 additions & 40 deletions xee-xslt-compiler/src/ast_ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ use ahash::{HashMap, HashMapExt, HashSetExt};
use xee_name::{Name, Namespaces, FN_NAMESPACE};

use std::collections::HashSet;
use std::path::PathBuf;
use xee_interpreter::{
context::StaticContext,
error,
Expand All @@ -19,6 +18,7 @@ use xee_xslt_ast::{
use xot::xmlname::{NameStrInfo, OwnedName};

use crate::priority::default_priority;
use crate::resolver::{FileSystemResolver, StylesheetResolver};

struct IrConverter<'a> {
variables: Variables,
Expand Down Expand Up @@ -69,6 +69,24 @@ pub fn parse_with_base_dir_and_initial_mode(
xslt: &str,
base_dir: Option<std::path::PathBuf>,
initial_mode: Option<String>,
) -> error::SpannedResult<interpreter::Program> {
let resolver = FileSystemResolver::new(base_dir);
parse_with_resolver_and_initial_mode(static_context, xslt, &resolver, initial_mode)
}

pub fn parse_with_resolver(
static_context: StaticContext,
xslt: &str,
resolver: &dyn StylesheetResolver,
) -> error::SpannedResult<interpreter::Program> {
parse_with_resolver_and_initial_mode(static_context, xslt, resolver, None)
}

pub fn parse_with_resolver_and_initial_mode(
static_context: StaticContext,
xslt: &str,
resolver: &dyn StylesheetResolver,
initial_mode: Option<String>,
) -> error::SpannedResult<interpreter::Program> {
let transform = parse_transform(xslt);
// TODO: better error handling
Expand All @@ -82,7 +100,7 @@ pub fn parse_with_base_dir_and_initial_mode(
// Process xsl:import and xsl:include directives
let mut visited = HashSet::new();
transform.declarations =
process_imports_and_includes(transform.declarations, base_dir, &mut visited)?;
process_imports_and_includes(transform.declarations, resolver, &mut visited)?;

let initial_mode = parse_initial_mode_value(initial_mode)?;
if matches!(initial_mode, ast::ApplyTemplatesModeValue::Unnamed) {
Expand Down Expand Up @@ -163,8 +181,8 @@ fn map_parse_error(xslt: &str, error: ElementError) -> error::SpannedError {

fn process_imports_and_includes(
declarations: ast::Declarations,
base_dir: Option<std::path::PathBuf>,
visited: &mut HashSet<PathBuf>,
resolver: &dyn StylesheetResolver,
visited: &mut HashSet<String>,
) -> error::SpannedResult<ast::Declarations> {
let mut local_declarations = Vec::new();
let mut imports = Vec::new(); // Collect imports in order
Expand All @@ -173,36 +191,34 @@ fn process_imports_and_includes(
match &decl {
ast::Declaration::Import(import) => {
// Load and parse the imported stylesheet
let (imported_decls, resolved_path) =
load_stylesheet(&import.href.to_string(), base_dir.as_ref())?;
if visited.contains(&resolved_path) {
let (imported_decls, resolved_key) =
load_stylesheet(&import.href.to_string(), resolver)?;
if visited.contains(&resolved_key) {
return Err(error::Error::Unsupported(format!(
"Circular import detected: '{}'",
resolved_path.display()
resolved_key
))
.into());
}
visited.insert(resolved_path);
visited.insert(resolved_key);
// Recursively process imports in the imported stylesheet
let processed =
process_imports_and_includes(imported_decls, base_dir.clone(), visited)?;
let processed = process_imports_and_includes(imported_decls, resolver, visited)?;
imports.push(processed);
}
ast::Declaration::Include(include) => {
// Load and parse the included stylesheet
let (included_decls, resolved_path) =
load_stylesheet(&include.href.to_string(), base_dir.as_ref())?;
if visited.contains(&resolved_path) {
let (included_decls, resolved_key) =
load_stylesheet(&include.href.to_string(), resolver)?;
if visited.contains(&resolved_key) {
return Err(error::Error::Unsupported(format!(
"Circular include detected: '{}'",
resolved_path.display()
resolved_key
))
.into());
}
visited.insert(resolved_path);
visited.insert(resolved_key);
// Recursively process imports in the included stylesheet
let processed =
process_imports_and_includes(included_decls, base_dir.clone(), visited)?;
let processed = process_imports_and_includes(included_decls, resolver, visited)?;
local_declarations.extend(processed);
}
_ => {
Expand All @@ -223,36 +239,19 @@ fn process_imports_and_includes(

fn load_stylesheet(
href: &str,
base_dir: Option<&std::path::PathBuf>,
) -> error::SpannedResult<(ast::Declarations, PathBuf)> {
// Resolve the file path
let path = if let Some(base_dir) = base_dir {
base_dir.join(href)
} else {
std::path::PathBuf::from(href)
};

let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());

// Try to read the file
let content = std::fs::read_to_string(&path).map_err(|e| {
error::Error::Unsupported(format!(
"Failed to load stylesheet '{}': {}",
path.display(),
e
))
})?;
resolver: &dyn StylesheetResolver,
) -> error::SpannedResult<(ast::Declarations, String)> {
let (content, key) = resolver.resolve(href)?;

// Parse the stylesheet
let transform = parse_transform(&content).map_err(|e| {
error::Error::Unsupported(format!(
"Failed to parse imported stylesheet '{}': {:?}",
path.display(),
e
href, e
))
})?;

Ok((transform.declarations, canonical))
Ok((transform.declarations, key))
}

impl<'a> IrConverter<'a> {
Expand Down
9 changes: 7 additions & 2 deletions xee-xslt-compiler/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
mod ast_ir;
mod priority;
mod resolver;
mod run;

pub use ast_ir::{parse, parse_with_base_dir, parse_with_base_dir_and_initial_mode};
pub use run::evaluate;
pub use ast_ir::{
parse, parse_with_base_dir, parse_with_base_dir_and_initial_mode, parse_with_resolver,
parse_with_resolver_and_initial_mode,
};
pub use resolver::{FileSystemResolver, InMemoryResolver, StylesheetResolver};
pub use run::{evaluate, evaluate_program, evaluate_with_resolver};
65 changes: 65 additions & 0 deletions xee-xslt-compiler/src/resolver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::collections::HashMap;
use std::path::PathBuf;

use xee_interpreter::error;

/// Resolves an `xsl:import` or `xsl:include` `href` to stylesheet source.
///
/// The returned key canonically identifies the resolved stylesheet and is used
/// to detect import cycles.
pub trait StylesheetResolver {
fn resolve(&self, href: &str) -> error::SpannedResult<(String, String)>;
}

/// Reads imported stylesheets from the filesystem, resolving each `href` against
/// an optional base directory.
pub struct FileSystemResolver {
base_dir: Option<PathBuf>,
}

impl FileSystemResolver {
pub fn new(base_dir: Option<PathBuf>) -> Self {
Self { base_dir }
}
}

impl StylesheetResolver for FileSystemResolver {
fn resolve(&self, href: &str) -> error::SpannedResult<(String, String)> {
let path = match &self.base_dir {
Some(base_dir) => base_dir.join(href),
None => PathBuf::from(href),
};
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
let content = std::fs::read_to_string(&path).map_err(|e| {
error::Error::Unsupported(format!(
"Failed to load stylesheet '{}': {}",
path.display(),
e
))
})?;
Ok((content, canonical.to_string_lossy().into_owned()))
}
}

/// Resolves imported stylesheets from an in-memory map keyed by `href`, so
/// `xsl:import` and `xsl:include` work without a filesystem.
pub struct InMemoryResolver {
sources: HashMap<String, String>,
}

impl InMemoryResolver {
pub fn new(sources: HashMap<String, String>) -> Self {
Self { sources }
}
}

impl StylesheetResolver for InMemoryResolver {
fn resolve(&self, href: &str) -> error::SpannedResult<(String, String)> {
self.sources
.get(href)
.map(|source| (source.clone(), href.to_string()))
.ok_or_else(|| {
error::Error::Unsupported(format!("Stylesheet not found: '{}'", href)).into()
})
}
}
20 changes: 19 additions & 1 deletion xee-xslt-compiler/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use xee_interpreter::error;
use xee_interpreter::interpreter::Program;
use xee_interpreter::sequence;

use crate::ast_ir::parse;
use crate::ast_ir::{parse, parse_with_resolver};
use crate::resolver::StylesheetResolver;

pub fn evaluate_program(
xot: &mut Xot,
Expand Down Expand Up @@ -35,3 +36,20 @@ pub fn evaluate(xot: &mut Xot, xml: &str, xslt: &str) -> error::SpannedResult<se
let program = parse(static_context, xslt).unwrap();
evaluate_program(xot, &program, root)
}

pub fn evaluate_with_resolver(
xot: &mut Xot,
xml: &str,
xslt: &str,
resolver: &dyn StylesheetResolver,
) -> error::SpannedResult<sequence::Sequence> {
let namespaces = Namespaces::new(
Namespaces::default_namespaces(),
"".to_string(),
FN_NAMESPACE.to_string(),
);
let static_context = StaticContext::from_namespaces(namespaces);
let root = xot.parse(xml).unwrap();
let program = parse_with_resolver(static_context, xslt, resolver)?;
evaluate_program(xot, &program, root)
}
72 changes: 71 additions & 1 deletion xee-xslt-compiler/tests/test_xslt.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::fmt::Write;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
Expand All @@ -9,7 +10,9 @@ use xee_interpreter::{
xml::Documents,
};
use xee_name::{Namespaces, FN_NAMESPACE};
use xee_xslt_compiler::{evaluate, parse, parse_with_base_dir};
use xee_xslt_compiler::{
evaluate, evaluate_with_resolver, parse, parse_with_base_dir, InMemoryResolver,
};
use xot::Xot;

fn xml(xot: &Xot, sequence: Sequence) -> String {
Expand Down Expand Up @@ -2330,3 +2333,70 @@ fn test_basic_iterate_params() {
"<o><baz>1</baz><baz>2</baz><baz>4</baz></o>"
);
}

fn base_stylesheet(marker: &str) -> String {
format!(
r#"<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3">
<xsl:template match="/"><{marker}/></xsl:template>
</xsl:transform>"#
)
}

#[test]
fn test_import_from_memory() {
let mut xot = Xot::new();
let mut sources = HashMap::new();
sources.insert("base.xsl".to_string(), base_stylesheet("imported"));
let resolver = InMemoryResolver::new(sources);
let output = evaluate_with_resolver(
&mut xot,
"<doc/>",
r#"
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3">
<xsl:import href="base.xsl"/>
</xsl:transform>"#,
&resolver,
)
.unwrap();
assert_eq!(xml(&xot, output), "<imported/>");
}

#[test]
fn test_include_from_memory() {
let mut xot = Xot::new();
let mut sources = HashMap::new();
sources.insert("base.xsl".to_string(), base_stylesheet("included"));
let resolver = InMemoryResolver::new(sources);
let output = evaluate_with_resolver(
&mut xot,
"<doc/>",
r#"
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3">
<xsl:include href="base.xsl"/>
</xsl:transform>"#,
&resolver,
)
.unwrap();
assert_eq!(xml(&xot, output), "<included/>");
}

#[test]
fn test_import_precedence_from_memory() {
// The importing stylesheet outranks the imported one.
let mut xot = Xot::new();
let mut sources = HashMap::new();
sources.insert("base.xsl".to_string(), base_stylesheet("imported"));
let resolver = InMemoryResolver::new(sources);
let output = evaluate_with_resolver(
&mut xot,
"<doc/>",
r#"
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3">
<xsl:import href="base.xsl"/>
<xsl:template match="/"><main/></xsl:template>
</xsl:transform>"#,
&resolver,
)
.unwrap();
assert_eq!(xml(&xot, output), "<main/>");
}