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