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.
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
+