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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +97 -0
- data/.gitignore +4 -0
- data/.rubocop_todo.yml +61 -75
- data/README.adoc +2 -0
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +45 -0
- data/docs/_advanced/index.adoc +15 -0
- data/docs/_advanced/schema.adoc +112 -0
- data/docs/_advanced/step-map.adoc +66 -0
- data/docs/_advanced/steps.adoc +88 -0
- data/docs/_advanced/test-builder.adoc +61 -0
- data/docs/_advanced/transform.adoc +92 -0
- data/docs/_config.yml +174 -0
- data/docs/_features/html-input.adoc +69 -0
- data/docs/_features/html-output.adoc +45 -0
- data/docs/_features/index.adoc +15 -0
- data/docs/_features/marks.adoc +86 -0
- data/docs/_features/node-types.adoc +124 -0
- data/docs/_features/user-mentions.adoc +47 -0
- data/docs/_guides/custom-nodes.adoc +107 -0
- data/docs/_guides/index.adoc +13 -0
- data/docs/_guides/round-trip-html.adoc +91 -0
- data/docs/_guides/serialization.adoc +109 -0
- data/docs/_pages/index.adoc +67 -0
- data/docs/_reference/document-api.adoc +49 -0
- data/docs/_reference/index.adoc +14 -0
- data/docs/_reference/node-api.adoc +79 -0
- data/docs/_reference/schema-api.adoc +95 -0
- data/docs/_reference/transform-api.adoc +77 -0
- data/docs/_understanding/document-model.adoc +65 -0
- data/docs/_understanding/fragment.adoc +52 -0
- data/docs/_understanding/index.adoc +14 -0
- data/docs/_understanding/resolved-position.adoc +53 -0
- data/docs/_understanding/slice.adoc +54 -0
- data/docs/lychee.toml +63 -0
- data/lib/prosereflect/blockquote.rb +9 -0
- data/lib/prosereflect/bullet_list.rb +25 -19
- data/lib/prosereflect/code_block.rb +1 -5
- data/lib/prosereflect/fragment.rb +249 -0
- data/lib/prosereflect/horizontal_rule.rb +9 -0
- data/lib/prosereflect/image.rb +9 -0
- data/lib/prosereflect/input/html.rb +96 -0
- data/lib/prosereflect/node.rb +141 -3
- data/lib/prosereflect/ordered_list.rb +2 -0
- data/lib/prosereflect/output/html.rb +227 -0
- data/lib/prosereflect/parser.rb +9 -0
- data/lib/prosereflect/resolved_pos.rb +256 -0
- data/lib/prosereflect/schema/attribute.rb +57 -0
- data/lib/prosereflect/schema/content_match.rb +656 -0
- data/lib/prosereflect/schema/fragment.rb +166 -0
- data/lib/prosereflect/schema/mark.rb +121 -0
- data/lib/prosereflect/schema/mark_type.rb +130 -0
- data/lib/prosereflect/schema/node.rb +236 -0
- data/lib/prosereflect/schema/node_type.rb +274 -0
- data/lib/prosereflect/schema/schema_main.rb +190 -0
- data/lib/prosereflect/schema/spec.rb +92 -0
- data/lib/prosereflect/schema.rb +39 -0
- data/lib/prosereflect/text.rb +24 -0
- data/lib/prosereflect/transform/attr_step.rb +157 -0
- data/lib/prosereflect/transform/insert_step.rb +115 -0
- data/lib/prosereflect/transform/mapping.rb +82 -0
- data/lib/prosereflect/transform/mark_step.rb +269 -0
- data/lib/prosereflect/transform/replace_around_step.rb +181 -0
- data/lib/prosereflect/transform/replace_step.rb +157 -0
- data/lib/prosereflect/transform/slice.rb +91 -0
- data/lib/prosereflect/transform/step.rb +89 -0
- data/lib/prosereflect/transform/step_map.rb +126 -0
- data/lib/prosereflect/transform/structure.rb +120 -0
- data/lib/prosereflect/transform/transform.rb +341 -0
- data/lib/prosereflect/transform.rb +26 -0
- data/lib/prosereflect/version.rb +1 -1
- data/lib/prosereflect.rb +3 -0
- data/spec/fixtures/documents/formatted_text.yaml +14 -0
- data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
- data/spec/fixtures/documents/lists_doc.yaml +32 -0
- data/spec/fixtures/documents/mixed_content.yaml +40 -0
- data/spec/fixtures/documents/nested_doc.yaml +20 -0
- data/spec/fixtures/documents/simple_doc.yaml +6 -0
- data/spec/fixtures/documents/table_doc.yaml +32 -0
- data/spec/fixtures/documents/transform_test.yaml +14 -0
- data/spec/fixtures/schema/custom_schema.rb +37 -0
- data/spec/fixtures/schema/test_schema.rb +46 -0
- data/spec/fixtures/test_builder/helpers.rb +212 -0
- data/spec/prosereflect/document_spec.rb +1 -1
- data/spec/prosereflect/fragment_spec.rb +273 -0
- data/spec/prosereflect/input/html_spec.rb +197 -1
- data/spec/prosereflect/node_spec.rb +128 -0
- data/spec/prosereflect/output/whitespace_spec.rb +248 -0
- data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
- data/spec/prosereflect/resolved_pos_spec.rb +74 -0
- data/spec/prosereflect/schema/conftest.rb +68 -0
- data/spec/prosereflect/schema/content_match_spec.rb +237 -0
- data/spec/prosereflect/schema/mark_spec.rb +274 -0
- data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
- data/spec/prosereflect/schema/node_type_spec.rb +142 -0
- data/spec/prosereflect/schema/schema_spec.rb +194 -0
- data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
- data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
- data/spec/prosereflect/transform/mapping_spec.rb +226 -0
- data/spec/prosereflect/transform/replace_spec.rb +832 -0
- data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
- data/spec/prosereflect/transform/slice_spec.rb +48 -0
- data/spec/prosereflect/transform/step_map_spec.rb +70 -0
- data/spec/prosereflect/transform/step_spec.rb +211 -0
- data/spec/prosereflect/transform/structure_spec.rb +98 -0
- data/spec/prosereflect/transform/transform_spec.rb +238 -0
- data/spec/spec_helper.rb +1 -0
- 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
|