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.
- checksums.yaml +4 -4
- data/lib/poml/components/base.rb +45 -7
- data/lib/poml/components/data.rb +259 -0
- data/lib/poml/components/examples.rb +159 -13
- data/lib/poml/components/formatting.rb +148 -0
- data/lib/poml/components/media.rb +34 -0
- data/lib/poml/components/meta.rb +248 -0
- data/lib/poml/components/template.rb +334 -0
- data/lib/poml/components/utilities.rb +508 -0
- data/lib/poml/components.rb +91 -2
- data/lib/poml/context.rb +41 -2
- data/lib/poml/parser.rb +128 -15
- data/lib/poml/renderer.rb +26 -7
- data/lib/poml/template_engine.rb +101 -4
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +67 -1
- data/{README.md → readme.md} +9 -1
- metadata +8 -4
- data/examples/_generate_expects.py +0 -35
@@ -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
|