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
@@ -0,0 +1,374 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require_relative '../document'
|
5
|
+
|
6
|
+
module Prosereflect
|
7
|
+
module Output
|
8
|
+
class Html
|
9
|
+
class << self
|
10
|
+
# Convert a Prosereflect::Document to HTML
|
11
|
+
def convert(document)
|
12
|
+
builder = Nokogiri::HTML::Builder.new do |doc|
|
13
|
+
doc.div do
|
14
|
+
process_node(document, doc)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
doc = Nokogiri::HTML(builder.to_html)
|
19
|
+
html = doc.at_css('div').children.to_html
|
20
|
+
|
21
|
+
code_blocks = {}
|
22
|
+
html.scan(%r{<code[^>]*>(.*?)</code>}m).each_with_index do |match, i|
|
23
|
+
code_content = match[0]
|
24
|
+
placeholder = "CODE_BLOCK_#{i}"
|
25
|
+
code_blocks[placeholder] = code_content
|
26
|
+
html.sub!(code_content, placeholder)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Remove newlines and spaces
|
30
|
+
html = html.gsub(/\n\s*/, '')
|
31
|
+
|
32
|
+
code_blocks.each do |placeholder, content|
|
33
|
+
html.sub!(placeholder, content)
|
34
|
+
end
|
35
|
+
|
36
|
+
html
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Process a node and its children
|
42
|
+
def process_node(node, builder)
|
43
|
+
return unless node
|
44
|
+
|
45
|
+
case node.type
|
46
|
+
when 'doc'
|
47
|
+
process_document(node, builder)
|
48
|
+
when 'paragraph'
|
49
|
+
process_paragraph(node, builder)
|
50
|
+
when 'heading'
|
51
|
+
process_heading(node, builder)
|
52
|
+
when 'text'
|
53
|
+
process_text(node, builder)
|
54
|
+
when 'table'
|
55
|
+
process_table(node, builder)
|
56
|
+
when 'table_row'
|
57
|
+
process_table_row(node, builder)
|
58
|
+
when 'table_cell'
|
59
|
+
process_table_cell(node, builder)
|
60
|
+
when 'table_header'
|
61
|
+
process_table_header(node, builder)
|
62
|
+
when 'hard_break'
|
63
|
+
builder.br
|
64
|
+
when 'image'
|
65
|
+
process_image(node, builder)
|
66
|
+
when 'user'
|
67
|
+
process_user(node, builder)
|
68
|
+
when 'bullet_list'
|
69
|
+
process_bullet_list(node, builder)
|
70
|
+
when 'ordered_list'
|
71
|
+
process_ordered_list(node, builder)
|
72
|
+
when 'list_item'
|
73
|
+
process_list_item(node, builder)
|
74
|
+
when 'blockquote'
|
75
|
+
process_blockquote(node, builder)
|
76
|
+
when 'horizontal_rule'
|
77
|
+
process_horizontal_rule(node, builder)
|
78
|
+
when 'code_block_wrapper'
|
79
|
+
process_code_block_wrapper(node, builder)
|
80
|
+
when 'code_block'
|
81
|
+
process_code_block(node, builder)
|
82
|
+
else
|
83
|
+
# Default handling for unknown nodes - treat as a container
|
84
|
+
process_children(node, builder)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Process the document node
|
89
|
+
def process_document(node, builder)
|
90
|
+
process_children(node, builder)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Process a paragraph node
|
94
|
+
def process_paragraph(node, builder)
|
95
|
+
builder.p do
|
96
|
+
process_children(node, builder)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Process a heading node
|
101
|
+
def process_heading(node, builder)
|
102
|
+
level = node.level || 1
|
103
|
+
builder.send("h#{level}") do
|
104
|
+
process_children(node, builder)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Process a text node, applying marks
|
109
|
+
def process_text(node, builder)
|
110
|
+
return unless node.text
|
111
|
+
|
112
|
+
if node.marks && !node.marks.empty?
|
113
|
+
apply_marks(node.text, node.marks, builder)
|
114
|
+
else
|
115
|
+
builder.text node.text
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Apply marks to text
|
120
|
+
def apply_marks(text, marks, builder)
|
121
|
+
return builder.text(text) if marks.empty?
|
122
|
+
|
123
|
+
current_mark = marks.first
|
124
|
+
remaining_marks = marks[1..]
|
125
|
+
|
126
|
+
mark_type = if current_mark.is_a?(Hash)
|
127
|
+
current_mark['type']
|
128
|
+
elsif current_mark.respond_to?(:type)
|
129
|
+
current_mark.type
|
130
|
+
else
|
131
|
+
'unknown'
|
132
|
+
end
|
133
|
+
|
134
|
+
case mark_type
|
135
|
+
when 'bold'
|
136
|
+
builder.strong do
|
137
|
+
apply_marks(text, remaining_marks, builder)
|
138
|
+
end
|
139
|
+
when 'italic'
|
140
|
+
builder.em do
|
141
|
+
apply_marks(text, remaining_marks, builder)
|
142
|
+
end
|
143
|
+
when 'code'
|
144
|
+
builder.code do
|
145
|
+
apply_marks(text, remaining_marks, builder)
|
146
|
+
end
|
147
|
+
when 'link'
|
148
|
+
href = find_href_attribute(current_mark)
|
149
|
+
if href
|
150
|
+
builder.a(href: href) do
|
151
|
+
apply_marks(text, remaining_marks, builder)
|
152
|
+
end
|
153
|
+
else
|
154
|
+
apply_marks(text, remaining_marks, builder)
|
155
|
+
end
|
156
|
+
when 'strike'
|
157
|
+
builder.del do
|
158
|
+
apply_marks(text, remaining_marks, builder)
|
159
|
+
end
|
160
|
+
when 'underline'
|
161
|
+
builder.u do
|
162
|
+
apply_marks(text, remaining_marks, builder)
|
163
|
+
end
|
164
|
+
when 'subscript'
|
165
|
+
builder.sub do
|
166
|
+
apply_marks(text, remaining_marks, builder)
|
167
|
+
end
|
168
|
+
when 'superscript'
|
169
|
+
builder.sup do
|
170
|
+
apply_marks(text, remaining_marks, builder)
|
171
|
+
end
|
172
|
+
else
|
173
|
+
# Unknown mark, just process inner content
|
174
|
+
apply_marks(text, remaining_marks, builder)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Find href attribute in a link mark
|
179
|
+
def find_href_attribute(mark)
|
180
|
+
if mark.is_a?(Hash)
|
181
|
+
if mark['attrs'].is_a?(Hash)
|
182
|
+
return mark['attrs']['href']
|
183
|
+
elsif mark['attrs'].is_a?(Array)
|
184
|
+
href_attr = mark['attrs'].find { |a| a.is_a?(Prosereflect::Attribute::Href) || (a.is_a?(Hash) && a['type'] == 'href') }
|
185
|
+
return href_attr['href'] if href_attr.is_a?(Hash) && href_attr['href']
|
186
|
+
return href_attr.href if href_attr.respond_to?(:href)
|
187
|
+
end
|
188
|
+
elsif mark.respond_to?(:attrs)
|
189
|
+
attrs = mark.attrs
|
190
|
+
if attrs.is_a?(Hash)
|
191
|
+
return attrs['href']
|
192
|
+
elsif attrs.is_a?(Array)
|
193
|
+
href_attr = attrs.find { |attr| attr.is_a?(Prosereflect::Attribute::Href) }
|
194
|
+
return href_attr&.href if href_attr
|
195
|
+
|
196
|
+
hash_attr = attrs.find { |attr| attr.is_a?(Hash) && attr['href'] }
|
197
|
+
return hash_attr['href'] if hash_attr
|
198
|
+
end
|
199
|
+
end
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
203
|
+
# Process a table node
|
204
|
+
def process_table(node, builder)
|
205
|
+
builder.table do
|
206
|
+
rows = node.rows || node.content
|
207
|
+
return if rows.empty?
|
208
|
+
|
209
|
+
has_header = rows.first&.content&.any? { |cell| cell.type == 'table_header' }
|
210
|
+
|
211
|
+
if has_header
|
212
|
+
builder.thead do
|
213
|
+
process_node(rows.first, builder)
|
214
|
+
end
|
215
|
+
rows = rows[1..]
|
216
|
+
end
|
217
|
+
|
218
|
+
builder.tbody do
|
219
|
+
rows.each do |row|
|
220
|
+
process_node(row, builder)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# Process a table row
|
227
|
+
def process_table_row(node, builder)
|
228
|
+
builder.tr do
|
229
|
+
process_children(node, builder)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process a table cell
|
234
|
+
def process_table_cell(node, builder)
|
235
|
+
builder.td do
|
236
|
+
if node.content&.size == 1 && node.content.first.type == 'paragraph'
|
237
|
+
node.content.first.content&.each do |child|
|
238
|
+
process_node(child, builder)
|
239
|
+
end
|
240
|
+
else
|
241
|
+
process_children(node, builder)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
# Process a table header cell
|
247
|
+
def process_table_header(node, builder)
|
248
|
+
attrs = {}
|
249
|
+
attrs[:scope] = node.scope if node.scope
|
250
|
+
attrs[:abbr] = node.abbr if node.abbr
|
251
|
+
attrs[:colspan] = node.colspan if node.colspan
|
252
|
+
|
253
|
+
builder.th(attrs) do
|
254
|
+
if node.content&.size == 1 && node.content.first.type == 'paragraph'
|
255
|
+
node.content.first.content&.each do |child|
|
256
|
+
process_node(child, builder)
|
257
|
+
end
|
258
|
+
else
|
259
|
+
process_children(node, builder)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Process an image node
|
265
|
+
def process_image(node, builder)
|
266
|
+
attrs = {
|
267
|
+
src: node.src,
|
268
|
+
alt: node.alt
|
269
|
+
}
|
270
|
+
attrs[:title] = node.title if node.title
|
271
|
+
attrs[:width] = node.width if node.width
|
272
|
+
attrs[:height] = node.height if node.height
|
273
|
+
|
274
|
+
builder.img(attrs)
|
275
|
+
end
|
276
|
+
|
277
|
+
# Process a user mention node
|
278
|
+
def process_user(node, builder)
|
279
|
+
builder << "<user-mention data-id=\"#{node.id}\"></user-mention>"
|
280
|
+
end
|
281
|
+
|
282
|
+
# Process a bullet list node
|
283
|
+
def process_bullet_list(node, builder)
|
284
|
+
builder.ul do
|
285
|
+
node.content&.each do |child|
|
286
|
+
if child.type == 'list_item'
|
287
|
+
process_node(child, builder)
|
288
|
+
else
|
289
|
+
builder.li do
|
290
|
+
process_node(child, builder)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Process an ordered list node
|
298
|
+
def process_ordered_list(node, builder)
|
299
|
+
attrs = {}
|
300
|
+
attrs[:start] = node.start if node.start && node.start != 1
|
301
|
+
|
302
|
+
builder.ol(attrs) do
|
303
|
+
process_children(node, builder)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# Process a list item node
|
308
|
+
def process_list_item(node, builder)
|
309
|
+
builder.li do
|
310
|
+
process_children(node, builder)
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Process a blockquote node
|
315
|
+
def process_blockquote(node, builder)
|
316
|
+
attrs = {}
|
317
|
+
attrs[:cite] = node.citation if node.citation
|
318
|
+
|
319
|
+
builder.blockquote(attrs) do
|
320
|
+
node.blocks&.each do |block|
|
321
|
+
process_node(block, builder)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
# Process a horizontal rule node
|
327
|
+
def process_horizontal_rule(node, builder)
|
328
|
+
attrs = {}
|
329
|
+
attrs[:style] = []
|
330
|
+
attrs[:style] << "border-style: #{node.style}" if node.style
|
331
|
+
attrs[:style] << "width: #{node.width}" if node.width
|
332
|
+
attrs[:style] << "border-width: #{node.thickness}px" if node.thickness
|
333
|
+
attrs[:style] = attrs[:style].join('; ') unless attrs[:style].empty?
|
334
|
+
|
335
|
+
builder.hr(attrs)
|
336
|
+
end
|
337
|
+
|
338
|
+
# Process a code block wrapper node
|
339
|
+
def process_code_block_wrapper(node, builder)
|
340
|
+
attrs = {}
|
341
|
+
if node.attrs
|
342
|
+
attrs['data-line-numbers'] = 'true' if node.attrs['line_numbers']
|
343
|
+
if node.attrs['highlight_lines'].is_a?(Array) && !node.attrs['highlight_lines'].empty? && node.attrs['highlight_lines'] != [0]
|
344
|
+
attrs['data-highlight-lines'] = node.attrs['highlight_lines'].join(',')
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
builder.pre(attrs) do
|
349
|
+
process_children(node, builder)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Process a code block node
|
354
|
+
def process_code_block(node, builder)
|
355
|
+
attrs = {}
|
356
|
+
attrs['class'] = "language-#{node.language}" if node.language
|
357
|
+
|
358
|
+
builder.code(attrs) do
|
359
|
+
builder.text node.content
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Process all children of a node
|
364
|
+
def process_children(node, builder)
|
365
|
+
return unless node.content
|
366
|
+
|
367
|
+
node.content.each do |child|
|
368
|
+
process_node(child, builder)
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
@@ -6,31 +6,42 @@ require_relative 'hard_break'
|
|
6
6
|
|
7
7
|
module Prosereflect
|
8
8
|
class Paragraph < Node
|
9
|
+
PM_TYPE = 'paragraph'
|
10
|
+
|
11
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
12
|
+
|
13
|
+
key_value do
|
14
|
+
map 'type', to: :type, render_default: true
|
15
|
+
map 'content', to: :content
|
16
|
+
map 'attrs', to: :attrs
|
17
|
+
map 'marks', to: :marks
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(params = {})
|
21
|
+
super
|
22
|
+
self.content ||= []
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.create(attrs = nil)
|
26
|
+
new(attrs: attrs)
|
27
|
+
end
|
28
|
+
|
9
29
|
def text_nodes
|
10
|
-
|
30
|
+
return [] unless content
|
31
|
+
|
32
|
+
content.select { |node| node.is_a?(Text) }
|
11
33
|
end
|
12
34
|
|
13
35
|
def text_content
|
36
|
+
return '' unless content
|
37
|
+
|
14
38
|
result = ''
|
15
39
|
content.each do |node|
|
16
|
-
result +=
|
17
|
-
node.text_content
|
18
|
-
elsif node.type == 'hard_break'
|
19
|
-
"\n"
|
20
|
-
else
|
21
|
-
node.text_content
|
22
|
-
end
|
40
|
+
result += node.text_content
|
23
41
|
end
|
24
42
|
result
|
25
43
|
end
|
26
44
|
|
27
|
-
# Create a new paragraph
|
28
|
-
def self.create(attrs = nil)
|
29
|
-
para = new({ 'type' => 'paragraph', 'content' => [] })
|
30
|
-
para.instance_variable_set(:@attrs, attrs) if attrs
|
31
|
-
para
|
32
|
-
end
|
33
|
-
|
34
45
|
# Add text to the paragraph
|
35
46
|
def add_text(text, marks = nil)
|
36
47
|
return if text.nil? || text.empty?
|
data/lib/prosereflect/parser.rb
CHANGED
@@ -6,8 +6,21 @@ require_relative 'paragraph'
|
|
6
6
|
require_relative 'table'
|
7
7
|
require_relative 'table_row'
|
8
8
|
require_relative 'table_cell'
|
9
|
+
require_relative 'table_header'
|
9
10
|
require_relative 'hard_break'
|
10
11
|
require_relative 'document'
|
12
|
+
require_relative 'heading'
|
13
|
+
require_relative 'mark/bold'
|
14
|
+
require_relative 'mark/italic'
|
15
|
+
require_relative 'mark/code'
|
16
|
+
require_relative 'mark/link'
|
17
|
+
require_relative 'ordered_list'
|
18
|
+
require_relative 'bullet_list'
|
19
|
+
require_relative 'list_item'
|
20
|
+
require_relative 'blockquote'
|
21
|
+
require_relative 'horizontal_rule'
|
22
|
+
require_relative 'image'
|
23
|
+
require_relative 'user'
|
11
24
|
|
12
25
|
module Prosereflect
|
13
26
|
class Parser
|
@@ -20,37 +33,111 @@ module Prosereflect
|
|
20
33
|
def self.parse_node(data)
|
21
34
|
return nil unless data.is_a?(Hash)
|
22
35
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
36
|
+
type = data['type']
|
37
|
+
text = data['text']
|
38
|
+
attrs = data['attrs']
|
39
|
+
marks_data = data['marks']
|
40
|
+
|
41
|
+
# Find the right class based on type
|
42
|
+
node_class = case type
|
43
|
+
when 'doc'
|
44
|
+
Document
|
45
|
+
when 'paragraph'
|
46
|
+
Paragraph
|
47
|
+
when 'text'
|
48
|
+
Text
|
49
|
+
when 'table'
|
50
|
+
Table
|
51
|
+
when 'table_row'
|
52
|
+
TableRow
|
53
|
+
when 'table_cell'
|
54
|
+
TableCell
|
55
|
+
when 'table_header'
|
56
|
+
TableHeader
|
57
|
+
when 'hard_break'
|
58
|
+
HardBreak
|
59
|
+
when 'heading'
|
60
|
+
Heading
|
61
|
+
when 'ordered_list'
|
62
|
+
OrderedList
|
63
|
+
when 'bullet_list'
|
64
|
+
BulletList
|
65
|
+
when 'list_item'
|
66
|
+
ListItem
|
67
|
+
when 'blockquote'
|
68
|
+
Blockquote
|
69
|
+
when 'horizontal_rule'
|
70
|
+
HorizontalRule
|
71
|
+
when 'image'
|
72
|
+
Image
|
73
|
+
when 'user'
|
74
|
+
User
|
75
|
+
else
|
76
|
+
Node
|
77
|
+
end
|
78
|
+
|
79
|
+
if type == 'text'
|
80
|
+
node = Text.new(text: text)
|
38
81
|
else
|
39
|
-
|
82
|
+
node = node_class.create(attrs)
|
83
|
+
|
84
|
+
# Process content recursively
|
85
|
+
if data['content'].is_a?(Array)
|
86
|
+
data['content'].each do |content_data|
|
87
|
+
child_node = parse_node(content_data)
|
88
|
+
node.add_child(child_node) if child_node
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Handle special attributes for specific node types
|
94
|
+
case type
|
95
|
+
when 'ordered_list'
|
96
|
+
node.start = attrs['start'].to_i if attrs && attrs['start']
|
97
|
+
when 'bullet_list'
|
98
|
+
node.bullet_style = attrs['bullet_style'] if attrs && attrs['bullet_style']
|
99
|
+
when 'blockquote'
|
100
|
+
node.citation = attrs['cite'] if attrs && attrs['cite']
|
101
|
+
when 'horizontal_rule'
|
102
|
+
if attrs
|
103
|
+
node.style = attrs['border_style'] if attrs['border_style']
|
104
|
+
node.width = attrs['width'] if attrs['width']
|
105
|
+
node.thickness = attrs['thickness'].to_i if attrs['thickness']
|
106
|
+
end
|
107
|
+
when 'image'
|
108
|
+
if attrs
|
109
|
+
node.src = attrs['src'] if attrs['src']
|
110
|
+
node.alt = attrs['alt'] if attrs['alt']
|
111
|
+
node.title = attrs['title'] if attrs['title']
|
112
|
+
node.dimensions = [attrs['width']&.to_i, attrs['height']&.to_i]
|
113
|
+
end
|
114
|
+
when 'table_header'
|
115
|
+
if attrs
|
116
|
+
node.scope = attrs['scope'] if attrs['scope']
|
117
|
+
node.abbr = attrs['abbr'] if attrs['abbr']
|
118
|
+
node.colspan = attrs['colspan'] if attrs['colspan']
|
119
|
+
end
|
40
120
|
end
|
121
|
+
|
122
|
+
node.marks = marks_data if marks_data && !marks_data.empty?
|
123
|
+
|
124
|
+
node
|
41
125
|
end
|
42
126
|
|
43
127
|
def self.parse_document(data)
|
44
|
-
raise ArgumentError, 'Input
|
45
|
-
raise ArgumentError,
|
128
|
+
raise ArgumentError, 'Input cannot be nil' if data.nil?
|
129
|
+
raise ArgumentError, "Input must be a hash, got #{data.class}" unless data.is_a?(Hash)
|
46
130
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
131
|
+
document = parse_node(data)
|
132
|
+
|
133
|
+
unless document.is_a?(Document)
|
134
|
+
# If the result isn't a Document, create one and add the node as content
|
135
|
+
doc = Document.create
|
136
|
+
doc.add_child(document) if document
|
137
|
+
document = doc
|
53
138
|
end
|
139
|
+
|
140
|
+
document
|
54
141
|
end
|
55
142
|
end
|
56
143
|
end
|
data/lib/prosereflect/table.rb
CHANGED
@@ -2,11 +2,36 @@
|
|
2
2
|
|
3
3
|
require_relative 'node'
|
4
4
|
require_relative 'table_row'
|
5
|
+
require_relative 'table_header'
|
5
6
|
|
6
7
|
module Prosereflect
|
8
|
+
# TODO: support for table attributes
|
9
|
+
# Table class represents a ProseMirror table.
|
10
|
+
# It contains rows, each of which can contain cells.
|
7
11
|
class Table < Node
|
12
|
+
PM_TYPE = 'table'
|
13
|
+
|
14
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
15
|
+
|
16
|
+
key_value do
|
17
|
+
map 'type', to: :type, render_default: true
|
18
|
+
map 'content', to: :content
|
19
|
+
map 'attrs', to: :attrs
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(attributes = {})
|
23
|
+
attributes[:content] ||= []
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.create(attrs = nil)
|
28
|
+
new(type: PM_TYPE, attrs: attrs, content: [])
|
29
|
+
end
|
30
|
+
|
8
31
|
def rows
|
9
|
-
|
32
|
+
return [] unless content
|
33
|
+
|
34
|
+
content
|
10
35
|
end
|
11
36
|
|
12
37
|
def header_row
|
@@ -27,18 +52,13 @@ module Prosereflect
|
|
27
52
|
data_row.cells[col_index]
|
28
53
|
end
|
29
54
|
|
30
|
-
# Create a new table
|
31
|
-
def self.create(attrs = nil)
|
32
|
-
table = new({ 'type' => 'table', 'content' => [] })
|
33
|
-
table.instance_variable_set(:@attrs, attrs) if attrs
|
34
|
-
table
|
35
|
-
end
|
36
|
-
|
37
55
|
# Add a header row to the table
|
38
56
|
def add_header(header_cells)
|
39
57
|
row = TableRow.create
|
40
58
|
header_cells.each do |cell_content|
|
41
|
-
|
59
|
+
header = TableHeader.create
|
60
|
+
header.add_paragraph(cell_content)
|
61
|
+
row.add_child(header)
|
42
62
|
end
|
43
63
|
add_child(row)
|
44
64
|
row
|
@@ -60,5 +80,16 @@ module Prosereflect
|
|
60
80
|
add_row(row_data)
|
61
81
|
end
|
62
82
|
end
|
83
|
+
|
84
|
+
# Override to_h to handle empty content and attributes properly
|
85
|
+
def to_h
|
86
|
+
result = super
|
87
|
+
result['content'] ||= []
|
88
|
+
if result['attrs']
|
89
|
+
result['attrs'] = result['attrs'].is_a?(Hash) && result['attrs'][:attrs] ? result['attrs'][:attrs] : result['attrs']
|
90
|
+
result.delete('attrs') if result['attrs'].empty?
|
91
|
+
end
|
92
|
+
result
|
93
|
+
end
|
63
94
|
end
|
64
95
|
end
|