Zero Markup Language (ZML) Specification Version 0.3¶
Introduction¶
Zero Markup Language (abbreviated ZML) is a template language and is designed to be human-friendly, lean and flexible. This specification is both an introduction to the ZML language and the concepts supporting it, and also a complete specification of the information needed to develop applications for processing ZML.
ZML represents hierarchically structured data in conjunction with template logic in an integrated system. Documents are structured by using indentation instead of opening and closing tags. ZML is designed for a platform- and implementation-independent use.
Currently there is only one implementation of the ZML specification. (the python “zml”-module implementing the specification for a zml renderer). We try to encourage the creation of implementations in different programming languages by developing a reliable specification standard.
The specification versions below 1.0 are working drafts. The first release version 1.0 will maintain backward compatibility for all 1.x versions by asserting the validation of a test suite. (a collection of tests for testing the output of an implementation to be the same as the output of previous versions. The maintainers of the different implementations should supply a community driven test-suite to assert the backward-compatibility for versatile test-cases.
Goals¶
The design goals for ZML are, in decreasing priority:
ZML is easily readable by humans
ZML templates a portable between different implementations and frameworks
ZML is extensible
ZML is a good base for the usage of javascript mvc frameworks in the templating
ZML is easy to implement and use
Syntax¶
Elements¶
An element is described by
an element name (f.e. ‘h1’, ‘div’, ‘p’) followed by
a colon followed by
one space character followed by
quoted content of the element
Single H1 headline with text
h1: 'One headline'
Result:
<h1>One headline</h1>
Teaser consisting of intro, title headline, bodytext, image and link.
p.intro: 'A teaser intro'
h1: 'One headline'
img src='article.jpg'
p: 'Some article text...'
a href='article1.html': 'more'
Result:
<p class="intro">A teaser intro</p>
<h1>One headline</h1>
<img src="article.jpg">
<p>Some article text...</p>
<a href="article1.html">more</a>
Nesting of structures:
div#sidebar:
div.teaser:
p.intro: 'A teaser intro'
h1: 'One headline'
img src='article.jpg'
p: 'Some article text'
a href='article1.html': 'more'
Result:
<div id="sidebar">
<div class="teaser">
<p class="intro">A teaser intro</p>
<h1>One headline</h1>
<img src="article.jpg">
<p>Some article text</p>
<a href="article1.html">more</a>
</div>
</div>
Attributes¶
Attributes are described by:
attribute name
optional group of:
equal sign [=]
attribute value
Attribute value can be quoted strings (single quote) or context variables. Quoted strings may include context properties surrounded by moustaches.
a href='article1.html': 'more'
Result:
<a href="article1.html">more</a>
Empty attributes are described by a single attribute name without the optional group of equal sign and quoted value.
form:
input type='text' disabled
Result:
<form>
<input type="text" disabled>
Moustaches¶
Moustaches describe elements of the template context. The template context is data which is rendered together with the zml template by a renderer into html code.
Render a “title” variable of the template context:
h1: '{title}'
Result:
<h1>Some headline in the title variable<h1>
(context dependent example)
Render a user object of the template context with the properties firstname, lastname and email:
div.card:
p: '{user.firstname}'
p: '{user.lastname}'
p: '{user.email}'
Result:
<div class="card" >
<p>Richard</p>
<p>Langly</p>
<p>ringo@l4ngly.org</p>
</div>
(context dependent example)
Inheriting templates¶
A template can inherit to another template with the #inherit statement: The template declares a context node for usage in the inheriting template by prepending the star sign:
%inherit 2col
*col1_content:
div.panel:
%for user in users:
h1: 'User'
div.card:
%if user.active:
p: '{user.firstname}'
p: '{user.lastname}'
p: '{user.email}'
%else:
p: 'The user is not active'
The inheriting template uses the context node by accessing it in the moustache:
html:
head:
title: 'ZML'
body:
h1: 'zml - zero markup language'
div.grid:
div.m66:
div.left: '{*col1_content}'
div.m33:
div.right: 'some sidebar stuff'
Components¶
You can import components from a separate file:
%import components
html:
head:
title: 'ZML'
%for style in page.stylesheets:
base-style src=style
%for script in page.scripts:
script src=script
body:
base-menu items=pages
The %import statement imports the file components.zml and loads its components. The component statement “base-menu” loads a component “menu” from the “base”-namespace. In this example there is a parameter for the compoment named “items”. The value “pages” is a property of the context, which can be supplied in a controller which calls the render function of the template with a template context or by using a data section.
Glyphs¶
In ZML there are glyphs which are used as a prefix to a descriptor to create special sections:
* Views
+ Models
% Logic
! Translation
# Data
& Metadata
@ Resources
~ Routes
| Slots
< Signal handler
> Signal emitter
" Comment
Namespaces¶
The file components.zml contains a %namespace statement which set the namespace-alias for the namespace “doonx.org/base” to “base”.
The components are defined with a * star symbol followed by the name of the component. The * (star) symbol is one of the “glyphs”, which are used in ZML to define special sections. (See chapter “Glyphs”)
%namespace base=doonx.org/base
*menu:
div.menu:
ul.navitems:
%for item in items:
li: '{#item.title}'
*style:
link rel='stylesheet' type='text/css' href='{src}'
Content wraps for components¶
You can wrap content in components. The child elements of a component instance can be referenced with the _children descriptor of the template context.
Referencing the child contents of the grid component with the _children descriptor in a components file:
%namespace base=doonx.org/base
*grid6633:
div.grid:
div.g66.gl:
div.gboxleft: '{_children[0]}'
div.g33.gr:
div.gboxright: '{_children[1]}'
Usage of the grid component with child contents:
%import components
base-grid6633:
div.content:
p: 'left content'
div.content:
p: 'right content'
The rendered result:
<div class="grid" >
<div class="g66 gl" >
<div class="gboxleft" >
<div class="content" >
<p>left content</p>
</div>
</div>
</div>
<div class="g33 gr" >
<div class="gboxright" >
<div class="content" >
<p>right content</p>
</div>
</div>
</div>
</div>
Text content nodes¶
You can omit the element name on the left side of the colon to create text content nodes without wrapping them in tags:
p:
: 'There are some'
strong: 'simple'
: 'rules to assign inline semantics to text.'
: 'This bodytext has some'
span.highlight:
em.red data-info='animate': 'important words'
: 'which are emphasized.'
The rendered result:
<p>
There are some
<strong>simple</strong>
rules to assign inline semantics to text.
This bodytext has some
<span class="highlight" >
<em class="red" data-info="animate">important words</em>
</span>
which are emphasized.
</p>
Inline semantics¶
You can use the same syntax which you use for nested nodes also inside inline content inside angle brackets. Please note that the syntax is an abbreviated notation compared to html as there is no opening or closing tag, but only one zml-node inside angle brackets.
p: 'View this <strong: simple> rules to assign inline semantics to text.'
You can also use attributes inside the inline semantics:
p: 'View this <strong.green data-info="cite": simple> rules to assign inline semantics to text.'
The rendered result:
<p>View this <strong class="green" data-info="cite">simple</strong> rules to assign inline semantics to text.</p>
Data sections¶
You can include data inside ZML by declaring context-nodes with a #-prefix. The context nodes can be accessed with dot notation inside moustaches:
%import components
%inherit base
#users:
-
firstname: 'Richard'
lastname: 'Langly'
email: 'ringo@l4ngly.org'
active: True
-
firstname: 'Melvin'
lastname: 'Frohike'
email: 'melvin@frohike1.net'
active: True
-
firstname: 'John Fitzgerald'
lastname: 'Byers'
email: 'jfb@byers23.org'
active: True
#pages:
-
title: 'About'
url: '/about'
-
title: 'Services'
url: '/services'
-
title: 'Contact'
url: '/contact'
#page:
stylesheets:
- 'files/css/base.css'
- 'files/css/content.css'
scripts:
- 'files/js/jquery.js'
- 'files/js/main.js'
#test1:
test2:
test3: 4+3
*content:
%for user in users:
div.card:
%if user.active:
p: '{user.firstname}'
p: '{user.lastname}'
p: '{user.email}'
%else:
p: 'The user is not active'
div: '{test1.test2.test3}'
Lists¶
Lists are defined with the ‘-‘ Glyph.
#nodes:
- 'first'
- 'second'
- 'third'
%for node in nodes:
div: '{node}'
The rendered result:
<div>first
</div>
<div>second
</div>
<div>third
</div>
Flask¶
You can find a flask example in the example folder. Rendering of ZML templates inside flask:
from flask import Flask
from flask import request
app = Flask(__name__)
import zml
@app.route("/")
def index():
return default({})
@app.route("/<path:path>")
def default(path=None):
gp = request.args.to_dict()
pp = request.form.to_dict()
html = zml.render('page.zml', path=path, get_params=gp, post_params=pp)
return html
if __name__ == "__main__":
app.run(debug=True)
Routes¶
Routes are defined by using ~ as a prefix. The routes will be used by a dispatcher and linkto components.
%import components
%inherit base
~routes:
list: '/blog/posts'
show: '/blog/post/{id}'
edit: '/blog/post/{id}/edit'
*content:
ul:
li:
base-linkto action='list': 'List'
li:
base-linkto action='show' id=1: 'Details'
li:
base-linkto action='edit' id=1: 'Edit'
The rendered result:
<!DOCTYPE html>
<html>
<head>
<title>zml</title>
</head>
<body>
<ul>
<li><a href="/blog/posts">List</a></li>
<li><a href="/blog/post/1">Details</a></li>
<li><a href="/blog/post/1/edit">Edit</a></li>
</ul>
</body>
</html>
The linkto-component of the base namespace is simply defined:
*linkto:
a href='{*core-path context=_context}': '{_value}'
%for child in _children:
'{child}'
The core-path function is a core function of the zml implementation. It will return an url path by interpolating the parameters into the route path definition.
F.e. the following line will use the linkto-component with the parameters action and id. The component can access the parameters by using the _contextz context property. The linkto-component will forward the _context property as a parameter of the core-path function.
~routes:
edit: '/blog/post/{id}/edit'
base-linkto action='edit' id=1': 'Some content'
Slots¶
Slots are defined with a | (pronounced “pipe”) prefix.:
html:
head:
title: 'ZML'
body:
main:
|main
footer:
|footer
The dispatcher maps the URLs to views, which are defined with a * (star). The mapping is defined in the ~ routes section.
%import components
%inherit base
~main:
index: '/'
list: 'blog/posts'
show: 'blog/post/{id}'
edit: 'blog/post/{id}/edit'
~footer:
userfooter: '/'
devfooter: 'develop/'
*nav:
ul.mainmenu:
li:
base-linkto action='list': 'List'
li:
base-linkto action='show' id=1: 'Show item with id 1'
li:
base-linkto action='edit' id=1: 'Edit item with id 1'
li:
base-linkto action='devfooter' router='footer': 'A developer sub section with a different footer'
*index:
p: 'index view'
div: x: '{_request.get.x}'
*list:
p: 'posts view'
*show:
p: 'show view'
*edit:
p: 'edit view'
*userfooter:
ul.footernav:
li: 'user footer item 1'
li: 'user footer item 2'
li: 'user footer item 3'
*devfooter:
ul.footernav:
li: 'dev footer item 1'
li: 'dev footer item 2'
li: 'dev footer item 3'
Open http://127.0.0.1:5000/ in your browser. The rendered result depends on the url you enter:
http://localhost:5000/blog/posts shows the posts view.
http://localhost:5000/blog/1 shows the detail view.
http://localhost:5000/blog/post/1/edit shows the edit view.
Views¶
Views are defined with a * glyph: The dispatcher maps the URLs to views, which are defined with a * (star). The mapping is defined in the ~ routes section. The dispatcher uses the route variables defined with moustaches f.e. {id} in the routes. See ~ section of the main router:
%import components
%inherit base
~main:
index: '/'
list: 'blog/posts'
show: 'blog/post/{id}'
edit: 'blog/post/{id}/edit'
~footer:
userfooter: '/'
devfooter: 'develop/'
*nav:
ul.mainmenu:
li:
base-linkto action='list': 'List'
li:
base-linkto action='show' id=1: 'Show item with id 1'
'li:
base-linkto action='edit' id=1: 'Edit item with id 1'
li:
base-linkto action='devfooter' router='footer': 'A developer sub section with a different footer'
*index:
p: 'index view'
div: x: {_request.get.x}
*list:
p: 'posts view'
*show:
p: 'show view'
*edit:
p: 'edit view'
*userfooter:
ul.footernav:
li: 'user footer item 1'
li: 'user footer item 2'
li: 'user footer item 3'
*devfooter:
ul.footernav:
li: 'dev footer item 1'
li: 'dev footer item 2'
li: 'dev footer item 3'
Open http://127.0.0.1:5000/ in your browser. The rendered result depends on the url you enter:
http://localhost:5000/blog/posts shows the posts view.
http://localhost:5000/blog/1 shows the detail view.
http://localhost:5000/blog/post/1/edit shows the edit view.
Translations¶
Add translations with the !-glyph followed by the two-letter code of the language.
%import components
%inherit base
!en:
labels:
title: 'Title'
date: 'Date'
bodytext: 'Bodytext'
buttons:
save: 'Save'
!de:
labels:
title: 'Titel'
date: 'Datum'
bodytext: 'Haupttext'
buttons:
save: 'Speichern'
*content:
form:
div.formrow:
label: !labels.title
input type='text' name='title'
div.formrow:
label: !labels.bodytext
textarea name='bodytext'
button type='submit': !buttons.save
Models¶
Add models to your app by using the +-glyph followed by the model descriptor. The base-form viewhelper expands the model to a form. The different model properties will be rendered with suitable form fields.
%import components
%inherit base
!en:
labels:
title: 'Title'
date: 'Date'
bodytext: 'Bodytext'
buttons:
save: 'Save'
!de:
labels:
title: 'Titel'
date: 'Datum'
bodytext: 'Haupttext'
buttons:
save: 'Speichern'
+post:
title:
&label: !labels.title
&type: 'str'
date:
&label: !labels.date
&type: 'datetime'
bodytext:
&label: !labels.bodytext
&type: 'str'
*content:
base-form model=+post
RESTful resources¶
RESTful resource are defined by using @ as a prefix. The resources can be used to load data into data sections.
In the following example the resource named ‘db’ is configured with a wikipedia api hostname. The ZML implementation loads the JSON data from the path /w/api.php?action=query&list=search&format=json&srsearch=rest. The JSON will be converted and is accessible by using the ‘pages’ context variable.
%import components
%inherit base
@db: 'en.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=rest'
#pages: @db
*content:
%for p in #pages.query.search:
p: '{#p.title}'
The rendered result:
<html>
<head>
<title>zml</title>
</head>
<body>
<p>Rest
</p>
<p>Representational state transfer
</p>
<p>ReStructuredText
</p>
<p>Rest (music)
</p>
<p>Rest in peace
</p>
<p>The Rest
</p>
<p>Bed rest
</p>
<p>Ain't No Rest for the Wicked
</p>
<p>Rabbit at Rest
</p>
<p>The Rest of the Story
</p>
</body>
</html>
Transclusions¶
External resources are defined using @ as a prefix. The resources can be used to load data into data sections or to reference external models or views.
In the following example the object #richard_langly is transcluded from an external object.
Immutable resources¶
The resource must be content-addressed and immutable. The current python implementation uses IPFS to retrieve external objects. The objects are stored decentralised and permanent (if pinned). The referencing servers/clients MUST pin the objects to prevent garbage collection. https://ipfs.io/
By using ‘speaking’ descriptors for the resource definition (f.e. @mybestfriend) we get a usable notation of references to external resources.
Content identifiers¶
CIDs are based on the content’s cryptographic hash. That means:
Any difference in content will produce a different CID and
The same piece of content added to two different IPFS nodes using the same settings will produce exactly the same CID.
By using content identifiers instead of URLs, we enforce linking between immutable objects, so the references can never be broken.
https://docs.ipfs.io/guides/concepts/cid/
@mybestfriend: 'QmPpZzmzgbpqVyqhp8qDWgDPsK4G22xUYrsNpQ5vnKtTYk'
#person:
first_name: 'Melvin'
last_name: 'Frohike'
profession: 'Developer'
friend: @mybestfriend#richard_langly
The ZML implementation loads the external object from http://localhost:8080/ipfs/QmPpZzmzgbpqVyqhp8qDWgDPsK4G22xUYrsNpQ5vnKtTYk
The IPFS object contains the ZML code for the object definition:
#richard_langly:
first_name: 'Richard'
last_name: 'Langly'
profession: 'Developer'
The object is transcluded into the property ‘friend’ of the object #person.
Transclusion of external defined models¶
In the following example the model ‘person’ is transcluded from an external document:
&id: 'neo.codes/persons/richard_langly'
&version: '1.0'
&description: 'Richard Langly'
@zmlperson: 'QmZ6s5H3zB8hJyCynKMn7WS3RQ17udZHsFbofcB3aUQBpo'
@zmladdress: 'QmTCSMqzWZ6MvYekWpEFm7Yf9HLbDWB99vJ48xieauEE7J'
&type: @zmlperson+person
#first_name: 'Richard'
#last_name: 'Langly'
#email: 'ringo@l4ngly.org'
#profession: 'Developer'
#contact:
&type: @zmladdress+address
first_name: #first_name
last_name: #last_name
email: #email
#billing_address: #contact
By using external defined models we build up an ubiquitous language of object types.
Canonicalization¶
Cryptographic operations like hashing and signing depend on that the target data does not change during serialization. With ZML we have no need to canonicalize the data in order to yield a consistent form, because the ZML standard cares for a canonical serialization of data objects.
In order to support consistent hashing of ZML documents for persistence into distributed hash tables (f.e. the IPFS merkledag) the canonical form allows unique content ids (CID standard).
Multiline contents¶
Multiline contents can be created by opening the multiline statement with a single quotation mark:
#ciphertext: '
----BEGIN PGP MESSAGE-----
jA0EBwMCKFOWDIApgLLx0o8BOb85gzkxIdVAE3tSIX9R/3yXthBUd5QPemx1Lfiz
pHpjmG/DOKJ1aN9ZwqzksAlgqLTf8UPRG9Ch/MPZoy9Q1R5KJv6QKlMPbn5XHqqo
NW5jSV5g2bX5pcl1FUqbCI9yfyDCw98Rxap01qWXxmlkD7uTp5tL2CFmg3SlDVKb
hAX8YpCjSYNDKlXL56O6rg==
=0C/y
-----END PGP MESSAGE-----
'
This will lead to the following local context:
{
'ciphertext': 'jA0EBwMCKFOWDIApgLLx0o8BOb85gzkxIdVAE3tSIX9R/3yXthBUd5QPemx1Lfiz\n pHpjmG/DOKJ1aN9ZwqzksAlgqLTf8UPRG9Ch/MPZoy9Q1R5KJv6QKlMPbn5XHqqo\n NW5jSV5g2bX5pcl1FUqbCI9yfyDCw98Rxap01qWXxmlkD7uTp5tL2CFmg3SlDVKb\n hAX8YpCjSYNDKlXL56O6rg==\n =0C/y\n'
}