├── .gitignore ├── .gitmodules ├── .travis.yml ├── COPYRIGHT ├── Changelog ├── LICENSE ├── Makefile ├── README.md ├── apps ├── hello-world.fun ├── instagram │ └── instagram.fun └── todo-mvc │ ├── todo-mvc.css │ └── todo-mvc.fun ├── bin └── fun ├── examples ├── chat.css ├── chat.fun ├── composit_statements.js ├── drag.fun ├── drag2.fun ├── for-loops.fun ├── if_else.fun ├── image.fun └── switch.fun ├── package.json ├── scripts └── run-tests.js ├── specification ├── compiler.txt ├── specification.txt └── types.md ├── src ├── README.md ├── compiler.js ├── dev-client.html ├── dev-server.js ├── highlighter │ ├── codePrinter.css │ └── codePrinter.js ├── info.js ├── modules │ ├── alert.fun │ ├── app.fun │ ├── console.fun │ ├── facebook.fun │ ├── jsonp.fun │ ├── list.fun │ ├── localstorage.fun │ ├── location.fun │ ├── mouse.fun │ ├── style.fun │ ├── tap.fun │ ├── text.fun │ ├── time.fun │ ├── twitter.fun │ ├── ui │ │ └── lists.fun │ ├── uuid.fun │ ├── viewport.fun │ └── xhr.fun ├── parser.js ├── resolver.js ├── runtime │ ├── expressions.js │ ├── library.js │ └── normalize.css ├── tokenizer.js └── util.js └── test ├── ast-mocks.js ├── parser-mocks.js ├── resolver-mocks.js ├── runtime-mocks.js └── tests ├── test-1-runtime-library.js ├── test-2-parser.js ├── test-3-resolver.js └── test-4-compiler.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *~ 4 | .*swp 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "node_modules/std"] 2 | path = node_modules/std 3 | url = https://github.com/marcuswestin/std.js.git 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 - Marcus Westin 2 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | v0.3.0 2 | + Alias of v0.2.23 3 | + v0.2.23 had enough changes to warrant a big version number bump 4 | 5 | v0.2.23 6 | + Wooe, so many improvements I can't even list them all. 7 | + Awesome dev environment! Automatically sync UI with file on every save. 8 | 9 | v0.2.22 10 | + Allow for keywords to be XML attribute names 11 | + Add --normalize.css=false and minify=true commandline options 12 | + text module with text.trim 13 | + Implement != and ! comparative operators 14 | + Rename filter module to list, and make it list.filter 15 | 16 | v0.2.21 17 | + Hash-expand xml attributes a la
18 | + New tap modules 19 | + Back to using npm node modules instead of git... 20 | 21 | v0.2.20 22 | + Add support for automatic less preprocessing of elements 23 | + Add support for stylus 24 | + Automatically detect that less or stylus is required, and install them dynamically at runtime if needed 25 | + Handler event arguments are now cancelable 26 | + Allow for commas between XML attributes, e.g.
27 | + Remove need for `let` in front of declarations. Now it's just `foo = "bar"` 28 | 29 | v0.2.19 30 | + Disabled minification for now 31 | + Add -v/--version flags 32 | 33 | v0.2.18 34 | + Allow for `foo is "image"` as a shorthand for `foo is = "image"` 35 | + Inline all stylesheets with static hrefs as a 9 | 10 | 11 |

12 | 	
13 | 	
32 | 


--------------------------------------------------------------------------------
/src/dev-server.js:
--------------------------------------------------------------------------------
  1 | require('colors')
  2 | 
  3 | var socketIo = require('socket.io'),
  4 | 	curry = require('std/curry'),
  5 | 	fs = require('fs'),
  6 | 	path = require('path'),
  7 | 	time = require('std/time'),
  8 | 	http = require('http')
  9 | 
 10 | module.exports = {
 11 | 	compileFile: compileFile,
 12 | 	listen: listen,
 13 | 	mountAndWatchFile: mountAndWatchFile,
 14 | 	serveDevClient: serveDevClient,
 15 | 	respond: respond
 16 | }
 17 | 
 18 | function compileFile(filename, opts, callback) {
 19 | 	loadCompiler().compileFile(_resolveFilename(filename), opts, callback)
 20 | }
 21 | 
 22 | function listen(port, filenameRaw, opts) {
 23 | 	if (!port) { port = 8080 }
 24 | 
 25 | 	var filename = _resolveFilename(filenameRaw)
 26 | 
 27 | 	try {
 28 | 		var stat = fs.statSync(filename)
 29 | 	} catch(e) {
 30 | 		console.error('Error:', e.message ? e.message : e.toString())
 31 | 		return process.exit(-1)
 32 | 	}
 33 | 
 34 | 	if (!stat.isFile()) {
 35 | 		console.error('Error:', filename, 'is not a file.')
 36 | 		return process.exit(-1)
 37 | 	}
 38 | 
 39 | 	var server = http.createServer(function(req, res) {
 40 | 		if (req.url == '/favicon.ico') {
 41 | 			return send404(res)
 42 | 		} else {
 43 | 			serveDevClient(req, res)
 44 | 		}
 45 | 	})
 46 | 
 47 | 	function send404(res) {
 48 | 		res.writeHead(404)
 49 | 		res.end()
 50 | 	}
 51 | 	
 52 | 	mountAndWatchFile(server, filename, opts)
 53 | 	server.listen(port)
 54 | 	console.log('Fun!'.magenta, 'Serving', filenameRaw.green, 'on', ('localhost:'+port).cyan, 'with these options:\n', opts)
 55 | }
 56 | 
 57 | function _resolveFilename(filename) {
 58 | 	return filename[0] == '/' ? filename : path.join(process.cwd(), filename)
 59 | }
 60 | 
 61 | function serveDevClient(req, res) {
 62 | 	fs.readFile(path.join(__dirname, './dev-client.html'), curry(respond, res))
 63 | }
 64 | 
 65 | function mountAndWatchFile(server, filename, opts) {
 66 | 	var serverIo = socketIo.listen(server)
 67 | 	serverIo.set('log level', 0)
 68 | 
 69 | 	serverIo.sockets.on('connection', function(socket) {
 70 | 		console.log("Dev client connected")
 71 | 		loadCompiler().compileFile(filename, opts, function broadcast(err, appHtml) {
 72 | 			socket.emit('change', { error:errorHtml(err), html:appHtml })
 73 | 		})
 74 | 	})
 75 | 
 76 | 	var lastChange = time.now()
 77 | 	fs.watch(filename, function(event, changedFilename) {
 78 | 		if (time.now() - lastChange < 1000) { return } // Node bug calls twice per change, see https://github.com/joyent/node/issues/2126
 79 | 		lastChange = time.now()
 80 | 		console.log(filename, "changed.", "Compiling and sending.")
 81 | 		loadCompiler().compileFile(filename, opts, function broadcast(err, appHtml) {
 82 | 			serverIo.sockets.emit('change', { error:errorHtml(err), html:appHtml })
 83 | 		})
 84 | 	})
 85 | }
 86 | 
 87 | function loadCompiler() {
 88 | 	for (var key in require.cache) {
 89 | 		delete require.cache[key]
 90 | 	}
 91 | 	return require('../src/compiler')
 92 | }
 93 | 
 94 | function respond(res, e, content) {
 95 | 	if (e) {
 96 | 		res.writeHead(500)
 97 | 		res.end(errorHtmlResponse(e))
 98 | 	} else {
 99 | 		res.writeHead(200)
100 | 		res.end(content.toString())
101 | 	}
102 | }
103 | 
104 | function errorHtmlResponse(e) {
105 | 	return ['','',
106 | 		'',
107 | 		errorHtml(e),
108 | 		''
109 | 	].join('\n')
110 | }
111 | 
112 | function errorHtml(e) {
113 | 	if (!e) { return null }
114 | 	return [
115 | 		'
',
116 | 			e.stack ? e.stack : e.message ? e.message : e.toString ? e.toString() : e || 'Error',
117 | 		'
' 118 | ].join('\n') 119 | } 120 | -------------------------------------------------------------------------------- /src/highlighter/codePrinter.css: -------------------------------------------------------------------------------- 1 | #verbose-output { 2 | position:fixed; 3 | top:5px; 4 | right:5px; 5 | width:500px; 6 | height:95%; 7 | border:1px solid #333; 8 | overflow:auto; 9 | padding: 2px; 10 | } 11 | 12 | #verbose-output pre { background-color: #002f58; color: #fff; } 13 | #verbose-output code .keyword { color: #ffee7d; } 14 | #verbose-output code .operator { color: #f29d20; } 15 | #verbose-output code .xml { color: #d27d00; } 16 | /*#verbose-output code .type { color: #ff9d1b; }*/ 17 | #verbose-output code .type { color: #f8d700; } 18 | #verbose-output code .global { color: #f8d700; } 19 | #verbose-output code .attr { color: #eb8f86; } 20 | #verbose-output code .string { color: #3ad900; } 21 | #verbose-output code .string * { color: #3ad900; } 22 | -------------------------------------------------------------------------------- /src/highlighter/codePrinter.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | 3 | exports.prettyPrint = function(ast, indent) { 4 | if (ast instanceof Array) { 5 | var result = [] 6 | for (var i=0; i < ast.length; i++) { 7 | result.push(util.prettyPrint(ast[i], indent)) 8 | } 9 | return "\n" + result.join('') 10 | } else if (typeof ast == 'object') { 11 | indent = (typeof indent == 'undefined' ? '' : indent + '\t') 12 | var result = [] 13 | for (var key in ast) { 14 | result.push(indent + key + ':' + util.prettyPrint(ast[key], indent)) 15 | } 16 | return "\n" + result.join('') 17 | } else { 18 | return JSON.stringify(ast) + "\n" 19 | } 20 | } 21 | 22 | exports.highlightCode = function(code) { 23 | var lines = code.split('\n'), newLines = [] 24 | for (var i=0; i < lines.length; i++) { 25 | newLines.push(exports.highlightLine(lines[i])) 26 | } 27 | return newLines.join('\n') 28 | } 29 | function highlightLine(line) { 30 | return line 31 | .replace(/"([^"]*)"/g, function(m) { return ''+m+''}) 32 | .replace(/'([^']*)'/g, function(m) { return ''+m+''}) 33 | .replace(/(let|for|in|if|else) /g, function(m) { return ''+m+''}) 34 | .replace(/(handler) ?/g, function(m) { return ''+m+''}) 35 | .replace(/(Session|Local|Global)/g, function(m) { return ''+m+''}) 36 | .replace(/(data|onClick)=/g, function(m,m2) { return ''+m2+'='}) 37 | .replace(/(<|>)/g, function(m) { return ''+m+''}) 38 | .replace(/ = /g, ' = ') 39 | } 40 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcuswestin/fun/63015e476a8aa51ab6ed64aacd24503d72c1b5a7/src/info.js -------------------------------------------------------------------------------- /src/modules/alert.fun: -------------------------------------------------------------------------------- 1 | alert = function(a,b,c) { 2 | 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/app.fun: -------------------------------------------------------------------------------- 1 | app = { 2 | whenLoaded:function(appLoadedHandler) { 3 | 8 | } 9 | } -------------------------------------------------------------------------------- /src/modules/console.fun: -------------------------------------------------------------------------------- 1 | console = { 2 | log: function(a,b,c) { 3 | 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/modules/facebook.fun: -------------------------------------------------------------------------------- 1 | import Local 2 | 3 | facebook = { 4 | connected: false, 5 | connect: javascriptHandler("facebookModule.connect"), 6 | user: { 7 | name: '', 8 | id: 0 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/modules/jsonp.fun: -------------------------------------------------------------------------------- 1 | jsonp = { 2 | get:function(path, args, responseHandler) { 3 | result = { loading:true, error:null, response:null } 4 | 28 | return result 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/list.fun: -------------------------------------------------------------------------------- 1 | list = { 2 | 3 | filter: function(list, func) { 4 | 15 | } 16 | 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/modules/localstorage.fun: -------------------------------------------------------------------------------- 1 | localstorage = { 2 | // TODO Make this a template so that it doesn't execute multiple times 3 | persist:function(variable, name) { 4 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/location.fun: -------------------------------------------------------------------------------- 1 | import Local 2 | 3 | location = { 4 | navigate: javascriptHandler("locationModule.navigate"), 5 | state: '', 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/modules/mouse.fun: -------------------------------------------------------------------------------- 1 | mouse = { 2 | x: 0, 3 | y: 0, 4 | isDown: false 5 | } 6 | 7 | 25 | -------------------------------------------------------------------------------- /src/modules/style.fun: -------------------------------------------------------------------------------- 1 | style = { 2 | gradient:function(from, to) { 3 | return { style: { 4 | background:'-webkit-gradient(linear, left top, left bottom, from('+from+'), to('+to+'))' 5 | } } 6 | } 7 | } -------------------------------------------------------------------------------- /src/modules/tap.fun: -------------------------------------------------------------------------------- 1 | tap = { 2 | 3 | button:function(selectHandler) { 4 | hashAttributes = { class:'tap-button' } 5 | 12 | return hashAttributes 13 | } 14 | 15 | listItem:function(selectHandler) { 16 | hashAttributes = { class:'tap-list-item' } 17 | 24 | return hashAttributes 25 | } 26 | 27 | } 28 | 29 | 46 | 47 | 48 | // TODO inject styles 49 | // .tap-button { 50 | // -webkit-touch-callout: none; 51 | // -webkit-user-select: none; /* Disable selection/Copy of UIWebView */ 52 | // -webkit-touch-callout: none; 53 | // -webkit-user-select: none; 54 | // -khtml-user-select: none; 55 | // -moz-user-select: none; 56 | // -ms-user-select: none; 57 | // user-select: none; 58 | // } 59 | // 60 | -------------------------------------------------------------------------------- /src/modules/text.fun: -------------------------------------------------------------------------------- 1 | text = { 2 | trim: function(text) { 3 | 7 | } 8 | } -------------------------------------------------------------------------------- /src/modules/time.fun: -------------------------------------------------------------------------------- 1 | time = { 2 | after:function(amount, notifyHandler) { 3 | 8 | } 9 | } -------------------------------------------------------------------------------- /src/modules/twitter.fun: -------------------------------------------------------------------------------- 1 | twitter = { 2 | search:function(searchQuery) { 3 | // var result = { loading:true, error:null, response:null } 4 | var result = null 5 | 33 | return result 34 | } 35 | } -------------------------------------------------------------------------------- /src/modules/ui/lists.fun: -------------------------------------------------------------------------------- 1 | lists = { 2 | 3 | makeScroller = function(viewSize, opts) { 4 | headHeight = opts.headSize ? opts.headSize : 45 5 | numViews = opts.numViews ? opts.numViews : 3 6 | contentSize = { 7 | width: viewSize.width 8 | height: viewSize.height - headHeight 9 | } 10 | size = { style:contentSize } 11 | float = { style:{ 12 | float:'left' 13 | } } 14 | scrollable = { style:{ 15 | 'overflow-y': 'scroll' 16 | '-webkit-overflow-scrolling': 'touch' 17 | } } 18 | crop = { style:{ 19 | overflowX:'hidden' 20 | } } 21 | scroller = { 22 | 23 | stack: [null] 24 | 25 | renderHead: template(renderHeadContent) { 26 |
27 | renderHeadContent() 28 |
29 | } 30 | 31 | renderBody: template(renderBodyContent) { 32 | offset = scroller.stack.length - 1 33 | sliderStyle = { 34 | height:contentSize.height 35 | width:contentSize.width * numViews 36 | '-webkit-transform':'translateX('+(-offset * contentSize.width)+'px)' 37 | '-webkit-transition':'-webkit-transform 0.70s' 38 | position:'relative' 39 | } 40 |
41 |
42 |
43 | for view in scroller.stack { 44 |
45 | renderBodyContent(view) 46 |
47 | } 48 |
49 |
50 |
51 | } 52 | 53 | push: handler(view) { 54 | scroller.stack push: view 55 | } 56 | 57 | pop: handler() { 58 | scroller.stack pop: null 59 | } 60 | } 61 | 62 | return scroller 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/modules/uuid.fun: -------------------------------------------------------------------------------- 1 | uuid = { 2 | v4:function() { 3 | result = null 4 | 17 | return result.copy() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/viewport.fun: -------------------------------------------------------------------------------- 1 | viewport = { 2 | size: { 3 | width: 0 4 | height: 0 5 | }, 6 | 7 | fitToDevice: template() { 8 | 9 | } 10 | } 11 | 12 | 61 | -------------------------------------------------------------------------------- /src/modules/xhr.fun: -------------------------------------------------------------------------------- 1 | xhr = { 2 | post:function(path, args, responseHandler) { 3 | return xhr._send('post', path, args, responseHandler) 4 | }, 5 | get: function(path, args, responseHandler) { 6 | return xhr._send('get', path, args, responseHandler) 7 | }, 8 | postJson:function(path, args, responseHandler) { 9 | return xhr._send('post', path, args, responseHandler, { 'Content-Type':'application/json' }) 10 | }, 11 | _send: function(method, path, args, responseHandler, headers) { 12 | result = { loading:true, error:null, response:null } 13 | 14 | 44 | return result 45 | }, 46 | loading: false 47 | } 48 | -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | var util = require('./util'), 2 | curry = require('std/curry'), 3 | isArray = require('std/isArray'), 4 | q = util.q, 5 | halt = util.halt, 6 | assert = util.assert 7 | 8 | var L_PAREN = '(', R_PAREN = ')', 9 | L_CURLY = '{', R_CURLY = '}', 10 | L_BRACKET = '[', R_BRACKET = ']' 11 | 12 | var gToken, gIndex, gTokens, gState 13 | 14 | exports.parse = function(tokens) { 15 | gTokens = tokens 16 | gIndex = -1 17 | gToken = null 18 | 19 | var ast = [] 20 | 21 | var setupAST 22 | while (setupAST = parseImports()) { ast.push(setupAST) } 23 | 24 | while (peek()) { ast.push(parseTemplateBlock()) } 25 | 26 | return util.cleanup(ast) 27 | } 28 | 29 | /************************************************ 30 | * Imports come before any of the emitting code * 31 | ************************************************/ 32 | function parseImports() { 33 | if (peek('keyword', 'import')) { return _parseImportStatement() } 34 | } 35 | 36 | var _parseImportStatement = astGenerator(function() { 37 | advance('keyword', 'import') 38 | if (peek('string')) { 39 | // back compat import "./foo/bar" 40 | var path = advance('string').value 41 | return { type: 'IMPORT_FILE', path: path.value } 42 | } else { 43 | if (peekNewline()) { halt(gToken, 'Expected an import path') } 44 | 45 | if (!peek('symbol', ['.', '/']) && !peek('name')) { 46 | halt(peek(), 'Expected an import path') 47 | } 48 | 49 | var first = advance(['symbol', 'name']) 50 | var path = first.value 51 | 52 | if (first.type == 'name' && peekNoWhitespace('symbol', '/')) { 53 | path += advance().value 54 | } else if (first.value == '.') { 55 | while(peekNoWhitespace('symbol', ['.','/'])) { 56 | path += advance().value 57 | } 58 | } 59 | 60 | assert(gToken, path[path.length-1] != '.', 'Bad import path') 61 | 62 | while(peekNoWhitespace('name')) { 63 | path += advance().value 64 | if (peekNoWhitespace('symbol', '/')) { 65 | path += advance().value 66 | } 67 | } 68 | 69 | return { type:'IMPORT', path:path } 70 | } 71 | }) 72 | 73 | /************* 74 | * Templates * 75 | *************/ 76 | var parseTemplateLiteral = astGenerator(function() { 77 | var callable = parseSignatureAndBlock('template', parseTemplateBlock) 78 | return { type:'TEMPLATE', signature:callable[0], block:callable[1] } 79 | }) 80 | 81 | var parseTemplateBlock = function() { 82 | var controlStatement = tryParseControlStatement(parseTemplateBlock) 83 | if (controlStatement) { return controlStatement } 84 | 85 | var inlineScript = tryParseInlineScript(parseTemplateBlock) 86 | if (inlineScript) { return inlineScript } 87 | 88 | if (peek('symbol', '<')) { return parseXML() } 89 | 90 | return parseExpression() 91 | } 92 | 93 | /************* 94 | * Functions * 95 | *************/ 96 | var parseFunctionLiteral = astGenerator(function() { 97 | var callable = parseSignatureAndBlock('function', _parseFunctionBlock) 98 | return { type:'FUNCTION', signature:callable[0], block:callable[1] } 99 | }) 100 | 101 | var _parseFunctionBlock = function() { 102 | var controlStatement = tryParseControlStatement(_parseFunctionBlock) 103 | if (controlStatement) { return controlStatement } 104 | 105 | var inlineScript = tryParseInlineScript(_parseFunctionBlock) 106 | if (inlineScript) { return inlineScript } 107 | 108 | if (peek('keyword', 'return')) { return _parseReturnStatement() } 109 | 110 | halt(peek(), 'Expected either a return statement or a control statement in this function block.') 111 | } 112 | 113 | var _parseReturnStatement = astGenerator(function() { 114 | advance('keyword', 'return') 115 | var value = parseExpression() 116 | return { type:'RETURN', value:value } 117 | }) 118 | 119 | /************ 120 | * Handlers * 121 | ************/ 122 | var parseHandlerLiteral = astGenerator(function() { 123 | var callable = parseSignatureAndBlock('handler', _parseHandlerBlock) 124 | return { type:'HANDLER', signature:callable[0], block:callable[1] } 125 | }) 126 | 127 | var _parseHandlerBlock = function() { 128 | var controlStatement = tryParseControlStatement(_parseHandlerBlock) 129 | if (controlStatement) { return controlStatement } 130 | 131 | var inlineScript = tryParseInlineScript(_parseHandlerBlock) 132 | if (inlineScript) { return inlineScript } 133 | 134 | return _parseMutationOrInvocation() 135 | } 136 | 137 | var _parseMutationOrInvocation = astGenerator(function() { 138 | var expression = parseExpression() 139 | 140 | if (!(peek('name') && peek('symbol', ':', 2))) { 141 | return expression 142 | } 143 | 144 | var operator = advance('name').value 145 | advance('symbol', ':') 146 | 147 | var args = [parseExpression()] 148 | while (peek('symbol', ',')) { 149 | advance('symbol', ',') 150 | args.push(parseExpression()) 151 | } 152 | 153 | return { type:'MUTATION', operand:expression, operator:operator, arguments:args } 154 | }) 155 | 156 | /*************************** 157 | * Control flow statements * 158 | ***************************/ 159 | var tryParseControlStatement = function(blockParseFunction) { 160 | if (peek('name') && peek('symbol', '=', 2)) { 161 | return _parseDeclaration() 162 | } 163 | switch(peek().value) { 164 | case 'for': return _parseForLoopStatement(blockParseFunction) 165 | case 'if': return _parseIfStatement(blockParseFunction) 166 | case 'switch': return _parseSwitchStatement(blockParseFunction) 167 | case 'debugger': return _parseDebuggerLiteral() 168 | } 169 | } 170 | 171 | var _parseForLoopStatement = astGenerator(function(statementParseFunction) { 172 | advance('keyword', 'for') 173 | 174 | var iteratorName, iterator 175 | _allowParens(function() { 176 | iteratorName = advance('name', null, 'for_loop\'s iterator reference').value 177 | iterator = createAST({ type:'REFERENCE', name:iteratorName }) 178 | }) 179 | 180 | advance('keyword', 'in', 'for_loop\'s "in" keyword') 181 | var iterable = parseExpression() 182 | 183 | var block = parseBlock(statementParseFunction, 'for_loop') 184 | 185 | return { type:'FOR_LOOP', iterable:iterable, iterator:iterator, block:block } 186 | }) 187 | 188 | var _allowParens = function(fn) { 189 | if (peek('symbol', L_PAREN)) { 190 | advance() 191 | _allowParens(fn) 192 | advance('symbol', R_PAREN) 193 | } else { 194 | fn() 195 | } 196 | } 197 | 198 | var _parseDeclaration = astGenerator(function() { 199 | var name = advance('name').value 200 | assert(gToken, 'a' <= name[0] && name[0] <= 'z', 'Variable names must start with a lowercase letter') 201 | advance('symbol', '=') 202 | var initialValue = parseExpression(parseExpression) 203 | return { type:'DECLARATION', name:name, initialValue:initialValue } 204 | }) 205 | 206 | var _parseIfStatement = astGenerator(function(statementParseFunction) { 207 | advance('keyword', 'if') 208 | var condition = parseExpression() 209 | 210 | var ifBlock = parseBlock(statementParseFunction, 'if statement') 211 | 212 | var elseBlock = null 213 | if (peek('keyword', 'else')) { 214 | advance('keyword', 'else') 215 | if (peek('keyword', 'if')) { 216 | elseBlock = [_parseIfStatement(statementParseFunction)] 217 | } else { 218 | elseBlock = parseBlock(statementParseFunction, 'else statement') 219 | } 220 | } 221 | 222 | return { type:'IF_STATEMENT', condition:condition, ifBlock:ifBlock, elseBlock:elseBlock } 223 | }) 224 | 225 | var _parseSwitchStatement = astGenerator(function(statementParseFunction) { 226 | advance('keyword', 'switch') 227 | var controlValue = parseExpression() 228 | var cases = parseBlock(curry(_parseCase, statementParseFunction), 'switch case statement') 229 | return { type:'SWITCH_STATEMENT', controlValue:controlValue, cases:cases } 230 | }) 231 | 232 | var _parseCase = astGenerator(function(statementParseFunction) { 233 | var labelToken = advance('keyword', ['case', 'default']), 234 | isDefault = (labelToken.value == 'default'), 235 | values = [], 236 | statements = [] 237 | 238 | if (labelToken.value == 'case') { 239 | while (true) { 240 | values.push(parseExpression()) 241 | if (!peek('symbol', ',')) { break } 242 | advance('symbol', ',') 243 | } 244 | } 245 | advance('symbol', ':') 246 | while (!peek('keyword', ['case', 'default']) && !peek('symbol', R_CURLY)) { 247 | statements.push(statementParseFunction()) 248 | } 249 | return { type:'SWITCH_CASE', values:values, statements:statements, isDefault:isDefault } 250 | }) 251 | 252 | var _parseDebuggerLiteral = astGenerator(function() { 253 | advance('keyword', 'debugger') 254 | return { type:'DEBUGGER' } 255 | }) 256 | 257 | /********************** 258 | * Inline script tags * 259 | **********************/ 260 | var tryParseInlineScript = function() { 261 | if (peek('symbol', '<') && peek('name', 'script', 2)) { return _parseInlineScript() } 262 | } 263 | 264 | var _parseInlineScript = astGenerator(function() { 265 | advance('symbol', '<', 'Script tag open') 266 | advance('name', 'script', 'Script tag name') 267 | 268 | var attributes = _parseXMLAttributes(false), 269 | js = [] 270 | advance('symbol', '>', 'end of the script tag') 271 | while (!(peek('symbol', '', 3))) { 272 | advance() 273 | if (gToken.hadNewline) { js.push('\n') } 274 | if (gToken.hadSpace) { js.push(' ') } 275 | 276 | if (gToken.type == 'string') { 277 | js.push(gToken.annotations.single ? "'"+gToken.value+"'" : '"'+gToken.value+'"') 278 | } else { 279 | js.push(gToken.value) 280 | } 281 | } 282 | advance('symbol', '') 285 | return { type:'SCRIPT_TAG', tagName:'script', attributes:attributes, inlineJavascript:js.join('') } 286 | }) 287 | 288 | /****************************************************************** 289 | * Expressions (literals, references, invocations, operators ...) * 290 | ******************************************************************/ 291 | var prefixOperators = ['-', '!'], 292 | binaryOperators = ['+','-','*','/','%','?'], 293 | conditionalOperators = ['<', '>', '<=', '>=', '==', '=', '!=', '!'], 294 | conditionalJoiners = ['and', 'or'] 295 | 296 | var bindingPowers = { 297 | '?':10, 298 | 'and': 20, 'or': 20, 299 | '<': 30, '>': 30, '<=': 30, '>=': 30, '==': 30, '=': 30, '!=': 30, '!': 30, 300 | '+': 40, '-': 40, 301 | '*': 50, '/': 50, '%': 50 302 | } 303 | 304 | var parseExpression = function() { 305 | return _parseMore(0) 306 | } 307 | 308 | var _parseMore = astGenerator(function(leftOperatorBinding) { 309 | if (leftOperatorBinding == null) { 310 | throw new Error("leftOperatorBinding should be defined: ") 311 | } 312 | 313 | if (peek('symbol', prefixOperators)) { 314 | // Prefix operators simply apply to the next expression 315 | // and does not modify the left operator binding 316 | var prefixOperator = advance('symbol').value 317 | return { type:'UNARY_OP', operator:prefixOperator, value:_parseMore(leftOperatorBinding) } 318 | } 319 | 320 | var expression 321 | if (peek('symbol', L_PAREN)) { 322 | // There are no value literals with parentheseseses. 323 | // If wee see a paren, group the inside expression. 324 | advance('symbol', L_PAREN) 325 | expression = _parseMore(0) 326 | advance('symbol', R_PAREN) 327 | expression = _addTightOperators(expression) 328 | } else { 329 | expression = _addTightOperators(_parseAtomicExpressions()) 330 | } 331 | 332 | var rightOperatorToken, impliedEqualityOp 333 | while (true) { 334 | // All conditional comparisons require the is keyword (e.g. `foo is < 10`) 335 | // to avoid ambiguity between the conditional operator < and the beginning of XML 336 | if (peek('keyword', 'is')) { 337 | rightOperatorToken = peek('symbol', conditionalOperators, 2) 338 | // It is OK to skip the comparative operator, and simple say `foo is "bar"` in place of `foo is = "bar"` 339 | if (!rightOperatorToken) { 340 | rightOperatorToken = { value:'=' } 341 | impliedEqualityOp = true 342 | } 343 | } else { 344 | rightOperatorToken = peek('symbol', binaryOperators) 345 | } 346 | 347 | var rightOperator = rightOperatorToken && rightOperatorToken.value, 348 | rightOperatorBinding = (bindingPowers[rightOperator] || 0) 349 | 350 | if (!rightOperator || leftOperatorBinding > rightOperatorBinding) { 351 | return expression 352 | } 353 | 354 | if (peek('symbol', '?')) { 355 | advance() 356 | var ifValue = _parseMore(0) 357 | advance('symbol',':') 358 | return { type:'TERNARY_OP', condition:expression, ifValue:ifValue, elseValue:_parseMore(0) } 359 | } 360 | 361 | if (peek('keyword', 'is')) { 362 | advance() // the "is" keyword 363 | } 364 | if (!impliedEqualityOp) { 365 | advance() // the operator 366 | } 367 | 368 | expression = { type:'BINARY_OP', left:expression, operator:rightOperator, right:_parseMore(rightOperatorBinding) } 369 | } 370 | }) 371 | 372 | var _parseAtomicExpressions = function() { 373 | // references, literals 374 | switch (peek().type) { 375 | case 'string': return _parseTextLiteral() 376 | case 'number': return _parseNumberLiteral() 377 | case 'name': return _parseReference() 378 | case 'symbol': 379 | switch(peek().value) { 380 | case L_BRACKET: return _parseListLiteral() 381 | case L_CURLY: return _parseObjectLiteral() 382 | default: halt(peek(), 'Unexpected symbol "'+peek().value+'" while looking for a value') 383 | } 384 | case 'keyword': 385 | switch(peek().value) { 386 | case 'null': return _parseNullLiteral() 387 | case 'true': return _parseTrueLiteral() 388 | case 'false': return _parseFalseLiteral() 389 | case 'template': return parseTemplateLiteral() 390 | case 'handler': return parseHandlerLiteral() 391 | case 'function': return parseFunctionLiteral() 392 | default: halt(peek(), 'Unexpected keyword "'+peek().value+'" while looking for a value') 393 | } 394 | default: halt(peek(), 'Unexpected token type "'+peek().type+'" while looking for a value') 395 | } 396 | } 397 | 398 | var _parseReference = astGenerator(function() { 399 | var name = advance('name').value 400 | return { type:'REFERENCE', name:name } 401 | }) 402 | 403 | var _parseNullLiteral = astGenerator(function() { 404 | advance('keyword', 'null') 405 | return { type:'NULL_LITERAL', value:null } 406 | }) 407 | 408 | var _parseTrueLiteral = astGenerator(function() { 409 | advance('keyword', 'true') 410 | return { type:'LOGIC_LITERAL', value:true } 411 | }) 412 | 413 | var _parseFalseLiteral = astGenerator(function() { 414 | advance('keyword', 'false') 415 | return { type:'LOGIC_LITERAL', value:false } 416 | }) 417 | 418 | var _parseTextLiteral = astGenerator(function() { 419 | return { type:'TEXT_LITERAL', value:advance('string').value } 420 | }) 421 | 422 | var _parseNumberLiteral = astGenerator(function() { 423 | return { type:'NUMBER_LITERAL', value:advance('number').value } 424 | }) 425 | 426 | 427 | var tightOperators = ['.', L_BRACKET, L_PAREN] 428 | var _addTightOperators = astGenerator(function(expression) { 429 | if (!peekNoWhitespace('symbol', tightOperators)) { return expression } 430 | switch (advance().value) { 431 | case '.': 432 | var key = { type:'TEXT_LITERAL', value:advance('name').value } 433 | return _addTightOperators({ type:'DEREFERENCE', key:key, value:expression }) 434 | case L_BRACKET: 435 | var key = parseExpression(), 436 | value = _addTightOperators({ type:'DEREFERENCE', key:key, value:expression }) 437 | advance('symbol', R_BRACKET) 438 | return value 439 | case L_PAREN: 440 | var args = parseList(R_PAREN, parseExpression) 441 | return _addTightOperators({ type:'INVOCATION', operand:expression, arguments:args }) 442 | default: 443 | throw new Error("Bad tight operator") 444 | } 445 | }) 446 | 447 | var _parseListLiteral = astGenerator(function() { 448 | advance('symbol', L_BRACKET) 449 | var content = parseList(R_BRACKET, parseExpression) 450 | return { type:'LIST_LITERAL', content:content } 451 | }) 452 | 453 | var _parseObjectLiteral = astGenerator(function() { 454 | advance('symbol', L_CURLY) 455 | var content = parseList(R_CURLY, astGenerator(function() { 456 | var name = advance(['name','string']).value 457 | parseSemiOrEqual() 458 | var value = parseExpression() 459 | return { name:name, value:value } 460 | })) 461 | return { type:'DICTIONARY_LITERAL', content:content } 462 | }) 463 | 464 | var parseSemiOrEqual = function() { 465 | if (peek('symbol', '=')) { advance('symbol', '=') } 466 | else { advance('symbol', ':') } 467 | } 468 | 469 | /**************** 470 | * XML literals * 471 | ****************/ 472 | var parseXML= astGenerator(function() { 473 | advance('symbol', '<', 'XML tag opening') 474 | advance('name', null, 'XML tag name') 475 | var tagName = gToken.value 476 | 477 | var attributes = _parseXMLAttributes(true) 478 | 479 | advance('symbol', ['/>', '>'], 'end of the XML tag') 480 | if (gToken.value == '/>') { 481 | return { type:'XML', tagName:tagName, attributes:attributes, block:[] } 482 | } else { 483 | var statements = [] 484 | while(true) { 485 | if (peek('symbol', '"Click" 491 | attributes = attributes.concat(_parseXMLAttributes(true)) 492 | advance('symbol', '>') 493 | 494 | return { type:'XML', tagName:tagName, attributes:attributes, block:statements } 495 | } 496 | }) 497 | var _parseXMLAttributes = function(allowHashExpand) { 498 | var XMLAttributes = [] 499 | while (!peek('symbol', ['/>','>'])) { 500 | XMLAttributes.push(_parseXMLAttribute(allowHashExpand)) 501 | if (peek('symbol', ',')) { advance() } // Allow for
502 | } 503 | return XMLAttributes 504 | } 505 | var _parseXMLAttribute = astGenerator(function(allowHashExpand) { 506 | if (peek('symbol', '#')) { 507 | if (!allowHashExpand) { 508 | halt(peek(), "Hash expanded attributes are not allowed in script tags - trust me, it would be messy") 509 | } 510 | advance() 511 | return { expand:parseExpression() } 512 | } else { 513 | var name = advance(['name', 'keyword']).value 514 | parseSemiOrEqual() 515 | return { name:name, value:parseExpression() } 516 | } 517 | }) 518 | 519 | 520 | /**************************** 521 | * Shared parsing functions * 522 | ****************************/ 523 | // parses comma-seperated statements until is encounteded (e.g. R_PAREN or R_BRACKET) 524 | var parseList = function(breakSymbol, statementParseFunction) { 525 | var list = [] 526 | while (true) { 527 | if (peek('symbol', breakSymbol)) { break } 528 | list.push(statementParseFunction()) 529 | if (peek('symbol', ',')) { advance() } // Allow for both "foo", "bar", "key" and "foo" "bar" "key" 530 | } 531 | advance('symbol', breakSymbol) 532 | return list 533 | } 534 | 535 | // parses a series of statements enclosed by curlies, e.g. { } 536 | var parseBlock = function(statementParseFn, statementType) { 537 | advance('symbol', L_CURLY, 'beginning of the '+statementType+'\'s block') 538 | var block = [] 539 | while(true) { 540 | if (peek('symbol', R_CURLY)) { break } 541 | block.push(statementParseFn()) 542 | } 543 | advance('symbol', R_CURLY, 'end of the '+statementType+' statement\'s block') 544 | return block 545 | } 546 | 547 | function parseSignatureAndBlock(keyword, blockParseFn) { 548 | advance('keyword', keyword) 549 | advance('symbol', L_PAREN) 550 | var signature = parseList(R_PAREN, function() { 551 | return createAST({ type:'ARGUMENT', name:advance('name').value }) 552 | }) 553 | var block = parseBlock(blockParseFn, keyword) 554 | return [signature, block] 555 | } 556 | 557 | /**************** 558 | * Token stream * 559 | ****************/ 560 | function advance(type, value, expressionType) { 561 | var nextToken = peek() 562 | if (!nextToken) { halt(null, 'Unexpected end of file') } 563 | function check(v1, v2) { 564 | if (v1 == v2) { return } 565 | halt(peek(), 'Expected a ' + q(type) 566 | + (value ? ' of value ' + (value instanceof Array ? value.join(' or ') : value) : ',') 567 | + (expressionType ? ' for the ' + expressionType : ''), 568 | + ' but found a' + q(nextToken.type) 569 | + ' of value' + q(nextToken.value)) 570 | } 571 | if (type) { check(findInArray(type, nextToken.type), nextToken.type) } 572 | if (value) { check(findInArray(value, nextToken.value), nextToken.value) } 573 | gToken = gTokens[++gIndex] 574 | return gToken 575 | } 576 | 577 | var peek = function(type, value, steps) { 578 | var token = gTokens[gIndex + (steps || 1)] 579 | if (!token) { return false } 580 | if (type && findInArray(type, token.type) != token.type) { return false } 581 | if (value && findInArray(value, token.value) != token.value) { return false } 582 | return token 583 | } 584 | 585 | var peekNoWhitespace = function(type, value, steps) { 586 | if (peekWhitespace(steps)) { return null } 587 | return peek(type, value) 588 | } 589 | 590 | var peekWhitespace = function(steps) { 591 | var token = gTokens[gIndex + 1] 592 | return token && token.hadSpace 593 | } 594 | 595 | var peekNewline = function(steps) { 596 | return gTokens[gIndex + 1].hadNewline 597 | } 598 | 599 | // Find an item in an array and return it 600 | // if target is in array, return target 601 | // if target is not in array, return array 602 | // if array is not an array, return array 603 | function findInArray(array, target) { 604 | if (!(array instanceof Array)) { return array } 605 | for (var i=0, item; item = array[i]; i++) { 606 | if (item == target) { return item } 607 | } 608 | return array 609 | } 610 | 611 | /********************* 612 | * Utility functions * 613 | *********************/ 614 | // Upgrades a function that creates AST to return properly annotated ASTs 615 | function astGenerator(generatorFn) { 616 | return function() { 617 | var startToken = peek(), 618 | ast = generatorFn.apply(this, arguments), 619 | endToken = peek() 620 | return createAST(ast, startToken, endToken) 621 | } 622 | } 623 | 624 | // Creates a proper AST object, annotated with info about where 625 | // in the source file it appeared (based on startToken and endToken) 626 | function createAST(astObj, startToken, endToken) { 627 | if (!startToken) { startToken = gToken } 628 | if (!endToken) { endToken = gToken } 629 | if (isArray(astObj)) { return astObj } 630 | var ast = Object.create({ 631 | info: { 632 | inputFile: startToken.inputFile, 633 | inputString: startToken.inputString, 634 | line: startToken.line, 635 | column: startToken.column, 636 | span: (startToken.line == endToken.line 637 | ? endToken.column - startToken.column + endToken.span 638 | : startToken.span) 639 | } 640 | }) 641 | for (var key in astObj) { 642 | if (!astObj.hasOwnProperty(key)) { continue } 643 | ast[key] = astObj[key] 644 | } 645 | return ast 646 | } 647 | -------------------------------------------------------------------------------- /src/resolver.js: -------------------------------------------------------------------------------- 1 | // The resolver injects imports and replace aliases with their aliased values 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | isArray = require('std/isArray'), 6 | map = require('std/map'), 7 | curry = require('std/curry'), 8 | copy = require('std/copy'), 9 | filter = require('std/filter'), 10 | blockFunction = require('std/blockFunction'), 11 | request = require('request'), 12 | cleanCSS = require('clean-css'), 13 | exec = require('child_process').exec, 14 | arrayToObject = require('std/arrayToObject') 15 | 16 | var tokenizer = require('./tokenizer'), 17 | parser = require('./parser') 18 | 19 | var util = require('./util'), 20 | each = util.each, 21 | assert = util.assert, 22 | halt = util.halt 23 | 24 | exports.resolve = function(ast, opts, callback) { 25 | var completion = blockFunction(function(err) { 26 | if (err) { callback(err, null) } 27 | else { callback(null, { expressions:expressions, imports:context.imports, headers:context.headers }) } 28 | }).addBlock() 29 | 30 | var context = { headers:[], imports:{}, names:{}, opts:opts, completion:completion } 31 | 32 | if (opts['normalize.css']) { 33 | addStylesheet(context, { href:path.join(__dirname, 'runtime/normalize.css') }) 34 | } 35 | context.headers.push('') 36 | 37 | var expressions = util.cleanup(resolve(context, ast)) 38 | 39 | completion.removeBlock() 40 | } 41 | 42 | /************************ 43 | * Top level statements * 44 | ************************/ 45 | var resolve = function(context, ast) { 46 | if (isArray(ast)) { return map(ast, curry(resolve, context)) } 47 | switch (ast.type) { 48 | // Setup statements 49 | case 'IMPORT': handleImport(context, ast) ;break 50 | 51 | case 'TEXT_LITERAL': return ast 52 | case 'NUMBER_LITERAL': return ast 53 | case 'LOGIC_LITERAL': return ast 54 | case 'NULL_LITERAL': return ast 55 | 56 | case 'ARGUMENT': return ast 57 | case 'VALUE': return ast 58 | case 'NULL': return ast 59 | case 'DEBUGGER': return ast 60 | 61 | case 'DECLARATION': return resolveVariableDeclaration(context, ast) 62 | case 'HANDLER': return resolveInvocable(context, ast) 63 | case 'TEMPLATE': return resolveInvocable(context, ast) 64 | case 'FUNCTION': return resolveInvocable(context, ast) 65 | 66 | case 'DICTIONARY_LITERAL': return resolveObjectLiteral(context, ast) 67 | case 'LIST_LITERAL': return resolveList(context, ast) 68 | 69 | case 'XML': return resolveXML(context, ast) 70 | case 'SCRIPT_TAG': return resolveXML(context, ast) 71 | case 'IF_STATEMENT': return resolveIfStatement(context, ast) 72 | case 'SWITCH_STATEMENT': return resolveSwitchStatement(context, ast) 73 | case 'FOR_LOOP': return resolveForLoop(context, ast) 74 | 75 | case 'INVOCATION': return resolveInvocation(context, ast) 76 | case 'REFERENCE': return lookup(context, ast) 77 | case 'DEREFERENCE': return resolveDereference(context, ast) 78 | case 'BINARY_OP': return resolveCompositeExpression(context, ast) 79 | case 'UNARY_OP': return resolveUnaryExpression(context, ast) 80 | case 'TERNARY_OP': return resolveTernaryExpression(context, ast) 81 | 82 | case 'MUTATION': return resolveMutation(context, ast) 83 | case 'RETURN': return resolveReturn(context, ast) 84 | 85 | default: halt(ast, '_resolveStatement: Unknown AST type "'+ast.type+'"') 86 | } 87 | } 88 | 89 | /**************** 90 | * Declarations * 91 | ****************/ 92 | var resolveVariableDeclaration = function(context, ast) { 93 | declare(context, ast, ast.name, ast) 94 | if (ast.initialValue) { 95 | ast.initialValue = resolve(context, ast.initialValue) 96 | } else { 97 | ast.initialValue = { type:'NULL_LITERAL', value:null } 98 | } 99 | return ast 100 | } 101 | 102 | var declare = function(context, ast, name, value) { 103 | assert(ast, !context.names.hasOwnProperty(ast.name), ast.name + ' is already declared in this scope') 104 | context.names[name] = value 105 | } 106 | 107 | /************************ 108 | * References & lookups * 109 | ************************/ 110 | var lookup = function(context, ast) { 111 | assert(ast, context.names[ast.name], 'Couldn\'t find a variable called "'+ast.name+'"') 112 | return ast 113 | } 114 | 115 | var resolveDereference = function(context, ast) { 116 | ast.key = resolve(context, ast.key) 117 | ast.value = resolve(context, ast.value) 118 | return ast 119 | } 120 | 121 | 122 | /**************************** 123 | * Object and List literals * 124 | ****************************/ 125 | function resolveObjectLiteral(context, ast) { 126 | var contentByName = {} 127 | each(ast.content, function(content) { 128 | contentByName[content.name] = resolve(context, content.value) 129 | }) 130 | ast.content = contentByName 131 | return ast 132 | } 133 | 134 | function resolveList(context, ast) { 135 | ast.content = map(ast.content, function(content) { 136 | return resolve(context, content) 137 | }) 138 | return ast 139 | } 140 | 141 | /************************ 142 | * Composite expressions * 143 | ************************/ 144 | var resolveCompositeExpression = function(context, ast) { 145 | ast.left = resolve(context, ast.left) 146 | ast.right = resolve(context, ast.right) 147 | return ast 148 | } 149 | 150 | var resolveUnaryExpression = function(context, ast) { 151 | ast.value = resolve(context, ast.value) 152 | return ast 153 | } 154 | 155 | var resolveTernaryExpression = function(context, ast) { 156 | ast.condition = resolve(context, ast.condition) 157 | ast.ifValue = resolve(context, ast.ifValue) 158 | ast.elseValue = resolve(context, ast.elseValue) 159 | return ast 160 | } 161 | 162 | /******* 163 | * XML * 164 | *******/ 165 | var resolveXML = function(context, ast) { 166 | each(ast.attributes, function(attribute) { 167 | if (attribute.expand) { 168 | attribute.expand = resolve(context, attribute.expand) 169 | } else { 170 | attribute.value = resolve(context, attribute.value) 171 | } 172 | }) 173 | ast.block = ast.block && filter(resolve(context, ast.block)) 174 | 175 | var staticAttrs 176 | if ((staticAttrs=getStylesheetAttrs(ast)) && !ast.block.length) { 177 | addStylesheet(context, staticAttrs) 178 | return null 179 | } else if ((staticAttrs=getStaticAttrs(ast, 'script')) && staticAttrs.src && !ast.inlineJavascript.length) { 180 | addScriptInclude(context, staticAttrs) 181 | return null 182 | } else { 183 | return ast 184 | } 185 | } 186 | 187 | function getStylesheetAttrs(ast) { 188 | var staticAttrs = getStaticAttrs(ast, 'link') 189 | return staticAttrs && staticAttrs.rel && staticAttrs.rel.match(/^stylesheet/) && staticAttrs 190 | } 191 | 192 | function getStaticAttrs(ast, tagName) { 193 | if (ast.tagName != tagName) { return } 194 | var attributes = ast.attributes, 195 | staticAttrs = {} 196 | each(attributes, function(attr) { 197 | var valueAST = attr.value 198 | if (valueAST.type != 'TEXT_LITERAL') { return } 199 | staticAttrs[attr.name] = valueAST.value 200 | }) 201 | return staticAttrs 202 | } 203 | 204 | var addStylesheet = function(context, attrs) { 205 | var linkHref = attrs.href, 206 | rel = attrs.rel || 'stylesheet/css' 207 | 208 | context.completion.addBlock() 209 | if (linkHref[0] != '/') { linkHref = path.join(context.opts.dirname, linkHref) } 210 | 211 | _getContent(context, linkHref, function doAddStyle(content) { 212 | var comment = '/* inlined stylesheet: ' + linkHref + ' */\n' 213 | if (context.opts.minify) { comment = '' } 214 | 215 | cssPreprocessors[rel](linkHref, comment + content, function(err, css) { 216 | if (err) { 217 | reportError(context, err, 'preprocess', linkHref) 218 | } else { 219 | context.headers.push('') 220 | context.completion.removeBlock() 221 | } 222 | }) 223 | }) 224 | } 225 | 226 | var addScriptInclude = function(context, attrs) { 227 | var scriptSrc = attrs.src 228 | 229 | context.completion.addBlock() 230 | if (scriptSrc[0] != '/') { scriptSrc = path.join(context.opts.dirname, scriptSrc) } 231 | _getContent(context, scriptSrc, function doAddScript(js) { 232 | var comment = context.opts.minify ? '' : ('/* inlined script tag: ' + scriptSrc + ' */\n') 233 | context.headers.push('') 234 | context.completion.removeBlock() 235 | }) 236 | } 237 | 238 | function _getContent(context, url, callback) { 239 | if (url.match(/^http/)) { 240 | console.error("Fetching", url) 241 | request.get(url, function(err, resp, content) { 242 | if (err) { 243 | reportError(context, err, 'fetch', url) 244 | } else if (resp.statusCode != 200) { 245 | reportError(context, new Error('Server returned non-200 status code: '+resp.statusCode), 'fetch', url) 246 | } else { 247 | console.error("Done fetching", linkHref) 248 | callback(content) 249 | } 250 | }) 251 | } else { 252 | fs.readFile(url, function(err, content) { 253 | if (err) { reportError(context, err, 'read', url) } 254 | else { callback(content.toString()) } 255 | }) 256 | } 257 | } 258 | 259 | function reportError(context, err, verb, resource) { 260 | console.log("Error", verb+'ing', resource) 261 | context.completion.fail(new Error('Could not '+verb+' '+resource+'\n'+err.message)) 262 | } 263 | 264 | var cssPreprocessors = { 265 | 'stylesheet': function(href, css, callback) { 266 | callback(null, css) 267 | }, 268 | 'stylesheet/css': function(href, css, callback) { 269 | callback(null, css) 270 | }, 271 | 'stylesheet/less': function(href, lessContent, callback) { 272 | require('less').render(lessContent, callback) 273 | }, 274 | 'stylesheet/stylus': function(href, stylusContent, callback) { 275 | require('stylus').render(stylusContent, { filename:href }, callback) 276 | } 277 | } 278 | 279 | /******************************* 280 | * Imports (imports and files) * 281 | *******************************/ 282 | var handleImport = function(context, ast) { 283 | var importPath = ast.path 284 | if (importPath[0] == '/') { 285 | importPath = path.normalize(importPath) 286 | } else if (importPath[0] == '.') { 287 | importPath = path.join(context.opts.dirname, importPath) 288 | } else { 289 | importPath = path.join(__dirname, 'modules', importPath) 290 | } 291 | 292 | try { 293 | if (fs.statSync(importPath).isDirectory()) { 294 | importPath = importPath+'/'+path.basename(importPath) 295 | } 296 | } catch(e) {} 297 | 298 | _importFile(context, ast, importPath+'.fun') 299 | } 300 | 301 | var _importFile = function(context, ast, filePath) { 302 | if (context.imports[filePath]) { return } 303 | try { assert(ast, fs.statSync(filePath).isFile(), 'Could not find module '+filePath) } 304 | catch(e) { halt(ast, e) } 305 | 306 | context = util.create(context) 307 | context.opts = util.create(context.opts, { dirname:path.dirname(filePath) }) 308 | 309 | var tokens = tokenizer.tokenizeFile(filePath), 310 | newAST = parser.parse(tokens), 311 | resolvedAST = util.cleanup(resolve(context, newAST)) 312 | 313 | context.imports[filePath] = resolvedAST 314 | } 315 | 316 | /*************** 317 | * Invocations * 318 | ***************/ 319 | var resolveInvocation = function(context, ast) { 320 | ast.operand = (ast.operand.type == 'REFERENCE' ? lookup(context, ast.operand) : resolve(context, ast.operand)) 321 | ast.arguments = resolve(context, ast.arguments) 322 | return ast 323 | } 324 | 325 | /************************************************** 326 | * Invocables - Functions, Handlers and Templates * 327 | **************************************************/ 328 | var resolveInvocable = function(context, ast) { 329 | if (ast.closure) { return ast } // already resolved 330 | setNonEnumerableProperty(ast, 'closure', addScope(context)) 331 | each(ast.signature, function(argument) { declare(ast.closure, ast, argument.name, argument) }) 332 | ast.block = filter(resolve(ast.closure, ast.block)) 333 | return ast 334 | } 335 | 336 | /******************************* 337 | * Handler/Function statements * 338 | *******************************/ 339 | var resolveMutation = function(context, ast) { 340 | ast.arguments = map(ast.arguments, curry(resolve, context)) 341 | ast.operand = resolve(context, ast.operand) 342 | return ast 343 | } 344 | 345 | var resolveReturn = function(context, ast) { 346 | ast.value = resolve(context, ast.value) 347 | return ast 348 | } 349 | 350 | /************* 351 | * For loops * 352 | *************/ 353 | var resolveForLoop = function(context, ast) { 354 | // ast.iterator.operand = ast.iterable 355 | ast.iterable = resolve(context, ast.iterable) 356 | ast.context = addScope(context) 357 | declare(ast.context, ast, ast.iterator.name, ast.iterator) 358 | ast.block = filter(resolve(ast.context, ast.block)) 359 | return ast 360 | } 361 | 362 | /***************** 363 | * If statements * 364 | *****************/ 365 | var resolveIfStatement = function(context, ast) { 366 | ast.condition = resolve(context, ast.condition) 367 | ast.ifContext = addScope(context) 368 | ast.ifBlock = resolve(ast.ifContext, ast.ifBlock) 369 | if (ast.elseBlock) { 370 | ast.elseContext = addScope(context) 371 | ast.elseBlock = resolve(ast.elseContext, ast.elseBlock) 372 | } 373 | return ast 374 | } 375 | 376 | /********************* 377 | * Switch statements * 378 | *********************/ 379 | var resolveSwitchStatement = function(context, ast) { 380 | ast.controlValue = resolve(context, ast.controlValue) 381 | each(ast.cases, function(aCase) { 382 | aCase.values = map(aCase.values, curry(resolve, context)) 383 | each(aCase.values, function(aCase) { 384 | assert(aCase, _caseValues[aCase.type], "Switch statements' case values must be numbers or texts (e.g. 2, 3, 'hello')") 385 | }) 386 | aCase.statements = map(aCase.statements, curry(resolve, context)) 387 | }) 388 | var defaultCases = util.pick(ast.cases, function(aCase) { return aCase.isDefault }) 389 | assert(ast, defaultCases.length < 2, "Found two default cases in switch statement - well, that doesn't make sense") 390 | return ast 391 | } 392 | var _caseValues = arrayToObject(['TEXT_LITERAL', 'NUMBER_LITERAL']) 393 | 394 | /*********** 395 | * Utility * 396 | ***********/ 397 | var setNonEnumerableProperty = function(obj, name, value) { 398 | // We want to be able to add state to objects that are not seen by the test code's assert.deepEquals. 399 | // We do this by assigning non-enumerable properties in the environments that support it, 400 | // most notably in node where the test suite is run. 401 | Object.defineProperty(obj, name, { value:value, enumerable:false }) 402 | return value 403 | } 404 | 405 | var addScope = function(context) { 406 | // Creates a scope by prototypically inheriting the names dictionary from the current context. 407 | // Reads will propegate up the prototype chain, while writes won't. 408 | // New names written to context.names will shadow names written further up the chain, but won't overwrite them. 409 | context = util.create(context) 410 | context.names = copy(context.names) // shouldn't this be create()? 411 | return context 412 | } 413 | -------------------------------------------------------------------------------- /src/runtime/expressions.js: -------------------------------------------------------------------------------- 1 | var proto = require('std/proto'), 2 | create = require('std/create'), 3 | map = require('std/map'), 4 | isArray = require('std/isArray'), 5 | bind = require('std/bind'), 6 | arrayToObject = require('std/arrayToObject'), 7 | copy = require('std/copy') 8 | 9 | // All values inherit from base 10 | /////////////////////////////// 11 | var base = module.exports.base = { 12 | observe:function(callback) { 13 | var id = this._onMutate(callback) 14 | callback() 15 | return id 16 | }, 17 | _onMutate:function(callback) {}, 18 | toJSON:function() { return JSON.parse(this.asLiteral()) }, 19 | isTruthy:function() { return true }, 20 | isNull:function() { return false }, 21 | iterate:function() {}, 22 | getters:{ 23 | copy:function() { 24 | return module.exports.Function(bind(this, function(yieldValue) { 25 | yieldValue(this.getContent()) 26 | })) 27 | }, 28 | Type:function() { 29 | return Text(this.getType()) 30 | } 31 | } 32 | } 33 | 34 | // Simple values 35 | //////////////// 36 | var constantAtomicBase = create(base, { 37 | isAtomic:function() { return true }, 38 | inspect:function() { return '<'+this._type+' ' + this.asLiteral() + '>' }, 39 | getType:function() { return this._type }, 40 | evaluate:function() { return this }, 41 | toString:function() { return this._content.toString() }, 42 | equals:function(that) { return (this._type == that.getType() && this._content == that.getContent()) ? Yes : No }, 43 | getContent:function() { return this._content }, 44 | mutate:function() { throw new Error("Called mutate on non-mutable value "+this.asLiteral() )}, 45 | dismiss:function(id) {}, 46 | render:function(hookName) { 47 | fun.hooks[hookName].innerHTML = '' 48 | fun.hooks[hookName].appendChild(document.createTextNode(this.toString())) 49 | } 50 | }) 51 | 52 | var Number = module.exports.Number = proto(constantAtomicBase, 53 | function Number(content) { 54 | if (typeof content != 'number') { typeMismatch() } 55 | this._content = content 56 | }, { 57 | _type:'Number', 58 | asLiteral:function() { return this._content }, 59 | isTruthy: function() { return this._content != 0 } 60 | } 61 | ) 62 | 63 | var Text = module.exports.Text = proto(constantAtomicBase, 64 | function Text(content) { 65 | if (typeof content != 'string') { typeMismatch() } 66 | this._content = content 67 | }, { 68 | _type:'Text', 69 | asLiteral:function() { return '"'+this._content+'"' } 70 | } 71 | ) 72 | 73 | var Logic = module.exports.Logic = function(content) { 74 | if (typeof content != 'boolean') { typeMismatch() } 75 | return content ? Yes : No 76 | } 77 | var LogicProto = proto(constantAtomicBase, 78 | function Logic(content) { 79 | this._content = content 80 | }, { 81 | _type:'Logic', 82 | toString:function() { return this._content ? 'yes' : 'no' }, 83 | asLiteral:function() { return this._content ? 'true' : 'false' }, 84 | isTruthy:function() { return this._content } 85 | } 86 | ) 87 | var Yes = module.exports.Yes = LogicProto(true), 88 | No = module.exports.No = LogicProto(false) 89 | 90 | module.exports.Null = function() { return Null } 91 | var Null = (proto(constantAtomicBase, 92 | function Null() { 93 | if (arguments.length) { typeMismatch() } 94 | }, { 95 | _type:'Null', 96 | inspect:function() { return '' }, 97 | toString:function() { return '' }, 98 | equals:function(that) { return that.getType() == 'Null' ? Yes : No }, 99 | asLiteral:function() { return 'null' }, 100 | isTruthy:function() { return false }, 101 | isNull:function() { return true } 102 | } 103 | ))(); 104 | 105 | // Invocable values 106 | /////////////////// 107 | module.exports.Function = proto(constantAtomicBase, 108 | function Function(block) { 109 | if (typeof block != 'function') { typeMismatch() } 110 | this._content = block 111 | }, { 112 | _type:'Function', 113 | invoke:function(args) { 114 | var result = variable(Null) 115 | this._withArgs(args, this._content, function(value) { 116 | result.mutate('set', [fromJsValue(value)]) 117 | }) 118 | return result 119 | }, 120 | render:function(hookName, args) { 121 | this._withArgs(args, this._content, function(value) { 122 | fromJsValue(value).render(hookName) 123 | }) 124 | }, 125 | _withArgs:function(args, content, yieldValue) { 126 | var executeBlock = bind(this, function() { 127 | if (args[1]) { 128 | // hack: isFirstExecution 129 | var useArgs = copy(args) 130 | args[1] = false 131 | content.apply(this, useArgs) 132 | } else { 133 | content.apply(this, args) 134 | } 135 | }) 136 | 137 | for (var i=0; i' }, 264 | equals:function(that) { return this.evaluate().equals(that) }, 265 | lookup:function(key) { return this._getCurrentValue().lookup(key) }, 266 | evaluate:function() { return this._getCurrentValue().evaluate() }, 267 | mutate:function(operator, args) { this._getCurrentValue().mutate(operator, args) }, 268 | _getCurrentValue:function() { 269 | var key = this.components.key.evaluate(), 270 | value = this.components.value, 271 | getterValue = value.evaluate(), 272 | getter = (key.getType() == 'Text' && getterValue.getters[key.toString()]) 273 | 274 | if (getter) { 275 | return getter.call(getterValue) 276 | } else if (value.isAtomic()) { 277 | return Null 278 | } else { 279 | return value.lookup(key) || Null 280 | } 281 | } 282 | } 283 | ) 284 | 285 | // Mutable values - variables and collections 286 | ///////////////////////////////////////////// 287 | var mutableBase = create(variableValueBase, { 288 | _initMutable:function() { 289 | this.observers = {} 290 | this.notify = bind(this, this._notifyObservers) 291 | }, 292 | _notifyObservers:function(mutation) { 293 | for (var id in this.observers) { 294 | this.observers[id](mutation) 295 | } 296 | }, 297 | _onMutate:function(callback) { 298 | var id = unique() 299 | this.observers[id] = callback 300 | return id 301 | }, 302 | dismiss:function(id) { 303 | if (!this.observers[id]) { throw new Error("Tried to dismiss an observer by incorrect ID") } 304 | delete this.observers[id] 305 | } 306 | }) 307 | 308 | var variable = module.exports.variable = proto(mutableBase, 309 | function variable(content) { 310 | this._initMutable() 311 | this.mutate('set', [content]) 312 | }, { 313 | _type:'variable', 314 | evaluate:function() { return this._content.evaluate() }, 315 | inspect:function() { return '' }, 316 | toString:function() { return this._content.toString() }, 317 | asLiteral:function() { return this._content.asLiteral() }, 318 | equals:function(that) { return this._content.equals(that) }, 319 | lookup:function(key) { return this._content.lookup(key) }, 320 | mutate: function(operator, args) { 321 | if (operator != 'set' || args.length == 2) { // Hmm, hacky, should methods with multiple arguments have different method names like in ObjC? 322 | return this._content.mutate(operator, args) 323 | } 324 | _checkArgs(args, 1) 325 | var newContent = args[0] 326 | 327 | if (this._observationID) { 328 | this._content.dismiss(this._observationID) 329 | } 330 | this._content = newContent 331 | this._observationID = newContent._onMutate(this.notify) 332 | 333 | var mutation = { operator:'set', affectedProperties:null } 334 | this.notify(mutation) 335 | } 336 | } 337 | ) 338 | 339 | var collectionBase = create(mutableBase, { 340 | _initCollection: function() { 341 | this._initMutable() 342 | this.propObservations = {} 343 | }, 344 | isAtomic:function() { return false }, 345 | getType:function() { return this._type }, 346 | evaluate:function() { return this }, 347 | getContent:function() { return this._content }, 348 | isTruthy:function() { return true }, 349 | 350 | _setProperty: function(propertyKey, newProperty, mutation) { 351 | this._forgetProperty(propertyKey) 352 | 353 | this._content[propertyKey] = newProperty 354 | 355 | this.propObservations[propertyKey] = newProperty._onMutate(bind(this, function(mutation) { 356 | mutation.affectedProperties = this._getAffectedProperties(propertyKey) 357 | this.notify(mutation) 358 | })) 359 | 360 | this.notify(mutation) 361 | }, 362 | _forgetProperty: function(propertyKey) { 363 | var oldId = this.propObservations[propertyKey] 364 | if (oldId) { 365 | this._content[propertyKey].dismiss(oldId) 366 | delete this.propObservations[propertyKey] 367 | } 368 | } 369 | }) 370 | 371 | var Dictionary = module.exports.Dictionary = proto(collectionBase, 372 | function Dictionary(content) { 373 | if (typeof content != 'object' || isArray(content) || content == null) { typeMismatch() } 374 | this._initCollection() 375 | this._content = {} 376 | for (var key in content) { 377 | this.mutate('set', [Text(key), content[key]]) 378 | } 379 | }, { 380 | _type:'Dictionary', 381 | asLiteral:function() { return '{ '+map(this._content, function(val, key) { return fromLiteral(key).asLiteral()+':'+val.asLiteral() }).join(', ')+' }' }, 382 | toString:function() { return this.asLiteral() }, 383 | inspect:function() { return '' }, 384 | lookup:function(key) { 385 | var value = this._content[key.asLiteral()] 386 | return value 387 | }, 388 | iterate:function(yieldFn) { 389 | var content = this._content 390 | for (var key in content) { 391 | yieldFn(content[key]) 392 | } 393 | }, 394 | equals:function(that) { 395 | that = that.evaluate() 396 | if (that._type != this._type) { 397 | return No 398 | } 399 | for (var key in this._content) { 400 | if (that._content[key] && that._content[key].equals(this._content[key])) { continue } 401 | return No 402 | } 403 | for (var key in that._content) { 404 | if (this._content[key] && this._content[key].equals(that._content[key])) { continue } 405 | return No 406 | } 407 | return Yes 408 | }, 409 | mutate:function(operator, args) { 410 | if (operator != 'set') { throw new Error('Bad Dictionary operator "'+operator+'"') } 411 | _checkArgs(args, 2) 412 | var key = args[0], 413 | value = args[1] 414 | if (!key || key.isNull() || !value) { BAD_ARGS } 415 | var mutation = { operator:'set', affectedProperties:arrayToObject([key.asLiteral()]) } 416 | this._setProperty(key.asLiteral(), value, mutation) 417 | }, 418 | _getAffectedProperties: function(propertyKey) { 419 | return arrayToObject([propertyKey]) 420 | } 421 | } 422 | ) 423 | 424 | var List = module.exports.List = proto(collectionBase, 425 | function List(content) { 426 | if (!isArray(content)) { typeMismatch() } 427 | this._initCollection() 428 | this._content = [] 429 | for (var i=0; i' }, 437 | lookup:function(index) { 438 | if (index.getType() == 'Number') { typeMismatch() } 439 | var value = this._content[index.getContent()] 440 | return value 441 | }, 442 | iterate:function(yieldFn) { 443 | var content = this._content 444 | for (var i=0; i=': greaterThanOrEquals, 539 | '<=': lessThanOrEquals, 540 | '<': lessThan, 541 | '>': greaterThan 542 | } 543 | 544 | function negative(value) { 545 | if (value.getType() != 'Number') { return Null } 546 | return Number(-value.getContent()) 547 | } 548 | 549 | function not(value) { 550 | return Logic(!value.isTruthy()) 551 | } 552 | 553 | function add(left, right) { 554 | if (left.getType() == 'Number' && right.getType() == 'Number') { 555 | return Number(left.getContent() + right.getContent()) 556 | } 557 | return Text(left.toString() + right.toString()) 558 | } 559 | 560 | function subtract(left, right) { 561 | if (left.getType() == 'Number' && right.getType() == 'Number') { 562 | return Number(left.getContent() - right.getContent()) 563 | } else { 564 | return Null 565 | } 566 | } 567 | 568 | function divide(left, right) { 569 | if (left.getType() == 'Number' && right.getType() == 'Number') { 570 | return Number(left.getContent() / right.getContent()) 571 | } else { 572 | return Null 573 | } 574 | } 575 | 576 | function multiply(left, right) { 577 | if (left.getType() == 'Number' && right.getType() == 'Number') { 578 | return Number(left.getContent() * right.getContent()) 579 | } else { 580 | return Null 581 | } 582 | } 583 | 584 | 585 | function equals(left, right) { 586 | return left.equals(right) 587 | } 588 | 589 | function notEquals(left, right) { 590 | return Logic(!left.equals(right).getContent()) 591 | } 592 | 593 | function greaterThanOrEquals(left, right) { 594 | // TODO Typecheck? 595 | return Logic(left.getContent() >= right.getContent()) 596 | } 597 | 598 | function lessThanOrEquals(left, right) { 599 | // TODO Typecheck? 600 | return Logic(left.getContent() <= right.getContent()) 601 | } 602 | 603 | function lessThan(left, right) { 604 | // TODO Typecheck? 605 | return Logic(left.getContent() < right.getContent()) 606 | } 607 | 608 | function greaterThan(left, right) { 609 | // TODO Typecheck? 610 | return Logic(left.getContent() > right.getContent()) 611 | } 612 | 613 | // Util 614 | /////// 615 | var _unique = 1 616 | function unique() { return 'u'+_unique++ } 617 | 618 | var fromJsValue = module.exports.fromJsValue = module.exports.value = function(val) { 619 | switch (typeof val) { 620 | case 'string': return Text(val) 621 | case 'number': return Number(val) 622 | case 'boolean': return Logic(val) 623 | case 'undefined': return Null 624 | case 'object': 625 | if (base.isPrototypeOf(val)) { return val } 626 | if (val == null) { 627 | return Null 628 | } 629 | if (isArray(val)) { 630 | var content = map(val, fromJsValue) 631 | return List(content) 632 | } 633 | var content = {} 634 | for (var key in val) { 635 | content[key] = fromJsValue(val[key]) 636 | } 637 | return Dictionary(content) 638 | } 639 | } 640 | 641 | var fromLiteral = module.exports.fromLiteral = module.exports.fromJSON = function(json) { 642 | try { var jsValue = JSON.parse(json) } 643 | catch(e) { return Null } 644 | return fromJsValue(jsValue) 645 | } 646 | 647 | var Event = module.exports.Event = function(jsEvent) { 648 | var funEvent = fromJsValue({ 649 | keyCode:jsEvent.keyCode, 650 | type:jsEvent.type, 651 | cancel:fun.expressions.Function(function() { 652 | if (jsEvent.preventDefault) { jsEvent.preventDefault() } 653 | else { jsEvent.returnValue = false } 654 | }) 655 | }) 656 | funEvent.jsEvent = jsEvent // For JS API 657 | return funEvent 658 | } 659 | 660 | function _cleanArgs(args, fn) { 661 | var diffArgs = fn.length - args.length 662 | while (diffArgs-- > 0) { args.push(Null) } 663 | return args 664 | } 665 | 666 | var _checkArgs = function(args, num) { 667 | if (!isArray(args) || args.length != num) { BAD_ARGS } 668 | } 669 | 670 | function typeMismatch() { throw new Error('Type mismatch') } 671 | -------------------------------------------------------------------------------- /src/runtime/library.js: -------------------------------------------------------------------------------- 1 | var expressions = require('./expressions'), 2 | each = require('std/each'), 3 | curry = require('std/curry'), 4 | throttle = require('std/throttle'), 5 | addClass = require('fun-dom/addClass'), 6 | removeClass = require('fun-dom/removeClass'), 7 | on = require('fun-dom/on'), 8 | off = require('fun-dom/off'), 9 | arrayToObject = require('std/arrayToObject') 10 | 11 | ;(function() { 12 | if (typeof fun == 'undefined') { fun = {} } 13 | var _unique, _hooks, _hookCallbacks 14 | 15 | fun.reset = function() { 16 | _unique = 0 17 | fun.expressions = expressions 18 | _hooks = fun.hooks = {} 19 | _hookCallbacks = {} 20 | } 21 | 22 | fun.name = function(readable) { return '_' + (readable || '') + '_' + (_unique++) } 23 | 24 | fun.expressions = expressions 25 | 26 | /* Values 27 | ********/ 28 | fun.value = function(val) { return expressions.fromJsValue(val) } 29 | 30 | fun.emit = function(parentHookName, value) { 31 | if (!value) { return } 32 | var hookName = fun.hook(fun.name(), parentHookName) 33 | value.observe(function() { 34 | _hooks[hookName].innerHTML = '' 35 | _hooks[hookName].appendChild(document.createTextNode(value)) 36 | }) 37 | } 38 | 39 | fun.set = function(value, chainStr, setValue) { 40 | if (arguments.length == 2) { 41 | setValue = chainStr 42 | chainStr = null 43 | } 44 | var chain = chainStr ? chainStr.split('.') : [] 45 | while (chain.length) { 46 | value = expressions.dereference(value, expressions.Text(chain.shift())) 47 | } 48 | value.mutate('set', [expressions.fromJsValue(setValue)]) 49 | } 50 | 51 | fun.dictSet = function(dict, prop, setValue) { 52 | dict.mutate('set', [expressions.fromJsValue(prop), expressions.fromJsValue(setValue)]) 53 | } 54 | 55 | fun.handleTemplateForLoopMutation = function(mutation, loopHookName, iterableValue, yieldFn) { 56 | var op = mutation && mutation.operator 57 | if (op == 'push') { 58 | var emitHookName = fun.name() 59 | fun.hook(emitHookName, loopHookName) 60 | var content = iterableValue.getContent(), 61 | item = content[content.length - 1] 62 | yieldFn(emitHookName, item) 63 | // TODO 64 | // } else if (op == 'pop') { 65 | // var parent = fun.hooks[loopHookName], 66 | // children = parent.childNodes 67 | // parent.removeChild(children[children.length - 1]) 68 | } else { 69 | fun.destroyHook(loopHookName) 70 | var emitHookName = fun.name() 71 | fun.hook(emitHookName, loopHookName) 72 | iterableValue.evaluate().iterate(function(item) { 73 | yieldFn(emitHookName, item) 74 | }) 75 | } 76 | } 77 | 78 | /* Hooks 79 | *******/ 80 | fun.setHook = function(name, dom) { _hooks[name] = dom } 81 | fun.hook = function(name, parentName, opts) { 82 | if (_hooks[name]) { return name } 83 | opts = opts || {} 84 | var parent = _hooks[parentName], 85 | hook = _hooks[name] = document.createElement(opts.tagName || 'hook') 86 | 87 | each(opts.attrs, function(attr) { 88 | if (attr.expand) { fun.attrExpand(name, attr.expand) } 89 | else { fun.attr(name, attr.name, attr.value) } 90 | }) 91 | 92 | if (_hookCallbacks[name]) { 93 | for (var i=0, callback; callback = _hookCallbacks[name][i]; i++) { 94 | callback(hook) 95 | } 96 | } 97 | 98 | if (!parent.childNodes.length || !opts.prepend) { parent.appendChild(hook) } 99 | else { parent.insertBefore(hook, parent.childNodes[0]) } 100 | 101 | return name 102 | } 103 | fun.destroyHook = function(hookName) { 104 | if (!_hooks[hookName]) { return } 105 | _hooks[hookName].innerHTML = '' 106 | } 107 | fun.withHook = function(hookName, callback) { 108 | if (_hooks[hookName]) { return callback(_hooks[hookName]) } 109 | else if (_hookCallbacks[hookName]) { _hookCallbacks[hookName].push(callback) } 110 | else { _hookCallbacks[hookName] = [callback] } 111 | } 112 | 113 | fun.attr = function(hookName, key, value) { 114 | if (key == 'data') { 115 | fun.reflectInput(hookName, value) 116 | return 117 | } 118 | var hook = _hooks[hookName], 119 | lastValue 120 | value.observe(function() { 121 | if (match = key.match(/^on(\w+)$/)) { 122 | if (lastValue) { off(hook, eventName, lastValue) } 123 | 124 | var eventName = match[1].toLowerCase() 125 | if (value.getType() != 'Handler') { 126 | console.warn('Event attribute', eventName, 'value is not a Handler') 127 | return 128 | } 129 | on(hook, eventName, lastValue = function(e) { 130 | e.hook = hook 131 | value.evaluate().invoke([expressions.Event(e)]) 132 | }) 133 | } else if (key == 'style') { 134 | // TODO remove old styles 135 | each(value.getContent(), function(val, key) { 136 | fun.setStyle(hook, key, val) 137 | }) 138 | } else if (key == 'class' || key == 'className') { 139 | if (lastValue) { removeClass(hook, lastValue) } 140 | addClass(hook, lastValue = value.getContent()) 141 | } else { 142 | hook.setAttribute(key, value.getContent()) 143 | } 144 | }) 145 | } 146 | 147 | fun.attrExpand = function(hookName, expandValue) { 148 | // TODO Observe the expandValue, and detect keys getting added/removed properly 149 | each(expandValue.getContent(), function(value, name) { 150 | name = _getDictionaryKeyString(name) 151 | fun.attr(hookName, name, value) 152 | }) 153 | } 154 | 155 | var _getDictionaryKeyString = function(key) { 156 | key = fun.expressions.fromLiteral(key) 157 | if (key.getType() != 'Text') { return } 158 | return key.getContent() 159 | } 160 | 161 | var skipPx = arrayToObject(['zIndex', 'z-index']) 162 | fun.setStyle = function(hook, key, value) { 163 | key = _getDictionaryKeyString(key) 164 | if (!key) { return } 165 | 166 | value = value.evaluate() 167 | var rawValue = value.toString() 168 | 169 | if ((value.getType() == 'Number' || rawValue.match(/^\d+$/)) && !skipPx[key]) { 170 | rawValue = rawValue + 'px' 171 | } 172 | if (key == 'float') { key = 'cssFloat' } 173 | hook.style[key] = rawValue 174 | } 175 | 176 | fun.reflectInput = function(hookName, property) { 177 | var input = _hooks[hookName] 178 | if (input.type == 'checkbox') { 179 | property.observe(function() { 180 | input.checked = property.getContent() ? true : false 181 | }) 182 | on(input, 'change', function() { 183 | setTimeout(function() { 184 | _doSet(property, input.checked ? fun.expressions.Yes : fun.expressions.No) 185 | }) 186 | }) 187 | } else { 188 | property.observe(function() { 189 | input.value = property.evaluate().toString() 190 | }) 191 | 192 | function update(e) { 193 | setTimeout(function() { 194 | var value = input.value 195 | if (property.getContent() === value) { return } 196 | _doSet(property, fun.expressions.Text(input.value)) 197 | input.value = value 198 | }, 0) 199 | } 200 | 201 | on(input, 'keypress', update) 202 | on(input, 'keyup', update) 203 | on(input, 'keydown', function(e) { 204 | if (e.keyCode == 86) { update(e) } // catch paste events 205 | }) 206 | } 207 | function _doSet(property, value) { 208 | if (property._type == 'dereference') { 209 | var components = property.components 210 | fun.dictSet(components.value, components.key, value) 211 | } else { 212 | fun.set(property, value) 213 | } 214 | } 215 | } 216 | 217 | /* init & export 218 | ***************/ 219 | fun.reset() 220 | if (typeof module != 'undefined') { module.exports = fun } 221 | })() 222 | -------------------------------------------------------------------------------- /src/runtime/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css 2012-03-11T12:53 UTC - http://github.com/necolas/normalize.css */ 2 | 3 | /* ============================================================================= 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects block display not defined in IE6/7/8/9 & FF3 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects inline-block display not defined in IE6/7/8/9 & FF3 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | *display: inline; 34 | *zoom: 1; 35 | } 36 | 37 | /* 38 | * Prevents modern browsers from displaying 'audio' without controls 39 | * Remove excess height in iOS5 devices 40 | */ 41 | 42 | audio:not([controls]) { 43 | display: none; 44 | height: 0; 45 | } 46 | 47 | /* 48 | * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 49 | * Known issue: no IE6 support 50 | */ 51 | 52 | [hidden] { 53 | display: none; 54 | } 55 | 56 | 57 | /* ============================================================================= 58 | Base 59 | ========================================================================== */ 60 | 61 | /* 62 | * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 63 | * http://clagnut.com/blog/348/#c790 64 | * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom 65 | * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 66 | */ 67 | 68 | html { 69 | font-size: 100%; /* 1 */ 70 | -webkit-text-size-adjust: 100%; /* 2 */ 71 | -ms-text-size-adjust: 100%; /* 2 */ 72 | } 73 | 74 | /* 75 | * Addresses font-family inconsistency between 'textarea' and other form elements. 76 | */ 77 | 78 | html, 79 | button, 80 | input, 81 | select, 82 | textarea { 83 | font-family: sans-serif; 84 | } 85 | 86 | /* 87 | * Addresses margins handled incorrectly in IE6/7 88 | */ 89 | 90 | body { 91 | margin: 0; 92 | } 93 | 94 | 95 | /* ============================================================================= 96 | Links 97 | ========================================================================== */ 98 | 99 | /* 100 | * Addresses outline displayed oddly in Chrome 101 | */ 102 | 103 | a:focus { 104 | outline: thin dotted; 105 | } 106 | 107 | /* 108 | * Improves readability when focused and also mouse hovered in all browsers 109 | * people.opera.com/patrickl/experiments/keyboard/test 110 | */ 111 | 112 | a:hover, 113 | a:active { 114 | outline: 0; 115 | } 116 | 117 | 118 | /* ============================================================================= 119 | Typography 120 | ========================================================================== */ 121 | 122 | /* 123 | * Addresses font sizes and margins set differently in IE6/7 124 | * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 125 | */ 126 | 127 | h1 { 128 | font-size: 2em; 129 | margin: 0.67em 0; 130 | } 131 | 132 | h2 { 133 | font-size: 1.5em; 134 | margin: 0.83em 0; 135 | } 136 | 137 | h3 { 138 | font-size: 1.17em; 139 | margin: 1em 0; 140 | } 141 | 142 | h4 { 143 | font-size: 1em; 144 | margin: 1.33em 0; 145 | } 146 | 147 | h5 { 148 | font-size: 0.83em; 149 | margin: 1.67em 0; 150 | } 151 | 152 | h6 { 153 | font-size: 0.75em; 154 | margin: 2.33em 0; 155 | } 156 | 157 | /* 158 | * Addresses styling not present in IE7/8/9, S5, Chrome 159 | */ 160 | 161 | abbr[title] { 162 | border-bottom: 1px dotted; 163 | } 164 | 165 | /* 166 | * Addresses style set to 'bolder' in FF3+, S4/5, Chrome 167 | */ 168 | 169 | b, 170 | strong { 171 | font-weight: bold; 172 | } 173 | 174 | blockquote { 175 | margin: 1em 40px; 176 | } 177 | 178 | /* 179 | * Addresses styling not present in S5, Chrome 180 | */ 181 | 182 | dfn { 183 | font-style: italic; 184 | } 185 | 186 | /* 187 | * Addresses styling not present in IE6/7/8/9 188 | */ 189 | 190 | mark { 191 | background: #ff0; 192 | color: #000; 193 | } 194 | 195 | /* 196 | * Addresses margins set differently in IE6/7 197 | */ 198 | 199 | p, 200 | pre { 201 | margin: 1em 0; 202 | } 203 | 204 | /* 205 | * Corrects font family set oddly in IE6, S4/5, Chrome 206 | * en.wikipedia.org/wiki/User:Davidgothberg/Test59 207 | */ 208 | 209 | pre, 210 | code, 211 | kbd, 212 | samp { 213 | font-family: monospace, serif; 214 | _font-family: 'courier new', monospace; 215 | font-size: 1em; 216 | } 217 | 218 | /* 219 | * Improves readability of pre-formatted text in all browsers 220 | */ 221 | 222 | pre { 223 | white-space: pre; 224 | white-space: pre-wrap; 225 | word-wrap: break-word; 226 | } 227 | 228 | /* 229 | * 1. Addresses CSS quotes not supported in IE6/7 230 | * 2. Addresses quote property not supported in S4 231 | */ 232 | 233 | /* 1 */ 234 | 235 | q { 236 | quotes: none; 237 | } 238 | 239 | /* 2 */ 240 | 241 | q:before, 242 | q:after { 243 | content: ''; 244 | content: none; 245 | } 246 | 247 | small { 248 | font-size: 75%; 249 | } 250 | 251 | /* 252 | * Prevents sub and sup affecting line-height in all browsers 253 | * gist.github.com/413930 254 | */ 255 | 256 | sub, 257 | sup { 258 | font-size: 75%; 259 | line-height: 0; 260 | position: relative; 261 | vertical-align: baseline; 262 | } 263 | 264 | sup { 265 | top: -0.5em; 266 | } 267 | 268 | sub { 269 | bottom: -0.25em; 270 | } 271 | 272 | 273 | /* ============================================================================= 274 | Lists 275 | ========================================================================== */ 276 | 277 | /* 278 | * Addresses margins set differently in IE6/7 279 | */ 280 | 281 | dl, 282 | menu, 283 | ol, 284 | ul { 285 | margin: 1em 0; 286 | } 287 | 288 | dd { 289 | margin: 0 0 0 40px; 290 | } 291 | 292 | /* 293 | * Addresses paddings set differently in IE6/7 294 | */ 295 | 296 | menu, 297 | ol, 298 | ul { 299 | padding: 0 0 0 40px; 300 | } 301 | 302 | /* 303 | * Corrects list images handled incorrectly in IE7 304 | */ 305 | 306 | nav ul, 307 | nav ol { 308 | list-style: none; 309 | list-style-image: none; 310 | } 311 | 312 | 313 | /* ============================================================================= 314 | Embedded content 315 | ========================================================================== */ 316 | 317 | /* 318 | * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 319 | * 2. Improves image quality when scaled in IE7 320 | * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 321 | */ 322 | 323 | img { 324 | border: 0; /* 1 */ 325 | -ms-interpolation-mode: bicubic; /* 2 */ 326 | } 327 | 328 | /* 329 | * Corrects overflow displayed oddly in IE9 330 | */ 331 | 332 | svg:not(:root) { 333 | overflow: hidden; 334 | } 335 | 336 | 337 | /* ============================================================================= 338 | Figures 339 | ========================================================================== */ 340 | 341 | /* 342 | * Addresses margin not present in IE6/7/8/9, S5, O11 343 | */ 344 | 345 | figure { 346 | margin: 0; 347 | } 348 | 349 | 350 | /* ============================================================================= 351 | Forms 352 | ========================================================================== */ 353 | 354 | /* 355 | * Corrects margin displayed oddly in IE6/7 356 | */ 357 | 358 | form { 359 | margin: 0; 360 | } 361 | 362 | /* 363 | * Define consistent border, margin, and padding 364 | */ 365 | 366 | fieldset { 367 | border: 1px solid #c0c0c0; 368 | margin: 0 2px; 369 | padding: 0.35em 0.625em 0.75em; 370 | } 371 | 372 | /* 373 | * 1. Corrects color not being inherited in IE6/7/8/9 374 | * 2. Corrects text not wrapping in FF3 375 | * 3. Corrects alignment displayed oddly in IE6/7 376 | */ 377 | 378 | legend { 379 | border: 0; /* 1 */ 380 | padding: 0; 381 | white-space: normal; /* 2 */ 382 | *margin-left: -7px; /* 3 */ 383 | } 384 | 385 | /* 386 | * 1. Corrects font size not being inherited in all browsers 387 | * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 388 | * 3. Improves appearance and consistency in all browsers 389 | */ 390 | 391 | button, 392 | input, 393 | select, 394 | textarea { 395 | font-size: 100%; /* 1 */ 396 | margin: 0; /* 2 */ 397 | vertical-align: baseline; /* 3 */ 398 | *vertical-align: middle; /* 3 */ 399 | } 400 | 401 | /* 402 | * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet 403 | */ 404 | 405 | button, 406 | input { 407 | line-height: normal; /* 1 */ 408 | } 409 | 410 | /* 411 | * 1. Improves usability and consistency of cursor style between image-type 'input' and others 412 | * 2. Corrects inability to style clickable 'input' types in iOS 413 | * 3. Removes inner spacing in IE7 without affecting normal text inputs 414 | * Known issue: inner spacing remains in IE6 415 | */ 416 | 417 | button, 418 | input[type="button"], 419 | input[type="reset"], 420 | input[type="submit"] { 421 | cursor: pointer; /* 1 */ 422 | -webkit-appearance: button; /* 2 */ 423 | *overflow: visible; /* 3 */ 424 | } 425 | 426 | /* 427 | * Re-set default cursor for disabled elements 428 | */ 429 | 430 | button[disabled], 431 | input[disabled] { 432 | cursor: default; 433 | } 434 | 435 | /* 436 | * 1. Addresses box sizing set to content-box in IE8/9 437 | * 2. Removes excess padding in IE8/9 438 | * 3. Removes excess padding in IE7 439 | Known issue: excess padding remains in IE6 440 | */ 441 | 442 | input[type="checkbox"], 443 | input[type="radio"] { 444 | box-sizing: border-box; /* 1 */ 445 | padding: 0; /* 2 */ 446 | *height: 13px; /* 3 */ 447 | *width: 13px; /* 3 */ 448 | } 449 | 450 | /* 451 | * 1. Addresses appearance set to searchfield in S5, Chrome 452 | * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) 453 | */ 454 | 455 | input[type="search"] { 456 | -webkit-appearance: textfield; /* 1 */ 457 | -moz-box-sizing: content-box; 458 | -webkit-box-sizing: content-box; /* 2 */ 459 | box-sizing: content-box; 460 | } 461 | 462 | /* 463 | * Removes inner padding and search cancel button in S5, Chrome on OS X 464 | */ 465 | 466 | input[type="search"]::-webkit-search-decoration, 467 | input[type="search"]::-webkit-search-cancel-button { 468 | -webkit-appearance: none; 469 | } 470 | 471 | /* 472 | * Removes inner padding and border in FF3+ 473 | * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 474 | */ 475 | 476 | button::-moz-focus-inner, 477 | input::-moz-focus-inner { 478 | border: 0; 479 | padding: 0; 480 | } 481 | 482 | /* 483 | * 1. Removes default vertical scrollbar in IE6/7/8/9 484 | * 2. Improves readability and alignment in all browsers 485 | */ 486 | 487 | textarea { 488 | overflow: auto; /* 1 */ 489 | vertical-align: top; /* 2 */ 490 | } 491 | 492 | 493 | /* ============================================================================= 494 | Tables 495 | ========================================================================== */ 496 | 497 | /* 498 | * Remove most spacing between table cells 499 | */ 500 | 501 | table { 502 | border-collapse: collapse; 503 | border-spacing: 0; 504 | } 505 | -------------------------------------------------------------------------------- /src/tokenizer.js: -------------------------------------------------------------------------------- 1 | // tokens.js 2 | // 2009-05-17 3 | 4 | // (c) 2006 Douglas Crockford 5 | 6 | // Small mods by Marcus Westin 2011 7 | 8 | var fs = require('fs'), 9 | util = require('./util') 10 | 11 | exports.tokenize = function(inputString) { 12 | return _doTokenize(inputString) 13 | } 14 | exports.tokenizeFile = function(inputFile) { 15 | return _doTokenize(fs.readFileSync(inputFile).toString(), inputFile) 16 | } 17 | 18 | var TokenizeError = function(file, line, column, msg) { 19 | this.name = 'TokenizeError'; 20 | this.message = ['on line', line + ',', 'column', column, 'of', '"'+file+'":', msg].join(' ') 21 | } 22 | TokenizeError.prototype = Error.prototype 23 | 24 | var types = 'Bool,Text,Number,Color,Enum,Event,Interface,Anything,Nothing'.split(',') // Template,Handler,Function ? 25 | var keywords = 'let,for,in,if,is,else,template,handler,function,new,debugger,switch,case,default,and,or,return,null,import,false,true'.split(',') 26 | 27 | function _doTokenize (inputString, inputFile) { 28 | var c; // The current character. 29 | var from; // The index of the start of the token. 30 | var i = 0; // The index of the current character. 31 | var line = 1; // The line of the current character. 32 | var lineStart = 0; // The index at the beginning of the line 33 | var inputString; // The string to tokenize 34 | var length; // The length of the input string 35 | var n; // The number value. 36 | var q; // The quote character. 37 | var str; // The string value. 38 | var prefix = '+-!=<>&|/'; 39 | var suffix = '=&|/>'; 40 | 41 | var result = []; // An array to hold the results. 42 | 43 | var halt = function (msg) { 44 | var col = from - lineStart + 1 45 | console.log('Tokenizer error', util.grabLine(inputString, line, col, i - from)); 46 | throw new TokenizeError(inputFile, line, col, msg); 47 | } 48 | 49 | var hadNewline = false; 50 | var hadSpace = false; 51 | var make = function (type, value, annotations) { 52 | 53 | // Make a token object. 54 | 55 | var result = { 56 | type: type, 57 | value: value, 58 | from: from, 59 | span: i - from, 60 | line: line, 61 | column: from - lineStart + 1, 62 | hadSpace: hadSpace, 63 | hadNewline: hadNewline, 64 | annotations: annotations 65 | }; 66 | hadSpace = false; 67 | hadNewline = false; 68 | if (inputFile) { result.inputFile = inputFile } 69 | else { result.inputString = inputString } 70 | return result 71 | }; 72 | 73 | // Allow for keyword lookup with "if (str in keywords) { ... }" 74 | 75 | for (var kWord, keyI=0; kWord = keywords[keyI]; keyI++) { 76 | keywords[kWord] = true; 77 | } 78 | 79 | for (var type, typeI=0; type = types[typeI]; typeI++) { types[type] = true } 80 | 81 | // Loop through the text, one character at a time. 82 | 83 | length = inputString.length 84 | 85 | c = inputString.charAt(i); 86 | while (c) { 87 | from = i; 88 | 89 | if (c == '\n') { 90 | 91 | // Keep track of the line number 92 | 93 | hadNewline = true; 94 | hadSpace = true; 95 | line += 1; 96 | i += 1; 97 | lineStart = i; 98 | c = inputString.charAt(i); 99 | 100 | // Ignore whitespace. 101 | 102 | } else if (c <= ' ') { 103 | hadSpace = true; 104 | i += 1; 105 | c = inputString.charAt(i); 106 | 107 | // name. 108 | 109 | } else if (c == '_' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { 110 | str = c; 111 | i += 1; 112 | for (;;) { 113 | c = inputString.charAt(i); 114 | if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || 115 | (c >= '0' && c <= '9') || c === '_') { 116 | str += c; 117 | i += 1; 118 | } else { 119 | break; 120 | } 121 | } 122 | result.push(make(keywords.indexOf(str) != -1 ? 'keyword' : types.indexOf(str) != -1 ? 'type' : 'name', str)); 123 | 124 | // number. 125 | 126 | // A number cannot start with a decimal point. It must start with a digit, 127 | // possibly '0'. 128 | 129 | } else if (c >= '0' && c <= '9') { 130 | str = c; 131 | i += 1; 132 | 133 | // Look for more digits. 134 | 135 | for (;;) { 136 | c = inputString.charAt(i); 137 | if (c < '0' || c > '9') { 138 | break; 139 | } 140 | i += 1; 141 | str += c; 142 | } 143 | 144 | // Look for a decimal fraction part. 145 | 146 | if (c === '.') { 147 | i += 1; 148 | str += c; 149 | for (;;) { 150 | c = inputString.charAt(i); 151 | if (c < '0' || c > '9') { 152 | break; 153 | } 154 | i += 1; 155 | str += c; 156 | } 157 | } 158 | 159 | // Look for an exponent part. 160 | 161 | if (c === 'e' || c === 'E') { 162 | i += 1; 163 | str += c; 164 | c = inputString.charAt(i); 165 | if (c === '-' || c === '+') { 166 | i += 1; 167 | str += c; 168 | c = inputString.charAt(i); 169 | } 170 | if (c < '0' || c > '9') { 171 | halt("Bad exponent"); 172 | } 173 | do { 174 | i += 1; 175 | str += c; 176 | c = inputString.charAt(i); 177 | } while (c >= '0' && c <= '9'); 178 | } 179 | 180 | // Make sure the next character is not a letter. 181 | 182 | if (c >= 'a' && c <= 'z') { 183 | str += c; 184 | i += 1; 185 | halt("Bad number - should not end in a letter"); 186 | } 187 | 188 | // Convert the string value to a number. If it is finite, then it is a good 189 | // token. 190 | 191 | n = +str; 192 | if (isFinite(n)) { 193 | result.push(make('number', n)); 194 | } else { 195 | halt("Bad number"); 196 | } 197 | 198 | // string 199 | 200 | } else if (c === '\'' || c === '"') { 201 | str = ''; 202 | q = c; 203 | i += 1; 204 | for (;;) { 205 | c = inputString.charAt(i); 206 | if (c < ' ' && (c !== '\n' && c !== '\r' && c !== '\t')) { 207 | halt(c === '' ? 208 | "Unterminated string." : 209 | "Control character in string.", make('', str)); 210 | } 211 | 212 | // Look for the closing quote. 213 | 214 | if (c === q) { 215 | break; 216 | } 217 | 218 | // Look for escapement. 219 | 220 | if (c === '\\') { 221 | i += 1; 222 | if (i >= length) { 223 | halt("Unterminated string"); 224 | } 225 | c = inputString.charAt(i); 226 | switch (c) { 227 | case 'b': 228 | c = '\b'; 229 | break; 230 | case 'f': 231 | c = '\f'; 232 | break; 233 | case 'n': 234 | c = '\n'; 235 | break; 236 | case 'r': 237 | c = '\r'; 238 | break; 239 | case 't': 240 | c = '\t'; 241 | break; 242 | case 'u': 243 | if (i >= length) { 244 | halt("Unterminated string"); 245 | } 246 | c = parseInt(inputString.substr(i + 1, 4), 16); 247 | if (!isFinite(c) || c < 0) { 248 | halt("Unterminated string"); 249 | } 250 | c = String.fromCharCode(c); 251 | i += 4; 252 | break; 253 | } 254 | } 255 | str += c; 256 | i += 1; 257 | } 258 | i += 1; 259 | result.push(make('string', str, { type:(q == "'" ? 'single' : 'double') })); 260 | c = inputString.charAt(i); 261 | 262 | // comment. 263 | 264 | } else if (c === '/' && inputString.charAt(i + 1) === '/') { 265 | i += 1; 266 | for (;;) { 267 | c = inputString.charAt(i); 268 | if (c === '\n' || c === '\r' || c === '') { 269 | break; 270 | } 271 | i += 1; 272 | } 273 | 274 | // combining 275 | 276 | } else if (prefix.indexOf(c) >= 0) { 277 | str = c; 278 | i += 1; 279 | while (i < length) { 280 | c = inputString.charAt(i); 281 | if (suffix.indexOf(c) < 0) { 282 | break; 283 | } 284 | str += c; 285 | i += 1; 286 | } 287 | result.push(make('symbol', str)); 288 | c = inputString.charAt(i); 289 | 290 | // single-character symbol 291 | 292 | } else { 293 | i += 1; 294 | result.push(make('symbol', c)); 295 | c = inputString.charAt(i); 296 | } 297 | } 298 | return result; 299 | }; 300 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | var util = module.exports 2 | 3 | var fs = require('fs'), 4 | repeat = require('std/repeat'), 5 | map = require('std/map'), 6 | filter = require('std/filter'), 7 | isArray = require('std/isArray') 8 | 9 | util.q = function(val) { return JSON.stringify(val) } 10 | 11 | var _uniqueId = 0 12 | util.name = function(readable) { return '_' + (readable || '') + '_' + (_uniqueId++) } 13 | 14 | var __uniqueID = 0 15 | util.uniqueID = function() { return '__uniqueID' + (__uniqueID++) } 16 | util.resetUniqueID = function() { __uniqueID = 0 } 17 | 18 | util.cleanup = function(ast) { 19 | function clean(ast) { 20 | if (ast instanceof Array) { 21 | if (ast.length == 1) { return clean(ast[0]) } 22 | return map(filter(ast), clean) 23 | } 24 | return ast || [] 25 | } 26 | var result = clean(ast) 27 | return !result ? [] : (isArray(result) ? result : [result]) 28 | } 29 | 30 | util.create = function(oldObject, props) { 31 | function F() {} 32 | F.prototype = oldObject; 33 | var newObject = new F(); 34 | for (var key in props) { newObject[key] = props[key] } 35 | return newObject 36 | } 37 | 38 | util.map = function(arr, fn) { 39 | var result = [] 40 | if (arr instanceof Array) { for (var i=0; i < arr.length; i++) result.push(fn(arr[i], i)) } 41 | else { for (var key in arr) result.push(fn(arr[key], key)) } 42 | return result 43 | } 44 | util.each = function(arr, fn) { 45 | for (var i=0; i < arr.length; i++) fn(arr[i], i) 46 | } 47 | util.pickOne = function(arr, fn) { 48 | for (var res, i=0; i < arr.length; i++) { 49 | res = fn(arr[i], i) 50 | if (typeof res != 'undefined') { return res } 51 | } 52 | } 53 | util.pick = function(arr, fn) { 54 | var result = [] 55 | for (var i=0, value; i < arr.length; i++) { 56 | value = fn(arr[i]) 57 | if (value) { result.push(value) } 58 | } 59 | return result 60 | } 61 | 62 | util.boxComment = function(msg) { 63 | var len = msg.length 64 | return '/**' + repeat('*', len) + "**\n" + 65 | ' * ' + msg + " *\n" + 66 | ' **' + repeat('*', len) + "**/" 67 | } 68 | 69 | 70 | util.grabLine = function(code, lineNumber, column, length) { 71 | if (!code) { return undefined } 72 | 73 | length = length || 1 74 | var lines = code.split('\n'), 75 | line = _replace(lines[lineNumber - 1], '\t', ' ') 76 | 77 | return '\n\t' + line + '\n\t' 78 | + repeat(' ', column - 1) + repeat('^', length) 79 | } 80 | 81 | util.assert = function(ast, ok, msg) { if (!ok) util.halt(ast, msg) } 82 | util.halt = function(ast, msg) { 83 | var info = ast.info || ast 84 | var prefix = info.inputFile 85 | ? msg+'\nOn line '+info.line+' of file '+info.inputFile+': ' 86 | : msg+'\nOn line '+info.line+' of input string: ' 87 | 88 | var code = (info.inputFile ? fs.readFileSync(info.inputFile).toString() : info.inputString) 89 | throw new Error(prefix + util.grabLine(code, info.line, info.column, info.span)) 90 | } 91 | 92 | util.listToObject = function(list) { 93 | var res = {} 94 | for (var i=0; i expectedCalls) { 120 | throw new Error("Expected only "+expectedCalls+" call"+(expectedCalls == 1 ? '' : 's')) 121 | } 122 | } 123 | } 124 | 125 | var q = function(val) { return JSON.stringify(val) } 126 | 127 | var waitingFor = [] 128 | var observeExpect = function(variable, chain, assert, values, throwOnExtraMutations) { 129 | assert.blocks.add(values.length) 130 | values = map(values, a.value) 131 | waitingFor.push({ original:map(values, a.value), now:values }) 132 | if (chain) { variable = a.reference(variable, chain) } 133 | return variable.observe(function() { 134 | if (!values[0]) { 135 | if (throwOnExtraMutations) { throw new Error("Received unexpected mutation") } 136 | return 137 | } 138 | var logic = variable.equals(values[0]) 139 | if (!logic.getContent()) { return } 140 | values.shift() 141 | assert.blocks.subtract() 142 | }) 143 | } 144 | 145 | function test(name, fn) { 146 | module.exports[name] = function(assert) { 147 | fun.reset() 148 | assert.blocks = { 149 | _count: 0, 150 | _done: false, 151 | add: function(num) { this._count += (typeof num == 'number' ? num : 1) }, 152 | subtract: function(num) { 153 | this._count-- 154 | this.tryNow() 155 | }, 156 | tryNow: function() { 157 | if (this._count || this._done) { return } 158 | this._done = true 159 | assert.done() 160 | } 161 | } 162 | assert.equals = function(val1, val2) { 163 | if (fun.expressions.base.isPrototypeOf(val1) || fun.expressions.base.isPrototypeOf(val2)) { 164 | return this.ok(val1.equals(val2).getContent()) 165 | } else { 166 | return this.deepEqual(val1, val2) 167 | } 168 | } 169 | assert.throws = function(fn) { 170 | var didThrow = false 171 | try { fn() } 172 | catch(e) { didThrow = true } 173 | this.ok(didThrow) 174 | } 175 | try { fn(assert) } 176 | catch(e) { console.log('ERROR', e.stack || e); assert.fail('Test threw: ' + e.message) } 177 | each(waitingFor, function(waitingFor) { 178 | if (!waitingFor.now.length) { return } 179 | console.log("Still waiting for:", waitingFor) 180 | }) 181 | assert.blocks.tryNow() 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /test/tests/test-2-parser.js: -------------------------------------------------------------------------------- 1 | var parser = require('../../src/parser'), 2 | tokenizer = require('../../src/tokenizer'), 3 | a = require('../parser-mocks'), 4 | util = require("../../src/util"), 5 | slice = require('std/slice') 6 | 7 | test('text literal') 8 | .code('"hello world"') 9 | .expect(a.literal("hello world")) 10 | 11 | test('number literal') 12 | .code('1') 13 | .expect(a.literal(1)) 14 | 15 | test('declaration') 16 | .code('greeting = "hello"') 17 | .expect(a.declaration('greeting', a.literal("hello"))) 18 | 19 | test('alias single namespace') 20 | .code('greeting') 21 | .expect(a.reference('greeting')) 22 | 23 | test('alias double namespace') 24 | .code('user.name') 25 | .expect(a.dereference(a.reference('user'), 'name')) 26 | 27 | test('parenthesized expression') 28 | .code('(1)') 29 | .expect(a.literal(1)) 30 | 31 | test('double parenthesized expression') 32 | .code('(("hello"))') 33 | .expect(a.literal("hello")) 34 | 35 | test('addition') 36 | .code('1+1') 37 | .expect(a.binaryOp(a.literal(1), '+', a.literal(1))) 38 | 39 | test('parenthesized subtraction') 40 | .code('(((1-1)))') 41 | .expect(a.binaryOp(a.literal(1), '-', a.literal(1))) 42 | 43 | test('simple if statement') 44 | .code('if 1 is < 2 { 1 }') 45 | .expect(a.ifElse(a.binaryOp(a.literal(1), '<', a.literal(2)), a.literal(1))) 46 | 47 | test('has no null statements or expressions') 48 | .code('foo="bar"\n1') 49 | .expect(a.declaration("foo",a.literal("bar")), a.literal(1)) 50 | 51 | test('variable declaration') 52 | .code('foo = "bar"') 53 | .expect(a.declaration('foo', a.literal('bar'))) 54 | 55 | test('parses empty program') 56 | .code('') 57 | .expect() 58 | 59 | test('* operator precedence 1') 60 | .code('1 + 2 * 3') 61 | .expect(a.binaryOp(a.literal(1), '+', a.binaryOp(a.literal(2), '*', a.literal(3)))) 62 | 63 | test('* operator precedence 2') 64 | .code('1 * 2 + 3') 65 | .expect(a.binaryOp(a.binaryOp(a.literal(1), '*', a.literal(2)), '+', a.literal(3))) 66 | 67 | test('triple nested operators') 68 | .code('1 + 2 + 3 + 4') 69 | .expect(a.binaryOp(a.literal(1), '+', a.binaryOp(a.literal(2), '+', a.binaryOp(a.literal(3), '+', a.literal(4))))) 70 | 71 | test('list literal') 72 | .code('["foo", 1, null]') 73 | .expect(a.literal(['foo', 1, null])) 74 | 75 | test('empty for loop over list literal') 76 | .code('for iterator in [1,2,3] {}') 77 | .expect(a.forLoop('iterator', a.literal([1,2,3]), [])) 78 | 79 | test('self-closing xml') 80 | .code('
') 81 | .expect(a.xml('div')) 82 | 83 | test('inline javascript') 84 | .code('foo = 1\n ') 85 | .expect( 86 | a.declaration('foo', a.literal(1)), 87 | a.inlineScript({ fooVariable:a.reference('foo') }, ' var i = 1; function a() { alert(i++) }; setInterval(a);') 88 | ) 89 | 90 | test('nested declaration') 91 | .code( 92 | 'foo = { nested: { cat:"yay" } }', 93 | 'foo bar foo.nested' 94 | ) 95 | .expect( 96 | a.declaration('foo', a.literal({ nested:{ cat:'yay' } })), 97 | a.reference('foo'), a.reference('bar'), a.dereference(a.reference('foo'), 'nested') 98 | ) 99 | 100 | test('deep nested declaration') 101 | .code('asd = {a:{b:{c:{d:{e:{f:{}}}}}}}') 102 | .expect(a.declaration('asd', a.literal({a:{b:{c:{d:{e:{f:{}}}}}}}))) 103 | 104 | test('just a declaration') 105 | .code('foo = { bar:1 }') 106 | .expect(a.declaration('foo', a.literal({ bar:1 }))) 107 | 108 | test('a handler') 109 | .code( 110 | 'aHandler = handler(){}' 111 | ) 112 | .expect( 113 | a.declaration('aHandler', a.handler()) 114 | ) 115 | 116 | test('a button which mutates state') 117 | .code( 118 | 'foo="bar"', 119 | '' 120 | ) 121 | .expect( 122 | a.declaration('foo', a.literal("bar")), 123 | a.xml('button', { 'onclick':a.handler([],[ 124 | a.mutation(a.reference('foo'), 'set', [a.literal("cat")]) 125 | ])}) 126 | ) 127 | 128 | test('handler with logic') 129 | .code( 130 | 'cat = "hi"', 131 | 'foo = handler() {', 132 | ' if cat is == "hi" { cat set: "bye" }', 133 | ' else { cat set: foo }', 134 | '}' 135 | ) 136 | .expect( 137 | a.declaration('cat', a.literal('hi')), 138 | a.declaration('foo', a.handler([], [ 139 | a.ifElse(a.binaryOp(a.reference('cat'), '==', a.literal('hi')),[ 140 | a.mutation(a.reference('cat'), 'set', [a.literal('bye')]) 141 | ], [ 142 | a.mutation(a.reference('cat'), 'set', [a.reference('foo')]) 143 | ]) 144 | ])) 145 | ) 146 | 147 | test('parse emits then declarations') 148 | .code( 149 | 'foo="foo"', 150 | '
', 151 | 'cat="cat"' 152 | ) 153 | .expect( 154 | a.declaration('foo', a.literal('foo')), 155 | a.xml('div'), 156 | a.declaration('cat', a.literal('cat')) 157 | ) 158 | 159 | test('variable declaration inside div') 160 | .code('
cat="cat"
') 161 | .expect(a.xml('div', null, [a.declaration('cat', a.literal('cat'))])) 162 | 163 | test('null value') 164 | .code('null') 165 | .expect(a.null()) 166 | 167 | test('function arguments') 168 | .code('fun = function(arg1, arg2) { return arg1 + arg2 }', 'fun(1, 2)') 169 | .expect( 170 | a.declaration('fun', a.function([a.argument('arg1'), a.argument('arg2')], [ 171 | a.return( 172 | a.binaryOp(a.reference('arg1'), '+', a.reference('arg2')) 173 | ) 174 | ])), 175 | a.invocation(a.reference('fun'), a.literal(1), a.literal(2)) 176 | ) 177 | 178 | test('if/else in a div') 179 | .code( 180 | '
if Mouse.x is >= 100 { "mouse.x is >= 100" }', 181 | 'else { "mouse.x is < 100" }
') 182 | .expect( 183 | a.xml('div', null, [ 184 | a.ifElse(a.binaryOp(a.dereference(a.reference('Mouse'), 'x'), '>=', a.literal(100)), [ 185 | a.literal('mouse.x is >= 100') 186 | ], [ 187 | a.literal('mouse.x is < 100') 188 | ]) 189 | ]) 190 | ) 191 | 192 | test('script tag in function parses') 193 | .code( 194 | 'foo = function(qwe) {', 195 | ' ', 197 | ' return 1', 198 | '}') 199 | .expect( 200 | a.declaration('foo', a.function(['qwe'], [ 201 | a.inlineScript({ missing:'missing' }), 202 | a.return(a.literal(1)) 203 | ])) 204 | ) 205 | 206 | test('for loop over object literal') 207 | .code('for foo in { bar:"bar", cat:"cat" } {}') 208 | .expect( 209 | a.forLoop('foo', a.literal({ bar:'bar', cat:'cat' }), []) 210 | ) 211 | 212 | test('double declaration') 213 | .code( 214 | 'foo = 1', 215 | 'bar = 2' 216 | ).expect(a.declaration('foo', a.literal(1)), a.declaration('bar', a.literal(2))) 217 | 218 | test('if, else if, else') 219 | .code( 220 | 'if (false) { "qwe" }', 221 | 'else if (true) { "foo" }', 222 | 'else { "cat" }' 223 | ).expect( 224 | a.ifElse(a.literal(false), a.literal("qwe"), a.ifElse(a.literal(true), a.literal("foo"), a.literal("cat"))) 225 | ) 226 | 227 | test('literals and xml with and without semicolons') 228 | .code( 229 | '[1 2 3, 4, "qwe"]', 230 | '{ foo:"bar" cat:2, qwe:"qwe" }', 231 | 'foo(1 2, 3 "qwe")', 232 | '
' 233 | ).expect( 234 | a.literal([1, 2, 3, 4, 'qwe']), 235 | a.literal({ foo:'bar', cat:2, qwe:'qwe' }), 236 | a.invocation(a.reference('foo'), a.literal(1), a.literal(2), a.literal(3), a.literal('qwe')), 237 | a.xml('div', { foo:a.literal("bar"), cat:a.literal(1), qwe:a.literal('qwe') }) 238 | ) 239 | 240 | test('dictionary literals and xml can use both = and : for key/value pairs') 241 | .code( 242 | '{ foo:1 bar=2, cat:3 }', 243 | '
' 244 | ).expect( 245 | a.literal({ foo:1, bar:2, cat:3 }), 246 | a.xml('div', { foo:a.literal(1), bar:a.literal(2) }) 247 | ) 248 | 249 | test('discern between invocation and parenthesized expression') 250 | .code('foo(1) foo (1)') 251 | .expect(a.invocation(a.reference('foo'), a.literal(1)), a.reference('foo'), a.literal(1)) 252 | 253 | test('xml hash-expand attribute') 254 | .code('
') 255 | .expect(a.xml('div', [{ expand:a.literal({ 'class':'cool' }) }])) 256 | 257 | test('simple import') 258 | .code('import foo') 259 | .expect(a.import('foo')) 260 | 261 | test('path import') 262 | .code('import foo/bar/cat') 263 | .expect(a.import('foo/bar/cat')) 264 | 265 | test('climb dir') 266 | .code('import ../../foo') 267 | .expect(a.import('../../foo')) 268 | 269 | test('absolute import') 270 | .code('import /foo/bar') 271 | .expect(a.import('/foo/bar')) 272 | 273 | test('double dereference') 274 | .code('foo.bar.cat') 275 | .expect(a.reference('foo.bar.cat')) 276 | 277 | test('dynamic dereference with static value') 278 | .code('foo["bar"] foo.bar') 279 | .expect(a.reference('foo.bar'), a.reference('foo.bar')) 280 | 281 | test('dynamic dereference') 282 | .code('foo[bar] cat[qwe.tag()]') 283 | .expect(a.dereference(a.reference('foo'), a.reference('bar')), a.dereference(a.reference('cat'), a.invocation(a.dereference(a.reference('qwe'), 'tag')))) 284 | 285 | test('foo = (1) + (1)') 286 | .code('foo = (1) + (1)') 287 | .expect(a.declaration('foo', a.binaryOp(a.literal(1), '+', a.literal(1)))) 288 | 289 | /* Util 290 | ******/ 291 | function test(name) { 292 | util.resetUniqueID() 293 | var input 294 | return { 295 | code: function() { 296 | util.resetUniqueID() 297 | input = slice(arguments).join('\n') 298 | return this 299 | }, 300 | expect: function() { 301 | var expected = slice(arguments), 302 | tokens = tokenizer.tokenize(input) 303 | module.exports['parse\t\t"'+name+'"'] = function(assert) { 304 | util.resetUniqueID() 305 | try { var output = parser.parse(tokens) } 306 | catch(e) { console.log("Parser threw"); throw e; } 307 | assert.deepEqual(expected, output) 308 | assert.done() 309 | } 310 | return this 311 | } 312 | } 313 | } 314 | 315 | /* Old, typed tests 316 | ******************/ 317 | // test('interface declarations') 318 | // .code( 319 | // 'Thing = { foo:Text, bar:Number }', 320 | // 'ListOfThings=[ Thing ]', 321 | // 'ListOfNumbers = [Number]', 322 | // 'NumberInterface = Number' 323 | // ) 324 | // .expect( 325 | // a.declaration('Thing', a.interface({ foo:a.Text, bar:a.Number })), 326 | // a.declaration('ListOfThings', a.interface([a.alias('Thing')])), 327 | // a.declaration('ListOfNumbers', a.interface([a.Number])), 328 | // a.declaration('NumberInterface', a.Number) 329 | // ) 330 | // 331 | // test('typed value declarations') 332 | // .code( 333 | // 'Response = { error:Text, result:Text }', 334 | // 'Response response = { error:"foo", result:"bar" }', 335 | // 'response' 336 | // ) 337 | // .expect( 338 | // a.declaration('Response', a.interface({ error:a.Text, result:a.Text })), 339 | // a.declaration('response', a.object({ error:a.literal('foo'), result:a.literal('bar') }), a.alias('Response')), 340 | // a.alias('response') 341 | // ) 342 | // 343 | // test('typed function declaration and invocation') 344 | // .code( 345 | // 'Response = { error:Text, result:Text }', 346 | // 'Response post = function(Text path, Anything params) {', 347 | // ' return { error:"foo", response:"bar" }', 348 | // '}', 349 | // 'response = post("/test", { foo:"bar" })' 350 | // ) 351 | // .expect( 352 | // a.declaration('Response', a.interface({ error:a.Text, result:a.Text })), 353 | // a.declaration('post', a.function([a.argument('path', a.Text), a.argument('params', a.Anything)], [ 354 | // a.return(a.object({ error:a.literal('foo'), response:a.literal('bar') })) 355 | // ]), a.alias('Response')), 356 | // a.declaration('response', a.invocation(a.alias('post'), a.literal('/test'), a.object({ foo:a.literal('bar')}))) 357 | // ) 358 | // 359 | // test('explicit interface declarations') 360 | // .code( 361 | // 'Thing = { foo:Text, bar:Number }', 362 | // 'Thing thing = null', 363 | // 'thing' 364 | // ) 365 | // .expect( 366 | // a.declaration('Thing', a.interface({ foo:a.Text, bar:a.Number })), 367 | // a.declaration('thing', a.null, a.alias('Thing')), 368 | // a.alias('thing') 369 | // ) 370 | // 371 | // test('type-inferred function invocation') 372 | // .code( 373 | // 'Response = { error:Text, result:Text }', 374 | // 'post = function(Text path, Anything params) {', 375 | // ' return { error:"foo", response:"bar" }', 376 | // '}', 377 | // 'response = post("/test", { foo:"bar" })' 378 | // ) 379 | // .expect( 380 | // a.declaration('Response', a.interface({ error:a.Text, result:a.Text })), 381 | // a.declaration('post', a.function([a.argument('path', a.Text), a.argument('params', a.Anything)], [ 382 | // a.return(a.object({ error:a.literal('foo'), response:a.literal('bar') })) 383 | // ])), 384 | // a.declaration('response', a.invocation(a.alias('post'), a.literal('/test'), a.object({ foo:a.literal('bar')}))) 385 | // ) 386 | // 387 | // Thing = { num:Number, foo:{ bar:[Text] }} 388 | // Number five = 5 389 | // Thing thing = { num:five, foo:{ bar:"cat" }} 390 | // { num:Number, foo:{ bar:Text } } thing = { num:five, foo:{ bar:"cat" }} 391 | // fun = function(Thing thing, { num:Number, foo:Text } alt) { ... } 392 | // Response post = function(path, params) { return XHR.post(path, params) } 393 | // response = post('/path', { foo:'bar' }) 394 | // assert response.type == Response 395 | // tar = XHR.post("/test", { foo:'bar' }) 396 | // Response tar = XHR.post("/test", { foo:'bar' }) 397 | // { error:String, result:String } tar = XHR.post("/test", { foo:'bar' }) 398 | -------------------------------------------------------------------------------- /test/tests/test-3-resolver.js: -------------------------------------------------------------------------------- 1 | var a = require('../resolver-mocks'), 2 | resolver = require('../../src/resolver'), 3 | parser = require('../../src/parser'), 4 | tokenizer = require('../../src/tokenizer'), 5 | util = require('../../src/util'), 6 | slice = require('std/slice') 7 | 8 | test("a declared alias for a string") 9 | .code( 10 | 'guy = "Marcus"', 11 | 'guy' 12 | ) 13 | .expect( 14 | a.declaration('guy', a.literal('Marcus')), 15 | a.reference('guy') 16 | ) 17 | 18 | test("an empty div") 19 | .code('
') 20 | .expect(a.xml('div')) 21 | 22 | test("nested aliases") 23 | .code( 24 | 'foo = { bar:1, cat:"cat" }', 25 | 'foo foo.bar foo.cat' 26 | ) 27 | .expect( 28 | a.declaration('foo', a.literal({ bar:1, cat:'cat' })), 29 | a.reference('foo'), 30 | a.reference('foo.bar'), 31 | a.reference('foo.cat') 32 | ) 33 | 34 | // test('nested values and references of references') 35 | // .code( 36 | // 'var foo = { nested: { cat:"yay" } }') 37 | // 'var bar = foo.nested', 38 | // 'var cat = bar.cat', 39 | // 'var cat2 = foo.nested.cat', 40 | // 'foo.nested.cat bar.cat cat bar') 41 | // .declarations( 42 | // ref(1, a.declaration('foo', a.object({ nested:a.object({ cat:a.literal('yay') }) }))) 43 | // ) 44 | // .expressions(a.reference(), ref(1), ref(1), ref(4)) 45 | // 46 | // all values are typed at runtime, and can change type. there are 47 | // atomics: numbers, text, bool, null 48 | // collections: list, object 49 | // collection references: foo.bar.cat, taw[1][4] 50 | // do we want dynamic dereferencing?: foo[bar] 51 | // an expression is 52 | 53 | test('clicking a button updates the UI') 54 | .code( 55 | 'foo = "bar"', 56 | 'qwe = "cat"', 57 | '
foo
', 58 | '') 62 | .expect( 63 | a.declaration('foo', a.literal('bar')), 64 | a.declaration('qwe', a.literal('cat')), 65 | a.xml('div', { id:a.literal('output') }, [ a.reference('foo') ]), 66 | a.xml('button', { id:a.literal('button'), onClick:a.handler([], [ 67 | a.mutation(a.reference('foo'), 'set', [a.literal('cat')]), 68 | a.mutation(a.reference('qwe'), 'set', [a.reference('foo')]) 69 | ]) }, [ a.literal('Click me') ]) 70 | ) 71 | 72 | test('variable declaration inside div') 73 | .code('
cat="cat"
') 74 | .expect(a.xml('div', [], [a.declaration('cat', a.literal('cat'))])) 75 | 76 | test('function invocation') 77 | .code('fun = function() { return 1 }', 'fun()') 78 | .expect(a.declaration('fun', a.function([], [a.return(a.literal(1))])), a.invocation(a.reference('fun'))) 79 | 80 | test('function arguments') 81 | .code('fun = function(arg1, arg2) { return arg1 + arg2 }', 'fun(1, 2)') 82 | .expect( 83 | a.declaration('fun', a.function([a.argument('arg1'), a.argument('arg2')], [ 84 | a.return(a.binaryOp(a.reference('arg1'), '+', a.reference('arg2'))) 85 | ])), 86 | a.invocation(a.reference('fun'), a.literal(1), a.literal(2)) 87 | ) 88 | 89 | test('missing script tag attribute value is caught') 90 | .code( 91 | 'foo = function(qwe) {', 92 | ' ', 94 | ' return 1', 95 | '}') 96 | .expectError(/^Couldn't find a variable called "missing"/) 97 | 98 | test('variable names must start with a lowercase letter') 99 | .code('Foo = "qwe"') 100 | .expectError(/^Variable names must start with/) 101 | 102 | test('xml hash-expand attribute') 103 | .code('
') 104 | .expect(a.xml('div', [{ expand:a.literal({ 'class':'cool' }) }])) 105 | 106 | test('import path') 107 | .code('import ui/lists', 'lists') 108 | .expect(a.reference('lists')) 109 | 110 | // Boolean values 111 | // Null values 112 | // Handlers, Functions and Templates as expressions and being emitted 113 | // 114 | // test('typed value values') 115 | // .code( 116 | // 'Response = { error:Text, result:Text }', 117 | // 'Response response = { error:"foo", result:"bar" }', 118 | // 'response' 119 | // ) 120 | // .expect( 121 | // a.declaration('Response', a.interface({ error:a.Text, result:a.Text })), 122 | // a.declaration('response', a.object({ error:a.literal('foo'), result:a.literal('bar') }), a.alias('Response')), 123 | // a.alias('response') 124 | // ) 125 | // 126 | // test('typed function declaration and invocation') 127 | // .code( 128 | // 'Response = { error:Text, result:Text }', 129 | // 'Response post = function(Text path, Anything params) {', 130 | // ' return { error:"foo", response:"bar" }', 131 | // '}', 132 | // 'response = post("/test", { foo:"bar" })' 133 | // ) 134 | // .expect( 135 | // a.declaration('Response', a.interface({ error:a.Text, result:a.Text })), 136 | // a.declaration('post', a.function([a.argument('path', a.Text), a.argument('params', a.Anything)], [ 137 | // a.return(a.object({ error:a.literal('foo'), response:a.literal('bar') })) 138 | // ]), a.alias('Response')), 139 | // a.declaration('response', a.invocation(a.alias('post'), a.literal('/test'), a.object({ foo:a.literal('bar')}))) 140 | // ) 141 | 142 | // TODO test file imports 143 | 144 | /* Util 145 | ******/ 146 | function test(name) { 147 | util.resetUniqueID() 148 | ref.references = {} 149 | var inputCode 150 | return { 151 | code: function(/* line1, line2, ... */) { 152 | inputCode = slice(arguments).join('\n') 153 | return this 154 | }, 155 | expect: function() { 156 | runTest(null, slice(arguments)) 157 | return this 158 | }, 159 | expectError: function(expectedErrorRe) { 160 | runTest(expectedErrorRe, null) 161 | return this 162 | } 163 | } 164 | function runTest(expectedErrorRe, expectedAST) { 165 | util.resetUniqueID() // TODO the unique IDs function should probably be on the resolver 166 | var count = 1, 167 | testName = '"'+name+'" ' + (count++ == 1 ? '' : count) 168 | while (module.exports[testName]) { 169 | testName = '"'+name+'" ' + (count++) 170 | } 171 | module.exports['resolve\t'+testName+''] = function(assert) { 172 | try { runTest() } 173 | catch(e) { onError(e) } 174 | 175 | function runTest() { 176 | var inputAST = parser.parse(tokenizer.tokenize(inputCode)), 177 | opts = { dirname:__dirname, minify:false } 178 | resolver.resolve(inputAST, opts, function(err, resolved) { 179 | if (err) { return onError(err) } 180 | var output = resolved.expressions 181 | assert.deepEqual(expectedAST, output) 182 | assert.done() 183 | }) 184 | } 185 | 186 | function onError(e) { 187 | if (expectedErrorRe && e.message.match(expectedErrorRe)) { 188 | assert.done() 189 | } else { 190 | console.log('resolver threw', e.stack) 191 | process.exit(0) 192 | } 193 | } 194 | } 195 | } 196 | } 197 | 198 | function ref(id, value) { 199 | var references = ref.references 200 | if (value) { 201 | if (references[id]) { throw new Error("Same test reference declared twice") } 202 | references[id] = value 203 | } else { 204 | if (!references[id]) { throw new Error("Referenced undeclared test reference") } 205 | } 206 | return references[id] 207 | } 208 | -------------------------------------------------------------------------------- /test/tests/test-4-compiler.js: -------------------------------------------------------------------------------- 1 | var compiler = require('../../src/compiler'), 2 | http = require('http'), 3 | zombie = require('zombie'), 4 | compilerServerPort = 9797, 5 | slice = require('std/slice'), 6 | each = require('std/each'), 7 | hasClass = require('fun-dom/hasClass') 8 | 9 | var currentTestCode, compilerServer 10 | 11 | startCompilerServer() 12 | 13 | test('a number and a string').code( 14 | '
"Hello " 1
') 15 | .textIs('#output', 'Hello 1') 16 | 17 | test('a variable in a div').code( 18 | 'foo = "foo"', 19 | '
foo
') 20 | .textIs('#output', 'foo') 21 | 22 | // test('clicking a button updates the UI').code( 23 | // 'foo = "bar"', 24 | // '
foo
', 25 | // '') 28 | // .textIs('#output', 'bar') 29 | // .click('#button') 30 | // .textIs('#output', 'cat') 31 | 32 | // test('object literals').code( 33 | // 'foo = { nested: { bar:1 } }', 34 | // '
foo foo.nested foo.nested.bar
') 35 | // .textIs('#output', '{ "nested":{ "bar":1 } }{ "bar":1 }1') 36 | 37 | // test('divs follow mouse').code( 38 | // 'import mouse', 39 | // '
', 40 | // '
') 41 | // .moveMouse(100, 100) 42 | // .positionIs('#output1', 100, 100) 43 | // .positionIs('#output2', 150, 150) 44 | 45 | // test('script tag variable passing').code( 46 | // 'foo = "foo"', 47 | // '', 50 | // '
foo
') 51 | // .textIs('#output', 'bar') 52 | 53 | // test('variable declaration inside div').code( 54 | // '
', 55 | // ' cat = "cat"', 56 | // '
cat
', 57 | // '
') 58 | // .textIs('#output', 'cat') 59 | 60 | // test('changing object literals').code( 61 | // 'foo = { a:1 }', 62 | // '
', 63 | // ' { foo: { a:foo.a } }', 64 | // ' { a:foo.a }', 65 | // ' foo', 66 | // '
') 67 | // .textIs('#output', '{ "foo":{ "a":1 } }{ "a":1 }{ "a":1 }') 68 | // .click('#output') 69 | // .textIs('#output', '{ "foo":{ "a":2 } }{ "a":2 }{ "a":2 }') 70 | 71 | // test('null values').code( 72 | // 'foo=null', 73 | // '
"null:"foo " null:"null
') 74 | // .textIs('#output', 'null: null:') 75 | 76 | // test('function invocation').code( 77 | // 'fun = function() { return 1 }', 78 | // '
fun()
') 79 | // .textIs('#output', '1') 80 | 81 | // test('function argument').code( 82 | // 'fun = function(arg) { return arg }', 83 | // '
fun(1) fun("hello")
') 84 | // .textIs('#output', '1hello') 85 | 86 | // test('statements after return do not evaluate').code( 87 | // 'fun = function() {', 88 | // ' return 1', 89 | // ' return 2', 90 | // '}', 91 | // '
fun()
') 92 | // .textIs('#output', '1') 93 | 94 | // test('if/else in a div -> if branch').code( 95 | // 'foo = 120', 96 | // '
if foo is >= 100 { "foo is >= 100" }', 97 | // 'else { "foo is < 100" }
') 98 | // .textIs('#output', 'foo is >= 100') 99 | 100 | // test('if/else in a div -> else branch').code( 101 | // 'foo = 120', 102 | // '
', 103 | // ' if foo is < 100 { "foo is < 100" }', 104 | // ' else { "foo is >= 100" }', 105 | // '
') 106 | // .textIs('#output', 'foo is >= 100') 107 | 108 | // test('if/else in a div -> first if branch, then else branch').code( 109 | // 'foo = 120', 110 | // '
', 111 | // ' if foo is < 100 { "foo is < 100" }', 112 | // ' else { "foo is >= 100" }', 113 | // '
', 114 | // '