# Introduction
js-dsl is Javascript framework for developing internal domain specific languages (DSLs) that let you declaratively build arbitrary trees to parallel your abstract syntax tree (AST) or semantic model (SM).
# Internal DSLs
An internal DSL is written inside an existing host language. A well known example of a Javascript internal DSL the testing framework, mocha. Mocha provides globally callable functions such as describe
and it
which can be used to write easy to understand tests such as:
var assert = require('assert');
describe('Array', function() {
describe('#indexOf()', function() {
it('should return -1 when the value is not present', function() {
assert.equal([1, 2, 3].indexOf(4), -1);
});
});
});
Internally, mocha translates each call to describe
and it
to nodes of an abstact syntax tree (AST) that captures the hierarchy of the calls as well as their content.
TIP
js-dsl allows for the easy development of arbitrary internal DSLs like mocha without the need to worry about context or building and maintaining an AST.
# Getting Started
Install with node:
npm install js-dsl
# Examples
# Creating Arbitary Tree
This example builds an arbitrary forest of tree
and tip
nodes like the one shown below. Calls to tree
translate to Tree
objects (likewise for tip
).
const forest = new TreeBuilder.build( () =>
tree( 'a', () => {
tree( 'b', () => {
tip( 'c' )
} )
tip( 'd' )
tree( 'e', () => {
tree( 'f', () => {
tree( 'g', () => {
tip( 'd' )
} )
tip( 'h' )
} )
} )
} ) )
js-dsl
generates the approriate hierarchy from nested calls to tree
and tip
.
└─a
├─b
│ └─c
├─d
└─e
└─f
├─g
│ └─d
└─h
js-dsl
needs to know how to translate tree
to Tree
(and tip
to Tip
). We do this by registering a factory for tree
and tip
.
const { JsDsl, AbstractFactory } = require( 'js-dsl' );
class TreeBuilder extends JsDsl {
constructor() {
super();
this.registerFactory( 'tree', new TreeFactory() );
this.registerFactory( 'tip', new TipFactory() );
}
}
Factories are how js-dsl
creates new nodes and ensures that they are inserted in the proper location in the builder's object hierarchy:
The Tree
factory:
class TreeFactory extends AbstractFactory {
newInstance( builder, name, args ) {
return new Tree( args );
}
setChild( builder, parent, child ) {
parent.addChild( child );
}
}
The Tip
Factory:
class TipFactory extends AbstractFactory {
isLeaf() {
return true;
}
newInstance( builder, name, args ) {
return new Tip( args );
}
}
Finally, the 'data' nodes:
class Tree {
constructor( name ) {
this.name = name;
this.children = []
}
addChild( child ) {
this.children.push( child );
}
}
class Tip {
constructor( name ) {
this.name = name;
}
}
# A DSL for HTML
Here's an example of a DSL that generates HTML:
div( { class: 'my-style' }, () => {
h2( 'Header 2' )
p( 'This is another paragraph' )
} )
Under the hood, the DSL generates a tree of element
nodes which render to this HTML snippet:
<div class="my-style">
<h2>Header 2</h2>
<p>This is another paragraph</p>
</div>
In this example, js-dsl translates calls to global methods such as div
to HTML element nodes.
# How It Works
You need to register the names of the nodes with js-dsl and the context under which nodes may appear. In in DSL, you are provided global method calls that map to the node names that you registered.
Method calls provided by js-dsl take the form:
someNode([value], [options], [config closure])
The optional value
and options
map are passed to the node's constructor.
# Factories
js-dsl will orchestrate the building of the node-tree and call a factory object that was registered with each node. The factory object is responsible for instantiating new nodes and inserting them appropriately in the AST hierarchy, among other things. Factory objects inherit from AbstractFactory
.
# Building the Tree
When js-dsl encounters a known node-method call, it asks factory registered for the node to supply an instance of the node by calling newInstance
, passing it any optional value
and options
.
js-dsl inserts the newly created node into the tree hiearchy by calling factory methods setParent
and setChild
(in that order), with the parent as the current node and new node as the child .
If the node has a config closure
, js-dsl will invoke the closure with the js-dsl instance as this
and the newly created node as the only argument.