prosereflect 0.1.0 → 0.1.1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +23 -4
  3. data/README.adoc +193 -12
  4. data/lib/prosereflect/attribute/base.rb +34 -0
  5. data/lib/prosereflect/attribute/bold.rb +20 -0
  6. data/lib/prosereflect/attribute/href.rb +24 -0
  7. data/lib/prosereflect/attribute/id.rb +24 -0
  8. data/lib/prosereflect/attribute.rb +13 -0
  9. data/lib/prosereflect/blockquote.rb +85 -0
  10. data/lib/prosereflect/bullet_list.rb +83 -0
  11. data/lib/prosereflect/code_block.rb +135 -0
  12. data/lib/prosereflect/code_block_wrapper.rb +66 -0
  13. data/lib/prosereflect/document.rb +99 -24
  14. data/lib/prosereflect/hard_break.rb +11 -9
  15. data/lib/prosereflect/heading.rb +64 -0
  16. data/lib/prosereflect/horizontal_rule.rb +70 -0
  17. data/lib/prosereflect/image.rb +126 -0
  18. data/lib/prosereflect/input/html.rb +505 -0
  19. data/lib/prosereflect/list_item.rb +65 -0
  20. data/lib/prosereflect/mark/base.rb +49 -0
  21. data/lib/prosereflect/mark/bold.rb +15 -0
  22. data/lib/prosereflect/mark/code.rb +14 -0
  23. data/lib/prosereflect/mark/italic.rb +15 -0
  24. data/lib/prosereflect/mark/link.rb +18 -0
  25. data/lib/prosereflect/mark/strike.rb +15 -0
  26. data/lib/prosereflect/mark/subscript.rb +15 -0
  27. data/lib/prosereflect/mark/superscript.rb +15 -0
  28. data/lib/prosereflect/mark/underline.rb +15 -0
  29. data/lib/prosereflect/mark.rb +11 -0
  30. data/lib/prosereflect/node.rb +181 -32
  31. data/lib/prosereflect/ordered_list.rb +85 -0
  32. data/lib/prosereflect/output/html.rb +374 -0
  33. data/lib/prosereflect/paragraph.rb +26 -15
  34. data/lib/prosereflect/parser.rb +111 -24
  35. data/lib/prosereflect/table.rb +40 -9
  36. data/lib/prosereflect/table_cell.rb +33 -8
  37. data/lib/prosereflect/table_header.rb +92 -0
  38. data/lib/prosereflect/table_row.rb +31 -8
  39. data/lib/prosereflect/text.rb +13 -17
  40. data/lib/prosereflect/user.rb +63 -0
  41. data/lib/prosereflect/version.rb +1 -1
  42. data/lib/prosereflect.rb +6 -0
  43. data/prosereflect.gemspec +1 -0
  44. data/spec/prosereflect/document_spec.rb +436 -36
  45. data/spec/prosereflect/hard_break_spec.rb +218 -22
  46. data/spec/prosereflect/input/html_spec.rb +797 -0
  47. data/spec/prosereflect/node_spec.rb +258 -89
  48. data/spec/prosereflect/output/html_spec.rb +369 -0
  49. data/spec/prosereflect/paragraph_spec.rb +424 -49
  50. data/spec/prosereflect/parser_spec.rb +304 -91
  51. data/spec/prosereflect/table_cell_spec.rb +268 -57
  52. data/spec/prosereflect/table_row_spec.rb +210 -40
  53. data/spec/prosereflect/table_spec.rb +392 -61
  54. data/spec/prosereflect/text_spec.rb +206 -48
  55. data/spec/prosereflect/user_spec.rb +73 -0
  56. data/spec/prosereflect_spec.rb +5 -0
  57. data/spec/support/shared_examples.rb +44 -15
  58. metadata +47 -3
  59. data/debug_loading.rb +0 -34
@@ -1,15 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'spec_helper'
4
+
3
5
  RSpec.describe Prosereflect::Node do
4
6
  describe 'initialization' do
5
7
  it 'initializes with empty data' do
6
8
  node = described_class.new
7
9
  expect(node.type).to be_nil
8
10
  expect(node.attrs).to be_nil
9
- expect(node.content).to eq([])
11
+ expect(node.content).to be_nil
10
12
  expect(node.marks).to be_nil
11
13
  end
12
14
 
15
+ # TODO: Update to lutaml-model
13
16
  it 'initializes with provided data' do
14
17
  data = {
15
18
  'type' => 'test_node',
@@ -47,29 +50,29 @@ RSpec.describe Prosereflect::Node do
47
50
  describe '#to_h' do
48
51
  it 'creates a hash representation with basic properties' do
49
52
  node = described_class.new({ 'type' => 'test_node' })
50
- hash = node.to_h
53
+ hash = node.to_hash
51
54
 
52
55
  expect(hash).to be_a(Hash)
53
56
  expect(hash['type']).to eq('test_node')
54
57
  end
55
58
 
56
59
  it 'includes attrs when present' do
57
- node = described_class.new({
58
- 'type' => 'test_node',
59
- 'attrs' => { 'key' => 'value' }
60
- })
60
+ node = described_class.new(
61
+ type: Prosereflect::Text.new(text: 'Hello'),
62
+ attrs: [Prosereflect::Attribute::Href.new('https://example.com')]
63
+ )
61
64
 
62
- hash = node.to_h
63
- expect(hash['attrs']).to eq({ 'key' => 'value' })
65
+ hash = node.to_hash
66
+ expect(hash['attrs']).to eq([{ 'href' => 'https://example.com' }])
64
67
  end
65
68
 
66
69
  it 'includes marks when present' do
67
- node = described_class.new({
68
- 'type' => 'test_node',
69
- 'marks' => [{ 'type' => 'bold' }]
70
- })
70
+ node = described_class.new(
71
+ type: Prosereflect::Text.new(text: 'Hello'),
72
+ marks: [Prosereflect::Mark::Bold.new]
73
+ )
71
74
 
72
- hash = node.to_h
75
+ hash = node.to_hash
73
76
  expect(hash['marks']).to eq([{ 'type' => 'bold' }])
74
77
  end
75
78
 
@@ -79,7 +82,7 @@ RSpec.describe Prosereflect::Node do
79
82
  'content' => [{ 'type' => 'text', 'text' => 'Hello' }]
80
83
  })
81
84
 
82
- hash = node.to_h
85
+ hash = node.to_hash
83
86
  expect(hash['content']).to be_an(Array)
84
87
  expect(hash['content'][0]['type']).to eq('text')
85
88
  end
@@ -139,118 +142,284 @@ RSpec.describe Prosereflect::Node do
139
142
  end
140
143
 
141
144
  describe '.create' do
142
- it 'creates a node with the specified type' do
145
+ it 'creates a simple node' do
143
146
  node = described_class.create('test_node')
144
- expect(node.type).to eq('test_node')
147
+
148
+ expected = {
149
+ 'type' => 'test_node'
150
+ }
151
+
152
+ expect(node.to_h).to eq(expected)
145
153
  end
146
154
 
147
155
  it 'creates a node with attributes' do
148
- attrs = { 'key' => 'value' }
149
- node = described_class.create('test_node', attrs)
156
+ node = described_class.create('test_node', {
157
+ 'key' => 'value',
158
+ 'number' => 42,
159
+ 'flag' => true
160
+ })
150
161
 
151
- expect(node.type).to eq('test_node')
152
- expect(node.attrs).to eq(attrs)
153
- end
162
+ expected = {
163
+ 'type' => 'test_node',
164
+ 'attrs' => {
165
+ 'key' => 'value',
166
+ 'number' => 42,
167
+ 'flag' => true
168
+ }
169
+ }
154
170
 
155
- it 'initializes with empty content' do
156
- node = described_class.create('test_node')
157
- expect(node.content).to eq([])
171
+ expect(node.to_h).to eq(expected)
158
172
  end
159
173
  end
160
174
 
161
- describe '#find_all' do
162
- let(:node) do
163
- root = described_class.new({ 'type' => 'root' })
175
+ describe 'node structure' do
176
+ it 'creates a node with content' do
177
+ node = described_class.create('parent')
178
+ node.add_child(Prosereflect::Text.create('First child'))
179
+ node.add_child(Prosereflect::Text.create('Second child'))
180
+
181
+ expected = {
182
+ 'type' => 'parent',
183
+ 'content' => [
184
+ {
185
+ 'type' => 'text',
186
+ 'text' => 'First child'
187
+ },
188
+ {
189
+ 'type' => 'text',
190
+ 'text' => 'Second child'
191
+ }
192
+ ]
193
+ }
164
194
 
165
- para1 = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
166
- para1.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Text 1' }))
195
+ expect(node.to_h).to eq(expected)
196
+ end
167
197
 
168
- para2 = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
169
- para2.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Text 2' }))
198
+ it 'creates a node with complex content' do
199
+ node = described_class.create('root')
170
200
 
171
- root.add_child(para1)
172
- root.add_child(para2)
173
- root
174
- end
201
+ # Add a paragraph with formatted text
202
+ para = Prosereflect::Paragraph.create
203
+ para.add_child(Prosereflect::Text.create('Bold', [Prosereflect::Mark::Bold.create]))
204
+ para.add_child(Prosereflect::Text.create(' and '))
205
+ para.add_child(Prosereflect::Text.create('italic', [Prosereflect::Mark::Italic.create]))
206
+ node.add_child(para)
175
207
 
176
- it 'finds all nodes of a specific type' do
177
- paragraphs = node.find_all('paragraph')
178
- expect(paragraphs.size).to eq(2)
179
- expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
208
+ # Add a list
209
+ list = Prosereflect::BulletList.create
210
+ list_item = Prosereflect::ListItem.create
211
+ list_item.add_child(Prosereflect::Paragraph.create)
212
+ list_item.content.first.add_child(Prosereflect::Text.create('List item'))
213
+ list.add_child(list_item)
214
+ node.add_child(list)
215
+
216
+ expected = {
217
+ 'type' => 'root',
218
+ 'content' => [
219
+ {
220
+ 'type' => 'paragraph',
221
+ 'content' => [
222
+ {
223
+ 'type' => 'text',
224
+ 'text' => 'Bold',
225
+ 'marks' => [{ 'type' => 'bold' }]
226
+ },
227
+ {
228
+ 'type' => 'text',
229
+ 'text' => ' and '
230
+ },
231
+ {
232
+ 'type' => 'text',
233
+ 'text' => 'italic',
234
+ 'marks' => [{ 'type' => 'italic' }]
235
+ }
236
+ ]
237
+ },
238
+ {
239
+ 'type' => 'bullet_list',
240
+ 'attrs' => {
241
+ 'bullet_style' => nil
242
+ },
243
+ 'content' => [
244
+ {
245
+ 'type' => 'list_item',
246
+ 'content' => [
247
+ {
248
+ 'type' => 'paragraph',
249
+ 'content' => [
250
+ {
251
+ 'type' => 'text',
252
+ 'text' => 'List item'
253
+ }
254
+ ]
255
+ }
256
+ ]
257
+ }
258
+ ]
259
+ }
260
+ ]
261
+ }
262
+
263
+ expect(node.to_h).to eq(expected)
180
264
  end
265
+ end
181
266
 
182
- it 'finds all nested nodes of a specific type' do
183
- texts = node.find_all('text')
184
- expect(texts.size).to eq(2)
185
- expect(texts).to all(be_a(Prosereflect::Text))
267
+ describe 'node operations' do
268
+ describe '#add_child' do
269
+ it 'adds a child node and returns it' do
270
+ parent = described_class.create('parent')
271
+ child = Prosereflect::Text.create('Child node')
272
+
273
+ result = parent.add_child(child)
274
+ expect(result).to eq(child)
275
+ expect(parent.content).to eq([child])
276
+ end
277
+
278
+ it 'maintains child order' do
279
+ parent = described_class.create('parent')
280
+ first = Prosereflect::Text.create('First')
281
+ second = Prosereflect::Text.create('Second')
282
+ third = Prosereflect::Text.create('Third')
283
+
284
+ parent.add_child(first)
285
+ parent.add_child(second)
286
+ parent.add_child(third)
287
+
288
+ expect(parent.content).to eq([first, second, third])
289
+ expect(parent.text_content).to eq('FirstSecondThird')
290
+ end
186
291
  end
187
292
 
188
- it 'returns empty array if no matching nodes are found' do
189
- result = node.find_all('nonexistent')
190
- expect(result).to eq([])
293
+ describe '#find_first' do
294
+ let(:node) do
295
+ root = described_class.create('root')
296
+ para = Prosereflect::Paragraph.create
297
+ text = Prosereflect::Text.create('Hello')
298
+ para.add_child(text)
299
+ root.add_child(para)
300
+ root
301
+ end
302
+
303
+ it 'finds nodes by type' do
304
+ expect(node.find_first('root')).to eq(node)
305
+ expect(node.find_first('paragraph')).to be_a(Prosereflect::Paragraph)
306
+ expect(node.find_first('text')).to be_a(Prosereflect::Text)
307
+ expect(node.find_first('nonexistent')).to be_nil
308
+ end
191
309
  end
192
- end
193
310
 
194
- describe '#find_children' do
195
- let(:node) do
196
- root = described_class.new({ 'type' => 'root' })
311
+ describe '#find_all' do
312
+ let(:node) do
313
+ root = described_class.create('root')
197
314
 
198
- root.add_child(Prosereflect::Paragraph.new({ 'type' => 'paragraph' }))
199
- root.add_child(Prosereflect::Table.new({ 'type' => 'table' }))
200
- root.add_child(Prosereflect::Paragraph.new({ 'type' => 'paragraph' }))
315
+ # First paragraph
316
+ para1 = Prosereflect::Paragraph.create
317
+ para1.add_child(Prosereflect::Text.create('First'))
318
+ root.add_child(para1)
201
319
 
202
- root
203
- end
320
+ # Second paragraph
321
+ para2 = Prosereflect::Paragraph.create
322
+ para2.add_child(Prosereflect::Text.create('Second'))
323
+ root.add_child(para2)
204
324
 
205
- it 'finds direct children of a specific type' do
206
- paragraphs = node.find_children('paragraph')
207
- expect(paragraphs.size).to eq(2)
208
- expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
209
- end
325
+ root
326
+ end
210
327
 
211
- it 'returns empty array if no matching children are found' do
212
- result = node.find_children('nonexistent')
213
- expect(result).to eq([])
328
+ it 'finds all nodes of a type' do
329
+ expect(node.find_all('paragraph').size).to eq(2)
330
+ expect(node.find_all('text').size).to eq(2)
331
+ expect(node.find_all('nonexistent')).to eq([])
332
+ end
214
333
  end
215
- end
216
334
 
217
- describe '#text_content' do
218
- it 'returns empty string for node without content' do
219
- node = described_class.new({ 'type' => 'empty' })
220
- expect(node.text_content).to eq('')
335
+ describe '#find_children' do
336
+ let(:node) do
337
+ root = described_class.create('root')
338
+ root.add_child(Prosereflect::Paragraph.create)
339
+ root.add_child(Prosereflect::Table.create)
340
+ root.add_child(Prosereflect::Paragraph.create)
341
+ root
342
+ end
343
+
344
+ it 'finds direct children by class' do
345
+ paragraphs = node.find_children(Prosereflect::Paragraph)
346
+ expect(paragraphs.size).to eq(2)
347
+ expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
348
+
349
+ tables = node.find_children(Prosereflect::Table)
350
+ expect(tables.size).to eq(1)
351
+ expect(tables.first).to be_a(Prosereflect::Table)
352
+ end
221
353
  end
222
354
 
223
- it 'concatenates text content from all child nodes' do
224
- node = described_class.new({ 'type' => 'parent' })
355
+ describe '#text_content' do
356
+ it 'concatenates text from all children' do
357
+ root = described_class.create('root')
225
358
 
226
- para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
227
- para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' }))
228
- para.add_child(Prosereflect::HardBreak.new({ 'type' => 'hard_break' }))
229
- para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'World' }))
359
+ para = Prosereflect::Paragraph.create
360
+ para.add_child(Prosereflect::Text.create('Hello'))
361
+ para.add_child(Prosereflect::HardBreak.create)
362
+ para.add_child(Prosereflect::Text.create('World'))
363
+ root.add_child(para)
230
364
 
231
- node.add_child(para)
365
+ expect(root.text_content).to eq("Hello\nWorld")
366
+ end
232
367
 
233
- expect(node.text_content).to eq("Hello\nWorld")
368
+ it 'returns empty string for empty node' do
369
+ node = described_class.create('empty')
370
+ expect(node.text_content).to eq('')
371
+ end
234
372
  end
235
373
  end
236
374
 
237
- describe '#text_content_with_breaks' do
238
- it 'returns empty string for node without content' do
239
- node = described_class.new({ 'type' => 'empty' })
240
- expect(node.text_content_with_breaks).to eq('')
241
- end
375
+ describe 'serialization' do
376
+ it 'serializes a node with all properties' do
377
+ node = described_class.create('test_node', {
378
+ 'key' => 'value',
379
+ 'number' => 42
380
+ })
242
381
 
243
- it 'includes newlines for hard breaks' do
244
- node = described_class.new({ 'type' => 'parent' })
382
+ text = Prosereflect::Text.create('Content', [
383
+ Prosereflect::Mark::Bold.create,
384
+ Prosereflect::Mark::Link.create({ 'href' => 'https://example.com' })
385
+ ])
245
386
 
246
- para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
247
- para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' }))
248
- para.add_child(Prosereflect::HardBreak.new({ 'type' => 'hard_break' }))
249
- para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'World' }))
387
+ node.add_child(text)
250
388
 
251
- node.add_child(para)
389
+ expected = {
390
+ 'type' => 'test_node',
391
+ 'attrs' => {
392
+ 'key' => 'value',
393
+ 'number' => 42
394
+ },
395
+ 'content' => [
396
+ {
397
+ 'type' => 'text',
398
+ 'text' => 'Content',
399
+ 'marks' => [
400
+ { 'type' => 'bold' },
401
+ {
402
+ 'type' => 'link',
403
+ 'attrs' => {
404
+ 'href' => 'https://example.com'
405
+ }
406
+ }
407
+ ]
408
+ }
409
+ ]
410
+ }
411
+
412
+ expect(node.to_h).to eq(expected)
413
+ end
414
+
415
+ it 'omits optional properties when empty' do
416
+ node = described_class.create('test_node')
417
+
418
+ expected = {
419
+ 'type' => 'test_node'
420
+ }
252
421
 
253
- expect(node.text_content_with_breaks).to eq("Hello\nWorld")
422
+ expect(node.to_h).to eq(expected)
254
423
  end
255
424
  end
256
425
  end