inversion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. data.tar.gz.sig +2 -0
  2. data/.gemtest +0 -0
  3. data/ChangeLog +836 -0
  4. data/History.md +4 -0
  5. data/Manifest.txt +74 -0
  6. data/README.rdoc +171 -0
  7. data/Rakefile +55 -0
  8. data/bin/inversion +276 -0
  9. data/lib/inversion.rb +98 -0
  10. data/lib/inversion/exceptions.rb +21 -0
  11. data/lib/inversion/mixins.rb +236 -0
  12. data/lib/inversion/monkeypatches.rb +20 -0
  13. data/lib/inversion/renderstate.rb +337 -0
  14. data/lib/inversion/sinatra.rb +35 -0
  15. data/lib/inversion/template.rb +250 -0
  16. data/lib/inversion/template/attrtag.rb +120 -0
  17. data/lib/inversion/template/calltag.rb +16 -0
  18. data/lib/inversion/template/codetag.rb +164 -0
  19. data/lib/inversion/template/commenttag.rb +54 -0
  20. data/lib/inversion/template/conditionaltag.rb +49 -0
  21. data/lib/inversion/template/configtag.rb +60 -0
  22. data/lib/inversion/template/containertag.rb +45 -0
  23. data/lib/inversion/template/elsetag.rb +62 -0
  24. data/lib/inversion/template/elsiftag.rb +49 -0
  25. data/lib/inversion/template/endtag.rb +55 -0
  26. data/lib/inversion/template/escapetag.rb +26 -0
  27. data/lib/inversion/template/fortag.rb +120 -0
  28. data/lib/inversion/template/iftag.rb +69 -0
  29. data/lib/inversion/template/importtag.rb +70 -0
  30. data/lib/inversion/template/includetag.rb +51 -0
  31. data/lib/inversion/template/node.rb +102 -0
  32. data/lib/inversion/template/parser.rb +297 -0
  33. data/lib/inversion/template/pptag.rb +28 -0
  34. data/lib/inversion/template/publishtag.rb +72 -0
  35. data/lib/inversion/template/subscribetag.rb +88 -0
  36. data/lib/inversion/template/tag.rb +150 -0
  37. data/lib/inversion/template/textnode.rb +43 -0
  38. data/lib/inversion/template/unlesstag.rb +60 -0
  39. data/lib/inversion/template/uriencodetag.rb +30 -0
  40. data/lib/inversion/template/yieldtag.rb +51 -0
  41. data/lib/inversion/tilt.rb +82 -0
  42. data/lib/inversion/utils.rb +235 -0
  43. data/spec/data/sinatra/hello.inversion +1 -0
  44. data/spec/inversion/mixins_spec.rb +177 -0
  45. data/spec/inversion/monkeypatches_spec.rb +35 -0
  46. data/spec/inversion/renderstate_spec.rb +291 -0
  47. data/spec/inversion/sinatra_spec.rb +59 -0
  48. data/spec/inversion/template/attrtag_spec.rb +216 -0
  49. data/spec/inversion/template/calltag_spec.rb +30 -0
  50. data/spec/inversion/template/codetag_spec.rb +51 -0
  51. data/spec/inversion/template/commenttag_spec.rb +84 -0
  52. data/spec/inversion/template/configtag_spec.rb +105 -0
  53. data/spec/inversion/template/containertag_spec.rb +54 -0
  54. data/spec/inversion/template/elsetag_spec.rb +105 -0
  55. data/spec/inversion/template/elsiftag_spec.rb +87 -0
  56. data/spec/inversion/template/endtag_spec.rb +78 -0
  57. data/spec/inversion/template/escapetag_spec.rb +59 -0
  58. data/spec/inversion/template/fortag_spec.rb +98 -0
  59. data/spec/inversion/template/iftag_spec.rb +241 -0
  60. data/spec/inversion/template/importtag_spec.rb +106 -0
  61. data/spec/inversion/template/includetag_spec.rb +108 -0
  62. data/spec/inversion/template/node_spec.rb +81 -0
  63. data/spec/inversion/template/parser_spec.rb +170 -0
  64. data/spec/inversion/template/pptag_spec.rb +51 -0
  65. data/spec/inversion/template/publishtag_spec.rb +69 -0
  66. data/spec/inversion/template/subscribetag_spec.rb +60 -0
  67. data/spec/inversion/template/tag_spec.rb +97 -0
  68. data/spec/inversion/template/textnode_spec.rb +86 -0
  69. data/spec/inversion/template/unlesstag_spec.rb +84 -0
  70. data/spec/inversion/template/uriencodetag_spec.rb +49 -0
  71. data/spec/inversion/template/yieldtag_spec.rb +54 -0
  72. data/spec/inversion/template_spec.rb +269 -0
  73. data/spec/inversion/tilt_spec.rb +47 -0
  74. data/spec/inversion_spec.rb +95 -0
  75. data/spec/lib/constants.rb +9 -0
  76. data/spec/lib/helpers.rb +160 -0
  77. metadata +316 -0
  78. metadata.gz.sig +0 -0
@@ -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( /&/, '&amp;' ).
229
+ gsub( /</, '&lt;' ).
230
+ gsub( />/, '&gt;' )
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
+