liquor 0.1.1 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -9
- data/Gemfile +7 -0
- data/Guardfile +11 -0
- data/MIT-LICENSE +6 -2
- data/README.md +4 -122
- data/Rakefile +20 -23
- data/doc/language-spec.html +768 -0
- data/doc/language-spec.md +698 -0
- data/lib/liquor.rb +39 -68
- data/lib/liquor/ast_tools.rb +28 -0
- data/lib/liquor/compiler.rb +110 -0
- data/lib/liquor/context.rb +76 -254
- data/lib/liquor/diagnostics.rb +151 -0
- data/lib/liquor/drop/drop.rb +168 -0
- data/lib/liquor/drop/drop_delegation.rb +24 -0
- data/lib/liquor/drop/drop_scope.rb +118 -0
- data/lib/liquor/drop/dropable.rb +17 -0
- data/lib/liquor/emitter.rb +313 -0
- data/lib/liquor/extensions/kaminari.rb +14 -0
- data/lib/liquor/extensions/pagination.rb +235 -0
- data/lib/liquor/extensions/rails.rb +97 -0
- data/lib/liquor/extensions/thinking_sphinx.rb +14 -0
- data/lib/liquor/extensions/tire.rb +30 -0
- data/lib/liquor/external.rb +79 -0
- data/lib/liquor/function.rb +94 -0
- data/lib/liquor/grammar/lexer.rb +1223 -0
- data/lib/liquor/grammar/lexer.rl +297 -0
- data/lib/liquor/grammar/parser.racc +288 -0
- data/lib/liquor/grammar/parser.rb +885 -0
- data/lib/liquor/library.rb +41 -0
- data/lib/liquor/manager.rb +146 -0
- data/lib/liquor/runtime.rb +167 -0
- data/lib/liquor/stdlib/builtin_functions.rb +315 -0
- data/lib/liquor/stdlib/builtin_tags.rb +228 -0
- data/lib/liquor/stdlib/html_truncater.rb +162 -0
- data/lib/liquor/stdlib/partial_tags.rb +76 -0
- data/lib/liquor/tag.rb +83 -14
- data/lib/liquor/version.rb +1 -1
- data/liquor.gemspec +29 -6
- data/spec/builtins_spec.rb +264 -0
- data/spec/compiler_spec.rb +136 -0
- data/spec/context_spec.rb +49 -0
- data/spec/drop_delegation_spec.rb +21 -0
- data/spec/drop_spec.rb +222 -0
- data/spec/errors_spec.rb +40 -0
- data/spec/external_spec.rb +207 -0
- data/spec/function_spec.rb +80 -0
- data/spec/lexer_spec.rb +173 -0
- data/spec/library_spec.rb +18 -0
- data/spec/manager_spec.rb +84 -0
- data/spec/parser_spec.rb +381 -0
- data/spec/partials_spec.rb +74 -0
- data/spec/runtime_spec.rb +97 -0
- data/spec/spec_helper.rb +94 -0
- data/spec/tag_spec.rb +7 -0
- metadata +216 -173
- data/AUTHORS +0 -2
- data/CHANGELOG +0 -48
- data/Gemfile.lock +0 -91
- data/History.txt +0 -44
- data/LICENSE +0 -23
- data/example/server/example_servlet.rb +0 -37
- data/example/server/liquid_servlet.rb +0 -28
- data/example/server/liquor_servlet.rb +0 -28
- data/example/server/server.rb +0 -12
- data/example/server/templates/index.liquid +0 -6
- data/example/server/templates/index.liquor +0 -6
- data/example/server/templates/products.liquid +0 -45
- data/example/server/templates/products.liquor +0 -45
- data/init.rb +0 -8
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/extras/liquor_view.rb +0 -51
- data/lib/liquor/block.rb +0 -101
- data/lib/liquor/condition.rb +0 -120
- data/lib/liquor/document.rb +0 -17
- data/lib/liquor/drop.rb +0 -256
- data/lib/liquor/errors.rb +0 -11
- data/lib/liquor/extensions.rb +0 -72
- data/lib/liquor/file_system.rb +0 -62
- data/lib/liquor/htmltags.rb +0 -74
- data/lib/liquor/module_ex.rb +0 -60
- data/lib/liquor/standardfilters.rb +0 -315
- data/lib/liquor/strainer.rb +0 -58
- data/lib/liquor/tags/assign.rb +0 -33
- data/lib/liquor/tags/capture.rb +0 -35
- data/lib/liquor/tags/case.rb +0 -83
- data/lib/liquor/tags/comment.rb +0 -9
- data/lib/liquor/tags/content_for.rb +0 -54
- data/lib/liquor/tags/cycle.rb +0 -59
- data/lib/liquor/tags/for.rb +0 -136
- data/lib/liquor/tags/if.rb +0 -80
- data/lib/liquor/tags/ifchanged.rb +0 -20
- data/lib/liquor/tags/include.rb +0 -56
- data/lib/liquor/tags/unless.rb +0 -33
- data/lib/liquor/tags/yield.rb +0 -49
- data/lib/liquor/template.rb +0 -181
- data/lib/liquor/variable.rb +0 -52
- data/performance/shopify.rb +0 -92
- data/performance/shopify/comment_form.rb +0 -33
- data/performance/shopify/database.rb +0 -45
- data/performance/shopify/json_filter.rb +0 -7
- data/performance/shopify/liquid.rb +0 -18
- data/performance/shopify/liquor.rb +0 -18
- data/performance/shopify/money_filter.rb +0 -18
- data/performance/shopify/paginate.rb +0 -93
- data/performance/shopify/shop_filter.rb +0 -98
- data/performance/shopify/tag_filter.rb +0 -25
- data/performance/shopify/vision.database.yml +0 -945
- data/performance/shopify/weight_filter.rb +0 -11
- data/performance/tests/dropify/article.liquid +0 -74
- data/performance/tests/dropify/blog.liquid +0 -33
- data/performance/tests/dropify/cart.liquid +0 -66
- data/performance/tests/dropify/collection.liquid +0 -22
- data/performance/tests/dropify/index.liquid +0 -47
- data/performance/tests/dropify/page.liquid +0 -8
- data/performance/tests/dropify/product.liquid +0 -68
- data/performance/tests/dropify/theme.liquid +0 -105
- data/performance/tests/ripen/article.liquid +0 -74
- data/performance/tests/ripen/blog.liquid +0 -13
- data/performance/tests/ripen/cart.liquid +0 -54
- data/performance/tests/ripen/collection.liquid +0 -29
- data/performance/tests/ripen/index.liquid +0 -32
- data/performance/tests/ripen/page.liquid +0 -4
- data/performance/tests/ripen/product.liquid +0 -75
- data/performance/tests/ripen/theme.liquid +0 -85
- data/performance/tests/tribble/404.liquid +0 -56
- data/performance/tests/tribble/article.liquid +0 -98
- data/performance/tests/tribble/blog.liquid +0 -41
- data/performance/tests/tribble/cart.liquid +0 -134
- data/performance/tests/tribble/collection.liquid +0 -70
- data/performance/tests/tribble/index.liquid +0 -94
- data/performance/tests/tribble/page.liquid +0 -56
- data/performance/tests/tribble/product.liquid +0 -116
- data/performance/tests/tribble/search.liquid +0 -51
- data/performance/tests/tribble/theme.liquid +0 -90
- data/performance/tests/vogue/article.liquid +0 -66
- data/performance/tests/vogue/blog.liquid +0 -32
- data/performance/tests/vogue/cart.liquid +0 -58
- data/performance/tests/vogue/collection.liquid +0 -19
- data/performance/tests/vogue/index.liquid +0 -22
- data/performance/tests/vogue/page.liquid +0 -3
- data/performance/tests/vogue/product.liquid +0 -62
- data/performance/tests/vogue/theme.liquid +0 -122
- data/test/assign_test.rb +0 -11
- data/test/block_test.rb +0 -58
- data/test/capture_test.rb +0 -41
- data/test/condition_test.rb +0 -115
- data/test/content_for_test.rb +0 -15
- data/test/context_test.rb +0 -479
- data/test/drop_test.rb +0 -162
- data/test/error_handling_test.rb +0 -89
- data/test/extra/breakpoint.rb +0 -547
- data/test/extra/caller.rb +0 -80
- data/test/file_system_test.rb +0 -30
- data/test/filter_test.rb +0 -147
- data/test/helper.rb +0 -24
- data/test/html_tag_test.rb +0 -31
- data/test/if_else_test.rb +0 -139
- data/test/include_tag_test.rb +0 -129
- data/test/module_ex_test.rb +0 -89
- data/test/output_test.rb +0 -121
- data/test/parsing_quirks_test.rb +0 -54
- data/test/regexp_test.rb +0 -45
- data/test/security_test.rb +0 -41
- data/test/standard_filter_test.rb +0 -170
- data/test/standard_tag_test.rb +0 -405
- data/test/statements_test.rb +0 -137
- data/test/strainer_test.rb +0 -27
- data/test/template_test.rb +0 -82
- data/test/test_helper.rb +0 -28
- data/test/unless_else_test.rb +0 -27
- data/test/variable_test.rb +0 -173
- data/test/yield_test.rb +0 -24
@@ -0,0 +1,80 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Liquor::Function do
|
4
|
+
it "allows to define a function" do
|
5
|
+
fun = Liquor::Function.new("test",
|
6
|
+
unnamed_arg: :integer,
|
7
|
+
mandatory_named_args: { test: :any, add: :integer },
|
8
|
+
optional_named_args: { once_more: :any }) do |arg, kw|
|
9
|
+
arg + kw[:add]
|
10
|
+
end
|
11
|
+
fun.unnamed_arg.should == :integer
|
12
|
+
fun.optional_named_args.should == { once_more: :any }
|
13
|
+
fun.call(1, add: 2).should == 3
|
14
|
+
end
|
15
|
+
|
16
|
+
it "requires a body" do
|
17
|
+
expect {
|
18
|
+
Liquor::Function.new("test")
|
19
|
+
}.to raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "checks types" do
|
23
|
+
fun = Liquor::Function.new("test",
|
24
|
+
unnamed_arg: [:string, :tuple],
|
25
|
+
optional_named_args: { some: :integer, other: :any }) do |arg, kw|
|
26
|
+
# nothing
|
27
|
+
end
|
28
|
+
|
29
|
+
expect { fun.call("a") }.not_to raise_error
|
30
|
+
expect { fun.call([1]) }.not_to raise_error
|
31
|
+
expect { fun.call(1) }.to raise_error(Liquor::ArgumentTypeError)
|
32
|
+
expect { fun.call("a", some: "A") }.to raise_error(Liquor::ArgumentTypeError)
|
33
|
+
expect { fun.call("a", other: 1) }.not_to raise_error
|
34
|
+
expect { fun.call("a", other: Class) }.to raise_error(Liquor::ArgumentTypeError)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "accepts an indexable external as a tuple" do
|
38
|
+
fun = Liquor::Function.new("test",
|
39
|
+
unnamed_arg: [:tuple]) do |arg, kw|
|
40
|
+
# nothing
|
41
|
+
end
|
42
|
+
|
43
|
+
ext = Class.new do
|
44
|
+
include Liquor::External
|
45
|
+
|
46
|
+
def [](index); end
|
47
|
+
def size; 0; end
|
48
|
+
def to_a; []; end
|
49
|
+
|
50
|
+
export :[], :size, :to_a
|
51
|
+
end
|
52
|
+
|
53
|
+
ext2 = Class.new do
|
54
|
+
include Liquor::External
|
55
|
+
|
56
|
+
def nothing; end
|
57
|
+
|
58
|
+
export :nothing
|
59
|
+
end
|
60
|
+
|
61
|
+
expect { fun.call(ext.new) }.not_to raise_error
|
62
|
+
expect { fun.call(ext2.new) }.to raise_error(Liquor::ArgumentTypeError)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "catches host errors" do
|
66
|
+
fun = Liquor::Function.new("test") do |arg, kw|
|
67
|
+
"".to_hash
|
68
|
+
end
|
69
|
+
|
70
|
+
expect { fun.call() }.to raise_error(Liquor::HostError)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "provides loc to the inside of functions" do
|
74
|
+
fun = Liquor::Function.new("whereami") do |arg, kw, loc|
|
75
|
+
loc.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
fun.call(nil, {}, {rabbit: 'hole'}).should == '{:rabbit=>"hole"}'
|
79
|
+
end
|
80
|
+
end
|
data/spec/lexer_spec.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Liquor::Lexer do
|
4
|
+
it "parses plaintext" do
|
5
|
+
lex("abc\ndef").should have_token_structure(
|
6
|
+
[:plaintext, "abc\ndef"]
|
7
|
+
)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "parses plaintext with braces" do
|
11
|
+
lex('abc { def').should have_token_structure( [:plaintext] )
|
12
|
+
lex('abc \{ def').should have_token_structure( [:plaintext] )
|
13
|
+
lex('abc \{% def').should have_token_structure( [:plaintext] )
|
14
|
+
lex('abc \{{\{% def').should have_token_structure( [:plaintext] )
|
15
|
+
end
|
16
|
+
|
17
|
+
it "parses comments" do
|
18
|
+
lex('abc {! def !} test').should have_token_structure(
|
19
|
+
[:plaintext, 'abc '],
|
20
|
+
[:plaintext, ' test']
|
21
|
+
)
|
22
|
+
lex('abc {! {% test %} !} test').should have_token_structure(
|
23
|
+
[:plaintext, 'abc '],
|
24
|
+
[:plaintext, ' test']
|
25
|
+
)
|
26
|
+
lex('abc {! {{ def }} !} test').should have_token_structure(
|
27
|
+
[:plaintext, 'abc '],
|
28
|
+
[:plaintext, ' test']
|
29
|
+
)
|
30
|
+
lex('abc {! test {! a !} b !} def').should have_token_structure(
|
31
|
+
[:plaintext, 'abc '],
|
32
|
+
[:plaintext, ' def'],
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "parses blocks and interpolations" do
|
37
|
+
lex('abc {{ def }}').should have_token_structure(
|
38
|
+
[:plaintext],
|
39
|
+
[:linterp],
|
40
|
+
[:ident],
|
41
|
+
[:rinterp],
|
42
|
+
)
|
43
|
+
lex('abc {% def %}').should have_token_structure(
|
44
|
+
[:plaintext],
|
45
|
+
[:lblock],
|
46
|
+
[:ident],
|
47
|
+
[:rblock],
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "fails on nested blocks and interpolations" do
|
52
|
+
expect { lex('abc {{ def {{') }.to raise_error(Liquor::SyntaxError, %r|unexpected `{'|)
|
53
|
+
expect { lex('abc {{ def {%') }.to raise_error(Liquor::SyntaxError, %r|unexpected `{'|)
|
54
|
+
expect { lex('abc {% def {{') }.to raise_error(Liquor::SyntaxError, %r|unexpected `{'|)
|
55
|
+
expect { lex('abc {% def {%') }.to raise_error(Liquor::SyntaxError, %r|unexpected `{'|)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "fails on unrecognized symbols" do
|
59
|
+
expect { lex('abc {{ # }}') }.to raise_error(Liquor::SyntaxError, %r|unexpected `#'|)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "fails invalid integer literals the correct way" do
|
63
|
+
expect { lex('{{ 1a }}') }.to raise_error(Liquor::SyntaxError, %r|unexpected `a'|)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "parses complex expressions" do
|
67
|
+
lex('{{ 1 * 2 + substr("abc" from: 1 to: 10 via: an.external) }}').should have_token_structure(
|
68
|
+
[:linterp],
|
69
|
+
[:integer, 1], [:op_mul], [:integer, 2], [:op_plus],
|
70
|
+
[:ident], [:lparen],
|
71
|
+
[:string, "abc"],
|
72
|
+
[:keyword, "from"], [:integer, 1],
|
73
|
+
[:keyword, "to"], [:integer, 10],
|
74
|
+
[:keyword, "via"], [:ident, "an"], [:dot], [:ident, "external"],
|
75
|
+
[:rparen],
|
76
|
+
[:rinterp],
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "parses blocks with embedded blocks" do
|
81
|
+
lex('{% for x in: [ 1, 2, q ] do: %} value: {{ x }} {% end for %}').should have_token_structure(
|
82
|
+
[:lblock],
|
83
|
+
[:ident, "for"], [:ident, "x"],
|
84
|
+
[:keyword, "in"],
|
85
|
+
[:lbracket], [:integer, 1], [:comma], [:integer, 2], [:comma], [:ident, "q"], [:rbracket],
|
86
|
+
[:keyword, "do"],
|
87
|
+
[:rblock],
|
88
|
+
[:plaintext],
|
89
|
+
[:linterp], [:ident], [:rinterp],
|
90
|
+
[:plaintext],
|
91
|
+
[:lblock2], [:endtag], [:rblock],
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "fails on multiline blocks and interpolations" do
|
96
|
+
expect { lex("{{ \n }}") }.to raise_error(Liquor::SyntaxError, %r|unexpected end of line|)
|
97
|
+
expect { lex("{% \n %}") }.to raise_error(Liquor::SyntaxError, %r|unexpected end of line|)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "parses complex string literals" do
|
101
|
+
lex('{{ "abc" + "def" }}').should have_token_structure(
|
102
|
+
[:linterp],
|
103
|
+
[:string, "abc"],
|
104
|
+
[:op_plus],
|
105
|
+
[:string, "def"],
|
106
|
+
[:rinterp]
|
107
|
+
)
|
108
|
+
|
109
|
+
expect { lex(%|{{ "test }}|) }.to raise_error(Liquor::SyntaxError, %r|literal not terminated|)
|
110
|
+
expect { lex(%|{{ "test\\" }}|) }.to raise_error(Liquor::SyntaxError, %r|literal not terminated|)
|
111
|
+
expect { lex(%|{{ "test\\\\" }}|) }.not_to raise_error
|
112
|
+
|
113
|
+
expect { lex(%|{{ 'test }}|) }.to raise_error(Liquor::SyntaxError, %r|literal not terminated|)
|
114
|
+
expect { lex(%|{{ 'test\\' }}|) }.to raise_error(Liquor::SyntaxError, %r|literal not terminated|)
|
115
|
+
expect { lex(%|{{ 'test\\\\' }}|) }.not_to raise_error
|
116
|
+
|
117
|
+
lex(%|{{ "test ' \\'" }}|).should have_token_structure(
|
118
|
+
[:linterp], [:string, %|test ' \\'|], [:rinterp]
|
119
|
+
)
|
120
|
+
lex(%|{{ 'test " \\'' }}|).should have_token_structure(
|
121
|
+
[:linterp], [:string, %|test " '|], [:rinterp]
|
122
|
+
)
|
123
|
+
|
124
|
+
expect { lex(%|{{ "test\n" }}|) }.to raise_error(Liquor::SyntaxError, %r|unexpected end of line|)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "parses nested tags correctly" do
|
128
|
+
lex('{% a do: %} 1 {% b %} 2 {% c do: %} 3 {% end c %} 4 {% end a %} 5 {% ender %}').
|
129
|
+
should have_token_structure(
|
130
|
+
[:lblock], [:ident, "a"], [:keyword, "do"], [:rblock],
|
131
|
+
[:plaintext, " 1 "],
|
132
|
+
[:lblock], [:ident, "b"], [:rblock],
|
133
|
+
[:plaintext, " 2 "],
|
134
|
+
[:lblock], [:ident, "c"], [:keyword, "do"], [:rblock],
|
135
|
+
[:plaintext, " 3 "],
|
136
|
+
[:lblock2], [:endtag], [:rblock],
|
137
|
+
[:plaintext, " 4 "],
|
138
|
+
[:lblock2], [:endtag], [:rblock],
|
139
|
+
[:plaintext, " 5 "],
|
140
|
+
[:lblock], [:ident, "ender"], [:rblock],
|
141
|
+
)
|
142
|
+
lex('{% capture do: %}{% if a then: %} 1 {% elsif: b then: %} 2 {% end if %}{% end capture %}').
|
143
|
+
should have_token_structure(
|
144
|
+
[:lblock], [:ident, "capture"], [:keyword, "do"], [:rblock],
|
145
|
+
[:lblock], [:ident, "if"], [:ident, "a"], [:keyword, "then"], [:rblock],
|
146
|
+
[:plaintext, " 1 "],
|
147
|
+
[:lblock2], [:keyword, "elsif"], [:ident, "b"], [:keyword, "then"], [:rblock],
|
148
|
+
[:plaintext, " 2 "],
|
149
|
+
[:lblock2], [:endtag], [:rblock],
|
150
|
+
[:lblock2], [:endtag], [:rblock],
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "understands syntactic sugar for =" do
|
155
|
+
lex('{% assign x = 1 %}').should have_token_structure(
|
156
|
+
[:lblock], [:ident, "assign"], [:ident, "x"], [:keyword, "="], [:integer, 1], [:rblock]
|
157
|
+
)
|
158
|
+
end
|
159
|
+
|
160
|
+
it "fails unmatched end tag" do
|
161
|
+
expect { lex('{% end tag %}') }.to raise_error(Liquor::SyntaxError)
|
162
|
+
expect { lex('{% unless x then: %} {% end for %}') }.to raise_error(Liquor::SyntaxError)
|
163
|
+
end
|
164
|
+
|
165
|
+
it "correctly fails on complex runaway strings" do
|
166
|
+
expect {
|
167
|
+
lex(%Q${% for artist in: list do: %}
|
168
|
+
{% end 'for %}
|
169
|
+
#{" \n" * 800}
|
170
|
+
'220x$)
|
171
|
+
}.to raise_error(Liquor::SyntaxError, %r|unexpected end of line .+?: line 2, column 28|)
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Liquor::Library do
|
4
|
+
it "allows to define a function and export it to a compiler" do
|
5
|
+
lib = Module.new do
|
6
|
+
include Liquor::Library
|
7
|
+
|
8
|
+
function "hello" do |arg, kw|
|
9
|
+
"hello world"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
compiler = Liquor::Compiler.new
|
14
|
+
lib.export compiler
|
15
|
+
compiler.compile parse('{{ hello() }}')
|
16
|
+
compiler.code.call.should == 'hello world'
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Liquor::Manager do
|
4
|
+
before do
|
5
|
+
@manager = Liquor::Manager.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be able to compile templates" do
|
9
|
+
@manager.register_template 'test', '{{ i }}', [:i]
|
10
|
+
@manager.compile.should be_true
|
11
|
+
|
12
|
+
@manager.render('test', i: 10).should == '10'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should decorate errors" do
|
16
|
+
@manager.register_template 'test', '{{ }}', [:i]
|
17
|
+
@manager.compile.should be_false
|
18
|
+
|
19
|
+
@manager.errors.count.should == 1
|
20
|
+
error = @manager.errors.first
|
21
|
+
|
22
|
+
@manager.decorate(error).should == [
|
23
|
+
"{{ }}",
|
24
|
+
" ^^"
|
25
|
+
]
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should check names for correctness" do
|
29
|
+
expect {
|
30
|
+
@manager.register_template '_test', '{{ i }}', [:i]
|
31
|
+
}.to raise_error ArgumentError, %r|is a partial|
|
32
|
+
expect {
|
33
|
+
@manager.register_partial 'test', '{{ i }}'
|
34
|
+
}.to raise_error ArgumentError, %r|is not a partial|
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should report presence of templates" do
|
38
|
+
@manager.register_template 'test', '{{ i }}', [:i]
|
39
|
+
@manager.compile.should be_true
|
40
|
+
|
41
|
+
@manager.has_template?('test').should be_true
|
42
|
+
@manager.has_template?('foo').should be_false
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should render layouts" do
|
46
|
+
@manager.register_layout 'layout', '{% yield "head" %} 1 {% yield %}'
|
47
|
+
@manager.register_template 'action', '{% content_for "head" capture: %} 2 {% end content_for %} 3'
|
48
|
+
@manager.compile.should be_true
|
49
|
+
|
50
|
+
@manager.render_with_layout('layout', {}, 'action', {}).
|
51
|
+
scan(/\d+/).should == %w(2 1 3)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "exports libraries to compiler" do
|
55
|
+
lib = Module.new do
|
56
|
+
include Liquor::Library
|
57
|
+
|
58
|
+
function "hello" do |arg, kw|
|
59
|
+
"world"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
manager = Liquor::Manager.new(import: [lib])
|
64
|
+
manager.register_template 'test', '{{ hello() }}'
|
65
|
+
manager.compile.should be_true
|
66
|
+
|
67
|
+
manager.render('test').should == 'world'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "dumps debug code" do
|
71
|
+
require 'tmpdir'
|
72
|
+
|
73
|
+
Dir.mktmpdir do |code_dir|
|
74
|
+
manager = Liquor::Manager.new(dump_intermediates: code_dir)
|
75
|
+
manager.register_template 'test', '{{ "hello world" }}'
|
76
|
+
manager.compile.should be_true
|
77
|
+
|
78
|
+
path = File.join(code_dir, 'test.liquor.rb')
|
79
|
+
|
80
|
+
File.exists?(path).should be_true
|
81
|
+
File.read(path).include?('hello world').should be_true
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,381 @@
|
|
1
|
+
#coding:utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Liquor::Parser do
|
6
|
+
it "parses plaintext" do
|
7
|
+
parse("abc\ndef").should have_node_structure(
|
8
|
+
[:plaintext, "abc\ndef"]
|
9
|
+
)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "recognizes interpolations" do
|
13
|
+
parse("a {{ 1 + a }} b").should have_node_structure(
|
14
|
+
[:plaintext, "a "],
|
15
|
+
[:interp,
|
16
|
+
[:plus,
|
17
|
+
[:integer, 1],
|
18
|
+
[:ident, "a"]]],
|
19
|
+
[:plaintext, " b"],
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "fails on incorrect syntax" do
|
24
|
+
expect { parse("{{ }}") }.to raise_error(Liquor::SyntaxError, %r|unexpected token `}}'|)
|
25
|
+
expect { parse("{{ 1 %}") }.to raise_error(Liquor::SyntaxError, %r|unexpected token `%}'|)
|
26
|
+
expect { parse("{{ -2] }}") }.to raise_error(Liquor::SyntaxError, %r|unexpected token `\]'|)
|
27
|
+
expect { parse("{{ a[2 }}") }.to raise_error(Liquor::SyntaxError, %r|unexpected token `}}'|)
|
28
|
+
expect { parse("{{ a && (b || ) }}") }.to raise_error(Liquor::SyntaxError, %r|unexpected token `\)'|)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "obeys operator precedence" do
|
32
|
+
parse('{{ 1 + 2 * 3 }}').should have_node_structure(
|
33
|
+
[:interp,
|
34
|
+
[:plus,
|
35
|
+
[:integer, 1],
|
36
|
+
[:mul,
|
37
|
+
[:integer, 2],
|
38
|
+
[:integer, 3]]]]
|
39
|
+
)
|
40
|
+
parse('{{ a && b || c && d }}').should have_node_structure(
|
41
|
+
[:interp,
|
42
|
+
[:or,
|
43
|
+
[:and, [:ident, "a"], [:ident, "b"]],
|
44
|
+
[:and, [:ident, "c"], [:ident, "d"]]]]
|
45
|
+
)
|
46
|
+
parse('{{ a || b && c || d }}').should have_node_structure(
|
47
|
+
[:interp,
|
48
|
+
[:or,
|
49
|
+
[:or,
|
50
|
+
[:ident, "a"],
|
51
|
+
[:and, [:ident, "b"], [:ident, "c"]]],
|
52
|
+
[:ident, "d"]]]
|
53
|
+
)
|
54
|
+
parse('{{ !x && x }}').should have_node_structure(
|
55
|
+
[:interp,
|
56
|
+
[:and,
|
57
|
+
[:not,
|
58
|
+
[:ident, "x"]],
|
59
|
+
[:ident, "x"]]]
|
60
|
+
)
|
61
|
+
expect {
|
62
|
+
exec(%|{% for x in: [1,2,3] do: %}{% if !x_loop.is_first then: %}y{% end if %}{% end for %}|)
|
63
|
+
}.not_to raise_error
|
64
|
+
end
|
65
|
+
|
66
|
+
it "changes evaluation order due to parentheses" do
|
67
|
+
parse('{{ a && (b || c) }}').should have_node_structure(
|
68
|
+
[:interp,
|
69
|
+
[:and,
|
70
|
+
[:ident, "a"],
|
71
|
+
[:or, [:ident, "b"], [:ident, "c"]]]]
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "recognizes every operator" do
|
76
|
+
expect { parse('{{ fun(a: 1 + 2 * 3 / 4 % 5 - 6 ' +
|
77
|
+
' b: !(a && b || c) c: (1 >= 2) || 1 < 2 || ' +
|
78
|
+
'a != b || b == c || 5 <= 6 || 7 > 8 ' +
|
79
|
+
'd: e.f e: ([1,2])[0] ) }}') }.not_to raise_error
|
80
|
+
|
81
|
+
parse('{{ 2 - -1 }}').should have_node_structure(
|
82
|
+
[:interp,
|
83
|
+
[:minus,
|
84
|
+
[:integer, 2],
|
85
|
+
[:uminus, [:integer, 1]]]]
|
86
|
+
)
|
87
|
+
parse('{{ -1 }}').should have_node_structure(
|
88
|
+
[:interp,
|
89
|
+
[:uminus, [:integer, 1]]]
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "accepts correct tuples" do
|
94
|
+
parse('{{ [] }}').should have_node_structure(
|
95
|
+
[:interp,
|
96
|
+
[:tuple, []]]
|
97
|
+
)
|
98
|
+
parse('{{ [1] }}').should have_node_structure(
|
99
|
+
[:interp,
|
100
|
+
[:tuple, [ [:integer, 1] ]]]
|
101
|
+
)
|
102
|
+
parse('{{ [1,] }}').should have_node_structure(
|
103
|
+
[:interp,
|
104
|
+
[:tuple, [ [:integer, 1] ]]]
|
105
|
+
)
|
106
|
+
parse('{{ [1,2] }}').should have_node_structure(
|
107
|
+
[:interp,
|
108
|
+
[:tuple, [ [:integer, 1], [:integer, 2] ]]]
|
109
|
+
)
|
110
|
+
parse('{{ [1,2,] }}').should have_node_structure(
|
111
|
+
[:interp,
|
112
|
+
[:tuple, [ [:integer, 1], [:integer, 2] ]]]
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "accepts correct method calls" do
|
117
|
+
parse('{{ a() }}').should have_node_structure(
|
118
|
+
[:interp,
|
119
|
+
[:call,
|
120
|
+
[:ident, "a"],
|
121
|
+
[:args, nil, {}]]]
|
122
|
+
)
|
123
|
+
parse('{{ a("str") }}').should have_node_structure(
|
124
|
+
[:interp,
|
125
|
+
[:call,
|
126
|
+
[:ident, "a"],
|
127
|
+
[:args, [:string, "str"], {}]]]
|
128
|
+
)
|
129
|
+
parse('{{ a("str" from: 1) }}').should have_node_structure(
|
130
|
+
[:interp,
|
131
|
+
[:call,
|
132
|
+
[:ident, "a"],
|
133
|
+
[:args, [:string, "str"],
|
134
|
+
{:from => [:integer, 1]}]]]
|
135
|
+
)
|
136
|
+
parse('{{ a(from: 1 to: 2) }}').should have_node_structure(
|
137
|
+
[:interp,
|
138
|
+
[:call,
|
139
|
+
[:ident, "a"],
|
140
|
+
[:args, nil,
|
141
|
+
{:from => [:integer, 1],
|
142
|
+
:to => [:integer, 2]}]]]
|
143
|
+
)
|
144
|
+
end
|
145
|
+
|
146
|
+
it "rejects method calls with duplicate keyword arguments" do
|
147
|
+
expect { parse('{{ a(to: 1 to: 1) }}') }.to \
|
148
|
+
raise_error(Liquor::SyntaxError, %r|duplicate keyword argument `to'|)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "accepts correct filter expressions" do
|
152
|
+
parse('{{ a | b }}').should have_node_structure(
|
153
|
+
[:interp,
|
154
|
+
[:call,
|
155
|
+
[:ident, "b"],
|
156
|
+
[:args,
|
157
|
+
[:ident, "a"],
|
158
|
+
{}]]]
|
159
|
+
)
|
160
|
+
parse('{{ a(x: 1) | b y: 2 z: 3 | c p: 4 }}').should have_node_structure(
|
161
|
+
[:interp,
|
162
|
+
[:call,
|
163
|
+
[:ident, "c"],
|
164
|
+
[:args,
|
165
|
+
[:call,
|
166
|
+
[:ident, "b"],
|
167
|
+
[:args,
|
168
|
+
[:call,
|
169
|
+
[:ident, "a"],
|
170
|
+
[:args,
|
171
|
+
nil,
|
172
|
+
{ :x => [:integer, 1] }]],
|
173
|
+
{ :y => [:integer, 2],
|
174
|
+
:z => [:integer, 3] }]],
|
175
|
+
{ :p => [:integer, 4] }]]]
|
176
|
+
)
|
177
|
+
end
|
178
|
+
|
179
|
+
it "rejects malformed filter expressions" do
|
180
|
+
expect { parse('{{ a b: 1 }}') }.to raise_error(Liquor::SyntaxError)
|
181
|
+
expect { parse('{{ a("str", b: 1) | b ') }.to raise_error(Liquor::SyntaxError, %r{unexpected token `|'})
|
182
|
+
expect { parse('{{ a | b(b: 1)') }.to raise_error(Liquor::SyntaxError, %r{unexpected token `\('})
|
183
|
+
expect { parse('{{ a | b("str", b: 1)') }.to raise_error(Liquor::SyntaxError, %r{unexpected token `\('})
|
184
|
+
end
|
185
|
+
|
186
|
+
it "parses blocks correctly" do
|
187
|
+
parse('{% yield %}').should have_node_structure(
|
188
|
+
[:tag,
|
189
|
+
[:ident, "yield"],
|
190
|
+
nil]
|
191
|
+
)
|
192
|
+
parse('{% yield 1 %}').should have_node_structure(
|
193
|
+
[:tag,
|
194
|
+
[:ident, "yield"],
|
195
|
+
[:integer, 1]]
|
196
|
+
)
|
197
|
+
parse('{% yield from: 1 %}').should have_node_structure(
|
198
|
+
[:tag,
|
199
|
+
[:ident, "yield"],
|
200
|
+
nil,
|
201
|
+
[:kwarg, [:keyword, "from"], [:integer, 1]]]
|
202
|
+
)
|
203
|
+
parse('{% yield "str" from: 1 %}').should have_node_structure(
|
204
|
+
[:tag,
|
205
|
+
[:ident, "yield"],
|
206
|
+
[:string, "str"],
|
207
|
+
[:kwarg, [:keyword, "from"], [:integer, 1]]]
|
208
|
+
)
|
209
|
+
parse('{% yield from: 1 to: 2 from: 3 %}').should have_node_structure(
|
210
|
+
[:tag,
|
211
|
+
[:ident, "yield"],
|
212
|
+
nil,
|
213
|
+
[:kwarg, [:keyword, "from" ], [:integer, 1]],
|
214
|
+
[:kwarg, [:keyword, "to" ], [:integer, 2]],
|
215
|
+
[:kwarg, [:keyword, "from" ], [:integer, 3]]]
|
216
|
+
)
|
217
|
+
parse('{% capture do: %} 1 {% end capture %}').
|
218
|
+
should have_node_structure(
|
219
|
+
[:tag,
|
220
|
+
[:ident, "capture"],
|
221
|
+
nil,
|
222
|
+
[:blockarg, [:keyword, "do"],
|
223
|
+
[
|
224
|
+
[:plaintext, " 1 "]
|
225
|
+
]]]
|
226
|
+
)
|
227
|
+
parse('{% capture "test" do: %} 1 {% end capture %}').
|
228
|
+
should have_node_structure(
|
229
|
+
[:tag,
|
230
|
+
[:ident, "capture"],
|
231
|
+
[:string, "test"],
|
232
|
+
[:blockarg, [:keyword, "do"],
|
233
|
+
[
|
234
|
+
[:plaintext, " 1 "]
|
235
|
+
]]]
|
236
|
+
)
|
237
|
+
parse('{% capture as: "test" do: %} 1 {% end capture %}').
|
238
|
+
should have_node_structure(
|
239
|
+
[:tag,
|
240
|
+
[:ident, "capture"],
|
241
|
+
nil,
|
242
|
+
[:kwarg, [:keyword, "as"], [:string, "test"]],
|
243
|
+
[:blockarg, [:keyword, "do"],
|
244
|
+
[
|
245
|
+
[:plaintext, " 1 "]
|
246
|
+
]]]
|
247
|
+
)
|
248
|
+
parse('{% if a1 then: %} 1 {% elsif: a2 then: %} 2 {% else: %} 3 {% end if %}').
|
249
|
+
should have_node_structure(
|
250
|
+
[:tag,
|
251
|
+
[:ident, "if"],
|
252
|
+
[:ident, "a1"],
|
253
|
+
[:blockarg, [:keyword, "then"],
|
254
|
+
[[:plaintext, " 1 "]]],
|
255
|
+
[:kwarg, [:keyword, "elsif"], [:ident, "a2"]],
|
256
|
+
[:blockarg, [:keyword, "then"],
|
257
|
+
[[:plaintext, " 2 "]]],
|
258
|
+
[:blockarg, [:keyword, "else"],
|
259
|
+
[[:plaintext, " 3 "]]]]
|
260
|
+
)
|
261
|
+
end
|
262
|
+
|
263
|
+
it "reports EOF errors" do
|
264
|
+
expect { parse('{% if a1 then: %} 1 {% wat %}') }.to raise_error(Liquor::SyntaxError, %r|unexpected end of program|)
|
265
|
+
end
|
266
|
+
|
267
|
+
it "handles lexer errors" do
|
268
|
+
parser = Liquor::Parser.new
|
269
|
+
parser.parse '{{ $'
|
270
|
+
parser.success?.should == false
|
271
|
+
parser.ast.should == nil
|
272
|
+
end
|
273
|
+
|
274
|
+
it "correctly handles multiple errors" do
|
275
|
+
parser = Liquor::Parser.new
|
276
|
+
parser.parse '{{ fun(a: 1 a: 1) + fun(b: 1 b: 1) }}'
|
277
|
+
parser.errors.count.should == 2
|
278
|
+
parser.ast.should_not == nil
|
279
|
+
end
|
280
|
+
|
281
|
+
it "resets error state on next parse" do
|
282
|
+
parser = Liquor::Parser.new
|
283
|
+
parser.parse '{{ fun(a: 1 a: 1) + fun(b: 1 b: 1) }}'
|
284
|
+
parser.errors.count.should == 2
|
285
|
+
parser.parse '{{ 1 }}'
|
286
|
+
parser.errors.count.should == 0
|
287
|
+
parser.success?.should == true
|
288
|
+
end
|
289
|
+
|
290
|
+
it "correctly handles empty blocks" do
|
291
|
+
parse('{% tag do: %}{% end tag %}').should have_node_structure(
|
292
|
+
[:tag,
|
293
|
+
[:ident, 'tag'],
|
294
|
+
nil,
|
295
|
+
[:blockarg,
|
296
|
+
[:keyword, 'do'],
|
297
|
+
[]]]
|
298
|
+
)
|
299
|
+
end
|
300
|
+
|
301
|
+
it "understands various constructs for externals" do
|
302
|
+
parse('{{ ext.test }}').should have_node_structure(
|
303
|
+
[:interp,
|
304
|
+
[:external,
|
305
|
+
[:ident, "ext"],
|
306
|
+
[:ident, "test"],
|
307
|
+
nil]]
|
308
|
+
)
|
309
|
+
parse('{{ ext.test() }}').should have_node_structure(
|
310
|
+
[:interp,
|
311
|
+
[:external,
|
312
|
+
[:ident, "ext"],
|
313
|
+
[:ident, "test"],
|
314
|
+
[:args,
|
315
|
+
nil,
|
316
|
+
{}]]]
|
317
|
+
)
|
318
|
+
parse('{{ a.b.c }}').should have_node_structure(
|
319
|
+
[:interp,
|
320
|
+
[:external,
|
321
|
+
[:external,
|
322
|
+
[:ident, "a"],
|
323
|
+
[:ident, "b"],
|
324
|
+
nil
|
325
|
+
],
|
326
|
+
[:ident, "c"],
|
327
|
+
nil]]
|
328
|
+
)
|
329
|
+
parse('{{ a.b().c }}').should have_node_structure(
|
330
|
+
[:interp,
|
331
|
+
[:external,
|
332
|
+
[:external,
|
333
|
+
[:ident, "a"],
|
334
|
+
[:ident, "b"],
|
335
|
+
[:args,
|
336
|
+
nil,
|
337
|
+
{}]
|
338
|
+
],
|
339
|
+
[:ident, "c"],
|
340
|
+
nil]]
|
341
|
+
)
|
342
|
+
parse('{{ a.b[0] }}').should have_node_structure(
|
343
|
+
[:interp,
|
344
|
+
[:index,
|
345
|
+
[:external,
|
346
|
+
[:ident, "a"],
|
347
|
+
[:ident, "b"],
|
348
|
+
nil,
|
349
|
+
],
|
350
|
+
[:integer, 0]]]
|
351
|
+
)
|
352
|
+
end
|
353
|
+
|
354
|
+
it "does not allow duplicate kwargs for external calls" do
|
355
|
+
expect { parse('{{ ext.call(kw: 1 kw: 2) }}') }.to raise_error(Liquor::SyntaxError, %r|duplicate keyword argument|)
|
356
|
+
end
|
357
|
+
|
358
|
+
it "does not want {% end elsif %}" do
|
359
|
+
expect {
|
360
|
+
compiler = Liquor::Compiler.new
|
361
|
+
parse %|
|
362
|
+
{% if !is_empty(clip_videos) then: %}
|
363
|
+
{% assign current_video = clip_videos[0] %}
|
364
|
+
{% elsif !is_empty(live_videos) then: %}
|
365
|
+
{% elsif: !is_empty(live_videos) then: %}
|
366
|
+
{% elsif !is_empty(live_videos) then: %}
|
367
|
+
{% end if %}
|
368
|
+
|, compiler
|
369
|
+
}.not_to raise_error
|
370
|
+
end
|
371
|
+
|
372
|
+
it "reports correct line numbers" do
|
373
|
+
expect {
|
374
|
+
parse("{\n%\n{% end if %}")
|
375
|
+
}.to raise_error(Liquor::SyntaxError, /line 3/)
|
376
|
+
end
|
377
|
+
|
378
|
+
it "does not return nil for empty string input" do
|
379
|
+
parse("").should_not == nil
|
380
|
+
end
|
381
|
+
end
|