mustache_render 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/MIT-LICENSE +1 -1
  2. data/README.rdoc +20 -1
  3. data/lib/generators/mustache_render/migration/install_generator.rb +53 -0
  4. data/lib/generators/mustache_render/migration/templates/active_record/migration.rb +37 -0
  5. data/lib/generators/mustache_render/migration/templates/models/active_record/mustache_render_folder.rb +6 -0
  6. data/lib/generators/mustache_render/migration/templates/models/active_record/mustache_render_template.rb +6 -0
  7. data/lib/mustache_render.rb +26 -0
  8. data/lib/mustache_render/config.rb +22 -0
  9. data/lib/mustache_render/controllers/mustache_render/manager/base_controller.rb +16 -0
  10. data/lib/mustache_render/controllers/mustache_render/manager/folders_controller.rb +46 -0
  11. data/lib/mustache_render/controllers/mustache_render/manager/templates_controller.rb +53 -0
  12. data/lib/mustache_render/core_ext/base_controller_ext.rb +41 -0
  13. data/lib/mustache_render/models/mustache_render_folder_mixin.rb +128 -0
  14. data/lib/mustache_render/models/mustache_render_template_mixin.rb +44 -0
  15. data/lib/mustache_render/mustache.rb +314 -0
  16. data/lib/mustache_render/mustache/context.rb +144 -0
  17. data/lib/mustache_render/mustache/generator.rb +197 -0
  18. data/lib/mustache_render/mustache/parser.rb +265 -0
  19. data/lib/mustache_render/mustache/settings.rb +234 -0
  20. data/lib/mustache_render/mustache/template.rb +60 -0
  21. data/lib/mustache_render/version.rb +8 -1
  22. data/lib/mustache_render/views/layouts/mustache_render/manager/base.html.erb +14 -0
  23. data/lib/mustache_render/views/mustache_render/manager/folders/_form.html.erb +31 -0
  24. data/lib/mustache_render/views/mustache_render/manager/folders/edit.html.erb +11 -0
  25. data/lib/mustache_render/views/mustache_render/manager/folders/index.html.erb +9 -0
  26. data/lib/mustache_render/views/mustache_render/manager/folders/new.html.erb +8 -0
  27. data/lib/mustache_render/views/mustache_render/manager/folders/show.html.erb +44 -0
  28. data/lib/mustache_render/views/mustache_render/manager/templates/_form.html.erb +36 -0
  29. data/lib/mustache_render/views/mustache_render/manager/templates/edit.html.erb +15 -0
  30. data/lib/mustache_render/views/mustache_render/manager/templates/new.html.erb +12 -0
  31. data/lib/mustache_render/views/mustache_render/manager/templates/show.html.erb +33 -0
  32. metadata +106 -30
@@ -0,0 +1,197 @@
1
+ module MustacheRender
2
+ class Mustache
3
+ # The Generator is in charge of taking an array of Mustache tokens,
4
+ # usually assembled by the Parser, and generating an interpolatable
5
+ # Ruby string. This string is considered the "compiled" template
6
+ # because at that point we're relying on Ruby to do the parsing and
7
+ # run our code.
8
+ #
9
+ # For example, let's take this template:
10
+ #
11
+ # Hi {{thing}}!
12
+ #
13
+ # If we run this through the Parser we'll get these tokens:
14
+ #
15
+ # [:multi,
16
+ # [:static, "Hi "],
17
+ # [:mustache, :etag, "thing"],
18
+ # [:static, "!\n"]]
19
+ #
20
+ # Now let's hand that to the Generator:
21
+ #
22
+ # >> puts Mustache::Generator.new.compile(tokens)
23
+ # "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
24
+ #
25
+ # You can see the generated Ruby string for any template with the
26
+ # mustache(1) command line tool:
27
+ #
28
+ # $ mustache --compile test.mustache
29
+ # "Hi #{CGI.escapeHTML(ctx[:thing].to_s)}!\n"
30
+ class Generator
31
+ # Options are unused for now but may become useful in the future.
32
+ def initialize(options = {})
33
+ @options = options
34
+ end
35
+
36
+ # Given an array of tokens, returns an interpolatable Ruby string.
37
+ def compile(exp)
38
+ "\"#{compile!(exp)}\""
39
+ end
40
+
41
+ # Given an array of tokens, converts them into Ruby code. In
42
+ # particular there are three types of expressions we are concerned
43
+ # with:
44
+ #
45
+ # :multi
46
+ # Mixed bag of :static, :mustache, and whatever.
47
+ #
48
+ # :static
49
+ # Normal HTML, the stuff outside of {{mustaches}}.
50
+ #
51
+ # :mustache
52
+ # Any Mustache tag, from sections to partials.
53
+ #
54
+ # To give you an idea of what you'll be dealing with take this
55
+ # template:
56
+ #
57
+ # Hello {{name}}
58
+ # You have just won ${{value}}!
59
+ # {{#in_ca}}
60
+ # Well, ${{taxed_value}}, after taxes.
61
+ # {{/in_ca}}
62
+ #
63
+ # If we run this through the Parser, we'll get back this array of
64
+ # tokens:
65
+ #
66
+ # [:multi,
67
+ # [:static, "Hello "],
68
+ # [:mustache, :etag, "name"],
69
+ # [:static, "\nYou have just won $"],
70
+ # [:mustache, :etag, "value"],
71
+ # [:static, "!\n"],
72
+ # [:mustache,
73
+ # :section,
74
+ # "in_ca",
75
+ # [:multi,
76
+ # [:static, "Well, $"],
77
+ # [:mustache, :etag, "taxed_value"],
78
+ # [:static, ", after taxes.\n"]]]]
79
+ def compile!(exp)
80
+ case exp.first
81
+ when :multi
82
+ exp[1..-1].map { |e| compile!(e) }.join
83
+ when :static
84
+ str(exp[1])
85
+ when :mustache
86
+ send("on_#{exp[1]}", *exp[2..-1])
87
+ else
88
+ raise "Unhandled exp: #{exp.first}"
89
+ end
90
+ end
91
+
92
+ # Callback fired when the compiler finds a section token. We're
93
+ # passed the section name and the array of tokens.
94
+ def on_section(name, content, raw, delims)
95
+ # Convert the tokenized content of this section into a Ruby
96
+ # string we can use.
97
+ code = compile(content)
98
+
99
+ # Compile the Ruby for this section now that we know what's
100
+ # inside the section.
101
+ ev(<<-compiled)
102
+ if v = #{compile!(name)}
103
+ if v == true
104
+ #{code}
105
+ elsif v.is_a?(Proc)
106
+ t = Mustache::Template.new(v.call(#{raw.inspect}).to_s)
107
+ def t.tokens(src=@source)
108
+ p = Parser.new
109
+ p.otag, p.ctag = #{delims.inspect}
110
+ p.compile(src)
111
+ end
112
+ t.render(ctx.dup)
113
+ else
114
+ # Shortcut when passed non-array
115
+ v = [v] unless v.is_a?(Array) || defined?(Enumerator) && v.is_a?(Enumerator)
116
+
117
+ v.map { |h| ctx.push(h); r = #{code}; ctx.pop; r }.join
118
+ end
119
+ end
120
+ compiled
121
+ end
122
+
123
+ # Fired when we find an inverted section. Just like `on_section`,
124
+ # we're passed the inverted section name and the array of tokens.
125
+ def on_inverted_section(name, content, raw, _)
126
+ # Convert the tokenized content of this section into a Ruby
127
+ # string we can use.
128
+ code = compile(content)
129
+
130
+ # Compile the Ruby for this inverted section now that we know
131
+ # what's inside.
132
+ ev(<<-compiled)
133
+ v = #{compile!(name)}
134
+ if v.nil? || v == false || v.respond_to?(:empty?) && v.empty?
135
+ #{code}
136
+ end
137
+ compiled
138
+ end
139
+
140
+ # Fired when the compiler finds a partial. We want to return code
141
+ # which calls a partial at runtime instead of expanding and
142
+ # including the partial's body to allow for recursive partials.
143
+ def on_partial(name, indentation)
144
+ ev("ctx.partial(#{name.to_sym.inspect}, #{indentation.inspect})")
145
+ end
146
+
147
+ # An unescaped tag.
148
+ def on_utag(name)
149
+ ev(<<-compiled)
150
+ v = #{compile!(name)}
151
+ if v.is_a?(Proc)
152
+ v = Mustache::Template.new(v.call.to_s).render(ctx.dup)
153
+ end
154
+ v.to_s
155
+ compiled
156
+ end
157
+
158
+ # An escaped tag.
159
+ def on_etag(name)
160
+ ev(<<-compiled)
161
+ v = #{compile!(name)}
162
+ if v.is_a?(Proc)
163
+ v = Mustache::Template.new(v.call.to_s).render(ctx.dup)
164
+ end
165
+ ctx.escapeHTML(v.to_s)
166
+ compiled
167
+ end
168
+
169
+ def on_fetch(names)
170
+ names = names.map { |n| n.to_sym }
171
+
172
+ if names.length == 0
173
+ "ctx[:to_s]"
174
+ elsif names.length == 1
175
+ "ctx[#{names.first.to_sym.inspect}]"
176
+ else
177
+ initial, *rest = names
178
+ <<-compiled
179
+ #{rest.inspect}.inject(ctx[#{initial.inspect}]) { |value, key|
180
+ value && ctx.find(value, key)
181
+ }
182
+ compiled
183
+ end
184
+ end
185
+
186
+ # An interpolation-friendly version of a string, for use within a
187
+ # Ruby string.
188
+ def ev(s)
189
+ "#\{#{s}}"
190
+ end
191
+
192
+ def str(s)
193
+ s.inspect[1..-2]
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,265 @@
1
+ require 'strscan'
2
+
3
+ module MustacheRender
4
+ class Mustache
5
+ # The Parser is responsible for taking a string template and
6
+ # converting it into an array of tokens and, really, expressions. It
7
+ # raises SyntaxError if there is anything it doesn't understand and
8
+ # knows which sigil corresponds to which tag type.
9
+ #
10
+ # For example, given this template:
11
+ #
12
+ # Hi {{thing}}!
13
+ #
14
+ # Run through the Parser we'll get these tokens:
15
+ #
16
+ # [:multi,
17
+ # [:static, "Hi "],
18
+ # [:mustache, :etag, "thing"],
19
+ # [:static, "!\n"]]
20
+ #
21
+ # You can see the array of tokens for any template with the
22
+ # mustache(1) command line tool:
23
+ #
24
+ # $ mustache --tokens test.mustache
25
+ # [:multi, [:static, "Hi "], [:mustache, :etag, "thing"], [:static, "!\n"]]
26
+ class Parser
27
+ # A SyntaxError is raised when the Parser comes across unclosed
28
+ # tags, sections, illegal content in tags, or anything of that
29
+ # sort.
30
+ class SyntaxError < StandardError
31
+ def initialize(message, position)
32
+ @message = message
33
+ @lineno, @column, @line, _ = position
34
+ @stripped_line = @line.strip
35
+ @stripped_column = @column - (@line.size - @line.lstrip.size)
36
+ end
37
+
38
+ def to_s
39
+ <<-EOF
40
+ #{@message}
41
+ Line #{@lineno}
42
+ #{@stripped_line}
43
+ #{' ' * @stripped_column}^
44
+ EOF
45
+ end
46
+ end
47
+
48
+ # After these types of tags, all whitespace until the end of the line will
49
+ # be skipped if they are the first (and only) non-whitespace content on
50
+ # the line.
51
+ SKIP_WHITESPACE = [ '#', '^', '/', '<', '>', '=', '!' ]
52
+
53
+ # The content allowed in a tag name.
54
+ ALLOWED_CONTENT = /(\w|[?!\/.-])*/
55
+
56
+ # These types of tags allow any content,
57
+ # the rest only allow ALLOWED_CONTENT.
58
+ ANY_CONTENT = [ '!', '=' ]
59
+
60
+ attr_reader :scanner, :result
61
+ attr_writer :otag, :ctag
62
+
63
+ # Accepts an options hash which does nothing but may be used in
64
+ # the future.
65
+ def initialize(options = {})
66
+ @options = {}
67
+ end
68
+
69
+ # The opening tag delimiter. This may be changed at runtime.
70
+ def otag
71
+ @otag ||= '{{'
72
+ end
73
+
74
+ # The closing tag delimiter. This too may be changed at runtime.
75
+ def ctag
76
+ @ctag ||= '}}'
77
+ end
78
+
79
+ # Given a string template, returns an array of tokens.
80
+ def compile(template)
81
+ if template.respond_to?(:encoding)
82
+ @encoding = template.encoding
83
+ template = template.dup.force_encoding("BINARY")
84
+ else
85
+ @encoding = nil
86
+ end
87
+
88
+ # Keeps information about opened sections.
89
+ @sections = []
90
+ @result = [:multi]
91
+ @scanner = StringScanner.new(template)
92
+
93
+ # Scan until the end of the template.
94
+ until @scanner.eos?
95
+ scan_tags || scan_text
96
+ end
97
+
98
+ if !@sections.empty?
99
+ # We have parsed the whole file, but there's still opened sections.
100
+ type, pos, result = @sections.pop
101
+ error "Unclosed section #{type.inspect}", pos
102
+ end
103
+
104
+ @result
105
+ end
106
+
107
+ # Find {{mustaches}} and add them to the @result array.
108
+ def scan_tags
109
+ # Scan until we hit an opening delimiter.
110
+ start_of_line = @scanner.beginning_of_line?
111
+ pre_match_position = @scanner.pos
112
+ last_index = @result.length
113
+
114
+ return unless x = @scanner.scan(/([ \t]*)?#{Regexp.escape(otag)}/)
115
+ padding = @scanner[1] || ''
116
+
117
+ # Don't touch the preceding whitespace unless we're matching the start
118
+ # of a new line.
119
+ unless start_of_line
120
+ @result << [:static, padding] unless padding.empty?
121
+ pre_match_position += padding.length
122
+ padding = ''
123
+ end
124
+
125
+ # Since {{= rewrites ctag, we store the ctag which should be used
126
+ # when parsing this specific tag.
127
+ current_ctag = self.ctag
128
+ type = @scanner.scan(/#|\^|\/|=|!|<|>|&|\{/)
129
+ @scanner.skip(/\s*/)
130
+
131
+ # ANY_CONTENT tags allow any character inside of them, while
132
+ # other tags (such as variables) are more strict.
133
+ if ANY_CONTENT.include?(type)
134
+ r = /\s*#{regexp(type)}?#{regexp(current_ctag)}/
135
+ content = scan_until_exclusive(r)
136
+ else
137
+ content = @scanner.scan(ALLOWED_CONTENT)
138
+ end
139
+
140
+ # We found {{ but we can't figure out what's going on inside.
141
+ error "Illegal content in tag" if content.empty?
142
+
143
+ fetch = [:mustache, :fetch, content.split('.')]
144
+ prev = @result
145
+
146
+ # Based on the sigil, do what needs to be done.
147
+ case type
148
+ when '#'
149
+ block = [:multi]
150
+ @result << [:mustache, :section, fetch, block]
151
+ @sections << [content, position, @result]
152
+ @result = block
153
+ when '^'
154
+ block = [:multi]
155
+ @result << [:mustache, :inverted_section, fetch, block]
156
+ @sections << [content, position, @result]
157
+ @result = block
158
+ when '/'
159
+ section, pos, result = @sections.pop
160
+ raw = @scanner.pre_match[pos[3]...pre_match_position] + padding
161
+ (@result = result).last << raw << [self.otag, self.ctag]
162
+
163
+ if section.nil?
164
+ error "Closing unopened #{content.inspect}"
165
+ elsif section != content
166
+ error "Unclosed section #{section.inspect}", pos
167
+ end
168
+ when '!'
169
+ # ignore comments
170
+ when '='
171
+ self.otag, self.ctag = content.split(' ', 2)
172
+ when '>', '<'
173
+ @result << [:mustache, :partial, content, padding]
174
+ when '{', '&'
175
+ # The closing } in unescaped tags is just a hack for
176
+ # aesthetics.
177
+ type = "}" if type == "{"
178
+ @result << [:mustache, :utag, fetch]
179
+ else
180
+ @result << [:mustache, :etag, fetch]
181
+ end
182
+
183
+ # Skip whitespace and any balancing sigils after the content
184
+ # inside this tag.
185
+ @scanner.skip(/\s+/)
186
+ @scanner.skip(regexp(type)) if type
187
+
188
+ # Try to find the closing tag.
189
+ unless close = @scanner.scan(regexp(current_ctag))
190
+ error "Unclosed tag"
191
+ end
192
+
193
+ # If this tag was the only non-whitespace content on this line, strip
194
+ # the remaining whitespace. If not, but we've been hanging on to padding
195
+ # from the beginning of the line, re-insert the padding as static text.
196
+ if start_of_line && !@scanner.eos?
197
+ if @scanner.peek(2) =~ /\r?\n/ && SKIP_WHITESPACE.include?(type)
198
+ @scanner.skip(/\r?\n/)
199
+ else
200
+ prev.insert(last_index, [:static, padding]) unless padding.empty?
201
+ end
202
+ end
203
+
204
+ # Store off the current scanner position now that we've closed the tag
205
+ # and consumed any irrelevant whitespace.
206
+ @sections.last[1] << @scanner.pos unless @sections.empty?
207
+
208
+ return unless @result == [:multi]
209
+ end
210
+
211
+ # Try to find static text, e.g. raw HTML with no {{mustaches}}.
212
+ def scan_text
213
+ text = scan_until_exclusive(/(^[ \t]*)?#{Regexp.escape(otag)}/)
214
+
215
+ if text.nil?
216
+ # Couldn't find any otag, which means the rest is just static text.
217
+ text = @scanner.rest
218
+ # Mark as done.
219
+ @scanner.terminate
220
+ end
221
+
222
+ text.force_encoding(@encoding) if @encoding
223
+
224
+ @result << [:static, text] unless text.empty?
225
+ end
226
+
227
+ # Scans the string until the pattern is matched. Returns the substring
228
+ # *excluding* the end of the match, advancing the scan pointer to that
229
+ # location. If there is no match, nil is returned.
230
+ def scan_until_exclusive(regexp)
231
+ pos = @scanner.pos
232
+ if @scanner.scan_until(regexp)
233
+ @scanner.pos -= @scanner.matched.size
234
+ @scanner.pre_match[pos..-1]
235
+ end
236
+ end
237
+
238
+ # Returns [lineno, column, line]
239
+ def position
240
+ # The rest of the current line
241
+ rest = @scanner.check_until(/\n|\Z/).to_s.chomp
242
+
243
+ # What we have parsed so far
244
+ parsed = @scanner.string[0...@scanner.pos]
245
+
246
+ lines = parsed.split("\n")
247
+
248
+ [ lines.size, lines.last.size - 1, lines.last + rest ]
249
+ end
250
+
251
+ # Used to quickly convert a string into a regular expression
252
+ # usable by the string scanner.
253
+ def regexp(thing)
254
+ /#{Regexp.escape(thing)}/
255
+ end
256
+
257
+ # Raises a SyntaxError. The message should be the name of the
258
+ # error - other details such as line number and position are
259
+ # handled for you.
260
+ def error(message, pos = position)
261
+ raise SyntaxError.new(message, pos)
262
+ end
263
+ end
264
+ end
265
+ end