enolib 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|