prosereflect 0.2.0 → 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 (110) 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/.gitignore +4 -0
  5. data/.rubocop_todo.yml +61 -75
  6. data/README.adoc +2 -0
  7. data/docs/Gemfile +10 -0
  8. data/docs/INDEX.adoc +45 -0
  9. data/docs/_advanced/index.adoc +15 -0
  10. data/docs/_advanced/schema.adoc +112 -0
  11. data/docs/_advanced/step-map.adoc +66 -0
  12. data/docs/_advanced/steps.adoc +88 -0
  13. data/docs/_advanced/test-builder.adoc +61 -0
  14. data/docs/_advanced/transform.adoc +92 -0
  15. data/docs/_config.yml +174 -0
  16. data/docs/_features/html-input.adoc +69 -0
  17. data/docs/_features/html-output.adoc +45 -0
  18. data/docs/_features/index.adoc +15 -0
  19. data/docs/_features/marks.adoc +86 -0
  20. data/docs/_features/node-types.adoc +124 -0
  21. data/docs/_features/user-mentions.adoc +47 -0
  22. data/docs/_guides/custom-nodes.adoc +107 -0
  23. data/docs/_guides/index.adoc +13 -0
  24. data/docs/_guides/round-trip-html.adoc +91 -0
  25. data/docs/_guides/serialization.adoc +109 -0
  26. data/docs/_pages/index.adoc +67 -0
  27. data/docs/_reference/document-api.adoc +49 -0
  28. data/docs/_reference/index.adoc +14 -0
  29. data/docs/_reference/node-api.adoc +79 -0
  30. data/docs/_reference/schema-api.adoc +95 -0
  31. data/docs/_reference/transform-api.adoc +77 -0
  32. data/docs/_understanding/document-model.adoc +65 -0
  33. data/docs/_understanding/fragment.adoc +52 -0
  34. data/docs/_understanding/index.adoc +14 -0
  35. data/docs/_understanding/resolved-position.adoc +53 -0
  36. data/docs/_understanding/slice.adoc +54 -0
  37. data/docs/lychee.toml +63 -0
  38. data/lib/prosereflect/blockquote.rb +9 -0
  39. data/lib/prosereflect/bullet_list.rb +25 -19
  40. data/lib/prosereflect/code_block.rb +1 -5
  41. data/lib/prosereflect/fragment.rb +249 -0
  42. data/lib/prosereflect/horizontal_rule.rb +9 -0
  43. data/lib/prosereflect/image.rb +9 -0
  44. data/lib/prosereflect/input/html.rb +96 -0
  45. data/lib/prosereflect/node.rb +141 -3
  46. data/lib/prosereflect/ordered_list.rb +2 -0
  47. data/lib/prosereflect/output/html.rb +227 -0
  48. data/lib/prosereflect/parser.rb +9 -0
  49. data/lib/prosereflect/resolved_pos.rb +256 -0
  50. data/lib/prosereflect/schema/attribute.rb +57 -0
  51. data/lib/prosereflect/schema/content_match.rb +656 -0
  52. data/lib/prosereflect/schema/fragment.rb +166 -0
  53. data/lib/prosereflect/schema/mark.rb +121 -0
  54. data/lib/prosereflect/schema/mark_type.rb +130 -0
  55. data/lib/prosereflect/schema/node.rb +236 -0
  56. data/lib/prosereflect/schema/node_type.rb +274 -0
  57. data/lib/prosereflect/schema/schema_main.rb +190 -0
  58. data/lib/prosereflect/schema/spec.rb +92 -0
  59. data/lib/prosereflect/schema.rb +39 -0
  60. data/lib/prosereflect/text.rb +24 -0
  61. data/lib/prosereflect/transform/attr_step.rb +157 -0
  62. data/lib/prosereflect/transform/insert_step.rb +115 -0
  63. data/lib/prosereflect/transform/mapping.rb +82 -0
  64. data/lib/prosereflect/transform/mark_step.rb +269 -0
  65. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  66. data/lib/prosereflect/transform/replace_step.rb +157 -0
  67. data/lib/prosereflect/transform/slice.rb +91 -0
  68. data/lib/prosereflect/transform/step.rb +89 -0
  69. data/lib/prosereflect/transform/step_map.rb +126 -0
  70. data/lib/prosereflect/transform/structure.rb +120 -0
  71. data/lib/prosereflect/transform/transform.rb +341 -0
  72. data/lib/prosereflect/transform.rb +26 -0
  73. data/lib/prosereflect/version.rb +1 -1
  74. data/lib/prosereflect.rb +3 -0
  75. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  76. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  77. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  78. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  79. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  80. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  81. data/spec/fixtures/documents/table_doc.yaml +32 -0
  82. data/spec/fixtures/documents/transform_test.yaml +14 -0
  83. data/spec/fixtures/schema/custom_schema.rb +37 -0
  84. data/spec/fixtures/schema/test_schema.rb +46 -0
  85. data/spec/fixtures/test_builder/helpers.rb +212 -0
  86. data/spec/prosereflect/document_spec.rb +1 -1
  87. data/spec/prosereflect/fragment_spec.rb +273 -0
  88. data/spec/prosereflect/input/html_spec.rb +197 -1
  89. data/spec/prosereflect/node_spec.rb +128 -0
  90. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  91. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  92. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  93. data/spec/prosereflect/schema/conftest.rb +68 -0
  94. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  95. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  96. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  97. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  98. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  99. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  100. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  101. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  102. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  103. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  104. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  105. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  106. data/spec/prosereflect/transform/step_spec.rb +211 -0
  107. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  108. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  109. data/spec/spec_helper.rb +1 -0
  110. metadata +90 -2
@@ -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
@@ -0,0 +1,269 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "step"
4
+ require_relative "step_map"
5
+
6
+ module Prosereflect
7
+ module Transform
8
+ # Base class for mark-related steps
9
+ class MarkStep < Step
10
+ attr_reader :from, :to, :mark
11
+
12
+ def initialize(from, to, mark)
13
+ super()
14
+ @from = from
15
+ @to = to
16
+ @mark = mark
17
+ end
18
+
19
+ def get_map
20
+ StepMap.new
21
+ end
22
+ end
23
+
24
+ # Add a mark to all content in a range
25
+ class AddMarkStep < MarkStep
26
+ def apply(doc)
27
+ return Result.fail("Invalid positions") if @from > @to || @from.negative?
28
+
29
+ begin
30
+ new_doc = add_mark_to_range(doc)
31
+ Result.ok(new_doc)
32
+ rescue StandardError => e
33
+ Result.fail(e.message)
34
+ end
35
+ end
36
+
37
+ def invert(_doc)
38
+ RemoveMarkStep.new(@from, @to, @mark)
39
+ end
40
+
41
+ def merge(other)
42
+ return nil unless other.is_a?(AddMarkStep)
43
+ return nil unless other.mark == @mark
44
+
45
+ if @to == other.from
46
+ AddMarkStep.new(@from, other.to, @mark)
47
+ elsif @from == other.to
48
+ AddMarkStep.new(other.from, @to, @mark)
49
+ end
50
+ end
51
+
52
+ def step_type
53
+ "addMark"
54
+ end
55
+
56
+ def to_json(*_args)
57
+ json = super
58
+ json["mark"] = @mark.to_h
59
+ json
60
+ end
61
+
62
+ def self.from_json(_schema, json)
63
+ mark = Prosereflect::Mark.from_h(json["mark"])
64
+ new(json["from"], json["to"], mark)
65
+ end
66
+
67
+ private
68
+
69
+ def add_mark_to_range(doc)
70
+ new_content = doc.content.map { |node| apply_mark_to_node(node) }
71
+ doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
72
+ end
73
+
74
+ def apply_mark_to_node(node)
75
+ return node unless node.is_a?(Prosereflect::Text)
76
+
77
+ Prosereflect::Text.new(
78
+ text: node.text,
79
+ marks: (node.marks || []) + [@mark],
80
+ attrs: node.attrs.dup,
81
+ )
82
+ end
83
+
84
+ def remove_mark_from_range(doc)
85
+ new_content = doc.content.map { |node| remove_mark_from_node_single(node) }
86
+ doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
87
+ end
88
+
89
+ def remove_mark_from_node_single(node)
90
+ return node unless node.is_a?(Prosereflect::Text)
91
+
92
+ new_marks = (node.marks || []).reject { |m| m.type == @mark.type }
93
+ Prosereflect::Text.new(
94
+ text: node.text,
95
+ marks: new_marks,
96
+ attrs: node.attrs.dup,
97
+ )
98
+ end
99
+ end
100
+
101
+ # Remove a mark from all content in a range
102
+ class RemoveMarkStep < MarkStep
103
+ def apply(doc)
104
+ return Result.fail("Invalid positions") if @from > @to || @from.negative?
105
+
106
+ begin
107
+ new_doc = remove_mark_from_range(doc)
108
+ Result.ok(new_doc)
109
+ rescue StandardError => e
110
+ Result.fail(e.message)
111
+ end
112
+ end
113
+
114
+ def invert(_doc)
115
+ AddMarkStep.new(@from, @to, @mark)
116
+ end
117
+
118
+ def merge(other)
119
+ return nil unless other.is_a?(RemoveMarkStep)
120
+ return nil unless other.mark == @mark
121
+
122
+ if @to == other.from
123
+ RemoveMarkStep.new(@from, other.to, @mark)
124
+ elsif @from == other.to
125
+ RemoveMarkStep.new(other.from, @to, @mark)
126
+ end
127
+ end
128
+
129
+ def step_type
130
+ "removeMark"
131
+ end
132
+
133
+ def to_json(*_args)
134
+ json = super
135
+ json["mark"] = @mark.to_h
136
+ json
137
+ end
138
+
139
+ def self.from_json(_schema, json)
140
+ mark = Prosereflect::Mark.from_h(json["mark"])
141
+ new(json["from"], json["to"], mark)
142
+ end
143
+ end
144
+
145
+ # Add mark to a specific node (not range-based)
146
+ class AddNodeMarkStep < Step
147
+ attr_reader :pos, :mark
148
+
149
+ def initialize(pos, mark)
150
+ super()
151
+ @pos = pos
152
+ @mark = mark
153
+ end
154
+
155
+ def apply(doc)
156
+ return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
157
+
158
+ begin
159
+ new_doc = add_mark_to_node(doc)
160
+ Result.ok(new_doc)
161
+ rescue StandardError => e
162
+ Result.fail(e.message)
163
+ end
164
+ end
165
+
166
+ def get_map
167
+ StepMap.new
168
+ end
169
+
170
+ def invert(_doc)
171
+ RemoveNodeMarkStep.new(@pos, @mark)
172
+ end
173
+
174
+ def step_type
175
+ "addNodeMark"
176
+ end
177
+
178
+ def to_json(*_args)
179
+ json = super
180
+ json["pos"] = @pos
181
+ json["mark"] = @mark.to_h
182
+ json
183
+ end
184
+
185
+ def self.from_json(_schema, json)
186
+ mark = Prosereflect::Mark.from_h(json["mark"])
187
+ new(json["pos"], mark)
188
+ end
189
+
190
+ private
191
+
192
+ def add_mark_to_node(doc)
193
+ new_content = doc.content.map { |node| add_mark_to_single_node(node) }
194
+ doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
195
+ end
196
+
197
+ def add_mark_to_single_node(node)
198
+ new_marks = (node.marks || []) + [@mark]
199
+ node.class.new(
200
+ content: node.content,
201
+ marks: new_marks,
202
+ attrs: node.attrs.dup,
203
+ )
204
+ end
205
+ end
206
+
207
+ # Remove mark from a specific node
208
+ class RemoveNodeMarkStep < Step
209
+ attr_reader :pos, :mark
210
+
211
+ def initialize(pos, mark)
212
+ super()
213
+ @pos = pos
214
+ @mark = mark
215
+ end
216
+
217
+ def apply(doc)
218
+ return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
219
+
220
+ begin
221
+ new_doc = remove_mark_from_node(doc)
222
+ Result.ok(new_doc)
223
+ rescue StandardError => e
224
+ Result.fail(e.message)
225
+ end
226
+ end
227
+
228
+ def get_map
229
+ StepMap.new
230
+ end
231
+
232
+ def invert(_doc)
233
+ AddNodeMarkStep.new(@pos, @mark)
234
+ end
235
+
236
+ def step_type
237
+ "removeNodeMark"
238
+ end
239
+
240
+ def to_json(*_args)
241
+ json = super
242
+ json["pos"] = @pos
243
+ json["mark"] = @mark.to_h
244
+ json
245
+ end
246
+
247
+ def self.from_json(_schema, json)
248
+ mark = Prosereflect::Mark.from_h(json["mark"])
249
+ new(json["pos"], mark)
250
+ end
251
+
252
+ private
253
+
254
+ def remove_mark_from_node(doc)
255
+ new_content = doc.content.map { |node| remove_mark_from_single_node(node) }
256
+ doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
257
+ end
258
+
259
+ def remove_mark_from_single_node(node)
260
+ new_marks = (node.marks || []).reject { |m| m.type == @mark.type }
261
+ node.class.new(
262
+ content: node.content,
263
+ marks: new_marks,
264
+ attrs: node.attrs.dup,
265
+ )
266
+ end
267
+ end
268
+ end
269
+ end