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,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
|
5
|
+
module Prosereflect
|
6
|
+
# CodeBlock class represents a code block in ProseMirror.
|
7
|
+
class CodeBlock < Node
|
8
|
+
PM_TYPE = 'code_block'
|
9
|
+
|
10
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
11
|
+
attribute :language, :string
|
12
|
+
attribute :line_numbers, :boolean
|
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[:attrs] ||= {
|
23
|
+
'content' => nil,
|
24
|
+
'language' => nil
|
25
|
+
}
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.create(attrs = nil)
|
30
|
+
new(attrs: attrs)
|
31
|
+
end
|
32
|
+
|
33
|
+
def language=(value)
|
34
|
+
@language = value
|
35
|
+
self.attrs ||= {}
|
36
|
+
attrs['language'] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def language
|
40
|
+
@language || attrs&.[]('language')
|
41
|
+
end
|
42
|
+
|
43
|
+
def line_numbers=(value)
|
44
|
+
@line_numbers = value
|
45
|
+
self.attrs ||= {}
|
46
|
+
attrs['line_numbers'] = value
|
47
|
+
end
|
48
|
+
|
49
|
+
def line_numbers
|
50
|
+
@line_numbers || attrs&.[]('line_numbers') || false
|
51
|
+
end
|
52
|
+
|
53
|
+
def content=(value)
|
54
|
+
@content = value
|
55
|
+
self.attrs ||= {}
|
56
|
+
attrs['content'] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
def content
|
60
|
+
@content || attrs&.[]('content')
|
61
|
+
end
|
62
|
+
|
63
|
+
attr_reader :highlight_lines_str
|
64
|
+
|
65
|
+
def highlight_lines=(lines)
|
66
|
+
@highlight_lines_str = if lines.is_a?(Array)
|
67
|
+
lines.join(',')
|
68
|
+
else
|
69
|
+
lines.to_s
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def highlight_lines
|
74
|
+
return [] unless @highlight_lines_str
|
75
|
+
|
76
|
+
@highlight_lines_str.split(',').map(&:to_i)
|
77
|
+
end
|
78
|
+
|
79
|
+
def text_content
|
80
|
+
content.to_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def to_h
|
84
|
+
hash = super
|
85
|
+
hash['attrs'] = {
|
86
|
+
'content' => content,
|
87
|
+
'language' => language
|
88
|
+
}
|
89
|
+
hash['attrs']['line_numbers'] = line_numbers if line_numbers
|
90
|
+
hash.delete('content')
|
91
|
+
hash
|
92
|
+
end
|
93
|
+
|
94
|
+
# Get code block attributes as a hash
|
95
|
+
def attributes
|
96
|
+
{
|
97
|
+
language: language,
|
98
|
+
line_numbers: line_numbers,
|
99
|
+
highlight_lines: highlight_lines
|
100
|
+
}.compact
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add a line of code
|
104
|
+
def add_line(text)
|
105
|
+
text_node = Text.new(text: text)
|
106
|
+
add_child(text_node)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Add multiple lines of code
|
110
|
+
def add_lines(lines)
|
111
|
+
lines.each { |line| add_line(line) }
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def normalize_content(content)
|
117
|
+
lines = content.split("\n")
|
118
|
+
return content if lines.empty?
|
119
|
+
|
120
|
+
min_indent = lines.reject(&:empty?)
|
121
|
+
.map { |line| line[/^\s*/].length }
|
122
|
+
.min || 0
|
123
|
+
|
124
|
+
normalized_lines = lines.map do |line|
|
125
|
+
if line.empty?
|
126
|
+
line
|
127
|
+
else
|
128
|
+
line[min_indent..] || ''
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
normalized_lines.join("\n")
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
require_relative 'code_block'
|
5
|
+
|
6
|
+
module Prosereflect
|
7
|
+
# CodeBlockWrapper class represents a pre tag that wraps code blocks in ProseMirror.
|
8
|
+
class CodeBlockWrapper < Node
|
9
|
+
PM_TYPE = 'code_block_wrapper'
|
10
|
+
|
11
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
12
|
+
attribute :line_numbers, :boolean
|
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
|
+
attributes[:attrs] = {
|
24
|
+
'line_numbers' => false
|
25
|
+
}
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.create(attrs = nil)
|
30
|
+
new(attrs: attrs)
|
31
|
+
end
|
32
|
+
|
33
|
+
def line_numbers=(value)
|
34
|
+
@line_numbers = value
|
35
|
+
self.attrs ||= {}
|
36
|
+
attrs['line_numbers'] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def line_numbers
|
40
|
+
@line_numbers || attrs&.[]('line_numbers') || false
|
41
|
+
end
|
42
|
+
|
43
|
+
def add_code_block(code = nil)
|
44
|
+
block = CodeBlock.new
|
45
|
+
block.content = code if code
|
46
|
+
add_child(block)
|
47
|
+
block
|
48
|
+
end
|
49
|
+
|
50
|
+
def code_blocks
|
51
|
+
content
|
52
|
+
end
|
53
|
+
|
54
|
+
def text_content
|
55
|
+
code_blocks.map(&:text_content).join("\n")
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_h
|
59
|
+
hash = super
|
60
|
+
hash['attrs'] = {
|
61
|
+
'line_numbers' => line_numbers
|
62
|
+
}
|
63
|
+
hash
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,41 +1,67 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
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'
|
4
14
|
|
5
15
|
module Prosereflect
|
6
16
|
# Document class represents a ProseMirror document.
|
7
17
|
class Document < Node
|
8
|
-
|
9
|
-
|
10
|
-
|
18
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
19
|
+
PM_TYPE = 'doc'
|
20
|
+
|
21
|
+
key_value do
|
22
|
+
map 'type', to: :type, render_default: true
|
23
|
+
map 'content', to: :content
|
24
|
+
map 'attrs', to: :attrs
|
11
25
|
end
|
12
26
|
|
13
|
-
def
|
14
|
-
|
27
|
+
def self.create(attrs = nil)
|
28
|
+
new(attrs: attrs, content: [])
|
15
29
|
end
|
16
30
|
|
17
|
-
|
18
|
-
|
31
|
+
# Override the to_h method to handle attribute arrays
|
32
|
+
def to_h
|
33
|
+
result = super
|
34
|
+
|
35
|
+
# Handle array of attribute objects specially for serialization
|
36
|
+
if attrs.is_a?(Array) && attrs.all? { |attr| attr.is_a?(Prosereflect::Attribute::Base) }
|
37
|
+
attrs_hash = {}
|
38
|
+
attrs.each do |attr|
|
39
|
+
attrs_hash.merge!(attr.to_h)
|
40
|
+
end
|
41
|
+
result['attrs'] = attrs_hash unless attrs_hash.empty?
|
42
|
+
end
|
43
|
+
|
44
|
+
result
|
19
45
|
end
|
20
46
|
|
21
|
-
def
|
22
|
-
|
47
|
+
def tables
|
48
|
+
find_children(Table)
|
23
49
|
end
|
24
50
|
|
25
|
-
def
|
26
|
-
|
51
|
+
def paragraphs
|
52
|
+
find_children(Paragraph)
|
27
53
|
end
|
28
54
|
|
29
|
-
#
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
55
|
+
# Add a heading to the document
|
56
|
+
def add_heading(level)
|
57
|
+
heading = Heading.new(attrs: { 'level' => level })
|
58
|
+
add_child(heading)
|
59
|
+
heading
|
34
60
|
end
|
35
61
|
|
36
62
|
# Add a paragraph with text to the document
|
37
63
|
def add_paragraph(text = nil, attrs = nil)
|
38
|
-
paragraph = Paragraph.
|
64
|
+
paragraph = Paragraph.new(attrs: attrs)
|
39
65
|
|
40
66
|
paragraph.add_text(text) if text
|
41
67
|
|
@@ -45,19 +71,68 @@ module Prosereflect
|
|
45
71
|
|
46
72
|
# Add a table to the document
|
47
73
|
def add_table(attrs = nil)
|
48
|
-
table = Table.
|
74
|
+
table = Table.new(attrs: attrs)
|
49
75
|
add_child(table)
|
50
76
|
table
|
51
77
|
end
|
52
78
|
|
53
|
-
#
|
54
|
-
def
|
55
|
-
|
79
|
+
# Add an image to the document
|
80
|
+
def add_image(src, alt = nil, _attrs = {})
|
81
|
+
image = Image.new
|
82
|
+
image.src = src
|
83
|
+
image.alt = alt if alt
|
84
|
+
add_child(image)
|
85
|
+
image
|
86
|
+
end
|
87
|
+
|
88
|
+
# Add a user mention to the document
|
89
|
+
def add_user(id)
|
90
|
+
user = User.new
|
91
|
+
user.id = id
|
92
|
+
add_child(user)
|
93
|
+
user
|
56
94
|
end
|
57
95
|
|
58
|
-
#
|
59
|
-
def
|
60
|
-
|
96
|
+
# Add a bullet list to the document
|
97
|
+
def add_bullet_list(attrs = nil)
|
98
|
+
list = BulletList.new(attrs: attrs)
|
99
|
+
add_child(list)
|
100
|
+
list
|
101
|
+
end
|
102
|
+
|
103
|
+
# Add an ordered list to the document
|
104
|
+
def add_ordered_list(attrs = nil)
|
105
|
+
list = OrderedList.new(attrs: attrs)
|
106
|
+
add_child(list)
|
107
|
+
list
|
108
|
+
end
|
109
|
+
|
110
|
+
# Add a blockquote to the document
|
111
|
+
def add_blockquote(attrs = nil)
|
112
|
+
quote = Blockquote.new(attrs: attrs)
|
113
|
+
add_child(quote)
|
114
|
+
quote
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add a horizontal rule to the document
|
118
|
+
def add_horizontal_rule(attrs = nil)
|
119
|
+
hr = HorizontalRule.new(attrs: attrs)
|
120
|
+
add_child(hr)
|
121
|
+
hr
|
122
|
+
end
|
123
|
+
|
124
|
+
# Add a code block wrapper to the document
|
125
|
+
def add_code_block_wrapper(attrs = nil)
|
126
|
+
wrapper = CodeBlockWrapper.new(attrs: attrs)
|
127
|
+
add_child(wrapper)
|
128
|
+
wrapper
|
129
|
+
end
|
130
|
+
|
131
|
+
# Get plain text content from all nodes
|
132
|
+
def text_content
|
133
|
+
return '' unless content
|
134
|
+
|
135
|
+
content.map { |node| node.respond_to?(:text_content) ? node.text_content : '' }.join("\n").strip
|
61
136
|
end
|
62
137
|
end
|
63
138
|
end
|
@@ -4,19 +4,21 @@ require_relative 'node'
|
|
4
4
|
|
5
5
|
module Prosereflect
|
6
6
|
class HardBreak < Node
|
7
|
-
|
8
|
-
"\n"
|
9
|
-
end
|
7
|
+
PM_TYPE = 'hard_break'
|
10
8
|
|
11
|
-
|
12
|
-
|
9
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
10
|
+
|
11
|
+
key_value do
|
12
|
+
map 'type', to: :type, render_default: true
|
13
|
+
map 'marks', to: :marks
|
13
14
|
end
|
14
15
|
|
15
|
-
# Create a new hard break
|
16
16
|
def self.create(marks = nil)
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
new(marks: marks)
|
18
|
+
end
|
19
|
+
|
20
|
+
def text_content
|
21
|
+
"\n"
|
20
22
|
end
|
21
23
|
end
|
22
24
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
require_relative 'text'
|
5
|
+
|
6
|
+
module Prosereflect
|
7
|
+
class Heading < Node
|
8
|
+
PM_TYPE = 'heading'
|
9
|
+
|
10
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
11
|
+
attribute :level, :integer
|
12
|
+
attribute :attrs, :hash
|
13
|
+
|
14
|
+
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
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(params = {})
|
22
|
+
super
|
23
|
+
self.content ||= []
|
24
|
+
|
25
|
+
# Extract level from attrs if provided
|
26
|
+
return unless params[:attrs]
|
27
|
+
|
28
|
+
@level = params[:attrs]['level']
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.create(attrs = nil)
|
32
|
+
new(attrs: attrs)
|
33
|
+
end
|
34
|
+
|
35
|
+
def level=(value)
|
36
|
+
@level = value
|
37
|
+
self.attrs ||= {}
|
38
|
+
attrs['level'] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def level
|
42
|
+
@level || attrs&.[]('level')
|
43
|
+
end
|
44
|
+
|
45
|
+
def text_content
|
46
|
+
return '' unless content
|
47
|
+
|
48
|
+
content.map(&:text_content).join
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_text(text)
|
52
|
+
text_node = Text.new(text: text)
|
53
|
+
add_child(text_node)
|
54
|
+
text_node
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_h
|
58
|
+
result = super
|
59
|
+
result['attrs'] ||= {}
|
60
|
+
result['attrs']['level'] = level if level
|
61
|
+
result
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
|
5
|
+
module Prosereflect
|
6
|
+
# HorizontalRule class represents a horizontal rule in ProseMirror.
|
7
|
+
class HorizontalRule < Node
|
8
|
+
PM_TYPE = 'horizontal_rule'
|
9
|
+
|
10
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
11
|
+
attribute :style, :string
|
12
|
+
attribute :width, :string
|
13
|
+
attribute :thickness, :integer
|
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 style=(value)
|
32
|
+
@style = value
|
33
|
+
self.attrs ||= {}
|
34
|
+
attrs['style'] = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def style
|
38
|
+
@style || attrs&.[]('style')
|
39
|
+
end
|
40
|
+
|
41
|
+
def width=(value)
|
42
|
+
@width = value
|
43
|
+
self.attrs ||= {}
|
44
|
+
attrs['width'] = value
|
45
|
+
end
|
46
|
+
|
47
|
+
def width
|
48
|
+
@width || attrs&.[]('width')
|
49
|
+
end
|
50
|
+
|
51
|
+
def thickness=(value)
|
52
|
+
@thickness = value
|
53
|
+
self.attrs ||= {}
|
54
|
+
attrs['thickness'] = value
|
55
|
+
end
|
56
|
+
|
57
|
+
def thickness
|
58
|
+
@thickness || attrs&.[]('thickness')
|
59
|
+
end
|
60
|
+
|
61
|
+
# Override content-related methods since horizontal rules don't have content
|
62
|
+
def add_child(*)
|
63
|
+
raise NotImplementedError, 'Horizontal rule nodes cannot have children'
|
64
|
+
end
|
65
|
+
|
66
|
+
def content
|
67
|
+
[]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'node'
|
4
|
+
|
5
|
+
module Prosereflect
|
6
|
+
# Image class represents a ProseMirror image node.
|
7
|
+
# It handles image attributes like src, alt, title, dimensions, etc.
|
8
|
+
class Image < Node
|
9
|
+
PM_TYPE = 'image'
|
10
|
+
|
11
|
+
attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
|
12
|
+
attribute :src, :string
|
13
|
+
attribute :alt, :string
|
14
|
+
attribute :title, :string
|
15
|
+
attribute :width, :integer
|
16
|
+
attribute :height, :integer
|
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 initialize(attributes = {})
|
25
|
+
# Images don't have content, they're self-contained
|
26
|
+
attributes[:content] = []
|
27
|
+
|
28
|
+
# Extract attributes from the attrs hash if provided
|
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']
|
35
|
+
end
|
36
|
+
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.create(attrs = nil)
|
41
|
+
new(attrs: attrs)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update the image source URL
|
45
|
+
def src=(src_url)
|
46
|
+
@src = src_url
|
47
|
+
self.attrs ||= {}
|
48
|
+
attrs['src'] = src_url
|
49
|
+
end
|
50
|
+
|
51
|
+
# Update the alt text
|
52
|
+
def alt=(alt_text)
|
53
|
+
@alt = alt_text
|
54
|
+
self.attrs ||= {}
|
55
|
+
attrs['alt'] = alt_text
|
56
|
+
end
|
57
|
+
|
58
|
+
# Update the title (tooltip)
|
59
|
+
def title=(title_text)
|
60
|
+
@title = title_text
|
61
|
+
self.attrs ||= {}
|
62
|
+
attrs['title'] = title_text
|
63
|
+
end
|
64
|
+
|
65
|
+
# Update the width
|
66
|
+
def width=(value)
|
67
|
+
@width = value
|
68
|
+
self.attrs ||= {}
|
69
|
+
attrs['width'] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
# Update the height
|
73
|
+
def height=(value)
|
74
|
+
@height = value
|
75
|
+
self.attrs ||= {}
|
76
|
+
attrs['height'] = value
|
77
|
+
end
|
78
|
+
|
79
|
+
# Update dimensions (width and height)
|
80
|
+
def dimensions=(dimensions)
|
81
|
+
width, height = dimensions
|
82
|
+
self.width = width if width
|
83
|
+
self.height = height if height
|
84
|
+
end
|
85
|
+
|
86
|
+
# Get image attributes as a hash
|
87
|
+
def image_attributes
|
88
|
+
{
|
89
|
+
src: src,
|
90
|
+
alt: alt,
|
91
|
+
title: title,
|
92
|
+
width: width,
|
93
|
+
height: height
|
94
|
+
}.compact
|
95
|
+
end
|
96
|
+
|
97
|
+
# Override content-related methods since images don't have content
|
98
|
+
def add_child(*)
|
99
|
+
raise NotImplementedError, 'Image nodes cannot have children'
|
100
|
+
end
|
101
|
+
|
102
|
+
def content
|
103
|
+
[]
|
104
|
+
end
|
105
|
+
|
106
|
+
def src
|
107
|
+
@src || attrs&.[]('src')
|
108
|
+
end
|
109
|
+
|
110
|
+
def alt
|
111
|
+
@alt || attrs&.[]('alt')
|
112
|
+
end
|
113
|
+
|
114
|
+
def title
|
115
|
+
@title || attrs&.[]('title')
|
116
|
+
end
|
117
|
+
|
118
|
+
def width
|
119
|
+
@width || attrs&.[]('width')
|
120
|
+
end
|
121
|
+
|
122
|
+
def height
|
123
|
+
@height || attrs&.[]('height')
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|