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