From 1e839cd3ba61d3732fabdc54a4aa441cec613ba2 Mon Sep 17 00:00:00 2001 From: Paul Duncan Date: Wed, 5 Sep 2018 22:33:21 -0400 Subject: ruby: add remaining tests, add full documentation, simplify gemspec, add "docs" task, minor bug fixes --- ruby/lib/luigi-template.rb | 409 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 380 insertions(+), 29 deletions(-) (limited to 'ruby/lib/luigi-template.rb') diff --git a/ruby/lib/luigi-template.rb b/ruby/lib/luigi-template.rb index 8b0e553..8b4d476 100644 --- a/ruby/lib/luigi-template.rb +++ b/ruby/lib/luigi-template.rb @@ -1,71 +1,181 @@ +require 'uri' +require 'json' +require 'openssl' # require 'pp' +# +# String templating library. See Luigi::Template for details. +# module Luigi # - # library version + # Version of Luigi Template. # VERSION = '0.4.2' + # + # Base class for all errors raised by Luigi Template. + # class LuigiError < Exception end + # + # Base class for unknown entry errors raised by Luigi Template. + # class BaseUnknownError < LuigiError - attr_reader :type, - :name + # + # Type of unknown entry (Symbol). + # + attr_reader :type + + # + # Name of unknown entry (String). + # + attr_reader :name + # + # Create a new BaseUnknownError instance. + # + # Parameters: + # + # * +type+: Type name (ex: "template", "filter", or "key"). + # * +name+: Item name. + # def initialize(type, name) @type, @name = type, name super("unknown #{type}: #{name}") end end + # + # Thrown by Luigi::Template#run when an unknown key is encountered. + # + # The key is available in the +name+ attribute. + # class UnknownKeyError < BaseUnknownError - def initialize(key) - super(:key, key) + # + # Create a new UnknownFilterError instance. + # + # Parameters: + # + # * +name+: Unknown key. + # + def initialize(name) + super(:key, name) end end + # + # Thrown by Luigi::Template#run when an unknown filter is encountered. + # + # The unknown filter name is available in the +name+ attribute. + # class UnknownFilterError < BaseUnknownError - def initialize(filter) - super(:filter, filter) + # + # Create a new UnknownFilterError instance. + # + # Parameters: + # + # * +name+: Name of the unknown filter. + # + def initialize(name) + super(:filter, name) end end + + # + # Thrown by Luigi::Cache#run when an unknown template is encountered. + # + # The unknown template name is available in the +name+ attribute. + # class UnknownTemplateError < BaseUnknownError - def initialize(template) - super(:template, template); + # + # Create a new UnknownTemplateError instance. + # + # Parameters: + # + # * +name+: Unknown template name. + # + def initialize(name) + super(:template, name); end end # - # built-in filters + # HTML entity map. + # + # Used by built-in +h+ filter. + # + HTML_ENTITIES = { + 38 => '&', + 60 => '<', + 62 => '>', + 34 => '"', + 39 => ''', + } + + # + # Map of built-in global filters. + # + # Default Filters: + # + # * +uc+: Convert string to upper-case. + # * +lc+: Convert string to lower-case. + # * +h+: HTML-escape string. + # * +u+: URL-escape string. + # * +json+: JSON-encode value. + # * +trim+: Strip leading and trailing whitespace from string. + # * +base64+: Base64-encode value. + # * +hash+: Hash value. Requires hash algorithm parameter (ex: + # "sha1", "md5", etc). + # + # You can add your own global filters, like so: + # + # # create custom global filter named 'foobarify' + # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" } + # + # # create template which uses custom "foobarify" filter + # tmpl = Luigi::Template.new('hello %{name | foobarify}') + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul' + # }) + # + # # prints "hello foo-Paul-bar" # FILTERS = { # upper-case string - uc: proc { |v, args, row, t| + uc: proc { |v| (v || '').to_s.upcase }, # lower-case string - lc: proc { |v, args, row, t| + lc: proc { |v| (v || '').to_s.downcase }, # html-escape string - h: proc { |v, args, row, t| - (v || '').to_s.gsub(/&/, '&').gsub(//, '>').gsub(/'/, '&apos').gsub(/"/, '"') + h: proc { |v| + (v || '').to_s.bytes.map { |b| + if b < 32 || b > 126 + "&##{b};" + elsif HTML_ENTITIES.key?(b) + HTML_ENTITIES[b] + else + b.chr + end + }.join }, # uri-escape string - u: proc { |v, args, row, t| - require 'uri' - URI.escape((v || '').to_s) + u: proc { |v| + URI.encode_www_form_component((v || '').to_s) }, # json-encode value - json: proc { |v, args, row, t| - require 'json' - v.to_json + json: proc { |v| + JSON.unparse(v) }, # trim leading and trailing whitespace from string @@ -75,12 +185,11 @@ module Luigi # base64-encode string base64: proc { |v, args, row, t| - [(v || '').to_s].pack('m') + [(v || '').to_s].pack('m').strip }, # hash string hash: proc { |v, args, row, t| - require 'openssl' OpenSSL::Digest.new(args[0]).hexdigest((v || '').to_s) }, } @@ -88,7 +197,7 @@ module Luigi # # Template parser. # - module Parser + module Parser # :nodoc: all RES = { action: %r{ # match opening brace @@ -191,12 +300,142 @@ module Luigi # # Template class. # + # Parse a template string into a Luigi::Template instance, and then + # apply the Luigi::Template via the Luigi::Template#run() method. + # + # Example: + # + # # load luigi template + # require 'luigi-template' + # + # # create template + # tmpl = Luigi::Template.new('hello %{name}') + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul' + # }) + # + # # prints "hello Paul" + # + # You can also filter values in templates, using the pipe symbol: + # + # # create template that converts name to upper-case + # tmpl = Luigi::Template.new('hello %{name | uc}') + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul' + # }) + # + # # prints "hello PAUL" + # + # Filters can be chained: + # + # # create template that converts name to upper-case and then + # # strips leading and trailing whitespace + # tmpl = Luigi::Template.new('hello %{name | uc | trim}') + # + # # run template and print result + # puts tmpl.run({ + # name: ' Paul ' + # }) + # + # # prints "hello PAUL" + # + # Filters can take arguments: + # + # # create template that converts name to lowercase and then + # # calculates the SHA-1 digest of the result + # tmpl = Luigi::Template.new('hello %{name | lc | hash sha1}') + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul', + # }) + # + # # prints "hello a027184a55211cd23e3f3094f1fdc728df5e0500" + # + # You can define custom global filters: + # + # # create custom global filter named 'foobarify' + # Luigi::FILTERS[:foobarify] = proc { |s| "foo-#{s}-bar" } + # + # # create template which uses custom "foobarify" filter + # tmpl = Luigi::Template.new('hello %{name | foobarify}') + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul' + # }) + # + # # prints "hello foo-Paul-bar" + # + # Or define custom filters for a template: + # + # # create template with custom filters rather than global filters + # tmpl = Luigi::Template.new('hello %{name | reverse}', { + # reverse: proc { |s| s.reverse } + # }) + # + # # run template and print result + # puts tmpl.run({ + # name: 'Paul', + # }) + # + # # prints "hello luaP" + # + # Your custom filters can accept arguments, too: + # + # # create custom global filter named 'foobarify' + # Luigi::FILTERS[:wrap] = proc { |s, args| + # case args.length + # when 2 + # '(%s, %s, %s)' % [args[0], s, args[1]] + # when 1 + # '(%s in %s)' % [s, args[0]] + # when 0 + # s + # else + # raise 'invalid argument count' + # end + # } + # + # # create template that uses custom "wrap" filter + # tmpl = Luigi::Template.new('sandwich: %{meat | wrap slice heel}, taco: %{meat | wrap shell}') + # + # # run template and print result + # puts tmpl.run({ + # meat: 'chicken' + # }) + # + # # prints "sandwich: (slice, chicken, heel), taco: (chicken in shell)" + # class Template + # + # Original template string. + # attr_reader :str # - # Create a new template and run it with the given arguments and - # filter. + # Create a new template, expand it with the given arguments and + # filters, and print the result. + # + # Parameters: + # + # * +str+: Template string. + # * +args+: Argument key/value map. + # * +filters+: Hash of filters. Defaults to Luigi::FILTERS if + # unspecified. + # + # Example: + # + # # create a template object, expand it, and print the result + # puts Luigi::Template.run('hello %{name}', { + # name: 'Paul' + # }) + # + # # prints "hello Paul" # def self.run(str, args = {}, filters = FILTERS) Template.new(str, filters).run(args) @@ -211,7 +450,31 @@ module Luigi end # - # Run template with given arguments + # Expand template with the given arguments and return the result. + # + # Parameters: + # + # * +args+: Argument key/value map. + # + # Example: + # + # # create a template object + # tmpl = Luigi::Template.new('hello %{name}') + # + # # apply template, print result + # puts tmpl.run({ name: 'Paul'}) + # + # # prints "hello Paul" + # + # This method is aliased as "%", so you can do this: + # + # # create template + # tmpl = Luigi::Template.new('hello %{name | uc}') + # + # # run template and print result + # puts tmpl % { name: 'Paul' } + # + # # prints "hello PAUL" # def run(args) @actions.map { |a| @@ -249,19 +512,60 @@ module Luigi }.join end + alias :'%' :run + + # + # Return the input template string. + # + # Example: + # + # # create a template object + # tmpl = Luigi::Template.new('hello %{name}') + # + # # create a template object + # puts tmpl.to_s + # + # # prints "hello %{name}" + # def to_s @str end end # - # Simple template cache. + # Minimal lazy-loading template cache. + # + # Group a set of templates together and only parse them on an + # as-needed basis. # class Cache # - # Create a new template cache with the given templates + # Create a new template cache with the given templates. + # + # Parameters: + # + # * +strings+: Map of template names to template strings. + # * +filters+: Hash of filters. Defaults to Luigi::FILTERS if + # unspecified. + # + # Example: + # + # # create template cache + # cache = Luigi::Cache.new({ + # hi: 'hi %{name}!' + # }) + # + # # run template from cache + # puts cache.run(:hi, { + # name: 'Paul' + # }) + # + # # prints "hi paul!" # def initialize(strings, filters = FILTERS) + # work with frozen copy of strings hash + strings = strings.freeze + @templates = Hash.new do |h, k| # always deal with symbols k = k.intern @@ -275,7 +579,54 @@ module Luigi end # - # Run specified template with given arguments. + # Get given template, or raise an UnknownTemplateError if the given + # template does not exist. + # + # Example: + # + # # create template cache + # cache = Luigi::Cache.new({ + # hi: 'hi %{name}!' + # }) + # + # # get template from cache + # tmpl = cache[:hi] + # + # # run template, print result + # puts tmpl.run(:hi, { + # name: 'Paul' + # }) + # + # # prints "hi Paul" + # + def [](key) + @templates[key] + end + + # + # Run specified template from cache with the given templates. + # + # Raises an UnknownTemplateError if the given template key does not + # exist. + # + # Parameters: + # + # * +key+: Template key. + # * +args+: Argument key/value map. + # + # Example: + # + # # create template cache + # cache = Luigi::Cache.new({ + # hi: 'hi %{name}!' + # }) + # + # # run template from cache + # puts cache.run(:hi, { + # name: 'Paul' + # }) + # + # # prints "hi paul!" # def run(key, args) # run template with args and return result -- cgit v1.2.3