Demeler Gem
Copyright (c) 2017-2019 Michael J Welch, Ph.D. [email protected]
NOTE: I apologize that the documentation isn't better than it is, but I'm running way behind in my work trying to make this into a gem in order to preserve and share it.
All files in this distribution are subject to the terms of the MIT license.
This gem builds HTML code on-the-fly. The advantages are:
- HTML code is properly formed with respect to tags and nesting;
- the code is dynamic, i.e., values from an object containing data (if used) are automatically extracted and inserted into the resultant HTML code; and
- if there are errors, the error message is generated also.
The French word démêler means "to unravel," and that's sort of what this gem does. Démêler is pronounced "day-meh-lay." It unravels your inputs to form HTML code. The diacritical marks are not used in the name for compatibility.
This class doesn't depend on any particular framework, but I use it with Ruby Sequel.
Review of HTML5
In 1980, physicist Tim Berners-Lee, a contractor at CERN, proposed and prototyped ENQUIRE, a system for CERN researchers to use and share documents. In 1989, Berners-Lee wrote a memo proposing an Internet-based hypertext system.[3] Berners-Lee specified HTML and wrote the browser and server software in late 1990. That year, Berners-Lee and CERN data systems engineer Robert Cailliau collaborated on a joint request for funding, but the project was not formally adopted by CERN. In his personal notes[4] from 1990 he listed[5] "some of the many areas in which hypertext is used" and put an encyclopedia first.
The first publicly available description of HTML was a document called "HTML Tags", first mentioned on the Internet by Tim Berners-Lee in late 1991.[6][7] It describes 18 elements comprising the initial, relatively simple design of HTML. Except for the hyperlink tag, these were strongly influenced by SGMLguid, an in-house Standard Generalized Markup Language (SGML)-based documentation format at CERN. Eleven of these elements still exist in HTML 4.[8]
HTML is a markup language that web browsers use to interpret and compose text, images, and other material into visual or audible web pages. -- Wikipedia https://en.wikipedia.org/wiki/HTML
HTML Tags
HTML tags are element names surrounded by angle brackets:
<opening-tag>content goes here<closing-tag>
The paragraph tag is a simple example of such a tag. For example:
<p>This is a paragraph. Space will precede and follow this text to set it apart as a paragraph.</p>
- HTML tags normally come in pairs like <p> and </p>.
- The first tag in a pair is the start or opening tag, the second tag is the end or closing tag.
- The end tag is written like the start tag, but with a forward slash inserted before the tag name.
- Some tags don't require a content, so they can be written in a shorthand notation omitting the closing tag thusly: <input ... />.
- Some tags don't require a closing tag, like <br>, but Demeler will generate these in the shorthand form <br />.
https://www.w3schools.com/html/html_intro.asp
Example of a Simple HTML Page
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
</head>
<body>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
</body>
</html>
Example Explained
- The <!DOCTYPE html> declaration defines this document to be HTML5
- The <html> element is the root element of an HTML page
- The <head> element contains meta information about the document
- The <title> element specifies a title for the document
- The <body> element contains the visible page content
- The <h1> element defines a large heading
- The <p> element defines a paragraph
- HTML is usually indented for readability, but does not require indentation, or even new lines (CR-LF or LF).
If you are reading this document, it is supposed that you are already familiar with HTML.
Introduction to Demeler Gem
The Demeler gem generates HTML from three inputs:
- A Ruby source file you write;
- A Hash-based object you provide, like Sequel::Model objects; and
- An errors list inside the Hash-based object.
Let's start with the most basic form, a simple example. Run irb
and enter this:
require 'demeler'
html = Demeler.build(nil, true) do
html do
head { title("Hello, World!") }
body { h1("Hello, World!") }
end
end; puts html
You'll get HTML code like this:
<!-- begin generated output -->
<html>
<head>
<title>Hello, World!</title>
</head>
<body>
<h1>Hello, World!</h1>
</body>
</html>
<!-- end generated output -->
The first argument (nil
) is the object
: we'll talk about that later. The second argument (true
) tells Demeler to generate formatted HTML. Otherwise, you'll get a string like this:
<html><head><title>Hello, World!</title></head><body><h1>Hello, World!</h1></body></html>
Unformated is usually preferred for production, as it produces slightly more compact output (but really, it's not a big deal for small files).
You can also use the gem directly
You can instantiate a Demeler object, and call its methods:
require 'demeler'
d = Demeler.new
d.html do
d.head do
d.title("Hello, World!")
end
d.body do
d.h1("Hello, World!")
end
end
puts d.to_html
Why bother with Demeler? Why not just write HTML?
There are several reasons to use this gem:
- You write in Ruby code
- Demeler balances out all the HTML tags
- Demeler optionally formats the HTML, producing human readable output
- Demeler can receive an object with data (such as a Sequel::Model object), and automatically insert the values into your HTML
- Demeler can insert error messages into the object from your Ruby code, if there is an error
Passing Variables into Demeler
There are three variables you'll be interested in in Demeler.
obj
The object, if any, that was passed as parameter 1 innew
orbuild
. I talk a little more about that one below.usr
The object, if any, that was passed as parameter 3 inbuild
or parameter 2 innew
. This object can be anything you need access to in the Demeler script. If you need to pass several objects, simply put them into an array or hash and pass that.out
This is an array where the intermediate results are held internally. To convert the array onto a String, useto_s
orto_html
if you created the Demeler object withnew
; set parameter 2 to false or true if you usedbuild
.
For example,
countries = ['USA', 'Canada', 'France']
html = Demeler.build(nil, true, countries) do
p usr.inspect
end
will generate:
<!-- begin generated output -->
<p>["USA", "Canada", "France"]</p>
<!-- end generated output -->
If you have more than one thing to pass into Demeler, put your things into an Array or Hash. For example, say you have two lists of countries and cities, you can pass them in a hash (Ruby's default) like this:
countries = ['USA', 'Canada', 'France']
cities = ['Los Angeles', 'Paris', 'Berlin']
Demeler.build(nil, true, :countries=>countries, :cities=>cities) do
p usr[:countries].inspect
p usr[:cities].inspect
end
The output is
<!-- begin generated output -->
<p>["USA", "Canada", "France"]</p>
<p>["Los Angeles", "Paris", "Berlin"]</p>
<!-- end generated output -->
Fields from an object can be inserted automatically
First, a word of warning: if you use variables other than those below, your script will crash. If your script crashes, don't blame Demeler first; look in your script for variables that shouldn't be there.
You can automatically load the values from a Sequel::Model object, or you can define an object and use it in place of Sequel. To define an object, use a definition similar to this:
class Obj<Hash
attr_accessor :errors
def initialize
@errors = {}
end
end
Your new object is just a Hash+, so you can assign it values like this:
something = Obj.new
something[:username] = "michael"
something[:password] = "my-password"
The object can now be used to fill input
fields in a form:
html = Demeler.build(something,true) do
text :username
password :password
end
puts html
That code will automatically insert the values from obj
for you.
<!-- begin generated output -->
<input name="username" type="text" value="michael" />
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
NOTE: When the first argument is a symbol, it is used as the name of the field. Also, the form object is called
something
on the outside, but when we pass it through, it's name isobj
on the inside.
Demeler creates error messages, too
You can put an error message into the object you created if your validation finds something wrong. Just insert a Hash element with the name of the element, and an array of lines, thusly:
obj = Obj.new
obj[:username] = "michael"
obj[:password] = "my-password"
obj.errors[:username] = ["This username is already taken"]
html = Demeler.build(obj,true) do
text :username
password :password
end
puts html
This will generate the HTML with the added message, as well:
<!-- begin generated output -->
<input name="username" type="text" value="michael" /><warn> <-- This username is already taken</warn>
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
Notice that the error is surrounded by <warn>...</warn>
tags. You can define how you want those to format the message using CSS. For example, if you just want the message to be displayed in red, use:
style "warn {color: red;}"
in your Demeler code like this:
html = Demeler.build(obj,true) do
style "warn {color: red;}"
text :username
password :password
end
puts html
Adding attributes to your tags
You can add attributes by just adding them to the end of the tag. For example, if I want the username input tag to display in with a blue background, I can use:
html = Demeler.build(obj,true) do
style ".blue {color: blue;}"
text :username, :class=>"blue"
password :password
end
puts html
The HTML code generated will be:
<!-- begin generated output -->
<style>.blue {color: blue;}</style>
<input name="username" class="blue" type="text" value="michael" /><warn> <-- This username is already taken</warn>
<input name="password" type="password" value="my-password" />
<!-- end generated output -->
Any attribute can be added in this way.
Embedding text between tags on one line
Normally, anything in brackets {} is embedded like this; p{"Some text."}
yields:
<!-- begin generated output -->
<p>
Some text.
</p>
<!-- end generated output -->
You can make it come out on one line by using the :text
attribute; p :text=>"Some text."
yields:
<!-- begin generated output -->
<p>Some text.</p>
<!-- end generated output -->
In most cases, this can be achieved just by eliminating the {}; `p "Some text." yields:
<!-- begin generated output -->
<p>Some text.</p>
<!-- end generated output -->
This is because the solo string is converted to a :text argument automatically.
Programming
How to create an HTML tag
Any non-input tag can be created using the tag name and the tag options, like:
<tag-name>(<tag-option-1>, <tag-option-2>, ...)
And since Ruby allows you to omit the parenthesis, you can also write:
<tag-name> <tag-option-1>, <tag-option-2>, ...
For example, a paragraph can be written as:
p("This is a paragraph.")
or
p "This is a paragraph."
If the tag requires a condition statement at the end, you'll need the parenthesis.
p("This paragraph may, or may not, be inserted into the HTML: #{obj.some_variable}.") unless obj.some_variable.nil?
Note that to do this, you have to pass in an object (obj
)or a hash (usr
) with the value you are inserting into the string and testing. (See the examples.)
How to create an input tag
An input tag can be created using:
input(<name>, <>)
Where name
is a ruby symbol such as :username or :password, and options
can be any tag options allowed for that kind of input, such as :size for a :type=>"text" input tag.
For example:
input(:username, :type=>"text", :size=>30, :value=>"joe.e.razsolli")
Or there is a shortcut for <input> HTML. Just use the tag type as the tag itself. For example:
text(:username, :size=>30, :value=>"joe.e.razsolli")
This format makes the Demeler code a little easier to read and write.
A standard input control is just a tag and options. Take the text
control, for example.
text :username, :size=>30, :value=>"joe.e.razsolli"
=> <input name="username" size="30" value="joe.e.razsolli" type="text" />
NOTE The button, color, date, datetime_local, email, hidden, image, month, number, password, range, reset, search, submit, tel, text, time, url, and week tags all work the that way.
The textarea control, on the other hand, puts it's value between the tags, so it uses a :text attribute instead of a :value attribute.
textarea :username, :size=>30, :text="joe.e.razsolli
=> <textarea name="username" size="30">joe.e.razsolli</textarea>
The textarea tag can take its text from a block, also.
textarea(:username, :size=>30) { "joe.e.razsolli" }
=> <textarea name="username" size="30">joe.e.razsolli</textarea>
Notice for the block form, you have to enclose the parameters to the textarea call in parenthesis.
How to Create a Checkbox, Radio, or Select Control
For a checkbox, radio, or select control, use the formats below.
checkbox(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle[1]" type="checkbox" value="volvo">Volvo</input>
<input name="vehicle[2]" type="checkbox" value="saab">Saab</input>
<input name="vehicle[3]" type="checkbox" value="mercedes">Mercedes</input>
<input name="vehicle[4]" type="checkbox" value="audi">Audi</input>
radio(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle" type="radio" value="volvo">Volvo</input>
<input name="vehicle" type="radio" value="saab">Saab</input>
<input name="vehicle" type="radio" value="mercedes">Mercedes</input>
<input name="vehicle" type="radio" value="audi">Audi</input>
select(:vehicle, opts, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<select name="vehicle">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
Opts represents a Hash with tag attributes. In particular, note that the :default
attribute will define the default in the case no obj
is passed to the control.
For example, if the :default
is :saab
or 'saab'
, you'll get
radio(:vehicle, {:default=>:saab}, :volvo=>"Volvo", :saab=>"Saab", :mercedes=>"Mercedes", :audi=>"Audi")
=>
<input name="vehicle" type="radio" value="volvo">Volvo</input>
<input name="vehicle" type="radio" value="saab" checked="true">Saab</input>
<input name="vehicle" type="radio" value="mercedes">Mercedes</input>
<input name="vehicle" type="radio" value="audi">Audi</input>
Reference Guide
def self.build(obj=nil, gen_html=false, usr=nil, &block)
This is the main Demeler call used to build your HTML. This call uses your code in the block, so it makes no sense to call build
without a block.
Name | Type | Value |
---|---|---|
obj | Hash+ | An object to use to get values and error messages. |
gen_html | Boolean | Create formatted HTML (true), or compact HTML (false: default). |
usr | * | A variable meant to pass a session in a web server, but you can use it for passing any other value as well. This value is for the caller's use and is not used by Demeler. |
block | Proc | The block with your code. |
def initialize(obj=nil, usr=nil, &block)
Initialize sets up the initial conditions in Demeler, and is called by new
.
Name | Type | Value |
---|---|---|
obj | Hash+ | An object to use to get values and error messages. |
usr | Hash | A variable meant to pass a session in a web server, but you can use it for passing any other value as well. This value is for the caller's use and is not used by Demeler. |
block | Proc | The block with your code. |
def clear
Clear resets the output variables in order to reuse Demeler without having to reinstantiate it.
method_missing(meth, *args, &block)
This is a Ruby method which catches method calls that have no real method. For example, when you code a body
tag, there is no method in Demeler to handle that, so it is caught by missing_method
. Missing_method passes the call along to tag_generator
to be coded.
Name | Type | Value |
---|---|---|
meth | Symbol | The name of the missing method being caught. |
*args | Array | An array of arguments from the call that was intercepted. Tag_generator will try to make sense of them. |
block | Proc | The block with your code. |
def p(*args, &block)
The p
method is a workaround to make 'p' tags work in build
.
Name | Type | Value |
---|---|---|
*args | Array | An array of arguments from the call that was intercepted. Tag_generator will try to make sense of them. |
block | Proc | The block with your code. |
def alink(text, args={}, parms={})
The alink
method is a shortcut to build an a
tag. You could write an a
tag like so:
Demeler.build do
a(:href=>"/") { "Home" }
end
but the alink method is a shortcut which supports added features. Note that HTML tag attributes are coded into args
, and parameters are coded into parms
. The alink method allows you to add attributes easily. Code it like this:
Demeler.build do
alink("Home", :href=>"/")
end
To add parameters, you have to place the attributes (in args
) in curly brackets, then list your parameters at the end like so:
params={:id=>77} # a hypothetical value
Demeler.build do
alink("Review Jobs", {:href=>"jobs"}, :id=>params[:id], :job=>'commercial job')
end
The attribute href
is coded as :html=>"jobs", where "jobs" is the link URL. The :href
value could also be coded as :jobs
. A quoted text is the customary way to code an :href
value, however. The HTML generated will look like this:
<!-- begin generated output -->
<a href="jobs?id=77&job=commercial+job">Jobs</a>
<!-- end generated output -->
Notice also that the parameter value is escaped, i.e., reserved and "unsafe" characters (such as space) are changed to a URI form.
Name | Type | Value |
---|---|---|
text | String | The text to be inserted into the tag. |
*args | Hash | An hash of attributes which must include the :href attribute. |
*parms | Hash | An hash of parameters to be passed when the link is clicked. |
def checkbox(name, opts, values)
This is a shortcut to build checkbox
tags. A properly formed check box is created for each value in the values
list. If the form object has one or more values set, those boxes will be checked.
Each check box name will begin with name
and have a number added, beginning with 1.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. It will be prepended with a number. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the check boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
radio(name, opts, values)
This is a shortcut to build radio buttons. All the radio buttons in a set are named the same, and only vary in value. Unlike the checkbox control, the radio control only has one value at a time. The opts are applied to each radio button.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the radio boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
def reset(text, opts={})
The reset shortcut creates a input
control of type 'reset'.
Name | Type | Value |
---|---|---|
text | String | The text displayed on the face of the button. |
opts | Hash | The attributes and options for the control. |
def select(name, opts, values)
The select control is unique in that it has select
tags surrounding a list of option
tags. Based on the attributes (opts), you can create a pure dropdown list, or a scrolling list. See https://www.w3schools.com for more info on HTML.
Name | Type | Value |
---|---|---|
name | Symbol | The name of the control. |
opts | Hash | The attributes and options for the control. |
values | Hash | The names and values of the radio boxes. |
The data value in the form object may be a String, Array or Hash. If this is a string, the values are comma separated. If this is an array, the elements are the values. If this is a hash, the values (right hand side of each pair) are the values.
def submit(text, opts={})
The submit shortcut creates a input
control of type 'submit'.
Name | Type | Value |
---|---|---|
text | String | The text displayed on the face of the button. |
opts | Hash | The attributes and options for the control. |
def tag_generator(meth, args=[], &block)
You don't normally call tag_generator
(although you can if you wish to). Tag_generator has many forms which are documented one by one below.
def tag_generator(meth, opts, &block)
This form is used for most simple input controls.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, &block)
This form is used for most simple controls which have no options specified.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
block | Proc | The block |
def tag_generator(meth, [text], &block)
This form is used for controls which consist of text between opening and closing tags.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
text | String | The string becomes the :text=>string attribute. |
block | Proc | The block |
def tag_generator(meth, [name], &block)
This form is used for simple input controls which have only a name, i.e., text(:username)
. You would use a control like this with a form object probably.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
block | Proc | The block |
def tag_generator(meth, [opts], &block)
This form is used for simple input controls which have only a name, i.e., text(:username)
. You would use a control like this with a form object probably. This option is equivalent to the first option which is the same except the opts are not in an array.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, [name, opts], &block)
This form is the same as the preceeding one, except the name is specified seperately for convenience.
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
opts | Hash | The attributes, i.e., :class="user-class", etc. |
block | Proc | The block |
def tag_generator(meth, [name, text], &block)
This form is the same as the preceeding one, except the text is placed between opening and closing tages, but if the meth is 'label', a for="text"
attribute is created; otherwise, a name="text"
. (This is the call that implements label
tags, obviously.)
Name | Type | Value |
---|---|---|
meth | Symbol | The method, i.e., the tag name: :p, :br, :input, etc. |
name | Symbol | The name of the control. |
text | String | The string becomes the :text=>string attribute. |
block | Proc | The block |
How the form object is harvested
For Demeler to pick up data from the form object and automatically set value
attributes, you need to have the following conditions:
- There must be a
name
attribute; - There must be a form object given in the
build
ornew
method; - The form object must contain the named key in the object's hash; and
- The retrieved data must not be
nil
and, if the retrieved data is a String, it must not beempty
.
The data will create a:
- (for
textarea
) :text attribute; or - (for all others) :value attribute.
Outputting the HTML
There are two ways to output the HTML: formatted and compressed.
Formatted code is human readable, like this:
<!-- begin generated output -->
<select name="vehicle" class="x">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</select>
<!-- end generated output -->
whereas compressed code looks like this:
<select name=\"vehicle\" class=\"x\"><option value=\"volvo\">Volvo</option><option value=\"saab\">Saab</option><option value=\"mercedes\">Mercedes</option><option value=\"audi\">Audi</option></select>
Compressed HTML is faster to generate, and is recommended for production.
A Bigger Example of a Demeler Script
Generate a login screen.
This example will be expanded in the future.
The Ruby code:
html = Demeler.build(nil,true) do
h1("Please Login")
form(:method=>:post, :action=>"/authenticate") do
table do
tr do
td { label(:username, "User Name") }
td { text(:username, :size=>30) }
end
tr do
td { label(:password, "Password") }
td { password(:password, :size=>30) }
end
tr do
td {}
td { submit("Log In") }
end
end
end
p { alink("Forgot password?", :href=>"/forgot") }
end
The generated HTML:
<!-- begin generated output -->
<h1>Please Login</h1>
<form method="post" action="/authenticate">
<table>
<tr>
<td>
<label for="username">User Name</label>
</td>
<td>
<input name="username" size="30" type="text" id="username" />
</td>
</tr>
<tr>
<td>
<label for="password">Password</label>
</td>
<td>
<input name="password" size="30" type="password" id="password" />
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="submit" value="Log In" />
</td>
</tr>
</table>
</form>
<p>
<a href="/forgot">Forgot password?</a>
</p>
<!-- end generated output -->