Onion ML

Onion ML is an XML template system designed with a bias toward modularity.

Onion ML lets you easily custom XML tags to make modular content design simple and easy to mix with HTML. It is somewhat comparable to XSLT and JSF, but intended to be easier to understand.

You define custom tags either as markup in XML files or as custom JavaScript functions which generate output.

Onion ML also provides several control flow methods necessary for dynamic content. Methods for iterating over data sets and conditionally displaying tags are core to Onion ML's functionality.

Introduction to Onion ML

Onion ML is a meta syntax language for XML/HTML. At its core it allows you to define XML tags, and as a result, define layouts, templates, widgets, and so on.

For example, to define a tag, you would write:

<tag:mytag>This is <i>my</i> tag.</tag:mytag>

Then if you were to use this tag somewhere, for example:

<p>Check out my tag: <mytag/></p>

It would be rendered as:

<p>Check out my tag: This is <i>my</i> tag.</p>

Because you can use tags with-in tags, the power increase. For example, if you wanted to make a web-site layout, you might write the tag:

<tag:mylayout>
<html>
 <head>
  <title><arg:title/></title>
  <style> ... </style>
 </head>
 <body>
<h1><arg:title/></h1> <div id="contents"><arg:body/></div> <div id="footer"><copyright><arg:year/></copyright></div> </body> </html> </tag:mylayout> <tag:copyright>Copyright <arg:all/> All rights reserved</tag:copyright>

Now, to use this tag, you might have a page that looks like:

<mylayout>
 <title>Hello world!</title>
 <body>This is my <b>world</b>, too.</body>
 <year>2008</year>
</mylayout>

This would be rendered as:

<html>
 <head>
  <title>Hello world!</title>
  <style> ... </style>
 </head>
 <body>
<h1>Hello world!</h1> <div id="contents">This is my <b>world</b>, too.</div> <div id="footer">Copyright 2008 All rights reserved</div> </body> </html>

Congratulations! You have now successfully separated your content from your layout! Let us try to understand what's happening.

<tag:mylayout>...</tag:mylayout> defines a tag that can be used anywhere. It contains both HTML and special OnionML tags. The <arg:title/> tag inside the mylayout tag allows you to use arguments to your tag (notice how the title is placed in both the html <title> and <h1> tags).

By separating <tag:footer> as a separate tag, it can now be used in other tags. The special <arg:all/> argument matches the entire contents passed to the tag.

Tags

Tags are simply defined as <tag:name> ... </tag:name>. This specifies a tag with the name "name."

Within tags you can reference any other tag. If it does not exist, it will be used as-is.

Attributes

You can set attributes with the special <set:attribute> tag. For example:

<tag:link>
 <a><set:href><arg:url/></set:href><arg:name/></a>
</tag:link>

This could then be used like so:

<link>
 <url>http://alt.cellosoft.com/</url>
 <name>Alt website</name>
</link>

Arguments can be passed as tags or attributes, for example, the following is equivalent short-hand for the four lines above:

<link url="http://alt.cellosoft.com/" name="Alt website"/>

If both an attribute and child tag are specified, behavior is undefined.

Both of the examples above will evaluate to:

<a href="http://alt.cellosoft.com/">Alt website</a>

Processing Data [under construction]

Data is an important part of a template engine. While the XML format does not allow for logic, several special tags are defined to conditionally display content. For example, to display a list of users, you may do something like:

<if data="users">
 <table>

  <for item="user" data="users">
   <tr>
    <td><get data="user.name"/></td> 
    <td><get data="user.email/></td>
    <td><date><get data="user.registerDate"/></date></td>
   </tr>

  </for>
 </table>

 <else>No users found.</else>

</if>

This demonstrates all three special tags: for, if, and else.

[Note: I am undecided on the syntax for these tags. I am also considering the idea of an alternative shorthand syntax (currently not supported):]

<table o:if="users">
 <tr o:for="user" o:data="users">
  <td><data get="user.name"/></td> 
  <td><data get="user.email"/></td>
  <td><date><data get="user.registerDate"/></date></td>
 </tr>
</table>

<if data="!users">No users found.</if>

Onion ML in the Alt Framework

To use Onion ML in JavaScript you first make an OnionML object with XML:

var onion = new Onion(
 <onion> 
  <tag:mytag>woot, <arg:all/>!</tag:mytag> 
 </onion>
);

onion.add(
 <tag:anothertag>play the <mytag>tuba</mytag></tag:anothertag>
);      

Then you can use that object to evaluate tags:

var output = onion.evaluate(<mytag>ice cream</mytag>);

The output is XML, so if you want to print it to the screen, write:

response.write(onion.evaluate(<mytag><b>ice cream</b></mytag>).toXMLString());

You pass variables as an optional second argument:

onion.evaluate(<mytag/>, { name: "Onion Man" });

Functional Tags

You can specify tags as functions in JavaScript:

var onion = new Onion;


onion.add("date", function() {
  return new Date().toString()
});

onion.add({
 multiply: function(onion,xml) { return contents.x * contents.y; },
 repeat: function(onion,xml) {
  var result = <></>;
  for (var i=0; i<xml.count; i++)
   result += onion.evaluateChildren(xml.body);
  return result;
 }

});

These functions take an optional XML object with the entire XML tag and contents and should return a new XML object. Important Note: by design the contents are not automatically evaluate. You can evaluate contents with the provided Onion.evaluate and Onion.evaluateChildren methods.

Advanced Functional Tags

Functional tags give you a surprising amount of power for very little code, for example, one could write an import tag for loading in other xml files with tags (usage: <import file="foo.xml"/>):

onion.add("import", function(onion,xml) {
   onion.add(XML.read(xml.@filename));
   return <></>; 
});

Or the data processing outlined above could be implemented as:


// this translates getItem({foo:{bar:1}},"foo.bar" to 1
function getItem(data,path) {
  var path = path.toString().split(/\./);
  while (path.length)
    data = data[path.shift()];
  return data;
}
onion.add({
  get:   function(onion,xml,data) {
           return getItem(data, xml.@data);
         },
  "if":  function(onion,xml,data) {
           if (getItem(data,xml.@data)) {
             delete xml['else'];
             return onion.evaluateChildren(xml).children();
           } else
             return onion.evaluateChildren(xml['else']).children();
         },
  "for": function(onion,xml,data) {
           var result = <></>;
           for each (var item in getItem(data,xml.@data)) {
             data[xml.@item] = item;
             result += onion.evaluateChildren(xml.copy(),data).children();
           }
           return result;
         }
});

Engine Background

Onion ML was designed so that it has very few moving parts. In fact, all tags are simply standard functional tags.

Generic tags are defined as a functional tag that call Onion.evaluateChildren:

Onion.prototype.getTagFunction = function(tag) {
  // return tag if it is defined
  if (this.tags[tag])
    return this.tags[tag];
  // otherwise return generic function

  return function(onion, xml, data) {
    return onion.evaluateChildren(xml,data);
  }
}

Onion.evaluateChildren:

Onion.prototype.evaluateChildren = function (xml, data) {
if (xml instanceof XML)
// recursively call evaluate on child elements.
for each (var child in xml.*)
if (child.nodeKind)
if (child.nodeKind() == 'element')
Onion.replaceWith(child, this.evaluate(child, data));
return xml;
}

Onion.evaluate is defined as:

Onion.prototype.evaluate = function(xml, data) {
 // get the associated tag function and call it
 return (this.getTagFunction(xml.localName()))(this,xml,data);
}

XML-defined tags are defined as (args is an array of args used, tagxml is the XML of the tag):

function(onion, xml, data) {
// copy the tag template XML
var result = tagxml.copy();

// Replace all <arg:*/> with the passed arguments
var ARG = Onion.ARG;
for each (var node in result..ARG::*) {
var arg = node.localName();
// <arg:all/> returns everything, otherwise look for an attribute
// or child node

var tagValue = arg=="all"
? xml.children()
: (xml.@[arg].toString() || xml[arg].children());
Onion.replaceWith(node, tagValue);
}
// Evaluate all children then knock off the top-level
return onion.evaluateChildren(result, data).children();
}

E4X does not support "replaceWith" so we wrote this helper function:

Onion.replaceWith = function(node,newnode) {
node.parent().replace(node.childIndex(), newnode);
}
page last updated: Saturday, February 16, 2008

copyright © 2006 cellosoft