prosereflect 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +23 -4
  3. data/README.adoc +193 -12
  4. data/lib/prosereflect/attribute/base.rb +34 -0
  5. data/lib/prosereflect/attribute/bold.rb +20 -0
  6. data/lib/prosereflect/attribute/href.rb +24 -0
  7. data/lib/prosereflect/attribute/id.rb +24 -0
  8. data/lib/prosereflect/attribute.rb +13 -0
  9. data/lib/prosereflect/blockquote.rb +85 -0
  10. data/lib/prosereflect/bullet_list.rb +83 -0
  11. data/lib/prosereflect/code_block.rb +135 -0
  12. data/lib/prosereflect/code_block_wrapper.rb +66 -0
  13. data/lib/prosereflect/document.rb +99 -24
  14. data/lib/prosereflect/hard_break.rb +11 -9
  15. data/lib/prosereflect/heading.rb +64 -0
  16. data/lib/prosereflect/horizontal_rule.rb +70 -0
  17. data/lib/prosereflect/image.rb +126 -0
  18. data/lib/prosereflect/input/html.rb +505 -0
  19. data/lib/prosereflect/list_item.rb +65 -0
  20. data/lib/prosereflect/mark/base.rb +49 -0
  21. data/lib/prosereflect/mark/bold.rb +15 -0
  22. data/lib/prosereflect/mark/code.rb +14 -0
  23. data/lib/prosereflect/mark/italic.rb +15 -0
  24. data/lib/prosereflect/mark/link.rb +18 -0
  25. data/lib/prosereflect/mark/strike.rb +15 -0
  26. data/lib/prosereflect/mark/subscript.rb +15 -0
  27. data/lib/prosereflect/mark/superscript.rb +15 -0
  28. data/lib/prosereflect/mark/underline.rb +15 -0
  29. data/lib/prosereflect/mark.rb +11 -0
  30. data/lib/prosereflect/node.rb +181 -32
  31. data/lib/prosereflect/ordered_list.rb +85 -0
  32. data/lib/prosereflect/output/html.rb +374 -0
  33. data/lib/prosereflect/paragraph.rb +26 -15
  34. data/lib/prosereflect/parser.rb +111 -24
  35. data/lib/prosereflect/table.rb +40 -9
  36. data/lib/prosereflect/table_cell.rb +33 -8
  37. data/lib/prosereflect/table_header.rb +92 -0
  38. data/lib/prosereflect/table_row.rb +31 -8
  39. data/lib/prosereflect/text.rb +13 -17
  40. data/lib/prosereflect/user.rb +63 -0
  41. data/lib/prosereflect/version.rb +1 -1
  42. data/lib/prosereflect.rb +6 -0
  43. data/prosereflect.gemspec +1 -0
  44. data/spec/prosereflect/document_spec.rb +436 -36
  45. data/spec/prosereflect/hard_break_spec.rb +218 -22
  46. data/spec/prosereflect/input/html_spec.rb +797 -0
  47. data/spec/prosereflect/node_spec.rb +258 -89
  48. data/spec/prosereflect/output/html_spec.rb +369 -0
  49. data/spec/prosereflect/paragraph_spec.rb +424 -49
  50. data/spec/prosereflect/parser_spec.rb +304 -91
  51. data/spec/prosereflect/table_cell_spec.rb +268 -57
  52. data/spec/prosereflect/table_row_spec.rb +210 -40
  53. data/spec/prosereflect/table_spec.rb +392 -61
  54. data/spec/prosereflect/text_spec.rb +206 -48
  55. data/spec/prosereflect/user_spec.rb +73 -0
  56. data/spec/prosereflect_spec.rb +5 -0
  57. data/spec/support/shared_examples.rb +44 -15
  58. metadata +47 -3
  59. data/debug_loading.rb +0 -34
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # {
6
+ # type: "subscript"
7
+ # }
8
+
9
+ module Prosereflect
10
+ module Mark
11
+ class Subscript < Base
12
+ PM_TYPE = 'subscript'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # {
6
+ # type: "superscript"
7
+ # }
8
+
9
+ module Prosereflect
10
+ module Mark
11
+ class Superscript < Base
12
+ PM_TYPE = 'superscript'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ # {
6
+ # type: "underline"
7
+ # }
8
+
9
+ module Prosereflect
10
+ module Mark
11
+ class Underline < Base
12
+ PM_TYPE = 'underline'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ module Mark
5
+ end
6
+ end
7
+
8
+ require_relative 'mark/bold'
9
+ require_relative 'mark/italic'
10
+ require_relative 'mark/code'
11
+ require_relative 'mark/link'
@@ -1,42 +1,180 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'lutaml/model'
4
+ require_relative 'attribute'
5
+ require_relative 'mark'
6
+
3
7
  module Prosereflect
4
- class Node
5
- attr_reader :type, :attrs, :marks
6
- attr_accessor :content
8
+ class Node < Lutaml::Model::Serializable
9
+ PM_TYPE = 'node'
10
+
11
+ attribute :type, :string
12
+ attribute :attrs, :hash
13
+ attribute :marks, Mark::Base, polymorphic: true, collection: true
14
+ attribute :content, Node, polymorphic: true, collection: true
7
15
 
8
- def initialize(data = {})
9
- @type = data['type']
10
- @attrs = data['attrs']
11
- @content = parse_content(data['content'])
12
- @marks = data['marks']
16
+ 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
21
  end
14
22
 
15
- def parse_content(content_data)
16
- return [] unless content_data
23
+ def initialize(data = nil, attrs = nil)
24
+ if data.is_a?(String)
25
+ super(type: data, attrs: attrs, content: [])
26
+ elsif data.is_a?(Hash)
27
+ # 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']
30
+ data = data.dup
31
+ data.delete('marks')
32
+ data.delete(:marks)
33
+ super(data)
34
+ self.marks = marks_data
35
+ else
36
+ # Handle attrs properly
37
+ if data[:attrs] || data['attrs']
38
+ data = data.dup
39
+ data[:attrs] = process_attrs_data(data[:attrs] || data['attrs'])
40
+ end
41
+ super(data)
42
+ end
43
+ else
44
+ super()
45
+ end
46
+ end
17
47
 
18
- content_data.map { |item| Parser.parse(item) }
48
+ def process_attrs_data(attrs_data)
49
+ if attrs_data.is_a?(Hash)
50
+ attrs_data.transform_keys(&:to_s)
51
+ else
52
+ attrs_data
53
+ end
54
+ end
55
+
56
+ def self.create(type = nil, attrs = nil)
57
+ new(type || self::PM_TYPE, attrs)
58
+ rescue NameError
59
+ new(type || 'node', attrs)
19
60
  end
20
61
 
21
- # Create a serializable hash representation of this node
62
+ # Convert to hash for serialization
22
63
  def to_h
23
- result = { 'type' => @type }
24
- result['attrs'] = @attrs if @attrs
25
- result['marks'] = @marks if @marks
26
- result['content'] = @content.map(&:to_h) unless @content.empty?
64
+ result = { 'type' => type }
65
+
66
+ if attrs && !attrs.empty?
67
+ 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) }
70
+ # Convert array of attribute objects to a hash
71
+ attrs_array = attrs.map do |attr|
72
+ attr.is_a?(Prosereflect::Attribute::Base) ? attr.to_h : attr
73
+ end
74
+ result['attrs'] = attrs_array unless attrs_array.empty?
75
+ end
76
+ end
77
+
78
+ if marks && !marks.empty?
79
+ result['marks'] = marks.map do |mark|
80
+ if mark.is_a?(Hash)
81
+ mark
82
+ elsif mark.respond_to?(:to_h)
83
+ mark.to_h
84
+ elsif mark.respond_to?(:type)
85
+ { 'type' => mark.type.to_s }
86
+ else
87
+ raise ArgumentError, "Invalid mark type: #{mark.class}"
88
+ end
89
+ end
90
+ end
91
+
92
+ 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 }
95
+ else
96
+ [content]
97
+ end
98
+ end
99
+
27
100
  result
28
101
  end
29
102
 
103
+ alias to_hash to_h
104
+
105
+ def marks
106
+ return nil if @marks.nil?
107
+ return [] if @marks.empty?
108
+
109
+ @marks.map do |mark|
110
+ if mark.is_a?(Hash)
111
+ mark
112
+ elsif mark.respond_to?(:to_h)
113
+ mark.to_h
114
+ elsif mark.respond_to?(:type)
115
+ { 'type' => mark.type.to_s }
116
+ else
117
+ raise ArgumentError, "Invalid mark type: #{mark.class}"
118
+ end
119
+ end
120
+ end
121
+
122
+ def raw_marks
123
+ @marks
124
+ end
125
+
126
+ def marks=(value)
127
+ if value.nil?
128
+ @marks = nil
129
+ elsif value.is_a?(Array) && value.empty?
130
+ @marks = []
131
+ elsif value.is_a?(Array)
132
+ @marks = value.map do |v|
133
+ if v.is_a?(Hash)
134
+ type = v['type'] || v[:type]
135
+ attrs = v['attrs'] || v[:attrs]
136
+ begin
137
+ mark_class = Prosereflect::Mark.const_get(type.to_s.capitalize)
138
+ mark_class.new(attrs: attrs)
139
+ rescue NameError
140
+ Mark::Base.new(type: type, attrs: attrs)
141
+ end
142
+ elsif v.is_a?(Mark::Base)
143
+ v
144
+ elsif v.respond_to?(:type)
145
+ begin
146
+ mark_class = Prosereflect::Mark.const_get(v.type.to_s.capitalize)
147
+ mark_class.new(attrs: v.attrs)
148
+ rescue NameError
149
+ Mark::Base.new(type: v.type, attrs: v.attrs)
150
+ end
151
+ else
152
+ raise ArgumentError, "Invalid mark type: #{v.class}"
153
+ end
154
+ end
155
+ else
156
+ super
157
+ end
158
+ end
159
+
160
+ def parse_content(content_data)
161
+ return [] unless content_data
162
+
163
+ content_data.map { |item| Parser.parse(item) }
164
+ end
165
+
30
166
  # Add a child node to this node's content
31
167
  def add_child(node)
32
- @content ||= []
33
- @content << node
168
+ self.content ||= []
169
+ content << node
34
170
  node
35
171
  end
36
172
 
37
173
  def find_first(node_type)
38
174
  return self if type == node_type
39
175
 
176
+ return nil unless content
177
+
40
178
  content.each do |child|
41
179
  result = child.find_first(node_type)
42
180
  return result if result
@@ -44,34 +182,45 @@ module Prosereflect
44
182
  nil
45
183
  end
46
184
 
47
- # Create a node of the specified type with optional attributes
48
- def self.create(node_type, attrs = nil)
49
- node = new({ 'type' => node_type })
50
- node.instance_variable_set(:@attrs, attrs) if attrs
51
- node.instance_variable_set(:@content, [])
52
- node
53
- end
54
-
55
185
  def find_all(node_type)
56
186
  results = []
57
187
  results << self if type == node_type
58
- content.each do |child|
59
- results.concat(child.find_all(node_type))
188
+
189
+ content&.each do |child|
190
+ child_results = child.find_all(node_type)
191
+ results.concat(child_results) if child_results
60
192
  end
193
+
61
194
  results
62
195
  end
63
196
 
64
197
  def find_children(node_type)
65
- content.select { |child| child.type == node_type }
198
+ return [] unless content
199
+
200
+ content.select { |child| child.is_a?(node_type) }
66
201
  end
67
202
 
68
203
  def text_content
204
+ return '' unless content
205
+
69
206
  content.map(&:text_content).join
70
207
  end
71
208
 
72
- # Hard breaks should add a newline in text content
73
- def text_content_with_breaks
74
- content.map(&:text_content_with_breaks).join
209
+ # Ensures YAML serialization outputs plain data instead of a Ruby object
210
+ def to_yaml(*args)
211
+ to_h.to_yaml(*args)
212
+ end
213
+
214
+ private
215
+
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
221
+ else
222
+ attrs
223
+ end
75
224
  end
76
225
  end
77
226
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'node'
4
+ require_relative 'list_item'
5
+
6
+ module Prosereflect
7
+ # OrderedList class represents a numbered list in ProseMirror.
8
+ class OrderedList < Node
9
+ PM_TYPE = 'ordered_list'
10
+
11
+ attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
12
+ attribute :start, :integer
13
+ attribute :attrs, :hash
14
+
15
+ key_value do
16
+ map 'type', to: :type, render_default: true
17
+ map 'attrs', to: :attrs
18
+ map 'content', to: :content
19
+ end
20
+
21
+ def initialize(attributes = {})
22
+ attributes[:content] ||= []
23
+ super
24
+ end
25
+
26
+ def self.create(attrs = nil)
27
+ new(attrs: attrs)
28
+ end
29
+
30
+ def start=(value)
31
+ @start = value
32
+ self.attrs ||= {}
33
+ attrs['start'] = value
34
+ end
35
+
36
+ def start
37
+ @start || attrs&.[]('start') || 1
38
+ end
39
+
40
+ def add_item(text)
41
+ item = ListItem.new
42
+ item.add_paragraph(text)
43
+ add_child(item)
44
+ item
45
+ end
46
+
47
+ def items
48
+ return [] unless content
49
+
50
+ content
51
+ end
52
+
53
+ # Add multiple items at once
54
+ def add_items(items_content)
55
+ items_content.each do |item_content|
56
+ add_item(item_content)
57
+ end
58
+ end
59
+
60
+ # Get item at specific position
61
+ def item_at(index)
62
+ return nil if index.negative?
63
+
64
+ items[index]
65
+ end
66
+
67
+ # Update the order (1 for numerical, 'a' for alphabetical, etc.)
68
+ def order=(order_value)
69
+ self.attrs ||= {}
70
+ attrs['order'] = order_value
71
+ end
72
+
73
+ # Get the order value
74
+ def order
75
+ attrs&.[]('order') || 1
76
+ end
77
+
78
+ # Get text content with proper formatting
79
+ def text_content
80
+ return '' unless content
81
+
82
+ content.map { |item| item.respond_to?(:text_content) ? item.text_content : '' }.join("\n")
83
+ end
84
+ end
85
+ end