enolib 0.5.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +52 -0
  4. data/lib/enolib.rb +42 -0
  5. data/lib/enolib/constants.rb +16 -0
  6. data/lib/enolib/context.rb +220 -0
  7. data/lib/enolib/elements/element.rb +42 -0
  8. data/lib/enolib/elements/element_base.rb +141 -0
  9. data/lib/enolib/elements/empty.rb +9 -0
  10. data/lib/enolib/elements/field.rb +63 -0
  11. data/lib/enolib/elements/fieldset.rb +151 -0
  12. data/lib/enolib/elements/fieldset_entry.rb +15 -0
  13. data/lib/enolib/elements/list.rb +107 -0
  14. data/lib/enolib/elements/list_item.rb +13 -0
  15. data/lib/enolib/elements/missing/missing_element_base.rb +44 -0
  16. data/lib/enolib/elements/missing/missing_empty.rb +13 -0
  17. data/lib/enolib/elements/missing/missing_field.rb +13 -0
  18. data/lib/enolib/elements/missing/missing_fieldset.rb +29 -0
  19. data/lib/enolib/elements/missing/missing_fieldset_entry.rb +13 -0
  20. data/lib/enolib/elements/missing/missing_list.rb +33 -0
  21. data/lib/enolib/elements/missing/missing_section.rb +105 -0
  22. data/lib/enolib/elements/missing/missing_section_element.rb +53 -0
  23. data/lib/enolib/elements/missing/missing_value_element_base.rb +21 -0
  24. data/lib/enolib/elements/section.rb +560 -0
  25. data/lib/enolib/elements/section_element.rb +141 -0
  26. data/lib/enolib/elements/value_element_base.rb +79 -0
  27. data/lib/enolib/errors.rb +25 -0
  28. data/lib/enolib/errors/parsing.rb +136 -0
  29. data/lib/enolib/errors/selections.rb +83 -0
  30. data/lib/enolib/errors/validation.rb +146 -0
  31. data/lib/enolib/grammar_regexp.rb +103 -0
  32. data/lib/enolib/lookup.rb +235 -0
  33. data/lib/enolib/messages/de.rb +79 -0
  34. data/lib/enolib/messages/en.rb +79 -0
  35. data/lib/enolib/messages/es.rb +79 -0
  36. data/lib/enolib/parse.rb +9 -0
  37. data/lib/enolib/parser.rb +708 -0
  38. data/lib/enolib/register.rb +24 -0
  39. data/lib/enolib/reporters/html_reporter.rb +115 -0
  40. data/lib/enolib/reporters/reporter.rb +258 -0
  41. data/lib/enolib/reporters/terminal_reporter.rb +107 -0
  42. data/lib/enolib/reporters/text_reporter.rb +46 -0
  43. metadata +130 -0
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ module Errors
5
+ module Validation
6
+ def self.comment_error(context, message, element)
7
+ ValidationError.new(
8
+ context.messages.comment_error(message),
9
+ context.reporter.new(context).report_comments(element).snippet(),
10
+ Selections::select_comments(element)
11
+ )
12
+ end
13
+
14
+ def self.element_error(context, message, element)
15
+ ValidationError.new(
16
+ message,
17
+ context.reporter.new(context).report_element(element).snippet(),
18
+ Selections::select_element(element)
19
+ )
20
+ end
21
+
22
+ def self.key_error(context, message, element)
23
+ ValidationError.new(
24
+ context.messages.key_error(message),
25
+ context.reporter.new(context).report_line(element).snippet(),
26
+ Selections::select_key(element)
27
+ )
28
+ end
29
+
30
+ def self.missing_comment(context, element)
31
+ ValidationError.new(
32
+ context.messages::MISSING_COMMENT,
33
+ context.reporter.new(context).report_line(element).snippet(), # TODO: Question-tag an empty line before an element with missing comment
34
+ Selections::selection(element, :line, RANGE_BEGIN)
35
+ )
36
+ end
37
+
38
+ def self.missing_element(context, key, parent, message)
39
+ ValidationError.new(
40
+ key ? context.messages.send(message + '_with_key', key) : context.messages.const_get(message.upcase), # TODO: Solve the upcase rather through a different generated message type, e.g. method instead of constant
41
+ context.reporter.new(context).report_missing_element(parent).snippet(),
42
+ parent[:type] == :document ? Selections::DOCUMENT_BEGIN : Selections::selection(parent, :line, RANGE_END)
43
+ )
44
+ end
45
+
46
+ def self.missing_value(context, element)
47
+ selection = {}
48
+
49
+ if element[:type] == :field ||
50
+ element[:type] == :empty_element ||
51
+ element[:type] == :multiline_field_begin
52
+ message = context.messages.missing_field_value(element[:key])
53
+
54
+ if element[:ranges].has_key?(:template)
55
+ selection[:from] = Selections::cursor(element, :template, RANGE_END)
56
+ elsif element[:ranges].has_key?(:element_operator)
57
+ selection[:from] = Selections::cursor(element, :element_operator, RANGE_END)
58
+ else
59
+ selection[:from] = Selections::cursor(element, :line, RANGE_END)
60
+ end
61
+ elsif element[:type] == :fieldset_entry
62
+ message = context.messages.missing_fieldset_entry_value(element[:key])
63
+ selection[:from] = Selections::cursor(element, :entry_operator, RANGE_END)
64
+ elsif element[:type] == :list_item
65
+ message = context.messages.missing_list_item_value(element[:parent][:key])
66
+ selection[:from] = Selections::cursor(element, :item_operator, RANGE_END)
67
+ end
68
+
69
+ snippet = context.reporter.new(context).report_element(element).snippet()
70
+
71
+ if element[:type] == :field and element.has_key?(:continuations)
72
+ selection[:to] = Selections::cursor(element[:continuations].last, :line, RANGE_END)
73
+ else
74
+ selection[:to] = Selections::cursor(element, :line, RANGE_END)
75
+ end
76
+
77
+ ValidationError.new(message, snippet, selection)
78
+ end
79
+
80
+ def self.unexpected_element(context, message, element)
81
+ ValidationError.new(
82
+ message || context.messages::UNEXPECTED_ELEMENT,
83
+ context.reporter.new(context).report_element(element).snippet(),
84
+ Selections::select_element(element)
85
+ )
86
+ end
87
+
88
+ def self.unexpected_multiple_elements(context, key, elements, message)
89
+ ValidationError.new(
90
+ key ? context.messages.send(message + '_with_key', key) : context.messages.const_get(message.upcase), # TODO: Solve the upcase rather through a different generated message type, e.g. method instead of constant
91
+ context.reporter.new(context).report_elements(elements).snippet(),
92
+ Selections::select_element(elements[0])
93
+ )
94
+ end
95
+
96
+ def self.unexpected_element_type(context, key, section, message)
97
+ ValidationError.new(
98
+ key ? context.messages.send(message + '_with_key', key) : context.messages.const_get(message.upcase), # TODO: Solve the upcase rather through a different generated message type, e.g. method instead of constant
99
+ context.reporter.new(context).report_element(section).snippet(),
100
+ Selections::select_element(section)
101
+ )
102
+ end
103
+
104
+ def self.value_error(context, message, element)
105
+ if element.has_key?(:mirror)
106
+ snippet = context.reporter.new(context).report_line(element).snippet()
107
+ select = select_key(element)
108
+ elsif element[:type] == :multiline_field_begin
109
+ if element.has_key?(:lines)
110
+ snippet = context.reporter.new(context).report_multiline_value(element).snippet()
111
+ select = Selections::selection(element[:lines][0], :line, RANGE_BEGIN, element[:lines][-1], :line, RANGE_END)
112
+ else
113
+ snippet = context.reporter.new(context).report_element(element).snippet()
114
+ select = Selections::selection(element, :line, RANGE_END)
115
+ end
116
+ else
117
+ snippet = context.reporter.new(context).report_element(element).snippet()
118
+ select = {}
119
+
120
+ if element[:ranges].has_key?(:value)
121
+ select[:from] = Selections::cursor(element, :value, RANGE_BEGIN)
122
+ elsif element[:ranges].has_key?(:element_operator)
123
+ select[:from] = Selections::cursor(element, :element_operator, RANGE_END)
124
+ elsif element[:ranges].has_key?(:entry_operator)
125
+ select[:from] = Selections::cursor(element, :entry_operator, RANGE_END)
126
+ elsif element[:type] == :list_item
127
+ select[:from] = Selections::cursor(element, :item_operator, RANGE_END)
128
+ else
129
+ # TODO: Possibly never reached - think through state permutations
130
+ select[:from] = Selections::cursor(element, :line, RANGE_END)
131
+ end
132
+
133
+ if element.has_key?(:continuations)
134
+ select[:to] = Selections::cursor(element[:continuations][-1], :line, RANGE_END)
135
+ elsif element[:ranges].has_key?(:value)
136
+ select[:to] = Selections::cursor(element, :value, RANGE_END)
137
+ else
138
+ select[:to] = Selections::cursor(element, :line, RANGE_END)
139
+ end
140
+ end
141
+
142
+ ValidationError.new(context.messages.value_error(message), snippet, select)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Note: Study this file from the bottom up
4
+
5
+ # TODO: Try out possesive quantifiers (careful x{2,}+ does not work in ruby, only xx++ (!)) - benchmark?
6
+
7
+ module Enolib
8
+ module Grammar
9
+ OPTIONAL = /([^\n]+?)?/.source
10
+ REQUIRED = /(\S[^\n]*?)/.source
11
+
12
+ #
13
+ EMPTY = /()/.source
14
+ EMPTY_LINE_INDEX = 1
15
+
16
+ # | value
17
+ DIRECT_LINE_CONTINUATION = /(\|)[^\S\n]*#{OPTIONAL}/.source
18
+ DIRECT_LINE_CONTINUATION_OPERATOR_INDEX = 2
19
+ DIRECT_LINE_CONTINUATION_VALUE_INDEX = 3
20
+
21
+ # \ value
22
+ SPACED_LINE_CONTINUATION = /(\\)[^\S\n]*#{OPTIONAL}/.source
23
+ SPACED_LINE_CONTINUATION_OPERATOR_INDEX = 4
24
+ SPACED_LINE_CONTINUATION_VALUE_INDEX = 5
25
+
26
+ CONTINUATION = /#{DIRECT_LINE_CONTINUATION}|#{SPACED_LINE_CONTINUATION}/.source
27
+
28
+ # > Comment
29
+ COMMENT = /(>)[^\S\n]*#{OPTIONAL}/.source
30
+ COMMENT_OPERATOR_INDEX = 6
31
+ COMMENT_VALUE_INDEX = 7
32
+
33
+ # - value
34
+ LIST_ITEM = /(-)(?!-)[^\S\n]*#{OPTIONAL}/.source
35
+ LIST_ITEM_OPERATOR_INDEX = 8
36
+ LIST_ITEM_VALUE_INDEX = 9
37
+
38
+ # -- key
39
+ MULTILINE_FIELD = /(-{2,})(?!-)[^\S\n]*#{REQUIRED}/.source
40
+ MULTILINE_FIELD_OPERATOR_INDEX = 10
41
+ MULTILINE_FIELD_KEY_INDEX = 11
42
+
43
+ # #
44
+ SECTION_OPERATOR = /(#+)(?!#)/.source
45
+ SECTION_OPERATOR_INDEX = 12
46
+
47
+ # # key
48
+ SECTION_KEY_UNESCAPED = /([^\s`<][^<\n]*?)/.source
49
+ SECTION_KEY_UNESCAPED_INDEX = 13
50
+
51
+ # # `key`
52
+ SECTION_KEY_ESCAPE_BEGIN_OPERATOR_INDEX = 14
53
+ SECTION_KEY_ESCAPED = /(`+)(?!`)[^\S\n]*(\S[^\n]*?)[^\S\n]*(#{"\\#{SECTION_KEY_ESCAPE_BEGIN_OPERATOR_INDEX}"})/.source # TODO: Should this exclude the backreference inside the quotes? (as in ((?:(?!\1).)+) ) here and elsewhere (probably not because it's not greedy.?!
54
+ SECTION_KEY_ESCAPED_INDEX = 15
55
+ SECTION_KEY_ESCAPE_END_OPERATOR_INDEX = 16
56
+
57
+ # # key < template
58
+ # # `key` < template
59
+ SECTION_KEY = /(?:#{SECTION_KEY_UNESCAPED}|#{SECTION_KEY_ESCAPED})/.source
60
+ SECTION_TEMPLATE = /(?:(<(?!<)|<<)[^\S\n]*#{REQUIRED})?/.source
61
+ SECTION = /#{SECTION_OPERATOR}\s*#{SECTION_KEY}[^\S\n]*#{SECTION_TEMPLATE}/.source
62
+ SECTION_COPY_OPERATOR_INDEX = 17
63
+ SECTION_TEMPLATE_INDEX = 18
64
+
65
+ EARLY_DETERMINED = /#{CONTINUATION}|#{COMMENT}|#{LIST_ITEM}|#{MULTILINE_FIELD}|#{SECTION}/.source
66
+
67
+ # key
68
+ KEY_UNESCAPED = /([^\s>#\-`\\|:=<][^\n:=<]*?)/.source
69
+ KEY_UNESCAPED_INDEX = 19
70
+
71
+ # `key`
72
+ KEY_ESCAPE_BEGIN_OPERATOR_INDEX = 20
73
+ KEY_ESCAPED = /(`+)(?!`)[^\S\n]*(\S[^\n]*?)[^\S\n]*(#{"\\#{KEY_ESCAPE_BEGIN_OPERATOR_INDEX}"})/.source
74
+ KEY_ESCAPED_INDEX = 21
75
+ KEY_ESCAPE_END_OPERATOR_INDEX = 22
76
+
77
+ KEY = /(?:#{KEY_UNESCAPED}|#{KEY_ESCAPED})/.source
78
+
79
+ # :
80
+ # : value
81
+ ELEMENT_OR_FIELD = /(:)[^\S\n]*#{OPTIONAL}/.source
82
+ ELEMENT_OPERATOR_INDEX = 23
83
+ FIELD_VALUE_INDEX = 24
84
+
85
+ # =
86
+ # = value
87
+ FIELDSET_ENTRY = /(=)[^\S\n]*#{OPTIONAL}/.source
88
+ FIELDSET_ENTRY_OPERATOR_INDEX = 25
89
+ FIELDSET_ENTRY_VALUE_INDEX = 26
90
+
91
+ # < template
92
+ # << template
93
+ COPY = /(<(?!<)|<<)\s*#{REQUIRED}/.source
94
+ COPY_OPERATOR_INDEX = 27
95
+ TEMPLATE_INDEX = 28
96
+
97
+ LATE_DETERMINED = /#{KEY}\s*(?:#{ELEMENT_OR_FIELD}|#{FIELDSET_ENTRY}|#{COPY})/.source
98
+
99
+ NOT_EMPTY = /(?:#{EARLY_DETERMINED}|#{LATE_DETERMINED})/.source
100
+
101
+ REGEX = /[^\S\n]*(?:#{EMPTY}|#{NOT_EMPTY})[^\S\n]*(?=\n|$)/
102
+ end
103
+ end
@@ -0,0 +1,235 @@
1
+ RANGE_BEGIN = 0
2
+ RANGE_END = 1
3
+
4
+ def check_multiline_field_by_line(field, line)
5
+ return false if line < field[:line] ||
6
+ line > field[:end][:line]
7
+
8
+ if line == field[:line]
9
+ { element: field, instruction: field }
10
+ elsif line == field[:end][:line]
11
+ { element: field, instruction: field[:end] }
12
+ else
13
+ {
14
+ element: field,
15
+ instruction: field[:lines].find { |candidate| candidate[:line] == line }
16
+ }
17
+ end
18
+ end
19
+
20
+ def check_multiline_field_by_index(field, index)
21
+ return false if index < field[:ranges][:line][RANGE_BEGIN] ||
22
+ index > field[:end][:ranges][:line][RANGE_END]
23
+
24
+ if index <= field[:ranges][:line][RANGE_END]
25
+ { element: field, instruction: field }
26
+ elsif index >= field[:end][:ranges][:line][RANGE_BEGIN]
27
+ { element: field, instruction: field[:end] }
28
+ else
29
+ {
30
+ element: field,
31
+ instruction: field[:lines].find { |candidate| index <= candidate[:ranges][:line][RANGE_END] }
32
+ }
33
+ end
34
+ end
35
+
36
+ def check_field_by_line(field, line)
37
+ return false if line < field[:line]
38
+ return { element: field, instruction: field } if line == field[:line]
39
+ return false unless field.has_key?(:continuations) &&
40
+ line <= field[:continuations].last[:line]
41
+
42
+ field[:continuations].each do |continuation|
43
+ return { element: field, instruction: continuation } if line == continuation[:line]
44
+ return { element: field, instruction: nil } if line < continuation[:line]
45
+ end
46
+ end
47
+
48
+ def check_field_by_index(field, index)
49
+ return false if index < field[:ranges][:line][RANGE_BEGIN]
50
+ return { element: field, instruction: field } if index <= field[:ranges][:line][RANGE_END]
51
+ return false unless field.has_key?(:continuations) &&
52
+ index <= field[:continuations].last[:ranges][:line][RANGE_END]
53
+
54
+ field[:continuations].each do |continuation|
55
+ return { element: field, instruction: nil } if index < continuation[:ranges][:line][RANGE_BEGIN]
56
+ return { element: field, instruction: continuation } if index <= continuation[:ranges][:line][RANGE_END]
57
+ end
58
+ end
59
+
60
+ def check_fieldset_by_line(fieldset, line)
61
+ return false if line < fieldset[:line]
62
+ return { element: fieldset, instruction: fieldset } if line == fieldset[:line]
63
+ return false unless fieldset.has_key?(:entries) &&
64
+ line <= fieldset[:entries].last[:line]
65
+
66
+
67
+ fieldset[:entries].each do |entry|
68
+ return { element: entry, instruction: entry } if line == entry[:line]
69
+ return { element: fieldset, instruction: nil } if line < entry[:line]
70
+
71
+ match_in_entry = check_field_by_line(entry, line)
72
+
73
+ return match_in_entry if match_in_entry
74
+ end
75
+ end
76
+
77
+ def check_fieldset_by_index(fieldset, index)
78
+ return false if index < fieldset[:ranges][:line][RANGE_BEGIN]
79
+ return { element: fieldset, instruction: fieldset } if index <= fieldset[:ranges][:line][RANGE_END]
80
+ return false unless fieldset.has_key?(:entries) &&
81
+ index <= fieldset[:entries].last[:ranges][:line][RANGE_END]
82
+
83
+ fieldset[:entries].each do |entry|
84
+ return { element: fieldset, instruction: nil } if index < entry[:ranges][:line][RANGE_BEGIN]
85
+ return { element: entry, instruction: entry } if index <= entry[:ranges][:line][RANGE_END]
86
+
87
+ match_in_entry = check_field_by_index(entry, index)
88
+
89
+ return match_in_entry if match_in_entry
90
+ end
91
+ end
92
+
93
+ def check_list_by_line(list, line)
94
+ return false if line < list[:line]
95
+ return { element: list, instruction: list } if line == list[:line]
96
+ return false unless list.has_key?(:items) && line > list[:items].last[:line]
97
+
98
+ list[:items].each do |item|
99
+ return { element: item, instruction: item } if line == item[:line]
100
+ return { element: list, instruction: nil } if line < item[:line]
101
+
102
+ match_in_item = check_field_by_line(item, line)
103
+
104
+ return match_in_item if match_in_item
105
+ end
106
+ end
107
+
108
+ def check_list_by_index(list, index)
109
+ return false if index < list[:ranges][:line][RANGE_BEGIN]
110
+ return { element: list, instruction: list } if index <= list[:ranges][:line][RANGE_END]
111
+ return false unless list.has_key?(:items) &&
112
+ index > list[:items].last[:ranges][:line][RANGE_END]
113
+
114
+ list[:items].each do |item|
115
+ return { element: list, instruction: nil } if index < item[:ranges][:line][RANGE_BEGIN]
116
+ return { element: item, instruction: item } if index <= item[:ranges][:line][RANGE_END]
117
+
118
+ match_in_item = check_field_by_index(item, index)
119
+
120
+ return match_in_item if match_in_item
121
+ end
122
+ end
123
+
124
+ def check_in_section_by_line(section, line)
125
+ section[:elements].reverse_each do |element|
126
+ next if element[:line] > line
127
+
128
+ return { element: element, instruction: element } if element[:line] == line
129
+
130
+ case element[:type]
131
+ when :field
132
+ match_in_field = check_field_by_line(element, line)
133
+ return match_in_field if match_in_field
134
+ when :fieldset
135
+ match_in_fieldset = check_fieldset_by_line(element, line)
136
+ return match_in_fieldset if match_in_fieldset
137
+ when :list
138
+ match_in_list = check_list_by_line(element, line)
139
+ return match_in_list if match_in_list
140
+ when :multiline_field_begin
141
+ unless element.has_key?(:template)
142
+ match_in_multiline_field = check_multiline_field_by_line(element, line)
143
+ return match_in_multiline_field if match_in_multiline_field
144
+ end
145
+ when :section
146
+ return check_in_section_by_line(element, line)
147
+ end
148
+
149
+ break
150
+ end
151
+
152
+ { element: section, instruction: nil }
153
+ end
154
+
155
+ def check_in_section_by_index(section, index)
156
+ section[:elements].reverse_each do |element|
157
+ next if index < element[:ranges][:line][RANGE_BEGIN]
158
+
159
+ return { element: element, instruction: element } if index <= element[:ranges][:line][RANGE_END]
160
+
161
+ case element[:type]
162
+ when :field
163
+ match_in_field = check_field_by_index(element, index)
164
+ return match_in_field if match_in_field
165
+ when :fieldset
166
+ match_in_fieldset = check_fieldset_by_index(element, index)
167
+ return match_in_fieldset if match_in_fieldset
168
+ when :list
169
+ match_in_list = check_list_by_index(element, index)
170
+ return match_in_list if match_in_list
171
+ when :multiline_field_begin
172
+ unless element.has_key?(:template)
173
+ match_in_multiline_field = check_multiline_field_by_index(element, index)
174
+ return match_in_multiline_field if match_in_multiline_field
175
+ end
176
+ when :section
177
+ return check_in_section_by_index(element, index)
178
+ end
179
+
180
+ break
181
+ end
182
+
183
+ { element: section, instruction: nil }
184
+ end
185
+
186
+ module Enolib
187
+ def self.lookup(input, column: nil, index: nil, line: nil, **options)
188
+ context = Context.new(input, **options)
189
+
190
+ match = nil
191
+ if index
192
+ if index < 0 || index > context.input.length
193
+ raise IndexError.new("You are trying to look up an index (#{index}) outside of the document's index range (0-#{context.input.length})")
194
+ end
195
+
196
+ match = check_in_section_by_index(context.document, index)
197
+ else
198
+ if line < 0 || line >= context.line_count
199
+ raise IndexError.new("You are trying to look up a line (#{line}) outside of the document's line range (0-#{context.line_count - 1})")
200
+ end
201
+
202
+ match = check_in_section_by_line(context.document, line)
203
+ end
204
+
205
+ result = {
206
+ element: Element.new(context, match[:element]),
207
+ range: nil
208
+ }
209
+
210
+ instruction = match[:instruction]
211
+
212
+ unless instruction
213
+ instruction = context.meta.find { |candidate| candidate[:line] == line }
214
+ return result unless instruction
215
+ end
216
+
217
+ rightmost_match = instruction[:ranges][:line][0]
218
+
219
+ unless index
220
+ index = instruction[:ranges][:line][0] + column
221
+ end
222
+
223
+ instruction[:ranges].each do |type, range|
224
+ next if type == :line
225
+
226
+ if index >= range[RANGE_BEGIN] && index <= range[RANGE_END] && range[RANGE_BEGIN] >= rightmost_match
227
+ result[:range] = type
228
+ # TODO: Provide content of range too as convenience
229
+ rightmost_match = index
230
+ end
231
+ end
232
+
233
+ result
234
+ end
235
+ end