prosereflect 0.1.0 → 0.2.0

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +4 -0
  3. data/.github/workflows/release.yml +5 -0
  4. data/.rubocop.yml +19 -1
  5. data/.rubocop_todo.yml +143 -174
  6. data/CLAUDE.md +78 -0
  7. data/Gemfile +8 -4
  8. data/README.adoc +193 -12
  9. data/Rakefile +3 -3
  10. data/lib/prosereflect/attribute/base.rb +32 -0
  11. data/lib/prosereflect/attribute/bold.rb +18 -0
  12. data/lib/prosereflect/attribute/href.rb +22 -0
  13. data/lib/prosereflect/attribute/id.rb +24 -0
  14. data/lib/prosereflect/attribute.rb +10 -0
  15. data/lib/prosereflect/blockquote.rb +84 -0
  16. data/lib/prosereflect/bullet_list.rb +84 -0
  17. data/lib/prosereflect/code_block.rb +135 -0
  18. data/lib/prosereflect/code_block_wrapper.rb +65 -0
  19. data/lib/prosereflect/document.rb +93 -26
  20. data/lib/prosereflect/hard_break.rb +13 -11
  21. data/lib/prosereflect/heading.rb +63 -0
  22. data/lib/prosereflect/horizontal_rule.rb +70 -0
  23. data/lib/prosereflect/image.rb +126 -0
  24. data/lib/prosereflect/input/html.rb +484 -0
  25. data/lib/prosereflect/input.rb +7 -0
  26. data/lib/prosereflect/list_item.rb +64 -0
  27. data/lib/prosereflect/mark/base.rb +47 -0
  28. data/lib/prosereflect/mark/bold.rb +13 -0
  29. data/lib/prosereflect/mark/code.rb +12 -0
  30. data/lib/prosereflect/mark/italic.rb +13 -0
  31. data/lib/prosereflect/mark/link.rb +16 -0
  32. data/lib/prosereflect/mark/strike.rb +13 -0
  33. data/lib/prosereflect/mark/subscript.rb +13 -0
  34. data/lib/prosereflect/mark/superscript.rb +13 -0
  35. data/lib/prosereflect/mark/underline.rb +13 -0
  36. data/lib/prosereflect/mark.rb +15 -0
  37. data/lib/prosereflect/node.rb +181 -32
  38. data/lib/prosereflect/ordered_list.rb +86 -0
  39. data/lib/prosereflect/output/html.rb +376 -0
  40. data/lib/prosereflect/output.rb +7 -0
  41. data/lib/prosereflect/paragraph.rb +29 -20
  42. data/lib/prosereflect/parser.rb +101 -33
  43. data/lib/prosereflect/table.rb +42 -12
  44. data/lib/prosereflect/table_cell.rb +36 -11
  45. data/lib/prosereflect/table_header.rb +92 -0
  46. data/lib/prosereflect/table_row.rb +34 -11
  47. data/lib/prosereflect/text.rb +15 -19
  48. data/lib/prosereflect/user.rb +63 -0
  49. data/lib/prosereflect/version.rb +1 -1
  50. data/lib/prosereflect.rb +27 -11
  51. data/prosereflect.gemspec +17 -15
  52. data/spec/prosereflect/document_spec.rb +477 -75
  53. data/spec/prosereflect/hard_break_spec.rb +226 -30
  54. data/spec/prosereflect/input/html_spec.rb +797 -0
  55. data/spec/prosereflect/node_spec.rb +307 -137
  56. data/spec/prosereflect/output/html_spec.rb +369 -0
  57. data/spec/prosereflect/paragraph_spec.rb +458 -82
  58. data/spec/prosereflect/parser_spec.rb +311 -93
  59. data/spec/prosereflect/table_cell_spec.rb +282 -71
  60. data/spec/prosereflect/table_row_spec.rb +218 -48
  61. data/spec/prosereflect/table_spec.rb +415 -82
  62. data/spec/prosereflect/text_spec.rb +231 -72
  63. data/spec/prosereflect/user_spec.rb +76 -0
  64. data/spec/prosereflect_spec.rb +30 -23
  65. data/spec/spec_helper.rb +6 -6
  66. data/spec/support/matchers.rb +6 -6
  67. data/spec/support/shared_examples.rb +79 -50
  68. metadata +53 -6
  69. data/debug_loading.rb +0 -34
  70. data/spec/prosereflect/version_spec.rb +0 -11
@@ -1,75 +1,301 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require "spec_helper"
4
4
 
5
5
  RSpec.describe Prosereflect::Table do
6
- describe 'initialization' do
7
- it 'initializes as a table node' do
8
- table = described_class.new({ 'type' => 'table' })
9
- expect(table.type).to eq('table')
6
+ describe "initialization" do
7
+ it "initializes as a table node" do
8
+ table = described_class.new({ "type" => "table" })
9
+ expect(table.type).to eq("table")
10
10
  end
11
11
  end
12
12
 
13
- describe '.create' do
14
- it 'creates an empty table' do
13
+ describe ".create" do
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
- it 'creates a table with attributes' do
22
- attrs = { 'width' => '100%' }
23
- table = described_class.create(attrs)
24
- expect(table.attrs).to eq(attrs)
25
+ it "creates a table with attributes" do
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
- describe '#rows' do
38
- it 'returns all rows' do
256
+ describe "#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
- 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')
265
+ describe "#header_row" do
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",
285
+ "New York"])
286
+ expect(data_rows.last.cells.map(&:text_content)).to eq(%w[Alice 30
287
+ London])
62
288
  end
63
289
  end
64
290
 
65
- describe '#cell_at' do
66
- it 'returns cell at specified position' do
291
+ describe "#cell_at" do
292
+ it "returns cell at specified position" do
67
293
  cell = table.cell_at(0, 1)
68
294
  expect(cell).to be_a(Prosereflect::TableCell)
69
- expect(cell.text_content).to eq('Data 2')
295
+ expect(cell.text_content).to eq("25")
70
296
  end
71
297
 
72
- it 'returns nil for out of bounds indices' do
298
+ it "returns nil for invalid positions" do
73
299
  expect(table.cell_at(-1, 0)).to be_nil
74
300
  expect(table.cell_at(0, -1)).to be_nil
75
301
  expect(table.cell_at(5, 0)).to be_nil
@@ -78,67 +304,174 @@ RSpec.describe Prosereflect::Table do
78
304
  end
79
305
  end
80
306
 
81
- describe 'table building methods' do
82
- describe '#add_header' do
83
- it 'adds a header row with cells' do
307
+ describe "table building" do
308
+ describe "#add_header" do
309
+ it "adds a header row with styled cells" do
84
310
  table = described_class.create
85
- header = table.add_header(['Col 1', 'Col 2'])
311
+ header = table.add_header(%w[Title Description])
312
+ header.cells.first.add_text(" (required)", [Prosereflect::Mark::Italic.new])
86
313
 
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'])
314
+ expected = {
315
+ "type" => "table",
316
+ "content" => [{
317
+ "type" => "table_row",
318
+ "content" => [{
319
+ "type" => "table_header",
320
+ "content" => [{
321
+ "type" => "paragraph",
322
+ "content" => [{
323
+ "type" => "text",
324
+ "text" => "Title",
325
+ }, {
326
+ "type" => "text",
327
+ "text" => " (required)",
328
+ "marks" => [{
329
+ "type" => "italic",
330
+ }],
331
+ }],
332
+ }],
333
+ }, {
334
+ "type" => "table_header",
335
+ "content" => [{
336
+ "type" => "paragraph",
337
+ "content" => [{
338
+ "type" => "text",
339
+ "text" => "Description",
340
+ }],
341
+ }],
342
+ }],
343
+ }],
344
+ }
345
+
346
+ expect(table.to_h).to eq(expected)
91
347
  end
92
348
  end
93
349
 
94
- describe '#add_row' do
95
- it 'adds a row with cells' do
350
+ describe "#add_rows" do
351
+ it "adds multiple rows with mixed content" do
96
352
  table = described_class.create
97
- row = table.add_row(['Data 1', 'Data 2'])
353
+ table.add_header(%w[Item Status])
98
354
 
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
355
+ table.add_rows([
356
+ ["Task 1", "Done"],
357
+ ["Task 2", "Pending"],
358
+ ])
104
359
 
105
- it 'adds an empty row when no data provided' do
106
- table = described_class.create
107
360
  row = table.add_row
361
+ cell = row.add_cell
362
+ cell.add_paragraph("Task 3")
363
+ cell = row.add_cell
364
+ para = cell.add_paragraph
365
+ para.add_text("In Progress", [Prosereflect::Mark::Bold.new])
108
366
 
109
- expect(table.rows.size).to eq(1)
110
- expect(row.cells).to be_empty
111
- end
112
- end
113
-
114
- describe '#add_rows' do
115
- it 'adds multiple rows at once' do
116
- table = described_class.create
117
- table.add_rows([
118
- ['Row 1, Cell 1', 'Row 1, Cell 2'],
119
- ['Row 2, Cell 1', 'Row 2, Cell 2']
120
- ])
367
+ expected = {
368
+ "type" => "table",
369
+ "content" => [{
370
+ "type" => "table_row",
371
+ "content" => [{
372
+ "type" => "table_header",
373
+ "content" => [{
374
+ "type" => "paragraph",
375
+ "content" => [{
376
+ "type" => "text",
377
+ "text" => "Item",
378
+ }],
379
+ }],
380
+ }, {
381
+ "type" => "table_header",
382
+ "content" => [{
383
+ "type" => "paragraph",
384
+ "content" => [{
385
+ "type" => "text",
386
+ "text" => "Status",
387
+ }],
388
+ }],
389
+ }],
390
+ }, {
391
+ "type" => "table_row",
392
+ "content" => [{
393
+ "type" => "table_cell",
394
+ "content" => [{
395
+ "type" => "paragraph",
396
+ "content" => [{
397
+ "type" => "text",
398
+ "text" => "Task 1",
399
+ }],
400
+ }],
401
+ }, {
402
+ "type" => "table_cell",
403
+ "content" => [{
404
+ "type" => "paragraph",
405
+ "content" => [{
406
+ "type" => "text",
407
+ "text" => "Done",
408
+ }],
409
+ }],
410
+ }],
411
+ }, {
412
+ "type" => "table_row",
413
+ "content" => [{
414
+ "type" => "table_cell",
415
+ "content" => [{
416
+ "type" => "paragraph",
417
+ "content" => [{
418
+ "type" => "text",
419
+ "text" => "Task 2",
420
+ }],
421
+ }],
422
+ }, {
423
+ "type" => "table_cell",
424
+ "content" => [{
425
+ "type" => "paragraph",
426
+ "content" => [{
427
+ "type" => "text",
428
+ "text" => "Pending",
429
+ }],
430
+ }],
431
+ }],
432
+ }, {
433
+ "type" => "table_row",
434
+ "content" => [{
435
+ "type" => "table_cell",
436
+ "content" => [{
437
+ "type" => "paragraph",
438
+ "content" => [{
439
+ "type" => "text",
440
+ "text" => "Task 3",
441
+ }],
442
+ }],
443
+ }, {
444
+ "type" => "table_cell",
445
+ "content" => [{
446
+ "type" => "paragraph",
447
+ "content" => [{
448
+ "type" => "text",
449
+ "text" => "In Progress",
450
+ "marks" => [{
451
+ "type" => "bold",
452
+ }],
453
+ }],
454
+ }],
455
+ }],
456
+ }],
457
+ }
121
458
 
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')
459
+ expect(table.to_h).to eq(expected)
127
460
  end
128
461
  end
129
462
  end
130
463
 
131
- describe 'serialization' do
132
- it 'converts to hash representation' do
133
- table = described_class.create
134
- table.add_header(['Col 1', 'Col 2'])
135
- table.add_row(['Data 1', 'Data 2'])
464
+ describe "serialization" do
465
+ it "converts to hash representation" do
466
+ table = described_class.new
467
+ table.add_header(["Col 1", "Col 2"])
468
+ table.add_row(["Data 1", "Data 2"])
136
469
 
137
470
  hash = table.to_h
138
- expect(hash['type']).to eq('table')
139
- expect(hash['content'].size).to eq(2)
140
- expect(hash['content'][0]['type']).to eq('table_row')
141
- expect(hash['content'][1]['type']).to eq('table_row')
471
+ expect(hash["type"]).to eq("table")
472
+ expect(hash["content"].size).to eq(2)
473
+ expect(hash["content"][0]["type"]).to eq("table_row")
474
+ expect(hash["content"][1]["type"]).to eq("table_row")
142
475
  end
143
476
  end
144
477
  end