hammer_builder 0.1.0 → 0.1.1

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.
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # HammerBuilder
2
2
 
3
3
  [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
4
- is a xhtml5 builder written in Ruby 1.9.2. It does not introduce anything special, you just
4
+ is a xhtml5 builder written in and for Ruby 1.9.2. It does not introduce anything special, you just
5
5
  use Ruby to get your xhtml. [`HammerBuilder`](https://github.com/ruby-hammer/hammer-builder)
6
6
  has been written with three objectives:
7
7
 
@@ -16,6 +16,7 @@ has been written with three objectives:
16
16
  * Yardoc: [http://hammer.pitr.ch/hammer-builder/](http://hammer.pitr.ch/hammer-builder/)
17
17
  * Issues: [https://github.com/ruby-hammer/hammer-builder/issues](https://github.com/ruby-hammer/hammer-builder/issues)
18
18
  * Changelog: [http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html](http://hammer.pitr.ch/hammer-builder/file.CHANGELOG.html)
19
+ * Gem: [https://rubygems.org/gems/hammer_builder](https://rubygems.org/gems/hammer_builder)
19
20
 
20
21
  ## Syntax
21
22
 
@@ -1,6 +1,7 @@
1
1
  require 'cgi'
2
2
  require 'active_support/core_ext/class/inheritable_attributes'
3
3
  require 'active_support/core_ext/string/inflections'
4
+ require 'hammer_builder/dynamic_classes'
4
5
 
5
6
  module HammerBuilder
6
7
  EXTRA_ATTRIBUTES = {
@@ -133,7 +134,7 @@ module HammerBuilder
133
134
  ]
134
135
 
135
136
  DOUBLE_TAGS = [
136
- 'a', 'abbr', 'article', 'aside', 'audio', 'address',
137
+ 'a', 'abbr', 'article', 'aside', 'audio', 'address',
137
138
  'b', 'bdo', 'blockquote', 'body', 'button',
138
139
  'canvas', 'caption', 'cite', 'code', 'colgroup', 'command',
139
140
  'datalist', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt',
@@ -207,64 +208,6 @@ module HammerBuilder
207
208
  end
208
209
  end
209
210
 
210
- module RedefinableClassTree
211
- # defines new class
212
- # @param [Symbol] class_name
213
- # @param [Symbol] superclass_name e.g. :AbstractEmptyTag
214
- # @yield definition block which is evaluated inside the new class, doing so defining the class's methods etc.
215
- def define_class(class_name, superclass_name = nil, &definition)
216
- class_name = class_name(class_name)
217
- superclass_name = class_name(superclass_name) if superclass_name
218
-
219
- raise "class: '#{class_name}' already defined" if respond_to? method_class(class_name)
220
-
221
- define_singleton_method method_class(class_name) do |builder|
222
- builder.instance_variable_get("@#{method_class(class_name)}") || begin
223
- klass = builder.send(method_class_definition(class_name), builder)
224
- builder.const_set class_name, klass
225
- builder.instance_variable_set("@#{method_class(class_name)}", klass)
226
- end
227
- end
228
-
229
- define_singleton_method method_class_definition(class_name) do |builder|
230
- superclass = if superclass_name
231
- builder.send method_class(superclass_name), builder
232
- else
233
- Object
234
- end
235
- Class.new(superclass, &definition)
236
- end
237
- end
238
-
239
- # extends existing class
240
- # @param [Symbol] class_name
241
- # @yield definition block which is evaluated inside the new class, doing so extending the class's methods etc.
242
- def extend_class(class_name, &definition)
243
- raise "class: '#{class_name}' not defined" unless respond_to? method_class(class_name)
244
-
245
- define_singleton_method method_class_definition(class_name) do |builder|
246
- ancestor = super(builder)
247
- count = 1; count += 1 while builder.const_defined? "#{class_name}Super#{count}"
248
- builder.const_set "#{class_name}Super#{count}", ancestor
249
- Class.new(ancestor, &definition)
250
- end
251
- end
252
-
253
- private
254
-
255
- def class_name(klass)
256
- klass.to_s.camelize
257
- end
258
-
259
- def method_class(klass)
260
- "#{klass.to_s.underscore}_class"
261
- end
262
-
263
- def method_class_definition(klass)
264
- "#{method_class(klass)}_definition"
265
- end
266
- end
267
-
268
211
  # Creating builder instances is expensive, therefore you can use Pool to go around that
269
212
  module Pool
270
213
  def self.included(base)
@@ -275,7 +218,7 @@ module HammerBuilder
275
218
  module ClassMethods
276
219
  # This the preferred way of getting new Builder. If you forget to release it, it does not matter -
277
220
  # builder gets GCed after you lose reference
278
- # @return [Standard, Formated]
221
+ # @return [Standard, Formated]
279
222
  def get
280
223
  mutex.synchronize do
281
224
  if free_builders.empty?
@@ -321,7 +264,7 @@ module HammerBuilder
321
264
 
322
265
  # Abstract implementation of Builder
323
266
  class Abstract
324
- extend RedefinableClassTree
267
+ extend DynamicClasses
325
268
  include Pool
326
269
 
327
270
  # << faster then +
@@ -330,142 +273,146 @@ module HammerBuilder
330
273
  # class_eval faster then define_method
331
274
  # beware of strings in methods -> creates a lot of garbage
332
275
 
333
- define_class :AbstractTag do
334
- def initialize(builder)
335
- @builder = builder
336
- @output = builder.instance_eval { @output }
337
- @stack = builder.instance_eval { @stack }
338
- @classes = []
339
- set_tag
340
- end
276
+ dc do
341
277
 
342
- def open(attributes = nil)
343
- @output << LT << @tag
344
- @builder.current = self
345
- attributes(attributes)
346
- default
347
- self
348
- end
278
+ define :AbstractTag do
279
+ def initialize(builder)
280
+ @builder = builder
281
+ @output = builder.instance_eval { @output }
282
+ @stack = builder.instance_eval { @stack }
283
+ @classes = []
284
+ set_tag
285
+ end
349
286
 
350
- # @example
351
- # div.attributes :id => 'id' # => <div id="id"></div>
352
- def attributes(attrs)
353
- return self unless attrs
354
- attrs.each do |attr, value|
355
- __send__(attr, *value)
287
+ def open(attributes = nil)
288
+ @output << LT << @tag
289
+ @builder.current = self
290
+ attributes(attributes)
291
+ default
292
+ self
356
293
  end
357
- self
358
- end
359
294
 
360
- # @example
361
- # div.attribute :id, 'id' # => <div id="id"></div>
362
- def attribute(attribute, content)
363
- @output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
364
- end
295
+ # @example
296
+ # div.attributes :id => 'id' # => <div id="id"></div>
297
+ def attributes(attrs)
298
+ return self unless attrs
299
+ attrs.each do |attr, value|
300
+ __send__(attr, *value)
301
+ end
302
+ self
303
+ end
365
304
 
366
- alias_method(:rclass, :class)
305
+ # @example
306
+ # div.attribute :id, 'id' # => <div id="id"></div>
307
+ # @deprecated Please use {#attributes} instead
308
+ def attribute(attribute, content) # TODO lose the method in 0.2
309
+ warn ("method #attribute is deprecated use #attributes instead, called from:#{caller[0]}" )
310
+ @output << SPACE << attribute.to_s << EQL_QUOTE << CGI.escapeHTML(content.to_s) << QUOTE
311
+ end
367
312
 
368
- class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
313
+ alias_method(:rclass, :class)
369
314
 
370
- def self.attributes
371
- self._attributes
372
- end
315
+ class_inheritable_array :_attributes, :instance_writer => false, :instance_reader => false
373
316
 
374
- # allows data-* attributes
375
- def method_missing(method, *args, &block)
376
- if method.to_s =~ /data_([a-z_]+)/
377
- self.rclass.attributes = [method.to_s]
378
- self.send method, *args, &block
379
- else
380
- super
317
+ def self.attributes
318
+ self._attributes
381
319
  end
382
- end
383
-
384
- protected
385
320
 
386
- # sets the right tag in descendants
387
- def self.set_tag(tag)
388
321
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
389
- def set_tag
390
- @tag = '#{tag}'.freeze
322
+ # allows data-* attributes
323
+ def method_missing(method, *args, &block)
324
+ if method.to_s =~ /data_([a-z_]+)/
325
+ self.rclass.attributes = [method.to_s]
326
+ self.send method, *args, &block
327
+ else
328
+ super
391
329
  end
330
+ end
392
331
  RUBYCODE
393
- end
394
332
 
395
- # this method is called on each tag opening, useful for default attributes
396
- # @example html tag uses this to add xmlns attr.
397
- # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
398
- def default
399
- end
333
+ protected
400
334
 
401
- # defines dynamically methods for attributes
402
- def self.define_attributes
403
- attributes.each do |attr|
404
- next if instance_methods.include?(attr.to_sym)
335
+ # sets the right tag in descendants
336
+ def self.set_tag(tag)
405
337
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
338
+ def set_tag
339
+ @tag = '#{tag}'.freeze
340
+ end
341
+ RUBYCODE
342
+ end
343
+
344
+ set_tag 'abstract'
345
+
346
+ # this method is called on each tag opening, useful for default attributes
347
+ # @example html tag uses this to add xmlns attr.
348
+ # html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
349
+ def default
350
+ end
351
+
352
+ # defines dynamically methods for attributes
353
+ def self.define_attributes
354
+ attributes.each do |attr|
355
+ next if instance_methods.include?(attr.to_sym)
356
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
406
357
  def #{attr}(content)
407
358
  @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
408
359
  self
409
360
  end
410
- RUBYCODE
361
+ RUBYCODE
362
+ end
363
+ define_attribute_constants
411
364
  end
412
- define_attribute_constants
413
- end
414
365
 
415
- # defines constant strings not to make garbage
416
- def self.define_attribute_constants
417
- attributes.each do |attr|
418
- const = "attr_#{attr}".upcase
419
- HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
366
+ # defines constant strings not to make garbage
367
+ def self.define_attribute_constants
368
+ attributes.each do |attr|
369
+ const = "attr_#{attr}".upcase
370
+ HammerBuilder.const_set const, " #{attr.gsub('_', '-')}=\"".freeze unless HammerBuilder.const_defined?(const)
371
+ end
420
372
  end
421
- end
422
-
423
- # adds attribute to class, triggers dynamical creation of needed instance methods etc.
424
- def self.attributes=(attributes)
425
- self._attributes = attributes
426
- define_attributes
427
- end
428
373
 
429
- # flushes classes to output
430
- def flush_classes
431
- unless @classes.empty?
432
- @output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
433
- @classes.clear
374
+ # adds attribute to class, triggers dynamical creation of needed instance methods etc.
375
+ def self.attributes=(attributes)
376
+ self._attributes = attributes
377
+ define_attributes
434
378
  end
435
- end
436
379
 
437
- def set_tag
438
- @tag = 'abstract'
439
- end
380
+ # flushes classes to output
381
+ def flush_classes
382
+ unless @classes.empty?
383
+ @output << ATTR_CLASS << CGI.escapeHTML(@classes.join(SPACE)) << QUOTE
384
+ @classes.clear
385
+ end
386
+ end
440
387
 
441
- public
388
+ public
442
389
 
443
- # global HTML5 attributes
444
- self.attributes = GLOBAL_ATTRIBUTES
390
+ # global HTML5 attributes
391
+ self.attributes = GLOBAL_ATTRIBUTES
445
392
 
446
- alias :[] :id
393
+ alias :[] :id
447
394
 
448
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
395
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
449
396
  def class(*classes)
450
397
  @classes.push(*classes)
451
398
  self
452
399
  end
453
- RUBYCODE
454
- end
400
+ RUBYCODE
401
+ end
455
402
 
456
- define_class :AbstractEmptyTag, :AbstractTag do
457
- def flush
458
- flush_classes
459
- @output << SLASH_GT
460
- nil
403
+ define :AbstractEmptyTag, :AbstractTag do
404
+ def flush
405
+ flush_classes
406
+ @output << SLASH_GT
407
+ nil
408
+ end
461
409
  end
462
- end
463
410
 
464
- define_class :AbstractDoubleTag, :AbstractTag do
465
- # defined by class_eval because there is a super calling, causing error:
466
- # super from singleton method that is defined to multiple classes is not supported;
467
- # this will be fixed in 1.9.3 or later (NotImplementedError)
468
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
411
+ define :AbstractDoubleTag, :AbstractTag do
412
+ # defined by class_eval because there is a super calling, causing error:
413
+ # super from singleton method that is defined to multiple classes is not supported;
414
+ # this will be fixed in 1.9.3 or later (NotImplementedError)
415
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
469
416
  def initialize(builder)
470
417
  super
471
418
  @content = nil
@@ -484,61 +431,62 @@ module HammerBuilder
484
431
  self
485
432
  end
486
433
  end
487
- RUBYCODE
434
+ RUBYCODE
488
435
 
489
- def flush
490
- flush_classes
491
- @output << GT
492
- @output << CGI.escapeHTML(@content) if @content
493
- @output << SLASH_LT << @stack.pop << GT
494
- @content = nil
495
- end
436
+ def flush
437
+ flush_classes
438
+ @output << GT
439
+ @output << CGI.escapeHTML(@content) if @content
440
+ @output << SLASH_LT << @stack.pop << GT
441
+ @content = nil
442
+ end
496
443
 
497
- # sets content of the double tag
498
- def content(content)
499
- @content = content.to_s
500
- self
501
- end
444
+ # sets content of the double tag
445
+ def content(content)
446
+ @content = content.to_s
447
+ self
448
+ end
502
449
 
503
- # renders content of the double tag with block
504
- def with
505
- flush_classes
506
- @output << GT
507
- @content = nil
508
- @builder.current = nil
509
- yield
510
- # if (content = yield).is_a?(String)
511
- # @output << CGI.escapeHTML(content)
512
- # end
513
- @builder.flush
514
- @output << SLASH_LT << @stack.pop << GT
515
- nil
516
- end
450
+ # renders content of the double tag with block
451
+ def with
452
+ flush_classes
453
+ @output << GT
454
+ @content = nil
455
+ @builder.current = nil
456
+ yield
457
+ # if (content = yield).is_a?(String)
458
+ # @output << CGI.escapeHTML(content)
459
+ # end
460
+ @builder.flush
461
+ @output << SLASH_LT << @stack.pop << GT
462
+ nil
463
+ end
517
464
 
518
- protected
465
+ protected
519
466
 
520
- def self.define_attributes
521
- attributes.each do |attr|
522
- next if instance_methods(false).include?(attr.to_sym)
523
- if instance_methods.include?(attr.to_sym)
524
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
467
+ def self.define_attributes
468
+ attributes.each do |attr|
469
+ next if instance_methods(false).include?(attr.to_sym)
470
+ if instance_methods.include?(attr.to_sym)
471
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
525
472
  def #{attr}(*args, &block)
526
473
  super(*args, &nil)
527
474
  return with(&block) if block
528
475
  self
529
476
  end
530
- RUBYCODE
531
- else
532
- class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
477
+ RUBYCODE
478
+ else
479
+ class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
533
480
  def #{attr}(content, &block)
534
481
  @output << ATTR_#{attr.upcase} << CGI.escapeHTML(content.to_s) << QUOTE
535
482
  return with(&block) if block
536
483
  self
537
484
  end
538
- RUBYCODE
485
+ RUBYCODE
486
+ end
539
487
  end
488
+ define_attribute_constants
540
489
  end
541
- define_attribute_constants
542
490
  end
543
491
  end
544
492
 
@@ -549,6 +497,7 @@ module HammerBuilder
549
497
 
550
498
  # defines instance method for +tag+ in builder
551
499
  def self.define_tag(tag)
500
+ tag = tag.to_s
552
501
  class_eval <<-RUBYCODE, __FILE__, __LINE__ + 1
553
502
  def #{tag}(*args, &block)
554
503
  flush
@@ -568,7 +517,7 @@ module HammerBuilder
568
517
  @current = nil
569
518
  # tag classes initialization
570
519
  tags.values.each do |klass|
571
- instance_variable_set(:"@#{klass}", self.class.send("#{klass}_class", self.class).new(self))
520
+ instance_variable_set(:"@#{klass}", self.class.dc[klass.camelize.to_sym].new(self))
572
521
  end
573
522
  end
574
523
 
@@ -631,6 +580,13 @@ module HammerBuilder
631
580
  self
632
581
  end
633
582
 
583
+ def set_variables(instance_variables)
584
+ instance_variables.each {|name,value| instance_variable_set("@#{name}", value) }
585
+ yield
586
+ instance_variables.each {|name,_| remove_instance_variable("@#{name}") }
587
+ self
588
+ end
589
+
634
590
  # @return [String] output
635
591
  def to_xhtml()
636
592
  flush
@@ -655,66 +611,86 @@ module HammerBuilder
655
611
  # Builder implementation without formating (one line)
656
612
  class Standard < Abstract
657
613
 
658
- (DOUBLE_TAGS - ['html']).each do |tag|
659
- define_class tag.camelize , :AbstractDoubleTag do
660
- set_tag tag
661
- self.attributes = EXTRA_ATTRIBUTES[tag]
614
+ dc do
615
+ (DOUBLE_TAGS - ['html']).each do |tag|
616
+ define tag.camelize.to_sym , :AbstractDoubleTag do
617
+ set_tag tag
618
+ self.attributes = EXTRA_ATTRIBUTES[tag]
619
+ end
620
+
621
+ base.define_tag(tag)
662
622
  end
663
623
 
664
- define_tag(tag)
665
- end
624
+ define :Html, :AbstractDoubleTag do
625
+ set_tag 'html'
626
+ self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
666
627
 
667
- define_class :Html, :AbstractDoubleTag do
668
- set_tag 'html'
669
- self.attributes = ['xmlns'] + EXTRA_ATTRIBUTES['html']
628
+ def default
629
+ xmlns('http://www.w3.org/1999/xhtml')
630
+ end
631
+ end
632
+ base.define_tag('html')
633
+
634
+ EMPTY_TAGS.each do |tag|
635
+ define tag.camelize.to_sym, :AbstractEmptyTag do
636
+ set_tag tag
637
+ self.attributes = EXTRA_ATTRIBUTES[tag]
638
+ end
670
639
 
671
- def default
672
- xmlns('http://www.w3.org/1999/xhtml')
640
+ base.define_tag(tag)
673
641
  end
674
642
  end
675
643
 
676
- define_tag('html')
677
-
678
644
  def js(js , options = {})
645
+ flush
679
646
  script({:type => "text/javascript"}.merge(options)) { cdata js }
680
647
  end
681
648
 
682
- EMPTY_TAGS.each do |tag|
683
- define_class tag.camelize, :AbstractEmptyTag do
684
- set_tag tag
685
- self.attributes = EXTRA_ATTRIBUTES[tag]
649
+ def join(collection, glue, &it)
650
+ flush
651
+ glue_block = if glue.is_a? String
652
+ lambda { text glue }
653
+ else
654
+ glue
655
+ end
656
+
657
+ collection.each_with_index do |obj, i|
658
+ glue_block.call() if i > 0
659
+ it.call(obj)
686
660
  end
687
-
688
- define_tag(tag)
689
661
  end
662
+
690
663
  end
691
664
 
692
665
  # Builder implementation with formating (indented by ' ')
693
666
  # Slow down is less then 1%
694
667
  class Formated < Standard
695
- extend_class :AbstractTag do
696
- def open(attributes = nil)
697
- @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
698
- @builder.current = self
699
- attributes(attributes)
700
- default
701
- self
668
+
669
+ dc do
670
+ extend :AbstractTag do
671
+ def open(attributes = nil)
672
+ @output << NEWLINE << SPACES.fetch(@stack.size, SPACE) << LT << @tag
673
+ @builder.current = self
674
+ attributes(attributes)
675
+ default
676
+ self
677
+ end
702
678
  end
703
- end
704
679
 
705
- extend_class :AbstractDoubleTag do
706
- def with
707
- flush_classes
708
- @output << GT
709
- @content = nil
710
- @builder.current = nil
711
- yield
712
- # if (content = yield).is_a?(String)
713
- # @output << CGI.escapeHTML(content)
714
- # end
715
- @builder.flush
716
- @output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
717
- nil
680
+ extend :AbstractDoubleTag do
681
+ def with
682
+ flush_classes
683
+ @output << GT
684
+ @content = nil
685
+ @builder.current = nil
686
+ yield
687
+ # if (content = yield).is_a?(String)
688
+ # @output << CGI.escapeHTML(content)
689
+ # end
690
+ @builder.flush
691
+ @output << NEWLINE << SPACES.fetch(@stack.size-1, SPACE) << SLASH_LT << @stack.pop << GT
692
+ nil
693
+ end
718
694
  end
719
695
  end
720
696
 
@@ -0,0 +1,205 @@
1
+ require 'active_support/core_ext/object/try'
2
+
3
+ # When extended into a class it enables easy defining and extending classes in extended class.
4
+ #
5
+ # class A
6
+ # extend DynamicClasses
7
+ # dc do
8
+ # define :A do
9
+ # def to_s
10
+ # 'a'
11
+ # end
12
+ # end
13
+ # define :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
+ # dc do
28
+ # extend :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 define(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(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 self[klass_definition.superclass_or_name]
128
+ when Class then
129
+ klass = Class.new(klass_definition.superclass_or_name)
130
+ klass.send :include, Describable
131
+ klass._description = "Describable#{klass_definition.superclass_or_name}"
132
+ klass
133
+ when nil then DescribableClass
134
+ end
135
+ klass = Class.new(superclass, &klass_definition.definition)
136
+ klass._description = "#{base}.dc[:#{klass_definition.name}]"
137
+
138
+ class_extensions(name).each do |klass_extension|
139
+ klass = Class.new(klass, &klass_extension.definition)
140
+ klass._description = "#{base}.dc[:#{klass_extension.name}]"
141
+ end
142
+
143
+ @classes[name] = klass
144
+ end
145
+
146
+ private
147
+
148
+ def class_names
149
+ ancestors.map(&:class_definitions).map(&:keys).flatten
150
+ end
151
+
152
+ def class_definition(name)
153
+ @class_definitions[name] || ancestor.try(:class_definition, name)
154
+ end
155
+
156
+ def class_extensions(name)
157
+ ( [*ancestor.try(:class_extensions, name)] + [@class_extensions[name]] ).compact
158
+ end
159
+
160
+ def ancestors
161
+ ( [self] + [*ancestor.try(:ancestors)] ).compact
162
+ end
163
+
164
+ def ancestor
165
+ @base.superclass.dynamic_classes if @base.superclass.kind_of?(DynamicClasses)
166
+ end
167
+ end
168
+
169
+ # hook to create Classes instance
170
+ def self.extended(base)
171
+ base.send :create_dynamic_classes
172
+ super
173
+ end
174
+
175
+ # hook to create Classes instance in descendants
176
+ def inherited(base)
177
+ base.send :create_dynamic_classes
178
+ super
179
+ end
180
+
181
+ # call this to get access to Classes instance to define/extend classes inside +definition+
182
+ # calls Classes#load! to preload defined classes
183
+ # @yield [Proc, nil] definition
184
+ # a Proc enables writing class definitions/extensions
185
+ # @return [Classes] when definition is nil
186
+ def dynamic_classes(&definition)
187
+ if definition
188
+ @dynamic_classes.instance_eval &definition
189
+ @dynamic_classes.load!
190
+ nil
191
+ else
192
+ @dynamic_classes
193
+ end
194
+ end
195
+
196
+ alias :dc :dynamic_classes
197
+
198
+ private
199
+
200
+ def create_dynamic_classes
201
+ @dynamic_classes = Classes.new(self)
202
+ end
203
+ end
204
+
205
+
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 0
9
- version: 0.1.0
8
+ - 1
9
+ version: 0.1.1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Petr Chalupa
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-05-11 00:00:00 +02:00
17
+ date: 2011-05-16 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -102,6 +102,7 @@ extra_rdoc_files:
102
102
  - README.md
103
103
  files:
104
104
  - lib/hammer_builder.rb
105
+ - lib/hammer_builder/dynamic_classes.rb
105
106
  - LICENSE
106
107
  - README.md
107
108
  - spec/hammer_builder_spec.rb