Search code examples
rubymetaprogrammingmethod-missing

Ruby metaprogramming with method_missing to make a HTML DSL


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>"

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.