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,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