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,22 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'table_cell'
4
-
5
3
  module Prosereflect
6
4
  # TableHeader class represents a header cell in a table (<th> tag).
7
5
  # It inherits from TableCell but adds header-specific attributes.
8
6
  class TableHeader < TableCell
9
- PM_TYPE = 'table_header'
7
+ PM_TYPE = "table_header"
10
8
 
11
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
9
+ attribute :type, :string, default: -> {
10
+ self.class.send(:const_get, "PM_TYPE")
11
+ }
12
12
  attribute :scope, :string # row, col, rowgroup, or colgroup
13
13
  attribute :abbr, :string # abbreviated version of content
14
14
  attribute :colspan, :integer # number of columns this header spans
15
15
 
16
16
  key_value do
17
- map 'type', to: :type, render_default: true
18
- map 'content', to: :content
19
- map 'attrs', to: :attrs
17
+ map "type", to: :type, render_default: true
18
+ map "content", to: :content
19
+ map "attrs", to: :attrs
20
20
  end
21
21
 
22
22
  def initialize(attributes = {})
@@ -41,21 +41,21 @@ module Prosereflect
41
41
  return unless %w[row col rowgroup colgroup].include?(scope_value)
42
42
 
43
43
  self.attrs ||= {}
44
- attrs['scope'] = scope_value
44
+ attrs["scope"] = scope_value
45
45
  end
46
46
 
47
47
  def scope
48
- attrs&.[]('scope')
48
+ attrs&.[]("scope")
49
49
  end
50
50
 
51
51
  # Set abbreviated version of the header content
52
52
  def abbr=(abbr_text)
53
53
  self.attrs ||= {}
54
- attrs['abbr'] = abbr_text
54
+ attrs["abbr"] = abbr_text
55
55
  end
56
56
 
57
57
  def abbr
58
- attrs&.[]('abbr')
58
+ attrs&.[]("abbr")
59
59
  end
60
60
 
61
61
  # Set the number of columns this header spans
@@ -63,11 +63,11 @@ module Prosereflect
63
63
  return unless span.to_i.positive?
64
64
 
65
65
  self.attrs ||= {}
66
- attrs['colspan'] = span.to_i
66
+ attrs["colspan"] = span.to_i
67
67
  end
68
68
 
69
69
  def colspan
70
- attrs&.[]('colspan')
70
+ attrs&.[]("colspan")
71
71
  end
72
72
 
73
73
  # Get header attributes as a hash
@@ -75,16 +75,16 @@ module Prosereflect
75
75
  {
76
76
  scope: scope,
77
77
  abbr: abbr,
78
- colspan: colspan
78
+ colspan: colspan,
79
79
  }.compact
80
80
  end
81
81
 
82
82
  # Override to_h to exclude nil attributes
83
83
  def to_h
84
84
  result = super
85
- if result['attrs']
86
- result['attrs'].reject! { |_, v| v.nil? }
87
- result.delete('attrs') if result['attrs'].empty?
85
+ if result["attrs"]
86
+ result["attrs"].compact!
87
+ result.delete("attrs") if result["attrs"].empty?
88
88
  end
89
89
  result
90
90
  end
@@ -1,18 +1,17 @@
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
- PM_TYPE = 'table_row'
5
+ PM_TYPE = "table_row"
9
6
 
10
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
11
10
 
12
11
  key_value do
13
- map 'type', to: :type, render_default: true
14
- map 'content', to: :content
15
- map 'attrs', to: :attrs
12
+ map "type", to: :type, render_default: true
13
+ map "content", to: :content
14
+ map "attrs", to: :attrs
16
15
  end
17
16
 
18
17
  def initialize(opts = {})
@@ -44,10 +43,11 @@ module Prosereflect
44
43
  # Override to_h to handle empty content and attributes properly
45
44
  def to_h
46
45
  result = super
47
- result['content'] ||= []
48
- if result['attrs']
49
- result['attrs'] = result['attrs'].is_a?(Hash) && result['attrs'][:attrs] ? result['attrs'][:attrs] : result['attrs']
50
- result.delete('attrs') if result['attrs'].empty?
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
51
  end
52
52
  result
53
53
  end
@@ -1,32 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
-
5
3
  module Prosereflect
6
4
  class Text < Node
7
- PM_TYPE = 'text'
5
+ PM_TYPE = "text"
8
6
 
9
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
10
- attribute :text, :string, default: ''
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
10
+ attribute :text, :string, default: ""
11
11
 
12
12
  key_value do
13
- map 'type', to: :type, render_default: true
14
- map 'text', to: :text
15
- map 'marks', to: :marks
13
+ map "type", to: :type, render_default: true
14
+ map "text", to: :text
15
+ map "marks", to: :marks
16
16
  end
17
17
 
18
- def self.create(text = '', marks = nil)
18
+ def self.create(text = "", marks = nil)
19
19
  new(text: text, marks: marks)
20
20
  end
21
21
 
22
22
  def text_content
23
- text || ''
23
+ text || ""
24
+ end
25
+
26
+ # Text node size is text length + 1 (for the opening token)
27
+ def node_size
28
+ (text || "").length + 1
29
+ end
30
+
31
+ # Text nodes are text nodes
32
+ def text?
33
+ true
34
+ end
35
+
36
+ # Return a copy of this text node with content restricted to range
37
+ def cut(from = 0, to = nil)
38
+ txt = text || ""
39
+ to ||= txt.length
40
+ self.class.new(text: txt[from...to], marks: raw_marks)
41
+ end
42
+
43
+ # Check equality with another text node
44
+ def eq?(other)
45
+ return false unless other.is_a?(self.class)
46
+
47
+ text == other.text && to_h == other.to_h
24
48
  end
25
49
 
26
50
  # Override the to_h method to include the text attribute
27
51
  def to_h
28
52
  result = super
29
- result['text'] = text
53
+ result["text"] = text
30
54
  result
31
55
  end
32
56
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "step"
4
+ require_relative "step_map"
5
+
6
+ module Prosereflect
7
+ module Transform
8
+ # Set or remove attributes on a node at a position
9
+ class AttrStep < Step
10
+ attr_reader :pos, :attrs
11
+
12
+ def initialize(pos, attrs)
13
+ super()
14
+ @pos = pos
15
+ @attrs = attrs
16
+ end
17
+
18
+ def apply(doc)
19
+ return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
20
+
21
+ begin
22
+ new_doc = set_node_attrs(doc)
23
+ Result.ok(new_doc)
24
+ rescue StandardError => e
25
+ Result.fail(e.message)
26
+ end
27
+ end
28
+
29
+ def get_map
30
+ StepMap.new
31
+ end
32
+
33
+ def invert(doc)
34
+ # Find what attrs were changed and revert them
35
+ old_attrs = get_old_attrs(doc)
36
+ AttrStep.new(@pos, old_attrs)
37
+ end
38
+
39
+ def step_type
40
+ "setAttr"
41
+ end
42
+
43
+ def to_json(*_args)
44
+ json = super
45
+ json["pos"] = @pos
46
+ json["attrs"] = @attrs
47
+ json
48
+ end
49
+
50
+ def self.from_json(_schema, json)
51
+ new(json["pos"], json["attrs"])
52
+ end
53
+
54
+ private
55
+
56
+ def set_node_attrs(doc)
57
+ target_node = find_node_at(doc, @pos)
58
+ return doc unless target_node
59
+
60
+ new_attrs = compute_new_attrs(target_node)
61
+ replace_node_with_new_attrs(doc, target_node, new_attrs)
62
+ end
63
+
64
+ def compute_new_attrs(target_node)
65
+ new_attrs = target_node.attrs.merge(@attrs)
66
+ new_attrs.compact!
67
+ new_attrs
68
+ end
69
+
70
+ def replace_node_with_new_attrs(doc, target_node, new_attrs)
71
+ new_content = doc.content.to_a.map { |node| replace_node(node, target_node, new_attrs) }
72
+ doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
73
+ end
74
+
75
+ def replace_node(node, target_node, new_attrs)
76
+ return node unless node == target_node
77
+
78
+ node.class.new(
79
+ content: node.content,
80
+ marks: node.marks,
81
+ attrs: new_attrs,
82
+ )
83
+ end
84
+
85
+ def find_node_at(doc, pos)
86
+ result = nil
87
+ doc.nodes_between(pos, pos + 1) do |node|
88
+ result = node
89
+ end
90
+ result
91
+ end
92
+
93
+ def get_old_attrs(doc)
94
+ target_node = find_node_at(doc, @pos)
95
+ return {} unless target_node
96
+
97
+ # Return only the attrs that we're changing
98
+ @attrs.keys.each_with_object({}) do |key, old|
99
+ old[key] = target_node.attrs[key] if target_node.attrs.key?(key)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Set or remove document-level attributes
105
+ class DocAttrStep < Step
106
+ attr_reader :attrs
107
+
108
+ def initialize(attrs)
109
+ super()
110
+ @attrs = attrs
111
+ end
112
+
113
+ def apply(doc)
114
+ new_doc = set_doc_attrs(doc)
115
+ Result.ok(new_doc)
116
+ rescue StandardError => e
117
+ Result.fail(e.message)
118
+ end
119
+
120
+ def get_map
121
+ StepMap.new
122
+ end
123
+
124
+ def invert(doc)
125
+ old_attrs = get_old_doc_attrs(doc)
126
+ DocAttrStep.new(old_attrs)
127
+ end
128
+
129
+ def step_type
130
+ "setDocAttr"
131
+ end
132
+
133
+ def to_json(*_args)
134
+ json = super
135
+ json["attrs"] = @attrs
136
+ json
137
+ end
138
+
139
+ def self.from_json(_schema, json)
140
+ new(json["attrs"])
141
+ end
142
+
143
+ private
144
+
145
+ def set_doc_attrs(doc)
146
+ new_attrs = doc.attrs.merge(@attrs).compact
147
+ doc.class.new(content: doc.content, attrs: new_attrs)
148
+ end
149
+
150
+ def get_old_doc_attrs(doc)
151
+ @attrs.keys.each_with_object({}) do |key, old|
152
+ old[key] = doc.attrs[key] if doc.attrs.key?(key)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "step"
4
+ require_relative "step_map"
5
+ require_relative "replace_step"
6
+ require_relative "slice"
7
+
8
+ module Prosereflect
9
+ module Transform
10
+ # Insert content at a position
11
+ class InsertStep < Step
12
+ attr_reader :pos, :content
13
+
14
+ def initialize(pos, content)
15
+ super()
16
+ @pos = pos
17
+ @content = content.is_a?(Fragment) ? content : Fragment.new(content)
18
+ end
19
+
20
+ def apply(doc)
21
+ return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
22
+
23
+ begin
24
+ slice = Slice.new(@content)
25
+ replace_step = ReplaceStep.new(@pos, @pos, slice)
26
+ replace_step.apply(doc)
27
+ rescue StandardError => e
28
+ Result.fail(e.message)
29
+ end
30
+ end
31
+
32
+ def get_map
33
+ delta = @content.size
34
+ StepMap.new([[@pos, @pos, @pos, @pos + delta]])
35
+ end
36
+
37
+ def invert(_doc)
38
+ DeleteStep.new(@pos, @pos + @content.size)
39
+ end
40
+
41
+ def step_type
42
+ "insert"
43
+ end
44
+
45
+ def to_json(*_args)
46
+ json = super
47
+ json["pos"] = @pos
48
+ json["content"] = @content.to_a.map(&:to_h)
49
+ json
50
+ end
51
+
52
+ def self.from_json(_schema, json)
53
+ content = (json["content"] || []).map { |h| Prosereflect::Node.from_h(h) }
54
+ new(json["pos"], Fragment.new(content))
55
+ end
56
+ end
57
+
58
+ # Delete content in a range
59
+ class DeleteStep < Step
60
+ attr_reader :from, :to
61
+
62
+ def initialize(from, to)
63
+ super()
64
+ @from = from
65
+ @to = to
66
+ end
67
+
68
+ def apply(doc)
69
+ return Result.fail("Invalid positions") if @from > @to
70
+ return Result.fail("from < 0") if @from.negative?
71
+ return Result.fail("to > doc size") if @to > doc.node_size
72
+
73
+ begin
74
+ replace_step = ReplaceStep.new(@from, @to, Slice.empty)
75
+ replace_step.apply(doc)
76
+ rescue StandardError => e
77
+ Result.fail(e.message)
78
+ end
79
+ end
80
+
81
+ def get_map
82
+ StepMap.delete(@from, @to)
83
+ end
84
+
85
+ def invert(doc)
86
+ # Find what was deleted
87
+ deleted = content_between(doc, @from, @to)
88
+ InsertStep.new(@from, deleted)
89
+ end
90
+
91
+ def step_type
92
+ "delete"
93
+ end
94
+
95
+ def to_json(*_args)
96
+ json = super
97
+ json["from"] = @from
98
+ json["to"] = @to
99
+ json
100
+ end
101
+
102
+ def self.from_json(_schema, json)
103
+ new(json["from"], json["to"])
104
+ end
105
+
106
+ private
107
+
108
+ def content_between(doc, from, to)
109
+ result = []
110
+ doc.nodes_between(from, to) { |node| result << node }
111
+ Fragment.new(result)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "step_map"
4
+
5
+ module Prosereflect
6
+ module Transform
7
+ # Tracks position changes through a series of steps.
8
+ # Maps positions forward through the transformation.
9
+ class Mapping
10
+ attr_reader :maps
11
+ attr_accessor :from, :to
12
+
13
+ def initialize(maps: [])
14
+ @maps = maps.dup
15
+ @from = 0
16
+ @to = maps.length
17
+ end
18
+
19
+ # Add a step map to this mapping
20
+ def add_map(step_map, index = nil)
21
+ if index
22
+ @maps.insert(index, step_map)
23
+ else
24
+ @maps << step_map
25
+ end
26
+ @to = @maps.length
27
+ end
28
+
29
+ # Map a position through all steps in this mapping
30
+ def map(pos, on_del: nil) # rubocop:disable Lint:UnusedMethodArgument
31
+ @maps.each do |step_map|
32
+ pos = step_map.map(pos)
33
+ end
34
+ pos
35
+ end
36
+
37
+ # Map a position with deletion tracking
38
+ def map_result(pos, on_del: nil)
39
+ deleted = false
40
+ @maps.each do |step_map|
41
+ result = step_map.map_result(pos, on_del: on_del)
42
+ deleted ||= result.deleted
43
+ pos = result.pos
44
+ end
45
+ { pos: pos, deleted: deleted }
46
+ end
47
+
48
+ # Map a position backwards through the mapping
49
+ def map_reverse(pos)
50
+ result = pos
51
+ (0...@maps.length).each do |i|
52
+ step_map = @maps[@maps.length - 1 - i]
53
+ result = step_map.map_reverse(result)
54
+ end
55
+ result
56
+ end
57
+
58
+ # Check if a position was deleted
59
+ def map_deletes(pos)
60
+ @maps.any? { |step_map| step_map.deleted?(pos) }
61
+ end
62
+
63
+ # Get the mapping as an array of step maps
64
+ def to_a
65
+ @maps.dup
66
+ end
67
+
68
+ # Create from a single step map
69
+ def self.from_step_map(step_map)
70
+ new(maps: [step_map])
71
+ end
72
+
73
+ def to_s
74
+ "<Mapping maps=#{@maps.length}>"
75
+ end
76
+
77
+ def inspect
78
+ to_s
79
+ end
80
+ end
81
+ end
82
+ end