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,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class SectionElement < ElementBase
5
+ attr_reader :instruction # TODO: Revisit this hacky exposition
6
+
7
+ def _untouched
8
+ return @instruction unless instance_variable_defined?(:@yielded)
9
+
10
+ return @instruction if instance_variable_defined?(:@empty) && !@empty.instance_variable_defined?(:@touched)
11
+ return @instruction if instance_variable_defined?(:@field) && !@field.instance_variable_defined?(:@touched)
12
+ return @fieldset._untouched if instance_variable_defined?(:@fieldset)
13
+ return @list._untouched if instance_variable_defined?(:@list)
14
+ return @section._untouched if instance_variable_defined?(:@section)
15
+ end
16
+
17
+ def to_empty
18
+ unless instance_variable_defined?(:@empty)
19
+ if instance_variable_defined?(:@yielded)
20
+ raise TypeError.new("This element was already yielded as #{PRETTY_TYPES[@yielded]} and can't be yielded again as an empty.")
21
+ end
22
+
23
+ unless @instruction[:type] == :empty_element
24
+ # TODO: Below and in all implementations - why nil for key as second parameter?
25
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_empty')
26
+ end
27
+
28
+ @empty = Empty.new(@context, @instruction, @parent)
29
+ @yielded = :empty_element
30
+ end
31
+
32
+ @empty
33
+ end
34
+
35
+ def to_field
36
+ unless instance_variable_defined?(:@field)
37
+ if instance_variable_defined?(:@yielded)
38
+ raise TypeError.new("This element was already yielded as #{PRETTY_TYPES[@yielded]} and can't be yielded again as a field.")
39
+ end
40
+
41
+ unless @instruction[:type] == :field ||
42
+ @instruction[:type] == :multiline_field_begin ||
43
+ @instruction[:type] == :empty_element
44
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_field')
45
+ end
46
+
47
+ @field = Field.new(@context, @instruction, @parent)
48
+ @yielded = :field
49
+ end
50
+
51
+ @field
52
+ end
53
+
54
+ def to_fieldset
55
+ unless instance_variable_defined?(:@fieldset)
56
+ if instance_variable_defined?(:@yielded)
57
+ raise TypeError.new("This element was already yielded as #{PRETTY_TYPES[@yielded]} and can't be yielded again as a fieldset.")
58
+ end
59
+
60
+ unless @instruction[:type] == :fieldset || @instruction[:type] == :empty_element
61
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_fieldset')
62
+ end
63
+
64
+ @fieldset = Fieldset.new(@context, @instruction, @parent)
65
+ @yielded = :fieldset
66
+ end
67
+
68
+ @fieldset
69
+ end
70
+
71
+ def to_list
72
+ unless instance_variable_defined?(:@list)
73
+ if instance_variable_defined?(:@yielded)
74
+ raise TypeError.new("This element was already yielded as #{PRETTY_TYPES[@yielded]} and can't be yielded again as a list.")
75
+ end
76
+
77
+ unless @instruction[:type] == :list || @instruction[:type] == :empty_element
78
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_list')
79
+ end
80
+
81
+ @list = List.new(@context, @instruction, @parent)
82
+ @yielded = :list
83
+ end
84
+
85
+ @list
86
+ end
87
+
88
+ def to_section
89
+ unless instance_variable_defined?(:@section)
90
+ unless @instruction[:type] == :section
91
+ raise Errors::Validation.unexpected_element_type(@context, nil, @instruction, 'expected_section')
92
+ end
93
+
94
+ @section = Section.new(@context, @instruction, @parent)
95
+ @yielded = :section
96
+ end
97
+
98
+ @section
99
+ end
100
+
101
+ def touch
102
+ # TODO: Here and other implementations: This needs to touch anyway; possibly not so small implications
103
+ return unless instance_variable_defined?(:@yielded)
104
+
105
+ # TODO: Revisit setting touched on foreign instances
106
+ @empty.touched = true if instance_variable_defined?(:@empty)
107
+ @field.touched = true if instance_variable_defined?(:@field)
108
+ @fieldset.touch if instance_variable_defined?(:@fieldset)
109
+ @list.touch if instance_variable_defined?(:@list)
110
+ @section.touch if instance_variable_defined?(:@section)
111
+ end
112
+
113
+ def yields_empty?
114
+ @instruction[:type] == :empty_element
115
+ end
116
+
117
+ def yields_field?
118
+ @instruction[:type] == :field ||
119
+ @instruction[:type] == :multiline_field_begin ||
120
+ @instruction[:type] == :empty_element
121
+ end
122
+
123
+ def yields_fieldset?
124
+ @instruction[:type] == :fieldset ||
125
+ @instruction[:type] == :empty_element
126
+ end
127
+
128
+ def yields_list?
129
+ @instruction[:type] == :list ||
130
+ @instruction[:type] == :empty_element
131
+ end
132
+
133
+ def yields_section?
134
+ @instruction[:type] == :section
135
+ end
136
+
137
+ def to_s
138
+ "#<Enolib::SectionElement key=#{@instruction[:key]}>"
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class ValueElementBase < ElementBase
5
+ def optional_string_value
6
+ _value(required: false)
7
+ end
8
+
9
+ def optional_value(loader = nil)
10
+ loader = Proc.new if block_given?
11
+
12
+ unless loader
13
+ raise ArgumentError.new('A loader function must be provided')
14
+ end
15
+
16
+ _value(loader, required: false)
17
+ end
18
+
19
+ def required_string_value
20
+ _value(required: true)
21
+ end
22
+
23
+ def required_value(loader = nil)
24
+ loader = Proc.new if block_given?
25
+
26
+ unless loader
27
+ raise ArgumentError.new('A loader function must be provided')
28
+ end
29
+
30
+ _value(loader, required: true)
31
+ end
32
+
33
+ def value_error(message = nil)
34
+ if block_given?
35
+ message = yield(@context.value(@instruction))
36
+ elsif message.is_a?(Proc)
37
+ message = message.call(@context.value(@instruction))
38
+ end
39
+
40
+ unless message
41
+ raise ArgumentError.new('A message or message function must be provided')
42
+ end
43
+
44
+ Errors::Validation.value_error(@context, message, @instruction)
45
+ end
46
+
47
+ private
48
+
49
+ def print_value
50
+ value = @context.value(@instruction)
51
+
52
+ return 'nil' unless value
53
+
54
+ value = "#{value[0..10]}..." if value.length > 14
55
+
56
+ value.gsub("\n", '\n')
57
+ end
58
+
59
+ def _value(loader = nil, required:)
60
+ @touched = true
61
+
62
+ value = @context.value(@instruction)
63
+
64
+ if value
65
+ return value unless loader
66
+
67
+ begin
68
+ loader.call(value)
69
+ rescue => message
70
+ raise Errors::Validation.value_error(@context, message, @instruction)
71
+ end
72
+ else
73
+ return nil unless required
74
+
75
+ raise Errors::Validation.missing_value(@context, @instruction)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ class Error < StandardError
5
+ attr_reader :selection, :snippet, :text
6
+
7
+ def initialize(text, snippet, selection)
8
+ super("#{text}\n\n#{snippet}")
9
+
10
+ @selection = selection
11
+ @snippet = snippet
12
+ @text = text
13
+ end
14
+
15
+ def cursor
16
+ @selection[0]
17
+ end
18
+ end
19
+
20
+ class ParseError < Error
21
+ end
22
+
23
+ class ValidationError < Error
24
+ end
25
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ module Errors
5
+ module Parsing
6
+ UNTERMINATED_ESCAPED_KEY = /^\s*#*\s*(`+)(?!`)((?:(?!\1).)+)$/
7
+
8
+ def self.cyclic_dependency(context, instruction, instruction_chain)
9
+ first_occurrence = instruction_chain.find_index(instruction)
10
+ feedback_chain = instruction_chain[first_occurrence..-1]
11
+
12
+ if feedback_chain.last.has_key?(:template)
13
+ copy_instruction = feedback_chain.last
14
+ elsif feedback_chain.first.has_key?(:template) # TODO: Here and elsewhere, do we even need to check this? One has to be it, right?
15
+ copy_instruction = feedback_chain.first
16
+ end
17
+
18
+ reporter = context.reporter.new(context)
19
+
20
+ reporter.report_line(copy_instruction)
21
+
22
+ feedback_chain.each do |element|
23
+ reporter.indicate_line(element) if element != copy_instruction
24
+ end
25
+
26
+ ParseError.new(
27
+ context.messages::cyclic_dependency(copy_instruction[:line] + Enolib::HUMAN_INDEXING, copy_instruction[:template]),
28
+ reporter.snippet,
29
+ Selections::select_template(copy_instruction)
30
+ )
31
+ end
32
+
33
+ def self.invalid_line(context, instruction)
34
+ line = context.input[instruction[:ranges][:line][RANGE_BEGIN]..instruction[:ranges][:line][RANGE_END]]
35
+
36
+ match = UNTERMINATED_ESCAPED_KEY.match(line)
37
+ return unterminated_escaped_key(context, instruction, match.end(1)) if match
38
+
39
+ ParseError.new(
40
+ context.messages::invalid_line(instruction[:line] + Enolib::HUMAN_INDEXING),
41
+ context.reporter.new(context).report_line(instruction).snippet,
42
+ Selections::select_line(instruction)
43
+ )
44
+ end
45
+
46
+ def self.missing_element_for_continuation(context, continuation)
47
+ ParseError.new(
48
+ context.messages::missing_element_for_continuation(continuation[:line] + Enolib::HUMAN_INDEXING),
49
+ context.reporter.new(context).report_line(continuation).snippet,
50
+ Selections::select_line(continuation)
51
+ )
52
+ end
53
+
54
+ def self.missing_fieldset_for_fieldset_entry(context, entry)
55
+ ParseError.new(
56
+ context.messages::missing_fieldset_for_fieldset_entry(entry[:line] + Enolib::HUMAN_INDEXING),
57
+ context.reporter.new(context).report_line(entry).snippet,
58
+ Selections::select_line(entry)
59
+ )
60
+ end
61
+
62
+ def self.missing_list_for_list_item(context, item)
63
+ ParseError.new(
64
+ context.messages::missing_list_for_list_item(item[:line] + Enolib::HUMAN_INDEXING),
65
+ context.reporter.new(context).report_line(item).snippet,
66
+ Selections::select_line(item)
67
+ )
68
+ end
69
+
70
+ def self.non_section_element_not_found(context, copy)
71
+ ParseError.new(
72
+ context.messages::non_section_element_not_found(copy[:line] + Enolib::HUMAN_INDEXING, copy[:template]),
73
+ context.reporter.new(context).report_line(copy).snippet,
74
+ Selections::select_line(copy)
75
+ )
76
+ end
77
+
78
+ def self.section_hierarchy_layer_skip(context, section, super_section)
79
+ reporter = context.reporter.new(context).report_line(section)
80
+
81
+ reporter.indicate_line(super_section) if super_section[:type] != :document
82
+
83
+ ParseError.new(
84
+ context.messages::section_hierarchy_layer_skip(section[:line] + Enolib::HUMAN_INDEXING),
85
+ reporter.snippet,
86
+ Selections::select_line(section)
87
+ )
88
+ end
89
+
90
+ def self.section_not_found(context, copy)
91
+ ParseError.new(
92
+ context.messages::section_not_found(copy[:line] + Enolib::HUMAN_INDEXING, copy[:template]),
93
+ context.reporter.new(context).report_line(copy).snippet,
94
+ Selections::select_line(copy)
95
+ )
96
+ end
97
+
98
+ def self.unterminated_escaped_key(context, instruction, selection_column)
99
+ ParseError.new(
100
+ context.messages::unterminated_escaped_key(instruction[:line] + Enolib::HUMAN_INDEXING),
101
+ context.reporter.new(context).report_line(instruction).snippet,
102
+ {
103
+ from: {
104
+ column: selection_column,
105
+ index: instruction[:ranges][:line][RANGE_BEGIN] + selection_column,
106
+ line: instruction[:line]
107
+ },
108
+ to: Selections::cursor(instruction, :line, RANGE_END)
109
+ }
110
+ )
111
+ end
112
+
113
+ def self.two_or_more_templates_found(context, copy, first_template, second_template)
114
+ ParseError.new(
115
+ context.messages::two_or_more_templates_found(copy[:template]),
116
+ context.reporter.new(context).report_line(copy).question_line(first_template).question_line(second_template).snippet,
117
+ Selections::select_line(copy)
118
+ )
119
+ end
120
+
121
+ def self.unterminated_multiline_field(context, field)
122
+ reporter = context.reporter.new(context).report_element(field)
123
+
124
+ context.meta.each do |instruction|
125
+ reporter.indicate_line(instruction) if instruction[:line] > field[:line]
126
+ end
127
+
128
+ ParseError.new(
129
+ context.messages::unterminated_multiline_field(field[:key], field[:line] + Enolib::HUMAN_INDEXING),
130
+ reporter.snippet,
131
+ Selections::select_line(field)
132
+ )
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enolib
4
+ module Selections
5
+ RANGE_BEGIN = 0
6
+ RANGE_END = 1
7
+ DOCUMENT_BEGIN = {
8
+ from: { column: 0, index: 0, line: 0 },
9
+ to: { column: 0, index: 0, line: 0 }
10
+ }
11
+
12
+ def self.last_in(element)
13
+ if (element[:type] == :field ||
14
+ element[:type] == :list_item ||
15
+ element[:type] == :fieldset_entry) && element.has_key?(:continuations)
16
+ element[:continuations].last
17
+ elsif element[:type] == :list && element.has_key?(:items)
18
+ last_in(element[:items].last)
19
+ elsif element[:type] == :fieldset && element.has_key?(:entries)
20
+ last_in(element[:entries].last)
21
+ elsif element[:type] == :multiline_field_begin
22
+ element[:end]
23
+ elsif element[:type] == :section && !element[:elements].empty?
24
+ last_in(element[:elements].last)
25
+ else
26
+ element
27
+ end
28
+ end
29
+
30
+ def self.cursor(instruction, range, position)
31
+ index = instruction[:ranges][range][position]
32
+
33
+ {
34
+ column: index - instruction[:ranges][:line][RANGE_BEGIN],
35
+ index: index,
36
+ line: instruction[:line]
37
+ }
38
+ end
39
+
40
+ def self.selection(instruction, range, position, *to)
41
+ to_instruction = to.find { |argument| argument.is_a?(Hash) } || instruction
42
+ to_range = to.find { |argument| argument.is_a?(Symbol) } || range
43
+ to_position = to.find { |argument| argument.is_a?(Numeric) } || position
44
+
45
+ {
46
+ from: cursor(instruction, range, position),
47
+ to: cursor(to_instruction, to_range, to_position)
48
+ }
49
+ end
50
+
51
+ def self.select_comments(element)
52
+ comments = element[:comments]
53
+
54
+ if comments.length == 1
55
+ if comments.first.has_key?(:comment)
56
+ selection(comments.first, :comment, RANGE_BEGIN, RANGE_END)
57
+ else
58
+ selection(comments.first, :line, RANGE_BEGIN, RANGE_END)
59
+ end
60
+ elsif comments.length > 1
61
+ selection(comments.first, :line, RANGE_BEGIN, comments.last, :line, RANGE_END)
62
+ else
63
+ selection(element, :line, RANGE_BEGIN)
64
+ end
65
+ end
66
+
67
+ def self.select_element(element)
68
+ selection(element, :line, RANGE_BEGIN, last_in(element), :line, RANGE_END)
69
+ end
70
+
71
+ def self.select_key(element)
72
+ selection(element, :key, RANGE_BEGIN, RANGE_END)
73
+ end
74
+
75
+ def self.select_line(element)
76
+ selection(element, :line, RANGE_BEGIN, RANGE_END)
77
+ end
78
+
79
+ def self.select_template(element)
80
+ selection(element, :template, RANGE_BEGIN, RANGE_END)
81
+ end
82
+ end
83
+ end