h2o 0.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|