not.js

not.js
is a DSL written in javascript,
for html. It allows you to easily define html without leaving the comfort of your own javascript file.
It's all just valid javascript... but... not.
Usage
####NOTE: Ensure any node app using not.js
is run with --harmony
, or hilarious failure will ensue
The tests are a great place to look, but here's the gist of it:
implied.not.js
:
module.exports = function() {
h1; $($scope.title); $h1
ul({class: 'un-list'})
for (var scratch in $scope.items) {
if ($scope.items.hasOwnProperty(scratch)) {
li.item
$('Item: '+$scope.items[scratch])
$li
}
}
$ul
};
You'll notice that virtually nothing in that function is defined within that file. How is this still valid, you ask?
Magic. And dynamic runtime environment overrides. Mostly magic.
Tags
All top-level identifiers aside from $scope
and $
are interpreted as tags. Bare identifiers are written as
opening tags. Identifiers ending with $
are interpreted as self-closing tags. Identifiers starting with
$
are ending tags. 'Call' a start or self-closing tag with an object to specify any attributes you'd like
on the tag. The $
function acts as a text node - strings you pass are escaped. You may instead pass a function
with a comment inside it, which will be interpreted as a multiline string. Passing 'true' as the second argument
disables escaping. Multiple tags can go on one line if separated by a semicolon.
Classes
Chaining off a tag or a called tag will result in classes being added to that tag.
div({class="col-md-6"}).content.main
-> <div class="col-md-6 content main">
Logic
Just write javascript. Really; write any javascript inline with your markup and it just works (tm).
Known exceptions to 'just working':
var
statements don't function immediately within an explicitly defined context.
Make an IIFE within the explicit with (like in the first test) to get around the problem,
or use implied contexts. Or don't use locals.
Scope
If using express, pass an object to the 'scope' field on the options object to reveal that object as '$scope' within
the dsl call. Otherwise, your API looks like so:
string
: builder function used for outputting as a string
create([builder])
: Outputs a proxy object for use - see test/html.js
for usage.
prepareFunc(func, [builder])
: Outputs a function that when called with a scope object, returns the builder's result
renderFunc(func, [scope], [builder])
: Shortcut for prepareFunc(func, builder)(scope)
renderPath(path, [options], callback)
: Connect-standard view engine callback (aliased to __express
).
Valid options are:
{
"explicit": boolean,
"builder": builder,
"scope": object
}
Shortcuts
There are some convenient shortcuts builtin for things the usual syntax makes awkward:
$(notjs.renderPathSync(path, $scope), true)
as
- include(path)
This will directly include the template at the path in question at that
point in this template, using the current scope object. It does not support
asynchronously resolving templates.
$('<!DOCTYPE '+value+'>', true)
as
- doctype(value)
Just a shorthand for writing out a doctype.
script({src: value}); $script;
as
- script(value)
A shorthand for writing out a standard script tag.
$('<!-- ', true);
<tags as normal>
$(' -->', true);
as
- comment
<tags as normal>
- $comment
A shorthand for using html comments, if you need to template those for some reason.
The general form is - <shortcut>[(<args>)]
. The -
is the unary minus operator -
if automatic semicolon insertion fails you (and it will), end the prior line with a
semicolon by hand to ensure it is interpreted as unary minus. If you fail to do so,
the interpreter has a penchant of doing things out of the order you anticipated,
yielding unexpected results. Additionally, the ~
and +
unary operator should function
identically - so - comment
and ~comment
should both be acceptable. Use whatever
operator suits your preference, but I recommend you at least stay consistent.
Suggestions for ease of use
The implied context by default is nice, however, that means the following silly example does not function:
var http = require('http');
module.exports = function() {
http.get('http://www.github.com', function(cli) {
var sink = '';
cli.on('data', function(data) {
sink += data;
});
cli.on('end', function() {
$(sink, true);
});
cli.on('error', function(e) {
html
head
meta({title: JSON.parse(e)})
$head
body
$(JSON.parse(e));
$body
$html
});
});
};
Since http
(and JSON
) will be overridden by the implicit context. You have two options:
- Export the desired functions into the
$scope
- Utilize explicit contexts for complicated files
On a per-file basis, the easiest is with 'explicit' contexts, like so:
var http = require('http');
var Promise = require('promise');
var notjs = require('not.js');
module.exports = function(scope) {
var context = notjs.create();
var promise = new Promise(function(resolve, reject) {
http.get('http://www.github.com', function(cli) {
var sink = '';
cli.on('data', function(data) {
sink += data;
});
cli.on('end', function() {
context(scope).$(sink, true);
resolve(context.collect());
});
cli.on('error', function(e) {
scope.error = JSON.parse(e);
with (context(scope)) {
html
head
meta({title: $scope.error})
$head
body
$($scope.error);
$body
$html
}
resolve(context.collect());
});
});
});
return promise;
};
The result object is built as tags appear. Don't mix async and sync calls - The template will probably
build in an unexplected order. Use promise chaining or the like to execute chunks of async template in
a known order.
Doing the same thing with a lot of things in the scope object but still within an implied context:
Scope object passed in the options object:
{
Promise: require('promise'),
http: require('http'),
JSON: JSON
}
not.js
File:
module.exports = function() {
var promise = new $scope.Promise(function(resolve, reject) {
$scope.http.get('http://www.github.com', function(cli) {
var sink = '';
cli.on('data', function(data) {
sink += data;
});
cli.on('end', function() {
$(sink, true);
resolve();
});
cli.on('error', function(e) {
var error = $scope.JSON.parse(e);
html
head
meta({title: error})
$head
body
$(error);
$body
$html
resolve();
});
});
});
return promise;
};
not.js
Is From The Future
not.js
relies on a draft of the harmony
Proxy object currently available
in your standard node
v0.10 installation with the --harmony
flag enabled. Currently, it only has an implementation
for the node
v0.10 proxy implementation (which is based on an older version of the spec), but another based on the
new Proxy spec (available now in your local Firefox instance) will be available soon (tm).
TODO
Doctype shorthand?
Shorthand for inlining a script (something shorter than script({type: 'text/javascript'}); $($scope.func.toString(), true); script
)
Benchmarks?
gh-pages
branch written with not.js
.