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,27 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'table'
5
- require_relative 'paragraph'
6
- require_relative 'image'
7
- require_relative 'bullet_list'
8
- require_relative 'ordered_list'
9
- require_relative 'blockquote'
10
- require_relative 'horizontal_rule'
11
- require_relative 'code_block_wrapper'
12
- require_relative 'heading'
13
- require_relative 'user'
14
-
15
3
  module Prosereflect
16
4
  # Document class represents a ProseMirror document.
17
5
  class Document < Node
18
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
19
- PM_TYPE = 'doc'
6
+ attribute :type, :string, default: -> {
7
+ self.class.send(:const_get, "PM_TYPE")
8
+ }
9
+ PM_TYPE = "doc"
20
10
 
21
11
  key_value do
22
- map 'type', to: :type, render_default: true
23
- map 'content', to: :content
24
- map 'attrs', to: :attrs
12
+ map "type", to: :type, render_default: true
13
+ map "content", to: :content
14
+ map "attrs", to: :attrs
25
15
  end
26
16
 
27
17
  def self.create(attrs = nil)
@@ -33,12 +23,12 @@ module Prosereflect
33
23
  result = super
34
24
 
35
25
  # Handle array of attribute objects specially for serialization
36
- if attrs.is_a?(Array) && attrs.all? { |attr| attr.is_a?(Prosereflect::Attribute::Base) }
26
+ if attrs.is_a?(Array) && attrs.all?(Prosereflect::Attribute::Base)
37
27
  attrs_hash = {}
38
28
  attrs.each do |attr|
39
29
  attrs_hash.merge!(attr.to_h)
40
30
  end
41
- result['attrs'] = attrs_hash unless attrs_hash.empty?
31
+ result["attrs"] = attrs_hash unless attrs_hash.empty?
42
32
  end
43
33
 
44
34
  result
@@ -54,7 +44,7 @@ module Prosereflect
54
44
 
55
45
  # Add a heading to the document
56
46
  def add_heading(level)
57
- heading = Heading.new(attrs: { 'level' => level })
47
+ heading = Heading.new(attrs: { "level" => level })
58
48
  add_child(heading)
59
49
  heading
60
50
  end
@@ -130,9 +120,11 @@ module Prosereflect
130
120
 
131
121
  # Get plain text content from all nodes
132
122
  def text_content
133
- return '' unless content
123
+ return "" unless content
134
124
 
135
- content.map { |node| node.respond_to?(:text_content) ? node.text_content : '' }.join("\n").strip
125
+ content.map do |node|
126
+ node.respond_to?(:text_content) ? node.text_content : ""
127
+ end.join("\n").strip
136
128
  end
137
129
  end
138
130
  end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ # Fragment represents a sequence of nodes.
5
+ # Used for document content, slice content, etc.
6
+ class Fragment
7
+ attr_reader :content
8
+
9
+ def initialize(content = [])
10
+ @content = if content.is_a?(Array)
11
+ content
12
+ elsif content.respond_to?(:to_a)
13
+ content.to_a
14
+ else
15
+ [content]
16
+ end
17
+ end
18
+
19
+ # Total size of all nodes in this fragment
20
+ def size
21
+ @content.sum { |n| n.respond_to?(:node_size) ? n.node_size : n.text_content.length + 1 }
22
+ end
23
+
24
+ # Check if fragment is empty
25
+ def empty?
26
+ @content.empty?
27
+ end
28
+
29
+ # Append another fragment to this one
30
+ def append(other)
31
+ if other.is_a?(Fragment)
32
+ Fragment.new(@content + other.content)
33
+ else
34
+ Fragment.new(@content + [other])
35
+ end
36
+ end
37
+
38
+ # Cut this fragment to a range
39
+ def cut(from = 0, to = nil)
40
+ to ||= size
41
+
42
+ return Fragment.new([]) if from >= to
43
+
44
+ cut_nodes(from, to)
45
+ end
46
+
47
+ def cut_nodes(from, to)
48
+ result = []
49
+ pos = 0
50
+
51
+ @content.each do |node|
52
+ node_end = pos + node.node_size
53
+
54
+ result << node if in_range_before_from?(pos, node_end, from)
55
+ result << node if overlaps_range?(pos, node_end, from, to)
56
+
57
+ pos = node_end
58
+ break if pos >= to
59
+ end
60
+
61
+ Fragment.new(result)
62
+ end
63
+
64
+ def in_range_before_from?(_pos, node_end, from)
65
+ node_end <= from
66
+ end
67
+
68
+ def overlaps_range?(pos, node_end, from, to)
69
+ (pos >= from && node_end <= to) || (pos < from && node_end > from)
70
+ end
71
+
72
+ # Replace child at index
73
+ def replace_child(index, replacement)
74
+ new_content = @content.dup
75
+ new_content[index] = replacement
76
+ Fragment.new(new_content)
77
+ end
78
+
79
+ # Iterate over all nodes between positions
80
+ def nodes_between(from, to, callback = nil, node_start = 0, &blk)
81
+ cb = callback || blk
82
+ return unless cb && to > from
83
+
84
+ pos = 0
85
+
86
+ @content.each do |node|
87
+ node_end = pos + node.node_size
88
+ next unless node_end > from
89
+
90
+ dispatch_node_callback(node, pos, node_end, from, to, cb, node_start)
91
+ pos = node_end
92
+ break if pos >= to
93
+ end
94
+ end
95
+
96
+ def dispatch_node_callback(node, pos, node_end, from, to, callback, node_start)
97
+ if node.text?
98
+ text_node_callback(node, pos, from, node_start, callback)
99
+ elsif node_fully_in_range?(pos, node_end, from, to)
100
+ full_node_callback(node, pos, node_end, from, to, callback, node_start)
101
+ elsif node_overlaps_from?(pos, node_end, from)
102
+ partial_node_callback(node, pos, node_end, from, to, callback, node_start)
103
+ end
104
+ end
105
+
106
+ def text_node_callback(node, pos, from, node_start, callback)
107
+ callback.call(node, node_start + (from - pos).clamp(0, node.node_size - 1))
108
+ end
109
+
110
+ def node_fully_in_range?(pos, node_end, from, to)
111
+ pos >= from && node_end <= to
112
+ end
113
+
114
+ def full_node_callback(node, _pos, _node_end, _from, _to, callback, node_start)
115
+ callback.call(node, node_start)
116
+ recurse_into_node(node, 0, node.content.size, callback, node_start)
117
+ end
118
+
119
+ def partial_node_callback(node, pos, _node_end, from, to, callback, node_start)
120
+ recurse_into_node(node, from - pos, [to - pos, node.content.size].min, callback, node_start)
121
+ end
122
+
123
+ def node_overlaps_from?(pos, node_end, from)
124
+ pos < from && node_end > from
125
+ end
126
+
127
+ def recurse_into_node(node, start_pos, end_pos, callback, node_start)
128
+ return unless node.respond_to?(:nodes_between)
129
+
130
+ node.nodes_between(start_pos, end_pos, callback, node_start)
131
+ end
132
+
133
+ # Iterate over all descendant nodes
134
+ def descendants(block, node_start = 0)
135
+ nodes_between(0, size, block, node_start)
136
+ end
137
+
138
+ # Extract text content between positions
139
+ def text_between(_from, _to, separator = "", _block_separator = "\n")
140
+ result = []
141
+ @content.each do |node|
142
+ if node.respond_to?(:text)
143
+ result << node.text
144
+ elsif node.respond_to?(:text_content)
145
+ result << node.text_content
146
+ end
147
+ end
148
+ result.join(separator)
149
+ end
150
+
151
+ # Find first position where two fragments differ
152
+ def find_diff_start(other)
153
+ min_length = [@content.length, other.content.length].min
154
+
155
+ pos = 0
156
+ min_length.times do |i|
157
+ return pos if @content[i] != other.content[i]
158
+
159
+ pos += @content[i].node_size
160
+ end
161
+
162
+ return nil if @content.length == other.content.length
163
+
164
+ pos
165
+ end
166
+
167
+ # Find last position where two fragments differ
168
+ def find_diff_end(other)
169
+ my_nodes = @content.reverse
170
+ other_nodes = other.content.reverse
171
+
172
+ i = 0
173
+ end_pos = size
174
+
175
+ while i < my_nodes.length && i < other_nodes.length
176
+ my_node = my_nodes[i]
177
+ other_node = other_nodes[i]
178
+
179
+ unless my_node == other_node
180
+ return end_pos
181
+ end
182
+
183
+ end_pos -= my_node.node_size
184
+ i += 1
185
+ end
186
+
187
+ nil
188
+ end
189
+
190
+ # Check equality
191
+ def eq?(other)
192
+ return false unless other.is_a?(Fragment)
193
+
194
+ @content.length == other.content.length &&
195
+ @content.zip(other.content).all? { |a, b| a.to_h == b.to_h }
196
+ end
197
+
198
+ alias == eq?
199
+
200
+ # Hash for use in sets/hashes
201
+ def hash
202
+ @content.map(&:to_h).hash
203
+ end
204
+
205
+ # Access by index
206
+ def [](index)
207
+ @content[index]
208
+ end
209
+
210
+ # Iterate
211
+ def each(&block)
212
+ @content.each(&block)
213
+ end
214
+
215
+ # Number of items
216
+ def length
217
+ @content.length
218
+ end
219
+
220
+ alias count length
221
+
222
+ # Convert to array
223
+ def to_a
224
+ @content.dup
225
+ end
226
+
227
+ # Create empty fragment
228
+ def self.empty
229
+ @empty ||= new([])
230
+ end
231
+
232
+ # Create from content
233
+ def self.from(content)
234
+ case content
235
+ when Fragment then content
236
+ when Array then new(content.flatten)
237
+ else new([content])
238
+ end
239
+ end
240
+
241
+ def to_s
242
+ "<Fragment #{@content.length} nodes>"
243
+ end
244
+
245
+ def inspect
246
+ to_s
247
+ end
248
+ end
249
+ end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  class HardBreak < Node
7
- PM_TYPE = 'hard_break'
5
+ PM_TYPE = "hard_break"
8
6
 
9
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
10
10
 
11
11
  key_value do
12
- map 'type', to: :type, render_default: true
13
- map 'marks', to: :marks
12
+ map "type", to: :type, render_default: true
13
+ map "marks", to: :marks
14
14
  end
15
15
 
16
16
  def self.create(marks = nil)
@@ -1,21 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'text'
5
-
6
3
  module Prosereflect
7
4
  class Heading < Node
8
- PM_TYPE = 'heading'
5
+ PM_TYPE = "heading"
9
6
 
10
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
11
10
  attribute :level, :integer
12
11
  attribute :attrs, :hash
13
12
 
14
13
  key_value do
15
- map 'type', to: :type, render_default: true
16
- map 'content', to: :content
17
- map 'attrs', to: :attrs
18
- map 'marks', to: :marks
14
+ map "type", to: :type, render_default: true
15
+ map "content", to: :content
16
+ map "attrs", to: :attrs
17
+ map "marks", to: :marks
19
18
  end
20
19
 
21
20
  def initialize(params = {})
@@ -25,7 +24,7 @@ module Prosereflect
25
24
  # Extract level from attrs if provided
26
25
  return unless params[:attrs]
27
26
 
28
- @level = params[:attrs]['level']
27
+ @level = params[:attrs]["level"]
29
28
  end
30
29
 
31
30
  def self.create(attrs = nil)
@@ -35,15 +34,15 @@ module Prosereflect
35
34
  def level=(value)
36
35
  @level = value
37
36
  self.attrs ||= {}
38
- attrs['level'] = value
37
+ attrs["level"] = value
39
38
  end
40
39
 
41
40
  def level
42
- @level || attrs&.[]('level')
41
+ @level || attrs&.[]("level")
43
42
  end
44
43
 
45
44
  def text_content
46
- return '' unless content
45
+ return "" unless content
47
46
 
48
47
  content.map(&:text_content).join
49
48
  end
@@ -56,8 +55,8 @@ module Prosereflect
56
55
 
57
56
  def to_h
58
57
  result = super
59
- result['attrs'] ||= {}
60
- result['attrs']['level'] = level if level
58
+ result["attrs"] ||= {}
59
+ result["attrs"]["level"] = level if level
61
60
  result
62
61
  end
63
62
  end
@@ -1,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  # HorizontalRule class represents a horizontal rule in ProseMirror.
7
5
  class HorizontalRule < Node
8
- PM_TYPE = 'horizontal_rule'
6
+ PM_TYPE = "horizontal_rule"
9
7
 
10
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
8
+ attribute :type, :string, default: -> {
9
+ self.class.send(:const_get, "PM_TYPE")
10
+ }
11
11
  attribute :style, :string
12
12
  attribute :width, :string
13
13
  attribute :thickness, :integer
14
14
  attribute :attrs, :hash
15
15
 
16
16
  key_value do
17
- map 'type', to: :type, render_default: true
18
- map 'attrs', to: :attrs
19
- map 'content', to: :content
17
+ map "type", to: :type, render_default: true
18
+ map "attrs", to: :attrs
19
+ map "content", to: :content
20
20
  end
21
21
 
22
22
  def initialize(attributes = {})
@@ -31,36 +31,45 @@ module Prosereflect
31
31
  def style=(value)
32
32
  @style = value
33
33
  self.attrs ||= {}
34
- attrs['style'] = value
34
+ attrs["style"] = value
35
35
  end
36
36
 
37
37
  def style
38
- @style || attrs&.[]('style')
38
+ @style || attrs&.[]("style")
39
39
  end
40
40
 
41
41
  def width=(value)
42
42
  @width = value
43
43
  self.attrs ||= {}
44
- attrs['width'] = value
44
+ attrs["width"] = value
45
45
  end
46
46
 
47
47
  def width
48
- @width || attrs&.[]('width')
48
+ @width || attrs&.[]("width")
49
49
  end
50
50
 
51
51
  def thickness=(value)
52
52
  @thickness = value
53
53
  self.attrs ||= {}
54
- attrs['thickness'] = value
54
+ attrs["thickness"] = value
55
55
  end
56
56
 
57
57
  def thickness
58
- @thickness || attrs&.[]('thickness')
58
+ @thickness || attrs&.[]("thickness")
59
59
  end
60
60
 
61
61
  # Override content-related methods since horizontal rules don't have content
62
+ def to_h
63
+ hash = super
64
+ if hash["attrs"]
65
+ %w[style width thickness].each { |k| hash["attrs"].delete(k) if hash["attrs"][k].nil? }
66
+ hash.delete("attrs") if hash["attrs"].empty?
67
+ end
68
+ hash
69
+ end
70
+
62
71
  def add_child(*)
63
- raise NotImplementedError, 'Horizontal rule nodes cannot have children'
72
+ raise NotImplementedError, "Horizontal rule nodes cannot have children"
64
73
  end
65
74
 
66
75
  def content
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  # Image class represents a ProseMirror image node.
7
5
  # It handles image attributes like src, alt, title, dimensions, etc.
8
6
  class Image < Node
9
- PM_TYPE = 'image'
7
+ PM_TYPE = "image"
10
8
 
11
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
9
+ attribute :type, :string, default: -> {
10
+ self.class.send(:const_get, "PM_TYPE")
11
+ }
12
12
  attribute :src, :string
13
13
  attribute :alt, :string
14
14
  attribute :title, :string
@@ -17,8 +17,8 @@ module Prosereflect
17
17
  attribute :attrs, :hash
18
18
 
19
19
  key_value do
20
- map 'type', to: :type, render_default: true
21
- map 'attrs', to: :attrs
20
+ map "type", to: :type, render_default: true
21
+ map "attrs", to: :attrs
22
22
  end
23
23
 
24
24
  def initialize(attributes = {})
@@ -27,11 +27,11 @@ module Prosereflect
27
27
 
28
28
  # Extract attributes from the attrs hash if provided
29
29
  if attributes[:attrs]
30
- @src = attributes[:attrs]['src']
31
- @alt = attributes[:attrs]['alt']
32
- @title = attributes[:attrs]['title']
33
- @width = attributes[:attrs]['width']
34
- @height = attributes[:attrs]['height']
30
+ @src = attributes[:attrs]["src"]
31
+ @alt = attributes[:attrs]["alt"]
32
+ @title = attributes[:attrs]["title"]
33
+ @width = attributes[:attrs]["width"]
34
+ @height = attributes[:attrs]["height"]
35
35
  end
36
36
 
37
37
  super
@@ -45,35 +45,35 @@ module Prosereflect
45
45
  def src=(src_url)
46
46
  @src = src_url
47
47
  self.attrs ||= {}
48
- attrs['src'] = src_url
48
+ attrs["src"] = src_url
49
49
  end
50
50
 
51
51
  # Update the alt text
52
52
  def alt=(alt_text)
53
53
  @alt = alt_text
54
54
  self.attrs ||= {}
55
- attrs['alt'] = alt_text
55
+ attrs["alt"] = alt_text
56
56
  end
57
57
 
58
58
  # Update the title (tooltip)
59
59
  def title=(title_text)
60
60
  @title = title_text
61
61
  self.attrs ||= {}
62
- attrs['title'] = title_text
62
+ attrs["title"] = title_text
63
63
  end
64
64
 
65
65
  # Update the width
66
66
  def width=(value)
67
67
  @width = value
68
68
  self.attrs ||= {}
69
- attrs['width'] = value
69
+ attrs["width"] = value
70
70
  end
71
71
 
72
72
  # Update the height
73
73
  def height=(value)
74
74
  @height = value
75
75
  self.attrs ||= {}
76
- attrs['height'] = value
76
+ attrs["height"] = value
77
77
  end
78
78
 
79
79
  # Update dimensions (width and height)
@@ -90,13 +90,22 @@ module Prosereflect
90
90
  alt: alt,
91
91
  title: title,
92
92
  width: width,
93
- height: height
93
+ height: height,
94
94
  }.compact
95
95
  end
96
96
 
97
+ def to_h
98
+ hash = super
99
+ if hash["attrs"]
100
+ %w[title width height].each { |k| hash["attrs"].delete(k) if hash["attrs"][k].nil? }
101
+ hash.delete("attrs") if hash["attrs"].empty?
102
+ end
103
+ hash
104
+ end
105
+
97
106
  # Override content-related methods since images don't have content
98
107
  def add_child(*)
99
- raise NotImplementedError, 'Image nodes cannot have children'
108
+ raise NotImplementedError, "Image nodes cannot have children"
100
109
  end
101
110
 
102
111
  def content
@@ -104,23 +113,23 @@ module Prosereflect
104
113
  end
105
114
 
106
115
  def src
107
- @src || attrs&.[]('src')
116
+ @src || attrs&.[]("src")
108
117
  end
109
118
 
110
119
  def alt
111
- @alt || attrs&.[]('alt')
120
+ @alt || attrs&.[]("alt")
112
121
  end
113
122
 
114
123
  def title
115
- @title || attrs&.[]('title')
124
+ @title || attrs&.[]("title")
116
125
  end
117
126
 
118
127
  def width
119
- @width || attrs&.[]('width')
128
+ @width || attrs&.[]("width")
120
129
  end
121
130
 
122
131
  def height
123
- @height || attrs&.[]('height')
132
+ @height || attrs&.[]("height")
124
133
  end
125
134
  end
126
135
  end