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,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
- def initialize(data = {})
9
- super(data)
10
- @type = 'doc'
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 tables
14
- find_children('table')
27
+ def self.create(attrs = nil)
28
+ new(attrs: attrs, content: [])
15
29
  end
16
30
 
17
- def paragraphs
18
- find_children('paragraph')
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 first_paragraph
22
- find_first('paragraph')
47
+ def tables
48
+ find_children(Table)
23
49
  end
24
50
 
25
- def first_table
26
- find_first('table')
51
+ def paragraphs
52
+ find_children(Paragraph)
27
53
  end
28
54
 
29
- # Create a new empty document
30
- def self.create(attrs = nil)
31
- doc = new({ 'type' => 'doc', 'content' => [] })
32
- doc.instance_variable_set(:@attrs, attrs) if attrs
33
- doc
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.create(attrs)
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.create(attrs)
74
+ table = Table.new(attrs: attrs)
49
75
  add_child(table)
50
76
  table
51
77
  end
52
78
 
53
- # Convert document to JSON
54
- def to_json(*_args)
55
- JSON.generate(to_h)
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
- # Convert document to YAML
59
- def to_yaml
60
- to_h.to_yaml
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
- def text_content
8
- "\n"
9
- end
7
+ PM_TYPE = 'hard_break'
10
8
 
11
- def text_content_with_breaks
12
- "\n"
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
- node = new({ 'type' => 'hard_break' })
18
- node.instance_variable_set(:@marks, marks) if marks
19
- node
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