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 "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Transform::ReplaceStep do
6
+ describe "creation" do
7
+ it "creates replace step with from, to, and slice" do
8
+ slice = Prosereflect::Transform::Slice.empty
9
+ step = described_class.new(0, 5, slice)
10
+ expect(step.from).to eq(0)
11
+ expect(step.to).to eq(5)
12
+ expect(step.slice).to eq(slice)
13
+ end
14
+
15
+ it "creates replace step with default empty slice" do
16
+ step = described_class.new(0, 5)
17
+ expect(step.slice).to eq(Prosereflect::Transform::Slice.empty)
18
+ end
19
+ end
20
+
21
+ describe "get_map" do
22
+ it "returns step map for the replacement" do
23
+ step = described_class.new(2, 5, Prosereflect::Transform::Slice.empty)
24
+ step_map = step.get_map
25
+ expect(step_map).to be_a(Prosereflect::Transform::StepMap)
26
+ end
27
+ end
28
+
29
+ describe "step_type" do
30
+ it "returns replace" do
31
+ step = described_class.new(0, 5)
32
+ expect(step.step_type).to eq("replace")
33
+ end
34
+ end
35
+
36
+ describe "merge" do
37
+ it "merges adjacent empty deletions" do
38
+ # step1: delete 3-4
39
+ step1 = described_class.new(3, 4, Prosereflect::Transform::Slice.empty)
40
+ # step2: delete 4-5 (adjacent - step1's end equals step2's start)
41
+ step2 = described_class.new(4, 5, Prosereflect::Transform::Slice.empty)
42
+
43
+ merged = step1.merge(step2)
44
+ expect(merged).not_to be_nil
45
+ end
46
+
47
+ it "does not merge overlapping deletions" do
48
+ # step1: delete 1-3
49
+ step1 = described_class.new(1, 3, Prosereflect::Transform::Slice.empty)
50
+ # step2: delete 2-4 (overlaps with step1)
51
+ step2 = described_class.new(2, 4, Prosereflect::Transform::Slice.empty)
52
+
53
+ merged = step1.merge(step2)
54
+ expect(merged).to be_nil
55
+ end
56
+
57
+ it "merges adjacent deletions extending backward" do
58
+ # step1: delete 3-4
59
+ step1 = described_class.new(3, 4, Prosereflect::Transform::Slice.empty)
60
+ # step2: delete 2-3 (extends backward from step1)
61
+ step2 = described_class.new(2, 3, Prosereflect::Transform::Slice.empty)
62
+
63
+ merged = step2.merge(step1)
64
+ expect(merged).not_to be_nil
65
+ end
66
+
67
+ it "does not merge far apart steps" do
68
+ step1 = described_class.new(1, 2, Prosereflect::Transform::Slice.empty)
69
+ step2 = described_class.new(5, 6, Prosereflect::Transform::Slice.empty)
70
+
71
+ merged = step1.merge(step2)
72
+ expect(merged).to be_nil
73
+ end
74
+ end
75
+
76
+ describe "can_extend_deletion?" do
77
+ it "returns true when other starts at this end with empty slice" do
78
+ step1 = described_class.new(2, 4, Prosereflect::Transform::Slice.empty)
79
+ step2 = described_class.new(4, 6, Prosereflect::Transform::Slice.empty)
80
+ expect(step1.can_extend_deletion?(step2)).to be true
81
+ end
82
+
83
+ it "returns false when other does not start at this end" do
84
+ step1 = described_class.new(2, 4, Prosereflect::Transform::Slice.empty)
85
+ step2 = described_class.new(5, 7, Prosereflect::Transform::Slice.empty)
86
+ expect(step1.can_extend_deletion?(step2)).to be false
87
+ end
88
+
89
+ it "returns false when self.slice is not empty" do
90
+ content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
91
+ step1 = described_class.new(2, 4, Prosereflect::Transform::Slice.new(content))
92
+ step2 = described_class.new(4, 6, Prosereflect::Transform::Slice.empty)
93
+ expect(step1.can_extend_deletion?(step2)).to be false
94
+ end
95
+ end
96
+
97
+ describe "can_prepend_deletion?" do
98
+ it "returns true when other ends at this start with empty slice" do
99
+ step1 = described_class.new(4, 6, Prosereflect::Transform::Slice.empty)
100
+ step2 = described_class.new(2, 4, Prosereflect::Transform::Slice.empty)
101
+ expect(step1.can_prepend_deletion?(step2)).to be true
102
+ end
103
+
104
+ it "returns false when other does not end at this start" do
105
+ step1 = described_class.new(4, 6, Prosereflect::Transform::Slice.empty)
106
+ step2 = described_class.new(2, 3, Prosereflect::Transform::Slice.empty)
107
+ expect(step1.can_prepend_deletion?(step2)).to be false
108
+ end
109
+ end
110
+
111
+ describe "can_append_content?" do
112
+ it "returns true when other starts at this end with content" do
113
+ step1 = described_class.new(2, 3, Prosereflect::Transform::Slice.empty)
114
+ content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
115
+ step2 = described_class.new(3, 3, Prosereflect::Transform::Slice.new(content))
116
+ expect(step1.can_append_content?(step2)).to be true
117
+ end
118
+
119
+ it "returns false when other starts at different position" do
120
+ step1 = described_class.new(2, 3, Prosereflect::Transform::Slice.empty)
121
+ content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
122
+ step2 = described_class.new(4, 4, Prosereflect::Transform::Slice.new(content))
123
+ expect(step1.can_append_content?(step2)).to be false
124
+ end
125
+
126
+ it "returns false when other has empty slice" do
127
+ step1 = described_class.new(2, 3, Prosereflect::Transform::Slice.empty)
128
+ step2 = described_class.new(3, 3, Prosereflect::Transform::Slice.empty)
129
+ expect(step1.can_append_content?(step2)).to be false
130
+ end
131
+ end
132
+
133
+ describe "can_prepend_content?" do
134
+ it "returns true when other ends at this start with content" do
135
+ content1 = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "y")])
136
+ step1 = described_class.new(3, 3, Prosereflect::Transform::Slice.new(content1))
137
+ content2 = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
138
+ step2 = described_class.new(1, 3, Prosereflect::Transform::Slice.new(content2))
139
+ expect(step1.can_prepend_content?(step2)).to be true
140
+ end
141
+
142
+ it "returns false when other.slice is empty" do
143
+ content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "y")])
144
+ step1 = described_class.new(3, 3, Prosereflect::Transform::Slice.new(content))
145
+ step2 = described_class.new(1, 3, Prosereflect::Transform::Slice.empty)
146
+ expect(step1.can_prepend_content?(step2)).to be false
147
+ end
148
+
149
+ it "returns false when other ends at different position" do
150
+ content = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "y")])
151
+ step1 = described_class.new(3, 3, Prosereflect::Transform::Slice.new(content))
152
+ content2 = Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")])
153
+ step2 = described_class.new(1, 2, Prosereflect::Transform::Slice.new(content2))
154
+ expect(step1.can_prepend_content?(step2)).to be false
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Transform::Slice do
6
+ let(:content) { Prosereflect::Fragment.new([]) }
7
+
8
+ describe "creation" do
9
+ it "creates empty slice" do
10
+ slice = described_class.new(content)
11
+ expect(slice.empty?).to be true
12
+ end
13
+
14
+ it "creates slice with open boundaries" do
15
+ slice = described_class.new(content, 1, 1)
16
+ expect(slice.open_start).to eq(1)
17
+ expect(slice.open_end).to eq(1)
18
+ end
19
+ end
20
+
21
+ describe "empty?" do
22
+ it "returns true for empty slice" do
23
+ expect(described_class.empty.empty?).to be true
24
+ end
25
+ end
26
+
27
+ describe "size" do
28
+ it "returns 0 for empty slice" do
29
+ expect(described_class.empty.size).to eq(0)
30
+ end
31
+ end
32
+
33
+ describe "cut" do
34
+ it "returns same slice when cutting full range" do
35
+ slice = described_class.new(content)
36
+ result = slice.cut(0, slice.size)
37
+ expect(result.size).to eq(slice.size)
38
+ end
39
+ end
40
+
41
+ describe "equality" do
42
+ it "compares equal slices" do
43
+ slice1 = described_class.new(content)
44
+ slice2 = described_class.new(content)
45
+ expect(slice1).to eq(slice2)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Transform::StepMap do
6
+ describe "creation" do
7
+ it "creates an empty step map" do
8
+ map = described_class.empty
9
+ expect(map.ranges).to eq([])
10
+ end
11
+
12
+ it "creates a delete step map" do
13
+ map = described_class.delete(5, 10)
14
+ expect(map.ranges).to eq([[5, 10, 5, 5]])
15
+ end
16
+
17
+ it "creates a replace step map" do
18
+ map = described_class.replace(5, 10, 5, 15)
19
+ expect(map.ranges).to eq([[5, 10, 5, 15]])
20
+ end
21
+ end
22
+
23
+ describe "map" do
24
+ it "maps position before any range unchanged" do
25
+ map = described_class.new([[5, 10, 5, 5]])
26
+ expect(map.map(3)).to eq(3)
27
+ end
28
+
29
+ it "maps position before range with offset" do
30
+ map = described_class.new([[5, 10, 3, 3]])
31
+ expect(map.map(3)).to eq(1)
32
+ end
33
+
34
+ it "maps position inside range" do
35
+ map = described_class.new([[5, 10, 3, 3]])
36
+ expect(map.map(7)).to eq(5)
37
+ end
38
+
39
+ it "maps position after range with offset" do
40
+ map = described_class.new([[5, 10, 3, 3]])
41
+ expect(map.map(15)).to eq(8)
42
+ end
43
+ end
44
+
45
+ describe "deleted?" do
46
+ it "returns false for position before range" do
47
+ map = described_class.new([[5, 10, 5, 5]])
48
+ expect(map.deleted?(3)).to be false
49
+ end
50
+
51
+ it "returns true for position inside range" do
52
+ map = described_class.new([[5, 10, 5, 5]])
53
+ expect(map.deleted?(7)).to be true
54
+ end
55
+
56
+ it "returns false for position after range" do
57
+ map = described_class.new([[5, 10, 5, 5]])
58
+ expect(map.deleted?(15)).to be false
59
+ end
60
+ end
61
+
62
+ describe "add_map" do
63
+ it "combines two independent maps" do
64
+ map1 = described_class.new([[0, 5, 0, 5]])
65
+ map2 = described_class.new([[10, 15, 10, 15]])
66
+ result = map1.add_map(map2)
67
+ expect(result.ranges.length).to eq(2)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Transform::Step do
6
+ describe "creation" do
7
+ it "can be instantiated" do
8
+ step = described_class.new
9
+ expect(step).to be_a(described_class)
10
+ end
11
+ end
12
+
13
+ describe "merge" do
14
+ it "merges adjacent empty deletions extending backward" do
15
+ # step1: delete 3-4 (empty deletion at position 3)
16
+ step1 = Prosereflect::Transform::ReplaceStep.new(
17
+ 3, 4,
18
+ Prosereflect::Transform::Slice.empty
19
+ )
20
+ # step2: delete 2-3 (empty deletion at position 2)
21
+ step2 = Prosereflect::Transform::ReplaceStep.new(
22
+ 2, 3,
23
+ Prosereflect::Transform::Slice.empty
24
+ )
25
+
26
+ # step1's end (4) equals step2's start (2)? No, 4 != 2
27
+ # step1's start (3) equals step2's end (3)? Yes!
28
+ # can_prepend_deletion: other.to == @from && other.slice.empty?
29
+ # 3 == 3 && true = true
30
+ merged = step2.merge(step1)
31
+ expect(merged).not_to be_nil
32
+ end
33
+
34
+ it "does not merge steps that are far apart" do
35
+ step1 = Prosereflect::Transform::ReplaceStep.new(
36
+ 2, 2,
37
+ Prosereflect::Transform::Slice.new(
38
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "a")]),
39
+ )
40
+ )
41
+ # step2 is at position 4, far from step1 at position 2
42
+ step2 = Prosereflect::Transform::ReplaceStep.new(
43
+ 4, 4,
44
+ Prosereflect::Transform::Slice.new(
45
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "b")]),
46
+ )
47
+ )
48
+
49
+ merged = step1.merge(step2)
50
+ expect(merged).to be_nil
51
+ end
52
+
53
+ it "merges adjacent empty deletions" do
54
+ # step1: delete 3-4
55
+ step1 = Prosereflect::Transform::ReplaceStep.new(
56
+ 3, 4,
57
+ Prosereflect::Transform::Slice.empty
58
+ )
59
+ # step2: delete 4-5 (adjacent - step1's end equals step2's start)
60
+ step2 = Prosereflect::Transform::ReplaceStep.new(
61
+ 4, 5,
62
+ Prosereflect::Transform::Slice.empty
63
+ )
64
+
65
+ # can_extend_deletion: @to == other.from && @slice.empty?
66
+ # 4 == 4 && true = true
67
+ merged = step1.merge(step2)
68
+ expect(merged).not_to be_nil
69
+ end
70
+
71
+ it "does not merge overlapping deletions" do
72
+ # step1: delete 1-3
73
+ step1 = Prosereflect::Transform::ReplaceStep.new(
74
+ 1, 3,
75
+ Prosereflect::Transform::Slice.empty
76
+ )
77
+ # step2: delete 2-4 (overlaps with step1)
78
+ step2 = Prosereflect::Transform::ReplaceStep.new(
79
+ 2, 4,
80
+ Prosereflect::Transform::Slice.empty
81
+ )
82
+
83
+ merged = step1.merge(step2)
84
+ expect(merged).to be_nil
85
+ end
86
+
87
+ it "appends content when second step starts at first's end" do
88
+ # step1: replace 2-3 with empty
89
+ step1 = Prosereflect::Transform::ReplaceStep.new(
90
+ 2, 3,
91
+ Prosereflect::Transform::Slice.empty
92
+ )
93
+ # step2: insert "x" at position 3 (step1's end)
94
+ step2 = Prosereflect::Transform::ReplaceStep.new(
95
+ 3, 3,
96
+ Prosereflect::Transform::Slice.new(
97
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")]),
98
+ )
99
+ )
100
+
101
+ # can_append_content: @to == other.from && !other.slice.empty?
102
+ merged = step1.merge(step2)
103
+ expect(merged).not_to be_nil
104
+ end
105
+ end
106
+
107
+ context "with ReplaceStep" do
108
+ describe "creation" do
109
+ it "creates replace step with from, to, and slice" do
110
+ slice = Prosereflect::Transform::Slice.empty
111
+ step = Prosereflect::Transform::ReplaceStep.new(0, 5, slice)
112
+ expect(step.from).to eq(0)
113
+ expect(step.to).to eq(5)
114
+ expect(step.slice).to eq(slice)
115
+ end
116
+
117
+ it "creates replace step with default empty slice" do
118
+ step = Prosereflect::Transform::ReplaceStep.new(0, 5)
119
+ expect(step.slice).to eq(Prosereflect::Transform::Slice.empty)
120
+ end
121
+ end
122
+
123
+ describe "get_map" do
124
+ it "returns step map for the replacement" do
125
+ step = Prosereflect::Transform::ReplaceStep.new(2, 5, Prosereflect::Transform::Slice.empty)
126
+ step_map = step.get_map
127
+ expect(step_map).to be_a(Prosereflect::Transform::StepMap)
128
+ end
129
+ end
130
+
131
+ describe "step_type" do
132
+ it "returns replace" do
133
+ step = Prosereflect::Transform::ReplaceStep.new(0, 5)
134
+ expect(step.step_type).to eq("replace")
135
+ end
136
+ end
137
+
138
+ describe "can_extend_deletion?" do
139
+ it "returns true when other starts at this end with empty slice" do
140
+ step1 = Prosereflect::Transform::ReplaceStep.new(2, 4, Prosereflect::Transform::Slice.empty)
141
+ step2 = Prosereflect::Transform::ReplaceStep.new(4, 6, Prosereflect::Transform::Slice.empty)
142
+ expect(step1.can_extend_deletion?(step2)).to be true
143
+ end
144
+
145
+ it "returns false when other does not start at this end" do
146
+ step1 = Prosereflect::Transform::ReplaceStep.new(2, 4, Prosereflect::Transform::Slice.empty)
147
+ step2 = Prosereflect::Transform::ReplaceStep.new(5, 7, Prosereflect::Transform::Slice.empty)
148
+ expect(step1.can_extend_deletion?(step2)).to be false
149
+ end
150
+ end
151
+
152
+ describe "can_prepend_deletion?" do
153
+ it "returns true when other ends at this start with empty slice" do
154
+ step1 = Prosereflect::Transform::ReplaceStep.new(4, 6, Prosereflect::Transform::Slice.empty)
155
+ step2 = Prosereflect::Transform::ReplaceStep.new(2, 4, Prosereflect::Transform::Slice.empty)
156
+ expect(step1.can_prepend_deletion?(step2)).to be true
157
+ end
158
+
159
+ it "returns false when other does not end at this start" do
160
+ step1 = Prosereflect::Transform::ReplaceStep.new(4, 6, Prosereflect::Transform::Slice.empty)
161
+ step2 = Prosereflect::Transform::ReplaceStep.new(2, 3, Prosereflect::Transform::Slice.empty)
162
+ expect(step1.can_prepend_deletion?(step2)).to be false
163
+ end
164
+ end
165
+
166
+ describe "can_append_content?" do
167
+ it "returns true when other starts at this end with content" do
168
+ step1 = Prosereflect::Transform::ReplaceStep.new(2, 3, Prosereflect::Transform::Slice.empty)
169
+ step2 = Prosereflect::Transform::ReplaceStep.new(
170
+ 3, 3,
171
+ Prosereflect::Transform::Slice.new(
172
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")]),
173
+ )
174
+ )
175
+ expect(step1.can_append_content?(step2)).to be true
176
+ end
177
+ end
178
+
179
+ describe "can_prepend_content?" do
180
+ it "returns true when other ends at this start with content" do
181
+ # step1: insert "y" at position 3 (start=3, end=3)
182
+ step1 = Prosereflect::Transform::ReplaceStep.new(
183
+ 3, 3,
184
+ Prosereflect::Transform::Slice.new(
185
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "y")]),
186
+ )
187
+ )
188
+ # step2: insert "x" at position 1-3 (ends at position 3 where step1 starts)
189
+ step2 = Prosereflect::Transform::ReplaceStep.new(
190
+ 1, 3,
191
+ Prosereflect::Transform::Slice.new(
192
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "x")]),
193
+ )
194
+ )
195
+ expect(step1.can_prepend_content?(step2)).to be true
196
+ end
197
+
198
+ it "returns false when other.slice is empty" do
199
+ step1 = Prosereflect::Transform::ReplaceStep.new(
200
+ 3, 3,
201
+ Prosereflect::Transform::Slice.new(
202
+ Prosereflect::Fragment.new([Prosereflect::Text.new(text: "y")]),
203
+ )
204
+ )
205
+ # step2 has empty slice
206
+ step2 = Prosereflect::Transform::ReplaceStep.new(1, 3, Prosereflect::Transform::Slice.empty)
207
+ expect(step1.can_prepend_content?(step2)).to be false
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Prosereflect::Transform::Structure do
6
+ let(:doc) do
7
+ Prosereflect::Parser.parse_document({
8
+ "type" => "doc",
9
+ "content" => [
10
+ {
11
+ "type" => "paragraph",
12
+ "content" => [
13
+ { "type" => "text", "text" => "Hello" },
14
+ ],
15
+ },
16
+ {
17
+ "type" => "paragraph",
18
+ "content" => [
19
+ { "type" => "text", "text" => "World" },
20
+ ],
21
+ },
22
+ ],
23
+ })
24
+ end
25
+
26
+ describe ".can_split?" do
27
+ it "returns false for negative position" do
28
+ expect(described_class.can_split?(doc, -1)).to be false
29
+ end
30
+
31
+ it "returns false for position beyond document" do
32
+ expect(described_class.can_split?(doc, 999)).to be false
33
+ end
34
+
35
+ it "returns true for valid position" do
36
+ expect(described_class.can_split?(doc, 1)).to be true
37
+ end
38
+
39
+ it "returns true for position 0" do
40
+ expect(described_class.can_split?(doc, 0)).to be true
41
+ end
42
+ end
43
+
44
+ describe ".can_join?" do
45
+ it "returns false for position 0" do
46
+ expect(described_class.can_join?(doc, 0)).to be false
47
+ end
48
+
49
+ it "returns false for position at end" do
50
+ expect(described_class.can_join?(doc, doc.node_size)).to be false
51
+ end
52
+
53
+ it "returns true for position between paragraphs" do
54
+ # Position 8 is between the two paragraphs (after "Hello" para, before "World" para)
55
+ expect(described_class.can_join?(doc, 8)).to be true
56
+ end
57
+ end
58
+
59
+ describe ".join_point?" do
60
+ it "returns nil for position at start" do
61
+ expect(described_class.join_point?(doc, 0)).to be false
62
+ end
63
+
64
+ it "returns nil at position within a paragraph" do
65
+ expect(described_class.join_point?(doc, 3)).to be false
66
+ end
67
+ end
68
+
69
+ describe ".lift_target" do
70
+ it "returns 0 for empty fragment" do
71
+ fragment = Prosereflect::Fragment.new([])
72
+ result = described_class.lift_target(fragment, 0, 5)
73
+ expect(result).to eq(0)
74
+ end
75
+
76
+ it "returns 0 for fragment with no defining nodes" do
77
+ text = Prosereflect::Text.new(text: "hello")
78
+ fragment = Prosereflect::Fragment.new([text])
79
+ result = described_class.lift_target(fragment, 0, 6)
80
+ expect(result).to eq(0)
81
+ end
82
+ end
83
+
84
+ describe ".find_wrapping" do
85
+ it "returns empty array for empty fragment" do
86
+ fragment = Prosereflect::Fragment.new([])
87
+ wrappers = described_class.find_wrapping(fragment, 0, 5, "paragraph")
88
+ expect(wrappers).to be_a(Array)
89
+ end
90
+
91
+ it "returns empty array for fragment with no defining nodes" do
92
+ text = Prosereflect::Text.new(text: "hello")
93
+ fragment = Prosereflect::Fragment.new([text])
94
+ wrappers = described_class.find_wrapping(fragment, 0, 6, "paragraph")
95
+ expect(wrappers).to eq([])
96
+ end
97
+ end
98
+ end