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
data/lib/prosereflect/parser.rb
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'node'
|
|
4
|
-
require_relative 'text'
|
|
5
|
-
require_relative 'paragraph'
|
|
6
|
-
require_relative 'table'
|
|
7
|
-
require_relative 'table_row'
|
|
8
|
-
require_relative 'table_cell'
|
|
9
|
-
require_relative 'table_header'
|
|
10
|
-
require_relative 'hard_break'
|
|
11
|
-
require_relative 'document'
|
|
12
|
-
require_relative 'heading'
|
|
13
|
-
require_relative 'mark/bold'
|
|
14
|
-
require_relative 'mark/italic'
|
|
15
|
-
require_relative 'mark/code'
|
|
16
|
-
require_relative 'mark/link'
|
|
17
|
-
require_relative 'ordered_list'
|
|
18
|
-
require_relative 'bullet_list'
|
|
19
|
-
require_relative 'list_item'
|
|
20
|
-
require_relative 'blockquote'
|
|
21
|
-
require_relative 'horizontal_rule'
|
|
22
|
-
require_relative 'image'
|
|
23
|
-
require_relative 'user'
|
|
24
|
-
|
|
25
3
|
module Prosereflect
|
|
26
4
|
class Parser
|
|
27
5
|
def self.parse(data)
|
|
@@ -33,57 +11,61 @@ module Prosereflect
|
|
|
33
11
|
def self.parse_node(data)
|
|
34
12
|
return nil unless data.is_a?(Hash)
|
|
35
13
|
|
|
36
|
-
type = data[
|
|
37
|
-
text = data[
|
|
38
|
-
attrs = data[
|
|
39
|
-
marks_data = data[
|
|
14
|
+
type = data["type"]
|
|
15
|
+
text = data["text"]
|
|
16
|
+
attrs = data["attrs"]
|
|
17
|
+
marks_data = data["marks"]
|
|
40
18
|
|
|
41
19
|
# Find the right class based on type
|
|
42
20
|
node_class = case type
|
|
43
|
-
when
|
|
21
|
+
when "doc"
|
|
44
22
|
Document
|
|
45
|
-
when
|
|
23
|
+
when "paragraph"
|
|
46
24
|
Paragraph
|
|
47
|
-
when
|
|
25
|
+
when "text"
|
|
48
26
|
Text
|
|
49
|
-
when
|
|
27
|
+
when "table"
|
|
50
28
|
Table
|
|
51
|
-
when
|
|
29
|
+
when "table_row"
|
|
52
30
|
TableRow
|
|
53
|
-
when
|
|
31
|
+
when "table_cell"
|
|
54
32
|
TableCell
|
|
55
|
-
when
|
|
33
|
+
when "table_header"
|
|
56
34
|
TableHeader
|
|
57
|
-
when
|
|
35
|
+
when "hard_break"
|
|
58
36
|
HardBreak
|
|
59
|
-
when
|
|
37
|
+
when "heading"
|
|
60
38
|
Heading
|
|
61
|
-
when
|
|
39
|
+
when "ordered_list"
|
|
62
40
|
OrderedList
|
|
63
|
-
when
|
|
41
|
+
when "bullet_list"
|
|
64
42
|
BulletList
|
|
65
|
-
when
|
|
43
|
+
when "list_item"
|
|
66
44
|
ListItem
|
|
67
|
-
when
|
|
45
|
+
when "blockquote"
|
|
68
46
|
Blockquote
|
|
69
|
-
when
|
|
47
|
+
when "horizontal_rule"
|
|
70
48
|
HorizontalRule
|
|
71
|
-
when
|
|
49
|
+
when "image"
|
|
72
50
|
Image
|
|
73
|
-
when
|
|
51
|
+
when "code_block"
|
|
52
|
+
CodeBlock
|
|
53
|
+
when "code_block_wrapper"
|
|
54
|
+
CodeBlockWrapper
|
|
55
|
+
when "user"
|
|
74
56
|
User
|
|
75
57
|
else
|
|
76
58
|
Node
|
|
77
59
|
end
|
|
78
60
|
|
|
79
|
-
if type ==
|
|
61
|
+
if type == "text"
|
|
80
62
|
node = Text.new(text: text)
|
|
81
63
|
else
|
|
82
64
|
node = node_class.create(attrs)
|
|
83
65
|
|
|
84
66
|
# Process content recursively
|
|
85
|
-
if data[
|
|
86
|
-
data[
|
|
67
|
+
if data["content"].is_a?(Array)
|
|
68
|
+
data["content"].each do |content_data|
|
|
87
69
|
child_node = parse_node(content_data)
|
|
88
70
|
node.add_child(child_node) if child_node
|
|
89
71
|
end
|
|
@@ -92,30 +74,35 @@ module Prosereflect
|
|
|
92
74
|
|
|
93
75
|
# Handle special attributes for specific node types
|
|
94
76
|
case type
|
|
95
|
-
when
|
|
96
|
-
node.start = attrs[
|
|
97
|
-
when
|
|
98
|
-
node.bullet_style = attrs[
|
|
99
|
-
when
|
|
100
|
-
node.citation = attrs[
|
|
101
|
-
when
|
|
77
|
+
when "ordered_list"
|
|
78
|
+
node.start = attrs["start"].to_i if attrs && attrs["start"]
|
|
79
|
+
when "bullet_list"
|
|
80
|
+
node.bullet_style = attrs["bullet_style"] if attrs && attrs["bullet_style"]
|
|
81
|
+
when "blockquote"
|
|
82
|
+
node.citation = attrs["cite"] if attrs && attrs["cite"]
|
|
83
|
+
when "horizontal_rule"
|
|
84
|
+
if attrs
|
|
85
|
+
node.style = attrs["border_style"] if attrs["border_style"]
|
|
86
|
+
node.width = attrs["width"] if attrs["width"]
|
|
87
|
+
node.thickness = attrs["thickness"].to_i if attrs["thickness"]
|
|
88
|
+
end
|
|
89
|
+
when "image"
|
|
102
90
|
if attrs
|
|
103
|
-
node.
|
|
104
|
-
node.
|
|
105
|
-
node.
|
|
91
|
+
node.src = attrs["src"] if attrs["src"]
|
|
92
|
+
node.alt = attrs["alt"] if attrs["alt"]
|
|
93
|
+
node.title = attrs["title"] if attrs["title"]
|
|
94
|
+
node.dimensions = [attrs["width"]&.to_i, attrs["height"]&.to_i]
|
|
106
95
|
end
|
|
107
|
-
when
|
|
96
|
+
when "table_header"
|
|
108
97
|
if attrs
|
|
109
|
-
node.
|
|
110
|
-
node.
|
|
111
|
-
node.
|
|
112
|
-
node.dimensions = [attrs['width']&.to_i, attrs['height']&.to_i]
|
|
98
|
+
node.scope = attrs["scope"] if attrs["scope"]
|
|
99
|
+
node.abbr = attrs["abbr"] if attrs["abbr"]
|
|
100
|
+
node.colspan = attrs["colspan"] if attrs["colspan"]
|
|
113
101
|
end
|
|
114
|
-
when
|
|
102
|
+
when "code_block"
|
|
115
103
|
if attrs
|
|
116
|
-
node.
|
|
117
|
-
node.
|
|
118
|
-
node.colspan = attrs['colspan'] if attrs['colspan']
|
|
104
|
+
node.language = attrs["language"] if attrs["language"]
|
|
105
|
+
node.line_numbers = attrs["line_numbers"] if attrs["line_numbers"]
|
|
119
106
|
end
|
|
120
107
|
end
|
|
121
108
|
|
|
@@ -125,8 +112,11 @@ module Prosereflect
|
|
|
125
112
|
end
|
|
126
113
|
|
|
127
114
|
def self.parse_document(data)
|
|
128
|
-
raise ArgumentError,
|
|
129
|
-
|
|
115
|
+
raise ArgumentError, "Input cannot be nil" if data.nil?
|
|
116
|
+
unless data.is_a?(Hash)
|
|
117
|
+
raise ArgumentError,
|
|
118
|
+
"Input must be a hash, got #{data.class}"
|
|
119
|
+
end
|
|
130
120
|
|
|
131
121
|
document = parse_node(data)
|
|
132
122
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosereflect
|
|
4
|
+
# ResolvedPos represents a document position that has been resolved
|
|
5
|
+
# to a specific location in the document tree.
|
|
6
|
+
#
|
|
7
|
+
# The path array contains: [parent_node, index, start, parent_node, index, start, ...]
|
|
8
|
+
# depth 0 = before any nodes, depth N = inside node at path[N*2]
|
|
9
|
+
class ResolvedPos
|
|
10
|
+
attr_reader :pos, :path, :depth
|
|
11
|
+
|
|
12
|
+
def initialize(pos, path, depth)
|
|
13
|
+
@pos = pos
|
|
14
|
+
@path = path
|
|
15
|
+
@depth = depth
|
|
16
|
+
@parent_offset = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The parent node at current depth
|
|
20
|
+
def parent
|
|
21
|
+
@path[@depth * 3]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Index within parent
|
|
25
|
+
def index(depth = @depth)
|
|
26
|
+
@path[(depth * 3) + 1]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Start position of current parent node
|
|
30
|
+
def start(depth = @depth)
|
|
31
|
+
@path[(depth * 3) + 2]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# End position of current parent node
|
|
35
|
+
def end_(depth = @depth)
|
|
36
|
+
start(depth) + parent.content.size
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The node at a given depth
|
|
40
|
+
def node(depth = @depth)
|
|
41
|
+
@path[depth * 3]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Position within the parent node
|
|
45
|
+
def parent_offset
|
|
46
|
+
@parent_offset ||= @pos - start
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Marks at this position
|
|
50
|
+
def marks
|
|
51
|
+
if depth.zero?
|
|
52
|
+
# At root - no marks
|
|
53
|
+
[]
|
|
54
|
+
else
|
|
55
|
+
parent_mark = parent.respond_to?(:marks) ? parent.marks : []
|
|
56
|
+
parent_mark || []
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Marks between two positions
|
|
61
|
+
def marks_between(from, to, marks)
|
|
62
|
+
result = marks.dup
|
|
63
|
+
nodes_between(from, to) do |node|
|
|
64
|
+
if node.respond_to?(:marks) && node.marks
|
|
65
|
+
result = result | node.marks
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Find shared depth with another position
|
|
72
|
+
def shared_depth(other_pos)
|
|
73
|
+
my_depth = depth
|
|
74
|
+
other_depth = other_pos.depth
|
|
75
|
+
|
|
76
|
+
while my_depth > other_depth
|
|
77
|
+
my_depth -= 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
while other_depth > my_depth
|
|
81
|
+
other_depth -= 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
while my_depth.positive?
|
|
85
|
+
break unless index(my_depth) == other_pos.index(my_depth)
|
|
86
|
+
|
|
87
|
+
my_depth -= 1
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
my_depth
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Get block range to another position
|
|
95
|
+
def block_range(other_pos = nil)
|
|
96
|
+
other_pos ||= self
|
|
97
|
+
NodeRange.new(self, other_pos)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if at block boundary
|
|
101
|
+
def block?
|
|
102
|
+
parent.respond_to?(:is_block?) && parent.is_block?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check if at inline boundary
|
|
106
|
+
def inline?
|
|
107
|
+
!block?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Check if in text block
|
|
111
|
+
def text_block?
|
|
112
|
+
parent.respond_to?(:is_textblock?) && parent.is_textblock?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if at start of parent
|
|
116
|
+
def start_of_parent?
|
|
117
|
+
parent_offset.zero?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Check if at end of parent
|
|
121
|
+
def end_of_parent?
|
|
122
|
+
parent_offset >= parent.content.size - 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Get position before current node
|
|
126
|
+
def before?
|
|
127
|
+
if depth.zero?
|
|
128
|
+
@pos.zero?
|
|
129
|
+
else
|
|
130
|
+
index.zero?
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get position after current node
|
|
135
|
+
def after?
|
|
136
|
+
if depth.zero?
|
|
137
|
+
@pos >= 0
|
|
138
|
+
else
|
|
139
|
+
index >= parent.content.size
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def eq?(other)
|
|
144
|
+
return false unless other.is_a?(ResolvedPos)
|
|
145
|
+
|
|
146
|
+
@pos == other.pos && @depth == other.depth
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
alias == eq?
|
|
150
|
+
|
|
151
|
+
def hash
|
|
152
|
+
[@pos, @depth].hash
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def to_s
|
|
156
|
+
"<ResolvedPos #{@pos}:#{depth}>"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def inspect
|
|
160
|
+
to_s
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def nodes_between(from, to, &block)
|
|
166
|
+
return unless to > from
|
|
167
|
+
|
|
168
|
+
depth.times do |d|
|
|
169
|
+
node = node(d)
|
|
170
|
+
node.nodes_between(from, to, &block) if node.respond_to?(:nodes_between)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# NodeRange represents a range between two resolved positions
|
|
176
|
+
class NodeRange
|
|
177
|
+
attr_reader :start, :end_
|
|
178
|
+
|
|
179
|
+
alias end end_
|
|
180
|
+
|
|
181
|
+
def initialize(start_resolved, end_resolved)
|
|
182
|
+
@start = start_resolved
|
|
183
|
+
@end_ = end_resolved
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Content fragment between start and end
|
|
187
|
+
def content
|
|
188
|
+
# Would extract the fragment
|
|
189
|
+
Fragment.new([])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Nodes within this range
|
|
193
|
+
def nodes
|
|
194
|
+
result = []
|
|
195
|
+
start.node.nodes_between(start.pos, end_.pos) { |n| result << n }
|
|
196
|
+
result
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def to_s
|
|
200
|
+
"<NodeRange #{start.pos}:#{end_.pos}>"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def inspect
|
|
204
|
+
to_s
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Extension to Node for position resolution
|
|
209
|
+
class Node
|
|
210
|
+
# Resolve a position to a ResolvedPos
|
|
211
|
+
def resolve(pos)
|
|
212
|
+
path = []
|
|
213
|
+
build_path_for_pos(pos, path)
|
|
214
|
+
depth = [(path.length / 3) - 1, 0].max
|
|
215
|
+
ResolvedPos.new(pos, path, depth)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def find_block_depth(common_depth)
|
|
221
|
+
block_depth = common_depth
|
|
222
|
+
while block_depth.positive?
|
|
223
|
+
current_node = node(block_depth)
|
|
224
|
+
break if current_node.respond_to?(:is_block?) && current_node.is_block?
|
|
225
|
+
|
|
226
|
+
block_depth -= 1
|
|
227
|
+
end
|
|
228
|
+
block_depth
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def build_path_for_pos(pos, path, index = 0, start_offset = 0)
|
|
232
|
+
path << self << index << start_offset
|
|
233
|
+
return if pos.zero?
|
|
234
|
+
|
|
235
|
+
traverse_children_for_resolve(pos, path)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def traverse_children_for_resolve(pos, path)
|
|
239
|
+
return unless content
|
|
240
|
+
|
|
241
|
+
content_offset = 1
|
|
242
|
+
child_index = 0
|
|
243
|
+
|
|
244
|
+
content.each do |child|
|
|
245
|
+
child_end = content_offset + child.node_size
|
|
246
|
+
if pos < child_end
|
|
247
|
+
child.send(:build_path_for_pos, pos - content_offset, path, child_index, content_offset)
|
|
248
|
+
return
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
content_offset = child_end
|
|
252
|
+
child_index += 1
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosereflect
|
|
4
|
+
class Schema
|
|
5
|
+
class Attribute
|
|
6
|
+
attr_reader :name, :default
|
|
7
|
+
|
|
8
|
+
def initialize(name:, default: nil, validate: nil)
|
|
9
|
+
@name = name
|
|
10
|
+
@default = default
|
|
11
|
+
@validate = validate
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def has_default?
|
|
15
|
+
!@default.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def required?
|
|
19
|
+
!has_default?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validate_value(value)
|
|
23
|
+
return true if @validate.nil?
|
|
24
|
+
return @validate.call(value) if @validate.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
# Handle string-based type validation like "string", "number", "string|null"
|
|
27
|
+
validate_type(value, @validate.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_type(value, type_str)
|
|
33
|
+
types = type_str.split("|")
|
|
34
|
+
actual_type = get_type_name(value)
|
|
35
|
+
|
|
36
|
+
unless types.include?(actual_type)
|
|
37
|
+
raise ::Prosereflect::SchemaErrors::ValidationError,
|
|
38
|
+
"Expected value of type #{types} for attribute #{@name}, got #{actual_type}"
|
|
39
|
+
end
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_type_name(value)
|
|
44
|
+
case value
|
|
45
|
+
when nil then "null"
|
|
46
|
+
when String then "string"
|
|
47
|
+
when Integer, Float then "number"
|
|
48
|
+
when TrueClass, FalseClass then "boolean"
|
|
49
|
+
when Hash then "object"
|
|
50
|
+
when Array then "object"
|
|
51
|
+
else
|
|
52
|
+
value.class.name.downcase
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|