mustache 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/mustache +78 -39
- data/lib/mustache.rb +12 -11
- data/lib/mustache/generator.rb +139 -0
- data/lib/mustache/parser.rb +216 -0
- data/lib/mustache/sinatra.rb +91 -42
- data/lib/mustache/template.rb +18 -132
- data/lib/mustache/version.rb +1 -1
- data/man/mustache.1 +41 -1
- data/man/mustache.1.html +35 -1
- data/man/mustache.1.ron +34 -1
- data/test/fixtures/delimiters.mustache +5 -3
- data/test/fixtures/delimiters.rb +6 -5
- data/test/mustache_test.rb +65 -6
- data/test/parser_test.rb +54 -0
- metadata +5 -2
data/bin/mustache
CHANGED
@@ -1,51 +1,90 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
require 'mustache'
|
4
3
|
require 'yaml'
|
4
|
+
require 'optparse'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
$ mustache
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
6
|
+
require 'mustache'
|
7
|
+
require 'mustache/version'
|
8
|
+
|
9
|
+
class Mustache
|
10
|
+
class CLI
|
11
|
+
# Return a structure describing the options.
|
12
|
+
def self.parse_options(args)
|
13
|
+
opts = OptionParser.new do |opts|
|
14
|
+
opts.banner = "Usage: mustache [-c] [-t] FILE ..."
|
15
|
+
|
16
|
+
opts.separator " "
|
17
|
+
|
18
|
+
opts.separator "Examples:"
|
19
|
+
opts.separator " $ mustache data.yml template.mustache"
|
20
|
+
opts.separator " $ cat data.yml | mustache - template.mustache"
|
21
|
+
opts.separator " $ mustache -c template.mustache"
|
22
|
+
|
23
|
+
opts.separator " "
|
24
|
+
|
25
|
+
opts.separator " See mustache(1) or " +
|
26
|
+
"http://defunkt.github.com/mustache/mustache.1.html"
|
27
|
+
opts.separator " for more details."
|
28
|
+
|
29
|
+
opts.separator " "
|
30
|
+
opts.separator "Options:"
|
31
|
+
|
32
|
+
opts.on("-c", "--compile FILE",
|
33
|
+
"Print the compiled Ruby for a given template.") do |file|
|
34
|
+
puts Mustache::Template.new(File.read(file)).compile
|
35
|
+
exit
|
36
|
+
end
|
28
37
|
|
29
|
-
|
30
|
-
|
38
|
+
opts.on("-t", "--tokens FILE",
|
39
|
+
"Print the tokenized form of a given template.") do |file|
|
40
|
+
require 'pp'
|
41
|
+
pp Mustache::Template.new(File.read(file)).tokens
|
42
|
+
exit
|
43
|
+
end
|
31
44
|
|
32
|
-
|
33
|
-
help
|
45
|
+
opts.separator "Common Options:"
|
34
46
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
47
|
+
opts.on("-v", "--version", "Print the version") do |v|
|
48
|
+
puts "Mustache v#{Mustache::Version}"
|
49
|
+
exit
|
50
|
+
end
|
39
51
|
|
40
|
-
|
41
|
-
|
42
|
-
|
52
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
53
|
+
puts opts
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
end
|
43
57
|
|
44
|
-
|
45
|
-
|
58
|
+
opts.separator ""
|
59
|
+
|
60
|
+
opts.parse!(args)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Does the dirty work of reading files from STDIN and the command
|
64
|
+
# line then processing them. The meat of this script, if you will.
|
65
|
+
def self.process_files(input_stream)
|
66
|
+
doc = input_stream.read
|
67
|
+
|
68
|
+
if doc =~ /^(\s*---(.+)---\s*)/m
|
69
|
+
yaml = $2.strip
|
70
|
+
template = doc.sub($1, '')
|
71
|
+
|
72
|
+
YAML.each_document(yaml) do |data|
|
73
|
+
puts Mustache.render(template, data)
|
74
|
+
end
|
75
|
+
else
|
76
|
+
puts doc
|
77
|
+
end
|
46
78
|
end
|
47
|
-
else
|
48
|
-
puts doc
|
49
79
|
end
|
50
|
-
exit
|
51
80
|
end
|
81
|
+
|
82
|
+
# Help is the default.
|
83
|
+
ARGV << '-h' if ARGV.empty? && $stdin.tty?
|
84
|
+
|
85
|
+
# Process options
|
86
|
+
Mustache::CLI.parse_options(ARGV) if $stdin.tty?
|
87
|
+
|
88
|
+
# Still here - process ARGF
|
89
|
+
Mustache::CLI.process_files(ARGF)
|
90
|
+
|
data/lib/mustache.rb
CHANGED
@@ -85,14 +85,6 @@ class Mustache
|
|
85
85
|
render(*args)
|
86
86
|
end
|
87
87
|
|
88
|
-
# Compiles a string template and returns it as a string for use as
|
89
|
-
# an interpolated Ruby string (not fully rendered HTML), e.g.
|
90
|
-
# >> Mustache.compile("Hi, {{person}}!")
|
91
|
-
# => "Hi, #{CGI.escapeHTML(ctx[:person].to_s)}!"
|
92
|
-
def self.compile(template)
|
93
|
-
templateify(template).to_s
|
94
|
-
end
|
95
|
-
|
96
88
|
# Given a file name and an optional context, attempts to load and
|
97
89
|
# render the file as a template.
|
98
90
|
def self.render_file(name, context = {})
|
@@ -145,7 +137,7 @@ class Mustache
|
|
145
137
|
# The template file is the absolute path of the file Mustache will
|
146
138
|
# use as its template. By default it's ./class_name.mustache
|
147
139
|
def self.template_file
|
148
|
-
@template_file || "#{path}/#{
|
140
|
+
@template_file || "#{path}/#{template_name}.#{template_extension}"
|
149
141
|
end
|
150
142
|
|
151
143
|
def self.template_file=(template_file)
|
@@ -271,7 +263,7 @@ class Mustache
|
|
271
263
|
if obj.is_a?(Template)
|
272
264
|
obj
|
273
265
|
else
|
274
|
-
Template.new(obj.to_s
|
266
|
+
Template.new(obj.to_s)
|
275
267
|
end
|
276
268
|
end
|
277
269
|
|
@@ -318,7 +310,16 @@ class Mustache
|
|
318
310
|
# Parses our fancy pants template file and returns normal file with
|
319
311
|
# all special {{tags}} and {{#sections}}replaced{{/sections}}.
|
320
312
|
def render(data = template, ctx = {})
|
321
|
-
templateify(data)
|
313
|
+
tpl = templateify(data)
|
314
|
+
|
315
|
+
return tpl.render(context) if ctx == {}
|
316
|
+
|
317
|
+
begin
|
318
|
+
context.push(ctx)
|
319
|
+
tpl.render(context)
|
320
|
+
ensure
|
321
|
+
context.pop
|
322
|
+
end
|
322
323
|
end
|
323
324
|
alias_method :to_html, :render
|
324
325
|
alias_method :to_text, :render
|
@@ -0,0 +1,139 @@
|
|
1
|
+
class Mustache
|
2
|
+
# The Generator is in charge of taking an array of Mustache tokens,
|
3
|
+
# usually assembled by the Parser, and generating an interpolatable
|
4
|
+
# Ruby string. This string is considered the "compiled" template
|
5
|
+
# because at that point we're relying on Ruby to do the parsing and
|
6
|
+
# run our code.
|
7
|
+
#
|
8
|
+
# For example, let's take this template:
|
9
|
+
#
|
10
|
+
# Hi {{thing}}!
|
11
|
+
#
|
12
|
+
# If we run this through the Parser we'll get these tokens:
|
13
|
+
#
|
14
|
+
# [:multi,
|
15
|
+
# [:static, "Hi "],
|
16
|
+
# [:mustache, :etag, "thing"],
|
17
|
+
# [:static, "!\n"]]
|
18
|
+
#
|
19
|
+
# Now let's hand that to the Generator:
|
20
|
+
#
|
21
|
+
# >> puts Mustache::Generator.new.compile(tokens)
|
22
|
+
# "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
|
23
|
+
#
|
24
|
+
# You can see the generated Ruby string for any template with the
|
25
|
+
# mustache(1) command line tool:
|
26
|
+
#
|
27
|
+
# $ mustache --compile test.mustache
|
28
|
+
# "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
|
29
|
+
class Generator
|
30
|
+
# Options are unused for now but may become useful in the future.
|
31
|
+
def initialize(options = {})
|
32
|
+
@options = options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Given an array of tokens, returns an interpolatable Ruby string.
|
36
|
+
def compile(exp)
|
37
|
+
"\"#{compile!(exp)}\""
|
38
|
+
end
|
39
|
+
|
40
|
+
# Given an array of tokens, converts them into Ruby code. In
|
41
|
+
# particular there are three types of expressions we are concerned
|
42
|
+
# with:
|
43
|
+
#
|
44
|
+
# :multi
|
45
|
+
# Mixed bag of :static, :mustache, and whatever.
|
46
|
+
#
|
47
|
+
# :static
|
48
|
+
# Normal HTML, the stuff outside of {{mustaches}}.
|
49
|
+
#
|
50
|
+
# :mustache
|
51
|
+
# Any Mustache tag, from sections to partials.
|
52
|
+
#
|
53
|
+
# To give you an idea of what you'll be dealing with take this
|
54
|
+
# template:
|
55
|
+
#
|
56
|
+
# Hello {{name}}
|
57
|
+
# You have just won ${{value}}!
|
58
|
+
# {{#in_ca}}
|
59
|
+
# Well, ${{taxed_value}}, after taxes.
|
60
|
+
# {{/in_ca}}
|
61
|
+
#
|
62
|
+
# If we run this through the Parser, we'll get back this array of
|
63
|
+
# tokens:
|
64
|
+
#
|
65
|
+
# [:multi,
|
66
|
+
# [:static, "Hello "],
|
67
|
+
# [:mustache, :etag, "name"],
|
68
|
+
# [:static, "\nYou have just won $"],
|
69
|
+
# [:mustache, :etag, "value"],
|
70
|
+
# [:static, "!\n"],
|
71
|
+
# [:mustache,
|
72
|
+
# :section,
|
73
|
+
# "in_ca",
|
74
|
+
# [:multi,
|
75
|
+
# [:static, "Well, $"],
|
76
|
+
# [:mustache, :etag, "taxed_value"],
|
77
|
+
# [:static, ", after taxes.\n"]]]]
|
78
|
+
def compile!(exp)
|
79
|
+
case exp.first
|
80
|
+
when :multi
|
81
|
+
exp[1..-1].map { |e| compile!(e) }.join
|
82
|
+
when :static
|
83
|
+
str(exp[1])
|
84
|
+
when :mustache
|
85
|
+
send("on_#{exp[1]}", *exp[2..-1])
|
86
|
+
else
|
87
|
+
raise "Unhandled exp: #{exp.first}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Callback fired when the compiler finds a section token. We're
|
92
|
+
# passed the section name and the array of tokens.
|
93
|
+
def on_section(name, content)
|
94
|
+
# Convert the tokenized content of this section into a Ruby
|
95
|
+
# string we can use.
|
96
|
+
code = compile(content)
|
97
|
+
|
98
|
+
# Compile the Ruby for this section now that we know what's
|
99
|
+
# inside the section.
|
100
|
+
ev(<<-compiled)
|
101
|
+
if v = ctx[#{name.to_sym.inspect}]
|
102
|
+
if v == true
|
103
|
+
#{code}
|
104
|
+
else
|
105
|
+
v = [v] unless v.is_a?(Array) # shortcut when passed non-array
|
106
|
+
v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
|
107
|
+
end
|
108
|
+
end
|
109
|
+
compiled
|
110
|
+
end
|
111
|
+
|
112
|
+
# Fired when the compiler finds a partial. We want to return code
|
113
|
+
# which calls a partial at runtime instead of expanding and
|
114
|
+
# including the partial's body to allow for recursive partials.
|
115
|
+
def on_partial(name)
|
116
|
+
ev("ctx.partial(#{name.to_sym.inspect})")
|
117
|
+
end
|
118
|
+
|
119
|
+
# An unescaped tag.
|
120
|
+
def on_utag(name)
|
121
|
+
ev("ctx[#{name.to_sym.inspect}]")
|
122
|
+
end
|
123
|
+
|
124
|
+
# An escaped tag.
|
125
|
+
def on_etag(name)
|
126
|
+
ev("CGI.escapeHTML(ctx[#{name.to_sym.inspect}].to_s)")
|
127
|
+
end
|
128
|
+
|
129
|
+
# An interpolation-friendly version of a string, for use within a
|
130
|
+
# Ruby string.
|
131
|
+
def ev(s)
|
132
|
+
"#\{#{s}}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def str(s)
|
136
|
+
s.inspect[1..-2]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
class Mustache
|
4
|
+
# The Parser is responsible for taking a string template and
|
5
|
+
# converting it into an array of tokens and, really, expressions. It
|
6
|
+
# raises SyntaxError if there is anything it doesn't understand and
|
7
|
+
# knows which sigil corresponds to which tag type.
|
8
|
+
#
|
9
|
+
# For example, given this template:
|
10
|
+
#
|
11
|
+
# Hi {{thing}}!
|
12
|
+
#
|
13
|
+
# Run through the Parser we'll get these tokens:
|
14
|
+
#
|
15
|
+
# [:multi,
|
16
|
+
# [:static, "Hi "],
|
17
|
+
# [:mustache, :etag, "thing"],
|
18
|
+
# [:static, "!\n"]]
|
19
|
+
#
|
20
|
+
# You can see the array of tokens for any template with the
|
21
|
+
# mustache(1) command line tool:
|
22
|
+
#
|
23
|
+
# $ mustache --tokens test.mustache
|
24
|
+
# [:multi, [:static, "Hi "], [:mustache, :etag, "thing"], [:static, "!\n"]]
|
25
|
+
class Parser
|
26
|
+
# A SyntaxError is raised when the Parser comes across unclosed
|
27
|
+
# tags, sections, illegal content in tags, or anything of that
|
28
|
+
# sort.
|
29
|
+
class SyntaxError < StandardError
|
30
|
+
def initialize(message, position)
|
31
|
+
@message = message
|
32
|
+
@lineno, @column, @line = position
|
33
|
+
@stripped_line = @line.strip
|
34
|
+
@stripped_column = @column - (@line.size - @line.lstrip.size)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_s
|
38
|
+
<<-EOF
|
39
|
+
#{@message}
|
40
|
+
Line #{@lineno}
|
41
|
+
#{@stripped_line}
|
42
|
+
#{' ' * @stripped_column}^
|
43
|
+
EOF
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# After these types of tags, all whitespace will be skipped.
|
48
|
+
SKIP_WHITESPACE = [ '#', '/' ]
|
49
|
+
|
50
|
+
# The content allowed in a tag name.
|
51
|
+
ALLOWED_CONTENT = /(\w|[?!-])*/
|
52
|
+
|
53
|
+
# These types of tags allow any content,
|
54
|
+
# the rest only allow ALLOWED_CONTENT.
|
55
|
+
ANY_CONTENT = [ '!', '=' ]
|
56
|
+
|
57
|
+
attr_reader :scanner, :result
|
58
|
+
attr_writer :otag, :ctag
|
59
|
+
|
60
|
+
# Accepts an options hash which does nothing but may be used in
|
61
|
+
# the future.
|
62
|
+
def initialize(options = {})
|
63
|
+
@options = {}
|
64
|
+
end
|
65
|
+
|
66
|
+
# The opening tag delimiter. This may be changed at runtime.
|
67
|
+
def otag
|
68
|
+
@otag ||= '{{'
|
69
|
+
end
|
70
|
+
|
71
|
+
# The closing tag delimiter. This too may be changed at runtime.
|
72
|
+
def ctag
|
73
|
+
@ctag ||= '}}'
|
74
|
+
end
|
75
|
+
|
76
|
+
# Given a string template, returns an array of tokens.
|
77
|
+
def compile(template)
|
78
|
+
# Keeps information about opened sections.
|
79
|
+
@sections = []
|
80
|
+
@result = [:multi]
|
81
|
+
@scanner = StringScanner.new(template)
|
82
|
+
|
83
|
+
# Scan until the end of the template.
|
84
|
+
until @scanner.eos?
|
85
|
+
scan_tags || scan_text
|
86
|
+
end
|
87
|
+
|
88
|
+
if !@sections.empty?
|
89
|
+
# We have parsed the whole file, but there's still opened sections.
|
90
|
+
type, pos, result = @sections.pop
|
91
|
+
error "Unclosed section #{type.inspect}", pos
|
92
|
+
end
|
93
|
+
|
94
|
+
@result
|
95
|
+
end
|
96
|
+
|
97
|
+
# Find {{mustaches}} and add them to the @result array.
|
98
|
+
def scan_tags
|
99
|
+
# Scan until we hit an opening delimiter.
|
100
|
+
return unless @scanner.scan(regexp(otag))
|
101
|
+
|
102
|
+
# Since {{= rewrites ctag, we store the ctag which should be used
|
103
|
+
# when parsing this specific tag.
|
104
|
+
current_ctag = self.ctag
|
105
|
+
type = @scanner.scan(/#|\/|=|!|<|>|&|\{/)
|
106
|
+
@scanner.skip(/\s*/)
|
107
|
+
|
108
|
+
# ANY_CONTENT tags allow any character inside of them, while
|
109
|
+
# other tags (such as variables) are more strict.
|
110
|
+
if ANY_CONTENT.include?(type)
|
111
|
+
r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
|
112
|
+
content = scan_until_exclusive(r)
|
113
|
+
else
|
114
|
+
content = @scanner.scan(ALLOWED_CONTENT)
|
115
|
+
end
|
116
|
+
|
117
|
+
# We found {{ but we can't figure out what's going on inside.
|
118
|
+
error "Illegal content in tag" if content.empty?
|
119
|
+
|
120
|
+
# Based on the sigil, do what needs to be done.
|
121
|
+
case type
|
122
|
+
when '#'
|
123
|
+
block = [:multi]
|
124
|
+
@result << [:mustache, :section, content, block]
|
125
|
+
@sections << [content, position, @result]
|
126
|
+
@result = block
|
127
|
+
when '/'
|
128
|
+
section, pos, result = @sections.pop
|
129
|
+
@result = result
|
130
|
+
|
131
|
+
if section.nil?
|
132
|
+
error "Closing unopened #{content.inspect}"
|
133
|
+
elsif section != content
|
134
|
+
error "Unclosed section #{section.inspect}", pos
|
135
|
+
end
|
136
|
+
when '!'
|
137
|
+
# ignore comments
|
138
|
+
when '='
|
139
|
+
self.otag, self.ctag = content.split(' ', 2)
|
140
|
+
when '>', '<'
|
141
|
+
@result << [:mustache, :partial, content]
|
142
|
+
when '{', '&'
|
143
|
+
# The closing } in unescaped tags is just a hack for
|
144
|
+
# aesthetics.
|
145
|
+
type = "}" if type == "{"
|
146
|
+
@result << [:mustache, :utag, content]
|
147
|
+
else
|
148
|
+
@result << [:mustache, :etag, content]
|
149
|
+
end
|
150
|
+
|
151
|
+
# Skip whitespace and any balancing sigils after the content
|
152
|
+
# inside this tag.
|
153
|
+
@scanner.skip(/\s+/)
|
154
|
+
@scanner.skip(regexp(type)) if type
|
155
|
+
|
156
|
+
# Try to find the closing tag.
|
157
|
+
unless close = @scanner.scan(regexp(current_ctag))
|
158
|
+
error "Unclosed tag"
|
159
|
+
end
|
160
|
+
|
161
|
+
# Skip whitespace following this tag if we need to.
|
162
|
+
@scanner.skip(/\s+/) if SKIP_WHITESPACE.include?(type)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Try to find static text, e.g. raw HTML with no {{mustaches}}.
|
166
|
+
def scan_text
|
167
|
+
text = scan_until_exclusive(regexp(otag))
|
168
|
+
|
169
|
+
if text.nil?
|
170
|
+
# Couldn't find any otag, which means the rest is just static text.
|
171
|
+
text = @scanner.rest
|
172
|
+
# Mark as done.
|
173
|
+
@scanner.clear
|
174
|
+
end
|
175
|
+
|
176
|
+
@result << [:static, text]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Scans the string until the pattern is matched. Returns the substring
|
180
|
+
# *excluding* the end of the match, advancing the scan pointer to that
|
181
|
+
# location. If there is no match, nil is returned.
|
182
|
+
def scan_until_exclusive(regexp)
|
183
|
+
pos = @scanner.pos
|
184
|
+
if @scanner.scan_until(regexp)
|
185
|
+
@scanner.pos -= @scanner.matched.size
|
186
|
+
@scanner.pre_match[pos..-1]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Returns [lineno, column, line]
|
191
|
+
def position
|
192
|
+
# The rest of the current line
|
193
|
+
rest = @scanner.check_until(/\n|\Z/).to_s.chomp
|
194
|
+
|
195
|
+
# What we have parsed so far
|
196
|
+
parsed = @scanner.string[0...@scanner.pos]
|
197
|
+
|
198
|
+
lines = parsed.split("\n")
|
199
|
+
|
200
|
+
[ lines.size, lines.last.size - 1, lines.last + rest ]
|
201
|
+
end
|
202
|
+
|
203
|
+
# Used to quickly convert a string into a regular expression
|
204
|
+
# usable by the string scanner.
|
205
|
+
def regexp(thing)
|
206
|
+
/#{Regexp.escape(thing)}/
|
207
|
+
end
|
208
|
+
|
209
|
+
# Raises a SyntaxError. The message should be the name of the
|
210
|
+
# error - other details such as line number and position are
|
211
|
+
# handled for you.
|
212
|
+
def error(message, pos = position)
|
213
|
+
raise SyntaxError.new(message, pos)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
data/lib/mustache/sinatra.rb
CHANGED
@@ -9,21 +9,21 @@ class Mustache
|
|
9
9
|
# class Hurl < Sinatra::Base
|
10
10
|
# register Mustache::Sinatra
|
11
11
|
#
|
12
|
-
#
|
13
|
-
#
|
12
|
+
# set :mustache, {
|
13
|
+
# # Should be the path to your .mustache template files.
|
14
|
+
# :templates => "path/to/mustache/templates",
|
14
15
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# set :mustaches, "path/to/mustache/views"
|
16
|
+
# # Should be the path to your .rb Mustache view files.
|
17
|
+
# :views => "path/to/mustache/views",
|
18
18
|
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
19
|
+
# # This tells Mustache where to look for the Views module,
|
20
|
+
# # under which your View classes should live. By default it's
|
21
|
+
# # the class of your app - in this case `Hurl`. That is, for an :index
|
22
|
+
# # view Mustache will expect Hurl::Views::Index by default.
|
23
|
+
# # If our Sinatra::Base subclass was instead Hurl::App,
|
24
|
+
# # we'd want to do `set :namespace, Hurl::App`
|
25
|
+
# :namespace => Hurl
|
26
|
+
# }
|
27
27
|
#
|
28
28
|
# get '/stats' do
|
29
29
|
# mustache :stats
|
@@ -43,32 +43,40 @@ class Mustache
|
|
43
43
|
module Helpers
|
44
44
|
# Call this in your Sinatra routes.
|
45
45
|
def mustache(template, options={}, locals={})
|
46
|
-
|
47
|
-
|
46
|
+
# Locals can be passed as options under the :locals key.
|
47
|
+
locals.update(options.delete(:locals) || {})
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
Mustache.view_namespace = options.namespace
|
49
|
+
# Grab any user-defined settings.
|
50
|
+
if settings.respond_to?(:mustache)
|
51
|
+
options = settings.send(:mustache).merge(options)
|
52
|
+
end
|
54
53
|
|
55
|
-
#
|
56
|
-
#
|
57
|
-
|
54
|
+
# Find and cache the view class we want. This ensures the
|
55
|
+
# compiled template is cached, too - no looking up and
|
56
|
+
# compiling templates on each page load.
|
57
|
+
klass = mustache_class(template, options)
|
58
58
|
|
59
|
-
#
|
60
|
-
|
59
|
+
# If they aren't explicitly diabling layouts, try to find
|
60
|
+
# one.
|
61
|
+
if options[:layout] != false
|
62
|
+
# If they passed a layout name use that.
|
63
|
+
layout = mustache_class(options[:layout] || :layout, options)
|
61
64
|
|
62
|
-
|
63
|
-
|
65
|
+
# If it's just an anonymous subclass then don't bother, otherwise
|
66
|
+
# give us a layout instance.
|
67
|
+
if layout.name.empty?
|
68
|
+
layout = nil
|
69
|
+
else
|
70
|
+
layout = layout.new
|
71
|
+
end
|
64
72
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
klass.
|
73
|
+
# Does the view subclass the layout? If so we'll use the
|
74
|
+
# view to render the layout so you can override layout
|
75
|
+
# methods in your view - tricky.
|
76
|
+
view_subclasses_layout = klass < layout.class if layout
|
69
77
|
end
|
70
78
|
|
71
|
-
# Create a new instance for playing with
|
79
|
+
# Create a new instance for playing with.
|
72
80
|
instance = klass.new
|
73
81
|
|
74
82
|
# Copy instance variables set in Sinatra to the view
|
@@ -76,24 +84,65 @@ class Mustache
|
|
76
84
|
instance.instance_variable_set(name, instance_variable_get(name))
|
77
85
|
end
|
78
86
|
|
79
|
-
#
|
80
|
-
|
81
|
-
|
87
|
+
# Render with locals.
|
88
|
+
rendered = instance.render(instance.template, locals)
|
89
|
+
|
90
|
+
# Now render the layout with the view we just rendered, if we
|
91
|
+
# need to.
|
92
|
+
if layout && view_subclasses_layout
|
93
|
+
rendered = instance.render(layout.template, :yield => rendered)
|
94
|
+
elsif layout
|
95
|
+
rendered = layout.render(layout.template, :yield => rendered)
|
82
96
|
end
|
83
97
|
|
84
|
-
#
|
85
|
-
|
86
|
-
|
98
|
+
# That's it.
|
99
|
+
rendered
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns a View class for a given template name.
|
103
|
+
def mustache_class(template, options)
|
104
|
+
@template_cache.fetch(:mustache, template) do
|
105
|
+
compile_mustache(template, options)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Given a view name and settings, finds and prepares an
|
110
|
+
# appropriate view class for this view.
|
111
|
+
def compile_mustache(view, options = {})
|
112
|
+
options[:templates] ||= settings.views if settings.respond_to?(:views)
|
113
|
+
options[:namespace] ||= self.class
|
114
|
+
|
115
|
+
factory = Class.new(Mustache) do
|
116
|
+
self.view_namespace = options[:namespace]
|
117
|
+
self.view_path = options[:views]
|
118
|
+
end
|
119
|
+
|
120
|
+
# Try to find the view class for a given view, e.g.
|
121
|
+
# :view => Hurl::Views::Index.
|
122
|
+
klass = factory.view_class(view)
|
123
|
+
|
124
|
+
# If there is no view class, issue a warning and use the one
|
125
|
+
# we just generated to cache the compiled template.
|
126
|
+
if klass == Mustache
|
127
|
+
warn "No view class found for #{view} in #{factory.view_path}"
|
128
|
+
klass = factory
|
129
|
+
|
130
|
+
# If this is a generic view class make sure we set the
|
131
|
+
# template name as it was given. That is, an anonymous
|
132
|
+
# subclass of Mustache won't know how to find the
|
133
|
+
# "index.mustache" template unless we tell it to.
|
134
|
+
klass.template_name = view.to_s
|
135
|
+
end
|
87
136
|
|
88
|
-
|
89
|
-
|
137
|
+
# Set the template path and return our class.
|
138
|
+
klass.template_path = options[:templates] if options[:templates]
|
139
|
+
klass
|
90
140
|
end
|
91
141
|
end
|
92
142
|
|
143
|
+
# Called when you `register Mustache::Sinatra` in your Sinatra app.
|
93
144
|
def self.registered(app)
|
94
145
|
app.helpers Mustache::Sinatra::Helpers
|
95
|
-
app.set :mustaches, app.views
|
96
|
-
app.set :namespace, app
|
97
146
|
end
|
98
147
|
end
|
99
148
|
end
|
data/lib/mustache/template.rb
CHANGED
@@ -1,46 +1,34 @@
|
|
1
1
|
require 'cgi'
|
2
2
|
|
3
|
+
require 'mustache/parser'
|
4
|
+
require 'mustache/generator'
|
5
|
+
|
3
6
|
class Mustache
|
4
|
-
# A Template
|
7
|
+
# A Template represents a Mustache template. It compiles and caches
|
8
|
+
# a raw string template into something usable.
|
5
9
|
#
|
6
10
|
# The idea is this: when handed a Mustache template, convert it into
|
7
11
|
# a Ruby string by transforming Mustache tags into interpolated
|
8
12
|
# Ruby.
|
9
13
|
#
|
10
|
-
# You shouldn't use this class directly
|
14
|
+
# You shouldn't use this class directly, instead:
|
15
|
+
#
|
16
|
+
# >> Mustache.render(template, hash)
|
11
17
|
class Template
|
12
|
-
# An UnclosedSection error is thrown when a {{# section }} is not
|
13
|
-
# closed.
|
14
|
-
#
|
15
|
-
# For example:
|
16
|
-
# {{# open }} blah {{/ close }}
|
17
|
-
class UnclosedSection < RuntimeError
|
18
|
-
attr_reader :message
|
19
|
-
|
20
|
-
# Report the line number of the offending unclosed section.
|
21
|
-
def initialize(source, matching_line, unclosed_section)
|
22
|
-
num = 0
|
23
|
-
|
24
|
-
source.split("\n").each_with_index do |line, i|
|
25
|
-
num = i + 1
|
26
|
-
break if line.strip == matching_line.strip
|
27
|
-
end
|
28
|
-
|
29
|
-
@message = "line #{num}: ##{unclosed_section.strip} is not closed"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
18
|
# Expects a Mustache template as a string along with a template
|
34
19
|
# path, which it uses to find partials.
|
35
|
-
def initialize(source
|
20
|
+
def initialize(source)
|
36
21
|
@source = source
|
37
|
-
@template_path = template_path
|
38
|
-
@template_extension = template_extension
|
39
22
|
@tmpid = 0
|
40
23
|
end
|
41
24
|
|
42
25
|
# Renders the `@source` Mustache template using the given
|
43
26
|
# `context`, which should be a simple hash keyed with symbols.
|
27
|
+
#
|
28
|
+
# The first time a template is rendered, this method is overriden
|
29
|
+
# and from then on it is "compiled". Subsequent calls will skip
|
30
|
+
# the compilation step and run the Ruby version of the template
|
31
|
+
# directly.
|
44
32
|
def render(context)
|
45
33
|
# Compile our Mustache template into a Ruby string
|
46
34
|
compiled = "def render(ctx) #{compile} end"
|
@@ -57,115 +45,13 @@ class Mustache
|
|
57
45
|
# Does the dirty work of transforming a Mustache template into an
|
58
46
|
# interpolation-friendly Ruby string.
|
59
47
|
def compile(src = @source)
|
60
|
-
|
48
|
+
Generator.new.compile(tokens)
|
61
49
|
end
|
62
50
|
alias_method :to_s, :compile
|
63
51
|
|
64
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
# If true, the section is displayed.
|
68
|
-
# If false, the section is not displayed.
|
69
|
-
# If enumerable, the return value is iterated over (a `for` loop).
|
70
|
-
def compile_sections(src)
|
71
|
-
res = ""
|
72
|
-
while src =~ /#{otag}\#([^\}]*)#{ctag}\s*(.+?)#{otag}\/\1#{ctag}\s*/m
|
73
|
-
# $` = The string to the left of the last successful match
|
74
|
-
res << compile_tags($`)
|
75
|
-
name = $1.strip.to_sym.inspect
|
76
|
-
code = compile($2)
|
77
|
-
res << ev(<<-compiled)
|
78
|
-
if v = ctx[#{name}]
|
79
|
-
v = [v] unless v.is_a?(Array) # shortcut when passed non-array
|
80
|
-
v.map { |h| ctx.push(h); c = #{code}; ctx.pop; c }.join
|
81
|
-
end
|
82
|
-
compiled
|
83
|
-
# $' = The string to the right of the last successful match
|
84
|
-
src = $'
|
85
|
-
end
|
86
|
-
res << compile_tags(src)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Find and replace all non-section tags.
|
90
|
-
# In particular we look for four types of tags:
|
91
|
-
# 1. Escaped variable tags - {{var}}
|
92
|
-
# 2. Unescaped variable tags - {{{var}}}
|
93
|
-
# 3. Comment variable tags - {{! comment}
|
94
|
-
# 4. Partial tags - {{> partial_name }}
|
95
|
-
def compile_tags(src)
|
96
|
-
res = ""
|
97
|
-
while src =~ /#{otag}(#|=|!|<|>|&|\{)?(.+?)\1?#{ctag}+/m
|
98
|
-
res << str($`)
|
99
|
-
case $1
|
100
|
-
when '#'
|
101
|
-
# Unclosed section - raise an error and
|
102
|
-
# report the line number
|
103
|
-
raise UnclosedSection.new(@source, $&, $2)
|
104
|
-
when '!'
|
105
|
-
# ignore comments
|
106
|
-
when '='
|
107
|
-
self.otag, self.ctag = $2.strip.split(' ', 2)
|
108
|
-
when '>', '<'
|
109
|
-
res << compile_partial($2.strip)
|
110
|
-
when '{', '&'
|
111
|
-
res << utag($2.strip)
|
112
|
-
else
|
113
|
-
res << etag($2.strip)
|
114
|
-
end
|
115
|
-
src = $'
|
116
|
-
end
|
117
|
-
res << str(src)
|
118
|
-
end
|
119
|
-
|
120
|
-
# Partials are basically a way to render views from inside other views.
|
121
|
-
def compile_partial(name)
|
122
|
-
name = name.to_s.to_sym.inspect
|
123
|
-
ev("ctx.partial(#{name})")
|
124
|
-
end
|
125
|
-
|
126
|
-
# Generate a temporary id, used when compiling code.
|
127
|
-
def tmpid
|
128
|
-
@tmpid += 1
|
129
|
-
end
|
130
|
-
|
131
|
-
# Get a (hopefully) literal version of an object, sans quotes
|
132
|
-
def str(s)
|
133
|
-
s.inspect[1..-2]
|
134
|
-
end
|
135
|
-
|
136
|
-
# {{ - opening tag delimiter
|
137
|
-
def otag
|
138
|
-
@otag ||= Regexp.escape('{{')
|
139
|
-
end
|
140
|
-
|
141
|
-
def otag=(tag)
|
142
|
-
@otag = Regexp.escape(tag)
|
143
|
-
end
|
144
|
-
|
145
|
-
# }} - closing tag delimiter
|
146
|
-
def ctag
|
147
|
-
@ctag ||= Regexp.escape('}}')
|
148
|
-
end
|
149
|
-
|
150
|
-
def ctag=(tag)
|
151
|
-
@ctag = Regexp.escape(tag)
|
152
|
-
end
|
153
|
-
|
154
|
-
# {{}} - an escaped tag
|
155
|
-
def etag(s)
|
156
|
-
ev("CGI.escapeHTML(ctx[#{s.strip.to_sym.inspect}].to_s)")
|
157
|
-
end
|
158
|
-
|
159
|
-
# {{{}}} - an unescaped tag
|
160
|
-
# Aliased as & - {{&name}}
|
161
|
-
def utag(s)
|
162
|
-
ev("ctx[#{s.strip.to_sym.inspect}]")
|
163
|
-
end
|
164
|
-
|
165
|
-
# An interpolation-friendly version of a string, for use within a
|
166
|
-
# Ruby string.
|
167
|
-
def ev(s)
|
168
|
-
"#\{#{s}}"
|
52
|
+
# Returns an array of tokens for a given template.
|
53
|
+
def tokens(src = @source)
|
54
|
+
Parser.new.compile(src)
|
169
55
|
end
|
170
56
|
end
|
171
57
|
end
|
data/lib/mustache/version.rb
CHANGED
data/man/mustache.1
CHANGED
@@ -7,7 +7,13 @@
|
|
7
7
|
\fBmustache\fR \-\- Mustache processor
|
8
8
|
.
|
9
9
|
.SH "SYNOPSIS"
|
10
|
-
|
10
|
+
.
|
11
|
+
.nf
|
12
|
+
mustache <YAML> <FILE>
|
13
|
+
mustache \-\-compile <FILE>
|
14
|
+
mustache \-\-tokens <FILE>
|
15
|
+
.
|
16
|
+
.fi
|
11
17
|
.
|
12
18
|
.SH "DESCRIPTION"
|
13
19
|
Mustache is a logic\-less templating system for HTML, config files,
|
@@ -107,6 +113,25 @@ Hi scott!
|
|
107
113
|
.
|
108
114
|
.IP "" 0
|
109
115
|
.
|
116
|
+
.SH "OPTIONS"
|
117
|
+
By default \fBmustache\fR will try to render a Mustache template using the
|
118
|
+
YAML frontmatter you provide. It can do a few other things, however.
|
119
|
+
.
|
120
|
+
.TP
|
121
|
+
\fB\-c\fR, \fB\-\-compile\fR
|
122
|
+
Print the compiled Ruby version of a given template. This is the
|
123
|
+
code that is actually used when rendering a template into a
|
124
|
+
string. Useful for debugging but only if you are familiar with
|
125
|
+
Mustache's internals.
|
126
|
+
.
|
127
|
+
.TP
|
128
|
+
\fB\-t\fR, \fB\-\-tokens\fR
|
129
|
+
Print the tokenized form of a given Mustache template. This can be
|
130
|
+
used to understand how Mustache parses a template. The tokens are
|
131
|
+
handed to a generator which compiles them into a Ruby
|
132
|
+
string. Syntax errors and confused tags, therefor, can probably be
|
133
|
+
identified by examining the tokens produced.
|
134
|
+
.
|
110
135
|
.SH "INSTALLATION"
|
111
136
|
If you have RubyGems installed:
|
112
137
|
.
|
@@ -119,6 +144,21 @@ gem install mustache
|
|
119
144
|
.
|
120
145
|
.IP "" 0
|
121
146
|
.
|
147
|
+
.SH "EXAMPLES"
|
148
|
+
.
|
149
|
+
.nf
|
150
|
+
$ mustache data.yml template.mustache
|
151
|
+
$ cat data.yml | mustache \- template.mustache
|
152
|
+
$ mustache \-c template.mustache
|
153
|
+
$ cat <<data | druby mustache \- template.mustache
|
154
|
+
\-\-\-
|
155
|
+
name: Bob
|
156
|
+
age: 30
|
157
|
+
\-\-\-
|
158
|
+
data
|
159
|
+
.
|
160
|
+
.fi
|
161
|
+
.
|
122
162
|
.SH "COPYRIGHT"
|
123
163
|
Mustache is Copyright (C) 2009 Chris Wanstrath
|
124
164
|
.
|
data/man/mustache.1.html
CHANGED
@@ -67,7 +67,10 @@
|
|
67
67
|
|
68
68
|
<h2>SYNOPSIS</h2>
|
69
69
|
|
70
|
-
<
|
70
|
+
<pre><code>mustache <YAML> <FILE>
|
71
|
+
mustache --compile <FILE>
|
72
|
+
mustache --tokens <FILE>
|
73
|
+
</code></pre>
|
71
74
|
|
72
75
|
<h2>DESCRIPTION</h2>
|
73
76
|
|
@@ -140,6 +143,24 @@ Hi mark!
|
|
140
143
|
Hi scott!
|
141
144
|
</code></pre>
|
142
145
|
|
146
|
+
<h2>OPTIONS</h2>
|
147
|
+
|
148
|
+
<p>By default <code>mustache</code> will try to render a Mustache template using the
|
149
|
+
YAML frontmatter you provide. It can do a few other things, however.</p>
|
150
|
+
|
151
|
+
<dl>
|
152
|
+
<dt><code>-c</code>, <code>--compile</code></dt><dd><p>Print the compiled Ruby version of a given template. This is the
|
153
|
+
code that is actually used when rendering a template into a
|
154
|
+
string. Useful for debugging but only if you are familiar with
|
155
|
+
Mustache's internals.</p></dd>
|
156
|
+
<dt><code>-t</code>, <code>--tokens</code></dt><dd><p>Print the tokenized form of a given Mustache template. This can be
|
157
|
+
used to understand how Mustache parses a template. The tokens are
|
158
|
+
handed to a generator which compiles them into a Ruby
|
159
|
+
string. Syntax errors and confused tags, therefor, can probably be
|
160
|
+
identified by examining the tokens produced.</p></dd>
|
161
|
+
</dl>
|
162
|
+
|
163
|
+
|
143
164
|
<h2>INSTALLATION</h2>
|
144
165
|
|
145
166
|
<p>If you have RubyGems installed:</p>
|
@@ -147,6 +168,19 @@ Hi scott!
|
|
147
168
|
<pre><code>gem install mustache
|
148
169
|
</code></pre>
|
149
170
|
|
171
|
+
<h2>EXAMPLES</h2>
|
172
|
+
|
173
|
+
<pre><code>$ mustache data.yml template.mustache
|
174
|
+
$ cat data.yml | mustache - template.mustache
|
175
|
+
$ mustache -c template.mustache
|
176
|
+
$ cat <<data | druby mustache - template.mustache
|
177
|
+
---
|
178
|
+
name: Bob
|
179
|
+
age: 30
|
180
|
+
---
|
181
|
+
data
|
182
|
+
</code></pre>
|
183
|
+
|
150
184
|
<h2>COPYRIGHT</h2>
|
151
185
|
|
152
186
|
<p>Mustache is Copyright (C) 2009 Chris Wanstrath</p>
|
data/man/mustache.1.ron
CHANGED
@@ -3,7 +3,9 @@ mustache(1) -- Mustache processor
|
|
3
3
|
|
4
4
|
## SYNOPSIS
|
5
5
|
|
6
|
-
|
6
|
+
mustache <YAML> <FILE>
|
7
|
+
mustache --compile <FILE>
|
8
|
+
mustache --tokens <FILE>
|
7
9
|
|
8
10
|
|
9
11
|
## DESCRIPTION
|
@@ -73,6 +75,24 @@ For example:
|
|
73
75
|
Hi mark!
|
74
76
|
Hi scott!
|
75
77
|
|
78
|
+
## OPTIONS
|
79
|
+
|
80
|
+
By default `mustache` will try to render a Mustache template using the
|
81
|
+
YAML frontmatter you provide. It can do a few other things, however.
|
82
|
+
|
83
|
+
* `-c`, `--compile`:
|
84
|
+
Print the compiled Ruby version of a given template. This is the
|
85
|
+
code that is actually used when rendering a template into a
|
86
|
+
string. Useful for debugging but only if you are familiar with
|
87
|
+
Mustache's internals.
|
88
|
+
|
89
|
+
* `-t`, `--tokens`:
|
90
|
+
Print the tokenized form of a given Mustache template. This can be
|
91
|
+
used to understand how Mustache parses a template. The tokens are
|
92
|
+
handed to a generator which compiles them into a Ruby
|
93
|
+
string. Syntax errors and confused tags, therefor, can probably be
|
94
|
+
identified by examining the tokens produced.
|
95
|
+
|
76
96
|
|
77
97
|
## INSTALLATION
|
78
98
|
|
@@ -81,6 +101,19 @@ If you have RubyGems installed:
|
|
81
101
|
gem install mustache
|
82
102
|
|
83
103
|
|
104
|
+
## EXAMPLES
|
105
|
+
|
106
|
+
$ mustache data.yml template.mustache
|
107
|
+
$ cat data.yml | mustache - template.mustache
|
108
|
+
$ mustache -c template.mustache
|
109
|
+
$ cat <<data | ruby mustache - template.mustache
|
110
|
+
---
|
111
|
+
name: Bob
|
112
|
+
age: 30
|
113
|
+
---
|
114
|
+
data
|
115
|
+
|
116
|
+
|
84
117
|
## COPYRIGHT
|
85
118
|
|
86
119
|
Mustache is Copyright (C) 2009 Chris Wanstrath
|
data/test/fixtures/delimiters.rb
CHANGED
@@ -4,16 +4,17 @@ require 'mustache'
|
|
4
4
|
class Delimiters < Mustache
|
5
5
|
self.path = File.dirname(__FILE__)
|
6
6
|
|
7
|
-
def
|
7
|
+
def start
|
8
8
|
"It worked the first time."
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
12
|
-
"And it worked the second time."
|
11
|
+
def middle
|
12
|
+
[ { :item => "And it worked the second time." },
|
13
|
+
{ :item => "As well as the third." } ]
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
"Then, surprisingly, it worked the
|
16
|
+
def final
|
17
|
+
"Then, surprisingly, it worked the final time."
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
data/test/mustache_test.rb
CHANGED
@@ -108,8 +108,9 @@ end_simple
|
|
108
108
|
* It worked the first time.
|
109
109
|
|
110
110
|
* And it worked the second time.
|
111
|
+
* As well as the third.
|
111
112
|
|
112
|
-
* Then, surprisingly, it worked the
|
113
|
+
* Then, surprisingly, it worked the final time.
|
113
114
|
end_template
|
114
115
|
end
|
115
116
|
|
@@ -203,9 +204,12 @@ data
|
|
203
204
|
instance[:list] = [ :item => 1234 ]
|
204
205
|
instance.template = '{{#list}} <li>{{item}}</li> {{/gist}}'
|
205
206
|
|
206
|
-
|
207
|
+
begin
|
207
208
|
instance.render
|
209
|
+
rescue => e
|
208
210
|
end
|
211
|
+
|
212
|
+
assert e.message.include?('Unclosed section')
|
209
213
|
end
|
210
214
|
|
211
215
|
def test_unclosed_sections_reports_the_line_number
|
@@ -218,7 +222,7 @@ data
|
|
218
222
|
rescue => e
|
219
223
|
end
|
220
224
|
|
221
|
-
assert e.message.include?('
|
225
|
+
assert e.message.include?('Line 3')
|
222
226
|
end
|
223
227
|
|
224
228
|
def test_enumerable_sections_accept_a_hash_as_a_context
|
@@ -300,8 +304,63 @@ data
|
|
300
304
|
assert instance.compiled?
|
301
305
|
end
|
302
306
|
|
303
|
-
def
|
304
|
-
|
305
|
-
|
307
|
+
def test_lots_of_staches
|
308
|
+
template = "{{{{foo}}}}"
|
309
|
+
|
310
|
+
begin
|
311
|
+
Mustache.render(template, :foo => "defunkt")
|
312
|
+
rescue => e
|
313
|
+
end
|
314
|
+
|
315
|
+
assert e.message.include?("Illegal content in tag")
|
316
|
+
end
|
317
|
+
|
318
|
+
def test_liberal_tag_names
|
319
|
+
template = "{{first-name}} {{middle_name!}} {{lastName?}}"
|
320
|
+
hash = {
|
321
|
+
'first-name' => 'chris',
|
322
|
+
'middle_name!' => 'j',
|
323
|
+
'lastName?' => 'strath'
|
324
|
+
}
|
325
|
+
|
326
|
+
assert_equal "chris j strath", Mustache.render(template, hash)
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_nested_sections_same_names
|
330
|
+
template = <<template
|
331
|
+
{{#items}}
|
332
|
+
start
|
333
|
+
{{#items}}
|
334
|
+
{{a}}
|
335
|
+
{{/items}}
|
336
|
+
end
|
337
|
+
{{/items}}
|
338
|
+
template
|
339
|
+
|
340
|
+
data = {
|
341
|
+
"items" => [
|
342
|
+
{ "items" => [ {"a" => 1}, {"a" => 2}, {"a" => 3} ] },
|
343
|
+
{ "items" => [ {"a" => 4}, {"a" => 5}, {"a" => 6} ] },
|
344
|
+
{ "items" => [ {"a" => 7}, {"a" => 8}, {"a" => 9} ] }
|
345
|
+
]
|
346
|
+
}
|
347
|
+
|
348
|
+
assert_equal <<expected, Mustache.render(template, data)
|
349
|
+
start
|
350
|
+
1
|
351
|
+
2
|
352
|
+
3
|
353
|
+
end
|
354
|
+
start
|
355
|
+
4
|
356
|
+
5
|
357
|
+
6
|
358
|
+
end
|
359
|
+
start
|
360
|
+
7
|
361
|
+
8
|
362
|
+
9
|
363
|
+
end
|
364
|
+
expected
|
306
365
|
end
|
307
366
|
end
|
data/test/parser_test.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
class ParserTest < Test::Unit::TestCase
|
5
|
+
def test_parser
|
6
|
+
lexer = Mustache::Parser.new
|
7
|
+
tokens = lexer.compile(<<-EOF)
|
8
|
+
<h1>{{header}}</h1>
|
9
|
+
{{#items}}
|
10
|
+
{{#first}}
|
11
|
+
<li><strong>{{name}}</strong></li>
|
12
|
+
{{/first}}
|
13
|
+
{{#link}}
|
14
|
+
<li><a href="{{url}}">{{name}}</a></li>
|
15
|
+
{{/link}}
|
16
|
+
{{/items}}
|
17
|
+
|
18
|
+
{{#empty}}
|
19
|
+
<p>The list is empty.</p>
|
20
|
+
{{/empty}}
|
21
|
+
EOF
|
22
|
+
|
23
|
+
expected = [:multi,
|
24
|
+
[:static, "<h1>"],
|
25
|
+
[:mustache, :etag, "header"],
|
26
|
+
[:static, "</h1>\n"],
|
27
|
+
[:mustache,
|
28
|
+
:section,
|
29
|
+
"items",
|
30
|
+
[:multi,
|
31
|
+
[:mustache,
|
32
|
+
:section,
|
33
|
+
"first",
|
34
|
+
[:multi,
|
35
|
+
[:static, "<li><strong>"],
|
36
|
+
[:mustache, :etag, "name"],
|
37
|
+
[:static, "</strong></li>\n"]]],
|
38
|
+
[:mustache,
|
39
|
+
:section,
|
40
|
+
"link",
|
41
|
+
[:multi,
|
42
|
+
[:static, "<li><a href=\""],
|
43
|
+
[:mustache, :etag, "url"],
|
44
|
+
[:static, "\">"],
|
45
|
+
[:mustache, :etag, "name"],
|
46
|
+
[:static, "</a></li>\n"]]]]],
|
47
|
+
[:mustache,
|
48
|
+
:section,
|
49
|
+
"empty",
|
50
|
+
[:multi, [:static, "<p>The list is empty.</p>\n"]]]]
|
51
|
+
|
52
|
+
assert_equal expected, tokens
|
53
|
+
end
|
54
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mustache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Wanstrath
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-03-
|
12
|
+
date: 2010-03-26 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -38,6 +38,8 @@ files:
|
|
38
38
|
- Rakefile
|
39
39
|
- LICENSE
|
40
40
|
- lib/mustache/context.rb
|
41
|
+
- lib/mustache/generator.rb
|
42
|
+
- lib/mustache/parser.rb
|
41
43
|
- lib/mustache/sinatra.rb
|
42
44
|
- lib/mustache/template.rb
|
43
45
|
- lib/mustache/version.rb
|
@@ -87,6 +89,7 @@ files:
|
|
87
89
|
- test/fixtures/unescaped.rb
|
88
90
|
- test/helper.rb
|
89
91
|
- test/mustache_test.rb
|
92
|
+
- test/parser_test.rb
|
90
93
|
- test/partial_test.rb
|
91
94
|
has_rdoc: true
|
92
95
|
homepage: http://github.com/defunkt/mustache
|