Skip to content

Commit

Permalink
Haml inspiration #20
Browse files Browse the repository at this point in the history
  • Loading branch information
pitr-ch committed Dec 2, 2011
1 parent ac65484 commit bf40b0f
Show file tree
Hide file tree
Showing 13 changed files with 487 additions and 310 deletions.
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use Ruby to get your xhtml. HammerBuilder has been written with three objectives
gem.add_development_dependency "bluecloth", "~> 2.0"
gem.add_development_dependency "jeweler", "~> 1.6"

gem.files = FileList['lib/hammer_builder.rb', 'lib/hammer_builder/*.rb'].to_a
gem.files = FileList['lib/hammer_builder.rb', 'lib/hammer_builder/**/*.rb'].to_a

gem.test_files = FileList["spec/**/*.*"].to_a
gem.extra_rdoc_files = FileList["README.md","CHANGELOG.md"].to_a
Expand Down
10 changes: 8 additions & 2 deletions hammer_builder.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Gem::Specification.new do |s|

s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
s.authors = ["Petr Chalupa"]
s.date = "2011-11-19"
s.date = "2011-12-02"
s.description = "is a xhtml5 builder written in Ruby. It does not introduce anything special, you just\nuse Ruby to get your xhtml. HammerBuilder has been written with three objectives: Speed, Rich API, Extensibility"
s.email = "email@pitr.ch"
s.extra_rdoc_files = [
Expand All @@ -19,13 +19,19 @@ Gem::Specification.new do |s|
s.files = [
"lib/hammer_builder.rb",
"lib/hammer_builder/abstract.rb",
"lib/hammer_builder/abstract/abstract_double_tag.rb",
"lib/hammer_builder/abstract/abstract_single_tag.rb",
"lib/hammer_builder/abstract/abstract_tag.rb",
"lib/hammer_builder/data.rb",
"lib/hammer_builder/data/html5.rb",
"lib/hammer_builder/doc.rb",
"lib/hammer_builder/dynamic_classes.rb",
"lib/hammer_builder/formatted.rb",
"lib/hammer_builder/helper.rb",
"lib/hammer_builder/pool.rb",
"lib/hammer_builder/standard.rb"
"lib/hammer_builder/rails.rb",
"lib/hammer_builder/standard.rb",
"lib/hammer_builder/strings.rb"
]
s.homepage = "https://github.com/ruby-hammer/hammer-builder"
s.licenses = ["MIT"]
Expand Down
14 changes: 0 additions & 14 deletions lib/hammer_builder.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,3 @@
require 'cgi'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/string/inflections'
require 'hammer_builder/dynamic_classes'

require 'hammer_builder/strings'
require 'hammer_builder/data'
require 'hammer_builder/data/html5'
require "hammer_builder/pool"
require "hammer_builder/helper"
require "hammer_builder/abstract"
require "hammer_builder/standard"
require "hammer_builder/formatted"

module HammerBuilder
end

307 changes: 16 additions & 291 deletions lib/hammer_builder/abstract.rb
Original file line number Diff line number Diff line change
@@ -1,306 +1,30 @@
require 'cgi'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/string/inflections'

require 'hammer_builder/dynamic_classes'
require 'hammer_builder/strings'
require 'hammer_builder/data'
require 'hammer_builder/data/html5'
require "hammer_builder/pool"
require "hammer_builder/helper"

module HammerBuilder

# Abstract implementation of Builder
class Abstract
extend DynamicClasses

require "hammer_builder/abstract/abstract_tag"
require "hammer_builder/abstract/abstract_single_tag"
require "hammer_builder/abstract/abstract_double_tag"

# << faster then +
# yield faster then block.call
# accessing ivar and constant is faster then accesing hash or cvar
# class_eval faster then define_method
# beware of strings in methods -> creates a lot of garbage

dynamic_classes do
define :AbstractTag do ###import

class_attribute :_attributes, :instance_writer => false, :instance_reader => false
self._attributes = []

# @return [Array<String>] array of available attributes for the tag
def self.attributes
_attributes
end

# @return [String] tag's name
def self.tag_name
@tag || superclass.tag_name
end

protected

# sets the tag's name
# @api private
def self.set_tag(tag)
@tag = tag.to_s.freeze
end

set_tag 'abstract'

# defines dynamically methods for attributes
# @api private
def self.define_attribute_methods
attributes.each { |attr| define_attribute_method(attr) }
end

def self.inherited(base)
base.define_attribute_methods
end

# defines dynamically method for +attribute+
# @param [Data::Attribute] attribute
# @api private
def self.define_attribute_method(attribute)
return if instance_methods.include?(attribute.name)
name = attribute.name.to_s
content_rendering = attribute_content_rendering(attribute)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}(content#{' = true' if attribute.type == :boolean})
#{content_rendering}
self
end
RUBY
end

# @api private
# @param [Data::Attribute] attribute
# @returns Ruby code as string
def self.attribute_content_rendering(attribute)
name = attribute.name.to_s
case attribute.type
when :string
Strings.add "attr_#{name}", " #{name.gsub('_', '-')}=\""
"@output << Strings::ATTR_#{name.upcase} << CGI.escapeHTML(content.to_s) << Strings::QUOTE"
when :boolean
Strings.add "attr_#{name}", " #{name.gsub('_', '-')}=\"#{name}\""
"@output << Strings::ATTR_#{name.upcase} if content"
end
end

# adds attribute to class, triggers dynamical creation of needed instance methods etc.
# @param [Array<Data::Attribute>] attributes
# @api private
def self.add_attributes(attributes)
attributes = [attributes] unless attributes.is_a? Array
raise ArgumentError, attributes.inspect unless attributes.all? { |v| v.is_a? Data::Attribute }
self.send :_attributes=, _attributes + attributes
define_attribute_methods
end

public

attr_reader :builder

# @api private
def initialize(builder)
@builder = builder
@output = builder.instance_eval { @_output }
@stack = builder.instance_eval { @_stack }
@classes = []
@tag_name = self.rclass.tag_name
end

# @api private
def open(attributes = nil)
@output << Strings::LT << @tag_name
@builder.current = self
attributes(attributes)
default
self
end

# @example
# div.attributes :id => 'id' # => <div id="id"></div>
# div :id => 'id', :class => %w{left right} # => <div id="id" class="left right"></div>
# img :src => 'path' # => <img src="path"></div>
# attribute`s methods are called on background (in this case #id is called)
def attributes(attrs)
return self unless attrs
attrs.each do |attr, value|
__send__(attr, *(value ? value : [nil]))
end
self
end

# original Ruby method for class, class is used for html classes
alias_method(:rclass, :class)

id_class = /^([\w]+)(!|)$/
data_attribute = /^data_([a-z_]+)$/
METHOD_MISSING_REGEXP = /#{data_attribute}|#{id_class}/ unless defined? METHOD_MISSING_REGEXP

class_eval <<-RUBY, __FILE__, __LINE__ + 1
# allows data-* attributes and id, classes by method_missing
def method_missing(method, *args, &block)
method = method.to_s
if method =~ METHOD_MISSING_REGEXP
if $1
self.rclass.add_attributes Data::Attribute.new(method, :string)
self.send method, *args
else
self.__send__($3 == '!' ? :id : :class, $2)
end
else
super(method, *args, &block)
end
end
def respond_to?(symbol, include_private = false)
symbol.to_s =~ METHOD_MISSING_REGEXP || super(symbol, include_private)
end
RUBY

Strings.add "attr_class", " class=\""
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def class(*classes)
@classes.push(*classes)
self
end
RUBY

protected

# this method is called on each tag opening, useful for default attributes
# @example html tag uses this to add xmlns attr.
# html # => <html xmlns="http://www.w3.org/1999/xhtml"></html>
def default
end

# flushes classes to output
# @api private
def flush_classes
unless @classes.empty?
@output << Strings::ATTR_CLASS << CGI.escapeHTML(@classes.join(Strings::SPACE)) << Strings::QUOTE
@classes.clear
end
end
end ###import

define :AbstractEmptyTag, :AbstractTag do ###import
nil

# @api private
# closes the tag
def flush
flush_classes
@output << Strings::SLASH_GT
nil
end
end ###import

define :AbstractDoubleTag, :AbstractTag do ###import
nil

# defined by class_eval because there is a error cased by super
# super from singleton method that is defined to multiple classes is not supported;
# this will be fixed in 1.9.3 or later (NotImplementedError)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
# @api private
def initialize(builder)
super
@content = nil
end
# allows data-* attributes and id, classes by method_missing
def method_missing(method, *args, &block)
method = method.to_s
if method =~ METHOD_MISSING_REGEXP
if $1
self.rclass.add_attributes Data::Attribute.new(method.to_sym, :string)
self.send method, *args, &block
else
self.content(args[0]) if args[0]
self.__send__($3 == '!' ? :id : :class, $2, &block)
end
else
super(method, *args, &block)
end
end
# @api private
def open(*args, &block)
attributes = if args.last.is_a?(Hash)
args.pop
end
content args[0]
super attributes
@stack << @tag_name
if block
with &block
else
self
end
end
RUBY

# @api private
# closes the tag
def flush
flush_classes
@output << Strings::GT
@output << CGI.escapeHTML(@content) if @content
@output << Strings::SLASH_LT << @stack.pop << Strings::GT
@content = nil
end

# sets content of the double tag
# @example
# div 'content' # => <div>content</div>
# div.content 'content' # => <div>content</div>
# div :content => 'content' # => <div>content</div>
def content(content)
@content = content.to_s
self
end

# renders content of the double tag with block
# @yield content of the tag
# @example
# div { text 'content' } # => <div>content</div>
# div :id => 'id' do
# text 'content'
# end # => <div id="id">content</div>
def with
flush_classes
@output << Strings::GT
@content = nil
@builder.current = nil
yield
#if (content = yield).is_a?(String)
# @output << EscapeUtils.escape_html(content)
#end
@builder.flush
@output << Strings::SLASH_LT << @stack.pop << Strings::GT
nil
end

protected

# @api private
def self.define_attribute_method(attribute)
return if instance_methods(false).include?(attribute.name)
name = attribute.name.to_s

if instance_methods.include?(attribute.name)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}(*args, &block)
super(*args, &nil)
return with(&block) if block
self
end
RUBY
else
content_rendering = attribute_content_rendering(attribute)
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}(content#{' = true' if attribute.type == :boolean}, &block)
#{content_rendering}
return with(&block) if block
self
end
RUBY
end
end
end ###import
end

class_attribute :tags, :instance_writer => false
self.tags = []
Expand Down Expand Up @@ -463,6 +187,7 @@ def js(js, options = { })
script({ :type => "text/javascript" }.merge(options)) { use_cdata ? cdata(js) : text(js) }
end

# TODO update presentation
# joins and renders +collection+ with +glue+
# @param [Array<Proc, Object>] collection of objects or lambdas
# @param [Proc, String] glue can be String which is rendered with #text or block to render
Expand Down
Loading

0 comments on commit bf40b0f

Please sign in to comment.