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.
@@ -0,0 +1,580 @@
1
+ module Poml
2
+ # AI Message component for wrapping AI responses
3
+ class AiMessageComponent < Component
4
+ def render
5
+ apply_stylesheet
6
+
7
+ content = @element.content.empty? ? render_children : @element.content
8
+
9
+ # Add to structured chat messages if context supports it
10
+ if @context.respond_to?(:chat_messages)
11
+ @context.chat_messages << {
12
+ 'role' => 'assistant',
13
+ 'content' => content
14
+ }
15
+ # Return empty for raw format to avoid duplication
16
+ return ''
17
+ end
18
+
19
+ if xml_mode?
20
+ render_as_xml('ai-msg', content, { speaker: 'ai' })
21
+ else
22
+ content
23
+ end
24
+ end
25
+ end
26
+
27
+ # Human Message component for wrapping user messages
28
+ class HumanMessageComponent < Component
29
+ def render
30
+ apply_stylesheet
31
+
32
+ content = @element.content.empty? ? render_children : @element.content
33
+
34
+ # Add to structured chat messages if context supports it
35
+ if @context.respond_to?(:chat_messages)
36
+ @context.chat_messages << {
37
+ 'role' => 'user',
38
+ 'content' => content
39
+ }
40
+ # Return empty for raw format to avoid duplication
41
+ return ''
42
+ end
43
+
44
+ if xml_mode?
45
+ render_as_xml('user-msg', content, { speaker: 'human' })
46
+ else
47
+ content
48
+ end
49
+ end
50
+ end
51
+
52
+ # System Message component for wrapping system messages
53
+ class SystemMessageComponent < Component
54
+ def render
55
+ apply_stylesheet
56
+
57
+ content = @element.content.empty? ? render_children : @element.content
58
+
59
+ # Add to structured chat messages if context supports it
60
+ if @context.respond_to?(:chat_messages)
61
+ @context.chat_messages << {
62
+ 'role' => 'system',
63
+ 'content' => content
64
+ }
65
+ # Return empty for raw format to avoid duplication
66
+ return ''
67
+ end
68
+
69
+ if xml_mode?
70
+ render_as_xml('system-msg', content, { speaker: 'system' })
71
+ else
72
+ content
73
+ end
74
+ end
75
+ end
76
+
77
+ # Message Content component for displaying message content
78
+ class MessageContentComponent < Component
79
+ def render
80
+ apply_stylesheet
81
+
82
+ content_attr = get_attribute('content')
83
+
84
+ if content_attr.is_a?(Array)
85
+ # Handle array of content items
86
+ content_attr.map { |item|
87
+ item.is_a?(String) ? item : item.to_s
88
+ }.join('')
89
+ elsif content_attr.is_a?(String)
90
+ content_attr
91
+ else
92
+ content_attr.to_s
93
+ end
94
+ end
95
+ end
96
+
97
+ # Conversation component for displaying chat conversations
98
+ class ConversationComponent < Component
99
+ def render
100
+ apply_stylesheet
101
+
102
+ messages_attr = get_attribute('messages')
103
+ messages = if messages_attr.is_a?(String)
104
+ begin
105
+ require 'json'
106
+ JSON.parse(messages_attr)
107
+ rescue JSON::ParserError
108
+ []
109
+ end
110
+ else
111
+ messages_attr || []
112
+ end
113
+
114
+ selected_messages = get_attribute('selectedMessages')
115
+
116
+ # Apply message selection if specified
117
+ if selected_messages
118
+ messages = apply_message_selection(messages, selected_messages)
119
+ end
120
+
121
+ if xml_mode?
122
+ render_conversation_xml(messages)
123
+ else
124
+ render_conversation_markdown(messages)
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ def apply_message_selection(messages, selection)
131
+ return messages unless selection
132
+
133
+ if selection.include?(':')
134
+ # Handle slice notation like "2:4" or "-6:"
135
+ start_idx, end_idx = parse_slice(selection, messages.length)
136
+ messages[start_idx...end_idx] || []
137
+ elsif selection.is_a?(Integer)
138
+ # Single message index
139
+ [messages[selection]].compact
140
+ else
141
+ messages
142
+ end
143
+ end
144
+
145
+ def parse_slice(slice_str, total_length)
146
+ if slice_str.start_with?('-') && slice_str.end_with?(':')
147
+ # Handle "-6:" (last 6 messages)
148
+ count = slice_str[1..-2].to_i
149
+ [total_length - count, total_length]
150
+ elsif slice_str.include?(':')
151
+ parts = slice_str.split(':')
152
+ start_idx = parts[0].empty? ? 0 : parts[0].to_i
153
+ end_idx = parts[1].empty? ? total_length : parts[1].to_i
154
+ [start_idx, end_idx]
155
+ else
156
+ index = slice_str.to_i
157
+ [index, index + 1]
158
+ end
159
+ end
160
+
161
+ def render_conversation_xml(messages)
162
+ result = ['<conversation>']
163
+ messages.each do |msg|
164
+ speaker = msg['speaker'] || 'human'
165
+ content = msg['content'] || ''
166
+ result << " <msg speaker=\"#{speaker}\">#{escape_xml(content)}</msg>"
167
+ end
168
+ result << '</conversation>'
169
+ result.join("\n")
170
+ end
171
+
172
+ def render_conversation_markdown(messages)
173
+ result = []
174
+ messages.each do |msg|
175
+ speaker = msg['speaker'] || 'human'
176
+ content = msg['content'] || ''
177
+
178
+ case speaker.downcase
179
+ when 'human', 'user'
180
+ result << "**Human:** #{content}"
181
+ when 'ai', 'assistant'
182
+ result << "**Assistant:** #{content}"
183
+ when 'system'
184
+ result << "**System:** #{content}"
185
+ else
186
+ result << "**#{speaker.capitalize}:** #{content}"
187
+ end
188
+ result << ""
189
+ end
190
+ result.join("\n")
191
+ end
192
+
193
+ def escape_xml(text)
194
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
195
+ end
196
+ end
197
+
198
+ # Folder component for displaying directory structures
199
+ class FolderComponent < Component
200
+ require 'find'
201
+
202
+ def render
203
+ apply_stylesheet
204
+
205
+ src = get_attribute('src')
206
+ filter = get_attribute('filter')
207
+ max_depth = get_attribute('maxDepth', 3).to_i
208
+ show_content = get_attribute('showContent', false)
209
+ syntax = get_attribute('syntax', 'text')
210
+
211
+ return '[Folder: no src specified]' unless src
212
+ return '[Folder: directory not found]' unless Dir.exist?(src)
213
+
214
+ tree_data = build_tree_structure(src, filter, max_depth, show_content)
215
+
216
+ if xml_mode?
217
+ render_folder_xml(tree_data)
218
+ else
219
+ case syntax
220
+ when 'markdown'
221
+ render_folder_markdown(tree_data)
222
+ when 'json'
223
+ require 'json'
224
+ JSON.pretty_generate(tree_data)
225
+ else
226
+ render_folder_text(tree_data)
227
+ end
228
+ end
229
+ end
230
+
231
+ private
232
+
233
+ def build_tree_structure(path, filter, max_depth, show_content, current_depth = 0)
234
+ return nil if current_depth >= max_depth
235
+
236
+ items = []
237
+
238
+ begin
239
+ Dir.entries(path).sort.each do |entry|
240
+ next if entry.start_with?('.')
241
+
242
+ full_path = File.join(path, entry)
243
+
244
+ if File.directory?(full_path)
245
+ # Check if directory should be included
246
+ if !filter || entry.match?(Regexp.new(filter))
247
+ if current_depth + 1 < max_depth
248
+ # If we can go deeper, recurse and include subdirectories
249
+ sub_items = build_tree_structure(full_path, filter, max_depth, show_content, current_depth + 1)
250
+ items << {
251
+ name: "#{entry}/",
252
+ type: 'directory',
253
+ children: sub_items || []
254
+ }
255
+ else
256
+ # At max depth, just show the directory without recursing
257
+ items << {
258
+ name: "#{entry}/",
259
+ type: 'directory',
260
+ children: []
261
+ }
262
+ end
263
+ end
264
+ else
265
+ # Check if file should be included
266
+ if !filter || entry.match?(Regexp.new(filter))
267
+ item = {
268
+ name: entry,
269
+ type: 'file'
270
+ }
271
+
272
+ if show_content
273
+ begin
274
+ content = File.read(full_path, encoding: 'utf-8')
275
+ item[:content] = content
276
+ rescue
277
+ item[:content] = '[Binary file or read error]'
278
+ end
279
+ end
280
+
281
+ items << item
282
+ end
283
+ end
284
+ end
285
+ rescue => e
286
+ return [{ name: "[Error: #{e.message}]", type: 'error' }]
287
+ end
288
+
289
+ items
290
+ end
291
+
292
+ def render_folder_text(items, indent = 0)
293
+ result = []
294
+ prefix = ' ' * indent
295
+
296
+ items.each do |item|
297
+ result << "#{prefix}#{item[:name]}"
298
+
299
+ if item[:content]
300
+ content_lines = item[:content].split("\n")
301
+ content_lines.each do |line|
302
+ result << "#{prefix} #{line}"
303
+ end
304
+ result << ""
305
+ end
306
+
307
+ if item[:children]
308
+ result << render_folder_text(item[:children], indent + 1)
309
+ end
310
+ end
311
+
312
+ result.join("\n")
313
+ end
314
+
315
+ def render_folder_markdown(items, indent = 0)
316
+ result = []
317
+ prefix = ' ' * indent
318
+
319
+ items.each do |item|
320
+ if item[:type] == 'directory'
321
+ result << "#{prefix}- **#{item[:name]}**"
322
+ else
323
+ result << "#{prefix}- #{item[:name]}"
324
+ end
325
+
326
+ if item[:content]
327
+ result << "#{prefix} ```"
328
+ item[:content].split("\n").each do |line|
329
+ result << "#{prefix} #{line}"
330
+ end
331
+ result << "#{prefix} ```"
332
+ result << ""
333
+ end
334
+
335
+ if item[:children]
336
+ result << render_folder_markdown(item[:children], indent + 1)
337
+ end
338
+ end
339
+
340
+ result.join("\n")
341
+ end
342
+
343
+ def render_folder_xml(items)
344
+ result = ['<folder>']
345
+ items.each do |item|
346
+ if item[:type] == 'directory'
347
+ result << " <directory name=\"#{escape_xml(item[:name])}\">"
348
+ if item[:children]
349
+ render_folder_xml_items(item[:children], result, 2)
350
+ end
351
+ result << " </directory>"
352
+ else
353
+ if item[:content]
354
+ result << " <file name=\"#{escape_xml(item[:name])}\">"
355
+ result << " <content>#{escape_xml(item[:content])}</content>"
356
+ result << " </file>"
357
+ else
358
+ result << " <file name=\"#{escape_xml(item[:name])}\"/>"
359
+ end
360
+ end
361
+ end
362
+ result << '</folder>'
363
+ result.join("\n")
364
+ end
365
+
366
+ def render_folder_xml_items(items, result, indent_level)
367
+ indent = ' ' * indent_level
368
+ items.each do |item|
369
+ if item[:type] == 'directory'
370
+ result << "#{indent}<directory name=\"#{escape_xml(item[:name])}\">"
371
+ if item[:children]
372
+ render_folder_xml_items(item[:children], result, indent_level + 1)
373
+ end
374
+ result << "#{indent}</directory>"
375
+ else
376
+ if item[:content]
377
+ result << "#{indent}<file name=\"#{escape_xml(item[:name])}\">"
378
+ result << "#{indent} <content>#{escape_xml(item[:content])}</content>"
379
+ result << "#{indent}</file>"
380
+ else
381
+ result << "#{indent}<file name=\"#{escape_xml(item[:name])}\"/>"
382
+ end
383
+ end
384
+ end
385
+ end
386
+
387
+ def escape_xml(text)
388
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
389
+ end
390
+ end
391
+
392
+ # Tree component for rendering tree structures
393
+ class TreeComponent < Component
394
+ def render
395
+ apply_stylesheet
396
+
397
+ items_attr = get_attribute('items')
398
+ items = if items_attr.is_a?(String)
399
+ begin
400
+ require 'json'
401
+ JSON.parse(items_attr)
402
+ rescue JSON::ParserError
403
+ []
404
+ end
405
+ else
406
+ items_attr || []
407
+ end
408
+
409
+ show_content = get_attribute('showContent', false)
410
+ syntax = get_attribute('syntax', 'text')
411
+
412
+ if xml_mode?
413
+ render_tree_xml(items, show_content)
414
+ else
415
+ case syntax
416
+ when 'markdown'
417
+ render_tree_markdown(items, show_content)
418
+ when 'json'
419
+ require 'json'
420
+ JSON.pretty_generate(items)
421
+ else
422
+ render_tree_text(items, show_content)
423
+ end
424
+ end
425
+ end
426
+
427
+ private
428
+
429
+ def render_tree_text(items, show_content, indent = 0)
430
+ result = []
431
+ prefix = ' ' * indent
432
+
433
+ items.each do |item|
434
+ result << "#{prefix}#{item['name'] || item[:name]}"
435
+
436
+ if show_content && (item['content'] || item[:content])
437
+ content = item['content'] || item[:content]
438
+ content.to_s.split("\n").each do |line|
439
+ result << "#{prefix} #{line}"
440
+ end
441
+ end
442
+
443
+ children = item['children'] || item[:children]
444
+ if children && !children.empty?
445
+ result << render_tree_text(children, show_content, indent + 1)
446
+ end
447
+ end
448
+
449
+ result.join("\n")
450
+ end
451
+
452
+ def render_tree_markdown(items, show_content, indent = 0)
453
+ result = []
454
+ prefix = ' ' * indent
455
+
456
+ items.each do |item|
457
+ name = item['name'] || item[:name]
458
+ result << "#{prefix}- #{name}"
459
+
460
+ if show_content && (item['content'] || item[:content])
461
+ content = item['content'] || item[:content]
462
+ result << "#{prefix} ```"
463
+ content.to_s.split("\n").each do |line|
464
+ result << "#{prefix} #{line}"
465
+ end
466
+ result << "#{prefix} ```"
467
+ end
468
+
469
+ children = item['children'] || item[:children]
470
+ if children && !children.empty?
471
+ result << render_tree_markdown(children, show_content, indent + 1)
472
+ end
473
+ end
474
+
475
+ result.join("\n")
476
+ end
477
+
478
+ def render_tree_xml(items, show_content, indent_level = 1)
479
+ result = ['<tree>']
480
+ render_tree_xml_items(items, result, show_content, indent_level)
481
+ result << '</tree>'
482
+ result.join("\n")
483
+ end
484
+
485
+ def render_tree_xml_items(items, result, show_content, indent_level)
486
+ indent = ' ' * indent_level
487
+
488
+ items.each do |item|
489
+ name = item['name'] || item[:name]
490
+ children = item['children'] || item[:children]
491
+ content = item['content'] || item[:content] if show_content
492
+
493
+ if children && !children.empty?
494
+ result << "#{indent}<item name=\"#{escape_xml(name)}\">"
495
+ if content
496
+ result << "#{indent} <content>#{escape_xml(content)}</content>"
497
+ end
498
+ render_tree_xml_items(children, result, show_content, indent_level + 1)
499
+ result << "#{indent}</item>"
500
+ else
501
+ if content
502
+ result << "#{indent}<item name=\"#{escape_xml(name)}\">"
503
+ result << "#{indent} <content>#{escape_xml(content)}</content>"
504
+ result << "#{indent}</item>"
505
+ else
506
+ result << "#{indent}<item name=\"#{escape_xml(name)}\"/>"
507
+ end
508
+ end
509
+ end
510
+ end
511
+
512
+ def escape_xml(text)
513
+ text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
514
+ end
515
+ end
516
+
517
+ # File component for reading and including file contents
518
+ class FileComponent < Component
519
+ def render
520
+ apply_stylesheet
521
+
522
+ src = get_attribute('src')
523
+
524
+ # Handle missing src attribute
525
+ unless src
526
+ return handle_error("no src specified")
527
+ end
528
+
529
+ # Resolve file path
530
+ file_path = resolve_file_path(src)
531
+
532
+ # Check if file exists
533
+ unless File.exist?(file_path)
534
+ return handle_error("file not found: #{src}")
535
+ end
536
+
537
+ # Read file content
538
+ begin
539
+ content = File.read(file_path, encoding: 'utf-8')
540
+
541
+ if xml_mode?
542
+ render_as_xml('file', content, { src: src })
543
+ else
544
+ content
545
+ end
546
+ rescue => e
547
+ handle_error("error reading file: #{e.message}")
548
+ end
549
+ end
550
+
551
+ private
552
+
553
+ def resolve_file_path(src)
554
+ # Handle absolute paths
555
+ return src if src.start_with?('/')
556
+
557
+ # Handle relative paths - try relative to source file first
558
+ if @context.source_path
559
+ base_path = File.dirname(@context.source_path)
560
+ candidate_path = File.join(base_path, src)
561
+ return candidate_path if File.exist?(candidate_path)
562
+ end
563
+
564
+ # Try relative to current working directory
565
+ candidate_path = File.join(Dir.pwd, src)
566
+ return candidate_path if File.exist?(candidate_path)
567
+
568
+ # Return the original path for final existence check
569
+ src
570
+ end
571
+
572
+ def handle_error(message)
573
+ if xml_mode?
574
+ render_as_xml('file-error', message)
575
+ else
576
+ "[File: #{message}]"
577
+ end
578
+ end
579
+ end
580
+ end
@@ -9,39 +9,130 @@ require_relative 'components/lists'
9
9
  require_relative 'components/layout'
10
10
  require_relative 'components/workflow'
11
11
  require_relative 'components/styling'
12
+ require_relative 'components/formatting'
13
+ require_relative 'components/media'
14
+ require_relative 'components/utilities'
15
+ require_relative 'components/meta'
16
+ require_relative 'components/template'
12
17
 
13
18
  module Poml
14
19
  # Update the component mapping after all components are loaded
15
20
  Components::COMPONENT_MAPPING.merge!({
21
+ # Basic components
16
22
  text: TextComponent,
23
+ poml: TextComponent,
24
+ p: ParagraphComponent,
25
+
26
+ # Formatting components
27
+ b: BoldComponent,
28
+ bold: BoldComponent,
29
+ i: ItalicComponent,
30
+ italic: ItalicComponent,
31
+ u: UnderlineComponent,
32
+ underline: UnderlineComponent,
33
+ s: StrikethroughComponent,
34
+ strike: StrikethroughComponent,
35
+ strikethrough: StrikethroughComponent,
36
+ span: InlineComponent,
37
+ inline: InlineComponent,
38
+ h: HeaderComponent,
39
+ header: HeaderComponent,
40
+ br: NewlineComponent,
41
+ newline: NewlineComponent,
42
+ code: CodeComponent,
43
+ section: SubContentComponent,
44
+ subcontent: SubContentComponent,
45
+
46
+ # Media components
47
+ audio: AudioComponent,
48
+
49
+ # Instruction components
17
50
  role: RoleComponent,
18
51
  task: TaskComponent,
19
52
  hint: HintComponent,
53
+
54
+ # Content components
20
55
  document: DocumentComponent,
21
56
  Document: DocumentComponent, # Capitalized version
57
+
58
+ # Data components
22
59
  table: TableComponent,
23
60
  Table: TableComponent, # Capitalized version
24
61
  img: ImageComponent,
25
- p: ParagraphComponent,
62
+ obj: ObjectComponent,
63
+ object: ObjectComponent,
64
+ dataobj: ObjectComponent,
65
+ 'data-obj': ObjectComponent,
66
+ webpage: WebpageComponent,
67
+
68
+ # Example components
26
69
  example: ExampleComponent,
27
70
  input: InputComponent,
28
71
  output: OutputComponent,
29
72
  'output-format': OutputFormatComponent,
30
73
  'outputformat': OutputFormatComponent,
74
+ examples: ExampleSetComponent,
75
+ 'example-set': ExampleSetComponent,
76
+ introducer: IntroducerComponent,
77
+
78
+ # List components
31
79
  list: ListComponent,
32
80
  item: ItemComponent,
81
+
82
+ # Layout components
33
83
  cp: CPComponent,
84
+ 'captioned-paragraph': CPComponent,
85
+
86
+ # Workflow components
34
87
  'stepwise-instructions': StepwiseInstructionsComponent,
35
88
  'stepwiseinstructions': StepwiseInstructionsComponent,
36
89
  StepwiseInstructions: StepwiseInstructionsComponent,
37
90
  'human-message': HumanMessageComponent,
38
91
  'humanmessage': HumanMessageComponent,
39
92
  HumanMessage: HumanMessageComponent,
93
+ 'user-msg': HumanMessageComponent,
40
94
  qa: QAComponent,
41
95
  QA: QAComponent,
96
+ question: QAComponent,
97
+
98
+ # Utility components
99
+ 'ai-msg': AiMessageComponent,
100
+ 'aimessage': AiMessageComponent,
101
+ ai: AiMessageComponent,
102
+ Ai: AiMessageComponent,
103
+ human: HumanMessageComponent,
104
+ Human: HumanMessageComponent,
105
+ system: SystemMessageComponent,
106
+ System: SystemMessageComponent,
107
+ 'system-msg': SystemMessageComponent,
108
+ 'systemmessage': SystemMessageComponent,
109
+ 'msg-content': MessageContentComponent,
110
+ 'message-content': MessageContentComponent,
111
+ conversation: ConversationComponent,
112
+ Conversation: ConversationComponent,
113
+ folder: FolderComponent,
114
+ Folder: FolderComponent,
115
+ tree: TreeComponent,
116
+ Tree: TreeComponent,
117
+ file: FileComponent,
118
+ File: FileComponent,
119
+
120
+ # Styling components
42
121
  let: LetComponent,
43
122
  Let: LetComponent,
44
123
  stylesheet: StylesheetComponent,
45
- Stylesheet: StylesheetComponent
124
+ Stylesheet: StylesheetComponent,
125
+
126
+ # Meta components
127
+ meta: MetaComponent,
128
+ Meta: MetaComponent,
129
+
130
+ # Template components
131
+ include: IncludeComponent,
132
+ Include: IncludeComponent,
133
+ if: IfComponent,
134
+ If: IfComponent,
135
+ for: ForComponent,
136
+ For: ForComponent
46
137
  })
47
138
  end