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.
Files changed (158) 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/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.gitignore +4 -0
  7. data/.rubocop.yml +19 -1
  8. data/.rubocop_todo.yml +119 -183
  9. data/CLAUDE.md +78 -0
  10. data/Gemfile +8 -4
  11. data/README.adoc +2 -0
  12. data/Rakefile +3 -3
  13. data/docs/Gemfile +10 -0
  14. data/docs/INDEX.adoc +45 -0
  15. data/docs/_advanced/index.adoc +15 -0
  16. data/docs/_advanced/schema.adoc +112 -0
  17. data/docs/_advanced/step-map.adoc +66 -0
  18. data/docs/_advanced/steps.adoc +88 -0
  19. data/docs/_advanced/test-builder.adoc +61 -0
  20. data/docs/_advanced/transform.adoc +92 -0
  21. data/docs/_config.yml +174 -0
  22. data/docs/_features/html-input.adoc +69 -0
  23. data/docs/_features/html-output.adoc +45 -0
  24. data/docs/_features/index.adoc +15 -0
  25. data/docs/_features/marks.adoc +86 -0
  26. data/docs/_features/node-types.adoc +124 -0
  27. data/docs/_features/user-mentions.adoc +47 -0
  28. data/docs/_guides/custom-nodes.adoc +107 -0
  29. data/docs/_guides/index.adoc +13 -0
  30. data/docs/_guides/round-trip-html.adoc +91 -0
  31. data/docs/_guides/serialization.adoc +109 -0
  32. data/docs/_pages/index.adoc +67 -0
  33. data/docs/_reference/document-api.adoc +49 -0
  34. data/docs/_reference/index.adoc +14 -0
  35. data/docs/_reference/node-api.adoc +79 -0
  36. data/docs/_reference/schema-api.adoc +95 -0
  37. data/docs/_reference/transform-api.adoc +77 -0
  38. data/docs/_understanding/document-model.adoc +65 -0
  39. data/docs/_understanding/fragment.adoc +52 -0
  40. data/docs/_understanding/index.adoc +14 -0
  41. data/docs/_understanding/resolved-position.adoc +53 -0
  42. data/docs/_understanding/slice.adoc +54 -0
  43. data/docs/lychee.toml +63 -0
  44. data/lib/prosereflect/attribute/base.rb +4 -6
  45. data/lib/prosereflect/attribute/bold.rb +2 -4
  46. data/lib/prosereflect/attribute/href.rb +1 -3
  47. data/lib/prosereflect/attribute/id.rb +7 -7
  48. data/lib/prosereflect/attribute.rb +4 -7
  49. data/lib/prosereflect/blockquote.rb +19 -11
  50. data/lib/prosereflect/bullet_list.rb +36 -29
  51. data/lib/prosereflect/code_block.rb +23 -27
  52. data/lib/prosereflect/code_block_wrapper.rb +12 -13
  53. data/lib/prosereflect/document.rb +14 -22
  54. data/lib/prosereflect/fragment.rb +249 -0
  55. data/lib/prosereflect/hard_break.rb +6 -6
  56. data/lib/prosereflect/heading.rb +14 -15
  57. data/lib/prosereflect/horizontal_rule.rb +23 -14
  58. data/lib/prosereflect/image.rb +32 -23
  59. data/lib/prosereflect/input/html.rb +179 -104
  60. data/lib/prosereflect/input.rb +7 -0
  61. data/lib/prosereflect/list_item.rb +11 -12
  62. data/lib/prosereflect/mark/base.rb +9 -11
  63. data/lib/prosereflect/mark/bold.rb +1 -3
  64. data/lib/prosereflect/mark/code.rb +1 -3
  65. data/lib/prosereflect/mark/italic.rb +1 -3
  66. data/lib/prosereflect/mark/link.rb +1 -3
  67. data/lib/prosereflect/mark/strike.rb +1 -3
  68. data/lib/prosereflect/mark/subscript.rb +1 -3
  69. data/lib/prosereflect/mark/superscript.rb +1 -3
  70. data/lib/prosereflect/mark/underline.rb +1 -3
  71. data/lib/prosereflect/mark.rb +9 -5
  72. data/lib/prosereflect/node.rb +171 -33
  73. data/lib/prosereflect/ordered_list.rb +17 -14
  74. data/lib/prosereflect/output/html.rb +279 -50
  75. data/lib/prosereflect/output.rb +7 -0
  76. data/lib/prosereflect/paragraph.rb +11 -13
  77. data/lib/prosereflect/parser.rb +56 -66
  78. data/lib/prosereflect/resolved_pos.rb +256 -0
  79. data/lib/prosereflect/schema/attribute.rb +57 -0
  80. data/lib/prosereflect/schema/content_match.rb +656 -0
  81. data/lib/prosereflect/schema/fragment.rb +166 -0
  82. data/lib/prosereflect/schema/mark.rb +121 -0
  83. data/lib/prosereflect/schema/mark_type.rb +130 -0
  84. data/lib/prosereflect/schema/node.rb +236 -0
  85. data/lib/prosereflect/schema/node_type.rb +274 -0
  86. data/lib/prosereflect/schema/schema_main.rb +190 -0
  87. data/lib/prosereflect/schema/spec.rb +92 -0
  88. data/lib/prosereflect/schema.rb +39 -0
  89. data/lib/prosereflect/table.rb +12 -13
  90. data/lib/prosereflect/table_cell.rb +13 -13
  91. data/lib/prosereflect/table_header.rb +17 -17
  92. data/lib/prosereflect/table_row.rb +12 -12
  93. data/lib/prosereflect/text.rb +35 -11
  94. data/lib/prosereflect/transform/attr_step.rb +157 -0
  95. data/lib/prosereflect/transform/insert_step.rb +115 -0
  96. data/lib/prosereflect/transform/mapping.rb +82 -0
  97. data/lib/prosereflect/transform/mark_step.rb +269 -0
  98. data/lib/prosereflect/transform/replace_around_step.rb +181 -0
  99. data/lib/prosereflect/transform/replace_step.rb +157 -0
  100. data/lib/prosereflect/transform/slice.rb +91 -0
  101. data/lib/prosereflect/transform/step.rb +89 -0
  102. data/lib/prosereflect/transform/step_map.rb +126 -0
  103. data/lib/prosereflect/transform/structure.rb +120 -0
  104. data/lib/prosereflect/transform/transform.rb +341 -0
  105. data/lib/prosereflect/transform.rb +26 -0
  106. data/lib/prosereflect/user.rb +15 -15
  107. data/lib/prosereflect/version.rb +1 -1
  108. data/lib/prosereflect.rb +30 -17
  109. data/prosereflect.gemspec +17 -16
  110. data/spec/fixtures/documents/formatted_text.yaml +14 -0
  111. data/spec/fixtures/documents/heading_paragraph.yaml +16 -0
  112. data/spec/fixtures/documents/lists_doc.yaml +32 -0
  113. data/spec/fixtures/documents/mixed_content.yaml +40 -0
  114. data/spec/fixtures/documents/nested_doc.yaml +20 -0
  115. data/spec/fixtures/documents/simple_doc.yaml +6 -0
  116. data/spec/fixtures/documents/table_doc.yaml +32 -0
  117. data/spec/fixtures/documents/transform_test.yaml +14 -0
  118. data/spec/fixtures/schema/custom_schema.rb +37 -0
  119. data/spec/fixtures/schema/test_schema.rb +46 -0
  120. data/spec/fixtures/test_builder/helpers.rb +212 -0
  121. data/spec/prosereflect/document_spec.rb +332 -330
  122. data/spec/prosereflect/fragment_spec.rb +273 -0
  123. data/spec/prosereflect/hard_break_spec.rb +125 -125
  124. data/spec/prosereflect/input/html_spec.rb +718 -522
  125. data/spec/prosereflect/node_spec.rb +311 -182
  126. data/spec/prosereflect/output/html_spec.rb +105 -105
  127. data/spec/prosereflect/output/whitespace_spec.rb +248 -0
  128. data/spec/prosereflect/paragraph_spec.rb +275 -274
  129. data/spec/prosereflect/parser/round_trip_spec.rb +472 -0
  130. data/spec/prosereflect/parser_spec.rb +185 -180
  131. data/spec/prosereflect/resolved_pos_spec.rb +74 -0
  132. data/spec/prosereflect/schema/conftest.rb +68 -0
  133. data/spec/prosereflect/schema/content_match_spec.rb +237 -0
  134. data/spec/prosereflect/schema/mark_spec.rb +274 -0
  135. data/spec/prosereflect/schema/mark_type_spec.rb +86 -0
  136. data/spec/prosereflect/schema/node_type_spec.rb +142 -0
  137. data/spec/prosereflect/schema/schema_spec.rb +194 -0
  138. data/spec/prosereflect/table_cell_spec.rb +183 -183
  139. data/spec/prosereflect/table_row_spec.rb +149 -149
  140. data/spec/prosereflect/table_spec.rb +320 -318
  141. data/spec/prosereflect/test_builder/marks_spec.rb +127 -0
  142. data/spec/prosereflect/text_spec.rb +133 -132
  143. data/spec/prosereflect/transform/equivalence_spec.rb +487 -0
  144. data/spec/prosereflect/transform/mapping_spec.rb +226 -0
  145. data/spec/prosereflect/transform/replace_spec.rb +832 -0
  146. data/spec/prosereflect/transform/replace_step_spec.rb +157 -0
  147. data/spec/prosereflect/transform/slice_spec.rb +48 -0
  148. data/spec/prosereflect/transform/step_map_spec.rb +70 -0
  149. data/spec/prosereflect/transform/step_spec.rb +211 -0
  150. data/spec/prosereflect/transform/structure_spec.rb +98 -0
  151. data/spec/prosereflect/transform/transform_spec.rb +238 -0
  152. data/spec/prosereflect/user_spec.rb +31 -28
  153. data/spec/prosereflect_spec.rb +28 -26
  154. data/spec/spec_helper.rb +7 -6
  155. data/spec/support/matchers.rb +6 -6
  156. data/spec/support/shared_examples.rb +49 -49
  157. metadata +96 -5
  158. data/spec/prosereflect/version_spec.rb +0 -11
@@ -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"
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'table_row'
5
- require_relative 'table_header'
6
-
7
3
  module Prosereflect
8
4
  # TODO: support for table attributes
9
5
  # Table class represents a ProseMirror table.
10
6
  # It contains rows, each of which can contain cells.
11
7
  class Table < Node
12
- PM_TYPE = 'table'
8
+ PM_TYPE = "table"
13
9
 
14
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
10
+ attribute :type, :string, default: -> {
11
+ self.class.send(:const_get, "PM_TYPE")
12
+ }
15
13
 
16
14
  key_value do
17
- map 'type', to: :type, render_default: true
18
- map 'content', to: :content
19
- map 'attrs', to: :attrs
15
+ map "type", to: :type, render_default: true
16
+ map "content", to: :content
17
+ map "attrs", to: :attrs
20
18
  end
21
19
 
22
20
  def initialize(attributes = {})
@@ -84,10 +82,11 @@ module Prosereflect
84
82
  # Override to_h to handle empty content and attributes properly
85
83
  def to_h
86
84
  result = super
87
- result['content'] ||= []
88
- if result['attrs']
89
- result['attrs'] = result['attrs'].is_a?(Hash) && result['attrs'][:attrs] ? result['attrs'][:attrs] : result['attrs']
90
- result.delete('attrs') if result['attrs'].empty?
85
+ result["content"] ||= []
86
+ if result["attrs"]
87
+ result["attrs"] =
88
+ result["attrs"].is_a?(Hash) && result["attrs"][:attrs] ? result["attrs"][:attrs] : result["attrs"]
89
+ result.delete("attrs") if result["attrs"].empty?
91
90
  end
92
91
  result
93
92
  end
@@ -1,18 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'node'
4
- require_relative 'paragraph'
5
-
6
3
  module Prosereflect
7
4
  class TableCell < Node
8
- PM_TYPE = 'table_cell'
5
+ PM_TYPE = "table_cell"
9
6
 
10
- attribute :type, :string, default: -> { send('const_get', 'PM_TYPE') }
7
+ attribute :type, :string, default: -> {
8
+ self.class.send(:const_get, "PM_TYPE")
9
+ }
11
10
 
12
11
  key_value do
13
- map 'type', to: :type, render_default: true
14
- map 'content', to: :content
15
- map 'attrs', to: :attrs
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(attributes = {})
@@ -27,7 +26,7 @@ module Prosereflect
27
26
  def paragraphs
28
27
  return [] unless content
29
28
 
30
- content.select { |node| node.is_a?(Paragraph) }
29
+ content.grep(Paragraph)
31
30
  end
32
31
 
33
32
  def text_content
@@ -51,10 +50,11 @@ module Prosereflect
51
50
  # Override to_h to handle empty content and attributes properly
52
51
  def to_h
53
52
  result = super
54
- result['content'] ||= []
55
- if result['attrs']
56
- result['attrs'] = result['attrs'].is_a?(Hash) && result['attrs'][:attrs] ? result['attrs'][:attrs] : result['attrs']
57
- result.delete('attrs') if result['attrs'].empty?
53
+ result["content"] ||= []
54
+ if result["attrs"]
55
+ result["attrs"] =
56
+ result["attrs"].is_a?(Hash) && result["attrs"][:attrs] ? result["attrs"][:attrs] : result["attrs"]
57
+ result.delete("attrs") if result["attrs"].empty?
58
58
  end
59
59
  result
60
60
  end