mustache_render 0.0.1 → 0.0.3

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