www_app 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +4 -0
- data/README.md +42 -24
- data/VERSION +1 -1
- data/bin/www_app +15 -4
- data/lib/public/vendor/hogan-3.0.2.min.js +5 -0
- data/lib/public/vendor/instruct_instruct_instruct.js +247 -0
- data/lib/public/vendor/jquery-2.1.3.min.js +4 -0
- data/lib/public/vendor/lodash.min.js +89 -0
- data/lib/public/www_app.js +885 -625
- data/lib/www_app/CSS.rb +310 -0
- data/lib/www_app/HTML.rb +219 -0
- data/lib/www_app/JavaScript.rb +51 -0
- data/lib/www_app/TO.rb +897 -0
- data/lib/www_app.rb +324 -945
- data/playground/config.ru +102 -0
- data/specs/client-side/index.html +1 -1
- data/specs/client-side/index.js +31 -379
- data/specs/lib/config.ru +60 -31
- data/specs/lib/helpers.rb +24 -2
- data/specs/server-side/0000-new.rb +1 -1
- data/specs/server-side/0001-underscore-double.rb +38 -0
- data/specs/server-side/0001-underscore.rb +73 -0
- data/specs/server-side/0010-attrs.rb +3 -4
- data/specs/server-side/0011-id.rb +5 -5
- data/specs/server-side/0020-tag.rb +2 -2
- data/specs/server-side/0020-tag_content.rb +1 -1
- data/specs/server-side/0021-body.rb +1 -1
- data/specs/server-side/0021-script.rb +66 -20
- data/specs/server-side/{0021-page_title.rb → 0021-title.rb} +5 -3
- data/specs/server-side/0030-mustache.rb +27 -20
- data/specs/server-side/0030-style.rb +64 -21
- data/specs/server-side/0040-css.rb +4 -4
- data/specs/server-side/0041-pseudo.rb +55 -0
- data/specs/server-side/0042-slash.rb +20 -0
- data/specs/server-side/0060-text.rb +26 -0
- metadata +18 -13
- data/lib/public/jquery-2.1.1.js +0 -4
- data/lib/public/underscore-1.7.0.js +0 -6
- data/lib/public/underscore-min.map +0 -1
- data/lib/public/underscore.string-2.3.0.js +0 -1
- data/lib/www_app/Clean.rb +0 -169
- data/lib/www_app/dsl.rb +0 -86
- data/lib/www_app/source.rb +0 -53
- data/specs/server-side/0050-on.rb +0 -64
- data/specs/server-side/0060-string.rb +0 -32
- /data/lib/public/{jquery.serialize-object.min.js → vendor/jquery.serialize-object.min.js} +0 -0
data/lib/www_app/TO.rb
ADDED
@@ -0,0 +1,897 @@
|
|
1
|
+
require 'mustache'
|
2
|
+
require 'escape_escape_escape'
|
3
|
+
|
4
|
+
# ===================================================================
|
5
|
+
# === Symbol customizations: ========================================
|
6
|
+
# ===================================================================
|
7
|
+
class Symbol
|
8
|
+
|
9
|
+
def to_mustache *args
|
10
|
+
WWW_App::Clean.mustache *args, self
|
11
|
+
end
|
12
|
+
|
13
|
+
end # === class Symbol
|
14
|
+
# ===================================================================
|
15
|
+
|
16
|
+
# ===================================================================
|
17
|
+
# === Mustache customizations: ======================================
|
18
|
+
# ===================================================================
|
19
|
+
Mustache.raise_on_context_miss = true
|
20
|
+
|
21
|
+
class Mustache
|
22
|
+
|
23
|
+
def render(data = template, ctx = {})
|
24
|
+
ctx = data
|
25
|
+
tpl = templateify(template)
|
26
|
+
|
27
|
+
begin
|
28
|
+
context.push(ctx)
|
29
|
+
tpl.render(context)
|
30
|
+
ensure
|
31
|
+
context.pop
|
32
|
+
end
|
33
|
+
end # === def render
|
34
|
+
|
35
|
+
class Generator
|
36
|
+
|
37
|
+
alias_method :w_syms_on_fetch, :on_fetch
|
38
|
+
|
39
|
+
def on_fetch(names)
|
40
|
+
if names.length == 2
|
41
|
+
"ctx[#{names.first.to_sym.inspect}, #{names.last.to_sym.inspect}]"
|
42
|
+
else
|
43
|
+
w_syms_on_fetch(names)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end # === class Generator
|
48
|
+
|
49
|
+
class Context
|
50
|
+
|
51
|
+
def fetch *args
|
52
|
+
raise ContextMiss.new("Can't find: #{args.inspect}") if args.size != 2
|
53
|
+
|
54
|
+
meth, key = args
|
55
|
+
|
56
|
+
@stack.each { |frame|
|
57
|
+
case
|
58
|
+
when frame.is_a?(Hash) && meth == :coll && !frame.has_key?(key)
|
59
|
+
return false
|
60
|
+
|
61
|
+
when frame.is_a?(Hash) && meth == :coll && frame.has_key?(key)
|
62
|
+
target = frame[key]
|
63
|
+
if target == true || target == false || target == nil || target.is_a?(Array) || target.is_a?(Hash)
|
64
|
+
return target
|
65
|
+
end
|
66
|
+
fail "Invalid value: #{key.inspect} (#{key.class})"
|
67
|
+
|
68
|
+
when frame.is_a?(Hash) && frame.has_key?(key)
|
69
|
+
return ::Escape_Escape_Escape.send(meth, frame[key])
|
70
|
+
|
71
|
+
end
|
72
|
+
}
|
73
|
+
|
74
|
+
raise ContextMiss.new("Can't find .#{meth}(#{key.inspect})")
|
75
|
+
end
|
76
|
+
|
77
|
+
# NOTE: :alias_method has to go after the re-definition of
|
78
|
+
# :fetch or else it uses the original :fetch method/definition.
|
79
|
+
alias_method :[], :fetch
|
80
|
+
|
81
|
+
|
82
|
+
end # === class Context
|
83
|
+
|
84
|
+
end # === class Mustache
|
85
|
+
# ===================================================================
|
86
|
+
|
87
|
+
class WWW_App
|
88
|
+
|
89
|
+
class Clean
|
90
|
+
|
91
|
+
MUSTACHE_Regex = /\A[a-z0-9\_\.]+\z/i
|
92
|
+
PERIOD = '.'.freeze
|
93
|
+
|
94
|
+
class << self
|
95
|
+
|
96
|
+
def mustache *args
|
97
|
+
case args.size
|
98
|
+
when 2
|
99
|
+
meth, val = args
|
100
|
+
escape_it = false
|
101
|
+
when 3
|
102
|
+
escape_it, meth, val = args
|
103
|
+
else
|
104
|
+
fail ::ArgumentError, "Unknown args: #{args}"
|
105
|
+
end
|
106
|
+
|
107
|
+
v = meth.to_s + PERIOD + val.to_s
|
108
|
+
fail "Unknown chars: #{args.inspect}" unless v[MUSTACHE_Regex]
|
109
|
+
|
110
|
+
if escape_it
|
111
|
+
"!{ #{v} }!"
|
112
|
+
else
|
113
|
+
"{{{ #{v} }}}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def method_missing name, *args
|
118
|
+
::Escape_Escape_Escape.send(name, *args)
|
119
|
+
end
|
120
|
+
|
121
|
+
end # === class << self
|
122
|
+
end # === class Clean
|
123
|
+
|
124
|
+
module TO
|
125
|
+
COMMA = ", ".freeze
|
126
|
+
SPACE = " ".freeze
|
127
|
+
NOTHING = "".freeze
|
128
|
+
GEM_PATH = File.dirname(__FILE__).sub('lib/www_app'.freeze, NOTHING)
|
129
|
+
VERSION = File.read(GEM_PATH + '/VERSION').strip
|
130
|
+
JS_FILE_PATHS = begin
|
131
|
+
public = "#{GEM_PATH}/lib/public"
|
132
|
+
all = Dir.glob("#{public}/**/*.{map,js}").map { |path|
|
133
|
+
"/www_app-#{VERSION}/#{path.gsub("#{public}/", NOTHING)}"
|
134
|
+
}
|
135
|
+
special = all.select { |f| f[/(instruct.js|www_app.js)$/] }
|
136
|
+
filtered = all.reject { |f| special.include?(f) }
|
137
|
+
filtered + special
|
138
|
+
end
|
139
|
+
|
140
|
+
INVALID_SCRIPT_TYPE_CHARS = /[^a-z0-9\-\/\_]+/
|
141
|
+
|
142
|
+
KEY_REQUIRED = proc { |hash, k|
|
143
|
+
fail "Key not set: #{k.inspect}"
|
144
|
+
}
|
145
|
+
|
146
|
+
def to_raw_text
|
147
|
+
str = ""
|
148
|
+
indent = 0
|
149
|
+
print_tag = lambda { |t|
|
150
|
+
info = t.select { |n| [:id, :class, :closed, :pseudo].include?( n ) }
|
151
|
+
info[:parent] = t[:parent] && t[:parent][:tag_name]
|
152
|
+
|
153
|
+
str += "#{" " * indent}#{t[:tag_name].inspect} -- #{info.inspect.gsub(/\A\{|\}\Z/, '')}\n"
|
154
|
+
indent += 1
|
155
|
+
if t[:children]
|
156
|
+
t[:children].each { |c|
|
157
|
+
str << print_tag.call(c)
|
158
|
+
}
|
159
|
+
end
|
160
|
+
indent -= 1
|
161
|
+
}
|
162
|
+
|
163
|
+
@tags.each { |t| print_tag.call(t) }
|
164
|
+
str
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_html *args
|
168
|
+
return @mustache.render(*args) if instance_variable_defined?(:@mustache)
|
169
|
+
|
170
|
+
final = ""
|
171
|
+
indent = 0
|
172
|
+
todo = @tags.dup
|
173
|
+
last = nil
|
174
|
+
stacks = {:js=>[], :script_tags=>[]}
|
175
|
+
last_open = nil
|
176
|
+
|
177
|
+
script_libs_added = false
|
178
|
+
|
179
|
+
doc = [
|
180
|
+
doc_type = {:tag_name=>:doc_type , :text => "<!DOCTYPE html>"},
|
181
|
+
html = {:tag_name=>:html , :children=>[
|
182
|
+
head = {:tag_name=>:head , :lang=>'en', :children=>[]},
|
183
|
+
body = {:tag_name=>:body , :children=>[]}
|
184
|
+
]}
|
185
|
+
]
|
186
|
+
style_tags = {:tag_name => :style_tags, :children => []}
|
187
|
+
|
188
|
+
tags = @tags.dup
|
189
|
+
while (t = tags.shift)
|
190
|
+
t_name = t[:tag_name]
|
191
|
+
parent = t[:parent]
|
192
|
+
|
193
|
+
case # ==============
|
194
|
+
when t_name == :title && !parent
|
195
|
+
fail "Title already set." if head[:children].detect { |c| c[:tag_name] == :title }
|
196
|
+
head[:children] << t
|
197
|
+
|
198
|
+
when t_name == :meta
|
199
|
+
head[:children] << t
|
200
|
+
|
201
|
+
when t_name == :style
|
202
|
+
style_tags[:children] << t
|
203
|
+
|
204
|
+
when t_name == :_ && !parent
|
205
|
+
body[:css] = (body[:css] || {}).merge(t[:css]) if t[:css]
|
206
|
+
body[:class] = (body[:class] || []).concat(t[:class]) if t[:class]
|
207
|
+
|
208
|
+
if t[:id]
|
209
|
+
fail ":body already has id: #{body[:id].inspect}, #{t[:id]}" if body[:id]
|
210
|
+
body[:id] = t[:id]
|
211
|
+
end
|
212
|
+
|
213
|
+
if t[:children]
|
214
|
+
body[:children].concat t[:children]
|
215
|
+
tags = t[:children].dup.concat(tags)
|
216
|
+
end
|
217
|
+
|
218
|
+
else # ==============
|
219
|
+
if !parent
|
220
|
+
body[:children] << t
|
221
|
+
end
|
222
|
+
|
223
|
+
if t[:css]
|
224
|
+
style_tags[:children] << t
|
225
|
+
end
|
226
|
+
|
227
|
+
if t[:children]
|
228
|
+
tags = t[:children].dup.concat(tags)
|
229
|
+
end
|
230
|
+
|
231
|
+
if t_name == :script
|
232
|
+
stacks[:script_tags] << t
|
233
|
+
end
|
234
|
+
|
235
|
+
if t_name == :js
|
236
|
+
stacks[:js].concat [css_selector(t[:parent])].concat(t[:value])
|
237
|
+
end
|
238
|
+
|
239
|
+
end # === case ========
|
240
|
+
end # === while
|
241
|
+
|
242
|
+
if body[:css] && !body[:css].empty?
|
243
|
+
style_tags[:children] << body
|
244
|
+
end
|
245
|
+
|
246
|
+
is_fragment = stacks[:script_tags].empty? && stacks[:js].empty? && style_tags[:children].empty? && head[:children].empty? && body.values_at(:css, :id, :class).compact.empty?
|
247
|
+
|
248
|
+
if is_fragment
|
249
|
+
doc = body[:children]
|
250
|
+
|
251
|
+
else # is doc
|
252
|
+
head[:children] << style_tags
|
253
|
+
content_type = head[:children].detect { |t| t[:tag_name] == :meta && t[:http_equiv] && t[:http_equiv].downcase=='Content-Type'.downcase }
|
254
|
+
if !content_type
|
255
|
+
head[:children].unshift(
|
256
|
+
{:tag_name=>:meta, :http_equiv=>'Content-Type', :content=>"text/html; charset=UTF-8"}
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
end # if is_fragment
|
261
|
+
|
262
|
+
todo = doc.dup
|
263
|
+
while (tag = todo.shift)
|
264
|
+
t_name = tag.is_a?(Hash) && tag[:tag_name]
|
265
|
+
|
266
|
+
case
|
267
|
+
|
268
|
+
when tag == :new_line
|
269
|
+
final << NEW_LINE
|
270
|
+
|
271
|
+
when tag == :open
|
272
|
+
attributes = stacks.delete :attributes
|
273
|
+
|
274
|
+
tag_sym = todo.shift
|
275
|
+
|
276
|
+
if [:script].include?(tag_sym) || (todo.first != :close && !indent.zero? && !HTML::NO_NEW_LINES.include?(last_open))
|
277
|
+
final << NEW_LINE << SPACES(indent)
|
278
|
+
end
|
279
|
+
|
280
|
+
if HTML::SELF_CLOSING_TAGS.include?(tag_sym)
|
281
|
+
final << (
|
282
|
+
attributes ?
|
283
|
+
"<#{tag_sym} #{attributes} />\n" :
|
284
|
+
"<#{tag_sym} />\n"
|
285
|
+
)
|
286
|
+
if todo.first == :close && todo[1] == tag_sym
|
287
|
+
todo.shift
|
288
|
+
todo.shift
|
289
|
+
end
|
290
|
+
|
291
|
+
else # === has closing tag
|
292
|
+
if attributes
|
293
|
+
final << "<#{tag_sym} #{attributes}>"
|
294
|
+
else
|
295
|
+
final << "<#{tag_sym}>"
|
296
|
+
end
|
297
|
+
end # === if HTML
|
298
|
+
|
299
|
+
last = indent
|
300
|
+
indent += 2
|
301
|
+
last_open = tag_sym
|
302
|
+
|
303
|
+
|
304
|
+
when tag == :close
|
305
|
+
indent -= 2
|
306
|
+
if last != indent
|
307
|
+
final << SPACES(indent)
|
308
|
+
end
|
309
|
+
last = indent
|
310
|
+
final << "</#{todo.shift}>"
|
311
|
+
|
312
|
+
when tag == :clean_attrs
|
313
|
+
attributes = todo.shift
|
314
|
+
target = todo.shift
|
315
|
+
tag_name = target[:tag_name]
|
316
|
+
|
317
|
+
attributes.each { |attr, val|
|
318
|
+
attributes[attr] = case
|
319
|
+
|
320
|
+
when attr == :src && tag_name == :script
|
321
|
+
fail ::ArgumentError, "Invalid type: #{val.inspect}" unless val.is_a?(String)
|
322
|
+
Clean.relative_href val
|
323
|
+
|
324
|
+
when attr == :type && tag_name == :script
|
325
|
+
clean = val.gsub(INVALID_SCRIPT_TYPE_CHARS, '')
|
326
|
+
clean = 'text/unknown' if clean.empty?
|
327
|
+
clean
|
328
|
+
|
329
|
+
when attr == :type && val == :hidden
|
330
|
+
'hidden'
|
331
|
+
|
332
|
+
when attr == :href && tag_name == :a
|
333
|
+
if val.is_a? Symbol
|
334
|
+
Clean.mustache :href, val
|
335
|
+
else
|
336
|
+
Clean.href val
|
337
|
+
end
|
338
|
+
|
339
|
+
when [:action, :src, :href].include?(attr)
|
340
|
+
Clean.relative_href(val)
|
341
|
+
|
342
|
+
when attr == :id
|
343
|
+
Clean.html_id(val.to_s)
|
344
|
+
|
345
|
+
when attr == :class
|
346
|
+
val.map { |name|
|
347
|
+
Clean.css_class_name(name.to_s)
|
348
|
+
}.join(" ".freeze)
|
349
|
+
|
350
|
+
when tag_name == :style && attr == :type
|
351
|
+
'text/css'
|
352
|
+
|
353
|
+
when ::WWW_App::HTML::TAGS_TO_ATTRIBUTES[tag_name].include?(attr)
|
354
|
+
Clean.html(val)
|
355
|
+
|
356
|
+
else
|
357
|
+
fail "Invalid attr: #{attr.inspect}"
|
358
|
+
|
359
|
+
end # case attr
|
360
|
+
|
361
|
+
} # === each attr
|
362
|
+
|
363
|
+
stacks[:attributes] = attributes.inject([]) { |memo, (k,v)|
|
364
|
+
memo << "#{k}=\"#{v}\""
|
365
|
+
memo
|
366
|
+
}.join " ".freeze
|
367
|
+
|
368
|
+
when t_name == :doc_type
|
369
|
+
if tag[:text] == "<!DOCTYPE html>"
|
370
|
+
final << tag[:text]
|
371
|
+
final << NEW_LINE
|
372
|
+
else
|
373
|
+
fail "Unknown doc type: #{tag[:text].inspect}"
|
374
|
+
end
|
375
|
+
|
376
|
+
when t_name == :text
|
377
|
+
final.<<(
|
378
|
+
tag[:skip_escape] ?
|
379
|
+
tag[:value] :
|
380
|
+
Clean.html(tag[:value])
|
381
|
+
)
|
382
|
+
|
383
|
+
|
384
|
+
when t_name == :meta
|
385
|
+
case
|
386
|
+
when tag[:http_equiv]
|
387
|
+
key_name = "http-equiv"
|
388
|
+
key_content = tag[:http_equiv].gsub(/[^a-zA-Z\/\;\ 0-9\=\-]+/, '')
|
389
|
+
content = tag[:content].gsub(/[^a-zA-Z\/\;\ 0-9\=\-]+/, '')
|
390
|
+
else
|
391
|
+
fail ArgumentError, tag.keys.inspect
|
392
|
+
end
|
393
|
+
|
394
|
+
final << (
|
395
|
+
%^#{SPACES(indent)}<meta #{key_name}="#{key_content}" content="#{content}" />^
|
396
|
+
)
|
397
|
+
|
398
|
+
when t_name == :html # === :html tag ================
|
399
|
+
todo = [
|
400
|
+
:clean_attrs, {:lang=>(tag[:lang] || 'en')}, tag,
|
401
|
+
:open, :html
|
402
|
+
].concat(tag[:children]).concat([:new_line, :close, :html]).concat(todo)
|
403
|
+
|
404
|
+
when t_name == :head # === :head tag =================
|
405
|
+
todo = [ :open, :head, :new_line ].
|
406
|
+
concat(tag[:children] || []).
|
407
|
+
concat([:new_line, :close, :head]).
|
408
|
+
concat(todo)
|
409
|
+
|
410
|
+
when t_name == :title && !parent(tag)
|
411
|
+
todo = [
|
412
|
+
:open, :title
|
413
|
+
].concat(tag[:children]).concat([:close, :title]).concat(todo)
|
414
|
+
|
415
|
+
when t_name == :_ # =============== :_ tag ========
|
416
|
+
nil # do nothing
|
417
|
+
|
418
|
+
when t_name == :js
|
419
|
+
next
|
420
|
+
|
421
|
+
when t_name == :script # =============== :script tag ===
|
422
|
+
|
423
|
+
attrs = tag.select { |k, v|
|
424
|
+
k == :src || k == :type || k == :class
|
425
|
+
}
|
426
|
+
|
427
|
+
new_todo = []
|
428
|
+
|
429
|
+
if attrs[:src] && !script_libs_added
|
430
|
+
new_todo << {:tag_name=>:js_to_script_tag}
|
431
|
+
script_libs_added = true
|
432
|
+
end
|
433
|
+
|
434
|
+
new_todo.concat [
|
435
|
+
:clean_attrs, attrs, tag,
|
436
|
+
:open, :script,
|
437
|
+
]
|
438
|
+
|
439
|
+
new_todo.concat(tag[:children]) if tag[:children]
|
440
|
+
|
441
|
+
if tag[:children] && !tag[:children].empty? && tag[:children].first[:tag_name] != :text && tag[:children].last[:tag_name] != :text
|
442
|
+
new_todo << :new_line
|
443
|
+
end
|
444
|
+
|
445
|
+
new_todo.concat [
|
446
|
+
:close, :script
|
447
|
+
].concat(todo)
|
448
|
+
|
449
|
+
todo = new_todo
|
450
|
+
|
451
|
+
when t_name == :js_to_script_tag
|
452
|
+
next if stacks[:js].empty? && stacks[:script_tags].empty?
|
453
|
+
stacks[:clean_text] ||= lambda { |raw_x|
|
454
|
+
x = case raw_x
|
455
|
+
when ::Symbol, ::String
|
456
|
+
Clean.html(raw_x.to_s)
|
457
|
+
when ::Array
|
458
|
+
raw_x.map { |x| stacks[:clean_text].call x }
|
459
|
+
when ::Numeric
|
460
|
+
x
|
461
|
+
else
|
462
|
+
fail "Unknown type for json: #{raw_x.inspect}"
|
463
|
+
end
|
464
|
+
}
|
465
|
+
|
466
|
+
script_tag = {:tag_name=>:script}.freeze
|
467
|
+
|
468
|
+
new_todo = []
|
469
|
+
JS_FILE_PATHS.each { |path|
|
470
|
+
new_todo.concat [
|
471
|
+
:clean_attrs, {:src=>path}, script_tag,
|
472
|
+
:open, :script,
|
473
|
+
:close, :script
|
474
|
+
]
|
475
|
+
}
|
476
|
+
|
477
|
+
clean_vals = stacks[:js].map { |raw_x| stacks[:clean_text].call(raw_x) }
|
478
|
+
content = <<-EOF
|
479
|
+
\n#{SPACES(indent)}WWW_App.run( #{::Escape_Escape_Escape.json_encode(clean_vals)} );
|
480
|
+
EOF
|
481
|
+
|
482
|
+
new_todo.concat [
|
483
|
+
:clean_attrs, {:type=>'application/javascript'}, script_tag,
|
484
|
+
:open, :script,
|
485
|
+
{:tag_name=>:text, :skip_escape=>true, :value=> content },
|
486
|
+
:close, :script
|
487
|
+
]
|
488
|
+
|
489
|
+
todo = new_todo.concat(todo)
|
490
|
+
|
491
|
+
when tag == :javascript
|
492
|
+
vals = todo.shift
|
493
|
+
|
494
|
+
when tag == :to_json
|
495
|
+
vals = todo.shift
|
496
|
+
::Escape_Escape_Escape.json_encode(to_clean_text(:javascript, vals))
|
497
|
+
|
498
|
+
|
499
|
+
when t_name == :style
|
500
|
+
next
|
501
|
+
|
502
|
+
when t_name == :style_tags # =============== <style ..> TAG =================
|
503
|
+
next if tag[:children].empty?
|
504
|
+
|
505
|
+
style_and_other_tags = tag[:children].dup
|
506
|
+
flatten = []
|
507
|
+
|
508
|
+
# === flatten groups
|
509
|
+
# style
|
510
|
+
# div, span {
|
511
|
+
# a:link, a:visited {
|
512
|
+
# --->
|
513
|
+
# style
|
514
|
+
# div a:link, div a:visited, span a:link, span a:visited {
|
515
|
+
#
|
516
|
+
prev = nil
|
517
|
+
|
518
|
+
while e = style_and_other_tags.shift
|
519
|
+
case
|
520
|
+
when e[:tag_name] == :style
|
521
|
+
style_and_other_tags = e[:children].dup.concat(style_and_other_tags)
|
522
|
+
|
523
|
+
when e[:tag_name] == :group
|
524
|
+
style_and_other_tags = e[:children].dup.concat(style_and_other_tags)
|
525
|
+
prev = nil
|
526
|
+
flatten << e
|
527
|
+
|
528
|
+
when parent?(e, :group)
|
529
|
+
if e[:__]
|
530
|
+
e[:__children] = []
|
531
|
+
end
|
532
|
+
|
533
|
+
if prev && prev[:__]
|
534
|
+
prev[:__children] << e
|
535
|
+
e[:__parent] = prev
|
536
|
+
end
|
537
|
+
prev = e
|
538
|
+
|
539
|
+
else
|
540
|
+
flatten << e
|
541
|
+
|
542
|
+
end # === case
|
543
|
+
end # === while
|
544
|
+
|
545
|
+
todo = [
|
546
|
+
:clean_attrs, {:type=>'text/css'}, {:tag_name=>:style},
|
547
|
+
:open, :style,
|
548
|
+
:flat_style_groups, flatten,
|
549
|
+
:close, :style
|
550
|
+
].concat(todo)
|
551
|
+
|
552
|
+
when tag == :flat_style_groups
|
553
|
+
|
554
|
+
indent += 2
|
555
|
+
css_final = ""
|
556
|
+
flatten = todo.shift
|
557
|
+
|
558
|
+
#
|
559
|
+
# Each produces:
|
560
|
+
#
|
561
|
+
# selectors {
|
562
|
+
# escaped/sanitized css;
|
563
|
+
# }
|
564
|
+
#
|
565
|
+
flatten.each { |style|
|
566
|
+
next if !style[:css] || style[:css].empty?
|
567
|
+
|
568
|
+
css_final << "\n" << SPACES(indent) << css_selector(style, :full) << " {\n".freeze
|
569
|
+
|
570
|
+
the_css = style[:css] || (parent?(style, :group) && style[:parent][:css])
|
571
|
+
if the_css
|
572
|
+
indent += 2
|
573
|
+
the_css.each { |raw_k, raw_val|
|
574
|
+
name = begin
|
575
|
+
clean_k = ::WWW_App::Clean.css_attr(raw_k.to_s.gsub('_','-'))
|
576
|
+
fail("Invalid name for css property name: #{raw_k.inspect}") if !clean_k || clean_k.empty?
|
577
|
+
clean_k
|
578
|
+
end
|
579
|
+
|
580
|
+
raw_val = raw_val.is_a?(Array) ? raw_val.join(COMMA) : raw_val.to_s
|
581
|
+
|
582
|
+
v = case
|
583
|
+
|
584
|
+
when name[IMAGE_AT_END]
|
585
|
+
case raw_val
|
586
|
+
when 'inherit', 'none'
|
587
|
+
raw_val
|
588
|
+
else
|
589
|
+
"url(#{Clean.href(raw_val)})"
|
590
|
+
end
|
591
|
+
|
592
|
+
when ::WWW_App::CSS::PROPERTIES.include?(raw_k)
|
593
|
+
Clean.css_value raw_val
|
594
|
+
|
595
|
+
else
|
596
|
+
fail "Invalid css attr: #{name.inspect}"
|
597
|
+
|
598
|
+
end # === case
|
599
|
+
|
600
|
+
css_final << SPACES(indent) << "#{name}: #{v};\n"
|
601
|
+
} # === each style
|
602
|
+
indent -= 2
|
603
|
+
end # === if style[:css]
|
604
|
+
|
605
|
+
css_final << SPACES(indent) << "}\n".freeze << SPACES(indent - 2)
|
606
|
+
}
|
607
|
+
|
608
|
+
indent -= 2
|
609
|
+
todo = [
|
610
|
+
{:tag_name=>:text, :skip_escape=>true, :value=>css_final}
|
611
|
+
].concat(todo)
|
612
|
+
|
613
|
+
when tag == :script # ============
|
614
|
+
h = vals
|
615
|
+
|
616
|
+
if h[:tag] == :script && h[:content] && !h[:content].empty?
|
617
|
+
return <<-EOF
|
618
|
+
<script type="text/css">
|
619
|
+
WWW_App.run(
|
620
|
+
#{to_clean_text :to_json, h[:content]}
|
621
|
+
);
|
622
|
+
</script>
|
623
|
+
EOF
|
624
|
+
end
|
625
|
+
|
626
|
+
html = h[:childs].map { |tag_index|
|
627
|
+
to_clean_text(:html, @tag_arr[tag_index])
|
628
|
+
}.join(NEW_LINE).strip
|
629
|
+
|
630
|
+
return unless h[:render?]
|
631
|
+
|
632
|
+
if html.empty? && h[:text]
|
633
|
+
html = if h[:text].is_a?(::Symbol)
|
634
|
+
h[:text].to_mustache(:html)
|
635
|
+
else
|
636
|
+
if h[:text].is_a?(::Hash)
|
637
|
+
if h[:text][:escape] == false
|
638
|
+
h[:text][:value]
|
639
|
+
else
|
640
|
+
Clean.html(h[:text][:value].strip)
|
641
|
+
end
|
642
|
+
else
|
643
|
+
Clean.html(h[:text].strip)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
end # === if html.empty?
|
647
|
+
|
648
|
+
(html = nil) if html.empty?
|
649
|
+
|
650
|
+
case
|
651
|
+
when h[:tag] == :render_if
|
652
|
+
key = h[:attrs][:key]
|
653
|
+
open = "{{# coll.#{key} }}"
|
654
|
+
close = "{{/ coll.#{key} }}"
|
655
|
+
|
656
|
+
when h[:tag] == :render_unless
|
657
|
+
key = h[:attrs][:key]
|
658
|
+
open = "{{^ coll.#{key} }}"
|
659
|
+
close = "{{/ coll.#{key} }}"
|
660
|
+
|
661
|
+
when Methods[:elements].include?(h[:tag])
|
662
|
+
open = "<#{h[:tag]}#{to_clean_text(:attrs, h)}"
|
663
|
+
if NO_END_TAGS.include?(h[:tag])
|
664
|
+
open += ' />'
|
665
|
+
close = nil
|
666
|
+
else
|
667
|
+
open += '>'
|
668
|
+
close = "</#{h[:tag]}>"
|
669
|
+
end
|
670
|
+
|
671
|
+
else
|
672
|
+
fail "Unknown html tag: #{h[:tag].inspect}"
|
673
|
+
|
674
|
+
end # === case h[:tag]
|
675
|
+
|
676
|
+
if h[:tag]
|
677
|
+
[open, html, close].compact.join
|
678
|
+
else
|
679
|
+
html
|
680
|
+
end # === if
|
681
|
+
|
682
|
+
when t_name && ::WWW_App::HTML::TAGS.include?(t_name) # === HTML tags =====
|
683
|
+
|
684
|
+
# ================================
|
685
|
+
# === Save this for last to allow
|
686
|
+
# certain tags
|
687
|
+
# to be over-riddent,
|
688
|
+
# like :script
|
689
|
+
# ================================
|
690
|
+
|
691
|
+
attrs = {}
|
692
|
+
attrs.default KEY_REQUIRED
|
693
|
+
|
694
|
+
new_todo = []
|
695
|
+
t2a = ::WWW_App::HTML::TAGS_TO_ATTRIBUTES
|
696
|
+
|
697
|
+
tag.each { |attr_name, v|
|
698
|
+
if t2a[:all].include?(attr_name) || (t2a[tag[:tag_name]] && t2a[tag[:tag_name]].include?(attr_name))
|
699
|
+
attrs[attr_name] = v
|
700
|
+
end
|
701
|
+
}
|
702
|
+
|
703
|
+
if !attrs.empty?
|
704
|
+
new_todo.concat [:clean_attrs, attrs, tag]
|
705
|
+
end
|
706
|
+
|
707
|
+
new_todo.concat [:open, tag[:tag_name]]
|
708
|
+
|
709
|
+
if tag[:children] && !tag[:children].empty?
|
710
|
+
new_todo.concat tag[:children]
|
711
|
+
if tag[:children].detect { |t| HTML::TAGS.include?(t[:tag_name]) }
|
712
|
+
new_todo << :new_line
|
713
|
+
end
|
714
|
+
end
|
715
|
+
new_todo.concat [:close, tag[:tag_name]]
|
716
|
+
todo = new_todo.concat(todo)
|
717
|
+
|
718
|
+
else
|
719
|
+
fail "Unknown: #{tag.inspect[0,30]}"
|
720
|
+
end # === case
|
721
|
+
end # === while
|
722
|
+
|
723
|
+
final
|
724
|
+
|
725
|
+
@mustache ||= begin
|
726
|
+
mustache = ::Mustache.new
|
727
|
+
mustache.template = Clean.clean_utf8(final)
|
728
|
+
mustache
|
729
|
+
end
|
730
|
+
|
731
|
+
to_html(*args)
|
732
|
+
end # === to_html
|
733
|
+
|
734
|
+
module OLD
|
735
|
+
#
|
736
|
+
# Examples
|
737
|
+
# dom_id -> the current dom id of the current element
|
738
|
+
# dom_id :default -> if no dom it, set/get default of current element
|
739
|
+
# dom_id {:element:} -> dom id of element: {:type=>:html, :tag=>...}
|
740
|
+
#
|
741
|
+
def dom_id *args
|
742
|
+
|
743
|
+
use_default = false
|
744
|
+
|
745
|
+
case
|
746
|
+
when args.empty?
|
747
|
+
e = tag!
|
748
|
+
# do nothing else
|
749
|
+
|
750
|
+
when args.size == 1 && args.first == :default
|
751
|
+
e = tag!
|
752
|
+
use_default = true
|
753
|
+
|
754
|
+
when args.size == 1 && args.first.is_a?(::Hash) && args.first[:type]==:html
|
755
|
+
e = args.first
|
756
|
+
|
757
|
+
else
|
758
|
+
fail "Unknown args: #{args.inspect}"
|
759
|
+
end
|
760
|
+
|
761
|
+
id = e[:attrs][:id]
|
762
|
+
return id if id
|
763
|
+
return nil unless use_default
|
764
|
+
|
765
|
+
e[:default_id] ||= begin
|
766
|
+
key = e[:tag]
|
767
|
+
@default_ids[key] ||= -1
|
768
|
+
@default_ids[key] += 1
|
769
|
+
end
|
770
|
+
end # === def dom_id
|
771
|
+
|
772
|
+
#
|
773
|
+
# Examples
|
774
|
+
# selector_id -> a series of ids and tags to be used as a JS selector
|
775
|
+
# Example:
|
776
|
+
# #id tag tag
|
777
|
+
# tag tag
|
778
|
+
#
|
779
|
+
#
|
780
|
+
def selector_id
|
781
|
+
i = tag![:tag_index]
|
782
|
+
id_given = false
|
783
|
+
classes = []
|
784
|
+
|
785
|
+
while !id_given && i && i > -1
|
786
|
+
e = @tag_arr[i]
|
787
|
+
id = dom_id e
|
788
|
+
(id_given = true) if id
|
789
|
+
|
790
|
+
if e[:tag] == :body && !classes.empty?
|
791
|
+
# do nothing because
|
792
|
+
# we do not want 'body tag.class tag.class'
|
793
|
+
else
|
794
|
+
case
|
795
|
+
when id
|
796
|
+
classes << "##{id}"
|
797
|
+
else
|
798
|
+
classes << e[:tag]
|
799
|
+
end # === case
|
800
|
+
end # === if
|
801
|
+
|
802
|
+
i = e[:parent_index]
|
803
|
+
end
|
804
|
+
|
805
|
+
return 'body' if classes.empty?
|
806
|
+
classes.join SPACE
|
807
|
+
end
|
808
|
+
|
809
|
+
#
|
810
|
+
# Examples
|
811
|
+
# css_id -> current css id of element.
|
812
|
+
# It uses the first class, if any, found.
|
813
|
+
# #id.class -> if #id and first class found.
|
814
|
+
# #id -> if class is missing and id given.
|
815
|
+
# #id tag.class -> if class given and ancestor has id.
|
816
|
+
# #id tag tag -> if no class given and ancestor has id.
|
817
|
+
# tag tag tag -> if no ancestor has class.
|
818
|
+
#
|
819
|
+
# css_id :my_class -> same as 'css_id()' except
|
820
|
+
# 'my_class' overrides :class attribute of current
|
821
|
+
# element.
|
822
|
+
#
|
823
|
+
#
|
824
|
+
def css_id *args
|
825
|
+
|
826
|
+
str_class = nil
|
827
|
+
|
828
|
+
case args.size
|
829
|
+
when 0
|
830
|
+
fail "Not in a tag." unless tag!
|
831
|
+
str_class = @css_id_override
|
832
|
+
when 1
|
833
|
+
str_class = args.first
|
834
|
+
else
|
835
|
+
fail "Unknown args: #{args.inspect}"
|
836
|
+
end
|
837
|
+
|
838
|
+
i = tag![:tag_index]
|
839
|
+
id_given = false
|
840
|
+
classes = []
|
841
|
+
|
842
|
+
while !id_given && i && i > -1
|
843
|
+
e = @tag_arr[i]
|
844
|
+
id = dom_id e
|
845
|
+
first_class = e[:attrs][:class].first
|
846
|
+
|
847
|
+
if id
|
848
|
+
id_given = true
|
849
|
+
if str_class
|
850
|
+
classes.unshift(
|
851
|
+
str_class.is_a?(::Symbol) ?
|
852
|
+
"##{id}.#{str_class}" :
|
853
|
+
"##{id}#{str_class}"
|
854
|
+
)
|
855
|
+
else
|
856
|
+
classes.unshift "##{id}"
|
857
|
+
end
|
858
|
+
|
859
|
+
else # no id given
|
860
|
+
if str_class
|
861
|
+
classes.unshift(
|
862
|
+
str_class.is_a?(::Symbol) ?
|
863
|
+
"#{e[:tag]}.#{str_class}" :
|
864
|
+
"#{e[:tag]}#{str_class}"
|
865
|
+
)
|
866
|
+
elsif first_class
|
867
|
+
classes.unshift "#{e[:tag]}.#{first_class}"
|
868
|
+
else
|
869
|
+
if e[:tag] != :body || (classes.empty?)
|
870
|
+
classes.unshift "#{e[:tag]}"
|
871
|
+
end
|
872
|
+
end # if first_class
|
873
|
+
|
874
|
+
end # if id
|
875
|
+
|
876
|
+
i = e[:parent_index]
|
877
|
+
break if i == @body[:tag_index] && !classes.empty?
|
878
|
+
end
|
879
|
+
|
880
|
+
classes.join SPACE
|
881
|
+
end
|
882
|
+
|
883
|
+
end # === module OLD
|
884
|
+
end # === module TO
|
885
|
+
end # === class WWW_App
|
886
|
+
|
887
|
+
|
888
|
+
|
889
|
+
__END__
|
890
|
+
<!DOCTYPE html>
|
891
|
+
<html lang="en">
|
892
|
+
<head>
|
893
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
894
|
+
:HEAD
|
895
|
+
</head>
|
896
|
+
:BODY
|
897
|
+
</html>
|