modl 0.3.15 → 0.3.16
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 +4 -4
- data/CHANGELOG.md +5 -0
- data/grammar_tests/base_tests.json +10 -1
- data/lib/modl/parser/array_processor.rb +120 -0
- data/lib/modl/parser/class_processor.rb +3 -3
- data/lib/modl/parser/file_importer.rb +1 -1
- data/lib/modl/parser/global_parse_context.rb +18 -0
- data/lib/modl/parser/interpreter.rb +2 -0
- data/lib/modl/parser/modl_array.rb +84 -0
- data/lib/modl/parser/parsed.rb +8 -0
- data/lib/modl/parser/ref_processor.rb +0 -1
- data/lib/modl/parser/version.rb +1 -1
- data/modl.gemspec +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: de1a3cee98476becc60a2c7dab825c65c30b47e5e8b017ae30c23b8505937e41
|
4
|
+
data.tar.gz: 85150763932b4136124d120017881baaf34c528cc989709ecc139eaf563bc868
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0276b480cfe85fa983b0deac3ca030f5a79d25e6676c781cf3ca8868162adfb30d837d29af4d26d935e41888819b57b9f83141d41d0311dd2fb8ab634e2799c5
|
7
|
+
data.tar.gz: d2e5547d0fd8225d3739d8d2c3754f40b7b77f0df6f5d415a42201f5dd696beb8f1d25e33c2ee18b635efc15bdc268df20ad97c7188c742825d498234312314f
|
data/CHANGELOG.md
CHANGED
@@ -1667,7 +1667,7 @@
|
|
1667
1667
|
},
|
1668
1668
|
{
|
1669
1669
|
"id": "176",
|
1670
|
-
"input": "_var=2;\n*L=\"http://
|
1670
|
+
"input": "_var=2;\n*L=\"http://modl.uk/tests/testing.txt!\";\nprint=%update_date\n",
|
1671
1671
|
"expected_output": "{\n \"print\": \"20180921 08:20 2\"\n}",
|
1672
1672
|
"tested_features": [
|
1673
1673
|
"object_ref",
|
@@ -3011,5 +3011,14 @@
|
|
3011
3011
|
"escape"
|
3012
3012
|
],
|
3013
3013
|
"minimised_modl": "_letters=abc;key=\"\\%letters\""
|
3014
|
+
},
|
3015
|
+
{
|
3016
|
+
"id": "316",
|
3017
|
+
"input": "*array(\n *id=p;\n *name=people;\n *of=person\n );\n\n *class(\n *id=n;\n *name=name\n );\n\n *class(\n *id=a;\n *name=age\n );\n\n *class(\n *id=person;\n *assign=[\n [n;a]\n ]\n );\n\n data(\n p[[John;18];[Jane;20]];\n person=[Fred;21]\n );\n p[[Mary;18];[Mungo;19];[Midge;20]];\n person=[Rod;23]\n",
|
3018
|
+
"expected_output": "{\n \"data\": {\n \"people\": [\n {\n \"name\": \"John\",\n \"age\": 18\n },\n {\n \"name\": \"Jane\",\n \"age\": 20\n }\n ],\n \"person\": {\n \"name\": \"Fred\",\n \"age\": 21\n }\n },\n \"people\": [\n {\n \"name\": \"Mary\",\n \"age\": 18\n },\n {\n \"name\": \"Mungo\",\n \"age\": 19\n },\n {\n \"name\": \"Midge\",\n \"age\": 20\n }\n ],\n \"person\": {\n \"name\": \"Rod\",\n \"age\": 23\n }\n}",
|
3019
|
+
"tested_features": [
|
3020
|
+
"*arrays"
|
3021
|
+
],
|
3022
|
+
"minimised_modl": "*array(*id=p;*name=people;*of=person);*class(*id=n;*name=name);*class(*id=a;*name=age);*class(*id=person;*assign=[[n;a]]);data(p[[John;18];[Jane;20]];person=[Fred;21]);p[[Mary;18];[Mungo;19];[Midge;20]];person=[Rod;23]"
|
3014
3023
|
}
|
3015
3024
|
]
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2019 NUM Technology Ltd
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
15
|
+
# all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
# THE SOFTWARE.
|
24
|
+
|
25
|
+
module MODL
|
26
|
+
module Parser
|
27
|
+
# This class handles the conversion of objects that refer to arrays into instances of those arrays.
|
28
|
+
# It works recursively since array usage can be nested.
|
29
|
+
class ArrayProcessor
|
30
|
+
# How deep can the class structure be?
|
31
|
+
MAX_RECURSION_DEPTH = 50
|
32
|
+
# global is a GlobalParseContext and obj is the extracted Array or Hash from MODL::Parser::Parsed.extract_json
|
33
|
+
def self.process(global, obj)
|
34
|
+
# Process each object in the array or just process the object if its a hash.
|
35
|
+
# Any other object is ignored.
|
36
|
+
raise StandardError, 'parameter "global" should be a GlobalParseContext' unless global.is_a?(GlobalParseContext)
|
37
|
+
|
38
|
+
if obj.is_a? Array
|
39
|
+
obj.each do |o|
|
40
|
+
process_obj global, o if o.is_a? Hash
|
41
|
+
end
|
42
|
+
elsif obj.is_a? Hash
|
43
|
+
process_obj global, obj
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Process the contents of the supplied array
|
50
|
+
def self.process_array(global, the_array, k, value)
|
51
|
+
|
52
|
+
return [k, value] if value.length.zero?
|
53
|
+
field_count = value[0].length
|
54
|
+
return [k, value] if field_count.zero?
|
55
|
+
|
56
|
+
# Get the class for the 'of' field of the array.
|
57
|
+
clazz = global.classs the_array.of
|
58
|
+
raise StandardError, 'No class with id or name =' + the_array.of + ' could be found.' if clazz.nil?
|
59
|
+
|
60
|
+
# Get the *assign array from the class and make sure we have an entry of size field_count
|
61
|
+
assignment_list = clazz.keylist_of_length field_count
|
62
|
+
raise StandardError, 'No assignment list of length ' + field_count.to_s + ' for class with id or name =' + the_array.of + ' could be found.' if assignment_list.nil?
|
63
|
+
|
64
|
+
result = []
|
65
|
+
|
66
|
+
value.each do |record|
|
67
|
+
object = {}
|
68
|
+
i = 0
|
69
|
+
assignment_list.each do |field|
|
70
|
+
field_class = global.classs field
|
71
|
+
if field_class
|
72
|
+
object[field_class.name_or_id] = record[i]
|
73
|
+
else
|
74
|
+
object[field] = record[i]
|
75
|
+
end
|
76
|
+
i += 1
|
77
|
+
end
|
78
|
+
result << object
|
79
|
+
end
|
80
|
+
|
81
|
+
return [the_array.name_or_id, result]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Replace the existing object with the new array instance and a new key
|
85
|
+
# We need to keep the same key order, hence this method below
|
86
|
+
def self.replace_value(obj, old_k, new_k, new_v)
|
87
|
+
tmp = obj.dup
|
88
|
+
obj.clear
|
89
|
+
tmp.keys.each do |tmpk|
|
90
|
+
tmpv = tmp[tmpk]
|
91
|
+
if tmpk == old_k
|
92
|
+
obj[new_k] = new_v
|
93
|
+
else
|
94
|
+
obj[tmpk] = tmpv
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.process_obj(global, obj)
|
100
|
+
obj.keys.each do |k|
|
101
|
+
value = obj[k]
|
102
|
+
# Does the key refer to an array that we have parsed or loaded?
|
103
|
+
the_array = global.arrays(k)
|
104
|
+
if the_array
|
105
|
+
# Yes so convert this value to an instance of that array
|
106
|
+
new_k, new_v = process_array global, the_array, k, value
|
107
|
+
# Replace the existing object with the new array instance and a new key
|
108
|
+
# We need to keep the same key order, hence this method below
|
109
|
+
replace_value obj, k, new_k, new_v
|
110
|
+
else
|
111
|
+
new_v = value
|
112
|
+
end
|
113
|
+
# Recurse into the value in case it has contents that also refer to arrays.
|
114
|
+
process global, new_v
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -57,7 +57,7 @@ module MODL
|
|
57
57
|
new_k, new_v = process_class global, k, value
|
58
58
|
# Replace the existing object with the new class instance and a new key
|
59
59
|
# We need to keep the same key order, hence this method below
|
60
|
-
|
60
|
+
replace_value(obj, k, new_k, new_v)
|
61
61
|
else
|
62
62
|
new_v = value
|
63
63
|
end
|
@@ -66,7 +66,7 @@ module MODL
|
|
66
66
|
end
|
67
67
|
end
|
68
68
|
|
69
|
-
def self.
|
69
|
+
def self.replace_value(obj, old_k, new_k, new_v)
|
70
70
|
tmp = obj.dup
|
71
71
|
obj.clear
|
72
72
|
tmp.keys.each do |tmpk|
|
@@ -252,7 +252,7 @@ module MODL
|
|
252
252
|
|
253
253
|
# Replace the value for this key if we've changed anything.
|
254
254
|
if new_value[new_k] != new_v
|
255
|
-
|
255
|
+
replace_value(new_value, new_k, new_k, new_v)
|
256
256
|
end
|
257
257
|
end
|
258
258
|
elsif new_value.is_a?(Array)
|
@@ -49,7 +49,7 @@ module MODL
|
|
49
49
|
force = file_name.end_with?('!')
|
50
50
|
file_name = Sutil.head(file_name) if force
|
51
51
|
file_name << '.modl' unless file_name.end_with?('.txt', '.modl')
|
52
|
-
file_name,
|
52
|
+
file_name, _new_val = RefProcessor.deref file_name, global if file_name.include?('%')
|
53
53
|
if force
|
54
54
|
# Don't use the cache if we're forcing a reload.
|
55
55
|
@cache.evict(file_name)
|
@@ -56,6 +56,9 @@ module MODL
|
|
56
56
|
@syntax_version = 1
|
57
57
|
@interpreter_syntax_version = 1
|
58
58
|
@loaded_files = []
|
59
|
+
# Arrays
|
60
|
+
@arrays_by_id = {}
|
61
|
+
@arrays_by_name = {}
|
59
62
|
end
|
60
63
|
|
61
64
|
def loaded_file(str)
|
@@ -103,6 +106,17 @@ module MODL
|
|
103
106
|
end
|
104
107
|
end
|
105
108
|
|
109
|
+
def arrays(key)
|
110
|
+
if key.is_a? String
|
111
|
+
result = @arrays_by_id[key]
|
112
|
+
result = @arrays_by_name[key] if result.nil?
|
113
|
+
result
|
114
|
+
elsif key.is_a? MODLArray
|
115
|
+
@arrays_by_id[key.id] = key if key.id
|
116
|
+
@arrays_by_name[key.name] = key if key.name
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
106
120
|
def merge_pairs(other)
|
107
121
|
@pairs.merge!(other.all_pairs)
|
108
122
|
end
|
@@ -125,6 +139,10 @@ module MODL
|
|
125
139
|
@classes_by_id.keys.include?(key) || @classes_by_name.keys.include?(key)
|
126
140
|
end
|
127
141
|
|
142
|
+
def has_array?(key)
|
143
|
+
@arrays_by_id.keys.include?(key) || @arrays_by_name.keys.include?(key)
|
144
|
+
end
|
145
|
+
|
128
146
|
def has_user_method?(key)
|
129
147
|
@methods_hash.keys.include?(key)
|
130
148
|
end
|
@@ -27,6 +27,7 @@ require 'modl/parser/MODLParserVisitor'
|
|
27
27
|
require 'modl/parser/MODLLexer'
|
28
28
|
require 'modl/parser/MODLParser'
|
29
29
|
require 'modl/parser/class_processor'
|
30
|
+
require 'modl/parser/array_processor'
|
30
31
|
require 'modl/parser/orphan_handler'
|
31
32
|
require 'modl/parser/parser'
|
32
33
|
require 'json'
|
@@ -68,6 +69,7 @@ module MODL
|
|
68
69
|
|
69
70
|
# Process any class definitions used by the MODL file.
|
70
71
|
MODL::Parser::ClassProcessor.process(parsed.global, interpreted)
|
72
|
+
MODL::Parser::ArrayProcessor.process(parsed.global, interpreted)
|
71
73
|
MODL::Parser::InstructionProcessor.process(parsed.global, interpreted)
|
72
74
|
# If the result is a simple string then just return it.
|
73
75
|
interpreted
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# The MIT License (MIT)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2019 NUM Technology Ltd
|
6
|
+
#
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
12
|
+
# furnished to do so, subject to the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be included in
|
15
|
+
# all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
23
|
+
# THE SOFTWARE.
|
24
|
+
|
25
|
+
module MODL
|
26
|
+
module Parser
|
27
|
+
# Represents a *array defined, or loaded by, a MODL document.
|
28
|
+
class MODLArray
|
29
|
+
attr_accessor :id
|
30
|
+
attr_accessor :name
|
31
|
+
attr_accessor :of
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@content = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def name_or_id
|
38
|
+
@name.nil? ? @id : @name
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Extract an array from a ParsedPair object
|
43
|
+
class ArrayExtractor
|
44
|
+
def self.extract(pair, global)
|
45
|
+
return unless pair.type == 'array'
|
46
|
+
|
47
|
+
the_array = MODLArray.new
|
48
|
+
map = pair.map if pair.map
|
49
|
+
map = pair.valueItem&.value&.map if pair.valueItem&.value&.map
|
50
|
+
|
51
|
+
map.mapItems.each do |item|
|
52
|
+
next unless item&.pair&.type
|
53
|
+
|
54
|
+
case item&.pair&.type
|
55
|
+
when 'id'
|
56
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
57
|
+
the_array.id = str_value
|
58
|
+
when 'name'
|
59
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
60
|
+
the_array.name = str_value
|
61
|
+
when 'of'
|
62
|
+
str_value = item.pair.valueItem.value.primitive.string.string
|
63
|
+
the_array.of = str_value
|
64
|
+
else
|
65
|
+
# Ignore
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
raise InterpreterError, 'Missing id for *array' if the_array.id.nil?
|
70
|
+
|
71
|
+
# Make sure the array name isn't redefining an existing array
|
72
|
+
if !global.has_array?(the_array.id) && !global.has_array?(the_array.name)
|
73
|
+
|
74
|
+
# store the arrays by id and name to make them easier to find later
|
75
|
+
global.arrays(the_array)
|
76
|
+
else
|
77
|
+
id = the_array.id.nil? ? 'undefined' : the_array.id
|
78
|
+
name = the_array.name.nil? ? 'undefined' : the_array.name
|
79
|
+
raise InterpreterError, '*Array name or id already defined - cannot redefine: ' + id + ', ' + name
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/modl/parser/parsed.rb
CHANGED
@@ -30,6 +30,7 @@ require 'modl/parser/file_importer'
|
|
30
30
|
require 'antlr4/runtime/parse_cancellation_exception'
|
31
31
|
require 'modl/parser/sutil'
|
32
32
|
require 'modl/parser/modl_class'
|
33
|
+
require 'modl/parser/modl_array'
|
33
34
|
require 'modl/parser/modl_method'
|
34
35
|
require 'modl/parser/modl_index'
|
35
36
|
require 'modl/parser/modl_keylist'
|
@@ -211,6 +212,8 @@ module MODL
|
|
211
212
|
@final = false
|
212
213
|
@file_importer = FileImporter.instance
|
213
214
|
@loaded = false
|
215
|
+
@array = nil
|
216
|
+
@map = nil
|
214
217
|
end
|
215
218
|
|
216
219
|
def find_property(key)
|
@@ -284,6 +287,7 @@ module MODL
|
|
284
287
|
return if @type == 'import'
|
285
288
|
return if @type == 'allow'
|
286
289
|
return if @type == 'expect'
|
290
|
+
return if @type == 'array'
|
287
291
|
|
288
292
|
{@key => @text}
|
289
293
|
end
|
@@ -336,6 +340,8 @@ module MODL
|
|
336
340
|
case @type
|
337
341
|
when 'class'
|
338
342
|
ClassExtractor.extract(self, @global)
|
343
|
+
when 'array'
|
344
|
+
ArrayExtractor.extract(self, @global)
|
339
345
|
when 'id'
|
340
346
|
extract_value
|
341
347
|
when 'name'
|
@@ -478,6 +484,8 @@ module MODL
|
|
478
484
|
@type = 'hidden' if @key.start_with? '_'
|
479
485
|
@type = 'allow' if @key.downcase == '*allow'
|
480
486
|
@type = 'expect' if @key.downcase == '*expect'
|
487
|
+
@type = 'of' if @key.downcase == '*of'
|
488
|
+
@type = 'array' if @key.downcase == '*array'
|
481
489
|
end
|
482
490
|
end
|
483
491
|
|
data/lib/modl/parser/version.rb
CHANGED
data/modl.gemspec
CHANGED
@@ -25,6 +25,6 @@ Gem::Specification.new do |spec|
|
|
25
25
|
|
26
26
|
spec.add_development_dependency 'rake', '~> 10.0'
|
27
27
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
-
spec.add_runtime_dependency 'antlr4-runtime', '= 0.2.
|
28
|
+
spec.add_runtime_dependency 'antlr4-runtime', '= 0.2.9'
|
29
29
|
spec.add_runtime_dependency 'punycode4r', '>= 0.2.0'
|
30
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: modl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Walmsley
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -44,14 +44,14 @@ dependencies:
|
|
44
44
|
requirements:
|
45
45
|
- - '='
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.2.
|
47
|
+
version: 0.2.9
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - '='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.2.
|
54
|
+
version: 0.2.9
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: punycode4r
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -113,12 +113,14 @@ files:
|
|
113
113
|
- lib/modl/parser/MODLParserBaseVisitor.rb
|
114
114
|
- lib/modl/parser/MODLParserListener.rb
|
115
115
|
- lib/modl/parser/MODLParserVisitor.rb
|
116
|
+
- lib/modl/parser/array_processor.rb
|
116
117
|
- lib/modl/parser/class_processor.rb
|
117
118
|
- lib/modl/parser/evaluator.rb
|
118
119
|
- lib/modl/parser/file_importer.rb
|
119
120
|
- lib/modl/parser/global_parse_context.rb
|
120
121
|
- lib/modl/parser/instruction_processor.rb
|
121
122
|
- lib/modl/parser/interpreter.rb
|
123
|
+
- lib/modl/parser/modl_array.rb
|
122
124
|
- lib/modl/parser/modl_class.rb
|
123
125
|
- lib/modl/parser/modl_index.rb
|
124
126
|
- lib/modl/parser/modl_keylist.rb
|