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,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "step"
|
|
4
|
+
require_relative "replace_step"
|
|
5
|
+
|
|
6
|
+
module Prosereflect
|
|
7
|
+
module Transform
|
|
8
|
+
# Structure predicates and helpers for document manipulation
|
|
9
|
+
class Structure
|
|
10
|
+
class << self
|
|
11
|
+
# Check if we can split at a position
|
|
12
|
+
# Returns true if the position allows a split (e.g., inside a text node)
|
|
13
|
+
def can_split?(doc, pos, types = nil)
|
|
14
|
+
return false if pos.negative? || pos > doc.node_size
|
|
15
|
+
|
|
16
|
+
resolved = doc.resolve(pos)
|
|
17
|
+
parent = resolved.parent
|
|
18
|
+
|
|
19
|
+
return false unless parent.respond_to?(:content)
|
|
20
|
+
|
|
21
|
+
node_type = parent.respond_to?(:type) ? parent.type : nil
|
|
22
|
+
|
|
23
|
+
if types
|
|
24
|
+
types.all? { |t| t.respond_to?(:name) ? t.name == node_type : t == node_type }
|
|
25
|
+
else
|
|
26
|
+
true
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Find the target depth for lifting content out of a wrapper
|
|
31
|
+
# Returns the depth to which content should be lifted
|
|
32
|
+
def lift_target(fragment, from, to)
|
|
33
|
+
depth = 0
|
|
34
|
+
pos = 0
|
|
35
|
+
|
|
36
|
+
fragment.content.each do |node|
|
|
37
|
+
node_end = pos + node.node_size
|
|
38
|
+
break if pos >= to
|
|
39
|
+
|
|
40
|
+
# This node is within the range being lifted
|
|
41
|
+
if pos < to && node_end > from && node.respond_to?(:defining?) && node.defining?
|
|
42
|
+
depth += 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
pos = node_end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
depth
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Find wrapper nodes needed to wrap a range
|
|
52
|
+
# Returns array of node types that would wrap the range
|
|
53
|
+
def find_wrapping(fragment, _from, to, node_type, attrs = nil)
|
|
54
|
+
wrappers = []
|
|
55
|
+
current_depth = 0
|
|
56
|
+
pos = 0
|
|
57
|
+
|
|
58
|
+
fragment.content.each do |node|
|
|
59
|
+
node_end = pos + node.node_size
|
|
60
|
+
break if pos >= to
|
|
61
|
+
|
|
62
|
+
if node.respond_to?(:defining?) && node.defining? && current_depth.zero?
|
|
63
|
+
# Found a defining node at the boundary
|
|
64
|
+
wrappers << build_wrapper(node_type, attrs)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
pos = node_end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
wrappers
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check if nodes can be joined at a position
|
|
74
|
+
def can_join?(doc, pos)
|
|
75
|
+
return false if pos <= 0 || pos >= doc.node_size
|
|
76
|
+
|
|
77
|
+
resolved = doc.resolve(pos)
|
|
78
|
+
|
|
79
|
+
# We need to be at a boundary between two children
|
|
80
|
+
# Check the node at the depth where we're at a child boundary
|
|
81
|
+
depth = resolved.depth
|
|
82
|
+
return false unless depth.positive?
|
|
83
|
+
|
|
84
|
+
parent = resolved.node(depth - 1)
|
|
85
|
+
return false unless parent.respond_to?(:content)
|
|
86
|
+
return false if parent.content.nil?
|
|
87
|
+
|
|
88
|
+
index = resolved.index(depth)
|
|
89
|
+
return false unless index.positive? && index < parent.content.size
|
|
90
|
+
|
|
91
|
+
prev_node = parent.content[index - 1]
|
|
92
|
+
next_node = parent.content[index]
|
|
93
|
+
|
|
94
|
+
prev_node.type == next_node.type
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Find positions where a join can happen
|
|
98
|
+
def join_point?(doc, pos)
|
|
99
|
+
return false unless can_join?(doc, pos)
|
|
100
|
+
|
|
101
|
+
pos
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def build_wrapper(node_type, attrs)
|
|
107
|
+
if node_type.is_a?(String)
|
|
108
|
+
Prosereflect::Node.from_h(
|
|
109
|
+
"type" => node_type,
|
|
110
|
+
"attrs" => attrs || {},
|
|
111
|
+
"content" => [],
|
|
112
|
+
)
|
|
113
|
+
else
|
|
114
|
+
node_type
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "step"
|
|
4
|
+
require_relative "step_map"
|
|
5
|
+
require_relative "mapping"
|
|
6
|
+
require_relative "slice"
|
|
7
|
+
require_relative "replace_step"
|
|
8
|
+
require_relative "replace_around_step"
|
|
9
|
+
require_relative "mark_step"
|
|
10
|
+
require_relative "attr_step"
|
|
11
|
+
require_relative "insert_step"
|
|
12
|
+
|
|
13
|
+
module Prosereflect
|
|
14
|
+
module Transform
|
|
15
|
+
# A chainable document transformation.
|
|
16
|
+
# Accumulates steps and their mappings.
|
|
17
|
+
class Transform
|
|
18
|
+
attr_reader :steps, :mapping
|
|
19
|
+
|
|
20
|
+
def initialize(doc)
|
|
21
|
+
@doc = doc
|
|
22
|
+
@steps = []
|
|
23
|
+
@mapping = Mapping.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Add a mark to all content in range
|
|
27
|
+
def add_mark(from, to, mark)
|
|
28
|
+
add_step(AddMarkStep.new(from, to, mark))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Remove a mark from all content in range
|
|
32
|
+
def remove_mark(from, to, mark)
|
|
33
|
+
add_step(RemoveMarkStep.new(from, to, mark))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Insert content at position
|
|
37
|
+
def insert(pos, content)
|
|
38
|
+
add_step(InsertStep.new(pos, content))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Delete content in range
|
|
42
|
+
def delete(from, to)
|
|
43
|
+
add_step(DeleteStep.new(from, to))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Replace content in range with slice
|
|
47
|
+
def replace(from, to, slice = Slice.empty)
|
|
48
|
+
add_step(ReplaceStep.new(from, to, slice))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Replace content with specific nodes
|
|
52
|
+
def replace_with(from, to, *nodes)
|
|
53
|
+
content = Fragment.new(nodes.flatten)
|
|
54
|
+
slice = Slice.new(content)
|
|
55
|
+
add_step(ReplaceStep.new(from, to, slice))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Set attribute on node at position
|
|
59
|
+
def set_node_attribute(pos, attrs)
|
|
60
|
+
add_step(AttrStep.new(pos, attrs))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Set document attribute
|
|
64
|
+
def set_doc_attribute(attrs)
|
|
65
|
+
add_step(DocAttrStep.new(attrs))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Apply all accumulated steps to the document
|
|
69
|
+
# Returns self for chaining
|
|
70
|
+
def apply
|
|
71
|
+
@steps.each do |step|
|
|
72
|
+
result = step.apply(@doc)
|
|
73
|
+
raise ApplyError, "Step #{step.class} failed: #{result.failed}" unless result.ok?
|
|
74
|
+
|
|
75
|
+
@doc = result.doc
|
|
76
|
+
end
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Apply and return the transformed document
|
|
81
|
+
def doc
|
|
82
|
+
# Apply pending steps
|
|
83
|
+
apply if @steps.any?
|
|
84
|
+
@doc
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if any steps have been applied
|
|
88
|
+
def empty?
|
|
89
|
+
@steps.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get the number of steps
|
|
93
|
+
def size
|
|
94
|
+
@steps.length
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Add a step and track its mapping
|
|
98
|
+
def add_step(step)
|
|
99
|
+
@steps << step
|
|
100
|
+
@mapping.add_map(step.get_map)
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Get the mapping for all applied steps
|
|
105
|
+
def maps
|
|
106
|
+
@mapping.to_a
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create a new transform with the same document
|
|
110
|
+
def clone
|
|
111
|
+
Transform.new(@doc)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Roll back the last step
|
|
115
|
+
def rollback
|
|
116
|
+
return self if @steps.empty?
|
|
117
|
+
|
|
118
|
+
step = @steps.pop
|
|
119
|
+
@mapping = Mapping.new(maps: @mapping.to_a[0...-1])
|
|
120
|
+
|
|
121
|
+
inverted = step.invert(@doc)
|
|
122
|
+
result = inverted.apply(@doc)
|
|
123
|
+
if result.ok?
|
|
124
|
+
@doc = result.doc
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if we can step forward
|
|
131
|
+
def can_apply?
|
|
132
|
+
@steps.all? { |step| step.apply(@doc).ok? }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Add mark using schema
|
|
136
|
+
def add_mark_by_type(from, to, mark_type_name, schema, attrs = nil)
|
|
137
|
+
mark_type = schema.mark_type(mark_type_name)
|
|
138
|
+
mark = mark_type.create(attrs)
|
|
139
|
+
add_mark(from, to, mark)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Remove mark using schema
|
|
143
|
+
def remove_mark_by_type(from, to, mark_type_name, schema)
|
|
144
|
+
mark_type = schema.mark_type(mark_type_name)
|
|
145
|
+
mark = mark_type.create
|
|
146
|
+
remove_mark(from, to, mark)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Lift content out of a wrapper node
|
|
150
|
+
# range: NodeRange representing the content to lift
|
|
151
|
+
# target: depth to lift to
|
|
152
|
+
def lift(range, target)
|
|
153
|
+
from_ = range.from
|
|
154
|
+
to_ = range.to
|
|
155
|
+
depth = range.depth
|
|
156
|
+
|
|
157
|
+
gap_start = position_before_depth(from_, depth + 1)
|
|
158
|
+
gap_end = position_after_depth(to_, depth + 1)
|
|
159
|
+
start = gap_start
|
|
160
|
+
end_pos = gap_end
|
|
161
|
+
|
|
162
|
+
before = Fragment.empty
|
|
163
|
+
open_start = 0
|
|
164
|
+
d = depth
|
|
165
|
+
splitting = false
|
|
166
|
+
while d > target
|
|
167
|
+
if splitting || node_index_at_depth(from_, d).positive?
|
|
168
|
+
splitting = true
|
|
169
|
+
before = Fragment.from(node_at_depth(from_, d).copy(before))
|
|
170
|
+
open_start += 1
|
|
171
|
+
else
|
|
172
|
+
start -= 1
|
|
173
|
+
end
|
|
174
|
+
d -= 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
after = Fragment.empty
|
|
178
|
+
open_end = 0
|
|
179
|
+
d = depth
|
|
180
|
+
splitting = false
|
|
181
|
+
while d > target
|
|
182
|
+
if splitting || position_after_depth(to_, d + 1) < node_end_at_depth(to_, d)
|
|
183
|
+
splitting = true
|
|
184
|
+
after = Fragment.from(node_at_depth(to_, d).copy(after))
|
|
185
|
+
open_end += 1
|
|
186
|
+
else
|
|
187
|
+
end_pos += 1
|
|
188
|
+
end
|
|
189
|
+
d -= 1
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
replace_around_step(
|
|
193
|
+
start,
|
|
194
|
+
end_pos,
|
|
195
|
+
gap_start,
|
|
196
|
+
gap_end,
|
|
197
|
+
Slice.new(before.append(after), open_start, open_end),
|
|
198
|
+
before.size - open_start,
|
|
199
|
+
true,
|
|
200
|
+
)
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Wrap content in nodes
|
|
205
|
+
# range: NodeRange representing the content to wrap
|
|
206
|
+
# wrappers: array of NodeTypeWithAttrs representing wrapper nodes
|
|
207
|
+
def wrap(range, wrappers)
|
|
208
|
+
content = Fragment.empty
|
|
209
|
+
i = wrappers.length - 1
|
|
210
|
+
while i >= 0
|
|
211
|
+
if content.size.positive?
|
|
212
|
+
match = wrappers[i].type.content_match.match_fragment(content)
|
|
213
|
+
unless match&.valid_end
|
|
214
|
+
raise TransformError, "Wrapper type given to Transform.wrap does not form valid content of its parent wrapper"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
content = Fragment.from(
|
|
218
|
+
wrappers[i].type.create(wrappers[i].attrs, content),
|
|
219
|
+
)
|
|
220
|
+
i -= 1
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
start = range.start
|
|
224
|
+
end_pos = range.end
|
|
225
|
+
replace_around_step(
|
|
226
|
+
start,
|
|
227
|
+
end_pos,
|
|
228
|
+
start,
|
|
229
|
+
end_pos,
|
|
230
|
+
Slice.new(content, 0, 0),
|
|
231
|
+
wrappers.length,
|
|
232
|
+
true,
|
|
233
|
+
)
|
|
234
|
+
self
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Split a node at a position
|
|
238
|
+
def split(pos, depth = 1)
|
|
239
|
+
resolved = @doc.resolve(pos)
|
|
240
|
+
before = Fragment.empty
|
|
241
|
+
open_start = 0
|
|
242
|
+
open_end = 0
|
|
243
|
+
|
|
244
|
+
d = depth
|
|
245
|
+
while d.positive?
|
|
246
|
+
node = resolved.node(d)
|
|
247
|
+
if d == depth
|
|
248
|
+
before = Fragment.from(node.copy(Fragment.empty))
|
|
249
|
+
open_start = node.is_a?(Prosereflect::Node) && node.content&.size.to_i.positive? ? 1 : 0
|
|
250
|
+
open_end = 0
|
|
251
|
+
else
|
|
252
|
+
before = Fragment.from(node.copy(before))
|
|
253
|
+
open_start += 1
|
|
254
|
+
end
|
|
255
|
+
d -= 1
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
step = ReplaceAroundStep.new(
|
|
259
|
+
pos,
|
|
260
|
+
pos,
|
|
261
|
+
pos,
|
|
262
|
+
pos,
|
|
263
|
+
Slice.new(before, open_start, open_end),
|
|
264
|
+
open_start,
|
|
265
|
+
structure: false,
|
|
266
|
+
)
|
|
267
|
+
add_step(step)
|
|
268
|
+
self
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Join two nodes at a position
|
|
272
|
+
def join(pos, depth = 1)
|
|
273
|
+
resolved = @doc.resolve(pos)
|
|
274
|
+
d = depth
|
|
275
|
+
while d.positive?
|
|
276
|
+
# Check if we can join at this depth
|
|
277
|
+
parent = resolved.node(d - 1) if d >= 1
|
|
278
|
+
idx = resolved.index(d)
|
|
279
|
+
|
|
280
|
+
if parent&.content && idx.positive? && idx < parent.content.size
|
|
281
|
+
before_node = parent.content[idx - 1]
|
|
282
|
+
after_node = parent.content[idx]
|
|
283
|
+
|
|
284
|
+
if before_node.type == after_node.type
|
|
285
|
+
# Join point is at the boundary between before_node and after_node
|
|
286
|
+
join_from = resolved.start(d) - before_node.node_size
|
|
287
|
+
join_to = resolved.start(d) + after_node.node_size
|
|
288
|
+
replace(join_from, join_to, Slice.empty)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
d -= 1
|
|
292
|
+
end
|
|
293
|
+
self
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
class ApplyError < StandardError; end
|
|
297
|
+
class TransformError < StandardError; end
|
|
298
|
+
|
|
299
|
+
def to_s
|
|
300
|
+
"<Transform steps=#{@steps.length}>"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def inspect
|
|
304
|
+
to_s
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
private
|
|
308
|
+
|
|
309
|
+
def replace_around_step(from, to, gap_from, gap_to, slice, insert, structure)
|
|
310
|
+
add_step(ReplaceAroundStep.new(from, to, gap_from, gap_to, slice, insert, structure: structure))
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def position_before_depth(pos, depth)
|
|
314
|
+
resolved = @doc.resolve(pos)
|
|
315
|
+
node = resolved.node(depth)
|
|
316
|
+
pos - (node.is_a?(Prosereflect::Node) ? 1 : 0)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def position_after_depth(pos, depth)
|
|
320
|
+
resolved = @doc.resolve(pos)
|
|
321
|
+
node = resolved.node(depth)
|
|
322
|
+
pos + (node.is_a?(Prosereflect::Node) ? 1 : 0)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def node_index_at_depth(pos, depth)
|
|
326
|
+
resolved = @doc.resolve(pos)
|
|
327
|
+
resolved.index(depth)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def node_at_depth(pos, depth)
|
|
331
|
+
resolved = @doc.resolve(pos)
|
|
332
|
+
resolved.node(depth)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def node_end_at_depth(pos, depth)
|
|
336
|
+
resolved = @doc.resolve(pos)
|
|
337
|
+
resolved.end(depth)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "transform/step_map"
|
|
4
|
+
require_relative "transform/mapping"
|
|
5
|
+
require_relative "transform/slice"
|
|
6
|
+
require_relative "transform/step"
|
|
7
|
+
require_relative "transform/replace_step"
|
|
8
|
+
require_relative "transform/mark_step"
|
|
9
|
+
require_relative "transform/attr_step"
|
|
10
|
+
require_relative "transform/insert_step"
|
|
11
|
+
require_relative "transform/structure"
|
|
12
|
+
require_relative "transform/transform"
|
|
13
|
+
|
|
14
|
+
module Prosereflect
|
|
15
|
+
# Transform system for chainable document transformations
|
|
16
|
+
# Based on ProseMirror's transform/step system
|
|
17
|
+
module Transform
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
20
|
+
# Raised when a step cannot be applied to a document
|
|
21
|
+
class ApplyError < Error; end
|
|
22
|
+
|
|
23
|
+
# Raised when a step cannot be inverted
|
|
24
|
+
class InvertError < Error; end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/prosereflect/version.rb
CHANGED
data/lib/prosereflect.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Prosereflect
|
|
|
11
11
|
autoload :CodeBlock, "prosereflect/code_block"
|
|
12
12
|
autoload :CodeBlockWrapper, "prosereflect/code_block_wrapper"
|
|
13
13
|
autoload :Document, "prosereflect/document"
|
|
14
|
+
autoload :Fragment, "prosereflect/fragment"
|
|
14
15
|
autoload :HardBreak, "prosereflect/hard_break"
|
|
15
16
|
autoload :Heading, "prosereflect/heading"
|
|
16
17
|
autoload :HorizontalRule, "prosereflect/horizontal_rule"
|
|
@@ -23,11 +24,13 @@ module Prosereflect
|
|
|
23
24
|
autoload :Output, "prosereflect/output"
|
|
24
25
|
autoload :Paragraph, "prosereflect/paragraph"
|
|
25
26
|
autoload :Parser, "prosereflect/parser"
|
|
27
|
+
autoload :ResolvedPos, "prosereflect/resolved_pos"
|
|
26
28
|
autoload :Table, "prosereflect/table"
|
|
27
29
|
autoload :TableCell, "prosereflect/table_cell"
|
|
28
30
|
autoload :TableHeader, "prosereflect/table_header"
|
|
29
31
|
autoload :TableRow, "prosereflect/table_row"
|
|
30
32
|
autoload :Text, "prosereflect/text"
|
|
33
|
+
autoload :Transform, "prosereflect/transform"
|
|
31
34
|
autoload :User, "prosereflect/user"
|
|
32
35
|
autoload :VERSION, "prosereflect/version"
|
|
33
36
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type: doc
|
|
2
|
+
content:
|
|
3
|
+
- type: bullet_list
|
|
4
|
+
content:
|
|
5
|
+
- type: list_item
|
|
6
|
+
content:
|
|
7
|
+
- type: paragraph
|
|
8
|
+
content:
|
|
9
|
+
- type: text
|
|
10
|
+
text: Bullet 1
|
|
11
|
+
- type: list_item
|
|
12
|
+
content:
|
|
13
|
+
- type: paragraph
|
|
14
|
+
content:
|
|
15
|
+
- type: text
|
|
16
|
+
text: Bullet 2
|
|
17
|
+
- type: ordered_list
|
|
18
|
+
attrs:
|
|
19
|
+
order: 1
|
|
20
|
+
content:
|
|
21
|
+
- type: list_item
|
|
22
|
+
content:
|
|
23
|
+
- type: paragraph
|
|
24
|
+
content:
|
|
25
|
+
- type: text
|
|
26
|
+
text: Item 1
|
|
27
|
+
- type: list_item
|
|
28
|
+
content:
|
|
29
|
+
- type: paragraph
|
|
30
|
+
content:
|
|
31
|
+
- type: text
|
|
32
|
+
text: Item 2
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type: doc
|
|
2
|
+
content:
|
|
3
|
+
- type: heading
|
|
4
|
+
attrs:
|
|
5
|
+
level: 2
|
|
6
|
+
content:
|
|
7
|
+
- type: text
|
|
8
|
+
text: Mixed Content
|
|
9
|
+
- type: paragraph
|
|
10
|
+
content:
|
|
11
|
+
- type: text
|
|
12
|
+
text: Normal text
|
|
13
|
+
- type: text
|
|
14
|
+
text: " bold"
|
|
15
|
+
marks:
|
|
16
|
+
- type: bold
|
|
17
|
+
- type: text
|
|
18
|
+
text: " and "
|
|
19
|
+
- type: text
|
|
20
|
+
text: italic
|
|
21
|
+
marks:
|
|
22
|
+
- type: italic
|
|
23
|
+
- type: text
|
|
24
|
+
text: " with a "
|
|
25
|
+
- type: text
|
|
26
|
+
text: link
|
|
27
|
+
marks:
|
|
28
|
+
- type: link
|
|
29
|
+
attrs:
|
|
30
|
+
href: https://example.com
|
|
31
|
+
- type: code_block
|
|
32
|
+
attrs:
|
|
33
|
+
language: ruby
|
|
34
|
+
content:
|
|
35
|
+
- type: text
|
|
36
|
+
text: puts 'hello'
|
|
37
|
+
- type: image
|
|
38
|
+
attrs:
|
|
39
|
+
src: https://example.com/image.png
|
|
40
|
+
alt: Example image
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type: doc
|
|
2
|
+
content:
|
|
3
|
+
- type: blockquote
|
|
4
|
+
content:
|
|
5
|
+
- type: paragraph
|
|
6
|
+
content:
|
|
7
|
+
- type: text
|
|
8
|
+
text: Quoted text
|
|
9
|
+
- type: paragraph
|
|
10
|
+
content:
|
|
11
|
+
- type: text
|
|
12
|
+
text: More quoted text
|
|
13
|
+
- type: horizontal_rule
|
|
14
|
+
- type: paragraph
|
|
15
|
+
content:
|
|
16
|
+
- type: text
|
|
17
|
+
text: "Line 1"
|
|
18
|
+
- type: hard_break
|
|
19
|
+
- type: text
|
|
20
|
+
text: "Line 2"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
type: doc
|
|
2
|
+
content:
|
|
3
|
+
- type: table
|
|
4
|
+
content:
|
|
5
|
+
- type: table_row
|
|
6
|
+
content:
|
|
7
|
+
- type: table_header
|
|
8
|
+
content:
|
|
9
|
+
- type: paragraph
|
|
10
|
+
content:
|
|
11
|
+
- type: text
|
|
12
|
+
text: Header 1
|
|
13
|
+
- type: table_header
|
|
14
|
+
content:
|
|
15
|
+
- type: paragraph
|
|
16
|
+
content:
|
|
17
|
+
- type: text
|
|
18
|
+
text: Header 2
|
|
19
|
+
- type: table_row
|
|
20
|
+
content:
|
|
21
|
+
- type: table_cell
|
|
22
|
+
content:
|
|
23
|
+
- type: paragraph
|
|
24
|
+
content:
|
|
25
|
+
- type: text
|
|
26
|
+
text: Cell 1
|
|
27
|
+
- type: table_cell
|
|
28
|
+
content:
|
|
29
|
+
- type: paragraph
|
|
30
|
+
content:
|
|
31
|
+
- type: text
|
|
32
|
+
text: Cell 2
|