coradoc 2.0.1 → 2.0.3
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/.rubocop_todo.yml +77 -146
- data/coradoc-adoc/lib/coradoc/asciidoc/model/base.rb +4 -3
- data/coradoc-adoc/lib/coradoc/asciidoc/model/document.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/model/include.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/model/resolver.rb +2 -2
- data/coradoc-adoc/lib/coradoc/asciidoc/model/serialization/asciidoc_transform.rb +3 -3
- data/coradoc-adoc/lib/coradoc/asciidoc/model/table_row.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/model/text_element.rb +4 -8
- data/coradoc-adoc/lib/coradoc/asciidoc/parse_error.rb +6 -6
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/adoc_serializer.rb +5 -10
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/formatter.rb +4 -3
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/serializers/base.rb +8 -20
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/serializers/block/core.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/serializers/document.rb +3 -6
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/serializers/inline/strikethrough.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/serializer/serializers/list/item.rb +5 -9
- data/coradoc-adoc/lib/coradoc/asciidoc/transform/from_core_model.rb +26 -34
- data/coradoc-adoc/lib/coradoc/asciidoc/transform/from_core_model_registrations.rb +18 -18
- data/coradoc-adoc/lib/coradoc/asciidoc/transform/to_core_model.rb +96 -123
- data/coradoc-adoc/lib/coradoc/asciidoc/transform/to_core_model_registrations.rb +10 -6
- data/coradoc-adoc/lib/coradoc/asciidoc/transformer/header_rules.rb +5 -5
- data/coradoc-adoc/lib/coradoc/asciidoc/transformer/list_rules.rb +2 -2
- data/coradoc-adoc/lib/coradoc/asciidoc/transformer/structural_rules.rb +1 -1
- data/coradoc-adoc/lib/coradoc/asciidoc/transformer.rb +5 -5
- data/coradoc-adoc/lib/coradoc/asciidoc.rb +1 -1
- data/coradoc-adoc/lib/coradoc/util/asciidoc.rb +4 -3
- data/coradoc-adoc/spec/coradoc/asciidoc/transform/from_core_model_spec.rb +4 -2
- data/coradoc-docx/lib/coradoc/docx/transform/context.rb +1 -1
- data/coradoc-docx/lib/coradoc/docx/transform/from_core_model.rb +13 -6
- data/coradoc-docx/lib/coradoc/docx/transform/numbering_resolver.rb +9 -7
- data/coradoc-docx/lib/coradoc/docx/transform/ordered_content.rb +2 -2
- data/coradoc-docx/lib/coradoc/docx/transform/rules/image_rule.rb +4 -1
- data/coradoc-docx/lib/coradoc/docx/transform/rules/math_rule.rb +2 -1
- data/coradoc-docx/lib/coradoc/docx/transform/rules/run_rule.rb +20 -30
- data/coradoc-docx/lib/coradoc/docx/transform/rules/simple_field_rule.rb +7 -5
- data/coradoc-docx/lib/coradoc/docx/transform/rules/table_rule.rb +3 -5
- data/coradoc-docx/lib/coradoc/docx/transform/style_resolver.rb +19 -24
- data/coradoc-docx/lib/coradoc/docx/transform/to_core_model.rb +18 -11
- data/coradoc-docx/lib/coradoc/docx.rb +6 -4
- data/coradoc-docx/spec/coradoc/docx/transform/from_core_model_spec.rb +5 -2
- data/coradoc-docx/spec/coradoc/docx/transform/rules/rule_unit_spec.rb +27 -7
- data/coradoc-docx/spec/coradoc/docx/transform/to_core_model_spec.rb +6 -2
- data/coradoc-html/lib/coradoc/html/base.rb +3 -7
- data/coradoc-html/lib/coradoc/html/converter_base.rb +5 -15
- data/coradoc-html/lib/coradoc/html/converters/base.rb +22 -28
- data/coradoc-html/lib/coradoc/html/converters/comment_line.rb +2 -2
- data/coradoc-html/lib/coradoc/html/converters/link.rb +1 -1
- data/coradoc-html/lib/coradoc/html/converters/list_item.rb +3 -3
- data/coradoc-html/lib/coradoc/html/converters/ordered.rb +1 -1
- data/coradoc-html/lib/coradoc/html/converters/span.rb +2 -2
- data/coradoc-html/lib/coradoc/html/converters/table.rb +3 -3
- data/coradoc-html/lib/coradoc/html/converters/table_cell.rb +14 -28
- data/coradoc-html/lib/coradoc/html/converters/table_row.rb +2 -2
- data/coradoc-html/lib/coradoc/html/converters/text_element.rb +1 -5
- data/coradoc-html/lib/coradoc/html/input/converters/a.rb +2 -2
- data/coradoc-html/lib/coradoc/html/input/converters/dl.rb +1 -1
- data/coradoc-html/lib/coradoc/html/input/converters/figure.rb +2 -2
- data/coradoc-html/lib/coradoc/html/input/converters/markup.rb +3 -3
- data/coradoc-html/lib/coradoc/html/input/converters/p.rb +1 -1
- data/coradoc-html/lib/coradoc/html/input/converters/td.rb +1 -1
- data/coradoc-html/lib/coradoc/html/input/converters.rb +1 -2
- data/coradoc-html/lib/coradoc/html/input/html_converter.rb +3 -3
- data/coradoc-html/lib/coradoc/html/input/plugin.rb +2 -2
- data/coradoc-html/lib/coradoc/html/renderer.rb +4 -6
- data/coradoc-html/lib/coradoc/html/theme/base.rb +2 -3
- data/coradoc-html/lib/coradoc/html/theme/classic_renderer.rb +9 -12
- data/coradoc-html/lib/coradoc/html/theme/modern/serializers/document_serializer.rb +3 -3
- data/coradoc-html/lib/coradoc/html.rb +1 -1
- data/coradoc-markdown/lib/coradoc/markdown/model/base.rb +6 -5
- data/coradoc-markdown/lib/coradoc/markdown/serializer.rb +2 -2
- data/coradoc-markdown/lib/coradoc/markdown/toc_generator.rb +4 -5
- data/coradoc-markdown/lib/coradoc/markdown/transform/from_core_model.rb +2 -2
- data/coradoc-markdown/lib/coradoc/markdown/transformer.rb +5 -3
- data/coradoc-markdown/lib/coradoc/markdown.rb +1 -1
- data/lib/coradoc/configurable.rb +6 -2
- data/lib/coradoc/coradoc.rb +18 -16
- data/lib/coradoc/core_model/base.rb +3 -3
- data/lib/coradoc/core_model/list_item.rb +3 -3
- data/lib/coradoc/core_model/toc_generator.rb +1 -1
- data/lib/coradoc/document_manipulator.rb +9 -13
- data/lib/coradoc/format_module.rb +16 -4
- data/lib/coradoc/input.rb +1 -1
- data/lib/coradoc/output.rb +1 -1
- data/lib/coradoc/query.rb +38 -186
- data/lib/coradoc/registry.rb +5 -7
- data/lib/coradoc/serializer/registry.rb +3 -5
- data/lib/coradoc/validation.rb +40 -21
- data/lib/coradoc/version.rb +1 -1
- metadata +1 -1
data/lib/coradoc/query.rb
CHANGED
|
@@ -3,44 +3,12 @@
|
|
|
3
3
|
module Coradoc
|
|
4
4
|
# Document querying and introspection API.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# document trees. It enables powerful document manipulation patterns.
|
|
8
|
-
#
|
|
9
|
-
# @example Querying documents
|
|
10
|
-
# doc = Coradoc.parse(adoc_text, format: :asciidoc)
|
|
11
|
-
#
|
|
12
|
-
# # Find all sections
|
|
13
|
-
# sections = doc.query('section')
|
|
14
|
-
#
|
|
15
|
-
# # Find level-2 sections
|
|
16
|
-
# doc.query('section.level-2').each do |section|
|
|
17
|
-
# puts section.title
|
|
18
|
-
# end
|
|
19
|
-
#
|
|
20
|
-
# # Find paragraphs with specific role
|
|
21
|
-
# examples = doc.query('[role=example]')
|
|
22
|
-
#
|
|
23
|
-
# # Complex selectors
|
|
24
|
-
# doc.query('section > paragraph:first-child')
|
|
25
|
-
#
|
|
6
|
+
# Provides CSS-like selectors for navigating and querying document trees.
|
|
26
7
|
module Query
|
|
27
8
|
# Selector parsing and matching
|
|
28
|
-
#
|
|
29
|
-
# Supports CSS-like selectors for document querying:
|
|
30
|
-
# - Element type: `section`, `paragraph`, `table`
|
|
31
|
-
# - Class/level: `.level-2`, `.important`
|
|
32
|
-
# - ID: `#intro`, `#section-1`
|
|
33
|
-
# - Attributes: `[id=intro]`, `[role=example]`, `[level>1]`
|
|
34
|
-
# - Pseudo-classes: `:first-child`, `:last-child`, `:nth-child(2)`
|
|
35
|
-
# - Combinators: `>` (child), space (descendant)
|
|
36
|
-
#
|
|
37
9
|
class Selector
|
|
38
10
|
attr_reader :element_type, :id, :classes, :attributes, :pseudo_classes
|
|
39
11
|
|
|
40
|
-
# Parse a selector string
|
|
41
|
-
#
|
|
42
|
-
# @param selector [String] CSS-like selector
|
|
43
|
-
# @return [Selector] Parsed selector object
|
|
44
12
|
def self.parse(selector)
|
|
45
13
|
new.parse(selector)
|
|
46
14
|
end
|
|
@@ -53,40 +21,31 @@ module Coradoc
|
|
|
53
21
|
@pseudo_classes = []
|
|
54
22
|
end
|
|
55
23
|
|
|
56
|
-
# Parse a selector string into this object
|
|
57
|
-
#
|
|
58
|
-
# @param selector [String] The selector to parse
|
|
59
|
-
# @return [self]
|
|
60
24
|
def parse(selector)
|
|
61
25
|
@original = selector.to_s.strip
|
|
62
26
|
return self if @original.empty?
|
|
63
27
|
|
|
64
|
-
# Parse element type
|
|
65
28
|
@original.sub!(/\A([a-z_][a-z0-9_-]*)/i) do |match|
|
|
66
29
|
@element_type = match.downcase
|
|
67
30
|
''
|
|
68
31
|
end
|
|
69
32
|
|
|
70
|
-
# Parse ID
|
|
71
33
|
@original.sub!(/#([a-z_][a-z0-9_-]*)/i) do
|
|
72
34
|
@id = ::Regexp.last_match(1)
|
|
73
35
|
''
|
|
74
36
|
end
|
|
75
37
|
|
|
76
|
-
# Parse classes
|
|
77
38
|
@original.gsub!(/\.([a-z_][a-z0-9_-]*)/i) do
|
|
78
39
|
@classes << ::Regexp.last_match(1)
|
|
79
40
|
''
|
|
80
41
|
end
|
|
81
42
|
|
|
82
|
-
# Parse attributes
|
|
83
43
|
@original.gsub!(/\[([^\]]+)\]/) do
|
|
84
44
|
attr_expr = ::Regexp.last_match(1)
|
|
85
45
|
parse_attribute(attr_expr)
|
|
86
46
|
''
|
|
87
47
|
end
|
|
88
48
|
|
|
89
|
-
# Parse pseudo-classes
|
|
90
49
|
@original.gsub!(/:([a-z-]+)(?:\(([^)]+)\))?/i) do
|
|
91
50
|
name = ::Regexp.last_match(1).downcase
|
|
92
51
|
arg = ::Regexp.last_match(2)
|
|
@@ -97,34 +56,16 @@ module Coradoc
|
|
|
97
56
|
self
|
|
98
57
|
end
|
|
99
58
|
|
|
100
|
-
# Check if an element matches this selector
|
|
101
|
-
#
|
|
102
|
-
# @param element [CoreModel::Base] The element to check
|
|
103
|
-
# @return [Boolean]
|
|
104
59
|
def matches?(element)
|
|
105
60
|
return false unless element
|
|
106
|
-
|
|
107
|
-
# Check element type
|
|
108
61
|
return false if @element_type && !type_matches?(element)
|
|
109
|
-
|
|
110
|
-
# Check ID
|
|
111
|
-
return false if @id && element_id(element) != @id
|
|
112
|
-
|
|
113
|
-
# Check classes/roles
|
|
62
|
+
return false if @id && element.id != @id
|
|
114
63
|
return false if @classes.any? && !classes_match?(element)
|
|
115
|
-
|
|
116
|
-
# Check attributes
|
|
117
64
|
return false if @attributes.any? && !attributes_match?(element)
|
|
118
65
|
|
|
119
66
|
true
|
|
120
67
|
end
|
|
121
68
|
|
|
122
|
-
# Check pseudo-class conditions
|
|
123
|
-
#
|
|
124
|
-
# @param element [CoreModel::Base] The element to check
|
|
125
|
-
# @param siblings [Array] Sibling elements
|
|
126
|
-
# @param index [Integer] Element's index among siblings
|
|
127
|
-
# @return [Boolean]
|
|
128
69
|
def matches_pseudo_classes?(element, siblings:, index:)
|
|
129
70
|
@pseudo_classes.all? do |pseudo|
|
|
130
71
|
case pseudo[:name]
|
|
@@ -134,20 +75,17 @@ module Coradoc
|
|
|
134
75
|
index == siblings.length - 1
|
|
135
76
|
when 'nth-child'
|
|
136
77
|
n = pseudo[:argument].to_i
|
|
137
|
-
index == n - 1
|
|
78
|
+
index == n - 1
|
|
138
79
|
when 'only-child'
|
|
139
80
|
siblings.length == 1
|
|
140
81
|
when 'empty'
|
|
141
82
|
empty_element?(element)
|
|
142
83
|
else
|
|
143
|
-
true
|
|
84
|
+
true
|
|
144
85
|
end
|
|
145
86
|
end
|
|
146
87
|
end
|
|
147
88
|
|
|
148
|
-
# Check if selector is universal (*)
|
|
149
|
-
#
|
|
150
|
-
# @return [Boolean]
|
|
151
89
|
def universal?
|
|
152
90
|
@element_type == '*' || @original == '*'
|
|
153
91
|
end
|
|
@@ -155,7 +93,6 @@ module Coradoc
|
|
|
155
93
|
private
|
|
156
94
|
|
|
157
95
|
def parse_attribute(expr)
|
|
158
|
-
# Handle different attribute operators
|
|
159
96
|
case expr
|
|
160
97
|
when /(\w+)\s*=\s*["']?([^"']+)["']?/
|
|
161
98
|
@attributes[::Regexp.last_match(1).to_sym] = {
|
|
@@ -183,7 +120,6 @@ module Coradoc
|
|
|
183
120
|
value: ::Regexp.last_match(2)
|
|
184
121
|
}
|
|
185
122
|
when /(\w+)/
|
|
186
|
-
# Attribute presence check
|
|
187
123
|
@attributes[::Regexp.last_match(1).to_sym] = { operator: :present }
|
|
188
124
|
end
|
|
189
125
|
end
|
|
@@ -191,14 +127,9 @@ module Coradoc
|
|
|
191
127
|
def type_matches?(element)
|
|
192
128
|
return true if @element_type == '*'
|
|
193
129
|
|
|
194
|
-
|
|
195
|
-
if element.respond_to?(:element_type) && element.element_type
|
|
196
|
-
return element.element_type.to_s.downcase == @element_type.downcase
|
|
197
|
-
end
|
|
130
|
+
return element.element_type.to_s.downcase == @element_type.downcase if (element.is_a?(CoreModel::StructuralElement) || element.is_a?(CoreModel::Block)) && element.element_type
|
|
198
131
|
|
|
199
|
-
# Then check the class-derived snake_case name (exact match only)
|
|
200
132
|
class_name = class_to_query_name(element.class)
|
|
201
|
-
|
|
202
133
|
class_name == @element_type
|
|
203
134
|
end
|
|
204
135
|
|
|
@@ -212,22 +143,25 @@ module Coradoc
|
|
|
212
143
|
.downcase
|
|
213
144
|
end
|
|
214
145
|
|
|
215
|
-
def element_id(element)
|
|
216
|
-
element.respond_to?(:id) ? element.id : nil
|
|
217
|
-
end
|
|
218
|
-
|
|
219
146
|
def classes_match?(element)
|
|
220
|
-
element_classes = if element.
|
|
221
|
-
element.
|
|
222
|
-
elsif element.
|
|
223
|
-
Array(element.classes).map(&:downcase)
|
|
224
|
-
else
|
|
147
|
+
element_classes = if element.is_a?(CoreModel::StructuralElement) && element.element_type
|
|
148
|
+
[element.element_type]
|
|
149
|
+
elsif element.is_a?(CoreModel::Base)
|
|
225
150
|
[]
|
|
151
|
+
else
|
|
152
|
+
extract_role(element)
|
|
226
153
|
end
|
|
227
154
|
|
|
228
155
|
@classes.all? { |c| element_classes.include?(c.downcase) }
|
|
229
156
|
end
|
|
230
157
|
|
|
158
|
+
def extract_role(element)
|
|
159
|
+
role = element.public_send(:role)
|
|
160
|
+
role ? role.to_s.split.map(&:downcase) : []
|
|
161
|
+
rescue NoMethodError
|
|
162
|
+
[]
|
|
163
|
+
end
|
|
164
|
+
|
|
231
165
|
def attributes_match?(element)
|
|
232
166
|
@attributes.all? do |attr_name, condition|
|
|
233
167
|
value = get_attribute_value(element, attr_name)
|
|
@@ -237,17 +171,23 @@ module Coradoc
|
|
|
237
171
|
|
|
238
172
|
def get_attribute_value(element, attr_name)
|
|
239
173
|
case attr_name
|
|
240
|
-
when :id
|
|
241
|
-
element.
|
|
174
|
+
when :id, :title
|
|
175
|
+
element.public_send(attr_name)
|
|
242
176
|
when :level
|
|
243
|
-
element.
|
|
244
|
-
|
|
245
|
-
|
|
177
|
+
if element.is_a?(CoreModel::StructuralElement)
|
|
178
|
+
element.level
|
|
179
|
+
else
|
|
180
|
+
element.public_send(:level)
|
|
181
|
+
end
|
|
182
|
+
when :element_type
|
|
183
|
+
element.element_type if element.is_a?(CoreModel::StructuralElement) || element.is_a?(CoreModel::Block)
|
|
246
184
|
when :type
|
|
247
|
-
element.
|
|
185
|
+
element.type if element.is_a?(CoreModel::AnnotationBlock) || element.is_a?(CoreModel::InlineElement)
|
|
248
186
|
else
|
|
249
|
-
element.
|
|
187
|
+
element.public_send(attr_name) if element.is_a?(CoreModel::Base) && element.class.attributes.key?(attr_name)
|
|
250
188
|
end
|
|
189
|
+
rescue NoMethodError
|
|
190
|
+
nil
|
|
251
191
|
end
|
|
252
192
|
|
|
253
193
|
def match_attribute_condition(value, condition)
|
|
@@ -270,7 +210,7 @@ module Coradoc
|
|
|
270
210
|
end
|
|
271
211
|
|
|
272
212
|
def empty_element?(element)
|
|
273
|
-
return true unless element.
|
|
213
|
+
return true unless element.is_a?(CoreModel::Block) || element.is_a?(CoreModel::StructuralElement)
|
|
274
214
|
|
|
275
215
|
content = element.content
|
|
276
216
|
case content
|
|
@@ -285,82 +225,46 @@ module Coradoc
|
|
|
285
225
|
end
|
|
286
226
|
|
|
287
227
|
# Query result set - collection of matched elements
|
|
288
|
-
#
|
|
289
|
-
# Provides array-like access with additional query methods for
|
|
290
|
-
# chaining and further filtering.
|
|
291
|
-
#
|
|
292
228
|
class ResultSet
|
|
293
229
|
include Enumerable
|
|
294
230
|
|
|
295
|
-
# @return [Array<CoreModel::Base>] Matched elements
|
|
296
231
|
attr_reader :elements
|
|
297
232
|
|
|
298
|
-
# Create a new result set
|
|
299
|
-
#
|
|
300
|
-
# @param elements [Array<CoreModel::Base>] Matched elements
|
|
301
233
|
def initialize(elements = [])
|
|
302
234
|
@elements = Array(elements).compact
|
|
303
235
|
end
|
|
304
236
|
|
|
305
|
-
# Iterate over elements
|
|
306
|
-
#
|
|
307
|
-
# @yield [CoreModel::Base] Each matched element
|
|
308
|
-
# @return [Enumerator]
|
|
309
237
|
def each(&block)
|
|
310
238
|
@elements.each(&block)
|
|
311
239
|
end
|
|
312
240
|
|
|
313
|
-
# Get element at index
|
|
314
|
-
#
|
|
315
|
-
# @param index [Integer] Element index
|
|
316
|
-
# @return [CoreModel::Base, nil]
|
|
317
241
|
def [](index)
|
|
318
242
|
@elements[index]
|
|
319
243
|
end
|
|
320
244
|
|
|
321
|
-
# Number of matched elements
|
|
322
|
-
#
|
|
323
|
-
# @return [Integer]
|
|
324
245
|
def length
|
|
325
246
|
@elements.length
|
|
326
247
|
end
|
|
327
248
|
alias size length
|
|
328
249
|
|
|
329
|
-
# Check if result set is empty
|
|
330
|
-
#
|
|
331
|
-
# @return [Boolean]
|
|
332
250
|
def empty?
|
|
333
251
|
@elements.empty?
|
|
334
252
|
end
|
|
335
253
|
|
|
336
|
-
# Get first element
|
|
337
|
-
#
|
|
338
|
-
# @return [CoreModel::Base, nil]
|
|
339
254
|
def first
|
|
340
255
|
@elements.first
|
|
341
256
|
end
|
|
342
257
|
|
|
343
|
-
# Get last element
|
|
344
|
-
#
|
|
345
|
-
# @return [CoreModel::Base, nil]
|
|
346
258
|
def last
|
|
347
259
|
@elements.last
|
|
348
260
|
end
|
|
349
261
|
|
|
350
|
-
# Filter results with an additional selector
|
|
351
|
-
#
|
|
352
|
-
# @param selector [String] CSS-like selector
|
|
353
|
-
# @return [ResultSet] Filtered results
|
|
354
262
|
def filter(selector)
|
|
355
263
|
parsed = Selector.parse(selector)
|
|
356
264
|
filtered = @elements.select { |e| parsed.matches?(e) }
|
|
357
265
|
ResultSet.new(filtered)
|
|
358
266
|
end
|
|
359
267
|
|
|
360
|
-
# Query within each element in the result set
|
|
361
|
-
#
|
|
362
|
-
# @param selector [String] CSS-like selector
|
|
363
|
-
# @return [ResultSet] Combined results
|
|
364
268
|
def query(selector)
|
|
365
269
|
results = @elements.flat_map do |element|
|
|
366
270
|
Query.query_within(element, selector).to_a
|
|
@@ -368,72 +272,40 @@ module Coradoc
|
|
|
368
272
|
ResultSet.new(results.uniq)
|
|
369
273
|
end
|
|
370
274
|
|
|
371
|
-
# Map over elements and return new result set
|
|
372
|
-
#
|
|
373
|
-
# @yield [CoreModel::Base] Block to transform elements
|
|
374
|
-
# @return [ResultSet]
|
|
375
275
|
def map(&block)
|
|
376
276
|
ResultSet.new(@elements.map(&block))
|
|
377
277
|
end
|
|
378
278
|
|
|
379
|
-
# Select elements matching block
|
|
380
|
-
#
|
|
381
|
-
# @yield [CoreModel::Base] Test block
|
|
382
|
-
# @return [ResultSet]
|
|
383
279
|
def select(&block)
|
|
384
280
|
ResultSet.new(@elements.select(&block))
|
|
385
281
|
end
|
|
386
282
|
|
|
387
|
-
# Reject elements matching block
|
|
388
|
-
#
|
|
389
|
-
# @yield [CoreModel::Base] Test block
|
|
390
|
-
# @return [ResultSet]
|
|
391
283
|
def reject(&block)
|
|
392
284
|
ResultSet.new(@elements.reject(&block))
|
|
393
285
|
end
|
|
394
286
|
|
|
395
|
-
# Convert to array
|
|
396
|
-
#
|
|
397
|
-
# @return [Array<CoreModel::Base>]
|
|
398
287
|
def to_a
|
|
399
288
|
@elements.dup
|
|
400
289
|
end
|
|
401
290
|
|
|
402
|
-
# Pretty print representation
|
|
403
|
-
#
|
|
404
|
-
# @return [String]
|
|
405
291
|
def inspect
|
|
406
292
|
"#<Coradoc::Query::ResultSet count=#{length}>"
|
|
407
293
|
end
|
|
408
294
|
end
|
|
409
295
|
|
|
410
296
|
# Query engine for executing selectors
|
|
411
|
-
#
|
|
412
297
|
class Engine
|
|
413
|
-
# Query a document or element
|
|
414
|
-
#
|
|
415
|
-
# @param document [CoreModel::Base] Root document/element
|
|
416
|
-
# @param selector [String] CSS-like selector
|
|
417
|
-
# @return [ResultSet] Matched elements
|
|
418
298
|
def self.query(document, selector)
|
|
419
299
|
new.query(document, selector)
|
|
420
300
|
end
|
|
421
301
|
|
|
422
|
-
# Query document with selector
|
|
423
|
-
#
|
|
424
|
-
# @param document [CoreModel::Base] Root element
|
|
425
|
-
# @param selector [String] CSS-like selector
|
|
426
|
-
# @return [ResultSet] Matched elements
|
|
427
302
|
def query(document, selector)
|
|
428
303
|
return ResultSet.new if document.nil? || selector.to_s.strip.empty?
|
|
429
304
|
|
|
430
|
-
# Handle comma-separated selectors
|
|
431
305
|
return query_multiple(document, selector.split(',').map(&:strip)) if selector.include?(',')
|
|
432
306
|
|
|
433
|
-
# Handle descendant combinator (space) and child combinator (>)
|
|
434
307
|
return query_with_combinators(document, selector) if selector.include?('>') || selector.include?(' ')
|
|
435
308
|
|
|
436
|
-
# Simple single selector
|
|
437
309
|
parsed = Selector.parse(selector)
|
|
438
310
|
results = []
|
|
439
311
|
|
|
@@ -462,11 +334,9 @@ module Coradoc
|
|
|
462
334
|
parts = parse_combinator_selector(selector)
|
|
463
335
|
results = []
|
|
464
336
|
|
|
465
|
-
# Find elements matching the first part
|
|
466
337
|
first_results = query(document, parts[:first])
|
|
467
338
|
return ResultSet.new if first_results.empty?
|
|
468
339
|
|
|
469
|
-
# For each first match, look for descendants/children
|
|
470
340
|
first_results.each do |parent|
|
|
471
341
|
find_matching_descendants(parent, parts[:rest]).each do |match|
|
|
472
342
|
results << match
|
|
@@ -477,7 +347,6 @@ module Coradoc
|
|
|
477
347
|
end
|
|
478
348
|
|
|
479
349
|
def parse_combinator_selector(selector)
|
|
480
|
-
# Simple parsing - handles "parent > child" and "parent child"
|
|
481
350
|
if selector.include?(' > ')
|
|
482
351
|
parts = selector.split(' > ', 2)
|
|
483
352
|
{ first: parts[0], rest: [{ combinator: :child, selector: parts[1] }] }
|
|
@@ -509,7 +378,6 @@ module Coradoc
|
|
|
509
378
|
results.concat(find_matching_descendants(child, remaining)) if parsed.matches?(child) && pseudo_matches?(
|
|
510
379
|
parsed, child, siblings, index
|
|
511
380
|
)
|
|
512
|
-
# Also search deeper
|
|
513
381
|
results.concat(find_matching_descendants(child, parts))
|
|
514
382
|
end
|
|
515
383
|
end
|
|
@@ -541,20 +409,10 @@ module Coradoc
|
|
|
541
409
|
|
|
542
410
|
# Module-level query methods
|
|
543
411
|
class << self
|
|
544
|
-
# Query a document with a selector
|
|
545
|
-
#
|
|
546
|
-
# @param document [CoreModel::Base] The document to query
|
|
547
|
-
# @param selector [String] CSS-like selector
|
|
548
|
-
# @return [ResultSet] Matched elements
|
|
549
412
|
def query(document, selector)
|
|
550
413
|
Engine.query(document, selector)
|
|
551
414
|
end
|
|
552
415
|
|
|
553
|
-
# Query within an element (not including the element itself)
|
|
554
|
-
#
|
|
555
|
-
# @param element [CoreModel::Base] The parent element
|
|
556
|
-
# @param selector [String] CSS-like selector
|
|
557
|
-
# @return [ResultSet] Matched elements
|
|
558
416
|
def query_within(element, selector)
|
|
559
417
|
parsed = Selector.parse(selector)
|
|
560
418
|
results = []
|
|
@@ -571,22 +429,16 @@ module Coradoc
|
|
|
571
429
|
ResultSet.new(results)
|
|
572
430
|
end
|
|
573
431
|
|
|
574
|
-
# Get navigable children from an element.
|
|
575
|
-
# Uses ChildrenContent#children when available, falls back to content.
|
|
576
|
-
#
|
|
577
|
-
# @param element [Object] Element to get children from
|
|
578
|
-
# @return [Array] Navigable child elements
|
|
579
432
|
def get_children(element)
|
|
580
433
|
return [] unless element
|
|
581
434
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
Array(children)
|
|
435
|
+
if element.is_a?(CoreModel::StructuralElement) && element.children&.any?
|
|
436
|
+
element.children
|
|
437
|
+
elsif element.is_a?(CoreModel::Block) && element.content
|
|
438
|
+
Array(element.content).select { |c| c.is_a?(CoreModel::Base) }
|
|
439
|
+
else
|
|
440
|
+
[]
|
|
441
|
+
end
|
|
590
442
|
end
|
|
591
443
|
|
|
592
444
|
private
|
data/lib/coradoc/registry.rb
CHANGED
|
@@ -46,8 +46,6 @@ module Coradoc
|
|
|
46
46
|
# @param options [Hash] optional per-item configuration
|
|
47
47
|
# @return [void]
|
|
48
48
|
def define(item, **opts)
|
|
49
|
-
return unless item.respond_to?(:processor_id)
|
|
50
|
-
|
|
51
49
|
register(item.processor_id, item, opts)
|
|
52
50
|
end
|
|
53
51
|
|
|
@@ -85,9 +83,7 @@ module Coradoc
|
|
|
85
83
|
|
|
86
84
|
# Direct access to the items hash (for backward compatibility)
|
|
87
85
|
# @return [Hash<Symbol, Object>]
|
|
88
|
-
|
|
89
|
-
@items
|
|
90
|
-
end
|
|
86
|
+
attr_reader :items
|
|
91
87
|
|
|
92
88
|
# Number of registered items
|
|
93
89
|
#
|
|
@@ -132,7 +128,9 @@ module Coradoc
|
|
|
132
128
|
# @return [Object, nil]
|
|
133
129
|
def for_file(filename)
|
|
134
130
|
@items.values.find do |item|
|
|
135
|
-
item.
|
|
131
|
+
item.processor_match?(filename)
|
|
132
|
+
rescue NoMethodError
|
|
133
|
+
false
|
|
136
134
|
end
|
|
137
135
|
end
|
|
138
136
|
|
|
@@ -149,7 +147,7 @@ module Coradoc
|
|
|
149
147
|
for_file(options[:filename])
|
|
150
148
|
end
|
|
151
149
|
|
|
152
|
-
label = @error_label ||
|
|
150
|
+
label = @error_label || 'processor'
|
|
153
151
|
raise ArgumentError, "No #{label} found for: #{options}" unless item
|
|
154
152
|
|
|
155
153
|
item.processor_execute(content, options)
|
|
@@ -100,14 +100,12 @@ module Coradoc
|
|
|
100
100
|
serializer_class = lookup(model)
|
|
101
101
|
return nil unless serializer_class
|
|
102
102
|
|
|
103
|
-
serializer = serializer_class.
|
|
103
|
+
serializer = serializer_class.is_a?(Class) ? serializer_class.new : serializer_class
|
|
104
104
|
|
|
105
|
-
if serializer.
|
|
105
|
+
if serializer.is_a?(Base)
|
|
106
106
|
serializer.serialize(model, format: format, **options)
|
|
107
|
-
elsif serializer.respond_to?(:to_s)
|
|
108
|
-
serializer.to_s
|
|
109
107
|
else
|
|
110
|
-
|
|
108
|
+
serializer.to_s
|
|
111
109
|
end
|
|
112
110
|
end
|
|
113
111
|
|
data/lib/coradoc/validation.rb
CHANGED
|
@@ -177,23 +177,26 @@ module Coradoc
|
|
|
177
177
|
class Rule
|
|
178
178
|
attr_reader :name, :options
|
|
179
179
|
|
|
180
|
-
# Create a validation rule
|
|
181
|
-
#
|
|
182
|
-
# @param name [Symbol] Rule name
|
|
183
|
-
# @param options [Hash] Rule options
|
|
184
180
|
def initialize(name, **options)
|
|
185
181
|
@name = name
|
|
186
182
|
@options = options
|
|
187
183
|
end
|
|
188
184
|
|
|
189
|
-
# Validate an element
|
|
190
|
-
#
|
|
191
|
-
# @param element [Object] Element to validate
|
|
192
|
-
# @param context [Hash] Validation context
|
|
193
|
-
# @return [Array<String>] Error messages
|
|
194
185
|
def validate(element, context = {})
|
|
195
186
|
raise NotImplementedError, 'Subclasses must implement #validate'
|
|
196
187
|
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def field_value(element, field)
|
|
192
|
+
if element.is_a?(CoreModel::Base)
|
|
193
|
+
element.public_send(field) if element.class.attributes.key?(field)
|
|
194
|
+
else
|
|
195
|
+
element.public_send(field)
|
|
196
|
+
end
|
|
197
|
+
rescue NoMethodError
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
197
200
|
end
|
|
198
201
|
|
|
199
202
|
# Built-in validation rules
|
|
@@ -212,7 +215,13 @@ module Coradoc
|
|
|
212
215
|
private
|
|
213
216
|
|
|
214
217
|
def get_value(element, field)
|
|
215
|
-
|
|
218
|
+
if element.is_a?(CoreModel::Base)
|
|
219
|
+
element.public_send(field) if element.class.attributes.key?(field)
|
|
220
|
+
else
|
|
221
|
+
element.public_send(field)
|
|
222
|
+
end
|
|
223
|
+
rescue NoMethodError
|
|
224
|
+
nil
|
|
216
225
|
end
|
|
217
226
|
end
|
|
218
227
|
|
|
@@ -221,7 +230,7 @@ module Coradoc
|
|
|
221
230
|
def validate(element, _context = {})
|
|
222
231
|
field = options[:field]
|
|
223
232
|
expected_type = options[:type]
|
|
224
|
-
value = element
|
|
233
|
+
value = field_value(element, field)
|
|
225
234
|
|
|
226
235
|
return [] if value.nil? && !options[:required]
|
|
227
236
|
return [] if value.nil?
|
|
@@ -236,12 +245,12 @@ module Coradoc
|
|
|
236
245
|
class Length < Rule
|
|
237
246
|
def validate(element, _context = {})
|
|
238
247
|
field = options[:field]
|
|
239
|
-
value = element
|
|
248
|
+
value = field_value(element, field)
|
|
240
249
|
|
|
241
250
|
return [] if value.nil?
|
|
242
251
|
|
|
243
252
|
errors = []
|
|
244
|
-
length = value.
|
|
253
|
+
length = value.is_a?(String) ? value.length : 0
|
|
245
254
|
|
|
246
255
|
errors << "#{field} must have at least #{options[:min]} characters/items" if options[:min] && length < options[:min]
|
|
247
256
|
|
|
@@ -255,12 +264,12 @@ module Coradoc
|
|
|
255
264
|
class Count < Rule
|
|
256
265
|
def validate(element, _context = {})
|
|
257
266
|
field = options[:field]
|
|
258
|
-
value = element
|
|
267
|
+
value = field_value(element, field)
|
|
259
268
|
|
|
260
269
|
return [] if value.nil?
|
|
261
270
|
|
|
262
271
|
errors = []
|
|
263
|
-
count = value.
|
|
272
|
+
count = value.is_a?(Enumerable) ? value.count : 0
|
|
264
273
|
|
|
265
274
|
errors << "#{field} must have at least #{options[:min]} items" if options[:min] && count < options[:min]
|
|
266
275
|
|
|
@@ -275,7 +284,7 @@ module Coradoc
|
|
|
275
284
|
def validate(element, _context = {})
|
|
276
285
|
field = options[:field]
|
|
277
286
|
pattern = options[:pattern]
|
|
278
|
-
value = element
|
|
287
|
+
value = field_value(element, field)
|
|
279
288
|
|
|
280
289
|
return [] if value.nil?
|
|
281
290
|
|
|
@@ -377,7 +386,7 @@ module Coradoc
|
|
|
377
386
|
private
|
|
378
387
|
|
|
379
388
|
def validate_field(document, name, config, result)
|
|
380
|
-
value = document
|
|
389
|
+
value = field_value(document, name)
|
|
381
390
|
path = name.to_s
|
|
382
391
|
|
|
383
392
|
# Check required
|
|
@@ -398,7 +407,7 @@ module Coradoc
|
|
|
398
407
|
end
|
|
399
408
|
|
|
400
409
|
# Check min_length
|
|
401
|
-
if config[:min_length] && value.
|
|
410
|
+
if config[:min_length] && value.is_a?(String) && (value.length < config[:min_length])
|
|
402
411
|
result.add_error(
|
|
403
412
|
"#{name} must have at least #{config[:min_length]} characters",
|
|
404
413
|
path: path,
|
|
@@ -407,7 +416,7 @@ module Coradoc
|
|
|
407
416
|
end
|
|
408
417
|
|
|
409
418
|
# Check max_length
|
|
410
|
-
if config[:max_length] && value.
|
|
419
|
+
if config[:max_length] && value.is_a?(String) && (value.length > config[:max_length])
|
|
411
420
|
result.add_error(
|
|
412
421
|
"#{name} must have at most #{config[:max_length]} characters",
|
|
413
422
|
path: path,
|
|
@@ -416,7 +425,7 @@ module Coradoc
|
|
|
416
425
|
end
|
|
417
426
|
|
|
418
427
|
# Check min_count
|
|
419
|
-
if config[:min_count] && value.
|
|
428
|
+
if config[:min_count] && value.is_a?(Enumerable) && (value.count < config[:min_count])
|
|
420
429
|
result.add_error(
|
|
421
430
|
"#{name} must have at least #{config[:min_count]} items",
|
|
422
431
|
path: path,
|
|
@@ -433,6 +442,16 @@ module Coradoc
|
|
|
433
442
|
code: :format
|
|
434
443
|
)
|
|
435
444
|
end
|
|
445
|
+
|
|
446
|
+
def field_value(document, field)
|
|
447
|
+
if document.is_a?(CoreModel::Base)
|
|
448
|
+
document.public_send(field) if document.class.attributes.key?(field)
|
|
449
|
+
else
|
|
450
|
+
document.public_send(field)
|
|
451
|
+
end
|
|
452
|
+
rescue NoMethodError
|
|
453
|
+
nil
|
|
454
|
+
end
|
|
436
455
|
end
|
|
437
456
|
|
|
438
457
|
# Schema generator from CoreModel types
|
|
@@ -479,7 +498,7 @@ module Coradoc
|
|
|
479
498
|
# )
|
|
480
499
|
#
|
|
481
500
|
def generate(model_class, required: [], ignored: [], custom_rules: {})
|
|
482
|
-
return nil unless model_class.
|
|
501
|
+
return nil unless model_class.is_a?(Class) && model_class < CoreModel::Base
|
|
483
502
|
|
|
484
503
|
# Pre-compute attribute definitions before the schema block
|
|
485
504
|
attribute_defs = compute_attribute_definitions(
|