htmless 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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