From 7fd6c6611252dbe1aae0ae61afab78f936b47c1f Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Wed, 5 Sep 2018 00:53:51 -0400 Subject: ruby: add gemspec, add Template::run --- ruby/lib/luigi-template.rb | 299 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 299 insertions(+) create mode 100644 ruby/lib/luigi-template.rb (limited to 'ruby/lib') diff --git a/ruby/lib/luigi-template.rb b/ruby/lib/luigi-template.rb new file mode 100644 index 0000000..13244da --- /dev/null +++ b/ruby/lib/luigi-template.rb @@ -0,0 +1,299 @@ +# 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(/'/, '&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]).hexdigest((v || '').to_s) + }, + } + + # + # Template parser. + # + module Parser + RES = { + action: %r{ + # match opening brace + %\{ + + # match optional whitespace + \s* + + # match key + (?[^\s\|\}]+) + + # match filter(s) + (?(\s*\|(\s*[^\s\|\}]+)+)*) + + # match optional whitespace + \s* + + # match closing brace + \} + + # or match up all non-% chars or a single % char + | (?[^%]* | %) + }mx, + + filter: %r{ + # match filter name + (?\S+) + + # match filter arguments (optional) + (?(\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 + attr_reader :str + + # + # Create a new template and run it with the given arguments and + # filter. + # + def self.run(str, args = {}, filters = FILTERS) + Template.new(str, filters).run(args) + end + + # + # 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 -- cgit v1.2.3