2

I'm learning metaprogramming and am trying to make a little DSL to generate HTML. The @result instance variable is not generating the correct answer because when the h1 method is called, the @result instance variable is reset. Is there an elegant way to deal with these 'nested' method calls (I know Ruby doesn't technically have nested methods). Here's my code:

class HtmlDsl
  attr_reader :result
  def initialize(&block)
    instance_eval(&block)
  end

  private

  def method_missing(name, *args, &block)
    tag = name.to_s
    content = args.first
    @result = "<#{tag}>#{block_given? ? instance_eval(&block) : content}</#{tag}>"
  end
end

html = HtmlDsl.new do
  html do
    head do
      title 'yoyo'
    end
    body do
      h1 'hey'
    end
  end
end
p html.result # => "<html><body><h1>hey</h1></body></html>"
# desired result # => "<html><head><title>yoyo</title></head><body><h1>hey</h1></body></html>"
Community
  • 1
  • 1
Powers
  • 18,150
  • 10
  • 103
  • 108
  • 1
    Fyi, if you're interested in using Ruby DSL to generate HTML in a production-grade project, you may want to look at [Erector](https://github.com/erector/erector). – Ajedi32 May 05 '14 at 13:47

1 Answers1

4

Your problem is not that @result is reset, only that you add into the @result the return value of instance_eval(&block), which is the last line in the block, and not the aggregated block. This should work better (although not perfectly):

class HtmlDsl
  attr_reader :result
  def initialize(&block)
    instance_eval(&block)
  end

  private

  def method_missing(name, *args, &block)
    tag = name.to_s
    content = args.first
    (@result ||= '') << "<#{tag}>"
    if block_given?
      instance_eval(&block)
    else
      @result << content
    end
    @result <<  "</#{tag}>"
  end
end

So now:

html = HtmlDsl.new do
  html do
    head do
      title 'yoyo'
    end
    body do
      h1 'hey'
    end
  end
end
p html.result
#=> "<html><head><title>yoyo</title></head><body><h1>hey</h1></body></html>" 

What I've done is that each call actually renders a fragment to the @result, so inner calls render inner fragments, each wrapping its own inner fragments with tags.

Uri Agassi
  • 36,848
  • 14
  • 76
  • 93