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
@@ -13,52 +13,276 @@ RSpec.describe Prosereflect::Table do
13
13
  describe '.create' do
14
14
  it 'creates an empty table' do
15
15
  table = described_class.create
16
- expect(table).to be_a(described_class)
17
- expect(table.type).to eq('table')
18
- expect(table.content).to be_empty
16
+
17
+ expected = {
18
+ 'type' => 'table',
19
+ 'content' => []
20
+ }
21
+
22
+ expect(table.to_h).to eq(expected)
19
23
  end
20
24
 
21
25
  it 'creates a table with attributes' do
22
- attrs = { 'width' => '100%' }
23
- table = described_class.create(attrs)
24
- expect(table.attrs).to eq(attrs)
26
+ table = described_class.create(attrs: {
27
+ 'width' => '100%',
28
+ 'alignment' => 'center',
29
+ 'border' => '1'
30
+ })
31
+
32
+ expected = {
33
+ 'type' => 'table',
34
+ 'attrs' => {
35
+ 'width' => '100%',
36
+ 'alignment' => 'center',
37
+ 'border' => '1'
38
+ },
39
+ 'content' => []
40
+ }
41
+
42
+ expect(table.to_h).to eq(expected)
43
+ end
44
+ end
45
+
46
+ describe 'table structure' do
47
+ it 'creates a simple table with header and data' do
48
+ table = described_class.create
49
+ table.add_header(%w[Name Age City])
50
+ table.add_row(['John', '25', 'New York'])
51
+ table.add_row(%w[Alice 30 London])
52
+
53
+ expected = {
54
+ 'type' => 'table',
55
+ 'content' => [{
56
+ 'type' => 'table_row',
57
+ 'content' => [{
58
+ 'type' => 'table_header',
59
+ 'content' => [{
60
+ 'type' => 'paragraph',
61
+ 'content' => [{
62
+ 'type' => 'text',
63
+ 'text' => 'Name'
64
+ }]
65
+ }]
66
+ }, {
67
+ 'type' => 'table_header',
68
+ 'content' => [{
69
+ 'type' => 'paragraph',
70
+ 'content' => [{
71
+ 'type' => 'text',
72
+ 'text' => 'Age'
73
+ }]
74
+ }]
75
+ }, {
76
+ 'type' => 'table_header',
77
+ 'content' => [{
78
+ 'type' => 'paragraph',
79
+ 'content' => [{
80
+ 'type' => 'text',
81
+ 'text' => 'City'
82
+ }]
83
+ }]
84
+ }]
85
+ }, {
86
+ 'type' => 'table_row',
87
+ 'content' => [{
88
+ 'type' => 'table_cell',
89
+ 'content' => [{
90
+ 'type' => 'paragraph',
91
+ 'content' => [{
92
+ 'type' => 'text',
93
+ 'text' => 'John'
94
+ }]
95
+ }]
96
+ }, {
97
+ 'type' => 'table_cell',
98
+ 'content' => [{
99
+ 'type' => 'paragraph',
100
+ 'content' => [{
101
+ 'type' => 'text',
102
+ 'text' => '25'
103
+ }]
104
+ }]
105
+ }, {
106
+ 'type' => 'table_cell',
107
+ 'content' => [{
108
+ 'type' => 'paragraph',
109
+ 'content' => [{
110
+ 'type' => 'text',
111
+ 'text' => 'New York'
112
+ }]
113
+ }]
114
+ }]
115
+ }, {
116
+ 'type' => 'table_row',
117
+ 'content' => [{
118
+ 'type' => 'table_cell',
119
+ 'content' => [{
120
+ 'type' => 'paragraph',
121
+ 'content' => [{
122
+ 'type' => 'text',
123
+ 'text' => 'Alice'
124
+ }]
125
+ }]
126
+ }, {
127
+ 'type' => 'table_cell',
128
+ 'content' => [{
129
+ 'type' => 'paragraph',
130
+ 'content' => [{
131
+ 'type' => 'text',
132
+ 'text' => '30'
133
+ }]
134
+ }]
135
+ }, {
136
+ 'type' => 'table_cell',
137
+ 'content' => [{
138
+ 'type' => 'paragraph',
139
+ 'content' => [{
140
+ 'type' => 'text',
141
+ 'text' => 'London'
142
+ }]
143
+ }]
144
+ }]
145
+ }]
146
+ }
147
+
148
+ expect(table.to_h).to eq(expected)
149
+ end
150
+
151
+ it 'creates a table with complex cell content' do
152
+ table = described_class.create
153
+ table.add_header(%w[Description Status])
154
+
155
+ row = table.add_row
156
+ cell = row.add_cell
157
+ para = cell.add_paragraph('This is ')
158
+ para.add_text('bold', [Prosereflect::Mark::Bold.new])
159
+ para.add_text(' and ')
160
+ para.add_text('italic', [Prosereflect::Mark::Italic.new])
161
+ para.add_hard_break
162
+ para.add_text('text')
163
+
164
+ cell = row.add_cell
165
+ para = cell.add_paragraph('Complete')
166
+ para.add_text(' ✓', [Prosereflect::Mark::Bold.new, Prosereflect::Mark::Strike.new])
167
+
168
+ expected = {
169
+ 'type' => 'table',
170
+ 'content' => [{
171
+ 'type' => 'table_row',
172
+ 'content' => [{
173
+ 'type' => 'table_header',
174
+ 'content' => [{
175
+ 'type' => 'paragraph',
176
+ 'content' => [{
177
+ 'type' => 'text',
178
+ 'text' => 'Description'
179
+ }]
180
+ }]
181
+ }, {
182
+ 'type' => 'table_header',
183
+ 'content' => [{
184
+ 'type' => 'paragraph',
185
+ 'content' => [{
186
+ 'type' => 'text',
187
+ 'text' => 'Status'
188
+ }]
189
+ }]
190
+ }]
191
+ }, {
192
+ 'type' => 'table_row',
193
+ 'content' => [{
194
+ 'type' => 'table_cell',
195
+ 'content' => [{
196
+ 'type' => 'paragraph',
197
+ 'content' => [{
198
+ 'type' => 'text',
199
+ 'text' => 'This is '
200
+ }, {
201
+ 'type' => 'text',
202
+ 'text' => 'bold',
203
+ 'marks' => [{
204
+ 'type' => 'bold'
205
+ }]
206
+ }, {
207
+ 'type' => 'text',
208
+ 'text' => ' and '
209
+ }, {
210
+ 'type' => 'text',
211
+ 'text' => 'italic',
212
+ 'marks' => [{
213
+ 'type' => 'italic'
214
+ }]
215
+ }, {
216
+ 'type' => 'hard_break'
217
+ }, {
218
+ 'type' => 'text',
219
+ 'text' => 'text'
220
+ }]
221
+ }]
222
+ }, {
223
+ 'type' => 'table_cell',
224
+ 'content' => [{
225
+ 'type' => 'paragraph',
226
+ 'content' => [{
227
+ 'type' => 'text',
228
+ 'text' => 'Complete'
229
+ }, {
230
+ 'type' => 'text',
231
+ 'text' => ' ✓',
232
+ 'marks' => [{
233
+ 'type' => 'bold'
234
+ }, {
235
+ 'type' => 'strike'
236
+ }]
237
+ }]
238
+ }]
239
+ }]
240
+ }]
241
+ }
242
+
243
+ expect(table.to_h).to eq(expected)
25
244
  end
26
245
  end
27
246
 
28
- describe 'row access methods' do
247
+ describe 'table operations' do
29
248
  let(:table) do
30
249
  t = described_class.create
31
- t.add_header(['Header 1', 'Header 2'])
32
- t.add_row(['Data 1', 'Data 2'])
33
- t.add_row(['Data 3', 'Data 4'])
250
+ t.add_header(%w[Name Age City])
251
+ t.add_row(['John', '25', 'New York'])
252
+ t.add_row(%w[Alice 30 London])
34
253
  t
35
254
  end
36
255
 
37
256
  describe '#rows' do
38
- it 'returns all rows' do
257
+ it 'returns all rows including header' do
39
258
  expect(table.rows.size).to eq(3)
40
259
  expect(table.rows).to all(be_a(Prosereflect::TableRow))
260
+ expect(table.rows.first.cells.first).to be_a(Prosereflect::TableHeader)
261
+ expect(table.rows[1].cells.first).to be_a(Prosereflect::TableCell)
41
262
  end
42
263
  end
43
264
 
44
265
  describe '#header_row' do
45
- it 'returns the first row' do
46
- expect(table.header_row).to be_a(Prosereflect::TableRow)
47
- expect(table.header_row.cells.first.text_content).to eq('Header 1')
266
+ it 'returns the first row with header cells' do
267
+ header = table.header_row
268
+ expect(header).to be_a(Prosereflect::TableRow)
269
+ expect(header.cells).to all(be_a(Prosereflect::TableHeader))
270
+ expect(header.cells.map(&:text_content)).to eq(%w[Name Age City])
48
271
  end
49
- end
50
272
 
51
- describe '#data_rows' do
52
- it 'returns all rows except the header' do
53
- expect(table.data_rows.size).to eq(2)
54
- expect(table.data_rows).to all(be_a(Prosereflect::TableRow))
55
- expect(table.data_rows.first.cells.first.text_content).to eq('Data 1')
273
+ it 'returns nil for empty table' do
274
+ empty_table = described_class.create
275
+ expect(empty_table.header_row).to be_nil
56
276
  end
277
+ end
57
278
 
58
- it 'returns empty array if there are no data rows' do
59
- table = described_class.create
60
- table.add_header(['Header'])
61
- expect(table.data_rows).to eq([])
279
+ describe '#data_rows' do
280
+ it 'returns all non-header rows' do
281
+ data_rows = table.data_rows
282
+ expect(data_rows.size).to eq(2)
283
+ expect(data_rows).to all(be_a(Prosereflect::TableRow))
284
+ expect(data_rows.first.cells.map(&:text_content)).to eq(['John', '25', 'New York'])
285
+ expect(data_rows.last.cells.map(&:text_content)).to eq(%w[Alice 30 London])
62
286
  end
63
287
  end
64
288
 
@@ -66,10 +290,10 @@ RSpec.describe Prosereflect::Table do
66
290
  it 'returns cell at specified position' do
67
291
  cell = table.cell_at(0, 1)
68
292
  expect(cell).to be_a(Prosereflect::TableCell)
69
- expect(cell.text_content).to eq('Data 2')
293
+ expect(cell.text_content).to eq('25')
70
294
  end
71
295
 
72
- it 'returns nil for out of bounds indices' do
296
+ it 'returns nil for invalid positions' do
73
297
  expect(table.cell_at(-1, 0)).to be_nil
74
298
  expect(table.cell_at(0, -1)).to be_nil
75
299
  expect(table.cell_at(5, 0)).to be_nil
@@ -78,59 +302,166 @@ RSpec.describe Prosereflect::Table do
78
302
  end
79
303
  end
80
304
 
81
- describe 'table building methods' do
305
+ describe 'table building' do
82
306
  describe '#add_header' do
83
- it 'adds a header row with cells' do
307
+ it 'adds a header row with styled cells' do
84
308
  table = described_class.create
85
- header = table.add_header(['Col 1', 'Col 2'])
86
-
87
- expect(table.rows.size).to eq(1)
88
- expect(header).to be_a(Prosereflect::TableRow)
89
- expect(header.cells.size).to eq(2)
90
- expect(header.cells.map(&:text_content)).to eq(['Col 1', 'Col 2'])
91
- end
92
- end
309
+ header = table.add_header(%w[Title Description])
310
+ header.cells.first.add_text(' (required)', [Prosereflect::Mark::Italic.new])
93
311
 
94
- describe '#add_row' do
95
- it 'adds a row with cells' do
96
- table = described_class.create
97
- row = table.add_row(['Data 1', 'Data 2'])
312
+ expected = {
313
+ 'type' => 'table',
314
+ 'content' => [{
315
+ 'type' => 'table_row',
316
+ 'content' => [{
317
+ 'type' => 'table_header',
318
+ 'content' => [{
319
+ 'type' => 'paragraph',
320
+ 'content' => [{
321
+ 'type' => 'text',
322
+ 'text' => 'Title'
323
+ }, {
324
+ 'type' => 'text',
325
+ 'text' => ' (required)',
326
+ 'marks' => [{
327
+ 'type' => 'italic'
328
+ }]
329
+ }]
330
+ }]
331
+ }, {
332
+ 'type' => 'table_header',
333
+ 'content' => [{
334
+ 'type' => 'paragraph',
335
+ 'content' => [{
336
+ 'type' => 'text',
337
+ 'text' => 'Description'
338
+ }]
339
+ }]
340
+ }]
341
+ }]
342
+ }
98
343
 
99
- expect(table.rows.size).to eq(1)
100
- expect(row).to be_a(Prosereflect::TableRow)
101
- expect(row.cells.size).to eq(2)
102
- expect(row.cells.map(&:text_content)).to eq(['Data 1', 'Data 2'])
103
- end
104
-
105
- it 'adds an empty row when no data provided' do
106
- table = described_class.create
107
- row = table.add_row
108
-
109
- expect(table.rows.size).to eq(1)
110
- expect(row.cells).to be_empty
344
+ expect(table.to_h).to eq(expected)
111
345
  end
112
346
  end
113
347
 
114
348
  describe '#add_rows' do
115
- it 'adds multiple rows at once' do
349
+ it 'adds multiple rows with mixed content' do
116
350
  table = described_class.create
351
+ table.add_header(%w[Item Status])
352
+
117
353
  table.add_rows([
118
- ['Row 1, Cell 1', 'Row 1, Cell 2'],
119
- ['Row 2, Cell 1', 'Row 2, Cell 2']
354
+ ['Task 1', 'Done'],
355
+ ['Task 2', 'Pending']
120
356
  ])
121
357
 
122
- expect(table.rows.size).to eq(2)
123
- expect(table.rows[0].cells.size).to eq(2)
124
- expect(table.rows[1].cells.size).to eq(2)
125
- expect(table.rows[0].cells.first.text_content).to eq('Row 1, Cell 1')
126
- expect(table.rows[1].cells.first.text_content).to eq('Row 2, Cell 1')
358
+ row = table.add_row
359
+ cell = row.add_cell
360
+ cell.add_paragraph('Task 3')
361
+ cell = row.add_cell
362
+ para = cell.add_paragraph
363
+ para.add_text('In Progress', [Prosereflect::Mark::Bold.new])
364
+
365
+ expected = {
366
+ 'type' => 'table',
367
+ 'content' => [{
368
+ 'type' => 'table_row',
369
+ 'content' => [{
370
+ 'type' => 'table_header',
371
+ 'content' => [{
372
+ 'type' => 'paragraph',
373
+ 'content' => [{
374
+ 'type' => 'text',
375
+ 'text' => 'Item'
376
+ }]
377
+ }]
378
+ }, {
379
+ 'type' => 'table_header',
380
+ 'content' => [{
381
+ 'type' => 'paragraph',
382
+ 'content' => [{
383
+ 'type' => 'text',
384
+ 'text' => 'Status'
385
+ }]
386
+ }]
387
+ }]
388
+ }, {
389
+ 'type' => 'table_row',
390
+ 'content' => [{
391
+ 'type' => 'table_cell',
392
+ 'content' => [{
393
+ 'type' => 'paragraph',
394
+ 'content' => [{
395
+ 'type' => 'text',
396
+ 'text' => 'Task 1'
397
+ }]
398
+ }]
399
+ }, {
400
+ 'type' => 'table_cell',
401
+ 'content' => [{
402
+ 'type' => 'paragraph',
403
+ 'content' => [{
404
+ 'type' => 'text',
405
+ 'text' => 'Done'
406
+ }]
407
+ }]
408
+ }]
409
+ }, {
410
+ 'type' => 'table_row',
411
+ 'content' => [{
412
+ 'type' => 'table_cell',
413
+ 'content' => [{
414
+ 'type' => 'paragraph',
415
+ 'content' => [{
416
+ 'type' => 'text',
417
+ 'text' => 'Task 2'
418
+ }]
419
+ }]
420
+ }, {
421
+ 'type' => 'table_cell',
422
+ 'content' => [{
423
+ 'type' => 'paragraph',
424
+ 'content' => [{
425
+ 'type' => 'text',
426
+ 'text' => 'Pending'
427
+ }]
428
+ }]
429
+ }]
430
+ }, {
431
+ 'type' => 'table_row',
432
+ 'content' => [{
433
+ 'type' => 'table_cell',
434
+ 'content' => [{
435
+ 'type' => 'paragraph',
436
+ 'content' => [{
437
+ 'type' => 'text',
438
+ 'text' => 'Task 3'
439
+ }]
440
+ }]
441
+ }, {
442
+ 'type' => 'table_cell',
443
+ 'content' => [{
444
+ 'type' => 'paragraph',
445
+ 'content' => [{
446
+ 'type' => 'text',
447
+ 'text' => 'In Progress',
448
+ 'marks' => [{
449
+ 'type' => 'bold'
450
+ }]
451
+ }]
452
+ }]
453
+ }]
454
+ }]
455
+ }
456
+
457
+ expect(table.to_h).to eq(expected)
127
458
  end
128
459
  end
129
460
  end
130
461
 
131
462
  describe 'serialization' do
132
463
  it 'converts to hash representation' do
133
- table = described_class.create
464
+ table = described_class.new
134
465
  table.add_header(['Col 1', 'Col 2'])
135
466
  table.add_row(['Data 1', 'Data 2'])
136
467