flooph 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/flooph.rb +243 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -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
|
data/lib/flooph.rb
ADDED
@@ -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: []
|