modl 0.0.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +15 -0
  3. data/.idea/vcs.xml +6 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +7 -0
  7. data/CHANGELOG.md +4 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +9 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +52 -0
  12. data/Rakefile +6 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/grammar_tests/1.modl +1 -0
  16. data/grammar_tests/2.modl +1 -0
  17. data/grammar_tests/3.modl +1 -0
  18. data/grammar_tests/a.modl +1 -0
  19. data/grammar_tests/b.modl +1 -0
  20. data/grammar_tests/base_tests.json +996 -0
  21. data/grammar_tests/c.modl +1 -0
  22. data/grammar_tests/demo_config.modl +9 -0
  23. data/grammar_tests/error_tests.json +70 -0
  24. data/grammar_tests/import_config.modl +9 -0
  25. data/grammar_tests/test_import_dir/nested_import1.txt +1 -0
  26. data/grammar_tests/test_import_dir/nested_import2.txt +1 -0
  27. data/grammar_tests/test_import_dir/nested_import3.txt +1 -0
  28. data/grammar_tests/test_import_dir/test_import.txt +9 -0
  29. data/lib/modl/interpreter.rb +10 -0
  30. data/lib/modl/parser/MODLLexer.interp +136 -0
  31. data/lib/modl/parser/MODLLexer.rb +324 -0
  32. data/lib/modl/parser/MODLLexer.tokens +41 -0
  33. data/lib/modl/parser/MODLParser.interp +95 -0
  34. data/lib/modl/parser/MODLParser.rb +2504 -0
  35. data/lib/modl/parser/MODLParser.tokens +41 -0
  36. data/lib/modl/parser/MODLParserBaseListener.rb +164 -0
  37. data/lib/modl/parser/MODLParserBaseVisitor.rb +107 -0
  38. data/lib/modl/parser/MODLParserListener.rb +151 -0
  39. data/lib/modl/parser/MODLParserVisitor.rb +56 -0
  40. data/lib/modl/parser/class_processor.rb +159 -0
  41. data/lib/modl/parser/evaluator.rb +164 -0
  42. data/lib/modl/parser/file_importer.rb +64 -0
  43. data/lib/modl/parser/global_parse_context.rb +249 -0
  44. data/lib/modl/parser/instruction_processor.rb +58 -0
  45. data/lib/modl/parser/interpreter.rb +38 -0
  46. data/lib/modl/parser/modl_class.rb +102 -0
  47. data/lib/modl/parser/modl_index.rb +30 -0
  48. data/lib/modl/parser/modl_keylist.rb +43 -0
  49. data/lib/modl/parser/modl_method.rb +132 -0
  50. data/lib/modl/parser/object_cache.rb +54 -0
  51. data/lib/modl/parser/parsed.rb +1410 -0
  52. data/lib/modl/parser/parser.rb +42 -0
  53. data/lib/modl/parser/ref_processor.rb +139 -0
  54. data/lib/modl/parser/substitutions.rb +67 -0
  55. data/lib/modl/parser/sutil.rb +78 -0
  56. data/lib/modl/parser/throwing_error_listener.rb +20 -0
  57. data/lib/modl/parser/version.rb +5 -0
  58. data/modl.gemspec +32 -0
  59. metadata +138 -11
  60. 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