prosereflect 0.1.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 (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +25 -0
  4. data/.gitignore +12 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +228 -0
  7. data/CODE_OF_CONDUCT.md +132 -0
  8. data/Gemfile +10 -0
  9. data/README.adoc +268 -0
  10. data/Rakefile +12 -0
  11. data/debug_loading.rb +34 -0
  12. data/lib/prosereflect/document.rb +63 -0
  13. data/lib/prosereflect/hard_break.rb +22 -0
  14. data/lib/prosereflect/node.rb +77 -0
  15. data/lib/prosereflect/paragraph.rb +50 -0
  16. data/lib/prosereflect/parser.rb +56 -0
  17. data/lib/prosereflect/table.rb +64 -0
  18. data/lib/prosereflect/table_cell.rb +37 -0
  19. data/lib/prosereflect/table_row.rb +32 -0
  20. data/lib/prosereflect/text.rb +37 -0
  21. data/lib/prosereflect/version.rb +5 -0
  22. data/lib/prosereflect.rb +17 -0
  23. data/prosereflect.gemspec +33 -0
  24. data/sig/prosemirror.rbs +4 -0
  25. data/spec/fixtures/ituob-1000/ituob-1000-DP.json +366 -0
  26. data/spec/fixtures/ituob-1000/ituob-1000-DP.yaml +182 -0
  27. data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.json +381 -0
  28. data/spec/fixtures/ituob-1000/ituob-1000-E118_IIN.yaml +182 -0
  29. data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.json +203 -0
  30. data/spec/fixtures/ituob-1000/ituob-1000-E164_ACN.yaml +98 -0
  31. data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.json +202 -0
  32. data/spec/fixtures/ituob-1000/ituob-1000-E212_MNC.yaml +101 -0
  33. data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.json +6948 -0
  34. data/spec/fixtures/ituob-1000/ituob-1000-F32_TDI.yaml +3519 -0
  35. data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.json +529 -0
  36. data/spec/fixtures/ituob-1000/ituob-1000-M1400_ICC.yaml +263 -0
  37. data/spec/fixtures/ituob-1000/ituob-1000-NNP.json +288 -0
  38. data/spec/fixtures/ituob-1000/ituob-1000-NNP.yaml +152 -0
  39. data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.json +1534 -0
  40. data/spec/fixtures/ituob-1000/ituob-1000-Q708_ISPC.yaml +789 -0
  41. data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.json +252 -0
  42. data/spec/fixtures/ituob-1000/ituob-1000-Q708_SANC.yaml +123 -0
  43. data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.json +428 -0
  44. data/spec/fixtures/ituob-1000/ituob-1000-R_SP_LM.V.yaml +208 -0
  45. data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.json +621 -0
  46. data/spec/fixtures/ituob-1000/ituob-1000-T35_NA.yaml +317 -0
  47. data/spec/fixtures/ituob-1001/ituob-1001-DP.json +532 -0
  48. data/spec/fixtures/ituob-1001/ituob-1001-DP.yaml +266 -0
  49. data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.json +1093 -0
  50. data/spec/fixtures/ituob-1001/ituob-1001-E118_IIN.yaml +519 -0
  51. data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.json +449 -0
  52. data/spec/fixtures/ituob-1001/ituob-1001-E164_ACN.yaml +214 -0
  53. data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.json +271 -0
  54. data/spec/fixtures/ituob-1001/ituob-1001-E164_CC.yaml +136 -0
  55. data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.json +199 -0
  56. data/spec/fixtures/ituob-1001/ituob-1001-E212_MNC.yaml +99 -0
  57. data/spec/fixtures/ituob-1001/ituob-1001-NNP.json +288 -0
  58. data/spec/fixtures/ituob-1001/ituob-1001-NNP.yaml +152 -0
  59. data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.json +960 -0
  60. data/spec/fixtures/ituob-1001/ituob-1001-Q708_ISPC.yaml +487 -0
  61. data/spec/prosereflect/document_spec.rb +149 -0
  62. data/spec/prosereflect/hard_break_spec.rb +51 -0
  63. data/spec/prosereflect/node_spec.rb +256 -0
  64. data/spec/prosereflect/paragraph_spec.rb +152 -0
  65. data/spec/prosereflect/parser_spec.rb +129 -0
  66. data/spec/prosereflect/table_cell_spec.rb +114 -0
  67. data/spec/prosereflect/table_row_spec.rb +77 -0
  68. data/spec/prosereflect/table_spec.rb +144 -0
  69. data/spec/prosereflect/text_spec.rb +116 -0
  70. data/spec/prosereflect/version_spec.rb +11 -0
  71. data/spec/prosereflect_spec.rb +57 -0
  72. data/spec/spec_helper.rb +21 -0
  73. data/spec/support/matchers.rb +131 -0
  74. data/spec/support/shared_examples.rb +220 -0
  75. metadata +133 -0
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Prosereflect::Node do
4
+ describe 'initialization' do
5
+ it 'initializes with empty data' do
6
+ node = described_class.new
7
+ expect(node.type).to be_nil
8
+ expect(node.attrs).to be_nil
9
+ expect(node.content).to eq([])
10
+ expect(node.marks).to be_nil
11
+ end
12
+
13
+ it 'initializes with provided data' do
14
+ data = {
15
+ 'type' => 'test_node',
16
+ 'attrs' => { 'key' => 'value' },
17
+ 'marks' => [{ 'type' => 'bold' }]
18
+ }
19
+ node = described_class.new(data)
20
+ expect(node.type).to eq('test_node')
21
+ expect(node.attrs).to eq({ 'key' => 'value' })
22
+ expect(node.marks).to eq([{ 'type' => 'bold' }])
23
+ end
24
+ end
25
+
26
+ describe '#parse_content' do
27
+ it 'returns empty array for nil content' do
28
+ node = described_class.new
29
+ expect(node.parse_content(nil)).to eq([])
30
+ end
31
+
32
+ it 'parses content items using Parser' do
33
+ content_data = [
34
+ { 'type' => 'text', 'text' => 'Hello' },
35
+ { 'type' => 'hard_break' }
36
+ ]
37
+
38
+ node = described_class.new
39
+ parsed_content = node.parse_content(content_data)
40
+
41
+ expect(parsed_content.size).to eq(2)
42
+ expect(parsed_content[0]).to be_a(Prosereflect::Text)
43
+ expect(parsed_content[1]).to be_a(Prosereflect::HardBreak)
44
+ end
45
+ end
46
+
47
+ describe '#to_h' do
48
+ it 'creates a hash representation with basic properties' do
49
+ node = described_class.new({ 'type' => 'test_node' })
50
+ hash = node.to_h
51
+
52
+ expect(hash).to be_a(Hash)
53
+ expect(hash['type']).to eq('test_node')
54
+ end
55
+
56
+ it 'includes attrs when present' do
57
+ node = described_class.new({
58
+ 'type' => 'test_node',
59
+ 'attrs' => { 'key' => 'value' }
60
+ })
61
+
62
+ hash = node.to_h
63
+ expect(hash['attrs']).to eq({ 'key' => 'value' })
64
+ end
65
+
66
+ it 'includes marks when present' do
67
+ node = described_class.new({
68
+ 'type' => 'test_node',
69
+ 'marks' => [{ 'type' => 'bold' }]
70
+ })
71
+
72
+ hash = node.to_h
73
+ expect(hash['marks']).to eq([{ 'type' => 'bold' }])
74
+ end
75
+
76
+ it 'includes content when present' do
77
+ node = described_class.new({
78
+ 'type' => 'test_node',
79
+ 'content' => [{ 'type' => 'text', 'text' => 'Hello' }]
80
+ })
81
+
82
+ hash = node.to_h
83
+ expect(hash['content']).to be_an(Array)
84
+ expect(hash['content'][0]['type']).to eq('text')
85
+ end
86
+ end
87
+
88
+ describe '#add_child' do
89
+ it 'adds a child node to content' do
90
+ parent = described_class.new({ 'type' => 'parent' })
91
+ child = described_class.new({ 'type' => 'child' })
92
+
93
+ parent.add_child(child)
94
+
95
+ expect(parent.content.size).to eq(1)
96
+ expect(parent.content[0]).to eq(child)
97
+ end
98
+
99
+ it 'returns the added child' do
100
+ parent = described_class.new({ 'type' => 'parent' })
101
+ child = described_class.new({ 'type' => 'child' })
102
+
103
+ result = parent.add_child(child)
104
+
105
+ expect(result).to eq(child)
106
+ end
107
+ end
108
+
109
+ describe '#find_first' do
110
+ let(:node) do
111
+ root = described_class.new({ 'type' => 'root' })
112
+ para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
113
+ text = Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' })
114
+
115
+ para.add_child(text)
116
+ root.add_child(para)
117
+ root
118
+ end
119
+
120
+ it 'returns self if type matches' do
121
+ result = node.find_first('root')
122
+ expect(result).to eq(node)
123
+ end
124
+
125
+ it 'finds a child node by type' do
126
+ result = node.find_first('paragraph')
127
+ expect(result).to be_a(Prosereflect::Paragraph)
128
+ end
129
+
130
+ it 'finds a nested node by type' do
131
+ result = node.find_first('text')
132
+ expect(result).to be_a(Prosereflect::Text)
133
+ end
134
+
135
+ it 'returns nil if no matching node is found' do
136
+ result = node.find_first('nonexistent')
137
+ expect(result).to be_nil
138
+ end
139
+ end
140
+
141
+ describe '.create' do
142
+ it 'creates a node with the specified type' do
143
+ node = described_class.create('test_node')
144
+ expect(node.type).to eq('test_node')
145
+ end
146
+
147
+ it 'creates a node with attributes' do
148
+ attrs = { 'key' => 'value' }
149
+ node = described_class.create('test_node', attrs)
150
+
151
+ expect(node.type).to eq('test_node')
152
+ expect(node.attrs).to eq(attrs)
153
+ end
154
+
155
+ it 'initializes with empty content' do
156
+ node = described_class.create('test_node')
157
+ expect(node.content).to eq([])
158
+ end
159
+ end
160
+
161
+ describe '#find_all' do
162
+ let(:node) do
163
+ root = described_class.new({ 'type' => 'root' })
164
+
165
+ para1 = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
166
+ para1.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Text 1' }))
167
+
168
+ para2 = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
169
+ para2.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Text 2' }))
170
+
171
+ root.add_child(para1)
172
+ root.add_child(para2)
173
+ root
174
+ end
175
+
176
+ it 'finds all nodes of a specific type' do
177
+ paragraphs = node.find_all('paragraph')
178
+ expect(paragraphs.size).to eq(2)
179
+ expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
180
+ end
181
+
182
+ it 'finds all nested nodes of a specific type' do
183
+ texts = node.find_all('text')
184
+ expect(texts.size).to eq(2)
185
+ expect(texts).to all(be_a(Prosereflect::Text))
186
+ end
187
+
188
+ it 'returns empty array if no matching nodes are found' do
189
+ result = node.find_all('nonexistent')
190
+ expect(result).to eq([])
191
+ end
192
+ end
193
+
194
+ describe '#find_children' do
195
+ let(:node) do
196
+ root = described_class.new({ 'type' => 'root' })
197
+
198
+ root.add_child(Prosereflect::Paragraph.new({ 'type' => 'paragraph' }))
199
+ root.add_child(Prosereflect::Table.new({ 'type' => 'table' }))
200
+ root.add_child(Prosereflect::Paragraph.new({ 'type' => 'paragraph' }))
201
+
202
+ root
203
+ end
204
+
205
+ it 'finds direct children of a specific type' do
206
+ paragraphs = node.find_children('paragraph')
207
+ expect(paragraphs.size).to eq(2)
208
+ expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
209
+ end
210
+
211
+ it 'returns empty array if no matching children are found' do
212
+ result = node.find_children('nonexistent')
213
+ expect(result).to eq([])
214
+ end
215
+ end
216
+
217
+ describe '#text_content' do
218
+ it 'returns empty string for node without content' do
219
+ node = described_class.new({ 'type' => 'empty' })
220
+ expect(node.text_content).to eq('')
221
+ end
222
+
223
+ it 'concatenates text content from all child nodes' do
224
+ node = described_class.new({ 'type' => 'parent' })
225
+
226
+ para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
227
+ para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' }))
228
+ para.add_child(Prosereflect::HardBreak.new({ 'type' => 'hard_break' }))
229
+ para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'World' }))
230
+
231
+ node.add_child(para)
232
+
233
+ expect(node.text_content).to eq("Hello\nWorld")
234
+ end
235
+ end
236
+
237
+ describe '#text_content_with_breaks' do
238
+ it 'returns empty string for node without content' do
239
+ node = described_class.new({ 'type' => 'empty' })
240
+ expect(node.text_content_with_breaks).to eq('')
241
+ end
242
+
243
+ it 'includes newlines for hard breaks' do
244
+ node = described_class.new({ 'type' => 'parent' })
245
+
246
+ para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
247
+ para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' }))
248
+ para.add_child(Prosereflect::HardBreak.new({ 'type' => 'hard_break' }))
249
+ para.add_child(Prosereflect::Text.new({ 'type' => 'text', 'text' => 'World' }))
250
+
251
+ node.add_child(para)
252
+
253
+ expect(node.text_content_with_breaks).to eq("Hello\nWorld")
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::Paragraph do
6
+ describe 'initialization' do
7
+ it 'initializes as a paragraph node' do
8
+ paragraph = described_class.new({ 'type' => 'paragraph' })
9
+ expect(paragraph.type).to eq('paragraph')
10
+ end
11
+ end
12
+
13
+ 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
19
+ end
20
+
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)
25
+ end
26
+ end
27
+
28
+ describe '#text_nodes' do
29
+ it 'returns all text nodes in the paragraph' do
30
+ paragraph = described_class.create
31
+ paragraph.add_text('First text')
32
+ paragraph.add_text('Second text')
33
+
34
+ expect(paragraph.text_nodes.size).to eq(2)
35
+ expect(paragraph.text_nodes).to all(be_a(Prosereflect::Text))
36
+ end
37
+
38
+ it 'returns empty array for paragraph with no text nodes' do
39
+ paragraph = described_class.create
40
+ expect(paragraph.text_nodes).to eq([])
41
+ end
42
+ end
43
+
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')
49
+
50
+ expect(paragraph.text_content).to eq('First text Second text')
51
+ end
52
+
53
+ it 'returns empty string for empty paragraph' do
54
+ paragraph = described_class.create
55
+ expect(paragraph.text_content).to eq('')
56
+ end
57
+
58
+ it 'handles hard breaks' do
59
+ paragraph = described_class.create
60
+ paragraph.add_text('First line')
61
+ paragraph.add_hard_break
62
+ paragraph.add_text('Second line')
63
+
64
+ expect(paragraph.text_content).to eq("First line\nSecond line")
65
+ end
66
+
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')
81
+ end
82
+ end
83
+
84
+ describe '#add_text' do
85
+ it 'adds text to the paragraph' do
86
+ paragraph = described_class.create
87
+ text_node = paragraph.add_text('Hello world')
88
+
89
+ expect(paragraph.content.size).to eq(1)
90
+ expect(text_node).to be_a(Prosereflect::Text)
91
+ expect(text_node.text).to eq('Hello world')
92
+ end
93
+
94
+ it 'adds text with marks' do
95
+ paragraph = described_class.create
96
+ marks = [{ 'type' => 'bold' }]
97
+ text_node = paragraph.add_text('Bold text', marks)
98
+
99
+ expect(text_node.marks).to eq(marks)
100
+ end
101
+
102
+ it 'does not add empty text' do
103
+ paragraph = described_class.create
104
+ result = paragraph.add_text('')
105
+
106
+ expect(paragraph.content).to be_empty
107
+ expect(result).to be_nil
108
+ end
109
+
110
+ it 'does not add nil text' do
111
+ paragraph = described_class.create
112
+ result = paragraph.add_text(nil)
113
+
114
+ expect(paragraph.content).to be_empty
115
+ expect(result).to be_nil
116
+ end
117
+ end
118
+
119
+ describe '#add_hard_break' do
120
+ it 'adds a hard break to the paragraph' do
121
+ paragraph = described_class.create
122
+ hard_break = paragraph.add_hard_break
123
+
124
+ expect(paragraph.content.size).to eq(1)
125
+ expect(hard_break).to be_a(Prosereflect::HardBreak)
126
+ end
127
+
128
+ it 'adds a hard break with marks' do
129
+ paragraph = described_class.create
130
+ marks = [{ 'type' => 'italic' }]
131
+ hard_break = paragraph.add_hard_break(marks)
132
+
133
+ expect(hard_break.marks).to eq(marks)
134
+ end
135
+ end
136
+
137
+ describe 'serialization' do
138
+ it 'converts to hash representation' do
139
+ paragraph = described_class.create
140
+ paragraph.add_text('Hello')
141
+ paragraph.add_hard_break
142
+ paragraph.add_text('World')
143
+
144
+ hash = paragraph.to_h
145
+ expect(hash['type']).to eq('paragraph')
146
+ expect(hash['content'].size).to eq(3)
147
+ expect(hash['content'][0]['type']).to eq('text')
148
+ expect(hash['content'][1]['type']).to eq('hard_break')
149
+ expect(hash['content'][2]['type']).to eq('text')
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::Parser do
6
+ let(:fixtures_path) { File.join(__dir__, '..', 'fixtures') }
7
+
8
+ describe '.parse_document' do
9
+ context 'with YAML fixtures' do
10
+ let(:yaml_files) { Dir.glob(File.join(fixtures_path, '*/*.yaml')) }
11
+
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)
16
+
17
+ expect do
18
+ document = described_class.parse_document(data)
19
+ expect(document).to be_a(Prosereflect::Document)
20
+ end.not_to raise_error
21
+ end
22
+ end
23
+ end
24
+
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
34
+ document = described_class.parse_document(data)
35
+ expect(document).to be_a(Prosereflect::Document)
36
+ end.not_to raise_error
37
+ end
38
+ end
39
+ end
40
+
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
45
+
46
+ it 'raises an error for non-hash input' do
47
+ expect { described_class.parse_document('not a hash') }.to raise_error(ArgumentError)
48
+ end
49
+ end
50
+ end
51
+
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)
57
+
58
+ # Test paragraph node
59
+ para_data = { 'type' => 'paragraph', 'content' => [] }
60
+ expect(described_class.parse_node(para_data)).to be_a(Prosereflect::Paragraph)
61
+
62
+ # Test text node
63
+ text_data = { 'type' => 'text', 'text' => 'Hello' }
64
+ expect(described_class.parse_node(text_data)).to be_a(Prosereflect::Text)
65
+
66
+ # Test hard_break node
67
+ break_data = { 'type' => 'hard_break' }
68
+ expect(described_class.parse_node(break_data)).to be_a(Prosereflect::HardBreak)
69
+
70
+ # Test table node
71
+ table_data = { 'type' => 'table', 'content' => [] }
72
+ expect(described_class.parse_node(table_data)).to be_a(Prosereflect::Table)
73
+
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)
77
+
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)
81
+
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
86
+
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
104
+
105
+ it 'handles nodes with marks' do
106
+ data = {
107
+ 'type' => 'text',
108
+ 'text' => 'Bold text',
109
+ 'marks' => [{ 'type' => 'bold' }]
110
+ }
111
+
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
116
+
117
+ it 'handles nodes with attributes' do
118
+ data = {
119
+ 'type' => 'table_cell',
120
+ 'attrs' => { 'colspan' => 2, 'rowspan' => 1 },
121
+ 'content' => []
122
+ }
123
+
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 })
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Prosereflect::TableCell do
6
+ describe 'initialization' do
7
+ it 'initializes as a table_cell node' do
8
+ cell = described_class.new({ 'type' => 'table_cell' })
9
+ expect(cell.type).to eq('table_cell')
10
+ end
11
+ end
12
+
13
+ describe '.create' do
14
+ it 'creates an empty table cell' do
15
+ cell = described_class.create
16
+ expect(cell).to be_a(described_class)
17
+ expect(cell.type).to eq('table_cell')
18
+ expect(cell.content).to be_empty
19
+ end
20
+
21
+ it 'creates a table cell with attributes' do
22
+ attrs = { 'colspan' => 2, 'rowspan' => 1 }
23
+ cell = described_class.create(attrs)
24
+ expect(cell.attrs).to eq(attrs)
25
+ end
26
+ end
27
+
28
+ describe '#paragraphs' do
29
+ it 'returns all paragraphs in the cell' do
30
+ cell = described_class.create
31
+ cell.add_paragraph('Para 1')
32
+ cell.add_paragraph('Para 2')
33
+
34
+ expect(cell.paragraphs.size).to eq(2)
35
+ expect(cell.paragraphs).to all(be_a(Prosereflect::Paragraph))
36
+ end
37
+
38
+ it 'returns empty array for cell with no paragraphs' do
39
+ cell = described_class.create
40
+ expect(cell.paragraphs).to eq([])
41
+ end
42
+ end
43
+
44
+ describe '#text_content' do
45
+ it 'returns concatenated text from all paragraphs with newlines' do
46
+ cell = described_class.create
47
+ cell.add_paragraph('First paragraph')
48
+ cell.add_paragraph('Second paragraph')
49
+
50
+ expect(cell.text_content).to eq("First paragraph\nSecond paragraph")
51
+ end
52
+
53
+ it 'returns empty string for empty cell' do
54
+ cell = described_class.create
55
+ expect(cell.text_content).to eq('')
56
+ end
57
+ end
58
+
59
+ describe '#lines' do
60
+ it 'splits text content into lines' do
61
+ cell = described_class.create
62
+ cell.add_paragraph('Line 1')
63
+ cell.add_paragraph('Line 2')
64
+
65
+ expect(cell.lines).to eq(['Line 1', 'Line 2'])
66
+ end
67
+
68
+ it 'returns empty array for empty cell' do
69
+ cell = described_class.create
70
+ expect(cell.lines).to eq([])
71
+ end
72
+
73
+ it 'handles multi-line paragraphs' do
74
+ cell = described_class.create
75
+ para = cell.add_paragraph('First line')
76
+ para.add_hard_break
77
+ para.add_text('Second line')
78
+
79
+ expect(cell.lines).to eq(['First line', 'Second line'])
80
+ end
81
+ end
82
+
83
+ describe '#add_paragraph' do
84
+ it 'adds a paragraph with text' do
85
+ cell = described_class.create
86
+ paragraph = cell.add_paragraph('Test content')
87
+
88
+ expect(cell.paragraphs.size).to eq(1)
89
+ expect(paragraph).to be_a(Prosereflect::Paragraph)
90
+ expect(paragraph.text_content).to eq('Test content')
91
+ end
92
+
93
+ it 'adds an empty paragraph' do
94
+ cell = described_class.create
95
+ paragraph = cell.add_paragraph
96
+
97
+ expect(cell.paragraphs.size).to eq(1)
98
+ expect(paragraph).to be_a(Prosereflect::Paragraph)
99
+ expect(paragraph.text_content).to eq('')
100
+ end
101
+ end
102
+
103
+ describe 'serialization' do
104
+ it 'converts to hash representation' do
105
+ cell = described_class.create
106
+ cell.add_paragraph('Test content')
107
+
108
+ hash = cell.to_h
109
+ expect(hash['type']).to eq('table_cell')
110
+ expect(hash['content'].size).to eq(1)
111
+ expect(hash['content'][0]['type']).to eq('paragraph')
112
+ end
113
+ end
114
+ end