Prescript is a framework for running macros over code that will eventually become javascript. It includes a number of beautiful macros out of the box.
View an example of code written with the out-of-the-box PreScript Macro's here..
Prescript does its best not to touch any code that does not explicitly match a defined macro pattern. Any valid javascript is acceptable in a prescript file without modification. When you decide you want to make use of a Prescript macro, just start using them, one at a time - you don't need to make any bold efforts to learn learn and transition to a new language.
Whitespace defined blocks are wrapped in curly brackets {}
if they were left out. The result is that the code looks a little Python, Ruby, or well written Coffeescript, if you want it to. If you want it to look like Javascript or C that's fine too.
Functions declarations containing variables marked with a #
will be automatically wrapped in an IIFE ensuring a closure is created around the #
marked parameters. Eg fn = (intParam, #extParam) -> body
. Simple IIFEs can also be generated using the (#) -> doImmediately()
syntax. Simple generator IIFEs (ie. those that use the yield
keyword) will automatically flush to an array Eg.
oneToTen = (#) -> for(x = 1 to 10) yield x
All valid ES6 is valid including array and object destructuring, object creation shorthand, await, and yield. By default babel is used in loose mode to make everything ES5 compatible. This is, however, configurable.
The *
(the generator function marker) and async
will automatically be included as a prefixes to function declarations where the keywords yield
and await
are found within a function body. This feature also allows you to notionaly use arrow function syntax while creating async and generator functions. Functions defined using the tilde-arrow ~>
will automatically be wrapped in new Promise(function(resove,reject){})
. eg.
get (url) ~> if(true) resolve(data) else reject(error)
Skinny arrow functions are simply shorthand for the standard function keyword. They'll be translated into hoisted function declarations if assigned directly to a variable defined in the current scope. Eg. hoistedFn = (params) -> fnBody
. Shorthand function assignment means you can exclude the assignment character (=
) when assigning an anonymous function to property or variable.
hoistedFn -> fnBody #create a hoisted function declaration with no parameters
obj.prop => fnBody #create a bound function with no parameters
obj.prop = (x, y) => fnBody #create a bound function with two parameters
list.map(item => item * 2) #create an anonymous function with one parameter
Code blocks to be run immediatley before an assignment or function invocation can be defined after the assignment or function invocation. Eg. x = add(a, 10) where a = multiply(3,7)
. This is useful where a function has a complex call signature or for grouping multiple statements in a block and providing context to that block. Another posfix option simply uses the assignment character (=
) but with a function invocation on the left-hand-side Eg. console.log() = "Hello World"
. In this situation, the right-hand-side will be passed as an additional argument to the function when it is invoked. To pass the right-hand-side as an argument in a specific position, use the asterix character Eg. setTimeout(*, 1000) => alert("Hello")
.
# each of the following are functionally identical
router.get('/path') = (req, res) => res.end("Done")
router.get('/path', *) = (req, res) => res.end("Done")
router.get('/path', handler) where handler = (req, res) => res.end("Done")
router.get('/path', (req, res) => res.end("Done"))
Variables can be set to automatically be defined within the scope that they are first used, using either var, let, or const. This can be configured to only apply to variables defined in loop declarations and catch statements. It is recommended to use semantic-colouring in your IDE if you have this feature enabled for cover all variables. Eg. default const
. Automatic variable declaration can also be switched off for a scope using default undefined
.
Tables of data can be defined using a shorthand syntax by defining a block after an assignment (=
). Each line within the block not ending in a comma is considered a new element of a parent array. Lists's of dictionaries can be created using this syntax too eg.
let table1 =
"A1", "B1", "C1"
"A2", "B2", "C2"
let table2 =
a:1, b:1, c:1
a:2, b:2, c:2
Allows individual expressions to be easily wrapped in a try/catch statement with the catch returning a default value. Eg. try JSON.parse(string) catch {error: 'poorly formed json'}
Lists of colon delimited pairs are wrapped in curly brackets {}
when used in direct assignment or when wrapped in curved brackets ()
. Eg. dict = a:b, c:d, e:f
or fn(a:b, c:d)
Deep properties of an object can be extracted without fear of cannot read property of undefined
by placing a single question mark at the end of a property list. eg. a.b.c.d?
. A new ?=
operator sets variables to a value only if that variable is not currently undefined
Eg. myArg ?= 10
.
Macros and string template 'tags' can be defined with near-identical syntax. The former are processed at compile and inlined. Note the macro
, tag
keywords below which are used to define a tag or macro, and the cast operator ::
in the example below. The ::
operator can also be used for expression typecasting with a static type checker like Flow or Typescript Eg. x = string :: getResult()
.
castOperatorExamples ->
require :: express, esprima, redis # this is an example of a macro
let realname = string :: input.name # this will resut in a typescript/flow type annotation on "realname"
graph = graph :: a -> b # detect when to terminate string expressions using smart rules (newline, comment, parens, block)
constraints.push(formula :: a + b ** 2) # compile time macro may convert this to an object literal representing the function
document.render(content) where content = jsx :: <span><b>{username} says</b>: {message}</span>
title.setAttribute('text') = i8n :: "Hello {{username}}" #quotes are optional
query = sql :: select * from table1
result = await promisify :: http.request(options)
An important additional benefit of the ::
operator is that it can be used by IDEs and static processors to help identify how to colour and validate embedded non-javascript syntax (think of it a little like a file extension but for a string expression). Types can be defined using the standard type typescript/flow syntax Eg. type callback = (error:boolean, result:string) -> void
or through use of the cast operator.
type callback = (boolean :: error, string :: result) -> void
For-in loops can be defined to extract both key and value at the same time by providing a second variable name when the loop is declared. Similarly, a third variable name allows the index to be captured. This feature exists to reduce confusion between for-of and for-in. Eg. for (key, value in dict)
. The to
keyword can be used to define standard for loops. Eg. for (x = 0 to array.length)
The result of one expression can be automatically passed to another expression by combining the >>
operator and asterix *
characters. eg. score = await db.get(input) >> JSON.parse(*) >> parseInt(*.value) / 100
.
The describe
keyword has been proposed as a block structure containing information relevant only at development time. This includes textual descriptions of functions and inline unit tests. This feature is not currently in development but is being considered.
#Reference example
addThree(number :: input) -> return input + 3
describe addThree
purpose: "Add three to a provided number"
input: "Any value or object that can contains a valueOf() function"
tests: addThree(4) == 7
#Self-contained example, useful for anonymous functions
addFour(input) ->
describe
purpose: "Add four to a provided number"
input: "Any value or object that can contains a valueOf() function"
tests: addThree(4) == 8
return input + 4
Prescript is pre-alpha and not ready for usage in any context. Pull requests and other contributions welcome.