nxtpression

Friendly observable-based template expression library.

const { of } = require ( ' rxjs ' ) const nxtpr = require ( ' nxtpression ' ) nxtpr . produceObservable ( ' {{names|greet}} ' , { names : of ( " Mike " , " Ike " ) , greet : x => ` Hi ${ x } . ` } ) . subscribe ( console . log )

You can get much fancier than that though!

🚼 Syntax Reference

NOTE: Every expression represents a stream of values. Mix and match as you see wish!

{ { [ { [ currentKey ] : currentInput } ] | map ( currentlySelectedTransform | processResult ( currentScheduler ) ) } }

Array [foo, bar]

{ { [ 1 ] } } { { [ 1 , 2 ] } } { { [ foo , 2 ] } }

Boolean true / false

{ { true } } { { false } }

Function Call foo(bar)

Note that no currying is performed.

{ { of ( 1 , 2 ) } } { { seq ( period , num ) } } { { mul ( 2 ) ( 3 ) } }

Index foo[bar]

{ { a [ b ] } }

Member foo.bar

{ { foo . bar } }

Null null

{ { null } }

Number 1.23

WARNING: this does not support scientific notation like 1e3

{ { 12 } } { { 1 . 2345 } }

Object { foo: foo, [bar]: baz }

Supports both static and dynamic keys

WARNING: does not yet support the same-name utility syntax { foo }

{ { { } } } { { { a : 1 } } } { { { a : foo } } } { { { a : 1 , b : 2 } } } { { { [ a ] : 1 } } } { { { [ a ] : 1 , [ b ] : 2 } } } { { { a : 1 , [ b ] : 2 } } }

Pipe foo | bar

{ { 2 | mul ( 3 ) } } { { " ff " | parseHex } } { { stream | sub ( 5 ) | mul ( 2 ) | add ( 1 ) } } { { x | map ( mul ( 2 ) | add ( 1 ) ) } }

Reference foo

{ { a } } { { myVar } }

String "foo {{ bar }}"

A bit more complicated, since nested templates are supported. Strings can be delimited by either ' s or " s.

WARNING: templates inside non-empty strings will have to convert all emissions to String .

WARNING: can't use ` delimiters

{ { " " } } { { ' ' } } { { " a " } } { { " {{ " " }} " } } { { " {{ 1 }} " } } { { " a{{ 1 }} " } } { { " {{ " a " }} " } } { { " hell{{ " o " }} world " } }

Undefined undefined

{ { undefined } }

🕯 Core API

The core design is really simple.

Tokenize (look at source code and find where the tokens are - information is found) Parse (take the tokens and produce a tree structure of nodes - relationships appear) Compile (create a factory function, mapping a context to an observable - code becomes callable) Inject a context (inject streams and values to produce a composite stream as defined in the template) Subscribe the stream! 🎳

For more information on steps 1-3, watch the destroyallsoftware screencast linked in the bottom for a terrific 30 minute short introduction on writing a compiler from scratch. For more information on steps 4-5, learn RxJS.

tokenize : (source) => tokens (throws on unexpected token)

Tokenizes the source string and returns an Array of tokens.

Notably, a string is not a string until it has been parsed. An actual string is formed during parsing.

type : String ; For a list of possible values, see tokenize.js

: ; For a list of possible values, see start : Number ; The index in the source string where the token starts.

: ; The index in the source string where the token starts. body : String ; The full slice of text as copied from the source string.

parseFromTokens : (source, tokens) => AST (throws on invalid semantics)

Parses tokens into an abstract syntax tree - a recursive structure of nodes.

A node has the following shape: { type, ...properties }

func - a function ( path : optional parent node, args - array of nodes called with)

- a function ( : optional parent node, - array of nodes called with) ref - a reference ( name : token.body , col : token.start )

- a reference ( : , : ) boolean - a boolean ( value : the parsed Boolean value)

- a boolean ( : the parsed value) object - an object ( props : array of { path, expr } nodes - there are two types of path , see below) constprop - a statically named property ( value : the static name of the property) dynprop - a dynamically named property ( expr : a node representing an expression for the property name)

- an object ( : array of nodes - there are two types of , see below) stringparts - a string possibly containing a nxtpression ( parts : a list of nodes of varying type) string - a string literal ( body : the raw string value)

- a string possibly containing a nxtpression ( : a list of nodes of varying type) index - an index into something ( node : the node representing something to index a property off of, expr : the node representing the value to index off of the thing)

- an index into something ( : the node representing something to index a property off of, : the node representing the value to index off of the thing) member - a member access ( node : the node representing something to access a property off of, property : the name of the property to access)

- a member access ( : the node representing something to access a property off of, : the name of the property to access) array - an array ( items : array of nodes reprenting the values in the array)

- an array ( : array of nodes reprenting the values in the array) number - a number ( value : the parsed Number value)

- a number ( : the parsed value) pipe - a function piping the left hand side to the right hand side ( parts : the nodes representing the objects to pipe through. Most of the time, the first node will reference a value from context and the other nodes are functions to pipe that value through)

- a function piping the left hand side to the right hand side ( : the nodes representing the objects to pipe through. Most of the time, the first node will reference a value from context and the other nodes are functions to pipe that value through) null - the value null (n/a)

- the value (n/a) undefined - the value undefined (n/a)

compileFromAST : (source, AST, options) => (context => Observable) (throws on undefined variable access if enabled)

Compiles a template string from an AST.

The options object supports the following properties:

throwOnUndefinedVariableAccess - if true , will throw when a value referenced in the context is undefined (default: false )

IGNORE : Symbol

Consider running {{ 4 | ignoreEven | doSideEffect }} , with ignoreEven: x => x % 2 === 0 ? IGNORE : x . This will not perform the side effect, because once an IGNORE symbol is discovered, the symbol will be emitted immediately.

WARNING: this WILL still emit once - with the IGNORE symbol - in order to ensure that observables complete.

♿️ 🕹 Utility API

There are some nice utilities to make your life easier. Basically just some wrappers around the core API and a heuristic to help determining whether a string might contain a template.

isNotATemplate : (string) => Boolean (does not throw)

If string is not a String containing '{{' , this will return true .

WARNING: this is only a heuristic when returning false . If you need to be certain, please parse the string and make sure no error is thrown.

parseFromSource : (source) => AST (throws on unexpected token or invalid semantics)

This utility function will tokenize and parseFromTokens in one go.

compileTemplate : (source, options) => (context => Observable) (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

This utility function will tokenize , parseFromTokens and compileFromAST in one go. For a list of options, see compileFromAST .

produceObservable : (source, context, options) => Observable (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

This utility function will just compileTemplate(source, options)(context) . For a list of options, see compileFromAST .

resolveTemplate : (source, context, options) => Promise (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

This creates a Promise awaiting the first value emitted by a template compiled from source directly when applied with the given context. For a list of options, see compileFromAST .

🍇 Object Templates API

An object template is a recursive structure of arrays and objects containing template strings, e.g.

{ a : [ { b : ' {{ 1 }} ' } ] , c : { d : ' {{ 2 }} ' } , e : ' {{ 3 }} ' }

However, it does not work recursively in returned values

resolveObjectTemplate ( { a : ' {{ t }} ' } , { t : ' {{ 1 }} ' } )

compileObjectTemplate : (obj, options) => (context => Observable) (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

Compiles and combines all templates in an object or array recursively. For a list of options, see compileFromAST .

produceObjectObservable : (obj, context, options) => Observable (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

This utility function will just compileObjectTemplate(source, options)(context) . For a list of options, see compileFromAST .

resolveObjectTemplate : (obj, context, options) => Promise (throws on unexpected token or invalid semantics or - if enabled, undefined variable access)

Like resolveTemplate but for object templates, this Promise s the latest value of all template strings once they all have emitted at least once. For a list of options, see compileFromAST .

🔌 Installation

With npm installed;

npm i nxtpression

🏆 Credit

Inspired by destroyallsoftware’s a compiler from scratch.

Prior art includes jinja2 and nxtedition templates.