inversion 0.0.1
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.tar.gz.sig +2 -0
- data/.gemtest +0 -0
- data/ChangeLog +836 -0
- data/History.md +4 -0
- data/Manifest.txt +74 -0
- data/README.rdoc +171 -0
- data/Rakefile +55 -0
- data/bin/inversion +276 -0
- data/lib/inversion.rb +98 -0
- data/lib/inversion/exceptions.rb +21 -0
- data/lib/inversion/mixins.rb +236 -0
- data/lib/inversion/monkeypatches.rb +20 -0
- data/lib/inversion/renderstate.rb +337 -0
- data/lib/inversion/sinatra.rb +35 -0
- data/lib/inversion/template.rb +250 -0
- data/lib/inversion/template/attrtag.rb +120 -0
- data/lib/inversion/template/calltag.rb +16 -0
- data/lib/inversion/template/codetag.rb +164 -0
- data/lib/inversion/template/commenttag.rb +54 -0
- data/lib/inversion/template/conditionaltag.rb +49 -0
- data/lib/inversion/template/configtag.rb +60 -0
- data/lib/inversion/template/containertag.rb +45 -0
- data/lib/inversion/template/elsetag.rb +62 -0
- data/lib/inversion/template/elsiftag.rb +49 -0
- data/lib/inversion/template/endtag.rb +55 -0
- data/lib/inversion/template/escapetag.rb +26 -0
- data/lib/inversion/template/fortag.rb +120 -0
- data/lib/inversion/template/iftag.rb +69 -0
- data/lib/inversion/template/importtag.rb +70 -0
- data/lib/inversion/template/includetag.rb +51 -0
- data/lib/inversion/template/node.rb +102 -0
- data/lib/inversion/template/parser.rb +297 -0
- data/lib/inversion/template/pptag.rb +28 -0
- data/lib/inversion/template/publishtag.rb +72 -0
- data/lib/inversion/template/subscribetag.rb +88 -0
- data/lib/inversion/template/tag.rb +150 -0
- data/lib/inversion/template/textnode.rb +43 -0
- data/lib/inversion/template/unlesstag.rb +60 -0
- data/lib/inversion/template/uriencodetag.rb +30 -0
- data/lib/inversion/template/yieldtag.rb +51 -0
- data/lib/inversion/tilt.rb +82 -0
- data/lib/inversion/utils.rb +235 -0
- data/spec/data/sinatra/hello.inversion +1 -0
- data/spec/inversion/mixins_spec.rb +177 -0
- data/spec/inversion/monkeypatches_spec.rb +35 -0
- data/spec/inversion/renderstate_spec.rb +291 -0
- data/spec/inversion/sinatra_spec.rb +59 -0
- data/spec/inversion/template/attrtag_spec.rb +216 -0
- data/spec/inversion/template/calltag_spec.rb +30 -0
- data/spec/inversion/template/codetag_spec.rb +51 -0
- data/spec/inversion/template/commenttag_spec.rb +84 -0
- data/spec/inversion/template/configtag_spec.rb +105 -0
- data/spec/inversion/template/containertag_spec.rb +54 -0
- data/spec/inversion/template/elsetag_spec.rb +105 -0
- data/spec/inversion/template/elsiftag_spec.rb +87 -0
- data/spec/inversion/template/endtag_spec.rb +78 -0
- data/spec/inversion/template/escapetag_spec.rb +59 -0
- data/spec/inversion/template/fortag_spec.rb +98 -0
- data/spec/inversion/template/iftag_spec.rb +241 -0
- data/spec/inversion/template/importtag_spec.rb +106 -0
- data/spec/inversion/template/includetag_spec.rb +108 -0
- data/spec/inversion/template/node_spec.rb +81 -0
- data/spec/inversion/template/parser_spec.rb +170 -0
- data/spec/inversion/template/pptag_spec.rb +51 -0
- data/spec/inversion/template/publishtag_spec.rb +69 -0
- data/spec/inversion/template/subscribetag_spec.rb +60 -0
- data/spec/inversion/template/tag_spec.rb +97 -0
- data/spec/inversion/template/textnode_spec.rb +86 -0
- data/spec/inversion/template/unlesstag_spec.rb +84 -0
- data/spec/inversion/template/uriencodetag_spec.rb +49 -0
- data/spec/inversion/template/yieldtag_spec.rb +54 -0
- data/spec/inversion/template_spec.rb +269 -0
- data/spec/inversion/tilt_spec.rb +47 -0
- data/spec/inversion_spec.rb +95 -0
- data/spec/lib/constants.rb +9 -0
- data/spec/lib/helpers.rb +160 -0
- metadata +316 -0
- metadata.gz.sig +0 -0
data/lib/inversion.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
|
7
|
+
# The Inversion templating system. This module provides the namespace for all the other
|
8
|
+
# classes and modules, and contains the logging subsystem. A good place to start for
|
9
|
+
# documentation would be to check out the examples in the README, and then
|
10
|
+
# Inversion::Template for a list of tags, configuration options, etc.
|
11
|
+
#
|
12
|
+
# == Authors
|
13
|
+
#
|
14
|
+
# * Michael Granger <ged@FaerieMUD.org>
|
15
|
+
# * Mahlon E. Smith <mahlon@martini.nu>
|
16
|
+
#
|
17
|
+
# :main: README.rdoc
|
18
|
+
#
|
19
|
+
module Inversion
|
20
|
+
|
21
|
+
warn ">>> Inversion requires Ruby 1.9.2 or later. <<<" if RUBY_VERSION < '1.9.2'
|
22
|
+
|
23
|
+
require 'inversion/exceptions'
|
24
|
+
require 'inversion/mixins'
|
25
|
+
require 'inversion/utils'
|
26
|
+
require 'inversion/monkeypatches'
|
27
|
+
|
28
|
+
# Library version constant
|
29
|
+
VERSION = '0.0.1'
|
30
|
+
|
31
|
+
# Version-control revision constant
|
32
|
+
REVISION = %q$Revision: 73c3d8215868 $
|
33
|
+
|
34
|
+
#
|
35
|
+
# Logging
|
36
|
+
#
|
37
|
+
|
38
|
+
# Log levels
|
39
|
+
LOG_LEVELS = {
|
40
|
+
'debug' => Logger::DEBUG,
|
41
|
+
'info' => Logger::INFO,
|
42
|
+
'warn' => Logger::WARN,
|
43
|
+
'error' => Logger::ERROR,
|
44
|
+
'fatal' => Logger::FATAL,
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
# Log levels keyed by level
|
48
|
+
LOG_LEVEL_NAMES = LOG_LEVELS.invert.freeze
|
49
|
+
|
50
|
+
@default_logger = Logger.new( $stderr )
|
51
|
+
@default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
|
52
|
+
|
53
|
+
@default_log_formatter = Inversion::LogFormatter.new( @default_logger )
|
54
|
+
@default_logger.formatter = @default_log_formatter
|
55
|
+
|
56
|
+
@logger = @default_logger
|
57
|
+
|
58
|
+
|
59
|
+
class << self
|
60
|
+
# the log formatter that will be used when the logging subsystem is reset
|
61
|
+
attr_accessor :default_log_formatter
|
62
|
+
|
63
|
+
# the logger that will be used when the logging subsystem is reset
|
64
|
+
attr_accessor :default_logger
|
65
|
+
|
66
|
+
# the logger that's currently in effect
|
67
|
+
attr_accessor :logger
|
68
|
+
alias_method :log, :logger
|
69
|
+
alias_method :log=, :logger=
|
70
|
+
end
|
71
|
+
|
72
|
+
|
73
|
+
### Reset the global logger object to the default
|
74
|
+
def self::reset_logger
|
75
|
+
self.logger = self.default_logger
|
76
|
+
self.logger.level = Logger::WARN
|
77
|
+
self.logger.formatter = self.default_log_formatter
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
### Returns +true+ if the global logger has not been set to something other than
|
82
|
+
### the default one.
|
83
|
+
def self::using_default_logger?
|
84
|
+
return self.logger == self.default_logger
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
### Get the Inversion version.
|
89
|
+
def self::version_string( include_buildnum=false )
|
90
|
+
vstring = "%s %s" % [ self.name, VERSION ]
|
91
|
+
vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
|
92
|
+
return vstring
|
93
|
+
end
|
94
|
+
|
95
|
+
require 'inversion/template'
|
96
|
+
|
97
|
+
end # module Inversion
|
98
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
#--
|
5
|
+
module Inversion
|
6
|
+
|
7
|
+
# An exception class raised from the Inversion::Template::Parser when
|
8
|
+
# a problem is encountered while parsing a template.
|
9
|
+
class ParseError < ::RuntimeError; end
|
10
|
+
|
11
|
+
# An exception class raised when a problem is detected in a template
|
12
|
+
# configuration option.
|
13
|
+
class OptionsError < ::RuntimeError; end
|
14
|
+
|
15
|
+
# An exception class raised when a template includes itself, either
|
16
|
+
# directly or indirectly.
|
17
|
+
class StackError < ::RuntimeError; end
|
18
|
+
|
19
|
+
end # module Inversion
|
20
|
+
|
21
|
+
|
@@ -0,0 +1,236 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set nosta noet ts=4 sw=4:
|
3
|
+
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
|
7
|
+
module Inversion
|
8
|
+
|
9
|
+
# Add logging to a Inversion class. Including classes get #log and
|
10
|
+
# #log_debug methods.
|
11
|
+
#
|
12
|
+
# class MyClass
|
13
|
+
# include Inversion::Loggable
|
14
|
+
#
|
15
|
+
# def a_method
|
16
|
+
# self.log.debug "Doing a_method stuff..."
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
module Loggable
|
21
|
+
|
22
|
+
### A logging proxy class that wraps calls to the logger into calls that include
|
23
|
+
### the name of the calling class.
|
24
|
+
class ClassNameProxy # :nodoc:
|
25
|
+
|
26
|
+
### Create a new proxy for the given +klass+.
|
27
|
+
def initialize( klass, force_debug=false )
|
28
|
+
@classname = klass.name
|
29
|
+
@force_debug = force_debug
|
30
|
+
end
|
31
|
+
|
32
|
+
### Delegate debug messages to the global logger with the appropriate class name.
|
33
|
+
def debug( msg=nil, &block )
|
34
|
+
Inversion.logger.add( Logger::DEBUG, msg, @classname, &block )
|
35
|
+
end
|
36
|
+
|
37
|
+
### Delegate info messages to the global logger with the appropriate class name.
|
38
|
+
def info( msg=nil, &block )
|
39
|
+
return self.debug( msg, &block ) if @force_debug
|
40
|
+
Inversion.logger.add( Logger::INFO, msg, @classname, &block )
|
41
|
+
end
|
42
|
+
|
43
|
+
### Delegate warn messages to the global logger with the appropriate class name.
|
44
|
+
def warn( msg=nil, &block )
|
45
|
+
return self.debug( msg, &block ) if @force_debug
|
46
|
+
Inversion.logger.add( Logger::WARN, msg, @classname, &block )
|
47
|
+
end
|
48
|
+
|
49
|
+
### Delegate error messages to the global logger with the appropriate class name.
|
50
|
+
def error( msg=nil, &block )
|
51
|
+
return self.debug( msg, &block ) if @force_debug
|
52
|
+
Inversion.logger.add( Logger::ERROR, msg, @classname, &block )
|
53
|
+
end
|
54
|
+
|
55
|
+
### Delegate fatal messages to the global logger with the appropriate class name.
|
56
|
+
def fatal( msg=nil, &block )
|
57
|
+
Inversion.logger.add( Logger::FATAL, msg, @classname, &block )
|
58
|
+
end
|
59
|
+
|
60
|
+
end # ClassNameProxy
|
61
|
+
|
62
|
+
#########
|
63
|
+
protected
|
64
|
+
#########
|
65
|
+
|
66
|
+
### Copy constructor -- clear the original's log proxy.
|
67
|
+
def initialize_copy( original )
|
68
|
+
@log_proxy = @log_debug_proxy = nil
|
69
|
+
super
|
70
|
+
end
|
71
|
+
|
72
|
+
### Return the proxied logger.
|
73
|
+
def log
|
74
|
+
@log_proxy ||= ClassNameProxy.new( self.class )
|
75
|
+
end
|
76
|
+
|
77
|
+
### Return a proxied "debug" logger that ignores other level specification.
|
78
|
+
def log_debug
|
79
|
+
@log_debug_proxy ||= ClassNameProxy.new( self.class, true )
|
80
|
+
end
|
81
|
+
|
82
|
+
end # module Loggable
|
83
|
+
|
84
|
+
|
85
|
+
# Hides your class's ::new method and adds a +pure_virtual+ method generator for
|
86
|
+
# defining API methods. If subclasses of your class don't provide implementations of
|
87
|
+
# "pure_virtual" methods, NotImplementedErrors will be raised if they are called.
|
88
|
+
#
|
89
|
+
# # AbstractClass
|
90
|
+
# class MyBaseClass
|
91
|
+
# include Inversion::AbstractClass
|
92
|
+
#
|
93
|
+
# # Define a method that will raise a NotImplementedError if called
|
94
|
+
# pure_virtual :api_method
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
module AbstractClass
|
98
|
+
|
99
|
+
### Methods to be added to including classes
|
100
|
+
module ClassMethods
|
101
|
+
|
102
|
+
### Define one or more "virtual" methods which will raise
|
103
|
+
### NotImplementedErrors when called via a concrete subclass.
|
104
|
+
def pure_virtual( *syms )
|
105
|
+
syms.each do |sym|
|
106
|
+
define_method( sym ) do |*args|
|
107
|
+
raise ::NotImplementedError,
|
108
|
+
"%p does not provide an implementation of #%s" % [ self.class, sym ],
|
109
|
+
caller(1)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
### Turn subclasses' new methods back to public.
|
116
|
+
def inherited( subclass )
|
117
|
+
subclass.module_eval { public_class_method :new }
|
118
|
+
super
|
119
|
+
end
|
120
|
+
|
121
|
+
end # module ClassMethods
|
122
|
+
|
123
|
+
|
124
|
+
extend ClassMethods
|
125
|
+
|
126
|
+
### Inclusion callback
|
127
|
+
def self::included( mod )
|
128
|
+
super
|
129
|
+
if mod.respond_to?( :new )
|
130
|
+
mod.extend( ClassMethods )
|
131
|
+
mod.module_eval { private_class_method :new }
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
end # module AbstractClass
|
137
|
+
|
138
|
+
|
139
|
+
# A collection of utilities for working with Hashes.
|
140
|
+
module HashUtilities
|
141
|
+
|
142
|
+
###############
|
143
|
+
module_function
|
144
|
+
###############
|
145
|
+
|
146
|
+
### Return a version of the given +hash+ with its keys transformed
|
147
|
+
### into Strings from whatever they were before.
|
148
|
+
###
|
149
|
+
### stringhash = stringify_keys( symbolhash )
|
150
|
+
###
|
151
|
+
def stringify_keys( hash )
|
152
|
+
newhash = {}
|
153
|
+
|
154
|
+
hash.each do |key,val|
|
155
|
+
if val.is_a?( Hash )
|
156
|
+
newhash[ key.to_s ] = stringify_keys( val )
|
157
|
+
else
|
158
|
+
newhash[ key.to_s ] = val
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
return newhash
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
### Return a duplicate of the given +hash+ with its identifier-like keys
|
167
|
+
### untainted and transformed into symbols from whatever they were before.
|
168
|
+
###
|
169
|
+
### symbolhash = symbolify_keys( stringhash )
|
170
|
+
###
|
171
|
+
def symbolify_keys( hash )
|
172
|
+
newhash = {}
|
173
|
+
|
174
|
+
hash.each do |key,val|
|
175
|
+
keysym = key.to_s.dup.untaint.to_sym
|
176
|
+
|
177
|
+
if val.is_a?( Hash )
|
178
|
+
newhash[ keysym ] = symbolify_keys( val )
|
179
|
+
else
|
180
|
+
newhash[ keysym ] = val
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
return newhash
|
185
|
+
end
|
186
|
+
alias_method :internify_keys, :symbolify_keys
|
187
|
+
|
188
|
+
end # module HashUtilities
|
189
|
+
|
190
|
+
|
191
|
+
# A mixin that adds configurable escaping to a tag class.
|
192
|
+
#
|
193
|
+
# class MyTag < Inversion::Template::Tag
|
194
|
+
# include Inversion::Escaping
|
195
|
+
#
|
196
|
+
# def render( renderstate )
|
197
|
+
# val = self.get_rendered_value
|
198
|
+
# return self.escape( val.to_s, renderstate )
|
199
|
+
# end
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# To add a new kind of escaping to Inversion, add a #escape_<formatname> method to this
|
203
|
+
# module similar to #escape_html.
|
204
|
+
module Escaping
|
205
|
+
|
206
|
+
# The fallback escape format
|
207
|
+
DEFAULT_ESCAPE_FORMAT = :none
|
208
|
+
|
209
|
+
|
210
|
+
### Escape the +output+ using the format specified by the given +render_state+'s config.
|
211
|
+
def escape( output, render_state )
|
212
|
+
format = render_state.options[:escape_format] || DEFAULT_ESCAPE_FORMAT
|
213
|
+
return output if format == :none
|
214
|
+
|
215
|
+
unless self.respond_to?( "escape_#{format}" )
|
216
|
+
self.log.error "Format %p not supported. To add support, define a #escape_%s to %s" %
|
217
|
+
[ format, format, __FILE__ ]
|
218
|
+
raise Inversion::OptionsError, "No such escape format %p" % [ format ]
|
219
|
+
end
|
220
|
+
|
221
|
+
return self.__send__( "escape_#{format}", output )
|
222
|
+
end
|
223
|
+
|
224
|
+
|
225
|
+
### Escape the given +output+ using HTML entity-encoding.
|
226
|
+
def escape_html( output )
|
227
|
+
return output.
|
228
|
+
gsub( /&/, '&' ).
|
229
|
+
gsub( /</, '<' ).
|
230
|
+
gsub( />/, '>' )
|
231
|
+
end
|
232
|
+
|
233
|
+
end # Escaping
|
234
|
+
|
235
|
+
end # module Inversion
|
236
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set nosta noet ts=4 sw=4:
|
3
|
+
|
4
|
+
require 'inversion' unless defined?( Inversion )
|
5
|
+
require 'ripper'
|
6
|
+
|
7
|
+
# Monkeypatch mixin to expose the 'tokens' instance variable of
|
8
|
+
# Ripper::TokenPattern::MatchData. Included in Ripper::TokenPattern::MatchData.
|
9
|
+
module Inversion::RipperAdditions
|
10
|
+
|
11
|
+
# the array of token tuples
|
12
|
+
attr_reader :tokens
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
# :stopdoc:
|
17
|
+
class Ripper::TokenPattern::MatchData
|
18
|
+
include Inversion::RipperAdditions
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,337 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# vim: set noet nosta sw=4 ts=4 :
|
3
|
+
|
4
|
+
require 'inversion' unless defined?( Inversion )
|
5
|
+
|
6
|
+
|
7
|
+
# An object that provides an encapsulation of the template's state while it is rendering.
|
8
|
+
class Inversion::RenderState
|
9
|
+
include Inversion::Loggable
|
10
|
+
|
11
|
+
### Create a new RenderState. If the template is being rendered inside another one, the
|
12
|
+
### containing template's RenderState will be passed as the +containerstate+. The
|
13
|
+
### +initial_attributes+ will be deep-copied, and the +options+ will be merged with
|
14
|
+
### Inversion::Template::DEFAULT_CONFIG. The +block+ is stored for use by
|
15
|
+
### template nodes.
|
16
|
+
def initialize( containerstate=nil, initial_attributes={}, options={}, &block )
|
17
|
+
|
18
|
+
# Shift hash arguments if created without a parent state
|
19
|
+
if containerstate.is_a?( Hash )
|
20
|
+
options = initial_attributes
|
21
|
+
initial_attributes = containerstate
|
22
|
+
containerstate = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
self.log.debug "Creating a render state with attributes: %p" %
|
26
|
+
[ initial_attributes ]
|
27
|
+
|
28
|
+
@containerstate = containerstate
|
29
|
+
@options = Inversion::Template::DEFAULT_CONFIG.merge( options )
|
30
|
+
@attributes = [ deep_copy(initial_attributes) ]
|
31
|
+
@block = block
|
32
|
+
@default_errhandler = self.method( :default_error_handler )
|
33
|
+
@errhandler = @default_errhandler
|
34
|
+
|
35
|
+
# The rendered output Array, and the stack of render destinations
|
36
|
+
@output = []
|
37
|
+
@destinations = [ @output ]
|
38
|
+
|
39
|
+
# Hash of subscribed Nodes, keyed by the subscription key as a Symbol
|
40
|
+
@subscriptions = Hash.new {|hsh, k| hsh[k] = [] } # Auto-vivify
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
######
|
46
|
+
public
|
47
|
+
######
|
48
|
+
|
49
|
+
# The Inversion::RenderState of the containing template, if any
|
50
|
+
attr_reader :containerstate
|
51
|
+
|
52
|
+
# The config options passed in from the template
|
53
|
+
attr_reader :options
|
54
|
+
|
55
|
+
# The block passed to the template's #render method, if there was one
|
56
|
+
attr_reader :block
|
57
|
+
|
58
|
+
# Subscribe placeholders for publish/subscribe
|
59
|
+
attr_reader :subscriptions
|
60
|
+
|
61
|
+
# The stack of rendered output destinations, most-recent last.
|
62
|
+
attr_reader :destinations
|
63
|
+
|
64
|
+
# The callable object that handles exceptions raised when a node is appended
|
65
|
+
attr_reader :errhandler
|
66
|
+
|
67
|
+
# The default error handler
|
68
|
+
attr_reader :default_errhandler
|
69
|
+
|
70
|
+
|
71
|
+
### Return the hash of attributes that are currently in effect in the
|
72
|
+
### rendering state.
|
73
|
+
def attributes
|
74
|
+
return @attributes.last
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
### Evaluate the specified +code+ in the context of itself and
|
79
|
+
### return the result.
|
80
|
+
def eval( code )
|
81
|
+
self.log.debug "Evaling: %p" [ code ]
|
82
|
+
return self.instance_eval( code )
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
### Override the state's attributes with the given +overrides+, call the +block+, then
|
87
|
+
### restore the attributes to their original values.
|
88
|
+
def with_attributes( overrides )
|
89
|
+
raise LocalJumpError, "no block given" unless block_given?
|
90
|
+
self.log.debug "Overriding template attributes with: %p" % [ overrides ]
|
91
|
+
|
92
|
+
begin
|
93
|
+
@attributes.push( @attributes.last.merge(overrides) )
|
94
|
+
yield( self )
|
95
|
+
ensure
|
96
|
+
@attributes.pop
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
### Override the state's render destination, call the block, then restore the original
|
102
|
+
### destination when the block returns.
|
103
|
+
def with_destination( new_destination )
|
104
|
+
raise LocalJumpError, "no block given" unless block_given?
|
105
|
+
self.log.debug "Overriding render destination with: %p" % [ new_destination ]
|
106
|
+
|
107
|
+
begin
|
108
|
+
@destinations.push( new_destination )
|
109
|
+
yield
|
110
|
+
ensure
|
111
|
+
self.log.debug " removing overridden render destination: %p" % [ @destinations.last ]
|
112
|
+
@destinations.pop
|
113
|
+
end
|
114
|
+
|
115
|
+
return new_destination
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
### Set the state's error handler to +handler+ for the duration of the block, restoring
|
120
|
+
### the previous handler after the block exits. +Handler+ must respond to #call, and will
|
121
|
+
### be called with two arguments: the node that raised the exception, and the exception object
|
122
|
+
### itself.
|
123
|
+
def with_error_handler( handler )
|
124
|
+
original_handler = self.errhandler
|
125
|
+
raise ArgumentError, "%p doesn't respond_to #call" unless handler.respond_to?( :call )
|
126
|
+
@errhandler = handler
|
127
|
+
|
128
|
+
yield
|
129
|
+
|
130
|
+
ensure
|
131
|
+
@errhandler = original_handler
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
### Return the current rendered output destination.
|
136
|
+
def destination
|
137
|
+
return self.destinations.last
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
### Returns a new RenderState containing the attributes and options of the receiver
|
142
|
+
### merged with those of the +otherstate+.
|
143
|
+
def merge( otherstate )
|
144
|
+
merged = self.dup
|
145
|
+
merged.merge!( otherstate )
|
146
|
+
return merged
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
### Merge the attributes and options of the +otherstate+ with those of the receiver,
|
151
|
+
### replacing any with the same keys.
|
152
|
+
def merge!( otherstate )
|
153
|
+
self.attributes.merge!( otherstate.attributes )
|
154
|
+
self.options.merge!( otherstate.options )
|
155
|
+
return self
|
156
|
+
end
|
157
|
+
|
158
|
+
|
159
|
+
### Append operator -- add an node to the final rendered output. If the +node+ renders
|
160
|
+
### as an object that itself responds to the #render method, #render will be called and
|
161
|
+
### the return value will be appended instead. This will continue until the returned
|
162
|
+
### object either doesn't respond to #render or #renders as itself.
|
163
|
+
def <<( node )
|
164
|
+
self.log.debug "Appending a %p to %p" % [ node.class, self ]
|
165
|
+
self.destination << self.make_node_comment( node ) if self.options[:debugging_comments]
|
166
|
+
original_node = node
|
167
|
+
previous_node = nil
|
168
|
+
|
169
|
+
begin
|
170
|
+
# Allow render to be delegated to subobjects
|
171
|
+
while node.respond_to?( :render ) && node != previous_node
|
172
|
+
self.log.debug " delegated rendering to: %p" % [ node ]
|
173
|
+
previous_node = node
|
174
|
+
node = node.render( self )
|
175
|
+
end
|
176
|
+
|
177
|
+
self.log.debug " adding a %p to the destination (%p)" %
|
178
|
+
[ node.class, self.destination.class ]
|
179
|
+
self.destination << node
|
180
|
+
self.log.debug " just appended %p to %p" % [ node, self.destination ]
|
181
|
+
rescue ::StandardError => err
|
182
|
+
self.log.debug " handling a %p while rendering: %s" % [ err.class, err.message ]
|
183
|
+
self.destination << self.handle_render_error( original_node, err )
|
184
|
+
end
|
185
|
+
|
186
|
+
return self
|
187
|
+
end
|
188
|
+
|
189
|
+
|
190
|
+
### Turn the rendered node structure into the final rendered String.
|
191
|
+
def to_s
|
192
|
+
return @output.flatten.map( &:to_s ).join
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
### Publish the given +nodes+ to all subscribers to the specified +key+.
|
197
|
+
def publish( key, *nodes )
|
198
|
+
key = key.to_sym
|
199
|
+
|
200
|
+
self.containerstate.publish( key, *nodes ) if self.containerstate
|
201
|
+
self.subscriptions[ key ].each do |subscriber|
|
202
|
+
subscriber.publish( *nodes )
|
203
|
+
end
|
204
|
+
end
|
205
|
+
alias_method :publish_nodes, :publish
|
206
|
+
|
207
|
+
|
208
|
+
### Subscribe the given +node+ to nodes published with the specified +key+.
|
209
|
+
def subscribe( key, node )
|
210
|
+
key = key.to_sym
|
211
|
+
self.subscriptions[ key ] << node
|
212
|
+
end
|
213
|
+
|
214
|
+
|
215
|
+
### Handle an +exception+ that was raised while appending a node by calling the
|
216
|
+
### #errhandler.
|
217
|
+
def handle_render_error( node, exception )
|
218
|
+
self.log.error "%s while rendering %p: %s" %
|
219
|
+
[ exception.class.name, node.as_comment_body, exception.message ]
|
220
|
+
|
221
|
+
handler = self.errhandler
|
222
|
+
raise ScriptError, "error handler %p isn't #call-able!" % [ handler ] unless
|
223
|
+
handler.respond_to?( :call )
|
224
|
+
|
225
|
+
self.log.debug "Handling %p with handler: %p" % [ exception.class, handler ]
|
226
|
+
return handler.call( self, node, exception )
|
227
|
+
|
228
|
+
rescue ::StandardError => err
|
229
|
+
# Handle exceptions from overridden error handlers (re-raised or errors in
|
230
|
+
# the handler itself) via the default handler.
|
231
|
+
if handler && handler != self.default_errhandler
|
232
|
+
self.log.error "%p (re)raised from custom error handler %p" % [ err.class, handler ]
|
233
|
+
self.default_errhandler.call( self, node, exception )
|
234
|
+
else
|
235
|
+
raise( err )
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
### Default exception handler: Handle an +exception+ while rendering +node+ according to the
|
241
|
+
### behavior specified by the `on_render_error` option. Returns the string which should be
|
242
|
+
### appended to the output, if any.
|
243
|
+
def default_error_handler( state, node, exception )
|
244
|
+
case self.options[:on_render_error].to_s
|
245
|
+
when 'ignore'
|
246
|
+
self.log.debug " not rendering anything for the error"
|
247
|
+
return ''
|
248
|
+
|
249
|
+
when 'comment'
|
250
|
+
self.log.debug " rendering error as a comment"
|
251
|
+
msg = "%s: %s" % [ exception.class.name, exception.message ]
|
252
|
+
return self.make_comment( msg )
|
253
|
+
|
254
|
+
when 'propagate'
|
255
|
+
self.log.debug " propagating error while rendering"
|
256
|
+
raise( exception )
|
257
|
+
|
258
|
+
else
|
259
|
+
raise Inversion::OptionsError,
|
260
|
+
"unknown exception-handling mode: %p" % [ self.options[:on_render_error] ]
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
### Return a human-readable representation of the object.
|
266
|
+
def inspect
|
267
|
+
return "#<%p:0x%08x containerstate: %s, attributes: %s, destination: %p>" % [
|
268
|
+
self.class,
|
269
|
+
self.object_id / 2,
|
270
|
+
self.containerstate ? "0x%08x" % [ self.containerstate.object_id ] : "nil",
|
271
|
+
self.attributes.keys.sort.join(', '),
|
272
|
+
self.destination.class,
|
273
|
+
]
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
#########
|
278
|
+
protected
|
279
|
+
#########
|
280
|
+
|
281
|
+
### Return the +node+ as a comment if debugging comments are enabled.
|
282
|
+
def make_node_comment( node )
|
283
|
+
comment_body = node.as_comment_body or return ''
|
284
|
+
return self.make_comment( comment_body )
|
285
|
+
end
|
286
|
+
|
287
|
+
|
288
|
+
### Return the specified +content+ inside of the configured comment characters.
|
289
|
+
def make_comment( content )
|
290
|
+
return [
|
291
|
+
self.options[:comment_start],
|
292
|
+
content,
|
293
|
+
self.options[:comment_end],
|
294
|
+
].join
|
295
|
+
end
|
296
|
+
|
297
|
+
|
298
|
+
### Handle attribute methods.
|
299
|
+
def method_missing( sym, *args, &block )
|
300
|
+
return super unless sym.to_s =~ /^[a-z]\w+[\?=!]?$/
|
301
|
+
self.log.debug "mapping missing method call to attribute: %p" % [ sym ]
|
302
|
+
return self.attributes[ sym ]
|
303
|
+
end
|
304
|
+
|
305
|
+
|
306
|
+
#######
|
307
|
+
private
|
308
|
+
#######
|
309
|
+
|
310
|
+
### Recursively copy the specified +obj+ and return the result.
|
311
|
+
def deep_copy( obj )
|
312
|
+
Inversion.log.debug "Deep copying: %p" % [ obj ]
|
313
|
+
|
314
|
+
# Handle mocks during testing
|
315
|
+
return obj if obj.class.name == 'RSpec::Mocks::Mock'
|
316
|
+
|
317
|
+
return case obj
|
318
|
+
when NilClass, Numeric, TrueClass, FalseClass, Symbol
|
319
|
+
obj
|
320
|
+
|
321
|
+
when Array
|
322
|
+
obj.map {|o| deep_copy(o) }
|
323
|
+
|
324
|
+
when Hash
|
325
|
+
newhash = {}
|
326
|
+
obj.each do |k,v|
|
327
|
+
newhash[ deep_copy(k) ] = deep_copy( v )
|
328
|
+
end
|
329
|
+
newhash
|
330
|
+
|
331
|
+
else
|
332
|
+
obj.clone
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
end # class Inversion::RenderState
|
337
|
+
|