Writing Your First Ruby DSL (DSL to generate HTML)

DSLs are easy to write in Ruby and are an elegant way to solve well-defined problems. This post shows how to build a DSL to generate HTML code without getting into too much detail like some of the other blog posts on this same topic. Here is the desired behavior of the DSL:

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

The HtmlDsl class is initialized with a block that specifies the nesting of the tags and the content. We’ll start by simplifying the problem and writing a DSL that does not handle nested tags:

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

  private

  def method_missing(name, *args)
    tag = name.to_s
    content = args.first
    @result ||= ''
    @result += "<#{tag}>#{content}</#{tag}>"
  end
end

html = HtmlDsl.new do
  h1 'h1 body'
  h2 'h2 body'
end
p html.result # => "<h1>h1 body</h1><h2>h2 body</h2>"

When the HtmlDsl class is initialized, instance_eval(&block) is run, which executes the block in the context of the newly created instance. The first line of the block calls the method :h1 with the argument ‘h1 body’. The HtmlDsl class does not define a h1 method, so method_missing() is called. In method_missing(), the name parameter equals the name of the method that was called (:h1 in this case) and the args parameter is an array of the arguments ([‘h1 body’]). The tag and content are concatenated and stored in the @result instance variable.

We can adjust this code to handle nested blocks and account for the nested nature of HTML markup.

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 ||= ''
    @result << "<#{tag}>"
    if block_given?
      instance_eval(&block)
    else
      @result << content
    end
    @result << "</#{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><head><title>yoyo</title></head><body><h1>hey</h1></body></html>"

HtmlDsl#method_missing() traverses the nested block structure and continues evaluating the nested blocks and adding opening tags to @result until hitting a method without a block. After hitting a method that doesn’t have a block, it will add tags and content associated with the method to @result and start adding the closing tags to @result.

I would like to thank Uri Agassi for helping me find a solution to this problem on StackOverflow.

Leave a comment