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.
@@ -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