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
|
@@ -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"
|
data/lib/prosereflect/table.rb
CHANGED
|
@@ -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 =
|
|
8
|
+
PM_TYPE = "table"
|
|
13
9
|
|
|
14
|
-
attribute :type, :string, default: -> {
|
|
10
|
+
attribute :type, :string, default: -> {
|
|
11
|
+
self.class.send(:const_get, "PM_TYPE")
|
|
12
|
+
}
|
|
15
13
|
|
|
16
14
|
key_value do
|
|
17
|
-
map
|
|
18
|
-
map
|
|
19
|
-
map
|
|
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[
|
|
88
|
-
if result[
|
|
89
|
-
result[
|
|
90
|
-
|
|
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 =
|
|
5
|
+
PM_TYPE = "table_cell"
|
|
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(attributes = {})
|
|
@@ -27,7 +26,7 @@ module Prosereflect
|
|
|
27
26
|
def paragraphs
|
|
28
27
|
return [] unless content
|
|
29
28
|
|
|
30
|
-
content.
|
|
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[
|
|
55
|
-
if result[
|
|
56
|
-
result[
|
|
57
|
-
|
|
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
|