poml 0.0.1 → 0.0.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e104ca36939275deeacddbc12fd7ad75129a288e15120ee5103928691188a33
4
- data.tar.gz: 997315c25a24ec17f3e0281d634fca83cb65535673ab6334a380402783d5a854
3
+ metadata.gz: 57f8a3904a43267d79cbc6fb76465dd51c05da5923e58635304242eb90b04b96
4
+ data.tar.gz: 591512861013d2ae3d302b342b4678696a4d3691c98a26a2d9cdcd042ec70422
5
5
  SHA512:
6
- metadata.gz: 429e8c22d2f9527a537d312a72975700f84d1dcda381aff1a98fd2842f5520130928780b7cf1c40fc60366180425ae61ac14719560996548ec363e777e619dca
7
- data.tar.gz: 776c99377cf172f0edc9cbfa277f83e3d7d1fd47442b64c57c416087a56d4817b50abdfe777e92835a6238a65d292d5e6928cd89bb298e52b522dd3fc5d9a3d4
6
+ metadata.gz: ba9aeb9b1e06de010b5daa9489d847dc41f5f5d73a9a02e70d81432e997b1c3a3a32116cf9d85ee2f4c70755024201272e62951381cf6363be59dc807c6c55b0
7
+ data.tar.gz: 2f763c41a44b095f64fbe75cc64f58af266b50c8d966e0d44e6c382945a21fdde9abdd83a49b17ab57608c90a8481fb3066eca6dbc3633533e16269af1693f61
@@ -16,7 +16,8 @@ module Poml
16
16
 
17
17
  def apply_stylesheet
18
18
  # Apply stylesheet rules to the element
19
- style_rules = @context.stylesheet[@element.tag_name.to_s] || {}
19
+ stylesheet = @context.respond_to?(:stylesheet) ? @context.stylesheet : {}
20
+ style_rules = stylesheet[@element.tag_name.to_s] || {}
20
21
  style_rules.each do |attr, value|
21
22
  @element.attributes[attr] ||= value
22
23
  end
@@ -24,7 +25,7 @@ module Poml
24
25
  # Apply class-based styles
25
26
  class_name = @element.attributes['classname'] || @element.attributes['className']
26
27
  if class_name
27
- class_rules = @context.stylesheet[".#{class_name}"] || {}
28
+ class_rules = stylesheet[".#{class_name}"] || {}
28
29
  class_rules.each do |attr, value|
29
30
  @element.attributes[attr] ||= value
30
31
  end
@@ -32,7 +33,11 @@ module Poml
32
33
  end
33
34
 
34
35
  def xml_mode?
35
- @context.determine_syntax(@element) == 'xml'
36
+ if @context.respond_to?(:determine_syntax)
37
+ @context.determine_syntax(@element) == 'xml'
38
+ else
39
+ false
40
+ end
36
41
  end
37
42
 
38
43
  def render_as_xml(tag_name, content = nil, attributes = {})
@@ -81,13 +86,17 @@ module Poml
81
86
  rendered_children.each_with_index do |child_content, index|
82
87
  result << child_content
83
88
 
84
- # Add spacing if current element is text and next element is a component
89
+ # Add spacing if current element is text and next element is a block-level component
85
90
  if index < rendered_children.length - 1
86
91
  current_element = @element.children[index]
87
92
  next_element = @element.children[index + 1]
88
93
 
94
+ # Only add spacing for block-level components, not inline components
89
95
  if current_element.text? && next_element.component?
90
- result << "\n\n"
96
+ next_component_class = Components::COMPONENT_MAPPING[next_element.tag_name]
97
+ if next_component_class && !is_inline_component?(next_component_class)
98
+ result << "\n\n"
99
+ end
91
100
  end
92
101
  end
93
102
  end
@@ -102,8 +111,9 @@ module Poml
102
111
  component_name = self.class.name.split('::').last.gsub('Component', '').downcase
103
112
 
104
113
  # Check for text transformation in stylesheet - first try component-specific, then "cp" (for captioned paragraph inheritance)
105
- transform = @context.stylesheet.dig(component_name, 'captionTextTransform') ||
106
- @context.stylesheet.dig('cp', 'captionTextTransform')
114
+ stylesheet = @context.respond_to?(:stylesheet) ? @context.stylesheet : {}
115
+ transform = stylesheet.dig(component_name, 'captionTextTransform') ||
116
+ stylesheet.dig('cp', 'captionTextTransform')
107
117
 
108
118
  case transform
109
119
  when 'upper'
@@ -116,6 +126,34 @@ module Poml
116
126
  text
117
127
  end
118
128
  end
129
+
130
+ def inline_component?
131
+ # Override this in inline components
132
+ false
133
+ end
134
+
135
+ def self.inline_component_classes
136
+ # List of component classes that should be treated as inline
137
+ @inline_component_classes ||= []
138
+ end
139
+
140
+ def self.register_inline_component(component_class)
141
+ inline_component_classes << component_class
142
+ end
143
+
144
+ private
145
+
146
+ def is_inline_component?(component_class)
147
+ # Check if the component class is registered as inline
148
+ # For now, we'll use a simple list of known inline formatting components
149
+ inline_component_names = %w[
150
+ BoldComponent ItalicComponent UnderlineComponent StrikethroughComponent
151
+ CodeComponent InlineComponent
152
+ ]
153
+
154
+ component_class_name = component_class.name.split('::').last
155
+ inline_component_names.include?(component_class_name)
156
+ end
119
157
  end
120
158
 
121
159
  # Component registry and factory
@@ -12,16 +12,19 @@ module Poml
12
12
  columns_attr = get_attribute('columns')
13
13
  parser = get_attribute('parser', 'auto')
14
14
  syntax = get_attribute('syntax')
15
- selected_columns = get_attribute('selectedColumns')
16
- selected_records = get_attribute('selectedRecords')
17
- max_records = get_attribute('maxRecords')
18
- max_columns = get_attribute('maxColumns')
15
+ selected_columns = parse_array_attribute('selectedColumns')
16
+ selected_records = parse_array_attribute('selectedRecords')
17
+ max_records = parse_integer_attribute('maxRecords')
18
+ max_columns = parse_integer_attribute('maxColumns')
19
19
 
20
20
  # Load data from source or use provided records
21
21
  data = if src
22
22
  load_table_data(src, parser)
23
23
  elsif records_attr
24
24
  parse_records_attribute(records_attr)
25
+ elsif @element.children.any? { |child| child.tag_name == :tr }
26
+ # Handle HTML-style table markup
27
+ parse_html_table_children
25
28
  else
26
29
  { records: [], columns: [] }
27
30
  end
@@ -133,7 +136,12 @@ module Poml
133
136
  def parse_records_attribute(records_attr)
134
137
  # Handle string records (JSON) or already parsed arrays
135
138
  records = if records_attr.is_a?(String)
136
- JSON.parse(records_attr)
139
+ begin
140
+ JSON.parse(records_attr)
141
+ rescue JSON::ParserError => e
142
+ # Return empty records on parse error
143
+ return { records: [], columns: [] }
144
+ end
137
145
  else
138
146
  records_attr
139
147
  end
@@ -147,6 +155,71 @@ module Poml
147
155
  { records: records.is_a?(Array) ? records : [records], columns: columns }
148
156
  end
149
157
 
158
+ def parse_array_attribute(attr_name)
159
+ attr_value = get_attribute(attr_name)
160
+ return nil unless attr_value
161
+
162
+ if attr_value.is_a?(String)
163
+ begin
164
+ parsed = JSON.parse(attr_value)
165
+ parsed.is_a?(Array) ? parsed : nil
166
+ rescue JSON::ParserError
167
+ # Try to handle as slice notation
168
+ attr_value.include?(':') ? attr_value : nil
169
+ end
170
+ elsif attr_value.is_a?(Array)
171
+ attr_value
172
+ else
173
+ nil
174
+ end
175
+ end
176
+
177
+ def parse_integer_attribute(attr_name)
178
+ attr_value = get_attribute(attr_name)
179
+ return nil unless attr_value
180
+
181
+ if attr_value.is_a?(String)
182
+ attr_value.to_i
183
+ elsif attr_value.is_a?(Integer)
184
+ attr_value
185
+ else
186
+ nil
187
+ end
188
+ end
189
+
190
+ def parse_html_table_children
191
+ records = []
192
+ columns = []
193
+
194
+ # Extract rows from tr children
195
+ @element.children.each do |child|
196
+ next unless child.tag_name == :tr
197
+
198
+ row_data = {}
199
+ child.children.each_with_index do |cell, index|
200
+ next unless cell.tag_name == :td || cell.tag_name == :th
201
+
202
+ # Get cell content (render children to get text)
203
+ cell_content = cell.children.map do |cell_child|
204
+ Components.render_element(cell_child, @context)
205
+ end.join('').strip
206
+
207
+ # Use cell content as content, index as key for now
208
+ column_key = "col_#{index}"
209
+ row_data[column_key] = cell_content
210
+
211
+ # Track columns
212
+ unless columns.any? { |col| col[:field] == column_key }
213
+ columns << { field: column_key, header: "Column #{index + 1}" }
214
+ end
215
+ end
216
+
217
+ records << row_data unless row_data.empty?
218
+ end
219
+
220
+ { records: records, columns: columns }
221
+ end
222
+
150
223
  def apply_selection(data, selected_columns, selected_records, max_records, max_columns)
151
224
  records = data[:records]
152
225
  columns = data[:columns]
@@ -183,13 +256,15 @@ module Poml
183
256
  end
184
257
  end
185
258
 
186
- # Apply max records
187
- if max_records && records.length > max_records
188
- # Show top half and bottom half with ellipsis
189
- top_rows = (max_records / 2.0).ceil
190
- bottom_rows = max_records - top_rows
191
- ellipsis_record = columns.each_with_object({}) { |col, record| record[col[:field]] = '...' }
192
- records = records[0...top_rows] + [ellipsis_record] + records[-bottom_rows..-1]
259
+ # Apply max records - simple truncation with ellipsis row
260
+ if max_records && max_records > 0 && records.length > max_records
261
+ truncated_records = records[0...max_records]
262
+ # Add ellipsis row if we truncated
263
+ if records.length > max_records && columns
264
+ ellipsis_record = columns.each_with_object({}) { |col, record| record[col[:field]] = '...' }
265
+ truncated_records << ellipsis_record
266
+ end
267
+ records = truncated_records
193
268
  end
194
269
 
195
270
  # Apply max columns
@@ -327,6 +402,229 @@ module Poml
327
402
  end
328
403
  end
329
404
 
405
+ # Object component for displaying structured data
406
+ class ObjectComponent < Component
407
+ require 'json'
408
+ require 'yaml'
409
+
410
+ def render
411
+ apply_stylesheet
412
+
413
+ data = get_attribute('data')
414
+ syntax = get_attribute('syntax', 'json')
415
+
416
+ return '' unless data
417
+
418
+ if xml_mode?
419
+ render_as_xml('obj', serialize_data(data, syntax))
420
+ else
421
+ serialize_data(data, syntax)
422
+ end
423
+ end
424
+
425
+ private
426
+
427
+ def serialize_data(data, syntax)
428
+ case syntax.downcase
429
+ when 'json'
430
+ JSON.pretty_generate(data)
431
+ when 'yaml', 'yml'
432
+ YAML.dump(data)
433
+ when 'xml'
434
+ # Simple XML serialization for basic data structures
435
+ serialize_to_xml(data)
436
+ else
437
+ data.to_s
438
+ end
439
+ rescue => e
440
+ "[Error serializing data: #{e.message}]"
441
+ end
442
+
443
+ def serialize_to_xml(data, root_name = 'data', indent = 0)
444
+ spaces = ' ' * indent
445
+
446
+ case data
447
+ when Hash
448
+ result = ["#{spaces}<#{root_name}>"]
449
+ data.each do |key, value|
450
+ result << serialize_to_xml(value, key, indent + 1)
451
+ end
452
+ result << "#{spaces}</#{root_name}>"
453
+ result.join("\n")
454
+ when Array
455
+ result = ["#{spaces}<#{root_name}>"]
456
+ data.each_with_index do |item, index|
457
+ result << serialize_to_xml(item, "item#{index}", indent + 1)
458
+ end
459
+ result << "#{spaces}</#{root_name}>"
460
+ result.join("\n")
461
+ else
462
+ "#{spaces}<#{root_name}>#{escape_xml(data)}</#{root_name}>"
463
+ end
464
+ end
465
+
466
+ def escape_xml(text)
467
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
468
+ end
469
+ end
470
+
471
+ # Webpage component for displaying web content
472
+ class WebpageComponent < Component
473
+ def render
474
+ apply_stylesheet
475
+
476
+ url = get_attribute('url')
477
+ src = get_attribute('src')
478
+ buffer = get_attribute('buffer')
479
+ base64 = get_attribute('base64')
480
+ extract_text = get_attribute('extractText', false)
481
+ selector = get_attribute('selector', 'body')
482
+ syntax = get_attribute('syntax', 'text')
483
+
484
+ content = if url
485
+ fetch_webpage_content(url, selector, extract_text)
486
+ elsif src
487
+ read_html_file(src, selector, extract_text)
488
+ elsif buffer
489
+ process_html_content(buffer, selector, extract_text)
490
+ elsif base64
491
+ require 'base64'
492
+ decoded = Base64.decode64(base64)
493
+ process_html_content(decoded, selector, extract_text)
494
+ else
495
+ '[Webpage: no source specified]'
496
+ end
497
+
498
+ if xml_mode?
499
+ render_as_xml('webpage', content)
500
+ else
501
+ content
502
+ end
503
+ end
504
+
505
+ private
506
+
507
+ def fetch_webpage_content(url, selector, extract_text)
508
+ begin
509
+ require 'net/http'
510
+ require 'uri'
511
+
512
+ uri = URI.parse(url)
513
+ http = Net::HTTP.new(uri.host, uri.port)
514
+ http.use_ssl = true if uri.scheme == 'https'
515
+ http.read_timeout = 10
516
+
517
+ request = Net::HTTP::Get.new(uri.request_uri)
518
+ request['User-Agent'] = 'POML/1.0'
519
+
520
+ response = http.request(request)
521
+
522
+ if response.code == '200'
523
+ process_html_content(response.body, selector, extract_text)
524
+ else
525
+ "[Webpage: HTTP #{response.code} error fetching #{url}]"
526
+ end
527
+ rescue => e
528
+ "[Webpage: Error fetching #{url}: #{e.message}]"
529
+ end
530
+ end
531
+
532
+ def read_html_file(file_path, selector, extract_text)
533
+ begin
534
+ # Resolve relative paths
535
+ full_path = if file_path.start_with?('/')
536
+ file_path
537
+ else
538
+ base_path = @context.source_path ? File.dirname(@context.source_path) : Dir.pwd
539
+ File.join(base_path, file_path)
540
+ end
541
+
542
+ unless File.exist?(full_path)
543
+ return "[Webpage: File not found: #{file_path}]"
544
+ end
545
+
546
+ html_content = File.read(full_path)
547
+ process_html_content(html_content, selector, extract_text)
548
+ rescue => e
549
+ "[Webpage: Error reading file #{file_path}: #{e.message}]"
550
+ end
551
+ end
552
+
553
+ def process_html_content(html_content, selector, extract_text)
554
+ begin
555
+ require 'nokogiri'
556
+
557
+ doc = Nokogiri::HTML(html_content)
558
+
559
+ # Apply selector if specified
560
+ if selector && selector != 'body'
561
+ selected = doc.css(selector).first
562
+ return "[Webpage: Selector '#{selector}' not found]" unless selected
563
+ doc = selected
564
+ end
565
+
566
+ if extract_text
567
+ # Extract plain text
568
+ doc.text.strip.gsub(/\s+/, ' ')
569
+ else
570
+ # Convert HTML to structured POML-like format
571
+ convert_html_to_poml(doc)
572
+ end
573
+ rescue LoadError
574
+ # Nokogiri not available, do simple text extraction
575
+ if extract_text
576
+ html_content.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ').strip
577
+ else
578
+ html_content
579
+ end
580
+ rescue => e
581
+ "[Webpage: Error processing HTML: #{e.message}]"
582
+ end
583
+ end
584
+
585
+ def convert_html_to_poml(element)
586
+ case element.name.downcase
587
+ when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
588
+ level = element.name[1].to_i
589
+ "#{('#' * level)} #{element.text.strip}\n\n"
590
+ when 'p'
591
+ "#{element.text.strip}\n\n"
592
+ when 'ul', 'ol'
593
+ items = element.css('li').map { |li| "- #{li.text.strip}" }
594
+ "#{items.join("\n")}\n\n"
595
+ when 'strong', 'b'
596
+ "**#{element.text.strip}**"
597
+ when 'em', 'i'
598
+ "*#{element.text.strip}*"
599
+ when 'code'
600
+ "`#{element.text.strip}`"
601
+ when 'pre'
602
+ "```\n#{element.text.strip}\n```\n\n"
603
+ when 'blockquote'
604
+ lines = element.text.strip.split("\n")
605
+ quoted = lines.map { |line| "> #{line}" }
606
+ "#{quoted.join("\n")}\n\n"
607
+ when 'a'
608
+ href = element['href']
609
+ text = element.text.strip
610
+ href ? "[#{text}](#{href})" : text
611
+ when 'img'
612
+ alt = element['alt'] || 'Image'
613
+ src = element['src']
614
+ src ? "![#{alt}](#{src})" : "[#{alt}]"
615
+ else
616
+ # For other elements, process children recursively
617
+ if element.children.any?
618
+ element.children.map { |child|
619
+ child.text? ? child.text : convert_html_to_poml(child)
620
+ }.join('')
621
+ else
622
+ element.text.strip
623
+ end
624
+ end
625
+ end
626
+ end
627
+
330
628
  # Image component
331
629
  class ImageComponent < Component
332
630
  def render
@@ -5,7 +5,38 @@ module Poml
5
5
  apply_stylesheet
6
6
 
7
7
  content = @element.content.empty? ? render_children : @element.content
8
- "#{content}\n\n"
8
+ caption = get_attribute('caption', 'Example')
9
+ caption_style = get_attribute('captionStyle', 'hidden')
10
+ chat = get_attribute('chat', nil)
11
+
12
+ if xml_mode?
13
+ render_as_xml('example', content)
14
+ else
15
+ # Determine if chat format should be used
16
+ if chat.nil?
17
+ # Auto-detect: use chat format for markup syntaxes by default
18
+ use_chat = @context.determine_syntax(@element) != 'xml'
19
+ else
20
+ use_chat = chat
21
+ end
22
+
23
+ if use_chat && caption_style == 'hidden'
24
+ content
25
+ else
26
+ case caption_style
27
+ when 'header'
28
+ "## #{caption}\n\n#{content}\n\n"
29
+ when 'bold'
30
+ "**#{caption}:** #{content}\n\n"
31
+ when 'plain'
32
+ "#{caption}: #{content}\n\n"
33
+ when 'hidden'
34
+ "#{content}\n\n"
35
+ else
36
+ "#{content}\n\n"
37
+ end
38
+ end
39
+ end
9
40
  end
10
41
  end
11
42
 
@@ -15,7 +46,26 @@ module Poml
15
46
  apply_stylesheet
16
47
 
17
48
  content = @element.content.empty? ? render_children : @element.content
18
- "#{content}\n\n"
49
+ caption = get_attribute('caption', 'Input')
50
+ caption_style = get_attribute('captionStyle', 'hidden')
51
+ speaker = get_attribute('speaker', 'human')
52
+
53
+ if xml_mode?
54
+ render_as_xml('input', content, { speaker: speaker })
55
+ else
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
+ "#{content}\n\n"
67
+ end
68
+ end
19
69
  end
20
70
  end
21
71
 
@@ -25,7 +75,71 @@ module Poml
25
75
  apply_stylesheet
26
76
 
27
77
  content = @element.content.empty? ? render_children : @element.content
28
- "#{content}\n\n"
78
+ caption = get_attribute('caption', 'Output')
79
+ caption_style = get_attribute('captionStyle', 'hidden')
80
+ speaker = get_attribute('speaker', 'ai')
81
+
82
+ if xml_mode?
83
+ render_as_xml('output', content, { speaker: speaker })
84
+ else
85
+ case caption_style
86
+ when 'header'
87
+ "## #{caption}\n\n#{content}\n\n"
88
+ when 'bold'
89
+ "**#{caption}:** #{content}\n\n"
90
+ when 'plain'
91
+ "#{caption}: #{content}\n\n"
92
+ when 'hidden'
93
+ "#{content}\n\n"
94
+ else
95
+ "#{content}\n\n"
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ # Example set component for managing multiple examples
102
+ class ExampleSetComponent < Component
103
+ def render
104
+ apply_stylesheet
105
+
106
+ caption = get_attribute('caption', 'Examples')
107
+ caption_style = get_attribute('captionStyle', 'header')
108
+ chat = get_attribute('chat', true)
109
+ introducer = get_attribute('introducer', '')
110
+
111
+ content = @context.with_chat_context(chat) { render_children }
112
+
113
+ if xml_mode?
114
+ render_as_xml('examples', content)
115
+ else
116
+ result = []
117
+
118
+ case caption_style
119
+ when 'header'
120
+ result << "# #{caption}"
121
+ when 'bold'
122
+ result << "**#{caption}:**"
123
+ when 'plain'
124
+ result << "#{caption}:"
125
+ when 'hidden'
126
+ # No caption
127
+ else
128
+ result << "# #{caption}"
129
+ end
130
+
131
+ result << "" unless result.empty? # Add blank line after caption
132
+
133
+ unless introducer.empty?
134
+ result << introducer
135
+ result << ""
136
+ end
137
+
138
+ result << content
139
+ result << ""
140
+
141
+ result.join("\n")
142
+ end
29
143
  end
30
144
  end
31
145
 
@@ -38,17 +152,49 @@ module Poml
38
152
  caption_style = get_attribute('captionStyle', 'header')
39
153
  content = @element.content.empty? ? render_children : @element.content
40
154
 
41
- case caption_style
42
- when 'header'
43
- "# #{caption}\n\n#{content}\n\n"
44
- when 'bold'
45
- "**#{caption}:** #{content}\n\n"
46
- when 'plain'
47
- "#{caption}: #{content}\n\n"
48
- when 'hidden'
49
- "#{content}\n\n"
155
+ if xml_mode?
156
+ render_as_xml('outputFormat', content)
157
+ else
158
+ case caption_style
159
+ when 'header'
160
+ "# #{caption}\n\n#{content}\n\n"
161
+ when 'bold'
162
+ "**#{caption}:** #{content}\n\n"
163
+ when 'plain'
164
+ "#{caption}: #{content}\n\n"
165
+ when 'hidden'
166
+ "#{content}\n\n"
167
+ else
168
+ "# #{caption}\n\n#{content}\n\n"
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Introducer component
175
+ class IntroducerComponent < Component
176
+ def render
177
+ apply_stylesheet
178
+
179
+ content = @element.content.empty? ? render_children : @element.content
180
+ caption = get_attribute('caption', 'Introducer')
181
+ caption_style = get_attribute('captionStyle', 'hidden')
182
+
183
+ if xml_mode?
184
+ render_as_xml('introducer', content)
50
185
  else
51
- "# #{caption}\n\n#{content}\n\n"
186
+ case caption_style
187
+ when 'header'
188
+ "# #{caption}\n\n#{content}\n\n"
189
+ when 'bold'
190
+ "**#{caption}:** #{content}\n\n"
191
+ when 'plain'
192
+ "#{caption}: #{content}\n\n"
193
+ when 'hidden'
194
+ "#{content}\n\n"
195
+ else
196
+ "#{content}\n\n"
197
+ end
52
198
  end
53
199
  end
54
200
  end