aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/guff/template.cr187
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