www_app 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +36 -0
  3. data/Gemfile +3 -0
  4. data/LICENSE +21 -0
  5. data/README.md +84 -0
  6. data/TODO.md +13 -0
  7. data/VERSION +1 -0
  8. data/bin/www_app +46 -0
  9. data/doc/Design.md +123 -0
  10. data/doc/Why_this_arch.rb +104 -0
  11. data/lib/public/jquery-2.1.1.js +4 -0
  12. data/lib/public/jquery.serialize-object.min.js +8 -0
  13. data/lib/public/underscore-1.7.0.js +6 -0
  14. data/lib/public/underscore-min.map +1 -0
  15. data/lib/public/underscore.string-2.3.0.js +1 -0
  16. data/lib/public/www_app.js +824 -0
  17. data/lib/www_app/Clean.rb +169 -0
  18. data/lib/www_app/dsl.rb +86 -0
  19. data/lib/www_app/source.rb +53 -0
  20. data/lib/www_app.rb +1024 -0
  21. data/specs/as_ruby/0000-new.rb +23 -0
  22. data/specs/as_ruby/0010-attrs.rb +29 -0
  23. data/specs/as_ruby/0011-class.rb +39 -0
  24. data/specs/as_ruby/0011-href.rb +37 -0
  25. data/specs/as_ruby/0011-id.rb +39 -0
  26. data/specs/as_ruby/0020-tag.rb +21 -0
  27. data/specs/as_ruby/0020-tag_content.rb +43 -0
  28. data/specs/as_ruby/0021-body.rb +16 -0
  29. data/specs/as_ruby/0021-form.rb +22 -0
  30. data/specs/as_ruby/0021-link.rb +26 -0
  31. data/specs/as_ruby/0021-script.rb +44 -0
  32. data/specs/as_ruby/0030-mustache.rb +113 -0
  33. data/specs/as_ruby/0040-css.rb +174 -0
  34. data/specs/as_ruby/0050-on.rb +64 -0
  35. data/specs/client-side/index.html +90 -0
  36. data/specs/client-side/index.js +777 -0
  37. data/specs/lib/config.ru +96 -0
  38. data/specs/lib/helpers.rb +230 -0
  39. data/specs/lib/qunit/qunit-1.15.0.css +237 -0
  40. data/specs/lib/qunit/qunit-1.15.0.js +2495 -0
  41. data/specs/lib/sample.rb +23 -0
  42. data/specs/sampe.2.rb +14 -0
  43. data/specs/sample.3.rb +17 -0
  44. data/specs/sample.rb +44 -0
  45. data/www_app.gemspec +38 -0
  46. metadata +271 -0
data/lib/www_app.rb ADDED
@@ -0,0 +1,1024 @@
1
+
2
+
3
+ require 'mustache'
4
+ require 'escape_escape_escape'
5
+
6
+
7
+ # ===================================================================
8
+ # === Mustache customizations: ======================================
9
+ # ===================================================================
10
+ Mustache.raise_on_context_miss = true
11
+
12
+ class Mustache
13
+
14
+ def render(data = template, ctx = {})
15
+ ctx = data
16
+ tpl = templateify(template)
17
+
18
+ begin
19
+ context.push(ctx)
20
+ tpl.render(context)
21
+ ensure
22
+ context.pop
23
+ end
24
+ end # === def render
25
+
26
+ class Context
27
+
28
+ def find *args
29
+ fail "No longer needed."
30
+ end
31
+
32
+ def fetch *args
33
+ raise ContextMiss.new("Can't find: #{args.inspect}") if args.size != 2
34
+
35
+ meth, key = args
36
+
37
+ @stack.each { |frame|
38
+ case
39
+ when frame.is_a?(Hash) && meth == :coll && !frame.has_key?(key)
40
+ return false
41
+
42
+ when frame.is_a?(Hash) && meth == :coll && frame.has_key?(key)
43
+ target = frame[key]
44
+ if target == true || target == false || target == nil || target.is_a?(Array) || target.is_a?(Hash)
45
+ return target
46
+ end
47
+ fail "Invalid value: #{key.inspect} (#{key.class})"
48
+
49
+ when frame.is_a?(Hash) && frame.has_key?(key)
50
+ return ::Escape_Escape_Escape.send(meth, frame[key])
51
+
52
+ end
53
+ }
54
+
55
+ raise ContextMiss.new("Can't find .#{meth}(#{key.inspect})")
56
+ end
57
+
58
+ alias_method :[], :fetch
59
+
60
+ end # === class Context
61
+
62
+ class Generator
63
+
64
+ alias_method :w_syms_on_fetch, :on_fetch
65
+
66
+ def on_fetch(names)
67
+ if names.length == 2
68
+ "ctx[#{names.first.to_sym.inspect}, #{names.last.to_sym.inspect}]"
69
+ else
70
+ w_syms_on_fetch(names)
71
+ end
72
+ end
73
+
74
+ end # === class Generator
75
+
76
+ end # === class Mustache
77
+ # ===================================================================
78
+
79
+
80
+ # ===================================================================
81
+ # === Symbol customizations: ========================================
82
+ # ===================================================================
83
+ class Symbol
84
+
85
+ def to_mustache meth
86
+ WWW_App::Sanitize.mustache meth, self
87
+ end
88
+
89
+ end # === class Symbol
90
+ # ===================================================================
91
+
92
+
93
+ # ===================================================================
94
+ # === WWW_App ====================================================
95
+ # ===================================================================
96
+ class WWW_App < BasicObject
97
+ # ===================================================================
98
+
99
+ include ::Kernel
100
+
101
+ Unescaped = ::Class.new(::StandardError)
102
+ Not_Unique = ::Class.new(::StandardError)
103
+ HTML_ID_Duplicate = ::Class.new(Not_Unique)
104
+
105
+ ALWAYS_END_TAGS = [:script]
106
+
107
+ SYM_CACHE = { attrs: {}, css_props: {}}
108
+
109
+ Classes = []
110
+ INVALID_ATTR_CHARS = /[^a-z0-9\_\-]/i
111
+ IMAGE_AT_END = /image\z/i
112
+
113
+ HASH = '#'
114
+ DOT = '.'
115
+ BANG = '!'
116
+ NEW_LINE = "\n"
117
+ SPACE = ' '
118
+ BLANK = ''
119
+ BODY = 'body'
120
+ UNDERSCORE = '_'
121
+
122
+ Document_Template = ::File.read(__FILE__).split("__END__").last.strip
123
+
124
+ NO_END_TAGS = [:br, :input, :link, :meta, :hr, :img]
125
+
126
+ Methods = {
127
+ :elements => %w[
128
+
129
+ title
130
+ body div span
131
+
132
+ img
133
+ b em i strong u a
134
+ abbr blockquote cite
135
+ br cite code
136
+ ul ol li p pre q
137
+ sup sub
138
+ form input button
139
+
140
+ link
141
+
142
+ script
143
+
144
+ ].map(&:to_sym),
145
+
146
+ :attributes => {
147
+ :all => [:id, :class],
148
+ :a => [:href, :rel],
149
+ :form => [:action, :method, :accept_charset],
150
+ :input => [:type, :name, :value],
151
+ :style => [:type],
152
+ :script => [:type, :src, :language],
153
+ :link => [:rel, :type, :sizes, :href, :title],
154
+ :meta => [:name, :http_equiv, :property, :content, :charset],
155
+ :img => [:src, :width, :height]
156
+ },
157
+
158
+ :css => {
159
+ :at_rules => [ 'font-face', 'media' ],
160
+ :protocols => [ :relative ],
161
+
162
+ # From: Sanitize::Config::RELAXED[:css][:properties]
163
+ :properties => %w[
164
+ background bottom font_variant_numeric position
165
+ background_attachment box_decoration_break font_variant_position quotes
166
+ background_clip box_shadow font_weight resize
167
+ background_color box_sizing height right
168
+ background_image clear hyphens tab_size
169
+ background_origin clip icon table_layout
170
+ background_position clip_path image_orientation text_align
171
+ background_repeat color image_rendering text_align_last
172
+ background_size column_count image_resolution text_combine_horizontal
173
+ border column_fill ime_mode text_decoration
174
+ border_bottom column_gap justify_content text_decoration_color
175
+ border_bottom_color column_rule left text_decoration_line
176
+ border_bottom_left_radius column_rule_color letter_spacing text_decoration_style
177
+ border_bottom_right_radius column_rule_style line_height text_indent
178
+ border_bottom_style column_rule_width list_style text_orientation
179
+ border_bottom_width column_span list_style_image text_overflow
180
+ border_collapse column_width list_style_position text_rendering
181
+ border_color columns list_style_type text_shadow
182
+ border_image content margin text_transform
183
+ border_image_outset counter_increment margin_bottom text_underline_position
184
+ border_image_repeat counter_reset margin_left top
185
+ border_image_slice cursor margin_right touch_action
186
+ border_image_source direction margin_top transform
187
+ border_image_width display marks transform_origin
188
+ border_left empty_cells mask transform_style
189
+ border_left_color filter mask_type transition
190
+ border_left_style float max_height transition_delay
191
+ border_left_width font max_width transition_duration
192
+ border_radius font_family min_height transition_property
193
+ border_right font_feature_settings min_width transition_timing_function
194
+ border_right_color font_kerning opacity unicode_bidi
195
+ border_right_style font_language_override order unicode_range
196
+ border_right_width font_size orphans vertical_align
197
+ border_spacing font_size_adjust overflow visibility
198
+ border_style font_stretch overflow_wrap white_space
199
+ border_top font_style overflow_x widows
200
+ border_top_color font_synthesis overflow_y width
201
+ border_top_left_radius font_variant padding word_break
202
+ border_top_right_radius font_variant_alternates padding_bottom word_spacing
203
+ border_top_style font_variant_caps padding_left word_wrap
204
+ border_top_width font_variant_east_asian padding_right z_index
205
+ border_width font_variant_ligatures padding_top
206
+ ].map(&:to_sym)
207
+ }
208
+
209
+ } # === end Methods
210
+
211
+ ALLOWED_ATTRS = Methods[:attributes].inject({}) { |memo, (tag, attrs)|
212
+ attrs.each { |a|
213
+ memo[a] ||= []
214
+ memo[a] << tag
215
+ }
216
+ memo
217
+ }
218
+
219
+ class << self # ===================================================
220
+ end # === class self ==============================================
221
+
222
+ def initialize *files
223
+ @js = []
224
+ @style = {}
225
+ @css_arr = []
226
+ @css_id_override = nil
227
+
228
+ @title = nil
229
+ @scripts = []
230
+ @body = []
231
+ @compiled = nil
232
+ @cache = {}
233
+ @is_doc = false
234
+ @default_ids = {}
235
+
236
+ @state = [:create]
237
+ @ids = {}
238
+
239
+ @tag_arr = []
240
+ @current_tag_index = nil
241
+ @mustache = nil
242
+
243
+ @html_ids = {}
244
+
245
+ tag(:head) {
246
+
247
+ @head = tag!
248
+
249
+ tag(:style) {
250
+ @style = tag!
251
+ @style[:css] = {}
252
+ }
253
+
254
+ tag(:script) {
255
+ tag![:content] = @js
256
+ }
257
+
258
+ } # === tag :head
259
+
260
+ tag(:body) {
261
+
262
+ @body = tag!
263
+
264
+ files.each { |file_name|
265
+ eval ::File.read(file_name), nil, file_name
266
+ }
267
+
268
+ if block_given?
269
+ case
270
+ when !::ENV['IS_DEV']
271
+ fail "Blocks not allowed during non-dev."
272
+ when ::ENV['IS_DEV']
273
+ instance_eval(&(::Proc.new))
274
+ end # === case
275
+ end
276
+ }
277
+
278
+ @mustache = ::Mustache.new
279
+ @mustache.template = to_mustache
280
+
281
+ freeze
282
+ end # === def new_class
283
+
284
+ def render_if name
285
+ tag(:render_if) { tag![:attrs][:key] = name; yield }
286
+ nil
287
+ end
288
+
289
+ def render_unless name
290
+ tag(:render_unless) { tag![:attrs][:key] = name; yield }
291
+ nil
292
+ end
293
+
294
+ def render raw_data = {}
295
+ @mustache.render raw_data
296
+ end
297
+
298
+ Allowed = {
299
+ :attr => {}
300
+ }
301
+
302
+ Methods[:attributes].each { |tag, attrs|
303
+ next if tag == :all
304
+ attrs.each { |raw_attr|
305
+ attr_name = raw_attr.to_s.gsub('-', '_').to_sym
306
+ Allowed[:attr][attr_name] ||= {}
307
+ Allowed[:attr][attr_name][tag.to_sym] = true
308
+ }
309
+ }
310
+
311
+ Allowed[:attr].each { |name, tags|
312
+ eval <<-EOF, nil, __FILE__, __LINE__ + 1
313
+ def #{name} val
314
+ allowed = Allowed[:attr][:#{name}]
315
+ allowed = allowed && allowed[tag![:tag]]
316
+ return super unless allowed
317
+
318
+ tag![:attrs][:#{name}] = val
319
+
320
+ if block_given?
321
+ close_tag { yield }
322
+ else
323
+ self
324
+ end
325
+ end
326
+ EOF
327
+ }
328
+
329
+ #
330
+ # Example:
331
+ # div.*('my_id') { }
332
+ #
333
+ def * raw_id
334
+ id = ::Escape_Escape_Escape.html_id(raw_id)
335
+
336
+ old_id = tag![:attrs][:id]
337
+ fail("Id already set: #{old_id} new: #{id}") if old_id
338
+
339
+ fail(HTML_ID_Duplicate, "Id already used: #{id.inspect}, tag index: #{@html_ids[id]}") if @html_ids[id]
340
+ @html_ids[id] = tag![:tag_index]
341
+
342
+ tag![:attrs][:id] = id
343
+
344
+ if block_given?
345
+ close_tag { yield }
346
+ else
347
+ self
348
+ end
349
+ end
350
+
351
+ #
352
+ # Example:
353
+ # div.^(:alert, :red_hot) { 'my content' }
354
+ #
355
+ def ^ *names
356
+ tag![:attrs][:class].concat(names).uniq!
357
+
358
+ if block_given?
359
+ close_tag { yield }
360
+ else
361
+ self
362
+ end
363
+ end
364
+
365
+ private # =========================================================
366
+
367
+ #
368
+ # NOTE: Properties are defined first,
369
+ # so :elements methods can over-write them,
370
+ # just in case there are duplicates.
371
+ Methods[:css][:properties].each { |name|
372
+ str_name = name.to_s.gsub('_', '-')
373
+ eval <<-EOF, nil, __FILE__, __LINE__ + 1
374
+ def #{name} *args
375
+ css_property(:#{name}, *args) {
376
+ yield if block_given?
377
+ }
378
+ end
379
+ EOF
380
+ }
381
+
382
+ Methods[:elements].each { |name|
383
+ eval <<-EOF, nil, __FILE__, __LINE__ + 1
384
+ def #{name} *args
385
+ if block_given?
386
+ tag(:#{name}, *args) { yield }
387
+ else
388
+ tag(:#{name}, *args)
389
+ end
390
+ end
391
+ EOF
392
+ }
393
+
394
+ # -----------------------------------------------
395
+ def section
396
+ fail
397
+ end
398
+
399
+ def on_top_of
400
+ fail
401
+ end
402
+
403
+ def in_middle_of
404
+ fail
405
+ end
406
+
407
+ def at_bottom_of
408
+ fail
409
+ end
410
+ # -----------------------------------------------
411
+
412
+ def is_doc?
413
+ @is_doc || !@style[:css].empty? || !@js.empty?
414
+ end
415
+
416
+ def first_class
417
+ tag![:attrs][:class].first
418
+ end
419
+
420
+ def html_element? e
421
+ e.is_a?(Hash) && e[:type] == :html
422
+ end
423
+
424
+ def dom_id?
425
+ tag![:attrs][:id]
426
+ end
427
+
428
+ #
429
+ # Examples
430
+ # dom_id -> the current dom id of the current element
431
+ # dom_id :default -> if no dom it, set/get default of current element
432
+ # dom_id {:element:} -> dom id of element: {:type=>:html, :tag=>...}
433
+ #
434
+ def dom_id *args
435
+
436
+ use_default = false
437
+
438
+ case
439
+ when args.empty?
440
+ e = tag!
441
+ # do nothing else
442
+
443
+ when args.size == 1 && args.first == :default
444
+ e = tag!
445
+ use_default = true
446
+
447
+ when args.size == 1 && args.first.is_a?(::Hash) && args.first[:type]==:html
448
+ e = args.first
449
+
450
+ else
451
+ fail "Unknown args: #{args.inspect}"
452
+ end
453
+
454
+ id = e[:attrs][:id]
455
+ return id if id
456
+ return nil unless use_default
457
+
458
+ e[:default_id] ||= begin
459
+ key = e[:tag]
460
+ @default_ids[key] ||= -1
461
+ @default_ids[key] += 1
462
+ end
463
+ end # === def dom_id
464
+
465
+ #
466
+ # Examples
467
+ # selector_id -> a series of ids and tags to be used as a JS selector
468
+ # Example:
469
+ # #id tag tag
470
+ # tag tag
471
+ #
472
+ #
473
+ def selector_id
474
+ i = tag![:tag_index]
475
+ id_given = false
476
+ classes = []
477
+
478
+ while !id_given && i && i > -1
479
+ e = @tag_arr[i]
480
+ id = dom_id e
481
+ (id_given = true) if id
482
+
483
+ if e[:tag] == :body && !classes.empty?
484
+ # do nothing because
485
+ # we do not want 'body tag.class tag.class'
486
+ else
487
+ case
488
+ when id
489
+ classes << "##{id}"
490
+ else
491
+ classes << e[:tag]
492
+ end # === case
493
+ end # === if
494
+
495
+ i = e[:parent_index]
496
+ end
497
+
498
+ return 'body' if classes.empty?
499
+ classes.join SPACE
500
+ end
501
+
502
+ #
503
+ # Examples
504
+ # css_id -> current css id of element.
505
+ # It uses the first class, if any, found.
506
+ # #id.class -> if #id and first class found.
507
+ # #id -> if class is missing and id given.
508
+ # #id tag.class -> if class given and ancestor has id.
509
+ # #id tag tag -> if no class given and ancestor has id.
510
+ # tag tag tag -> if no ancestor has class.
511
+ #
512
+ # css_id :my_class -> same as 'css_id()' except
513
+ # 'my_class' overrides :class attribute of current
514
+ # element.
515
+ #
516
+ #
517
+ def css_id *args
518
+
519
+ str_class = nil
520
+
521
+ case args.size
522
+ when 0
523
+ fail "Not in a tag." unless tag!
524
+ str_class = @css_id_override
525
+ when 1
526
+ str_class = args.first
527
+ else
528
+ fail "Unknown args: #{args.inspect}"
529
+ end
530
+
531
+ i = tag![:tag_index]
532
+ id_given = false
533
+ classes = []
534
+
535
+ while !id_given && i && i > -1
536
+ e = @tag_arr[i]
537
+ id = dom_id e
538
+ first_class = e[:attrs][:class].first
539
+
540
+ if id
541
+ id_given = true
542
+ if str_class
543
+ classes.unshift(
544
+ str_class.is_a?(::Symbol) ?
545
+ "##{id}.#{str_class}" :
546
+ "##{id}#{str_class}"
547
+ )
548
+ else
549
+ classes.unshift "##{id}"
550
+ end
551
+
552
+ else # no id given
553
+ if str_class
554
+ classes.unshift(
555
+ str_class.is_a?(::Symbol) ?
556
+ "#{e[:tag]}.#{str_class}" :
557
+ "#{e[:tag]}#{str_class}"
558
+ )
559
+ elsif first_class
560
+ classes.unshift "#{e[:tag]}.#{first_class}"
561
+ else
562
+ if e[:tag] != :body || (classes.empty?)
563
+ classes.unshift "#{e[:tag]}"
564
+ end
565
+ end # if first_class
566
+
567
+ end # if id
568
+
569
+ i = e[:parent_index]
570
+ break if i == @body[:tag_index] && !classes.empty?
571
+ end
572
+
573
+ classes.join SPACE
574
+ end
575
+
576
+ # =================================================================
577
+ # Parent-related methods
578
+ # =================================================================
579
+
580
+ def css_parent?
581
+ !@css_arr.empty?
582
+ end
583
+
584
+ def parents
585
+ fail "not done"
586
+ end
587
+
588
+ def parent? *args
589
+ return(tag! && !tag![:parent_index].nil?) if args.empty?
590
+ fail("Unknown args: #{args.first}") if args.size > 1
591
+ return false unless parent
592
+
593
+ sym_tag = args.first
594
+
595
+ case sym_tag
596
+ when :html, :css, :script
597
+ parent[:type] == sym_tag
598
+ else
599
+ parent[:tag] == sym_tag
600
+ end
601
+ end
602
+
603
+ def parent
604
+ fail "Not in a tag." unless tag!
605
+ fail "No parent: #{tag![:tag].inspect}, #{tag![:tag_index]}" if !tag![:parent_index]
606
+ @tag_arr[tag![:parent_index]]
607
+ end
608
+
609
+ # =================================================================
610
+ # Tag (aka element)-related methods
611
+ # =================================================================
612
+
613
+ def tag!
614
+ return nil unless @current_tag_index.is_a?(::Numeric)
615
+ @tag_arr[@current_tag_index]
616
+ end
617
+
618
+ def tag? sym_tag
619
+ tag![:tag] == sym_tag
620
+ end
621
+
622
+ def tag sym_name
623
+ e = {
624
+ :type => :html,
625
+ :tag => sym_name,
626
+ :attrs => {:class=>[]},
627
+ :text => nil,
628
+ :childs => [],
629
+ :parent_index => @current_tag_index,
630
+ :is_closed => false,
631
+ :tag_index => @tag_arr.size
632
+ }
633
+
634
+ @tag_arr << e
635
+ @current_tag_index = e[:tag_index]
636
+
637
+ if parent?
638
+ parent[:childs] << e[:tag_index]
639
+ else
640
+ if !([:head, :body].include? e[:tag])
641
+ fail "No parent found for: #{sym_name.inspect}"
642
+ end
643
+ end
644
+
645
+ if block_given?
646
+ close_tag { yield }
647
+ else
648
+ self
649
+ end
650
+ end
651
+
652
+ def in_tag t
653
+ orig = @current_tag_index
654
+ @current_tag_index = t[:tag_index]
655
+ yield
656
+ @current_tag_index = orig
657
+ nil
658
+ end
659
+
660
+ public def /
661
+ fail "No block allowed here: :/" if block_given?
662
+ close_tag
663
+ end
664
+
665
+ def close_tag
666
+ orig_tag = tag!
667
+ is_script = tag?(:script)
668
+
669
+ if block_given?
670
+
671
+ results = yield
672
+
673
+ results = nil if is_script
674
+
675
+ # The :yield may have left some opened tags, :input, :br/
676
+ # So we make sure we are in the original tag/element
677
+ # when we want to make some final changes.
678
+ in_tag(orig_tag) {
679
+ if tag?(:form)
680
+ input(:hidden, :auth_token, :auth_token.to_mustache(:html))
681
+ end
682
+ (tag![:text] = results) if results.is_a?(::String) || results.is_a?(::Symbol)
683
+ }
684
+ end
685
+
686
+ orig_tag[:is_closed] = true
687
+ @current_tag_index = orig_tag[:parent_index]
688
+
689
+ nil
690
+ end
691
+
692
+ # =================================================================
693
+ # CSS-related methods
694
+ # =================================================================
695
+
696
+ def css_property name, val = nil
697
+ prop = {:name=>name, :value=>val, :parent=>parent? ? parent : nil}
698
+
699
+ id = css_id
700
+ @style[:css][id] ||= {}
701
+
702
+ @css_arr << prop
703
+ @style[:css][id][@css_arr.map { |c| c[:name] }.join('_').to_sym] = val
704
+ yield if block_given?
705
+ @css_arr.pop
706
+ end
707
+
708
+
709
+ # =================================================================
710
+
711
+ def page_title
712
+ @is_doc = true
713
+ in_tag(@head) {
714
+ tag(:title) { yield }
715
+ }
716
+ self
717
+ end
718
+
719
+ def meta *args
720
+ fail "No block allowed." if block_given?
721
+ fail "Not allowed here." unless tag?(:body)
722
+ c = nil
723
+ in_tag(@tag) { c = tag(:meta, *args) }
724
+ c
725
+ end
726
+
727
+ def to_clean_text type, vals, tag = nil
728
+ case
729
+
730
+ when type == :javascript && vals.is_a?(::Array)
731
+ clean_vals = vals.map { |raw_x|
732
+ x = case raw_x
733
+ when ::Symbol, ::String
734
+ Sanitize.html(raw_x.to_s)
735
+ when ::Array
736
+ to_clean_text :javascript, raw_x
737
+ when ::Numeric
738
+ x
739
+ else
740
+ fail "Unknown type for json: #{raw_x.inspect}"
741
+ end
742
+ }
743
+
744
+ when type == :to_json && vals.is_a?(::Array)
745
+ ::Escape_Escape_Escape.json_encode(to_clean_text(:javascript, vals))
746
+
747
+ when type == :style_classes && vals.is_a?(::Hash)
748
+ h = vals
749
+ h.map { |raw_k,styles|
750
+ k = raw_k.to_s
751
+ <<-EOF
752
+ #{Sanitize.css_selector k} {
753
+ #{to_clean_text :styles, styles}
754
+ }
755
+ EOF
756
+ }.join.strip
757
+
758
+ when type == :styles && vals.is_a?(::Hash)
759
+ h = vals
760
+ h.map { |k,raw_v|
761
+ name = begin
762
+ clean_k = ::WWW_App::Sanitize.css_attr(k.to_s.gsub('_','-'))
763
+ fail("Invalid name for css property name: #{k.inspect}") if !clean_k || clean_k.empty?
764
+ clean_k
765
+ end
766
+
767
+ raw_v = raw_v.to_s
768
+
769
+ v = case
770
+
771
+ when name[IMAGE_AT_END]
772
+ case raw_v
773
+ when 'inherit', 'none'
774
+ raw_v
775
+ else
776
+ "url(#{Sanitize.href(raw_v)})"
777
+ end
778
+
779
+ when Methods[:css][:properties].include?(k)
780
+ Sanitize.css_value raw_v
781
+
782
+ else
783
+ fail "Invalid css attr: #{name.inspect}"
784
+
785
+ end # === case
786
+
787
+ %^#{name}: #{v};^
788
+ }.join("\n").strip
789
+
790
+ when type == :attrs && vals.is_a?(::Hash)
791
+ h = vals[:attrs]
792
+ tag = vals
793
+ final = h.map { |k,raw_v|
794
+
795
+ fail "Unknown attr: #{k.inspect}" if !ALLOWED_ATTRS.include?(k)
796
+
797
+ next if raw_v.is_a?(::Array) && raw_v.empty?
798
+
799
+ v = raw_v
800
+
801
+ attr_name = k.to_s.gsub(::WWW_App::INVALID_ATTR_CHARS, '_')
802
+ fail("Invalid name for html attr: #{k.inspect}") if !attr_name || attr_name.empty?
803
+
804
+ attr_val = case
805
+ when k == :href && tag[:tag] == :a
806
+ Sanitize.mustache :href, v
807
+
808
+ when k == :action || k == :src || k == :href
809
+ Sanitize.relative_href(v)
810
+
811
+ when k == :class
812
+ v.map { |n|
813
+ Sanitize.css_class_name(n)
814
+ }.join SPACE
815
+
816
+ when k == :id
817
+ Sanitize.html_id v.to_s
818
+
819
+ when ALLOWED_ATTRS[k]
820
+ Sanitize.html(v)
821
+
822
+ else
823
+ fail "Invalid attr: #{k.inspect}"
824
+
825
+ end # === case
826
+
827
+ %*#{attr_name}="#{attr_val}"*
828
+
829
+ }.compact.join SPACE
830
+
831
+ final.empty? ?
832
+ '' :
833
+ (" " << final)
834
+
835
+ when type == :html && vals.is_a?(::Array)
836
+ a = vals
837
+ a.map { |tag_index|
838
+ to_clean_text(:html, @tag_arr[tag_index])
839
+ }.join NEW_LINE
840
+
841
+ when type == :html && vals.is_a?(::Hash)
842
+
843
+ h = vals
844
+
845
+ fail("Unknown type: #{h.inspect}") if h[:type] != :html
846
+
847
+ if h[:tag] == :style
848
+ return <<-EOF
849
+ <style type="text/css">
850
+ #{to_clean_text :style_classes, h[:css]}
851
+ </style>
852
+ EOF
853
+ end
854
+
855
+ if h[:tag] == :script && h[:content] && !h[:content].empty?
856
+ return <<-EOF
857
+ <script type="text/css">
858
+ WWW_App.compile(
859
+ #{to_clean_text :to_json, h[:content]}
860
+ );
861
+ </script>
862
+ EOF
863
+ end
864
+
865
+ html = h[:childs].map { |tag_index|
866
+ to_clean_text(:html, @tag_arr[tag_index])
867
+ }.join(NEW_LINE).strip
868
+
869
+ if html.empty? && h[:text]
870
+ html = h[:text].is_a?(::Symbol) ?
871
+ h[:text].to_mustache(:html) :
872
+ Sanitize.html(h[:text].strip)
873
+ end # === if html.empty?
874
+
875
+ (html = nil) if html.empty?
876
+
877
+ case
878
+ when h[:tag] == :render_if
879
+ key = h[:attrs][:key]
880
+ open = "{{# coll.#{key} }}"
881
+ close = "{{/ coll.#{key} }}"
882
+
883
+ when h[:tag] == :render_unless
884
+ key = h[:attrs][:key]
885
+ open = "{{^ coll.#{key} }}"
886
+ close = "{{/ coll.#{key} }}"
887
+
888
+ when Methods[:elements].include?(h[:tag])
889
+ open = "<#{h[:tag]}#{to_clean_text(:attrs, h)}"
890
+ if NO_END_TAGS.include?(h[:tag])
891
+ open += ' />'
892
+ close = nil
893
+ else
894
+ open += '>'
895
+ close = "</#{h[:tag]}>"
896
+ end
897
+
898
+ else
899
+ fail "Unknown html tag: #{h[:tag].inspect}"
900
+
901
+ end # === case h[:tag]
902
+
903
+ if h[:tag]
904
+ [open, html, close].compact.join
905
+ else
906
+ html
907
+ end
908
+
909
+ else
910
+ fail "Unknown vals: #{type.inspect}, #{vals.inspect}"
911
+
912
+ end # case
913
+ end # def to_clean_text
914
+
915
+ def in_html?
916
+ @state.last == :html
917
+ end
918
+
919
+ def creating_html?
920
+ @state.last == :create_html
921
+ end
922
+
923
+ def js *args
924
+ fail("No js event defined.") if @js.empty?
925
+ if args.empty?
926
+ @js.last
927
+ else
928
+ @js.last.concat args
929
+ end
930
+ end
931
+
932
+ def on name, &blok
933
+ fail "Block required." unless blok
934
+
935
+ @js << 'create_event'
936
+ @js << [selector_id, name]
937
+
938
+ orig = @css_id_override
939
+ @css_id_override = name
940
+ results = yield
941
+ @css_id_override = orig
942
+
943
+ if @js.last.size == 2
944
+ @js.pop
945
+ @js.pop
946
+ end
947
+
948
+ results
949
+ end
950
+
951
+ def to_mustache
952
+
953
+ return @compiled if @compiled
954
+
955
+ final = if is_doc?
956
+ # Remember: to use !BODY first, because
957
+ # :head content might include a '!HEAD'
958
+ # value.
959
+ (page_title { 'Unknown Page Title' }) unless @page_title
960
+
961
+ Document_Template.
962
+ sub('!BODY', to_clean_text(:html, @body)).
963
+ sub('!HEAD', to_clean_text(:html, @head[:childs]))
964
+ else
965
+ to_clean_text(:html, @body[:childs])
966
+ end
967
+
968
+ utf_8 = Sanitize.clean_utf8(final)
969
+
970
+ @compiled = utf_8
971
+ end # === def to_mustache
972
+
973
+
974
+ def input *args
975
+ case
976
+ when args.size === 3
977
+ tag(:input).type(args[0]).name(args[1]).value(args[2])
978
+ else
979
+ super
980
+ end
981
+ end
982
+
983
+ def add_class name
984
+ js("add_class", [name])
985
+ end
986
+
987
+ class Sanitize
988
+
989
+ MUSTACHE_Regex = /\A\{\{\{? [a-z0-9\_\.]+ \}\}\}?\z/i
990
+
991
+ class << self
992
+
993
+ def mustache *args
994
+ meth, val = args
995
+ if val.is_a?(Symbol)
996
+ m = "{{{ #{meth}.#{val} }}}"
997
+ fail "Unknown chars: #{args.inspect}" unless m[MUSTACHE_Regex]
998
+ else
999
+ m = ::Escape_Escape_Escape.send(meth, val)
1000
+ end
1001
+ m
1002
+ end
1003
+
1004
+ def method_missing name, *args
1005
+ if args.last.is_a?(::Symbol)
1006
+ args.push(args.pop.to_s)
1007
+ end
1008
+ ::Escape_Escape_Escape.send(name, *args)
1009
+ end
1010
+
1011
+ end # === class << self
1012
+ end # === class Sanitize
1013
+
1014
+ end # === class WWW_App ==========================================
1015
+
1016
+ __END__
1017
+ <!DOCTYPE html>
1018
+ <html lang="en">
1019
+ <head>
1020
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
1021
+ !HEAD
1022
+ </head>
1023
+ !BODY
1024
+ </html>