diff options
author | Paul Duncan <pabs@pablotron.org> | 2016-07-19 13:42:34 -0400 |
---|---|---|
committer | Paul Duncan <pabs@pablotron.org> | 2016-07-19 13:42:34 -0400 |
commit | 356016b39f901d63f02bf3ff31a1c3c638a822e8 (patch) | |
tree | f5ac205e35ac0c1cf27a64c7372ae1b9bfb226ef | |
parent | 9dcf367e911a0cf29dfb8ab5a7128d1f9cdae692 (diff) | |
download | guff-356016b39f901d63f02bf3ff31a1c3c638a822e8.tar.bz2 guff-356016b39f901d63f02bf3ff31a1c3c638a822e8.zip |
add guff/template
-rw-r--r-- | src/guff/template.cr | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/src/guff/template.cr b/src/guff/template.cr new file mode 100644 index 0000000..3068669 --- /dev/null +++ b/src/guff/template.cr @@ -0,0 +1,187 @@ +require "base64" + +# +# Crystal port of LuigiTemplate +# +# TODO: eventually this should be broken into a separate library +# +class Guff::Template + getter :string + + FILTERS = { + "h" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + HTML.escape(s) + }, + + "u" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + URI.escape(s) + }, + + "trim" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + s.strip + }, + + "uc" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + s.upcase + }, + + "lc" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + s.downcase + }, + + "reverse" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + s.reverse + }, + + "base64" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + Base64.encode(s) + }, + + "hash" => ->( + s : String, + args : Array(String), + row : Hash(String, String)) { + algo = (args.size > 0) ? args.first : "SHA1" + d = OpenSSL::Digest.new(algo) + d << s + d.hexdigest + }, + } of String => Proc(String, Array(String), Hash(String, String), String) + + class Cache + def initialize(@templates : Hash(String, String)) + @cache = {} of String => Template + end + + def [](key : String) : Template + raise "unknown template: #{key}" unless @templates[key]? + @cache[key] ||= Template.new(@templates[key]) + end + end + + class Filter + def initialize( + @name : String, + @args : Array(String) = [] of String + ) + end + + def run( + val : String, + row : Hash(String, String) + ) : String + raise "unknown filter: \"#{@name}\"" unless FILTERS.includes?(@name) + FILTERS[@name].call(val, @args, row) + end + end + + module Ops + abstract class Op + abstract def run(row : Hash(String, String)) : String + end + + class LiteralOp < Op + def initialize(@string : String) + end + + def run(row : Hash(String, String)) : String + @string + end + end + + class FilterOp < Op + def initialize(@key : String, @filters : Array(Filter)) + end + + def run(row : Hash(String, String)) : String + unless row.includes?(@key) + raise "missing template key: \"#{@key}\"" unless row.key?(@key) + end + + @filters.reduce(row[@key]) do |r, f| + f.run(r, row) + end + end + end + end + + def initialize(@string : String) + @ops = parse(@string) as Array(Ops::Op) + end + + def run(args : Hash(String, String)) + String.build do |io| + @ops.each do |op| + io << op.run(args) + end + end + end + + SCAN_RE = %r{ + # match opening brace + %\{ + + # optional whitespace + \s* + + # key + (?<key>[^\s\|]+) + + # optional whitespace + \s* + + # filters + (?<filters>(\|(\s*[^\s\|\}]+\s*)+)*) + + # closing brace + \} + + | (?<text>[^%]* | %) + }mx + + private def parse(string : String) : Array(Ops::Op) + r = string.scan(SCAN_RE).map do |md| + p md + if md["text"]? + Ops::LiteralOp.new(md["text"]) + else + filters = md["filters"].split(/\s*\|\s*/m) + filters.shift + + Ops::FilterOp.new(md["key"], filters.map { |fs| + args = fs.split(/\s+/) + Filter.new(args.first, args[1, args.size - 1]) + }) + end + end + end +end + +if false + # basic test + puts Guff::Template.new("test = %{test}, test|h = %{test|h}, test = %{ + test | h | h | u | base64 | hash MD5}").run({ + "test" => "<>&", + }) +end |