poml 0.0.1 → 0.0.2

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,334 @@
1
+ module Poml
2
+ # Conditional component for if-then logic
3
+ class IfComponent < Component
4
+ def render
5
+ apply_stylesheet
6
+
7
+ condition = get_attribute('condition')
8
+ return '' unless condition
9
+
10
+ # Evaluate the condition
11
+ if evaluate_condition(condition)
12
+ # Render child content
13
+ @element.children.map do |child|
14
+ Components.render_element(child, @context)
15
+ end.join('')
16
+ else
17
+ ''
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def evaluate_condition(condition)
24
+ # Handle both raw variable names and template expressions
25
+ condition = condition.strip
26
+
27
+ # First, substitute any template variables in the condition
28
+ substituted_condition = @context.template_engine.substitute(condition)
29
+
30
+ # If it's a template expression, it may have been pre-substituted
31
+ # If condition looks like a substituted value, try to parse it
32
+ case substituted_condition
33
+ when 'true'
34
+ true
35
+ when 'false'
36
+ false
37
+ when /^(.+?)\s*(>=|<=|==|!=|>|<)\s*(.+)$/
38
+ # Handle comparison operators
39
+ left_operand = $1.strip
40
+ operator = $2.strip
41
+ right_operand = $3.strip
42
+
43
+ # Convert operands to appropriate types
44
+ left_value = convert_operand(left_operand)
45
+ right_value = convert_operand(right_operand)
46
+
47
+ # Perform comparison
48
+ case operator
49
+ when '>'
50
+ left_value > right_value
51
+ when '<'
52
+ left_value < right_value
53
+ when '>='
54
+ left_value >= right_value
55
+ when '<='
56
+ left_value <= right_value
57
+ when '=='
58
+ left_value == right_value
59
+ when '!='
60
+ left_value != right_value
61
+ else
62
+ false
63
+ end
64
+ when /^{{(.+)}}$/
65
+ # Extract the variable name and evaluate it directly
66
+ var_name = $1.strip
67
+ result = @context.template_engine.evaluate_attribute_expression(var_name)
68
+ convert_to_boolean(result)
69
+ else
70
+ # Try to evaluate as a variable name
71
+ result = @context.template_engine.evaluate_attribute_expression(substituted_condition)
72
+ convert_to_boolean(result)
73
+ end
74
+ end
75
+
76
+ def convert_operand(operand)
77
+ # First substitute any template variables
78
+ substituted = @context.template_engine.substitute(operand)
79
+
80
+ # Try to convert to number if possible, otherwise keep as string
81
+ if substituted =~ /^-?\d+$/
82
+ substituted.to_i
83
+ elsif substituted =~ /^-?\d*\.\d+$/
84
+ substituted.to_f
85
+ else
86
+ substituted
87
+ end
88
+ end
89
+
90
+ def convert_to_boolean(result)
91
+ case result
92
+ when true, false
93
+ result
94
+ when nil, 0, '', []
95
+ false
96
+ when 'true'
97
+ true
98
+ when 'false'
99
+ false
100
+ else
101
+ true
102
+ end
103
+ end
104
+ end
105
+
106
+ # Loop component for iterating over arrays
107
+ class ForComponent < Component
108
+ def render
109
+ apply_stylesheet
110
+
111
+ variable = get_attribute('variable')
112
+ items_expr = get_attribute('items')
113
+
114
+ return '' unless variable && items_expr
115
+
116
+ # Evaluate the items expression
117
+ items = evaluate_items(items_expr)
118
+ return '' unless items.is_a?(Array)
119
+
120
+ # Store original content of elements to avoid mutation
121
+ original_contents = store_original_contents(@element)
122
+
123
+ # Render content for each item
124
+ results = []
125
+ items.each_with_index do |item, index|
126
+ # Restore original content before each iteration
127
+ restore_original_contents(@element, original_contents)
128
+
129
+ # Create child context with loop variable
130
+ child_context = @context.create_child_context
131
+ child_context.variables[variable] = item
132
+ child_context.variables['loop'] = { 'index' => index + 1 } # 1-based index
133
+
134
+ # Render children with loop variable substitution
135
+ item_content = @element.children.map do |child|
136
+ render_element_with_substitution(child, child_context)
137
+ end.join('')
138
+
139
+ results << item_content
140
+ end
141
+
142
+ results.join('')
143
+ end
144
+
145
+ private
146
+
147
+ def store_original_contents(element)
148
+ contents = {}
149
+ contents[element.object_id] = element.content.dup if element.content
150
+
151
+ element.children.each do |child|
152
+ contents.merge!(store_original_contents(child))
153
+ end
154
+
155
+ contents
156
+ end
157
+
158
+ def restore_original_contents(element, original_contents)
159
+ if original_contents[element.object_id]
160
+ element.content = original_contents[element.object_id].dup
161
+ end
162
+
163
+ element.children.each do |child|
164
+ restore_original_contents(child, original_contents)
165
+ end
166
+ end
167
+
168
+ private
169
+
170
+ def render_element_with_substitution(element, context)
171
+ # First substitute in the element's own content if it has template variables
172
+ if element.content && element.content.include?('{{')
173
+ element.content = context.template_engine.substitute(element.content)
174
+ end
175
+
176
+ # If this is a text element, return substituted content directly
177
+ if element.tag_name == :text
178
+ return element.content
179
+ end
180
+
181
+ # For non-text elements, recursively substitute in their children
182
+ substitute_in_element(element, context)
183
+ Components.render_element(element, context)
184
+ end
185
+
186
+ def substitute_in_element(element, context)
187
+ # Substitute in the element's own content
188
+ if element.content && element.content.include?('{{')
189
+ element.content = context.template_engine.substitute(element.content)
190
+ end
191
+
192
+ # Recursively substitute variables in children
193
+ element.children.each do |child|
194
+ if child.tag_name == :text && child.content.include?('{{')
195
+ child.content = context.template_engine.substitute(child.content)
196
+ else
197
+ substitute_in_element(child, context)
198
+ end
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ def evaluate_items(items_expr)
205
+ # Handle both raw variable names and template expressions
206
+ items_expr = items_expr.strip
207
+
208
+ case items_expr
209
+ when /^{{(.+)}}$/
210
+ # Extract the variable name and evaluate it directly
211
+ var_name = $1.strip
212
+ @context.template_engine.evaluate_attribute_expression(var_name)
213
+ else
214
+ # Try to evaluate as a variable name or expression
215
+ @context.template_engine.evaluate_attribute_expression(items_expr)
216
+ end
217
+ end
218
+ end
219
+
220
+ # Include component for including other POML files
221
+ class IncludeComponent < Component
222
+ def render
223
+ apply_stylesheet
224
+
225
+ src = get_attribute('src')
226
+ return '[Include: no src specified]' unless src
227
+
228
+ # Handle conditional and loop attributes
229
+ if_condition = @element.attributes['if']
230
+ for_attribute = @element.attributes['for']
231
+
232
+ # Check if condition
233
+ if if_condition && !evaluate_if_condition(if_condition)
234
+ return ''
235
+ end
236
+
237
+ # Handle for loop
238
+ if for_attribute
239
+ return render_with_for_loop(src, for_attribute)
240
+ end
241
+
242
+ # Simple include
243
+ include_file(src)
244
+ end
245
+
246
+ private
247
+
248
+ def include_file(src)
249
+ begin
250
+ # Resolve file path
251
+ file_path = if src.start_with?('/')
252
+ src
253
+ else
254
+ base_path = @context.source_path ? File.dirname(@context.source_path) : Dir.pwd
255
+ File.join(base_path, src)
256
+ end
257
+
258
+ unless File.exist?(file_path)
259
+ return "[Include: #{src} (not found)]"
260
+ end
261
+
262
+ # Read and parse the included file
263
+ included_content = File.read(file_path)
264
+
265
+ # Create a new parser context with current variables
266
+ included_context = @context.create_child_context
267
+ included_context.source_path = file_path
268
+
269
+ parser = Parser.new(included_context)
270
+ elements = parser.parse(included_content)
271
+
272
+ # Render the included elements
273
+ elements.map { |element| Components.render_element(element, included_context) }.join('')
274
+
275
+ rescue => e
276
+ "[Include: #{src} (error: #{e.message})]"
277
+ end
278
+ end
279
+
280
+ def render_with_for_loop(src, for_attribute)
281
+ # Parse for attribute like "i in [1,2,3]" or "item in items"
282
+ if for_attribute =~ /^(\w+)\s+in\s+(.+)$/
283
+ loop_var = $1
284
+ list_expr = $2.strip
285
+
286
+ # Evaluate the list expression
287
+ list = @context.template_engine.evaluate_attribute_expression(list_expr)
288
+ return '' unless list.is_a?(Array)
289
+
290
+ # Render for each item in the list
291
+ results = []
292
+ list.each_with_index do |item, index|
293
+ # Create loop context
294
+ old_loop_var = @context.variables[loop_var]
295
+ old_loop_context = @context.variables['loop']
296
+
297
+ @context.variables[loop_var] = item
298
+ @context.variables['loop'] = {
299
+ 'index' => index,
300
+ 'length' => list.length,
301
+ 'first' => index == 0,
302
+ 'last' => index == list.length - 1
303
+ }
304
+
305
+ # Include the file with current loop context
306
+ result = include_file(src)
307
+ results << result
308
+
309
+ # Restore previous context
310
+ if old_loop_var
311
+ @context.variables[loop_var] = old_loop_var
312
+ else
313
+ @context.variables.delete(loop_var)
314
+ end
315
+
316
+ if old_loop_context
317
+ @context.variables['loop'] = old_loop_context
318
+ else
319
+ @context.variables.delete('loop')
320
+ end
321
+ end
322
+
323
+ results.join('')
324
+ else
325
+ "[Include: invalid for syntax: #{for_attribute}]"
326
+ end
327
+ end
328
+
329
+ def evaluate_if_condition(condition)
330
+ value = @context.template_engine.evaluate_attribute_expression(condition)
331
+ !!value
332
+ end
333
+ end
334
+ end