prosereflect 0.1.1 → 0.3.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +97 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.gitignore +4 -0
  7. data/.rubocop.yml +19 -1
  8. data/.rubocop_todo.yml +119 -183
  9. data/CLAUDE.md +78 -0
  10. data/Gemfile +8 -4
  11. data/README.adoc +2 -0
  12. data/Rakefile +3 -3
  13. data/docs/Gemfile +10 -0
  14. data/docs/INDEX.adoc +45 -0
  15. data/docs/_advanced/index.adoc +15 -0
  16. data/docs/_advanced/schema.adoc +112 -0
  17. data/docs/_advanced/step-map.adoc +66 -0
  18. data/docs/_advanced/steps.adoc +88 -0
  19. data/docs/_advanced/test-builder.adoc +61 -0
  20. data/docs/_advanced/transform.adoc +92 -0
  21. data/docs/_config.yml +174 -0
  22. data/docs/_features/html-input.adoc +69 -0
  23. data/docs/_features/html-output.adoc +45 -0
  24. data/docs/_features/index.adoc +15 -0
  25. data/docs/_features/marks.adoc +86 -0
  26. data/docs/_features/node-types.adoc +124 -0
  27. data/docs/_features/user-mentions.adoc +47 -0
  28. data/docs/_guides/custom-nodes.adoc +107 -0
  29. data/docs/_guides/index.adoc +13 -0
  30. data/docs/_guides/round-trip-html.adoc +91 -0
  31. data/docs/_guides/serialization.adoc +109 -0
  32. data/docs/_pages/index.adoc +67 -0
  33. data/docs/_reference/document-api.adoc +49 -0
  34. data/docs/_reference/index.adoc +14 -0
  35. data/docs/_reference/node-api.adoc +79 -0
  36. data/docs/_reference/schema-api.adoc +95 -0
  37. data/docs/_reference/transform-api.adoc +77 -0
  38. data/docs/_understanding/document-model.adoc +65 -0
  39. data/docs/_understanding/fragment.adoc +52 -0
  40. data/docs/_understanding/index.adoc +14 -0
  41. data/docs/_understanding/resolved-position.adoc +53 -0
  42. data/docs/_understanding/slice.adoc +54 -0
  43. data/docs/lychee.toml +63 -0
  44. data/lib/prosereflect/attribute/base.rb +4 -6
  45. data/lib/prosereflect/attribute/bold.rb +2 -4
  46. data/lib/prosereflect/attribute/href.rb +1 -3
  47. data/lib/prosereflect/attribute/id.rb +7 -7
  48. data/lib/prosereflect/attribute.rb +4 -7
  49. data/lib/prosereflect/blockquote.rb +19 -11
  50. data/lib/prosereflect/bullet_list.rb +36 -29
  51. data/lib/prosereflect/code_block.rb +23 -27
  52. data/lib/prosereflect/code_block_wrapper.rb +12 -13
  53. data/lib/prosereflect/document.rb +14 -22
  54. data/lib/prosereflect/fragment.rb +249 -0
  55. data/lib/prosereflect/hard_break.rb +6 -6
  56. data/lib/prosereflect/heading.rb +14 -15
  57. data/lib/prosereflect/horizontal_rule.rb +23 -14
  58. data/lib/prosereflect/image.rb +32 -23
  59. data/lib/prosereflect/input/html.rb +179 -104
  60. data/lib/prosereflect/input.rb +7 -0
  61. data/lib/prosereflect/list_item.rb +11 -12
  62. data/lib/prosereflect/mark/base.rb +9 -11
  63. data/lib/prosereflect/mark/bold.rb +1 -3
  64. data/lib/prosereflect/mark/code.rb +1 -3
  65. data/lib/prosereflect/mark/italic.rb +1 -3
  66. data/lib/prosereflect/mark/link.rb +1 -3
  67. data/lib/prosereflect/mark/strike.rb +1 -3
  68. data/lib/prosereflect/mark/subscript.rb +1 -3
  69. data/lib/prosereflect/mark/superscript.rb +1 -3
  70. data/lib/prosereflect/mark/underline.rb +1 -3
  71. data/lib/prosereflect/mark.rb +9 -5
  72. data/lib/prosereflect/node.rb +171 -33
  73. data/lib/prosereflect/ordered_list.rb +17 -14
  74. data/lib/prosereflect/output/html.rb +279 -50
  75. data/lib/prosereflect/output.rb +7 -0
  76. data/lib/prosereflect/paragraph.rb +11 -13
  77. data/lib/prosereflect/parser.rb +56 -66
  78. data/lib/prosereflect/resolved_pos.rb +256 -0
  79. data/lib/prosereflect/schema/attribute.rb +57 -0
  80. data/lib/prosereflect/schema/content_match.rb +656 -0
  81. data/lib/prosereflect/schema/fragment.rb +166 -0
  82. data/lib/prosereflect/schema/mark.rb +121 -0
  83. data/lib/prosereflect/schema/mark_type.rb +130 -0
  84. data/lib/prosereflect/schema/node.rb +236 -0
  85. data/lib/prosereflect/schema/node_type.rb +274 -0
  86. data/lib/prosereflect/schema/schema_main.rb +190 -0
  87. data/lib/prosereflect/schema/spec.rb +92 -0
  88. data/lib/prosereflect/schema.rb +39 -0
  89. data/lib/prosereflect/table.rb +12 -13
  90. data/lib/prosereflect/table_cell.rb +13 -13
  91. data/lib/prosereflect/table_header.rb +17 -17
  92. data/lib/prosereflect/table_row.rb +12 -12
  93. data/lib/prosereflect/text.rb +35 -11
  94. data/lib/prosereflect/transform/attr_step.rb +157 -0
  95. data/lib/prosereflect/transform/insert_step.rb +115 -0
  96. data/lib/prosereflect/transform/mapping.rb +82 -0
  97. data/lib/prosereflect/transform/mark_step.rb +269 -0
  98. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  99. data/lib/prosereflect/transform/replace_step.rb +157 -0
  100. data/lib/prosereflect/transform/slice.rb +91 -0
  101. data/lib/prosereflect/transform/step.rb +89 -0
  102. data/lib/prosereflect/transform/step_map.rb +126 -0
  103. data/lib/prosereflect/transform/structure.rb +120 -0
  104. data/lib/prosereflect/transform/transform.rb +341 -0
  105. data/lib/prosereflect/transform.rb +26 -0
  106. data/lib/prosereflect/user.rb +15 -15
  107. data/lib/prosereflect/version.rb +1 -1
  108. data/lib/prosereflect.rb +30 -17
  109. data/prosereflect.gemspec +17 -16
  110. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  111. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  112. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  113. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  114. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  115. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  116. data/spec/fixtures/documents/table_doc.yaml +32 -0
  117. data/spec/fixtures/documents/transform_test.yaml +14 -0
  118. data/spec/fixtures/schema/custom_schema.rb +37 -0
  119. data/spec/fixtures/schema/test_schema.rb +46 -0
  120. data/spec/fixtures/test_builder/helpers.rb +212 -0
  121. data/spec/prosereflect/document_spec.rb +332 -330
  122. data/spec/prosereflect/fragment_spec.rb +273 -0
  123. data/spec/prosereflect/hard_break_spec.rb +125 -125
  124. data/spec/prosereflect/input/html_spec.rb +718 -522
  125. data/spec/prosereflect/node_spec.rb +311 -182
  126. data/spec/prosereflect/output/html_spec.rb +105 -105
  127. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  128. data/spec/prosereflect/paragraph_spec.rb +275 -274
  129. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  130. data/spec/prosereflect/parser_spec.rb +185 -180
  131. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  132. data/spec/prosereflect/schema/conftest.rb +68 -0
  133. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  134. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  135. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  136. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  137. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  138. data/spec/prosereflect/table_cell_spec.rb +183 -183
  139. data/spec/prosereflect/table_row_spec.rb +149 -149
  140. data/spec/prosereflect/table_spec.rb +320 -318
  141. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  142. data/spec/prosereflect/text_spec.rb +133 -132
  143. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  144. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  145. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  146. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  147. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  148. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  149. data/spec/prosereflect/transform/step_spec.rb +211 -0
  150. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  151. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  152. data/spec/prosereflect/user_spec.rb +31 -28
  153. data/spec/prosereflect_spec.rb +28 -26
  154. data/spec/spec_helper.rb +7 -6
  155. data/spec/support/matchers.rb +6 -6
  156. data/spec/support/shared_examples.rb +49 -49
  157. metadata +96 -5
  158. data/spec/prosereflect/version_spec.rb +0 -11
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require "spec_helper"
4
4
 
5
5
  RSpec.describe Prosereflect::Node do
6
- describe 'initialization' do
7
- it 'initializes with empty data' do
6
+ describe "initialization" do
7
+ it "initializes with empty data" do
8
8
  node = described_class.new
9
9
  expect(node.type).to be_nil
10
10
  expect(node.attrs).to be_nil
@@ -13,29 +13,29 @@ RSpec.describe Prosereflect::Node do
13
13
  end
14
14
 
15
15
  # TODO: Update to lutaml-model
16
- it 'initializes with provided data' do
16
+ it "initializes with provided data" do
17
17
  data = {
18
- 'type' => 'test_node',
19
- 'attrs' => { 'key' => 'value' },
20
- 'marks' => [{ 'type' => 'bold' }]
18
+ "type" => "test_node",
19
+ "attrs" => { "key" => "value" },
20
+ "marks" => [{ "type" => "bold" }],
21
21
  }
22
22
  node = described_class.new(data)
23
- expect(node.type).to eq('test_node')
24
- expect(node.attrs).to eq({ 'key' => 'value' })
25
- expect(node.marks).to eq([{ 'type' => 'bold' }])
23
+ expect(node.type).to eq("test_node")
24
+ expect(node.attrs).to eq({ "key" => "value" })
25
+ expect(node.marks).to eq([{ "type" => "bold" }])
26
26
  end
27
27
  end
28
28
 
29
- describe '#parse_content' do
30
- it 'returns empty array for nil content' do
29
+ describe "#parse_content" do
30
+ it "returns empty array for nil content" do
31
31
  node = described_class.new
32
32
  expect(node.parse_content(nil)).to eq([])
33
33
  end
34
34
 
35
- it 'parses content items using Parser' do
35
+ it "parses content items using Parser" do
36
36
  content_data = [
37
- { 'type' => 'text', 'text' => 'Hello' },
38
- { 'type' => 'hard_break' }
37
+ { "type" => "text", "text" => "Hello" },
38
+ { "type" => "hard_break" },
39
39
  ]
40
40
 
41
41
  node = described_class.new
@@ -47,51 +47,52 @@ RSpec.describe Prosereflect::Node do
47
47
  end
48
48
  end
49
49
 
50
- describe '#to_h' do
51
- it 'creates a hash representation with basic properties' do
52
- node = described_class.new({ 'type' => 'test_node' })
50
+ describe "#to_h" do
51
+ it "creates a hash representation with basic properties" do
52
+ node = described_class.new({ "type" => "test_node" })
53
53
  hash = node.to_hash
54
54
 
55
55
  expect(hash).to be_a(Hash)
56
- expect(hash['type']).to eq('test_node')
56
+ expect(hash["type"]).to eq("test_node")
57
57
  end
58
58
 
59
- it 'includes attrs when present' do
59
+ it "includes attrs when present" do
60
60
  node = described_class.new(
61
- type: Prosereflect::Text.new(text: 'Hello'),
62
- attrs: [Prosereflect::Attribute::Href.new('https://example.com')]
61
+ type: Prosereflect::Text.new(text: "Hello"),
62
+ attrs: [Prosereflect::Attribute::Href.new("https://example.com")],
63
63
  )
64
64
 
65
65
  hash = node.to_hash
66
- expect(hash['attrs']).to eq([{ 'href' => 'https://example.com' }])
66
+ expect(hash["attrs"]).to eq([{ "href" => "https://example.com" }])
67
67
  end
68
68
 
69
- it 'includes marks when present' do
69
+ it "includes marks when present" do
70
70
  node = described_class.new(
71
- type: Prosereflect::Text.new(text: 'Hello'),
72
- marks: [Prosereflect::Mark::Bold.new]
71
+ type: Prosereflect::Text.new(text: "Hello"),
72
+ marks: [Prosereflect::Mark::Bold.new],
73
73
  )
74
74
 
75
75
  hash = node.to_hash
76
- expect(hash['marks']).to eq([{ 'type' => 'bold' }])
76
+ expect(hash["marks"]).to eq([{ "type" => "bold" }])
77
77
  end
78
78
 
79
- it 'includes content when present' do
79
+ it "includes content when present" do
80
80
  node = described_class.new({
81
- 'type' => 'test_node',
82
- 'content' => [{ 'type' => 'text', 'text' => 'Hello' }]
81
+ "type" => "test_node",
82
+ "content" => [{ "type" => "text",
83
+ "text" => "Hello" }],
83
84
  })
84
85
 
85
86
  hash = node.to_hash
86
- expect(hash['content']).to be_an(Array)
87
- expect(hash['content'][0]['type']).to eq('text')
87
+ expect(hash["content"]).to be_an(Array)
88
+ expect(hash["content"][0]["type"]).to eq("text")
88
89
  end
89
90
  end
90
91
 
91
- describe '#add_child' do
92
- it 'adds a child node to content' do
93
- parent = described_class.new({ 'type' => 'parent' })
94
- child = described_class.new({ 'type' => 'child' })
92
+ describe "#add_child" do
93
+ it "adds a child node to content" do
94
+ parent = described_class.new({ "type" => "parent" })
95
+ child = described_class.new({ "type" => "child" })
95
96
 
96
97
  parent.add_child(child)
97
98
 
@@ -99,9 +100,9 @@ RSpec.describe Prosereflect::Node do
99
100
  expect(parent.content[0]).to eq(child)
100
101
  end
101
102
 
102
- it 'returns the added child' do
103
- parent = described_class.new({ 'type' => 'parent' })
104
- child = described_class.new({ 'type' => 'child' })
103
+ it "returns the added child" do
104
+ parent = described_class.new({ "type" => "parent" })
105
+ child = described_class.new({ "type" => "child" })
105
106
 
106
107
  result = parent.add_child(child)
107
108
 
@@ -109,239 +110,239 @@ RSpec.describe Prosereflect::Node do
109
110
  end
110
111
  end
111
112
 
112
- describe '#find_first' do
113
+ describe "#find_first" do
113
114
  let(:node) do
114
- root = described_class.new({ 'type' => 'root' })
115
- para = Prosereflect::Paragraph.new({ 'type' => 'paragraph' })
116
- text = Prosereflect::Text.new({ 'type' => 'text', 'text' => 'Hello' })
115
+ root = described_class.new({ "type" => "root" })
116
+ para = Prosereflect::Paragraph.new({ "type" => "paragraph" })
117
+ text = Prosereflect::Text.new({ "type" => "text", "text" => "Hello" })
117
118
 
118
119
  para.add_child(text)
119
120
  root.add_child(para)
120
121
  root
121
122
  end
122
123
 
123
- it 'returns self if type matches' do
124
- result = node.find_first('root')
124
+ it "returns self if type matches" do
125
+ result = node.find_first("root")
125
126
  expect(result).to eq(node)
126
127
  end
127
128
 
128
- it 'finds a child node by type' do
129
- result = node.find_first('paragraph')
129
+ it "finds a child node by type" do
130
+ result = node.find_first("paragraph")
130
131
  expect(result).to be_a(Prosereflect::Paragraph)
131
132
  end
132
133
 
133
- it 'finds a nested node by type' do
134
- result = node.find_first('text')
134
+ it "finds a nested node by type" do
135
+ result = node.find_first("text")
135
136
  expect(result).to be_a(Prosereflect::Text)
136
137
  end
137
138
 
138
- it 'returns nil if no matching node is found' do
139
- result = node.find_first('nonexistent')
139
+ it "returns nil if no matching node is found" do
140
+ result = node.find_first("nonexistent")
140
141
  expect(result).to be_nil
141
142
  end
142
143
  end
143
144
 
144
- describe '.create' do
145
- it 'creates a simple node' do
146
- node = described_class.create('test_node')
145
+ describe ".create" do
146
+ it "creates a simple node" do
147
+ node = described_class.create("test_node")
147
148
 
148
149
  expected = {
149
- 'type' => 'test_node'
150
+ "type" => "test_node",
150
151
  }
151
152
 
152
153
  expect(node.to_h).to eq(expected)
153
154
  end
154
155
 
155
- it 'creates a node with attributes' do
156
- node = described_class.create('test_node', {
157
- 'key' => 'value',
158
- 'number' => 42,
159
- 'flag' => true
156
+ it "creates a node with attributes" do
157
+ node = described_class.create("test_node", {
158
+ "key" => "value",
159
+ "number" => 42,
160
+ "flag" => true,
160
161
  })
161
162
 
162
163
  expected = {
163
- 'type' => 'test_node',
164
- 'attrs' => {
165
- 'key' => 'value',
166
- 'number' => 42,
167
- 'flag' => true
168
- }
164
+ "type" => "test_node",
165
+ "attrs" => {
166
+ "key" => "value",
167
+ "number" => 42,
168
+ "flag" => true,
169
+ },
169
170
  }
170
171
 
171
172
  expect(node.to_h).to eq(expected)
172
173
  end
173
174
  end
174
175
 
175
- describe 'node structure' do
176
- it 'creates a node with content' do
177
- node = described_class.create('parent')
178
- node.add_child(Prosereflect::Text.create('First child'))
179
- node.add_child(Prosereflect::Text.create('Second child'))
176
+ describe "node structure" do
177
+ it "creates a node with content" do
178
+ node = described_class.create("parent")
179
+ node.add_child(Prosereflect::Text.create("First child"))
180
+ node.add_child(Prosereflect::Text.create("Second child"))
180
181
 
181
182
  expected = {
182
- 'type' => 'parent',
183
- 'content' => [
183
+ "type" => "parent",
184
+ "content" => [
184
185
  {
185
- 'type' => 'text',
186
- 'text' => 'First child'
186
+ "type" => "text",
187
+ "text" => "First child",
187
188
  },
188
189
  {
189
- 'type' => 'text',
190
- 'text' => 'Second child'
191
- }
192
- ]
190
+ "type" => "text",
191
+ "text" => "Second child",
192
+ },
193
+ ],
193
194
  }
194
195
 
195
196
  expect(node.to_h).to eq(expected)
196
197
  end
197
198
 
198
- it 'creates a node with complex content' do
199
- node = described_class.create('root')
199
+ it "creates a node with complex content" do
200
+ node = described_class.create("root")
200
201
 
201
202
  # Add a paragraph with formatted text
202
203
  para = Prosereflect::Paragraph.create
203
- para.add_child(Prosereflect::Text.create('Bold', [Prosereflect::Mark::Bold.create]))
204
- para.add_child(Prosereflect::Text.create(' and '))
205
- para.add_child(Prosereflect::Text.create('italic', [Prosereflect::Mark::Italic.create]))
204
+ para.add_child(Prosereflect::Text.create("Bold", [Prosereflect::Mark::Bold.create]))
205
+ para.add_child(Prosereflect::Text.create(" and "))
206
+ para.add_child(Prosereflect::Text.create("italic", [Prosereflect::Mark::Italic.create]))
206
207
  node.add_child(para)
207
208
 
208
209
  # Add a list
209
210
  list = Prosereflect::BulletList.create
210
211
  list_item = Prosereflect::ListItem.create
211
212
  list_item.add_child(Prosereflect::Paragraph.create)
212
- list_item.content.first.add_child(Prosereflect::Text.create('List item'))
213
+ list_item.content.first.add_child(Prosereflect::Text.create("List item"))
213
214
  list.add_child(list_item)
214
215
  node.add_child(list)
215
216
 
216
217
  expected = {
217
- 'type' => 'root',
218
- 'content' => [
218
+ "type" => "root",
219
+ "content" => [
219
220
  {
220
- 'type' => 'paragraph',
221
- 'content' => [
221
+ "type" => "paragraph",
222
+ "content" => [
222
223
  {
223
- 'type' => 'text',
224
- 'text' => 'Bold',
225
- 'marks' => [{ 'type' => 'bold' }]
224
+ "type" => "text",
225
+ "text" => "Bold",
226
+ "marks" => [{ "type" => "bold" }],
226
227
  },
227
228
  {
228
- 'type' => 'text',
229
- 'text' => ' and '
229
+ "type" => "text",
230
+ "text" => " and ",
230
231
  },
231
232
  {
232
- 'type' => 'text',
233
- 'text' => 'italic',
234
- 'marks' => [{ 'type' => 'italic' }]
235
- }
236
- ]
233
+ "type" => "text",
234
+ "text" => "italic",
235
+ "marks" => [{ "type" => "italic" }],
236
+ },
237
+ ],
237
238
  },
238
239
  {
239
- 'type' => 'bullet_list',
240
- 'attrs' => {
241
- 'bullet_style' => nil
240
+ "type" => "bullet_list",
241
+ "attrs" => {
242
+ "bullet_style" => nil,
242
243
  },
243
- 'content' => [
244
+ "content" => [
244
245
  {
245
- 'type' => 'list_item',
246
- 'content' => [
246
+ "type" => "list_item",
247
+ "content" => [
247
248
  {
248
- 'type' => 'paragraph',
249
- 'content' => [
249
+ "type" => "paragraph",
250
+ "content" => [
250
251
  {
251
- 'type' => 'text',
252
- 'text' => 'List item'
253
- }
254
- ]
255
- }
256
- ]
257
- }
258
- ]
259
- }
260
- ]
252
+ "type" => "text",
253
+ "text" => "List item",
254
+ },
255
+ ],
256
+ },
257
+ ],
258
+ },
259
+ ],
260
+ },
261
+ ],
261
262
  }
262
263
 
263
264
  expect(node.to_h).to eq(expected)
264
265
  end
265
266
  end
266
267
 
267
- describe 'node operations' do
268
- describe '#add_child' do
269
- it 'adds a child node and returns it' do
270
- parent = described_class.create('parent')
271
- child = Prosereflect::Text.create('Child node')
268
+ describe "node operations" do
269
+ describe "#add_child" do
270
+ it "adds a child node and returns it" do
271
+ parent = described_class.create("parent")
272
+ child = Prosereflect::Text.create("Child node")
272
273
 
273
274
  result = parent.add_child(child)
274
275
  expect(result).to eq(child)
275
276
  expect(parent.content).to eq([child])
276
277
  end
277
278
 
278
- it 'maintains child order' do
279
- parent = described_class.create('parent')
280
- first = Prosereflect::Text.create('First')
281
- second = Prosereflect::Text.create('Second')
282
- third = Prosereflect::Text.create('Third')
279
+ it "maintains child order" do
280
+ parent = described_class.create("parent")
281
+ first = Prosereflect::Text.create("First")
282
+ second = Prosereflect::Text.create("Second")
283
+ third = Prosereflect::Text.create("Third")
283
284
 
284
285
  parent.add_child(first)
285
286
  parent.add_child(second)
286
287
  parent.add_child(third)
287
288
 
288
289
  expect(parent.content).to eq([first, second, third])
289
- expect(parent.text_content).to eq('FirstSecondThird')
290
+ expect(parent.text_content).to eq("FirstSecondThird")
290
291
  end
291
292
  end
292
293
 
293
- describe '#find_first' do
294
+ describe "#find_first" do
294
295
  let(:node) do
295
- root = described_class.create('root')
296
+ root = described_class.create("root")
296
297
  para = Prosereflect::Paragraph.create
297
- text = Prosereflect::Text.create('Hello')
298
+ text = Prosereflect::Text.create("Hello")
298
299
  para.add_child(text)
299
300
  root.add_child(para)
300
301
  root
301
302
  end
302
303
 
303
- it 'finds nodes by type' do
304
- expect(node.find_first('root')).to eq(node)
305
- expect(node.find_first('paragraph')).to be_a(Prosereflect::Paragraph)
306
- expect(node.find_first('text')).to be_a(Prosereflect::Text)
307
- expect(node.find_first('nonexistent')).to be_nil
304
+ it "finds nodes by type" do
305
+ expect(node.find_first("root")).to eq(node)
306
+ expect(node.find_first("paragraph")).to be_a(Prosereflect::Paragraph)
307
+ expect(node.find_first("text")).to be_a(Prosereflect::Text)
308
+ expect(node.find_first("nonexistent")).to be_nil
308
309
  end
309
310
  end
310
311
 
311
- describe '#find_all' do
312
+ describe "#find_all" do
312
313
  let(:node) do
313
- root = described_class.create('root')
314
+ root = described_class.create("root")
314
315
 
315
316
  # First paragraph
316
317
  para1 = Prosereflect::Paragraph.create
317
- para1.add_child(Prosereflect::Text.create('First'))
318
+ para1.add_child(Prosereflect::Text.create("First"))
318
319
  root.add_child(para1)
319
320
 
320
321
  # Second paragraph
321
322
  para2 = Prosereflect::Paragraph.create
322
- para2.add_child(Prosereflect::Text.create('Second'))
323
+ para2.add_child(Prosereflect::Text.create("Second"))
323
324
  root.add_child(para2)
324
325
 
325
326
  root
326
327
  end
327
328
 
328
- it 'finds all nodes of a type' do
329
- expect(node.find_all('paragraph').size).to eq(2)
330
- expect(node.find_all('text').size).to eq(2)
331
- expect(node.find_all('nonexistent')).to eq([])
329
+ it "finds all nodes of a type" do
330
+ expect(node.find_all("paragraph").size).to eq(2)
331
+ expect(node.find_all("text").size).to eq(2)
332
+ expect(node.find_all("nonexistent")).to eq([])
332
333
  end
333
334
  end
334
335
 
335
- describe '#find_children' do
336
+ describe "#find_children" do
336
337
  let(:node) do
337
- root = described_class.create('root')
338
+ root = described_class.create("root")
338
339
  root.add_child(Prosereflect::Paragraph.create)
339
340
  root.add_child(Prosereflect::Table.create)
340
341
  root.add_child(Prosereflect::Paragraph.create)
341
342
  root
342
343
  end
343
344
 
344
- it 'finds direct children by class' do
345
+ it "finds direct children by class" do
345
346
  paragraphs = node.find_children(Prosereflect::Paragraph)
346
347
  expect(paragraphs.size).to eq(2)
347
348
  expect(paragraphs).to all(be_a(Prosereflect::Paragraph))
@@ -352,74 +353,202 @@ RSpec.describe Prosereflect::Node do
352
353
  end
353
354
  end
354
355
 
355
- describe '#text_content' do
356
- it 'concatenates text from all children' do
357
- root = described_class.create('root')
356
+ describe "#text_content" do
357
+ it "concatenates text from all children" do
358
+ root = described_class.create("root")
358
359
 
359
360
  para = Prosereflect::Paragraph.create
360
- para.add_child(Prosereflect::Text.create('Hello'))
361
+ para.add_child(Prosereflect::Text.create("Hello"))
361
362
  para.add_child(Prosereflect::HardBreak.create)
362
- para.add_child(Prosereflect::Text.create('World'))
363
+ para.add_child(Prosereflect::Text.create("World"))
363
364
  root.add_child(para)
364
365
 
365
366
  expect(root.text_content).to eq("Hello\nWorld")
366
367
  end
367
368
 
368
- it 'returns empty string for empty node' do
369
- node = described_class.create('empty')
370
- expect(node.text_content).to eq('')
369
+ it "returns empty string for empty node" do
370
+ node = described_class.create("empty")
371
+ expect(node.text_content).to eq("")
371
372
  end
372
373
  end
373
374
  end
374
375
 
375
- describe 'serialization' do
376
- it 'serializes a node with all properties' do
377
- node = described_class.create('test_node', {
378
- 'key' => 'value',
379
- 'number' => 42
376
+ describe "serialization" do
377
+ it "serializes a node with all properties" do
378
+ node = described_class.create("test_node", {
379
+ "key" => "value",
380
+ "number" => 42,
380
381
  })
381
382
 
382
- text = Prosereflect::Text.create('Content', [
383
+ text = Prosereflect::Text.create("Content", [
383
384
  Prosereflect::Mark::Bold.create,
384
- Prosereflect::Mark::Link.create({ 'href' => 'https://example.com' })
385
+ Prosereflect::Mark::Link.create({ "href" => "https://example.com" }),
385
386
  ])
386
387
 
387
388
  node.add_child(text)
388
389
 
389
390
  expected = {
390
- 'type' => 'test_node',
391
- 'attrs' => {
392
- 'key' => 'value',
393
- 'number' => 42
391
+ "type" => "test_node",
392
+ "attrs" => {
393
+ "key" => "value",
394
+ "number" => 42,
394
395
  },
395
- 'content' => [
396
+ "content" => [
396
397
  {
397
- 'type' => 'text',
398
- 'text' => 'Content',
399
- 'marks' => [
400
- { 'type' => 'bold' },
398
+ "type" => "text",
399
+ "text" => "Content",
400
+ "marks" => [
401
+ { "type" => "bold" },
401
402
  {
402
- 'type' => 'link',
403
- 'attrs' => {
404
- 'href' => 'https://example.com'
405
- }
406
- }
407
- ]
408
- }
409
- ]
403
+ "type" => "link",
404
+ "attrs" => {
405
+ "href" => "https://example.com",
406
+ },
407
+ },
408
+ ],
409
+ },
410
+ ],
410
411
  }
411
412
 
412
413
  expect(node.to_h).to eq(expected)
413
414
  end
414
415
 
415
- it 'omits optional properties when empty' do
416
- node = described_class.create('test_node')
416
+ it "omits optional properties when empty" do
417
+ node = described_class.create("test_node")
417
418
 
418
419
  expected = {
419
- 'type' => 'test_node'
420
+ "type" => "test_node",
420
421
  }
421
422
 
422
423
  expect(node.to_h).to eq(expected)
423
424
  end
424
425
  end
426
+
427
+ describe "#node_size" do
428
+ it "returns 1 for empty node" do
429
+ node = described_class.create("empty")
430
+ expect(node.node_size).to eq(1)
431
+ end
432
+
433
+ it "includes text children" do
434
+ node = described_class.create("parent")
435
+ node.add_child(Prosereflect::Text.create("hello"))
436
+ # 1 (parent) + 6 (text "hello") = 7
437
+ expect(node.node_size).to eq(7)
438
+ end
439
+
440
+ it "sums multiple children" do
441
+ node = described_class.create("parent")
442
+ node.add_child(Prosereflect::Text.create("ab"))
443
+ node.add_child(Prosereflect::Text.create("cd"))
444
+ # 1 (parent) + 3 ("ab") + 3 ("cd") = 7
445
+ expect(node.node_size).to eq(7)
446
+ end
447
+
448
+ it "handles deeply nested content" do
449
+ doc = Prosereflect::Document.create
450
+ para = Prosereflect::Paragraph.create
451
+ para.add_child(Prosereflect::Text.create("hi"))
452
+ doc.add_child(para)
453
+ # 1 (doc) + 1 (para) + 3 (text "hi") = 5
454
+ expect(doc.node_size).to eq(5)
455
+ end
456
+ end
457
+
458
+ describe "#text?" do
459
+ it "returns false for regular nodes" do
460
+ expect(described_class.create("node").text?).to be false
461
+ expect(Prosereflect::Paragraph.create.text?).to be false
462
+ expect(Prosereflect::Document.create.text?).to be false
463
+ end
464
+
465
+ it "returns true for Text nodes" do
466
+ expect(Prosereflect::Text.create("hello").text?).to be true
467
+ end
468
+ end
469
+
470
+ describe "#cut" do
471
+ it "returns self for full range" do
472
+ node = described_class.create("node")
473
+ expect(node.cut(0, 1)).to eq(node)
474
+ end
475
+
476
+ it "returns self for default range" do
477
+ node = described_class.create("node")
478
+ expect(node.cut).to eq(node)
479
+ end
480
+
481
+ it "returns copy with subset of content" do
482
+ node = described_class.create("parent")
483
+ node.add_child(Prosereflect::Text.create("first"))
484
+ node.add_child(Prosereflect::Text.create("second"))
485
+ cut_node = node.cut(0, 1 + 7) # 1 parent + first text (7)
486
+ expect(cut_node).not_to eq(node)
487
+ end
488
+ end
489
+
490
+ describe "#nodes_between" do
491
+ it "yields children in range" do
492
+ node = described_class.create("parent")
493
+ t1 = Prosereflect::Text.create("ab")
494
+ t2 = Prosereflect::Text.create("cd")
495
+ node.add_child(t1)
496
+ node.add_child(t2)
497
+
498
+ visited = []
499
+ node.nodes_between(0, 6) { |n, _pos, _i| visited << n }
500
+ expect(visited).to include(t1)
501
+ end
502
+
503
+ it "does not yield for empty range" do
504
+ node = described_class.create("parent")
505
+ visited = []
506
+ node.nodes_between(0, 0) { |n| visited << n }
507
+ expect(visited).to be_empty
508
+ end
509
+ end
510
+
511
+ describe "#descendants" do
512
+ it "iterates over all descendants" do
513
+ doc = Prosereflect::Document.create
514
+ para = Prosereflect::Paragraph.create
515
+ text = Prosereflect::Text.create("hello")
516
+ para.add_child(text)
517
+ doc.add_child(para)
518
+
519
+ visited = []
520
+ doc.descendants { |n, _pos, _i| visited << n }
521
+ expect(visited).to include(para)
522
+ end
523
+ end
524
+
525
+ describe "#eq?" do
526
+ it "returns true for structurally equal nodes" do
527
+ n1 = described_class.create("node")
528
+ n2 = described_class.create("node")
529
+ expect(n1.eq?(n2)).to be true
530
+ end
531
+
532
+ it "returns false for different types" do
533
+ n1 = described_class.create("a")
534
+ n2 = described_class.create("b")
535
+ expect(n1.eq?(n2)).to be false
536
+ end
537
+ end
538
+
539
+ describe "#copy" do
540
+ it "creates a shallow copy with same type and attrs" do
541
+ node = described_class.create("node", "key" => "val")
542
+ copy = node.copy
543
+ expect(copy.to_h).to eq(node.to_h)
544
+ expect(copy).not_to equal(node)
545
+ end
546
+
547
+ it "creates copy with new content" do
548
+ node = described_class.create("parent")
549
+ copy = node.copy([Prosereflect::Text.create("new")])
550
+ expect(copy.content.length).to eq(1)
551
+ expect(node.content).to be_empty
552
+ end
553
+ end
425
554
  end