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,505 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
require_relative '../document'
|
5
|
+
require_relative '../paragraph'
|
6
|
+
require_relative '../text'
|
7
|
+
require_relative '../table'
|
8
|
+
require_relative '../table_row'
|
9
|
+
require_relative '../table_cell'
|
10
|
+
require_relative '../table_header'
|
11
|
+
require_relative '../hard_break'
|
12
|
+
require_relative '../mark/bold'
|
13
|
+
require_relative '../mark/italic'
|
14
|
+
require_relative '../mark/code'
|
15
|
+
require_relative '../mark/link'
|
16
|
+
require_relative '../mark/strike'
|
17
|
+
require_relative '../mark/subscript'
|
18
|
+
require_relative '../mark/superscript'
|
19
|
+
require_relative '../mark/underline'
|
20
|
+
require_relative '../attribute/href'
|
21
|
+
require_relative '../ordered_list'
|
22
|
+
require_relative '../bullet_list'
|
23
|
+
require_relative '../list_item'
|
24
|
+
require_relative '../blockquote'
|
25
|
+
require_relative '../horizontal_rule'
|
26
|
+
require_relative '../image'
|
27
|
+
require_relative '../code_block_wrapper'
|
28
|
+
require_relative '../code_block'
|
29
|
+
require_relative '../heading'
|
30
|
+
require_relative '../user'
|
31
|
+
|
32
|
+
module Prosereflect
|
33
|
+
module Input
|
34
|
+
class Html
|
35
|
+
class << self
|
36
|
+
# Parse HTML content and return a Prosereflect::Document
|
37
|
+
def parse(html)
|
38
|
+
html_doc = Nokogiri::HTML(html)
|
39
|
+
document = Document.create # Use create instead of new to initialize content array
|
40
|
+
|
41
|
+
content_node = html_doc.at_css('body') || html_doc.root
|
42
|
+
|
43
|
+
# Process all child nodes
|
44
|
+
process_node_children(content_node, document)
|
45
|
+
|
46
|
+
document
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Process children of a node and add to parent
|
52
|
+
def process_node_children(html_node, parent_node)
|
53
|
+
return unless html_node&.children
|
54
|
+
|
55
|
+
html_node.children.each do |child|
|
56
|
+
node = convert_node(child)
|
57
|
+
|
58
|
+
if node.is_a?(Array)
|
59
|
+
node.each { |n| parent_node.add_child(n) }
|
60
|
+
elsif node
|
61
|
+
parent_node.add_child(node)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Convert an HTML node to a ProseMirror node
|
67
|
+
def convert_node(html_node)
|
68
|
+
return nil if html_node.comment? || html_node.text? && html_node.text.strip.empty?
|
69
|
+
|
70
|
+
case html_node.name
|
71
|
+
when 'text', '#text'
|
72
|
+
create_text_node(html_node)
|
73
|
+
when 'p'
|
74
|
+
create_paragraph_node(html_node)
|
75
|
+
when /^h([1-6])$/
|
76
|
+
create_heading_node(html_node, Regexp.last_match(1).to_i)
|
77
|
+
when 'br'
|
78
|
+
HardBreak.new
|
79
|
+
when 'table'
|
80
|
+
create_table_node(html_node)
|
81
|
+
when 'tr'
|
82
|
+
create_table_row_node(html_node)
|
83
|
+
when 'th', 'td'
|
84
|
+
create_table_cell_node(html_node)
|
85
|
+
when 'ol'
|
86
|
+
create_ordered_list_node(html_node)
|
87
|
+
when 'ul'
|
88
|
+
create_bullet_list_node(html_node)
|
89
|
+
when 'li'
|
90
|
+
create_list_item_node(html_node)
|
91
|
+
when 'blockquote'
|
92
|
+
create_blockquote_node(html_node)
|
93
|
+
when 'hr'
|
94
|
+
create_horizontal_rule_node(html_node)
|
95
|
+
when 'img'
|
96
|
+
create_image_node(html_node)
|
97
|
+
when 'user-mention'
|
98
|
+
create_user_node(html_node)
|
99
|
+
when 'div', 'span'
|
100
|
+
# For containers, we process their children
|
101
|
+
handle_container_node(html_node)
|
102
|
+
when 'pre'
|
103
|
+
create_code_block_wrapper(html_node)
|
104
|
+
when 'strong', 'b', 'em', 'i', 'code', 'a', 'strike', 's', 'del', 'sub', 'sup', 'u'
|
105
|
+
# For inline elements with text styling, we handle differently
|
106
|
+
handle_styled_text(html_node)
|
107
|
+
else
|
108
|
+
# Default handling for unknown elements - try to extract content
|
109
|
+
handle_container_node(html_node)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Create a text node from HTML text
|
114
|
+
def create_text_node(html_node)
|
115
|
+
Text.new(text: html_node.text)
|
116
|
+
end
|
117
|
+
|
118
|
+
# Create a paragraph node from HTML paragraph
|
119
|
+
def create_paragraph_node(html_node)
|
120
|
+
paragraph = Paragraph.new
|
121
|
+
process_node_children(html_node, paragraph)
|
122
|
+
paragraph
|
123
|
+
end
|
124
|
+
|
125
|
+
# Create a table node from HTML table
|
126
|
+
def create_table_node(html_node)
|
127
|
+
table = Table.new
|
128
|
+
|
129
|
+
thead = html_node.at_css('thead')
|
130
|
+
thead&.css('tr')&.each do |tr|
|
131
|
+
process_table_row(tr, table, true)
|
132
|
+
end
|
133
|
+
|
134
|
+
tbody = html_node.at_css('tbody') || html_node
|
135
|
+
tbody.css('tr').each do |tr|
|
136
|
+
process_table_row(tr, table, false)
|
137
|
+
end
|
138
|
+
|
139
|
+
table
|
140
|
+
end
|
141
|
+
|
142
|
+
# Process a table row
|
143
|
+
def create_table_row_node(html_node)
|
144
|
+
row = TableRow.new
|
145
|
+
html_node.css('th, td').each do |cell|
|
146
|
+
row.add_child(create_table_cell_node(cell))
|
147
|
+
end
|
148
|
+
row
|
149
|
+
end
|
150
|
+
|
151
|
+
# Add a row to a table
|
152
|
+
def process_table_row(tr_node, table, _is_header)
|
153
|
+
row = create_table_row_node(tr_node)
|
154
|
+
table.add_child(row)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Create a table cell node from HTML cell
|
158
|
+
def create_table_cell_node(html_node)
|
159
|
+
# Create either a TableHeader or TableCell based on the tag name
|
160
|
+
cell = if html_node.name == 'th'
|
161
|
+
header = TableHeader.create
|
162
|
+
|
163
|
+
# Handle header-specific attributes
|
164
|
+
header.scope = html_node['scope'] if html_node['scope']
|
165
|
+
header.abbr = html_node['abbr'] if html_node['abbr']
|
166
|
+
header.colspan = html_node['colspan'] if html_node['colspan']
|
167
|
+
|
168
|
+
header
|
169
|
+
else
|
170
|
+
TableCell.create
|
171
|
+
end
|
172
|
+
|
173
|
+
if contains_only_text_or_inline(html_node)
|
174
|
+
paragraph = Paragraph.new
|
175
|
+
process_node_children(html_node, paragraph)
|
176
|
+
cell.add_child(paragraph)
|
177
|
+
else
|
178
|
+
process_node_children(html_node, cell)
|
179
|
+
end
|
180
|
+
|
181
|
+
cell
|
182
|
+
end
|
183
|
+
|
184
|
+
# Handle a container-like node (div, span, etc.)
|
185
|
+
def handle_container_node(html_node)
|
186
|
+
# For top-level divs, process children directly
|
187
|
+
if html_node.name == 'div'
|
188
|
+
results = []
|
189
|
+
html_node.children.each do |child|
|
190
|
+
next if child.text? && child.text.strip.empty?
|
191
|
+
|
192
|
+
node = convert_node(child)
|
193
|
+
if node.is_a?(Array)
|
194
|
+
results.concat(node)
|
195
|
+
elsif node
|
196
|
+
results << node
|
197
|
+
end
|
198
|
+
end
|
199
|
+
return results if results.any?
|
200
|
+
end
|
201
|
+
|
202
|
+
if contains_only_text_or_inline(html_node)
|
203
|
+
paragraph = Paragraph.new
|
204
|
+
process_node_children(html_node, paragraph)
|
205
|
+
return paragraph
|
206
|
+
end
|
207
|
+
|
208
|
+
children = []
|
209
|
+
html_node.children.each do |child|
|
210
|
+
node = convert_node(child)
|
211
|
+
next unless node
|
212
|
+
|
213
|
+
if node.is_a?(Array)
|
214
|
+
children.concat(node)
|
215
|
+
else
|
216
|
+
children << node
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
children
|
221
|
+
end
|
222
|
+
|
223
|
+
# Handle styled text (bold, italic, etc.)
|
224
|
+
def handle_styled_text(html_node)
|
225
|
+
# Create mark based on the current node
|
226
|
+
mark = case html_node.name
|
227
|
+
when 'strong', 'b'
|
228
|
+
mark = Mark::Bold.new
|
229
|
+
mark.type = 'bold'
|
230
|
+
mark
|
231
|
+
when 'em', 'i'
|
232
|
+
mark = Mark::Italic.new
|
233
|
+
mark.type = 'italic'
|
234
|
+
mark
|
235
|
+
when 'code'
|
236
|
+
mark = Mark::Code.new
|
237
|
+
mark.type = 'code'
|
238
|
+
mark
|
239
|
+
when 'a'
|
240
|
+
mark = Mark::Link.new
|
241
|
+
mark.type = 'link'
|
242
|
+
mark.attrs = { 'href' => html_node['href'] } if html_node['href']
|
243
|
+
mark
|
244
|
+
when 'strike', 's', 'del'
|
245
|
+
mark = Mark::Strike.new
|
246
|
+
mark.type = 'strike'
|
247
|
+
mark
|
248
|
+
when 'sub'
|
249
|
+
mark = Mark::Subscript.new
|
250
|
+
mark.type = 'subscript'
|
251
|
+
mark
|
252
|
+
when 'sup'
|
253
|
+
mark = Mark::Superscript.new
|
254
|
+
mark.type = 'superscript'
|
255
|
+
mark
|
256
|
+
when 'u'
|
257
|
+
mark = Mark::Underline.new
|
258
|
+
mark.type = 'underline'
|
259
|
+
mark
|
260
|
+
end
|
261
|
+
|
262
|
+
return convert_node(html_node.children.first) unless mark
|
263
|
+
|
264
|
+
# If the node has children that are not just text, process them
|
265
|
+
if html_node.children.any? { |child| !child.text? }
|
266
|
+
# Process children and add the current mark to their marks
|
267
|
+
results = []
|
268
|
+
html_node.children.each do |child|
|
269
|
+
node = convert_node(child)
|
270
|
+
next unless node
|
271
|
+
|
272
|
+
if node.is_a?(Array)
|
273
|
+
node.each do |n|
|
274
|
+
n.marks = (n.raw_marks || []) + [mark]
|
275
|
+
results << n
|
276
|
+
end
|
277
|
+
else
|
278
|
+
node.marks = (node.raw_marks || []) + [mark]
|
279
|
+
results << node
|
280
|
+
end
|
281
|
+
end
|
282
|
+
results
|
283
|
+
else
|
284
|
+
# Create a text node with the mark
|
285
|
+
text = Text.new(text: html_node.text)
|
286
|
+
text.marks = [mark]
|
287
|
+
text
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Check if a node contains only text or inline elements
|
292
|
+
def contains_only_text_or_inline(node)
|
293
|
+
node.children.all? do |child|
|
294
|
+
child.text? ||
|
295
|
+
%w[strong b em i code a br span strike s del sub sup u].include?(child.name) ||
|
296
|
+
(child.element? && contains_only_text_or_inline(child))
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Create an ordered list node from HTML ol
|
301
|
+
def create_ordered_list_node(html_node)
|
302
|
+
list = OrderedList.new
|
303
|
+
|
304
|
+
# Handle start attribute
|
305
|
+
start_val = (html_node['start'] || '1').to_i
|
306
|
+
list.start = start_val
|
307
|
+
|
308
|
+
# Process list items
|
309
|
+
html_node.css('> li').each do |li|
|
310
|
+
list.add_child(create_list_item_node(li))
|
311
|
+
end
|
312
|
+
|
313
|
+
list
|
314
|
+
end
|
315
|
+
|
316
|
+
# Create a bullet list node from HTML ul
|
317
|
+
def create_bullet_list_node(html_node)
|
318
|
+
list = BulletList.new
|
319
|
+
list.bullet_style = nil
|
320
|
+
|
321
|
+
# Handle style attribute if present
|
322
|
+
if html_node['style']&.include?('list-style-type')
|
323
|
+
style = case html_node['style']
|
324
|
+
when /disc/ then 'disc'
|
325
|
+
when /circle/ then 'circle'
|
326
|
+
when /square/ then 'square'
|
327
|
+
end
|
328
|
+
list.bullet_style = style
|
329
|
+
end
|
330
|
+
|
331
|
+
process_node_children(html_node, list)
|
332
|
+
list
|
333
|
+
end
|
334
|
+
|
335
|
+
# Create a list item node from HTML li
|
336
|
+
def create_list_item_node(html_node)
|
337
|
+
item = ListItem.new
|
338
|
+
|
339
|
+
# Handle text content first
|
340
|
+
text_content = html_node.children.select { |child| child.text? || inline_element?(child) }
|
341
|
+
if text_content.any?
|
342
|
+
paragraph = Paragraph.new
|
343
|
+
text_content.each do |child|
|
344
|
+
node = convert_node(child)
|
345
|
+
paragraph.add_child(node) if node
|
346
|
+
end
|
347
|
+
item.add_content(paragraph)
|
348
|
+
end
|
349
|
+
|
350
|
+
# Handle nested content
|
351
|
+
html_node.children.reject { |child| child.text? || inline_element?(child) }.each do |child|
|
352
|
+
node = convert_node(child)
|
353
|
+
if node.is_a?(Array)
|
354
|
+
node.each { |n| item.add_content(n) }
|
355
|
+
elsif node
|
356
|
+
item.add_content(node)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
item
|
361
|
+
end
|
362
|
+
|
363
|
+
# Check if a node is an inline element
|
364
|
+
def inline_element?(node)
|
365
|
+
return false unless node.element?
|
366
|
+
|
367
|
+
%w[strong b em i code a br span strike s del sub sup u].include?(node.name)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Create a blockquote node from HTML blockquote
|
371
|
+
def create_blockquote_node(html_node)
|
372
|
+
quote = Blockquote.new
|
373
|
+
|
374
|
+
# Handle cite attribute if present
|
375
|
+
quote.citation = html_node['cite'] if html_node['cite']
|
376
|
+
|
377
|
+
# Process each child separately to maintain block structure
|
378
|
+
html_node.children.each do |child|
|
379
|
+
next if child.text? && child.text.strip.empty?
|
380
|
+
|
381
|
+
if child.text? || inline_element?(child)
|
382
|
+
# Wrap loose text in paragraphs
|
383
|
+
para = Paragraph.new
|
384
|
+
para.add_child(convert_node(child))
|
385
|
+
quote.add_block(para)
|
386
|
+
else
|
387
|
+
node = convert_node(child)
|
388
|
+
if node.is_a?(Array)
|
389
|
+
node.each { |n| quote.add_block(n) }
|
390
|
+
elsif node
|
391
|
+
quote.add_block(node)
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
quote
|
397
|
+
end
|
398
|
+
|
399
|
+
# Create a horizontal rule node from HTML hr
|
400
|
+
def create_horizontal_rule_node(html_node)
|
401
|
+
hr = HorizontalRule.new
|
402
|
+
|
403
|
+
# Handle style attributes if present
|
404
|
+
if html_node['style']
|
405
|
+
style = html_node['style']
|
406
|
+
|
407
|
+
# Parse border-style
|
408
|
+
hr.style = Regexp.last_match(1) if style =~ /border-style:\s*(solid|dashed|dotted)/
|
409
|
+
|
410
|
+
# Parse width
|
411
|
+
hr.width = Regexp.last_match(1) if style =~ /width:\s*(\d+(?:px|%)?)/
|
412
|
+
|
413
|
+
# Parse thickness (border-width)
|
414
|
+
hr.thickness = Regexp.last_match(1).to_i if style =~ /border-width:\s*(\d+)px/
|
415
|
+
end
|
416
|
+
|
417
|
+
hr
|
418
|
+
end
|
419
|
+
|
420
|
+
# Create an image node from HTML img
|
421
|
+
def create_image_node(html_node)
|
422
|
+
# Skip images without src
|
423
|
+
return nil unless html_node['src']
|
424
|
+
|
425
|
+
image = Image.new
|
426
|
+
|
427
|
+
# Handle required src attribute
|
428
|
+
image.src = html_node['src']
|
429
|
+
|
430
|
+
# Handle optional attributes
|
431
|
+
image.alt = html_node['alt'] if html_node['alt']
|
432
|
+
image.title = html_node['title'] if html_node['title']
|
433
|
+
|
434
|
+
# Handle dimensions
|
435
|
+
width = html_node['width']&.to_i
|
436
|
+
height = html_node['height']&.to_i
|
437
|
+
image.dimensions = [width, height] if width || height
|
438
|
+
|
439
|
+
image
|
440
|
+
end
|
441
|
+
|
442
|
+
# Create a code block wrapper from HTML pre tag
|
443
|
+
def create_code_block_wrapper(html_node)
|
444
|
+
wrapper = CodeBlockWrapper.new
|
445
|
+
wrapper.attrs = {
|
446
|
+
'line_numbers' => false
|
447
|
+
}
|
448
|
+
|
449
|
+
code_node = html_node.at_css('code')
|
450
|
+
if code_node
|
451
|
+
block = create_code_block(code_node)
|
452
|
+
wrapper.add_child(block)
|
453
|
+
end
|
454
|
+
|
455
|
+
wrapper.to_h['attrs'] = {
|
456
|
+
'line_numbers' => false
|
457
|
+
}
|
458
|
+
wrapper
|
459
|
+
end
|
460
|
+
|
461
|
+
# Create a code block from HTML code tag
|
462
|
+
def create_code_block(html_node)
|
463
|
+
block = CodeBlock.new
|
464
|
+
content = html_node.text.strip
|
465
|
+
language = extract_language(html_node)
|
466
|
+
|
467
|
+
block.attrs = {
|
468
|
+
'content' => content,
|
469
|
+
'language' => language,
|
470
|
+
'line_numbers' => nil
|
471
|
+
}
|
472
|
+
block.content = content
|
473
|
+
|
474
|
+
block
|
475
|
+
end
|
476
|
+
|
477
|
+
def extract_language(html_node)
|
478
|
+
return nil unless html_node['class']
|
479
|
+
|
480
|
+
return unless html_node['class'] =~ /language-(\w+)/
|
481
|
+
|
482
|
+
Regexp.last_match(1)
|
483
|
+
end
|
484
|
+
|
485
|
+
# Create a heading node from HTML heading tag (h1-h6)
|
486
|
+
def create_heading_node(html_node, level)
|
487
|
+
heading = Heading.new
|
488
|
+
heading.level = level
|
489
|
+
process_node_children(html_node, heading)
|
490
|
+
heading
|
491
|
+
end
|
492
|
+
|
493
|
+
# Create a user mention node from HTML user-mention element
|
494
|
+
def create_user_node(html_node)
|
495
|
+
# Skip user mentions without data-id
|
496
|
+
return nil unless html_node['data-id']
|
497
|
+
|
498
|
+
user = User.new
|
499
|
+
user.id = html_node['data-id']
|
500
|
+
user
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
require_relative 'paragraph'
|
5
|
+
require_relative 'text'
|
6
|
+
require_relative 'hard_break'
|
7
|
+
|
8
|
+
module Prosereflect
|
9
|
+
# ListItem class represents a list item in ProseMirror.
|
10
|
+
class ListItem < Node
|
11
|
+
PM_TYPE = 'list_item'
|
12
|
+
|
13
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
14
|
+
attribute :attrs, :hash
|
15
|
+
|
16
|
+
key_value do
|
17
|
+
map 'type', to: :type, render_default: true
|
18
|
+
map 'attrs', to: :attrs
|
19
|
+
map 'content', to: :content
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(attributes = {})
|
23
|
+
attributes[:content] ||= []
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.create(attrs = nil)
|
28
|
+
new(attrs: attrs)
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_paragraph(text = nil)
|
32
|
+
paragraph = Paragraph.new
|
33
|
+
paragraph.add_text(text) if text
|
34
|
+
add_child(paragraph)
|
35
|
+
paragraph
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_content(content)
|
39
|
+
add_child(content)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Add text to the last paragraph, or create a new one if none exists
|
43
|
+
def add_text(text, marks = nil)
|
44
|
+
last_paragraph = content&.last
|
45
|
+
last_paragraph = add_paragraph if !last_paragraph || !last_paragraph.is_a?(Paragraph)
|
46
|
+
last_paragraph.add_text(text, marks)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add a hard break to the last paragraph, or create a new one if none exists
|
51
|
+
def add_hard_break(marks = nil)
|
52
|
+
last_paragraph = content&.last
|
53
|
+
last_paragraph = add_paragraph if !last_paragraph || !last_paragraph.is_a?(Paragraph)
|
54
|
+
last_paragraph.add_hard_break(marks)
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get plain text content from all nodes
|
59
|
+
def text_content
|
60
|
+
return '' unless content
|
61
|
+
|
62
|
+
content.map { |node| node.respond_to?(:text_content) ? node.text_content : '' }.join("\n").strip
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lutaml/model'
|
4
|
+
|
5
|
+
module Prosereflect
|
6
|
+
module Mark
|
7
|
+
class Base < Lutaml::Model::Serializable
|
8
|
+
PM_TYPE = 'mark'
|
9
|
+
|
10
|
+
attribute :type, :string, default: lambda {
|
11
|
+
begin
|
12
|
+
self.class.const_get(:PM_TYPE)
|
13
|
+
rescue StandardError
|
14
|
+
'mark'
|
15
|
+
end
|
16
|
+
}
|
17
|
+
attribute :attrs, :hash
|
18
|
+
|
19
|
+
key_value do
|
20
|
+
map 'type', to: :type, render_default: true
|
21
|
+
map 'attrs', to: :attrs
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.create(attrs = nil)
|
25
|
+
new(type: const_get(:PM_TYPE), attrs: attrs)
|
26
|
+
rescue NameError
|
27
|
+
new(type: 'mark', attrs: attrs)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Convert to hash for serialization
|
31
|
+
def to_h
|
32
|
+
result = { 'type' => type }
|
33
|
+
result['attrs'] = attrs if attrs && !attrs.empty?
|
34
|
+
result
|
35
|
+
end
|
36
|
+
|
37
|
+
# Override initialize to ensure the type is set correctly
|
38
|
+
def initialize(options = {})
|
39
|
+
super(options)
|
40
|
+
# Only set the type to PM_TYPE if no type was provided in options
|
41
|
+
self.type = begin
|
42
|
+
options[:type] || self.class.const_get(:PM_TYPE)
|
43
|
+
rescue StandardError
|
44
|
+
'mark'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
# {
|
6
|
+
# type: "link",
|
7
|
+
# attrs: {
|
8
|
+
# href: @node.attribute('href').value
|
9
|
+
# }
|
10
|
+
# }
|
11
|
+
|
12
|
+
module Prosereflect
|
13
|
+
module Mark
|
14
|
+
class Link < Base
|
15
|
+
PM_TYPE = 'link'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|