poml 0.0.1
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +239 -0
- data/TUTORIAL.md +987 -0
- data/bin/poml +80 -0
- data/examples/101_explain_character.poml +30 -0
- data/examples/102_render_xml.poml +40 -0
- data/examples/103_word_todos.poml +27 -0
- data/examples/104_financial_analysis.poml +33 -0
- data/examples/105_write_blog_post.poml +48 -0
- data/examples/106_research.poml +36 -0
- data/examples/107_read_report_pdf.poml +4 -0
- data/examples/201_orders_qa.poml +50 -0
- data/examples/202_arc_agi.poml +36 -0
- data/examples/301_generate_poml.poml +46 -0
- data/examples/README.md +50 -0
- data/examples/_generate_expects.py +35 -0
- data/examples/assets/101_jerry_mouse.jpg +0 -0
- data/examples/assets/101_tom_and_jerry.docx +0 -0
- data/examples/assets/101_tom_cat.jpg +0 -0
- data/examples/assets/101_tom_introduction.txt +9 -0
- data/examples/assets/103_prompt_wizard.docx +0 -0
- data/examples/assets/104_chart_normalized_price.png +0 -0
- data/examples/assets/104_chart_price.png +0 -0
- data/examples/assets/104_mag7.xlsx +0 -0
- data/examples/assets/107_usenix_paper.pdf +0 -0
- data/examples/assets/201_order_instructions.json +7 -0
- data/examples/assets/201_orderlines.csv +2 -0
- data/examples/assets/201_orders.csv +3 -0
- data/examples/assets/202_arc_agi_data.json +1 -0
- data/examples/expects/101_explain_character.txt +117 -0
- data/examples/expects/102_render_xml.txt +28 -0
- data/examples/expects/103_word_todos.txt +121 -0
- data/examples/expects/104_financial_analysis.txt +86 -0
- data/examples/expects/105_write_blog_post.txt +41 -0
- data/examples/expects/106_research.txt +29 -0
- data/examples/expects/107_read_report_pdf.txt +151 -0
- data/examples/expects/201_orders_qa.txt +44 -0
- data/examples/expects/202_arc_agi.txt +64 -0
- data/examples/expects/301_generate_poml.txt +153 -0
- data/examples/ruby_expects/101_explain_character.txt +17 -0
- data/examples/ruby_expects/102_render_xml.txt +28 -0
- data/examples/ruby_expects/103_word_todos.txt +14 -0
- data/examples/ruby_expects/104_financial_analysis.txt +0 -0
- data/examples/ruby_expects/105_write_blog_post.txt +57 -0
- data/examples/ruby_expects/106_research.txt +5 -0
- data/examples/ruby_expects/107_read_report_pdf.txt +403 -0
- data/examples/ruby_expects/201_orders_qa.txt +41 -0
- data/examples/ruby_expects/202_arc_agi.txt +17 -0
- data/examples/ruby_expects/301_generate_poml.txt +17 -0
- data/lib/poml/components/base.rb +132 -0
- data/lib/poml/components/content.rb +156 -0
- data/lib/poml/components/data.rb +346 -0
- data/lib/poml/components/examples.rb +55 -0
- data/lib/poml/components/instructions.rb +93 -0
- data/lib/poml/components/layout.rb +50 -0
- data/lib/poml/components/lists.rb +82 -0
- data/lib/poml/components/styling.rb +36 -0
- data/lib/poml/components/text.rb +8 -0
- data/lib/poml/components/workflow.rb +63 -0
- data/lib/poml/components.rb +47 -0
- data/lib/poml/components_new.rb +297 -0
- data/lib/poml/components_old.rb +1096 -0
- data/lib/poml/context.rb +53 -0
- data/lib/poml/parser.rb +153 -0
- data/lib/poml/renderer.rb +147 -0
- data/lib/poml/template_engine.rb +66 -0
- data/lib/poml/version.rb +5 -0
- data/lib/poml.rb +53 -0
- data/media/logo-16-purple.png +0 -0
- data/media/logo-64-white.png +0 -0
- metadata +149 -0
@@ -0,0 +1,82 @@
|
|
1
|
+
module Poml
|
2
|
+
# List component
|
3
|
+
class ListComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
if xml_mode?
|
8
|
+
# In XML mode, lists don't exist - items are rendered directly
|
9
|
+
@element.children.map do |child|
|
10
|
+
if child.tag_name == :item
|
11
|
+
Components.render_element(child, @context)
|
12
|
+
end
|
13
|
+
end.compact.join('')
|
14
|
+
else
|
15
|
+
list_style = get_attribute('listStyle', 'dash')
|
16
|
+
items = []
|
17
|
+
index = 0
|
18
|
+
|
19
|
+
@element.children.each do |child|
|
20
|
+
if child.tag_name == :item
|
21
|
+
index += 1
|
22
|
+
|
23
|
+
bullet = case list_style
|
24
|
+
when 'decimal', 'number', 'numbered'
|
25
|
+
"#{index}. "
|
26
|
+
when 'star'
|
27
|
+
"* "
|
28
|
+
when 'plus'
|
29
|
+
"+ "
|
30
|
+
when 'dash', 'bullet', 'unordered'
|
31
|
+
"- "
|
32
|
+
else
|
33
|
+
"- "
|
34
|
+
end
|
35
|
+
|
36
|
+
# Get text content and nested elements separately
|
37
|
+
text_content = child.content.strip
|
38
|
+
nested_elements = child.children.reject { |c| c.tag_name == :text }
|
39
|
+
|
40
|
+
if nested_elements.any?
|
41
|
+
# Item has both text and nested elements (like nested lists)
|
42
|
+
nested_content = nested_elements.map { |nested_child|
|
43
|
+
Components.render_element(nested_child, @context)
|
44
|
+
}.join('').strip
|
45
|
+
|
46
|
+
# Format with text content on first line, nested content indented
|
47
|
+
indented_nested = nested_content.split("\n").map { |line|
|
48
|
+
line.strip.empty? ? "" : " #{line}"
|
49
|
+
}.join("\n").strip
|
50
|
+
|
51
|
+
if text_content.empty?
|
52
|
+
items << "#{bullet}#{indented_nested}"
|
53
|
+
else
|
54
|
+
items << "#{bullet}#{text_content} \n\n#{indented_nested}"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
# Simple text-only item
|
58
|
+
items << "#{bullet}#{text_content}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
return "\n\n" if items.empty?
|
64
|
+
items.join("\n") + "\n\n"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Item component (for list items)
|
70
|
+
class ItemComponent < Component
|
71
|
+
def render
|
72
|
+
apply_stylesheet
|
73
|
+
content = @element.content.empty? ? render_children : @element.content.strip
|
74
|
+
|
75
|
+
if xml_mode?
|
76
|
+
"<item>#{content}</item>\n"
|
77
|
+
else
|
78
|
+
content
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Poml
|
2
|
+
# Let component (for variable definitions)
|
3
|
+
class LetComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
name = get_attribute('name')
|
8
|
+
value = @element.content.empty? ? render_children : @element.content
|
9
|
+
|
10
|
+
# Add to context variables
|
11
|
+
@context.variables[name] = value if name
|
12
|
+
|
13
|
+
# Let components produce no output
|
14
|
+
''
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Stylesheet component
|
19
|
+
class StylesheetComponent < Component
|
20
|
+
def render
|
21
|
+
# Parse and apply stylesheet
|
22
|
+
begin
|
23
|
+
stylesheet_content = @element.content.strip
|
24
|
+
if stylesheet_content.start_with?('{') && stylesheet_content.end_with?('}')
|
25
|
+
stylesheet = JSON.parse(stylesheet_content)
|
26
|
+
@context.stylesheet.merge!(stylesheet) if stylesheet.is_a?(Hash)
|
27
|
+
end
|
28
|
+
rescue => e
|
29
|
+
# Silently fail JSON parsing errors
|
30
|
+
end
|
31
|
+
|
32
|
+
# Stylesheet components don't produce output
|
33
|
+
''
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Poml
|
2
|
+
# Stepwise Instructions component
|
3
|
+
class StepwiseInstructionsComponent < Component
|
4
|
+
def render
|
5
|
+
apply_stylesheet
|
6
|
+
|
7
|
+
content = @element.content.empty? ? render_children : @element.content
|
8
|
+
|
9
|
+
if xml_mode?
|
10
|
+
render_as_xml('stepwise-instructions', content)
|
11
|
+
else
|
12
|
+
caption = apply_text_transform(get_attribute('caption', 'Stepwise Instructions'))
|
13
|
+
caption_style = get_attribute('captionStyle', 'header')
|
14
|
+
|
15
|
+
case caption_style
|
16
|
+
when 'header'
|
17
|
+
"# #{caption}\n\n#{content}\n\n"
|
18
|
+
when 'bold'
|
19
|
+
"**#{caption}:** #{content}\n\n"
|
20
|
+
when 'plain'
|
21
|
+
"#{caption}: #{content}\n\n"
|
22
|
+
when 'hidden'
|
23
|
+
"#{content}\n\n"
|
24
|
+
else
|
25
|
+
"# #{caption}\n\n#{content}\n\n"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Human Message component
|
32
|
+
class HumanMessageComponent < Component
|
33
|
+
def render
|
34
|
+
apply_stylesheet
|
35
|
+
|
36
|
+
content = @element.content.empty? ? render_children : @element.content
|
37
|
+
speaker = get_attribute('speaker', 'human')
|
38
|
+
|
39
|
+
if xml_mode?
|
40
|
+
render_as_xml('human-message', content)
|
41
|
+
else
|
42
|
+
"#{content}\n\n"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Question-Answer component
|
48
|
+
class QAComponent < Component
|
49
|
+
def render
|
50
|
+
apply_stylesheet
|
51
|
+
|
52
|
+
content = @element.content.empty? ? render_children : @element.content
|
53
|
+
question_caption = get_attribute('questionCaption', 'Question')
|
54
|
+
answer_caption = get_attribute('answerCaption', 'Answer')
|
55
|
+
|
56
|
+
if xml_mode?
|
57
|
+
render_as_xml('qa', content)
|
58
|
+
else
|
59
|
+
"**#{question_caption}:** #{content}\n\n**#{answer_caption}:**"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# Main components file - requires all component modules
|
2
|
+
require_relative 'components/base'
|
3
|
+
require_relative 'components/text'
|
4
|
+
require_relative 'components/instructions'
|
5
|
+
require_relative 'components/content'
|
6
|
+
require_relative 'components/data'
|
7
|
+
require_relative 'components/examples'
|
8
|
+
require_relative 'components/lists'
|
9
|
+
require_relative 'components/layout'
|
10
|
+
require_relative 'components/workflow'
|
11
|
+
require_relative 'components/styling'
|
12
|
+
|
13
|
+
module Poml
|
14
|
+
# Update the component mapping after all components are loaded
|
15
|
+
Components::COMPONENT_MAPPING.merge!({
|
16
|
+
text: TextComponent,
|
17
|
+
role: RoleComponent,
|
18
|
+
task: TaskComponent,
|
19
|
+
hint: HintComponent,
|
20
|
+
document: DocumentComponent,
|
21
|
+
Document: DocumentComponent, # Capitalized version
|
22
|
+
table: TableComponent,
|
23
|
+
Table: TableComponent, # Capitalized version
|
24
|
+
img: ImageComponent,
|
25
|
+
p: ParagraphComponent,
|
26
|
+
example: ExampleComponent,
|
27
|
+
input: InputComponent,
|
28
|
+
output: OutputComponent,
|
29
|
+
'output-format': OutputFormatComponent,
|
30
|
+
'outputformat': OutputFormatComponent,
|
31
|
+
list: ListComponent,
|
32
|
+
item: ItemComponent,
|
33
|
+
cp: CPComponent,
|
34
|
+
'stepwise-instructions': StepwiseInstructionsComponent,
|
35
|
+
'stepwiseinstructions': StepwiseInstructionsComponent,
|
36
|
+
StepwiseInstructions: StepwiseInstructionsComponent,
|
37
|
+
'human-message': HumanMessageComponent,
|
38
|
+
'humanmessage': HumanMessageComponent,
|
39
|
+
HumanMessage: HumanMessageComponent,
|
40
|
+
qa: QAComponent,
|
41
|
+
QA: QAComponent,
|
42
|
+
let: LetComponent,
|
43
|
+
Let: LetComponent,
|
44
|
+
stylesheet: StylesheetComponent,
|
45
|
+
Stylesheet: StylesheetComponent
|
46
|
+
})
|
47
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
# Main components file - requires all component modules
|
2
|
+
require_relative 'components/base'
|
3
|
+
require_relative 'components/text'
|
4
|
+
require_relative 'components/instructions'
|
5
|
+
require_relative 'components/content'
|
6
|
+
require_relative 'components/data'
|
7
|
+
|
8
|
+
# TODO: Create remaining component files:
|
9
|
+
# require_relative 'components/examples' # ExampleComponent, InputComponent, OutputComponent, OutputFormatComponent
|
10
|
+
# require_relative 'components/lists' # ListComponent, ItemComponent
|
11
|
+
# require_relative 'components/layout' # CPComponent (CaptionedParagraph)
|
12
|
+
# require_relative 'components/workflow' # StepwiseInstructionsComponent, HumanMessageComponent, QAComponent
|
13
|
+
# require_relative 'components/styling' # StylesheetComponent, LetComponent
|
14
|
+
|
15
|
+
# Temporary: Keep remaining components inline until they are split out
|
16
|
+
module Poml
|
17
|
+
# Example component
|
18
|
+
class ExampleComponent < Component
|
19
|
+
def render
|
20
|
+
apply_stylesheet
|
21
|
+
|
22
|
+
content = @element.content.empty? ? render_children : @element.content
|
23
|
+
"#{content}\n\n"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Input component (for examples)
|
28
|
+
class InputComponent < Component
|
29
|
+
def render
|
30
|
+
apply_stylesheet
|
31
|
+
|
32
|
+
content = @element.content.empty? ? render_children : @element.content
|
33
|
+
"#{content}\n\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Output component (for examples)
|
38
|
+
class OutputComponent < Component
|
39
|
+
def render
|
40
|
+
apply_stylesheet
|
41
|
+
|
42
|
+
content = @element.content.empty? ? render_children : @element.content
|
43
|
+
"#{content}\n\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Output format component
|
48
|
+
class OutputFormatComponent < Component
|
49
|
+
def render
|
50
|
+
apply_stylesheet
|
51
|
+
|
52
|
+
caption = get_attribute('caption', 'Output Format')
|
53
|
+
caption_style = get_attribute('captionStyle', 'header')
|
54
|
+
content = @element.content.empty? ? render_children : @element.content
|
55
|
+
|
56
|
+
case caption_style
|
57
|
+
when 'header'
|
58
|
+
"# #{caption}\n\n#{content}\n\n"
|
59
|
+
when 'bold'
|
60
|
+
"**#{caption}:** #{content}\n\n"
|
61
|
+
when 'plain'
|
62
|
+
"#{caption}: #{content}\n\n"
|
63
|
+
when 'hidden'
|
64
|
+
"#{content}\n\n"
|
65
|
+
else
|
66
|
+
"# #{caption}\n\n#{content}\n\n"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# List component
|
72
|
+
class ListComponent < Component
|
73
|
+
def render
|
74
|
+
apply_stylesheet
|
75
|
+
|
76
|
+
if xml_mode?
|
77
|
+
# In XML mode, lists don't exist - items are rendered directly
|
78
|
+
@element.children.map do |child|
|
79
|
+
if child.tag_name == :item
|
80
|
+
Components.render_element(child, @context)
|
81
|
+
end
|
82
|
+
end.compact.join('')
|
83
|
+
else
|
84
|
+
list_style = get_attribute('listStyle', 'dash')
|
85
|
+
items = []
|
86
|
+
index = 0
|
87
|
+
|
88
|
+
@element.children.each do |child|
|
89
|
+
if child.tag_name == :item
|
90
|
+
index += 1
|
91
|
+
|
92
|
+
bullet = case list_style
|
93
|
+
when 'decimal', 'number', 'numbered'
|
94
|
+
"#{index}. "
|
95
|
+
when 'star'
|
96
|
+
"* "
|
97
|
+
when 'plus'
|
98
|
+
"+ "
|
99
|
+
when 'dash', 'bullet', 'unordered'
|
100
|
+
"- "
|
101
|
+
else
|
102
|
+
"- "
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get text content and nested elements separately
|
106
|
+
text_content = child.content.strip
|
107
|
+
nested_elements = child.children.reject { |c| c.tag_name == :text }
|
108
|
+
|
109
|
+
if nested_elements.any?
|
110
|
+
# Item has both text and nested elements (like nested lists)
|
111
|
+
nested_content = nested_elements.map { |nested_child|
|
112
|
+
Components.render_element(nested_child, @context)
|
113
|
+
}.join('').strip
|
114
|
+
|
115
|
+
# Format with text content on first line, nested content indented
|
116
|
+
indented_nested = nested_content.split("\n").map { |line|
|
117
|
+
line.strip.empty? ? "" : " #{line}"
|
118
|
+
}.join("\n").strip
|
119
|
+
|
120
|
+
if text_content.empty?
|
121
|
+
items << "#{bullet}#{indented_nested}"
|
122
|
+
else
|
123
|
+
items << "#{bullet}#{text_content} \n\n#{indented_nested}"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
# Simple text-only item
|
127
|
+
items << "#{bullet}#{text_content}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
return "\n\n" if items.empty?
|
133
|
+
items.join("\n") + "\n\n"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Item component (for list items)
|
139
|
+
class ItemComponent < Component
|
140
|
+
def render
|
141
|
+
apply_stylesheet
|
142
|
+
content = @element.content.empty? ? render_children : @element.content.strip
|
143
|
+
|
144
|
+
if xml_mode?
|
145
|
+
"<item>#{content}</item>\n"
|
146
|
+
else
|
147
|
+
content
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# CP component (custom component with caption)
|
153
|
+
class CPComponent < Component
|
154
|
+
def render
|
155
|
+
apply_stylesheet
|
156
|
+
|
157
|
+
caption = get_attribute('caption', '')
|
158
|
+
caption_serialized = get_attribute('captionSerialized', caption)
|
159
|
+
|
160
|
+
# Render children with increased header level for nested CPs
|
161
|
+
content = if @element.content.empty?
|
162
|
+
@context.with_increased_header_level { render_children }
|
163
|
+
else
|
164
|
+
@element.content
|
165
|
+
end
|
166
|
+
|
167
|
+
if xml_mode?
|
168
|
+
# Use captionSerialized for XML tag name, fallback to caption
|
169
|
+
tag_name = caption_serialized.empty? ? caption : caption_serialized
|
170
|
+
return render_as_xml(tag_name, content) unless tag_name.empty?
|
171
|
+
# If no caption, just return content
|
172
|
+
return "#{content}\n\n"
|
173
|
+
else
|
174
|
+
caption_style = get_attribute('captionStyle', 'header')
|
175
|
+
# Use captionSerialized for the actual header if provided
|
176
|
+
display_caption = caption_serialized.empty? ? caption : caption_serialized
|
177
|
+
|
178
|
+
# Apply stylesheet text transformation
|
179
|
+
display_caption = apply_text_transform(display_caption)
|
180
|
+
|
181
|
+
return content + "\n\n" if display_caption.empty?
|
182
|
+
|
183
|
+
case caption_style
|
184
|
+
when 'header'
|
185
|
+
header_prefix = '#' * @context.header_level
|
186
|
+
"#{header_prefix} #{display_caption}\n\n#{content}\n\n"
|
187
|
+
when 'bold'
|
188
|
+
"**#{display_caption}:** #{content}\n\n"
|
189
|
+
when 'plain'
|
190
|
+
"#{display_caption}: #{content}\n\n"
|
191
|
+
when 'hidden'
|
192
|
+
"#{content}\n\n"
|
193
|
+
else
|
194
|
+
header_prefix = '#' * @context.header_level
|
195
|
+
"#{header_prefix} #{display_caption}\n\n#{content}\n\n"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Stepwise Instructions component
|
202
|
+
class StepwiseInstructionsComponent < Component
|
203
|
+
def render
|
204
|
+
apply_stylesheet
|
205
|
+
|
206
|
+
content = @element.content.empty? ? render_children : @element.content
|
207
|
+
|
208
|
+
if xml_mode?
|
209
|
+
render_as_xml('stepwise-instructions', content)
|
210
|
+
else
|
211
|
+
caption = apply_text_transform(get_attribute('caption', 'Stepwise Instructions'))
|
212
|
+
caption_style = get_attribute('captionStyle', 'header')
|
213
|
+
|
214
|
+
case caption_style
|
215
|
+
when 'header'
|
216
|
+
"# #{caption}\n\n#{content}\n\n"
|
217
|
+
when 'bold'
|
218
|
+
"**#{caption}:** #{content}\n\n"
|
219
|
+
when 'plain'
|
220
|
+
"#{caption}: #{content}\n\n"
|
221
|
+
when 'hidden'
|
222
|
+
"#{content}\n\n"
|
223
|
+
else
|
224
|
+
"# #{caption}\n\n#{content}\n\n"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Human Message component
|
231
|
+
class HumanMessageComponent < Component
|
232
|
+
def render
|
233
|
+
apply_stylesheet
|
234
|
+
|
235
|
+
content = @element.content.empty? ? render_children : @element.content
|
236
|
+
speaker = get_attribute('speaker', 'human')
|
237
|
+
|
238
|
+
if xml_mode?
|
239
|
+
render_as_xml('human-message', content)
|
240
|
+
else
|
241
|
+
"#{content}\n\n"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Question-Answer component
|
247
|
+
class QAComponent < Component
|
248
|
+
def render
|
249
|
+
apply_stylesheet
|
250
|
+
|
251
|
+
content = @element.content.empty? ? render_children : @element.content
|
252
|
+
question_caption = get_attribute('questionCaption', 'Question')
|
253
|
+
answer_caption = get_attribute('answerCaption', 'Answer')
|
254
|
+
|
255
|
+
if xml_mode?
|
256
|
+
render_as_xml('qa', content)
|
257
|
+
else
|
258
|
+
"**#{question_caption}:** #{content}\n\n**#{answer_caption}:**"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Let component (for variable definitions)
|
264
|
+
class LetComponent < Component
|
265
|
+
def render
|
266
|
+
apply_stylesheet
|
267
|
+
|
268
|
+
name = get_attribute('name')
|
269
|
+
value = @element.content.empty? ? render_children : @element.content
|
270
|
+
|
271
|
+
# Add to context variables
|
272
|
+
@context.variables[name] = value if name
|
273
|
+
|
274
|
+
# Let components produce no output
|
275
|
+
''
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Stylesheet component
|
280
|
+
class StylesheetComponent < Component
|
281
|
+
def render
|
282
|
+
# Parse and apply stylesheet
|
283
|
+
begin
|
284
|
+
stylesheet_content = @element.content.strip
|
285
|
+
if stylesheet_content.start_with?('{') && stylesheet_content.end_with?('}')
|
286
|
+
stylesheet = JSON.parse(stylesheet_content)
|
287
|
+
@context.stylesheet.merge!(stylesheet) if stylesheet.is_a?(Hash)
|
288
|
+
end
|
289
|
+
rescue => e
|
290
|
+
# Silently fail JSON parsing errors
|
291
|
+
end
|
292
|
+
|
293
|
+
# Stylesheet components don't produce output
|
294
|
+
''
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|