mustache 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|