masterview 0.0.6 → 0.0.7
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.
- data/Rakefile +8 -5
- data/init.rb +43 -0
- data/lib/facets/AUTHORS +36 -0
- data/lib/facets/CHANGELOG +266 -0
- data/lib/facets/COPYING +403 -0
- data/lib/facets/ProjectInfo +74 -0
- data/lib/facets/README +252 -0
- data/lib/facets/core/string/blank.rb +36 -0
- data/lib/facets/core/string/indent.rb +68 -0
- data/lib/facets/core/string/starts_with.rb +24 -0
- data/lib/masterview/directive_base.rb +163 -0
- data/lib/masterview/directive_helpers.rb +176 -0
- data/lib/masterview/directives/block.rb +30 -0
- data/lib/masterview/directives/content.rb +10 -0
- data/lib/masterview/directives/else.rb +25 -0
- data/lib/masterview/directives/elsif.rb +26 -0
- data/lib/masterview/directives/form.rb +19 -0
- data/lib/masterview/directives/global_inline_erb.rb +39 -0
- data/lib/masterview/directives/hidden_field.rb +31 -0
- data/lib/masterview/directives/if.rb +24 -0
- data/lib/masterview/directives/insert_generated_comment.rb +30 -0
- data/lib/masterview/directives/javascript_include.rb +15 -0
- data/lib/masterview/directives/link_to.rb +17 -0
- data/lib/masterview/directives/link_to_if.rb +21 -0
- data/lib/masterview/directives/link_to_remote.rb +17 -0
- data/lib/masterview/directives/password_field.rb +33 -0
- data/lib/masterview/directives/preview.rb +10 -0
- data/lib/masterview/directives/replace.rb +18 -0
- data/lib/masterview/directives/stylesheet_link.rb +14 -0
- data/lib/masterview/directives/submit.rb +14 -0
- data/lib/masterview/directives/testfilter.rb +55 -0
- data/lib/masterview/directives/text_area.rb +34 -0
- data/lib/masterview/directives/text_field.rb +33 -0
- data/lib/masterview/extras/rails_init.rb +67 -0
- data/lib/masterview/extras/watcher.rb +30 -0
- data/lib/masterview/masterview_version.rb +9 -0
- data/lib/masterview/parser.rb +585 -0
- data/lib/masterview/plugin_load_tracking.rb +41 -0
- data/lib/masterview/runtime_helpers.rb +9 -0
- data/lib/masterview/string_extensions.rb +15 -0
- data/lib/masterview.rb +129 -0
- data/test/block_test.rb +47 -0
- data/test/content_test.rb +26 -0
- data/test/else_test.rb +31 -0
- data/test/elsif_test.rb +31 -0
- data/test/example_test.rb +11 -0
- data/test/filter_helpers_test.rb +142 -0
- data/test/form_test.rb +66 -0
- data/test/global_inline_erb_test.rb +30 -0
- data/test/hidden_field_test.rb +62 -0
- data/test/if_test.rb +23 -0
- data/test/javascript_include_test.rb +26 -0
- data/test/link_to_if_test.rb +27 -0
- data/test/link_to_test.rb +52 -0
- data/test/parser_test.rb +166 -0
- data/test/password_field_test.rb +89 -0
- data/test/replace_test.rb +27 -0
- data/test/run_parser_test.rb +27 -0
- data/test/stylesheet_link_test.rb +26 -0
- data/test/submit_test.rb +54 -0
- data/test/template_file_watcher_test.rb +50 -0
- data/test/template_test.rb +181 -0
- data/test/test_helper.rb +24 -0
- data/test/text_area_test.rb +81 -0
- data/test/text_field_test.rb +89 -0
- metadata +71 -11
@@ -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!(''', '\'')
|
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,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
|