InfraRuby Vision delivers solutions for mobile and web applications.
You can try InfraRuby live in your browser!
Try the InfraRuby statically typed Ruby compiler live in your browser. Try our examples or write your own code.
Follow @InfraRuby on Twitter for news and updates.

Generating an HTML generator

In this post you will see how to generate an HTML generator using the meta-ruby gem. Our HTML generator will be similar to Markaby, a Ruby library to generate HTML in which the document structure is expressed in Ruby source code.

Markaby is a Ruby library to generate HTML. Each tag in HTML is supported as a Ruby method in Markaby. Here is an example from the documentation:

m = Markaby::Builder.new
m.html do
	head { title "Boats.com" }
	body do
		h1 "Boats.com has great deals"
		ul do
			li "$49 for a canoe"
			li "$39 for a raft"
			li "$29 for a huge boot..."
		end
	end
end
puts m.to_s

Makaby is implemented using the traditional approach to metaprogramming: the methods for the HTML tags are defined using the reflection features of the language:

module Markaby
	class Builder
		HTML5.tags.each do |name|
			class_eval <<-CODE, __FILE__, __LINE__
				def #{name}(*args, &block)
					html_tag(#{name.inspect}, *args, &block)
				end
			CODE
		end
	end
end

Our HTML generator has a similar programming interface:

v = View.new(STDOUT)
v.html {
	head {
		title { self << "Boats.com" }
	}
	body {
		h1 { self << "Boats.com has great deals" }
		ul {
			li { self << "$49 for a canoe" }
			li { self << "$39 for a raft" }
			li { self << "$29 for a huge boot..." }
		}
	}
}

The implementation requires one class, View, and two modules, Markup and HTML.

The Markup module defines methods to write text to the IO stream given by the abstract method io:

## <>
##   #io: -> InfraRuby::Writer
module Markup
	ENCODE_MAP = {
		'"' => "&quot;",
		'&' => "&amp;",
		'<' => "&lt;",
		'>' => "&gt;",
	}

	class << self
		## String -> String
		def encode_text(s)
			return s.gsub(/["&<>]/) { |s| ENCODE_MAP.fetch(s) }
		end
	end

	## String -> self
	def <<(s)
		io << Markup.encode_text(s)
		return self
	end
end

The HTML module includes the Markup module and should also define methods for the HTML tags:

## <>
module HTML
	## <>
	include Markup

	## &#{self -> void}? -> void
	def html(&block)
		io << "<html>"
		if block_given?
			instance_eval(&block)
			io << "</html>"
		end
		return
	end

	## &#{self -> void}? -> void
	def head(&block)
		# ...
	end

	# ...
end

But there are a lot of HTML tags and we want to keep this DRY (don’t repeat yourself), so we do this with metaprogramming. Here are the s-expressions we need to define the methods for the HTML module:

## <>
module MetaHTML
	## <>
	class Generator < MetaRuby::Generator
		TAG_NAMES = %w[html head title body h1 ul li] # ...

		## String -> MetaRuby::Sexp
		def html_tag_sexp(name)
			s(:defn, name, s(:args, "&block"),
				s(:call, s(:call, nil, "io"), "<<", s(:lit, "<#{name}>")),
				s(:if, s(:call, nil, "block_given?"), s(:block,
					s(:call, nil, "instance_eval", s(:block_pass, s(:lvar, "block"))),
					s(:call, s(:call, nil, "io"), "<<", s(:lit, "</#{name}>")),
				),
					nil
				),
				s(:return)
			)
		end

		## -> MetaRuby::Sexp
		def html_module_sexp
			s(:module, "HTML").tap { |sexp|
				sexp << s(:call, nil, "include", s(:const, "Markup"))
				TAG_NAMES.each do |name|
					sexp << html_tag_sexp(name)
				end
			}
		end
	end
end

You can generate Ruby source code for the HTML module like this:

g = MetaHTML::Generator.new
d = MetaRuby::Directory.new
d.instance_eval do
	write("HTML.rb", g.html_module_sexp)
end

Finally, the View class includes the HTML module and also defines a reader io for an IO stream:

## <>
##   @io: InfraRuby::Writer
class View
	## <>
	include HTML

	## InfraRuby::Writer -> void
	def initialize(io)
		@io = io
		return
	end

	attr_reader :io
end

Try the InfraRuby statically typed Ruby compiler live in your browser. You can use InfraRuby for your own projects with our free download!

Follow @InfraRuby on Twitter
Copyright © 2011-2017 InfraRuby Vision Limited