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
@@ -5,125 +5,338 @@ require 'spec_helper'
5
5
  RSpec.describe Prosereflect::Parser do
6
6
  let(:fixtures_path) { File.join(__dir__, '..', 'fixtures') }
7
7
 
8
- describe '.parse_document' do
9
- context 'with YAML fixtures' do
10
- let(:yaml_files) { Dir.glob(File.join(fixtures_path, '*/*.yaml')) }
8
+ describe 'document parsing' do
9
+ describe '.parse_document' do
10
+ it 'parses a simple document' do
11
+ data = {
12
+ 'type' => 'doc',
13
+ 'content' => [
14
+ {
15
+ 'type' => 'paragraph',
16
+ 'content' => [
17
+ { 'type' => 'text', 'text' => 'Hello world' }
18
+ ]
19
+ }
20
+ ]
21
+ }
11
22
 
12
- it 'parses all YAML fixtures successfully' do
13
- yaml_files.each do |yaml_file|
14
- yaml_content = File.read(yaml_file)
15
- data = YAML.safe_load(yaml_content)
23
+ document = described_class.parse_document(data)
24
+ expect(document).to be_a(Prosereflect::Document)
25
+ expect(document.text_content).to eq('Hello world')
26
+ end
27
+
28
+ it 'parses a complex document with multiple blocks' do
29
+ data = {
30
+ 'type' => 'doc',
31
+ 'content' => [
32
+ {
33
+ 'type' => 'paragraph',
34
+ 'content' => [
35
+ { 'type' => 'text', 'text' => 'First paragraph' }
36
+ ]
37
+ },
38
+ {
39
+ 'type' => 'bullet_list',
40
+ 'content' => [
41
+ {
42
+ 'type' => 'list_item',
43
+ 'content' => [
44
+ {
45
+ 'type' => 'paragraph',
46
+ 'content' => [
47
+ { 'type' => 'text', 'text' => 'List item' }
48
+ ]
49
+ }
50
+ ]
51
+ }
52
+ ]
53
+ },
54
+ {
55
+ 'type' => 'blockquote',
56
+ 'content' => [
57
+ {
58
+ 'type' => 'paragraph',
59
+ 'content' => [
60
+ { 'type' => 'text', 'text' => 'Quote text' }
61
+ ]
62
+ }
63
+ ]
64
+ }
65
+ ]
66
+ }
67
+
68
+ document = described_class.parse_document(data)
69
+ expect(document).to be_a(Prosereflect::Document)
70
+ expect(document.content.size).to eq(3)
71
+ expect(document.content[0]).to be_a(Prosereflect::Paragraph)
72
+ expect(document.content[1]).to be_a(Prosereflect::BulletList)
73
+ expect(document.content[2]).to be_a(Prosereflect::Blockquote)
74
+ end
16
75
 
17
- expect do
76
+ context 'with fixtures' do
77
+ it 'parses all YAML fixtures successfully' do
78
+ Dir.glob(File.join(fixtures_path, '*/*.yaml')).each do |yaml_file|
79
+ data = YAML.safe_load(File.read(yaml_file))
18
80
  document = described_class.parse_document(data)
19
81
  expect(document).to be_a(Prosereflect::Document)
20
- end.not_to raise_error
82
+ end
21
83
  end
22
- end
23
- end
24
84
 
25
- context 'with JSON fixtures' do
26
- let(:json_files) { Dir.glob(File.join(fixtures_path, '*/*.json')) }
27
-
28
- it 'parses all JSON fixtures successfully' do
29
- json_files.each do |json_file|
30
- json_content = File.read(json_file)
31
- data = JSON.parse(json_content)
32
-
33
- expect do
85
+ it 'parses all JSON fixtures successfully' do
86
+ Dir.glob(File.join(fixtures_path, '*/*.json')).each do |json_file|
87
+ data = JSON.parse(File.read(json_file))
34
88
  document = described_class.parse_document(data)
35
89
  expect(document).to be_a(Prosereflect::Document)
36
- end.not_to raise_error
90
+ end
37
91
  end
38
92
  end
39
- end
40
93
 
41
- context 'with invalid input' do
42
- it 'raises an error for nil input' do
43
- expect { described_class.parse_document(nil) }.to raise_error(ArgumentError)
44
- end
94
+ context 'with invalid input' do
95
+ it 'raises an error for nil input' do
96
+ expect { described_class.parse_document(nil) }.to raise_error(ArgumentError)
97
+ end
45
98
 
46
- it 'raises an error for non-hash input' do
47
- expect { described_class.parse_document('not a hash') }.to raise_error(ArgumentError)
99
+ it 'raises an error for non-hash input' do
100
+ expect { described_class.parse_document('not a hash') }.to raise_error(ArgumentError)
101
+ end
102
+
103
+ it 'wraps non-document nodes in a document' do
104
+ data = { 'type' => 'paragraph', 'content' => [] }
105
+ document = described_class.parse_document(data)
106
+ expect(document).to be_a(Prosereflect::Document)
107
+ expect(document.content.first).to be_a(Prosereflect::Paragraph)
108
+ end
109
+
110
+ it 'wraps generic nodes in a document' do
111
+ document = described_class.parse_document({ 'content' => [] })
112
+ expect(document).to be_a(Prosereflect::Document)
113
+ expect(document.content.size).to eq(1)
114
+ expect(document.content.first).to be_a(Prosereflect::Node)
115
+ expect(document.content.first.type).to eq('node')
116
+ end
48
117
  end
49
118
  end
50
119
  end
51
120
 
52
- describe '.parse_node' do
53
- it 'creates the correct node type based on the input' do
54
- # Test document node
55
- doc_data = { 'type' => 'doc', 'content' => [] }
56
- expect(described_class.parse_node(doc_data)).to be_a(Prosereflect::Document)
121
+ describe 'node parsing' do
122
+ describe '.parse_node' do
123
+ context 'basic nodes' do
124
+ it 'parses text nodes' do
125
+ data = {
126
+ 'type' => 'text',
127
+ 'text' => 'Hello world',
128
+ 'marks' => [
129
+ { 'type' => 'bold' },
130
+ { 'type' => 'italic' }
131
+ ]
132
+ }
57
133
 
58
- # Test paragraph node
59
- para_data = { 'type' => 'paragraph', 'content' => [] }
60
- expect(described_class.parse_node(para_data)).to be_a(Prosereflect::Paragraph)
134
+ node = described_class.parse_node(data)
135
+ expect(node).to be_a(Prosereflect::Text)
136
+ expect(node.text).to eq('Hello world')
137
+ expect(node.marks.map { |m| m['type'] }).to eq(%w[bold italic])
138
+ end
61
139
 
62
- # Test text node
63
- text_data = { 'type' => 'text', 'text' => 'Hello' }
64
- expect(described_class.parse_node(text_data)).to be_a(Prosereflect::Text)
140
+ it 'parses paragraph nodes' do
141
+ data = {
142
+ 'type' => 'paragraph',
143
+ 'content' => [
144
+ { 'type' => 'text', 'text' => 'First' },
145
+ { 'type' => 'hard_break' },
146
+ { 'type' => 'text', 'text' => 'Second' }
147
+ ]
148
+ }
65
149
 
66
- # Test hard_break node
67
- break_data = { 'type' => 'hard_break' }
68
- expect(described_class.parse_node(break_data)).to be_a(Prosereflect::HardBreak)
150
+ node = described_class.parse_node(data)
151
+ expect(node).to be_a(Prosereflect::Paragraph)
152
+ expect(node.content.size).to eq(3)
153
+ expect(node.text_content).to eq("First\nSecond")
154
+ end
69
155
 
70
- # Test table node
71
- table_data = { 'type' => 'table', 'content' => [] }
72
- expect(described_class.parse_node(table_data)).to be_a(Prosereflect::Table)
156
+ it 'parses hard break nodes' do
157
+ data = { 'type' => 'hard_break' }
158
+ node = described_class.parse_node(data)
159
+ expect(node).to be_a(Prosereflect::HardBreak)
160
+ end
73
161
 
74
- # Test table_row node
75
- row_data = { 'type' => 'table_row', 'content' => [] }
76
- expect(described_class.parse_node(row_data)).to be_a(Prosereflect::TableRow)
162
+ it 'parses horizontal rule nodes' do
163
+ data = {
164
+ 'type' => 'horizontal_rule',
165
+ 'attrs' => {
166
+ 'border_style' => 'dashed',
167
+ 'width' => '80%'
168
+ }
169
+ }
77
170
 
78
- # Test table_cell node
79
- cell_data = { 'type' => 'table_cell', 'content' => [] }
80
- expect(described_class.parse_node(cell_data)).to be_a(Prosereflect::TableCell)
171
+ node = described_class.parse_node(data)
172
+ expect(node).to be_a(Prosereflect::HorizontalRule)
173
+ expect(node.style).to eq('dashed')
174
+ expect(node.width).to eq('80%')
175
+ end
176
+ end
81
177
 
82
- # Test generic node
83
- generic_data = { 'type' => 'unknown_type' }
84
- expect(described_class.parse_node(generic_data)).to be_a(Prosereflect::Node)
85
- end
178
+ context 'list nodes' do
179
+ it 'parses bullet lists' do
180
+ data = {
181
+ 'type' => 'bullet_list',
182
+ 'attrs' => { 'bullet_style' => 'square' },
183
+ 'content' => [
184
+ {
185
+ 'type' => 'list_item',
186
+ 'content' => [
187
+ {
188
+ 'type' => 'paragraph',
189
+ 'content' => [{ 'type' => 'text', 'text' => 'Item 1' }]
190
+ }
191
+ ]
192
+ }
193
+ ]
194
+ }
86
195
 
87
- it 'handles nodes with content' do
88
- data = {
89
- 'type' => 'paragraph',
90
- 'content' => [
91
- { 'type' => 'text', 'text' => 'Hello' },
92
- { 'type' => 'hard_break' },
93
- { 'type' => 'text', 'text' => 'World' }
94
- ]
95
- }
96
-
97
- node = described_class.parse_node(data)
98
- expect(node).to be_a(Prosereflect::Paragraph)
99
- expect(node.content.size).to eq(3)
100
- expect(node.content[0]).to be_a(Prosereflect::Text)
101
- expect(node.content[1]).to be_a(Prosereflect::HardBreak)
102
- expect(node.content[2]).to be_a(Prosereflect::Text)
103
- end
196
+ node = described_class.parse_node(data)
197
+ expect(node).to be_a(Prosereflect::BulletList)
198
+ expect(node.bullet_style).to eq('square')
199
+ expect(node.items.first.text_content).to eq('Item 1')
200
+ end
104
201
 
105
- it 'handles nodes with marks' do
106
- data = {
107
- 'type' => 'text',
108
- 'text' => 'Bold text',
109
- 'marks' => [{ 'type' => 'bold' }]
110
- }
202
+ it 'parses ordered lists' do
203
+ data = {
204
+ 'type' => 'ordered_list',
205
+ 'attrs' => { 'start' => 3 },
206
+ 'content' => [
207
+ {
208
+ 'type' => 'list_item',
209
+ 'content' => [
210
+ {
211
+ 'type' => 'paragraph',
212
+ 'content' => [{ 'type' => 'text', 'text' => 'Item 3' }]
213
+ }
214
+ ]
215
+ }
216
+ ]
217
+ }
111
218
 
112
- node = described_class.parse_node(data)
113
- expect(node).to be_a(Prosereflect::Text)
114
- expect(node.marks).to eq([{ 'type' => 'bold' }])
115
- end
219
+ node = described_class.parse_node(data)
220
+ expect(node).to be_a(Prosereflect::OrderedList)
221
+ expect(node.start).to eq(3)
222
+ expect(node.items.first.text_content).to eq('Item 3')
223
+ end
224
+
225
+ it 'parses nested lists' do
226
+ data = {
227
+ 'type' => 'bullet_list',
228
+ 'content' => [
229
+ {
230
+ 'type' => 'list_item',
231
+ 'content' => [
232
+ {
233
+ 'type' => 'paragraph',
234
+ 'content' => [{ 'type' => 'text', 'text' => 'Level 1' }]
235
+ },
236
+ {
237
+ 'type' => 'ordered_list',
238
+ 'content' => [
239
+ {
240
+ 'type' => 'list_item',
241
+ 'content' => [
242
+ {
243
+ 'type' => 'paragraph',
244
+ 'content' => [{ 'type' => 'text', 'text' => 'Level 2' }]
245
+ }
246
+ ]
247
+ }
248
+ ]
249
+ }
250
+ ]
251
+ }
252
+ ]
253
+ }
116
254
 
117
- it 'handles nodes with attributes' do
118
- data = {
119
- 'type' => 'table_cell',
120
- 'attrs' => { 'colspan' => 2, 'rowspan' => 1 },
121
- 'content' => []
122
- }
255
+ node = described_class.parse_node(data)
256
+ expect(node).to be_a(Prosereflect::BulletList)
257
+ expect(node.items.first.content.size).to eq(2)
258
+ expect(node.items.first.content[1]).to be_a(Prosereflect::OrderedList)
259
+ end
260
+ end
261
+
262
+ context 'table nodes' do
263
+ it 'parses tables with complex content' do
264
+ data = {
265
+ 'type' => 'table',
266
+ 'content' => [
267
+ {
268
+ 'type' => 'table_row',
269
+ 'content' => [
270
+ {
271
+ 'type' => 'table_cell',
272
+ 'attrs' => { 'colspan' => 2 },
273
+ 'content' => [
274
+ {
275
+ 'type' => 'paragraph',
276
+ 'content' => [
277
+ {
278
+ 'type' => 'text',
279
+ 'text' => 'Header',
280
+ 'marks' => [{ 'type' => 'bold' }]
281
+ }
282
+ ]
283
+ }
284
+ ]
285
+ }
286
+ ]
287
+ }
288
+ ]
289
+ }
290
+
291
+ node = described_class.parse_node(data)
292
+ expect(node).to be_a(Prosereflect::Table)
293
+ expect(node.rows.first.cells.first.attrs['colspan']).to eq(2)
294
+ expect(node.rows.first.cells.first.text_content).to eq('Header')
295
+ end
296
+ end
123
297
 
124
- node = described_class.parse_node(data)
125
- expect(node).to be_a(Prosereflect::TableCell)
126
- expect(node.attrs).to eq({ 'colspan' => 2, 'rowspan' => 1 })
298
+ context 'blockquote nodes' do
299
+ it 'parses blockquotes with citation' do
300
+ data = {
301
+ 'type' => 'blockquote',
302
+ 'attrs' => { 'cite' => 'Author' },
303
+ 'content' => [
304
+ {
305
+ 'type' => 'paragraph',
306
+ 'content' => [{ 'type' => 'text', 'text' => 'Quote' }]
307
+ }
308
+ ]
309
+ }
310
+
311
+ node = described_class.parse_node(data)
312
+ expect(node).to be_a(Prosereflect::Blockquote)
313
+ expect(node.citation).to eq('Author')
314
+ expect(node.text_content).to eq('Quote')
315
+ end
316
+ end
317
+
318
+ context 'with invalid input' do
319
+ it 'returns generic node for unknown types' do
320
+ data = { 'type' => 'unknown_type' }
321
+ node = described_class.parse_node(data)
322
+ expect(node).to be_a(Prosereflect::Node)
323
+ expect(node.type).to eq('node')
324
+ end
325
+
326
+ it 'handles missing content gracefully' do
327
+ data = { 'type' => 'paragraph' }
328
+ node = described_class.parse_node(data)
329
+ expect(node).to be_a(Prosereflect::Paragraph)
330
+ expect(node.content).to eq([])
331
+ end
332
+
333
+ it 'handles missing attributes gracefully' do
334
+ data = { 'type' => 'table_cell' }
335
+ node = described_class.parse_node(data)
336
+ expect(node).to be_a(Prosereflect::TableCell)
337
+ expect(node.attrs).to be_nil
338
+ end
339
+ end
127
340
  end
128
341
  end
129
342
  end