prosereflect 0.1.1 → 0.3.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 (158) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +97 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.gitignore +4 -0
  7. data/.rubocop.yml +19 -1
  8. data/.rubocop_todo.yml +119 -183
  9. data/CLAUDE.md +78 -0
  10. data/Gemfile +8 -4
  11. data/README.adoc +2 -0
  12. data/Rakefile +3 -3
  13. data/docs/Gemfile +10 -0
  14. data/docs/INDEX.adoc +45 -0
  15. data/docs/_advanced/index.adoc +15 -0
  16. data/docs/_advanced/schema.adoc +112 -0
  17. data/docs/_advanced/step-map.adoc +66 -0
  18. data/docs/_advanced/steps.adoc +88 -0
  19. data/docs/_advanced/test-builder.adoc +61 -0
  20. data/docs/_advanced/transform.adoc +92 -0
  21. data/docs/_config.yml +174 -0
  22. data/docs/_features/html-input.adoc +69 -0
  23. data/docs/_features/html-output.adoc +45 -0
  24. data/docs/_features/index.adoc +15 -0
  25. data/docs/_features/marks.adoc +86 -0
  26. data/docs/_features/node-types.adoc +124 -0
  27. data/docs/_features/user-mentions.adoc +47 -0
  28. data/docs/_guides/custom-nodes.adoc +107 -0
  29. data/docs/_guides/index.adoc +13 -0
  30. data/docs/_guides/round-trip-html.adoc +91 -0
  31. data/docs/_guides/serialization.adoc +109 -0
  32. data/docs/_pages/index.adoc +67 -0
  33. data/docs/_reference/document-api.adoc +49 -0
  34. data/docs/_reference/index.adoc +14 -0
  35. data/docs/_reference/node-api.adoc +79 -0
  36. data/docs/_reference/schema-api.adoc +95 -0
  37. data/docs/_reference/transform-api.adoc +77 -0
  38. data/docs/_understanding/document-model.adoc +65 -0
  39. data/docs/_understanding/fragment.adoc +52 -0
  40. data/docs/_understanding/index.adoc +14 -0
  41. data/docs/_understanding/resolved-position.adoc +53 -0
  42. data/docs/_understanding/slice.adoc +54 -0
  43. data/docs/lychee.toml +63 -0
  44. data/lib/prosereflect/attribute/base.rb +4 -6
  45. data/lib/prosereflect/attribute/bold.rb +2 -4
  46. data/lib/prosereflect/attribute/href.rb +1 -3
  47. data/lib/prosereflect/attribute/id.rb +7 -7
  48. data/lib/prosereflect/attribute.rb +4 -7
  49. data/lib/prosereflect/blockquote.rb +19 -11
  50. data/lib/prosereflect/bullet_list.rb +36 -29
  51. data/lib/prosereflect/code_block.rb +23 -27
  52. data/lib/prosereflect/code_block_wrapper.rb +12 -13
  53. data/lib/prosereflect/document.rb +14 -22
  54. data/lib/prosereflect/fragment.rb +249 -0
  55. data/lib/prosereflect/hard_break.rb +6 -6
  56. data/lib/prosereflect/heading.rb +14 -15
  57. data/lib/prosereflect/horizontal_rule.rb +23 -14
  58. data/lib/prosereflect/image.rb +32 -23
  59. data/lib/prosereflect/input/html.rb +179 -104
  60. data/lib/prosereflect/input.rb +7 -0
  61. data/lib/prosereflect/list_item.rb +11 -12
  62. data/lib/prosereflect/mark/base.rb +9 -11
  63. data/lib/prosereflect/mark/bold.rb +1 -3
  64. data/lib/prosereflect/mark/code.rb +1 -3
  65. data/lib/prosereflect/mark/italic.rb +1 -3
  66. data/lib/prosereflect/mark/link.rb +1 -3
  67. data/lib/prosereflect/mark/strike.rb +1 -3
  68. data/lib/prosereflect/mark/subscript.rb +1 -3
  69. data/lib/prosereflect/mark/superscript.rb +1 -3
  70. data/lib/prosereflect/mark/underline.rb +1 -3
  71. data/lib/prosereflect/mark.rb +9 -5
  72. data/lib/prosereflect/node.rb +171 -33
  73. data/lib/prosereflect/ordered_list.rb +17 -14
  74. data/lib/prosereflect/output/html.rb +279 -50
  75. data/lib/prosereflect/output.rb +7 -0
  76. data/lib/prosereflect/paragraph.rb +11 -13
  77. data/lib/prosereflect/parser.rb +56 -66
  78. data/lib/prosereflect/resolved_pos.rb +256 -0
  79. data/lib/prosereflect/schema/attribute.rb +57 -0
  80. data/lib/prosereflect/schema/content_match.rb +656 -0
  81. data/lib/prosereflect/schema/fragment.rb +166 -0
  82. data/lib/prosereflect/schema/mark.rb +121 -0
  83. data/lib/prosereflect/schema/mark_type.rb +130 -0
  84. data/lib/prosereflect/schema/node.rb +236 -0
  85. data/lib/prosereflect/schema/node_type.rb +274 -0
  86. data/lib/prosereflect/schema/schema_main.rb +190 -0
  87. data/lib/prosereflect/schema/spec.rb +92 -0
  88. data/lib/prosereflect/schema.rb +39 -0
  89. data/lib/prosereflect/table.rb +12 -13
  90. data/lib/prosereflect/table_cell.rb +13 -13
  91. data/lib/prosereflect/table_header.rb +17 -17
  92. data/lib/prosereflect/table_row.rb +12 -12
  93. data/lib/prosereflect/text.rb +35 -11
  94. data/lib/prosereflect/transform/attr_step.rb +157 -0
  95. data/lib/prosereflect/transform/insert_step.rb +115 -0
  96. data/lib/prosereflect/transform/mapping.rb +82 -0
  97. data/lib/prosereflect/transform/mark_step.rb +269 -0
  98. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  99. data/lib/prosereflect/transform/replace_step.rb +157 -0
  100. data/lib/prosereflect/transform/slice.rb +91 -0
  101. data/lib/prosereflect/transform/step.rb +89 -0
  102. data/lib/prosereflect/transform/step_map.rb +126 -0
  103. data/lib/prosereflect/transform/structure.rb +120 -0
  104. data/lib/prosereflect/transform/transform.rb +341 -0
  105. data/lib/prosereflect/transform.rb +26 -0
  106. data/lib/prosereflect/user.rb +15 -15
  107. data/lib/prosereflect/version.rb +1 -1
  108. data/lib/prosereflect.rb +30 -17
  109. data/prosereflect.gemspec +17 -16
  110. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  111. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  112. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  113. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  114. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  115. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  116. data/spec/fixtures/documents/table_doc.yaml +32 -0
  117. data/spec/fixtures/documents/transform_test.yaml +14 -0
  118. data/spec/fixtures/schema/custom_schema.rb +37 -0
  119. data/spec/fixtures/schema/test_schema.rb +46 -0
  120. data/spec/fixtures/test_builder/helpers.rb +212 -0
  121. data/spec/prosereflect/document_spec.rb +332 -330
  122. data/spec/prosereflect/fragment_spec.rb +273 -0
  123. data/spec/prosereflect/hard_break_spec.rb +125 -125
  124. data/spec/prosereflect/input/html_spec.rb +718 -522
  125. data/spec/prosereflect/node_spec.rb +311 -182
  126. data/spec/prosereflect/output/html_spec.rb +105 -105
  127. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  128. data/spec/prosereflect/paragraph_spec.rb +275 -274
  129. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  130. data/spec/prosereflect/parser_spec.rb +185 -180
  131. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  132. data/spec/prosereflect/schema/conftest.rb +68 -0
  133. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  134. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  135. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  136. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  137. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  138. data/spec/prosereflect/table_cell_spec.rb +183 -183
  139. data/spec/prosereflect/table_row_spec.rb +149 -149
  140. data/spec/prosereflect/table_spec.rb +320 -318
  141. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  142. data/spec/prosereflect/text_spec.rb +133 -132
  143. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  144. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  145. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  146. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  147. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  148. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  149. data/spec/prosereflect/transform/step_spec.rb +211 -0
  150. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  151. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  152. data/spec/prosereflect/user_spec.rb +31 -28
  153. data/spec/prosereflect_spec.rb +28 -26
  154. data/spec/spec_helper.rb +7 -6
  155. data/spec/support/matchers.rb +6 -6
  156. data/spec/support/shared_examples.rb +49 -49
  157. metadata +96 -5
  158. data/spec/prosereflect/version_spec.rb +0 -11
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  # User class represents a user mention in ProseMirror.
7
5
  class User < Node
8
- PM_TYPE = 'user'
6
+ PM_TYPE = "user"
9
7
 
10
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
8
+ attribute :type, :string, default: -> {
9
+ self.class.send(:const_get, "PM_TYPE")
10
+ }
11
11
  attribute :id, :string
12
12
  attribute :attrs, :hash
13
13
 
14
14
  key_value do
15
- map 'type', to: :type, render_default: true
16
- map 'attrs', to: :attrs
17
- map 'content', to: :content
15
+ map "type", to: :type, render_default: true
16
+ map "attrs", to: :attrs
17
+ map "content", to: :content
18
18
  end
19
19
 
20
20
  def initialize(attributes = {})
@@ -23,8 +23,8 @@ module Prosereflect
23
23
 
24
24
  return unless attributes[:attrs]
25
25
 
26
- @id = attributes[:attrs]['id']
27
- self.attrs = { 'id' => @id }
26
+ @id = attributes[:attrs]["id"]
27
+ self.attrs = { "id" => @id }
28
28
  end
29
29
 
30
30
  def self.create(attrs = nil)
@@ -35,16 +35,16 @@ module Prosereflect
35
35
  def id=(user_id)
36
36
  @id = user_id
37
37
  self.attrs ||= {}
38
- attrs['id'] = user_id
38
+ attrs["id"] = user_id
39
39
  end
40
40
 
41
41
  def id
42
- @id || attrs&.[]('id')
42
+ @id || attrs&.[]("id")
43
43
  end
44
44
 
45
45
  # Override content-related methods since user mentions don't have content
46
46
  def add_child(*)
47
- raise NotImplementedError, 'User mention nodes cannot have children'
47
+ raise NotImplementedError, "User mention nodes cannot have children"
48
48
  end
49
49
 
50
50
  def content
@@ -53,10 +53,10 @@ module Prosereflect
53
53
 
54
54
  def to_h
55
55
  hash = super
56
- hash['attrs'] = {
57
- 'id' => id
56
+ hash["attrs"] = {
57
+ "id" => id,
58
58
  }
59
- hash['content'] = []
59
+ hash["content"] = []
60
60
  hash
61
61
  end
62
62
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prosereflect
4
- VERSION = '0.1.1'
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/prosereflect.rb CHANGED
@@ -1,23 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'prosereflect/version'
4
- require_relative 'prosereflect/node'
5
- require_relative 'prosereflect/mark'
6
- require_relative 'prosereflect/attribute'
7
- require_relative 'prosereflect/text'
8
- require_relative 'prosereflect/paragraph'
9
- require_relative 'prosereflect/hard_break'
10
- require_relative 'prosereflect/table'
11
- require_relative 'prosereflect/table_row'
12
- require_relative 'prosereflect/table_cell'
13
- require_relative 'prosereflect/heading'
14
- require_relative 'prosereflect/document'
15
- require_relative 'prosereflect/parser'
16
- require_relative 'prosereflect/input/html'
17
- require_relative 'prosereflect/output/html'
18
- require_relative 'prosereflect/user'
3
+ require "lutaml/model"
19
4
 
20
5
  module Prosereflect
21
6
  class Error < StandardError; end
22
- # 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 :Fragment, "prosereflect/fragment"
15
+ autoload :HardBreak, "prosereflect/hard_break"
16
+ autoload :Heading, "prosereflect/heading"
17
+ autoload :HorizontalRule, "prosereflect/horizontal_rule"
18
+ autoload :Image, "prosereflect/image"
19
+ autoload :Input, "prosereflect/input"
20
+ autoload :ListItem, "prosereflect/list_item"
21
+ autoload :Mark, "prosereflect/mark"
22
+ autoload :Node, "prosereflect/node"
23
+ autoload :OrderedList, "prosereflect/ordered_list"
24
+ autoload :Output, "prosereflect/output"
25
+ autoload :Paragraph, "prosereflect/paragraph"
26
+ autoload :Parser, "prosereflect/parser"
27
+ autoload :ResolvedPos, "prosereflect/resolved_pos"
28
+ autoload :Table, "prosereflect/table"
29
+ autoload :TableCell, "prosereflect/table_cell"
30
+ autoload :TableHeader, "prosereflect/table_header"
31
+ autoload :TableRow, "prosereflect/table_row"
32
+ autoload :Text, "prosereflect/text"
33
+ autoload :Transform, "prosereflect/transform"
34
+ autoload :User, "prosereflect/user"
35
+ autoload :VERSION, "prosereflect/version"
23
36
  end
data/prosereflect.gemspec CHANGED
@@ -1,34 +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 'nokogiri', '~> 1.18'
33
+ spec.add_dependency "lutaml-model", "~> 0.8"
34
+ spec.add_dependency "nokogiri", "~> 1.18"
34
35
  end
@@ -0,0 +1,14 @@
1
+ type: doc
2
+ content:
3
+ - type: paragraph
4
+ content:
5
+ - type: text
6
+ text: Hello
7
+ marks:
8
+ - type: bold
9
+ - type: text
10
+ text: " World"
11
+ - type: text
12
+ text: "!"
13
+ marks:
14
+ - type: italic
@@ -0,0 +1,16 @@
1
+ type: doc
2
+ content:
3
+ - type: heading
4
+ attrs:
5
+ level: 1
6
+ content:
7
+ - type: text
8
+ text: Title
9
+ - type: paragraph
10
+ content:
11
+ - type: text
12
+ text: First paragraph
13
+ - type: paragraph
14
+ content:
15
+ - type: text
16
+ text: Second paragraph
@@ -0,0 +1,32 @@
1
+ type: doc
2
+ content:
3
+ - type: bullet_list
4
+ content:
5
+ - type: list_item
6
+ content:
7
+ - type: paragraph
8
+ content:
9
+ - type: text
10
+ text: Bullet 1
11
+ - type: list_item
12
+ content:
13
+ - type: paragraph
14
+ content:
15
+ - type: text
16
+ text: Bullet 2
17
+ - type: ordered_list
18
+ attrs:
19
+ order: 1
20
+ content:
21
+ - type: list_item
22
+ content:
23
+ - type: paragraph
24
+ content:
25
+ - type: text
26
+ text: Item 1
27
+ - type: list_item
28
+ content:
29
+ - type: paragraph
30
+ content:
31
+ - type: text
32
+ text: Item 2
@@ -0,0 +1,40 @@
1
+ type: doc
2
+ content:
3
+ - type: heading
4
+ attrs:
5
+ level: 2
6
+ content:
7
+ - type: text
8
+ text: Mixed Content
9
+ - type: paragraph
10
+ content:
11
+ - type: text
12
+ text: Normal text
13
+ - type: text
14
+ text: " bold"
15
+ marks:
16
+ - type: bold
17
+ - type: text
18
+ text: " and "
19
+ - type: text
20
+ text: italic
21
+ marks:
22
+ - type: italic
23
+ - type: text
24
+ text: " with a "
25
+ - type: text
26
+ text: link
27
+ marks:
28
+ - type: link
29
+ attrs:
30
+ href: https://example.com
31
+ - type: code_block
32
+ attrs:
33
+ language: ruby
34
+ content:
35
+ - type: text
36
+ text: puts 'hello'
37
+ - type: image
38
+ attrs:
39
+ src: https://example.com/image.png
40
+ alt: Example image
@@ -0,0 +1,20 @@
1
+ type: doc
2
+ content:
3
+ - type: blockquote
4
+ content:
5
+ - type: paragraph
6
+ content:
7
+ - type: text
8
+ text: Quoted text
9
+ - type: paragraph
10
+ content:
11
+ - type: text
12
+ text: More quoted text
13
+ - type: horizontal_rule
14
+ - type: paragraph
15
+ content:
16
+ - type: text
17
+ text: "Line 1"
18
+ - type: hard_break
19
+ - type: text
20
+ text: "Line 2"
@@ -0,0 +1,6 @@
1
+ type: doc
2
+ content:
3
+ - type: paragraph
4
+ content:
5
+ - type: text
6
+ text: Hello World
@@ -0,0 +1,32 @@
1
+ type: doc
2
+ content:
3
+ - type: table
4
+ content:
5
+ - type: table_row
6
+ content:
7
+ - type: table_header
8
+ content:
9
+ - type: paragraph
10
+ content:
11
+ - type: text
12
+ text: Header 1
13
+ - type: table_header
14
+ content:
15
+ - type: paragraph
16
+ content:
17
+ - type: text
18
+ text: Header 2
19
+ - type: table_row
20
+ content:
21
+ - type: table_cell
22
+ content:
23
+ - type: paragraph
24
+ content:
25
+ - type: text
26
+ text: Cell 1
27
+ - type: table_cell
28
+ content:
29
+ - type: paragraph
30
+ content:
31
+ - type: text
32
+ text: Cell 2
@@ -0,0 +1,14 @@
1
+ type: doc
2
+ content:
3
+ - type: paragraph
4
+ content:
5
+ - type: text
6
+ text: abcdef
7
+ - type: paragraph
8
+ content:
9
+ - type: text
10
+ text: ghijkl
11
+ - type: paragraph
12
+ content:
13
+ - type: text
14
+ text: mnopqr
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prosereflect"
4
+ require "prosereflect/schema"
5
+
6
+ # Custom schema with mark exclusion rules
7
+ # Used for testing that link excludes emoji
8
+ module CustomSchemaFixture
9
+ CUSTOM_SCHEMA_NODES = {
10
+ "doc" => { "content" => "block+" },
11
+ "paragraph" => { "content" => "inline*" },
12
+ "text" => {},
13
+ }.freeze
14
+
15
+ CUSTOM_SCHEMA_MARKS = {
16
+ "link" => { "excludes" => "emoji" },
17
+ "bold" => {},
18
+ "italic" => {},
19
+ "emoji" => {},
20
+ }.freeze
21
+
22
+ def self.build
23
+ Prosereflect::Schema.new(
24
+ nodes_spec: CUSTOM_SCHEMA_NODES,
25
+ marks_spec: CUSTOM_SCHEMA_MARKS,
26
+ top_node: "doc",
27
+ )
28
+ end
29
+
30
+ def self.nodes
31
+ CUSTOM_SCHEMA_NODES
32
+ end
33
+
34
+ def self.marks
35
+ CUSTOM_SCHEMA_MARKS
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prosereflect"
4
+ require "prosereflect/schema"
5
+
6
+ # Test schema matching prosemirror-py test_schema
7
+ # Used for testing content expressions, mark handling, etc.
8
+ module TestSchemaFixture
9
+ TEST_SCHEMA_NODES = {
10
+ "doc" => { "content" => "block+" },
11
+ "paragraph" => { "content" => "inline*" },
12
+ "code_block" => { "content" => "text*", "marks" => "" },
13
+ "heading" => { "content" => "inline*", "attrs" => { "level" => { "default" => 1 } } },
14
+ "blockquote" => { "content" => "block+", "defining" => true },
15
+ "horizontal_rule" => {},
16
+ "text" => {},
17
+ "image" => { "attrs" => { "src" => {}, "alt" => { "default" => "" }, "title" => { "default" => "" } } },
18
+ "hard_break" => { "selectable" => false },
19
+ "ordered_list" => { "content" => "list_item+", "attrs" => { "order" => { "default" => 1 } }, "group" => "block" },
20
+ "bullet_list" => { "content" => "list_item+", "group" => "block" },
21
+ "list_item" => { "content" => "paragraph block*", "defining" => true },
22
+ }.freeze
23
+
24
+ TEST_SCHEMA_MARKS = {
25
+ "link" => { "attrs" => { "href" => {}, "title" => { "default" => "" } }, "inclusive" => false },
26
+ "em" => {},
27
+ "strong" => {},
28
+ "code" => {},
29
+ }.freeze
30
+
31
+ def self.build
32
+ Prosereflect::Schema.new(
33
+ nodes_spec: TEST_SCHEMA_NODES,
34
+ marks_spec: TEST_SCHEMA_MARKS,
35
+ top_node: "doc",
36
+ )
37
+ end
38
+
39
+ def self.nodes
40
+ TEST_SCHEMA_NODES
41
+ end
42
+
43
+ def self.marks
44
+ TEST_SCHEMA_MARKS
45
+ end
46
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TestBuilder - Port of prosemirror-py test_builder/build.py
4
+ # Provides helpers for creating test nodes from string representations
5
+
6
+ module TestBuilder
7
+ # Parse a test string into a document
8
+ # Examples:
9
+ # TestBuilder.parse("doc(p(\"hello world\"))")
10
+ # TestBuilder.parse("doc(p(\"hello<|a> world\"))")
11
+ def self.parse(str)
12
+ builder = Builder.new
13
+ builder.parse(str)
14
+ end
15
+
16
+ # Create a builder for a specific schema
17
+ def self.for_schema(schema)
18
+ Builder.new(schema: schema)
19
+ end
20
+
21
+ class Builder
22
+ attr_reader :schema
23
+
24
+ def initialize(schema: nil)
25
+ @schema = schema
26
+ end
27
+
28
+ def parse(str)
29
+ # Parse the string into a node tree
30
+ # This is a simplified parser for test strings like:
31
+ # doc(p("hello", em("world")))
32
+ # doc(p("hello<|a> world"))
33
+ content = extract_content(str)
34
+ return nil if content.nil?
35
+
36
+ parse_content(content)
37
+ end
38
+
39
+ def parse_content(str)
40
+ # Simple recursive descent parser for test strings
41
+ # Handles: doc(...), p(...), em(...), strong(...), etc.
42
+ # And string literals: "hello world"
43
+ tokens = tokenize(str)
44
+ parse_tokens(tokens)
45
+ end
46
+
47
+ private
48
+
49
+ def tokenize(str)
50
+ # Simple tokenizer
51
+ tokens = []
52
+ i = 0
53
+ while i < str.length
54
+ case str[i]
55
+ when /\s/
56
+ i += 1
57
+ when /[a-z_]/
58
+ # Identifier
59
+ start = i
60
+ i += 1
61
+ while i < str.length && str[i] =~ /[a-z0-9_]/
62
+ i += 1
63
+ end
64
+ tokens << [:identifier, str[start...i]]
65
+ when '"'
66
+ # String literal
67
+ i += 1
68
+ start = i
69
+ while i < str.length && str[i] != '"'
70
+ i += 1
71
+ end
72
+ tokens << [:string, str[start...i]]
73
+ i += 1 if i < str.length
74
+ when "(", ")", "<", ">", "|", ","
75
+ tokens << [str[i], str[i]]
76
+ i += 1
77
+ else
78
+ i += 1
79
+ end
80
+ end
81
+ tokens
82
+ end
83
+
84
+ def parse_tokens(tokens)
85
+ return nil if tokens.empty?
86
+
87
+ token = tokens.first
88
+ return nil unless token
89
+
90
+ if token[0] == :identifier
91
+ name = token[1]
92
+ tokens.shift
93
+ consume("(", tokens)
94
+ if name == "doc"
95
+ children = parse_children(tokens)
96
+ consume(")", tokens)
97
+ build_doc(children)
98
+ elsif name == "text"
99
+ str_token = tokens.shift
100
+ consume(")", tokens)
101
+ build_text(str_token ? str_token[1] : "")
102
+ else
103
+ # Assume it's a paragraph or other node
104
+ children = parse_children(tokens)
105
+ consume(")", tokens)
106
+ build_node(name, children)
107
+ end
108
+ elsif token[0] == :string
109
+ tokens.shift
110
+ build_text(token[1])
111
+ end
112
+ end
113
+
114
+ def parse_children(tokens)
115
+ children = []
116
+ while tokens.any? && tokens.first[0] != ")"
117
+ child = parse_tokens(tokens)
118
+ children << child if child
119
+ break if tokens.empty?
120
+
121
+ if tokens.first[0] == ","
122
+ tokens.shift
123
+ end
124
+ end
125
+ children
126
+ end
127
+
128
+ def consume(expected, tokens)
129
+ return if tokens.empty?
130
+
131
+ token = tokens.first
132
+ return unless token && token[0] == expected
133
+
134
+ tokens.shift
135
+ end
136
+
137
+ def build_doc(children)
138
+ nodes = children.flat_map do |child|
139
+ if child.is_a?(Array)
140
+ child
141
+ else
142
+ [child].compact
143
+ end
144
+ end
145
+ Prosereflect::Document.new(content: nodes)
146
+ end
147
+
148
+ def build_node(type, children)
149
+ case type
150
+ when "p"
151
+ content = children.flat_map do |child|
152
+ if child.is_a?(Prosereflect::Node)
153
+ [child]
154
+ else
155
+ []
156
+ end
157
+ end
158
+ Prosereflect::Paragraph.new(content: content)
159
+ when "text"
160
+ children.first || ""
161
+ else
162
+ # Generic node - return children as content
163
+ children.flatten.compact.first
164
+ end
165
+ end
166
+
167
+ def build_text(str)
168
+ return str if str.is_a?(String)
169
+
170
+ Prosereflect::Text.new(text: str.to_s)
171
+ end
172
+
173
+ def extract_content(str)
174
+ # Extract content between outermost parentheses
175
+ paren_depth = 0
176
+ start = nil
177
+ str.each_char.with_index do |char, i|
178
+ if char == "(" && paren_depth == 0
179
+ start = i + 1
180
+ paren_depth += 1
181
+ elsif char == "("
182
+ paren_depth += 1
183
+ elsif char == ")"
184
+ paren_depth -= 1
185
+ if paren_depth == 0 && start
186
+ return str[start...i]
187
+ end
188
+ end
189
+ end
190
+ nil
191
+ end
192
+ end
193
+
194
+ # Extract position markers from a test string
195
+ # Returns [content_string, positions_hash]
196
+ # Example: "doc(p(\"hello<|a> world<|b>\"))" => ["doc(p(\"hello world\"))", {"a" => 6, "b" => 12}]
197
+ def self.extract_markers(str)
198
+ positions = {}
199
+ marker_regex = /<\|([a-z0-9]+)>|<([0-9]+)>|<\|>/
200
+ result = str.gsub(marker_regex) do |_match|
201
+ if $1
202
+ positions[$1] = $~.offset(0)[0]
203
+ elsif $2
204
+ positions[$2.to_i] = $~.offset(0)[0]
205
+ else
206
+ positions[:cursor] = $~.offset(0)[0]
207
+ end
208
+ ""
209
+ end
210
+ [result, positions]
211
+ end
212
+ end