Question

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>"
Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top