Skip to content

Instantly share code, notes, and snippets.

@Phrogz
Created April 6, 2018 23:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Phrogz/c164b9d8d6bb7fc84c28900a82af0e71 to your computer and use it in GitHub Desktop.
Save Phrogz/c164b9d8d6bb7fc84c28900a82af0e71 to your computer and use it in GitHub Desktop.
template = <<END
Hello, {=name}!
{?debug}Go soak your head.{.}
{?trollLocation="cave"}
There is a vicious troll glaring at you.
{|}
The air smells bad here, like rotting meat.
{.}
{?dogs>0}
I own {=dogs} dogg{?dogs=1}y{|}ies{.} now.
{.}
{? cats=42 }
I have exactly 42 cats! I'll never buy more.
{| cats=1 }
I have a cat. If I buy another, I'll have two.
{| cats>1 }
I have {=cats} cats. If I buy another, I'll have {=cats+1}.
{|}
I don't have any cats.
{.}
END
my_template = PishTemplate.new
my_template.update_variables <<END
name: "World"
debug: false
dogs: 1
cats: 2
trollLocation: "cave"
END
puts my_template.transform(template)
#=> Hello, World!
#=>
#=> There is a vicious troll glaring at you.
#=>
#=> I own 1 doggy now.
#=>
#=> I have 2 cats. If I buy another, I'll have 3.
require 'parslet'
class PishTemplate < Parslet::Parser
attr_accessor :vars
# `vars` is a hash of symbol-to-values for referencing in templates.
# vars can also be safely set using the #assign-values method.
def initialize(vars={})
super()
@vars = vars
end
# Convert a template like the following.
# If you don't supply `vars`, the existing variable values are used.
#
# Hello, {=name}! # Look up values from variables.
# #
# {?debug}Go soak your head.{.} # Conditional based on if the variable exists (and isn't false)
# #
# {?trollLocation="cave"} # Conditional based on boolean expressions. See #conditional for details.
# There is a troll glaring at you. #
# {|} # Traditional 'else' clause.
# The air smells bad here, like rotting meat. #
# {.} # End of the if/else.
# #
# {?dogs>0} #
# I own {=dogs} dogg{?dogs=1}y{|}ies{.} now. # Conditionals can be inline.
# {.} #
# #
# {? cats=42 } #
# I have exactly 42 cats! I'll never get more. #
# {| cats=1 } # Else-if for chained conditionals.
# I have a cat. If I get another, I'll have two. #
# {| cats>1 } #
# I have {=cats} cats. #
# If I get another, I'll have {=cats+1}. # Output can do simple addition/subtraction.
# {|} #
# I don't have any cats. #
# {.} #
def transform(str, vars=nil)
parse_and_transform(:mkup, str, vars).gsub(/\n{3,}/, "\n\n")
end
# Evaluate simple conditional expressions to a boolean value, with variable lookup. Summary:
# * Numeric and string comparisons, using < > = == ≤ <= ≥ >= ≠ !=
# * Non-present variables or invalid comparisons always result in false
# * Variable presence/truthiness using just name (isDead) or with optional trailing question mark (isDead?).
# * Boolean composition using a | b & c & (d | !e) || !(f && g)
# * & has higher precedence than |
# * | is the same as ||; & is the same as &&
def conditional(str, vars=nil)
parse_and_transform(:boolean_expression, str, vars)
end
# Parse a simple hash setup for setting and updating values. For example:
#
# s = SafeTemplate.new # No variables yet
# s.update_variables <<-END
# debug: false
# cats: 17
# alive: yes
# trollLocation: "cave"
# END
#
# s.update_variables "cats: cats + 1"
def update_variables(str, vars=nil)
parse_and_transform(:varset, str, vars)
@vars
end
# def value(str, vars=nil)
# parse_and_transform(:value, str, vars)
# end
def parse_and_transform(root_rule, str, vars)
@vars = vars if vars
begin
str = str.gsub(/^[ \t]+|[ \t]+$/, '')
tree = send(root_rule).parse(str)
Transform.new.eval(tree, @vars)
rescue Parslet::ParseFailed => error
puts "#{self} failed when parsing #{str.inspect}"
puts error.parse_failure_cause.ascii_tree
puts
end
end
# template
rule(:mkup) { (proz | spit.as(:proz) | cond).repeat.as(:result) }
rule(:proz) { ((str('{=').absent? >> str('{?').absent? >> str('{|').absent? >> str('{.}').absent? >> any).repeat(1)).as(:proz) }
rule(:spit) { str('{=') >> value >> str('}') }
rule(:cond) do
test.as(:test) >> mkup.as(:out) >>
(elif.as(:test) >> mkup.as(:out)).repeat.as(:elifs) >>
(ells >> mkup.as(:proz)).repeat(0,1).as(:else) >>
stop
end
rule(:test) { str('{?') >> sp >> boolean_expression >> sp >> str('}') }
rule(:elif) { str('{|') >> sp >> boolean_expression >> sp >> str('}') }
rule(:ells) { str('{|}') }
rule(:stop) { str('{.}') }
# conditional
rule(:boolean_expression) { orrs }
rule(:orrs) { ands.as(:and) >> (sp >> str('|').repeat(1,2) >> sp >> ands.as(:and)).repeat.as(:rest) }
rule(:ands) { bxpr.as(:orr) >> (sp >> str('&').repeat(1,2) >> sp >> bxpr.as(:orr)).repeat.as(:rest) }
rule(:bxpr) do
((var.as(:lookup) | num | text).as(:a) >> sp >> cmpOp.as(:cmpOp) >> sp >> (var.as(:lookup) | num | text).as(:b)) |
(str('!').maybe.as(:no) >> var.as(:lookup) >> str('?').maybe) |
(str('!').maybe.as(:no) >> str('(') >> sp >> orrs.as(:orrs) >> sp >> str(')'))
end
rule(:cmpOp) { (match['<>='] >> str('=').maybe) | match['≤≥≠'] | str('!=') }
# assignment
rule(:varset){ pair >> (match("\n") >> pair.maybe).repeat }
rule(:pair) { var.as(:set) >> sp >> str(':') >> sp >> value.as(:val) >> sp }
rule(:value) { bool | adds | num | text | var.as(:lookup) }
rule(:adds) { (var.as(:lookup) | num).as(:a) >> sp >> match['+-'].as(:addOp) >> sp >> (var.as(:lookup) | num).as(:b) }
rule(:sp) { match[' \t'].repeat }
# shared
rule(:bool) { (str('true') | str('false') | str('yes') | str('no')).as(:bool) }
rule(:text) { str('"') >> match["^\"\n"].repeat.as(:text) >> str('"') }
rule(:num) { (str('-').maybe >> match['\d'].repeat(1) >> (str('.') >> match['\d'].repeat(1)).maybe).as(:num) }
rule(:var) { match['a-zA-Z'] >> match('\w').repeat }
rule(:sp) { match[' \t'].repeat }
class Transform < Parslet::Transform
rule(proz:simple(:s)){ s.to_s }
rule(test:simple(:test), out:simple(:out), elifs:subtree(:elifs), else:subtree(:elseout)) do
if test
out
elsif valid = elifs.find{ |h| h[:test] }
valid[:out]
else
elseout[0]
end
end
rule(result:sequence(:a)){ a.join }
# conditional
rule(a:simple(:a), cmpOp:simple(:op), b:simple(:b)) do
begin
case op
when '<' then a<b
when '>' then a>b
when '=', '==' then a==b
when '≤', '<=' then a<=b
when '≥', '>=' then a>=b
when '≠', '!=' then a!=b
end
rescue NoMethodError, ArgumentError
# nil can't compare with anyone, and we can't compare strings and numbers
false
end
end
rule(orr:simple(:value)) { value }
rule(and:simple(:value)) { value }
rule(orr:simple(:first), rest:sequence(:rest)) { [first, *rest].all? }
rule(and:simple(:first), rest:sequence(:rest)) { [first, *rest].any? }
rule(no:simple(:invert), orrs:simple(:val)) { invert ? !val : val }
rule(no:simple(:invert), lookup:simple(:s)){ v = vars[s.to_sym]; invert ? !v : v }
# assignment
rule(set:simple(:var), val:simple(:val)){ vars[var.to_sym] = val }
rule(a:simple(:a), addOp:simple(:op), b:simple(:b)) do
x = a.is_a?(Parslet::Slice) ? vars[a.to_sym] : a
y = b.is_a?(Parslet::Slice) ? vars[b.to_sym] : b
if x.nil? || y.nil?
nil
else
case op
when '+' then x+y
when '-' then x-y
end
end
end
# shared
rule(lookup:simple(:s)) { vars[s.to_sym] }
rule(num:simple(:str)) { f=str.to_f; i=str.to_i; f==i ? i : f }
rule(bool:simple(:s)){ d = s.str.downcase; d=='true' || d=='yes' }
rule(text:simple(:s)){ s.str }
def eval(tree, vars)
apply(tree, vars:vars)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment