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
@@ -8,26 +8,320 @@ RSpec.describe Prosereflect::Paragraph do
8
8
  paragraph = described_class.new({ 'type' => 'paragraph' })
9
9
  expect(paragraph.type).to eq('paragraph')
10
10
  end
11
+
12
+ it 'initializes with content' do
13
+ text_node = Prosereflect::Text.new(text: 'Hello')
14
+ paragraph = described_class.new
15
+ paragraph.add_child(text_node)
16
+
17
+ expect(paragraph.content.first).to be_a(Prosereflect::Text)
18
+ expect(paragraph.content.first.text).to eq('Hello')
19
+ end
20
+
21
+ it 'initializes with complex content' do
22
+ content = [
23
+ { 'type' => 'text', 'text' => 'Hello', 'marks' => [{ 'type' => 'bold' }] }
24
+ ]
25
+ paragraph = described_class.new(content: content)
26
+
27
+ expect(paragraph.content[0].raw_marks.first.type).to eq('bold')
28
+ end
11
29
  end
12
30
 
13
31
  describe '.create' do
14
- it 'creates an empty paragraph' do
15
- paragraph = described_class.create
16
- expect(paragraph).to be_a(described_class)
17
- expect(paragraph.type).to eq('paragraph')
18
- expect(paragraph.content).to be_empty
32
+ it 'creates a simple paragraph with text' do
33
+ paragraph = described_class.new
34
+ paragraph.add_text('This is a test paragraph.')
35
+
36
+ expected = {
37
+ 'type' => 'paragraph',
38
+ 'content' => [{
39
+ 'type' => 'text',
40
+ 'text' => 'This is a test paragraph.'
41
+ }]
42
+ }
43
+
44
+ expect(paragraph.to_h).to eq(expected)
45
+ end
46
+
47
+ it 'creates a paragraph with styled text' do
48
+ paragraph = described_class.new
49
+ paragraph.add_text('This is ')
50
+ paragraph.add_text('bold', [Prosereflect::Mark::Bold.new])
51
+ paragraph.add_text(' and ')
52
+ paragraph.add_text('italic', [Prosereflect::Mark::Italic.new])
53
+ paragraph.add_text(' text.')
54
+
55
+ expected = {
56
+ 'type' => 'paragraph',
57
+ 'content' => [{
58
+ 'type' => 'text',
59
+ 'text' => 'This is '
60
+ }, {
61
+ 'type' => 'text',
62
+ 'text' => 'bold',
63
+ 'marks' => [{
64
+ 'type' => 'bold'
65
+ }]
66
+ }, {
67
+ 'type' => 'text',
68
+ 'text' => ' and '
69
+ }, {
70
+ 'type' => 'text',
71
+ 'text' => 'italic',
72
+ 'marks' => [{
73
+ 'type' => 'italic'
74
+ }]
75
+ }, {
76
+ 'type' => 'text',
77
+ 'text' => ' text.'
78
+ }]
79
+ }
80
+
81
+ expect(paragraph.to_h).to eq(expected)
19
82
  end
20
83
 
21
- it 'creates a paragraph with attributes' do
22
- attrs = { 'align' => 'center' }
23
- paragraph = described_class.create(attrs)
24
- expect(paragraph.attrs).to eq(attrs)
84
+ it 'creates a paragraph with multiple text styles' do
85
+ paragraph = described_class.new
86
+ paragraph.add_text('This is ')
87
+ paragraph.add_text('struck', [Prosereflect::Mark::Strike.new])
88
+ paragraph.add_text(' and ')
89
+ paragraph.add_text('underlined', [Prosereflect::Mark::Underline.new])
90
+ paragraph.add_text(' and ')
91
+ paragraph.add_text('sub', [Prosereflect::Mark::Subscript.new])
92
+ paragraph.add_text(' and ')
93
+ paragraph.add_text('super', [Prosereflect::Mark::Superscript.new])
94
+ paragraph.add_text(' text.')
95
+
96
+ expected = {
97
+ 'type' => 'paragraph',
98
+ 'content' => [{
99
+ 'type' => 'text',
100
+ 'text' => 'This is '
101
+ }, {
102
+ 'type' => 'text',
103
+ 'text' => 'struck',
104
+ 'marks' => [{
105
+ 'type' => 'strike'
106
+ }]
107
+ }, {
108
+ 'type' => 'text',
109
+ 'text' => ' and '
110
+ }, {
111
+ 'type' => 'text',
112
+ 'text' => 'underlined',
113
+ 'marks' => [{
114
+ 'type' => 'underline'
115
+ }]
116
+ }, {
117
+ 'type' => 'text',
118
+ 'text' => ' and '
119
+ }, {
120
+ 'type' => 'text',
121
+ 'text' => 'sub',
122
+ 'marks' => [{
123
+ 'type' => 'subscript'
124
+ }]
125
+ }, {
126
+ 'type' => 'text',
127
+ 'text' => ' and '
128
+ }, {
129
+ 'type' => 'text',
130
+ 'text' => 'super',
131
+ 'marks' => [{
132
+ 'type' => 'superscript'
133
+ }]
134
+ }, {
135
+ 'type' => 'text',
136
+ 'text' => ' text.'
137
+ }]
138
+ }
139
+
140
+ expect(paragraph.to_h).to eq(expected)
141
+ end
142
+
143
+ it 'creates a paragraph with mixed text styles' do
144
+ paragraph = described_class.new
145
+ paragraph.add_text('Bold and italic', [
146
+ Prosereflect::Mark::Bold.new,
147
+ Prosereflect::Mark::Italic.new
148
+ ])
149
+ paragraph.add_text(' and ')
150
+ paragraph.add_text('underlined and struck', [
151
+ Prosereflect::Mark::Underline.new,
152
+ Prosereflect::Mark::Strike.new
153
+ ])
154
+
155
+ expected = {
156
+ 'type' => 'paragraph',
157
+ 'content' => [{
158
+ 'type' => 'text',
159
+ 'text' => 'Bold and italic',
160
+ 'marks' => [{
161
+ 'type' => 'bold'
162
+ }, {
163
+ 'type' => 'italic'
164
+ }]
165
+ }, {
166
+ 'type' => 'text',
167
+ 'text' => ' and '
168
+ }, {
169
+ 'type' => 'text',
170
+ 'text' => 'underlined and struck',
171
+ 'marks' => [{
172
+ 'type' => 'underline'
173
+ }, {
174
+ 'type' => 'strike'
175
+ }]
176
+ }]
177
+ }
178
+
179
+ expect(paragraph.to_h).to eq(expected)
180
+ end
181
+
182
+ it 'creates a paragraph with line breaks' do
183
+ paragraph = described_class.new
184
+ paragraph.add_text('First line')
185
+ paragraph.add_hard_break
186
+ paragraph.add_text('Second line')
187
+ paragraph.add_hard_break
188
+ paragraph.add_text('Third line')
189
+
190
+ expected = {
191
+ 'type' => 'paragraph',
192
+ 'content' => [{
193
+ 'type' => 'text',
194
+ 'text' => 'First line'
195
+ }, {
196
+ 'type' => 'hard_break'
197
+ }, {
198
+ 'type' => 'text',
199
+ 'text' => 'Second line'
200
+ }, {
201
+ 'type' => 'hard_break'
202
+ }, {
203
+ 'type' => 'text',
204
+ 'text' => 'Third line'
205
+ }]
206
+ }
207
+
208
+ expect(paragraph.to_h).to eq(expected)
209
+ end
210
+
211
+ it 'creates a paragraph with alignment' do
212
+ paragraph = described_class.new(attrs: { 'align' => 'center' })
213
+ paragraph.add_text('Centered text')
214
+
215
+ expected = {
216
+ 'type' => 'paragraph',
217
+ 'attrs' => {
218
+ 'align' => 'center'
219
+ },
220
+ 'content' => [{
221
+ 'type' => 'text',
222
+ 'text' => 'Centered text'
223
+ }]
224
+ }
225
+
226
+ expect(paragraph.to_h).to eq(expected)
227
+ end
228
+
229
+ it 'creates a paragraph with complex content' do
230
+ paragraph = described_class.new
231
+ paragraph.add_text('A formula: ')
232
+ paragraph.add_text('H', [Prosereflect::Mark::Bold.new])
233
+ paragraph.add_text('2', [Prosereflect::Mark::Subscript.new])
234
+ paragraph.add_text('O + 2H')
235
+ paragraph.add_text('2', [Prosereflect::Mark::Subscript.new])
236
+ paragraph.add_text(' → 2H')
237
+ paragraph.add_text('2', [Prosereflect::Mark::Subscript.new])
238
+ paragraph.add_text('O')
239
+
240
+ expected = {
241
+ 'type' => 'paragraph',
242
+ 'content' => [{
243
+ 'type' => 'text',
244
+ 'text' => 'A formula: '
245
+ }, {
246
+ 'type' => 'text',
247
+ 'text' => 'H',
248
+ 'marks' => [{
249
+ 'type' => 'bold'
250
+ }]
251
+ }, {
252
+ 'type' => 'text',
253
+ 'text' => '2',
254
+ 'marks' => [{
255
+ 'type' => 'subscript'
256
+ }]
257
+ }, {
258
+ 'type' => 'text',
259
+ 'text' => 'O + 2H'
260
+ }, {
261
+ 'type' => 'text',
262
+ 'text' => '2',
263
+ 'marks' => [{
264
+ 'type' => 'subscript'
265
+ }]
266
+ }, {
267
+ 'type' => 'text',
268
+ 'text' => ' → 2H'
269
+ }, {
270
+ 'type' => 'text',
271
+ 'text' => '2',
272
+ 'marks' => [{
273
+ 'type' => 'subscript'
274
+ }]
275
+ }, {
276
+ 'type' => 'text',
277
+ 'text' => 'O'
278
+ }]
279
+ }
280
+
281
+ expect(paragraph.to_h).to eq(expected)
282
+ end
283
+
284
+ it 'creates a paragraph with mathematical expressions' do
285
+ paragraph = described_class.new
286
+ paragraph.add_text('The quadratic formula: x = -b ± ')
287
+ paragraph.add_text('√', [Prosereflect::Mark::Bold.new])
288
+ paragraph.add_text('(b')
289
+ paragraph.add_text('2', [Prosereflect::Mark::Superscript.new])
290
+ paragraph.add_text(' - 4ac) / 2a')
291
+
292
+ expected = {
293
+ 'type' => 'paragraph',
294
+ 'content' => [{
295
+ 'type' => 'text',
296
+ 'text' => 'The quadratic formula: x = -b ± '
297
+ }, {
298
+ 'type' => 'text',
299
+ 'text' => '√',
300
+ 'marks' => [{
301
+ 'type' => 'bold'
302
+ }]
303
+ }, {
304
+ 'type' => 'text',
305
+ 'text' => '(b'
306
+ }, {
307
+ 'type' => 'text',
308
+ 'text' => '2',
309
+ 'marks' => [{
310
+ 'type' => 'superscript'
311
+ }]
312
+ }, {
313
+ 'type' => 'text',
314
+ 'text' => ' - 4ac) / 2a'
315
+ }]
316
+ }
317
+
318
+ expect(paragraph.to_h).to eq(expected)
25
319
  end
26
320
  end
27
321
 
28
322
  describe '#text_nodes' do
29
323
  it 'returns all text nodes in the paragraph' do
30
- paragraph = described_class.create
324
+ paragraph = described_class.new
31
325
  paragraph.add_text('First text')
32
326
  paragraph.add_text('Second text')
33
327
 
@@ -36,27 +330,42 @@ RSpec.describe Prosereflect::Paragraph do
36
330
  end
37
331
 
38
332
  it 'returns empty array for paragraph with no text nodes' do
39
- paragraph = described_class.create
333
+ paragraph = described_class.new
40
334
  expect(paragraph.text_nodes).to eq([])
41
335
  end
42
- end
43
336
 
44
- describe '#text_content' do
45
- it 'returns concatenated text from all text nodes' do
46
- paragraph = described_class.create
47
- paragraph.add_text('First text')
48
- paragraph.add_text(' Second text')
337
+ it 'returns text nodes with marks' do
338
+ paragraph = described_class.new
339
+ paragraph.add_text('Hello', [Prosereflect::Mark::Bold.new])
49
340
 
50
- expect(paragraph.text_content).to eq('First text Second text')
341
+ expect(paragraph.text_nodes[0].raw_marks.first.type).to eq('bold')
51
342
  end
52
343
 
53
- it 'returns empty string for empty paragraph' do
54
- paragraph = described_class.create
55
- expect(paragraph.text_content).to eq('')
344
+ it 'ignores non-text nodes' do
345
+ paragraph = described_class.new
346
+ paragraph.add_text('Text')
347
+ paragraph.add_hard_break
348
+ paragraph.add_text('More text')
349
+
350
+ expect(paragraph.text_nodes.size).to eq(2)
351
+ expect(paragraph.text_nodes.map(&:text)).to eq(['Text', 'More text'])
56
352
  end
353
+ end
57
354
 
58
- it 'handles hard breaks' do
59
- paragraph = described_class.create
355
+ describe '#text_content' do
356
+ it 'returns plain text content without marks' do
357
+ paragraph = described_class.new
358
+ paragraph.add_text('This is ')
359
+ paragraph.add_text('bold', [Prosereflect::Mark::Bold.new])
360
+ paragraph.add_text(' and ')
361
+ paragraph.add_text('italic', [Prosereflect::Mark::Italic.new])
362
+ paragraph.add_text(' text.')
363
+
364
+ expect(paragraph.text_content).to eq('This is bold and italic text.')
365
+ end
366
+
367
+ it 'handles line breaks in text content' do
368
+ paragraph = described_class.new
60
369
  paragraph.add_text('First line')
61
370
  paragraph.add_hard_break
62
371
  paragraph.add_text('Second line')
@@ -64,26 +373,15 @@ RSpec.describe Prosereflect::Paragraph do
64
373
  expect(paragraph.text_content).to eq("First line\nSecond line")
65
374
  end
66
375
 
67
- it 'handles mixed content types' do
68
- paragraph = described_class.create
69
- paragraph.add_text('Text with ')
70
-
71
- # Add a custom node that is neither text nor hard_break
72
- custom_node = Prosereflect::Node.create('custom_node')
73
- custom_node.instance_eval do
74
- def text_content
75
- 'custom content'
76
- end
77
- end
78
- paragraph.add_child(custom_node)
79
-
80
- expect(paragraph.text_content).to eq('Text with custom content')
376
+ it 'handles empty paragraphs' do
377
+ paragraph = described_class.new
378
+ expect(paragraph.text_content).to eq('')
81
379
  end
82
380
  end
83
381
 
84
382
  describe '#add_text' do
85
383
  it 'adds text to the paragraph' do
86
- paragraph = described_class.create
384
+ paragraph = described_class.new
87
385
  text_node = paragraph.add_text('Hello world')
88
386
 
89
387
  expect(paragraph.content.size).to eq(1)
@@ -92,15 +390,27 @@ RSpec.describe Prosereflect::Paragraph do
92
390
  end
93
391
 
94
392
  it 'adds text with marks' do
95
- paragraph = described_class.create
96
- marks = [{ 'type' => 'bold' }]
97
- text_node = paragraph.add_text('Bold text', marks)
393
+ paragraph = described_class.new
394
+ marks = [Prosereflect::Mark::Bold.new]
395
+ text_node = paragraph.add_text('Hello', marks)
98
396
 
99
- expect(text_node.marks).to eq(marks)
397
+ expect(text_node.raw_marks).to eq(marks)
398
+ end
399
+
400
+ it 'adds text with multiple marks' do
401
+ paragraph = described_class.new
402
+ marks = [
403
+ Prosereflect::Mark::Bold.new,
404
+ Prosereflect::Mark::Italic.new,
405
+ Prosereflect::Mark::Underline.new
406
+ ]
407
+ text_node = paragraph.add_text('Hello', marks)
408
+
409
+ expect(text_node.raw_marks.map(&:type)).to eq(%w[bold italic underline])
100
410
  end
101
411
 
102
412
  it 'does not add empty text' do
103
- paragraph = described_class.create
413
+ paragraph = described_class.new
104
414
  result = paragraph.add_text('')
105
415
 
106
416
  expect(paragraph.content).to be_empty
@@ -108,17 +418,27 @@ RSpec.describe Prosereflect::Paragraph do
108
418
  end
109
419
 
110
420
  it 'does not add nil text' do
111
- paragraph = described_class.create
421
+ paragraph = described_class.new
112
422
  result = paragraph.add_text(nil)
113
423
 
114
424
  expect(paragraph.content).to be_empty
115
425
  expect(result).to be_nil
116
426
  end
427
+
428
+ it 'preserves existing content when adding text' do
429
+ paragraph = described_class.new
430
+ paragraph.add_text('First')
431
+ paragraph.add_hard_break
432
+ paragraph.add_text('Second')
433
+
434
+ expect(paragraph.content.size).to eq(3)
435
+ expect(paragraph.text_content).to eq("First\nSecond")
436
+ end
117
437
  end
118
438
 
119
439
  describe '#add_hard_break' do
120
440
  it 'adds a hard break to the paragraph' do
121
- paragraph = described_class.create
441
+ paragraph = described_class.new
122
442
  hard_break = paragraph.add_hard_break
123
443
 
124
444
  expect(paragraph.content.size).to eq(1)
@@ -126,17 +446,37 @@ RSpec.describe Prosereflect::Paragraph do
126
446
  end
127
447
 
128
448
  it 'adds a hard break with marks' do
129
- paragraph = described_class.create
130
- marks = [{ 'type' => 'italic' }]
449
+ paragraph = described_class.new
450
+ marks = [Prosereflect::Mark::Italic.new]
131
451
  hard_break = paragraph.add_hard_break(marks)
132
452
 
133
- expect(hard_break.marks).to eq(marks)
453
+ expect(hard_break.raw_marks).to eq(marks)
454
+ end
455
+
456
+ it 'adds multiple hard breaks' do
457
+ paragraph = described_class.new
458
+ paragraph.add_hard_break
459
+ paragraph.add_hard_break
460
+ paragraph.add_hard_break
461
+
462
+ expect(paragraph.content.size).to eq(3)
463
+ expect(paragraph.content).to all(be_a(Prosereflect::HardBreak))
464
+ end
465
+
466
+ it 'preserves existing content when adding hard breaks' do
467
+ paragraph = described_class.new
468
+ paragraph.add_text('Text')
469
+ paragraph.add_hard_break
470
+ paragraph.add_text('More')
471
+
472
+ expect(paragraph.content.size).to eq(3)
473
+ expect(paragraph.text_content).to eq("Text\nMore")
134
474
  end
135
475
  end
136
476
 
137
477
  describe 'serialization' do
138
478
  it 'converts to hash representation' do
139
- paragraph = described_class.create
479
+ paragraph = described_class.new
140
480
  paragraph.add_text('Hello')
141
481
  paragraph.add_hard_break
142
482
  paragraph.add_text('World')
@@ -148,5 +488,40 @@ RSpec.describe Prosereflect::Paragraph do
148
488
  expect(hash['content'][1]['type']).to eq('hard_break')
149
489
  expect(hash['content'][2]['type']).to eq('text')
150
490
  end
491
+
492
+ it 'converts to hash with marks' do
493
+ paragraph = described_class.new
494
+ paragraph.add_text('Bold', [Prosereflect::Mark::Bold.new])
495
+ paragraph.add_text(' and ')
496
+ paragraph.add_text('italic', [Prosereflect::Mark::Italic.new])
497
+
498
+ hash = paragraph.to_h
499
+ expect(hash['content'][0]['marks'][0]['type']).to eq('bold')
500
+ expect(hash['content'][2]['marks'][0]['type']).to eq('italic')
501
+ end
502
+
503
+ it 'converts to hash with attributes' do
504
+ paragraph = described_class.new(attrs: { 'align' => 'center' })
505
+ paragraph.add_text('Centered text')
506
+
507
+ hash = paragraph.to_h
508
+ expect(hash['attrs']).to eq({ 'align' => 'center' })
509
+ end
510
+
511
+ it 'converts complex content to hash' do
512
+ paragraph = described_class.new
513
+ paragraph.add_text('Mixed', [
514
+ Prosereflect::Mark::Bold.new,
515
+ Prosereflect::Mark::Italic.new
516
+ ])
517
+ paragraph.add_hard_break([Prosereflect::Mark::Strike.new])
518
+ paragraph.add_text('Styles')
519
+
520
+ hash = paragraph.to_h
521
+ expect(hash['content'].size).to eq(3)
522
+ expect(hash['content'][0]['marks'].size).to eq(2)
523
+ expect(hash['content'][1]['marks'].size).to eq(1)
524
+ expect(hash['content'][2]['marks']).to be_nil
525
+ end
151
526
  end
152
527
  end