Created
April 6, 2018 23:10
-
-
Save Phrogz/c164b9d8d6bb7fc84c28900a82af0e71 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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