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.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +84 -0
- data/TODO.md +13 -0
- data/VERSION +1 -0
- data/bin/www_app +46 -0
- data/doc/Design.md +123 -0
- data/doc/Why_this_arch.rb +104 -0
- data/lib/public/jquery-2.1.1.js +4 -0
- data/lib/public/jquery.serialize-object.min.js +8 -0
- data/lib/public/underscore-1.7.0.js +6 -0
- data/lib/public/underscore-min.map +1 -0
- data/lib/public/underscore.string-2.3.0.js +1 -0
- data/lib/public/www_app.js +824 -0
- data/lib/www_app/Clean.rb +169 -0
- data/lib/www_app/dsl.rb +86 -0
- data/lib/www_app/source.rb +53 -0
- data/lib/www_app.rb +1024 -0
- data/specs/as_ruby/0000-new.rb +23 -0
- data/specs/as_ruby/0010-attrs.rb +29 -0
- data/specs/as_ruby/0011-class.rb +39 -0
- data/specs/as_ruby/0011-href.rb +37 -0
- data/specs/as_ruby/0011-id.rb +39 -0
- data/specs/as_ruby/0020-tag.rb +21 -0
- data/specs/as_ruby/0020-tag_content.rb +43 -0
- data/specs/as_ruby/0021-body.rb +16 -0
- data/specs/as_ruby/0021-form.rb +22 -0
- data/specs/as_ruby/0021-link.rb +26 -0
- data/specs/as_ruby/0021-script.rb +44 -0
- data/specs/as_ruby/0030-mustache.rb +113 -0
- data/specs/as_ruby/0040-css.rb +174 -0
- data/specs/as_ruby/0050-on.rb +64 -0
- data/specs/client-side/index.html +90 -0
- data/specs/client-side/index.js +777 -0
- data/specs/lib/config.ru +96 -0
- data/specs/lib/helpers.rb +230 -0
- data/specs/lib/qunit/qunit-1.15.0.css +237 -0
- data/specs/lib/qunit/qunit-1.15.0.js +2495 -0
- data/specs/lib/sample.rb +23 -0
- data/specs/sampe.2.rb +14 -0
- data/specs/sample.3.rb +17 -0
- data/specs/sample.rb +44 -0
- data/www_app.gemspec +38 -0
- 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>
|