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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Safe-guard against conflicting loader names (e.g. previous definition or native library function conflict)
4
+
5
+ module Enolib
6
+ def self.register(definitions)
7
+ definitions.each do |name, loader|
8
+ ElementBase.send(:define_method, "#{name}_key") { key(loader) }
9
+ ElementBase.send(:define_method, "optional_#{name}_comment") { optional_comment(loader) }
10
+ ElementBase.send(:define_method, "required_#{name}_comment") { required_comment(loader) }
11
+ ValueElementBase.send(:define_method, "optional_#{name}_value") { optional_value(loader) }
12
+ ValueElementBase.send(:define_method, "required_#{name}_value") { required_value(loader) }
13
+ List.send(:define_method, "optional_#{name}_values") { optional_values(loader) }
14
+ List.send(:define_method, "required_#{name}_values") { required_values(loader) }
15
+ MissingElementBase.send(:alias_method, "#{name}_key", :string_key)
16
+ MissingElementBase.send(:alias_method, "optional_#{name}_comment", :optional_string_comment)
17
+ MissingElementBase.send(:alias_method, "required_#{name}_comment", :required_string_comment)
18
+ MissingValueElementBase.send(:alias_method, "optional_#{name}_value", :optional_string_value)
19
+ MissingValueElementBase.send(:alias_method, "required_#{name}_value", :required_string_value)
20
+ MissingList.send(:alias_method, "optional_#{name}_values", :optional_string_values)
21
+ MissingList.send(:alias_method, "required_#{name}_values", :required_string_values)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class HtmlReporter < Reporter
5
+ def self.report(context, emphasized = [], marked = [])
6
+ emphasized = [emphasized] unless emphasized.is_a?(Array)
7
+ marked = [marked] unless marked.is_a?(Array)
8
+
9
+ content_header = context.messages::CONTENT_HEADER
10
+ gutter_header = context.messages::GUTTER_HEADER
11
+ omission = line('...', '...')
12
+
13
+ snippet = '<pre class="eno-report">'
14
+
15
+ snippet += "<div>#{context.sourceLabel}</div>" if context.source
16
+ snippet += line(gutter_header, content_header)
17
+
18
+ in_omission = false
19
+
20
+ context[:instructions].each do |instruction|
21
+ emphasize = emphasized.include?(instruction)
22
+ mark = marked.include?(instruction)
23
+
24
+ show = (emphasized + marked).any? do |marked_instruction|
25
+ instruction[:line] >= marked_instruction[:line] - 2 &&
26
+ instruction[:line] <= marked_instruction[:line] + 2
27
+ end
28
+
29
+ if show
30
+ classes = []
31
+
32
+ if emphasize
33
+ classes.push('eno-report-line-emphasized')
34
+ elsif mark
35
+ classes.push('eno-report-line-marked')
36
+ end
37
+
38
+ snippet += line(
39
+ (instruction[:line] + Enolib::HUMAN_INDEXING).to_s,
40
+ context[:input][instruction[:index], instruction[:length]],
41
+ classes
42
+ )
43
+
44
+ in_omission = false
45
+ elsif !in_omission
46
+ snippet += omission
47
+ in_omission = true
48
+ end
49
+ end
50
+
51
+ snippet += '</pre>'
52
+
53
+ snippet
54
+ end
55
+
56
+ private
57
+
58
+ def print_line(line, tag)
59
+ return markup('...', '...') if tag == :omission
60
+
61
+ number = (line + HUMAN_INDEXING).to_s
62
+ instruction = @index[line]
63
+
64
+ content = ''
65
+ if instruction
66
+ content = @context.input[instruction[:ranges][:line][RANGE_BEGIN]..instruction[:ranges][:line][RANGE_END]]
67
+ end
68
+
69
+ tag_class =
70
+ case tag
71
+ when :emphasize
72
+ 'eno-report-line-emphasized'
73
+ when :indicate
74
+ 'eno-report-line-indicated'
75
+ when :question
76
+ 'eno-report-line-questioned'
77
+ else
78
+ ''
79
+ end
80
+
81
+ markup(number, content, tag_class)
82
+ end
83
+
84
+ def escape(string)
85
+ string.gsub(/[&<>"'\/]/) { |c| HTML_ESCAPE[c] }
86
+ end
87
+
88
+ def markup(gutter, content, tag_class = '')
89
+ "<div class=\"eno-report-line #{tag_class}\">" +
90
+ "<div class=\"eno-report-gutter\">#{gutter.rjust(10)}</div>" +
91
+ "<div class=\"eno-report-content\">#{escape(content)}</div>" +
92
+ '</div>'
93
+ end
94
+
95
+ def print
96
+ columns_header = markup(@context.messages::GUTTER_HEADER, @context.messages::CONTENT_HEADER)
97
+ snippet = @snippet.each_with_index.map { |tag, line| print_line(line, tag) if tag }.compact.join("\n")
98
+
99
+ if @context.source
100
+ return "<div><div>#{@context.source}</div><pre class=\"eno-report\">#{columns_header}#{snippet}</pre></div>"
101
+ end
102
+
103
+ "<pre class=\"eno-report\">#{columns_header}#{snippet}</pre>"
104
+ end
105
+
106
+ HTML_ESCAPE = {
107
+ '&' => '&amp;',
108
+ '<' => '&lt;',
109
+ '>' => '&gt;',
110
+ '"' => '&quot;',
111
+ "'" => '&#39;',
112
+ '/' => '&#x2F;'
113
+ }.freeze
114
+ end
115
+ end
@@ -0,0 +1,258 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class Reporter
5
+ def initialize(context)
6
+ @context = context
7
+ @index = Array.new(@context.line_count)
8
+ @snippet = Array.new(@context.line_count)
9
+
10
+ build_index
11
+ end
12
+
13
+ def indicate_line(element)
14
+ @snippet[element[:line]] = :indicate
15
+ self
16
+ end
17
+
18
+ def question_line(element)
19
+ @snippet[element[:line]] = :question
20
+ self
21
+ end
22
+
23
+ def report_comments(element)
24
+ @snippet[element[:line]] = :indicate
25
+ element[:comments].each do |comment|
26
+ @snippet[comment[:line]] = :emphasize
27
+ end
28
+
29
+ self
30
+ end
31
+
32
+ def report_element(element)
33
+ @snippet[element[:line]] = :emphasize
34
+ tag_element(element, :indicate)
35
+
36
+ self
37
+ end
38
+
39
+ def report_elements(elements)
40
+ elements.each do |element|
41
+ @snippet[element[:line]] = :emphasize
42
+ tag_element(element, :indicate)
43
+ end
44
+
45
+ self
46
+ end
47
+
48
+ def report_line(instruction)
49
+ @snippet[instruction[:line]] = :emphasize
50
+
51
+ self
52
+ end
53
+
54
+ def report_multiline_value(element)
55
+ element[:lines].each do |line|
56
+ @snippet[line[:line]] = :emphasize
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+ def report_missing_element(parent)
63
+ @snippet[parent[:line]] = :indicate unless parent[:type] == :document
64
+
65
+ if parent[:type] == :section
66
+ tag_section(parent, :question, false)
67
+ else
68
+ tag_element(parent, :question)
69
+ end
70
+
71
+ self
72
+ end
73
+
74
+ def snippet
75
+ if @snippet.none?
76
+ (0...@snippet.length).each do |line|
77
+ @snippet[line] = :question
78
+ end
79
+ else
80
+ # TODO: Possibly better algorithm for this
81
+ @snippet.each_with_index do |tag, line|
82
+ next if tag
83
+
84
+ if line + 2 < @context.line_count && @snippet[line + 2] && @snippet[line + 2] != :display ||
85
+ line - 2 > 0 && @snippet[line - 2] && @snippet[line - 2] != :display ||
86
+ line + 1 < @context.line_count && @snippet[line + 1] && @snippet[line + 1] != :display ||
87
+ line - 1 > 0 && @snippet[line - 1] && @snippet[line - 1] != :display
88
+ @snippet[line] = :display
89
+ elsif line + 3 < @context.line_count && @snippet[line + 3] && @snippet[line + 3] != :display
90
+ @snippet[line] = :omission
91
+ end
92
+ end
93
+
94
+ @snippet[-1] = :omission unless @snippet[-1]
95
+ end
96
+
97
+ print
98
+ end
99
+
100
+ private
101
+
102
+ def index_comments(element)
103
+ return unless element.has_key?(:comments)
104
+
105
+ element[:comments].each do |comment|
106
+ @index[comment[:line]] = comment
107
+ end
108
+ end
109
+
110
+ def traverse(section)
111
+ section[:elements].each do |element|
112
+ index_comments(element)
113
+
114
+ @index[element[:line]] = element
115
+
116
+ if element[:type] == :section
117
+ traverse(element)
118
+ elsif element[:type] == :field
119
+ if element.has_key?(:continuations)
120
+ element[:continuations].each do |continuation|
121
+ @index[continuation[:line]] = continuation
122
+ end
123
+ end
124
+ elsif element[:type] == :multiline_field_begin
125
+ # Missing when reporting an unterminated multiline field
126
+ if element.has_key?(:end)
127
+ @index[element[:end][:line]] = element[:end]
128
+ end
129
+
130
+ if element.has_key?(:lines)
131
+ element[:lines].each do |line|
132
+ @index[line[:line]] = line
133
+ end
134
+ end
135
+ elsif element[:type] == :list
136
+ if element.has_key?(:items)
137
+ element[:items].each do |item|
138
+ index_comments(item)
139
+
140
+ @index[item[:line]] = item
141
+
142
+ if item.has_key?(:continuations)
143
+ item[:continuations].each do |continuation|
144
+ @index[continuation[:line]] = continuation
145
+ end
146
+ end
147
+ end
148
+ end
149
+ elsif element[:type] == :fieldset
150
+ if element.has_key?(:entries)
151
+ element[:entries].each do |entry|
152
+ index_comments(entry)
153
+
154
+ @index[entry[:line]] = entry
155
+
156
+ if entry.has_key?(:continuations)
157
+ entry[:continuations].each do |continuation|
158
+ @index[continuation[:line]] = continuation
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ def build_index
168
+ traverse(@context.document)
169
+
170
+ @context.meta.each do |instruction|
171
+ @index[instruction[:line]] = instruction
172
+ end
173
+ end
174
+
175
+ def tag_continuations(element, tag)
176
+ scan_line = element[:line] + 1
177
+
178
+ return scan_line unless element.has_key?(:continuations)
179
+
180
+ element[:continuations].each do |continuation|
181
+ while scan_line < continuation[:line]
182
+ @snippet[scan_line] = tag
183
+ scan_line += 1
184
+ end
185
+
186
+ @snippet[continuation[:line]] = tag
187
+ scan_line += 1
188
+ end
189
+
190
+ scan_line
191
+ end
192
+
193
+ def tag_continuables(element, collection, tag)
194
+ scan_line = element[:line] + 1
195
+
196
+ return scan_line unless element.has_key?(collection)
197
+
198
+ element[collection].each do |continuable|
199
+ while scan_line < continuable[:line]
200
+ @snippet[scan_line] = tag
201
+ scan_line += 1
202
+ end
203
+
204
+ @snippet[continuable[:line]] = tag
205
+
206
+ scan_line = tag_continuations(continuable, tag)
207
+ end
208
+
209
+ scan_line
210
+ end
211
+
212
+ def tag_element(element, tag)
213
+ if element[:type] == :field || element[:type] == :list_item || element[:type] == :fieldset_entry
214
+ return tag_continuations(element, tag)
215
+ elsif element[:type] == :list
216
+ return tag_continuables(element, :items, tag)
217
+ elsif element[:type] == :fieldset && element.has_key?(:entries)
218
+ return tag_continuables(element, :entries, tag)
219
+ elsif element[:type] == :multiline_field_begin
220
+ if element.has_key?(:lines)
221
+ element[:lines].each do |line|
222
+ @snippet[line[:line]] = tag
223
+ end
224
+ end
225
+
226
+ if element.has_key?(:end)
227
+ @snippet[element[:end][:line]] = tag
228
+ return element[:end][:line] + 1
229
+ elsif element.has_key?(:lines)
230
+ return element[:lines][-1][:line] + 1
231
+ else
232
+ return element[:line] + 1
233
+ end
234
+ elsif element[:type] == :section
235
+ return tag_section(element, tag)
236
+ end
237
+ end
238
+
239
+ def tag_section(section, tag, recursive=true)
240
+ scan_line = section[:line] + 1
241
+
242
+ section[:elements].each do |element|
243
+ while scan_line < element[:line]
244
+ @snippet[scan_line] = :indicate
245
+ scan_line += 1
246
+ end
247
+
248
+ break if element[:type] == :section && !recursive
249
+
250
+ @snippet[element[:line]] = :indicate
251
+
252
+ scan_line = tag_element(element, tag)
253
+ end
254
+
255
+ scan_line
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ RESET = "\x1b[0m"
4
+ BOLD = "\x1b[1m"
5
+ DIM = "\x1b[2m"
6
+
7
+ BLACK = "\x1b[30m"
8
+ BRIGHT_BLACK = "\x1b[90m"
9
+ WHITE = "\x1b[37m"
10
+ BRIGHT_WHITE = "\x1b[97m"
11
+
12
+ BRIGHT_BLACK_BACKGROUND = "\x1b[40m"
13
+ BRIGHT_RED_BACKGROUND = "\x1b[101m"
14
+ WHITE_BACKGROUND = "\x1b[47m"
15
+
16
+ INDICATORS = {
17
+ display: ' ',
18
+ emphasize: '>',
19
+ indicate: '*',
20
+ question: '?'
21
+ }
22
+
23
+ GUTTER_STYLE = {
24
+ display: BRIGHT_BLACK_BACKGROUND,
25
+ emphasize: BLACK + BRIGHT_RED_BACKGROUND,
26
+ indicate: BLACK + WHITE_BACKGROUND,
27
+ question: BLACK + WHITE_BACKGROUND
28
+ }
29
+
30
+ RANGE_STYLE = {
31
+ 'element_operator': WHITE,
32
+ 'escape_begin_operator': WHITE,
33
+ 'escape_end_operator': WHITE,
34
+ 'item_operator': WHITE,
35
+ 'entry_operator': WHITE,
36
+ 'section_operator': WHITE,
37
+ 'copy_operator': WHITE,
38
+ 'deepCopy_operator': WHITE,
39
+ 'multiline_field_operator': WHITE,
40
+ 'direct_line_continuation_operator': WHITE,
41
+ 'spaced_line_continuation_operator': WHITE,
42
+ 'key': BOLD + BRIGHT_WHITE,
43
+ 'template': BOLD + BRIGHT_WHITE,
44
+ 'value': DIM + WHITE
45
+ }
46
+
47
+ module Enolib
48
+ class TerminalReporter < Reporter
49
+ def initialize(context)
50
+ super(context)
51
+
52
+ highest_shown_line_number = @snippet.length
53
+
54
+ @snippet.reverse.each_with_index do |tag, index|
55
+ if tag && tag != :omission
56
+ highest_shown_line_number = index + 1
57
+ break
58
+ end
59
+ end
60
+
61
+ @line_number_padding = [4, highest_shown_line_number.to_s.length].max
62
+ @header = ''
63
+
64
+ if @context.source
65
+ @header += "#{BLACK + BRIGHT_RED_BACKGROUND} #{INDICATORS[EMPHASIZE]} #{' '.rjust(@line_number_padding)} #{RESET} #{BOLD}#{@context.source}#{RESET}\n"
66
+ end
67
+ end
68
+
69
+ def print_line(line, tag)
70
+ if tag == :omission
71
+ return "#{DIM + BRIGHT_BLACK_BACKGROUND}#{'...'.rjust(@line_number_padding + 2)} #{RESET}"
72
+ end
73
+
74
+ number = (line + HUMAN_INDEXING).to_s
75
+ instruction = @index[line]
76
+
77
+ content = ''
78
+ if instruction
79
+ if instruction[:type] == :comment or instruction[:type] == :unparsed
80
+ content = BRIGHT_BLACK + @context.input[instruction[:ranges][:line][RANGE_BEGIN]...instruction[:ranges][:line][RANGE_END]] + RESET
81
+ else
82
+ content = @context.input[instruction[:ranges][:line][RANGE_BEGIN]...instruction[:ranges][:line][RANGE_END]]
83
+
84
+ instruction[:ranges].sort_by { |type, range| range[0] }.reverse_each do |type, range|
85
+ next if type == :line
86
+
87
+ before = content[0...range[RANGE_BEGIN] - instruction[:ranges][:line][RANGE_BEGIN]]
88
+ after = content[range[RANGE_END] - instruction[:ranges][:line][RANGE_BEGIN]..-1]
89
+
90
+ # TODO: Here and everywhere: Why is RANGE_BEGIN without anything and like that even working? Check soundness
91
+ content = before + RANGE_STYLE[type] + @context.input[range[RANGE_BEGIN]...range[RANGE_END]] + RESET + after
92
+ end
93
+ end
94
+ end
95
+
96
+ "#{GUTTER_STYLE[tag]} #{INDICATORS[tag]} #{number.rjust(@line_number_padding)} #{RESET} #{content}"
97
+ end
98
+
99
+ private
100
+
101
+ def print
102
+ snippet = @snippet.each_with_index.map { |tag, line| print_line(line, tag) if tag }.compact.join("\n")
103
+
104
+ @header + snippet
105
+ end
106
+ end
107
+ end