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