htmless 0.4

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.
@@ -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