cutaneous 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2012 Garry Hill
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -0,0 +1,151 @@
1
+ # Cutaneous
2
+
3
+ Cutaneous is a Ruby (1.9+) templating engine designed for flexibility and simplicity.
4
+
5
+ It supports having multiple output formats, multiple syntaxes and borrows a template inheritance mechanism from Python template engines such as [Django's](https://docs.djangoproject.com/en/dev/topics/templates/), [Jinja](http://jinja.pocoo.org/) and [Mako](http://www.makotemplates.org/).
6
+
7
+ Cutaneous is the template engine designed for and used by [Spontaneous CMS](http://spontaneouscms.org).
8
+
9
+ ## Quickstart
10
+
11
+ <script src="https://gist.github.com/3169319.js"> </script>
12
+
13
+ <script src="https://gist.github.com/3169327.js"> </script>
14
+
15
+ ## Features
16
+
17
+ ### Template Inheritance
18
+
19
+ Cutaneous features a block based template inheritance mechanism.
20
+
21
+ Including a `%{ extends "parent" }` tag at the start of a template
22
+ makes it inherit from the named template ("parent" in this case).
23
+
24
+ Parent templates define a series of blocks using a `%{ block :block_name}
25
+ ... %{ endblock}` syntax. Child templates can then override any of
26
+ these individual blocks with their own content.
27
+
28
+ Calling `%{ blocksuper }` within a child template's block allows you
29
+ to insert the code from the parent template (much like calling `super`
30
+ in an object subclass).
31
+
32
+ So for example using the following templates:
33
+
34
+ <script src="https://gist.github.com/3169196.js"> </script>
35
+
36
+ <script src="https://gist.github.com/3169203.js"> </script>
37
+
38
+ `engine.render("child", context, "html")` would result in the following output:
39
+
40
+
41
+ Title
42
+
43
+ Template inheritance is great!
44
+ Template inheritance is great!
45
+
46
+ Do it like this...
47
+
48
+ And like this...
49
+
50
+ The template hierarchy can be as long as you need/like. Template 'd' could extend 'c' which extends 'b' which extends 'a' etc..
51
+
52
+ ### Formats
53
+
54
+ Cutaneous allows templates for multiple formats to exist alongside each other. In the examples above the `html` format is exclsuively used but instead of this I could render the same template as `txt`
55
+
56
+ result = engine.render("quickstart", context, "txt")
57
+
58
+ This would look for a `quickstart.txt.cut` template under the template roots. The format used is maintained across both `include` and `extend` calls so when using these you should reference them without any extension.
59
+
60
+ If your standard format isn't "html" then you can set a new default when creating your template engine instance:
61
+
62
+ engine = Cutaneous::Engine.new("/template/root", Cutaneous::FirstPassSyntax, "txt")
63
+
64
+
65
+ ### Whitespace
66
+
67
+ If you want to remove trailing whitespace after a tag, then suffix it with a `-` character, e.g.
68
+
69
+ %{ include "something" -}
70
+
71
+ ### Caching
72
+
73
+ If you create an instance of `Cutaneous::CachingEngine` instead of the default `Engine` class then the compiled templates will be cached in-memory for the lifetime of the engine. In order to render templates Cutaneous converts them to simple Ruby code. As part of the caching this generated code will be written to disk alongside the original template with the extension `.format.rb`.
74
+
75
+ If you want to turn off the writing of the compiled Ruby files (such as in a development environment), set `write_compiled_scripts` to false:
76
+
77
+ engine.write_compiled_scripts = false
78
+
79
+ The cached ruby will only be used if it is fresher than the template it was compiled from so updating the template will re-write the `.rb` equivalent.
80
+
81
+
82
+ ### Syntaxes
83
+
84
+ Cutaneous supports the concept of syntaxes. This is used by Spontaneous to provide a two-stage rendering process (first-pass templates output second-pass templates which are rendered on demand -- in this way you can create a very responsive dynamic site because you have precached 99% of the page).
85
+
86
+ The two syntaxes are:
87
+
88
+ #### First-pass
89
+
90
+ - Statements: `%{ ruby code... }`
91
+ - Expressions: `${ value }`
92
+ - Escaped expressions: `$${ unsafe value }`
93
+ - Comments: `!{ comment... }`
94
+
95
+ #### Second-pass
96
+
97
+ - Statements: `{% ruby code... %}`
98
+ - Expressions: `{{ value }}`
99
+ - Escaped expressions: `{$ unsafe value $}`
100
+ - Comments: `!{ comment... }`
101
+
102
+ You choose which one of these you wish to use when you create your `Cutaneous::Engine` instance:
103
+
104
+ engine = Cutaneous::Engine.new("/template/root", Cutaneous::SecondPassSyntax)
105
+
106
+ ## Contexts
107
+
108
+ Cutaneous doesn't try to remove code from your templates it instead allows you to write as much Ruby as you want directly in-place. This is done in order to make the development of your front-end code as quick as possible. If you later want to clean up your template code you can instead use helper methods either on the context you pass to the renderer or the object you wrap that context around.
109
+
110
+ If you want to add features to your context, or `helpers` as they would be known in Rails-land then create a new Context class and include your helper methods there:
111
+
112
+ ```ruby
113
+
114
+ module MyHelperMethods
115
+ def my_helpful_method
116
+ # ... do something complex that you want to keep out of the template
117
+ end
118
+ end
119
+
120
+ # You *must* inherit from Cutaneous::Context!
121
+ class MyContext < Cutaneous::Context
122
+ include MyHelperMethods
123
+ end
124
+
125
+ context = MyContext.new(instance, parameter: "value")
126
+
127
+ result = engine.render("template", context)
128
+ ```
129
+
130
+ ### Errors
131
+
132
+ Cutaneous silently swallows errors about missing expressions in templates. If you want to instead report these errors override the `__handle_error` context method:
133
+
134
+ ```ruby
135
+ class MyContext < Cutaneous::Context
136
+ def __handle_error(e)
137
+ logger.warn(e)
138
+ end
139
+ end
140
+ ```
141
+
142
+ Cutaneous will do its best to keep the line numbers consistent between templates and the generated code (although see "Bugs" below...). This will hopefully make debugging easier.
143
+
144
+ ## Bugs/TODO
145
+
146
+ - Make the Syntax more powerful and capable of dealing with any syntax (e.g. implement an ERB like syntax). At the moment they only deal with brace-based syntaxes and brace counting is built into the Lexer.
147
+ - Using template inheritance messes up the line numbers of errors... Not sure what to do about that...
148
+
149
+ ## License
150
+
151
+ Cutaneous is released under an MIT license (see LICENSE).
data/cutaneous.gemspec CHANGED
@@ -8,25 +8,28 @@ Gem::Specification.new do |s|
8
8
  s.specification_version = 2 if s.respond_to? :specification_version=
9
9
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
10
  s.rubygems_version = '1.3.5'
11
+ s.required_ruby_version = ">= 1.9.2"
11
12
 
12
13
  ## Leave these as is they will be modified for you by the rake gemspec task.
13
14
  ## If your rubyforge_project name is different, then edit it and comment out
14
15
  ## the sub! line in the Rakefile
15
16
  s.name = 'cutaneous'
16
- s.version = '0.1.1'
17
- s.date = '2012-07-23'
17
+ s.version = '0.1.2'
18
+ s.date = '2012-07-25'
18
19
  s.rubyforge_project = 'cutaneous'
19
20
 
20
21
  ## Make sure your summary is short. The description may be as long
21
22
  ## as you like.
22
- s.summary = "Short description used in Gem listings."
23
- s.description = "Long description. Maybe copied from the README."
23
+ s.summary = "A Ruby templating language with Django style template inheritance"
24
+ s.description = "Cutaneous is the Ruby templating language designed for " \
25
+ "use with Spontaneous CMS. It has a simple syntax but powerful" \
26
+ "features such as Djano style template inheritance through blocks."
24
27
 
25
28
  ## List the primary authors. If there are a bunch of authors, it's probably
26
29
  ## better to set the email to an email list or something. If you don't have
27
30
  ## a custom homepage, consider using your GitHub URL or the like.
28
31
  s.authors = ["Garry Hill"]
29
- s.email = 'garry@spontaneous.io'
32
+ s.email = 'garry@magnetised.net'
30
33
  s.homepage = 'https://github.com/SpontaneousCMS/cutaneous'
31
34
 
32
35
  ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
@@ -71,11 +74,13 @@ Gem::Specification.new do |s|
71
74
  test/fixtures/a.html.cut
72
75
  test/fixtures/b.html.cut
73
76
  test/fixtures/c.html.cut
74
- test/fixtures/comments.html.cut
77
+ test/fixtures/comments1.html.cut
78
+ test/fixtures/comments2.html.cut
75
79
  test/fixtures/d.html.cut
76
80
  test/fixtures/e.html.cut
77
81
  test/fixtures/error.html.cut
78
- test/fixtures/expressions.html.cut
82
+ test/fixtures/expressions1.html.cut
83
+ test/fixtures/expressions2.html.cut
79
84
  test/fixtures/include.html.cut
80
85
  test/fixtures/include.rss.cut
81
86
  test/fixtures/included_error.html.cut
@@ -87,9 +92,11 @@ Gem::Specification.new do |s|
87
92
  test/fixtures/partial.html.cut
88
93
  test/fixtures/partial.rss.cut
89
94
  test/fixtures/render.html.cut
90
- test/fixtures/statements.html.cut
95
+ test/fixtures/statements1.html.cut
96
+ test/fixtures/statements2.html.cut
91
97
  test/fixtures/target.html.cut
92
- test/fixtures/whitespace.html.cut
98
+ test/fixtures/whitespace1.html.cut
99
+ test/fixtures/whitespace2.html.cut
93
100
  test/helper.rb
94
101
  test/test_blocks.rb
95
102
  test/test_cache.rb
@@ -1,4 +1,5 @@
1
1
  require 'delegate'
2
+ require 'cgi'
2
3
 
3
4
  module Cutaneous
4
5
  class Context < Delegator
@@ -15,7 +16,7 @@ module Cutaneous
15
16
  end
16
17
 
17
18
  def escape(value)
18
- value
19
+ CGI::escapeHTML(value)
19
20
  end
20
21
 
21
22
  def include(template_name, locals = {})
@@ -57,8 +58,26 @@ module Cutaneous
57
58
  end
58
59
  end
59
60
 
61
+ # Sets up the local variables and also creates singleton methods on this
62
+ # instance so that the local values will override any method implementations
63
+ # on the context itself. i.e.:
64
+ #
65
+ # class MyContext < Cutanteous::Context
66
+ # def monkey
67
+ # "puzzle"
68
+ # end
69
+ # end
70
+ #
71
+ # context = MyContext.new(Object.new, monkey: "magic")
72
+ #
73
+ # context.monkey #=> "magic" not "puzzle"
74
+ #
60
75
  def __update_with_locals(locals)
61
76
  @__locals.update(locals)
77
+ singleton = singleton_class
78
+ locals.each do |name, value|
79
+ singleton.__send__(:define_method, name) { value }
80
+ end
62
81
  self
63
82
  end
64
83
 
@@ -5,7 +5,7 @@ module Cutaneous
5
5
  attr_reader :roots
6
6
  attr_accessor :loader_class, :default_format
7
7
 
8
- def initialize(template_roots, syntax, default_format = "html")
8
+ def initialize(template_roots, syntax = Cutaneous::FirstPassSyntax, default_format = "html")
9
9
  @roots = Array(template_roots)
10
10
  @syntax = syntax
11
11
  @loader_class = FileLoader
@@ -75,18 +75,4 @@ module Cutaneous
75
75
  [:text, expression]
76
76
  end
77
77
  end
78
-
79
- FirstPassSyntax = Cutaneous::Syntax.new({
80
- :comment => %w(!{ }),
81
- :expression => %w(${ }),
82
- :escaped_expression => %w($${ }),
83
- :statement => %w(%{ })
84
- })
85
-
86
- SecondPassSyntax = Cutaneous::Syntax.new({
87
- :comment => %w(!{ }),
88
- :expression => %w({{ }}),
89
- :escaped_expression => %w({$ $}),
90
- :statement => %w({% %})
91
- })
92
78
  end
data/lib/cutaneous.rb CHANGED
@@ -7,7 +7,7 @@ require 'cutaneous/lexer'
7
7
  require 'cutaneous/compiler'
8
8
 
9
9
  module Cutaneous
10
- VERSION = "0.1.1"
10
+ VERSION = "0.1.2"
11
11
 
12
12
  class CompilationError < Exception; end
13
13
 
@@ -20,4 +20,18 @@ module Cutaneous
20
20
  def self.extension
21
21
  "cut"
22
22
  end
23
+
24
+ FirstPassSyntax = Cutaneous::Syntax.new({
25
+ :comment => %w(!{ }),
26
+ :expression => %w(${ }),
27
+ :escaped_expression => %w($${ }),
28
+ :statement => %w(%{ })
29
+ })
30
+
31
+ SecondPassSyntax = Cutaneous::Syntax.new({
32
+ :comment => %w(!{ }),
33
+ :expression => %w({{ }}),
34
+ :escaped_expression => %w({$ $}),
35
+ :statement => %w({% %})
36
+ })
23
37
  end
@@ -0,0 +1 @@
1
+ !{ this is a comment }
@@ -0,0 +1 @@
1
+ This is {{ right }} {$ code $}
@@ -0,0 +1,3 @@
1
+ {% 3.times do |n| %}
2
+ This is {{ right }} {{ n }}
3
+ {% end %}
@@ -0,0 +1,3 @@
1
+ {% 3.times do |n| -%}
2
+ here {{ n }}
3
+ {% end -%}
data/test/helper.rb CHANGED
@@ -6,9 +6,6 @@ require 'minitest/autorun'
6
6
  require 'cutaneous'
7
7
 
8
8
  class TestContext < Cutaneous::Context
9
- def escape(value)
10
- value.gsub(/</, "&lt;").gsub(/>/, "&gt;")
11
- end
12
9
  end
13
10
 
14
11
  class MiniTest::Spec
data/test/test_core.rb CHANGED
@@ -1,33 +1,70 @@
1
1
  require File.expand_path('../helper', __FILE__)
2
2
 
3
- describe Cutaneous do
3
+ describe "First pass parser" do
4
4
  let(:template_root) { File.expand_path("../fixtures", __FILE__) }
5
- let(:engine) { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }
5
+ subject { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }
6
6
 
7
- it "Will parse & execute a simple template with expressions" do
7
+ it "will parse & execute a simple template with expressions" do
8
8
  context = ContextHash(right: "right", code: "<tag/>")
9
- result = engine.render("expressions", context)
9
+ result = subject.render("expressions1", context)
10
10
  result.must_equal "This is right &lt;tag/&gt;\n"
11
11
  end
12
12
 
13
- it "Will parse & execute a simple template with statements" do
13
+ it "will parse & execute a simple template with statements" do
14
14
  context = ContextHash(right: "right")
15
- result = engine.render("statements", context)
15
+ result = subject.render("statements1", context)
16
16
  result.must_equal "\nThis is right 0\n\nThis is right 1\n\nThis is right 2\n\n"
17
17
  end
18
18
 
19
- it "Will parse & execute a simple template with comments" do
19
+ it "will parse & execute a simple template with comments" do
20
20
  context = ContextHash(right: "right")
21
- result = engine.render("comments", context)
21
+ result = subject.render("comments1", context)
22
22
  result.must_equal "\n"
23
23
  end
24
24
 
25
- it "Will remove whitespace after tags with a closing '-'" do
25
+ it "will remove whitespace after tags with a closing '-'" do
26
26
  context = ContextHash(right: "right")
27
- result = engine.render("whitespace", context)
27
+ result = subject.render("whitespace1", context)
28
28
  expected = ["aa", "here 0", "here 1", "here 2\n", "ac\n", "ad\n", "ae\n", "af\n", "ag\n"].join("\n")
29
29
  result.must_equal expected
30
30
  end
31
+ end
32
+
33
+ describe "Second pass parser" do
34
+ let(:template_root) { File.expand_path("../fixtures", __FILE__) }
35
+ subject { Cutaneous::Engine.new(template_root, Cutaneous::SecondPassSyntax, "html") }
36
+
37
+ it "will parse & execute a simple template with expressions" do
38
+ context = ContextHash(right: "right", code: "<tag/>")
39
+ result = subject.render("expressions2", context)
40
+ result.must_equal "This is right &lt;tag/&gt;\n"
41
+ end
42
+
43
+ it "will parse & execute a simple template with statements" do
44
+ context = ContextHash(right: "right")
45
+ result = subject.render("statements2", context)
46
+ result.must_equal "\nThis is right 0\n\nThis is right 1\n\nThis is right 2\n\n"
47
+ end
48
+
49
+ it "will parse & execute a simple template with comments" do
50
+ context = ContextHash(right: "right")
51
+ result = subject.render("comments2", context)
52
+ result.must_equal "\n"
53
+ end
54
+
55
+ it "will remove whitespace after tags with a closing '-'" do
56
+ context = ContextHash(right: "right")
57
+ result = subject.render("whitespace2", context)
58
+ expected = ["here 0", "here 1", "here 2\n"].join("\n")
59
+ result.must_equal expected
60
+ end
61
+ end
62
+
63
+ describe Cutaneous do
64
+ let(:template_root) { File.expand_path("../fixtures", __FILE__) }
65
+ let(:engine) { engine1 }
66
+ let(:engine1) { Cutaneous::Engine.new(template_root, Cutaneous::FirstPassSyntax, "html") }
67
+ let(:engine2) { Cutaneous::Engine.new(template_root, Cutaneous::SecondPassSyntax, "html") }
31
68
 
32
69
  it "Allows you to include other templates and pass them parameters" do
33
70
  context = ContextHash(right: "right")
@@ -129,12 +166,12 @@ describe Cutaneous do
129
166
 
130
167
  it "Accepts absolute template paths" do
131
168
  context = ContextHash(right: "right", code: "<tag/>")
132
- result = engine.render(File.join(template_root, "expressions"), context)
169
+ result = engine.render(File.join(template_root, "expressions1"), context)
133
170
  result.must_equal "This is right &lt;tag/&gt;\n"
134
171
  end
135
172
 
136
173
  it "Tests for the existence of a template file for a certain format" do
137
- assert engine.template_exists?(template_root, "expressions", "html")
174
+ assert engine.template_exists?(template_root, "expressions1", "html")
138
175
  assert engine.template_exists?(template_root, "other/error", "html")
139
176
  assert engine.template_exists?(template_root, "include", "rss")
140
177
  refute engine.template_exists?(template_root, "missing", "rss")
@@ -157,12 +194,25 @@ describe Cutaneous do
157
194
  it "Overwrites object methods with parameters" do
158
195
  klass = Class.new(Object) do
159
196
  def monkey; "see"; end
197
+ def to_s ; "object"; end
160
198
  end
161
199
  context = TestContext.new(klass.new)
162
- result = engine.render_string("${ monkey }", context)
163
- result.must_equal "see"
164
- context = TestContext.new(klass.new, monkey: "magic")
165
- result = engine.render_string("${ monkey }", context)
166
- result.must_equal "magic"
200
+ result = engine.render_string("${ monkey } ${ to_s }", context)
201
+ result.must_equal "see object"
202
+ context = TestContext.new(klass.new, monkey: "magic", to_s: "fairy")
203
+ result = engine.render_string("${ monkey } ${ to_s }", context)
204
+ result.must_equal "magic fairy"
205
+ end
206
+
207
+ it "Overwrites helper methods with local values" do
208
+ context_class = Class.new(TestContext) do
209
+ def monkey
210
+ "wrong"
211
+ end
212
+ end
213
+ context = context_class.new(Object.new, monkey: "magic", to_s: "fairy")
214
+ # def context.monkey; "wrong"; end
215
+ result = engine.render_string("${ monkey } ${ to_s }", context)
216
+ result.must_equal "magic fairy"
167
217
  end
168
218
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cutaneous
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,10 +9,12 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-23 00:00:00.000000000 Z
12
+ date: 2012-07-25 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Long description. Maybe copied from the README.
15
- email: garry@spontaneous.io
14
+ description: Cutaneous is the Ruby templating language designed for use with Spontaneous
15
+ CMS. It has a simple syntax but powerfulfeatures such as Djano style template inheritance
16
+ through blocks.
17
+ email: garry@magnetised.net
16
18
  executables: []
17
19
  extensions: []
18
20
  extra_rdoc_files:
@@ -36,11 +38,13 @@ files:
36
38
  - test/fixtures/a.html.cut
37
39
  - test/fixtures/b.html.cut
38
40
  - test/fixtures/c.html.cut
39
- - test/fixtures/comments.html.cut
41
+ - test/fixtures/comments1.html.cut
42
+ - test/fixtures/comments2.html.cut
40
43
  - test/fixtures/d.html.cut
41
44
  - test/fixtures/e.html.cut
42
45
  - test/fixtures/error.html.cut
43
- - test/fixtures/expressions.html.cut
46
+ - test/fixtures/expressions1.html.cut
47
+ - test/fixtures/expressions2.html.cut
44
48
  - test/fixtures/include.html.cut
45
49
  - test/fixtures/include.rss.cut
46
50
  - test/fixtures/included_error.html.cut
@@ -52,9 +56,11 @@ files:
52
56
  - test/fixtures/partial.html.cut
53
57
  - test/fixtures/partial.rss.cut
54
58
  - test/fixtures/render.html.cut
55
- - test/fixtures/statements.html.cut
59
+ - test/fixtures/statements1.html.cut
60
+ - test/fixtures/statements2.html.cut
56
61
  - test/fixtures/target.html.cut
57
- - test/fixtures/whitespace.html.cut
62
+ - test/fixtures/whitespace1.html.cut
63
+ - test/fixtures/whitespace2.html.cut
58
64
  - test/helper.rb
59
65
  - test/test_blocks.rb
60
66
  - test/test_cache.rb
@@ -71,7 +77,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
77
  requirements:
72
78
  - - ! '>='
73
79
  - !ruby/object:Gem::Version
74
- version: '0'
80
+ version: 1.9.2
75
81
  required_rubygems_version: !ruby/object:Gem::Requirement
76
82
  none: false
77
83
  requirements:
@@ -83,7 +89,7 @@ rubyforge_project: cutaneous
83
89
  rubygems_version: 1.8.21
84
90
  signing_key:
85
91
  specification_version: 2
86
- summary: Short description used in Gem listings.
92
+ summary: A Ruby templating language with Django style template inheritance
87
93
  test_files:
88
94
  - test/test_blocks.rb
89
95
  - test/test_cache.rb