inversion 0.12.3 → 0.14.0

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 (59) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -1
  3. data.tar.gz.sig +0 -0
  4. data/ChangeLog +305 -9
  5. data/Examples.rdoc +134 -0
  6. data/GettingStarted.rdoc +44 -0
  7. data/Guide.rdoc +47 -0
  8. data/History.rdoc +15 -0
  9. data/Manifest.txt +7 -2
  10. data/README.rdoc +9 -10
  11. data/Rakefile +23 -10
  12. data/Tags.rdoc +561 -0
  13. data/lib/inversion.rb +2 -2
  14. data/lib/inversion/renderstate.rb +46 -11
  15. data/lib/inversion/template.rb +85 -7
  16. data/lib/inversion/template/attrtag.rb +1 -1
  17. data/lib/inversion/template/begintag.rb +8 -8
  18. data/lib/inversion/template/fragmenttag.rb +60 -0
  19. data/lib/inversion/template/rescuetag.rb +1 -1
  20. data/spec/{lib/helpers.rb → helpers.rb} +7 -30
  21. data/spec/inversion/mixins_spec.rb +55 -65
  22. data/spec/inversion/monkeypatches_spec.rb +2 -12
  23. data/spec/inversion/parser_spec.rb +34 -44
  24. data/spec/inversion/renderstate_spec.rb +123 -69
  25. data/spec/inversion/sinatra_spec.rb +6 -19
  26. data/spec/inversion/template/attrtag_spec.rb +56 -76
  27. data/spec/inversion/template/begintag_spec.rb +24 -41
  28. data/spec/inversion/template/calltag_spec.rb +1 -18
  29. data/spec/inversion/template/codetag_spec.rb +6 -24
  30. data/spec/inversion/template/commenttag_spec.rb +9 -27
  31. data/spec/inversion/template/configtag_spec.rb +5 -16
  32. data/spec/inversion/template/containertag_spec.rb +4 -21
  33. data/spec/inversion/template/defaulttag_spec.rb +6 -23
  34. data/spec/inversion/template/elsetag_spec.rb +9 -26
  35. data/spec/inversion/template/elsiftag_spec.rb +7 -24
  36. data/spec/inversion/template/endtag_spec.rb +6 -23
  37. data/spec/inversion/template/escapetag_spec.rb +10 -25
  38. data/spec/inversion/template/fortag_spec.rb +20 -37
  39. data/spec/inversion/template/fragmenttag_spec.rb +40 -0
  40. data/spec/inversion/template/iftag_spec.rb +23 -40
  41. data/spec/inversion/template/importtag_spec.rb +8 -25
  42. data/spec/inversion/template/includetag_spec.rb +27 -42
  43. data/spec/inversion/template/node_spec.rb +6 -15
  44. data/spec/inversion/template/pptag_spec.rb +10 -23
  45. data/spec/inversion/template/publishtag_spec.rb +4 -21
  46. data/spec/inversion/template/rescuetag_spec.rb +12 -29
  47. data/spec/inversion/template/subscribetag_spec.rb +8 -25
  48. data/spec/inversion/template/tag_spec.rb +24 -37
  49. data/spec/inversion/template/textnode_spec.rb +8 -24
  50. data/spec/inversion/template/timedeltatag_spec.rb +31 -43
  51. data/spec/inversion/template/unlesstag_spec.rb +7 -24
  52. data/spec/inversion/template/uriencodetag_spec.rb +6 -23
  53. data/spec/inversion/template/yieldtag_spec.rb +3 -20
  54. data/spec/inversion/template_spec.rb +155 -108
  55. data/spec/inversion/tilt_spec.rb +7 -16
  56. data/spec/inversion_spec.rb +7 -22
  57. metadata +63 -40
  58. metadata.gz.sig +0 -0
  59. data/spec/lib/constants.rb +0 -9
data/lib/inversion.rb CHANGED
@@ -26,10 +26,10 @@ module Inversion
26
26
  warn ">>> Inversion requires Ruby 1.9.2 or later. <<<" if RUBY_VERSION < '1.9.2'
27
27
 
28
28
  # Library version constant
29
- VERSION = '0.12.3'
29
+ VERSION = '0.14.0'
30
30
 
31
31
  # Version-control revision constant
32
- REVISION = %q$Revision: 100ed23eb2ba $
32
+ REVISION = %q$Revision: 9bb165feaf57 $
33
33
 
34
34
 
35
35
  ### Get the Inversion version.
@@ -21,8 +21,9 @@ class Inversion::RenderState
21
21
 
22
22
  ### Create a new RenderState::Scope with its initial tag locals set to
23
23
  ### +locals+.
24
- def initialize( locals={} )
24
+ def initialize( locals={}, fragments={} )
25
25
  @locals = locals
26
+ @fragments = fragments
26
27
  end
27
28
 
28
29
 
@@ -41,7 +42,7 @@ class Inversion::RenderState
41
42
  ### Return a copy of the receiving Scope merged with the given +values+,
42
43
  ### which can be either another Scope or a Hash.
43
44
  def +( values )
44
- return Scope.new( @locals.merge(values) )
45
+ return Scope.new( self.__locals__.merge(values), self.__fragments__ )
45
46
  end
46
47
 
47
48
 
@@ -52,6 +53,12 @@ class Inversion::RenderState
52
53
  alias_method :to_hash, :__locals__
53
54
 
54
55
 
56
+ ### Returns the Hash of rendered fragments that belong to this scope.
57
+ def __fragments__
58
+ return @fragments
59
+ end
60
+
61
+
55
62
  #########
56
63
  protected
57
64
  #########
@@ -60,7 +67,7 @@ class Inversion::RenderState
60
67
  ### and map them into values from the Scope's locals.
61
68
  def method_missing( sym, *args, &block )
62
69
  return super unless sym =~ /^\w+$/
63
- @locals[ sym ]
70
+ return @locals[ sym ].nil? ? @fragments[ sym ] : @locals[ sym ]
64
71
  end
65
72
 
66
73
  end # class Scope
@@ -104,6 +111,7 @@ class Inversion::RenderState
104
111
  # as a Symbol
105
112
  @subscriptions = Hash.new {|hsh, k| hsh[k] = [] } # Auto-vivify to an Array
106
113
  @published_nodes = Hash.new {|hsh, k| hsh[k] = [] }
114
+ @fragments = Hash.new {|hsh, k| hsh[k] = [] }
107
115
 
108
116
  end
109
117
 
@@ -127,6 +135,9 @@ class Inversion::RenderState
127
135
  # Published nodes, keyed by subscription
128
136
  attr_reader :published_nodes
129
137
 
138
+ # Fragment nodes, keyed by fragment name
139
+ attr_reader :fragments
140
+
130
141
  # The stack of rendered output destinations, most-recent last.
131
142
  attr_reader :destinations
132
143
 
@@ -296,14 +307,7 @@ class Inversion::RenderState
296
307
 
297
308
  ### Turn the rendered node structure into the final rendered String.
298
309
  def to_s
299
- strings = @output.flatten.map( &:to_s )
300
-
301
- if enc = self.options[ :encoding ]
302
- self.log.debug "Encoding rendered template parts to %s" % [ enc ]
303
- strings.map! {|str| str.encode(enc, invalid: :replace, undef: :replace) }
304
- end
305
-
306
- return strings.join
310
+ return self.stringify_nodes( @output )
307
311
  end
308
312
 
309
313
 
@@ -337,6 +341,23 @@ class Inversion::RenderState
337
341
  end
338
342
 
339
343
 
344
+ ### Add one or more rendered +nodes+ to the state as a reusable fragment associated
345
+ ### with the specified +name+.
346
+ def add_fragment( name, *nodes )
347
+ nodes.flatten!
348
+ self.fragments[ name.to_sym ] = nodes
349
+ self.scope.__fragments__[ name.to_sym ] = nodes
350
+ end
351
+
352
+
353
+ ### Return the current fragments Hash rendered as Strings.
354
+ def rendered_fragments
355
+ return self.fragments.each_with_object( {} ) do |(key, nodes), accum|
356
+ accum[ key ] = self.stringify_nodes( nodes )
357
+ end
358
+ end
359
+
360
+
340
361
  ### Handle an +exception+ that was raised while appending a node by calling the
341
362
  ### #errhandler.
342
363
  def handle_render_error( node, exception )
@@ -454,6 +475,20 @@ class Inversion::RenderState
454
475
  end
455
476
 
456
477
 
478
+ ### Return the given +nodes+ as a String in the configured encoding.
479
+ def stringify_nodes( nodes )
480
+ self.log.debug "Rendering nodes: %p" % [ nodes ]
481
+ strings = nodes.flatten.map( &:to_s )
482
+
483
+ if enc = self.options[ :encoding ]
484
+ self.log.debug "Encoding rendered template parts to %s" % [ enc ]
485
+ strings.map! {|str| str.encode(enc, invalid: :replace, undef: :replace) }
486
+ end
487
+
488
+ return strings.join
489
+ end
490
+
491
+
457
492
  ### Handle attribute methods.
458
493
  def method_missing( sym, *args, &block )
459
494
  return super unless sym.to_s =~ /^\w+$/
@@ -13,9 +13,76 @@ rescue LoadError
13
13
  end
14
14
 
15
15
 
16
- # The main template class. Instances of this class are created by parsing template
17
- # source and combining the resulting node tree with a set of attributes that
18
- # can be used to populate it when rendered.
16
+ # The main template class.
17
+ #
18
+ # Inversion templates are the primary objects you'll be interacting with. Templates
19
+ # can be created from a string:
20
+ #
21
+ # Inversion::Template.new( template_source )
22
+ #
23
+ # or from a file:
24
+ #
25
+ # Inversion::Template.load( 'path/to/template.tmpl' )
26
+ #
27
+ #
28
+ # == Template Options
29
+ #
30
+ # Inversion supports the {Configurability}[http://rubygems.org/gems/configurability]
31
+ # API, and registers itself with the +templates+ key. This means you can either add
32
+ # a +templates+ section to your Configurability config, or call
33
+ # ::configure yourself with a config Hash (or something that quacks like one).
34
+ #
35
+ # To set options on a per-template basis, you can pass an options hash to either
36
+ # Inversion::Template::load or Inversion::Template::new, or set them from within the template
37
+ # itself using the {config tag}[rdoc-ref:Tags@config].
38
+ #
39
+ # The available options are:
40
+ #
41
+ # [:ignore_unknown_tags]
42
+ # Setting to false causes unknown tags used in templates to raise an
43
+ # Inversion::ParseError. Defaults to +true+.
44
+ #
45
+ # [:on_render_error]
46
+ # Dictates the behavior of exceptions during rendering. Defaults to +:comment+.
47
+ #
48
+ # [:ignore]
49
+ # Exceptions are silently ignored.
50
+ # [:comment]
51
+ # Exceptions are rendered inline as comments.
52
+ # [:propagate]
53
+ # Exceptions bubble up to the caller of Inversion::Template#render.
54
+ #
55
+ #
56
+ # [:debugging_comments]
57
+ # Insert various Inversion parse and render statements while rendering. Defaults to +false+.
58
+ #
59
+ # [:comment_start]
60
+ # When rendering debugging comments, the comment is started with these characters.
61
+ # Defaults to <code>"<!--"</code>.
62
+ #
63
+ # [:comment_end]
64
+ # When rendering debugging comments, the comment is finished with these characters.
65
+ # Defaults to <code>"-->"</code>.
66
+ #
67
+ # [:template_paths]
68
+ # An array of filesystem paths to search for templates within, when loaded or
69
+ # included with a relative path. The current working directory is always the
70
+ # last checked member of this. Defaults to <code>[]</code>.
71
+ #
72
+ # [:escape_format]
73
+ # The escaping used by tags such as +escape+ and +pp+. Default: +:html+.
74
+ #
75
+ # [:strip_tag_lines]
76
+ # If a tag's presence introduces a blank line into the output, this option
77
+ # removes it. Defaults to +true+.
78
+ #
79
+ # [:stat_delay]
80
+ # Templates know when they've been altered on disk, and can dynamically
81
+ # reload themselves in long running applications. Setting this option creates
82
+ # a purposeful delay between reloads for busy servers. Defaults to +0+
83
+ # (disabled).
84
+ #
85
+ #
19
86
  class Inversion::Template
20
87
  extend Loggability
21
88
  include Inversion::DataUtilities
@@ -65,13 +132,15 @@ class Inversion::Template
65
132
  }.freeze
66
133
 
67
134
 
68
- ### Global config
69
- @config = DEFAULT_CONFIG.dup
135
+ ##
136
+ # Global config
70
137
  class << self; attr_accessor :config; end
138
+ self.config = DEFAULT_CONFIG.dup
71
139
 
140
+ ##
72
141
  # Global template search path
73
- @template_paths = []
74
142
  class << self; attr_accessor :template_paths; end
143
+ self.template_paths = []
75
144
 
76
145
 
77
146
  ### Configure the templating system.
@@ -145,8 +214,9 @@ class Inversion::Template
145
214
 
146
215
  @source = source
147
216
  @node_tree = [] # Parser expects this to always be an Array
148
- @options = opts
217
+ @options = self.class.config.merge( opts )
149
218
  @attributes = {}
219
+ @fragments = {}
150
220
  @source_file = nil
151
221
  @created_at = Time.now
152
222
  @last_checked = @created_at
@@ -159,6 +229,7 @@ class Inversion::Template
159
229
  def initialize_copy( other )
160
230
  @options = deep_copy( other.options )
161
231
  @attributes = deep_copy( other.attributes )
232
+ @fragments = deep_copy( other.fragments )
162
233
  end
163
234
 
164
235
 
@@ -175,6 +246,9 @@ class Inversion::Template
175
246
  # The Hash of template attributes
176
247
  attr_reader :attributes
177
248
 
249
+ # The Hash of rendered template fragments
250
+ attr_reader :fragments
251
+
178
252
  # The Template's configuration options hash
179
253
  attr_reader :options
180
254
 
@@ -216,6 +290,8 @@ class Inversion::Template
216
290
  opts = self.options
217
291
  opts.merge!( parentstate.options ) if parentstate
218
292
 
293
+ self.fragments.clear
294
+
219
295
  state = Inversion::RenderState.new( parentstate, self.attributes, opts, &block )
220
296
 
221
297
  # self.log.debug " rendering node tree: %p" % [ @node_tree ]
@@ -223,6 +299,8 @@ class Inversion::Template
223
299
  self.log.info " done rendering template 0x%08x: %0.4fs" %
224
300
  [ self.object_id/2, state.time_elapsed ]
225
301
 
302
+ self.fragments.replace( state.rendered_fragments )
303
+
226
304
  return state.to_s
227
305
  end
228
306
  alias_method :to_s, :render
@@ -82,7 +82,7 @@ class Inversion::Template::AttrTag < Inversion::Template::CodeTag
82
82
  value = self.evaluate( renderstate ) # :FIXME: or return value # nil or false?
83
83
 
84
84
  # Apply the format if there is one
85
- if self.format
85
+ if self.format && value
86
86
  return self.format % value
87
87
  else
88
88
  return value
@@ -20,15 +20,15 @@ require 'inversion/template/rescuetag'
20
20
  # <?begin ?><?call employees.length ?><?end?>
21
21
  #
22
22
  # <?begin ?>
23
- # <?for employee in employees.all ?>
24
- # <?attr employee.name ?> --> <?attr employee.title ?>
25
- # <?end for?>
23
+ # <?for employee in employees.all ?>
24
+ # <?attr employee.name ?> --> <?attr employee.title ?>
25
+ # <?end for?>
26
26
  # <?rescue DatabaseError => err ?>
27
- # Oh no!! I can't talk to the database for some reason. The
28
- # error was as follows:
29
- # <pre>
30
- # <?attr err.message ?>
31
- # </pre>
27
+ # Oh no!! I can't talk to the database for some reason. The
28
+ # error was as follows:
29
+ # <pre>
30
+ # <?attr err.message ?>
31
+ # </pre>
32
32
  # <?end?>
33
33
  #
34
34
  class Inversion::Template::BeginTag < Inversion::Template::Tag
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set noet nosta sw=4 ts=4 :
3
+
4
+ require 'inversion/template/tag'
5
+ require 'inversion/template/containertag'
6
+
7
+
8
+ # Inversion 'fragment' tag.
9
+ #
10
+ # This tag provides a way to generate a fragment of content once in a template
11
+ # as an attribute, and then reuse it later either in the same template or even
12
+ # outside of it.
13
+ #
14
+ # == Syntax
15
+ #
16
+ # <?fragment subject ?>Receipt for Order #<?call order.number ?><?end subject ?>
17
+ #
18
+ class Inversion::Template::FragmentTag < Inversion::Template::Tag
19
+ include Inversion::Template::ContainerTag
20
+
21
+
22
+ ### Create a new FragmentTag with the given +body+.
23
+ def initialize( body, line=nil, column=nil )
24
+ super
25
+
26
+ key = self.body[ /^([a-z]\w+)$/ ] or
27
+ raise Inversion::ParseError,
28
+ "malformed key: expected simple identifier, got %p" % [ self.body ]
29
+ @key = key.to_sym
30
+ end
31
+
32
+
33
+ ######
34
+ public
35
+ ######
36
+
37
+ # The fragment key; corresponds to the name of the attribute that will be set
38
+ # by the rendered contents of the fragment.
39
+ attr_reader :key
40
+
41
+
42
+ ### Render the fragment and store it as an attribute.
43
+ def render( renderstate )
44
+ self.log.debug "Publishing %d nodes as %s" % [ self.subnodes.length, self.key ]
45
+ rendered_nodes = []
46
+ renderstate.with_destination( rendered_nodes ) do
47
+ sn = self.render_subnodes( renderstate )
48
+ self.log.debug " subnodes are: %p" % [ sn ]
49
+ sn
50
+ end
51
+
52
+ self.log.debug " rendered nodes are: %p" % [ rendered_nodes ]
53
+ renderstate.add_fragment( self.key, rendered_nodes )
54
+
55
+ return nil
56
+ end
57
+
58
+
59
+ end # class Inversion::Template::FragmentTag
60
+
@@ -42,7 +42,7 @@ class Inversion::Template::RescueTag < Inversion::Template::Tag
42
42
 
43
43
 
44
44
  ### Parsing callback -- check to be sure the node tree can have the
45
- ### 'else' tag appended to it.
45
+ ### 'rescue' tag appended to it.
46
46
  def before_appending( parsestate )
47
47
  condtag = parsestate.node_stack.reverse.find do |node|
48
48
  case node
@@ -1,40 +1,18 @@
1
1
  #!/usr/bin/ruby
2
2
  # coding: utf-8
3
3
 
4
- BEGIN {
5
- require 'pathname'
6
- basedir = Pathname.new( __FILE__ ).dirname.parent
7
-
8
- libdir = basedir + "lib"
9
-
10
- $LOAD_PATH.unshift( libdir.to_s ) unless $LOAD_PATH.include?( libdir.to_s )
11
- }
12
-
13
4
  # SimpleCov test coverage reporting; enable this using the :coverage rake task
14
- if ENV['COVERAGE']
15
- require 'simplecov'
16
- SimpleCov.start do
17
- add_filter 'spec'
18
- add_group "Tags" do |file|
19
- file.filename =~ /tag.rb$/
20
- end
21
- add_group "Needing tests" do |file|
22
- file.covered_percent < 90
23
- end
24
- end
25
- end
5
+ require 'simplecov' if ENV['COVERAGE']
26
6
 
27
7
  require 'rspec'
28
8
  require 'loggability'
29
9
  require 'loggability/spechelpers'
30
10
 
31
11
  require 'inversion'
32
- require 'spec/lib/constants'
33
12
 
34
13
 
35
14
  ### RSpec helper functions.
36
15
  module Inversion::SpecHelpers
37
- include Inversion::TestConstants
38
16
 
39
17
  ###############
40
18
  module_function
@@ -52,22 +30,21 @@ module Inversion::SpecHelpers
52
30
  return "<?#{name} #{data} ?>"
53
31
  end
54
32
 
55
-
56
33
  end
57
34
 
58
35
 
59
36
  ### Mock with RSpec
60
37
  RSpec.configure do |c|
61
- include Inversion::TestConstants
62
- include Loggability::SpecHelpers
63
38
 
64
- c.mock_with :rspec
39
+ c.run_all_when_everything_filtered = true
40
+ c.filter_run :focus
41
+ c.order = 'random'
42
+ c.mock_with( :rspec ) do |mock|
43
+ mock.syntax = :expect
44
+ end
65
45
 
66
46
  c.include( Inversion::SpecHelpers )
67
47
  c.include( Loggability::SpecHelpers )
68
-
69
- c.filter_run_excluding( :ruby_1_9_only => true ) if
70
- Inversion::SpecHelpers.vvec( RUBY_VERSION ) < Inversion::SpecHelpers.vvec('1.9.0')
71
48
  end
72
49
 
73
50
  # vim: set nosta noet ts=4 sw=4: