384 lines
15 KiB
Python
384 lines
15 KiB
Python
#!/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}"
|
|
|
|
|