flooph 0.1

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.
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: []