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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +23 -4
- data/README.adoc +193 -12
- data/lib/prosereflect/attribute/base.rb +34 -0
- data/lib/prosereflect/attribute/bold.rb +20 -0
- data/lib/prosereflect/attribute/href.rb +24 -0
- data/lib/prosereflect/attribute/id.rb +24 -0
- data/lib/prosereflect/attribute.rb +13 -0
- data/lib/prosereflect/blockquote.rb +85 -0
- data/lib/prosereflect/bullet_list.rb +83 -0
- data/lib/prosereflect/code_block.rb +135 -0
- data/lib/prosereflect/code_block_wrapper.rb +66 -0
- data/lib/prosereflect/document.rb +99 -24
- data/lib/prosereflect/hard_break.rb +11 -9
- data/lib/prosereflect/heading.rb +64 -0
- data/lib/prosereflect/horizontal_rule.rb +70 -0
- data/lib/prosereflect/image.rb +126 -0
- data/lib/prosereflect/input/html.rb +505 -0
- data/lib/prosereflect/list_item.rb +65 -0
- data/lib/prosereflect/mark/base.rb +49 -0
- data/lib/prosereflect/mark/bold.rb +15 -0
- data/lib/prosereflect/mark/code.rb +14 -0
- data/lib/prosereflect/mark/italic.rb +15 -0
- data/lib/prosereflect/mark/link.rb +18 -0
- data/lib/prosereflect/mark/strike.rb +15 -0
- data/lib/prosereflect/mark/subscript.rb +15 -0
- data/lib/prosereflect/mark/superscript.rb +15 -0
- data/lib/prosereflect/mark/underline.rb +15 -0
- data/lib/prosereflect/mark.rb +11 -0
- data/lib/prosereflect/node.rb +181 -32
- data/lib/prosereflect/ordered_list.rb +85 -0
- data/lib/prosereflect/output/html.rb +374 -0
- data/lib/prosereflect/paragraph.rb +26 -15
- data/lib/prosereflect/parser.rb +111 -24
- data/lib/prosereflect/table.rb +40 -9
- data/lib/prosereflect/table_cell.rb +33 -8
- data/lib/prosereflect/table_header.rb +92 -0
- data/lib/prosereflect/table_row.rb +31 -8
- data/lib/prosereflect/text.rb +13 -17
- data/lib/prosereflect/user.rb +63 -0
- data/lib/prosereflect/version.rb +1 -1
- data/lib/prosereflect.rb +6 -0
- data/prosereflect.gemspec +1 -0
- data/spec/prosereflect/document_spec.rb +436 -36
- data/spec/prosereflect/hard_break_spec.rb +218 -22
- data/spec/prosereflect/input/html_spec.rb +797 -0
- data/spec/prosereflect/node_spec.rb +258 -89
- data/spec/prosereflect/output/html_spec.rb +369 -0
- data/spec/prosereflect/paragraph_spec.rb +424 -49
- data/spec/prosereflect/parser_spec.rb +304 -91
- data/spec/prosereflect/table_cell_spec.rb +268 -57
- data/spec/prosereflect/table_row_spec.rb +210 -40
- data/spec/prosereflect/table_spec.rb +392 -61
- data/spec/prosereflect/text_spec.rb +206 -48
- data/spec/prosereflect/user_spec.rb +73 -0
- data/spec/prosereflect_spec.rb +5 -0
- data/spec/support/shared_examples.rb +44 -15
- metadata +47 -3
- data/debug_loading.rb +0 -34
data/lib/prosereflect/node.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
16
|
-
|
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
|
-
|
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
|
-
#
|
62
|
+
# Convert to hash for serialization
|
22
63
|
def to_h
|
23
|
-
result = { 'type' =>
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
33
|
-
|
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
|
-
|
59
|
-
|
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
|
-
|
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
|
-
#
|
73
|
-
def
|
74
|
-
|
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
|