aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2018-09-05 22:33:21 -0400
committerPaul Duncan <pabs@pablotron.org>2018-09-05 22:33:21 -0400
commit1e839cd3ba61d3732fabdc54a4aa441cec613ba2 (patch)
treedc4dfe49c226b0eacf54614a77c52bbe99a89003
parentabe810de48da22fbc8e28369f50e2863291fe505 (diff)
downloadluigi-template-1e839cd3ba61d3732fabdc54a4aa441cec613ba2.tar.bz2
luigi-template-1e839cd3ba61d3732fabdc54a4aa441cec613ba2.zip
ruby: add remaining tests, add full documentation, simplify gemspec, add "docs" task, minor bug fixes
-rw-r--r--.gitignore1
-rw-r--r--ruby/Rakefile8
-rw-r--r--ruby/lib/luigi-template.rb409
-rw-r--r--ruby/luigi-template.gemspec7
-rw-r--r--ruby/test/test_cache.rb2
-rw-r--r--ruby/test/test_filters.rb2
6 files changed, 392 insertions, 37 deletions
diff --git a/.gitignore b/.gitignore
index 34ff2e6..675d0c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ php/vendor
php/docs/
php/examples/*
ruby/*.gem
+ruby/docs/
diff --git a/ruby/Rakefile b/ruby/Rakefile
index debc11c..0d302f9 100644
--- a/ruby/Rakefile
+++ b/ruby/Rakefile
@@ -1,8 +1,16 @@
require 'rake/testtask'
+require 'rdoc/task'
Rake::TestTask.new do |t|
t.libs << 'test'
end
+RDoc::Task.new :docs do |t|
+ t.main = "README.mkd"
+ t.rdoc_files.include('README.mkd', 'lib/*.rb')
+ t.rdoc_dir = 'docs'
+ # t.options << "--all"
+end
+
desc "Run tests"
task :default => :test
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 => '&amp;',
+ 60 => '&lt;',
+ 62 => '&gt;',
+ 34 => '&quot;',
+ 39 => '&apos;',
+ }
+
+ #
+ # 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(/&/, '&amp;').gsub(/</, '&lt;').gsub(/>/, '&gt;').gsub(/'/, '&apos').gsub(/"/, '&quot;')
+ 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
diff --git a/ruby/luigi-template.gemspec b/ruby/luigi-template.gemspec
index 0bc63e5..c67d157 100644
--- a/ruby/luigi-template.gemspec
+++ b/ruby/luigi-template.gemspec
@@ -10,10 +10,5 @@ Gem::Specification.new do |s|
s.email = 'pabs@pablotron.org'
s.homepage = 'https://github.com/pablotron/luigi-template'
s.license = 'MIT'
- s.files = %w{
- README.mkd
- Rakefile
- test/test_template.rb
- lib/luigi-template.rb
- }
+ s.files = Dir['{lib,test}/*.rb'] + %w{README.mkd Rakefile}
end
diff --git a/ruby/test/test_cache.rb b/ruby/test/test_cache.rb
index 4296410..66a7e9f 100644
--- a/ruby/test/test_cache.rb
+++ b/ruby/test/test_cache.rb
@@ -1,7 +1,7 @@
require 'minitest/autorun'
require 'luigi-template'
-class TemplateTest < MiniTest::Test
+class CacheTest < MiniTest::Test
def test_cache
cache = Luigi::Cache.new({
foo: 'foo%{bar}',
diff --git a/ruby/test/test_filters.rb b/ruby/test/test_filters.rb
index 9c34cd5..03159d7 100644
--- a/ruby/test/test_filters.rb
+++ b/ruby/test/test_filters.rb
@@ -1,7 +1,7 @@
require 'minitest/autorun'
require 'luigi-template'
-class TemplateTest < MiniTest::Test
+class FiltersTest < MiniTest::Test
def test_filter
r = Luigi::Template.run('foo%{bar|h}', {
bar: '<',