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,832 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "Replace step operations" do # rubocop:disable RSpec/DescribeClass
|
|
6
|
+
# Helper to build a simple doc with one paragraph containing text
|
|
7
|
+
def build_doc_with_text(text)
|
|
8
|
+
Prosereflect::Parser.parse_document(
|
|
9
|
+
"type" => "doc",
|
|
10
|
+
"content" => [
|
|
11
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => text }] },
|
|
12
|
+
],
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Helper to build a doc with multiple paragraphs
|
|
17
|
+
def build_doc_with_paragraphs(*texts)
|
|
18
|
+
Prosereflect::Parser.parse_document(
|
|
19
|
+
"type" => "doc",
|
|
20
|
+
"content" => texts.map do |t|
|
|
21
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => t }] }
|
|
22
|
+
end,
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# doc=1 + para=1 + text("Hello")=6 = 8 total
|
|
27
|
+
# Positions: 0=doc start, 1=para start, 2="H", 3="e", 4="l", 5="l", 6="o", 7=para end
|
|
28
|
+
|
|
29
|
+
describe "#apply" do
|
|
30
|
+
context "when deleting text" do
|
|
31
|
+
it "deletes a range of text within a paragraph" do
|
|
32
|
+
doc = build_doc_with_text("Hello")
|
|
33
|
+
# Delete "ell" (positions 3-6)
|
|
34
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
35
|
+
result = step.apply(doc)
|
|
36
|
+
|
|
37
|
+
expect(result).to be_ok
|
|
38
|
+
expect(result.doc).to be_a(Prosereflect::Document)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "deletes at the start of the document" do
|
|
42
|
+
doc = build_doc_with_text("abc")
|
|
43
|
+
# Delete "ab" (positions 2-4)
|
|
44
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 4, Prosereflect::Transform::Slice.empty)
|
|
45
|
+
result = step.apply(doc)
|
|
46
|
+
|
|
47
|
+
expect(result).to be_ok
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "deletes at the end of the document" do
|
|
51
|
+
doc = build_doc_with_text("abc")
|
|
52
|
+
# Delete "bc" (positions 3-5)
|
|
53
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 5, Prosereflect::Transform::Slice.empty)
|
|
54
|
+
result = step.apply(doc)
|
|
55
|
+
|
|
56
|
+
expect(result).to be_ok
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "deletes an entire paragraph" do
|
|
60
|
+
doc = build_doc_with_text("abc")
|
|
61
|
+
# doc=1, para=1, text("abc")=4, total=6
|
|
62
|
+
# Delete entire paragraph range (1-5)
|
|
63
|
+
step = Prosereflect::Transform::ReplaceStep.new(1, 5, Prosereflect::Transform::Slice.empty)
|
|
64
|
+
result = step.apply(doc)
|
|
65
|
+
|
|
66
|
+
expect(result).to be_ok
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "deletes from start of document to end" do
|
|
70
|
+
doc = build_doc_with_text("abc")
|
|
71
|
+
step = Prosereflect::Transform::ReplaceStep.new(0, doc.node_size, Prosereflect::Transform::Slice.empty)
|
|
72
|
+
result = step.apply(doc)
|
|
73
|
+
|
|
74
|
+
expect(result).to be_ok
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
context "when replacing text" do
|
|
79
|
+
it "replaces text with new content" do
|
|
80
|
+
doc = build_doc_with_text("Hello")
|
|
81
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
82
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
83
|
+
)
|
|
84
|
+
# Replace "ell" (positions 3-6) with "XY"
|
|
85
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, replacement)
|
|
86
|
+
result = step.apply(doc)
|
|
87
|
+
|
|
88
|
+
expect(result).to be_ok
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "replaces a single character" do
|
|
92
|
+
doc = build_doc_with_text("abc")
|
|
93
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
94
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "X")]),
|
|
95
|
+
)
|
|
96
|
+
# Replace "b" (position 3-4) with "X"
|
|
97
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 4, replacement)
|
|
98
|
+
result = step.apply(doc)
|
|
99
|
+
|
|
100
|
+
expect(result).to be_ok
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "replaces text with longer content" do
|
|
104
|
+
doc = build_doc_with_text("ab")
|
|
105
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
106
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XYZ")]),
|
|
107
|
+
)
|
|
108
|
+
# Replace "a" (position 2-3) with "XYZ"
|
|
109
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 3, replacement)
|
|
110
|
+
result = step.apply(doc)
|
|
111
|
+
|
|
112
|
+
expect(result).to be_ok
|
|
113
|
+
# New doc should be larger
|
|
114
|
+
expect(result.doc.node_size).to be > doc.node_size
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "replaces text with shorter content" do
|
|
118
|
+
doc = build_doc_with_text("abcdef")
|
|
119
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
120
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "Z")]),
|
|
121
|
+
)
|
|
122
|
+
# Replace "bcde" (positions 3-7) with "Z"
|
|
123
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 7, replacement)
|
|
124
|
+
result = step.apply(doc)
|
|
125
|
+
|
|
126
|
+
expect(result).to be_ok
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
context "when inserting text" do
|
|
131
|
+
it "inserts text at a position (zero-width range)" do
|
|
132
|
+
doc = build_doc_with_text("ac")
|
|
133
|
+
insert = Prosereflect::Transform::Slice.new(
|
|
134
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "b")]),
|
|
135
|
+
)
|
|
136
|
+
# Insert "b" at position 3 (between "a" and "c")
|
|
137
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 3, insert)
|
|
138
|
+
result = step.apply(doc)
|
|
139
|
+
|
|
140
|
+
expect(result).to be_ok
|
|
141
|
+
expect(result.doc.node_size).to be > doc.node_size
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "inserts at the beginning of a paragraph" do
|
|
145
|
+
doc = build_doc_with_text("cd")
|
|
146
|
+
insert = Prosereflect::Transform::Slice.new(
|
|
147
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "ab")]),
|
|
148
|
+
)
|
|
149
|
+
# Insert at position 2 (start of text inside paragraph)
|
|
150
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 2, insert)
|
|
151
|
+
result = step.apply(doc)
|
|
152
|
+
|
|
153
|
+
expect(result).to be_ok
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "inserts at the end of a paragraph" do
|
|
157
|
+
doc = build_doc_with_text("ab")
|
|
158
|
+
# doc=1, para=1, text("ab")=3, total=5
|
|
159
|
+
insert = Prosereflect::Transform::Slice.new(
|
|
160
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "cd")]),
|
|
161
|
+
)
|
|
162
|
+
# Insert at position 4 (end of text)
|
|
163
|
+
step = Prosereflect::Transform::ReplaceStep.new(4, 4, insert)
|
|
164
|
+
result = step.apply(doc)
|
|
165
|
+
|
|
166
|
+
expect(result).to be_ok
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context "with Slice objects" do
|
|
171
|
+
it "replaces content with a slice containing multiple text nodes" do
|
|
172
|
+
doc = build_doc_with_text("Hello")
|
|
173
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
174
|
+
Prosereflect::Fragment.new(
|
|
175
|
+
[
|
|
176
|
+
Prosereflect::Text.new(text: "X"),
|
|
177
|
+
Prosereflect::Text.new(text: "Y"),
|
|
178
|
+
],
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 7, replacement)
|
|
182
|
+
result = step.apply(doc)
|
|
183
|
+
|
|
184
|
+
expect(result).to be_ok
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "replaces with an empty slice that has open boundaries" do
|
|
188
|
+
doc = build_doc_with_text("abc")
|
|
189
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
190
|
+
Prosereflect::Fragment.new([]),
|
|
191
|
+
1,
|
|
192
|
+
0,
|
|
193
|
+
)
|
|
194
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 5, slice)
|
|
195
|
+
result = step.apply(doc)
|
|
196
|
+
|
|
197
|
+
expect(result).to be_ok
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "replaces a paragraph with a slice containing a paragraph" do
|
|
201
|
+
doc = build_doc_with_text("old")
|
|
202
|
+
new_para = Prosereflect::Paragraph.new(type: "paragraph")
|
|
203
|
+
new_para.add_text("new")
|
|
204
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
205
|
+
Prosereflect::Fragment.new([new_para]),
|
|
206
|
+
)
|
|
207
|
+
# Replace entire paragraph content (positions 1-5)
|
|
208
|
+
step = Prosereflect::Transform::ReplaceStep.new(1, 5, replacement)
|
|
209
|
+
result = step.apply(doc)
|
|
210
|
+
|
|
211
|
+
expect(result).to be_ok
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
context "with validation" do
|
|
216
|
+
it "fails when from > to" do
|
|
217
|
+
doc = build_doc_with_text("abc")
|
|
218
|
+
step = Prosereflect::Transform::ReplaceStep.new(5, 2, Prosereflect::Transform::Slice.empty)
|
|
219
|
+
result = step.apply(doc)
|
|
220
|
+
|
|
221
|
+
expect(result).not_to be_ok
|
|
222
|
+
expect(result.failed).to eq("Invalid positions")
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "fails when from is negative" do
|
|
226
|
+
doc = build_doc_with_text("abc")
|
|
227
|
+
step = Prosereflect::Transform::ReplaceStep.new(-1, 2, Prosereflect::Transform::Slice.empty)
|
|
228
|
+
result = step.apply(doc)
|
|
229
|
+
|
|
230
|
+
expect(result).not_to be_ok
|
|
231
|
+
expect(result.failed).to eq("from < 0")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "fails when to exceeds document size" do
|
|
235
|
+
doc = build_doc_with_text("abc")
|
|
236
|
+
# doc.node_size = 6
|
|
237
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 100, Prosereflect::Transform::Slice.empty)
|
|
238
|
+
result = step.apply(doc)
|
|
239
|
+
|
|
240
|
+
expect(result).not_to be_ok
|
|
241
|
+
expect(result.failed).to eq("to > doc size")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it "succeeds when from equals to (insertion)" do
|
|
245
|
+
doc = build_doc_with_text("abc")
|
|
246
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
247
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")]),
|
|
248
|
+
)
|
|
249
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 3, slice)
|
|
250
|
+
result = step.apply(doc)
|
|
251
|
+
|
|
252
|
+
expect(result).to be_ok
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
it "succeeds when from=0 and to=doc.node_size (replace entire document)" do
|
|
256
|
+
doc = build_doc_with_text("abc")
|
|
257
|
+
step = Prosereflect::Transform::ReplaceStep.new(0, doc.node_size, Prosereflect::Transform::Slice.empty)
|
|
258
|
+
result = step.apply(doc)
|
|
259
|
+
|
|
260
|
+
expect(result).to be_ok
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
describe "#invert" do
|
|
266
|
+
it "produces a ReplaceStep that reverses a deletion" do
|
|
267
|
+
doc = build_doc_with_text("Hello")
|
|
268
|
+
# Delete "ell" (positions 3-6)
|
|
269
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
270
|
+
inverted = step.invert(doc)
|
|
271
|
+
|
|
272
|
+
expect(inverted).to be_a(Prosereflect::Transform::ReplaceStep)
|
|
273
|
+
# invert: ReplaceStep.new(@from, @from + @slice.size, removed)
|
|
274
|
+
# @from=3, @slice.size=0 (empty slice), so inverted.to = 3 + 0 = 3
|
|
275
|
+
expect(inverted.from).to eq(3)
|
|
276
|
+
expect(inverted.to).to eq(3)
|
|
277
|
+
# removed should contain the content that was between 3 and 6
|
|
278
|
+
expect(inverted.slice).not_to be_nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
it "produces a step that reverses an insertion" do
|
|
282
|
+
doc = build_doc_with_text("ac")
|
|
283
|
+
insert = Prosereflect::Transform::Slice.new(
|
|
284
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "b")]),
|
|
285
|
+
)
|
|
286
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 3, insert)
|
|
287
|
+
inverted = step.invert(doc)
|
|
288
|
+
|
|
289
|
+
expect(inverted).to be_a(Prosereflect::Transform::ReplaceStep)
|
|
290
|
+
# invert: from = 3, to = 3 + slice.size
|
|
291
|
+
# slice contains text "b" (node_size=2), slice.size = 2 + 0 + 0 = 2
|
|
292
|
+
expect(inverted.from).to eq(3)
|
|
293
|
+
expect(inverted.to).to eq(5)
|
|
294
|
+
# The inverted step's slice is the content that was at positions 3-3 (empty)
|
|
295
|
+
# Note: invert returns a Fragment, not a Slice, due to content_between
|
|
296
|
+
expect(inverted.slice).to be_a(Prosereflect::Fragment)
|
|
297
|
+
expect(inverted.slice.empty?).to be true
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
it "produces a step that reverses a replacement" do
|
|
301
|
+
doc = build_doc_with_text("Hello")
|
|
302
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
303
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
304
|
+
)
|
|
305
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, replacement)
|
|
306
|
+
inverted = step.invert(doc)
|
|
307
|
+
|
|
308
|
+
expect(inverted).to be_a(Prosereflect::Transform::ReplaceStep)
|
|
309
|
+
# invert: from = 3, to = 3 + slice.size = 3 + 3 (text "XY" = node_size 3)
|
|
310
|
+
expect(inverted.from).to eq(3)
|
|
311
|
+
expect(inverted.to).to eq(6)
|
|
312
|
+
# The inverted slice should contain what was at positions 3-6
|
|
313
|
+
expect(inverted.slice).not_to be_nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
it "inverted step has correct from position matching original step from" do
|
|
317
|
+
doc = build_doc_with_text("Hello")
|
|
318
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 7, Prosereflect::Transform::Slice.empty)
|
|
319
|
+
inverted = step.invert(doc)
|
|
320
|
+
|
|
321
|
+
expect(inverted.from).to eq(2)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
it "inverted step to equals from plus slice size" do
|
|
325
|
+
doc = build_doc_with_text("Hello")
|
|
326
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
327
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "abc")]),
|
|
328
|
+
)
|
|
329
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 7, replacement)
|
|
330
|
+
inverted = step.invert(doc)
|
|
331
|
+
|
|
332
|
+
# slice.size for text "abc" = node_size 4 + open_start 0 + open_end 0 = 4
|
|
333
|
+
expect(inverted.to).to eq(2 + replacement.size)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
describe "#get_map" do
|
|
338
|
+
it "returns a StepMap with correct ranges for deletion" do
|
|
339
|
+
# Delete positions 3-6
|
|
340
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
341
|
+
step_map = step.get_map
|
|
342
|
+
|
|
343
|
+
expect(step_map).to be_a(Prosereflect::Transform::StepMap)
|
|
344
|
+
# delta = 0 - (6-3) = -3
|
|
345
|
+
# ranges: [[3, 6, 3, 0]]
|
|
346
|
+
expect(step_map.ranges).to eq([[3, 6, 3, 0]])
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
it "returns a StepMap with correct ranges for insertion" do
|
|
350
|
+
# Insert "XY" (node_size=3) at position 2
|
|
351
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
352
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
353
|
+
)
|
|
354
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 2, slice)
|
|
355
|
+
step_map = step.get_map
|
|
356
|
+
|
|
357
|
+
# delta = 3 - (2-2) = 3
|
|
358
|
+
# ranges: [[2, 2, 2, 5]]
|
|
359
|
+
expect(step_map.ranges).to eq([[2, 2, 2, 5]])
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
it "returns a StepMap with correct ranges for same-size replacement" do
|
|
363
|
+
# Replace 3 positions with 3-node_size content (net zero)
|
|
364
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
365
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "ab")]),
|
|
366
|
+
)
|
|
367
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 5, slice)
|
|
368
|
+
step_map = step.get_map
|
|
369
|
+
|
|
370
|
+
# delta = 3 - (5-2) = 0
|
|
371
|
+
# ranges: [[2, 5, 2, 2]]
|
|
372
|
+
expect(step_map.ranges).to eq([[2, 5, 2, 2]])
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
it "maps positions before deletion range unchanged" do
|
|
376
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
377
|
+
step_map = step.get_map
|
|
378
|
+
|
|
379
|
+
# Position 1 is before range start 3, so it stays unchanged
|
|
380
|
+
expect(step_map.map(1)).to eq(1)
|
|
381
|
+
expect(step_map.map(2)).to eq(2)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
it "maps positions after deletion range with offset" do
|
|
385
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
386
|
+
step_map = step.get_map
|
|
387
|
+
|
|
388
|
+
# For range [3,6,3,0]: positions >= 6 get offset by (new_end - old_end) = (0-6) = -6
|
|
389
|
+
# pos 6 -> 0, pos 7 -> 1, pos 8 -> 2
|
|
390
|
+
expect(step_map.map(6)).to eq(0)
|
|
391
|
+
expect(step_map.map(7)).to eq(1)
|
|
392
|
+
expect(step_map.map(8)).to eq(2)
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
it "maps positions after insertion range with positive offset" do
|
|
396
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
397
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
398
|
+
)
|
|
399
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 2, slice)
|
|
400
|
+
step_map = step.get_map
|
|
401
|
+
|
|
402
|
+
# For range [2,2,2,5]: positions >= 2 get offset by (5-2) = 3
|
|
403
|
+
expect(step_map.map(5)).to eq(8)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it "marks positions in deleted range as deleted" do
|
|
407
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
408
|
+
step_map = step.get_map
|
|
409
|
+
|
|
410
|
+
expect(step_map.deleted?(4)).to be true
|
|
411
|
+
expect(step_map.deleted?(2)).to be false
|
|
412
|
+
expect(step_map.deleted?(7)).to be false
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it "returns correct map_result for deleted positions" do
|
|
416
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
417
|
+
step_map = step.get_map
|
|
418
|
+
|
|
419
|
+
result = step_map.map_result(4)
|
|
420
|
+
expect(result.deleted).to be true
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
it "returns correct map_result for non-deleted positions" do
|
|
424
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
425
|
+
step_map = step.get_map
|
|
426
|
+
|
|
427
|
+
result = step_map.map_result(2)
|
|
428
|
+
expect(result.deleted).to be false
|
|
429
|
+
expect(result.pos).to eq(2)
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
describe "mapping through Mapping" do
|
|
434
|
+
it "maps positions through a single deletion step" do
|
|
435
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
436
|
+
mapping = Prosereflect::Transform::Mapping.from_step_map(step.get_map)
|
|
437
|
+
|
|
438
|
+
# Position 1 is before deletion range, stays the same
|
|
439
|
+
expect(mapping.map(1)).to eq(1)
|
|
440
|
+
# Position 7 is after deletion range, gets offset by -6
|
|
441
|
+
expect(mapping.map(7)).to eq(1)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
it "maps positions through a single insertion step" do
|
|
445
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
446
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
447
|
+
)
|
|
448
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 2, slice)
|
|
449
|
+
mapping = Prosereflect::Transform::Mapping.from_step_map(step.get_map)
|
|
450
|
+
|
|
451
|
+
# Position 1 is before insertion, stays the same
|
|
452
|
+
expect(mapping.map(1)).to eq(1)
|
|
453
|
+
# Position 5 is after insertion, shifts by +3
|
|
454
|
+
expect(mapping.map(5)).to eq(8)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
it "maps positions through multiple steps" do
|
|
458
|
+
# First step: delete positions 3-6
|
|
459
|
+
step1 = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
460
|
+
# Second step: delete positions 4-6 (in the post-step1 doc)
|
|
461
|
+
step2 = Prosereflect::Transform::ReplaceStep.new(4, 6, Prosereflect::Transform::Slice.empty)
|
|
462
|
+
|
|
463
|
+
mapping = Prosereflect::Transform::Mapping.new
|
|
464
|
+
mapping.add_map(step1.get_map)
|
|
465
|
+
mapping.add_map(step2.get_map)
|
|
466
|
+
|
|
467
|
+
# Position 8 should be mapped through both steps
|
|
468
|
+
mapped = mapping.map(8)
|
|
469
|
+
expect(mapped).to be < 8
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
it "maps positions through empty mapping as identity" do
|
|
473
|
+
mapping = Prosereflect::Transform::Mapping.new
|
|
474
|
+
expect(mapping.map(5)).to eq(5)
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
it "reports deleted positions via map_result" do
|
|
478
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
479
|
+
mapping = Prosereflect::Transform::Mapping.from_step_map(step.get_map)
|
|
480
|
+
|
|
481
|
+
result = mapping.map_result(4)
|
|
482
|
+
expect(result[:deleted]).to be true
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
it "reports non-deleted positions via map_result" do
|
|
486
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
487
|
+
mapping = Prosereflect::Transform::Mapping.from_step_map(step.get_map)
|
|
488
|
+
|
|
489
|
+
result = mapping.map_result(2)
|
|
490
|
+
expect(result[:deleted]).to be false
|
|
491
|
+
expect(result[:pos]).to eq(2)
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
it "adds step maps at specific indices" do
|
|
495
|
+
mapping = Prosereflect::Transform::Mapping.new
|
|
496
|
+
step1 = Prosereflect::Transform::ReplaceStep.new(3, 6, Prosereflect::Transform::Slice.empty)
|
|
497
|
+
step2 = Prosereflect::Transform::ReplaceStep.new(4, 8, Prosereflect::Transform::Slice.empty)
|
|
498
|
+
|
|
499
|
+
mapping.add_map(step1.get_map)
|
|
500
|
+
mapping.add_map(step2.get_map, 0)
|
|
501
|
+
|
|
502
|
+
expect(mapping.to_a.length).to eq(2)
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
describe "ReplaceAroundStep" do
|
|
507
|
+
describe "creation" do
|
|
508
|
+
it "creates a ReplaceAroundStep with all parameters" do
|
|
509
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
510
|
+
Prosereflect::Fragment.new([Prosereflect::Blockquote.create]),
|
|
511
|
+
)
|
|
512
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
513
|
+
1, 7, 1, 7, slice, 0, structure: false
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
expect(step.from).to eq(1)
|
|
517
|
+
expect(step.to).to eq(7)
|
|
518
|
+
expect(step.gap_from).to eq(1)
|
|
519
|
+
expect(step.gap_to).to eq(7)
|
|
520
|
+
expect(step.insert).to eq(0)
|
|
521
|
+
expect(step.structure).to be false
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it "defaults structure to false" do
|
|
525
|
+
slice = Prosereflect::Transform::Slice.empty
|
|
526
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
527
|
+
1, 5, 1, 5, slice, 0
|
|
528
|
+
)
|
|
529
|
+
expect(step.structure).to be false
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
it "sets structure to true when specified" do
|
|
533
|
+
slice = Prosereflect::Transform::Slice.empty
|
|
534
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
535
|
+
1, 5, 1, 5, slice, 0, structure: true
|
|
536
|
+
)
|
|
537
|
+
expect(step.structure).to be true
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
describe "#step_type" do
|
|
542
|
+
it "returns replaceAround" do
|
|
543
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
544
|
+
1, 5, 1, 5, Prosereflect::Transform::Slice.empty, 0
|
|
545
|
+
)
|
|
546
|
+
expect(step.step_type).to eq("replaceAround")
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
describe "#get_map" do
|
|
551
|
+
it "returns a StepMap for the around-replacement" do
|
|
552
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
553
|
+
Prosereflect::Fragment.new([Prosereflect::Blockquote.create]),
|
|
554
|
+
)
|
|
555
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
556
|
+
1, 7, 1, 7, slice, 0
|
|
557
|
+
)
|
|
558
|
+
step_map = step.get_map
|
|
559
|
+
|
|
560
|
+
expect(step_map).to be_a(Prosereflect::Transform::StepMap)
|
|
561
|
+
expect(step_map.ranges).to be_an(Array)
|
|
562
|
+
expect(step_map.ranges.length).to be > 0
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
it "produces correct ranges for a wrapping operation" do
|
|
566
|
+
# Wrapping a paragraph: from=1, to=7, gap_from=1, gap_to=7
|
|
567
|
+
# Slice is a blockquote (node_size=1), insert=0
|
|
568
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
569
|
+
Prosereflect::Fragment.new([Prosereflect::Blockquote.create]),
|
|
570
|
+
)
|
|
571
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
572
|
+
1, 7, 1, 7, slice, 0
|
|
573
|
+
)
|
|
574
|
+
step_map = step.get_map
|
|
575
|
+
|
|
576
|
+
# get_map returns: [from, gap_from-from, insert, gap_to, to-gap_to, slice.size-insert]
|
|
577
|
+
# = [1, 0, 0, 7, 0, 1]
|
|
578
|
+
expect(step_map.ranges).to eq([1, 0, 0, 7, 0, 1])
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
it "produces correct ranges for a lift operation" do
|
|
582
|
+
# Lift: from=1, to=9, gap_from=3, gap_to=7, slice=empty, insert=0
|
|
583
|
+
slice = Prosereflect::Transform::Slice.empty
|
|
584
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
585
|
+
1, 9, 3, 7, slice, 0
|
|
586
|
+
)
|
|
587
|
+
step_map = step.get_map
|
|
588
|
+
|
|
589
|
+
# get_map returns: [1, 2, 0, 7, 2, 0]
|
|
590
|
+
expect(step_map.ranges).to eq([1, 2, 0, 7, 2, 0])
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe "#to_json" do
|
|
595
|
+
it "serializes core fields to a hash" do
|
|
596
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
597
|
+
Prosereflect::Fragment.new([Prosereflect::Blockquote.create]),
|
|
598
|
+
)
|
|
599
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
600
|
+
1, 7, 1, 7, slice, 0, structure: true
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Verify attributes directly since to_json has a bug with Fragment#map
|
|
604
|
+
expect(step.step_type).to eq("replaceAround")
|
|
605
|
+
expect(step.from).to eq(1)
|
|
606
|
+
expect(step.to).to eq(7)
|
|
607
|
+
expect(step.gap_from).to eq(1)
|
|
608
|
+
expect(step.gap_to).to eq(7)
|
|
609
|
+
expect(step.insert).to eq(0)
|
|
610
|
+
expect(step.structure).to be true
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it "calls to_json which builds the expected hash structure" do
|
|
614
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
615
|
+
Prosereflect::Fragment.new([Prosereflect::Blockquote.create]),
|
|
616
|
+
)
|
|
617
|
+
step = Prosereflect::Transform::ReplaceAroundStep.new(
|
|
618
|
+
1, 7, 1, 7, slice, 0, structure: true
|
|
619
|
+
)
|
|
620
|
+
json = step.to_json
|
|
621
|
+
|
|
622
|
+
expect(json["stepType"]).to eq("replaceAround")
|
|
623
|
+
expect(json["from"]).to eq(1)
|
|
624
|
+
expect(json["to"]).to eq(7)
|
|
625
|
+
expect(json["gapFrom"]).to eq(1)
|
|
626
|
+
expect(json["gapTo"]).to eq(7)
|
|
627
|
+
expect(json["insert"]).to eq(0)
|
|
628
|
+
expect(json["structure"]).to be true
|
|
629
|
+
expect(json["slice"]).to be_an(Array)
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
describe "edge cases" do
|
|
635
|
+
context "with empty document content" do
|
|
636
|
+
it "applies a step to an empty paragraph" do
|
|
637
|
+
doc = Prosereflect::Parser.parse_document(
|
|
638
|
+
"type" => "doc",
|
|
639
|
+
"content" => [
|
|
640
|
+
{ "type" => "paragraph" },
|
|
641
|
+
],
|
|
642
|
+
)
|
|
643
|
+
# Empty paragraph: doc=1, para=1, total=2
|
|
644
|
+
insert = Prosereflect::Transform::Slice.new(
|
|
645
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "hi")]),
|
|
646
|
+
)
|
|
647
|
+
step = Prosereflect::Transform::ReplaceStep.new(2, 2, insert)
|
|
648
|
+
result = step.apply(doc)
|
|
649
|
+
|
|
650
|
+
expect(result).to be_ok
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
it "inserts into an empty document" do
|
|
654
|
+
doc = Prosereflect::Parser.parse_document(
|
|
655
|
+
"type" => "doc",
|
|
656
|
+
"content" => [],
|
|
657
|
+
)
|
|
658
|
+
# Empty doc: node_size=1
|
|
659
|
+
expect(doc.node_size).to eq(1)
|
|
660
|
+
# Cannot insert at position 2 since doc size is 1
|
|
661
|
+
step = Prosereflect::Transform::ReplaceStep.new(1, 1, Prosereflect::Transform::Slice.empty)
|
|
662
|
+
result = step.apply(doc)
|
|
663
|
+
|
|
664
|
+
expect(result).to be_ok
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
context "with nested structures" do
|
|
669
|
+
it "replaces content inside a nested blockquote" do
|
|
670
|
+
doc = Prosereflect::Parser.parse_document(
|
|
671
|
+
"type" => "doc",
|
|
672
|
+
"content" => [
|
|
673
|
+
{
|
|
674
|
+
"type" => "blockquote",
|
|
675
|
+
"content" => [
|
|
676
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "quoted" }] },
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
)
|
|
681
|
+
# doc=1 + bq=1 + para=1 + text("quoted")=7 = 10
|
|
682
|
+
# Delete text inside blockquote (positions 4-9)
|
|
683
|
+
step = Prosereflect::Transform::ReplaceStep.new(4, 9, Prosereflect::Transform::Slice.empty)
|
|
684
|
+
result = step.apply(doc)
|
|
685
|
+
|
|
686
|
+
expect(result).to be_ok
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
it "applies a step to a document with multiple block types" do
|
|
690
|
+
doc = Prosereflect::Parser.parse_document(
|
|
691
|
+
"type" => "doc",
|
|
692
|
+
"content" => [
|
|
693
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "first" }] },
|
|
694
|
+
{ "type" => "heading", "attrs" => { "level" => 2 },
|
|
695
|
+
"content" => [{ "type" => "text", "text" => "title" }] },
|
|
696
|
+
{ "type" => "paragraph", "content" => [{ "type" => "text", "text" => "last" }] },
|
|
697
|
+
],
|
|
698
|
+
)
|
|
699
|
+
# Delete the heading (positions 8-15)
|
|
700
|
+
# first para: 1+1+6=8, heading: 1+1+6=8, heading starts at pos 8
|
|
701
|
+
step = Prosereflect::Transform::ReplaceStep.new(8, 15, Prosereflect::Transform::Slice.empty)
|
|
702
|
+
result = step.apply(doc)
|
|
703
|
+
|
|
704
|
+
expect(result).to be_ok
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
context "when replacing across node boundaries" do
|
|
709
|
+
it "deletes content spanning two paragraphs" do
|
|
710
|
+
doc = build_doc_with_paragraphs("ab", "cd")
|
|
711
|
+
# doc=1 + (para=1+text=3) + (para=1+text=3) = 9
|
|
712
|
+
# Positions: 0=doc, 1=para1, 2="a", 3="b", 4=para1_end/para2_start
|
|
713
|
+
# 5=para2, 6="c", 7="d", 8=end
|
|
714
|
+
# Delete from middle of para1 to middle of para2 (3-7)
|
|
715
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 7, Prosereflect::Transform::Slice.empty)
|
|
716
|
+
result = step.apply(doc)
|
|
717
|
+
|
|
718
|
+
expect(result).to be_ok
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
it "replaces content spanning two paragraphs with new content" do
|
|
722
|
+
doc = build_doc_with_paragraphs("ab", "cd")
|
|
723
|
+
replacement = Prosereflect::Transform::Slice.new(
|
|
724
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "XY")]),
|
|
725
|
+
)
|
|
726
|
+
step = Prosereflect::Transform::ReplaceStep.new(3, 7, replacement)
|
|
727
|
+
result = step.apply(doc)
|
|
728
|
+
|
|
729
|
+
expect(result).to be_ok
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
context "with table structures" do
|
|
734
|
+
it "deletes content inside a table cell" do
|
|
735
|
+
doc = Prosereflect::Parser.parse_document(
|
|
736
|
+
"type" => "doc",
|
|
737
|
+
"content" => [
|
|
738
|
+
{
|
|
739
|
+
"type" => "table",
|
|
740
|
+
"content" => [
|
|
741
|
+
{
|
|
742
|
+
"type" => "table_row",
|
|
743
|
+
"content" => [
|
|
744
|
+
{
|
|
745
|
+
"type" => "table_cell",
|
|
746
|
+
"content" => [
|
|
747
|
+
{ "type" => "paragraph",
|
|
748
|
+
"content" => [{ "type" => "text", "text" => "cell" }] },
|
|
749
|
+
],
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
},
|
|
755
|
+
],
|
|
756
|
+
)
|
|
757
|
+
# doc=1, table=1, row=1, cell=1, para=1, text("cell")=5 = 10
|
|
758
|
+
# Positions inside text: 6="c", 7="e", 8="l", 9="l"
|
|
759
|
+
step = Prosereflect::Transform::ReplaceStep.new(7, 9, Prosereflect::Transform::Slice.empty)
|
|
760
|
+
result = step.apply(doc)
|
|
761
|
+
|
|
762
|
+
expect(result).to be_ok
|
|
763
|
+
end
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
context "with Slice operations" do
|
|
767
|
+
it "Slice.empty creates a truly empty slice" do
|
|
768
|
+
slice = Prosereflect::Transform::Slice.empty
|
|
769
|
+
expect(slice.empty?).to be true
|
|
770
|
+
expect(slice.open_start).to eq(0)
|
|
771
|
+
expect(slice.open_end).to eq(0)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
it "Slice with content reports correct size" do
|
|
775
|
+
content = Prosereflect::Fragment.new(
|
|
776
|
+
[
|
|
777
|
+
Prosereflect::Text.new(text: "abc"),
|
|
778
|
+
],
|
|
779
|
+
)
|
|
780
|
+
slice = Prosereflect::Transform::Slice.new(content, 1, 1)
|
|
781
|
+
|
|
782
|
+
# content_size = 4 (text "abc" = 3+1), size = 4+1+1 = 6
|
|
783
|
+
expect(slice.size).to eq(6)
|
|
784
|
+
expect(slice.open_start).to eq(1)
|
|
785
|
+
expect(slice.open_end).to eq(1)
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
it "Slice equality checks open_start and open_end" do
|
|
789
|
+
content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "a")])
|
|
790
|
+
slice1 = Prosereflect::Transform::Slice.new(content, 0, 0)
|
|
791
|
+
slice2 = Prosereflect::Transform::Slice.new(content, 1, 0)
|
|
792
|
+
|
|
793
|
+
expect(slice1).not_to eq(slice2)
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
it "two identical slices are equal" do
|
|
797
|
+
content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
|
|
798
|
+
slice1 = Prosereflect::Transform::Slice.new(content, 0, 0)
|
|
799
|
+
slice2 = Prosereflect::Transform::Slice.new(
|
|
800
|
+
Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")]), 0, 0
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
expect(slice1).to eq(slice2)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
it "Slice.empty with open boundaries is not empty" do
|
|
807
|
+
slice = Prosereflect::Transform::Slice.new(
|
|
808
|
+
Prosereflect::Fragment.new([]), 1, 0
|
|
809
|
+
)
|
|
810
|
+
expect(slice.empty?).to be false
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
context "with StepMap composition" do
|
|
815
|
+
it "adding an empty map to a non-empty map returns the non-empty map" do
|
|
816
|
+
step_map = Prosereflect::Transform::StepMap.new([[0, 5, 0, 5]])
|
|
817
|
+
empty_map = Prosereflect::Transform::StepMap.empty
|
|
818
|
+
result = step_map.add_map(empty_map)
|
|
819
|
+
|
|
820
|
+
expect(result.ranges).to eq([[0, 5, 0, 5]])
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
it "adding a non-empty map to an empty map returns the non-empty map" do
|
|
824
|
+
empty_map = Prosereflect::Transform::StepMap.empty
|
|
825
|
+
step_map = Prosereflect::Transform::StepMap.new([[0, 5, 0, 5]])
|
|
826
|
+
result = empty_map.add_map(step_map)
|
|
827
|
+
|
|
828
|
+
expect(result.ranges).to eq([[0, 5, 0, 5]])
|
|
829
|
+
end
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
end
|