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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3e104ca36939275deeacddbc12fd7ad75129a288e15120ee5103928691188a33
4
- data.tar.gz: 997315c25a24ec17f3e0281d634fca83cb65535673ab6334a380402783d5a854
3
+ metadata.gz: 2c09d0d76e15b722fd0ac52fc6dfe843eebe6f85e18f9b4a4444c43d6c8ca651
4
+ data.tar.gz: fb88b15378606b36242f7c80b3db61502bc3be8c0403565601250d0e9bce748a
5
5
  SHA512:
6
- metadata.gz: 429e8c22d2f9527a537d312a72975700f84d1dcda381aff1a98fd2842f5520130928780b7cf1c40fc60366180425ae61ac14719560996548ec363e777e619dca
7
- data.tar.gz: 776c99377cf172f0edc9cbfa277f83e3d7d1fd47442b64c57c416087a56d4817b50abdfe777e92835a6238a65d292d5e6928cd89bb298e52b522dd3fc5d9a3d4
6
+ metadata.gz: 0f61b599f4f5548c38e74fd1413fba6ed3489f8cf8ca4b518d943dcf7205cf8101ae962ef95fc9d9a8eba46ace47f303f0a8d2abcdce643fa1e32be7d6c86abf
7
+ data.tar.gz: a28b8954f93dd2c5a54a2353029401f37e78047b9d2e74b721d43ec3eb03ab5939cffedf52eabdbc38d73ae6f9d37dcbc65ddf059750167da5a8bd1ee24208d1
@@ -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
@@ -22,6 +22,9 @@ module Poml
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
@@ -147,6 +150,39 @@ module Poml
147
150
  { records: records.is_a?(Array) ? records : [records], columns: columns }
148
151
  end
149
152
 
153
+ def parse_html_table_children
154
+ records = []
155
+ columns = []
156
+
157
+ # Extract rows from tr children
158
+ @element.children.each do |child|
159
+ next unless child.tag_name == :tr
160
+
161
+ row_data = {}
162
+ child.children.each_with_index do |cell, index|
163
+ next unless cell.tag_name == :td || cell.tag_name == :th
164
+
165
+ # Get cell content (render children to get text)
166
+ cell_content = cell.children.map do |cell_child|
167
+ Components.render_element(cell_child, @context)
168
+ end.join('').strip
169
+
170
+ # Use cell content as content, index as key for now
171
+ column_key = "col_#{index}"
172
+ row_data[column_key] = cell_content
173
+
174
+ # Track columns
175
+ unless columns.any? { |col| col[:field] == column_key }
176
+ columns << { field: column_key, header: "Column #{index + 1}" }
177
+ end
178
+ end
179
+
180
+ records << row_data unless row_data.empty?
181
+ end
182
+
183
+ { records: records, columns: columns }
184
+ end
185
+
150
186
  def apply_selection(data, selected_columns, selected_records, max_records, max_columns)
151
187
  records = data[:records]
152
188
  columns = data[:columns]
@@ -327,6 +363,229 @@ module Poml
327
363
  end
328
364
  end
329
365
 
366
+ # Object component for displaying structured data
367
+ class ObjectComponent < Component
368
+ require 'json'
369
+ require 'yaml'
370
+
371
+ def render
372
+ apply_stylesheet
373
+
374
+ data = get_attribute('data')
375
+ syntax = get_attribute('syntax', 'json')
376
+
377
+ return '' unless data
378
+
379
+ if xml_mode?
380
+ render_as_xml('obj', serialize_data(data, syntax))
381
+ else
382
+ serialize_data(data, syntax)
383
+ end
384
+ end
385
+
386
+ private
387
+
388
+ def serialize_data(data, syntax)
389
+ case syntax.downcase
390
+ when 'json'
391
+ JSON.pretty_generate(data)
392
+ when 'yaml', 'yml'
393
+ YAML.dump(data)
394
+ when 'xml'
395
+ # Simple XML serialization for basic data structures
396
+ serialize_to_xml(data)
397
+ else
398
+ data.to_s
399
+ end
400
+ rescue => e
401
+ "[Error serializing data: #{e.message}]"
402
+ end
403
+
404
+ def serialize_to_xml(data, root_name = 'data', indent = 0)
405
+ spaces = ' ' * indent
406
+
407
+ case data
408
+ when Hash
409
+ result = ["#{spaces}<#{root_name}>"]
410
+ data.each do |key, value|
411
+ result << serialize_to_xml(value, key, indent + 1)
412
+ end
413
+ result << "#{spaces}</#{root_name}>"
414
+ result.join("\n")
415
+ when Array
416
+ result = ["#{spaces}<#{root_name}>"]
417
+ data.each_with_index do |item, index|
418
+ result << serialize_to_xml(item, "item#{index}", indent + 1)
419
+ end
420
+ result << "#{spaces}</#{root_name}>"
421
+ result.join("\n")
422
+ else
423
+ "#{spaces}<#{root_name}>#{escape_xml(data)}</#{root_name}>"
424
+ end
425
+ end
426
+
427
+ def escape_xml(text)
428
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
429
+ end
430
+ end
431
+
432
+ # Webpage component for displaying web content
433
+ class WebpageComponent < Component
434
+ def render
435
+ apply_stylesheet
436
+
437
+ url = get_attribute('url')
438
+ src = get_attribute('src')
439
+ buffer = get_attribute('buffer')
440
+ base64 = get_attribute('base64')
441
+ extract_text = get_attribute('extractText', false)
442
+ selector = get_attribute('selector', 'body')
443
+ syntax = get_attribute('syntax', 'text')
444
+
445
+ content = if url
446
+ fetch_webpage_content(url, selector, extract_text)
447
+ elsif src
448
+ read_html_file(src, selector, extract_text)
449
+ elsif buffer
450
+ process_html_content(buffer, selector, extract_text)
451
+ elsif base64
452
+ require 'base64'
453
+ decoded = Base64.decode64(base64)
454
+ process_html_content(decoded, selector, extract_text)
455
+ else
456
+ '[Webpage: no source specified]'
457
+ end
458
+
459
+ if xml_mode?
460
+ render_as_xml('webpage', content)
461
+ else
462
+ content
463
+ end
464
+ end
465
+
466
+ private
467
+
468
+ def fetch_webpage_content(url, selector, extract_text)
469
+ begin
470
+ require 'net/http'
471
+ require 'uri'
472
+
473
+ uri = URI.parse(url)
474
+ http = Net::HTTP.new(uri.host, uri.port)
475
+ http.use_ssl = true if uri.scheme == 'https'
476
+ http.read_timeout = 10
477
+
478
+ request = Net::HTTP::Get.new(uri.request_uri)
479
+ request['User-Agent'] = 'POML/1.0'
480
+
481
+ response = http.request(request)
482
+
483
+ if response.code == '200'
484
+ process_html_content(response.body, selector, extract_text)
485
+ else
486
+ "[Webpage: HTTP #{response.code} error fetching #{url}]"
487
+ end
488
+ rescue => e
489
+ "[Webpage: Error fetching #{url}: #{e.message}]"
490
+ end
491
+ end
492
+
493
+ def read_html_file(file_path, selector, extract_text)
494
+ begin
495
+ # Resolve relative paths
496
+ full_path = if file_path.start_with?('/')
497
+ file_path
498
+ else
499
+ base_path = @context.source_path ? File.dirname(@context.source_path) : Dir.pwd
500
+ File.join(base_path, file_path)
501
+ end
502
+
503
+ unless File.exist?(full_path)
504
+ return "[Webpage: File not found: #{file_path}]"
505
+ end
506
+
507
+ html_content = File.read(full_path)
508
+ process_html_content(html_content, selector, extract_text)
509
+ rescue => e
510
+ "[Webpage: Error reading file #{file_path}: #{e.message}]"
511
+ end
512
+ end
513
+
514
+ def process_html_content(html_content, selector, extract_text)
515
+ begin
516
+ require 'nokogiri'
517
+
518
+ doc = Nokogiri::HTML(html_content)
519
+
520
+ # Apply selector if specified
521
+ if selector && selector != 'body'
522
+ selected = doc.css(selector).first
523
+ return "[Webpage: Selector '#{selector}' not found]" unless selected
524
+ doc = selected
525
+ end
526
+
527
+ if extract_text
528
+ # Extract plain text
529
+ doc.text.strip.gsub(/\s+/, ' ')
530
+ else
531
+ # Convert HTML to structured POML-like format
532
+ convert_html_to_poml(doc)
533
+ end
534
+ rescue LoadError
535
+ # Nokogiri not available, do simple text extraction
536
+ if extract_text
537
+ html_content.gsub(/<[^>]*>/, ' ').gsub(/\s+/, ' ').strip
538
+ else
539
+ html_content
540
+ end
541
+ rescue => e
542
+ "[Webpage: Error processing HTML: #{e.message}]"
543
+ end
544
+ end
545
+
546
+ def convert_html_to_poml(element)
547
+ case element.name.downcase
548
+ when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
549
+ level = element.name[1].to_i
550
+ "#{('#' * level)} #{element.text.strip}\n\n"
551
+ when 'p'
552
+ "#{element.text.strip}\n\n"
553
+ when 'ul', 'ol'
554
+ items = element.css('li').map { |li| "- #{li.text.strip}" }
555
+ "#{items.join("\n")}\n\n"
556
+ when 'strong', 'b'
557
+ "**#{element.text.strip}**"
558
+ when 'em', 'i'
559
+ "*#{element.text.strip}*"
560
+ when 'code'
561
+ "`#{element.text.strip}`"
562
+ when 'pre'
563
+ "```\n#{element.text.strip}\n```\n\n"
564
+ when 'blockquote'
565
+ lines = element.text.strip.split("\n")
566
+ quoted = lines.map { |line| "> #{line}" }
567
+ "#{quoted.join("\n")}\n\n"
568
+ when 'a'
569
+ href = element['href']
570
+ text = element.text.strip
571
+ href ? "[#{text}](#{href})" : text
572
+ when 'img'
573
+ alt = element['alt'] || 'Image'
574
+ src = element['src']
575
+ src ? "![#{alt}](#{src})" : "[#{alt}]"
576
+ else
577
+ # For other elements, process children recursively
578
+ if element.children.any?
579
+ element.children.map { |child|
580
+ child.text? ? child.text : convert_html_to_poml(child)
581
+ }.join('')
582
+ else
583
+ element.text.strip
584
+ end
585
+ end
586
+ end
587
+ end
588
+
330
589
  # Image component
331
590
  class ImageComponent < Component
332
591
  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