porthole 0.99.4

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