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 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
@@ -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
@@ -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
+