prosereflect 0.1.1 → 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/.github/workflows/rake.yml +4 -0
- data/.github/workflows/release.yml +5 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +119 -183
- data/CLAUDE.md +78 -0
- data/Gemfile +8 -4
- data/README.adoc +2 -0
- data/Rakefile +3 -3
- 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/attribute/base.rb +4 -6
- data/lib/prosereflect/attribute/bold.rb +2 -4
- data/lib/prosereflect/attribute/href.rb +1 -3
- data/lib/prosereflect/attribute/id.rb +7 -7
- data/lib/prosereflect/attribute.rb +4 -7
- data/lib/prosereflect/blockquote.rb +19 -11
- data/lib/prosereflect/bullet_list.rb +36 -29
- data/lib/prosereflect/code_block.rb +23 -27
- data/lib/prosereflect/code_block_wrapper.rb +12 -13
- data/lib/prosereflect/document.rb +14 -22
- data/lib/prosereflect/fragment.rb +249 -0
- data/lib/prosereflect/hard_break.rb +6 -6
- data/lib/prosereflect/heading.rb +14 -15
- data/lib/prosereflect/horizontal_rule.rb +23 -14
- data/lib/prosereflect/image.rb +32 -23
- data/lib/prosereflect/input/html.rb +179 -104
- data/lib/prosereflect/input.rb +7 -0
- data/lib/prosereflect/list_item.rb +11 -12
- data/lib/prosereflect/mark/base.rb +9 -11
- data/lib/prosereflect/mark/bold.rb +1 -3
- data/lib/prosereflect/mark/code.rb +1 -3
- data/lib/prosereflect/mark/italic.rb +1 -3
- data/lib/prosereflect/mark/link.rb +1 -3
- data/lib/prosereflect/mark/strike.rb +1 -3
- data/lib/prosereflect/mark/subscript.rb +1 -3
- data/lib/prosereflect/mark/superscript.rb +1 -3
- data/lib/prosereflect/mark/underline.rb +1 -3
- data/lib/prosereflect/mark.rb +9 -5
- data/lib/prosereflect/node.rb +171 -33
- data/lib/prosereflect/ordered_list.rb +17 -14
- data/lib/prosereflect/output/html.rb +279 -50
- data/lib/prosereflect/output.rb +7 -0
- data/lib/prosereflect/paragraph.rb +11 -13
- data/lib/prosereflect/parser.rb +56 -66
- 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/table.rb +12 -13
- data/lib/prosereflect/table_cell.rb +13 -13
- data/lib/prosereflect/table_header.rb +17 -17
- data/lib/prosereflect/table_row.rb +12 -12
- data/lib/prosereflect/text.rb +35 -11
- 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/user.rb +15 -15
- data/lib/prosereflect/version.rb +1 -1
- data/lib/prosereflect.rb +30 -17
- data/prosereflect.gemspec +17 -16
- 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 +332 -330
- data/spec/prosereflect/fragment_spec.rb +273 -0
- data/spec/prosereflect/hard_break_spec.rb +125 -125
- data/spec/prosereflect/input/html_spec.rb +718 -522
- data/spec/prosereflect/node_spec.rb +311 -182
- data/spec/prosereflect/output/html_spec.rb +105 -105
- data/spec/prosereflect/output/whitespace_spec.rb +248 -0
- data/spec/prosereflect/paragraph_spec.rb +275 -274
- data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
- data/spec/prosereflect/parser_spec.rb +185 -180
- 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/table_cell_spec.rb +183 -183
- data/spec/prosereflect/table_row_spec.rb +149 -149
- data/spec/prosereflect/table_spec.rb +320 -318
- data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
- data/spec/prosereflect/text_spec.rb +133 -132
- 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/prosereflect/user_spec.rb +31 -28
- data/spec/prosereflect_spec.rb +28 -26
- data/spec/spec_helper.rb +7 -6
- data/spec/support/matchers.rb +6 -6
- data/spec/support/shared_examples.rb +49 -49
- metadata +96 -5
- data/spec/prosereflect/version_spec.rb +0 -11
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'table_cell'
|
|
4
|
-
|
|
5
3
|
module Prosereflect
|
|
6
4
|
# TableHeader class represents a header cell in a table (<th> tag).
|
|
7
5
|
# It inherits from TableCell but adds header-specific attributes.
|
|
8
6
|
class TableHeader < TableCell
|
|
9
|
-
PM_TYPE =
|
|
7
|
+
PM_TYPE = "table_header"
|
|
10
8
|
|
|
11
|
-
attribute :type, :string, default: -> {
|
|
9
|
+
attribute :type, :string, default: -> {
|
|
10
|
+
self.class.send(:const_get, "PM_TYPE")
|
|
11
|
+
}
|
|
12
12
|
attribute :scope, :string # row, col, rowgroup, or colgroup
|
|
13
13
|
attribute :abbr, :string # abbreviated version of content
|
|
14
14
|
attribute :colspan, :integer # number of columns this header spans
|
|
15
15
|
|
|
16
16
|
key_value do
|
|
17
|
-
map
|
|
18
|
-
map
|
|
19
|
-
map
|
|
17
|
+
map "type", to: :type, render_default: true
|
|
18
|
+
map "content", to: :content
|
|
19
|
+
map "attrs", to: :attrs
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def initialize(attributes = {})
|
|
@@ -41,21 +41,21 @@ module Prosereflect
|
|
|
41
41
|
return unless %w[row col rowgroup colgroup].include?(scope_value)
|
|
42
42
|
|
|
43
43
|
self.attrs ||= {}
|
|
44
|
-
attrs[
|
|
44
|
+
attrs["scope"] = scope_value
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def scope
|
|
48
|
-
attrs&.[](
|
|
48
|
+
attrs&.[]("scope")
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Set abbreviated version of the header content
|
|
52
52
|
def abbr=(abbr_text)
|
|
53
53
|
self.attrs ||= {}
|
|
54
|
-
attrs[
|
|
54
|
+
attrs["abbr"] = abbr_text
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def abbr
|
|
58
|
-
attrs&.[](
|
|
58
|
+
attrs&.[]("abbr")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Set the number of columns this header spans
|
|
@@ -63,11 +63,11 @@ module Prosereflect
|
|
|
63
63
|
return unless span.to_i.positive?
|
|
64
64
|
|
|
65
65
|
self.attrs ||= {}
|
|
66
|
-
attrs[
|
|
66
|
+
attrs["colspan"] = span.to_i
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def colspan
|
|
70
|
-
attrs&.[](
|
|
70
|
+
attrs&.[]("colspan")
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Get header attributes as a hash
|
|
@@ -75,16 +75,16 @@ module Prosereflect
|
|
|
75
75
|
{
|
|
76
76
|
scope: scope,
|
|
77
77
|
abbr: abbr,
|
|
78
|
-
colspan: colspan
|
|
78
|
+
colspan: colspan,
|
|
79
79
|
}.compact
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
# Override to_h to exclude nil attributes
|
|
83
83
|
def to_h
|
|
84
84
|
result = super
|
|
85
|
-
if result[
|
|
86
|
-
result[
|
|
87
|
-
result.delete(
|
|
85
|
+
if result["attrs"]
|
|
86
|
+
result["attrs"].compact!
|
|
87
|
+
result.delete("attrs") if result["attrs"].empty?
|
|
88
88
|
end
|
|
89
89
|
result
|
|
90
90
|
end
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'node'
|
|
4
|
-
require_relative 'table_cell'
|
|
5
|
-
|
|
6
3
|
module Prosereflect
|
|
7
4
|
class TableRow < Node
|
|
8
|
-
PM_TYPE =
|
|
5
|
+
PM_TYPE = "table_row"
|
|
9
6
|
|
|
10
|
-
attribute :type, :string, default: -> {
|
|
7
|
+
attribute :type, :string, default: -> {
|
|
8
|
+
self.class.send(:const_get, "PM_TYPE")
|
|
9
|
+
}
|
|
11
10
|
|
|
12
11
|
key_value do
|
|
13
|
-
map
|
|
14
|
-
map
|
|
15
|
-
map
|
|
12
|
+
map "type", to: :type, render_default: true
|
|
13
|
+
map "content", to: :content
|
|
14
|
+
map "attrs", to: :attrs
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
def initialize(opts = {})
|
|
@@ -44,10 +43,11 @@ module Prosereflect
|
|
|
44
43
|
# Override to_h to handle empty content and attributes properly
|
|
45
44
|
def to_h
|
|
46
45
|
result = super
|
|
47
|
-
result[
|
|
48
|
-
if result[
|
|
49
|
-
result[
|
|
50
|
-
|
|
46
|
+
result["content"] ||= []
|
|
47
|
+
if result["attrs"]
|
|
48
|
+
result["attrs"] =
|
|
49
|
+
result["attrs"].is_a?(Hash) && result["attrs"][:attrs] ? result["attrs"][:attrs] : result["attrs"]
|
|
50
|
+
result.delete("attrs") if result["attrs"].empty?
|
|
51
51
|
end
|
|
52
52
|
result
|
|
53
53
|
end
|
data/lib/prosereflect/text.rb
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'node'
|
|
4
|
-
|
|
5
3
|
module Prosereflect
|
|
6
4
|
class Text < Node
|
|
7
|
-
PM_TYPE =
|
|
5
|
+
PM_TYPE = "text"
|
|
8
6
|
|
|
9
|
-
attribute :type, :string, default: -> {
|
|
10
|
-
|
|
7
|
+
attribute :type, :string, default: -> {
|
|
8
|
+
self.class.send(:const_get, "PM_TYPE")
|
|
9
|
+
}
|
|
10
|
+
attribute :text, :string, default: ""
|
|
11
11
|
|
|
12
12
|
key_value do
|
|
13
|
-
map
|
|
14
|
-
map
|
|
15
|
-
map
|
|
13
|
+
map "type", to: :type, render_default: true
|
|
14
|
+
map "text", to: :text
|
|
15
|
+
map "marks", to: :marks
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def self.create(text =
|
|
18
|
+
def self.create(text = "", marks = nil)
|
|
19
19
|
new(text: text, marks: marks)
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def text_content
|
|
23
|
-
text ||
|
|
23
|
+
text || ""
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Text node size is text length + 1 (for the opening token)
|
|
27
|
+
def node_size
|
|
28
|
+
(text || "").length + 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Text nodes are text nodes
|
|
32
|
+
def text?
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Return a copy of this text node with content restricted to range
|
|
37
|
+
def cut(from = 0, to = nil)
|
|
38
|
+
txt = text || ""
|
|
39
|
+
to ||= txt.length
|
|
40
|
+
self.class.new(text: txt[from...to], marks: raw_marks)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check equality with another text node
|
|
44
|
+
def eq?(other)
|
|
45
|
+
return false unless other.is_a?(self.class)
|
|
46
|
+
|
|
47
|
+
text == other.text && to_h == other.to_h
|
|
24
48
|
end
|
|
25
49
|
|
|
26
50
|
# Override the to_h method to include the text attribute
|
|
27
51
|
def to_h
|
|
28
52
|
result = super
|
|
29
|
-
result[
|
|
53
|
+
result["text"] = text
|
|
30
54
|
result
|
|
31
55
|
end
|
|
32
56
|
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "step"
|
|
4
|
+
require_relative "step_map"
|
|
5
|
+
|
|
6
|
+
module Prosereflect
|
|
7
|
+
module Transform
|
|
8
|
+
# Set or remove attributes on a node at a position
|
|
9
|
+
class AttrStep < Step
|
|
10
|
+
attr_reader :pos, :attrs
|
|
11
|
+
|
|
12
|
+
def initialize(pos, attrs)
|
|
13
|
+
super()
|
|
14
|
+
@pos = pos
|
|
15
|
+
@attrs = attrs
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def apply(doc)
|
|
19
|
+
return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
new_doc = set_node_attrs(doc)
|
|
23
|
+
Result.ok(new_doc)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
Result.fail(e.message)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def get_map
|
|
30
|
+
StepMap.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def invert(doc)
|
|
34
|
+
# Find what attrs were changed and revert them
|
|
35
|
+
old_attrs = get_old_attrs(doc)
|
|
36
|
+
AttrStep.new(@pos, old_attrs)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def step_type
|
|
40
|
+
"setAttr"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_json(*_args)
|
|
44
|
+
json = super
|
|
45
|
+
json["pos"] = @pos
|
|
46
|
+
json["attrs"] = @attrs
|
|
47
|
+
json
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.from_json(_schema, json)
|
|
51
|
+
new(json["pos"], json["attrs"])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def set_node_attrs(doc)
|
|
57
|
+
target_node = find_node_at(doc, @pos)
|
|
58
|
+
return doc unless target_node
|
|
59
|
+
|
|
60
|
+
new_attrs = compute_new_attrs(target_node)
|
|
61
|
+
replace_node_with_new_attrs(doc, target_node, new_attrs)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def compute_new_attrs(target_node)
|
|
65
|
+
new_attrs = target_node.attrs.merge(@attrs)
|
|
66
|
+
new_attrs.compact!
|
|
67
|
+
new_attrs
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def replace_node_with_new_attrs(doc, target_node, new_attrs)
|
|
71
|
+
new_content = doc.content.to_a.map { |node| replace_node(node, target_node, new_attrs) }
|
|
72
|
+
doc.class.new(content: Fragment.new(new_content), attrs: doc.attrs.dup)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def replace_node(node, target_node, new_attrs)
|
|
76
|
+
return node unless node == target_node
|
|
77
|
+
|
|
78
|
+
node.class.new(
|
|
79
|
+
content: node.content,
|
|
80
|
+
marks: node.marks,
|
|
81
|
+
attrs: new_attrs,
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def find_node_at(doc, pos)
|
|
86
|
+
result = nil
|
|
87
|
+
doc.nodes_between(pos, pos + 1) do |node|
|
|
88
|
+
result = node
|
|
89
|
+
end
|
|
90
|
+
result
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_old_attrs(doc)
|
|
94
|
+
target_node = find_node_at(doc, @pos)
|
|
95
|
+
return {} unless target_node
|
|
96
|
+
|
|
97
|
+
# Return only the attrs that we're changing
|
|
98
|
+
@attrs.keys.each_with_object({}) do |key, old|
|
|
99
|
+
old[key] = target_node.attrs[key] if target_node.attrs.key?(key)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Set or remove document-level attributes
|
|
105
|
+
class DocAttrStep < Step
|
|
106
|
+
attr_reader :attrs
|
|
107
|
+
|
|
108
|
+
def initialize(attrs)
|
|
109
|
+
super()
|
|
110
|
+
@attrs = attrs
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply(doc)
|
|
114
|
+
new_doc = set_doc_attrs(doc)
|
|
115
|
+
Result.ok(new_doc)
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
Result.fail(e.message)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def get_map
|
|
121
|
+
StepMap.new
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def invert(doc)
|
|
125
|
+
old_attrs = get_old_doc_attrs(doc)
|
|
126
|
+
DocAttrStep.new(old_attrs)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def step_type
|
|
130
|
+
"setDocAttr"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_json(*_args)
|
|
134
|
+
json = super
|
|
135
|
+
json["attrs"] = @attrs
|
|
136
|
+
json
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.from_json(_schema, json)
|
|
140
|
+
new(json["attrs"])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def set_doc_attrs(doc)
|
|
146
|
+
new_attrs = doc.attrs.merge(@attrs).compact
|
|
147
|
+
doc.class.new(content: doc.content, attrs: new_attrs)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def get_old_doc_attrs(doc)
|
|
151
|
+
@attrs.keys.each_with_object({}) do |key, old|
|
|
152
|
+
old[key] = doc.attrs[key] if doc.attrs.key?(key)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "step"
|
|
4
|
+
require_relative "step_map"
|
|
5
|
+
require_relative "replace_step"
|
|
6
|
+
require_relative "slice"
|
|
7
|
+
|
|
8
|
+
module Prosereflect
|
|
9
|
+
module Transform
|
|
10
|
+
# Insert content at a position
|
|
11
|
+
class InsertStep < Step
|
|
12
|
+
attr_reader :pos, :content
|
|
13
|
+
|
|
14
|
+
def initialize(pos, content)
|
|
15
|
+
super()
|
|
16
|
+
@pos = pos
|
|
17
|
+
@content = content.is_a?(Fragment) ? content : Fragment.new(content)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def apply(doc)
|
|
21
|
+
return Result.fail("Invalid position") if @pos.negative? || @pos > doc.node_size
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
slice = Slice.new(@content)
|
|
25
|
+
replace_step = ReplaceStep.new(@pos, @pos, slice)
|
|
26
|
+
replace_step.apply(doc)
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
Result.fail(e.message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_map
|
|
33
|
+
delta = @content.size
|
|
34
|
+
StepMap.new([[@pos, @pos, @pos, @pos + delta]])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def invert(_doc)
|
|
38
|
+
DeleteStep.new(@pos, @pos + @content.size)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def step_type
|
|
42
|
+
"insert"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_json(*_args)
|
|
46
|
+
json = super
|
|
47
|
+
json["pos"] = @pos
|
|
48
|
+
json["content"] = @content.to_a.map(&:to_h)
|
|
49
|
+
json
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.from_json(_schema, json)
|
|
53
|
+
content = (json["content"] || []).map { |h| Prosereflect::Node.from_h(h) }
|
|
54
|
+
new(json["pos"], Fragment.new(content))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Delete content in a range
|
|
59
|
+
class DeleteStep < Step
|
|
60
|
+
attr_reader :from, :to
|
|
61
|
+
|
|
62
|
+
def initialize(from, to)
|
|
63
|
+
super()
|
|
64
|
+
@from = from
|
|
65
|
+
@to = to
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def apply(doc)
|
|
69
|
+
return Result.fail("Invalid positions") if @from > @to
|
|
70
|
+
return Result.fail("from < 0") if @from.negative?
|
|
71
|
+
return Result.fail("to > doc size") if @to > doc.node_size
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
replace_step = ReplaceStep.new(@from, @to, Slice.empty)
|
|
75
|
+
replace_step.apply(doc)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Result.fail(e.message)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def get_map
|
|
82
|
+
StepMap.delete(@from, @to)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def invert(doc)
|
|
86
|
+
# Find what was deleted
|
|
87
|
+
deleted = content_between(doc, @from, @to)
|
|
88
|
+
InsertStep.new(@from, deleted)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def step_type
|
|
92
|
+
"delete"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_json(*_args)
|
|
96
|
+
json = super
|
|
97
|
+
json["from"] = @from
|
|
98
|
+
json["to"] = @to
|
|
99
|
+
json
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.from_json(_schema, json)
|
|
103
|
+
new(json["from"], json["to"])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def content_between(doc, from, to)
|
|
109
|
+
result = []
|
|
110
|
+
doc.nodes_between(from, to) { |node| result << node }
|
|
111
|
+
Fragment.new(result)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "step_map"
|
|
4
|
+
|
|
5
|
+
module Prosereflect
|
|
6
|
+
module Transform
|
|
7
|
+
# Tracks position changes through a series of steps.
|
|
8
|
+
# Maps positions forward through the transformation.
|
|
9
|
+
class Mapping
|
|
10
|
+
attr_reader :maps
|
|
11
|
+
attr_accessor :from, :to
|
|
12
|
+
|
|
13
|
+
def initialize(maps: [])
|
|
14
|
+
@maps = maps.dup
|
|
15
|
+
@from = 0
|
|
16
|
+
@to = maps.length
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Add a step map to this mapping
|
|
20
|
+
def add_map(step_map, index = nil)
|
|
21
|
+
if index
|
|
22
|
+
@maps.insert(index, step_map)
|
|
23
|
+
else
|
|
24
|
+
@maps << step_map
|
|
25
|
+
end
|
|
26
|
+
@to = @maps.length
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Map a position through all steps in this mapping
|
|
30
|
+
def map(pos, on_del: nil) # rubocop:disable Lint:UnusedMethodArgument
|
|
31
|
+
@maps.each do |step_map|
|
|
32
|
+
pos = step_map.map(pos)
|
|
33
|
+
end
|
|
34
|
+
pos
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Map a position with deletion tracking
|
|
38
|
+
def map_result(pos, on_del: nil)
|
|
39
|
+
deleted = false
|
|
40
|
+
@maps.each do |step_map|
|
|
41
|
+
result = step_map.map_result(pos, on_del: on_del)
|
|
42
|
+
deleted ||= result.deleted
|
|
43
|
+
pos = result.pos
|
|
44
|
+
end
|
|
45
|
+
{ pos: pos, deleted: deleted }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Map a position backwards through the mapping
|
|
49
|
+
def map_reverse(pos)
|
|
50
|
+
result = pos
|
|
51
|
+
(0...@maps.length).each do |i|
|
|
52
|
+
step_map = @maps[@maps.length - 1 - i]
|
|
53
|
+
result = step_map.map_reverse(result)
|
|
54
|
+
end
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if a position was deleted
|
|
59
|
+
def map_deletes(pos)
|
|
60
|
+
@maps.any? { |step_map| step_map.deleted?(pos) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Get the mapping as an array of step maps
|
|
64
|
+
def to_a
|
|
65
|
+
@maps.dup
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Create from a single step map
|
|
69
|
+
def self.from_step_map(step_map)
|
|
70
|
+
new(maps: [step_map])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_s
|
|
74
|
+
"<Mapping maps=#{@maps.length}>"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
to_s
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|