mattly-hpreserve 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.mkdn +90 -0
- data/Rakefile +40 -0
- data/lib/hpreserve.rb +13 -0
- data/lib/hpreserve/abstract_cacher.rb +35 -0
- data/lib/hpreserve/extensions.rb +17 -0
- data/lib/hpreserve/file_cacher.rb +25 -0
- data/lib/hpreserve/filters.rb +74 -0
- data/lib/hpreserve/parser.rb +86 -0
- data/lib/hpreserve/standard_filters.rb +60 -0
- data/lib/hpreserve/variables.rb +41 -0
- data/spec/abstract_cacher_spec.rb +41 -0
- data/spec/file_cacher_spec.rb +46 -0
- data/spec/filters_spec.rb +49 -0
- data/spec/parser_spec.rb +213 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/standard_filters_spec.rb +140 -0
- data/spec/variables_spec.rb +116 -0
- metadata +79 -0
data/README.mkdn
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# Hpreserve
|
2
|
+
|
3
|
+
by Matthew Lyon <matt@flowerpowered.com>
|
4
|
+
|
5
|
+
## DESCRIPTION:
|
6
|
+
|
7
|
+
Hpreserve is a humane, eval-safe template system built atop the Hpricot DOM
|
8
|
+
manipulation library. Its primary goal is to not require an interpreter to preview
|
9
|
+
a design in a browser. The designer provides their own sample data which is replaced
|
10
|
+
by the template parser at runtime.
|
11
|
+
|
12
|
+
Unlike similar DOM-replacement libraries lilu and Amrita, Hpreserve does not rely
|
13
|
+
on matching DOM IDs to variable names, recognizing that DOM IDs often have semantic
|
14
|
+
meaning in their own right. Rather, templates are driven by custom attributes on
|
15
|
+
DOM elements that are removed at runtime.
|
16
|
+
|
17
|
+
## FEATURES/PROBLEMS:
|
18
|
+
|
19
|
+
* Content Replacement: elements with a "content" attribute will have their contents
|
20
|
+
replaced with the equivalent variable. Namespaced variables are available to the
|
21
|
+
template using '.' to separate the namespaces.
|
22
|
+
|
23
|
+
* Content Replacement with Collections: if the variable for an element's content
|
24
|
+
attribute returns an Array, the element's first child node will be used as a
|
25
|
+
template for iterating over the content of the array. While iterating, the current
|
26
|
+
item in the array is made available as a variable specified by the parent element's
|
27
|
+
"local" attribute:
|
28
|
+
|
29
|
+
`<ul content='album.songs' local='song'><li content='song.name'>Song Name</li></ul>`
|
30
|
+
|
31
|
+
**TODO**: Currently, no attempt is made to prevent this local context variable naame
|
32
|
+
from clobbering an equivalent variable name elsewhere in the variable namespace.
|
33
|
+
|
34
|
+
* Partial Includes: Since this huge productivity booster can be replicated in
|
35
|
+
Textmate (and presumably other decent html editors), elements with an "include"
|
36
|
+
tag have their content replaced by the given variable. You may provide a default 'root'
|
37
|
+
in the variable namespace with "include_base=". Variable substitution is performed
|
38
|
+
on the given value, and a default is available:
|
39
|
+
|
40
|
+
`<div include='{section.name}_sidebar | sidebar'></div>`
|
41
|
+
|
42
|
+
This would render f.e. 'blog_sidebar' if 'section.name' resolved to 'blog'. If this
|
43
|
+
value returns empty (that is, there is no 'blog_sidebar') it will render the default
|
44
|
+
'sidebar' instead.
|
45
|
+
|
46
|
+
* Filters: given by an element's "filter" attribute and specified using a syntax
|
47
|
+
similar to the "style" attribute in HTML, filters operate on the node itself,
|
48
|
+
either modifying the element's contents or altering the element's properties. Filter
|
49
|
+
directives are separated by semi-colons, they may be given arguments after a colon, and
|
50
|
+
multiple arguments are separated by commas:
|
51
|
+
|
52
|
+
`<a filter='capitalize; link_to: {thing.link}; truncate: 30, ...'>text</a>`
|
53
|
+
|
54
|
+
* Planned Features include more sophisticated controls for iterating over an array
|
55
|
+
variable, and methods for escaping html entities in variables, including an option
|
56
|
+
to do this automatically.
|
57
|
+
|
58
|
+
## SYNOPSIS:
|
59
|
+
|
60
|
+
template = File.open('example.html')
|
61
|
+
variables = {'name' => 'value'}
|
62
|
+
Hpreserve::Parser.render(template, variables)
|
63
|
+
|
64
|
+
## REQUIREMENTS:
|
65
|
+
|
66
|
+
* Hpricot
|
67
|
+
* Rspec, if you wish to run the spec suite
|
68
|
+
|
69
|
+
## LICENSE:
|
70
|
+
|
71
|
+
Copyright (c) 2008 Matt Lyon
|
72
|
+
|
73
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
74
|
+
a copy of this software and associated documentation files (the
|
75
|
+
"Software"), to deal in the Software without restriction, including
|
76
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
77
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
78
|
+
permit persons to whom the Software is furnished to do so, subject to
|
79
|
+
the following conditions:
|
80
|
+
|
81
|
+
The above copyright notice and this permission notice shall be
|
82
|
+
included in all copies or substantial portions of the Software.
|
83
|
+
|
84
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
87
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
88
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
89
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
90
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/gempackagetask"
|
3
|
+
Dir['tasks/**/*.rake'].each { |rake| load rake }
|
4
|
+
|
5
|
+
desc "Run the specs."
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
spec = Gem::Specification.new do |s|
|
9
|
+
s.name = 'hpreserve'
|
10
|
+
s.version = '0.2.1'
|
11
|
+
s.summary = "eval-safe HTML templates using HTML"
|
12
|
+
s.description = "A humane, eval-safe HTML templating system expressed in HTML."
|
13
|
+
|
14
|
+
s.author = "Matthew Lyon"
|
15
|
+
s.email = "matt@flowerpowered.com"
|
16
|
+
s.homepage = "http://github.com/mattly/hpreserve"
|
17
|
+
|
18
|
+
# code
|
19
|
+
s.require_path = "lib"
|
20
|
+
s.files = %w( README.mkdn Rakefile ) + Dir["{spec,lib}/**/*"]
|
21
|
+
|
22
|
+
# rdoc
|
23
|
+
s.has_rdoc = false
|
24
|
+
|
25
|
+
# Dependencies
|
26
|
+
s.add_dependency "hpricot", [">= 0.6.0"]
|
27
|
+
|
28
|
+
# Requirements
|
29
|
+
s.required_ruby_version = ">= 1.8.6"
|
30
|
+
|
31
|
+
s.platform = Gem::Platform::RUBY
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "create .gemspec file (useful for github)"
|
35
|
+
task :gemspec do
|
36
|
+
filename = "#{spec.name}.gemspec"
|
37
|
+
File.open(filename, "w") do |f|
|
38
|
+
f.puts spec.to_ruby
|
39
|
+
end
|
40
|
+
end
|
data/lib/hpreserve.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
module Hpreserve
|
4
|
+
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'hpreserve/parser'
|
8
|
+
require 'hpreserve/extensions'
|
9
|
+
require 'hpreserve/abstract_cacher'
|
10
|
+
require 'hpreserve/file_cacher'
|
11
|
+
require 'hpreserve/variables'
|
12
|
+
require 'hpreserve/filters'
|
13
|
+
require 'hpreserve/standard_filters'
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Hpreserve
|
2
|
+
class AbstractCacher
|
3
|
+
|
4
|
+
attr_accessor :patterns, :storage
|
5
|
+
|
6
|
+
def initialize(patterns)
|
7
|
+
self.patterns = patterns
|
8
|
+
end
|
9
|
+
|
10
|
+
def match?(variable)
|
11
|
+
pattern = patterns.detect {|p| variable.match(p[:match]) }
|
12
|
+
return nil unless pattern
|
13
|
+
variable.gsub(pattern[:match], pattern[:key])
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# overwrite these in your ConcreteCacher to do use whatever you use
|
18
|
+
|
19
|
+
def retrieve(key)
|
20
|
+
@storage ||= {}
|
21
|
+
storage[key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def store(key, value)
|
25
|
+
@storage ||= {}
|
26
|
+
storage[key] = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def expire(pattern)
|
30
|
+
@storage ||= {}
|
31
|
+
storage.delete_if {|key, value| key.match(pattern) }
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
class Proc
|
5
|
+
def to_hpreserve
|
6
|
+
self.call
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Time
|
11
|
+
def to_hpreserve
|
12
|
+
{ 'default' => self.rfc2822,
|
13
|
+
'year' => self.year, 'month' => self.month, 'day' => self.day,
|
14
|
+
'short' => self.strftime('%e %b %y, %H:%M'),
|
15
|
+
'long' => self.strftime('%A %e %B %Y, %I:%M%p') }
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Hpreserve::FileCacher < Hpreserve::AbstractCacher
|
2
|
+
|
3
|
+
attr_accessor :base_dir
|
4
|
+
|
5
|
+
def initialize(patterns, base)
|
6
|
+
super(patterns)
|
7
|
+
self.base_dir = base
|
8
|
+
end
|
9
|
+
|
10
|
+
def store(key, value)
|
11
|
+
file = File.join(base_dir, key)
|
12
|
+
FileUtils.mkdir_p(File.dirname(file)) unless File.directory?(File.dirname(file))
|
13
|
+
File.open(file, 'w') {|f| f << value }
|
14
|
+
end
|
15
|
+
|
16
|
+
def retrieve(key)
|
17
|
+
file = File.join(base_dir, key)
|
18
|
+
File.read(file) if File.exists?(file)
|
19
|
+
end
|
20
|
+
|
21
|
+
def expire(keys)
|
22
|
+
Dir[File.join(base_dir, keys)].each {|f| File.delete(f) }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Hpreserve
|
2
|
+
|
3
|
+
# todo: rewrite this
|
4
|
+
|
5
|
+
# Acts as a sandbox to run filters in, most everything except the self.parse method
|
6
|
+
# was stolen from Liquid's Strainer class. Almost all the standard class methods
|
7
|
+
# (especially instance_eval) are undefined, and respond_to? is modified to only
|
8
|
+
# return true on methods defined by the filter modules registered.
|
9
|
+
class Filters
|
10
|
+
|
11
|
+
@@filter_modules = []
|
12
|
+
|
13
|
+
def self.register(filterset)
|
14
|
+
[filterset].flatten.each do |mod|
|
15
|
+
raise StandardError("passed filter is not a module") unless mod.is_a?(Module)
|
16
|
+
@@filter_modules << mod
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.create
|
21
|
+
filterset = Filters.new
|
22
|
+
@@filter_modules.each { |m| filterset.extend m }
|
23
|
+
filterset
|
24
|
+
end
|
25
|
+
|
26
|
+
# The filter parser expects a string formatted similar to an html element's "style"
|
27
|
+
# attribute:
|
28
|
+
#
|
29
|
+
# * <tt>filter="date: %d %b; truncate_words: 15, ...; capitalize"</tt>
|
30
|
+
#
|
31
|
+
# (to provide an example of two currently filters you wouldn't use
|
32
|
+
# together...) (todo: better examples fool!). Filters are separated by semicolons
|
33
|
+
# and are executed in order given. If the filter needs arguments, those are given
|
34
|
+
# after a colon and separated by commas.
|
35
|
+
def self.parse(str='')
|
36
|
+
str.split(';').inject([]) do |memo, rule|
|
37
|
+
list = rule.split(':')
|
38
|
+
filter = list.shift.strip
|
39
|
+
set = [filter]
|
40
|
+
unless list.empty?
|
41
|
+
list = list.join(':') # get us back to our original string
|
42
|
+
list = list.split(',') # becase we care about something else
|
43
|
+
set += list.collect {|a| a.strip } # get rid of any pesky whitespace
|
44
|
+
end
|
45
|
+
memo << set
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
@@required_methods = %w(__send__ __id__ debugger run inspect methods respond_to? extend)
|
50
|
+
|
51
|
+
# :nodoc
|
52
|
+
# keeping inspect around simply to make irb happy.
|
53
|
+
def inspect; end
|
54
|
+
|
55
|
+
def respond_to?(method)
|
56
|
+
method_name = method.to_s
|
57
|
+
return false if method_name =~ /^__/
|
58
|
+
return false if @@required_methods.include?(method_name)
|
59
|
+
super
|
60
|
+
end
|
61
|
+
|
62
|
+
def run(filter, node, *args)
|
63
|
+
|
64
|
+
__send__(filter, node, *args) if respond_to?(filter)
|
65
|
+
end
|
66
|
+
|
67
|
+
instance_methods.each do |m|
|
68
|
+
unless @@required_methods.include?(m)
|
69
|
+
undef_method m
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Hpreserve
|
2
|
+
class Parser
|
3
|
+
|
4
|
+
def self.render(doc='', variables={})
|
5
|
+
doc = new(doc)
|
6
|
+
doc.render(variables)
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_accessor :doc, :variables, :filter_sandbox, :include_base, :cacher
|
10
|
+
|
11
|
+
def initialize(doc='')
|
12
|
+
self.doc = Hpricot(doc)
|
13
|
+
self.filter_sandbox = Hpreserve::Filters.create
|
14
|
+
end
|
15
|
+
|
16
|
+
def variables=(vars)
|
17
|
+
@variables = Hpreserve::Variables.new(vars)
|
18
|
+
end
|
19
|
+
|
20
|
+
def render(vars=nil)
|
21
|
+
self.variables = vars unless vars.nil?
|
22
|
+
render_includes
|
23
|
+
render_nodes
|
24
|
+
doc.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
def render_includes
|
28
|
+
(doc/"[@include]").each do |node|
|
29
|
+
var, default = node['include'].split('|').collect {|s| s.strip }
|
30
|
+
incl = variables.substitute(var)
|
31
|
+
incl = [include_base, incl.split('.')].flatten.compact
|
32
|
+
node.inner_html = variables[incl] || variables[default]
|
33
|
+
node.remove_attribute('include')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def render_nodes(base=doc)
|
38
|
+
(base/'meta[@content]').each {|node| node.set_attribute('content', variables.substitute(node['content']))}
|
39
|
+
(base/'[@content]:not(meta)').each {|node| render_node_content(node) }
|
40
|
+
(base/'[@filter]').each {|node| render_node_filters(node) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def render_node_content(node)
|
44
|
+
variable = node.remove_attribute('content').strip
|
45
|
+
cache = cacher.nil? ? nil : cacher.match?(variable)
|
46
|
+
if cache and stored = cacher.retrieve(cache)
|
47
|
+
node.swap(stored)
|
48
|
+
else
|
49
|
+
value = variables[variable.split('.')]
|
50
|
+
value = value['default'] if value.respond_to?(:has_key?)
|
51
|
+
if value.is_a?(Array)
|
52
|
+
render_collection(node, value)
|
53
|
+
else
|
54
|
+
node.inner_html = value
|
55
|
+
end
|
56
|
+
render_node_filters(node) if node['filter']
|
57
|
+
cacher.store(cache, node.to_s) if cache
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def render_node_filters(node)
|
62
|
+
filters = Hpreserve::Filters.parse(node.remove_attribute('filter'))
|
63
|
+
filters.each do |filterset|
|
64
|
+
filter = filterset.shift
|
65
|
+
next unless filter_sandbox.respond_to?(filter)
|
66
|
+
args = filterset.collect {|a| variables.substitute(a) }
|
67
|
+
filter_sandbox.run(filter, node, *args)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def render_collection(node, values=[])
|
72
|
+
variable_name = node.remove_attribute('local') || 'item'
|
73
|
+
base = node.children.detect {|n| !n.is_a?(Hpricot::Text) }
|
74
|
+
base.following.remove
|
75
|
+
base.preceding.remove
|
76
|
+
template = base.to_s
|
77
|
+
values.each_with_index do |value, index|
|
78
|
+
variables.storage[variable_name] = value
|
79
|
+
ele = (Hpricot(template)/'*').first
|
80
|
+
node.insert_after(ele, node.children.last)
|
81
|
+
render_nodes(ele)
|
82
|
+
end
|
83
|
+
base.swap('')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Hpreserve
|
2
|
+
module StandardFilters
|
3
|
+
|
4
|
+
def capitalize(node)
|
5
|
+
node.inner_html = node.inner_text.capitalize
|
6
|
+
node
|
7
|
+
end
|
8
|
+
|
9
|
+
def date(node, strftime)
|
10
|
+
time = Time.parse(node.inner_html)
|
11
|
+
node.inner_html = time.strftime(strftime)
|
12
|
+
node
|
13
|
+
end
|
14
|
+
|
15
|
+
# node modification filters
|
16
|
+
|
17
|
+
def remove(node)
|
18
|
+
node.parent.children.delete(node)
|
19
|
+
end
|
20
|
+
|
21
|
+
def unwrap(node)
|
22
|
+
node.swap node.inner_html
|
23
|
+
end
|
24
|
+
|
25
|
+
def link(node, url)
|
26
|
+
attr(node, 'href', url)
|
27
|
+
node
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_class(node, *klasses)
|
31
|
+
set_class(node, [node.classes, klasses].flatten)
|
32
|
+
node
|
33
|
+
end
|
34
|
+
|
35
|
+
def set_class(node, *klasses)
|
36
|
+
klasses = klasses.flatten.map! {|c| c.gsub(/[^\-\w]+/,'-').gsub(/^[^a-zA-Z]/,'') }
|
37
|
+
attr(node, 'class', klasses.uniq.join(' '))
|
38
|
+
node
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_id(node, id)
|
42
|
+
attr(node, 'id', id)
|
43
|
+
node
|
44
|
+
end
|
45
|
+
|
46
|
+
def attr(node, attrib, value)
|
47
|
+
node.set_attribute(attrib, value)
|
48
|
+
node
|
49
|
+
end
|
50
|
+
|
51
|
+
def attr_on_child(node, child, attrib, value)
|
52
|
+
child = node.at('#'+child)
|
53
|
+
child.set_attribute(attrib, value) if child
|
54
|
+
node
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
Hpreserve::Filters.register(Hpreserve::StandardFilters)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Hpreserve
|
2
|
+
|
3
|
+
class Variables
|
4
|
+
|
5
|
+
attr_accessor :storage
|
6
|
+
|
7
|
+
def initialize(vars={})
|
8
|
+
self.storage = vars
|
9
|
+
end
|
10
|
+
|
11
|
+
# climbs the branches of the variable hash's tree, handling non-hashes along the way.
|
12
|
+
def [](*path)
|
13
|
+
path = path.flatten
|
14
|
+
return '' if path.empty?
|
15
|
+
stack = @storage
|
16
|
+
path.each do |piece|
|
17
|
+
# much of this stolen blatantly from liquid
|
18
|
+
if (stack.respond_to?(:has_key?) and stack.has_key?(piece)) ||
|
19
|
+
(stack.respond_to?(:fetch) and piece =~ /^\d+$/)
|
20
|
+
piece = piece.to_i if piece =~ /^\d+$/
|
21
|
+
stack[piece] = stack[piece].to_hpreserve if stack[piece].respond_to?(:to_hpreserve)
|
22
|
+
stack = stack[piece]
|
23
|
+
elsif %w(first last size).include?(piece) and stack.respond_to?(piece)
|
24
|
+
item = stack.send(piece)
|
25
|
+
stack = item.respond_to?(:to_hpreserve) ? item.to_hpreserve : item
|
26
|
+
else
|
27
|
+
return nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
stack
|
31
|
+
end
|
32
|
+
|
33
|
+
def substitute(str='')
|
34
|
+
str.gsub(/\{([\w\.]+)[\s\|]*([^\{\}]+)?\}/) do |m|
|
35
|
+
val, default = $1, $2
|
36
|
+
self["#{val}".split('.')] || "#{default}";
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hpreserve::AbstractCacher do
|
4
|
+
|
5
|
+
it "returns nil if no match for the given variable" do
|
6
|
+
cacher = Hpreserve::AbstractCacher.new([])
|
7
|
+
cacher.match?('things').should be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "matches against the given variable and return the key" do
|
11
|
+
cacher = Hpreserve::AbstractCacher.new([{:match => %r|^things\.(.*)|, :key => 'thing::\1'}])
|
12
|
+
cacher.match?('things.foo').should == 'thing::foo'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "returns nil if nothing is retrieved from the cache" do
|
16
|
+
cacher = Hpreserve::AbstractCacher.new([])
|
17
|
+
cacher.storage = {}
|
18
|
+
cacher.retrieve('thing::foo').should == nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns the stored value if it exists" do
|
22
|
+
cacher = Hpreserve::AbstractCacher.new([])
|
23
|
+
cacher.storage = {'thing::foo' => 'value'}
|
24
|
+
cacher.retrieve('thing::foo').should == 'value'
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets the value" do
|
28
|
+
cacher = Hpreserve::AbstractCacher.new([])
|
29
|
+
cacher.storage = {}
|
30
|
+
cacher.store('thing::foo', 'value')
|
31
|
+
cacher.storage['thing::foo'].should == 'value'
|
32
|
+
end
|
33
|
+
|
34
|
+
it "expires keys matching a given pattern" do
|
35
|
+
cacher = Hpreserve::AbstractCacher.new([])
|
36
|
+
cacher.storage = {'thing::foo' => 'foo', 'thing::bar' => 'bar', 'non::thing' => 'bee'}
|
37
|
+
cacher.expire(/^thing::.*/)
|
38
|
+
cacher.storage.should == {'non::thing' => 'bee'}
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'tmpdir'
|
2
|
+
require 'digest/md5'
|
3
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
4
|
+
|
5
|
+
describe Hpreserve::FileCacher do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@cacher = Hpreserve::FileCacher.new([], Dir.tmpdir)
|
9
|
+
@key = Digest::MD5.hexdigest("#{Time.now}--#{inspect}")
|
10
|
+
end
|
11
|
+
|
12
|
+
it "knows about its base directory" do
|
13
|
+
@cacher.patterns.should == []
|
14
|
+
@cacher.base_dir.should == Dir.tmpdir
|
15
|
+
end
|
16
|
+
|
17
|
+
it "stores values in files matching the key" do
|
18
|
+
@cacher.store(@key, 'new value')
|
19
|
+
File.should be_file(File.join(Dir.tmpdir, @key))
|
20
|
+
File.read(File.join(Dir.tmpdir, @key)).should == 'new value'
|
21
|
+
end
|
22
|
+
|
23
|
+
it "creates directories as needed if they don't exist" do
|
24
|
+
@cacher.store("#{@key}/foo", 'new value')
|
25
|
+
File.should be_directory(File.join(Dir.tmpdir, @key))
|
26
|
+
File.should be_file(File.join(Dir.tmpdir, @key, 'foo'))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "retrieves values from files matching the key" do
|
30
|
+
File.open(File.join(Dir.tmpdir, @key), 'w') {|f| f << 'existing value'}
|
31
|
+
@cacher.retrieve(@key).should == 'existing value'
|
32
|
+
end
|
33
|
+
|
34
|
+
it "returns nil when retrieving a key for a file that doens't exist" do
|
35
|
+
File.should_not be_file(File.join(Dir.tmpdir, @key))
|
36
|
+
@cacher.retrieve(@key).should be_nil
|
37
|
+
end
|
38
|
+
|
39
|
+
it "deletes files when expiring keys" do
|
40
|
+
files = [1,2].collect {|i| File.join(Dir.tmpdir, "#{@key}-#{i}") }
|
41
|
+
files.each {|file| File.open(file, 'w') {|f| f << 'hi'} }
|
42
|
+
@cacher.expire("#{@key}*")
|
43
|
+
files.each {|file| File.should_not be_file(file)}
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hpreserve::Filters do
|
4
|
+
|
5
|
+
describe "sandbox" do
|
6
|
+
before { @f = Hpreserve::Filters.create }
|
7
|
+
# note: #should and #should_not are undefined at initialize and are not available
|
8
|
+
|
9
|
+
it "extends with @@filter_modules" do
|
10
|
+
@f.respond_to?('capitalize').should be_true
|
11
|
+
end
|
12
|
+
|
13
|
+
it "does not respond to or send non-allowed methods" do
|
14
|
+
lambda { @f.__send__('instance_eval')}.should raise_error(NoMethodError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "executes filters via the 'run' method" do
|
18
|
+
@doc = Hpricot('<span>foo</span>')
|
19
|
+
@f.run('capitalize', @doc.at('span'))
|
20
|
+
@doc.at('span').inner_html.should == 'Foo'
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "filterstring parser" do
|
26
|
+
it "handles single filters without arguments" do
|
27
|
+
Hpreserve::Filters.parse('upcase').should == [['upcase']]
|
28
|
+
end
|
29
|
+
|
30
|
+
it "handles multiple filters without arguments" do
|
31
|
+
Hpreserve::Filters.parse('upcase; downcase').should == [['upcase'], ['downcase']]
|
32
|
+
end
|
33
|
+
|
34
|
+
it "handles single filters with arguments" do
|
35
|
+
Hpreserve::Filters.parse('truncate: 30, ...').should == [['truncate', '30', '...']]
|
36
|
+
end
|
37
|
+
|
38
|
+
it "handles complex filter directives" do
|
39
|
+
Hpreserve::Filters.parse(
|
40
|
+
'truncate: 30, ...; capitalize; link_to: @item.link; add_class: @item.type'
|
41
|
+
).should == [
|
42
|
+
['truncate', '30', '...'], ['capitalize'], ['link_to', '@item.link'],
|
43
|
+
['add_class', '@item.type']
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hpreserve::Parser do
|
4
|
+
|
5
|
+
describe "includes" do
|
6
|
+
before do
|
7
|
+
@doc = Hpreserve::Parser.new("<div include='header'> </div>")
|
8
|
+
@doc.variables = {'header' => 'value'}
|
9
|
+
@doc.render_includes
|
10
|
+
end
|
11
|
+
|
12
|
+
it "replaces includes with their content" do
|
13
|
+
@doc.doc.at('div').inner_html.should == "value"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "removes include attribute from the node" do
|
17
|
+
@doc.doc.at('div').has_attribute?('include').should be_false
|
18
|
+
end
|
19
|
+
|
20
|
+
it "handles namespaced includes" do
|
21
|
+
@doc.doc = Hpricot("<div include='contrived.example'> </div>")
|
22
|
+
@doc.variables = {'contrived' => {'example' => 'value'}}
|
23
|
+
@doc.render_includes
|
24
|
+
@doc.doc.at('div').inner_html.should == "value"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "uses the include_base attribute" do
|
28
|
+
@doc.include_base = 'filesystem'
|
29
|
+
@doc.variables = {'filesystem' => {'header' => 'value'}}
|
30
|
+
@doc.render_includes
|
31
|
+
@doc.doc.at('div').inner_html.should == 'value'
|
32
|
+
end
|
33
|
+
|
34
|
+
it "substitutes variables in the include string" do
|
35
|
+
@doc.doc = Hpricot("<div include='{a}_sidebar'> </div>")
|
36
|
+
@doc.variables = {'a' => 'b', 'b_sidebar' => 'value'}
|
37
|
+
@doc.render_includes
|
38
|
+
@doc.doc.at('div').inner_html.should == 'value'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "ignores a default include if the given include exists" do
|
42
|
+
@doc.doc = Hpricot("<div include='{a}_sidebar | sidebar'> </div>")
|
43
|
+
@doc.variables = {'a' => 'a', 'a_sidebar' => 'value'}
|
44
|
+
@doc.render_includes
|
45
|
+
@doc.doc.at('div').inner_html.should == 'value'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "falls back to a default include if the given value returns empty" do
|
49
|
+
@doc.doc = Hpricot("<div include='{a}_sidebar | sidebar'> </div>")
|
50
|
+
@doc.variables = {'a' => 'b', 'sidebar' => 'value'}
|
51
|
+
@doc.render_includes
|
52
|
+
@doc.doc.at('div').inner_html.should == 'value'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "doesn't care about whitespace (or lack of) in the include directive" do
|
56
|
+
@doc.variables = {'a' => 'a', 'a_sidebar' => 'a_value', 'sidebar' => 'value'}
|
57
|
+
|
58
|
+
["{a}_sidebar|sidebar", " {a}_sidebar | sidebar ", " {a}_sidebar "].each do |i|
|
59
|
+
@doc.doc = Hpricot("<div include='#{i}'> </div>")
|
60
|
+
@doc.render_includes
|
61
|
+
@doc.doc.at('div').inner_html.should == 'a_value'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "simple content replacement" do
|
68
|
+
|
69
|
+
before do
|
70
|
+
@doc = Hpreserve::Parser.new("Hello <span content='name'>Name</span>.")
|
71
|
+
@doc.variables = {'name' => 'Jack'}
|
72
|
+
@doc.render_node_content(@doc.doc.at('span'))
|
73
|
+
end
|
74
|
+
|
75
|
+
it "replaces content= nodes with string content" do
|
76
|
+
@doc.doc.to_plain_text.should == "Hello Jack."
|
77
|
+
end
|
78
|
+
|
79
|
+
it "removes content attributes from the nodes" do
|
80
|
+
@doc.doc.at('span').has_attribute?('content').should be_false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe "content replacement" do
|
85
|
+
|
86
|
+
it "ignores nested content attributes" do
|
87
|
+
@doc = Hpreserve::Parser.new("Hi <span content='name'><span content='firstname'>Name</span></span>")
|
88
|
+
@doc.variables = {'name' => 'Jack Shepherd', 'firstname' => 'Jack'}
|
89
|
+
@doc.doc.search('span').each {|n| @doc.render_node_content(n) }
|
90
|
+
@doc.doc.to_plain_text.should == "Hi Jack Shepherd"
|
91
|
+
end
|
92
|
+
|
93
|
+
it "handles variable paths that end up in hashes" do
|
94
|
+
@doc = Hpreserve::Parser.new("Hi <span content='name'>Name</span>")
|
95
|
+
@doc.variables = {'name' => {'default' => 'Jack Shepherd', 'first' => 'Jack', 'last' => 'Shepherd'}}
|
96
|
+
@doc.render_node_content(@doc.doc.at('span'))
|
97
|
+
@doc.doc.to_plain_text.should == "Hi Jack Shepherd"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "ignores whitespace in variable names" do
|
101
|
+
@doc = Hpreserve::Parser.new("Hi <span content=' name '>Name</span>")
|
102
|
+
@doc.variables = {'name' => 'Jack Shepherd'}
|
103
|
+
@doc.render_node_content(@doc.doc.at('span'))
|
104
|
+
@doc.doc.to_plain_text.should == 'Hi Jack Shepherd'
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "on meta tags" do
|
108
|
+
before do
|
109
|
+
@doc = Hpreserve::Parser.new("<head><meta name='foo' content='plain content' /><meta name='bar' content='{bar}' /></head>")
|
110
|
+
@doc.variables = {'bar' => 'value'}
|
111
|
+
@doc.render
|
112
|
+
end
|
113
|
+
|
114
|
+
it "does not insert the content into the node" do
|
115
|
+
@doc.doc.at('meta[@name=foo]').inner_html.should == ''
|
116
|
+
@doc.doc.at('meta[@name=bar]').inner_html.should == ''
|
117
|
+
end
|
118
|
+
|
119
|
+
it "ignores lack of variables in content string" do
|
120
|
+
@doc.doc.at('meta[@name=foo]')['content'].should == 'plain content'
|
121
|
+
end
|
122
|
+
|
123
|
+
it "substitutes variables in content string" do
|
124
|
+
@doc.doc.at('meta[@name=bar]')['content'].should == 'value'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
describe "collections" do
|
131
|
+
|
132
|
+
it "handles simple collections" do
|
133
|
+
@doc = Hpreserve::Parser.new("<ul content='items' local='item'><li content='item'>One</li><li>Another</li></ul>")
|
134
|
+
@doc.variables = {'items' => %w(one two three four)}
|
135
|
+
@doc.render_node_content(@doc.doc.at('ul'))
|
136
|
+
@doc.doc.to_html.should == "<ul><li>one</li><li>two</li><li>three</li><li>four</li></ul>"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "handles nested collections" do
|
140
|
+
@doc = Hpreserve::Parser.new("<ul content='sections' local='section'><li><span content='section.name'>Section</span>: <span content='section.authors' local='author'><span content='author'>Author</span></span></li></ul>")
|
141
|
+
@doc.variables = {'sections' => [{'name' => 'one', 'authors' => ['me', 'you']}, {'name' => 'two', 'authors' => ['me']}]}
|
142
|
+
@doc.render_node_content(@doc.doc.at('ul'))
|
143
|
+
@doc.doc.to_html.should == "<ul><li><span>one</span>: <span><span>me</span><span>you</span></span></li><li><span>two</span>: <span><span>me</span></span></li></ul>"
|
144
|
+
end
|
145
|
+
|
146
|
+
it "handles whitespace" do
|
147
|
+
@doc = Hpreserve::Parser.new("<ul content='items' local='item'>
|
148
|
+
|
149
|
+
<li content='item'>1</li>
|
150
|
+
|
151
|
+
</ul>")
|
152
|
+
@doc.variables = {'items' => ['one']}
|
153
|
+
@doc.render_node_content(@doc.doc.at('ul'))
|
154
|
+
@doc.doc.to_html.should == "<ul><li>one</li></ul>"
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "caching" do
|
160
|
+
before do
|
161
|
+
@doc = Hpreserve::Parser.new("<span content='some.thing'>non-rendered</span>")
|
162
|
+
@doc.variables = {'some' => {'thing' => 'from variables'}}
|
163
|
+
cacher_matches([{:match => /^some\.thing/, :key => 'something'}])
|
164
|
+
end
|
165
|
+
|
166
|
+
def cacher_matches(match=[])
|
167
|
+
@doc.cacher = Hpreserve::AbstractCacher.new(match)
|
168
|
+
end
|
169
|
+
|
170
|
+
def render
|
171
|
+
@doc.render_node_content(@doc.doc.at('span'))
|
172
|
+
end
|
173
|
+
|
174
|
+
it "ignores the cacher if no match is found" do
|
175
|
+
cacher_matches()
|
176
|
+
render
|
177
|
+
@doc.doc.to_s.should == '<span>from variables</span>'
|
178
|
+
end
|
179
|
+
|
180
|
+
it "checks the cache if a match is found" do
|
181
|
+
@doc.cacher.should_receive(:retrieve).with('something')
|
182
|
+
render
|
183
|
+
end
|
184
|
+
|
185
|
+
it "uses the value from the cache if a match is found and the key exists in the cache" do
|
186
|
+
@doc.cacher.store('something', 'from cacher')
|
187
|
+
render
|
188
|
+
@doc.doc.to_s.should == 'from cacher'
|
189
|
+
end
|
190
|
+
|
191
|
+
it "does not render if a match is found and the key exists in the cache" do
|
192
|
+
@doc.cacher.store('something', 'from cacher')
|
193
|
+
@doc.variables.should_not_receive(:[])
|
194
|
+
render
|
195
|
+
end
|
196
|
+
|
197
|
+
it "stores the value in the cache if a match is found and no key is pre-existing" do
|
198
|
+
@doc.cacher.should_receive(:store).with('something','<span>from variables</span>')
|
199
|
+
render
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "filter handler" do
|
204
|
+
|
205
|
+
it "runs the given filterset on a node" do
|
206
|
+
@doc = Hpreserve::Parser.new("<span filter='capitalize'>foo</span>")
|
207
|
+
@doc.render_node_filters(@doc.doc.at('span'))
|
208
|
+
@doc.doc.at('span').inner_html.should == 'Foo'
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
gem 'rspec'
|
6
|
+
require 'spec'
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'rubygems'
|
10
|
+
gem 'ruby-debug'
|
11
|
+
require 'ruby-debug'
|
12
|
+
|
13
|
+
require "hpricot"
|
14
|
+
require "#{File.dirname(__FILE__)}/../lib/hpreserve"
|
15
|
+
|
16
|
+
Debugger.start
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hpreserve::StandardFilters do
|
4
|
+
|
5
|
+
describe "upcase" do
|
6
|
+
before { @f = Hpreserve::Filters.create }
|
7
|
+
|
8
|
+
it "capitalizes the text of the node" do
|
9
|
+
@doc = Hpricot("<span>foo</span>")
|
10
|
+
@f.run 'capitalize', @doc.at('span')
|
11
|
+
@doc.at('span').inner_text.should == 'Foo'
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "date" do
|
17
|
+
before { @f = Hpreserve::Filters.create }
|
18
|
+
|
19
|
+
it "handles strftime arguments" do
|
20
|
+
@doc = Hpricot("<span>Wed, 26 Mar 2008 23:45:55 -0700</span>")
|
21
|
+
@f.run 'date', @doc.at('span'), '%e %b %y'
|
22
|
+
@doc.at('span').inner_html.should == '26 Mar 08'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
describe "remove" do
|
28
|
+
before { @f = Hpreserve::Filters.create }
|
29
|
+
|
30
|
+
it "removes the node from the document" do
|
31
|
+
@doc = Hpricot("<div>blah <div>Attention Client: This will be cooler</div><div>blah</div></div>")
|
32
|
+
@f.run 'remove', @doc.at('div div')
|
33
|
+
@doc.to_plain_text.should == "blah blah"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "unwrap" do
|
38
|
+
before { @f = Hpreserve::Filters.create }
|
39
|
+
|
40
|
+
it "replaces the node with its content" do
|
41
|
+
@doc = Hpricot("<div>Value</div>")
|
42
|
+
@f.run 'unwrap', @doc.at('div')
|
43
|
+
@doc.to_s.should == 'Value'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "link" do
|
48
|
+
before { @f = Hpreserve::Filters.create }
|
49
|
+
|
50
|
+
it "sets the href attribute to the url value" do
|
51
|
+
@doc = Hpricot("<a href=''>Foo</a>")
|
52
|
+
@f.run 'link', @doc.at('a'), 'foo.com'
|
53
|
+
@doc.at('a')['href'].should == 'foo.com'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "add_class" do
|
58
|
+
before { @f = Hpreserve::Filters.create }
|
59
|
+
|
60
|
+
it "appends the class to the element's classes" do
|
61
|
+
@doc = Hpricot("<span class='foo'>Foo</span>")
|
62
|
+
@f.run 'add_class', @doc.at('span'), 'bar'
|
63
|
+
@doc.at('span').classes.should == ['foo', 'bar']
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "set_class" do
|
68
|
+
before do
|
69
|
+
@f = Hpreserve::Filters.create
|
70
|
+
@doc = Hpricot("<span class='foo'>Foo</span>")
|
71
|
+
end
|
72
|
+
|
73
|
+
it "replacess the element's classes" do
|
74
|
+
@f.run 'set_class', @doc.at('span'), 'bar'
|
75
|
+
@doc.at('span').classes.should == ['bar']
|
76
|
+
end
|
77
|
+
|
78
|
+
it "santizes the class name" do
|
79
|
+
@f.run 'set_class', @doc.at('span'), '.foo and bar'
|
80
|
+
@doc.at('span').classes.should == ['foo-and-bar']
|
81
|
+
end
|
82
|
+
|
83
|
+
it "handles multiple class names with commas" do
|
84
|
+
@f.run 'set_class', @doc.at('span'), 'foo', 'bar'
|
85
|
+
@doc.at('span').classes.should == %w(foo bar)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "set_id" do
|
91
|
+
before { @f = Hpreserve::Filters.create }
|
92
|
+
|
93
|
+
it "sets the element's id" do
|
94
|
+
@doc = Hpricot("<span>foo</span>")
|
95
|
+
@f.run 'set_id', @doc.at('span'), 'bar'
|
96
|
+
@doc.at('span')['id'].should == 'bar'
|
97
|
+
end
|
98
|
+
|
99
|
+
it "replaces the element's id" do
|
100
|
+
@doc = Hpricot("<span id='foo'>foo</span>")
|
101
|
+
@f.run 'set_id', @doc.at('span'), 'bar'
|
102
|
+
@doc.at('span')['id'].should == 'bar'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
describe "attr" do
|
108
|
+
before { @f = Hpreserve::Filters.create }
|
109
|
+
|
110
|
+
it "sets the attr for src" do
|
111
|
+
@doc = Hpricot('<span>foo</span>')
|
112
|
+
@f.run 'attr', @doc.at('span'), 'src', '/lolcat.jpg'
|
113
|
+
@doc.at('span')['src'].should == '/lolcat.jpg'
|
114
|
+
end
|
115
|
+
|
116
|
+
it "clobbers the attr for src" do
|
117
|
+
@doc = Hpricot('<img src="/loldog.jpg" />')
|
118
|
+
@f.run 'attr', @doc.at('img'), 'src', '/lolcat.jpg'
|
119
|
+
@doc.at('img')['src'].should == '/lolcat.jpg'
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "attr_on_child" do
|
124
|
+
before { @f = Hpreserve::Filters.create }
|
125
|
+
|
126
|
+
it "sets the attr on the named child element" do
|
127
|
+
@doc = Hpricot('<div><span id="foo">foo</span><span id="bar">bar</span></div>')
|
128
|
+
@f.run 'attr_on_child', @doc.at('div'), 'foo', 'class', 'active'
|
129
|
+
@doc.at('#foo').classes.should == ['active']
|
130
|
+
end
|
131
|
+
|
132
|
+
it "handles a child not found" do
|
133
|
+
html = '<div><span id="bar">bar</span></div>'
|
134
|
+
@doc = Hpricot(html)
|
135
|
+
@f.run 'attr_on_child', @doc.at('div'), 'foo', 'class', 'active'
|
136
|
+
@doc.to_s.should == html
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe Hpreserve::Variables do
|
4
|
+
|
5
|
+
describe "initialization" do
|
6
|
+
it "stores the variables in @storage" do
|
7
|
+
v = Hpreserve::Variables.new('foo')
|
8
|
+
v.instance_variable_get(:@storage).should == 'foo'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "substitute" do
|
13
|
+
before do
|
14
|
+
@var = Hpreserve::Variables.new({'a' => 'b', 'b' => {'c' => 'val'}})
|
15
|
+
end
|
16
|
+
|
17
|
+
it "substitutes variables" do
|
18
|
+
@var.substitute('{a}').should == 'b'
|
19
|
+
end
|
20
|
+
|
21
|
+
it "substitutes variables with other things around them" do
|
22
|
+
@var.substitute('foo{a}ar').should == 'foobar'
|
23
|
+
end
|
24
|
+
|
25
|
+
it "substitutes multiple variables in the string" do
|
26
|
+
@var.substitute('foo{a}ar_{b.c}').should == 'foobar_val'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "ignores strings without substitute values" do
|
30
|
+
@var.substitute('foo').should == 'foo'
|
31
|
+
end
|
32
|
+
|
33
|
+
it "uses defaults if no value found" do
|
34
|
+
@var.substitute('{foo | bar}').should == 'bar'
|
35
|
+
end
|
36
|
+
|
37
|
+
it "ignores defaults if value found" do
|
38
|
+
@var.substitute('{a | bar}').should == 'b'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "ignores whitespace around default" do
|
42
|
+
@var.substitute('{foo | bar}').should == 'bar'
|
43
|
+
@var.substitute('{foo|bar}').should == 'bar'
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns an empty string if no default and no value found" do
|
47
|
+
@var.substitute('{foo}').should == ''
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "retrieval" do
|
52
|
+
before do
|
53
|
+
@var = Hpreserve::Variables.new({'a' => {'b' => {'c' => 'value'}}})
|
54
|
+
end
|
55
|
+
|
56
|
+
it "ignores requests for empty arrays" do
|
57
|
+
@var[[]].should == ''
|
58
|
+
end
|
59
|
+
|
60
|
+
it "pulls the variables out of the nest" do
|
61
|
+
@var['a','b','c'].should == 'value'
|
62
|
+
@var[%w(a b c)].should == 'value'
|
63
|
+
end
|
64
|
+
|
65
|
+
it "doesn't have a problem with non-existant variables" do
|
66
|
+
@var[%w(z y x)].should == nil
|
67
|
+
end
|
68
|
+
|
69
|
+
it "calls proc variables" do
|
70
|
+
@var.storage['x'] = proc { 'value' }
|
71
|
+
@var['x'].should == 'value'
|
72
|
+
end
|
73
|
+
|
74
|
+
it "replaces proc variables with their results, thus calling them only once" do
|
75
|
+
i = 0
|
76
|
+
@var.storage['x'] = proc { i+=1 }
|
77
|
+
3.times { @var['x'] }
|
78
|
+
@var['x'].should == 1
|
79
|
+
end
|
80
|
+
|
81
|
+
it "descends into proc variables" do
|
82
|
+
@var.storage['x'] = proc { {'a' => 'value'} }
|
83
|
+
@var[%w(x a)].should == 'value'
|
84
|
+
end
|
85
|
+
|
86
|
+
it "handles date and time variables" do
|
87
|
+
time = Time.now
|
88
|
+
@var.storage['today'] = time
|
89
|
+
@var['today']['default'].should == time.rfc2822
|
90
|
+
@var['today']['year'].should == time.year
|
91
|
+
end
|
92
|
+
|
93
|
+
it "knows the size of arrays" do
|
94
|
+
@var.storage['x'] = %w(one two three four)
|
95
|
+
@var['x','size'].should == 4
|
96
|
+
end
|
97
|
+
|
98
|
+
it "handles first and last on arrays" do
|
99
|
+
@var.storage['x'] = %w(one two three four)
|
100
|
+
@var['x','first'].should == 'one'
|
101
|
+
@var['x','last'].should == 'four'
|
102
|
+
end
|
103
|
+
|
104
|
+
it "handles numbers on arrays" do
|
105
|
+
@var.storage['x'] = %w(one two three four)
|
106
|
+
@var['x','1'].should == 'two'
|
107
|
+
end
|
108
|
+
|
109
|
+
it "handles procs in arrays" do
|
110
|
+
@var.storage['x'] = [proc {'one'}, proc {'two'}]
|
111
|
+
@var['x','first'].should == 'one'
|
112
|
+
@var['x','1'].should == 'two'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mattly-hpreserve
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matthew Lyon
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-07-06 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: hpricot
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.6.0
|
23
|
+
version:
|
24
|
+
description: A humane, eval-safe HTML templating system expressed in HTML
|
25
|
+
email: matt@flowerpowered.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files: []
|
31
|
+
|
32
|
+
files:
|
33
|
+
- README.mkdn
|
34
|
+
- Rakefile
|
35
|
+
- spec/abstract_cacher_spec.rb
|
36
|
+
- spec/file_cacher_spec.rb
|
37
|
+
- spec/filters_spec.rb
|
38
|
+
- spec/parser_spec.rb
|
39
|
+
- spec/spec.opts
|
40
|
+
- spec/spec_helper.rb
|
41
|
+
- spec/standard_filters_spec.rb
|
42
|
+
- spec/variables_spec.rb
|
43
|
+
- lib/hpreserve
|
44
|
+
- lib/hpreserve/abstract_cacher.rb
|
45
|
+
- lib/hpreserve/extensions.rb
|
46
|
+
- lib/hpreserve/file_cacher.rb
|
47
|
+
- lib/hpreserve/filters.rb
|
48
|
+
- lib/hpreserve/parser.rb
|
49
|
+
- lib/hpreserve/standard_filters.rb
|
50
|
+
- lib/hpreserve/variables.rb
|
51
|
+
- lib/hpreserve.rb
|
52
|
+
has_rdoc: false
|
53
|
+
homepage: http://github.com/mattly/hpreserve
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 1.8.6
|
64
|
+
version:
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 1.2.0
|
75
|
+
signing_key:
|
76
|
+
specification_version: 2
|
77
|
+
summary: eval-safe HTML templates using HTML
|
78
|
+
test_files: []
|
79
|
+
|