Markuper: The Opera Unite Application template library
24th April 2012: Please note
Starting with Opera 12, Opera Unite will be turned off for new users and completely removed in a later release. If you're interested in building addons for Opera, we recommend going with our extensions platform — check out our extensions documentation to get started.
Introduction
Markuper is a template library that provides an easy way to develop Unite services.
Usually, when developing an Opera Unite service, you need to output all content through the WebServerResponse.write* functions. That can easily be turned into a cumbersome task whenever there’s a need to change the document produced, for instance, when the designer wants to revamp the layout of the page. It also violates abstraction layers, between business logic and presentation, unless you create your own functions to separate them.
The Markuper template library tries to solve these problems, as well as world hunger, by using a specific syntax that developers can use to create bindings between JavaScript code and HTML documents. In this article I’ll show you how to use the most important features of this library.
The contents of this article are as follows:
My first template
The easiest way to show the template library in action is to output a simple HTML file, something that can already be easily achieved by calling WebServerResponse.writeFile, but nevertheless a task that will demonstrate the library perfectly.
How to include the library in the service
Before going into coding you first need to include the template library in your service — to do so, you must create a script tag in the base index.html
file (or the file specified on the widgetfile
tag of config.xml
), pointing to the template.js
file. You can find a bare bones service with these details already configured for you in the list of files.
Since the template library depends on the File I/O API its inclusion in the service is mandatory. To include this API you only need to specify it using the feature
element of the config.xml
. Here's an example:
<feature name="http://xmlns.opera.com/fileio"></feature>
Outputting a simple HTML file
First, we create a simple HTML file to output, and place it in a templates/
directory in the root of the service. A simple example:
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
Markuper Tutorial
</body>
</html>
To be able to interact with the requests received by Opera Unite we first must listen to the _request
event of opera.io.webserver
for incoming connections. If you’re using the provided bare bones service, this code is meant to be in the /scripts/main.js
file.
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var template = new Markuper( 'templates/tutorial.html' );
response.write( template.html() );
response.close();
}
The constructor receives the location of the file to output as an argument. The html
function will return a String representation of the template that we’ll output through the response
object, as seen in Figure 1.
Figure 1: Simple template output of the first example.
You can download the complete first example service code.
Using JavaScript variables in the Template
Just outputting plain HTML files doesn’t make the template library very useful by itself. The real value is apparent when you start binding JavaScript variables to the template.
Along with the file location, the Markuper constructor also accepts an object containing values to be bound to the template as a second argument. You can think of this object as a JSON data object, meaning that you can organize your variables in a hierarchical structure.
A special syntax is used to bind one of the variables in the data object to the template: The path to the variable within the object, in double curly brackets — {{path.to.variable}}
.
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<p>
This variable is further down the data object hierarchy:
'{{further.down.the.hierarchy}}'
</p>
</body>
</html>
In the above example we’re binding to two JavaScript variables: name
and further.down.the.hierarchy
. These two strings will later be replaced by their respective values given in the second argument of the Markuper constructor.
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template',
further :
{
down :
{
the :
{
hierarchy: 'yes it is!'
}
}
}
};
var template = new Markuper( 'templates/tutorial.html', data );
response.write( template.parse().html() );
response.close();
}
In the JavaScript file we’ll need to create the variables we've just referenced in the template file and give them the appropriate values.
The data
variable will be the JSON data object given to the constructor as the object. In it, we’re defining two properties, name
— with a value ofTemplate
and further.down.the.hierarchy
— with a value of yes it is!
.
Before we can ask for the HTML string we need to explicitly parse()
the template in order to proceed with the binding substitutions.
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Template Tutorial</H1>
<P>
This variable is further down the data object hierarchy:
'yes it is!'
</P>
</BODY>
</HTML>
You can download the complete second example service code.
The Markuper is DOM-based
The template engine is entirely DOM-based, meaning that all libraries depending on the DOM API — such as jQuery or YUI — and all DOM-based code can be used to manipulate the template as if it were a common web page.
There are two functions available to retrieve Element
s from the template — xpath
and select
. The former utilizes an XPath expression to select elements; the latter a CSS 3 attribute selector.
Example
First, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<div id="div1">
This is going to be <span>removed</span>
</div>
</body>
</html>
XPath
XPath can be used as follows to select an element:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template'
};
var template = new Markuper( 'templates/tutorial.html', data );
var span = template.xpath( "//div[@id='div1']/span[1]" )[0];
span.parentNode.removeChild( span );
response.write( template.parse().html() );
response.close();
}
CSS Selector
CSS can be used to select an element like this:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template'
};
var template = new Markuper( 'templates/tutorial.html', data );
var span = template.select( "#div1 > span" )[0];
span.parentNode.removeChild( span );
response.write( template.parse().html() );
response.close();
}
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>{{name}} Tutorial</H1>
<DIV id="div1">
This is going to be
</DIV>
</BODY>
</HTML>
You can download the complete third example service code.
How to control presentation of HTML elements
Presentation logic
Until now we’ve seen how we can effectively seperate the logic layer from the presentation layer, writing all logic in the JavaScript file and then binding the values created to small pieces of text in the template (HTML) file.
However, logic can itself be split into two different sub domains, business and presentation logic. Business logic refers to all rules related to your specific domain model and problems you're trying to solve. Presentation logic, in the other hand, deals with how you want to display the information created by the business logic to the user.
Note that here we are not referring to presentation in the sense of “HTML for structure, CSS for presentation”; we are talking in terms of the desktop application paradigm, which is so often applied to the development of full-blown web applications. In this model the layers are comonly referred to as presentation, application and storage, with HTML and CSS (and often JavaScript) being referred to as the presentation layer. See the Wikipedia web applications article for more details.
Binding HTML elements to JavaScript functions
To achieve this layer of presentation logic the Markuper library implements mechanisms to effectively control any HTML element from a JavaScript function. These take the form of bindings — an attribute in the element itself specifying which JavaScript function should control it.
Using data-*
attributes
To bind an HTML element to a JavaScript function first we must register that function on the template object. The way to achieve this is to call the registerDataAttribute
function, which receives as parameters the data attribute name and the callback function that will be bound to all elements with that particular data attribute.
The callback function will be called with four arguments:
node
: the node element with the data attribute.data
: the data object given in the constructor.key
: the string value of the attribute.value
: if thekey
value represents an index to thedata
object then a fourth argument will be sent with the value pointed to by the index.
Transforming HTML into text
This example transforms the inner HTML of an element into a text node, kind of like a view source code feature.
First we will have a look at the scripts/main.js
file for this example:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template',
};
var template = new Markuper( 'templates/tutorial.html', data );
template.registerDataAttribute( 'show-html', function( node, data, key )
{
if( key == 'true' )
{
node.textContent = node.innerHTML;
}
});
response.write( template.parse().html() );
response.close();
}
Next, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<pre data-show-html="true">
<div id="header"></div>
<div id="content">
<p>paragraph</p>
</div>
<div id="footer"></div>
</pre>
</body>
</html>
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<PRE data-show-html="true">
<DIV id="header"></DIV>
<DIV id="content">
<P>paragraph</P>
</DIV>
<DIV id="footer"></DIV>
</PRE>
</BODY>
</HTML>
You can download the complete fourth example service code.
Listing and highlighting source code
Now for a more complex example of changing the contents of an element. Here we take a function, use toString()
to decompile it, generate some markup for syntax highlighting and append it to the element.
First, the scripts/main.js
file for this example:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template',
func : function foo()
{
var baz = 3;
return 'bar';
}
};
var template = new Markuper( 'templates/tutorial.html', data );
template.registerDataAttribute( 'list-code', function( node, data, key, value )
{
var keywords = ['function', 'var', 'return'];
var regexp = new RegExp( keywords.join('|'), 'g' );
value = value.toString().replace( regexp, function( keyword )
{
return '<span style="color: blue">' + keyword + '</span>';
});
node.innerHTML = value;
});
response.write( template.parse().html() );
response.close();
}
Next, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<pre data-list-code="func"></pre>
</body>
</html>
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<PRE data-list-code="func"><SPAN style="color: blue">function</SPAN> foo()
{
<SPAN style="color: blue">var</SPAN> baz = 3;
<SPAN style="color: blue">return</SPAN> 'bar';
}</PRE>
</BODY>
</HTML>
You can download the complete example five service code here.
Adding and removing HTML elements
This function creates a HTML element based on the contents of data-header
(using the same semantics as LaTeX), appends it to the element as a node
sibling and finally removes the node
itself.
First, scripts/main.js
:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template',
};
var template = new Markuper( 'templates/tutorial.html', data );
template.registerDataAttribute( 'header', function( node, data, key )
{
var types =
{
'section' : 'h1',
'subsection' : 'h2',
'subsubsection' : 'h3',
'paragraph' : 'h4',
'subparagraph' : 'h5'
}
var header = document.createElement( types[key] );
header.textContent = node.textContent;
node.parentNode.insertBefore( header, node );
node.parentNode.removeChild( node );
});
response.write( template.parse().html() );
response.close();
}
Next, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<p data-header="section">Section</p>
<p>This is a section</p>
<p data-header="subsection">SubSection</p>
<p>This is a subsection</p>
<p data-header="paragraph">Paragraph</p>
<p>This is a paragraph</p>
</body>
</html>
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<H1>Section</H1>
<P>This is a section</P>
<H2>SubSection</H2>
<P>This is a subsection</P>
<H4>Paragraph</H4>
<P>This is a paragraph</P>
</BODY>
</HTML>
You can download the complete sixth example service code here.
Built-in data-*
attributes
The Markuper library comes with some built-in data-*
attributes for common tasks such as iterating through arrays/objects, removing nodes and importing other templates.
data-list
— Iterating through arrays/objects
This function duplicates the node as many times as there are elements in the value specified by the data-list
attribute key. This function is useful for creating lists with the contents of an array where each list item corresponds to an array element.
If the value pointed by the data-list
attribute is an array, then as many nodes as elements in the array will be created. If it is an object, then as many nodes as object properties will be created instead.
One additional field will be created for each iteration, and this will give access within the template to the corresponding array/object element. This field will be named <data-list>[]
.
For example, in a node with data-list="cities"
there will be a <nobr>cities[]
</nobr> named value, accessible within that node, with the corresponding array/object element set as that value.
In the case that the data-list
attribute points to an Object
this aditional element (<nobr><data-list>[]
</nobr>) will be an Object
with two properties — key
and value
— that will correspond to each object’s property name and value respectively.
Example time! First, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<ul>
<li data-list="cities">
{{cities[].city}}: {{cities[].temperature}} degrees
</li>
</ul>
</body>
</html>
Now for the JavaScript file that does all the work — scripts/main.js
:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template',
cities :
[
{city: 'Lisbon', temperature: 20},
{city: 'Oslo' , temperature: -2}
]
};
var template = new Markuper( 'templates/tutorial.html', data );
response.write( template.parse().html() );
response.close();
}
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<UL>
<LI>
Lisbon: 20 degrees
</LI>
<LI>
Oslo: -2 degrees
</LI>
</UL>
</BODY>
</HTML>
You can download the complete example seven service code here.
data-remove/keep-if
— Removing unwanted elements
Elements can be removed from the template conditionally. The value of the attribute must be an index to a boolean value or a boolean expression composed of &&
, ||
and indexes. In the specific case of data-remove-if
the evaluation of the boolean expression will decide if the element will be removed and in data-keep-if
if the element will remain in the document.
First, the JavaScript for this example — scripts/main.js
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var isAdmin = true;
var readAccess = false;
var data =
{
name : 'Template',
isAdmin : false,
hasReadAccess : true
};
var template = new Markuper( 'templates/tutorial.html', data );
response.write( template.parse().html() );
response.close();
}
Next, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<h1 data-remove-if="false">DRAFT</h1>
<p data-keep-if="isAdmin">Admin Eyes Only</p>
<p data-keep-if="hasReadAccess || isAdmin">very important info</p>
</body>
</html>
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<H1>DRAFT</H1>
<P>very important info</P>
</BODY>
</HTML>
You can download the complete eighth example service code.
data-import
— Importing other templates
Last but not least, we present a data attribute that allows you to import other templates, inserting them into a specific element.
First, let’s look at the scripts/main.js
file:
opera.io.webserver.addEventListener( '_request', handleRequest, false );
function handleRequest( event )
{
var response = event.connection.response;
var data =
{
name : 'Template'
};
var template = new Markuper( 'templates/tutorial.html', data );
response.write( template.parse().html() );
response.close();
}
Now, the HTML template for this example — templates/tutorial.html
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Tutorial</title>
</head>
<body>
<h1>{{name}} Tutorial</h1>
<div data-import="templates/import.html"></div>
</body>
</html>
This example also has a third file involved — the template to be imported, templates/import.html
:
yay! I was imported from {{name}}!!
The resulting web page will be:
<!doctype html>
<HTML>
<HEAD>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
<TITLE>Tutorial</TITLE>
</HEAD>
<BODY>
<H1>Markuper Tutorial</H1>
<DIV>yay! I was imported from Template!!</DIV>
</BODY>
</HTML>
You can download the complete example 10 service code.
List of Files
- Bare bones service with template library already included
- My first template
- Using JavaScript Variables in the Template
- Transforming HTML into text
- Listing highlighted source code
- Adding / Removing HTML elements
- Iterating through arrays/objects
- Removing unwanted elements
- Importing other templates
- The Markuper is DOM Based
This article is licensed under a Creative Commons Attribution, Non Commercial - Share Alike 2.5 license.
Comments
The forum archive of this article is still available on My Opera.