porthole 0.99.4

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