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,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'yaml'
4
+ require 'optparse'
5
+
6
+ require 'porthole'
7
+ require 'porthole/version'
8
+
9
+ class Porthole
10
+ class CLI
11
+ # Return a structure describing the options.
12
+ def self.parse_options(args)
13
+ opts = OptionParser.new do |opts|
14
+ opts.banner = "Usage: porthole [-c] [-t] [-r library] FILE ..."
15
+
16
+ opts.separator " "
17
+
18
+ opts.separator "Examples:"
19
+ opts.separator " $ porthole data.yml template.porthole"
20
+ opts.separator " $ cat data.yml | porthole - template.porthole"
21
+ opts.separator " $ porthole -c template.porthole"
22
+
23
+ opts.separator " "
24
+
25
+ opts.separator " See porthole(1) or " +
26
+ "http://porthole.github.com/porthole.1.html"
27
+ opts.separator " for more details."
28
+
29
+ opts.separator " "
30
+ opts.separator "Options:"
31
+
32
+ opts.on("-c", "--compile FILE",
33
+ "Print the compiled Ruby for a given template.") do |file|
34
+ puts Porthole::Template.new(File.read(file)).compile
35
+ exit
36
+ end
37
+
38
+ opts.on("-t", "--tokens FILE",
39
+ "Print the tokenized form of a given template.") do |file|
40
+ require 'pp'
41
+ pp Porthole::Template.new(File.read(file)).tokens
42
+ exit
43
+ end
44
+
45
+ opts.on('-r', '--require LIB', 'Require a Ruby library before running.') do |lib|
46
+ require lib
47
+ end
48
+
49
+ opts.separator "Common Options:"
50
+
51
+ opts.on("-v", "--version", "Print the version") do |v|
52
+ puts "Porthole v#{Porthole::Version}"
53
+ exit
54
+ end
55
+
56
+ opts.on_tail("-h", "--help", "Show this message") do
57
+ puts opts
58
+ exit
59
+ end
60
+ end
61
+
62
+ opts.separator ""
63
+
64
+ opts.parse!(args)
65
+ end
66
+
67
+ # Does the dirty work of reading files from STDIN and the command
68
+ # line then processing them. The meat of this script, if you will.
69
+ def self.process_files(input_stream)
70
+ doc = input_stream.read
71
+
72
+ if doc =~ /^(\s*---(.+)---\s*)/m
73
+ yaml = $2.strip
74
+ template = doc.sub($1, '')
75
+
76
+ YAML.each_document(yaml) do |data|
77
+ puts Porthole.render(template, data)
78
+ end
79
+ else
80
+ puts Porthole.render(doc)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Help is the default.
87
+ ARGV << '-h' if ARGV.empty? && $stdin.tty?
88
+
89
+ # Process options
90
+ Porthole::CLI.parse_options(ARGV) if $stdin.tty?
91
+
92
+ # Still here - process ARGF
93
+ Porthole::CLI.process_files(ARGF)
94
+
@@ -0,0 +1,304 @@
1
+ require 'porthole/template'
2
+ require 'porthole/context'
3
+ require 'porthole/settings'
4
+
5
+ # Porthole is the base class from which your Porthole subclasses
6
+ # should inherit (though it can be used on its own).
7
+ #
8
+ # The typical Porthole workflow is as follows:
9
+ #
10
+ # * Create a Porthole subclass: class Stats < Porthole
11
+ # * Create a template: stats.porthole
12
+ # * Instantiate an instance: view = Stats.new
13
+ # * Render that instance: view.render
14
+ #
15
+ # You can skip the instantiation by calling `Stats.render` directly.
16
+ #
17
+ # While Porthole will do its best to load and render a template for
18
+ # you, this process is completely customizable using a few options.
19
+ #
20
+ # All settings can be overriden at the class level.
21
+ #
22
+ # For example, going with the above example, we can use
23
+ # `Stats.template_path = "/usr/local/templates"` to specify the path
24
+ # Porthole uses to find templates.
25
+ #
26
+ # Here are the available options:
27
+ #
28
+ # * template_path
29
+ #
30
+ # The `template_path` setting determines the path Porthole uses when
31
+ # looking for a template. By default it is "."
32
+ # Setting it to /usr/local/templates, for example, means (given all
33
+ # other settings are default) a Porthole subclass `Stats` will try to
34
+ # load /usr/local/templates/stats.porthole
35
+ #
36
+ # * template_extension
37
+ #
38
+ # The `template_extension` is the extension Porthole uses when looking
39
+ # for template files. By default it is "porthole"
40
+ #
41
+ # * template_file
42
+ #
43
+ # You can tell Porthole exactly which template to use with this
44
+ # setting. It can be a relative or absolute path.
45
+ #
46
+ # * template
47
+ #
48
+ # Sometimes you want Porthole to render a string, not a file. In those
49
+ # cases you may set the `template` setting. For example:
50
+ #
51
+ # >> Porthole.render("Hello {{planet}}", :planet => "World!")
52
+ # => "Hello World!"
53
+ #
54
+ # The `template` setting is also available on instances.
55
+ #
56
+ # view = Porthole.new
57
+ # view.template = "Hi, {{person}}!"
58
+ # view[:person] = 'Mom'
59
+ # view.render # => Hi, mom!
60
+ #
61
+ # * view_namespace
62
+ #
63
+ # To make life easy on those developing Porthole plugins for web frameworks or
64
+ # other libraries, Porthole will attempt to load view classes (i.e. Porthole
65
+ # subclasses) using the `view_class` class method. The `view_namespace` tells
66
+ # Porthole under which constant view classes live. By default it is `Object`.
67
+ #
68
+ # * view_path
69
+ #
70
+ # Similar to `template_path`, the `view_path` option tells Porthole where to look
71
+ # for files containing view classes when using the `view_class` method.
72
+ #
73
+ class Porthole
74
+
75
+ #
76
+ # Public API
77
+ #
78
+
79
+ # Instantiates an instance of this class and calls `render` with
80
+ # the passed args.
81
+ #
82
+ # Returns a rendered String version of a template
83
+ def self.render(*args)
84
+ new.render(*args)
85
+ end
86
+
87
+ class << self
88
+ alias_method :to_html, :render
89
+ alias_method :to_text, :render
90
+ end
91
+
92
+ # Parses our fancy pants template file and returns normal file with
93
+ # all special {{tags}} and {{#sections}}replaced{{/sections}}.
94
+ #
95
+ # data - A String template or a Hash context. If a Hash is given,
96
+ # we'll try to figure out the template from the class.
97
+ # ctx - A Hash context if `data` is a String template.
98
+ #
99
+ # Examples
100
+ #
101
+ # @view.render("Hi {{thing}}!", :thing => :world)
102
+ #
103
+ # View.template = "Hi {{thing}}!"
104
+ # @view = View.new
105
+ # @view.render(:thing => :world)
106
+ #
107
+ # Returns a rendered String version of a template
108
+ def render(data = template, ctx = {})
109
+ if data.is_a? Hash
110
+ ctx = data
111
+ tpl = templateify(template)
112
+ elsif data.is_a? Symbol
113
+ self.template_name = data
114
+ tpl = templateify(template)
115
+ else
116
+ tpl = templateify(data)
117
+ end
118
+
119
+ return tpl.render(context) if ctx == {}
120
+
121
+ begin
122
+ context.push(ctx)
123
+ tpl.render(context)
124
+ ensure
125
+ context.pop
126
+ end
127
+ end
128
+
129
+ alias_method :to_html, :render
130
+ alias_method :to_text, :render
131
+
132
+ # Context accessors.
133
+ #
134
+ # view = Porthole.new
135
+ # view[:name] = "Jon"
136
+ # view.template = "Hi, {{name}}!"
137
+ # view.render # => "Hi, Jon!"
138
+ def [](key)
139
+ context[key.to_sym]
140
+ end
141
+
142
+ def []=(key, value)
143
+ context[key.to_sym] = value
144
+ end
145
+
146
+ # A helper method which gives access to the context at a given time.
147
+ # Kind of a hack for now, but useful when you're in an iterating section
148
+ # and want access to the hash currently being iterated over.
149
+ def context
150
+ @context ||= Context.new(self)
151
+ end
152
+
153
+ # Given a file name and an optional context, attempts to load and
154
+ # render the file as a template.
155
+ def self.render_file(name, context = {})
156
+ render(partial(name), context)
157
+ end
158
+
159
+ # Given a file name and an optional context, attempts to load and
160
+ # render the file as a template.
161
+ def render_file(name, context = {})
162
+ self.class.render_file(name, context)
163
+ end
164
+
165
+ # Given a name, attempts to read a file and return the contents as a
166
+ # string. The file is not rendered, so it might contain
167
+ # {{portholes}}.
168
+ #
169
+ # Call `render` if you need to process it.
170
+ def self.partial(name)
171
+ File.read("#{template_path}/#{name}.#{template_extension}")
172
+ end
173
+
174
+ # Override this in your subclass if you want to do fun things like
175
+ # reading templates from a database. It will be rendered by the
176
+ # context, so all you need to do is return a string.
177
+ def partial(name)
178
+ self.class.partial(name)
179
+ end
180
+
181
+ # Override this to provide custom escaping.
182
+ #
183
+ # class PersonView < Porthole
184
+ # def escapeHTML(str)
185
+ # my_html_escape_method(str)
186
+ # end
187
+ # end
188
+ #
189
+ # Returns a String
190
+ def escapeHTML(str)
191
+ CGI.escapeHTML(str)
192
+ end
193
+
194
+
195
+ #
196
+ # Private API
197
+ #
198
+
199
+ # When given a symbol or string representing a class, will try to produce an
200
+ # appropriate view class.
201
+ # e.g.
202
+ # Porthole.view_namespace = Hurl::Views
203
+ # Porthole.view_class(:Partial) # => Hurl::Views::Partial
204
+ def self.view_class(name)
205
+ if name != classify(name.to_s)
206
+ name = classify(name.to_s)
207
+ end
208
+
209
+ # Emptiness begets emptiness.
210
+ if name.to_s == ''
211
+ return Porthole
212
+ end
213
+
214
+ file_name = underscore(name)
215
+ name = "#{view_namespace}::#{name}"
216
+
217
+ if const = const_get!(name)
218
+ const
219
+ elsif File.exists?(file = "#{view_path}/#{file_name}.rb")
220
+ require "#{file}".chomp('.rb')
221
+ const_get!(name) || Porthole
222
+ else
223
+ Porthole
224
+ end
225
+ end
226
+
227
+ # Supercharged version of Module#const_get.
228
+ #
229
+ # Always searches under Object and can find constants by their full name,
230
+ # e.g. Porthole::Views::Index
231
+ #
232
+ # name - The full constant name to find.
233
+ #
234
+ # Returns the constant if found
235
+ # Returns nil if nothing is found
236
+ def self.const_get!(name)
237
+ name.split('::').inject(Object) do |klass, cname|
238
+ klass.const_get(cname)
239
+ end
240
+ rescue NameError
241
+ nil
242
+ end
243
+
244
+ # Has this template already been compiled? Compilation is somewhat
245
+ # expensive so it may be useful to check this before attempting it.
246
+ def self.compiled?
247
+ @template.is_a? Template
248
+ end
249
+
250
+ # Has this instance or its class already compiled a template?
251
+ def compiled?
252
+ (@template && @template.is_a?(Template)) || self.class.compiled?
253
+ end
254
+
255
+ # template_partial => TemplatePartial
256
+ # template/partial => Template::Partial
257
+ def self.classify(underscored)
258
+ underscored.split('/').map do |namespace|
259
+ namespace.split(/[-_]/).map do |part|
260
+ part[0] = part[0].chr.upcase; part
261
+ end.join
262
+ end.join('::')
263
+ end
264
+
265
+ # TemplatePartial => template_partial
266
+ # Template::Partial => template/partial
267
+ # Takes a string but defaults to using the current class' name.
268
+ def self.underscore(classified = name)
269
+ classified = name if classified.to_s.empty?
270
+ classified = superclass.name if classified.to_s.empty?
271
+
272
+ string = classified.dup.split("#{view_namespace}::").last
273
+
274
+ string.split('::').map do |part|
275
+ part[0] = part[0].chr.downcase
276
+ part.gsub(/[A-Z]/) { |s| "_#{s.downcase}"}
277
+ end.join('/')
278
+ end
279
+
280
+ # Turns a string into a Porthole::Template. If passed a Template,
281
+ # returns it.
282
+ def self.templateify(obj)
283
+ if obj.is_a?(Template)
284
+ obj
285
+ else
286
+ Template.new(obj.to_s)
287
+ end
288
+ end
289
+
290
+ def templateify(obj)
291
+ self.class.templateify(obj)
292
+ end
293
+
294
+ # Return the value of the configuration setting on the superclass, or return
295
+ # the default.
296
+ #
297
+ # attr_name - Symbol name of the attribute. It should match the instance variable.
298
+ # default - Default value to use if the superclass does not respond.
299
+ #
300
+ # Returns the inherited or default configuration setting.
301
+ def self.inheritable_config_for(attr_name, default)
302
+ superclass.respond_to?(attr_name) ? superclass.send(attr_name) : default
303
+ end
304
+ end
@@ -0,0 +1,142 @@
1
+ class Porthole
2
+ # A ContextMiss is raised whenever a tag's target can not be found
3
+ # in the current context if `Porthole#raise_on_context_miss?` is
4
+ # set to true.
5
+ #
6
+ # For example, if your View class does not respond to `music` but
7
+ # your template contains a `{{music}}` tag this exception will be raised.
8
+ #
9
+ # By default it is not raised. See Porthole.raise_on_context_miss.
10
+ class ContextMiss < RuntimeError; end
11
+
12
+ # A Context represents the context which a Porthole template is
13
+ # executed within. All Porthole tags reference keys in the Context.
14
+ class Context
15
+ # Expect to be passed an instance of `Porthole`.
16
+ def initialize(porthole)
17
+ @stack = [porthole]
18
+ end
19
+
20
+ # A {{>partial}} tag translates into a call to the context's
21
+ # `partial` method, which would be this sucker right here.
22
+ #
23
+ # If the Porthole view handling the rendering (e.g. the view
24
+ # representing your profile page or some other template) responds
25
+ # to `partial`, we call it and render the result.
26
+ def partial(name, indentation = '')
27
+ # Look for the first Porthole in the stack.
28
+ porthole = porthole_in_stack
29
+
30
+ # Indent the partial template by the given indentation.
31
+ part = porthole.partial(name).to_s.gsub(/^/, indentation)
32
+
33
+ # Call the Porthole's `partial` method and render the result.
34
+ result = porthole.render(part, self)
35
+ end
36
+
37
+ # Find the first Porthole in the stack. If we're being rendered
38
+ # inside a Porthole object as a context, we'll use that one.
39
+ def porthole_in_stack
40
+ @stack.detect { |frame| frame.is_a?(Porthole) }
41
+ end
42
+
43
+ # Allows customization of how Porthole escapes things.
44
+ #
45
+ # Returns a String.
46
+ def escapeHTML(str)
47
+ porthole_in_stack.escapeHTML(str)
48
+ end
49
+
50
+ # Adds a new object to the context's internal stack.
51
+ #
52
+ # Returns the Context.
53
+ def push(new)
54
+ @stack.unshift(new)
55
+ self
56
+ end
57
+ alias_method :update, :push
58
+
59
+ # Removes the most recently added object from the context's
60
+ # internal stack.
61
+ #
62
+ # Returns the Context.
63
+ def pop
64
+ @stack.shift
65
+ self
66
+ end
67
+
68
+ # Can be used to add a value to the context in a hash-like way.
69
+ #
70
+ # context[:name] = "Chris"
71
+ def []=(name, value)
72
+ push(name => value)
73
+ end
74
+
75
+ # Alias for `fetch`.
76
+ def [](name)
77
+ fetch(name, nil)
78
+ end
79
+
80
+ # Do we know about a particular key? In other words, will calling
81
+ # `context[key]` give us a result that was set. Basically.
82
+ def has_key?(key)
83
+ !!fetch(key)
84
+ rescue ContextMiss
85
+ false
86
+ end
87
+
88
+ # Similar to Hash#fetch, finds a value by `name` in the context's
89
+ # stack. You may specify the default return value by passing a
90
+ # second parameter.
91
+ #
92
+ # If no second parameter is passed (or raise_on_context_miss is
93
+ # set to true), will raise a ContextMiss exception on miss.
94
+ def fetch(name, default = :__raise)
95
+ @stack.each do |frame|
96
+ # Prevent infinite recursion.
97
+ next if frame == self
98
+
99
+ value = find(frame, name, :__missing)
100
+ if value != :__missing
101
+ return value
102
+ end
103
+ end
104
+
105
+ if default == :__raise || porthole_in_stack.raise_on_context_miss?
106
+ raise ContextMiss.new("Can't find #{name} in #{@stack.inspect}")
107
+ else
108
+ default
109
+ end
110
+ end
111
+
112
+ # Finds a key in an object, using whatever method is most
113
+ # appropriate. If the object is a hash, does a simple hash lookup.
114
+ # If it's an object that responds to the key as a method call,
115
+ # invokes that method. You get the idea.
116
+ #
117
+ # obj - The object to perform the lookup on.
118
+ # key - The key whose value you want.
119
+ # default - An optional default value, to return if the
120
+ # key is not found.
121
+ #
122
+ # Returns the value of key in obj if it is found and default otherwise.
123
+ def find(obj, key, default = nil)
124
+ hash = obj.respond_to?(:has_key?)
125
+
126
+ if hash && obj.has_key?(key)
127
+ obj[key]
128
+ elsif hash && obj.has_key?(key.to_s)
129
+ obj[key.to_s]
130
+ elsif !hash && obj.respond_to?(key)
131
+ meth = obj.method(key) rescue proc { obj.send(key) }
132
+ if meth.arity == 1
133
+ meth.to_proc
134
+ else
135
+ meth[]
136
+ end
137
+ else
138
+ default
139
+ end
140
+ end
141
+ end
142
+ end