‘Iacta alea est’

Hypertext DSL: Template Variables and Helpers

In previous posts we established proofs of concept for integrating both variable interpolation in templates and mixing in helper methods to the Hypertext DSL template context. However, we proved the ability to accomplish these two things independently of one another.

I’d like to figure out if we can bring both variable interpolation and helper methods together within the Hypertext DSL.

I’ll begin with the most recent proof, the solution for helper methods. This involves moving the DSL out to a module and including this module into a context class, so that instances of the class have access to both the helper methods (defined in the context) and the DSL methods (defined in the included module).

To jog our memory we can consider the render method which takes our Context and evaluates a template definition:

def render(template)
  instance_eval <<-CODE
    Context.new do
      #{File.read(template)}
    end.to_s
  CODE
end

Context is a class mixing in the DSL and including definitions of any desired helper methods:

class Context
  include DSL

  def id_generator
    "id-#{rand(100)}"
  end
end

For variable interpolation our render method had the following implementation:

def render(template)
  instance_eval <<-CODE
    params = @params
    Hypertext::DSL.new do
      params.each do |key, val|
        next if [:ht, :dom].include?(key)
        instance_variable_set(:"@#{key}", val)
      end
      #{File.read(template)}
    end.to_s
  CODE
end

The @params is evidence that this method was part of a class that contained a reference to the parameters necessary for variable interpolation. We iterated over this Hash in the DSL context and dynamically set an instance variable for each key-value pair. For example, a Hash with value { title: "Hello, World!"} would allow the template to reference the title via an instance variable carrying the same name as the key in the Hash, @title.

Let’s shed the class and pass the template parameters as method parameters to the render method:

def render(template, params)
  …
end

Now we need to alter the rest of the method to match our desire to use the Context with a mixed in DSL rather than the DSL directly:

def render(template, params)
  instance_eval <<-CODE
    Context.new do
      params.each do |key, val|
        next if [:ht, :dom].include?(key)
        var = sprintf("@%s", key)
        instance_variable_set(var, val))
      end
      #{File.read(template)}
    end.to_s
  CODE
end

And there we have both helper methods and interpolation working together. At least that’s what I think having read the code. What if I let the computer read the code with the following index.ht file?

html lang: "en-GB" do
  head do
    title do
      text @page_title
    end
  end
  body do
    h1 id: id_generator do
      text "Welcome"
    end
  end
end

The computer confirms that both are working together in harmony by producing the following output:

<html lang="en-GB">
  <head>
    <title>
      Hello, World!
    </title>
  </head>
  <body>
    <h1 id="id-95">
      Welcome
    </h1>
  </body>
</html>

We see the variable @title has been replaced by the value passed in via parameter Hello, World!. The value of the id attribute on the h1 element has been computed by the Context#id_generator method and integrated into the template’s output.

I made one change because for some reason in this construction when looping through the Hash the key variable wasn’t being recognized when interpolated into the string. I replaced it with a call to sprintf.

  def render(template)
    instance_eval <<-CODE
      Context.new do
        params.each do |key, val|
          next if [:ht, :dom].include?(key)
-         instance_variable_set(:"@#{key}", val)
+         var = sprintf("@%s", key)
+         instance_variable_set(var, val)
        end
        #{File.read(template)}
      end.to_s
    CODE
  end

So rather than two independent proofs of concept we now have one integrated proof of concept which demonstrates both variable interpolation and helper methods in Hypertext. I’m still not sure whether this is the appropriate implementation or API but it is something to start with. I’d like to take it for a spin with more involved examples to see how this approach stands up.

Sunday 28th March 2021.