diff options
-rw-r--r-- | ruby/luigi-template.rb | 289 |
1 files changed, 289 insertions, 0 deletions
diff --git a/ruby/luigi-template.rb b/ruby/luigi-template.rb new file mode 100644 index 0000000..d737fdc --- /dev/null +++ b/ruby/luigi-template.rb @@ -0,0 +1,289 @@ +# require 'pp' + +module Luigi + # + # library version + # + VERSION = '0.4.0' + + # + # built-in filters + # + FILTERS = { + # upper-case string + uc: proc { |v, args, row, t| + (v || '').to_s.upcase + }, + + # lower-case string + lc: proc { |v, args, row, t| + (v || '').to_s.downcase + }, + + # html-escape string + h: proc { |v, args, row, t| + (v || '').to_s.gsub(/&/, '&').gsub(/</, '<').gsub(/>/, '>').gsub(/'/, '&apos').gsub(/"/, '"') + }, + + # uri-escape string + u: proc( { |v, args, row, t| + require 'uri' + URI.escape((v || '').to_s) + }, + + # json-encode value + json: proc { |v, args, row, t| + require 'json' + v.to_json + }, + + # trim leading and trailing whitespace from string + trim: proc { |v, args, row, t| + (v || '').to_s.strip + }, + + # base64-encode string + base64: proc { |v, args, row, t| + [(v || '').to_s].pack('m') + }, + + # hash string + hash: proc { |v, args, row, t| + require 'openssl' + OpenSSL::Digest.new(args[0] || 'md5').hexdigest((v || '').to_s) + }, + } + + # + # Template parser. + # + module Parser + RES = { + action: %r{ + # match opening brace + %\{ + + # match optional whitespace + \s* + + # match key + (?<key>[^\s\|\}]+) + + # match filter(s) + (?<filters>(\s*\|(\s*[^\s\|\}]+)+)*) + + # match optional whitespace + \s* + + # match closing brace + \} + + # or match up all non-% chars or a single % char + | (?<text>[^%]* | %) + }mx, + + filter: %r{ + # match filter name + (?<name>\S+) + + # match filter arguments (optional) + (?<args>(\s*\S+)*) + + # optional trailing whitespace + \s* + }mx, + + delim_filters: %r{ + \s*\|\s* + }mx, + + delim_args: %r{ + \s+ + }, + } + + # + # Parse a (possibly empty) string into an array of actions. + # + def self.parse_template(str) + str.scan(RES[:action]).map { |m| + if m[0] && m[0].length > 0 + r = { + type: :action, + key: m[0].intern, + filters: parse_filters(m[1]), + } + else + # literal text + r = { type: :text, text: m[2] } + end + + # pp r + + # return result + r + } + end + + # + # Parse a (possibly empty) string into an array of filters. + # + def self.parse_filters(str) + # strip leading and trailing whitespace + str = (str || '').strip + + if str.length > 0 + str.strip.split(RES[:delim_filters]).inject([]) do |r, f| + # strip whitespace + f = f.strip + + if f.length > 0 + md = f.match(RES[:filter]) + raise "invalid filter: #{f}" unless md + # pp md + + # get args + args = md[:args].strip + + # add to result + r << { + name: md[:name].intern, + args: args.length > 0 ? args.split(RES[:delim_args]) : [], + } + end + + # return result + r + end + else + # return empty filter set + [] + end + end + end + + # + # Template class. + # + class Template + # + # Create a new Template from the given string. + # + def initialize(str, filters = FILTERS) + @str, @filters = str, filters + @actions = Parser.parse_template(str) + end + + # + # Run template with given arguments + # + def run(args) + @actions.map { |a| + # pp a + + case a[:type] + when :action + # check key and get value + val = if args.key?(a[:key]) + args[a[:key]] + elsif args.key?(a[:key].to_s) + args[a[:key].to_s] + else + # invalid key + raise "unknown argument: #{a[:key]}" + end + + # filter value + a[:filters].inject(val) do |r, f| + # check filter name + raise "unknown filter: #{f[:name]}" unless @filters.key?(f[:name]) + + # call filter, return result + @filters[f[:name]].call(r, f[:args], args, self) + end + when :text + # literal text + a[:text] + else + # never reached + raise "unknown action type: #{a[:type]}" + end + }.join + end + end + + # + # Simple template cache. + # + class Cache + # + # Create a new template cache with the given templates + # + def initialize(strings, filters = FILTERS) + @templates = Hash.new do |h, k| + # always deal with symbols + k = k.intern + + # make sure template exists + raise "unknown template: #{k}" unless strings.key?(k) + + # create template + h[k] = Template.new(strings[k], filters) + end + end + + # + # Run specified template with given arguments. + # + def run(key, args) + # run template with args and return result + @templates[key].run(args) + end + end + + # + # test module + # + module Test + # test template + TEMPLATE = ' + basic test: hello %{name} + test filters: hello %{name | uc | base64 | hash sha1} + test custom: hello %{name|custom} + test custom_with_arg: %{name|custom_with_arg asdf} + ' + + CUSTOM_FILTERS = { + custom: proc { + 'custom' + }, + + custom_with_arg: proc { |v, args| + args.first || 'custom' + }, + } + + # test template cache + CACHE = { + test: TEMPLATE + } + + # test arguments + ARGS = { + name: 'paul', + } + + def self.run + # add custom filters + Luigi::FILTERS.update(CUSTOM_FILTERS) + + # test individual template + puts Template.new(TEMPLATE).run(ARGS) + + # test template cache + puts Cache.new(CACHE).run(:test, ARGS) + end + end +end + +Luigi::Test.run if __FILE__ == $0 |