lema-basic-web-backend 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ from collections.abc import Mapping
2
+ from html import escape
3
+
4
+ from ..exceptions import TemplateRenderError, UndefinedVariableError
5
+ from .nodes import TextNode, VariableNode, IfNode, ForNode
6
+
7
+ _MISSING = object()
8
+
9
+ def render(nodes, context, autoescape=True, template_name=None):
10
+ if context is None:
11
+ context = {}
12
+ else:
13
+ context = dict(context)
14
+
15
+ return _render_nodes(nodes=nodes, context=context, autoescape=autoescape, template_name=template_name)
16
+
17
+ def _render_nodes(nodes, context, autoescape, template_name):
18
+ output_parts = []
19
+
20
+ for node in nodes:
21
+ if isinstance(node, TextNode):
22
+ output_parts.append(node.content)
23
+ continue
24
+
25
+ if isinstance(node, VariableNode):
26
+ value = _resolve_variable(path=node.path, context=context)
27
+
28
+ rendered_value = str(value)
29
+ if autoescape:
30
+ rendered_value = escape(rendered_value, quote=True)
31
+
32
+ output_parts.append(rendered_value)
33
+ continue
34
+
35
+ if isinstance(node, IfNode):
36
+ condition_value = _resolve_variable(path=node.condition, context=context)
37
+
38
+ if condition_value:
39
+ selected_nodes = node.body
40
+ else:
41
+ selected_nodes = node.else_body
42
+
43
+ rendered_branch = _render_nodes(
44
+ nodes=selected_nodes,
45
+ context=context,
46
+ autoescape=autoescape,
47
+ template_name=template_name
48
+ )
49
+
50
+ output_parts.append(rendered_branch)
51
+ continue
52
+
53
+ if isinstance(node, ForNode):
54
+ iterable_value = _resolve_variable(path=node.iterable, context=context)
55
+
56
+ try:
57
+ iterator = iter(iterable_value)
58
+ except TypeError as error:
59
+ iterable_name = '.'.join(node.iterable)
60
+ raise TemplateRenderError(
61
+ f"Template value is not iterable: {iterable_name}") from error
62
+
63
+ for item in iterator:
64
+ local_context = dict(context)
65
+
66
+ local_context[node.variable_name] = item
67
+ rendered_item = _render_nodes(
68
+ nodes=node.body,
69
+ context=local_context,
70
+ autoescape=autoescape,
71
+ template_name=template_name
72
+ )
73
+ output_parts.append(rendered_item)
74
+ continue
75
+
76
+ raise TemplateRenderError(
77
+ f"Unsupported node type: {type(node).__name__}")
78
+
79
+ return ''.join(output_parts)
80
+
81
+ def _resolve_variable(path, context):
82
+ variable_name = '.'.join(path)
83
+
84
+ if not path:
85
+ raise UndefinedVariableError(variable_name="")
86
+
87
+ first_name = path[0]
88
+
89
+ if first_name not in context:
90
+ raise UndefinedVariableError(variable_name=variable_name)
91
+
92
+ value = context[first_name]
93
+
94
+ for name in path[1:]:
95
+ value = _resolve_part(
96
+ value=value,
97
+ name=name,
98
+ variable_name=variable_name
99
+ )
100
+
101
+ return value
102
+
103
+ def _resolve_part(value, name, variable_name):
104
+ if isinstance(value, Mapping):
105
+ if name not in value:
106
+ raise UndefinedVariableError(variable_name=variable_name)
107
+ return value[name]
108
+
109
+ attribute = getattr(value, name, _MISSING)
110
+
111
+ if attribute is _MISSING:
112
+ raise UndefinedVariableError(variable_name=variable_name)
113
+
114
+ return attribute
@@ -0,0 +1,127 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from ..exceptions import TemplateSyntaxError, TemplateSyntaxError
5
+
6
+ class TokenType(Enum):
7
+ TEXT = "TEXT"
8
+ VARIABLE = "VARIABLE"
9
+ STATEMENT = "STATEMENT"
10
+
11
+ @dataclass
12
+ class Token:
13
+ type: TokenType
14
+ value: str
15
+ line: int
16
+ column: int
17
+
18
+ OPENING_DELIMITER = {
19
+ "{{": ("}}", TokenType.VARIABLE, "Unclosed variable expression."),
20
+ "{%": ("%}", TokenType.STATEMENT, "Unclosed statement block."),
21
+ "{#": ("#}", None, "Unclosed template comment.")
22
+ }
23
+
24
+ def tokenize(source, template_name=None):
25
+ tokens = []
26
+
27
+ position = 0
28
+ line = 1
29
+ column = 1
30
+
31
+ while position < len(source):
32
+ opening_position, opening = (
33
+ _find_next_opening(source, position)
34
+ )
35
+ if opening is None:
36
+ remaining_text = source[position:]
37
+
38
+ if remaining_text:
39
+ tokens.append(
40
+ Token(
41
+ type=TokenType.TEXT,
42
+ value=remaining_text,
43
+ line=line,
44
+ column=column
45
+ )
46
+ )
47
+ break
48
+
49
+ if opening_position > position:
50
+ text = source[position:opening_position]
51
+ tokens.append(
52
+ Token(
53
+ type=TokenType.TEXT,
54
+ value=text,
55
+ line=line,
56
+ column=column
57
+ )
58
+ )
59
+ line, column = _advance_position(text, line, column)
60
+
61
+ position = opening_position
62
+
63
+ token_line = line
64
+ token_column = column
65
+
66
+ closing, token_type, error_message = OPENING_DELIMITER[opening]
67
+ closing_position = source.find(closing, position + len(opening))
68
+
69
+ if closing_position == -1:
70
+ raise TemplateSyntaxError(
71
+ message=error_message,
72
+ template_name=template_name,
73
+ line=token_line,
74
+ column=token_column
75
+ )
76
+
77
+ content_start = position + len(opening)
78
+ content = source[content_start:closing_position]
79
+
80
+ end_position = closing_position + len(closing)
81
+
82
+ complete_section = source[position:end_position]
83
+
84
+ if token_type is not None:
85
+ tokens.append(
86
+ Token(
87
+ type=token_type,
88
+ value=content.strip(),
89
+ line=token_line,
90
+ column=token_column
91
+ )
92
+ )
93
+
94
+ line, column = _advance_position(text=complete_section, line=line, column=column)
95
+ position = end_position
96
+
97
+ return tokens
98
+
99
+ def _find_next_opening(source, start):
100
+ next_position = None
101
+ next_opening = None
102
+
103
+ for opening in OPENING_DELIMITER:
104
+ position = source.find(opening, start)
105
+
106
+ if position == -1:
107
+ continue
108
+
109
+ if next_position is None or position < next_position:
110
+ next_position = position
111
+ next_opening = opening
112
+
113
+ return next_position, next_opening
114
+
115
+ def _advance_position(text, line, column):
116
+ newline_count = text.count("\n")
117
+
118
+ if newline_count == 0:
119
+ return line, column + len(text)
120
+
121
+ line += newline_count
122
+
123
+ text_after_last_newline = text.rsplit("\n", maxsplit=1)[1]
124
+ column = len(text_after_last_newline) + 1
125
+
126
+ return line, column
127
+
@@ -0,0 +1,34 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ class TemplateNode:
4
+ """Base class for template syntax nodes."""
5
+ pass
6
+
7
+ @dataclass
8
+ class TextNode(TemplateNode):
9
+ content: str
10
+ line: int = 1
11
+ column: int = 1
12
+
13
+ @dataclass
14
+ class VariableNode(TemplateNode):
15
+ path: list[str]
16
+ line: int = 1
17
+ column: int = 1
18
+
19
+ @dataclass
20
+ class IfNode(TemplateNode):
21
+ condition: list[str]
22
+ body: list[TemplateNode] = field(default_factory=list)
23
+ else_body: list[TemplateNode] = field(default_factory=list)
24
+ line: int = 1
25
+ column: int = 1
26
+
27
+ @dataclass
28
+ class ForNode(TemplateNode):
29
+ variable_name: str
30
+ iterable: list[str]
31
+ body: list[TemplateNode] = field(default_factory=list)
32
+ line: int = 1
33
+ column: int = 1
34
+
@@ -0,0 +1,221 @@
1
+ import re
2
+
3
+ from ..exceptions import TemplateSyntaxError
4
+ from .lexer import TokenType
5
+ from .nodes import TextNode, VariableNode, IfNode, ForNode
6
+
7
+ VARIABLE_PATH_PATTERN = re.compile(
8
+ r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*"
9
+ )
10
+
11
+ FOR_STATEMENT_PATTERN = re.compile(
12
+ r"for\s+(?P<variable>[A-Za-z_]\w*)"
13
+ r"\s+in\s+(?P<iterable>.+)"
14
+ )
15
+
16
+ def parse(tokens, template_name=None):
17
+ parser = Parser(tokens, template_name)
18
+ return parser.parse()
19
+
20
+ class Parser:
21
+ def __init__(self, tokens, template_name=None):
22
+ self.tokens = list(tokens)
23
+ self.template_name = template_name
24
+ self.position = 0
25
+
26
+ def parse(self):
27
+ nodes, closing_statment = self._parse_nodes(stop_statements=set())
28
+
29
+ if closing_statment is not None:
30
+ token = self.tokens[self.position]
31
+
32
+ self._raise_unexpected_statment(token)
33
+
34
+ return nodes
35
+
36
+ def _parse_nodes(self, stop_statements):
37
+ nodes = []
38
+
39
+ while self.position < len(self.tokens):
40
+ token = self.tokens[self.position]
41
+
42
+ if token.type == TokenType.TEXT:
43
+ nodes.append(
44
+ TextNode(
45
+ content=token.value,
46
+ line=token.line,
47
+ column=token.column
48
+ )
49
+ )
50
+ self.position += 1
51
+ continue
52
+
53
+ if token.type == TokenType.VARIABLE:
54
+ nodes.append(self._parse_variable(token))
55
+ self.position += 1
56
+ continue
57
+
58
+ if token.type == TokenType.STATEMENT:
59
+ statement = token.value
60
+
61
+ if statement in stop_statements:
62
+ return nodes, statement
63
+
64
+ keyword = self._get_keyword(statement)
65
+
66
+ if keyword == "if":
67
+ nodes.append(self._parse_if(opening_token=token))
68
+ continue
69
+
70
+ if keyword == "for":
71
+ nodes.append(self._parse_for(opening_token=token))
72
+ continue
73
+
74
+ if statement in {"endif", "else", "endfor"}:
75
+ self._raise_unexpected_statment(token)
76
+
77
+ self._raise_syntax_error(
78
+ message=f"Unknown template statement: {statement}",
79
+ token=token
80
+ )
81
+
82
+ self._raise_syntax_error(
83
+ message=f"Unkonwn template token type: {token.type}",
84
+ token=token
85
+ )
86
+
87
+ return nodes, None
88
+
89
+ def _parse_variable(self, token):
90
+ path = self._parse_variable_path(expression=token.value, token=token, empty_message="Variable expression cannot be empty.")
91
+
92
+ return VariableNode(
93
+ path=path,
94
+ line=token.line,
95
+ column=token.column
96
+ )
97
+
98
+ def _parse_if(self, opening_token):
99
+ parts = opening_token.value.split(maxsplit=1)
100
+
101
+ if len(parts) == 1:
102
+ self._raise_syntax_error(
103
+ message="If statement requires a condition.",
104
+ token=opening_token
105
+ )
106
+
107
+ condition_expression = parts[1].strip()
108
+
109
+ condition = self._parse_variable_path(
110
+ expression=condition_expression,
111
+ token=opening_token,
112
+ empty_message="If statement requires a condition."
113
+ )
114
+
115
+ self.position += 1
116
+
117
+ body, closing_statement = self._parse_nodes(stop_statements={"else", "endif"})
118
+
119
+ if closing_statement is None:
120
+ self._raise_syntax_error(
121
+ message="Missing endif statement.",
122
+ token=opening_token
123
+ )
124
+
125
+ else_body = []
126
+
127
+ if closing_statement == "else":
128
+ self.position += 1
129
+
130
+ else_body, closing_statement = self._parse_nodes(stop_statements={"endif"})
131
+
132
+ if closing_statement is None:
133
+ self._raise_syntax_error(
134
+ message="Missing endif statement.",
135
+ token=opening_token
136
+ )
137
+
138
+ self.position += 1
139
+
140
+ return IfNode(
141
+ condition=condition,
142
+ body=body,
143
+ else_body=else_body,
144
+ line=opening_token.line,
145
+ column=opening_token.column
146
+ )
147
+
148
+ def _parse_variable_path(self, expression, token, empty_message):
149
+ if not expression:
150
+ self._raise_syntax_error(
151
+ message=empty_message,
152
+ token=token
153
+ )
154
+
155
+ if VARIABLE_PATH_PATTERN.fullmatch(expression) is None:
156
+ self._raise_syntax_error(
157
+ message=f"Invalid variable expression: {expression}",
158
+ token=token
159
+ )
160
+
161
+ return expression.split(".")
162
+
163
+ def _get_keyword(self, statement):
164
+ parts = statement.split(maxsplit=1)
165
+ if not parts:
166
+ return ""
167
+
168
+ return parts[0]
169
+
170
+ def _raise_unexpected_statment(self, token):
171
+ self._raise_syntax_error(
172
+ message=f"Unexpected {token.value} statement.",
173
+ token=token
174
+ )
175
+
176
+ def _raise_syntax_error(self, message, token):
177
+ raise TemplateSyntaxError(
178
+ message=message,
179
+ template_name=self.template_name,
180
+ line=token.line,
181
+ column=token.column
182
+ )
183
+
184
+ def _parse_for(self, opening_token):
185
+ match = FOR_STATEMENT_PATTERN.fullmatch(opening_token.value)
186
+
187
+ if match is None:
188
+ self._raise_syntax_error(
189
+ message=f"Invalid for statement. Expected format: 'for <variable> in <iterable>'.",
190
+ token=opening_token
191
+ )
192
+
193
+ variable_name = match.group("variable")
194
+
195
+ iterable_expression = match.group("iterable").strip()
196
+
197
+ iterable = self._parse_variable_path(
198
+ expression=iterable_expression,
199
+ token=opening_token,
200
+ empty_message="Invalid for statement. Expected format: 'for <variable> in <iterable>'."
201
+ )
202
+
203
+ self.position += 1
204
+
205
+ body, closing_statement = self._parse_nodes(stop_statements={"endfor"})
206
+
207
+ if closing_statement is None:
208
+ self._raise_syntax_error(
209
+ message="Missing endfor statement.",
210
+ token=opening_token
211
+ )
212
+
213
+ self.position += 1
214
+
215
+ return ForNode(
216
+ variable_name=variable_name,
217
+ iterable=iterable,
218
+ body=body,
219
+ line=opening_token.line,
220
+ column=opening_token.column
221
+ )