Skip to content

Instantly share code, notes, and snippets.

@zeitnot
Created February 24, 2019 16:13
Show Gist options
  • Save zeitnot/959d5501bb2e15858cca87a3d9db4cb6 to your computer and use it in GitHub Desktop.
Save zeitnot/959d5501bb2e15858cca87a3d9db4cb6 to your computer and use it in GitHub Desktop.
Ruby html builder with DSL
require 'test/unit'
class HtmlBuilder < BasicObject
attr_accessor :html_data
SELF_CLOSING_TAGS = %i(area base br col embed iframe hr img input link meta param source track wbr command keygen menuitem)
class << self
attr_accessor :output
def build(&block)
object = new
object.instance_eval(&block)
self.output = object.html_data
end
def clear_output
self.output = nil
end
end
private
def build_closing_tag(tag)
return if SELF_CLOSING_TAGS.member?(tag)
"</#{tag}>"
end
def build_opening_tag(tag, *attributes, **hash_attributes)
tag_str = "<#{tag}"
tag_str << ' ' << hash_attributes.map{ |key,val| "#{key}='#{val}'" }.join(' ') if hash_attributes.any?
if SELF_CLOSING_TAGS.member?(tag)
tag_str << '/>'
else
tag_str << '>'
end
end
def method_missing(name, *args, &block)
self.html_data ||= ''
self.html_data += build_opening_tag(name, *args)
if str = args.first
self.html_data += str if str.is_a?(::String)
end
if ::Kernel.block_given?
call_block = block.call
if call_block.is_a?(::Proc)
call_block.call
else
self.html_data += call_block.to_s
end
end
self.html_data += build_closing_tag(name).to_s
''
end
end
class TestHtmlBuilder < Test::Unit::TestCase
def setup
HtmlBuilder.clear_output
@builder = HtmlBuilder
end
def test_self_closing_tags
@builder.build do
html do
body do
area
br
end
end
end
str = '<html><body><area/><br/></body></html>'
assert_equal(@builder.output, str)
end
def test_html_attributes
@builder.build do
html class: :html, id: :html1 do
body class: :body do
div 'div content', class: :content, id: '1', onclick: 'return someFunctionn()'
end
end
end
str = <<~STR.tr("\n",'').gsub(/(?<=\>)\s+(?=\<)/, '')
<html class='html' id='html1'>
<body class='body'>
<div class='content' id='1' onclick='return someFunctionn()'>div content</div>
</body>
</html>
STR
assert_equal(@builder.output, str)
end
def test_nested_tags
@builder.build do
html class: :html, id: :html1 do
body class: :body do
div 'div content', class: :content, id: '1', onclick: 'return someFunctionn()' do
article 'this is article 1'
article 'this is article 2'
div do
p 'this is paragraph'
div 'this is inner div'
end
end
end
end
end
str = <<~STR.tr("\n",'').gsub(/(?<=\>)\s+(?=\<)/, '')
<html class='html' id='html1'>
<body class='body'>
<div class='content' id='1' onclick='return someFunctionn()'>div content<article>this is article 1</article>
<article>this is article 2</article>
<div>
<p>this is paragraph</p>
<div>this is inner div</div>
</div>
</div>
</body>
</html>
STR
assert_equal(@builder.output, str)
end
end
@zeitnot
Copy link
Author

zeitnot commented Feb 24, 2019

HtmlBuilder class extends BasicObject due to fact that BasicObject has few methods. This class makes it easy to implement great DSLs.

HtmlBuilder uses method_missing function to apply to meta programming techniques while creating a DSL. Due to few methods of BasicObject, the rest of other method calls will be forwarded to method_missing and this function will do the rest of the magic.

HtmlBuilder.build do
  html do
    title 'This is title'
    description 'This is description'
    body class: :body do
      span 'This is span'
      span { 'This is span2 ' }
      div('This is div', class: 'wrapper container', id: 'uniq-id') do
        div do
          'Inner div 1'
        end

        div 'Inner div 2'
        img src: 'http://example.com/test.jog'
        br
      end
    end
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment