poml 0.0.5 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/docs/tutorial/advanced/performance.md +695 -0
  3. data/docs/tutorial/advanced/tool-registration.md +776 -0
  4. data/docs/tutorial/basic-usage.md +351 -0
  5. data/docs/tutorial/components/chat-components.md +552 -0
  6. data/docs/tutorial/components/formatting.md +623 -0
  7. data/docs/tutorial/components/index.md +366 -0
  8. data/docs/tutorial/components/media-components.md +259 -0
  9. data/docs/tutorial/components/schema-components.md +668 -0
  10. data/docs/tutorial/index.md +184 -0
  11. data/docs/tutorial/output-formats.md +688 -0
  12. data/docs/tutorial/quickstart.md +30 -0
  13. data/docs/tutorial/template-engine.md +540 -0
  14. data/examples/303_new_component_syntax.poml +45 -0
  15. data/lib/poml/components/base.rb +150 -3
  16. data/lib/poml/components/content.rb +10 -3
  17. data/lib/poml/components/data.rb +539 -19
  18. data/lib/poml/components/examples.rb +235 -1
  19. data/lib/poml/components/formatting.rb +184 -18
  20. data/lib/poml/components/layout.rb +7 -2
  21. data/lib/poml/components/lists.rb +69 -35
  22. data/lib/poml/components/meta.rb +191 -6
  23. data/lib/poml/components/output_schema.rb +103 -0
  24. data/lib/poml/components/template.rb +72 -61
  25. data/lib/poml/components/text.rb +30 -1
  26. data/lib/poml/components/tool.rb +81 -0
  27. data/lib/poml/components/tool_definition.rb +427 -0
  28. data/lib/poml/components/tools.rb +14 -0
  29. data/lib/poml/components/utilities.rb +34 -18
  30. data/lib/poml/components.rb +29 -0
  31. data/lib/poml/context.rb +19 -4
  32. data/lib/poml/parser.rb +90 -64
  33. data/lib/poml/renderer.rb +191 -9
  34. data/lib/poml/template_engine.rb +138 -13
  35. data/lib/poml/version.rb +1 -1
  36. data/lib/poml.rb +16 -1
  37. data/readme.md +154 -27
  38. metadata +34 -4
  39. data/TUTORIAL.md +0 -987
@@ -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
- "<#{tag_name}#{attrs_str}/>\n"
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
- "<#{tag_name}#{attrs_str}>#{content}</#{tag_name}>\n"
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('&amp;', '&')
218
+ .gsub('&lt;', '<')
219
+ .gsub('&gt;', '>')
220
+ .gsub('&quot;', '"')
221
+ .gsub('&apos;', "'")
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
@@ -162,9 +261,57 @@ module Poml
162
261
  COMPONENT_MAPPING = {}
163
262
 
164
263
  def self.render_element(element, context)
165
- component_class = COMPONENT_MAPPING[element.tag_name] || TextComponent
264
+ # Try to find component using multiple key formats for compatibility
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
+
272
+ component_class = COMPONENT_MAPPING[tag_name] ||
273
+ COMPONENT_MAPPING[tag_name.to_s] ||
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
+
166
281
  component = component_class.new(element, context)
167
282
  component.render
168
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
169
316
  end
170
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
- File.read(file_path)
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
- content = @element.content.empty? ? render_children : @element.content
153
- "#{content}\n\n"
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