masterview 0.1.5 → 0.2.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 (105) hide show
  1. data/CHANGELOG +17 -0
  2. data/README +36 -504
  3. data/RELEASE_NOTES +126 -45
  4. data/Rakefile +215 -86
  5. data/TODO +8 -3
  6. data/doc/configuration.html +485 -0
  7. data/doc/directives.html +1085 -0
  8. data/doc/guide.html +243 -0
  9. data/doc/index.html +287 -0
  10. data/doc/installation.html +376 -0
  11. data/doc/stylesheets/masterview.css +206 -0
  12. data/doc/stylesheets/mv-config.css +23 -0
  13. data/doc/stylesheets/mv-directives.css +18 -0
  14. data/doc/stylesheets/mv-installation.css +10 -0
  15. data/doc/troubleshooting.html +18 -0
  16. data/examples/product.html +256 -0
  17. data/examples/product.html.old +107 -0
  18. data/examples/rails_app_config/masterview/environment/development.rb +22 -0
  19. data/examples/rails_app_config/masterview/environment/production.rb +9 -0
  20. data/examples/rails_app_config/masterview/settings.rb +59 -0
  21. data/examples/test.import +80 -0
  22. data/init.rb +26 -12
  23. data/lib/masterview/analyzer.rb +25 -15
  24. data/lib/masterview/directive_base.rb +4 -0
  25. data/lib/masterview/directive_helpers.rb +7 -5
  26. data/lib/masterview/directives/import_render.rb +6 -0
  27. data/lib/masterview/directives/insert_generated_comment.rb +8 -8
  28. data/lib/masterview/extras/app/controllers/masterview_controller.rb +154 -2
  29. data/lib/masterview/extras/app/views/masterview/admin/create.rhtml +4 -4
  30. data/lib/masterview/extras/app/views/masterview/admin/empty.rhtml +1 -1
  31. data/lib/masterview/extras/app/views/masterview/admin/list.rhtml +14 -9
  32. data/lib/masterview/extras/app/views/masterview/admin/view_rhtml.rhtml +70 -0
  33. data/lib/masterview/extras/init_logger.rb +102 -0
  34. data/lib/masterview/extras/init_rails_erb_mv_direct.rb +117 -0
  35. data/lib/masterview/extras/init_rails_reparse_checking.rb +59 -0
  36. data/lib/masterview/extras/watcher.rb +17 -23
  37. data/lib/masterview/filter_helpers.rb +26 -0
  38. data/lib/masterview/initializer.rb +912 -0
  39. data/lib/masterview/io.rb +352 -0
  40. data/lib/masterview/keyword_expander.rb +116 -0
  41. data/lib/masterview/masterview_version.rb +2 -2
  42. data/lib/masterview/mtime_tracking_hash.rb +44 -0
  43. data/lib/masterview/parser.rb +64 -92
  44. data/lib/masterview/pathname_extensions.rb +33 -0
  45. data/lib/masterview/template_spec.rb +49 -56
  46. data/lib/masterview.rb +40 -85
  47. data/test/fixtures/configs/fake_rails_app_with_config/config/masterview/environments/development.rb +12 -0
  48. data/test/fixtures/configs/fake_rails_app_with_config/config/masterview/environments/production.rb +11 -0
  49. data/test/fixtures/configs/fake_rails_app_with_config/config/masterview/settings.rb +23 -0
  50. data/test/fixtures/templates/product.html +256 -0
  51. data/test/fixtures/templates/test.import +80 -0
  52. data/test/test_helper.rb +5 -3
  53. data/test/tmp/template/foo.txt +1 -0
  54. data/test/tmp/templates_src/product.html +256 -0
  55. data/test/tmp/views/layouts/product.rhtml +35 -0
  56. data/test/tmp/views/product/_form.rhtml +30 -0
  57. data/test/tmp/views/product/_product.rhtml +14 -0
  58. data/test/tmp/views/product/_show.rhtml +27 -0
  59. data/test/tmp/views/product/destroy.rhtml +27 -0
  60. data/test/tmp/views/product/edit.rhtml +26 -0
  61. data/test/tmp/views/product/list.rhtml +31 -0
  62. data/test/tmp/views/product/new.rhtml +29 -0
  63. data/test/tmp/views/product/show.rhtml +16 -0
  64. data/test/unit/config_settings_test.rb +172 -0
  65. data/test/{attr_test.rb → unit/directive_attr_test.rb} +2 -2
  66. data/test/{block_test.rb → unit/directive_block_test.rb} +2 -2
  67. data/test/{content_test.rb → unit/directive_content_test.rb} +2 -2
  68. data/test/{else_test.rb → unit/directive_else_test.rb} +2 -2
  69. data/test/{elsif_test.rb → unit/directive_elsif_test.rb} +2 -2
  70. data/test/{form_test.rb → unit/directive_form_test.rb} +2 -2
  71. data/test/{global_inline_erb_test.rb → unit/directive_global_inline_erb_test.rb} +2 -2
  72. data/test/{hidden_field_test.rb → unit/directive_hidden_field_test.rb} +2 -2
  73. data/test/{if_test.rb → unit/directive_if_test.rb} +2 -2
  74. data/test/unit/directive_import_render_test.rb +62 -0
  75. data/test/{import_test.rb → unit/directive_import_test.rb} +2 -2
  76. data/test/{javascript_include_test.rb → unit/directive_javascript_include_test.rb} +2 -2
  77. data/test/{link_to_if_test.rb → unit/directive_link_to_if_test.rb} +2 -2
  78. data/test/{link_to_test.rb → unit/directive_link_to_test.rb} +2 -2
  79. data/test/{omit_tag_test.rb → unit/directive_omit_tag_test.rb} +2 -2
  80. data/test/{password_field_test.rb → unit/directive_password_field_test.rb} +2 -2
  81. data/test/{replace_test.rb → unit/directive_replace_test.rb} +2 -2
  82. data/test/{stylesheet_link_test.rb → unit/directive_stylesheet_link_test.rb} +2 -2
  83. data/test/{submit_test.rb → unit/directive_submit_test.rb} +2 -2
  84. data/test/{text_area_test.rb → unit/directive_text_area_test.rb} +2 -2
  85. data/test/{text_field_test.rb → unit/directive_text_field_test.rb} +2 -2
  86. data/test/{example_test.rb → unit/example_test.rb} +1 -1
  87. data/test/unit/file_mio_test.rb +368 -0
  88. data/test/{filter_helpers_test.rb → unit/filter_helpers_test.rb} +1 -1
  89. data/test/unit/keyword_expander_test.rb +95 -0
  90. data/test/unit/mio_test.rb +110 -0
  91. data/test/unit/mtime_string_hash_mio_tree_test.rb +289 -0
  92. data/test/unit/mtime_tracking_hash_test.rb +38 -0
  93. data/test/{parser_test.rb → unit/parser_test.rb} +19 -1
  94. data/test/unit/pathname_extensions_test.rb +46 -0
  95. data/test/{run_parser_test.rb → unit/run_parser_test.rb} +7 -3
  96. data/test/unit/string_hash_mio_test.rb +320 -0
  97. data/test/unit/template_file_watcher_test.rb +107 -0
  98. data/test/{template_spec_test.rb → unit/template_spec_test.rb} +57 -21
  99. data/test/{template_test.rb → unit/template_test.rb} +123 -22
  100. data/test/xtras/config-mv-logger_config.rb +109 -0
  101. data/test/xtras/config_initialize_standalone.rb +53 -0
  102. metadata +111 -38
  103. data/lib/masterview/extras/rails_init.rb +0 -72
  104. data/test/import_render_test.rb +0 -30
  105. data/test/template_file_watcher_test.rb +0 -50
@@ -0,0 +1,352 @@
1
+ require 'ostruct'
2
+ require 'pathname'
3
+ require 'stringio'
4
+ require File.join( File.dirname(__FILE__), 'pathname_extensions' )
5
+ require File.join( File.dirname(__FILE__), 'mtime_tracking_hash' )
6
+ require File.join( File.dirname(__FILE__), '../facets/core/string/starts_with' )
7
+ require File.join( File.dirname(__FILE__), 'filter_helpers' )
8
+
9
+ module MasterView
10
+ # IOManager which retrieves the proper MasterViewIOTree object for the type of object being requested
11
+ # set and access MasterViewIOTree by using accessors
12
+ # IOMgr.template = FileMIOTree()
13
+ # ftree = IOMgr.template
14
+ # IOMgr.erb = FileMIOTree()
15
+ # etree = IOMgr.erb
16
+ # Once the mioTree is obtained it may be used for obtaining mio objects for reading and writing
17
+ class MIOTrees < OpenStruct
18
+ end
19
+
20
+ # MIOTreeInterface
21
+ # path(path)
22
+ # find(options, &block) options[:path] = 'foo' limits find to dir foo
23
+ # cleanup_path_get_relative_pathname(path)
24
+
25
+ # MIOInterface
26
+ # pathname()
27
+ # full_pathname() # for FileMIO returns full_pathname otherwise for all others identical to pathname
28
+ # read(options={}) options[:disable_cache]=true bypasses cache filter, options[:disable_logging] = true turns off log of this event
29
+ # write(content=nil, options={}, &block) options[:disable_logging]=true turns off log of this event options[:force] = true forces the write even if identical
30
+ # exist?(path)
31
+ # identical?(content)
32
+ # mtime()
33
+
34
+
35
+ # mixin which encapsulates the apply filter to new MIO objects and
36
+ # the default filter block
37
+ module MasterViewIOTreeMixin
38
+ DefaultExtensionInstances = {}
39
+
40
+ # apply filters to object, use block if provided otherwise use
41
+ # default_mio_filter_block which is customized by options hash
42
+ # options[:escape_erb] = true enables erb escaping
43
+ # options[:tidy] = true enables tidy processing to cleanup bad xhtml
44
+ # options[:caching] = true enables caching so that reads are cached
45
+ # options[:logging] = true enables logging of reads and writes
46
+ def apply_filters(mio, options, block)
47
+ if block
48
+ block.call(mio)
49
+ else
50
+ default_mio_filter_block(options).call(mio)
51
+ end
52
+ mio
53
+ end
54
+
55
+ def default_mio_filter_block(options)
56
+ lambda do |mio|
57
+ mio.extend EscapeErbMIOFilter if options[:escape_erb]
58
+ mio.extend TidyMIOFilter if options[:tidy]
59
+ mio.extend CachingMIOFilter if options[:caching]
60
+ mio.extend ReadWriteLoggingMIOFilter if options[:logging]
61
+ end
62
+ end
63
+
64
+ def default_extension
65
+ DefaultExtensionInstances[object_id]
66
+ end
67
+
68
+ def default_extension=(default_extension)
69
+ DefaultExtensionInstances[object_id] = default_extension
70
+ end
71
+
72
+ # clean up ../.. check if path starts with root path and if so get relative, otherwise return path, root_path is only
73
+ # used in FileMIO, so all others just simply clean the path. FileMIO will override this with its own implementation.
74
+ def cleanup_path_get_relative_pathname(path)
75
+ pathname = Pathname.for_path(path)
76
+ pathname = Pathname.for_path(pathname.to_s+self.default_extension) if pathname.extname.empty? && self.default_extension && !self.default_extension.empty?
77
+ pathname
78
+ end
79
+ end
80
+
81
+ class FileMIOTree < Pathname
82
+ include MasterViewIOTreeMixin
83
+ def initialize(root_path=Pathname.getwd, default_extension=nil, options = {}, &block)
84
+ @options = options
85
+ self.default_extension = default_extension
86
+ super(Pathname.for_path(root_path).cleanpath)
87
+ @new_mio_block = block
88
+ end
89
+
90
+ def path(path)
91
+ raise InvalidPathError.new('Invalid path specified ('+path.to_s+'). Path is limited to directories under root_path ('+self.to_s+')') unless (self+path).expand_path.to_s.starts_with?(self.expand_path.to_s) #ensure sandbox
92
+ mio = FileMIO.new(path, self + path)
93
+ apply_filters(mio, @options, @new_mio_block)
94
+ end
95
+
96
+ # returns mio objects, if block_given then it yields passing the mio object to the block
97
+ def find(options = {}, &block)
98
+ path = options[:path] || '.'
99
+ found = []
100
+ working_path = Pathname.safe_concat(self, path)
101
+ working_path.find { |p| found << p unless p.directory? }
102
+ found = found.select { |f| f.basename.fnmatch?(options[:pattern]) } if options[:pattern]
103
+ found = found.collect{ |f| self.path(f.relative_path_from(self)) } # create mio objects
104
+ found = found.sort { |a,b| a.pathname.to_s <=> b.pathname.to_s }
105
+ found.each { |mio| yield mio } if block_given?
106
+ found
107
+ end
108
+
109
+ # clean up ../.. check if path starts with root path and if so get relative, otherwise return path
110
+ def cleanup_path_get_relative_pathname(path)
111
+ pathname = Pathname.for_path(path)
112
+ pathname = pathname.relative_path_from(self) if pathname.to_s.starts_with?(self.to_s)
113
+ pathname = Pathname.for_path(pathname.to_s+self.default_extension) if pathname.extname.empty? && self.default_extension && !self.default_extension.empty?
114
+ pathname
115
+ end
116
+ end
117
+
118
+ # MIO implemented as simple hash of path to string content
119
+ # This MIOTree does not keep track of mtimes and thus will always return new mtimes
120
+ class StringHashMIOTree
121
+ include MasterViewIOTreeMixin
122
+ attr_reader :string_hash
123
+ def initialize(string_hash = {}, default_extension=nil, options={}, &block)
124
+ @string_hash = string_hash
125
+ @options = options
126
+ self.default_extension = default_extension
127
+ @new_mio_block = block
128
+ end
129
+
130
+ def string_hash=(string_hash)
131
+ @string_hash = string_hash
132
+ end
133
+
134
+ def path(path)
135
+ clean_path = Pathname.for_path(path).cleanpath
136
+ mio = StringMIO.new(clean_path, @string_hash)
137
+ mio.pathname = clean_path
138
+ apply_filters(mio, @options, @new_mio_block)
139
+ end
140
+
141
+ def find(options = {})
142
+ path = options[:path] || ''
143
+ path = '' if path == '.'
144
+ found = []
145
+ @string_hash.each { |p,v| found << p if p.starts_with?(path) }
146
+ found = found.select { |p| Pathname.for_path(p).basename.fnmatch?(options[:pattern]) } if options[:pattern]
147
+ found.sort!
148
+ found = found.collect{ |p| self.path(p) } # create mio objects
149
+ found.each { |mio| yield mio } if block_given?
150
+ found
151
+ end
152
+
153
+ end
154
+
155
+ class ActiveRecordMIOTree #todo
156
+ include MasterViewIOTreeMixin
157
+ def initialize
158
+ self.default_extension = default_extension
159
+ end
160
+
161
+ def path(path)
162
+ end
163
+
164
+ def find(options, &block)
165
+ end
166
+ end
167
+
168
+ # MIOTree which works like StringHashMIO but uses MTimeTrackingHash to track mod times
169
+ class MTimeStringHashMIOTree < StringHashMIOTree
170
+
171
+ def initialize(default_extension=nil, options={}, &block)
172
+ super(MTimeTrackingHash.new, default_extension, options)
173
+ end
174
+
175
+ def path(path)
176
+ clean_path = Pathname.for_path(path).cleanpath
177
+ mio = MTimeStringMIO.new(clean_path, @string_hash)
178
+ mio.pathname = clean_path
179
+ apply_filters(mio, @options, @new_mio_block)
180
+ end
181
+
182
+ def string_hash=(mtime_tracking_hash)
183
+ raise "mtime_tracking_hash passed in does not respond_to mtime method call" unless mtime_tracking_hash.respond_to?(:mtime)
184
+ @string_hash = mtime_tracking_hash
185
+ end
186
+
187
+ end
188
+
189
+ # mio which allows skipping the intermediate erb file generation in rails.
190
+ # Current implementation inherits its functionality from MTimeStringHashMIOTree
191
+ class RailsErbCacheMIOTree < MTimeStringHashMIOTree
192
+ end
193
+
194
+
195
+ class MIOBase
196
+ attr_accessor :pathname
197
+
198
+ def initialize(path)
199
+ @pathname = Pathname.for_path(path)
200
+ end
201
+
202
+ # for mio objects other than FileMIO, full_pathname is same as pathname
203
+ def full_pathname
204
+ @pathname
205
+ end
206
+ end
207
+
208
+ module MasterViewIOIdenticalMixin
209
+ def identical?(compare_content)
210
+ self.exist? && compare_content == self.read(:disable_logging => true ) # eliminate the noise, simply checking if different
211
+ end
212
+ end
213
+
214
+
215
+ class FileMIO < Pathname
216
+ include MasterViewIOIdenticalMixin
217
+ attr_reader :pathname
218
+
219
+ def initialize(path, full_path)
220
+ super(full_path)
221
+ @pathname = Pathname.for_path(path)
222
+ end
223
+
224
+ def full_pathname
225
+ self
226
+ end
227
+
228
+ def read(options = {})
229
+ super()
230
+ end
231
+
232
+ def write(content = nil, options={}, &block)
233
+ written = false
234
+ if block_given?
235
+ sio = ::StringIO.new
236
+ yield sio
237
+ content = sio.string
238
+ end
239
+ if options[:force] || !identical?(content)
240
+ self.dirname.mkpath unless self.dirname.exist?
241
+ self.open('w') do |io|
242
+ io << content
243
+ end
244
+ written = true
245
+ end
246
+ written
247
+ end
248
+ end
249
+
250
+ class StringMIO < MIOBase
251
+ include MasterViewIOIdenticalMixin
252
+ attr_accessor :exist
253
+ attr_reader :mtime
254
+
255
+ def initialize(path, string_hash, mtime = Time.new)
256
+ super(path)
257
+ @string_hash = string_hash
258
+ @mtime = mtime
259
+ end
260
+
261
+ def read(options = {})
262
+ @string_hash[self.pathname.to_s]
263
+ end
264
+
265
+ def write(content = nil, options = {}, &block)
266
+ written = false
267
+ if block_given?
268
+ sio = ::StringIO.new
269
+ yield sio
270
+ content = sio.string
271
+ end
272
+
273
+ if options[:force] || !self.identical?(content)
274
+ @string_hash[self.pathname.to_s] = content
275
+ @mtime = Time.now
276
+ written = true
277
+ end
278
+ written
279
+ end
280
+
281
+ def exist?
282
+ @string_hash.has_key?(self.pathname.to_s)
283
+ end
284
+ end
285
+
286
+ # used by MTimeStringHashMIOTree to create MIO objects that check
287
+ # their mod time using the MTimeTrackingHash
288
+ class MTimeStringMIO < StringMIO
289
+ def initialize(path, mtime_tracking_hash)
290
+ super(path, mtime_tracking_hash, nil)
291
+ end
292
+
293
+ def mtime
294
+ @string_hash.mtime(self.pathname.to_s)
295
+ end
296
+ end
297
+
298
+
299
+ class ActiveRecordMIO < MIOBase
300
+ include MasterViewIOIdenticalMixin
301
+ def exist?; end
302
+ def mtime; end
303
+ end
304
+
305
+
306
+
307
+ module CachingMIOFilter #todo
308
+ def read(options={})
309
+ if options[:disable_cache]
310
+ super
311
+ else #caching on
312
+ super #todo obtain from cache
313
+ end
314
+ end
315
+ end
316
+
317
+ module TidyMIOFilter
318
+ def read(options={})
319
+ content = super
320
+ ::MasterView::TidyHelper.tidy(content)
321
+ end
322
+ end
323
+
324
+ module EscapeErbMIOFilter
325
+ def read(options={})
326
+ content = super
327
+ ::MasterView::EscapeErbHelper.escape_erb(content)
328
+ end
329
+ end
330
+
331
+ module ReadWriteLoggingMIOFilter #todo
332
+ def read(options={})
333
+ Log.debug { "reading file="+self.full_pathname } unless options[:disable_logging]
334
+ super
335
+ end
336
+
337
+ def write(content = nil, options = {}, &block)
338
+ Log.debug { "writing file="+self.full_pathname } unless options[:disable_logging]
339
+ super
340
+ end
341
+ end
342
+
343
+
344
+ # raised when an invalid path is given, or if the path is outside of the root_path
345
+ class InvalidPathError < RuntimeError
346
+ end
347
+
348
+ # raised when a method is not called with the appropriate arguments
349
+ class InvalidArgumentError < RuntimeError
350
+ end
351
+
352
+ end
@@ -0,0 +1,116 @@
1
+ module MasterView
2
+ # Keyword expander is used to hold variables defined to be used in expansion of
3
+ # attributes.
4
+ class KeywordExpander
5
+
6
+ #--
7
+ # keywords related to path information about the template being processed
8
+ #++
9
+ # relative path of the template being processed within the template src dir
10
+ # (does *not* include the template flie extension)
11
+ KW_TEMPLATE_PATH = '{template_path}'
12
+ # relative path of the directory containing the template being processed
13
+ # within the template src dir.
14
+ # ('' if template is directly contained in the templates src dir)
15
+ KW_TEMPLATE_DIR_PATH = '{template_dir_path}'
16
+ # base filename of the template being processed (no file extension)
17
+ KW_TEMPLATE_BASENAME = '{template_basename}'
18
+ # filename extension of the template being processed (ordinarily '.html')
19
+ KW_EXTENSION = '{extension}'
20
+ # the default extension for generated output (ordinarily '.rhthml')
21
+ KW_DEFAULT_EXTENSION = '{default_extension}'
22
+ # list of all supported keywords related to the template being processed
23
+ TEMPLATE_KEYWORDS = [ KW_TEMPLATE_PATH, KW_TEMPLATE_DIR_PATH, KW_TEMPLATE_BASENAME, KW_EXTENSION ]
24
+
25
+ def initialize(hash = {})
26
+ @hash = hash
27
+ build_key_value_array_sorted_by_desc_length
28
+ end
29
+
30
+ # Sets a variety of path related keywords:
31
+ #
32
+ # MasterView supports the following keyword expansions for
33
+ # mv:generate, mv:gen_partial, mv:import, and mv:import_render
34
+ #
35
+ #--
36
+ # The default generated output extension is MasterView::GeneratedFileDefaultExtension,
37
+ # per the masterview config.output_filename_extension setting.
38
+ #++
39
+ #
40
+ # For template file <code>one/foo/bar.html</code> when default generated output extension '.rhtml':
41
+ # {template_path} == use original masterview template path with default output extension (one/foo/bar.rhtml)
42
+ # {template_path}.ext == use original masterview template path with the specified extension (one/foo/bar.ext)
43
+ # {template_dir_path} == direct_parent_dirname (one/foo)
44
+ # {template_basename} == basename (bar)
45
+ # {extension} == extension (.html)
46
+ #
47
+ # For example:
48
+ # with MasterView template file <code>one/two/three.html</code> and default generated output extension '.rhtml'
49
+ # mv:generate="{template_path}" expands to mv:generate="one/two/three.rhtml" (default generation output extension used)
50
+ # mv:generate="{template_path}.rcss" expands to mv:generate="one/two/three.rcss"
51
+ # mv:generate="{template_dir_path}/{template_basename}" expands to mv:generate="one/two/three" (no extension)
52
+ # mv:generate="{template_dir_path}/{template_basename}.rcss" expands to mv:generate="one/two/three.rcss"
53
+ # mv:generate="{template_dir_path}/{template_basename}-bar.rtxt" expands to mv:generate="one/two/three-bar.rtxt"
54
+ # mv:generate="somewhere/{template_basename}-bar{extension}" expands to mv:generate="somewhere/three-bar.html"
55
+ #
56
+ # Note: All path values use forward slashes
57
+ #
58
+ def set_template_pathname(template_pathname, default_erb_extension)
59
+ return if template_pathname.nil?
60
+ pn = Pathname.for_path(template_pathname)
61
+ default_erb_extension = '' if default_erb_extension.nil?
62
+ @hash[KW_TEMPLATE_PATH] = convert_pathname_to_s(pn.path_no_ext) #defer decision: + default_erb_extension
63
+ @hash[KW_TEMPLATE_DIR_PATH] = convert_pathname_to_s(pn.dirname)
64
+ @hash[KW_TEMPLATE_BASENAME] = convert_pathname_to_s(pn.basename(pn.extname))
65
+ @hash[KW_EXTENSION] = pn.extname
66
+ @hash[KW_DEFAULT_EXTENSION] = default_erb_extension
67
+ build_key_value_array_sorted_by_desc_length
68
+ end
69
+
70
+ # set the value, regenerate a reverse sorted hash value array so that it will
71
+ # replace the longer replacement keys before the shorter ones, so FOO_BAR will be
72
+ # replaced before FOO
73
+ def set(key, value)
74
+ @hash[key] = value
75
+ build_key_value_array_sorted_by_desc_length
76
+ end
77
+
78
+ def [](key)
79
+ @hash[key]
80
+ end
81
+
82
+ # expand all keywords, return value
83
+ def expand_keywords(str)
84
+ # maybe consider: add parm check_template_path=false that mv:generate processor would turn on
85
+ # so that ext. defaulting cleverness only applies in that specific context.
86
+ # Might be overaggressive (or just unnecessary) to this this on *all* substitution mappings
87
+ # [DJL 18-Jun-2006]
88
+ return nil if str.nil?
89
+ append_default_ext = str.ends_with?(KW_TEMPLATE_PATH) and @hash.has_key?(KW_DEFAULT_EXTENSION)
90
+ val = str.clone
91
+ @hash_values_by_desc_key_length.each { |k,v| val.gsub!(k,v) }
92
+ val << @hash[KW_DEFAULT_EXTENSION] if append_default_ext
93
+ val
94
+ end
95
+
96
+ # finds the attribute using the key deleting it from hash, expands value using current binding
97
+ # which allows KEYWORD substitutions before returning string, return nil if not found
98
+ def resolveAttrAndDelete(attributes, key)
99
+ attr_value = attributes.delete(key)
100
+ attr_value = expand_keywords(attr_value) unless attr_value.nil?
101
+ attr_value
102
+ end
103
+
104
+ private
105
+
106
+ # convert pathname to string, for '.' we want empty string
107
+ def convert_pathname_to_s(pathname)
108
+ (pathname.to_s == '.') ? '' : pathname.to_s
109
+ end
110
+
111
+ def build_key_value_array_sorted_by_desc_length
112
+ @hash_values_by_desc_key_length = @hash.sort { |a,b| b[0].length <=> a[0].length } # longest keys first
113
+ end
114
+ end
115
+ end
116
+
@@ -1,8 +1,8 @@
1
1
  module MasterView
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 1
5
- TINY = 5
4
+ MINOR = 2
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,44 @@
1
+ module MasterView
2
+
3
+ # this hash is designed to track modified times of its values
4
+ # this behaviour is only supported through using []= and store
5
+ # mechanisms and thus default semantics have been excluded
6
+ # other update methods such as merge! have been excluded from
7
+ # this implementation and will raise errors
8
+ class MTimeTrackingHash < Hash
9
+
10
+ # MTimeTrackingHash does not support defaults and thus does
11
+ # not take default object or block
12
+ def initialize
13
+ super
14
+ @mtimes = {}
15
+ end
16
+
17
+ def []=(key, value)
18
+ update_mtime(key)
19
+ super
20
+ end
21
+
22
+ def store(key, value)
23
+ update_mtime(key)
24
+ super
25
+ end
26
+
27
+ # returns the Time object for this key
28
+ def mtime(key)
29
+ @mtimes[key]
30
+ end
31
+
32
+ private
33
+
34
+ def update_mtime(key)
35
+ @mtimes[key] = Time.now
36
+ end
37
+
38
+ def default=(obj); raise "Not implemented"; end
39
+ def merge!(obj,&block); raise "Not implemented"; end
40
+ def reject!(obj,&block); raise "Not implemented"; end
41
+ def replace(obj); raise "Not implemented"; end
42
+
43
+ end
44
+ end