""" Transclusion context management. Provides context objects that manage variables, paths, and state during transclusion processing. """ from pathlib import Path from typing import Dict, Any, Optional, Set, List class TransclusionContext: """ Context object for transclusion operations. Manages variables, paths, processing state, and circular reference detection during transclusion processing. """ def __init__(self, base_path: Optional[Path] = None, variables: Optional[Dict[str, Any]] = None, max_depth: int = 10): """ Initialize transclusion context. Args: base_path: Base path for relative file resolution variables: Initial variables for substitution max_depth: Maximum inclusion depth to prevent infinite recursion """ self.base_path = base_path or Path.cwd() self.variables = variables or {} self.max_depth = max_depth self.current_depth = 0 self.inclusion_stack: List[Path] = [] self.processed_files: Set[Path] = set() def enter_file(self, file_path: Path) -> None: """ Enter processing of a file. Args: file_path: Path of file being processed Raises: CircularReferenceError: If file creates circular reference DepthLimitError: If max depth exceeded """ from ..errors import CircularReferenceError, DepthLimitError # Check depth limit if self.current_depth >= self.max_depth: raise DepthLimitError(f"Maximum inclusion depth {self.max_depth} exceeded") # Check for circular references resolved_path = file_path.resolve() if resolved_path in self.inclusion_stack: cycle_start = self.inclusion_stack.index(resolved_path) cycle = self.inclusion_stack[cycle_start:] + [resolved_path] cycle_str = " -> ".join(str(p) for p in cycle) raise CircularReferenceError(f"Circular reference detected: {cycle_str}") # Enter file self.inclusion_stack.append(resolved_path) self.current_depth += 1 def exit_file(self, file_path: Path) -> None: """ Exit processing of a file. Args: file_path: Path of file being exited """ resolved_path = file_path.resolve() if self.inclusion_stack and self.inclusion_stack[-1] == resolved_path: self.inclusion_stack.pop() self.current_depth -= 1 self.processed_files.add(resolved_path) def resolve_path(self, path: str) -> Path: """ Resolve a path relative to the current base path. Args: path: Path to resolve Returns: Resolved Path object """ path_obj = Path(path) if path_obj.is_absolute(): return path_obj else: return self.base_path / path_obj def set_variable(self, name: str, value: Any) -> None: """ Set a variable in the context. Args: name: Variable name value: Variable value """ self.variables[name] = value def get_variable(self, name: str, default: Any = None) -> Any: """ Get a variable from the context. Args: name: Variable name default: Default value if variable not found Returns: Variable value or default """ return self.variables.get(name, default) def substitute_variables(self, text: str) -> str: """ Substitute variables in text using simple {{variable}} syntax. Args: text: Text containing variable references Returns: Text with variables substituted """ import re def replace_var(match): var_name = match.group(1).strip() return str(self.get_variable(var_name, match.group(0))) return re.sub(r'\{\{([^}]+)\}\}', replace_var, text) def create_child_context(self, new_base_path: Optional[Path] = None) -> 'TransclusionContext': """ Create a child context for nested processing. Args: new_base_path: New base path for the child context Returns: New TransclusionContext with inherited state """ child = TransclusionContext( base_path=new_base_path or self.base_path, variables=self.variables.copy(), max_depth=self.max_depth ) child.current_depth = self.current_depth child.inclusion_stack = self.inclusion_stack.copy() child.processed_files = self.processed_files.copy() return child