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.