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
@@ -0,0 +1,797 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::Input::Html do
6
+ describe '.parse' do
7
+ it 'parses simple HTML into a document' do
8
+ html = '<p>This is a test paragraph.</p>'
9
+ document = described_class.parse(html)
10
+
11
+ expected = {
12
+ 'type' => 'doc',
13
+ 'content' => [{
14
+ 'type' => 'paragraph',
15
+ 'content' => [{
16
+ 'type' => 'text',
17
+ 'text' => 'This is a test paragraph.'
18
+ }]
19
+ }]
20
+ }
21
+
22
+ expect(document.to_h).to eq(expected)
23
+ end
24
+
25
+ it 'renders basic styled text correctly' do
26
+ html = '<p>This is <strong>bold</strong> and <em>italic</em> text.</p>'
27
+ document = described_class.parse(html)
28
+
29
+ expected = {
30
+ 'type' => 'doc',
31
+ 'content' => [{
32
+ 'type' => 'paragraph',
33
+ 'content' => [{
34
+ 'type' => 'text',
35
+ 'text' => 'This is '
36
+ }, {
37
+ 'type' => 'text',
38
+ 'text' => 'bold',
39
+ 'marks' => [{
40
+ 'type' => 'bold'
41
+ }]
42
+ }, {
43
+ 'type' => 'text',
44
+ 'text' => ' and '
45
+ }, {
46
+ 'type' => 'text',
47
+ 'text' => 'italic',
48
+ 'marks' => [{
49
+ 'type' => 'italic'
50
+ }]
51
+ }, {
52
+ 'type' => 'text',
53
+ 'text' => ' text.'
54
+ }]
55
+ }]
56
+ }
57
+
58
+ expect(document.to_h).to eq(expected)
59
+ end
60
+
61
+ it 'parses strike text correctly' do
62
+ html = '<p>This is <strike>struck through</strike> text and <s>this too</s> and <del>deleted</del>.</p>'
63
+ document = described_class.parse(html)
64
+
65
+ expected = {
66
+ 'type' => 'doc',
67
+ 'content' => [{
68
+ 'type' => 'paragraph',
69
+ 'content' => [{
70
+ 'type' => 'text',
71
+ 'text' => 'This is '
72
+ }, {
73
+ 'type' => 'text',
74
+ 'text' => 'struck through',
75
+ 'marks' => [{
76
+ 'type' => 'strike'
77
+ }]
78
+ }, {
79
+ 'type' => 'text',
80
+ 'text' => ' text and '
81
+ }, {
82
+ 'type' => 'text',
83
+ 'text' => 'this too',
84
+ 'marks' => [{
85
+ 'type' => 'strike'
86
+ }]
87
+ }, {
88
+ 'type' => 'text',
89
+ 'text' => ' and '
90
+ }, {
91
+ 'type' => 'text',
92
+ 'text' => 'deleted',
93
+ 'marks' => [{
94
+ 'type' => 'strike'
95
+ }]
96
+ }, {
97
+ 'type' => 'text',
98
+ 'text' => '.'
99
+ }]
100
+ }]
101
+ }
102
+
103
+ expect(document.to_h).to eq(expected)
104
+ end
105
+
106
+ it 'parses subscript text correctly' do
107
+ html = '<p>H<sub>2</sub>O and E = mc<sub>2</sub></p>'
108
+ document = described_class.parse(html)
109
+
110
+ expected = {
111
+ 'type' => 'doc',
112
+ 'content' => [{
113
+ 'type' => 'paragraph',
114
+ 'content' => [{
115
+ 'type' => 'text',
116
+ 'text' => 'H'
117
+ }, {
118
+ 'type' => 'text',
119
+ 'text' => '2',
120
+ 'marks' => [{
121
+ 'type' => 'subscript'
122
+ }]
123
+ }, {
124
+ 'type' => 'text',
125
+ 'text' => 'O and E = mc'
126
+ }, {
127
+ 'type' => 'text',
128
+ 'text' => '2',
129
+ 'marks' => [{
130
+ 'type' => 'subscript'
131
+ }]
132
+ }]
133
+ }]
134
+ }
135
+
136
+ expect(document.to_h).to eq(expected)
137
+ end
138
+
139
+ it 'parses superscript text correctly' do
140
+ html = '<p>x<sup>2</sup> + y<sup>2</sup> = z<sup>2</sup></p>'
141
+ document = described_class.parse(html)
142
+
143
+ expected = {
144
+ 'type' => 'doc',
145
+ 'content' => [{
146
+ 'type' => 'paragraph',
147
+ 'content' => [{
148
+ 'type' => 'text',
149
+ 'text' => 'x'
150
+ }, {
151
+ 'type' => 'text',
152
+ 'text' => '2',
153
+ 'marks' => [{
154
+ 'type' => 'superscript'
155
+ }]
156
+ }, {
157
+ 'type' => 'text',
158
+ 'text' => ' + y'
159
+ }, {
160
+ 'type' => 'text',
161
+ 'text' => '2',
162
+ 'marks' => [{
163
+ 'type' => 'superscript'
164
+ }]
165
+ }, {
166
+ 'type' => 'text',
167
+ 'text' => ' = z'
168
+ }, {
169
+ 'type' => 'text',
170
+ 'text' => '2',
171
+ 'marks' => [{
172
+ 'type' => 'superscript'
173
+ }]
174
+ }]
175
+ }]
176
+ }
177
+
178
+ expect(document.to_h).to eq(expected)
179
+ end
180
+
181
+ it 'parses underlined text correctly' do
182
+ html = '<p>This is <u>underlined</u> text.</p>'
183
+ document = described_class.parse(html)
184
+
185
+ expected = {
186
+ 'type' => 'doc',
187
+ 'content' => [{
188
+ 'type' => 'paragraph',
189
+ 'content' => [{
190
+ 'type' => 'text',
191
+ 'text' => 'This is '
192
+ }, {
193
+ 'type' => 'text',
194
+ 'text' => 'underlined',
195
+ 'marks' => [{
196
+ 'type' => 'underline'
197
+ }]
198
+ }, {
199
+ 'type' => 'text',
200
+ 'text' => ' text.'
201
+ }]
202
+ }]
203
+ }
204
+
205
+ expect(document.to_h).to eq(expected)
206
+ end
207
+
208
+ it 'handles mixed text styles correctly' do
209
+ html = '<p><strong><u>Bold and underlined</u></strong> and <em><strike>italic struck</strike></em></p>'
210
+ document = described_class.parse(html)
211
+
212
+ expected = {
213
+ 'type' => 'doc',
214
+ 'content' => [{
215
+ 'type' => 'paragraph',
216
+ 'content' => [{
217
+ 'type' => 'text',
218
+ 'text' => 'Bold and underlined',
219
+ 'marks' => [{
220
+ 'type' => 'underline'
221
+ }, {
222
+ 'type' => 'bold'
223
+ }]
224
+ }, {
225
+ 'type' => 'text',
226
+ 'text' => ' and '
227
+ }, {
228
+ 'type' => 'text',
229
+ 'text' => 'italic struck',
230
+ 'marks' => [{
231
+ 'type' => 'strike'
232
+ }, {
233
+ 'type' => 'italic'
234
+ }]
235
+ }]
236
+ }]
237
+ }
238
+
239
+ expect(document.to_h).to eq(expected)
240
+ end
241
+
242
+ it 'handles complex mixed text styles correctly' do
243
+ html = '<p>x<sup>2</sup> + <u>y<sub>1</sub></u> = <strike>z<sup>n</sup></strike></p>'
244
+ document = described_class.parse(html)
245
+
246
+ expected = {
247
+ 'type' => 'doc',
248
+ 'content' => [{
249
+ 'type' => 'paragraph',
250
+ 'content' => [{
251
+ 'type' => 'text',
252
+ 'text' => 'x'
253
+ }, {
254
+ 'type' => 'text',
255
+ 'text' => '2',
256
+ 'marks' => [{
257
+ 'type' => 'superscript'
258
+ }]
259
+ }, {
260
+ 'type' => 'text',
261
+ 'text' => ' + '
262
+ }, {
263
+ 'type' => 'text',
264
+ 'text' => 'y',
265
+ 'marks' => [{
266
+ 'type' => 'underline'
267
+ }]
268
+ }, {
269
+ 'type' => 'text',
270
+ 'text' => '1',
271
+ 'marks' => [{
272
+ 'type' => 'subscript'
273
+ }, {
274
+ 'type' => 'underline'
275
+ }]
276
+ }, {
277
+ 'type' => 'text',
278
+ 'text' => ' = '
279
+ }, {
280
+ 'type' => 'text',
281
+ 'text' => 'z',
282
+ 'marks' => [{
283
+ 'type' => 'strike'
284
+ }]
285
+ }, {
286
+ 'type' => 'text',
287
+ 'text' => 'n',
288
+ 'marks' => [{
289
+ 'type' => 'superscript'
290
+ }, {
291
+ 'type' => 'strike'
292
+ }]
293
+ }]
294
+ }]
295
+ }
296
+
297
+ expect(document.to_h).to eq(expected)
298
+ end
299
+
300
+ it 'parses tables correctly' do
301
+ html = <<~HTML
302
+ <table>
303
+ <tr>
304
+ <td>Row 1, Cell 1</td>
305
+ <td>Row 1, Cell 2</td>
306
+ </tr>
307
+ <tr>
308
+ <td>Row 2, Cell 1</td>
309
+ <td>Row 2, Cell 2</td>
310
+ </tr>
311
+ </table>
312
+ HTML
313
+
314
+ document = described_class.parse(html)
315
+
316
+ expected = {
317
+ 'type' => 'doc',
318
+ 'content' => [{
319
+ 'type' => 'table',
320
+ 'content' => [{
321
+ 'type' => 'table_row',
322
+ 'content' => [{
323
+ 'type' => 'table_cell',
324
+ 'content' => [{
325
+ 'type' => 'paragraph',
326
+ 'content' => [{
327
+ 'type' => 'text',
328
+ 'text' => 'Row 1, Cell 1'
329
+ }]
330
+ }]
331
+ }, {
332
+ 'type' => 'table_cell',
333
+ 'content' => [{
334
+ 'type' => 'paragraph',
335
+ 'content' => [{
336
+ 'type' => 'text',
337
+ 'text' => 'Row 1, Cell 2'
338
+ }]
339
+ }]
340
+ }]
341
+ }, {
342
+ 'type' => 'table_row',
343
+ 'content' => [{
344
+ 'type' => 'table_cell',
345
+ 'content' => [{
346
+ 'type' => 'paragraph',
347
+ 'content' => [{
348
+ 'type' => 'text',
349
+ 'text' => 'Row 2, Cell 1'
350
+ }]
351
+ }]
352
+ }, {
353
+ 'type' => 'table_cell',
354
+ 'content' => [{
355
+ 'type' => 'paragraph',
356
+ 'content' => [{
357
+ 'type' => 'text',
358
+ 'text' => 'Row 2, Cell 2'
359
+ }]
360
+ }]
361
+ }]
362
+ }]
363
+ }]
364
+ }
365
+
366
+ expect(document.to_h).to eq(expected)
367
+ end
368
+
369
+ it 'parses links correctly' do
370
+ html = '<p>This is a <a href="https://example.com">link</a></p>'
371
+ document = described_class.parse(html)
372
+
373
+ expected = {
374
+ 'type' => 'doc',
375
+ 'content' => [{
376
+ 'type' => 'paragraph',
377
+ 'content' => [{
378
+ 'type' => 'text',
379
+ 'text' => 'This is a '
380
+ }, {
381
+ 'type' => 'text',
382
+ 'text' => 'link',
383
+ 'marks' => [{
384
+ 'type' => 'link',
385
+ 'attrs' => {
386
+ 'href' => 'https://example.com'
387
+ }
388
+ }]
389
+ }]
390
+ }]
391
+ }
392
+
393
+ expect(document.to_h).to eq(expected)
394
+ end
395
+
396
+ it 'handles line breaks correctly' do
397
+ html = '<p>Line 1<br>Line 2</p>'
398
+ document = described_class.parse(html)
399
+
400
+ expected = {
401
+ 'type' => 'doc',
402
+ 'content' => [{
403
+ 'type' => 'paragraph',
404
+ 'content' => [{
405
+ 'type' => 'text',
406
+ 'text' => 'Line 1'
407
+ }, {
408
+ 'type' => 'hard_break'
409
+ }, {
410
+ 'type' => 'text',
411
+ 'text' => 'Line 2'
412
+ }]
413
+ }]
414
+ }
415
+
416
+ expect(document.to_h).to eq(expected)
417
+ end
418
+
419
+ it 'parses ordered lists with start attribute correctly' do
420
+ html = <<~HTML
421
+ <ol start="3">
422
+ <li>Third item</li>
423
+ <li>Fourth item</li>
424
+ </ol>
425
+ HTML
426
+
427
+ document = described_class.parse(html)
428
+
429
+ expected = {
430
+ 'type' => 'doc',
431
+ 'content' => [{
432
+ 'type' => 'ordered_list',
433
+ 'attrs' => {
434
+ 'start' => 3
435
+ },
436
+ 'content' => [{
437
+ 'type' => 'list_item',
438
+ 'content' => [{
439
+ 'type' => 'paragraph',
440
+ 'content' => [{
441
+ 'type' => 'text',
442
+ 'text' => 'Third item'
443
+ }]
444
+ }]
445
+ }, {
446
+ 'type' => 'list_item',
447
+ 'content' => [{
448
+ 'type' => 'paragraph',
449
+ 'content' => [{
450
+ 'type' => 'text',
451
+ 'text' => 'Fourth item'
452
+ }]
453
+ }]
454
+ }]
455
+ }]
456
+ }
457
+
458
+ expect(document.to_h).to eq(expected)
459
+ end
460
+
461
+ it 'parses bullet lists with styles correctly' do
462
+ html = <<~HTML
463
+ <ul style="list-style-type: square">
464
+ <li>First bullet</li>
465
+ <li>Second bullet</li>
466
+ </ul>
467
+ HTML
468
+
469
+ document = described_class.parse(html)
470
+
471
+ expected = {
472
+ 'type' => 'doc',
473
+ 'content' => [{
474
+ 'type' => 'bullet_list',
475
+ 'attrs' => {
476
+ 'bullet_style' => 'square'
477
+ },
478
+ 'content' => [{
479
+ 'type' => 'list_item',
480
+ 'content' => [{
481
+ 'type' => 'paragraph',
482
+ 'content' => [{
483
+ 'type' => 'text',
484
+ 'text' => 'First bullet'
485
+ }]
486
+ }]
487
+ }, {
488
+ 'type' => 'list_item',
489
+ 'content' => [{
490
+ 'type' => 'paragraph',
491
+ 'content' => [{
492
+ 'type' => 'text',
493
+ 'text' => 'Second bullet'
494
+ }]
495
+ }]
496
+ }]
497
+ }]
498
+ }
499
+
500
+ expect(document.to_h).to eq(expected)
501
+ end
502
+
503
+ it 'renders headings with mixed content correctly' do
504
+ html = <<~HTML
505
+ <h1>Title with <strong>bold</strong> and <a href="https://example.com">link</a></h1>
506
+ HTML
507
+
508
+ expected = {
509
+ 'type' => 'doc',
510
+ 'content' => [{
511
+ 'type' => 'heading',
512
+ 'attrs' => {
513
+ 'level' => 1
514
+ },
515
+ 'content' => [{
516
+ 'type' => 'text',
517
+ 'text' => 'Title with '
518
+ }, {
519
+ 'type' => 'text',
520
+ 'text' => 'bold',
521
+ 'marks' => [{
522
+ 'type' => 'bold'
523
+ }]
524
+ }, {
525
+ 'type' => 'text',
526
+ 'text' => ' and '
527
+ }, {
528
+ 'type' => 'text',
529
+ 'text' => 'link',
530
+ 'marks' => [{
531
+ 'type' => 'link',
532
+ 'attrs' => {
533
+ 'href' => 'https://example.com'
534
+ }
535
+ }]
536
+ }]
537
+ }]
538
+ }
539
+
540
+ document = described_class.parse(html)
541
+ expect(document.to_h).to eq(expected)
542
+ end
543
+
544
+ it 'renders lists with nested content correctly' do
545
+ html = <<~HTML
546
+ <ul>
547
+ <li>First item with <em>emphasis</em></li>
548
+ <li>Second item with <code>code</code></li>
549
+ </ul>
550
+ HTML
551
+
552
+ expected = {
553
+ 'type' => 'doc',
554
+ 'content' => [{
555
+ 'type' => 'bullet_list',
556
+ 'attrs' => {
557
+ 'bullet_style' => nil
558
+ },
559
+ 'content' => [{
560
+ 'type' => 'list_item',
561
+ 'content' => [{
562
+ 'type' => 'paragraph',
563
+ 'content' => [{
564
+ 'type' => 'text',
565
+ 'text' => 'First item with '
566
+ }, {
567
+ 'type' => 'text',
568
+ 'text' => 'emphasis',
569
+ 'marks' => [{
570
+ 'type' => 'italic'
571
+ }]
572
+ }]
573
+ }]
574
+ }, {
575
+ 'type' => 'list_item',
576
+ 'content' => [{
577
+ 'type' => 'paragraph',
578
+ 'content' => [{
579
+ 'type' => 'text',
580
+ 'text' => 'Second item with '
581
+ }, {
582
+ 'type' => 'text',
583
+ 'text' => 'code',
584
+ 'marks' => [{
585
+ 'type' => 'code'
586
+ }]
587
+ }]
588
+ }]
589
+ }]
590
+ }]
591
+ }
592
+
593
+ document = described_class.parse(html)
594
+ expect(document.to_h).to eq(expected)
595
+ end
596
+
597
+ it 'renders blockquotes with citations correctly' do
598
+ html = <<~HTML
599
+ <blockquote cite="https://example.com">
600
+ <p>A quote with <strong>bold</strong> text</p>
601
+ </blockquote>
602
+ HTML
603
+
604
+ expected = {
605
+ 'type' => 'doc',
606
+ 'content' => [{
607
+ 'type' => 'blockquote',
608
+ 'attrs' => {
609
+ 'citation' => 'https://example.com'
610
+ },
611
+ 'content' => [{
612
+ 'type' => 'paragraph',
613
+ 'content' => [{
614
+ 'type' => 'text',
615
+ 'text' => 'A quote with '
616
+ }, {
617
+ 'type' => 'text',
618
+ 'text' => 'bold',
619
+ 'marks' => [{
620
+ 'type' => 'bold'
621
+ }]
622
+ }, {
623
+ 'type' => 'text',
624
+ 'text' => ' text'
625
+ }]
626
+ }]
627
+ }]
628
+ }
629
+
630
+ document = described_class.parse(html)
631
+ expect(document.to_h).to eq(expected)
632
+ end
633
+
634
+ it 'renders code blocks with language correctly' do
635
+ html = <<~HTML
636
+ <pre><code class="language-ruby">def example
637
+ puts "Hello"
638
+ end</code></pre>
639
+ HTML
640
+
641
+ expected = {
642
+ 'type' => 'doc',
643
+ 'content' => [{
644
+ 'type' => 'code_block_wrapper',
645
+ 'attrs' => {
646
+ 'line_numbers' => false
647
+ },
648
+ 'content' => [{
649
+ 'type' => 'code_block',
650
+ 'attrs' => {
651
+ 'content' => "def example\n puts \"Hello\"\nend",
652
+ 'language' => 'ruby'
653
+ }
654
+ }]
655
+ }]
656
+ }
657
+
658
+ document = described_class.parse(html)
659
+ expect(document.to_h).to eq(expected)
660
+ end
661
+
662
+ it 'renders images with attributes correctly' do
663
+ html = '<img src="test.jpg" alt="Test image" title="Test title" width="800" height="600">'
664
+
665
+ expected = {
666
+ 'type' => 'doc',
667
+ 'content' => [{
668
+ 'type' => 'image',
669
+ 'attrs' => {
670
+ 'src' => 'test.jpg',
671
+ 'alt' => 'Test image',
672
+ 'title' => 'Test title',
673
+ 'width' => 800,
674
+ 'height' => 600
675
+ }
676
+ }]
677
+ }
678
+
679
+ document = described_class.parse(html)
680
+ expect(document.to_h).to eq(expected)
681
+ end
682
+
683
+ it 'renders horizontal rules with styles correctly' do
684
+ html = '<hr style="border-style: dashed; width: 80%; border-width: 2px">'
685
+
686
+ expected = {
687
+ 'type' => 'doc',
688
+ 'content' => [{
689
+ 'type' => 'horizontal_rule',
690
+ 'attrs' => {
691
+ 'style' => 'dashed',
692
+ 'width' => '80%',
693
+ 'thickness' => 2
694
+ }
695
+ }]
696
+ }
697
+
698
+ document = described_class.parse(html)
699
+ expect(document.to_h).to eq(expected)
700
+ end
701
+
702
+ it 'parses user mentions correctly' do
703
+ html = '<user-mention data-id="123"></user-mention>'
704
+
705
+ expected = {
706
+ 'type' => 'doc',
707
+ 'content' => [{
708
+ 'type' => 'user',
709
+ 'attrs' => {
710
+ 'id' => '123'
711
+ },
712
+ 'content' => []
713
+ }]
714
+ }
715
+
716
+ document = described_class.parse(html)
717
+ expect(document.to_h).to eq(expected)
718
+ end
719
+
720
+ it 'parses user mentions in paragraphs' do
721
+ html = '<p>Hello <user-mention data-id="123"></user-mention>!</p>'
722
+
723
+ expected = {
724
+ 'type' => 'doc',
725
+ 'content' => [{
726
+ 'type' => 'paragraph',
727
+ 'content' => [
728
+ {
729
+ 'type' => 'text',
730
+ 'text' => 'Hello '
731
+ },
732
+ {
733
+ 'type' => 'user',
734
+ 'attrs' => {
735
+ 'id' => '123'
736
+ },
737
+ 'content' => []
738
+ },
739
+ {
740
+ 'type' => 'text',
741
+ 'text' => '!'
742
+ }
743
+ ]
744
+ }]
745
+ }
746
+
747
+ document = described_class.parse(html)
748
+ expect(document.to_h).to eq(expected)
749
+ end
750
+
751
+ it 'ignores user mentions without data-id' do
752
+ html = '<user-mention></user-mention>'
753
+
754
+ expected = {
755
+ 'type' => 'doc'
756
+ }
757
+
758
+ document = described_class.parse(html)
759
+ expect(document.to_h).to eq(expected)
760
+ end
761
+
762
+ it 'parses multiple user mentions' do
763
+ html = '<div>Mentioned: <user-mention data-id="123"></user-mention> and <user-mention data-id="456"></user-mention></div>'
764
+
765
+ expected = {
766
+ 'type' => 'doc',
767
+ 'content' => [
768
+ {
769
+ 'type' => 'text',
770
+ 'text' => 'Mentioned: '
771
+ },
772
+ {
773
+ 'type' => 'user',
774
+ 'attrs' => {
775
+ 'id' => '123'
776
+ },
777
+ 'content' => []
778
+ },
779
+ {
780
+ 'type' => 'text',
781
+ 'text' => ' and '
782
+ },
783
+ {
784
+ 'type' => 'user',
785
+ 'attrs' => {
786
+ 'id' => '456'
787
+ },
788
+ 'content' => []
789
+ }
790
+ ]
791
+ }
792
+
793
+ document = described_class.parse(html)
794
+ expect(document.to_h).to eq(expected)
795
+ end
796
+ end
797
+ end