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,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ class Schema
5
+ class NodeType
6
+ attr_reader :name, :attrs, :content_match, :groups, :schema, :spec,
7
+ :content_expression
8
+ attr_accessor :mark_set
9
+
10
+ def initialize(name:, attrs: {}, content_expression: nil, groups: [],
11
+ schema: nil, spec: nil, inline: false, atom: false)
12
+ @name = name
13
+ @attrs = attrs
14
+ @groups = groups
15
+ @schema = schema
16
+ @spec = spec
17
+ @inline = inline
18
+ @atom = atom
19
+ @mark_set = nil
20
+ @content_expression = content_expression
21
+
22
+ # Content match will be built later by Schema#build_content_matches
23
+ # to avoid circular dependency issues during node type construction
24
+ @content_match = ContentMatch.empty
25
+ end
26
+
27
+ def self.from_spec(name, schema, spec)
28
+ attrs = {}
29
+ if spec.respond_to?(:attrs)
30
+ spec_attrs = spec.attrs
31
+ attrs = if spec_attrs.is_a?(Hash)
32
+ # New format: {"attr_name" => {default: value}} or attr_name as key
33
+ spec_attrs.each_with_object({}) do |(attr_name, attr_spec), hash|
34
+ if attr_spec.respond_to?(:name)
35
+ hash[attr_name] =
36
+ Attribute.new(name: attr_spec.name,
37
+ default: attr_spec.default)
38
+ else
39
+ # attr_spec is a Hash with :default, :name, etc as keys
40
+ attr_name = attr_spec[:name] || attr_spec["name"] || attr_name
41
+ default = attr_spec[:default] || attr_spec["default"]
42
+ hash[attr_name] =
43
+ Attribute.new(name: attr_name, default: default)
44
+ end
45
+ end
46
+ else
47
+ # Old format: values are attribute objects with .name and .default
48
+ spec_attrs.transform_values do |a|
49
+ Attribute.new(name: a.name, default: a.default)
50
+ end
51
+ end
52
+ elsif spec.is_a?(Hash)
53
+ spec_attrs = spec[:attrs] || spec["attrs"] || {}
54
+ attrs = spec_attrs.transform_values do |v|
55
+ Attribute.new(
56
+ name: v[:name] || v["name"] || v.keys.first,
57
+ default: v[:default] || v["default"],
58
+ )
59
+ end
60
+ end
61
+
62
+ content_expr = spec.respond_to?(:content) ? spec.content : (spec[:content] || spec["content"])
63
+ groups = spec.respond_to?(:groups) ? spec.groups : parse_groups(spec)
64
+ inline = spec.respond_to?(:inline) ? spec.inline : (spec[:inline] || spec["inline"] || false)
65
+ atom = spec.respond_to?(:atom) ? spec.atom : (spec[:atom] || spec["atom"] || false)
66
+
67
+ new(
68
+ name: name,
69
+ attrs: attrs,
70
+ content_expression: content_expr,
71
+ groups: groups,
72
+ schema: schema,
73
+ spec: spec,
74
+ inline: inline,
75
+ atom: atom,
76
+ )
77
+ end
78
+
79
+ def self.parse_groups(spec)
80
+ group_str = spec[:group] || spec["group"]
81
+ return [] unless group_str
82
+
83
+ group_str.is_a?(Array) ? group_str : group_str.to_s.split
84
+ end
85
+
86
+ def is_block?
87
+ !@inline && @name != "text"
88
+ end
89
+
90
+ def is_inline?
91
+ !is_block?
92
+ end
93
+
94
+ alias inline? is_inline?
95
+
96
+ def text?
97
+ @name == "text"
98
+ end
99
+
100
+ def is_leaf?
101
+ @content_match == ContentMatch.empty || @content_match.edge_count.zero?
102
+ end
103
+
104
+ def is_atom?
105
+ is_leaf? || @atom
106
+ end
107
+
108
+ def is_textblock?
109
+ is_block? && @content_match.inline_content?
110
+ end
111
+
112
+ def in_group?(group_name)
113
+ return true if group_name == "inline" && text?
114
+ return true if group_name == "block" && is_block?
115
+
116
+ @groups.include?(group_name)
117
+ end
118
+
119
+ def has_required_attrs?
120
+ @attrs.values.any?(&:required?)
121
+ end
122
+
123
+ def default_attrs
124
+ defaults = {}
125
+ @attrs.each do |name, attr_def|
126
+ return nil unless attr_def.has_default?
127
+
128
+ defaults[name] = attr_def.default
129
+ end
130
+ defaults.empty? ? nil : defaults
131
+ end
132
+
133
+ def compute_attrs(attrs)
134
+ return default_attrs if attrs.nil?
135
+
136
+ built = {}
137
+ @attrs.each do |name, attr_def|
138
+ if attrs.key?(name)
139
+ built[name] = attrs[name]
140
+ elsif attr_def.has_default?
141
+ built[name] = attr_def.default
142
+ else
143
+ raise Prosereflect::SchemaErrors::ValidationError,
144
+ "No value supplied for attribute #{name} on node #{@name}"
145
+ end
146
+ end
147
+ built
148
+ end
149
+
150
+ # Create a node with validation
151
+ def create(attrs = nil, content = nil, marks = [])
152
+ if text?
153
+ raise Prosereflect::SchemaErrors::Error,
154
+ "NodeType.create cannot construct text nodes"
155
+ end
156
+
157
+ content_fragment = case content
158
+ when Fragment then content
159
+ when nil then Fragment.empty
160
+ when Array then Fragment.new(content)
161
+ when Node then Fragment.new([content])
162
+ end
163
+
164
+ attrs = compute_attrs(attrs)
165
+ Node.new(type: self, attrs: attrs, content: content_fragment,
166
+ marks: marks)
167
+ end
168
+
169
+ # Create a node with content validation
170
+ def create_checked(attrs = nil, content = nil, marks = [])
171
+ content_fragment = case content
172
+ when Fragment then content
173
+ when nil then Fragment.empty
174
+ when Array then Fragment.new(content)
175
+ when Node then Fragment.new([content])
176
+ end
177
+
178
+ check_content(content_fragment)
179
+ attrs = compute_attrs(attrs)
180
+ Node.new(type: self, attrs: attrs, content: content_fragment,
181
+ marks: marks)
182
+ end
183
+
184
+ # Create and fill a node with default content
185
+ def create_and_fill(attrs = nil, content = nil, marks = [])
186
+ attrs = compute_attrs(attrs)
187
+ content_fragment = Fragment.from(content)
188
+
189
+ if content_fragment.size.positive?
190
+ before = @content_match.fill_before(after: content_fragment,
191
+ to_end: false)
192
+ return nil unless before
193
+
194
+ content_fragment = before.append(content_fragment)
195
+ end
196
+
197
+ matched = @content_match.match_fragment(content_fragment)
198
+ return nil unless matched
199
+
200
+ after = matched.fill_before(after: Fragment.empty, to_end: true)
201
+ return nil unless after
202
+
203
+ full_content = content_fragment.append(after)
204
+ Node.new(type: self, attrs: attrs, content: full_content, marks: marks)
205
+ end
206
+
207
+ def valid_content?(fragment)
208
+ result = @content_match.match_fragment(fragment)
209
+ return false unless result&.valid_end
210
+
211
+ fragment.content.each do |child|
212
+ return false unless allows_marks?(child.marks)
213
+ end
214
+ true
215
+ end
216
+
217
+ def check_content(fragment)
218
+ return if valid_content?(fragment)
219
+
220
+ raise Prosereflect::SchemaErrors::ValidationError,
221
+ "Invalid content for node #{@name}: #{fragment.to_s[0, 50]}"
222
+ end
223
+
224
+ def check_attrs(attrs)
225
+ attrs ||= {}
226
+ attrs.each_key do |attr_name|
227
+ unless @attrs.key?(attr_name)
228
+ raise Prosereflect::SchemaErrors::ValidationError,
229
+ "Unsupported attribute #{attr_name} for node #{@name}"
230
+ end
231
+ end
232
+
233
+ @attrs.each do |name, attr_def|
234
+ attr_def.validate_value(attrs[name]) if attrs.key?(name)
235
+ end
236
+ end
237
+
238
+ def allows_mark_type?(mark_type)
239
+ @mark_set.nil? || @mark_set.include?(mark_type)
240
+ end
241
+
242
+ def allows_marks?(marks)
243
+ return true if @mark_set.nil?
244
+
245
+ marks.all? { |mark| allows_mark_type?(mark.type) }
246
+ end
247
+
248
+ def allowed_marks(marks)
249
+ return marks if @mark_set.nil?
250
+
251
+ result = marks.dup
252
+ filtered = false
253
+
254
+ marks.each_with_index do |mark, i|
255
+ unless allows_mark_type?(mark.type)
256
+ result = marks[0...i].dup
257
+ filtered = true
258
+ break
259
+ end
260
+ end
261
+
262
+ filtered ? result : marks
263
+ end
264
+
265
+ def compatible_content?(other)
266
+ self == other || @content_match.compatible?(other.content_match)
267
+ end
268
+
269
+ def to_s
270
+ "<NodeType #{@name}>"
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Schema implementation - reopen Prosereflect::Schema class to add instance methods
4
+ module Prosereflect
5
+ class Schema
6
+ # Alias ValidationError for backwards compatibility
7
+ ValidationError = Prosereflect::SchemaErrors::ValidationError
8
+
9
+ attr_reader :spec, :nodes, :marks
10
+
11
+ def initialize(nodes_spec:, marks_spec: {}, top_node: nil)
12
+ @spec = SchemaSpec.from_hashes(nodes_spec: nodes_spec,
13
+ marks_spec: marks_spec, top_node: top_node)
14
+ @nodes = {} # name -> NodeType
15
+ @marks = {} # name -> MarkType
16
+
17
+ # Build NodeTypes
18
+ @spec.nodes.each do |name, node_spec|
19
+ @nodes[name] = NodeType.from_spec(name, self, node_spec)
20
+ end
21
+
22
+ # Build MarkTypes
23
+ rank = 0
24
+ @spec.marks.each do |name, mark_spec|
25
+ @marks[name] = MarkType.from_spec(name, rank, self, mark_spec)
26
+ rank += 1
27
+ end
28
+
29
+ # Validate schema
30
+ validate_schema
31
+
32
+ # Build content expressions
33
+ build_content_matches
34
+
35
+ # Build mark sets
36
+ build_mark_sets
37
+
38
+ # Build mark exclusions
39
+ build_mark_exclusions
40
+
41
+ @top_node_type = @nodes[@spec.top_node]
42
+ end
43
+
44
+ def top_node_type
45
+ @top_node_type
46
+ end
47
+
48
+ def node_type(name)
49
+ @nodes[name] || raise(::Prosereflect::SchemaErrors::ValidationError,
50
+ "Unknown node type: #{name}")
51
+ end
52
+
53
+ def mark_type(name)
54
+ @marks[name] || raise(::Prosereflect::SchemaErrors::ValidationError,
55
+ "Unknown mark type: #{name}")
56
+ end
57
+
58
+ # Create a node
59
+ def node(type, attrs = nil, content = nil, marks = nil)
60
+ type_obj = type.is_a?(String) ? node_type(type) : type
61
+ unless type_obj.is_a?(NodeType)
62
+ raise ::Prosereflect::SchemaErrors::Error,
63
+ "Invalid node type: #{type}"
64
+ end
65
+ if type_obj.schema != self
66
+ raise ::Prosereflect::SchemaErrors::Error,
67
+ "Node type from different schema used (#{type_obj.name})"
68
+ end
69
+
70
+ type_obj.create_checked(attrs, content, marks)
71
+ end
72
+
73
+ # Create a text node
74
+ def text(text, marks = nil)
75
+ type = node_type("text")
76
+ TextNode.new(type: type, attrs: {}, text: text, marks: marks || [])
77
+ end
78
+
79
+ # Create a mark
80
+ def mark(type, attrs = nil)
81
+ type_obj = type.is_a?(String) ? mark_type(type) : type
82
+ type_obj.create(attrs)
83
+ end
84
+
85
+ # Deserialize node from JSON
86
+ def node_from_json(json_data)
87
+ Node.from_json(self, json_data)
88
+ end
89
+
90
+ # Deserialize mark from JSON
91
+ def mark_from_json(json_data)
92
+ type = mark_type(json_data["type"])
93
+ attrs = json_data["attrs"] || {}
94
+ type.create(attrs)
95
+ end
96
+
97
+ private
98
+
99
+ def validate_schema
100
+ unless @nodes.key?(@spec.top_node)
101
+ raise ::Prosereflect::SchemaErrors::ValidationError,
102
+ "Schema is missing its top node type #{@spec.top_node}"
103
+ end
104
+
105
+ unless @nodes.key?("text")
106
+ raise ::Prosereflect::SchemaErrors::ValidationError,
107
+ "every schema needs a 'text' type"
108
+ end
109
+
110
+ if @nodes["text"].attrs && !@nodes["text"].attrs.empty?
111
+ raise ::Prosereflect::SchemaErrors::ValidationError,
112
+ "the text node type should not have attributes"
113
+ end
114
+
115
+ @nodes.each_key do |name|
116
+ if @marks.key?(name)
117
+ raise ::Prosereflect::SchemaErrors::ValidationError,
118
+ "#{name} can not be both a node and a mark"
119
+ end
120
+ end
121
+ end
122
+
123
+ def build_content_matches
124
+ @nodes.each_value do |node_type|
125
+ content_expr = node_type.content_expression
126
+ next unless content_expr
127
+
128
+ node_type.instance_variable_set(
129
+ :@content_match,
130
+ ContentMatch.parse(content_expr, @nodes),
131
+ )
132
+ end
133
+ end
134
+
135
+ def build_mark_sets
136
+ @nodes.each_value do |node_type|
137
+ mark_expr = if node_type.spec.respond_to?(:[])
138
+ node_type.spec[:marks] || node_type.spec["marks"]
139
+ elsif node_type.spec.respond_to?(:marks)
140
+ node_type.spec.marks
141
+ end
142
+
143
+ node_type.mark_set = if mark_expr == "_"
144
+ nil # All marks allowed
145
+ elsif mark_expr.is_a?(String) && !mark_expr.empty?
146
+ gather_marks(mark_expr.split)
147
+ elsif mark_expr == "" || !node_type.content_match.inline_content?
148
+ []
149
+ end
150
+ end
151
+ end
152
+
153
+ def gather_marks(names)
154
+ result = []
155
+ names.each do |name|
156
+ if @marks.key?(name)
157
+ result << @marks[name]
158
+ else
159
+ # Check groups
160
+ @marks.each_value do |mark|
161
+ group_str = mark.spec.respond_to?(:group) ? mark.spec.group : nil
162
+
163
+ if group_str && group_str.to_s.split.include?(name)
164
+ result << mark
165
+ end
166
+ end
167
+ end
168
+ end
169
+ result
170
+ end
171
+
172
+ def build_mark_exclusions
173
+ @marks.each_value do |mark|
174
+ excl = if mark.spec.respond_to?(:[])
175
+ mark.spec[:excludes] || mark.spec["excludes"]
176
+ elsif mark.spec.respond_to?(:excludes)
177
+ mark.spec.excludes
178
+ end
179
+
180
+ mark.excluded = if excl.nil?
181
+ [mark]
182
+ elsif excl == ""
183
+ []
184
+ else
185
+ gather_marks(excl.split)
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ class Schema
5
+ # Represents a node specification from a schema
6
+ class NodeSpec
7
+ attr_reader :name, :attrs, :content, :groups, :inline, :atom, :marks
8
+
9
+ def initialize(name:, attrs: {}, content: nil, groups: [], inline: false,
10
+ atom: false, marks: nil)
11
+ @name = name
12
+ @attrs = attrs
13
+ @content = content
14
+ @groups = groups
15
+ @inline = inline
16
+ @atom = atom
17
+ @marks = marks
18
+ end
19
+
20
+ def self.from_hash(name, spec)
21
+ new(
22
+ name: name,
23
+ attrs: spec[:attrs] || spec["attrs"] || {},
24
+ content: spec[:content] || spec["content"],
25
+ groups: parse_groups(spec),
26
+ inline: spec[:inline] || spec["inline"] || false,
27
+ atom: spec[:atom] || spec["atom"] || false,
28
+ marks: spec[:marks] || spec["marks"],
29
+ )
30
+ end
31
+
32
+ def self.parse_groups(spec)
33
+ group_str = spec[:group] || spec["group"]
34
+ return [] unless group_str
35
+
36
+ group_str.is_a?(Array) ? group_str : group_str.to_s.split
37
+ end
38
+ end
39
+
40
+ # Represents a mark specification from a schema
41
+ class MarkSpec
42
+ attr_reader :name, :attrs, :excludes, :inclusive, :group
43
+
44
+ def initialize(name:, attrs: {}, excludes: nil, inclusive: true,
45
+ group: nil)
46
+ @name = name
47
+ @attrs = attrs
48
+ @excludes = excludes
49
+ @inclusive = inclusive
50
+ @group = group
51
+ end
52
+
53
+ def self.from_hash(name, spec)
54
+ new(
55
+ name: name,
56
+ attrs: spec[:attrs] || spec["attrs"] || {},
57
+ excludes: spec[:excludes] || spec["excludes"],
58
+ inclusive: if spec.key?(:inclusive)
59
+ spec[:inclusive]
60
+ elsif spec.key?("inclusive")
61
+ spec["inclusive"]
62
+ else
63
+ true
64
+ end,
65
+ group: spec[:group] || spec["group"],
66
+ )
67
+ end
68
+ end
69
+
70
+ # Represents a complete schema specification
71
+ class SchemaSpec
72
+ attr_reader :nodes, :marks, :top_node
73
+
74
+ def initialize(nodes: {}, marks: {}, top_node: nil)
75
+ @nodes = nodes
76
+ @marks = marks
77
+ @top_node = top_node || "doc"
78
+ end
79
+
80
+ def self.from_hashes(nodes_spec:, marks_spec: {}, top_node: nil)
81
+ nodes = nodes_spec.each_with_object({}) do |(name, spec), hash|
82
+ hash[name] = NodeSpec.from_hash(name, spec)
83
+ end
84
+ marks = marks_spec.each_with_object({}) do |(name, spec), hash|
85
+ hash[name] = MarkSpec.from_hash(name, spec)
86
+ end
87
+
88
+ new(nodes: nodes, marks: marks, top_node: top_node)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosereflect
4
+ module SchemaErrors
5
+ class Error < StandardError; end
6
+ class AttributeParseError < Error; end
7
+ class ContentMatchError < Error; end
8
+ class ValidationError < Error; end
9
+ end
10
+ end
11
+
12
+ # Define Schema as a proper class (not module) that will be extended by schema_main.rb
13
+ # The nested classes (NodeType, MarkType, etc.) are defined in their own files
14
+ # and accessed as Prosereflect::Schema::NodeType etc.
15
+ module Prosereflect
16
+ class Schema
17
+ class << self
18
+ attr_accessor :node_types, :mark_types
19
+ end
20
+ self.node_types = []
21
+ self.mark_types = []
22
+
23
+ # Alias ContentMatchError for backwards compatibility
24
+ ContentMatchError = Prosereflect::SchemaErrors::ContentMatchError
25
+
26
+ # Alias Error for backwards compatibility
27
+ Error = Prosereflect::SchemaErrors::Error
28
+ end
29
+ end
30
+
31
+ require_relative "schema/attribute"
32
+ require_relative "schema/spec"
33
+ require_relative "schema/fragment"
34
+ require_relative "schema/mark"
35
+ require_relative "schema/node"
36
+ require_relative "schema/content_match"
37
+ require_relative "schema/mark_type"
38
+ require_relative "schema/node_type"
39
+ require_relative "schema/schema_main"
@@ -23,6 +23,30 @@ module Prosereflect
23
23
  text || ""
24
24
  end
25
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
48
+ end
49
+
26
50
  # Override the to_h method to include the text attribute
27
51
  def to_h
28
52
  result = super