Announcing Marko!

We are excited to announce the release of Marko! Earlier this year, we undertook the task of making substantial usability improvements to the Marko language and API based on the feedback received from the community. We also made significant performance improvements that resulted in Marko being far faster and leaner than all competitors (see benchmarks).

If you are not familiar with Marko, it is an extensible, streaming, asynchronous, high performance, HTML-based templating language that can be used in Node.js or in the browser. Marko was founded on the philosophy that an HTML-based templating language is more natural and intuitive for generating HTML.

In this release the language is fundamentally the same, but we have removed the friction that made it more difficult to adopt Marko templates in the past. Notably, we have switched from an XML parser to an HTML parser. In addition, the AMD-based modules were replaced with CommonJS-based modules for better compatibility with the Node.js module loader.

Table of Contents

Marko at a Glance

Design Philosophy

  • Readable: Templates should be as close to the output HTML as possible to keep templates readable. Cryptic syntax and symbols should be avoided.
  • Simple: The number of new concepts should be minimized and complexity should be avoided.
  • Extensible: The template engine should be easily extensible at both compile-time and runtime.
  • High Performance: Runtime and compiled output should be optimized for low CPU and memory usage and have a small footprint. All expressions should be native JavaScript to avoid runtime interpretation.
  • Not Restrictive: Whether or not to go less logic or more logic is up to the developer.
  • Asynchronous and Streaming Output: It should be possible to render HTML out-of-order, but the output HTML should be streamed out in the correct order. This minimizes idle time and reduces the time to first byte.
  • Intuitive: The templating engine should introduce as few surprises as possible.
  • Browser and Server Compatibility: Templates should compile down to JavaScript that can be executed on both the server and the client.
  • Debuggable: Compiled JavaScript should be debuggable and readable.
  • Compile-Time Checks: Syntax, custom tags and custom attributes should be validated at compile-time.
  • Tools Support: Tools should be enabled to offer auto-completion and validation for improved productivity and safety.
  • Modular: Runtime and compiled templates should be based on CommonJS modules for improved dependency management. Template dependencies (such as custom tags) should be resolved based on a template's file system path instead of relying on a shared registry.

Installation

To install the marko module into your Node.js project, the following command should be used:

npm install marko --save

Sample Code

A basic template with text replacement, looping and conditionals is shown below:

Hello ${data.name}!

<ul if="notEmpty(data.colors)">
    <li style="color: $color" for="color in data.colors">
        $color
    </li>
</ul>
<div c-else>
    No colors!
</div>

The template can then be rendered as shown in the following sample code:

var template = require('./hello.marko');

template.render({
        name: 'World',
        colors: ["red", "green", "blue"]
    },
    function(err, output) {
        console.log(output);
    });

Alternatively, you can choose to stream out the rendered HTML. For example:

var template = require('./hello.marko');
var out = require('fs').createWriteStream('hello.html', 'utf8');

template.stream(data)
    .pipe(out);

With Marko you can start streaming rendered templates directly to an HTTP response stream. For example, with Express:

var template = require('./profile.marko');

app.get('/profile', function(req, res) {
    template.stream({
            name: 'Frank'
        })
        .pipe(res);
});

Summary of Changes

Here's a summary of the changes in this new major release:

  • Code cleaned up and reorganized
    • Split out Marko into its own top-level module: marko
    • Moved documentation into README.md
    • Significant reduction in code size
    • Flattened the directory structure
  • Switched from an XML parser to an HTML parser
    • Dropped the root <c:template> tag
    • Taglibs no longer have to be imported (no more xmlns)
    • Dropped support for the params attribute
    • Dropped namespaces in favor of dash separated names
  • Switched to CommonJS modules everywhere
    • Switched from AMD to CommonJS for the runtime
    • Templates now compile down to CommonJS modules for better integration with Node.js (no named templates)
    • Designed to support Node.js and Node.js module bundlers that target the browser
  • Simplified the JavaScript API
    • Templates can now be easily loaded to get back an object with a render method
    • Improved streaming support
    • Simplified the asynchronous rendering API
  • Simplified taglibs
    • Switched from XML to JSON for taglibs
    • Custom tags and custom attributes must have a dash in the name
    • Taglibs automatically discovered based on template location
    • Taglibs installed via npm will automatically be discovered
    • Added support for directory scanning for automatic taglibs
    • Custom tag renderers now referenced by relative path
    • Custom tag attribute definitions are now optional
    • Custom tag definition can be put into the renderer's JavaScript file instead of in a separate file
  • Miscellaneous
    • Node.js modules can be imported into templates for use as helpers
    • Introduced a simple and powerful command-line compiler: markoc
    • Improved performance now far exceeds the competition! Please see benchmarks.
    • Improved debugging

Change Details

Separated out Marko into a new top-level module

The https://www.npmjs.org/package/marko module can now be installed independently of any other modules:

npm install marko

Project links:

Switched from XML to HTML

OLD:

<c:template xmlns:c="core"
    xmlns:app="app"
    params="name,colors">

    Hello $name!

    <app:button label="Hello World"/>

    <ul c:if="notEmpty(colors)">
        <li style="color: $color" c:for="color in colors">
            $color
        </li>
    </ul>
    <div c:else="">
        No colors!
    </div>

</c:template>

NEW:

Hello $data.name!

<app-button label="Hello World"/>

<ul if="notEmpty(data.colors)">
    <li style="color: $color" for="color in data.colors">
        $color
    </li>
</ul>
<div c-else>
    No colors!
</div>

Simplified the JavaScript API

var template = require('./hello.marko');

template.render({
        name: 'World',
        colors: ["red", "green", "blue"]
    },
    function(err, output) {
        console.log(output);
    });

Improved streaming support

var template = require('./hello.marko');
var out = require('fs').createWriteStream('hello.html', 'utf8');

template.render(data, out);

Switched to CommonJS modules for compiled templates

Sample compiled template:

module.exports = function create(__helpers) {
  var empty = __helpers.e,
      notEmpty = __helpers.ne,
      escapeXml = __helpers.x,
      forEach = __helpers.f;

  return function render(data, context) {
    context.w('Hello ' +
      escapeXml(data.name) +
      '! ');

    if (notEmpty(data.colors)) {
      context.w('<ul>');

      forEach(data.colors, function(color) {
        context.w('<li class="color">' +
          escapeXml(color) +
          '</li>');
      });

      context.w('</ul>');
    }
    else {
      context.w('<div>No colors!</div>');
    }
  };
}

Added support for importing Node.js modules as template helpers

Since all templates are now compiled into CommonJS modules, you can easily import other Node.js modules into your template for use as helpers:

<require module="change-case" var="changeCase"/>

Hello ${changeCase.titleCase(name)}!

NOTE: This assumes that the change-case module was installed into the project using the following command: npm install change-case

Introduced a simple command line utility for compiling templates

Usage:

markoc hello.marko

Running the above command will generate a file named hello.marko.js.

Please see Template Compilation for more details.

Improved debugging

With the new version of Marko, every compiled template is written to disk next to the original file. The CommonJS module that is written to disk is then loaded using the standard Node.js require. By loading the compiled template from disk, you can easily step through the generated code and read through the generated code in your editor.

For example, running the following command:

markoc hello.marko

Will result in a new hello.marko.js file in the same directory.

You will want to add *.marko.js to your source code ignore file (e.g. .gitignore).

Simplified the asynchronous rendering API

module.exports = function render(input, context) {
    var asyncContext = context.beginAsync();
    userService.loadUser(input.userId, function(err, userInfo) {
        asyncContext.write('Hello ' + userInfo.name + '!');
        asyncContext.end();
    })
};

Introduced directory scanning for easier taglib maintenance

Please see Scanning for Tags

Switched from XML to JSON for taglibs

Old taglib:

rebootstrap.rtld

<raptor-taglib>
    <uri>rebootstrap</uri>
    <import-taglib path="rebootstrap/ui/components/buttons/Button/Button.rtld"/>
</raptor-taglib>

./ui-components/buttons/Button/Button.rtld

<raptor-taglib>
    <tag name="button" renderer="rebootstrap/ui/components/buttons/Button/ButtonRenderer" dynamic-attributes="true">
        <attribute name="id" type="string"/>
        <attribute name="label" type="string"/>
        <attribute name="href" type="string"/>
        <attribute name="variant" type="string" description="primary | info | success | warning | danger | inverse"/>
        <attribute name="size" type="string" description="large | small | mini"/>
        <attribute name="toggle" type="boolean"/>
        <attribute name="toggled" type="boolean"/>
        <attribute name="dropdown" type="boolean"/>

        <!-- Support widget attributes -->
        <attribute name="id" uri="widgets"/>
        <attribute name="event-click" uri="widgets"/>
    </tag>
</raptor-taglib>

New taglib:

raptor-taglib.json

{
    "tags": {
        "app-button": "./components/app-button/raptor-tag.json",
        "app-hello": {
            "renderer": "./components/app-hello/renderer.js",
            "attributes": {
                "name": "string",
            }
        }
    }
}

./components/app-button/raptor-tag.json

{
    "renderer": "./renderer",
    "attributes": {
        "id": "string",
        "label": "string",
        "href": "string",
        "variant": {
            "type": "string",
            "description": "primary | info | success | warning | danger | inverse"
        },
        "size": {
            "type": "string",
            "description": "large | small | mini"
        },
        "toggle": "boolean",
        "toggled": "boolean",
        "dropdown": "boolean",
        "id": {
            "namespace": "raptor-widgets"
        },
        "event-click": {
            "namespace": "raptor-widgets"
        }
    }
}

For more details, please see marko » README.md » Custom Taglibs.

Further Reading

Try out Marko Today!

Check out the latest Marko documentation and get started today. If you find any problems or have any feature requests please create a new issue on the Github Issues page.

Please share this post to expand the community and help make the new Marko the templating language of choice for more developers.

Comments

comments powered by Disqus