poml 0.0.6 → 0.0.7
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/docs/tutorial/advanced/performance.md +695 -0
- data/docs/tutorial/advanced/tool-registration.md +776 -0
- data/docs/tutorial/basic-usage.md +351 -0
- data/docs/tutorial/components/chat-components.md +552 -0
- data/docs/tutorial/components/formatting.md +623 -0
- data/docs/tutorial/components/index.md +366 -0
- data/docs/tutorial/components/media-components.md +259 -0
- data/docs/tutorial/components/schema-components.md +668 -0
- data/docs/tutorial/index.md +184 -0
- data/docs/tutorial/output-formats.md +688 -0
- data/docs/tutorial/quickstart.md +30 -0
- data/docs/tutorial/template-engine.md +540 -0
- data/lib/poml/components/base.rb +146 -4
- data/lib/poml/components/content.rb +10 -3
- data/lib/poml/components/data.rb +539 -19
- data/lib/poml/components/examples.rb +235 -1
- data/lib/poml/components/formatting.rb +184 -18
- data/lib/poml/components/layout.rb +7 -2
- data/lib/poml/components/lists.rb +69 -35
- data/lib/poml/components/meta.rb +134 -5
- data/lib/poml/components/output_schema.rb +19 -1
- data/lib/poml/components/template.rb +72 -61
- data/lib/poml/components/text.rb +30 -1
- data/lib/poml/components/tool.rb +81 -0
- data/lib/poml/components/tool_definition.rb +339 -10
- data/lib/poml/components/tools.rb +14 -0
- data/lib/poml/components/utilities.rb +34 -18
- data/lib/poml/components.rb +19 -0
- data/lib/poml/context.rb +19 -4
- data/lib/poml/parser.rb +88 -63
- data/lib/poml/renderer.rb +191 -9
- data/lib/poml/template_engine.rb +138 -13
- data/lib/poml/version.rb +1 -1
- data/lib/poml.rb +16 -1
- data/readme.md +154 -27
- metadata +31 -4
- data/TUTORIAL.md +0 -987
data/lib/poml/components/base.rb
CHANGED
@@ -40,13 +40,44 @@ module Poml
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
def inline?
|
44
|
+
# Check if component should render inline
|
45
|
+
get_attribute('inline', false) == true || get_attribute('inline') == 'true'
|
46
|
+
end
|
47
|
+
|
48
|
+
def render_with_inline_support(content)
|
49
|
+
# Render content with inline vs block consideration
|
50
|
+
if inline?
|
51
|
+
render_inline(content)
|
52
|
+
else
|
53
|
+
render_block(content)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def render_inline(content)
|
58
|
+
# Inline rendering - no extra whitespace or newlines
|
59
|
+
content.to_s.strip
|
60
|
+
end
|
61
|
+
|
62
|
+
def render_block(content)
|
63
|
+
# Block rendering - traditional with proper spacing
|
64
|
+
content.to_s.rstrip + "\n\n"
|
65
|
+
end
|
66
|
+
|
43
67
|
def render_as_xml(tag_name, content = nil, attributes = {})
|
44
68
|
# Render as XML element with proper formatting
|
45
69
|
content ||= render_children
|
46
70
|
attrs_str = attributes.map { |k, v| " #{k}=\"#{v}\"" }.join('')
|
47
71
|
|
72
|
+
# Check if this is an inline component that shouldn't have trailing newlines
|
73
|
+
is_inline_component = is_inline_component_name?(self.class.name.split('::').last)
|
74
|
+
|
48
75
|
if content.strip.empty?
|
49
|
-
|
76
|
+
if is_inline_component
|
77
|
+
"<#{tag_name}#{attrs_str}/>"
|
78
|
+
else
|
79
|
+
"<#{tag_name}#{attrs_str}/>\n"
|
80
|
+
end
|
50
81
|
else
|
51
82
|
# Add line breaks for nice formatting
|
52
83
|
if content.include?('<item>')
|
@@ -57,11 +88,60 @@ module Poml
|
|
57
88
|
"<#{tag_name}#{attrs_str}>\n #{indented_content}\n</#{tag_name}>\n"
|
58
89
|
else
|
59
90
|
# Simple content
|
60
|
-
|
91
|
+
if is_inline_component
|
92
|
+
"<#{tag_name}#{attrs_str}>#{content}</#{tag_name}>"
|
93
|
+
else
|
94
|
+
"<#{tag_name}#{attrs_str}>#{content}</#{tag_name}>\n"
|
95
|
+
end
|
61
96
|
end
|
62
97
|
end
|
63
98
|
end
|
64
99
|
|
100
|
+
# Helper method for robust file reading with encoding support
|
101
|
+
def read_file_with_encoding(file_path, encoding: 'utf-8')
|
102
|
+
# Normalize file path for cross-platform compatibility
|
103
|
+
normalized_path = File.expand_path(file_path)
|
104
|
+
|
105
|
+
# Try primary encoding first (UTF-8)
|
106
|
+
begin
|
107
|
+
return File.read(normalized_path, encoding: encoding)
|
108
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
|
109
|
+
# If UTF-8 fails, try with binary mode and force encoding
|
110
|
+
begin
|
111
|
+
content = File.read(normalized_path, mode: 'rb')
|
112
|
+
# Try to detect and convert encoding
|
113
|
+
return content.force_encoding('utf-8').encode('utf-8', invalid: :replace, undef: :replace)
|
114
|
+
rescue => encoding_error
|
115
|
+
# If all else fails, read as binary and provide meaningful error
|
116
|
+
raise "File encoding error for #{file_path}: #{e.message}. Original encoding detection failed: #{encoding_error.message}"
|
117
|
+
end
|
118
|
+
rescue => e
|
119
|
+
# Re-raise other file reading errors with context
|
120
|
+
raise "Error reading file #{file_path}: #{e.message}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Helper method for reading file lines with encoding support
|
125
|
+
def read_file_lines_with_encoding(file_path, encoding: 'utf-8')
|
126
|
+
# Normalize file path for cross-platform compatibility
|
127
|
+
normalized_path = File.expand_path(file_path)
|
128
|
+
|
129
|
+
begin
|
130
|
+
return File.readlines(normalized_path, encoding: encoding)
|
131
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError => e
|
132
|
+
# If UTF-8 fails, try with binary mode and force encoding line by line
|
133
|
+
begin
|
134
|
+
content = File.read(normalized_path, mode: 'rb')
|
135
|
+
lines = content.force_encoding('utf-8').encode('utf-8', invalid: :replace, undef: :replace).lines
|
136
|
+
return lines
|
137
|
+
rescue => encoding_error
|
138
|
+
raise "File encoding error for #{file_path}: #{e.message}. Original encoding detection failed: #{encoding_error.message}"
|
139
|
+
end
|
140
|
+
rescue => e
|
141
|
+
raise "Error reading file #{file_path}: #{e.message}"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
65
145
|
def get_attribute(name, default = nil)
|
66
146
|
value = @element.attributes[name.to_s.downcase]
|
67
147
|
case value
|
@@ -131,6 +211,15 @@ module Poml
|
|
131
211
|
# Override this in inline components
|
132
212
|
false
|
133
213
|
end
|
214
|
+
|
215
|
+
def unescape_xml_entities(text)
|
216
|
+
# Unescape XML entities that were escaped during preprocessing
|
217
|
+
text.gsub('&', '&')
|
218
|
+
.gsub('<', '<')
|
219
|
+
.gsub('>', '>')
|
220
|
+
.gsub('"', '"')
|
221
|
+
.gsub(''', "'")
|
222
|
+
end
|
134
223
|
|
135
224
|
def self.inline_component_classes
|
136
225
|
# List of component classes that should be treated as inline
|
@@ -154,6 +243,16 @@ module Poml
|
|
154
243
|
component_class_name = component_class.name.split('::').last
|
155
244
|
inline_component_names.include?(component_class_name)
|
156
245
|
end
|
246
|
+
|
247
|
+
def is_inline_component_name?(component_class_name)
|
248
|
+
# Check if the component class name is registered as inline
|
249
|
+
inline_component_names = %w[
|
250
|
+
BoldComponent ItalicComponent UnderlineComponent StrikethroughComponent
|
251
|
+
CodeComponent InlineComponent
|
252
|
+
]
|
253
|
+
|
254
|
+
inline_component_names.include?(component_class_name)
|
255
|
+
end
|
157
256
|
end
|
158
257
|
|
159
258
|
# Component registry and factory
|
@@ -164,12 +263,55 @@ module Poml
|
|
164
263
|
def self.render_element(element, context)
|
165
264
|
# Try to find component using multiple key formats for compatibility
|
166
265
|
tag_name = element.tag_name
|
266
|
+
|
267
|
+
# In HTML output format, preserve HTML elements as-is
|
268
|
+
if context.output_format == 'html' && is_html_element?(element)
|
269
|
+
return render_html_element_as_is(element, context)
|
270
|
+
end
|
271
|
+
|
167
272
|
component_class = COMPONENT_MAPPING[tag_name] ||
|
168
273
|
COMPONENT_MAPPING[tag_name.to_s] ||
|
169
|
-
COMPONENT_MAPPING[tag_name.to_sym]
|
170
|
-
|
274
|
+
COMPONENT_MAPPING[tag_name.to_sym]
|
275
|
+
|
276
|
+
# Use UnknownComponent for unrecognized tags (except :text which should use TextComponent)
|
277
|
+
if component_class.nil?
|
278
|
+
component_class = tag_name == :text ? TextComponent : UnknownComponent
|
279
|
+
end
|
280
|
+
|
171
281
|
component = component_class.new(element, context)
|
172
282
|
component.render
|
173
283
|
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def self.is_html_element?(element)
|
288
|
+
# Common HTML elements that should be preserved in HTML format
|
289
|
+
html_elements = [:b, :i, :u, :strong, :em, :span, :div, :p, :h1, :h2, :h3, :h4, :h5, :h6,
|
290
|
+
:table, :thead, :tbody, :tfoot, :tr, :th, :td, :ul, :ol, :li, :a, :img, :code]
|
291
|
+
|
292
|
+
return html_elements.include?(element.tag_name)
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.render_html_element_as_is(element, context)
|
296
|
+
tag_name = element.tag_name.to_s
|
297
|
+
|
298
|
+
# Build attributes string
|
299
|
+
attr_string = ""
|
300
|
+
if element.attributes && !element.attributes.empty?
|
301
|
+
attr_string = element.attributes.map { |k, v| %{#{k}="#{v}"} }.join(" ")
|
302
|
+
attr_string = " " + attr_string
|
303
|
+
end
|
304
|
+
|
305
|
+
# Render children
|
306
|
+
children_content = element.children.map { |child| render_element(child, context) }.join('')
|
307
|
+
content = children_content.empty? ? element.content : children_content
|
308
|
+
|
309
|
+
# Return as HTML element
|
310
|
+
if content.empty?
|
311
|
+
"<#{tag_name}#{attr_string} />"
|
312
|
+
else
|
313
|
+
"<#{tag_name}#{attr_string}>#{content}</#{tag_name}>"
|
314
|
+
end
|
315
|
+
end
|
174
316
|
end
|
175
317
|
end
|
@@ -38,7 +38,7 @@ module Poml
|
|
38
38
|
elsif file_path.downcase.end_with?('.docx')
|
39
39
|
read_docx_content(file_path)
|
40
40
|
else
|
41
|
-
|
41
|
+
read_file_with_encoding(file_path)
|
42
42
|
end
|
43
43
|
rescue => e
|
44
44
|
"[Document: #{@src} (error reading: #{e.message})]"
|
@@ -149,8 +149,15 @@ module Poml
|
|
149
149
|
def render
|
150
150
|
apply_stylesheet
|
151
151
|
|
152
|
-
|
153
|
-
|
152
|
+
# If the element has children, render them (for mixed content like text + formatting)
|
153
|
+
# Otherwise, use the element's direct content
|
154
|
+
content = @element.children.empty? ? @element.content : render_children
|
155
|
+
|
156
|
+
if xml_mode? || @context.output_format == 'html'
|
157
|
+
"<p>#{content}</p>"
|
158
|
+
else
|
159
|
+
render_with_inline_support(content)
|
160
|
+
end
|
154
161
|
end
|
155
162
|
end
|
156
163
|
end
|