prosereflect 0.1.0 → 0.2.0

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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rake.yml +4 -0
  3. data/.github/workflows/release.yml +5 -0
  4. data/.rubocop.yml +19 -1
  5. data/.rubocop_todo.yml +143 -174
  6. data/CLAUDE.md +78 -0
  7. data/Gemfile +8 -4
  8. data/README.adoc +193 -12
  9. data/Rakefile +3 -3
  10. data/lib/prosereflect/attribute/base.rb +32 -0
  11. data/lib/prosereflect/attribute/bold.rb +18 -0
  12. data/lib/prosereflect/attribute/href.rb +22 -0
  13. data/lib/prosereflect/attribute/id.rb +24 -0
  14. data/lib/prosereflect/attribute.rb +10 -0
  15. data/lib/prosereflect/blockquote.rb +84 -0
  16. data/lib/prosereflect/bullet_list.rb +84 -0
  17. data/lib/prosereflect/code_block.rb +135 -0
  18. data/lib/prosereflect/code_block_wrapper.rb +65 -0
  19. data/lib/prosereflect/document.rb +93 -26
  20. data/lib/prosereflect/hard_break.rb +13 -11
  21. data/lib/prosereflect/heading.rb +63 -0
  22. data/lib/prosereflect/horizontal_rule.rb +70 -0
  23. data/lib/prosereflect/image.rb +126 -0
  24. data/lib/prosereflect/input/html.rb +484 -0
  25. data/lib/prosereflect/input.rb +7 -0
  26. data/lib/prosereflect/list_item.rb +64 -0
  27. data/lib/prosereflect/mark/base.rb +47 -0
  28. data/lib/prosereflect/mark/bold.rb +13 -0
  29. data/lib/prosereflect/mark/code.rb +12 -0
  30. data/lib/prosereflect/mark/italic.rb +13 -0
  31. data/lib/prosereflect/mark/link.rb +16 -0
  32. data/lib/prosereflect/mark/strike.rb +13 -0
  33. data/lib/prosereflect/mark/subscript.rb +13 -0
  34. data/lib/prosereflect/mark/superscript.rb +13 -0
  35. data/lib/prosereflect/mark/underline.rb +13 -0
  36. data/lib/prosereflect/mark.rb +15 -0
  37. data/lib/prosereflect/node.rb +181 -32
  38. data/lib/prosereflect/ordered_list.rb +86 -0
  39. data/lib/prosereflect/output/html.rb +376 -0
  40. data/lib/prosereflect/output.rb +7 -0
  41. data/lib/prosereflect/paragraph.rb +29 -20
  42. data/lib/prosereflect/parser.rb +101 -33
  43. data/lib/prosereflect/table.rb +42 -12
  44. data/lib/prosereflect/table_cell.rb +36 -11
  45. data/lib/prosereflect/table_header.rb +92 -0
  46. data/lib/prosereflect/table_row.rb +34 -11
  47. data/lib/prosereflect/text.rb +15 -19
  48. data/lib/prosereflect/user.rb +63 -0
  49. data/lib/prosereflect/version.rb +1 -1
  50. data/lib/prosereflect.rb +27 -11
  51. data/prosereflect.gemspec +17 -15
  52. data/spec/prosereflect/document_spec.rb +477 -75
  53. data/spec/prosereflect/hard_break_spec.rb +226 -30
  54. data/spec/prosereflect/input/html_spec.rb +797 -0
  55. data/spec/prosereflect/node_spec.rb +307 -137
  56. data/spec/prosereflect/output/html_spec.rb +369 -0
  57. data/spec/prosereflect/paragraph_spec.rb +458 -82
  58. data/spec/prosereflect/parser_spec.rb +311 -93
  59. data/spec/prosereflect/table_cell_spec.rb +282 -71
  60. data/spec/prosereflect/table_row_spec.rb +218 -48
  61. data/spec/prosereflect/table_spec.rb +415 -82
  62. data/spec/prosereflect/text_spec.rb +231 -72
  63. data/spec/prosereflect/user_spec.rb +76 -0
  64. data/spec/prosereflect_spec.rb +30 -23
  65. data/spec/spec_helper.rb +6 -6
  66. data/spec/support/matchers.rb +6 -6
  67. data/spec/support/shared_examples.rb +79 -50
  68. metadata +53 -6
  69. data/debug_loading.rb +0 -34
  70. data/spec/prosereflect/version_spec.rb +0 -11
@@ -1,12 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'paragraph'
5
-
6
3
  module Prosereflect
7
4
  class TableCell < Node
5
+ PM_TYPE = "table_cell"
6
+
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
10
+
11
+ key_value do
12
+ map "type", to: :type, render_default: true
13
+ map "content", to: :content
14
+ map "attrs", to: :attrs
15
+ end
16
+
17
+ def initialize(attributes = {})
18
+ attributes[:content] ||= []
19
+ super
20
+ end
21
+
22
+ def self.create(attrs = nil)
23
+ new(type: PM_TYPE, attrs: attrs, content: [])
24
+ end
25
+
8
26
  def paragraphs
9
- content.select { |node| node.type == 'paragraph' }
27
+ return [] unless content
28
+
29
+ content.grep(Paragraph)
10
30
  end
11
31
 
12
32
  def text_content
@@ -17,13 +37,6 @@ module Prosereflect
17
37
  text_content.split("\n").map(&:strip).reject(&:empty?)
18
38
  end
19
39
 
20
- # Create a new table cell
21
- def self.create(attrs = nil)
22
- cell = new({ 'type' => 'table_cell', 'content' => [] })
23
- cell.instance_variable_set(:@attrs, attrs) if attrs
24
- cell
25
- end
26
-
27
40
  # Add a paragraph to the cell
28
41
  def add_paragraph(text = nil)
29
42
  paragraph = Paragraph.create
@@ -33,5 +46,17 @@ module Prosereflect
33
46
  add_child(paragraph)
34
47
  paragraph
35
48
  end
49
+
50
+ # Override to_h to handle empty content and attributes properly
51
+ def to_h
52
+ result = super
53
+ result["content"] ||= []
54
+ if result["attrs"]
55
+ result["attrs"] =
56
+ result["attrs"].is_a?(Hash) && result["attrs"][:attrs] ? result["attrs"][:attrs] : result["attrs"]
57
+ result.delete("attrs") if result["attrs"].empty?
58
+ end
59
+ result
60
+ end
36
61
  end
37
62
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ # TableHeader class represents a header cell in a table (<th> tag).
5
+ # It inherits from TableCell but adds header-specific attributes.
6
+ class TableHeader < TableCell
7
+ PM_TYPE = "table_header"
8
+
9
+ attribute :type, :string, default: -> {
10
+ self.class.send(:const_get, "PM_TYPE")
11
+ }
12
+ attribute :scope, :string # row, col, rowgroup, or colgroup
13
+ attribute :abbr, :string # abbreviated version of content
14
+ attribute :colspan, :integer # number of columns this header spans
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
+
31
+ # Add text to the last paragraph, or create a new one if none exists
32
+ def add_text(text, marks = nil)
33
+ last_paragraph = content&.last
34
+ last_paragraph = add_paragraph if !last_paragraph || !last_paragraph.is_a?(Paragraph)
35
+ last_paragraph.add_text(text, marks)
36
+ self
37
+ end
38
+
39
+ # Set the scope of the header (row, col, rowgroup, or colgroup)
40
+ def scope=(scope_value)
41
+ return unless %w[row col rowgroup colgroup].include?(scope_value)
42
+
43
+ self.attrs ||= {}
44
+ attrs["scope"] = scope_value
45
+ end
46
+
47
+ def scope
48
+ attrs&.[]("scope")
49
+ end
50
+
51
+ # Set abbreviated version of the header content
52
+ def abbr=(abbr_text)
53
+ self.attrs ||= {}
54
+ attrs["abbr"] = abbr_text
55
+ end
56
+
57
+ def abbr
58
+ attrs&.[]("abbr")
59
+ end
60
+
61
+ # Set the number of columns this header spans
62
+ def colspan=(span)
63
+ return unless span.to_i.positive?
64
+
65
+ self.attrs ||= {}
66
+ attrs["colspan"] = span.to_i
67
+ end
68
+
69
+ def colspan
70
+ attrs&.[]("colspan")
71
+ end
72
+
73
+ # Get header attributes as a hash
74
+ def header_attributes
75
+ {
76
+ scope: scope,
77
+ abbr: abbr,
78
+ colspan: colspan,
79
+ }.compact
80
+ end
81
+
82
+ # Override to_h to exclude nil attributes
83
+ def to_h
84
+ result = super
85
+ if result["attrs"]
86
+ result["attrs"].compact!
87
+ result.delete("attrs") if result["attrs"].empty?
88
+ end
89
+ result
90
+ end
91
+ end
92
+ end
@@ -1,24 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'table_cell'
5
-
6
3
  module Prosereflect
7
4
  class TableRow < Node
8
- def cells
9
- content.select { |node| node.type == 'table_cell' }
5
+ PM_TYPE = "table_row"
6
+
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
10
+
11
+ key_value do
12
+ map "type", to: :type, render_default: true
13
+ map "content", to: :content
14
+ map "attrs", to: :attrs
15
+ end
16
+
17
+ def initialize(opts = {})
18
+ opts[:content] ||= []
19
+ super
10
20
  end
11
21
 
12
- # Create a new table row
13
22
  def self.create(attrs = nil)
14
- row = new({ 'type' => 'table_row', 'content' => [] })
15
- row.instance_variable_set(:@attrs, attrs) if attrs
16
- row
23
+ new(type: PM_TYPE, attrs: attrs, content: [])
24
+ end
25
+
26
+ def cells
27
+ content || []
17
28
  end
18
29
 
19
30
  # Add a cell to the row
20
- def add_cell(content_text = nil)
21
- cell = TableCell.create
31
+ def add_cell(content_text = nil, attrs: nil)
32
+ cell = TableCell.create(attrs)
22
33
 
23
34
  if content_text
24
35
  paragraph = cell.add_paragraph
@@ -28,5 +39,17 @@ module Prosereflect
28
39
  add_child(cell)
29
40
  cell
30
41
  end
42
+
43
+ # Override to_h to handle empty content and attributes properly
44
+ def to_h
45
+ result = super
46
+ result["content"] ||= []
47
+ if result["attrs"]
48
+ result["attrs"] =
49
+ result["attrs"].is_a?(Hash) && result["attrs"][:attrs] ? result["attrs"][:attrs] : result["attrs"]
50
+ result.delete("attrs") if result["attrs"].empty?
51
+ end
52
+ result
53
+ end
31
54
  end
32
55
  end
@@ -1,36 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  class Text < Node
7
- attr_reader :text
8
- attr_accessor :marks
5
+ PM_TYPE = "text"
9
6
 
10
- def initialize(data = {})
11
- super
12
- @text = data['text'] || ''
13
- end
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
10
+ attribute :text, :string, default: ""
14
11
 
15
- def text_content
16
- @text || ''
12
+ key_value do
13
+ map "type", to: :type, render_default: true
14
+ map "text", to: :text
15
+ map "marks", to: :marks
17
16
  end
18
17
 
19
- def text_content_with_breaks
20
- @text || ''
18
+ def self.create(text = "", marks = nil)
19
+ new(text: text, marks: marks)
21
20
  end
22
21
 
23
- # Create a new text node
24
- def self.create(text, marks = nil)
25
- node = new({ 'type' => 'text', 'text' => text })
26
- node.instance_variable_set(:@marks, marks) if marks
27
- node
22
+ def text_content
23
+ text || ""
28
24
  end
29
25
 
30
- # Convert to hash representation
26
+ # Override the to_h method to include the text attribute
31
27
  def to_h
32
28
  result = super
33
- result['text'] = @text if @text
29
+ result["text"] = text
34
30
  result
35
31
  end
36
32
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ # User class represents a user mention in ProseMirror.
5
+ class User < Node
6
+ PM_TYPE = "user"
7
+
8
+ attribute :type, :string, default: -> {
9
+ self.class.send(:const_get, "PM_TYPE")
10
+ }
11
+ attribute :id, :string
12
+ attribute :attrs, :hash
13
+
14
+ key_value do
15
+ map "type", to: :type, render_default: true
16
+ map "attrs", to: :attrs
17
+ map "content", to: :content
18
+ end
19
+
20
+ def initialize(attributes = {})
21
+ attributes[:content] = []
22
+ super
23
+
24
+ return unless attributes[:attrs]
25
+
26
+ @id = attributes[:attrs]["id"]
27
+ self.attrs = { "id" => @id }
28
+ end
29
+
30
+ def self.create(attrs = nil)
31
+ new(attrs: attrs)
32
+ end
33
+
34
+ # Update the user ID
35
+ def id=(user_id)
36
+ @id = user_id
37
+ self.attrs ||= {}
38
+ attrs["id"] = user_id
39
+ end
40
+
41
+ def id
42
+ @id || attrs&.[]("id")
43
+ end
44
+
45
+ # Override content-related methods since user mentions don't have content
46
+ def add_child(*)
47
+ raise NotImplementedError, "User mention nodes cannot have children"
48
+ end
49
+
50
+ def content
51
+ []
52
+ end
53
+
54
+ def to_h
55
+ hash = super
56
+ hash["attrs"] = {
57
+ "id" => id,
58
+ }
59
+ hash["content"] = []
60
+ hash
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosereflect
4
- VERSION = '0.1.0'
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/prosereflect.rb CHANGED
@@ -1,17 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'prosereflect/version'
4
- require_relative 'prosereflect/node'
5
- require_relative 'prosereflect/text'
6
- require_relative 'prosereflect/paragraph'
7
- require_relative 'prosereflect/hard_break'
8
- require_relative 'prosereflect/table'
9
- require_relative 'prosereflect/table_row'
10
- require_relative 'prosereflect/table_cell'
11
- require_relative 'prosereflect/document'
12
- require_relative 'prosereflect/parser'
3
+ require "lutaml/model"
13
4
 
14
5
  module Prosereflect
15
6
  class Error < StandardError; end
16
- # Your code goes here...
7
+
8
+ autoload :Attribute, "prosereflect/attribute"
9
+ autoload :Blockquote, "prosereflect/blockquote"
10
+ autoload :BulletList, "prosereflect/bullet_list"
11
+ autoload :CodeBlock, "prosereflect/code_block"
12
+ autoload :CodeBlockWrapper, "prosereflect/code_block_wrapper"
13
+ autoload :Document, "prosereflect/document"
14
+ autoload :HardBreak, "prosereflect/hard_break"
15
+ autoload :Heading, "prosereflect/heading"
16
+ autoload :HorizontalRule, "prosereflect/horizontal_rule"
17
+ autoload :Image, "prosereflect/image"
18
+ autoload :Input, "prosereflect/input"
19
+ autoload :ListItem, "prosereflect/list_item"
20
+ autoload :Mark, "prosereflect/mark"
21
+ autoload :Node, "prosereflect/node"
22
+ autoload :OrderedList, "prosereflect/ordered_list"
23
+ autoload :Output, "prosereflect/output"
24
+ autoload :Paragraph, "prosereflect/paragraph"
25
+ autoload :Parser, "prosereflect/parser"
26
+ autoload :Table, "prosereflect/table"
27
+ autoload :TableCell, "prosereflect/table_cell"
28
+ autoload :TableHeader, "prosereflect/table_header"
29
+ autoload :TableRow, "prosereflect/table_row"
30
+ autoload :Text, "prosereflect/text"
31
+ autoload :User, "prosereflect/user"
32
+ autoload :VERSION, "prosereflect/version"
17
33
  end
data/prosereflect.gemspec CHANGED
@@ -1,33 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/prosereflect/version'
3
+ require_relative "lib/prosereflect/version"
4
4
 
5
5
  all_files_in_git = Dir.chdir(File.expand_path(__dir__)) do
6
6
  `git ls-files -z`.split("\x0")
7
7
  end
8
8
 
9
9
  Gem::Specification.new do |spec|
10
- spec.name = 'prosereflect'
10
+ spec.name = "prosereflect"
11
11
  spec.version = Prosereflect::VERSION
12
- spec.authors = ['Ribose']
13
- spec.email = ['open.source@ribose.com']
12
+ spec.authors = ["Ribose"]
13
+ spec.email = ["open.source@ribose.com"]
14
14
 
15
- spec.summary = 'Ruby model accessor for prosereflect document trees.'
16
- spec.homepage = 'https://github.com/metanorma/prosereflect'
17
- spec.license = 'BSD-2-Clause'
18
- spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0')
15
+ spec.summary = "Ruby model accessor for prosereflect document trees."
16
+ spec.homepage = "https://github.com/metanorma/prosereflect"
17
+ spec.license = "BSD-2-Clause"
18
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.6.0")
19
19
 
20
- spec.metadata['homepage_uri'] = spec.homepage
21
- spec.metadata['source_code_uri'] = spec.homepage
22
- spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
23
+ spec.metadata["rubygems_mfa_required"] = "true"
23
24
 
24
25
  # Specify which files should be added to the gem when it is released.
25
26
  spec.files = all_files_in_git
26
- .reject { |f| f.match(%r{\A(?:test|features|bin|\.)/}) }
27
+ .reject { |f| f.match(%r{\A(?:test|features|bin|\.)/}) }
27
28
 
28
- spec.bindir = 'exe'
29
+ spec.bindir = "exe"
29
30
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
- spec.require_paths = ['lib']
31
+ spec.require_paths = ["lib"]
31
32
 
32
- spec.add_dependency 'lutaml-model', '~> 0.7'
33
+ spec.add_dependency "lutaml-model", "~> 0.8"
34
+ spec.add_dependency "nokogiri", "~> 1.18"
33
35
  end