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
@@ -19,23 +19,415 @@ RSpec.describe Prosereflect::Document do
19
19
  end
20
20
 
21
21
  describe '.create' do
22
- it 'creates an empty document' do
23
- doc = described_class.create
24
- expect(doc).to be_a(described_class)
25
- expect(doc.type).to eq('doc')
26
- expect(doc.content).to be_empty
22
+ it 'creates a simple document with a paragraph' do
23
+ document = described_class.new
24
+ document.add_paragraph('This is a test paragraph.')
25
+
26
+ expected = {
27
+ 'type' => 'doc',
28
+ 'content' => [{
29
+ 'type' => 'paragraph',
30
+ 'content' => [{
31
+ 'type' => 'text',
32
+ 'text' => 'This is a test paragraph.'
33
+ }]
34
+ }]
35
+ }
36
+
37
+ expect(document.to_h).to eq(expected)
38
+ end
39
+
40
+ it 'creates a document with multiple paragraphs and styles' do
41
+ document = described_class.new
42
+
43
+ # Add a heading
44
+ heading = document.add_heading(1)
45
+ heading.add_text('Document Title')
46
+
47
+ # Add paragraphs with different styles
48
+ para1 = document.add_paragraph
49
+ para1.add_text('This is a paragraph with ')
50
+ para1.add_text('bold', [Prosereflect::Mark::Bold.new])
51
+ para1.add_text(' and ')
52
+ para1.add_text('italic', [Prosereflect::Mark::Italic.new])
53
+ para1.add_text(' text.')
54
+
55
+ para2 = document.add_paragraph
56
+ para2.add_text('This paragraph has ')
57
+ para2.add_text('underlined', [Prosereflect::Mark::Underline.new])
58
+ para2.add_text(' and ')
59
+ para2.add_text('struck through', [Prosereflect::Mark::Strike.new])
60
+ para2.add_text(' text.')
61
+
62
+ expected = {
63
+ 'type' => 'doc',
64
+ 'content' => [{
65
+ 'type' => 'heading',
66
+ 'attrs' => { 'level' => 1 },
67
+ 'content' => [{
68
+ 'type' => 'text',
69
+ 'text' => 'Document Title'
70
+ }]
71
+ }, {
72
+ 'type' => 'paragraph',
73
+ 'content' => [{
74
+ 'type' => 'text',
75
+ 'text' => 'This is a paragraph with '
76
+ }, {
77
+ 'type' => 'text',
78
+ 'text' => 'bold',
79
+ 'marks' => [{ 'type' => 'bold' }]
80
+ }, {
81
+ 'type' => 'text',
82
+ 'text' => ' and '
83
+ }, {
84
+ 'type' => 'text',
85
+ 'text' => 'italic',
86
+ 'marks' => [{ 'type' => 'italic' }]
87
+ }, {
88
+ 'type' => 'text',
89
+ 'text' => ' text.'
90
+ }]
91
+ }, {
92
+ 'type' => 'paragraph',
93
+ 'content' => [{
94
+ 'type' => 'text',
95
+ 'text' => 'This paragraph has '
96
+ }, {
97
+ 'type' => 'text',
98
+ 'text' => 'underlined',
99
+ 'marks' => [{ 'type' => 'underline' }]
100
+ }, {
101
+ 'type' => 'text',
102
+ 'text' => ' and '
103
+ }, {
104
+ 'type' => 'text',
105
+ 'text' => 'struck through',
106
+ 'marks' => [{ 'type' => 'strike' }]
107
+ }, {
108
+ 'type' => 'text',
109
+ 'text' => ' text.'
110
+ }]
111
+ }]
112
+ }
113
+
114
+ expect(document.to_h).to eq(expected)
115
+ end
116
+
117
+ it 'creates a document with tables and lists' do
118
+ document = described_class.new
119
+
120
+ # Add a table
121
+ table = document.add_table
122
+ table.add_header(%w[Product Price Quantity])
123
+ table.add_row(['Widget', '$10.00', '5'])
124
+ table.add_row(['Gadget', '$15.00', '3'])
125
+
126
+ # Add an ordered list
127
+ list = document.add_ordered_list
128
+ list.start = 1
129
+ list.add_item('First item')
130
+ list.add_item('Second item with ')
131
+ .add_text('emphasis', [Prosereflect::Mark::Italic.new])
132
+
133
+ expected = {
134
+ 'type' => 'doc',
135
+ 'content' => [{
136
+ 'type' => 'table',
137
+ 'content' => [{
138
+ 'type' => 'table_row',
139
+ 'content' => [{
140
+ 'type' => 'table_header',
141
+ 'content' => [{
142
+ 'type' => 'paragraph',
143
+ 'content' => [{
144
+ 'type' => 'text',
145
+ 'text' => 'Product'
146
+ }]
147
+ }]
148
+ }, {
149
+ 'type' => 'table_header',
150
+ 'content' => [{
151
+ 'type' => 'paragraph',
152
+ 'content' => [{
153
+ 'type' => 'text',
154
+ 'text' => 'Price'
155
+ }]
156
+ }]
157
+ }, {
158
+ 'type' => 'table_header',
159
+ 'content' => [{
160
+ 'type' => 'paragraph',
161
+ 'content' => [{
162
+ 'type' => 'text',
163
+ 'text' => 'Quantity'
164
+ }]
165
+ }]
166
+ }]
167
+ }, {
168
+ 'type' => 'table_row',
169
+ 'content' => [{
170
+ 'type' => 'table_cell',
171
+ 'content' => [{
172
+ 'type' => 'paragraph',
173
+ 'content' => [{
174
+ 'type' => 'text',
175
+ 'text' => 'Widget'
176
+ }]
177
+ }]
178
+ }, {
179
+ 'type' => 'table_cell',
180
+ 'content' => [{
181
+ 'type' => 'paragraph',
182
+ 'content' => [{
183
+ 'type' => 'text',
184
+ 'text' => '$10.00'
185
+ }]
186
+ }]
187
+ }, {
188
+ 'type' => 'table_cell',
189
+ 'content' => [{
190
+ 'type' => 'paragraph',
191
+ 'content' => [{
192
+ 'type' => 'text',
193
+ 'text' => '5'
194
+ }]
195
+ }]
196
+ }]
197
+ }, {
198
+ 'type' => 'table_row',
199
+ 'content' => [{
200
+ 'type' => 'table_cell',
201
+ 'content' => [{
202
+ 'type' => 'paragraph',
203
+ 'content' => [{
204
+ 'type' => 'text',
205
+ 'text' => 'Gadget'
206
+ }]
207
+ }]
208
+ }, {
209
+ 'type' => 'table_cell',
210
+ 'content' => [{
211
+ 'type' => 'paragraph',
212
+ 'content' => [{
213
+ 'type' => 'text',
214
+ 'text' => '$15.00'
215
+ }]
216
+ }]
217
+ }, {
218
+ 'type' => 'table_cell',
219
+ 'content' => [{
220
+ 'type' => 'paragraph',
221
+ 'content' => [{
222
+ 'type' => 'text',
223
+ 'text' => '3'
224
+ }]
225
+ }]
226
+ }]
227
+ }]
228
+ }, {
229
+ 'type' => 'ordered_list',
230
+ 'attrs' => { 'start' => 1 },
231
+ 'content' => [{
232
+ 'type' => 'list_item',
233
+ 'content' => [{
234
+ 'type' => 'paragraph',
235
+ 'content' => [{
236
+ 'type' => 'text',
237
+ 'text' => 'First item'
238
+ }]
239
+ }]
240
+ }, {
241
+ 'type' => 'list_item',
242
+ 'content' => [{
243
+ 'type' => 'paragraph',
244
+ 'content' => [{
245
+ 'type' => 'text',
246
+ 'text' => 'Second item with '
247
+ }, {
248
+ 'type' => 'text',
249
+ 'text' => 'emphasis',
250
+ 'marks' => [{ 'type' => 'italic' }]
251
+ }]
252
+ }]
253
+ }]
254
+ }]
255
+ }
256
+
257
+ expect(document.to_h).to eq(expected)
258
+ end
259
+
260
+ it 'creates a document with code blocks and blockquotes' do
261
+ document = described_class.create
262
+ wrapper = document.add_code_block_wrapper
263
+ wrapper.line_numbers = true
264
+
265
+ code_block = wrapper.add_code_block
266
+ code_block.language = 'ruby'
267
+ code_block.content = "def example\n puts 'Hello'\nend"
268
+
269
+ quote = document.add_blockquote
270
+ quote.citation = 'https://example.com'
271
+ quote.add_paragraph('A test quote')
272
+
273
+ expected = {
274
+ 'type' => 'doc',
275
+ 'content' => [{
276
+ 'type' => 'code_block_wrapper',
277
+ 'attrs' => {
278
+ 'line_numbers' => true
279
+ },
280
+ 'content' => [{
281
+ 'type' => 'code_block',
282
+ 'attrs' => {
283
+ 'content' => "def example\n puts 'Hello'\nend",
284
+ 'language' => 'ruby'
285
+ }
286
+ }]
287
+ }, {
288
+ 'type' => 'blockquote',
289
+ 'attrs' => {
290
+ 'citation' => 'https://example.com'
291
+ },
292
+ 'content' => [{
293
+ 'type' => 'paragraph',
294
+ 'content' => [{
295
+ 'type' => 'text',
296
+ 'text' => 'A test quote'
297
+ }]
298
+ }]
299
+ }]
300
+ }
301
+
302
+ expect(document.to_h).to eq(expected)
303
+ end
304
+
305
+ it 'creates a document with mathematical content' do
306
+ document = described_class.new
307
+
308
+ # Add a heading
309
+ heading = document.add_heading(2)
310
+ heading.add_text('Mathematical Expressions')
311
+
312
+ # Add equations
313
+ para1 = document.add_paragraph
314
+ para1.add_text('The quadratic formula: x = -b ± ')
315
+ para1.add_text('√', [Prosereflect::Mark::Bold.new])
316
+ para1.add_text('(b')
317
+ para1.add_text('2', [Prosereflect::Mark::Superscript.new])
318
+ para1.add_text(' - 4ac) / 2a')
319
+
320
+ para2 = document.add_paragraph
321
+ para2.add_text('Water molecule: H')
322
+ para2.add_text('2', [Prosereflect::Mark::Subscript.new])
323
+ para2.add_text('O')
324
+
325
+ expected = {
326
+ 'type' => 'doc',
327
+ 'content' => [{
328
+ 'type' => 'heading',
329
+ 'attrs' => { 'level' => 2 },
330
+ 'content' => [{
331
+ 'type' => 'text',
332
+ 'text' => 'Mathematical Expressions'
333
+ }]
334
+ }, {
335
+ 'type' => 'paragraph',
336
+ 'content' => [{
337
+ 'type' => 'text',
338
+ 'text' => 'The quadratic formula: x = -b ± '
339
+ }, {
340
+ 'type' => 'text',
341
+ 'text' => '√',
342
+ 'marks' => [{ 'type' => 'bold' }]
343
+ }, {
344
+ 'type' => 'text',
345
+ 'text' => '(b'
346
+ }, {
347
+ 'type' => 'text',
348
+ 'text' => '2',
349
+ 'marks' => [{ 'type' => 'superscript' }]
350
+ }, {
351
+ 'type' => 'text',
352
+ 'text' => ' - 4ac) / 2a'
353
+ }]
354
+ }, {
355
+ 'type' => 'paragraph',
356
+ 'content' => [{
357
+ 'type' => 'text',
358
+ 'text' => 'Water molecule: H'
359
+ }, {
360
+ 'type' => 'text',
361
+ 'text' => '2',
362
+ 'marks' => [{ 'type' => 'subscript' }]
363
+ }, {
364
+ 'type' => 'text',
365
+ 'text' => 'O'
366
+ }]
367
+ }]
368
+ }
369
+
370
+ expect(document.to_h).to eq(expected)
27
371
  end
28
372
 
29
- it 'creates a document with attributes' do
30
- attrs = { 'version' => 1 }
31
- doc = described_class.create(attrs)
32
- expect(doc.attrs).to eq(attrs)
373
+ it 'creates a document with images and links' do
374
+ document = described_class.new
375
+
376
+ # Add an image
377
+ image = document.add_image('example.jpg', 'Example image')
378
+ image.title = 'A beautiful landscape'
379
+ image.width = 800
380
+ image.height = 600
381
+
382
+ # Add a paragraph with links
383
+ para = document.add_paragraph
384
+ para.add_text('Visit our ')
385
+ link_text = Prosereflect::Text.new(text: 'website')
386
+ link_mark = Prosereflect::Mark::Link.new
387
+ link_mark.attrs = { 'href' => 'https://example.com' }
388
+ link_text.marks = [link_mark]
389
+ para.add_child(link_text)
390
+ para.add_text(' for more information.')
391
+
392
+ expected = {
393
+ 'type' => 'doc',
394
+ 'content' => [{
395
+ 'type' => 'image',
396
+ 'attrs' => {
397
+ 'src' => 'example.jpg',
398
+ 'alt' => 'Example image',
399
+ 'title' => 'A beautiful landscape',
400
+ 'width' => 800,
401
+ 'height' => 600
402
+ }
403
+ }, {
404
+ 'type' => 'paragraph',
405
+ 'content' => [{
406
+ 'type' => 'text',
407
+ 'text' => 'Visit our '
408
+ }, {
409
+ 'type' => 'text',
410
+ 'text' => 'website',
411
+ 'marks' => [{
412
+ 'type' => 'link',
413
+ 'attrs' => {
414
+ 'href' => 'https://example.com'
415
+ }
416
+ }]
417
+ }, {
418
+ 'type' => 'text',
419
+ 'text' => ' for more information.'
420
+ }]
421
+ }]
422
+ }
423
+
424
+ expect(document.to_h).to eq(expected)
33
425
  end
34
426
  end
35
427
 
36
428
  describe '#paragraphs' do
37
429
  it 'returns all paragraphs in the document' do
38
- doc = described_class.create
430
+ doc = described_class.new
39
431
  doc.add_paragraph('First paragraph')
40
432
  doc.add_paragraph('Second paragraph')
41
433
 
@@ -46,7 +438,7 @@ RSpec.describe Prosereflect::Document do
46
438
 
47
439
  describe '#tables' do
48
440
  it 'returns all tables in the document' do
49
- doc = described_class.create
441
+ doc = described_class.new
50
442
  doc.add_table
51
443
  doc.add_table
52
444
 
@@ -55,27 +447,9 @@ RSpec.describe Prosereflect::Document do
55
447
  end
56
448
  end
57
449
 
58
- describe '#first_table' do
59
- it 'returns the first table in the document' do
60
- doc = described_class.create
61
- table1 = doc.add_table
62
- table1.add_header(['T1 Header'])
63
-
64
- table2 = doc.add_table
65
- table2.add_header(['T2 Header'])
66
-
67
- expect(doc.first_table).to eq(table1)
68
- end
69
-
70
- it 'returns nil if no tables exist' do
71
- doc = described_class.create
72
- expect(doc.first_table).to be_nil
73
- end
74
- end
75
-
76
450
  describe '#add_paragraph' do
77
451
  it 'adds a paragraph with text' do
78
- doc = described_class.create
452
+ doc = described_class.new
79
453
  para = doc.add_paragraph('Test paragraph')
80
454
 
81
455
  expect(doc.paragraphs.size).to eq(1)
@@ -84,7 +458,7 @@ RSpec.describe Prosereflect::Document do
84
458
  end
85
459
 
86
460
  it 'adds an empty paragraph' do
87
- doc = described_class.create
461
+ doc = described_class.new
88
462
  para = doc.add_paragraph
89
463
 
90
464
  expect(doc.paragraphs.size).to eq(1)
@@ -95,7 +469,7 @@ RSpec.describe Prosereflect::Document do
95
469
 
96
470
  describe '#add_table' do
97
471
  it 'adds a table to the document' do
98
- doc = described_class.create
472
+ doc = described_class.new
99
473
  table = doc.add_table
100
474
 
101
475
  expect(doc.tables.size).to eq(1)
@@ -103,7 +477,7 @@ RSpec.describe Prosereflect::Document do
103
477
  end
104
478
 
105
479
  it 'adds a table with attributes' do
106
- doc = described_class.create
480
+ doc = described_class.new
107
481
  attrs = { 'width' => '100%' }
108
482
  table = doc.add_table(attrs)
109
483
 
@@ -113,7 +487,7 @@ RSpec.describe Prosereflect::Document do
113
487
 
114
488
  describe 'serialization' do
115
489
  it 'converts to hash representation' do
116
- doc = described_class.create
490
+ doc = described_class.new
117
491
  doc.add_paragraph('Test paragraph')
118
492
  table = doc.add_table
119
493
  table.add_header(['Header'])
@@ -127,7 +501,7 @@ RSpec.describe Prosereflect::Document do
127
501
  end
128
502
 
129
503
  it 'converts to YAML' do
130
- doc = described_class.create
504
+ doc = described_class.new
131
505
  doc.add_paragraph('Test paragraph')
132
506
 
133
507
  yaml = doc.to_yaml
@@ -137,7 +511,7 @@ RSpec.describe Prosereflect::Document do
137
511
  end
138
512
 
139
513
  it 'converts to JSON' do
140
- doc = described_class.create
514
+ doc = described_class.new
141
515
  doc.add_paragraph('Test paragraph')
142
516
 
143
517
  json = doc.to_json
@@ -146,4 +520,30 @@ RSpec.describe Prosereflect::Document do
146
520
  expect(json).to include('"type":"paragraph"')
147
521
  end
148
522
  end
523
+
524
+ describe '#text_content' do
525
+ it 'returns plain text content from all nodes' do
526
+ document = described_class.new
527
+
528
+ # Add various types of content
529
+ document.add_heading(1).add_text('Title')
530
+
531
+ para = document.add_paragraph
532
+ para.add_text('This is ')
533
+ para.add_text('bold', [Prosereflect::Mark::Bold.new])
534
+ para.add_text(' text.')
535
+
536
+ list = document.add_bullet_list
537
+ list.add_item('First item')
538
+ list.add_item('Second item')
539
+
540
+ expected_text = "Title\nThis is bold text.\nFirst item\nSecond item"
541
+ expect(document.text_content).to eq(expected_text)
542
+ end
543
+
544
+ it 'handles empty documents' do
545
+ document = described_class.new
546
+ expect(document.text_content).to eq('')
547
+ end
548
+ end
149
549
  end