mustache 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def help
7
- puts <<-usage
8
- Usage:
9
- $ cat data.yml template.mustache | mustache
10
- $ mustache data.yml template.mustache
11
- $ cat <<data | druby mustache - template.mustache
12
- ---
13
- name: Bob
14
- age: 30
15
- ---
16
- data
17
-
18
- See compiled Ruby string:
19
- $ mustache -c FILE
20
-
21
- Help:
22
- $ mustache -h
23
-
24
- See mustache(1) or http://defunkt.github.com/mustache/mustache.1.html
25
- for an overview.
26
- usage
27
- end
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
- if (ARGV.delete('-c') || ARGV.delete('--compile')) && (file = ARGV[0])
30
- puts Mustache.compile(File.read(file))
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
- elsif ($stdin.tty? && ARGV.empty?) || ARGV.delete('-h') || ARGV.delete('--help')
33
- help
45
+ opts.separator "Common Options:"
34
46
 
35
- else
36
- # Not at a terminal, read from STDIN and print rendered templates to
37
- # STDOUT.
38
- doc = ARGF.read
47
+ opts.on("-v", "--version", "Print the version") do |v|
48
+ puts "Mustache v#{Mustache::Version}"
49
+ exit
50
+ end
39
51
 
40
- if doc =~ /^(\s*---(.+)---\s*)/m
41
- yaml = $2.strip
42
- template = doc.sub($1, '')
52
+ opts.on_tail("-h", "--help", "Show this message") do
53
+ puts opts
54
+ exit
55
+ end
56
+ end
43
57
 
44
- YAML.each_document(yaml) do |data|
45
- puts Mustache.render(template, data)
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
+
@@ -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}/#{underscore}.#{template_extension}"
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, template_path, template_extension)
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).render(context.update(ctx))
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
@@ -9,21 +9,21 @@ class Mustache
9
9
  # class Hurl < Sinatra::Base
10
10
  # register Mustache::Sinatra
11
11
  #
12
- # # Should be the path to your .mustache template files.
13
- # set :views, "path/to/mustache/templates"
12
+ # set :mustache, {
13
+ # # Should be the path to your .mustache template files.
14
+ # :templates => "path/to/mustache/templates",
14
15
  #
15
- # # Should be the path to your .rb Mustache view files.
16
- # # Only needed if different from the `views` setting
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
- # # 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
- #
24
- # # If our Sinatra::Base subclass was instead Hurl::App,
25
- # # we'd want to do `set :namespace, Hurl::App`
26
- # set :namespace, Hurl
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
- render :mustache, template, options, locals
47
- end
46
+ # Locals can be passed as options under the :locals key.
47
+ locals.update(options.delete(:locals) || {})
48
48
 
49
- # This is called by Sinatra's `render` with the proper paths
50
- # and, potentially, a block containing a sub-view
51
- def render_mustache(template, data, opts, locals, &block)
52
- # If you have Hurl::App::Views, namespace should be set to Hurl::App.
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
- # This is probably the same as options.views, but we'll set it anyway.
56
- # It's used to tell Mustache where to look for view classes.
57
- Mustache.view_path = options.mustaches
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
- # Grab the class!
60
- klass = Mustache.view_class(template)
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
- # Only cache the data if this isn't the generic base class.
63
- klass.template = data unless klass == Mustache
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
- # Confusingly Sinatra's `views` setting tells Mustache where the
66
- # templates are found. It's fine, blame Chris.
67
- if klass.template_path != options.views
68
- klass.template_path = options.views
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
- # Locals get added to the view's context
80
- locals.each do |local, value|
81
- instance[local] = value
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
- # If we're paseed a block it's a subview. Sticking it in yield
85
- # lets us use {{yield}} in layout.html to render the actual page.
86
- instance[:yield] = block.call if block
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
- instance.template = data unless instance.compiled?
89
- instance.to_html
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
@@ -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 is a compiled version of a Mustache 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, template_path = '.', template_extension = 'mustache')
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
- "\"#{compile_sections(src)}\""
48
+ Generator.new.compile(tokens)
61
49
  end
62
50
  alias_method :to_s, :compile
63
51
 
64
- # {{#sections}}okay{{/sections}}
65
- #
66
- # Sections can return true, false, or an enumerable.
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
@@ -1,3 +1,3 @@
1
1
  class Mustache
2
- Version = '0.7.0'
2
+ Version = '0.9.0'
3
3
  end
@@ -7,7 +7,13 @@
7
7
  \fBmustache\fR \-\- Mustache processor
8
8
  .
9
9
  .SH "SYNOPSIS"
10
- \fBcat data.yml template.mustache | mustache\fR
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
  .
@@ -67,7 +67,10 @@
67
67
 
68
68
  <h2>SYNOPSIS</h2>
69
69
 
70
- <p><code>cat data.yml template.mustache | mustache</code></p>
70
+ <pre><code>mustache &lt;YAML> &lt;FILE>
71
+ mustache --compile &lt;FILE>
72
+ mustache --tokens &lt;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 &lt;&lt;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>
@@ -3,7 +3,9 @@ mustache(1) -- Mustache processor
3
3
 
4
4
  ## SYNOPSIS
5
5
 
6
- `cat data.yml template.mustache | mustache`
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
@@ -1,6 +1,8 @@
1
1
  {{=<% %>=}}
2
- * <% first %>
2
+ * <% start %>
3
3
  <%=| |=%>
4
- * | second |
4
+ |# middle |
5
+ * | item |
6
+ |/ middle |
5
7
  |={{ }}=|
6
- * {{ third }}
8
+ * {{ final }}
@@ -4,16 +4,17 @@ require 'mustache'
4
4
  class Delimiters < Mustache
5
5
  self.path = File.dirname(__FILE__)
6
6
 
7
- def first
7
+ def start
8
8
  "It worked the first time."
9
9
  end
10
10
 
11
- def second
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 third
16
- "Then, surprisingly, it worked the third time."
16
+ def final
17
+ "Then, surprisingly, it worked the final time."
17
18
  end
18
19
  end
19
20
 
@@ -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 third time.
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
- assert_raise Mustache::Template::UnclosedSection do
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?('line 3')
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 test_compile
304
- assert_equal '"Hi, #{CGI.escapeHTML(ctx[:person].to_s)}!"',
305
- Mustache.compile("Hi, {{person}}!")
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
@@ -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.7.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-08 00:00:00 -08:00
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