enolib 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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