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,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]