#!/usr/bin/env python3.9 # -*- coding: utf-8 -*- """ JFinal Parser - Template Parser """ from typing import Optional from .ast.Stat import Stat, StatList from ..Env import Env class Parser: """Template parser for parsing template files""" def __init__(self, env: Env, content: str, name: Optional[str] = None): """ Initialize parser Args: env: Template environment content: Template content name: Template name (optional) """ self._env = env self._content = content self._name = name def parse(self) -> Stat: """ Parse template content Returns: Root statement """ from .ast.Text import Text from .ast.Output import Output from .ast.For import For stat_list = StatList() content = self._content length = len(content) pos = 0 while pos < length: # Find the next # character next_hash = content.find("#", pos) if next_hash == -1: # No more # characters, add the remaining text if pos < length: stat_list.add_stat(Text(content[pos:])) break # Add text before the # character if next_hash > pos: stat_list.add_stat(Text(content[pos:next_hash])) # Check what follows the # if next_hash + 1 < length: next_char = content[next_hash + 1] # Handle #( expression if next_char == "(": # Find the end of the expression end = self._find_matching_parenthesis(content, next_hash + 2) if end != -1: # Extract the expression content expr_content = content[next_hash + 2:end] # Create a simple expression list for evaluation expr_list = SimpleExprList(expr_content) # Add output stat stat_list.add_stat(Output(expr_list)) # Move to next position pos = end + 1 continue # Check for #for directive elif next_char == "f" and next_hash + 4 <= length and content[next_hash:next_hash + 4] == "#for": # Handle #for directive with optional parentheses and different syntax for_start = next_hash + 4 # Skip whitespace while for_start < length and content[for_start].isspace(): for_start += 1 # Check if #for is followed by ( if for_start < length and content[for_start] == "(": # Find the end of the parentheses for_paren_end = self._find_matching_parenthesis(content, for_start + 1) if for_paren_end != -1: # Parse for content inside parentheses for_content = content[for_start + 1:for_paren_end].strip() for_end = for_paren_end + 1 else: # No matching parenthesis, treat as text stat_list.add_stat(Text(content[next_hash:next_hash + 4])) pos = next_hash + 4 continue else: # Find the end of the #for line for_end = content.find("\n", next_hash) if for_end == -1: for_end = length # Parse for content for_content = content[for_start:for_end].strip() # Handle both "item in items" and "x : listAaa" syntax if " in " in for_content: # Python style: for item in items parts = for_content.split(" in ") var_name = parts[0].strip() iter_expr = parts[1].strip() elif " : " in for_content: # Java style: for x : listAaa parts = for_content.split(" : ") var_name = parts[0].strip() iter_expr = parts[1].strip() else: # Invalid for syntax, treat as text stat_list.add_stat(Text(content[next_hash:for_end])) pos = for_end continue # Find the matching #end directive end_pos = self._find_matching_end(content, for_end) if end_pos != -1: # Extract the loop body content body_content = content[for_end:end_pos] # Parse the loop body body_parser = Parser(self._env, body_content, self._name) body_stat = body_parser.parse() # Create For directive for_directive = For(var_name, iter_expr, body_stat) # Add for directive to stat list stat_list.add_stat(for_directive) # Move to position after #end directive pos = end_pos + 4 # Skip "#end" continue # If we get here, it's just a # character, add as text stat_list.add_stat(Text("#")) pos = next_hash + 1 return stat_list def _find_matching_end(self, content: str, start: int) -> int: """ Find matching #end directive Args: content: Content string start: Start position Returns: Position of matching #end directive or -1 if not found """ pos = start length = len(content) depth = 1 # Start with 1 for the current #for while pos < length: # Find the next # character next_hash = content.find("#", pos) if next_hash == -1: break # Check for #for (increase depth) if next_hash + 5 <= length and content[next_hash:next_hash + 5] == "#for ": depth += 1 pos = next_hash + 5 # Check for #end (decrease depth) elif next_hash + 4 <= length and content[next_hash:next_hash + 4] == "#end": depth -= 1 if depth == 0: return next_hash pos = next_hash + 4 # Otherwise, move past this # else: pos = next_hash + 1 return -1 def _find_matching_parenthesis(self, content: str, start: int) -> int: """ Find matching parenthesis Args: content: Content string start: Start position Returns: Position of matching parenthesis or -1 if not found """ count = 1 pos = start length = len(content) while pos < length and count > 0: char = content[pos] if char == '(': count += 1 elif char == ')': count -= 1 if count == 0: return pos pos += 1 return -1 class SimpleExprList: """ Simple expression list implementation for evaluating template expressions """ def __init__(self, expr_str: str): """ Initialize simple expression list Args: expr_str: Expression string """ self._expr_str = expr_str def eval(self, scope: 'Scope') -> any: """ Evaluate expression in the given scope Args: scope: Execution scope Returns: Evaluation result """ try: # Create a local context with scope variables local_vars = scope._data.copy() # Create a special dict wrapper that allows attribute access def wrap_dict(d): """Wrap a dict to allow attribute access""" if isinstance(d, dict): # Create a wrapper that allows both dot access and bracket access class DictWrapper: def __init__(self, data): self.__dict__ = data def __getitem__(self, key): return data[key] return DictWrapper(d) return d # Wrap all dict values in the local vars wrapped_vars = {} for key, value in local_vars.items(): if isinstance(value, dict): wrapped_vars[key] = wrap_dict(value) elif isinstance(value, list): # Wrap dictionaries in lists wrapped_vars[key] = [wrap_dict(item) for item in value] else: wrapped_vars[key] = value # Handle special case for 'for' variable which is a keyword in Python expr_str = self._expr_str if 'for' in wrapped_vars: # Create a wrapper to avoid keyword conflict wrapped_vars['_for'] = wrapped_vars['for'] # Replace 'for.' with '_for.' in the expression expr_str = expr_str.replace('for.', '_for.') # Remove newline characters from expression expr_str = expr_str.replace('\n', '').replace('\r', '') # Split expression by | to handle filters expr_parts = expr_str.split('|') main_expr = expr_parts[0].strip() # Evaluate main expression first if '??' in main_expr: # Custom evaluation for expressions with ?? operator def evaluate_null_coalescing(expr): if '??' not in expr: try: return eval(expr, {}, wrapped_vars) except (NameError, AttributeError): return None # Process from right to left for nested ?? idx = expr.rfind('??') # Find left expression left_expr = expr[:idx].strip() right_expr = expr[idx+2:].strip() # Evaluate left part left_val = evaluate_null_coalescing(left_expr) if left_val is not None: return left_val else: # Evaluate right part return evaluate_null_coalescing(right_expr) result = evaluate_null_coalescing(main_expr) else: # Regular evaluation for expressions without ?? try: result = eval(main_expr, {}, wrapped_vars) except (NameError, AttributeError): result = None # Apply filters for filter_part in expr_parts[1:]: filter_part = filter_part.strip() if not filter_part: continue # Parse filter name and arguments if '(' in filter_part: filter_name = filter_part[:filter_part.find('(')].strip() # Extract arguments args_str = filter_part[filter_part.find('(')+1:filter_part.rfind(')')].strip() # Simple argument parsing - split by commas, ignoring commas inside quotes args = [] current_arg = '' in_quotes = False quote_char = '' for char in args_str: if char in "'\"" and (not in_quotes or char == quote_char): if not in_quotes: in_quotes = True quote_char = char else: in_quotes = False current_arg += char elif char == ',' and not in_quotes: args.append(current_arg.strip()) current_arg = '' else: current_arg += char if current_arg: args.append(current_arg.strip()) else: filter_name = filter_part.strip() args = [] # Apply filter if filter_name == 'join': # Join filter implementation if len(args) > 0: # Remove quotes from separator if present sep = args[0] if (sep.startswith('"') and sep.endswith('"')) or (sep.startswith("'") and sep.endswith("'")): sep = sep[1:-1] else: sep = '' if isinstance(result, (list, tuple)): result = sep.join(str(item) for item in result) else: result = str(result) elif filter_name == 'upper': # Upper case filter result = str(result).upper() elif filter_name == 'lower': # Lower case filter result = str(result).lower() elif filter_name == 'strip': # Strip whitespace filter result = str(result).strip() return result except Exception as e: # Handle evaluation errors gracefully return f"Error evaluating expression '{self._expr_str}': {e}"