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,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
4
-
5
3
  # {
6
4
  # type: "strike"
7
5
  # }
@@ -9,7 +7,7 @@ require_relative 'base'
9
7
  module Prosereflect
10
8
  module Mark
11
9
  class Strike < Base
12
- PM_TYPE = 'strike'
10
+ PM_TYPE = "strike"
13
11
  end
14
12
  end
15
13
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
4
-
5
3
  # {
6
4
  # type: "subscript"
7
5
  # }
@@ -9,7 +7,7 @@ require_relative 'base'
9
7
  module Prosereflect
10
8
  module Mark
11
9
  class Subscript < Base
12
- PM_TYPE = 'subscript'
10
+ PM_TYPE = "subscript"
13
11
  end
14
12
  end
15
13
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
4
-
5
3
  # {
6
4
  # type: "superscript"
7
5
  # }
@@ -9,7 +7,7 @@ require_relative 'base'
9
7
  module Prosereflect
10
8
  module Mark
11
9
  class Superscript < Base
12
- PM_TYPE = 'superscript'
10
+ PM_TYPE = "superscript"
13
11
  end
14
12
  end
15
13
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'base'
4
-
5
3
  # {
6
4
  # type: "underline"
7
5
  # }
@@ -9,7 +7,7 @@ require_relative 'base'
9
7
  module Prosereflect
10
8
  module Mark
11
9
  class Underline < Base
12
- PM_TYPE = 'underline'
10
+ PM_TYPE = "underline"
13
11
  end
14
12
  end
15
13
  end
@@ -2,10 +2,14 @@
2
2
 
3
3
  module Prosereflect
4
4
  module Mark
5
+ autoload :Base, "#{__dir__}/mark/base"
6
+ autoload :Bold, "#{__dir__}/mark/bold"
7
+ autoload :Italic, "#{__dir__}/mark/italic"
8
+ autoload :Code, "#{__dir__}/mark/code"
9
+ autoload :Link, "#{__dir__}/mark/link"
10
+ autoload :Strike, "#{__dir__}/mark/strike"
11
+ autoload :Subscript, "#{__dir__}/mark/subscript"
12
+ autoload :Superscript, "#{__dir__}/mark/superscript"
13
+ autoload :Underline, "#{__dir__}/mark/underline"
5
14
  end
6
15
  end
7
-
8
- require_relative 'mark/bold'
9
- require_relative 'mark/italic'
10
- require_relative 'mark/code'
11
- require_relative 'mark/link'
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'lutaml/model'
4
- require_relative 'attribute'
5
- require_relative 'mark'
6
-
7
3
  module Prosereflect
8
4
  class Node < Lutaml::Model::Serializable
9
- PM_TYPE = 'node'
5
+ PM_TYPE = "node"
10
6
 
11
7
  attribute :type, :string
12
8
  attribute :attrs, :hash
@@ -14,10 +10,10 @@ module Prosereflect
14
10
  attribute :content, Node, polymorphic: true, collection: true
15
11
 
16
12
  key_value do
17
- map 'type', to: :type, render_default: true
18
- map 'attrs', to: :attrs
19
- map 'marks', to: :marks
20
- map 'content', to: :content
13
+ map "type", to: :type, render_default: true
14
+ map "attrs", to: :attrs
15
+ map "marks", to: :marks
16
+ map "content", to: :content
21
17
  end
22
18
 
23
19
  def initialize(data = nil, attrs = nil)
@@ -25,18 +21,18 @@ module Prosereflect
25
21
  super(type: data, attrs: attrs, content: [])
26
22
  elsif data.is_a?(Hash)
27
23
  # Handle marks in a special way to preserve expected behavior in tests
28
- if data[:marks] || data['marks']
29
- marks_data = data[:marks] || data['marks']
24
+ if data[:marks] || data["marks"]
25
+ marks_data = data[:marks] || data["marks"]
30
26
  data = data.dup
31
- data.delete('marks')
27
+ data.delete("marks")
32
28
  data.delete(:marks)
33
29
  super(data)
34
30
  self.marks = marks_data
35
31
  else
36
32
  # Handle attrs properly
37
- if data[:attrs] || data['attrs']
33
+ if data[:attrs] || data["attrs"]
38
34
  data = data.dup
39
- data[:attrs] = process_attrs_data(data[:attrs] || data['attrs'])
35
+ data[:attrs] = process_attrs_data(data[:attrs] || data["attrs"])
40
36
  end
41
37
  super(data)
42
38
  end
@@ -56,33 +52,35 @@ module Prosereflect
56
52
  def self.create(type = nil, attrs = nil)
57
53
  new(type || self::PM_TYPE, attrs)
58
54
  rescue NameError
59
- new(type || 'node', attrs)
55
+ new(type || "node", attrs)
60
56
  end
61
57
 
62
58
  # Convert to hash for serialization
63
59
  def to_h
64
- result = { 'type' => type }
60
+ result = { "type" => type }
65
61
 
66
62
  if attrs && !attrs.empty?
67
63
  if attrs.is_a?(Hash)
68
- result['attrs'] = process_node_attributes(attrs, type)
69
- elsif attrs.is_a?(Array) && attrs.all? { |attr| attr.respond_to?(:to_h) }
64
+ result["attrs"] = process_node_attributes(attrs, type)
65
+ elsif attrs.is_a?(Array) && attrs.all? do |attr|
66
+ attr.respond_to?(:to_h)
67
+ end
70
68
  # Convert array of attribute objects to a hash
71
69
  attrs_array = attrs.map do |attr|
72
70
  attr.is_a?(Prosereflect::Attribute::Base) ? attr.to_h : attr
73
71
  end
74
- result['attrs'] = attrs_array unless attrs_array.empty?
72
+ result["attrs"] = attrs_array unless attrs_array.empty?
75
73
  end
76
74
  end
77
75
 
78
76
  if marks && !marks.empty?
79
- result['marks'] = marks.map do |mark|
77
+ result["marks"] = marks.map do |mark|
80
78
  if mark.is_a?(Hash)
81
79
  mark
82
80
  elsif mark.respond_to?(:to_h)
83
81
  mark.to_h
84
82
  elsif mark.respond_to?(:type)
85
- { 'type' => mark.type.to_s }
83
+ { "type" => mark.type.to_s }
86
84
  else
87
85
  raise ArgumentError, "Invalid mark type: #{mark.class}"
88
86
  end
@@ -90,8 +88,10 @@ module Prosereflect
90
88
  end
91
89
 
92
90
  if content && !content.empty?
93
- result['content'] = if content.is_a?(Array)
94
- content.map { |item| item.respond_to?(:to_h) ? item.to_h : item }
91
+ result["content"] = if content.is_a?(Array)
92
+ content.map do |item|
93
+ item.respond_to?(:to_h) ? item.to_h : item
94
+ end
95
95
  else
96
96
  [content]
97
97
  end
@@ -112,7 +112,7 @@ module Prosereflect
112
112
  elsif mark.respond_to?(:to_h)
113
113
  mark.to_h
114
114
  elsif mark.respond_to?(:type)
115
- { 'type' => mark.type.to_s }
115
+ { "type" => mark.type.to_s }
116
116
  else
117
117
  raise ArgumentError, "Invalid mark type: #{mark.class}"
118
118
  end
@@ -131,8 +131,8 @@ module Prosereflect
131
131
  elsif value.is_a?(Array)
132
132
  @marks = value.map do |v|
133
133
  if v.is_a?(Hash)
134
- type = v['type'] || v[:type]
135
- attrs = v['attrs'] || v[:attrs]
134
+ type = v["type"] || v[:type]
135
+ attrs = v["attrs"] || v[:attrs]
136
136
  begin
137
137
  mark_class = Prosereflect::Mark.const_get(type.to_s.capitalize)
138
138
  mark_class.new(attrs: attrs)
@@ -197,27 +197,165 @@ module Prosereflect
197
197
  def find_children(node_type)
198
198
  return [] unless content
199
199
 
200
- content.select { |child| child.is_a?(node_type) }
200
+ content.grep(node_type)
201
201
  end
202
202
 
203
203
  def text_content
204
- return '' unless content
204
+ return "" unless content
205
205
 
206
206
  content.map(&:text_content).join
207
207
  end
208
208
 
209
+ # Size of this node in the document tree.
210
+ # For non-text nodes: 1 (opening token) + sum of children's node_size.
211
+ # For text nodes: overridden to text.length + 1.
212
+ def node_size
213
+ size = 1
214
+ content&.each { |child| size += child.node_size }
215
+ size
216
+ end
217
+
218
+ # Whether this node represents a text node.
219
+ # Overridden to true in Text class.
220
+ def text?
221
+ false
222
+ end
223
+
224
+ # Return a copy of this node with content restricted to the given range.
225
+ # Positions are relative to the start of this node's content.
226
+ def cut(from = 0, to = nil)
227
+ to ||= node_size
228
+ return self if from.zero? && to == node_size
229
+
230
+ if text?
231
+ # Text nodes override this
232
+ self
233
+ else
234
+ copy(cut_content(from, to))
235
+ end
236
+ end
237
+
238
+ # Iterate over all nodes between two positions in this node.
239
+ # Accepts a block or a callable as the third positional argument.
240
+ def nodes_between(from, to, callback = nil, node_start = 0, &block)
241
+ cb = callback || block
242
+ return unless cb && to > from && content
243
+
244
+ pos = 0
245
+ content.each_with_index do |child, i|
246
+ break if pos >= to
247
+
248
+ child_end = pos + child.node_size
249
+ next unless child_end > from
250
+
251
+ child_start = node_start + pos + 1
252
+ if cb.call(child, child_start, i) != false && child.content && child.content.any?
253
+ child.nodes_between(
254
+ [0, from - pos - 1].max,
255
+ [child.content ? child.content.size : 0, to - pos - 1].min,
256
+ cb,
257
+ child_start,
258
+ )
259
+ end
260
+
261
+ pos = child_end
262
+ end
263
+ end
264
+
265
+ # Iterate over all descendant nodes.
266
+ def descendants(&block)
267
+ nodes_between(0, node_size - 1, &block)
268
+ end
269
+
270
+ # Check structural equality with another node.
271
+ def eq?(other)
272
+ return false unless other.is_a?(Node)
273
+
274
+ type == other.type && to_h == other.to_h
275
+ end
276
+
277
+ # Create a copy of this node with different content.
278
+ def copy(new_content = nil)
279
+ new_node = self.class.new(type: type, attrs: attrs, marks: raw_marks)
280
+ case new_content
281
+ when nil
282
+ # no content
283
+ when Array
284
+ new_node.content = new_content
285
+ when Fragment
286
+ new_node.content = new_content.to_a
287
+ else
288
+ new_node.content = [new_content]
289
+ end
290
+ new_node
291
+ end
292
+
209
293
  # Ensures YAML serialization outputs plain data instead of a Ruby object
210
294
  def to_yaml(*args)
211
295
  to_h.to_yaml(*args)
212
296
  end
213
297
 
298
+ # Resolve a document position to a ResolvedPos
299
+ def resolve(pos)
300
+ path = []
301
+ build_path_for_pos(pos, path)
302
+ depth = [(path.length / 3) - 1, 0].max
303
+ ResolvedPos.new(pos, path, depth)
304
+ end
305
+
306
+ # Get the node at a given depth in the path
307
+ def node(depth)
308
+ @path[depth * 2]
309
+ end
310
+
214
311
  private
215
312
 
216
- def process_node_attributes(attrs, node_type)
217
- if attrs['attrs'].is_a?(Hash)
218
- attrs['attrs']
219
- elsif node_type == 'bullet_list' && attrs['bullet_style'].nil?
220
- nil
313
+ def cut_content(from, to)
314
+ return [] unless content
315
+
316
+ result = []
317
+ pos = 0
318
+ content.each do |child|
319
+ child_end = pos + child.node_size
320
+ if pos >= from && child_end <= to
321
+ result << child
322
+ elsif pos < to && child_end > from
323
+ result << child.cut([0, from - pos - 1].max, child.node_size - [0, child_end - to].max)
324
+ end
325
+ pos = child_end
326
+ break if pos >= to
327
+ end
328
+ result
329
+ end
330
+
331
+ def build_path_for_pos(pos, path, index = 0, start_offset = 0)
332
+ path << self << index << start_offset
333
+ return if pos.zero?
334
+
335
+ traverse_children_for_resolve(pos, path)
336
+ end
337
+
338
+ def traverse_children_for_resolve(pos, path)
339
+ return unless content
340
+
341
+ content_offset = 1
342
+ child_index = 0
343
+
344
+ content.each do |child|
345
+ child_end = content_offset + child.node_size
346
+ if pos < child_end
347
+ child.send(:build_path_for_pos, pos - content_offset, path, child_index, content_offset)
348
+ return
349
+ end
350
+
351
+ content_offset = child_end
352
+ child_index += 1
353
+ end
354
+ end
355
+
356
+ def process_node_attributes(attrs, _node_type)
357
+ if attrs["attrs"].is_a?(Hash)
358
+ attrs["attrs"]
221
359
  else
222
360
  attrs
223
361
  end
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'list_item'
5
-
6
3
  module Prosereflect
7
4
  # OrderedList class represents a numbered list in ProseMirror.
8
5
  class OrderedList < Node
9
- PM_TYPE = 'ordered_list'
6
+ PM_TYPE = "ordered_list"
10
7
 
11
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
8
+ attribute :type, :string, default: -> {
9
+ self.class.send(:const_get, "PM_TYPE")
10
+ }
12
11
  attribute :start, :integer
13
12
  attribute :attrs, :hash
14
13
 
15
14
  key_value do
16
- map 'type', to: :type, render_default: true
17
- map 'attrs', to: :attrs
18
- map 'content', to: :content
15
+ map "type", to: :type, render_default: true
16
+ map "attrs", to: :attrs
17
+ map "content", to: :content
19
18
  end
20
19
 
21
20
  def initialize(attributes = {})
@@ -29,12 +28,14 @@ module Prosereflect
29
28
 
30
29
  def start=(value)
31
30
  @start = value
31
+ return if value.nil?
32
+
32
33
  self.attrs ||= {}
33
- attrs['start'] = value
34
+ attrs["start"] = value
34
35
  end
35
36
 
36
37
  def start
37
- @start || attrs&.[]('start') || 1
38
+ @start || attrs&.[]("start") || 1
38
39
  end
39
40
 
40
41
  def add_item(text)
@@ -67,19 +68,21 @@ module Prosereflect
67
68
  # Update the order (1 for numerical, 'a' for alphabetical, etc.)
68
69
  def order=(order_value)
69
70
  self.attrs ||= {}
70
- attrs['order'] = order_value
71
+ attrs["order"] = order_value
71
72
  end
72
73
 
73
74
  # Get the order value
74
75
  def order
75
- attrs&.[]('order') || 1
76
+ attrs&.[]("order") || 1
76
77
  end
77
78
 
78
79
  # Get text content with proper formatting
79
80
  def text_content
80
- return '' unless content
81
+ return "" unless content
81
82
 
82
- content.map { |item| item.respond_to?(:text_content) ? item.text_content : '' }.join("\n")
83
+ content.map do |item|
84
+ item.respond_to?(:text_content) ? item.text_content : ""
85
+ end.join("\n")
83
86
  end
84
87
  end
85
88
  end