masterview 0.0.2

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/CHANGELOG +1 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README +574 -0
  4. data/Rakefile +149 -0
  5. data/generators/masterview/USAGE +32 -0
  6. data/generators/masterview/masterview_generator.rb +277 -0
  7. data/generators/masterview/templates/controller.rb +50 -0
  8. data/generators/masterview/templates/fields_scaffold.rhtml +1 -0
  9. data/generators/masterview/templates/form_scaffold.rhtml +3 -0
  10. data/generators/masterview/templates/functional_test.rb +83 -0
  11. data/generators/masterview/templates/helper.rb +2 -0
  12. data/generators/masterview/templates/layout.rhtml +11 -0
  13. data/generators/masterview/templates/list_head_scaffold.rhtml +4 -0
  14. data/generators/masterview/templates/list_line_scaffold.rhtml +6 -0
  15. data/generators/masterview/templates/masterview.rhtml +182 -0
  16. data/generators/masterview/templates/mvpreview.js +31 -0
  17. data/generators/masterview/templates/semantic.cache +84 -0
  18. data/generators/masterview/templates/show_scaffold.rhtml +3 -0
  19. data/generators/masterview/templates/style.css +61 -0
  20. data/generators/masterview_gem_plugin/masterview_gem_plugin_generator.rb +21 -0
  21. data/generators/masterview_gem_plugin/templates/init.rb +43 -0
  22. data/init.rb +43 -0
  23. data/lib/masterview/directive_base.rb +163 -0
  24. data/lib/masterview/directive_helpers.rb +176 -0
  25. data/lib/masterview/directives/block.rb +30 -0
  26. data/lib/masterview/directives/content.rb +10 -0
  27. data/lib/masterview/directives/else.rb +25 -0
  28. data/lib/masterview/directives/elsif.rb +26 -0
  29. data/lib/masterview/directives/form.rb +19 -0
  30. data/lib/masterview/directives/global_inline_erb.rb +39 -0
  31. data/lib/masterview/directives/hidden_field.rb +31 -0
  32. data/lib/masterview/directives/if.rb +24 -0
  33. data/lib/masterview/directives/insert_generated_comment.rb +30 -0
  34. data/lib/masterview/directives/javascript_include.rb +15 -0
  35. data/lib/masterview/directives/link_to.rb +17 -0
  36. data/lib/masterview/directives/link_to_if.rb +21 -0
  37. data/lib/masterview/directives/link_to_remote.rb +17 -0
  38. data/lib/masterview/directives/password_field.rb +33 -0
  39. data/lib/masterview/directives/preview.rb +10 -0
  40. data/lib/masterview/directives/replace.rb +18 -0
  41. data/lib/masterview/directives/stylesheet_link.rb +14 -0
  42. data/lib/masterview/directives/submit.rb +14 -0
  43. data/lib/masterview/directives/testfilter.rb +55 -0
  44. data/lib/masterview/directives/text_area.rb +34 -0
  45. data/lib/masterview/directives/text_field.rb +33 -0
  46. data/lib/masterview/extras/rails_init.rb +67 -0
  47. data/lib/masterview/extras/watcher.rb +30 -0
  48. data/lib/masterview/masterview_version.rb +9 -0
  49. data/lib/masterview/parser.rb +585 -0
  50. data/lib/masterview/plugin_load_tracking.rb +41 -0
  51. data/lib/masterview/runtime_helpers.rb +9 -0
  52. data/lib/masterview/string_extensions.rb +15 -0
  53. data/lib/masterview.rb +130 -0
  54. data/test/block_test.rb +47 -0
  55. data/test/content_test.rb +26 -0
  56. data/test/else_test.rb +31 -0
  57. data/test/elsif_test.rb +31 -0
  58. data/test/example_test.rb +11 -0
  59. data/test/filter_helpers_test.rb +142 -0
  60. data/test/form_test.rb +66 -0
  61. data/test/global_inline_erb_test.rb +30 -0
  62. data/test/hidden_field_test.rb +62 -0
  63. data/test/if_test.rb +23 -0
  64. data/test/javascript_include_test.rb +26 -0
  65. data/test/link_to_if_test.rb +27 -0
  66. data/test/link_to_test.rb +52 -0
  67. data/test/parser_test.rb +166 -0
  68. data/test/password_field_test.rb +89 -0
  69. data/test/replace_test.rb +27 -0
  70. data/test/run_parser_test.rb +27 -0
  71. data/test/stylesheet_link_test.rb +26 -0
  72. data/test/submit_test.rb +54 -0
  73. data/test/template_file_watcher_test.rb +50 -0
  74. data/test/template_test.rb +181 -0
  75. data/test/test_helper.rb +24 -0
  76. data/test/text_area_test.rb +81 -0
  77. data/test/text_field_test.rb +89 -0
  78. metadata +136 -0
@@ -0,0 +1,585 @@
1
+ module MasterView
2
+
3
+ class RenderLevel #contains render modes, each gen level
4
+ attr_accessor :render_modes
5
+
6
+ def initialize(render_modes = [])
7
+ @render_modes = render_modes
8
+ end
9
+
10
+ def push(render_mode)
11
+ @render_modes.push render_mode
12
+ end
13
+ end
14
+
15
+ class RenderMode #contains tags and output, each output style
16
+ attr_accessor :output, :tags, :mode_type
17
+ def initialize(output = nil, mode_type = :normal)
18
+ @output = output
19
+ @tags = []
20
+ @mode_type = mode_type
21
+ end
22
+
23
+ def tag
24
+ @tags.last
25
+ end
26
+
27
+ def directives
28
+ @tags.last.directives
29
+ end
30
+
31
+ def render_directives(method_name, context=tag.create_context)
32
+ dcs = DirectiveCallStack.new
33
+ tag_name = tag.tag_name.downcase
34
+ depth = tags.size-1
35
+ @tags.each do |tag|
36
+ case depth
37
+ when 0
38
+ dcs << tag.directives.determine_dcs(method_name)
39
+ when 1
40
+ dcs << tag.directives.determine_dcs(determine_method(:child, tag_name, method_name))
41
+ dcs << tag.directives.determine_dcs(determine_method(:child, :any, method_name))
42
+ dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name))
43
+ dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name))
44
+ else
45
+ dcs << tag.directives.determine_dcs(determine_method(:descendant, tag_name, method_name))
46
+ dcs << tag.directives.determine_dcs(determine_method(:descendant, :any, method_name))
47
+ end
48
+ depth -= 1
49
+ end
50
+ dcs.context = context
51
+ dcs.render
52
+ end
53
+
54
+ def determine_method(modifier, tag_name, base)
55
+ type = "#{modifier.to_s}_#{tag_name.to_s}_#{base.to_s}"
56
+ end
57
+ end
58
+
59
+ class DirectiveCallStack
60
+ attr_reader :directives_to_call
61
+ attr_accessor :context
62
+
63
+ def initialize
64
+ @directives_to_call = []
65
+ end
66
+
67
+ def <<(directive_calls)
68
+ if directive_calls.is_a? DirectiveCallStack
69
+ @directives_to_call << directive_calls.directives_to_call
70
+ else
71
+ @directives_to_call << directive_calls
72
+ end
73
+ @directives_to_call.flatten!
74
+ self
75
+ end
76
+
77
+ def render
78
+ return [] if @directives_to_call.empty?
79
+ directive_proc = @directives_to_call.shift
80
+ directive_proc.call(self)
81
+ end
82
+ end
83
+
84
+ class DirectiveSet
85
+ attr_accessor :directives
86
+ def initialize
87
+ @directives = []
88
+ end
89
+
90
+ def <<(directive)
91
+ @directives << directive
92
+ @directives.flatten!
93
+ self
94
+ end
95
+
96
+ def determine_dcs(method_name)
97
+ method_name_sym = method_name.to_sym
98
+ dcs = DirectiveCallStack.new
99
+ @directives.each do |directive|
100
+ if directive.respond_to? method_name_sym
101
+ dcs << create_call_proc(directive, method_name_sym)
102
+ end
103
+ end
104
+ dcs
105
+ end
106
+
107
+ def create_call_proc(directive, method_name_sym)
108
+ lambda do |dcs|
109
+ directive.save_directive_call_stack(dcs) if directive.respond_to? :save_directive_call_stack
110
+ directive.send(method_name_sym, dcs)
111
+ end
112
+ end
113
+
114
+ end
115
+
116
+
117
+ class SimpleRenderHandler
118
+ def description
119
+ 'SimpleRenderHandler is the default renderer for nodes, it should be invoked as the last directive and will output node normally'
120
+ end
121
+
122
+ def stag(dcs)
123
+ context = dcs.context
124
+ ret = []
125
+ ret << "<#{context[:tag].tag_name}"
126
+ context[:tag].attributes.sort.each do |name, value|
127
+ ret << " #{name}=\"#{value}\""
128
+ end
129
+ ret << '>' #must output as separate string so simplify_empty_elements can find it
130
+ end
131
+
132
+ def characters(dcs)
133
+ context = dcs.context
134
+ [] << context[:content_part]
135
+ end
136
+
137
+ def comment(dcs)
138
+ context = dcs.context
139
+ [] << '<!-- ' << context[:content_part] << ' -->'
140
+ end
141
+
142
+ def cdata(dcs)
143
+ context = dcs.context
144
+ [] << '<![CDATA[' << context[:content_part] << ']]>'
145
+ end
146
+
147
+ def etag(dcs)
148
+ context = dcs.context
149
+ [] << '</' << "#{context[:tag].tag_name}>" #must output </ as separate string so simplify_empty_elements can find it
150
+ end
151
+
152
+ end
153
+
154
+ class Tag
155
+ attr_accessor :directives, :tag_name, :attributes, :mode_type, :stag, :content, :etag, :parent
156
+ def initialize(directives, tag_name, attributes, mode_type, parent)
157
+ @tag_name = tag_name
158
+ @attributes = attributes
159
+ @mode_type = mode_type
160
+ @directives = directives
161
+ @stag = []
162
+ @content = []
163
+ @etag = []
164
+ @parent = parent
165
+ end
166
+
167
+ # creates a tag context using tag itself and mode type, also merge in any additional
168
+ # values passed in via values hash
169
+ def create_context( values = {} )
170
+ {
171
+ :tag => self,
172
+ :mode_type => @mode_type
173
+ }.merge!(values)
174
+ end
175
+
176
+ def data
177
+ [] << @stag << @content << @etag
178
+ end
179
+ end
180
+
181
+ class InvalidPathException < Exception
182
+ def initialize(msg)
183
+ super(msg)
184
+ end
185
+ end
186
+
187
+ # Serializer which can serialize output to file system.
188
+ # It will create any directories that are necessary before writing the file.
189
+ # It will overwrite any existing file that existed.
190
+ class FileSerializer
191
+ def serialize(render_mode, tag)
192
+ Log.debug { "outputting mode=#{render_mode.mode_type} to file=#{render_mode.output}".indent(2*(Renderer.last_renderer.render_levels.size - 1)) }
193
+ dir_name = File.dirname render_mode.output
194
+ FileUtils.makedirs(dir_name) unless File.exist?(dir_name) #ensure path exists
195
+
196
+ data_to_write = tag.data.join
197
+ if File.exist? render_mode.output
198
+ existing_file_contents = File.readlines(render_mode.output).join
199
+ if data_to_write == existing_file_contents
200
+ Log.debug { "file identical, skipping output of #{render_mode.output}" }
201
+ return false
202
+ end
203
+ end
204
+
205
+ File.open(render_mode.output, 'w') do |io|
206
+ io << data_to_write
207
+ end
208
+ true
209
+ end
210
+ end
211
+
212
+ # Serializer which simply serializes output to the console
213
+ class ConsoleSerializer
214
+ def serialize(render_mode, tag)
215
+ puts "outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}"
216
+ puts tag.data.join
217
+ puts ''
218
+ true
219
+ end
220
+ end
221
+
222
+ # Serializer which simply outputs each fragment to a hash with the key representing
223
+ # the path and the value the string contents.
224
+ # You may specify this serializer as an option to the parser (:serializer => HashSerializer.new(output_hash)).
225
+ # It takes an empty hash as the single constructor parameter to which the contents will be output.
226
+ class HashSerializer
227
+ def initialize( output_hash )
228
+ @output_hash = output_hash
229
+ end
230
+
231
+ def serialize(render_mode, tag)
232
+ Log.debug { "adding to hash - outputing mode=#{render_mode.mode_type} to file=#{render_mode.output}" }
233
+ @output_hash[ render_mode.output ] = tag.data.join
234
+ true
235
+ end
236
+ end
237
+
238
+ class Renderer
239
+ include DirectiveHelpers
240
+ attr_reader :restrict_output_to_directory, :directive_load_paths, :mv_ns
241
+ attr_accessor :render_levels, :directive_classes, :default_render_handler, :serializer, :template_path
242
+
243
+ def self.last_renderer; @@last_renderer; end
244
+
245
+ def initialize( options = {} )
246
+ @@last_renderer = self; #save last renderer for convenient access
247
+ @default_render_handler = SimpleRenderHandler.new
248
+ @render_levels = [
249
+ RenderLevel.new( [RenderMode.new] )
250
+ ]
251
+
252
+ serializer = options[:serializer] || DefaultSerializer
253
+ self.serializer = serializer.is_a?(Class) ? serializer.new : serializer #one can pass in Serializer class or an instance
254
+ self.restrict_output_to_directory = options[:output_dir] || nil
255
+ self.mv_ns = options[:namespace] || NamespacePrefix
256
+ self.directive_load_paths = ( DefaultDirectiveLoadPaths << options[:additional_directive_paths] ).flatten
257
+ self.template_path = options[:template_path] || ''
258
+ end
259
+
260
+ def mv_ns=(namespace_prefix)
261
+ @mv_ns = namespace_prefix
262
+ Log.debug { 'namespace_prefix set to '+namespace_prefix }
263
+ end
264
+
265
+ def restrict_output_to_directory=(dir)
266
+ @restrict_output_to_directory = (!dir.nil?) ? File.expand_path(dir) : nil
267
+ Log.debug { 'restrict_output_to_directory set to '+@restrict_output_to_directory.to_s }
268
+ end
269
+
270
+ # Sets directive_load_paths, re-requiring all the new load paths, however any directives that were
271
+ # already required (and loaded) will still be in memory because these are not reset.
272
+ def directive_load_paths=( directive_paths )
273
+ @directive_classes = {}
274
+ @auto_directives = []
275
+ directive_paths.each do |directive_path|
276
+ next if directive_path.nil?
277
+ raise InvalidPathException.new('directive_path does not exist, path='+directive_path) unless File.exist? directive_path
278
+ Dir.open( directive_path ).each { |fn| require "#{directive_path}/#{fn}" if fn =~ /[.]rb$/ }
279
+ end
280
+
281
+ Log.debug { 'directive plugins loaded:' +
282
+ (DirectiveBase.loaded_classes.collect do |c|
283
+ c.name.split(':').last #strip off Module prefixes for brevity
284
+ end).inspect
285
+ }
286
+ DirectiveBase.loaded_classes.each do |lc|
287
+ lc.on_load if lc.respond_to?(:on_load)
288
+ full_attr_name = (lc.respond_to? :full_attr_name) ? lc.full_attr_name(@mv_ns) : build_full_attribute_name(@mv_ns, lc)
289
+ @directive_classes[full_attr_name] = lc
290
+ lcinstance = lc.new(nil)
291
+ @auto_directives << lc if lcinstance.respond_to?(:global_directive?) && lcinstance.global_directive?
292
+ end
293
+ Log.debug { 'auto_directives='+@auto_directives.inspect }
294
+ end
295
+
296
+ # this method is invoked to build the full attribute name (the attribute which will be watched for in
297
+ # html attibutes. It concatenates the namespace prefix to the class name after first removing any module
298
+ # prefixes and then downcasing the first letter
299
+ def build_full_attribute_name(mv_ns, directive_class)
300
+ mv_ns+directive_class.name.split(':').last.downcase_first_letter
301
+ end
302
+
303
+ def modes
304
+ @render_levels.last.render_modes
305
+ end
306
+ def push_level(render_level)
307
+ render_level.render_modes.each do |mode|
308
+ enforce_sandbox!(mode.output)
309
+ end
310
+ @render_levels.push render_level
311
+ end
312
+
313
+ def enforce_sandbox!(path)
314
+ unless @restrict_output_to_directory.nil?
315
+ expanded_path = File.expand_path(path, @restrict_output_to_directory)
316
+ unless expanded_path.starts_with? @restrict_output_to_directory
317
+ raise InvalidPathException.new( "invalid path=#{path} resticted to path=#{@restrict_output_to_directory}")
318
+ end
319
+ path.replace expanded_path
320
+ end
321
+ end
322
+
323
+
324
+ def push_tag(tag_name, attributes)
325
+ modes.each do |mode|
326
+ attributes_copy = attributes.clone #these get changed in select_active_directives
327
+ directives = select_active_directives(tag_name, attributes_copy, mode)
328
+ parent = (mode.tags.empty?) ? nil : mode.tag
329
+ mode.tags.push Tag.new(directives, tag_name, attributes_copy, mode.mode_type, parent)
330
+ mode.tag.stag << mode.render_directives(:stag)
331
+ end
332
+ end
333
+
334
+ def append_content(type, content)
335
+ modes.each do |mode|
336
+ if mode.tag
337
+ mode.tag.content << mode.render_directives( type, mode.tag.create_context(:content_part => content) )
338
+ end
339
+ end
340
+ end
341
+
342
+ #does not call any directives, direct output
343
+ def append_raw(raw_output)
344
+ modes.each do |mode|
345
+ if mode.tag
346
+ mode.tag.content << raw_output
347
+ end
348
+ end
349
+ end
350
+
351
+ def pop_level
352
+ @render_levels.pop
353
+ end
354
+
355
+ def pop_tag
356
+ need_to_pop_level = false
357
+ modes.each do |mode|
358
+ mode.tag.etag << mode.render_directives(:etag)
359
+ content = []
360
+ content << mode.tag.stag << mode.tag.content << mode.tag.etag
361
+ content = simplify_empty_elements content
362
+ last_tag = mode.tags.pop
363
+ if mode.tags.empty?
364
+ unless mode.output.nil?
365
+ @serializer.serialize(mode, last_tag)
366
+ need_to_pop_level = true
367
+ end
368
+ else #add it to the parent
369
+ mode.tag.content << content
370
+ end
371
+ end
372
+ pop_level if need_to_pop_level
373
+ end
374
+
375
+ def simplify_empty_elements(content) #relies on the fact that > and </ are individual strings and are back to back with nothing in between
376
+ ret = []
377
+ next_to_last = nil
378
+ last = nil
379
+ content.flatten!
380
+ content.each do |item|
381
+ if next_to_last == '>' && last == '</'
382
+ ret.pop #remove '>'
383
+ ret.pop #remove '</'
384
+ ret << '/>'
385
+ else
386
+ ret << item
387
+ end
388
+ next_to_last = last
389
+ last = item
390
+ end
391
+ ret
392
+ end
393
+
394
+ def capitalize_first_letter(string)
395
+ string[0,1].upcase + string[1..-1]
396
+ end
397
+
398
+ def select_active_directives(tag_name, attributes, mode)
399
+ selected = DirectiveSet.new
400
+ directives_needed = []
401
+ @auto_directives.each do |directive_class|
402
+ directives_needed << directive_class.new(nil)
403
+ end
404
+ @directive_classes.each do |key,directive_class|
405
+ directives_needed << directive_class.new(attributes.delete(key)) if attributes[key]
406
+ end
407
+ sorted_directives = directives_needed.sort do |x,y|
408
+ xval = (x.respond_to?(:priority)) ? x.priority : DirectivePriorities::Medium
409
+ yval = (y.respond_to?(:priority)) ? y.priority : DirectivePriorities::Medium
410
+ xval <=> yval
411
+ end
412
+ sorted_directives << @default_render_handler #insure this is last
413
+ selected << sorted_directives
414
+ end
415
+
416
+ end
417
+
418
+
419
+ class MasterViewListener
420
+ include REXML::SAX2Listener
421
+ include DirectiveHelpers
422
+
423
+ def initialize( options = {} )
424
+ @renderer = Renderer.new(options)
425
+ end
426
+
427
+ def xmldecl(version, encoding, standalone)
428
+ #todo
429
+ end
430
+
431
+ def start_document
432
+ #todo
433
+ end
434
+
435
+ def doctype(name, pub, sys, long_name, uri)
436
+ #todo
437
+ end
438
+
439
+ def start_element(uri, localname, qname, attributes)
440
+ unescape_attributes!(attributes)
441
+ push_levels(attributes)
442
+ @renderer.push_tag(qname, attributes)
443
+ end
444
+
445
+ def characters(text)
446
+ @renderer.append_content(:characters, text)
447
+ end
448
+
449
+ def comment(comment)
450
+ @renderer.append_content(:comment, comment)
451
+ end
452
+
453
+ def cdata(content)
454
+ @renderer.append_content(:cdata, content)
455
+ end
456
+
457
+ def end_element(uri, localname, qname)
458
+ @renderer.pop_tag
459
+ end
460
+
461
+ def end_document
462
+ #todo
463
+ end
464
+
465
+ def unescape_attributes!(attributes)
466
+ attributes.each do |name, value|
467
+ value.replace CGI::unescapeHTML(value)
468
+ value.gsub!('&apos;', '\'')
469
+ end
470
+ end
471
+
472
+ def generate_replace(value)
473
+ @renderer.append_raw ERB_EVAL+value+ERB_END
474
+ end
475
+
476
+ # handle a mv:gen_render attribute, which calls generate and outputs a token
477
+ # it takes an optional :dir => 'foo/bar' which is prepended to partial path,
478
+ # otherwise it just uses what is in partial.
479
+ # This creates a generate attribute value which will be used later.
480
+ # Parameters
481
+ # value = attribute value for gen_render
482
+ # attributes = all remaining attributes hash
483
+ def generate_render(value, attributes)
484
+ prepend_dir = find_string_val_in_string_hash(value, :dir) #only used for masterview
485
+ partial = find_string_val_in_string_hash(value, :partial)
486
+ return if partial.nil?
487
+ dir = File.dirname(partial)
488
+ base = File.basename(partial)
489
+ filename = '_'+base+'.rhtml'
490
+ path = ( (dir != '.') ? File.join(dir,filename) : filename )
491
+ path = File.join(prepend_dir, path) if prepend_dir
492
+ generate_attribute = attributes[@renderer.mv_ns+'generate'] || ''
493
+ generate_attribute = path + (generate_attribute.blank? ? '' : ', '+generate_attribute)
494
+ attributes[@renderer.mv_ns+'generate'] = generate_attribute
495
+ @renderer.append_raw( ERB_EVAL+'render( '+value+' )'+ERB_END )
496
+ end
497
+
498
+ def push_levels(attributes)
499
+ gen_render = attributes.delete(@renderer.mv_ns+'gen_render') #get and delete from map
500
+ generate_render( gen_render, attributes ) unless gen_render.nil?
501
+
502
+ gen_replace = attributes.delete(@renderer.mv_ns+'gen_replace') #get and delete from map
503
+ generate_replace( gen_replace ) unless gen_replace.nil?
504
+
505
+ gen = attributes.delete(@renderer.mv_ns+'generate') #get and delete from map
506
+ unless gen.nil?
507
+ attributes[@renderer.mv_ns+'insert_generated_comment'] = @renderer.template_path unless OmitGeneratedComments #add the comment directive, so it will be written to each gen'd file
508
+ render_level = nil
509
+ gen_values = parse_eval_into_hash(gen, :normal)
510
+
511
+ #Log.debug { 'generate_hash='+gen_values.inspect }
512
+
513
+ gen_values.each do |key,value|
514
+ mode_type = key.to_sym
515
+ arr_values = (value.is_a?(Enumerable)) ? value : [value] #if not enumerable add it to array
516
+ value.each do |path|
517
+ path.strip!
518
+ Log.debug { ('pushing mode='+mode_type.to_s+' path='+path).indent(2*@renderer.render_levels.size) }
519
+ render_level ||= RenderLevel.new
520
+ render_level.push RenderMode.new(path, mode_type)
521
+ end
522
+ end
523
+ @renderer.push_level(render_level) unless render_level.nil?
524
+ end
525
+ end
526
+
527
+ end
528
+
529
+ class Parser
530
+ # parse a MasterView template by first reading from file and render output.
531
+ # template_file_path param is file path to template
532
+ # options are the optional parameters which control the output (:output_dir, :namespace)
533
+ def self.parse_file( template_file_path, output_dir, options = DefaultParserOptions.clone)
534
+ Log.debug { "Parsing file=#{File.expand_path(template_file_path)} output_dir=#{File.expand_path(output_dir)}" }
535
+ options[:template_path]=File.expand_path(template_file_path)
536
+ options[:output_dir] = output_dir
537
+ template = File.new( template_file_path )
538
+ template = template.readlines.join if options[:tidy] || options[:escape_erb]
539
+ self.parse( template, options )
540
+ end
541
+
542
+ # parse a MasterView template and render output.
543
+ # template param is actual template source passed in as string or array.
544
+ # options are the optional parameters which control the output (:output_dir, :namespace, :serializer)
545
+ def self.parse( template, options = DefaultParserOptions.clone)
546
+ begin
547
+ if options[:tidy]
548
+ template = self.tidy(template)
549
+ elsif options[:escape_erb]
550
+ template = self.escape_erb(template)
551
+ end
552
+ parser = REXML::Parsers::SAX2Parser.new( template )
553
+ parser.listen( MasterViewListener.new(options) )
554
+ parser.parse
555
+ rescue Exception => e
556
+ if RescueExceptions
557
+ Log.error { "Failure to parse template. Exception="+e }
558
+ Log.debug { e.backtrace.join("\n") }
559
+ else
560
+ raise
561
+ end
562
+ end
563
+ end
564
+
565
+ def self.tidy(html)
566
+ Tidy.path = TidyPath unless Tidy.path
567
+ xml = Tidy.open do |tidy|
568
+ tidy.options.output_xml = true
569
+ tidy.options.indent = true
570
+ tidy.options.wrap = 0
571
+ xml = tidy.clean(html)
572
+ end
573
+ xml = self.escape_erb(xml)
574
+ Log.debug { 'tidy corrected xml='+xml }
575
+ xml
576
+ end
577
+
578
+ def self.escape_erb(html)
579
+ html = html.gsub(/<%/, InlineErbStart)
580
+ html.gsub!(/%>/, InlineErbEnd)
581
+ html
582
+ end
583
+ end
584
+
585
+ end
@@ -0,0 +1,41 @@
1
+ module MasterView
2
+
3
+ # mix this in so that these become class methods
4
+ # mix in like so
5
+ # class FooPluginBase
6
+ # include PluginLoadTracking
7
+ # end
8
+ module PluginLoadTracking
9
+
10
+ module InstanceMethods
11
+ #put any instance methods here
12
+ end
13
+
14
+ module ClassMethods
15
+ @@loaded_classes = []
16
+
17
+ # called when a class inherits from this
18
+ def inherited(plugin_class)
19
+ self.register_class(plugin_class)
20
+ end
21
+
22
+ # register a loaded class, called from inherited and can be called manually.
23
+ def register_class(plugin_class)
24
+ @@loaded_classes << plugin_class
25
+ end
26
+
27
+ def loaded_classes
28
+ @@loaded_classes
29
+ end
30
+
31
+ end
32
+
33
+ def self::included(other_module)
34
+ other_module.module_eval{ include InstanceMethods }
35
+ other_module.extend ClassMethods
36
+ other_module
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,9 @@
1
+ module MasterView
2
+ module RuntimeHelpers
3
+ include ObjectSpace
4
+ def ObjectSpace.is_class_loaded?( class_name )
5
+ self.each_object( Class ) { |k| return true if k.to_s == class_name }
6
+ false
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ class String
2
+
3
+ #returns a string with the first letter downcased, the rest is unchanged
4
+ def downcase_first_letter
5
+ self[0,1].downcase + self[1..-1]
6
+ end
7
+
8
+ #downcases the first letter in place
9
+ def downcase_first_letter!
10
+ lc_first_letter = self[0,1].downcase
11
+ self[0] = lc_first_letter
12
+ self
13
+ end
14
+
15
+ end