Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Marko is an extensible, streaming, asynchronous, high performance, HTML-based templating language that can be used in Node.js or in the browser.
![Gitter](https://badges.gitter.im/Join Chat.svg)
Marko 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. Because the Marko compiler understands the structure of the HTML document, the directives in template files are less obtrusive and more powerful. In addition, Marko allows developers to introduce custom tags and custom attributes to extend the HTML grammar (much like Web Components—only you can use it now).
Marko is a perfect match for Node.js since it supports writing directly to an output stream so that HTML can be sent over the wire sooner. Marko automatically flushes around asynchronous fragments so that the HTML is delivered in the optimized number of chunks. Because Marko is an asynchronous templating language, additional data can be asynchronously fetched even after rendering has begun. These characteristics make Marko an excellent choice for creating high performance websites.
Syntax highlighting available for Atom by installing the language-marko package.
To install the marko
module into your project you should use the following command:
npm install marko --save
To install the optional markoc
command line interface to compile templates you can use the following command:
npm install marko --global
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 else>
No colors!
</div>
The template can then be rendered as shown in the following sample code:
var templatePath = require.resolve('./hello.marko');
var template = require('marko').load(templatePath);
template.render({
name: 'World',
colors: ["red", "green", "blue"]
},
function(err, output) {
console.log(output);
});
The output of running the above program will be the following (formatted for readability):
Hello World!
<ul>
<li>red</li>
<li>green</li>
<li>blue</li>
</ul>
For comparison, given the following data consisting of an empty array of colors:
{
name: 'World',
colors: []
}
The output would be the following:
Hello World!
<div>No colors!</div>
The streaming API can be used to stream the output to an HTTP response stream or any other writable stream. For example, with Express:
var template = require('marko').load(require.resolve('./template.marko'));
app.get('/profile', function(req, res) {
template.stream({
name: 'Frank'
})
.pipe(res);
});
Marko also supports custom tags so you can easily extend the HTML grammar to support things like the following:
Welcome to Marko!
<ui-tabs>
<ui-tab label="Home">
Content for Home
</ui-tab>
<ui-tab label="Profile">
Content for Profile
</ui-tab>
<ui-tab label="Messages">
Content for Messages
</ui-tab>
</ui-tabs>
The above template is a very simple way to generate the much more complicated HTML output shown below:
<div class="tabs">
<ul class="nav nav-tabs">
<li class="active">
<a href="#tab0" data-toggle="tab">Home</a>
</li>
<li>
<a href="#tab1" data-toggle="tab">Profile</a>
</li>
<li>
<a href="#tab2" data-toggle="tab">Messages</a>
</li>
</ul>
<div class="tab-content">
<div id="tab0" class="tab-pane active">
Content for Home
</div>
<div id="tab1" class="tab-pane">
Content for Profile
</div>
<div id="tab2" class="tab-pane">
Content for Messages
</div>
</div>
</div>
The custom tags encapsulate rendering logic and help avoid repeating the same HTML (and potentially the same mistakes).
Most front-end developers are familiar with, and comfortable with, templating languages such as Handlebars, Dust or Mustache so why was Marko introduced?
What makes Marko different is that it is an HTML-based templating language that does not rely on a custom language grammar. Any HTML file is a valid Marko template and vice-versa, and the Marko compiler uses an off-the-shelf HTML parser. Because Marko understands the HTML structure of the templates, it can do more powerful things that would not be possible in a text-based templating languages such as Handlerbars, Dust or Mustache. Marko allows developers to extend the HTML language by introducing custom HTML elements and attributes. On top of that, utilizing the HTML structure for applying templating directives makes templates more readable and allows data templates to more closely resemble the final HTML structure.
Let's compare Marko with Handlebars (a text-based templating language):
Handlebars:
Hello {{name}}!
{{#if colors}}
<ul>
{{#each colors}}
<li class="color">
{{this}}
</li>
{{/each}}
</ul>
{{else}}
<div>
No colors!
</div>
{{/if}}
Marko:
Hello ${data.name}!
<ul if="notEmpty(data.colors)">
<li class="color" for="color in data.colors">
${color}
</li>
</ul>
<div else>
No colors!
</div>
A few things to note for the Marko template:
Beyond Marko being an HTML-based templating language, it was also designed with extreme performance and extensibility in mind. The Marko compiler gives developers full control over how templates are compiled to JavaScript and the runtime was designed to be as efficient as possible. Marko fully embraces the JavaScript language for better performance and flexibility (e.g. favoring JavaScript expressions over a custom expression language).
Finally, another distinguishing feature of Marko is that it supports asynchronous template rendering. This powerful feature allows portions of the template to be rendered asynchronously. Instead of waiting for all data to come back from remote services before beginning to render the template, you can now immediately start rendering the template and the portions of the template that depend on asynchronous data will render as soon as the asynchronous data becomes available. The Marko rendering engine ensures that the final HTML will be streamed out in the correct order.
var template = require('marko').load('template.marko');
template.render({
name: 'Frank',
count: 30
},
function(err, output) {
if (err) {
console.error('Rendering failed');
return;
}
console.log('Output HTML: ' + output);
});
var template = require('marko').load('template.marko');
var out = require('fs').createWriteStream('index.html', 'utf8');
// Render the template to 'index.html'
template.stream({
name: 'Frank',
count: 30
})
.pipe(out);
Alternatively, you can render directly to an existing stream to avoid creating an intermediate stream:
var template = require('marko').load('template.marko');
var out = require('fs').createWriteStream('index.html', 'utf8');
// Render the template to 'index.html'
template.render({
name: 'Frank',
count: 30
}, out);
NOTE: This will end the target output stream.
If you know that your template rendering requires no asynchronous rendering then you can use the synchronous API to render a template to a String:
var template = require('marko').load('template.marko');
var output = template.renderSync({
name: 'Frank',
count: 30
});
console.log('Output HTML: ' + output);
var fs = require('fs');
var marko = require('marko');
var template = marko.load('template.marko');
var out = marko.createWriter(fs.createWriteStream('index.html', 'utf8'));
// Render the first chunk asynchronously (after 1s delay):
var asyncOut = out.beginAsync();
setTimeout(function() {
asyncOut.write('BEGIN ');
asyncOut.end();
}, 1000);
// Render the template to the original writer:
template.render({
name: 'World'
},
out);
// Write the last chunk synchronously:
out.write(' END');
// End the rendering out
out.end();
Despite rendering the first chunk asynchronously, the above program will stream out the output in the correct order to index.html
:
BEGIN Hello World! END
For more details, please see the documentation for the async-writer module.
Given the following module code that will be used to render a template on the client-side:
run.js:
var templatePath = require.resolve('./hello.marko');
var template = require('marko').load(templatePath);
templatePath.render({
name: 'John'
},
function(err, output) {
document.body.innerHTML = output;
});
You can then bundle up the above program for running in the browser using either optimizer (recommended) or browserify.
The optimizer
CLI can be used to generate resource bundles that includes all application modules and all referenced Marko template files using a command similar to the following:
# First install the optimizer and the optimizer-marko plugin
npm install optimizer --global
npm install optimizer-marko
optimizer --main run.js --name my-page --plugins optimizer-marko
This will produce a JSON file named build/my-page.html.json
that contains the HTML markup that should be used to include the required JavaScript and CSS resources that resulted from the page optimization.
Alternatively, you can inject the HTML markup into a static HTML file using the following command:
optimizer --main run.js --name my-page --plugins optimizer-marko --inject-into my-page.html
The markoify
transform for browserify must be enabled in order to automatically compile and include referenced Marko template files.
# Install the markoify plugin from npm:
npm install markoify --save
# Build the browser bundle:
browserify -t markoify run.js > browser.js
The Marko compiler produces a Node.js-compatible, CommonJS module as output. This output format has the advantage that compiled template modules can benefit from a context-aware module loader and templates can easily be transported to work in the browser using the RaptorJS Optimizer or Browserify.
The marko
module will automatically compile templates loaded by your application on the server, but you can also choose to precompile all templates. This can be helpful as a build or test step to catch errors early.
You can either use the command line interface or the JavaScript API to compile a Marko template file. To use the CLI you must first install the marko
module globally using the following command:
npm install marko --global
You can then compile single templates using the following command:
markoc hello.marko
This will produce a file named hello.marko.js
next to the original file.
You can also recursively compile all templates in the current directory (the node_modules
and .*
directories will be ignored by default)
markoc .
You can also specify multiple directories or files
markoc foo/ bar/ template.marko
To delete all of the generated *.marko.js
files you can add the --clean
argument. For example:
markoc . --clean
Alternatively, you can use the JavaScript API to compile a module as shown in the following sample code:
require('marko/compiler').compileFile(path, function(err, src) {
// Do something with the compiled output
});
module.exports = function create(__helpers) {
var empty = __helpers.e,
notEmpty = __helpers.ne,
escapeXml = __helpers.x,
forEach = __helpers.f,
escapeXmlAttr = __helpers.xa;
return function render(data, out) {
out.w('Hello ' +
escapeXml(data.name) +
'! ');
if (notEmpty(data.colors)) {
out.w('<ul>');
forEach(data.colors, function(color) {
out.w('<li style="color: ' +
escapeXmlAttr(color) +
'">' +
escapeXml(color) +
'</li>');
});
out.w('</ul>');
}
else {
out.w('<div>No colors!</div>');
}
};
}
The compiled output is designed to be both extremely readable and minifiable. The minified code is shown below:
module.exports=function(a){var d=a.ne,c=a.x,e=a.f,f=a.xa;return function(a,b){b.w("Hello "+c(a.name)+"! ");d(a.colors)?(b.w("<ul>"),e(a.colors,function(a){b.w('<li style="color: '+f(a)+'">'+c(a)+"</li>")}),b.w("</ul>")):b.w("<div>No colors!</div>")}};
File size: 190 bytes gzipped (251 bytes uncompressed)
Almost all of the Marko templating directives can be used as either an attribute or as an element. For example:
Applying directives using attributes:
<!-- Colors available -->
<ul if="notEmpty(colors)">
<li for="color in colors">
$color
</li>
</ul>
<!-- No colors available -->
<div if="empty(colors)">
No colors!
</div>
Applying directives using elements:
<!-- Colors available -->
<if test="notEmpty(colors)">
<ul>
<for each="color in colors">
<li>
$color
</li>
</for>
</ul>
</if>
<!-- No colors available -->
<if test="empty(colors)">
<div>
No colors!
</div>
</if>
The disadvantage of using elements to control structural logic is that they change the nesting of the elements which can impact readability. For this reason it is often more suitable to apply directives as attributes.
Dynamic text is supported using either $<variable-reference>
or ${<javascript-expression>}
.
Examples:
Hello $data.name!
Hello ${data.name}!
Hello ${data.name.toUpperCase()}!
By default, all special HTML characters will be escaped in dynamic text to prevent Cross-site Scripting (XSS) Attacks. To disable HTML escaping, you can use $!
as shown in the following sample code:
Hello $!{data.name}! <!-- Do not escape -->
If necessary, you can escape $
using a forward slash to have it be treated as text instead of a placeholder token:
Test: \${hello}
<!-- Rendered Ouptut:
Test: ${hello}
-->
Wherever expressions are allowed, they are treated as JavaScript expressions and copied out to the compiled template verbatim. However, you can choose to use alternate versions of the following JavaScript operators:
JavaScript Operator | Marko Equivalent |
---|---|
&& | and |
|| | or |
=== | eq |
!== | ne |
< | lt |
> | gt |
<= | le |
>= | ge |
For example, both of the following are valid and equivalent:
<div if="searchResults.length > 100">
Show More
</div>
<div if="searchResults.length gt 100">
Show More
</div>
Other Marko files can be included using the <include>
tag and a relative path. For example:
<include template="./greeting.marko" name="Frank" count="30"/>
Input data passed to a template is made available using a special data
variable. It's possible to declare your own variables as shown in the following sample code:
<var name="name" value="data.name.toUpperCase()" />
The <with>
directive can be used to create scoped variables as shown in the following sample code:
<with vars="nameUpper=data.name.toUpperCase(); nameLower=data.name.toLowerCase()">
Hello $nameUpper!
Hello $nameLower!
</with>
Any element or fragment of HTML can be made conditional using the if
, else-if
or else
directive.
Applied as attributes:
<!--Simple if-->
<div if="someCondition">
Hello World
</div>
<!--Complex if-->
<div if="test === 'a'">
A
</div>
<div else-if="test === 'b'">
B
</div>
<div else-if="test === 'c'">
C
</div>
<div else>
Something else
</div>
Applied as elements:
<!-- Colors available -->
<!--Simple if-->
<if test="someCondition">
<div>
Hello World
</div>
</if>
<!--Complex if-->
<if test="test === 'a'">
<div>
A
</div>
</if>
<else-if test="test === 'b'">
<div>
B
</div>
</else-if>
<else-if test="test === 'c'">
<div>
C
</div>
</else-if>
<else>
<div>
Something else
</div>
</else>
Shorthand conditionals allow for conditional values inside attributes or wherever expressions are allowed. Shorthand conditionals are of the following form:
{?<expression>;<true-template>[;<false-template>]}
For example:
<div class="{?active;tab-active}">Hello</div>
With a value of true
for active
, the output would be the following:
<div class="tab-active">Hello</div>
With a value of false
for active
, the output would be the following:
<div>Hello</div>
NOTE: If the expression inside an attribute evaluates to null
or an empty string then the attribute is not included in the output.
As shown in the previous example, the "else" block for shorthand conditionals is optional. The usage of an else block is shown below:
<div class="{?active;tab-active;tab-inactive}">Hello</div>
With a value of false
for active
, the output would be the following:
<div class="tab-inactive">Hello</div>
Any element can be repeated for every item in an array using the for
directive. The directive can be applied as an element or as an attribute.
Applied as an attribute:
<ul>
<li for="item in items">${item}</li>
</ul>
Applied as an element:
<ul>
<for each="item in items">
<li>${item}</li>
</for>
</ul>
Given the following value for items:
["red", "green", "blue"]
The output would be the following:
<ul>
<li>red</li>
<li>green</li>
<li>blue</li>
</ul>
The for
directive also supports a loop status variable in case you need to know the current loop index. For example:
<ul>
<li for="color in colors; status-var=loop">
${loop.getIndex()+1}) $color
<if test="loop.isFirst()"> - FIRST</if>
<if test="loop.isLast()"> - LAST</if>
</li>
</ul>
<for each="color in colors" separator=", ">$color</for>
<div>
<span for="color in colors; separator=', '" style="color: $color">$color</span>
</div>
A range can be provided in the following format; <var-name> from <from> to <to>[ step <step>]
.
The from
, to
and step
values must be numerical expressions. If not specified, step defaults to 1.
<ul>
<li for="i from 0 to 10">
$i
</li>
</ul>
<ul>
<li for="i from 0 to 10 step 2">
$i
</li>
</ul>
<ul>
<li for="i from 0 to myArray.length-1">
${myArray[i]}
</li>
</ul>
<ul>
<li for="(name,value) in settings">
<b>$name</b>:
$value
</li>
</ul>
A custom iterator function can be passed as part of the view model to the template to control looping over data.
A sample custom iterator function that loops over an array in reverse is shown below:
{
reverseIterator: function(arrayList, callback) {
for(var i=arrayList.length-1; i>=0; i--){
callback(arrayList[i]);
}
}
}
The custom iterator can then be used in a template as shown below:
Applied as part of a for
attribute:
<div for="item in ['a', 'b', 'c']; iterator=data.reverseIterator">
$item
</div>
<!--
Output:
<div>c</div><div>b</div><div>a</div>
-->
Applied as part of a <for>
element:
<for each="item in ['a', 'b', 'c']" iterator="data.reverseIterator">
$item
</for>
<!--
Output:
cba
-->
Custom iterators also support providing a custom status object for each loop iteration:
{
reverseIterator: function(arrayList, callback){
var statusVar = {first: 0, last: arrayList.length-1};
for(var i=arrayList.length-1; i>=0; i--){
statusVar.index = i;
callback(arrayList[i], statusVar);
}
}
}
Applied as part of a for
attribute:
<div for="item in ['a', 'b', 'c']; iterator=data.reverseIterator; status-var=status">
${status.index}$item
</div>
<!--
Output:
<div>2c</div><div>1b</div><div>0a</div>
-->
Applied as part of a <for>
element:
<for each="item in ['a', 'b', 'c']" iterator="data.reverseIterator" status-var="status">
${status.index}$item
</for>
<!--
Output:
2c1b0a
-->
Parameterized macros allow for reusable fragments within an HTML template. A macro can be defined using the <def>
directive.
The <def>
directive can be used to define a reusable function within a template.
<def function="greeting(name, count)">
Hello $name! You have $count new messages.
</def>
The above macro can then be invoked as part of any expression. Alternatively, the <invoke>
directive can be used invoke a macro function using named attributes. The following sample template shows how to use macro functions inside expressions:
<def function="greeting(name, count)">
Hello $name! You have $count new messages.
</def>
<p>
${greeting("John", 10)}
</p>
<p>
${greeting("Frank", 20)}
</p>
The <invoke>
directive can be used to invoke a function defined using the <def>
directive or a function that is part of the input data to a template. The <invoke>
directive allows arguments to be passed using element attributes, but that format is only supported for functions that were previously defined using the <def>
directive.
<def function="greeting(name, count)">
Hello ${name}! You have ${count} new messages.
</def>
<invoke function="greeting" name="John" count="${10}"/>
<invoke function="greeting('Frank', 20)"/>
The output for the above template would be the following:
<p>
Hello John! You have 10 new messages.
</p>
<p>
Hello Frank! You have 20 new messages.
</p>
NOTE: By default, the arguments will be of type "string" when using <invoke>.
However, argument attributes support JavaScript expressions which allow for other types of arguments. Example:
count="10" <!-- string argument -->
count="${10}" <!-- number argument -->
The attrs
attribute allows attributes to be dynamically added to an element at runtime. The value of the attrs attribute should be an expression that resolves to an object with properties that correspond to the dynamic attributes. For example:
<div attrs="myAttrs">
Hello World!
</div>
Given the following value for the myAttrs
variable:
{style: "background-color: #FF0000;", "class": "my-div"}
The output would then be the following:
<div style="background-color: #FF0000;" class="my-div">
Hello World!
</div>
If you find that you have a wrapper element that is conditional, but whose body should always be rendered then you can use the body-only-if
attribute to handle this use case. For example, to only render a wrapping <a>
tag if there is a valid URL then you could do the following:
<a href="${data.linkUrl}" body-only-if="!data.linkUrl">
Some body content
</a>
Given a value of "http://localhost/"
for the data.linkUrl
variable: , the output would be the following:
<a href="http://localhost/">
Some body content
</a>
Given a value of undefined
for the data.linkUrl
variable: , the output would be the following:
Some body content
Standard HTML comments can be used to add comments to your template. The HTML comments will not show up in the rendered HTML.
Example comments:
<!-- This is a comment that will not be rendered -->
<h1>Hello</h1>
If you would like for your HTML comment to show up in the final output then you can use the custom html-comment
tag:
<html-comment>This is a comment that *will* be rendered</html-comment>
<h1>Hello</h1>
Output:
<!--This is a comment that *will* be rendered-->
<h1>Hello</h1>
The Marko compiler will remove unnecessary whitespace based on some builtin rules, by default. These rules are partially based on the rules that browser's use to normalize whitespace and partially based on the goal of allowing nicely indented markup with minified output. These rules are as follows:
text.replace(/^\n\s*/g, '')
text.replace(/\n\s*$/g, '')
text.replace(/^\n\s*$/g, '')
In addition, whitespace within the following tags is preserved by default:
<pre>
<textarea>
<script>
Example template:
<div>
<a href="/home">
Home
</a>
<a href="/Profile">
My Profile
</a>
<textarea>
Hello
World</textarea
</div>
Example output:
<div><a href="/home">Home</a><a href="/Profile">My Profile</a><textarea>
Hello
World</textarea</div>
The following options are available to control whitespace removal:
Option 1) Disable whitespace removal using the c-whitespace
attribute:
<div c-whitespace="preserve">
<img src="foo.jpg">
<img src="foo.jpg">
</div>
Option 2) Disable all whitespace removal by changing a compiler option
require('marko/compiler').defaultOptions.preserveWhitespace = true;
Option 3) Control whitespace removal for specific tags
require('marko/compiler').defaultOptions.preserveWhitespace = {
'pre': true,
'textarea': true,
'script': true
};
Option 4) Configured a custom tag to preserve whitespace
Adding the "preserve-whitespace": true
property to a tag definition will result in the Marko compiler preserving whitespace wherever that tag is encountered in a template.
Since Marko template files compile into CommonJS modules, any Node.js module can be "imported" into a template for use as a helper module. For example, given the following helper module:
src/util.js:
exports.reverse = function(str) {
var out = "";
for (var i=str.length-1; i>=0; i--) {
out += str.charAt(i);
}
return out;
};
The above module can then be imported into a template as shown in the following sample template:
src/template.marko:
<require module="./util" var="util" />
<div>${util.reverse('reverse test')}</div>
It's also possible to pass helper functions to a template as part of the view model:
var template = require('marko').load(require.resolve('./template.marko'));
template.render({
reverse: function(str) {
var out = "";
for (var i=str.length-1; i>=0; i--) {
out += str.charAt(i);
}
return out;
}
},
function(err, html) { ... });
Usage inside template:
<div>${data.reverse('reverse test')}</div>
Marko supports extending the language with custom tags and attributes. A custom tag or a custom attribute must have at least one dash to indicate that is not part of the standard HTML grammar.
Below illustrates how to use a simple custom tag:
<div>
<my-hello name="World"/>
</div>
The output of the above template might be the following:
<div>
Hello World!
</div>
For information on how to use and create taglibs, please see the Custom Taglibs section below.
The async taglib allows portions of your template to be rendere asynchronously. An asynchronous fragment can be bound to a function that accepts an "args" objects and callback argument. When the data provider function completes and invokes the callback with the resulting data, the body of the async fragment is then rendered with the asynchronous data assigned to the specified variable. Asynchronous fragments allow parts of your page to render out-of-order while still providing the final HTML in the correct order.
Example:
template.render({
userProfileDataProvider: function(arg, callback) {
var userId = arg.userId;
userProfileService.getUserProfile(userId, callback);
}
}, ...);
<async-fragment data-provider="data.userProfileDataProvider"
var="userProfile"
arg-userId="${data.userId}">
<ul>
<li>
First name: ${userProfile.firstName}
</li>
<li>
Last name: ${userProfile.lastName}
</li>
<li>
Email address: ${userProfile.email}
</li>
</ul>
</async-fragment>
For more details, please see https://github.com/raptorjs/marko-async.
Marko provides a layout
taglib to support separating out layout from content. The usage of of the layout
taglib is shown in the sample code below:
default-layout.marko:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title><layout-placeholder name="title"/></title>
</head>
<body>
<h1 if="data.showHeader !== false">
<layout-placeholder name="title"/>
</h1>
<p>
<layout-placeholder name="body"/>
</p>
<div>
<layout-placeholder name="footer">
Default Footer
</layout-placeholder>
</div>
</body>
</html>
Usage of default-layout.marko
:
<layout-use template="./default-layout.marko" show-header="$true">
<layout-put into="title">My Page</layout-put>
<layout-put into="body">BODY CONTENT</layout-put>
</layout-use>
For more details, please see https://github.com/raptorjs/marko-layout.
Every tag should be mapped to a "renderer". A renderer is just a function that takes two arguments (data
and out
). The data
argument is an arbitrary object that contains the data for the renderer. The out
argument is an asynchronous rendering out that wraps an output stream. Output can be produced using out.write(someString)
There is no class hierarchy or tie-ins to Marko when implementing a tag renderer. A simple tag renderer is shown below:
module.exports = function(data, out) {
out.write('Hello ' + data.name + '!');
}
If, and only if, a tag has nested content, then a special invokeBody
method will be added to the data
object. If a renderer wants to render the nested body content then it must call the invokeBody
method. For example:
module.exports = function(data, out) {
out.write('BEFORE BODY');
if (data.invokeBody) {
data.invokeBody();
}
out.write('AFTER BODY');
}
A tag renderer should be mapped to a custom tag by creating a marko-taglib.json
as shown in the next few sections.
{
"tags": {
"my-hello": {
"renderer": "./hello-renderer",
"attributes": {
"name": "string"
}
}
}
}
Tags can be defined by adding a "tags"
property to your marko-taglib.json
:
{
"tags": {
"my-hello": {
"renderer": "./hello-renderer",
"attributes": {
"name": "string"
}
},
"my-foo": {
"renderer": "./foo-renderer",
"attributes": {
"*": "string"
}
}
}
}
Every tag should be associated with a renderer. When a custom tag is used in a template, the renderer will be invoked at render time to produce the HTML/output.
If you provide attributes then the Marko compiler will do validation to make sure only the supported attributes are provided. A wildcard attribute ("*"
) allows any attribute to be passed in. Below are sample attribute definitions:
Multiple attributes:
"attributes": {
"message": "string", // String
"my-data": "expression", // JavaScript expression
"*": "string" // Everything else will be added to a special "*" property
}
Marko supports a directory scanner to make it easier to maintain a taglib by introducing a few conventions:
renderer.js
that is used as the tag renderer or, alternatively, a template.marko
marko-tag.json
file or the tag definition can be embedded into renderer.js
With this approach, marko-taglib.json
will be much simpler:
{
"tags-dir": "./components"
}
Given the following directory structure:
The following three tags will be exported:
<my-hello>
<my-foo>
<my-bar>
Directory scanning only supports one tag per directory and it will only look at directories one level deep. The tag definition can be embedded into the renderer.js
file or it can be put into a separate marko-tag.json
. For example:
In renderer.js
:
exports.tag = {
"attributes": {
"name": "string"
}
}
In marko-tag.json
:
{
"attributes": {
"name": "string"
}
}
NOTE: It is not necessary to declare the renderer
since the scanner will automatically use renderer.js
as the renderer.
It is often necessary for tags to have a parent/child or ancestor/descendent relationship. For example:
<ui-tabs>
<ui-tab label="Overview"></ui-tab>
<ui-tab label="Language Guide"></ui-tab>
<ui-tab label="JavaScript API"></ui-tab>
</ui-tabs>
Marko supports this by leveraging JavaScript closures in the compiled output. A tag can introduce scoped variables that are available to nested tags. This is shown in the sample marko-taglib.json
below:
{
"tags": {
"ui-tabs": {
"renderer": "./tabs-tag",
"var": "tabs"
},
"ui-tab": {
"renderer": "./tab-tag",
"import-var": {
"tabs": "tabs"
},
"attributes": {
"title": "string"
}
}
}
}
In the above example, the <ui-tabs>
tag will introduce a scoped variable named tabs
that is then automatically imported by the nested <ui-tab>
tags. When the nested <ui-tab>
tags render they can use the scoped variable to communicate with the renderer for the <ui-tabs>
tag.
The complete code for this example is shown below:
components/tabs/renderer.js:
var templatePath = require.resolve('./template.marko');
var template = require('marko').load(templatePath);
module.exports = function render(data, out) {
var nestedTabs = [];
// Invoke the body function to discover nested <ui-tab> tags
data.invokeBody({ // Invoke the body with the scoped "tabs" variable
addTab: function(tab) {
tab.id = tab.id || ("tab" + tabs.length);
nestedTabs.push(tab);
}
});
// Now render the markup for the tabs:
template.render({
tabs: nestedTabs
}, out);
};
components/tab/renderer.js:
module.exports = function render(data, out) {
// Register with parent but don't render anything
data.tabs.addTab(data);
};
components/tabs/template.marko:
<div class="tabs">
<ul class="nav nav-tabs">
<li class="tab" for="tab in data.tabs">
<a href="#${tab.id}" data-toggle="tab">
${tab.title}
</a>
</li>
</ul>
<div class="tab-content">
<div id="${tab.id}" class="tab-pane" for="tab in data.tabs">
<invoke function="tab.invokeBody()"/>
</div>
</div>
</div>
Given a template file, the marko
module will automatically discover all taglibs by searching relative to the template file. The taglib discoverer will search up and also look into node_modules
to discover applicable taglibs.
As an example, given a template at path /my-project/src/pages/login/template.marko
, the search path will be the following:
/my-project/src/pages/login/marko-taglib.json
/my-project/src/pages/login/node_modules/*/marko-taglib.json
/my-project/src/pages/marko-taglib.json
/my-project/src/pages/node_modules/*/marko-taglib.json
/my-project/src/marko-taglib.json
/my-project/src/node_modules/*/marko-taglib.json
/my-project/marko-taglib.json
/my-project/node_modules/*/marko-taglib.json
Question: Is Marko ready for production use?
Answer: Yes, Marko has been battle-tested at eBay and other companies for well over a year and has been designed with high performance, scalability, security and stability in mind.
Question: Can templates be compiled on the client?
Answer: Possibly, but it is not recommended and it will likely not work in older browsers. The compiler is optimized to produce small, high performance compiled templates, but the compiler itself is not small and it comes bundled with some heavyweight modules such as a JavaScript HTML parser. In short, always compile your templates on the server. The RaptorJS Optimizer is recommended for including compiled templates as part of a web page.
Question: Which web browsers are supported?
Answer: The runtime for template rendering is supported in all web browsers. If you find an issue please report a bug.
Question: How can Marko be used with Express?
Answer: The recommended way to use Marko with Express is to bypass the Express view engine and instead have Marko render directly to the response stream as shown in the following code:
var template = require('marko').load(require.resolve('./template.marko'));
app.get('/profile', function(req, res) {
template
.render({
name: 'Frank'
}, res);
});
With this approach, you can benefit from streaming and there is no middleman (less complexity).
Alternatively, you can use the streaming API to produce an intermediate stream that can then be piped to the response stream as shown below:
var template = require('view-engine').load(require.resolve('./template.marko'));
app.get('/profile', function(req, res) {
template.stream({
name: 'Frank'
})
.pipe(res);
});
Question: What is the recommended directory structure for templates and "partials"
Answer: Your templates should be organized just like all other JavaScript modules. You should put your templates right next to the code that refers to them. That is, do not create a separate "templates" directory. For a sample Express app that uses Marko, please see marko-express.
Question: How is Marko related to RaptorJS?
Answer: Marko is one of the modules that is part of the RaptorJS toolkit. It used to be a submodule, but now it has been split out into its own top-level Node.js module (for history, please see the RaptorJS 3 Plan page).
Please post questions or comments on the RaptorJS Google Groups Discussion Forum.
Pull Requests welcome. Please submit Github issues for any feature enhancements, bugs or documentation problems.
Apache License v2.0
FAQs
UI Components + streaming, async, high performance, HTML templating for Node.js and the browser.
The npm package marko receives a total of 7,438 weekly downloads. As such, marko popularity was classified as popular.
We found that marko demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 7 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.