h2o 0.2 → 0.3.0
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/.autotest +11 -0
- data/.gitignore +1 -0
- data/.project +17 -0
- data/README.md +0 -0
- data/Rakefile +32 -0
- data/TODO.md +5 -0
- data/VERSION +1 -0
- data/benchmark/parser.rb +57 -0
- data/benchmark/source.html +4212 -0
- data/example/h2o/base.html +0 -1
- data/example/h2o/index.html +10 -0
- data/example/h2o/layout.html +11 -0
- data/example/request.html +0 -0
- data/example/server +1 -1
- data/h2o.gemspec +101 -41
- data/init.rb +1 -0
- data/lib/.DS_Store +0 -0
- data/lib/core_ext/object.rb +0 -0
- data/lib/h2o.rb +29 -39
- data/lib/h2o/constants.rb +2 -2
- data/lib/h2o/context.rb +3 -10
- data/lib/h2o/error.rb +9 -0
- data/lib/h2o/filters/default.rb +0 -0
- data/lib/h2o/nodes.rb +5 -1
- data/lib/h2o/tags.rb +1 -6
- data/lib/h2o/tags/block.rb +0 -30
- data/lib/h2o/tags/extends.rb +33 -0
- data/lib/h2o/tags/for.rb +7 -6
- data/lib/h2o/tags/with.rb +0 -0
- data/lib/h2o/template.rb +33 -0
- data/spec/fixtures/_partial.html +0 -0
- data/spec/fixtures/a.html +1 -0
- data/spec/fixtures/b.html +0 -0
- data/spec/fixtures/deep/folder/c.html +0 -0
- data/spec/h2o/context_spec.rb +134 -0
- data/spec/h2o/default.html +93 -0
- data/spec/h2o/file_loader_spec.rb +35 -0
- data/spec/h2o/filters_spec.rb +57 -0
- data/spec/h2o/parser_spec.rb +78 -0
- data/spec/h2o/tags/block_spec.rb +26 -0
- data/spec/h2o/tags/for_spec.rb +65 -0
- data/spec/h2o/tags/if_spec.rb +58 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +24 -0
- metadata +81 -27
- data/lib/h2o/errors.rb +0 -7
- data/lib/h2o/tags/recurse.rb +0 -55
data/lib/h2o/tags/with.rb
CHANGED
File without changes
|
data/lib/h2o/template.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module H2o
|
2
|
+
class Template
|
3
|
+
attr_reader :context
|
4
|
+
|
5
|
+
def initialize (file, env = {})
|
6
|
+
@file = file
|
7
|
+
@nodelist = Template.load(@file, env)
|
8
|
+
end
|
9
|
+
|
10
|
+
def render (context = {})
|
11
|
+
@context = Context.new(context)
|
12
|
+
output_stream = []
|
13
|
+
@nodelist.render(@context, output_stream)
|
14
|
+
output_stream.join
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.parse source, env = {}
|
18
|
+
parser = Parser.new(source, false, env)
|
19
|
+
parsed = parser.parse
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.load file, env = {}
|
23
|
+
unless H2o.loader
|
24
|
+
env[:searchpath] ||= File.expand_path('../', file)
|
25
|
+
H2o.loader = H2o::FileLoader.new(env[:searchpath])
|
26
|
+
end
|
27
|
+
source = H2o.loader.read(file)
|
28
|
+
|
29
|
+
parser = Parser.new(source, file, env)
|
30
|
+
parser.parse
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
{{ hello }}
|
File without changes
|
File without changes
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class User
|
4
|
+
h2o_expose :name, :age
|
5
|
+
attr_accessor :name, :age
|
6
|
+
|
7
|
+
def initialize name, age
|
8
|
+
@name, @age = name, age
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_h2o
|
12
|
+
{
|
13
|
+
:name => self.name,
|
14
|
+
:age => self.age
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe H2o::Context do
|
20
|
+
before do
|
21
|
+
@scope = {
|
22
|
+
:person => {'name' => 'peter', 'age' => 18},
|
23
|
+
:weather => 'sunny and warm',
|
24
|
+
:items => ['apple', 'orange', 'pear'],
|
25
|
+
:user => User.new('taylor', 19)
|
26
|
+
}
|
27
|
+
@context = H2o::Context.new(@scope)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
describe "Resolve name" do
|
32
|
+
it "should resolve name with either string or symbol" do
|
33
|
+
@context.resolve(:person).should == @scope[:person]
|
34
|
+
@context.resolve(:person).should == @scope[:person]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should return nil for non existing name context" do
|
38
|
+
@context.resolve(:something).should be_nil
|
39
|
+
@context.resolve(:where_is_that).should be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should be able to resolve local variables" do
|
43
|
+
@context.resolve(:person).should_not be_nil
|
44
|
+
@context.resolve(:'person.name').should == 'peter'
|
45
|
+
@context.resolve(:'person.age').should == 18
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should resolve array index using (dot)" do
|
49
|
+
@context.resolve(:'items.0').should == 'apple'
|
50
|
+
@context.resolve(:'items.1').should == 'orange'
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should resolve array methods such as length, count ..." do
|
54
|
+
@context.resolve(:'items.length').should == @scope[:items].length
|
55
|
+
@context.resolve(:'items.first').should == 'apple'
|
56
|
+
@context.resolve(:'items.last').should == 'pear'
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should resolve object methods" do
|
60
|
+
|
61
|
+
@context.resolve(:'user.name').should == 'taylor'
|
62
|
+
@context.resolve(:'user.age').should == 19
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
it "should resolve proc object and cache inline" do
|
67
|
+
@context.stack do
|
68
|
+
@context['procs'] = {
|
69
|
+
:test => lambda{ "testing" },
|
70
|
+
:generation => lambda{ Time.now }
|
71
|
+
}
|
72
|
+
|
73
|
+
# Resolve a proc object
|
74
|
+
@context.resolve(:'procs.test').should == 'testing'
|
75
|
+
result = @context.resolve(:'procs.generation')
|
76
|
+
result.should be_a(Time)
|
77
|
+
|
78
|
+
# Cached inline
|
79
|
+
@context.resolve(:'procs.generation').usec.should == result.usec
|
80
|
+
@context.resolve(:'procs.generation').usec.should == result.usec
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "Local lookup" do
|
86
|
+
it "should allow local variable lookup with using symbol" do
|
87
|
+
@context[:person].should be_kind_of(Hash)
|
88
|
+
@context[:weather].should =~ /sunny/
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should able to set new name and value into context" do
|
93
|
+
@context[:city] = 'five dock'
|
94
|
+
@context[:state] = 'nsw'
|
95
|
+
|
96
|
+
@context[:city].should == 'five dock'
|
97
|
+
@context[:state].should == 'nsw'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "Context stack" do
|
102
|
+
it "should allow pushing and popping local context layer in the stack" do
|
103
|
+
@context.push
|
104
|
+
@context[:ass] = 'hole'
|
105
|
+
@context[:ass].should == 'hole'
|
106
|
+
@context.pop
|
107
|
+
|
108
|
+
@context.push(:bowling => 'on sunday')
|
109
|
+
@context[:bowling].should_not be_nil
|
110
|
+
@context.pop
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should allow using stack method to ease push/pop and remain in local context" do
|
114
|
+
@context.stack do
|
115
|
+
@context[:name] = 'a'
|
116
|
+
|
117
|
+
@context.stack do
|
118
|
+
@context[:name] = 'b'
|
119
|
+
|
120
|
+
@context.stack do
|
121
|
+
@context[:age] = 19
|
122
|
+
@context[:name] = 'c'
|
123
|
+
@context.resolve(:'name').should == 'c'
|
124
|
+
end
|
125
|
+
|
126
|
+
@context.resolve(:'age').should == nil
|
127
|
+
@context.resolve(:'name').should == 'b'
|
128
|
+
end
|
129
|
+
|
130
|
+
@context.resolve(:'name') == 'a'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml">
|
3
|
+
<head>
|
4
|
+
<meta content="text/html; charset=ISO-8859-1" http-equiv="content-type" />
|
5
|
+
<title>{{ site.title | capitalize }} - Design, Development and New Media Marketing</title>
|
6
|
+
|
7
|
+
{{ 'stylesheets/layout.css'| css_tag }}
|
8
|
+
|
9
|
+
<!--[if IE]>
|
10
|
+
<style type="text/css">
|
11
|
+
@import "ie_layout.css";
|
12
|
+
</style>
|
13
|
+
<![endif]-->
|
14
|
+
</head>
|
15
|
+
<body>
|
16
|
+
<div id="centered">
|
17
|
+
<div id="top">
|
18
|
+
<!-- Left column -->
|
19
|
+
<div id="left">
|
20
|
+
<!-- Logo -->
|
21
|
+
<a href="#" id="logo">jnCentral.com</a>
|
22
|
+
<!-- Mini navigation -->
|
23
|
+
<ul id="mininav">
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
{% if page.children %}
|
28
|
+
<li><a href="#" class="active"><span class="homepage">Homepage</span></a></li>
|
29
|
+
<ul>
|
30
|
+
{% for page in page.children %}
|
31
|
+
<li>{{ page.title | links_to page.url }}</li>
|
32
|
+
{% endfor %}
|
33
|
+
</ul>
|
34
|
+
{% endif %}
|
35
|
+
|
36
|
+
<li><a href="#"><span class="products">View our products catalog</span></a></li>
|
37
|
+
<li><a href="#"><span class="services">Check out our wide range of services</span></a></li>
|
38
|
+
<li><a href="#"><span class="about">Find out about our company</span></a></li>
|
39
|
+
<li><a href="#"><span class="contact">Get in contact with us</span></a></li>
|
40
|
+
</ul>
|
41
|
+
<!-- Gallery -->
|
42
|
+
<div id="gallery">
|
43
|
+
<div>
|
44
|
+
<ul>
|
45
|
+
<li><a href="#"><img src="images/img1.gif" alt="" /></a></li>
|
46
|
+
<li><a href="#"><img src="images/img2.gif" alt="" /></a></li>
|
47
|
+
<li><a href="#"><img src="images/img3.gif" alt="" /></a></li>
|
48
|
+
</ul>
|
49
|
+
<ul>
|
50
|
+
<li><a href="#"><img src="images/img5.gif" alt="" /></a></li>
|
51
|
+
<li><a href="#"><img src="images/img6.gif" alt="" /></a></li>
|
52
|
+
<li><a href="#"><img src="images/img7.gif" alt="" /></a></li>
|
53
|
+
</ul>
|
54
|
+
</div>
|
55
|
+
<img src="{{'images/mypics.gif'| base_url}}" alt="" class="floatright" />
|
56
|
+
</div>
|
57
|
+
<!-- Latest News -->
|
58
|
+
<div id="news">
|
59
|
+
<h2 id="latestnews">Latest News</h2>
|
60
|
+
|
61
|
+
{% for article in site.latest_articles %}
|
62
|
+
{{ article.title| links_to article.url }} <span class="date">{{article.published_at}}</span><br />
|
63
|
+
<p>{{ article.excerpt}}</p>
|
64
|
+
{% endfor %}
|
65
|
+
|
66
|
+
<br />
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
<!-- Right column -->
|
70
|
+
<div id="right">
|
71
|
+
<!-- Main navigation -->
|
72
|
+
<ul id="navigation">
|
73
|
+
<li><a href="#"><span>Home</span></a></li>
|
74
|
+
<li><a href="#"><span>Products</span></a></li>
|
75
|
+
<li><a href="#"><span>Services</span></a></li>
|
76
|
+
<li><a href="#"><span>Contact us</span></a></li>
|
77
|
+
</ul>
|
78
|
+
{{ content_for_layout }}
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
</div>
|
82
|
+
<!-- Footer -->
|
83
|
+
<div id="footer">
|
84
|
+
<div class="copyright">
|
85
|
+
<h4>Copyright © jnCentral.com. All rights reserved.</h4>
|
86
|
+
<!-- Mini navigation -->
|
87
|
+
<div class="nav">
|
88
|
+
<a href="#">Home</a> <a href="#">Products</a> <a href="#">Services</a> <a href="#">About us</a> <a href="#">Contact</a>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
</body>
|
93
|
+
</html>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe H2o::FileLoader do
|
4
|
+
before do
|
5
|
+
path = File.expand_path("../../fixtures", __FILE__)
|
6
|
+
@loader = H2o::FileLoader.new(path)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "search for template on a search path on file system" do
|
10
|
+
@loader.exist?('a.html').should == true
|
11
|
+
@loader.exist?('deep/folder/c.html').should == true
|
12
|
+
@loader.exist?('non-existence.html').should == false
|
13
|
+
end
|
14
|
+
|
15
|
+
it "read for files on the searchpath" do
|
16
|
+
@loader.read('a.html').should == '{{ hello }}'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "raises error when template doesn't exist" do
|
20
|
+
expect { @loader.read('non-existence.html') }.should raise_error
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "H2o::HashLoader" do
|
25
|
+
it "read file on the same namespace in a hash" do
|
26
|
+
H2o.loader = H2o::HashLoader.new(
|
27
|
+
'base.html' => '{% block content %}test{% endblock %}',
|
28
|
+
'a.html' => "{% extends 'base.html' %}{% block content %}test2{% endblock %}"
|
29
|
+
)
|
30
|
+
H2o.loader.read('base.html').should == '{% block content %}test{% endblock %}'
|
31
|
+
|
32
|
+
H2o::Template.new('base.html').render.should == 'test'
|
33
|
+
H2o::Template.new('a.html').render.should == 'test2'
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe H2o::Filters do
|
4
|
+
it "should pass input object as first param" do
|
5
|
+
H2o::Filters << TestFilters
|
6
|
+
|
7
|
+
# try a standard filter
|
8
|
+
render('{{ "something" | upper }}').should == 'SOMETHING'
|
9
|
+
|
10
|
+
# # try a array input object
|
11
|
+
list = ["man","women"]
|
12
|
+
render('{{ object| test_filter }}', :object => list).should == list.inspect
|
13
|
+
|
14
|
+
# Try a array subclass
|
15
|
+
list = CustomCollection.new(["man", "woman"])
|
16
|
+
render('{{ object| test_filter }}', :object => list).should == list.inspect
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should be able to pass aditional parameters" do
|
20
|
+
render('{{ "test"| test_filter_2 1, 2 }}').should == 'test-1-2'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe DefaultFilters do
|
25
|
+
it "should privide a set of default filters" do
|
26
|
+
|
27
|
+
render('{{ "test" |upper }}').should == 'TEST'
|
28
|
+
|
29
|
+
render('{{ "TEST" |lower }}').should == 'test'
|
30
|
+
|
31
|
+
render('{{ "test" |capitalize }}').should == 'Test'
|
32
|
+
|
33
|
+
render('{{ list |first }}', :list => [1,2]).should == "1"
|
34
|
+
|
35
|
+
render('{{ list |last }}', :list => [1,2]).should == "2"
|
36
|
+
|
37
|
+
render('{{ list |join }}', :list => [1,2]).should == "1, 2"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
module TestFilters
|
43
|
+
def test_filter (input)
|
44
|
+
"#{input.inspect}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_filter_2 (string, param1, param2)
|
48
|
+
"#{string}-#{param1}-#{param2}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class CustomCollection < Array
|
53
|
+
end
|
54
|
+
|
55
|
+
def render(src, context = {})
|
56
|
+
H2o::Template.parse(src).render(context)
|
57
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Parser regex" do
|
4
|
+
|
5
|
+
it "should match whitespace" do
|
6
|
+
H2o::WHITESPACE_RE.should =~ ' '
|
7
|
+
H2o::WHITESPACE_RE.should =~ ' '
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should match variable names" do
|
11
|
+
H2o::NAME_RE.should =~ 'variable'
|
12
|
+
H2o::NAME_RE.should =~ 'variable.property'
|
13
|
+
H2o::NAME_RE.should =~ 'variable.property'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should match string" do
|
17
|
+
H2o::STRING_RE.should =~ '"this is a string"'
|
18
|
+
H2o::STRING_RE.should =~ '"She has \"The thing\""'
|
19
|
+
H2o::STRING_RE.should =~ "'the doctor is good'"
|
20
|
+
H2o::STRING_RE.should =~ "'She can\'t do it'"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should match numeric values" do
|
24
|
+
H2o::NUMBER_RE.should =~ '1.2'
|
25
|
+
H2o::NUMBER_RE.should =~ '-3.2'
|
26
|
+
H2o::NUMBER_RE.should =~ '100000'
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should match operators" do
|
30
|
+
%w(== > < >= <= != ! not and or).each do |operator|
|
31
|
+
H2o::OPERATOR_RE.should =~ operator
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should match named arguments" do
|
36
|
+
named_args = ["name: 'peter'", 'name: object.property', 'price: 2.3', 'age: 29', 'alt: "my company logo"']
|
37
|
+
named_args.each do |arg|
|
38
|
+
H2o::NAMED_ARGS_RE.should =~ arg
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe 'H2o::Parser argument parsing' do
|
44
|
+
|
45
|
+
it "should parse named arguments" do
|
46
|
+
r = H2o::Parser.parse_arguments(
|
47
|
+
"something | filter 11, name: 'something', age: 18, var: variable, active: true"
|
48
|
+
)
|
49
|
+
|
50
|
+
r.should == [:something,
|
51
|
+
[:filter, 11, {
|
52
|
+
:name=> "something",
|
53
|
+
:age => 18,
|
54
|
+
:var=> :variable,
|
55
|
+
:active => true }
|
56
|
+
]
|
57
|
+
]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "Whitespace stripping syntax" do
|
62
|
+
it "should rstrip previous text node for {%- %}" do
|
63
|
+
H2o::Template.parse(' {%- if true %}{% endif %}').render.should == ''
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should lstrip next text node for {% -%}" do
|
67
|
+
H2o::Template.parse('{% if true -%} {% endif %}').render.should == ''
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should strip whitespace on both site with {%- -%}" do
|
71
|
+
H2o::Template.parse('
|
72
|
+
{%- if true -%}
|
73
|
+
hello
|
74
|
+
{%- endif -%}
|
75
|
+
').render.should == 'hello'
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe H2o::Tags::Block do
|
4
|
+
|
5
|
+
it "should render block content" do
|
6
|
+
parse('{% block something %}content{% endblock %}').render.should == 'content'
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
context "block variable" do
|
11
|
+
it "should return block name" do
|
12
|
+
parse(block(:something, '{{ block.name }}')).render.should == 'something'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return current block depth" do
|
16
|
+
source = block(:something, '{{ block.depth }}')
|
17
|
+
parse(source).render.should == "1"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should return parent block content"
|
21
|
+
end
|
22
|
+
|
23
|
+
def block(name, content)
|
24
|
+
"{% block #{name.to_s} %}#{content}{% endblock %}"
|
25
|
+
end
|
26
|
+
end
|