www_app 1.0.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 (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>