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,144 @@
1
+ module Htmless
2
+ class Abstract
3
+ dynamic_classes do
4
+
5
+ def_class :AbstractDoubleTag, :AbstractTag do ###import
6
+ nil
7
+
8
+ # @api private
9
+ def initialize(builder)
10
+ super
11
+ @content = nil
12
+ end
13
+
14
+ # allows data-* attributes and id, classes by method_missing
15
+ def method_missing(method, *args, &block)
16
+ method = method.to_s
17
+ if method =~ METHOD_MISSING_REGEXP
18
+ if $1
19
+ self.rclass.add_attributes Data::Attribute.new(method.to_sym, :string)
20
+ self.send method, *args, &block
21
+ else
22
+ attributes(if args.last.is_a?(Hash)
23
+ args.pop
24
+ end)
25
+ content args.first
26
+ self.__send__($3 == '!' ? :id : :class, $2.gsub(@_str_underscore, @_str_dash), &block)
27
+ end
28
+ else
29
+ super(method, *args, &block)
30
+ end
31
+ end
32
+
33
+ # @api private
34
+ def open(*args, &block)
35
+ attributes = if args.last.is_a?(Hash)
36
+ args.pop
37
+ end
38
+ content args[0]
39
+ super attributes
40
+ @stack << @tag_name
41
+ if block
42
+ with &block
43
+ else
44
+ self
45
+ end
46
+ end
47
+
48
+ # @api private
49
+ # closes the tag
50
+ def flush
51
+ flush_classes
52
+ @output << @_str_gt
53
+ @output << CGI.escapeHTML(@content) if @content
54
+ @output << @_str_slash_lt << @stack.pop << @_str_gt
55
+ @content = nil
56
+ end
57
+
58
+ # sets content of the double tag
59
+ # @example
60
+ # div 'content' # => <div>content</div>
61
+ # div.content 'content' # => <div>content</div>
62
+ # div :content => 'content' # => <div>content</div>
63
+ def content(content)
64
+ @content = content.to_s
65
+ self
66
+ end
67
+
68
+ # renders content of the double tag with block
69
+ # @yield content of the tag
70
+ # @example
71
+ # div { text 'content' } # => <div>content</div>
72
+ # div :id => 'id' do
73
+ # text 'content'
74
+ # end # => <div id="id">content</div>
75
+ def with
76
+ flush_classes
77
+ @output << @_str_gt
78
+ @content = nil
79
+ @builder.current = nil
80
+ yield
81
+ #if (content = yield).is_a?(String)
82
+ # @output << EscapeUtils.escape_html(content)
83
+ #end
84
+ @builder.flush
85
+ @output << @_str_slash_lt << @stack.pop << @_str_gt
86
+ nil
87
+ end
88
+
89
+ alias_method :w, :with
90
+
91
+ def mimic(obj, &block)
92
+ super(obj, &nil)
93
+ return with(&block) if block
94
+ self
95
+ end
96
+
97
+ def data(hash, &block)
98
+ super(hash, &nil)
99
+ return with(&block) if block
100
+ self
101
+ end
102
+
103
+ def attribute(name, value, &block)
104
+ super(name, value, &nil)
105
+ return with(&block) if block
106
+ self
107
+ end
108
+
109
+ def attributes(attrs, &block)
110
+ super(attrs, &nil)
111
+ return with(&block) if block
112
+ self
113
+ end
114
+
115
+ protected
116
+
117
+ # @api private
118
+ def self.define_attribute_method(attribute)
119
+ return if instance_methods(false).include?(attribute.name)
120
+ name = attribute.name.to_s
121
+
122
+ if instance_methods.include?(attribute.name)
123
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
124
+ def #{name}(*args, &block)
125
+ super(*args, &nil)
126
+ return with(&block) if block
127
+ self
128
+ end
129
+ RUBY
130
+ else
131
+ content_rendering = attribute_content_rendering(attribute)
132
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
133
+ def #{name}(content#{' = true' if attribute.type == :boolean}, &block)
134
+ #{content_rendering}
135
+ return with(&block) if block
136
+ self
137
+ end
138
+ RUBY
139
+ end
140
+ end
141
+ end ###import
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,18 @@
1
+ module Htmless
2
+ class Abstract
3
+ dynamic_classes do
4
+ def_class :AbstractSingleTag, :AbstractTag do ###import
5
+ nil
6
+
7
+ # @api private
8
+ # closes the tag
9
+ def flush
10
+ flush_classes
11
+ @output << @_str_slash_gt
12
+ nil
13
+ end
14
+ end ###import
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,246 @@
1
+ module Htmless
2
+ class Abstract
3
+ dynamic_classes do
4
+ def_class :AbstractTag do ###import
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 ###import
243
+
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,252 @@
1
+ require 'cgi'
2
+ #require 'active_support/core_ext/string/inflections'
3
+
4
+ require 'htmless/dynamic_classes'
5
+ require 'htmless/strings_injector'
6
+ require 'htmless/data'
7
+ require 'htmless/data/html5'
8
+ require "htmless/pool"
9
+ require "htmless/helper"
10
+
11
+ module Htmless
12
+
13
+ # Abstract implementation of Builder
14
+ class Abstract
15
+ extend DynamicClasses
16
+
17
+ require "htmless/abstract/abstract_tag"
18
+ require "htmless/abstract/abstract_single_tag"
19
+ require "htmless/abstract/abstract_double_tag"
20
+
21
+ def self.strings_injector
22
+ @strings_injector ||= StringsInjector.new do
23
+ add :lt, '<'
24
+ add :gt, '>'
25
+ add :slash_lt, '</'
26
+ add :slash_gt, ' />'
27
+ add :dash, '-'
28
+ add :underscore, '_'
29
+ add :space, ' '
30
+ add :spaces, Array.new(300) { |i| (' ' * i).freeze }
31
+ add :newline, "\n"
32
+ add :quote, '"'
33
+ add :eql, '='
34
+ add :eql_quote, self[:eql] + self[:quote]
35
+ add :comment_start, '<!--'
36
+ add :comment_end, '-->'
37
+ add :cdata_start, '<![CDATA['
38
+ add :cdata_end, ']]>'
39
+ end
40
+ end
41
+
42
+ # << faster then +
43
+ # yield faster then block.call
44
+ # accessing ivar and constant is faster then accesing hash or cvar
45
+ # class_eval faster then define_method
46
+ # beware of strings in methods -> creates a lot of garbage
47
+
48
+ def self.tags=(tags)
49
+ @tags = tags
50
+ end
51
+
52
+ def self.tags
53
+ @tags or (superclass.tags if superclass.respond_to? :tags)
54
+ end
55
+
56
+ def tags
57
+ self.class.tags
58
+ end
59
+
60
+ self.tags = []
61
+
62
+ protected
63
+
64
+ # defines instance method for +tag+ in builder
65
+ def self.define_tag(tag)
66
+ tag = tag.to_s
67
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
68
+ def #{tag}(*args, &block)
69
+ flush
70
+ @_#{tag}.open(*args, &block)
71
+ end
72
+ RUBY
73
+ self.tags += [tag]
74
+ end
75
+
76
+ public
77
+
78
+
79
+ # current tag being builded
80
+ attr_accessor :_current
81
+ alias_method :current, :_current
82
+ alias_method :current=, :_current=
83
+
84
+
85
+ # creates a new builder
86
+ # This is quite expensive, HammerBuilder::Pool should be used
87
+ def initialize()
88
+ @_output = ""
89
+ @_stack = []
90
+ @_current = nil
91
+
92
+ self.class.strings_injector.inject_to self
93
+
94
+ # tag classes initialization
95
+ tags.each do |klass|
96
+ instance_variable_set(:"@_#{klass}", self.class.dynamic_classes[camelize_string(klass).to_sym].new(self))
97
+ end
98
+ end
99
+
100
+ # escapes +text+ to output
101
+ def text(text)
102
+ flush
103
+ @_output << CGI.escapeHTML(text.to_s)
104
+ end
105
+
106
+ # unescaped +text+ to output
107
+ def raw(text)
108
+ flush
109
+ @_output << text.to_s
110
+ end
111
+
112
+ # inserts +comment+
113
+ def comment(comment)
114
+ flush
115
+ @_output << @_str_comment_start << comment.to_s << @_str_comment_end
116
+ end
117
+
118
+ # insersts CDATA with +content+
119
+ def cdata(content)
120
+ flush
121
+ @_output << @_str_cdata_start << content.to_s << @_str_cdata_end
122
+ end
123
+
124
+ # renders html5 doc type
125
+ # @example
126
+ # html5 # => <!DOCTYPE html>
127
+ def html5
128
+ raw "<!DOCTYPE html>\n"
129
+ end
130
+
131
+ # resets the builder to the state after creation - much faster then creating a new one
132
+ def reset
133
+ flush
134
+ @_output.clear
135
+ @_stack.clear
136
+ self
137
+ end
138
+
139
+ #def capture
140
+ # flush
141
+ # _output = @_output.clone
142
+ # _stack = @_stack.clone
143
+ # @_output.clear
144
+ # @_stack.clear
145
+ # yield
146
+ # to_html
147
+ #ensure
148
+ # @_output.replace _output
149
+ # @_stack.replace _stack
150
+ #end
151
+
152
+ # enables you to evaluate +block+ inside the builder with +variables+
153
+ # @example
154
+ # HammerBuilder::Formatted.new.go_in('asd') do |string|
155
+ # div string
156
+ # end.to_html #=> "<div>asd</div>"
157
+ #
158
+ def go_in(*variables, &block)
159
+ instance_exec *variables, &block
160
+ self
161
+ end
162
+
163
+ alias_method :dive, :go_in
164
+
165
+ # sets instance variables when block is yielded
166
+ # @param [Hash{String => Object}] instance_variables hash of names and values to set
167
+ # @yield block when variables are set, variables are cleaned up afterwards
168
+ def set_variables(instance_variables)
169
+ instance_variables.each { |name, value| instance_variable_set("@#{name}", value) }
170
+ yield(self)
171
+ instance_variables.each { |name, _| remove_instance_variable("@#{name}") }
172
+ self
173
+ end
174
+
175
+ # @return [String] output
176
+ def to_html()
177
+ flush
178
+ @_output.clone
179
+ end
180
+
181
+ # flushes open tag
182
+ # @api private
183
+ def flush
184
+ if @_current
185
+ @_current.flush
186
+ @_current = nil
187
+ end
188
+ end
189
+
190
+ # renders +object+ with +method+
191
+ # @param [Object] object an object to render
192
+ # @param [Symbol] method a method name which is used for rendering
193
+ # @param args arguments passed to rendering method
194
+ # @yield block passed to rendering method
195
+ def render(object, method, *args, &block)
196
+ object.__send__ method, self, *args, &block
197
+ self
198
+ end
199
+
200
+ alias_method :r, :render
201
+
202
+ # renders js
203
+ # @option options [Boolean] :cdata (false) should cdata be used?
204
+ # @example
205
+ # js 'a_js_function();' #=> <script type="text/javascript">a_js_function();</script>
206
+ def js(js, options = {})
207
+ use_cdata = options.delete(:cdata) || false
208
+ script({ :type => "text/javascript" }.merge(options)) { use_cdata ? cdata(js) : text(js) }
209
+ end
210
+
211
+ # joins and renders +collection+ with +glue+
212
+ # @param [Array<Proc, Object>] collection of objects or lambdas
213
+ # @param [Proc, String] glue can be String which is rendered with #text or block to render
214
+ # @yield how to render objects from +collection+, Proc in collection does not use this block
215
+ # @example
216
+ # join([1, 1.2], lambda { text ', ' }) {|o| text o } # => "1, 1.2"
217
+ # join([1, 1.2], ', ') {|o| text o } # => "1, 1.2"
218
+ # join([->{ text 1 }, 1.2], ', ') {|o| text o } # => "1, 1.2"
219
+ def join(collection, glue = nil, &it)
220
+ # TODO as helper? two block method call #join(collection, &item).with(&glue)
221
+ glue_block = case glue
222
+ when String
223
+ lambda { text glue }
224
+ when Proc
225
+ glue
226
+ else
227
+ lambda {}
228
+ end
229
+
230
+ collection.each_with_index do |obj, i|
231
+ glue_block.call() if i > 0
232
+ obj.is_a?(Proc) ? obj.call : it.call(obj)
233
+ end
234
+ end
235
+
236
+
237
+ private
238
+
239
+ def self.camelize_string(term)
240
+ term.
241
+ to_s.
242
+ sub(/^[a-z\d]*/) { $&.capitalize }.
243
+ gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }.
244
+ gsub('/', '::')
245
+ end
246
+
247
+ def camelize_string(term)
248
+ self.class.camelize_string term
249
+ end
250
+
251
+ end
252
+ end