htmless 0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,216 @@
1
+ module Htmless
2
+
3
+ # When extended into a class it enables easy defining and extending classes in the class.
4
+ #
5
+ # class A
6
+ # extend DynamicClasses
7
+ # dynamic_classes do
8
+ # def_class :A do
9
+ # def to_s
10
+ # 'a'
11
+ # end
12
+ # end
13
+ # def_class :B, :A do
14
+ # class_eval <<-RUBYCODE, __FILE__, __LINE__+1
15
+ # def to_s
16
+ # super + 'b'
17
+ # end
18
+ # RUBYCODE
19
+ # end
20
+ # end
21
+ # end
22
+ #
23
+ # class B < A
24
+ # end
25
+ #
26
+ # class C < A
27
+ # dynamic_classes do
28
+ # extend_class :A do
29
+ # def to_s
30
+ # 'aa'
31
+ # end
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # puts A.dc[:A] # => #<Class:0x00000001d449b8(A.dc[:A])>
37
+ # puts B.dc[:A] # => #<Class:0x00000001d42398(B.dc[:A])>
38
+ # puts B.dc[:A].new # => a
39
+ # puts B.dc[:B].new # => ab
40
+ # puts C.dc[:B].new # => aab
41
+ #
42
+ # Last example is the most interesting. It prints 'aab' not 'ab' because of the extension in class C. Class :B has
43
+ # as ancestor extended class :A from C therefore the two 'a'.
44
+ module DynamicClasses
45
+
46
+ # Adds ability to describe itself when class is defined without constant
47
+ module Describable
48
+ def self.included(base)
49
+ base.singleton_class.send :alias_method, :original_to_s, :to_s
50
+ base.extend ClassMethods
51
+ end
52
+
53
+ module ClassMethods
54
+ # sets +description+
55
+ # @param [String] description
56
+ def _description=(description)
57
+ @_description = description
58
+ end
59
+
60
+ def to_s
61
+ super.gsub(/>$/, "(#{@_description})>")
62
+ end
63
+ end
64
+
65
+ def to_s
66
+ klass = respond_to?(:rclass) ? self.rclass : self.class
67
+ super.gsub(klass.original_to_s, klass.to_s)
68
+ end
69
+ end
70
+
71
+ class DescribableClass
72
+ include Describable
73
+ end
74
+
75
+ ClassDefinition = Struct.new(:name, :base, :superclass_or_name, :definition)
76
+ ClassExtension = Struct.new(:name, :base, :definition)
77
+
78
+ class Classes
79
+ attr_reader :base, :class_definitions, :classes, :class_extensions
80
+
81
+ def initialize(base)
82
+ raise unless base.is_a? Class
83
+ @base = base
84
+ @class_definitions = { }
85
+ @class_extensions = { }
86
+ @classes = { }
87
+ end
88
+
89
+ # define a class
90
+ # @param [Symbol] name
91
+ # @param [Symbol, Class, nil] superclass_or_name
92
+ # when Symbol then dynamic class is found
93
+ # when Class then this class is used
94
+ # when nil then Object is used
95
+ # @yield definition block is evaluated inside the class defining it
96
+ def def_class(name, superclass_or_name = nil, &definition)
97
+ raise ArgumentError, "name is not a Symbol" unless name.is_a?(Symbol)
98
+ unless superclass_or_name.is_a?(Symbol) || superclass_or_name.is_a?(Class) || superclass_or_name.nil?
99
+ raise ArgumentError, "superclass_or_name is not a Symbol, Class or nil"
100
+ end
101
+ raise ArgumentError, "definition is nil" unless definition
102
+ raise ArgumentError, "Class #{name} already defined" if class_definition(name)
103
+ @class_definitions[name] = ClassDefinition.new(name, base, superclass_or_name, definition)
104
+ end
105
+
106
+ # extends already defined class by adding a child,
107
+ # @param [Symbol] name
108
+ # @yield definition block is evaluated inside the class extending it
109
+ def extend_class(name, &definition)
110
+ raise ArgumentError, "name is not a Symbol" unless name.is_a?(Symbol)
111
+ raise ArgumentError, "definition is nil" unless definition
112
+ raise ArgumentError, "Class #{name} not defined" unless class_definition(name)
113
+ @class_extensions[name] = ClassExtension.new(name, base, definition)
114
+ end
115
+
116
+ # triggers loading of all defined classes
117
+ def load!
118
+ class_names.each { |name| self[name] }
119
+ end
120
+
121
+ # @return [Class] defined class
122
+ def [](name)
123
+ return @classes[name] if @classes[name]
124
+ return nil unless klass_definition = class_definition(name)
125
+
126
+ superclass = case klass_definition.superclass_or_name
127
+ when Symbol then
128
+ self[klass_definition.superclass_or_name]
129
+ when Class then
130
+ klass = Class.new(klass_definition.superclass_or_name)
131
+ klass.send :include, Describable
132
+ klass._description = "Describable#{klass_definition.superclass_or_name}"
133
+ klass
134
+ when nil then
135
+ DescribableClass
136
+ end
137
+
138
+ set_up_klass = lambda do |klass, description, block|
139
+ klass._description = description
140
+ klass.instance_variable_set :@dynamic_class_base, base
141
+ klass.singleton_class.send :attr_reader, :dynamic_class_base
142
+ klass.class_eval &block
143
+ end
144
+
145
+ klass = Class.new(superclass)
146
+ set_up_klass.call klass, "#{base}.dc[:#{klass_definition.name}]", klass_definition.definition
147
+
148
+ class_extensions(name).each do |klass_extension|
149
+ klass = Class.new klass
150
+ set_up_klass.call klass, "#{base}.dc[:#{klass_extension.name}]", klass_extension.definition
151
+ end
152
+
153
+ @classes[name] = klass
154
+ end
155
+
156
+ def class_names
157
+ ancestors.map(&:class_definitions).map(&:keys).flatten
158
+ end
159
+
160
+ private
161
+
162
+ def class_definition(name)
163
+ @class_definitions[name] or (ancestor.send :class_definition, name if ancestor)
164
+ end
165
+
166
+ def class_extensions(name)
167
+ ([*(ancestor.send :class_extensions, name if ancestor)] + [@class_extensions[name]]).compact
168
+ end
169
+
170
+ def ancestors
171
+ ([self] + [*(ancestor.ancestors if ancestor)]).compact
172
+ end
173
+
174
+ def ancestor
175
+ @base.superclass.dynamic_classes if @base.superclass.kind_of?(DynamicClasses)
176
+ end
177
+ end
178
+
179
+ # hook to create Classes instance
180
+ def self.extended(base)
181
+ base.send :create_dynamic_classes
182
+ super
183
+ end
184
+
185
+ # hook to create Classes instance in descendants
186
+ def inherited(base)
187
+ base.send :create_dynamic_classes
188
+ super
189
+ end
190
+
191
+ # call this to get access to Classes instance to define/extend classes inside +definition+
192
+ # calls Classes#load! to preload defined classes
193
+ # @yield [Proc, nil] definition
194
+ # a Proc enables writing class definitions/extensions
195
+ # @return [Classes] when definition is nil
196
+ def dynamic_classes(&definition)
197
+ if definition
198
+ @dynamic_classes.instance_eval &definition
199
+ # @dynamic_classes.load!
200
+ nil
201
+ else
202
+ @dynamic_classes
203
+ end
204
+ end
205
+
206
+ alias_method :dc, :dynamic_classes
207
+
208
+ private
209
+
210
+ def create_dynamic_classes
211
+ @dynamic_classes = Classes.new(self)
212
+ end
213
+ end
214
+ end
215
+
216
+
@@ -0,0 +1,43 @@
1
+ require 'htmless/standard'
2
+
3
+ module Htmless
4
+ # Builder implementation with formatting (indented by ' ')
5
+ # Slow down is less then 1%
6
+ class Formatted < Standard
7
+
8
+ dynamic_classes do
9
+ extend_class :AbstractTag do
10
+ def open(attributes = nil)
11
+ @output << @_str_newline << @_str_spaces.fetch(@stack.size, @_str_space) << @_str_lt << @tag_name
12
+ @builder.current = self
13
+ attributes(attributes)
14
+ default
15
+ self
16
+ end
17
+ end
18
+
19
+ extend_class :AbstractDoubleTag do
20
+ def with
21
+ flush_classes
22
+ @output << @_str_gt
23
+ @content = nil
24
+ @builder.current = nil
25
+ yield
26
+ #if (content = yield).is_a?(String)
27
+ # @output << EscapeUtils.escape_html(content, false)
28
+ #end
29
+ @builder.flush
30
+ @output << @_str_newline << @_str_spaces.fetch(@stack.size-1, @_str_space) << @_str_slash_lt <<
31
+ @stack.pop << @_str_gt
32
+ nil
33
+ end
34
+ end
35
+ end
36
+
37
+ def comment(comment)
38
+ flush
39
+ @_output << @_str_newline << @_str_spaces.fetch(@_stack.size, @_str_space) << @_str_comment_start <<
40
+ comment.to_s << @_str_comment_end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,24 @@
1
+ module Htmless
2
+ module Helper
3
+
4
+ # adds instance method to the class. Method accepts any instance of builder and returns it after rendering.
5
+ # @param [Symbol] method_name
6
+ # @yield [self] builder_block is evaluated inside builder and accepts instance of a rendered object as parameter
7
+ # @example
8
+ # class User
9
+ # # ...
10
+ # include HammerBuilder::Helper
11
+ #
12
+ # builder :menu do |user|
13
+ # li user.name
14
+ # end
15
+ # end
16
+ #
17
+ # User.new.menu(HammerBuilder::Standard.get).to_html! #=> "<li>Name</li>"
18
+ def builder(method_name, &builder_block)
19
+ define_method(method_name) do |builder, *args|
20
+ builder.dive(self, *args, &builder_block)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ module Htmless
2
+
3
+ # Creating builder instances is expensive, therefore you can use Pool to go around that
4
+ # @example
5
+ # pool = Pool.new Formatted
6
+ # pool.get.go_in do
7
+ # # some rendering
8
+ # end.to_xhtml! # => output and releases the builder to pool
9
+ class Pool
10
+
11
+ module Helper
12
+ def release
13
+ @_origin.release self
14
+ end
15
+
16
+ # @return [String] output and releases the builder to pool
17
+ def to_html!
18
+ to_html
19
+ ensure
20
+ release
21
+ end
22
+ end
23
+
24
+ attr_reader :klass
25
+
26
+ def initialize(klass)
27
+ @klass = klass
28
+ @pool = []
29
+ klass.send :include, Helper
30
+ end
31
+
32
+ # This the preferred way of getting new Builder. If you forget to release it, it does not matter -
33
+ # builder gets GCed after you lose reference
34
+ # @return [Abstract]
35
+ def get
36
+ if @pool.empty?
37
+ @klass.new.instance_exec(self) { |origin| @_origin = origin; self }
38
+ else
39
+ @pool.pop
40
+ end
41
+ end
42
+
43
+ # returns +builder+ back into pool *DONT* forget to lose the reference to the +builder+
44
+ # @param [Abstract]
45
+ def release(builder)
46
+ raise TypeError unless builder.is_a? @klass
47
+ builder.reset
48
+ @pool.push builder
49
+ nil
50
+ end
51
+
52
+ def size
53
+ @pool.size
54
+ end
55
+ end
56
+
57
+ class SynchronizedPool < Pool
58
+ def initialize(klass)
59
+ super(klass)
60
+ @mutex = Mutex.new
61
+ end
62
+
63
+ def get
64
+ @mutex.synchronize { super }
65
+ end
66
+
67
+ def release(builder)
68
+ @mutex.synchronize { super(builder) }
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,40 @@
1
+ require 'htmless/formatted'
2
+
3
+ warn '"htmless/rails" is very early experiment'
4
+
5
+ module Htmless::Rails
6
+ class AbstractBuilder
7
+ extend Htmless::Helper
8
+
9
+ attr_reader :controller
10
+
11
+ def initialize(controller)
12
+ @controller = controller
13
+ end
14
+
15
+ end
16
+
17
+ ActionController::Renderers.add :hb do |klass_or_obj, options|
18
+ obj = case
19
+ when klass_or_obj.kind_of?(Class)
20
+ klass_or_obj.new(self)
21
+ when klass_or_obj.nil? || klass_or_obj == self
22
+ self.class.to_s.gsub(/Controller/, 'Builder').constantize.new(self)
23
+ else
24
+ klass_or_obj
25
+ end
26
+
27
+ $htmless_pool ||= Htmless::SynchronizedPool.new(Htmless::Formatted) # FIXME
28
+
29
+ render(
30
+ :text => $htmless_pool.get.
31
+ go_in { render obj, "#{options[:method] || options[:template]}" }.to_html!,
32
+ :layout => true)
33
+ end
34
+
35
+
36
+ end
37
+
38
+
39
+
40
+
@@ -0,0 +1,48 @@
1
+ require 'htmless/abstract'
2
+
3
+ module Htmless
4
+
5
+ # Builder implementation without formating (one line output)
6
+ class Standard < Abstract
7
+
8
+ dynamic_classes do
9
+ extend_class :AbstractTag do
10
+ # add global HTML5 attributes
11
+ self.add_attributes Data::HTML5.abstract_attributes
12
+ end
13
+
14
+ Data::HTML5.double_tags.each do |tag|
15
+ next if tag.name == :html
16
+
17
+ def_class Abstract.camelize_string(tag.name.to_s).to_sym, :AbstractDoubleTag do
18
+ set_tag tag.name
19
+ self.add_attributes tag.attributes
20
+ end
21
+
22
+ base.define_tag(tag.name)
23
+ end
24
+
25
+ html_tag = Data::HTML5.double_tags.find { |t| t.name == :html }
26
+ def_class :Html, :AbstractDoubleTag do
27
+ set_tag html_tag.name
28
+ self.add_attributes html_tag.attributes
29
+
30
+ def default
31
+ attribute :xmlns ,'http://www.w3.org/1999/xhtml'
32
+ end
33
+ end
34
+ base.define_tag(html_tag.name)
35
+
36
+ Data::HTML5.single_tags.each do |tag|
37
+ def_class Abstract.camelize_string(tag.name.to_s).to_sym, :AbstractSingleTag do
38
+ set_tag tag.name
39
+ self.add_attributes tag.attributes
40
+ end
41
+
42
+ base.define_tag(tag.name)
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+
@@ -0,0 +1,43 @@
1
+ module Htmless
2
+ class StringsInjector
3
+
4
+ attr_reader :strings, :objects_to_update
5
+
6
+ def initialize(&block)
7
+ @strings = Hash.new {|hash, key| raise ArgumentError "missing key #{key}" }
8
+ @objects_to_update = []
9
+ instance_eval &block
10
+ end
11
+
12
+ def [](name)
13
+ strings[name]
14
+ end
15
+
16
+ def add(name, value)
17
+ name = name.to_sym
18
+ raise "string #{name} is already set to #{value}" if strings.has_key?(name) && self[name] != value
19
+ replace name, value
20
+ end
21
+
22
+ def replace(name, value)
23
+ name = name.to_sym
24
+ strings[name] = value
25
+ update_objects name
26
+ end
27
+
28
+ def inject_to(obj)
29
+ @objects_to_update << obj
30
+ strings.keys.each { |name| update_object obj, name }
31
+ end
32
+
33
+ private
34
+
35
+ def update_objects(name)
36
+ objects_to_update.each { |obj| update_object obj, name }
37
+ end
38
+
39
+ def update_object(obj, name)
40
+ obj.instance_variable_set(:"@_str_#{name}", self[name])
41
+ end
42
+ end
43
+ end
data/lib/htmless.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "htmless/formatted"
2
+
3
+
data/lib/js.rb ADDED
@@ -0,0 +1,176 @@
1
+ path = File.expand_path(File.dirname(__FILE__))
2
+ $: << path unless $:.include? path
3
+
4
+ require "htmless"
5
+ require 'v8'
6
+
7
+ #cxt = V8::Context.new
8
+ #p cxt.eval(<<-JS)
9
+ # var a = a || {};
10
+ # JSON.stringify(a)
11
+ #JS
12
+
13
+ # TODO escape " generated by builder
14
+
15
+ class JSBuilder < Htmless::Standard
16
+ strings_injector.strings.each do |key, value|
17
+ if value =~ /"/
18
+ strings_injector.replace key, value.gsub('"', '\"')
19
+ end
20
+ end
21
+ end
22
+
23
+ POOL = Htmless::Pool.new(JSBuilder)
24
+
25
+ class Injector # < BasicObject
26
+ def initialize(name)
27
+ @name, @methods = name, []
28
+ end
29
+
30
+ def [](key)
31
+ method_missing(key)
32
+ end
33
+
34
+ def method_missing(name, *args, &block)
35
+ @methods << name.to_s
36
+ self
37
+ end
38
+
39
+ def __reset__!
40
+ @methods.clear
41
+ end
42
+
43
+ def __name__
44
+ @name
45
+ end
46
+
47
+ def to_s
48
+ js = "{{#{@name}.#{@methods.join('.')}}}"
49
+ __reset__!
50
+ js
51
+ end
52
+
53
+ def to_str
54
+ to_s
55
+ end
56
+ end
57
+
58
+ module ToJs
59
+ include Htmless::Helper
60
+
61
+ def builder(name, &block)
62
+ JsRenderers.add_renderer self, name, &block
63
+ super name, &block
64
+ end
65
+
66
+ def as_json
67
+ { :class => self.class.to_s, :data => json_data }
68
+ end
69
+ end
70
+
71
+ class JsRenderersImpl
72
+ def initialize
73
+ @render_methods = { }
74
+ @templates = { }
75
+ end
76
+
77
+ def add_renderer(klass, name, &block)
78
+ @render_methods[klass] ||= { }
79
+ @render_methods[klass][name] = block
80
+ end
81
+
82
+ def renderer(klass, name)
83
+ @render_methods[klass][name]
84
+ end
85
+
86
+ def to_js
87
+ @render_methods.each do |klass, names|
88
+ @templates[klass] ||= { }
89
+ names.each do |name, block|
90
+ @templates[klass][name] = JsMethodBuilder.new(klass, name, block).build
91
+ end
92
+ end
93
+ @templates
94
+ end
95
+ end
96
+
97
+ class JsMethodBuilder
98
+ attr_reader :klass, :name, :block, :builder
99
+
100
+ def initialize(klass, name, block)
101
+ @klass, @name, @block = klass, name, block
102
+ @builder = POOL.get
103
+ @injectors = Array.new(block.arity) { |i| Injector.new("arg#{i}") }
104
+ end
105
+
106
+ def build
107
+ builder.raw head
108
+ builder.dive(*@injectors, &block)
109
+ builder.raw foot
110
+ builder.to_html!.gsub(/\{\{([^}]*)\}\}/, '"+\1+"')
111
+ ensure
112
+ @builder = nil
113
+ end
114
+
115
+ def head
116
+ <<-JS.chomp
117
+ var templates = templates || {};
118
+ templates["#{klass}"] = templates["#{klass}"] || {};
119
+ templates["#{klass}"].#{name} = function (#{@injectors.map(&:__name__).join(', ')}) {
120
+ var _buf;
121
+ _buf = "
122
+ JS
123
+ end
124
+
125
+ def foot
126
+ %(";\n return _buf;\n};\n)
127
+ end
128
+ end
129
+
130
+ JsRenderers = JsRenderersImpl.new
131
+
132
+ A = Module.new
133
+ class A::Record
134
+ extend ToJs
135
+
136
+ def name
137
+ 'a_record'
138
+ end
139
+
140
+ def klass
141
+ 'class'
142
+ end
143
+
144
+ def items
145
+ [{ :name => 'a' }, { :name => 'b' }]
146
+ end
147
+
148
+ def json_data
149
+ { :name => name,
150
+ :klass => klass,
151
+ :items => items
152
+ }
153
+ end
154
+
155
+ builder :content do |r|
156
+ p :class => r.klass do
157
+ text r.name
158
+ #ul do
159
+ # r.items.each do |item|
160
+ # r.item self, item
161
+ # end
162
+ #end
163
+ end
164
+ end
165
+
166
+ builder :item do |_, item|
167
+ li item[:name]
168
+ end
169
+ end
170
+
171
+ record = A::Record.new
172
+
173
+ #puts POOL.get.render(record, :content).to_html!
174
+
175
+ puts JsRenderers.to_js[A::Record][:content]
176
+ puts JsRenderers.to_js[A::Record][:item]