flooph 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/flooph.rb +243 -0
  3. metadata +69 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 28f98cd187750e628f79e3dd298d7d3f27ae9911
4
+ data.tar.gz: c0bca7b0c97038f0f5dbc23cdce33e1d78feb0fe
5
+ SHA512:
6
+ metadata.gz: 3f1fc0c32ed6b6d12fbda06a5c647446cc8fb319a0a62228dfd828914c7f6985d3c31ab49a7fa9fe5b8c3f5b167acb3a6dd4d04efc84c7fe7ec69f7b6352344a
7
+ data.tar.gz: 32e845a6d33ab09a451bc74e3d72133a575f9b6484f9f942ec59f6d8c55c61db42123b2e2c4c81162c53e3958f8c459768a65e567020bc07d4fe273d8f48c784
@@ -0,0 +1,243 @@
1
+ require 'parslet'
2
+
3
+ class Flooph < Parslet::Parser
4
+ VERSION = 0.1
5
+
6
+ # The current values used when evaluating templates and conditionals.
7
+ # Can also be updated by user input using #update_variables.
8
+ attr_accessor :vars
9
+
10
+ # Create a new, reusable template parser and evaluator.
11
+ #
12
+ # @param vars [Hash] symbol-to-values used in templates and conditional evaluations.
13
+ def initialize(vars={})
14
+ super()
15
+ @vars = vars
16
+ end
17
+
18
+ # Evaluate a template like the following example, inserting content and
19
+ # evaluating conditional branches. If you don't supply `vars` then the
20
+ # existing values for the instance are used.
21
+ #
22
+ # Hello, {=name}! # Insert values from variables.
23
+ # #
24
+ # {?trollLocation="cave"} # Conditional based on boolean expressions.
25
+ # There is a troll glaring at you. # See #conditional for how to write conditions.
26
+ # {|} # Conditional 'else' clause.
27
+ # The air smells bad here, like rotting meat. #
28
+ # {.} # End of the if/else.
29
+ # #
30
+ # {?debug}Troll is at {=trollLocation}.{.} # Conditional based on if the variable exists (and isn't false)
31
+ # #
32
+ # {?dogs>0} #
33
+ # I own {=dogs} dogg{?dogs=1}y{|}ies{.} now. # Conditionals can be inline.
34
+ # {.} #
35
+ # #
36
+ # {? cats=42 } #
37
+ # I have exactly 42 cats! I'll never get more. #
38
+ # {| cats=1 } # Else-if for chained conditionals.
39
+ # I have a cat. If I get another, I'll have two. #
40
+ # {| cats>1 } #
41
+ # I have {=cats} cats. #
42
+ # If I get another, I'll have {=cats+1}. # Output can do simple addition/subtraction.
43
+ # {|} #
44
+ # I don't have any cats. #
45
+ # {.} #
46
+ #
47
+ # @param vars [Hash] variable values to use for this and future evaluations.
48
+ # If omitted, existing variable values will be used.
49
+ # (see also #vars and #update_variables)
50
+ # @return [String] the template after transformation.
51
+ def transform(str, vars=nil)
52
+ parse_and_transform(:mkup, str, vars).tap do |result|
53
+ result.gsub!(/\n{3,}/, "\n\n") if result
54
+ end
55
+ end
56
+
57
+ # Evaluate simple conditional expressions to a boolean value, with variable lookup.
58
+ # Examples:
59
+ #
60
+ # cats? # variable cats is set (and not set to `false` or `no`)
61
+ # cats = 42 # variable `cats` equals 42
62
+ # cats>0 & dogs>0 # if both variables are numbers greater than zero
63
+ #
64
+ # * Numeric and string comparisons, using < > = == ≤ <= ≥ >= ≠ !=
65
+ # * Non-present variables or invalid comparisons always result in false
66
+ # * Variable presence/truthiness using just name (isDead) or with optional trailing question mark (isDead?).
67
+ # * Boolean composition using a | b & c & (d | !e) || !(f && g)
68
+ # * !foo means "not foo", inverting the meaning
69
+ # * & has higher precedence than |
70
+ # * | is the same as ||; & is the same as &&
71
+ #
72
+ # @param vars [Hash] variable values to use for this and future evaluations.
73
+ # If omitted, existing variable values will be used.
74
+ # (see also #vars and #update_variables)
75
+ # @return [true, false] the result of the evaluation.
76
+ def conditional(str, vars=nil)
77
+ parse_and_transform(:boolean_expression, str, vars)
78
+ end
79
+
80
+ # Parse a simple hash setup for setting and updating values. For example:
81
+ #
82
+ # f = Flooph.new # No variables yet
83
+ # f.update_variables <<-END
84
+ # debug: false
85
+ # cats: 17
86
+ # alive: yes
87
+ # trollLocation: "cave"
88
+ # END
89
+ # f.conditional "cats > 3"
90
+ # #=> true
91
+ # f.update_variables "oldCats:cats \n cats: cats + 1"
92
+ # f.calculate "cats"
93
+ # #=> 18
94
+ # f.calculate "oldCats"
95
+ # #=> 17
96
+ #
97
+ # Legal value types are:
98
+ #
99
+ # * Booleans: `true`, `false`, `yes`, `no`
100
+ # * Numbers: `-3`, `12`, `3.1415`
101
+ # * Strings: `"foo"`, `"Old Barn"` _must use double quotes_
102
+ # * Variables: `cats + 7` _only supports add/subtract, not multiplication_
103
+ #
104
+ # @param vars [Hash] initial variable values to base references on.
105
+ # If omitted, existing variable values will be used.
106
+ # @return [Hash] the new variable values after updating.
107
+ def update_variables(str, vars=nil)
108
+ parse_and_transform(:varset, str, vars)
109
+ @vars
110
+ end
111
+
112
+ # Evaluate an expression, looking up values and performing simple math.
113
+ #
114
+ # f = Flooph.new cats:17, dogs:25
115
+ # p f.calculate("cats + dogs")
116
+ # #=> 42
117
+ #
118
+ # @param vars [Hash] variable values to use for this and future evaluations.
119
+ # If omitted, existing variable values will be used.
120
+ # (see also #vars and #update_variables)
121
+ # @return [Hash] the new variable values after updating.
122
+ def calculate(str, vars=nil)
123
+ parse_and_transform(:value, str, vars)
124
+ end
125
+
126
+ # Common implementation for other methods
127
+ # @!visibility private
128
+ def parse_and_transform(root_rule, str, vars)
129
+ @vars = vars if vars
130
+ begin
131
+ str = str.strip.gsub(/^[ \t]+|[ \t]+$/, '')
132
+ tree = send(root_rule).parse(str)
133
+ Transform.new.eval(tree, @vars)
134
+ rescue Parslet::ParseFailed => error
135
+ puts "Flooph failed to parse #{str.inspect}"
136
+ puts error.parse_failure_cause.ascii_tree
137
+ puts
138
+ # TODO: catch transformation errors
139
+ end
140
+ end
141
+
142
+ # template
143
+ rule(:mkup) { (proz | spit.as(:proz) | cond).repeat.as(:result) }
144
+ rule(:proz) { ((str('{=').absent? >> str('{?').absent? >> str('{|').absent? >> str('{.}').absent? >> any).repeat(1)).as(:proz) }
145
+ rule(:spit) { str('{=') >> sp >> value >> sp >> str('}') }
146
+ rule(:cond) do
147
+ test.as(:test) >> mkup.as(:out) >>
148
+ (elif.as(:test) >> mkup.as(:out)).repeat.as(:elifs) >>
149
+ (ells >> mkup.as(:proz)).repeat(0,1).as(:else) >>
150
+ stop
151
+ end
152
+ rule(:test) { str('{?') >> sp >> boolean_expression >> sp >> str('}') }
153
+ rule(:elif) { str('{|') >> sp >> boolean_expression >> sp >> str('}') }
154
+ rule(:ells) { str('{|}') }
155
+ rule(:stop) { str('{.}') }
156
+
157
+ # conditional
158
+ rule(:boolean_expression) { orrs }
159
+ rule(:orrs) { ands.as(:and) >> (sp >> str('|').repeat(1,2) >> sp >> ands.as(:and)).repeat.as(:rest) }
160
+ rule(:ands) { bxpr.as(:orr) >> (sp >> str('&').repeat(1,2) >> sp >> bxpr.as(:orr)).repeat.as(:rest) }
161
+ rule(:bxpr) do
162
+ ((var.as(:lookup) | num | text).as(:a) >> sp >> cmpOp.as(:cmpOp) >> sp >> (var.as(:lookup) | num | text).as(:b)) |
163
+ (str('!').maybe.as(:no) >> var.as(:lookup) >> str('?').maybe) |
164
+ (str('!').maybe.as(:no) >> str('(') >> sp >> orrs.as(:orrs) >> sp >> str(')'))
165
+ end
166
+ rule(:cmpOp) { (match['<>='] >> str('=').maybe) | match['≤≥≠'] | str('!=') }
167
+
168
+ # assignment
169
+ rule(:varset){ pair >> (match("\n") >> pair.maybe).repeat }
170
+ rule(:pair) { var.as(:set) >> sp >> str(':') >> sp >> value.as(:val) >> sp }
171
+ rule(:value) { bool | adds | num | text | var.as(:lookup) }
172
+ rule(:adds) { (var.as(:lookup) | num).as(:a) >> sp >> match['+-'].as(:addOp) >> sp >> (var.as(:lookup) | num).as(:b) }
173
+ rule(:sp) { match[' \t'].repeat }
174
+
175
+ # shared
176
+ rule(:bool) { (str('true') | str('false') | str('yes') | str('no')).as(:bool) }
177
+ rule(:text) { str('"') >> match["^\"\n"].repeat.as(:text) >> str('"') }
178
+ rule(:num) { (str('-').maybe >> match['\d'].repeat(1) >> (str('.') >> match['\d'].repeat(1)).maybe).as(:num) }
179
+ rule(:var) { match['a-zA-Z'] >> match('\w').repeat }
180
+ rule(:sp) { match[' \t'].repeat }
181
+
182
+ class Transform < Parslet::Transform
183
+ rule(proz:simple(:s)){ s.to_s }
184
+ rule(test:simple(:test), out:simple(:out), elifs:subtree(:elifs), else:subtree(:elseout)) do
185
+ if test
186
+ out
187
+ elsif valid = elifs.find{ |h| h[:test] }
188
+ valid[:out]
189
+ else
190
+ elseout[0]
191
+ end
192
+ end
193
+ rule(result:sequence(:a)){ a.join }
194
+
195
+ # conditional
196
+ rule(a:simple(:a), cmpOp:simple(:op), b:simple(:b)) do
197
+ begin
198
+ case op
199
+ when '<' then a<b
200
+ when '>' then a>b
201
+ when '=', '==' then a==b
202
+ when '≤', '<=' then a<=b
203
+ when '≥', '>=' then a>=b
204
+ when '≠', '!=' then a!=b
205
+ end
206
+ rescue NoMethodError, ArgumentError
207
+ # nil can't compare with anyone, and we can't compare strings and numbers
208
+ false
209
+ end
210
+ end
211
+ rule(orr:simple(:value)) { value }
212
+ rule(and:simple(:value)) { value }
213
+ rule(orr:simple(:first), rest:sequence(:rest)) { [first, *rest].all? }
214
+ rule(and:simple(:first), rest:sequence(:rest)) { [first, *rest].any? }
215
+ rule(no:simple(:invert), orrs:simple(:val)) { invert ? !val : val }
216
+ rule(no:simple(:invert), lookup:simple(:s)){ v = vars[s.to_sym]; invert ? !v : v }
217
+
218
+ # assignment
219
+ rule(set:simple(:var), val:simple(:val)){ vars[var.to_sym] = val }
220
+ rule(a:simple(:a), addOp:simple(:op), b:simple(:b)) do
221
+ x = a.is_a?(Parslet::Slice) ? vars[a.to_sym] : a
222
+ y = b.is_a?(Parslet::Slice) ? vars[b.to_sym] : b
223
+ if x.nil? || y.nil?
224
+ nil
225
+ else
226
+ case op
227
+ when '+' then x+y
228
+ when '-' then x-y
229
+ end
230
+ end
231
+ end
232
+
233
+ # shared
234
+ rule(lookup:simple(:s)) { vars[s.to_sym] }
235
+ rule(num:simple(:str)) { f=str.to_f; i=str.to_i; f==i ? i : f }
236
+ rule(bool:simple(:s)){ d = s.str.downcase; d=='true' || d=='yes' }
237
+ rule(text:simple(:s)){ s.str }
238
+
239
+ def eval(tree, vars)
240
+ apply(tree, vars:vars)
241
+ end
242
+ end
243
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flooph
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Gavin Kistner
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: parslet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.8'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.8'
27
+ description: "\n Flooph is a Ruby library designed to let you take code from (possibly-malicious)
28
+ users and evaluate it safely.\n Instead of evaluating arbitrary Ruby code (or
29
+ JavaScript, or any other interpreter), it specifies a custom 'language', with its
30
+ own parser and evaluation.\n\n Flooph provides four core pieces of functionality:\n\n
31
+ \ * A simple syntax for specifying key/value pairs (much like a Ruby Hash literal).\n
32
+ \ * A simple template language that supports conditional content and injecting
33
+ content.\n * Standalone functionality for evaluating conditional expressions
34
+ based on the key/values (also used in the templates).\n * Standalone functionality
35
+ for evaluating value expressions based on the key/values (also used in the templates).\n
36
+ \ "
37
+ email:
38
+ - gavin@phrogz.net
39
+ executables: []
40
+ extensions: []
41
+ extra_rdoc_files: []
42
+ files:
43
+ - lib/flooph.rb
44
+ homepage: https://github.com/Phrogz/Flooph
45
+ licenses:
46
+ - MIT
47
+ metadata: {}
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project:
64
+ rubygems_version: 2.6.14
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: A template markup and evaluator designed to be simple and safe from malicious
68
+ input.
69
+ test_files: []