modl 0.0.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +15 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +4 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +52 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/grammar_tests/1.modl +1 -0
- data/grammar_tests/2.modl +1 -0
- data/grammar_tests/3.modl +1 -0
- data/grammar_tests/a.modl +1 -0
- data/grammar_tests/b.modl +1 -0
- data/grammar_tests/base_tests.json +996 -0
- data/grammar_tests/c.modl +1 -0
- data/grammar_tests/demo_config.modl +9 -0
- data/grammar_tests/error_tests.json +70 -0
- data/grammar_tests/import_config.modl +9 -0
- data/grammar_tests/test_import_dir/nested_import1.txt +1 -0
- data/grammar_tests/test_import_dir/nested_import2.txt +1 -0
- data/grammar_tests/test_import_dir/nested_import3.txt +1 -0
- data/grammar_tests/test_import_dir/test_import.txt +9 -0
- data/lib/modl/interpreter.rb +10 -0
- data/lib/modl/parser/MODLLexer.interp +136 -0
- data/lib/modl/parser/MODLLexer.rb +324 -0
- data/lib/modl/parser/MODLLexer.tokens +41 -0
- data/lib/modl/parser/MODLParser.interp +95 -0
- data/lib/modl/parser/MODLParser.rb +2504 -0
- data/lib/modl/parser/MODLParser.tokens +41 -0
- data/lib/modl/parser/MODLParserBaseListener.rb +164 -0
- data/lib/modl/parser/MODLParserBaseVisitor.rb +107 -0
- data/lib/modl/parser/MODLParserListener.rb +151 -0
- data/lib/modl/parser/MODLParserVisitor.rb +56 -0
- data/lib/modl/parser/class_processor.rb +159 -0
- data/lib/modl/parser/evaluator.rb +164 -0
- data/lib/modl/parser/file_importer.rb +64 -0
- data/lib/modl/parser/global_parse_context.rb +249 -0
- data/lib/modl/parser/instruction_processor.rb +58 -0
- data/lib/modl/parser/interpreter.rb +38 -0
- data/lib/modl/parser/modl_class.rb +102 -0
- data/lib/modl/parser/modl_index.rb +30 -0
- data/lib/modl/parser/modl_keylist.rb +43 -0
- data/lib/modl/parser/modl_method.rb +132 -0
- data/lib/modl/parser/object_cache.rb +54 -0
- data/lib/modl/parser/parsed.rb +1410 -0
- data/lib/modl/parser/parser.rb +42 -0
- data/lib/modl/parser/ref_processor.rb +139 -0
- data/lib/modl/parser/substitutions.rb +67 -0
- data/lib/modl/parser/sutil.rb +78 -0
- data/lib/modl/parser/throwing_error_listener.rb +20 -0
- data/lib/modl/parser/version.rb +5 -0
- data/modl.gemspec +32 -0
- metadata +138 -11
- data/lib/modl.rb +0 -5
@@ -0,0 +1,43 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Extracts an index definition from a ParsedPair
|
4
|
+
class KeylistExtractor
|
5
|
+
def self.extract(pair, item)
|
6
|
+
# the item must be an array of arrays
|
7
|
+
pair.key_lists = []
|
8
|
+
last_keylist_len = 0
|
9
|
+
if item.is_a?(Parsed::ParsedValueItem) && item.value.is_a?(Parsed::ParsedValue) && item.value.array
|
10
|
+
item.value.array.abstractArrayItems.each do |avi|
|
11
|
+
key_list = []
|
12
|
+
avi.arrayValueItem.array.abstractArrayItems.each do |key|
|
13
|
+
key_list << key.arrayValueItem.primitive.string.string if key.arrayValueItem.primitive.string
|
14
|
+
key_list << key.arrayValueItem.primitive.number.num if key.arrayValueItem.primitive.number
|
15
|
+
end
|
16
|
+
if key_list.length > last_keylist_len
|
17
|
+
last_keylist_len = key_list.length
|
18
|
+
else
|
19
|
+
raise InterpreterError, 'Error: Key lists in *assign are not in ascending order of list length: ' + key_list.to_s
|
20
|
+
end
|
21
|
+
pair.key_lists << key_list
|
22
|
+
end
|
23
|
+
elsif item.is_a?(Parsed::ParsedArray)
|
24
|
+
item.abstractArrayItems.each do |avi|
|
25
|
+
key_list = []
|
26
|
+
avi.arrayValueItem.array.abstractArrayItems.each do |key|
|
27
|
+
key_list << key.arrayValueItem.primitive.string.string if key.arrayValueItem.primitive.string
|
28
|
+
key_list << key.arrayValueItem.primitive.number.num if key.arrayValueItem.primitive.number
|
29
|
+
end
|
30
|
+
if key_list.length > last_keylist_len
|
31
|
+
last_keylist_len = key_list.length
|
32
|
+
else
|
33
|
+
raise InterpreterError, 'Error: Key lists in *assign are not in ascending order of list length.'
|
34
|
+
end
|
35
|
+
pair.key_lists << key_list
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise InterpreterError, 'Array of arrays expected for: ' + pair.key
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module Modl
|
2
|
+
module Parser
|
3
|
+
# Represents a *method defined by a MODL document.
|
4
|
+
class MODLMethod
|
5
|
+
attr_accessor :id
|
6
|
+
attr_accessor :name
|
7
|
+
attr_accessor :transform
|
8
|
+
|
9
|
+
def name_or_id
|
10
|
+
@name.nil? ? @id : @name
|
11
|
+
end
|
12
|
+
|
13
|
+
# There is a user-defined method transform to run on the str
|
14
|
+
def run(str)
|
15
|
+
# Consume the elements of the transform spec until there are none left.
|
16
|
+
transform = @transform
|
17
|
+
while transform && transform.length > 0
|
18
|
+
if transform.start_with? 'replace'
|
19
|
+
close_bracket = transform.index(')')
|
20
|
+
m = Sutil.head(transform, close_bracket + 1).sub!('replace', 'r')
|
21
|
+
str = StandardMethods.run_method(m, str)
|
22
|
+
# Consume the subst clause
|
23
|
+
close_bracket = transform.index(')')
|
24
|
+
transform = Sutil.tail(transform, close_bracket + 2)
|
25
|
+
elsif transform.start_with? 'trim'
|
26
|
+
close_bracket = transform.index(')')
|
27
|
+
m = Sutil.head(transform, close_bracket + 1).sub!('trim', 't')
|
28
|
+
str = StandardMethods.run_method(m, str)
|
29
|
+
# Consume the trunc clause
|
30
|
+
close_bracket = transform.index(')')
|
31
|
+
transform = Sutil.tail(transform, close_bracket + 2)
|
32
|
+
elsif transform.start_with? 'initcap'
|
33
|
+
str = str.split.map(&:capitalize) * ' '
|
34
|
+
transform = Sutil.tail(transform, 8)
|
35
|
+
elsif transform.start_with? 'upcase'
|
36
|
+
raise InterpreterError, 'NOT IMPLEMENTED'
|
37
|
+
elsif transform.start_with? 'downcase'
|
38
|
+
raise InterpreterError, 'NOT IMPLEMENTED'
|
39
|
+
elsif transform.start_with? 'sentence'
|
40
|
+
raise InterpreterError, 'NOT IMPLEMENTED'
|
41
|
+
elsif transform.start_with? 'urlencode'
|
42
|
+
raise InterpreterError, 'NOT IMPLEMENTED'
|
43
|
+
else
|
44
|
+
raise InterpreterError, 'NOT IMPLEMENTED'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
str
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# Extracts a method definition from a ParsedPair
|
53
|
+
class MethodExtractor
|
54
|
+
def self.extract(pair, global)
|
55
|
+
return unless pair.type == 'method'
|
56
|
+
|
57
|
+
mthd = MODLMethod.new
|
58
|
+
map = pair.map if pair.map
|
59
|
+
map = pair.valueItem&.value&.map if pair.valueItem&.value&.map
|
60
|
+
|
61
|
+
map.mapItems.each do |item|
|
62
|
+
next unless item&.pair&.type
|
63
|
+
|
64
|
+
case item&.pair&.type
|
65
|
+
when 'id'
|
66
|
+
mthd.id = item.pair.valueItem.value.primitive.string.string
|
67
|
+
when 'transform'
|
68
|
+
mthd.transform = item.pair.valueItem.value.primitive.string.string
|
69
|
+
when 'name'
|
70
|
+
mthd.name = item.pair.valueItem.value.primitive.string.string
|
71
|
+
else
|
72
|
+
raise InterpreterError, 'Invalid *method - only *id, *name, and *transform fields expected'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
raise InterpreterError, 'Missing id for method' if mthd.id.nil?
|
77
|
+
raise InterpreterError, 'Missing name for method' if mthd.name.nil?
|
78
|
+
raise InterpreterError, 'Duplicate method name: ' + mthd.name if global.has_user_method?(mthd.name)
|
79
|
+
raise InterpreterError, 'Duplicate method id: ' + mthd.id if global.has_user_method?(mthd.id)
|
80
|
+
|
81
|
+
# store the methods by id and name to make them easier to find later
|
82
|
+
global.user_method_id(mthd.id, mthd)
|
83
|
+
global.user_method(mthd.name, mthd)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class StandardMethods
|
88
|
+
def self.run_method(m, str)
|
89
|
+
case m[0]
|
90
|
+
when 'u'
|
91
|
+
str.upcase
|
92
|
+
when 'd'
|
93
|
+
str.downcase
|
94
|
+
when 'i'
|
95
|
+
str.split.map(&:capitalize) * ' '
|
96
|
+
when 's'
|
97
|
+
split = str.split
|
98
|
+
split[0].capitalize!
|
99
|
+
split.join(' ')
|
100
|
+
when 'e'
|
101
|
+
CGI.escape(str)
|
102
|
+
when 'r'
|
103
|
+
s1, s2 = get_subst_parts m
|
104
|
+
str.sub(s1, s2)
|
105
|
+
when 't'
|
106
|
+
s1 = extract_params m
|
107
|
+
i = str.index(s1)
|
108
|
+
Sutil.head(str, i)
|
109
|
+
when 'p'
|
110
|
+
Punycode.decode(str)
|
111
|
+
else
|
112
|
+
str + '.' + m
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Extract the method parameters
|
117
|
+
def self.get_subst_parts(s)
|
118
|
+
# should be of the form .r(s1,s2)
|
119
|
+
result = extract_params(s).split(',')
|
120
|
+
result[0] = '' if result.length.zero? || result[0].nil?
|
121
|
+
result[1] = '' if result.length == 1 || result[1].nil?
|
122
|
+
result
|
123
|
+
end
|
124
|
+
|
125
|
+
# Extract the method parameter
|
126
|
+
def self.extract_params(str)
|
127
|
+
# should be of the form .r(s1,s2)
|
128
|
+
Sutil.between(str, '(', ')')
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
|
2
|
+
module Modl
|
3
|
+
module Parser
|
4
|
+
# Store any files for up to 1 hour by default.
|
5
|
+
class ObjectCache
|
6
|
+
# A cache record to keep track of the time since an object was last cached.
|
7
|
+
class CacheEntry
|
8
|
+
TTL_ONE_HOUR = 3_600 # seconds
|
9
|
+
|
10
|
+
attr_reader :object
|
11
|
+
|
12
|
+
# Initialiase the CacheEntry with an object and an optional ttl in seconds (default 1 hour)
|
13
|
+
def initialize(object, ttl = nil)
|
14
|
+
ttl = TTL_ONE_HOUR if ttl.nil?
|
15
|
+
@object = object
|
16
|
+
@expiry_time = Time.now + ttl
|
17
|
+
end
|
18
|
+
|
19
|
+
# Check whether the CacheEntry is live
|
20
|
+
def expired?
|
21
|
+
@expiry_time < Time.now
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set up and empty cache.
|
26
|
+
def initialize
|
27
|
+
@cache = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
# Cache an object with the given key and optional ttl in seconds (default 1 hour)
|
31
|
+
def put(key, object, ttl = nil)
|
32
|
+
@cache[key] = CacheEntry.new(object, ttl) unless key.nil? || object.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Evict a cache entry
|
36
|
+
def evict(key)
|
37
|
+
@cache.delete(key) unless key.nil?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the object with the given key if one exists and has not expired.
|
41
|
+
def get(key)
|
42
|
+
# Return nothing if not in the cache or it has expired.
|
43
|
+
return if key.nil?
|
44
|
+
|
45
|
+
entry = @cache[key]
|
46
|
+
return unless entry
|
47
|
+
return if entry.expired?
|
48
|
+
|
49
|
+
# Otherwise return the cached object.
|
50
|
+
entry.object
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|