htmless 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,179 @@
1
+ module Htmless
2
+ module Data
3
+
4
+ single_tags = %w[area base br col embed hr img input link meta param]
5
+
6
+ double_tags = %w[
7
+ a abbr article aside audio address b bdo blockquote body button canvas caption cite code colgroup command
8
+ datalist dd del details dfn div dl dt em fieldset figure footer form h1 h2 h3 h4 h5 h6 head header hgroup html
9
+ i iframe ins keygen kbd label legend li map mark meter nav noscript object ol optgroup option p pre progress
10
+ q ruby rt rp s samp script section select small source span strong style sub sup table tbody td textarea tfoot
11
+ th thead time title tr u ul var video]
12
+
13
+ global_attributes = %w[
14
+ accesskey class contenteditable contextmenu dir draggable dropzone hidden id lang
15
+ spellcheck style tabindex title onabort onblur oncanplay oncanplaythrough onchange
16
+ onclick oncontextmenu oncuechange ondblclick ondrag ondragend ondragenter ondragleave
17
+ ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus oninput
18
+ oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart
19
+ onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onpause onplay
20
+ onplaying onprogress onratechange onreadystatechange onreset onscroll onseeked onseeking
21
+ onselect onshow onstalled onsubmit onsuspend ontimeupdate onvolumechange onwaiting ]
22
+
23
+ tag_attributes = {
24
+ :a => %w[href target ping rel media hreflang type],
25
+ :abbr => %w[], :address => %w[],
26
+ :area => %w[alt coords shape href target ping rel media hreflang type],
27
+ :article => %w[], :aside => %w[],
28
+ :audio => %w[src preload autoplay mediagroup loop controls],
29
+ :b => %w[],
30
+ :base => %w[href target],
31
+ :bdi => %w[], :bdo => %w[],
32
+ :blockquote => %w[cite],
33
+ :body => %w[onafterprint onbeforeprint onbeforeunload onblur onerror onfocus onhashchange onload
34
+ onmessage onoffline ononline onpagehide onpageshow onpopstate onredo onresize onscroll
35
+ onstorage onundo onunload],
36
+ :br => %w[],
37
+ :button => %w[autofocus disabled form formaction formenctype formmethod formnovalidate formtarget name
38
+ type value],
39
+ :canvas => %w[width height],
40
+ :caption => %w[], :cite => %w[], :code => %w[], :col => %w[span],
41
+ :colgroup => %w[span],
42
+ :command => %w[type label icon disabled checked radiogroup],
43
+ :datalist => %w[option],
44
+ :dd => %w[],
45
+ :del => %w[cite datetime],
46
+ :details => %w[open],
47
+ :dfn => %w[], :div => %w[], :dl => %w[], :dt => %w[], :em => %w[],
48
+ :embed => %w[src type width height],
49
+ :fieldset => %w[disabled form name],
50
+ :figcaption => %w[], :figure => %w[], :footer => %w[],
51
+ :form => %w[action autocomplete enctype method name novalidate target accept_charset],
52
+ :h1 => %w[], :h2 => %w[], :h3 => %w[], :h4 => %w[], :h5 => %w[], :h6 => %w[], :head => %w[],
53
+ :header => %w[], :hgroup => %w[], :hr => %w[],
54
+ :html => %w[manifest],
55
+ :i => %w[],
56
+ :iframe => %w[src srcdoc name sandbox seamless width height],
57
+ :img => %w[alt src usemap ismap width height],
58
+ :input => %w[accept alt autocomplete autofocus checked dirname disabled form formaction formenctype
59
+ formmethod formnovalidate formtarget height list max maxlength min multiple name pattern
60
+ placeholder readonly required size src step type value width],
61
+ :ins => %w[cite datetime],
62
+ :kbd => %w[],
63
+ :keygen => %w[autofocus challenge disabled form keytype name],
64
+ :label => %w[form for],
65
+ :legend => %w[],
66
+ :li => %w[value],
67
+ :link => %w[href rel media hreflang type sizes],
68
+ :map => %w[name],
69
+ :mark => %w[],
70
+ :menu => %w[type label],
71
+ :meta => %w[name content charset http_equiv],
72
+ :meter => %w[value min max low high optimum form],
73
+ :nav => %w[], :noscript => %w[],
74
+ :object => %w[data type name usemap form width height],
75
+ :ol => %w[reversed start],
76
+ :optgroup => %w[disabled label],
77
+ :option => %w[disabled label selected value],
78
+ :output => %w[for form name],
79
+ :p => %w[],
80
+ :param => %w[name value],
81
+ :pre => %w[],
82
+ :progress => %w[value max form],
83
+ :q => %w[cite],
84
+ :rp => %w[], :rt => %w[], :ruby => %w[], :s => %w[], :samp => %w[],
85
+ :script => %w[src async defer type charset],
86
+ :section => %w[],
87
+ :select => %w[autofocus disabled form multiple name required size],
88
+ :small => %w[],
89
+ :source => %w[src type media],
90
+ :span => %w[], :strong => %w[],
91
+ :style => %w[media type scoped],
92
+ :sub => %w[], :summary => %w[], :sup => %w[],
93
+ :table => %w[border],
94
+ :tbody => %w[],
95
+ :td => %w[colspan rowspan headers],
96
+ :textarea => %w[autofocus cols disabled form maxlength name placeholder readonly required rows wrap],
97
+ :tfoot => %w[],
98
+ :th => %w[colspan rowspan headers scope],
99
+ :thead => %w[],
100
+ :time => %w[datetime pubdate],
101
+ :title => %w[], :tr => %w[],
102
+ :track => %w[default kind label src srclang],
103
+ :u => %w[], :ul => %w[], :var => %w[],
104
+ :video => %w[src poster preload autoplay mediagroup loop controls width height],
105
+ :wbr => %w[]
106
+ }
107
+
108
+ boolean_attributes = {
109
+ :all => %w{hidden draggable iscontenteditable spellcheck},
110
+ :style => %w{scoped},
111
+ :script => %w{async defer},
112
+ :ol => %w{reversed},
113
+ :time => %w{pubdate},
114
+ :img => %w{ismap},
115
+ :iframe => %w{seamless},
116
+ :track => %w{default},
117
+ :audio => %w{autoplay loop controls},
118
+ :video => %w{autoplay loop controls},
119
+ :form => %w{novalidate},
120
+ :fieldset => %w{disabled},
121
+ :input => %w{autofocus checked disabled formnovalidate multiple readonly
122
+ required},
123
+ :button => %w{autofocus disabled formnovalidate},
124
+ :select => %w{autofocus disabled multiple required},
125
+ :optgroup => %w{disabled},
126
+ :option => %w{disabled selected},
127
+ :textarea => %w{autofocus disabled readonly required},
128
+ :keygen => %w{autofocus disabled},
129
+ :details => %w{open},
130
+ :command => %w{disabled checked}
131
+ }
132
+
133
+ enumerable_attributes = {
134
+ 'all' => [{ 'dir' => %w{ltr rtl auto} }],
135
+ 'meta' => [{ 'http-equiv' => %w{content-language content-type default-style refresh set-cookie} }],
136
+ 'track' => [{ 'kind' => %w{subtitles captions descriptions chapters metadata} }],
137
+ 'video' => [{ 'preload' => %w{none metadata auto} }],
138
+ 'area' => [{ 'shape' => %w{circle default poly rect} }],
139
+ 'th' => [{ 'scope' => %w{row col rowgroup colgroup auto} }],
140
+ 'form' => [{ 'autocomplete' => %w{on off} }], # ala boolean
141
+ 'input' => [{ 'type' => %w{hidden text search tel url email password datetime date month week time
142
+ datetime-local number range color checkbox radio file submit image reset button} },
143
+ 'autocomplete' => %w{on off}], # ommited for default?,
144
+ 'button' => [{ 'type' => %w{submit reset button} }],
145
+ 'textarea' => [{ 'wrap' => %w{soft hard} }]
146
+ # TODO
147
+ }
148
+
149
+ attribute_type = lambda do |tag, attr|
150
+ if boolean_attributes[:all].include?(attr) || (boolean_attributes[tag] && boolean_attributes[tag].include?(attr))
151
+ :boolean
152
+ else
153
+ :string
154
+ end
155
+ end
156
+
157
+ HTML5 = OpenStruct.new(
158
+ :abstract_attributes => global_attributes.map { |attr| Attribute.new(attr.to_sym, attribute_type[name, attr]) },
159
+
160
+ :single_tags => single_tags.map(&:to_sym).map do |name|
161
+ Tag.new(name,
162
+ tag_attributes[name].map do |attr|
163
+ Attribute.new(attr.to_sym, attribute_type[name, attr])
164
+ end)
165
+ end,
166
+
167
+ :double_tags => double_tags.map(&:to_sym).map do |name|
168
+ Tag.new(name,
169
+ tag_attributes[name].map do |attr|
170
+ Attribute.new(attr.to_sym, attribute_type[name, attr])
171
+ end)
172
+ end)
173
+
174
+ #require 'pp'
175
+ #pp HTML5
176
+ #pp HTML5.abstract_attributes
177
+ #pp HTML5.simple_tags
178
+ end
179
+ end
@@ -0,0 +1,11 @@
1
+ require 'ostruct'
2
+
3
+ module Htmless
4
+ module Data
5
+
6
+ Attribute = Struct.new(:name, :type)
7
+ Tag = Struct.new(:name, :attributes)
8
+
9
+ end
10
+ end
11
+
@@ -0,0 +1,394 @@
1
+ module Htmless
2
+ module StubBuilderForDocumentation
3
+ class AbstractTag
4
+
5
+
6
+ def self.strings_injector
7
+ dynamic_class_base.strings_injector
8
+ end
9
+
10
+ def self._attributes=(_attributes)
11
+ @_attributes = _attributes
12
+ end
13
+
14
+ def self._attributes
15
+ @_attributes or (superclass._attributes if superclass.respond_to? :_attributes)
16
+ end
17
+
18
+ self._attributes = []
19
+
20
+ # @return [Array<String>] array of available attributes for the tag
21
+ def self.attributes
22
+ _attributes
23
+ end
24
+
25
+ # @return [String] tag's name
26
+ def self.tag_name
27
+ @tag || superclass.tag_name
28
+ end
29
+
30
+ protected
31
+
32
+ # sets the tag's name
33
+ # @api private
34
+ def self.set_tag(tag)
35
+ @tag = tag.to_s.freeze
36
+ end
37
+
38
+ set_tag 'abstract'
39
+
40
+ # defines dynamically methods for attributes
41
+ # @api private
42
+ def self.define_attribute_methods
43
+ attributes.each { |attr| define_attribute_method(attr) }
44
+ end
45
+
46
+ def self.inherited(base)
47
+ base.define_attribute_methods
48
+ end
49
+
50
+ # defines dynamically method for +attribute+
51
+ # @param [Data::Attribute] attribute
52
+ # @api private
53
+ def self.define_attribute_method(attribute)
54
+ return if instance_methods.include?(attribute.name)
55
+ name = attribute.name.to_s
56
+ content_rendering = attribute_content_rendering(attribute)
57
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
58
+ def #{name}(content#{' = true' if attribute.type == :boolean})
59
+ #{content_rendering}
60
+ self
61
+ end
62
+ RUBY
63
+ end
64
+
65
+ # @api private
66
+ # @param [Data::Attribute] attribute
67
+ # @returns Ruby code as string
68
+ def self.attribute_content_rendering(attribute)
69
+ name = attribute.name.to_s
70
+ case attribute.type
71
+ when :string
72
+ strings_injector.add "attr_#{name}", " #{name.gsub('_', '-')}=#{strings_injector[:quote]}"
73
+ "@output << @_str_attr_#{name} << CGI.escapeHTML(content.to_s) << @_str_quote"
74
+ when :boolean
75
+ strings_injector.add(
76
+ "attr_#{name}",
77
+ " #{name.gsub('_', '-')}=#{strings_injector[:quote]}#{name}#{strings_injector[:quote]}")
78
+ "@output << @_str_attr_#{name} if content"
79
+ end
80
+ end
81
+
82
+ # adds attribute to class, triggers dynamical creation of needed instance methods etc.
83
+ # @param [Array<Data::Attribute>] attributes
84
+ # @api private
85
+ def self.add_attributes(attributes)
86
+ attributes = [attributes] unless attributes.is_a? Array
87
+ raise ArgumentError, attributes.inspect unless attributes.all? { |v| v.is_a? Data::Attribute }
88
+ self.send :_attributes=, _attributes + attributes
89
+ define_attribute_methods
90
+ end
91
+
92
+ public
93
+
94
+ attr_reader :builder
95
+
96
+ # @api private
97
+ def initialize(builder)
98
+ @builder = builder
99
+ @output = builder.instance_eval { @_output }
100
+ @stack = builder.instance_eval { @_stack }
101
+ @classes = []
102
+ @tag_name = self.rclass.tag_name
103
+
104
+ self.rclass.strings_injector.inject_to self
105
+ end
106
+
107
+ # @api private
108
+ def open(attributes = nil)
109
+ @output << @_str_lt << @tag_name
110
+ @builder.current = self
111
+ attributes(attributes)
112
+ default
113
+ self
114
+ end
115
+
116
+ # it renders attribute using defined attribute method or by rendering attribute directly
117
+ # @param [String, Symbol] name
118
+ # @param [#to_s] value
119
+ def attribute(name, value)
120
+ return __send__(name, value) if respond_to?(name)
121
+ @output << @_str_space << name.to_s << @_str_eql_quote << CGI.escapeHTML(value.to_s) << @_str_quote
122
+ self
123
+ end
124
+
125
+ # @example
126
+ # div.attributes :id => 'id' # => <div id="id"></div>
127
+ # div :id => 'id', :class => %w{left right} # => <div id="id" class="left right"></div>
128
+ # img :src => 'path' # => <img src="path"></div>
129
+ # attribute`s methods are called on background (in this case #id is called)
130
+ def attributes(attrs)
131
+ return self unless attrs
132
+ attrs.each do |attr, value|
133
+ if value.kind_of?(Array)
134
+ __send__(attr, *value)
135
+ else
136
+ __send__(attr, value)
137
+ end
138
+ end
139
+ self
140
+ end
141
+
142
+ # original Ruby method for class, class is used for html classes
143
+ alias_method(:rclass, :class)
144
+
145
+ id_class = /^([\w]+)(!|)$/
146
+ data_attribute = /^data_([a-z_]+)$/
147
+ METHOD_MISSING_REGEXP = /#{data_attribute}|#{id_class}/ unless defined? METHOD_MISSING_REGEXP
148
+
149
+ # allows data-* attributes and id, classes by method_missing
150
+ def method_missing(method, *args, &block)
151
+ method = method.to_s
152
+ if method =~ METHOD_MISSING_REGEXP
153
+ if $1
154
+ self.rclass.add_attributes Data::Attribute.new(method, :string)
155
+ self.send method, *args
156
+ else
157
+ self.__send__($3 == '!' ? :id : :class, $2.gsub(@_str_underscore, @_str_dash))
158
+ self.attributes args.first
159
+ end
160
+ else
161
+ super(method, *args, &block)
162
+ end
163
+ end
164
+
165
+ #def respond_to?(symbol, include_private = false)
166
+ # symbol.to_s =~ METHOD_MISSING_REGEXP || super(symbol, include_private)
167
+ #end
168
+
169
+ strings_injector.add "attr_class", " class=#{strings_injector[:quote]}"
170
+ # adds classes to the tag by joining +classes+ with ' ' and skipping non-true classes
171
+ # @param [Array<#to_s>] classes
172
+ # @example
173
+ # class(!visible? && 'hidden', 'left') #=> class="hidden left" or class="left"
174
+ def class(*classes)
175
+ @classes.push(*classes.select { |c| c })
176
+ self
177
+ end
178
+
179
+ strings_injector.add "attr_id", " id=#{strings_injector[:quote]}"
180
+ # adds id to the tag by joining +values+ with '_'
181
+ # @param [Array<#to_s>] values
182
+ # @example
183
+ # id('user', 12) #=> id="user-15"
184
+ def id(*values)
185
+ @output << @_str_attr_id << CGI.escapeHTML(values.select { |v| v }.join(@_str_dash)) << @_str_quote
186
+ self
187
+ end
188
+
189
+ # adds id and class to a tag by an object
190
+ # @param [Object] obj
191
+ # To determine the class it looks for .htmless_ref or
192
+ # it uses class.to_s.underscore.tr('/', '-').
193
+ # To determine id it looks for #htmless_ref or it takes class and #id or #object_id.
194
+ # @example
195
+ # div[AUser.new].with { text 'a' } # => <div id="a_user_1" class="a_user">a</div>
196
+ def mimic(obj)
197
+ klass = if obj.class.respond_to? :htmless_ref
198
+ obj.class.htmless_ref
199
+ else
200
+ obj.class.to_s.scan(/[A-Z][a-z\d]*/).join('_').downcase.gsub('::', '-')
201
+ end
202
+
203
+ id = case
204
+ when obj.respond_to?(:htmless_ref)
205
+ obj.htmless_ref
206
+ when obj.respond_to?(:id)
207
+ [klass, obj.id]
208
+ else
209
+ [klass, obj.object_id]
210
+ end
211
+ #noinspection RubyArgCount
212
+ self.class(klass).id(id)
213
+ end
214
+
215
+ alias_method :[], :mimic
216
+
217
+ # renders data-* attributes by +hash+
218
+ # @param [Hash] hash
219
+ # @example
220
+ # div.data(:remote => true, :id => 'an_id') # => <div data-remote="true" data-id="an_id"></div>
221
+ def data(hash)
222
+ hash.each { |k, v| __send__ "data_#{k}", v }
223
+ self
224
+ end
225
+
226
+ protected
227
+
228
+ # this method is called on each tag opening, useful for default attributes
229
+ # @example html tag uses this to add xmlns attr.
230
+ # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
231
+ def default
232
+ end
233
+
234
+ # flushes classes to output
235
+ # @api private
236
+ def flush_classes
237
+ unless @classes.empty?
238
+ @output << @_str_attr_class << CGI.escapeHTML(@classes.join(@_str_space)) << @_str_quote
239
+ @classes.clear
240
+ end
241
+ end
242
+ end
243
+ class AbstractSingleTag < AbstractTag
244
+
245
+ nil
246
+
247
+ # @api private
248
+ # closes the tag
249
+ def flush
250
+ flush_classes
251
+ @output << @_str_slash_gt
252
+ nil
253
+ end
254
+ end
255
+ class AbstractDoubleTag < AbstractTag
256
+
257
+ nil
258
+
259
+ # @api private
260
+ def initialize(builder)
261
+ super
262
+ @content = nil
263
+ end
264
+
265
+ # allows data-* attributes and id, classes by method_missing
266
+ def method_missing(method, *args, &block)
267
+ method = method.to_s
268
+ if method =~ METHOD_MISSING_REGEXP
269
+ if $1
270
+ self.rclass.add_attributes Data::Attribute.new(method.to_sym, :string)
271
+ self.send method, *args, &block
272
+ else
273
+ attributes(if args.last.is_a?(Hash)
274
+ args.pop
275
+ end)
276
+ content args.first
277
+ self.__send__($3 == '!' ? :id : :class, $2.gsub(@_str_underscore, @_str_dash), &block)
278
+ end
279
+ else
280
+ super(method, *args, &block)
281
+ end
282
+ end
283
+
284
+ # @api private
285
+ def open(*args, &block)
286
+ attributes = if args.last.is_a?(Hash)
287
+ args.pop
288
+ end
289
+ content args[0]
290
+ super attributes
291
+ @stack << @tag_name
292
+ if block
293
+ with &block
294
+ else
295
+ self
296
+ end
297
+ end
298
+
299
+ # @api private
300
+ # closes the tag
301
+ def flush
302
+ flush_classes
303
+ @output << @_str_gt
304
+ @output << CGI.escapeHTML(@content) if @content
305
+ @output << @_str_slash_lt << @stack.pop << @_str_gt
306
+ @content = nil
307
+ end
308
+
309
+ # sets content of the double tag
310
+ # @example
311
+ # div 'content' # => <div>content</div>
312
+ # div.content 'content' # => <div>content</div>
313
+ # div :content => 'content' # => <div>content</div>
314
+ def content(content)
315
+ @content = content.to_s
316
+ self
317
+ end
318
+
319
+ # renders content of the double tag with block
320
+ # @yield content of the tag
321
+ # @example
322
+ # div { text 'content' } # => <div>content</div>
323
+ # div :id => 'id' do
324
+ # text 'content'
325
+ # end # => <div id="id">content</div>
326
+ def with
327
+ flush_classes
328
+ @output << @_str_gt
329
+ @content = nil
330
+ @builder.current = nil
331
+ yield
332
+ #if (content = yield).is_a?(String)
333
+ # @output << EscapeUtils.escape_html(content)
334
+ #end
335
+ @builder.flush
336
+ @output << @_str_slash_lt << @stack.pop << @_str_gt
337
+ nil
338
+ end
339
+
340
+ alias_method :w, :with
341
+
342
+ def mimic(obj, &block)
343
+ super(obj, &nil)
344
+ return with(&block) if block
345
+ self
346
+ end
347
+
348
+ def data(hash, &block)
349
+ super(hash, &nil)
350
+ return with(&block) if block
351
+ self
352
+ end
353
+
354
+ def attribute(name, value, &block)
355
+ super(name, value, &nil)
356
+ return with(&block) if block
357
+ self
358
+ end
359
+
360
+ def attributes(attrs, &block)
361
+ super(attrs, &nil)
362
+ return with(&block) if block
363
+ self
364
+ end
365
+
366
+ protected
367
+
368
+ # @api private
369
+ def self.define_attribute_method(attribute)
370
+ return if instance_methods(false).include?(attribute.name)
371
+ name = attribute.name.to_s
372
+
373
+ if instance_methods.include?(attribute.name)
374
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
375
+ def #{name}(*args, &block)
376
+ super(*args, &nil)
377
+ return with(&block) if block
378
+ self
379
+ end
380
+ RUBY
381
+ else
382
+ content_rendering = attribute_content_rendering(attribute)
383
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
384
+ def #{name}(content#{' = true' if attribute.type == :boolean}, &block)
385
+ #{content_rendering}
386
+ return with(&block) if block
387
+ self
388
+ end
389
+ RUBY
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end