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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/lib/enolib.rb +42 -0
- data/lib/enolib/constants.rb +16 -0
- data/lib/enolib/context.rb +220 -0
- data/lib/enolib/elements/element.rb +42 -0
- data/lib/enolib/elements/element_base.rb +141 -0
- data/lib/enolib/elements/empty.rb +9 -0
- data/lib/enolib/elements/field.rb +63 -0
- data/lib/enolib/elements/fieldset.rb +151 -0
- data/lib/enolib/elements/fieldset_entry.rb +15 -0
- data/lib/enolib/elements/list.rb +107 -0
- data/lib/enolib/elements/list_item.rb +13 -0
- data/lib/enolib/elements/missing/missing_element_base.rb +44 -0
- data/lib/enolib/elements/missing/missing_empty.rb +13 -0
- data/lib/enolib/elements/missing/missing_field.rb +13 -0
- data/lib/enolib/elements/missing/missing_fieldset.rb +29 -0
- data/lib/enolib/elements/missing/missing_fieldset_entry.rb +13 -0
- data/lib/enolib/elements/missing/missing_list.rb +33 -0
- data/lib/enolib/elements/missing/missing_section.rb +105 -0
- data/lib/enolib/elements/missing/missing_section_element.rb +53 -0
- data/lib/enolib/elements/missing/missing_value_element_base.rb +21 -0
- data/lib/enolib/elements/section.rb +560 -0
- data/lib/enolib/elements/section_element.rb +141 -0
- data/lib/enolib/elements/value_element_base.rb +79 -0
- data/lib/enolib/errors.rb +25 -0
- data/lib/enolib/errors/parsing.rb +136 -0
- data/lib/enolib/errors/selections.rb +83 -0
- data/lib/enolib/errors/validation.rb +146 -0
- data/lib/enolib/grammar_regexp.rb +103 -0
- data/lib/enolib/lookup.rb +235 -0
- data/lib/enolib/messages/de.rb +79 -0
- data/lib/enolib/messages/en.rb +79 -0
- data/lib/enolib/messages/es.rb +79 -0
- data/lib/enolib/parse.rb +9 -0
- data/lib/enolib/parser.rb +708 -0
- data/lib/enolib/register.rb +24 -0
- data/lib/enolib/reporters/html_reporter.rb +115 -0
- data/lib/enolib/reporters/reporter.rb +258 -0
- data/lib/enolib/reporters/terminal_reporter.rb +107 -0
- data/lib/enolib/reporters/text_reporter.rb +46 -0
- 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
|