Overview

Install quaint locally in order to use its programmatic interface:

npm install quaint

The basic interface looks like this:

var quaint = require("quaint");
var html = quaint.toHTML("Some __Quaint markup!");
console.log(html);

If you want more/different functionality, you can install plugins locally and make your own quaint engine:

var quaint = require("quaint");
var qjs = require("quaint-javascript");
var qhl = require("quaint-highlight");

// Instantiate with plugins
var q = quaint(qjs, qhl({"default": "python"}));

// Set some environment variables
q.setenv({name: "Bob", surname: "Smith"});

// Add a rule
q.registerRules({
  "\\a <=> \\b": function (engine, vars) {
    var whitespace = vars._wide ? " " : "";
    return [engine.gen(vars.b), whitespace, engine.gen(vars.a)];
  }
});

var html = q.toHTML("{name} {surname} <=> hello");
console.log(html); // "hello Bob Smith"

Interface

There are three classes of objects you will interact with when extending Quaint and writing plugins:

Now, if you were to add a rule to Quaint it might look like this:

engine = quaint();
engine.registerRules({
  "\\a <=> \\b": function (engine, vars) {
    var whitespace = vars._wide ? " " : "";
    children = [engine.gen(vars.b)
                whitespace
                engine.gen(vars.a)];
    return quaint.h("div.swapped", {}, children);
  }
});

Salient points:

QAst

QAst is the class all the nodes in Quaint's syntax tree are an instance of.

Tree structure

That tree is in the same order as the source, which is to say, if you iterate over a QAst tree depth first and print out the the leaves as you encounter them, you recover the original source code exactly. This is not literally what you get, but to put it simply, the parser returns something like this:

quaint.parse("(x + y) - z")
=> [["", "(", ["x", " + ", "y"], ")", ""], " - ", "z"]

The whitespace is taken up by the operators whenever possible so that you don't have to worry about leading or trailing whitespace on the operands.

Note that there are indeed empty nodes before and after the parentheses. This is useful because it preserves the invariant that every element at an odd index is an operator (an Oper leaf).

Useful methods

args()

This is shorthand for node.sexp(false).slice(1). In short:

quaint.parse("a + b").args() ==> [a, b]

collapse()

Flatten a node with all its children on the right side that have the same operator. Basically:

quaint.parse("a + b + c + d").collapse() ==> [a, b, c, d]
q.registerMacros({
    sum: function (engine, xs) {
        return xs.collapse().reduce(function (a, b) {
            return a + parseFloat(b.raw());
        }, 0)
    }
});
q.toHTML("sum :: 1 + 20 + 34"); // 55
q.toHTML("sum :: 1 / 20 / 34"); // 55 as well, since we don't actually check 
                                // what operator this is

empty()

Returns true if the node is whitespace. That is to say, node.empty() is equivalent to node.raw().trim() === "".

extract(rules)

This matches a node against a list of possible rule patterns and returns the first one that matches.

If a rule matches, the variables declared in the rule are set in the returned object. If no rules match, the return value is false.

q.registerMacros({
    addition: function (engine, x) {
        var r = x.extract("\\x + \\y");
        if (r)
           return parseFloat(r.x.raw()) + parseFloat(r.y.raw());
        else
           return "NO"
    }
});
q.toHTML("[addition :: 12 + 3] [addition :: hello]"); // 15 NO

If there are several rules, they can either be given as multiple arguments to extract or as a dictionary that maps a name to a rule. The _which field of the returned object will contain the index or the name of the matching rule.

q.registerMacros({
    check: function (engine, x) {
        [x.extract("\\x + \\y - \\z", "\\z")._which
         x.extract({add: "\\x + \\y", other: "\\z"})._which]
    }
});
q.toHTML("[check :: 12 + 3 - 7] [check :: hello]"); // 0add 1other

Patterns can be as complex as you want, but keep in mind that all operators in Quaint are right-associative.

raw()

node.raw() returns the source code that produced the node as a string.

q.registerMacros({
    shout: function (engine, text) {
        return text.raw().toUpperCase();
    }
});
q.toHTML("shout :: hello!"); // HELLO!

sexp(recursive [= true])

This converts a node to “s-expression” format. To explain it simply:

quaint.parse("a + b - c").sexp() ==> ["+", a, ["-", b, c]]

Note that this erases some information, for example the information about the whitespace around the operators, but nonetheless, it can be a handy way to navigate the AST.

shed(n [= 1])

The shed method removes one (or n) outer layer of square grouping brackets []. If there are no outer brackets, this does nothing and returns the node itself.

q.registerMacros({
    shout2: function (engine, text) {
        return text.shed().raw().toUpperCase();
    }
});
q.toHTML("shout2 :: [[[hello friends!]]]"); // [[HELLO FRIENDS!]]

shedAll()

This is the same as shed(Infinity): it sheds all outer square brackets.

Note that Quaint does not display brackets by default, so you only really need to do this if you need to dig inside a node, either because you want to get the string value of what's inside, or because you want to extract fields using a pattern and the brackets would get in your way.

q.registerMacros({
    shout3: function (engine, text) {
        return text.shedAll().raw().toUpperCase();
    }
});
q.toHTML("shout3 :: [[[hello friends!]]]"); // HELLO FRIENDS!

shedIndent()

An indented block is a special node in Quaint (the I( )I operator, to be exact). It is sometimes useful to shed that node so that we can better get to what's inside it.

statements()

The statements method transforms a newline-separated “body” into a list of statements. This is handy if you want to implement something like the meta macro, or simply apply a rule to every element in a list.

q.registerMacros({
    greet: function (engine, body) {
        body.statements().map(function (person) {
            return ["Hello ", engine.gen(person)]
        })
    }
});
q.toHTML("greet ::\n Alice\n Bob\n Charlie");
// Hello AliceHello BobHello Charlie

Engine

Quaint's Engine encapsulates most functionality. Extensions and plugins all operate on it.

Notable methods

There are two groups of notable methods, the user-facing methods and the plugin-facing methods:

User-facing

Plugin-facing

deferred(function)

By using deferred you can delay the generation of the node at this position. Typically this is because you may need information that is located below, and you need to let the engine fill it in. For example, a table of contents needs to know about all the sections in the document, but these sections come later, so it will use deferred.

The function given to deferred must take two arguments, path and documents. path is a unique identifier for the location of the node in the source. documents is the same as engine.documents, and it contains various “documents” such as a sections document, a meta document, and so on.

Consider the following macro that gets the title meta-information from the meta document:

q.registerMacros({
    title: function (engine, body) {
        engine.deferred(function (path, docs) {
            return docs.meta.get("title");
        });
    }
});
q.toHTML("[title ::][meta :: title = hello]"); // hello

Even though the meta-information is set after the macro call, deferred waits for it to be set before executing the function.

Now, you may wonder what happens if the title was set by another deferred, and this other deferred came after. The answer is that it will still work, because Quaint may execute a deferred multiple times. Essentially, Quaint detects what documents are consulted by a deferred, and it executes it again if these change, until an equilibrium is achieved, or a maximum number of iterations is reached (currently that maximum is 10, so buggy or ill-behaved deferred can only do limited damage).

eval(expr, env)

Evaluate an expression using the current evaluator (which may be a key/value store, or a JavaScript evaluator, or an Earl Grey evaluator, etc.) Optionally the method takes an environment parameter (variables to define for the evaluation).

q.plug(quaint-javascript);
q.eval("2 + 2") // 4
q.eval("a + b", {"a": 1, "b": 8}) // 9

Environment variables may also be set with setenv.

fork()

Create a new Engine from the current one. The new Engine has its own scope, meaning that rules and environment variables set on it will not affect its parent.

gen(node)

Generate an ENode from a QAst using all the Engine‘s rules and macros. Unlike toENode, however, gen does not evaluate Deferred nodes.

gen is the method that should be used to recursively process nodes in a rule or macro (if such processing is desired).

genFromSource(src)

This is just like gen, but instead of taking a QAst as input, it takes a source string, which it parses and then processes. It is basically equivalent to:

node.genFromSource(x) <=> node.gen(quaint.parse(x))

into(document, value)

into lets you generate data into a “document” which isn't the main one. For instance, meta :: title = hello generates a key/value pair in the meta document, whereas a header may generate information in the sections document, an error handler may register the error into the errors document, and so on.

The result of this operation will not show in the output, unless a different operation decides to display the stashed value (for example, headers stash information into sections, and toc extracts it to display a table of contents).

Also, note that into is declarative, it doesn't actually have side-effects when called. Therefore, even though the return value of into is not displayed, a plugin needs to return it, or embed it in the return value, in order for it to do anything.

Extracting values stashed in sub-documents with into is typically done with deferred. This ensures the extraction operates after all the stashing is done.

plug(plugin, ...)

Execute one or more plugins on the Engine.

redefer(node, function)

If a macro or rule wishes to execute a deferred to inspect its contents, for instance to implement a conditional, redefer will take a generated result (which may be an ENode, a string, a Deferred, and so on) and a function, and it will execute the function on the node either immediately (if the node is not a Deferred) or when the Deferred is executed.

For example:

q.registerMacros({
    "if": function (engine, cond, body) {
        return engine.redefer(engine.gen(cond), function (result) {
            if (result)
                return engine.gen(body)
            else
                return ""
        });
    }
});
q.toHTML("[if meta::x :: hi][meta :: x: true]"); // hi

Thanks to redefer we can ensure that the condition is computed after the meta-information is set. In fact, redefer is more or less required for it to work.

Just like deferred, redefer may be executed multiple times. For instance, it is possible that as a result of the order in which some operations are done, the value of a condition changes, and then the if will be recomputed.

registerDocuments(docs)

Register new documents. You would use this if you wanted to create a references document for bibliographies, or a links document that gathers all links on the page, and so on.

There are two document types you may instantiate:

q.registerDocuments({
    references: quaint.MapDocument(),
    links: quaint.SeqDocument()
});

registerMacros(macros)

A macro named m is meant to be used as m :: body or m arg :: body, or even m arg1 arg2 :: body, and so on.

q.registerMacros({
    ignore: function (engine, body) {
        return "";
    }
});
q.toHTML("1[ignore :: 2]3"); // 13

The first argument is always the Engine. The last is always the body.

registerMethods(methods)

The object given as methods is merged with the Engine, in other words, this adds methods to the Engine.

engine.registerMethods({berry: function () { return "juicy!"; }});
engine.berry()  // "juicy!"

registerResolvers(res)

A resolver is a function that takes a filename or symbol of sorts, and returns a string corresponding to the contents. If, for instance, you have the Quaint statement:

template :: xyz

This will resolve xyz by calling engine.resolvers.template("xyz"). Each macro that may import files has a resolver, and there is a default resolver as well (which just assumes the path is relative to the current working directory).

include :: file.json   ;; engine.resolvers.include("file.json")
format file.json ::    ;; engine.resolvers.format("file.json")
plugin xyz ::          ;; engine.resolvers.plugin("xyz")

Note that the latter will never use the default resolver, since it seeks a package and not a file.

registerRules(rules)

Define a new rule. A rule is a pattern where certain words are made into variables by prefixing them with a backslash. Of course, when defining such a pattern in a programming language like JavaScript, one ought to use two backslashes.

q.registerRules({
    "$\\x": function (engine, vars) {
        return [engine.gen(vars.x), " dollars"];
    }
});
q.toHTML("I give you $100"); // I give you 100 dollars

setenv(env)

Set variables for use by the current evaluator inside curly braces. The syntax for the evaluator depends on the plugins used.

q.plug(quaint-javascript);
q.setenv({"a": 11, "b": 22});
q.toHTML("{a + b}"); // 33

toENode(src, options)

Generate an ENode from text. Same as translate(src, "enode", options). See translate for the options.

toHTML(src, options)

Generate HTML from text. Same as translate(src, "html", options). See translate for the options.

translate(src, format, options)

Execute Quaint on the source text and return something in the desired format.

Available formats:
  • "enode"
  • "html"
Options:
  • paragraph: wrap the result in a paragraph tag (default false)
  • noTemplate: avoid executing templates (default false)