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.
- data/MIT-LICENSE +1 -1
- data/README.rdoc +20 -1
- data/lib/generators/mustache_render/migration/install_generator.rb +53 -0
- data/lib/generators/mustache_render/migration/templates/active_record/migration.rb +37 -0
- data/lib/generators/mustache_render/migration/templates/models/active_record/mustache_render_folder.rb +6 -0
- data/lib/generators/mustache_render/migration/templates/models/active_record/mustache_render_template.rb +6 -0
- data/lib/mustache_render.rb +26 -0
- data/lib/mustache_render/config.rb +22 -0
- data/lib/mustache_render/controllers/mustache_render/manager/base_controller.rb +16 -0
- data/lib/mustache_render/controllers/mustache_render/manager/folders_controller.rb +46 -0
- data/lib/mustache_render/controllers/mustache_render/manager/templates_controller.rb +53 -0
- data/lib/mustache_render/core_ext/base_controller_ext.rb +41 -0
- data/lib/mustache_render/models/mustache_render_folder_mixin.rb +128 -0
- data/lib/mustache_render/models/mustache_render_template_mixin.rb +44 -0
- data/lib/mustache_render/mustache.rb +314 -0
- data/lib/mustache_render/mustache/context.rb +144 -0
- data/lib/mustache_render/mustache/generator.rb +197 -0
- data/lib/mustache_render/mustache/parser.rb +265 -0
- data/lib/mustache_render/mustache/settings.rb +234 -0
- data/lib/mustache_render/mustache/template.rb +60 -0
- data/lib/mustache_render/version.rb +8 -1
- data/lib/mustache_render/views/layouts/mustache_render/manager/base.html.erb +14 -0
- data/lib/mustache_render/views/mustache_render/manager/folders/_form.html.erb +31 -0
- data/lib/mustache_render/views/mustache_render/manager/folders/edit.html.erb +11 -0
- data/lib/mustache_render/views/mustache_render/manager/folders/index.html.erb +9 -0
- data/lib/mustache_render/views/mustache_render/manager/folders/new.html.erb +8 -0
- data/lib/mustache_render/views/mustache_render/manager/folders/show.html.erb +44 -0
- data/lib/mustache_render/views/mustache_render/manager/templates/_form.html.erb +36 -0
- data/lib/mustache_render/views/mustache_render/manager/templates/edit.html.erb +15 -0
- data/lib/mustache_render/views/mustache_render/manager/templates/new.html.erb +12 -0
- data/lib/mustache_render/views/mustache_render/manager/templates/show.html.erb +33 -0
- 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
|