liquor 0.1.1 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -9
  5. data/Gemfile +7 -0
  6. data/Guardfile +11 -0
  7. data/MIT-LICENSE +6 -2
  8. data/README.md +4 -122
  9. data/Rakefile +20 -23
  10. data/doc/language-spec.html +768 -0
  11. data/doc/language-spec.md +698 -0
  12. data/lib/liquor.rb +39 -68
  13. data/lib/liquor/ast_tools.rb +28 -0
  14. data/lib/liquor/compiler.rb +110 -0
  15. data/lib/liquor/context.rb +76 -254
  16. data/lib/liquor/diagnostics.rb +151 -0
  17. data/lib/liquor/drop/drop.rb +168 -0
  18. data/lib/liquor/drop/drop_delegation.rb +24 -0
  19. data/lib/liquor/drop/drop_scope.rb +118 -0
  20. data/lib/liquor/drop/dropable.rb +17 -0
  21. data/lib/liquor/emitter.rb +313 -0
  22. data/lib/liquor/extensions/kaminari.rb +14 -0
  23. data/lib/liquor/extensions/pagination.rb +235 -0
  24. data/lib/liquor/extensions/rails.rb +97 -0
  25. data/lib/liquor/extensions/thinking_sphinx.rb +14 -0
  26. data/lib/liquor/extensions/tire.rb +30 -0
  27. data/lib/liquor/external.rb +79 -0
  28. data/lib/liquor/function.rb +94 -0
  29. data/lib/liquor/grammar/lexer.rb +1223 -0
  30. data/lib/liquor/grammar/lexer.rl +297 -0
  31. data/lib/liquor/grammar/parser.racc +288 -0
  32. data/lib/liquor/grammar/parser.rb +885 -0
  33. data/lib/liquor/library.rb +41 -0
  34. data/lib/liquor/manager.rb +146 -0
  35. data/lib/liquor/runtime.rb +167 -0
  36. data/lib/liquor/stdlib/builtin_functions.rb +315 -0
  37. data/lib/liquor/stdlib/builtin_tags.rb +228 -0
  38. data/lib/liquor/stdlib/html_truncater.rb +162 -0
  39. data/lib/liquor/stdlib/partial_tags.rb +76 -0
  40. data/lib/liquor/tag.rb +83 -14
  41. data/lib/liquor/version.rb +1 -1
  42. data/liquor.gemspec +29 -6
  43. data/spec/builtins_spec.rb +264 -0
  44. data/spec/compiler_spec.rb +136 -0
  45. data/spec/context_spec.rb +49 -0
  46. data/spec/drop_delegation_spec.rb +21 -0
  47. data/spec/drop_spec.rb +222 -0
  48. data/spec/errors_spec.rb +40 -0
  49. data/spec/external_spec.rb +207 -0
  50. data/spec/function_spec.rb +80 -0
  51. data/spec/lexer_spec.rb +173 -0
  52. data/spec/library_spec.rb +18 -0
  53. data/spec/manager_spec.rb +84 -0
  54. data/spec/parser_spec.rb +381 -0
  55. data/spec/partials_spec.rb +74 -0
  56. data/spec/runtime_spec.rb +97 -0
  57. data/spec/spec_helper.rb +94 -0
  58. data/spec/tag_spec.rb +7 -0
  59. metadata +216 -173
  60. data/AUTHORS +0 -2
  61. data/CHANGELOG +0 -48
  62. data/Gemfile.lock +0 -91
  63. data/History.txt +0 -44
  64. data/LICENSE +0 -23
  65. data/example/server/example_servlet.rb +0 -37
  66. data/example/server/liquid_servlet.rb +0 -28
  67. data/example/server/liquor_servlet.rb +0 -28
  68. data/example/server/server.rb +0 -12
  69. data/example/server/templates/index.liquid +0 -6
  70. data/example/server/templates/index.liquor +0 -6
  71. data/example/server/templates/products.liquid +0 -45
  72. data/example/server/templates/products.liquor +0 -45
  73. data/init.rb +0 -8
  74. data/lib/extras/liquid_view.rb +0 -51
  75. data/lib/extras/liquor_view.rb +0 -51
  76. data/lib/liquor/block.rb +0 -101
  77. data/lib/liquor/condition.rb +0 -120
  78. data/lib/liquor/document.rb +0 -17
  79. data/lib/liquor/drop.rb +0 -256
  80. data/lib/liquor/errors.rb +0 -11
  81. data/lib/liquor/extensions.rb +0 -72
  82. data/lib/liquor/file_system.rb +0 -62
  83. data/lib/liquor/htmltags.rb +0 -74
  84. data/lib/liquor/module_ex.rb +0 -60
  85. data/lib/liquor/standardfilters.rb +0 -315
  86. data/lib/liquor/strainer.rb +0 -58
  87. data/lib/liquor/tags/assign.rb +0 -33
  88. data/lib/liquor/tags/capture.rb +0 -35
  89. data/lib/liquor/tags/case.rb +0 -83
  90. data/lib/liquor/tags/comment.rb +0 -9
  91. data/lib/liquor/tags/content_for.rb +0 -54
  92. data/lib/liquor/tags/cycle.rb +0 -59
  93. data/lib/liquor/tags/for.rb +0 -136
  94. data/lib/liquor/tags/if.rb +0 -80
  95. data/lib/liquor/tags/ifchanged.rb +0 -20
  96. data/lib/liquor/tags/include.rb +0 -56
  97. data/lib/liquor/tags/unless.rb +0 -33
  98. data/lib/liquor/tags/yield.rb +0 -49
  99. data/lib/liquor/template.rb +0 -181
  100. data/lib/liquor/variable.rb +0 -52
  101. data/performance/shopify.rb +0 -92
  102. data/performance/shopify/comment_form.rb +0 -33
  103. data/performance/shopify/database.rb +0 -45
  104. data/performance/shopify/json_filter.rb +0 -7
  105. data/performance/shopify/liquid.rb +0 -18
  106. data/performance/shopify/liquor.rb +0 -18
  107. data/performance/shopify/money_filter.rb +0 -18
  108. data/performance/shopify/paginate.rb +0 -93
  109. data/performance/shopify/shop_filter.rb +0 -98
  110. data/performance/shopify/tag_filter.rb +0 -25
  111. data/performance/shopify/vision.database.yml +0 -945
  112. data/performance/shopify/weight_filter.rb +0 -11
  113. data/performance/tests/dropify/article.liquid +0 -74
  114. data/performance/tests/dropify/blog.liquid +0 -33
  115. data/performance/tests/dropify/cart.liquid +0 -66
  116. data/performance/tests/dropify/collection.liquid +0 -22
  117. data/performance/tests/dropify/index.liquid +0 -47
  118. data/performance/tests/dropify/page.liquid +0 -8
  119. data/performance/tests/dropify/product.liquid +0 -68
  120. data/performance/tests/dropify/theme.liquid +0 -105
  121. data/performance/tests/ripen/article.liquid +0 -74
  122. data/performance/tests/ripen/blog.liquid +0 -13
  123. data/performance/tests/ripen/cart.liquid +0 -54
  124. data/performance/tests/ripen/collection.liquid +0 -29
  125. data/performance/tests/ripen/index.liquid +0 -32
  126. data/performance/tests/ripen/page.liquid +0 -4
  127. data/performance/tests/ripen/product.liquid +0 -75
  128. data/performance/tests/ripen/theme.liquid +0 -85
  129. data/performance/tests/tribble/404.liquid +0 -56
  130. data/performance/tests/tribble/article.liquid +0 -98
  131. data/performance/tests/tribble/blog.liquid +0 -41
  132. data/performance/tests/tribble/cart.liquid +0 -134
  133. data/performance/tests/tribble/collection.liquid +0 -70
  134. data/performance/tests/tribble/index.liquid +0 -94
  135. data/performance/tests/tribble/page.liquid +0 -56
  136. data/performance/tests/tribble/product.liquid +0 -116
  137. data/performance/tests/tribble/search.liquid +0 -51
  138. data/performance/tests/tribble/theme.liquid +0 -90
  139. data/performance/tests/vogue/article.liquid +0 -66
  140. data/performance/tests/vogue/blog.liquid +0 -32
  141. data/performance/tests/vogue/cart.liquid +0 -58
  142. data/performance/tests/vogue/collection.liquid +0 -19
  143. data/performance/tests/vogue/index.liquid +0 -22
  144. data/performance/tests/vogue/page.liquid +0 -3
  145. data/performance/tests/vogue/product.liquid +0 -62
  146. data/performance/tests/vogue/theme.liquid +0 -122
  147. data/test/assign_test.rb +0 -11
  148. data/test/block_test.rb +0 -58
  149. data/test/capture_test.rb +0 -41
  150. data/test/condition_test.rb +0 -115
  151. data/test/content_for_test.rb +0 -15
  152. data/test/context_test.rb +0 -479
  153. data/test/drop_test.rb +0 -162
  154. data/test/error_handling_test.rb +0 -89
  155. data/test/extra/breakpoint.rb +0 -547
  156. data/test/extra/caller.rb +0 -80
  157. data/test/file_system_test.rb +0 -30
  158. data/test/filter_test.rb +0 -147
  159. data/test/helper.rb +0 -24
  160. data/test/html_tag_test.rb +0 -31
  161. data/test/if_else_test.rb +0 -139
  162. data/test/include_tag_test.rb +0 -129
  163. data/test/module_ex_test.rb +0 -89
  164. data/test/output_test.rb +0 -121
  165. data/test/parsing_quirks_test.rb +0 -54
  166. data/test/regexp_test.rb +0 -45
  167. data/test/security_test.rb +0 -41
  168. data/test/standard_filter_test.rb +0 -170
  169. data/test/standard_tag_test.rb +0 -405
  170. data/test/statements_test.rb +0 -137
  171. data/test/strainer_test.rb +0 -27
  172. data/test/template_test.rb +0 -82
  173. data/test/test_helper.rb +0 -28
  174. data/test/unless_else_test.rb +0 -27
  175. data/test/variable_test.rb +0 -173
  176. 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
@@ -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
@@ -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