├── .gitignore ├── .jshintrc ├── GruntFile.js ├── LICENSE-MIT ├── README.md ├── bin ├── rosetta └── usage.txt ├── lib ├── Resolver.js ├── Token.js ├── debug.js ├── errors.js ├── lexer.js ├── nodes.js ├── output │ ├── css.js │ ├── js.js │ └── jstemplates │ │ ├── commonjs.txt │ │ ├── flat.txt │ │ ├── preamble.js │ │ └── requirejs.txt ├── parser.js ├── rosetta.js └── util.js ├── package.json ├── tasks └── rosetta.js └── test ├── basic.rose └── circular_deps.rose /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tmp 3 | TODO -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": false, 4 | "immed": true, 5 | "latedef": false, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "quotmark": "single", 11 | 12 | "asi": true, 13 | "boss": true, 14 | "eqnull": true, 15 | "node": true, 16 | "es5": true, 17 | "expr": true 18 | } -------------------------------------------------------------------------------- /GruntFile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | jshint: { 8 | all: [ 9 | 'Gruntfile.js', 10 | 'lib/**/*.js', 11 | '<%= nodeunit.tests %>', 12 | ], 13 | options: { 14 | jshintrc: '.jshintrc', 15 | } 16 | }, 17 | 18 | nodeunit: { 19 | tests: ['test/**/*_test.js'] 20 | }, 21 | 22 | watch: { 23 | all: { 24 | files: ['<%= jshint.all %>'], 25 | tasks: 'jshint' 26 | } 27 | }, 28 | 29 | clean: { 30 | tmp: ['tmp'] 31 | }, 32 | 33 | rosetta: { 34 | testBasic: { 35 | src: ['test/basic.rose'], 36 | options: { 37 | jsFormat: 'requirejs', 38 | cssFormat: 'less', 39 | jsOut: 'tmp/rosetta.js', 40 | cssOut: 'tmp/css/{{ns}}.less', 41 | } 42 | }, 43 | } 44 | }); 45 | 46 | grunt.loadTasks('tasks'); 47 | 48 | grunt.loadNpmTasks('grunt-contrib-jshint'); 49 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 50 | grunt.loadNpmTasks('grunt-contrib-watch'); 51 | grunt.loadNpmTasks('grunt-contrib-clean'); 52 | 53 | grunt.registerTask('default', ['jshint nodeunit']); 54 | 55 | }; -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ned Burns 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rosetta 2 | 3 | **The JS API has changed to a synchronous model in v0.3; see "Javascript API", below.** 4 | 5 | Rosetta is a CSS *pre-* preprocessor that allows you to share variables between your Javascript code and a CSS preprocessor such as [Stylus](http://learnboost.github.com/stylus/), [Sass](http://sass-lang.com/), or [LESS](http://lesscss.org/). 6 | 7 | It works like this: 8 | 9 | 1. You define your shared variables in one or more `.rose` files. 10 | 2. Rosetta compiles your `.rose` files into a Javascript module and one or more Stylus/Sass/LESS files. 11 | 12 | Rosetta supports the following export formats: 13 | * **Javascript:** CommonJS module, RequireJS module, or flat JS file. 14 | * **CSS:** Stylus, Sass/Scss, or LESS syntax. 15 | 16 | You can also add your own export formats; see the command-line documentation for more information. 17 | 18 | ## Example 19 | 20 | Imagine you want to want to create a shared variable: 21 | 22 | $thumbnailSize = 250px 23 | 24 | Rosetta allows you to use this variable in both your Javascript: 25 | ```js 26 | var rosetta = require('./rosetta'); 27 | console.log('Thumbnail size is:', rosetta.thumbnailSize.val); 28 | ``` 29 | 30 | ...and your CSS (in this case, a Stylus file): 31 | ```css 32 | @import rosetta 33 | .thumb { 34 | width: $thumbnailSize 35 | height: $thumbnailSize 36 | } 37 | ``` 38 | 39 | ## How to install 40 | 41 | You can use Rosetta via the command-line, as a [Grunt](http://gruntjs.com) plugin, or as a Javascript library. 42 | 43 | To install for use on the command-line: 44 | ``` 45 | $ sudo npm install -g rosetta 46 | ``` 47 | 48 | To install for Grunt or as a JS library: 49 | ``` 50 | $ npm install rosetta 51 | ``` 52 | 53 | See [How to run Rosetta](#howToRun) for instructions on how to invoke the compiler. 54 | 55 | ## File format 56 | 57 | Rosetta uses the same variable declaration syntax as Stylus. It looks like this: 58 | ``` 59 | $myVar = 55px 60 | ``` 61 | Semicolons are optional. 62 | 63 | You can use a variety of data types: 64 | ``` 65 | $number = 45px 66 | $color = #00FF00 67 | $rgb = rgba(255, 13, 17, 0.3) 68 | $url = url('/penguins.png') 69 | $string = 'hello, world' 70 | $css = top left, center center 71 | ``` 72 | 73 | Variables can reference other variables and be combined using arithmetic expressions: 74 | ``` 75 | $foo = 35px 76 | $bar = $foo + 5 // bar is 40px 77 | $baz = foo * (bar - 45) 78 | ``` 79 | 80 | Finally, you can organize your variables into namespaces: 81 | ``` 82 | colors: 83 | $red = #990000 84 | $selection = #1122CC 85 | $highlight = #1199AA 86 | 87 | prompts: 88 | $text = #222 89 | $warn = #F0F 90 | $error = #F00 91 | 92 | // You can 'add' to a namespace after the fact like this. 93 | // This can even occur in a separate .rose file 94 | colors.somethingElse: 95 | $foo = colors.prompts.$error // fully-qualified references! 96 | ``` 97 | 98 | Rosetta can either dump each namespace to its own CSS file or concat them into a single large file. 99 | 100 | ## Accessing Rosetta variables 101 | 102 | ### Javascript 103 | Rosetta creates a JS object whose structure reflects your namespace structure. Given a Rosetta file like this: 104 | ``` 105 | $numShapes = 5 106 | animationDurations: 107 | $dialogAppear = 400ms 108 | $dialogDismiss = 200ms 109 | ``` 110 | ...the vars can be accessed like this: 111 | ```js 112 | // in this example, we're using the CommonJS output format 113 | var rosetta = require('./rosetta'); 114 | ... 115 | rosetta.numShapes.val; // 5 116 | rosetta.animationDurations.dialogAppear.val; // 400 117 | rosetta.animationDurations.dialogAppear.unit; // 'ms' 118 | ``` 119 | 120 | Every Rosetta variable has the following properties: 121 | * `val` - The 'value' part of the variable. For numbers this means just the number part (e.g. `400` from `400px`). For colors, it will be a 24-bit number (e.g. 0xAC2B39). For URLs, it will be the URL itself. Strings and raw CSS are both just strings. 122 | * `type` - One of `number`, `color`, `string`, `url`, or `css`. 123 | 124 | Some datatypes have additional properties: 125 | 126 | #### number 127 | * `unit` - The unit associated with the number, e.g. `px` or `%`. `null` if no unit specified. 128 | 129 | #### color 130 | * `r` - Red (0-255) 131 | * `g` - Green (0-255) 132 | * `b` - Blue (0-255) 133 | * `a` - Alpha (0-1) 134 | 135 | ### CSS 136 | All your variables will be exported to the format you specified, e.g. 137 | ``` 138 | @highlight: #2211CC // Sass format 139 | ``` 140 | 141 | In addition, all variables declared inside of a namespace will also be exported with a fully-qualified name: 142 | 143 | ``` 144 | colors.dialog: 145 | $highlight = #2211CC 146 | ``` 147 | ...becomes... 148 | ``` 149 | @highlight: #2211CC 150 | @colors-dialog-highlight: #2211CC 151 | ``` 152 | 153 | This allows you to access the variable even if its shortname gets trampled by something else. 154 | 155 | ## How to run Rosetta 156 | 157 | ### Command-line 158 | 159 | ``` 160 | Usage: rosetta {OPTIONS} [files] 161 | 162 | Example: 163 | rosetta --jsout "lib/css.js" --cssout "stylus/{{ns}}.styl" rosetta/**/*.rose 164 | 165 | Options: 166 | 167 | --jsOut Write the JS module to this file. 168 | 169 | --cssOut Write the CSS to this file. If the path contains the string 170 | '{{ns}}', then a file will be created for every namespace in 171 | your .rose files, replacing the {{ns}} with the name of each 172 | of your namspaces. 173 | 174 | --jsFormat The desired output format for the JS module. Supports 175 | 'commonjs', 'requirejs', and 'flat'. Default: 'commonjs'. 176 | 177 | --cssFormat Desired output format for the CSS file(s). Should be one of 178 | 'stylus', 'sass', 'scss', or 'less'. Default: 'stylus'. 179 | 180 | --jsTemplate A custom template that defines how the Javascript should be 181 | formatted. This should be in the format of an Underscore.js 182 | template, and must specify slots for variables named 183 | 'preamble' and 'blob'. For example: 184 | $'<%= preamble %>\n var x = <%= blob %>;' 185 | (the leading $ is required if you want bash to understand \n) 186 | 187 | --cssTemplate A custom template that defines how a single CSS variable 188 | should be formatted. This should be a string in the form of 189 | an Underscore.js template, and must specify slots for 190 | variables named 'k' (the name of the variable) and 'v' (the 191 | value of the variable). For example: 192 | '$<%= k %>: <%= v %>;' 193 | 194 | --version, -v Print the current version to stdout. 195 | 196 | --help, -h Show this message. 197 | 198 | [files] can be a list of any number of files. Glob syntax is supported, 199 | e.g. 'rosetta/**/*.rose' will resolve to all files that are contained in the 200 | 'rosetta' directory (or any of its subdirectories) and that end with '.rose'. 201 | ``` 202 | 203 | Note: Normally, rosetta will dump your CSS to a single file. However, if your `cssOut` path contains the string `{{ns}}`, then it will instead dump each namespace to its own file, replacing `{{ns}}` with the namespace's name. This allows you to `@include` these files individually, which can be nice if you have a lot of them, e.g. 204 | 205 | ```css 206 | @import colors 207 | @import colors/prompts 208 | @import animation/prompts 209 | ``` 210 | 211 | ### As a Grunt plugin 212 | 213 | All options are the same as those for the command-line. At the very least, you should specify paths for `jsOut` and `cssOut`. 214 | 215 | For example: 216 | ```js 217 | module.exports = function(grunt) { 218 | grunt.initConfig({ 219 | ... 220 | rosetta: { 221 | default: { 222 | src: ['rosetta/**/*.rose'], 223 | options: { 224 | jsFormat: 'requirejs', 225 | cssFormat: 'less', 226 | jsOut: 'lib/rosetta.js', 227 | cssOut: 'less/rosetta/{{ns}}.less', 228 | } 229 | } 230 | } 231 | }); 232 | ... 233 | grunt.loadNpmTasks('rosetta'); 234 | }; 235 | ``` 236 | 237 | ### Javascript API 238 | 239 | Example: 240 | ```js 241 | try { 242 | var outfiles = rosetta.compile(['foo.rose', 'bar.rose'], { 243 | jsFormat: 'flat', 244 | cssFormat: 'less', 245 | jsOut: 'lib/rosetta.js', 246 | cssOut: 'less/rosetta.less' 247 | }); 248 | rosetta.writeFiles(outfiles); 249 | } catch (e) { 250 | if (e instanceof rosetta.RosettaError) { 251 | console.error(rosetta.formatError(e)); 252 | } else { 253 | throw e; 254 | } 255 | } 256 | ``` 257 | 258 | Rosetta exposes three functions: 259 | 260 | ```js 261 | rosetta.compile(sources, options); 262 | ``` 263 | ...where `sources` is an array of paths and `options` is an hashmap of options (see below). Returns `outfiles`, which will be an array of `{path, text}` objects. You can pass this directly to `rosetta.writeFiles()`. 264 | 265 | `options` are the same as those for the command-line API. 266 | 267 | ```js 268 | rosetta.writeFiles([{path, text}]); 269 | ``` 270 | `writeFiles` will actually write all of the compiled files to disk, creating directories as necessary. 271 | 272 | ```js 273 | rosetta.formatError(e) 274 | ``` 275 | Converts a Rosetta error object into human-readable error string, including a snipper of the code that generated the error. Most useful when printing errors from `rosetta.compile`. 276 | 277 | ## License 278 | 279 | Licensed under the MIT license. 280 | http://github.com/7sempra/rosetta/blob/master/LICENSE-MIT -------------------------------------------------------------------------------- /bin/rosetta: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var rosetta = require('../'), 4 | fs = require('fs'), 5 | path = require('path'), 6 | nopt = require('nopt'), 7 | glob = require('glob'), 8 | _ = require('underscore'); 9 | 10 | var knownOpts = { 11 | 'jsFormat': String, 12 | 'cssFormat': String, 13 | 'jsOut': path, 14 | 'cssOut': path, 15 | 'jsTemplate': String, 16 | 'cssTemplate': String, 17 | 'version': Boolean, 18 | 'debug': Boolean, 19 | 'help': Boolean 20 | }; 21 | 22 | var shortHands = { 23 | 'v': ['--version'], 24 | 'h': ['--help'] 25 | }; 26 | 27 | var jsFormats = {'commonjs': true, 'requirejs': true, 'flat': true}; 28 | var cssFormats = {'stylus': true, 'sass': true, 'scss': true, 'less': true}; 29 | 30 | var opts = nopt(knownOpts, shortHands, process.argv, 2); 31 | 32 | if (process.argv.length < 3 || opts.help) { 33 | printUsage(); 34 | return; 35 | } 36 | 37 | if (opts.version) { 38 | return process.stdout.write('v' + require('../package.json').version + '\n'); 39 | } 40 | 41 | var requiredOpts = ['jsOut', 'cssOut']; 42 | for (var a = 0; a < requiredOpts.length; a++) { 43 | if (!opts[requiredOpts[a]]) { 44 | die('Missing required parameter: --' + requiredOpts[a]); 45 | } 46 | } 47 | 48 | if (opts.jsFormat && !jsFormats[opts.jsFormat]) { 49 | die('Unrecognized JS format: ' + opts.jsFormat); 50 | } 51 | 52 | if (opts.cssFormat && !cssFormats[opts.cssFormat]) { 53 | die('Unrecognized CSS format: ' + opts.cssFormat); 54 | } 55 | 56 | var globs = opts.argv.remain; 57 | if (globs.length < 1) { 58 | die('You must specify at least one source file'); 59 | } 60 | 61 | var sources = {}; 62 | for (var a = 0; a < globs.length; a++) { 63 | var paths = glob.sync(globs[a]); 64 | for (var b = 0; b < paths.length; b++) { 65 | sources[paths[b]] = true; 66 | } 67 | } 68 | 69 | try { 70 | rosetta.writeFiles(rosetta.compile(_.keys(sources), opts)); 71 | } catch (e) { 72 | if (e instanceof rosetta.RosettaError) { 73 | die(rosetta.formatError(e)); 74 | } else { 75 | throw e; 76 | } 77 | } 78 | 79 | 80 | 81 | function printUsage() { 82 | fs.createReadStream(__dirname + '/usage.txt') 83 | .pipe(process.stdout) 84 | .on('close', function () { process.exit(1) }); 85 | } 86 | 87 | function die(str) { 88 | console.error(str); 89 | process.exit(1); 90 | } -------------------------------------------------------------------------------- /bin/usage.txt: -------------------------------------------------------------------------------- 1 | 2 | Usage: rosetta {OPTIONS} [files] 3 | 4 | Example: 5 | rosetta --jsout "lib/css.js" --cssout "stylus/{{ns}}.styl" rosetta/**/*.rose 6 | 7 | Options: 8 | 9 | --jsOut Write the JS module to this file. 10 | 11 | --cssOut Write the CSS to this file. If the path contains the string 12 | '{{ns}}', then a file will be created for every namespace in 13 | your .rose files, replacing the {{ns}} with the name of each 14 | of your namspaces. 15 | 16 | --jsFormat The desired output format for the JS module. Supports 17 | 'commonjs', 'requirejs', and 'flat'. Default: 'commonjs'. 18 | 19 | --cssFormat Desired output format for the CSS file(s). Should be one of 20 | 'stylus', 'sass', 'scss', or 'less'. Default: 'stylus'. 21 | 22 | --jsTemplate A custom template that defines how the Javascript should be 23 | formatted. This should be in the format of an Underscore.js 24 | template, and must specify slots for variables named 25 | 'preamble' and 'blob'. For example: 26 | $'<%= preamble %>\n var x = <%= blob %>;' 27 | (the leading $ is required if you want bash to understand \n) 28 | 29 | --cssTemplate A custom template that defines how a single CSS variable 30 | should be formatted. This should be a string in the form of 31 | an Underscore.js template, and must specify slots for 32 | variables named 'k' (the name of the variable) and 'v' (the 33 | value of the variable). For example: 34 | '$<%= k %>: <%= v %>;'' 35 | 36 | --version, -v Print the current version to stdout. 37 | 38 | --help, -h Show this message. 39 | 40 | [files] can be a list of any number of files. Glob syntax is supported, 41 | e.g. 'rosetta/**/*.rose' will resolve to all files that are contained in the 42 | 'rosetta' directory (or any of its subdirectories) and that end with '.rose'. 43 | 44 | -------------------------------------------------------------------------------- /lib/Resolver.js: -------------------------------------------------------------------------------- 1 | var classdef = require('classdef'); 2 | 3 | var errors = require('./errors'); 4 | var Token = require('./Token'); 5 | 6 | var Resolver = module.exports = classdef({ 7 | constructor: function() { 8 | this._rootNs = new Scope('root', null, []); 9 | }, 10 | 11 | printScopeTree: function() { 12 | _printScopeTree(this._rootNs, 0); 13 | }, 14 | 15 | addAst: function(ast) { 16 | this._fillScope(this._rootNs, ast, 0); 17 | }, 18 | 19 | resolve: function() { 20 | this._resolveScope(this._rootNs); 21 | return this._rootNs; 22 | }, 23 | 24 | _fillScope: function(scope, nsNode, depth) { 25 | var prefix = new Array(depth + 1).join(' '); 26 | 27 | for (var a = 0; a < nsNode.nl.length; a++) { 28 | var node = nsNode.nl[a]; 29 | if (node.type === 'namespace') { 30 | var ns = resolveNamespace(scope, node.name); 31 | this._fillScope(ns, node, depth + 1); 32 | 33 | } else if (node.type === 'assign') { 34 | var varname = node.target.val; 35 | if (scope.vars[varname]) { 36 | var otherDef = scope.vars[varname]; 37 | error(node.target, 'Variable "$' + varname + '" already defined' + 38 | ' on line ' + otherDef.node.target.line + 39 | ' of ' + otherDef.node.target.src.path); 40 | } 41 | 42 | var symdef = new SymbolDefinition(varname, node, scope); 43 | scope.vars[varname] = symdef; 44 | 45 | } else { 46 | console.log('WTF'); 47 | } 48 | } 49 | }, 50 | 51 | _resolveScope: function(scope) { 52 | //console.log('Resolving scope', scope.name); 53 | for (var vname in scope.vars) { 54 | var symdef = scope.vars[vname]; 55 | 56 | if (!symdef.value) { 57 | //console.log('Resolving', '$' + symdef.varname); 58 | symdef.resolving = true; 59 | symdef.value = this._resolveExpression(scope, symdef.node.value); 60 | symdef.resolving = false; 61 | } 62 | //console.log(getNamespacePath(symdef.scope) + '.$' + symdef.varname, '->', 63 | // symdef.value.val, symdef.value.unit, symdef.value.type); 64 | } 65 | 66 | for (var nsname in scope.namespaces) { 67 | this._resolveScope(scope.namespaces[nsname]); 68 | } 69 | }, 70 | 71 | _resolveExpression: function(scope, node) { 72 | switch (node.type) { 73 | case 'atom': 74 | switch(node.token.type) { 75 | case Token.STRING_LIT: 76 | return new StringValue(node.token.val, node.token.wrapChar); 77 | case Token.COLOR_HASH: 78 | return _parseColorHash(node.token.val); 79 | case Token.IDENT: 80 | return new SymbolValue('ident', node.token.val); 81 | case Token.VARIABLE: 82 | return this._computeVarValue(scope, node); 83 | default: 84 | compilerError(); 85 | } 86 | break; 87 | case 'binop': 88 | return computeBinOp(node.op, 89 | this._resolveExpression(scope, node.left), 90 | this._resolveExpression(scope, node.right)); 91 | case 'unop': 92 | return computeUnOp(node.op, this._resolveExpression(scope, node.right)); 93 | case 'number': 94 | return new NumberValue(parseFloat(node.value.val), node.unit && 95 | node.unit.val); 96 | case 'url': 97 | return new UrlValue(node.url.val, node.url.wrapChar); 98 | case 'rgb': 99 | case 'rgba': 100 | return _parseColorExpr(node); 101 | case 'property': 102 | return this._computeVarValue(scope, node); 103 | default: 104 | compilerError(); 105 | } 106 | }, 107 | 108 | _computeVarValue: function(scope, refNode) { 109 | var refDef; 110 | var token; 111 | 112 | if (refNode.type === 'property') { 113 | token = refNode.left; 114 | refDef = this._resolveAbsolutePath(refNode); 115 | if (!refDef) { 116 | error(refNode.left.token, 'Cannot find variable definition for ' + 117 | pathToStr(refNode)); 118 | } 119 | } else if (refNode.type === 'atom') { 120 | token = refNode.token; 121 | refDef = this._resolveVarname(scope, token.val); 122 | if (!refDef) { 123 | error(refNode.token, 'Cannot find variable definition for "$' + 124 | token.val + '"'); 125 | } 126 | } else { 127 | compilerError(); 128 | } 129 | 130 | if (refDef.value == null) { 131 | if (refDef.resolving) { 132 | error(refNode.token, 'Circular definition.'); 133 | } 134 | 135 | refDef.resolving = true; // TODO: make this refernces to the depending symboldef 136 | refDef.value = this._resolveExpression(refDef.scope, refDef.node.value); 137 | refDef.resolving = false; 138 | } 139 | 140 | return refDef.value; 141 | }, 142 | 143 | _resolveAbsolutePath: function(propNode) { 144 | var ns = this._rootNs; 145 | var n = propNode; 146 | while (n) { 147 | if (n.type === 'property') { 148 | ns = ns.namespaces[n.left.val]; 149 | if (!ns) { 150 | error(n.left, 'No namespace with that name found.'); 151 | } 152 | n = n.right; 153 | } else if (n.type ==='atom' && n.token.type === Token.VARIABLE) { 154 | return ns.vars[n.token.val]; 155 | } else { 156 | compilerError(); 157 | } 158 | } 159 | }, 160 | 161 | _resolveVarname: function(scope, varname) { 162 | while (scope) { 163 | if (scope.vars[varname]) { 164 | return scope.vars[varname] 165 | } 166 | scope = scope.parent; 167 | } 168 | return null; 169 | } 170 | }); 171 | 172 | 173 | function error(token, message) { 174 | throw new errors.CompileError('CompileError', message, token); 175 | } 176 | 177 | function compilerError(message) { 178 | throw new errors.RosettaError(message); 179 | } 180 | 181 | function resolveNamespace(rootNs, name) { 182 | var path = []; 183 | var t = name; 184 | while (t) { 185 | if (t.type === 'property') { 186 | path.push(t.left.val); 187 | t = t.right; 188 | } else if (t.type === 'atom') { 189 | path.push(t.token.val); 190 | t = null; 191 | } else { 192 | compilerError(); 193 | } 194 | } 195 | 196 | var ns = rootNs; 197 | for (var a = 0; a < path.length; a++) { 198 | var nsName = path[a]; 199 | if (!ns.namespaces[nsName]) { 200 | ns.namespaces[nsName] = new Scope( 201 | nsName, 202 | ns, 203 | path.slice(0, a + 1) 204 | ); 205 | } 206 | ns = ns.namespaces[nsName]; 207 | } 208 | 209 | return ns; 210 | } 211 | 212 | function getNamespacePath(ns) { 213 | var path = [ns.name]; 214 | while(ns = ns.parent) { 215 | path.push(ns.name); 216 | } 217 | return path.reverse().join('.'); 218 | } 219 | 220 | function _printScopeTree(scope, depth) { 221 | var prefix = new Array(depth + 1).join(' |'); 222 | for (var v in scope.vars) { 223 | console.log(prefix, '|-', v); 224 | } 225 | for (var nsv in scope.namespaces) { 226 | var ns = scope.namespaces[nsv]; 227 | console.log(prefix, ns.name + ':'); 228 | _printScopeTree(ns, depth + 1); 229 | } 230 | } 231 | 232 | function pathToStr(propNode) { 233 | var pieces = []; 234 | while (true) { 235 | pieces.push(propNode.left.val); 236 | if (propNode.right.type === 'atom') { 237 | pieces.push('$' + propNode.right.token.val); 238 | break; 239 | } else if (propNode.right.type !== 'property') { 240 | compilerError(); 241 | } 242 | propNode = propNode.right; 243 | } 244 | 245 | return pieces.join('.'); 246 | } 247 | 248 | function computeBinOp(op, left, right) { 249 | if (left.unit && right.unit && left.unit !== right.unit) { 250 | error(op, 'Unit don\'t match. Left side is "' + left.unit + 251 | '" but right side is "' + right.unit + '".'); 252 | } 253 | 254 | if (op.type !== Token.PLUS && (left.type !== 'number' || right.type !== 'number')) { 255 | error(op, 'This operation is only valid between two numbers.'); 256 | } 257 | 258 | var val, unit, type; 259 | 260 | switch (op.type) { 261 | case Token.PLUS: 262 | val = left.val + right.val; 263 | break; 264 | case Token.MINUS: 265 | val = left.val - right.val; 266 | break; 267 | case Token.MULT: 268 | val = left.val * right.val; 269 | break; 270 | case Token.DIV: 271 | val = left.val / right.val; // TODO: DETECT DIV-BY-ZERO 272 | break; 273 | } 274 | unit = left.unit || right.unit; 275 | type = left.type === 'number' && right.type === 'number' ? 'number' : 276 | 'string'; 277 | 278 | return new SymbolValue(type, val, unit); 279 | } 280 | 281 | function computeUnOp(op, right) { 282 | if (right.type !== 'number') { 283 | error(op, 'This operator only valid for numbers.'); 284 | } 285 | 286 | var val; 287 | switch(op.type) { 288 | case Token.PLUS: 289 | val = right.val; 290 | break; 291 | case Token.MINUS: 292 | val = -right.val; 293 | break; 294 | default: 295 | compilerError(); 296 | } 297 | 298 | return new NumberValue(val, right.unit); 299 | } 300 | 301 | function _parseColorHash(hash) { 302 | var val = parseInt(hash, 16); 303 | return new ColorValue( 304 | val, 305 | (val & 0xFF0000) >> 16, 306 | (val & 0x00FF00) >> 8, 307 | val & 0x0000FF, 308 | null 309 | ); 310 | } 311 | 312 | function _parseColorExpr(expr) { 313 | var r = _parseColorCell(expr.r); 314 | var g = _parseColorCell(expr.g); 315 | var b = _parseColorCell(expr.b); 316 | var a = null; 317 | if (expr.a) { 318 | a = parseFloat(expr.a.val); 319 | if (a < 0 || a > 1) { 320 | error(expr.a, 'Alpha must be a value between 0 and 1'); 321 | } 322 | } 323 | 324 | return new ColorValue((r << 16) + (g << 8) + b, r, g, b, a); 325 | } 326 | 327 | function _parseColorCell(token) { 328 | var val = parseInt(token.val, 10); 329 | if (token.val.indexOf('.') != -1 || val < 0 || val > 255) { 330 | error(token, 'Must be an integer between 0 and 255'); 331 | } 332 | return val; 333 | } 334 | 335 | function Scope(name, parent, path) { 336 | this.name = name; 337 | this.parent = parent; 338 | this.path = path; 339 | this.vars = {}; 340 | this.namespaces = {}; 341 | } 342 | 343 | function SymbolDefinition(varname, node, scope) { 344 | this.varname = varname; 345 | this.node = node; 346 | this.scope = scope; 347 | this.value = null; 348 | this.resolving = false; 349 | } 350 | 351 | function SymbolValue(type, value, unit) { 352 | this.type = type; 353 | this.val = value; 354 | } 355 | 356 | var NumberValue = classdef({ 357 | constructor: function(val, unit) { 358 | SymbolValue.call(this, 'number', val); 359 | this.unit = unit; 360 | } 361 | }); 362 | 363 | var ColorValue = classdef(SymbolValue, { 364 | constructor: function(val, r, g, b, a) { 365 | SymbolValue.call(this, 'color', val); 366 | this.r = r; 367 | this.g = g; 368 | this.b = b; 369 | this.a = a; 370 | } 371 | }); 372 | 373 | var StringValue = classdef(SymbolValue, { 374 | constructor: function(val, wrapChar) { 375 | SymbolValue.call(this, 'string', val); 376 | this.wrapChar = wrapChar; 377 | } 378 | }); 379 | 380 | var UrlValue = classdef(SymbolValue, { 381 | constructor: function(val, wrapChar) { 382 | SymbolValue.call(this, 'url', val); 383 | this.wrapChar = wrapChar; 384 | } 385 | }); -------------------------------------------------------------------------------- /lib/Token.js: -------------------------------------------------------------------------------- 1 | 2 | var Token = module.exports = {}; 3 | 4 | Token.VARIABLE = 0; 5 | Token.STRING_LIT = 1; 6 | Token.NUMBER = 2; 7 | Token.COLOR_HASH = 3; 8 | Token.IDENT = 4; 9 | 10 | Token.EQ = 5; 11 | Token.LPAREN = 6; 12 | Token.RPAREN = 7; 13 | Token.COMMA = 8; 14 | Token.PERC = 9; 15 | Token.COLON = 10; 16 | Token.PERIOD = 11; 17 | 18 | Token.PLUS = 12; 19 | Token.MINUS = 13; 20 | Token.MULT = 14; 21 | Token.DIV = 15; 22 | 23 | Token.INDENT = 16; 24 | Token.EOL = 17; 25 | Token.EOF = 18; 26 | 27 | Token.COMMENT = 19; 28 | Token.SEMI = 20; 29 | 30 | Token.INVALID = 21; 31 | 32 | // Node-only types 33 | 34 | Token.OPERATOR = 22; 35 | 36 | var _tokenNames = {}; 37 | _tokenNames[Token.VARIABLE] = 'VARIABLE'; 38 | _tokenNames[Token.STRING_LIT] = 'STRING_LIT'; 39 | _tokenNames[Token.NUMBER] = 'NUMBER'; 40 | _tokenNames[Token.COLOR_HASH] = 'COLOR_HASH'; 41 | _tokenNames[Token.IDENT] = 'IDENT'; 42 | 43 | _tokenNames[Token.EQ] = 'EQ'; 44 | _tokenNames[Token.LPAREN] = 'LPAREN'; 45 | _tokenNames[Token.RPAREN] = 'RPAREN'; 46 | _tokenNames[Token.COMMA] = 'COMMA'; 47 | _tokenNames[Token.PERC] = 'PERC'; 48 | _tokenNames[Token.COLON] = 'COLON'; 49 | _tokenNames[Token.PERIOD] = 'PERIOD'; 50 | 51 | _tokenNames[Token.PLUS] = 'PLUS'; 52 | _tokenNames[Token.MINUS] = 'MINUS'; 53 | _tokenNames[Token.MULT] = 'MULT'; 54 | _tokenNames[Token.DIV] = 'DIV'; 55 | 56 | _tokenNames[Token.INDENT] = 'INDENT'; 57 | _tokenNames[Token.EOL] = 'EOL'; 58 | _tokenNames[Token.EOF] = 'EOF'; 59 | 60 | _tokenNames[Token.COMMENT] = 'COMMENT'; 61 | _tokenNames[Token.SEMI] = 'SEMI'; 62 | _tokenNames[Token.INVALID] = 'INVALID'; 63 | 64 | Token.typeToStr = function(type) { 65 | return _tokenNames[type]; 66 | } 67 | 68 | Token.typeToCharOrDesc = function(type) { 69 | // TODO: Actually return something nice 70 | return _tokenNames[type]; 71 | } -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | 2 | var Token = require('./Token'); 3 | 4 | exports.printTokenStream = function(tokens) { 5 | return ' ' + tokens.map(function(t) { 6 | var str = Token.typeToStr(t.type) + (t.val != null ? '(' + t.val + ')' : ''); 7 | if (t.type === Token.EOL) { 8 | str += '\n'; 9 | } 10 | return str; 11 | }).join(' '); 12 | }; 13 | 14 | exports.printAst = function(root) { 15 | var out = []; 16 | printNode(root, '', out); 17 | return out.join(''); 18 | }; 19 | 20 | function printNode(node, padding, out) { 21 | if (node.type === 'namespace') { 22 | printNamespace(node, padding, out); 23 | } else { 24 | printGenericNode(node, padding, out); 25 | } 26 | } 27 | 28 | var INDENT_STEP = ' '; 29 | 30 | function printGenericNode(node, padding, out) { 31 | out.push(node.type); 32 | out.push('\n'); 33 | 34 | for (var v in node) { 35 | if (v == 'type') { 36 | continue; 37 | } 38 | 39 | out.push(padding); 40 | out.push(v); 41 | out.push(': '); 42 | 43 | var val = node[v]; 44 | if (val == null) { 45 | out.push('null\n'); 46 | } else if (typeof(val.type) === 'string') { 47 | printNode(node[v], padding + INDENT_STEP, out); 48 | } else if (typeof(val.type) === 'number') { 49 | out.push(Token.typeToStr(val.type) + (val.val != null ? '(' + val.val + ')' : '') + '\n'); 50 | } else { 51 | console.log('Warning: strange node type:', v, '->', node[v]); 52 | } 53 | } 54 | } 55 | 56 | function printNamespace(node, padding, out) { 57 | out.push(node.type); 58 | 59 | 60 | if (node.name) { 61 | out.push('\n'); 62 | out.push(padding); 63 | out.push('name: '); 64 | printNode(node.name, padding + INDENT_STEP, out); 65 | } else { 66 | out.push(' (root)'); 67 | out.push('\n'); 68 | } 69 | 70 | for (var a = 0; a < node.nl.length; a++) { 71 | out.push(padding); 72 | out.push(a); 73 | out.push(': '); 74 | printNode(node.nl[a], padding + INDENT_STEP, out); 75 | } 76 | } 77 | 78 | /* 79 | function printAssignment(node, padding, out) { 80 | out.push(node.type); 81 | out.push('\n'); 82 | 83 | out.push(padding); 84 | out.push('target: $'); 85 | out.push(node.target.val); 86 | out.push('\n'); 87 | 88 | out.push(padding); 89 | out.push('value: '); 90 | printNode(node.value, padding + INDENT_STEP); 91 | } 92 | 93 | function printBinOp(node, padding, out) { 94 | out.push(node.type); 95 | out.push(' '); 96 | out.push(Token.typeToStr(node.op)); 97 | out.push('\n'); 98 | 99 | out.push(padding); 100 | out.push('left: '); 101 | printNode(node.left, padding + INDENT_STEP); 102 | 103 | out.push(padding); 104 | out.push('right: '); 105 | printNode(node.right, padding + INDENT_STEP); 106 | } 107 | 108 | function printUnaryOp(node, padding, out) { 109 | out.push(node.type); 110 | out.push(' '); 111 | out.push(Token.typeToStr(node.op)); 112 | out.push('\n'); 113 | 114 | out.push(padding); 115 | out.push('right: '); 116 | printNode(node.right, padding + INDENT_STEP); 117 | } 118 | 119 | function printAtom(node, padding, out) { 120 | 121 | } 122 | */ -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var classdef = require('classdef'); 2 | 3 | var RosettaError = exports.RosettaError = classdef(Error, { 4 | name: 'RosettaError', 5 | 6 | constructor: function(message) { 7 | Error.call(this); 8 | Error.captureStackTrace(this, RosettaError); 9 | this.message = message; 10 | } 11 | }); 12 | 13 | var CompileError = exports.CompileError = classdef(RosettaError, { 14 | name: 'CompileError', 15 | 16 | constructor: function(type, message, token) { 17 | RosettaError.call(this, message); 18 | 19 | this.type = type; 20 | this.token = token; 21 | } 22 | }); -------------------------------------------------------------------------------- /lib/lexer.js: -------------------------------------------------------------------------------- 1 | var errors = require('./errors'); 2 | var Token = require('./Token'); 3 | 4 | // poor man's unicode support 5 | var VARIABLE = /\$[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*/g; 6 | var STRING_LITERAL_SINGLE = /'((\\.|[^\n\\'])*)(.)/g; 7 | var STRING_LITERAL_DOUBLE = /"((\\.|[^\n\\"])*)(.)/g; 8 | var NUMBER_START = /[0-9]/; 9 | var NUMBER = /\d*\.?\d+/g; 10 | var COMMENT = /([^\n]*)(\n|$)/g; 11 | var COLOR_HASH = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; 12 | var WHITESPACE = /[^\S\n]+/g; 13 | var IDENT = /[a-zA-Z][a-zA-Z0-9\-]*/g; 14 | 15 | var _simpleTokens = { 16 | '=': Token.EQ, 17 | '(': Token.LPAREN, 18 | ')': Token.RPAREN, 19 | ',': Token.COMMA, 20 | '%': Token.PERC, 21 | ':': Token.COLON, 22 | ';': Token.SEMI, 23 | '*': Token.MULT, 24 | '+': Token.PLUS, 25 | '-': Token.MINUS 26 | }; 27 | 28 | exports.lex = function(sourceFile) { 29 | var tokens = []; 30 | var prevToken = null; 31 | 32 | var a, c, c1, strlen; 33 | var lineNo = 0; 34 | var lineStart = 0; 35 | var lineStarts = [0]; 36 | 37 | var m = null; 38 | var t = null; 39 | 40 | function token(type, val) { 41 | return { 42 | type: type, 43 | val: val, 44 | pos: { 45 | line: lineNo, 46 | col: a - lineStart, 47 | file: sourceFile 48 | } 49 | } 50 | } 51 | 52 | // TODO: remove all of the 'return's in front of calls to this function 53 | function error(message) { 54 | throw new errors.CompileError('LexError', message, token(Token.INVALID)); 55 | } 56 | 57 | var str = sourceFile.text; 58 | for (a = 0, strlen = str.length; a < strlen; a++) { 59 | c = str[a]; 60 | t = null; 61 | 62 | //console.log('C:', a, c, c.charCodeAt(0)); 63 | 64 | if (_simpleTokens[c] != null) { 65 | t = token(_simpleTokens[c]); 66 | 67 | } else if (c === '$') { 68 | VARIABLE.lastIndex = a, m = VARIABLE.exec(str); 69 | if (!m) { 70 | return error('Missing variable name'); 71 | } 72 | t = token(Token.VARIABLE, m[0].substr(1)); 73 | a = VARIABLE.lastIndex - 1; 74 | 75 | } else if (c === '\'') { 76 | STRING_LITERAL_SINGLE.lastIndex = a, m = STRING_LITERAL_SINGLE.exec(str); 77 | if (!m || m[3] != '\'' || m.index !== a) { 78 | return error('Missing closing \''); 79 | } 80 | t = token(Token.STRING_LIT, m[1]); 81 | t.wrapChar = '\''; 82 | a = STRING_LITERAL_SINGLE.lastIndex - 1; 83 | 84 | } else if (c === '"') { 85 | STRING_LITERAL_DOUBLE.lastIndex = a, m = STRING_LITERAL_DOUBLE.exec(str); 86 | if (!m || m[3] !== '"' || m.index !== a) { 87 | return error('Missing closing "'); 88 | } 89 | t = token(Token.STRING_LIT, m[1]); 90 | t.wrapChar = '"'; 91 | a = STRING_LITERAL_DOUBLE.lastIndex - 1; 92 | 93 | } else if (NUMBER_START.test(c)) { 94 | NUMBER.lastIndex = a, m = NUMBER.exec(str); 95 | t = token(Token.NUMBER, m[0]); 96 | a = NUMBER.lastIndex - 1; 97 | 98 | } else if (c === '.') { 99 | if (NUMBER_START.test(str[a + 1])) { 100 | NUMBER.lastIndex = a, m = NUMBER.exec(str); 101 | t = token(Token.NUMBER, m[0]); 102 | a = NUMBER.lastIndex - 1; 103 | } else { 104 | t = token(Token.PERIOD); 105 | } 106 | 107 | } else if (c === '/') { 108 | if (str[a + 1] === '/') { 109 | COMMENT.lastIndex = a + 2, m = COMMENT.exec(str); 110 | t = token(Token.COMMENT, m[1] || ''); 111 | if (m[2] === '\n') { 112 | a = COMMENT.lastIndex - 2; 113 | } else { 114 | a = COMMENT.lastIndex - 1; 115 | } 116 | 117 | } else { 118 | t = token(Token.DIV); 119 | } 120 | 121 | } else if (c === '#') { 122 | COLOR_HASH.lastIndex = a, m = COLOR_HASH.exec(str); 123 | if (!m) { 124 | return error('Color definitions must be either 3 or 6 hexadecimal characters'); 125 | } 126 | t = token(Token.COLOR_HASH, m[1]); 127 | a = COLOR_HASH.lastIndex - 1; 128 | 129 | } else if ((WHITESPACE.lastIndex = 0, WHITESPACE.test(c))) { 130 | WHITESPACE.lastIndex = a; 131 | m = WHITESPACE.exec(str); 132 | if (prevToken && prevToken.type === Token.EOL) { 133 | t = token(Token.INDENT, m[0]); 134 | } 135 | a = WHITESPACE.lastIndex - 1; 136 | 137 | } else if (c === '\n') { 138 | if (prevToken && prevToken.type !== Token.EOL) { 139 | t = token(Token.EOL); 140 | } 141 | lineNo++; 142 | lineStart = a + 1; 143 | lineStarts.push(lineStart); 144 | 145 | } else if ((IDENT.lastIndex = 0, IDENT.test(c))) { 146 | IDENT.lastIndex = a, m = IDENT.exec(str); 147 | t = token(Token.IDENT, m[0]); 148 | a = IDENT.lastIndex - 1; 149 | 150 | } else { 151 | return error('Unexpected symbol: ' + c); 152 | } 153 | 154 | if (t) { 155 | tokens.push(t); 156 | prevToken = t; 157 | //console.log(Token.typeToStr(t.type) + (t.val != null ? '(' + t.val + ')' : '')); 158 | } 159 | } 160 | 161 | if (prevToken !== Token.EOL) { 162 | tokens.push(token(Token.EOL)); 163 | } 164 | 165 | return tokens; 166 | } -------------------------------------------------------------------------------- /lib/nodes.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | Namespace: function(parentScope, indentLen) { 4 | //console.log('+namespace'); 5 | return { 6 | type: 'namespace', 7 | parentScope: parentScope, 8 | indentLen: null, 9 | name: null, 10 | nl: [] 11 | }; 12 | }, 13 | 14 | Assignment: function(target, value) { 15 | //console.log('+assignment', target.val); 16 | return { 17 | type: 'assign', 18 | target: target, 19 | value: value 20 | }; 21 | }, 22 | 23 | BinOp: function(op, left, right) { 24 | //console.log('+binOp'); 25 | return { 26 | type: 'binop', 27 | op: op, 28 | left: left, 29 | right: right 30 | }; 31 | }, 32 | 33 | UnaryOp: function(op, right) { 34 | //console.log('+unaryOp'); 35 | return { 36 | type: 'unop', 37 | op: op, 38 | right: right 39 | }; 40 | }, 41 | 42 | Atom: function(token) { 43 | //console.log('+atom', token.type, token.val); 44 | return { 45 | type: 'atom', 46 | token: token 47 | }; 48 | }, 49 | 50 | Number: function(value, unit) { 51 | //console.log('+number', value.val, unit ? unit.val : null); 52 | return { 53 | type: 'number', 54 | value: value, 55 | unit: unit 56 | }; 57 | }, 58 | 59 | Url: function(val) { 60 | return { 61 | type: 'url', 62 | url: val 63 | }; 64 | }, 65 | 66 | Rgb: function(r, g, b) { 67 | return { 68 | type: 'rgb', 69 | r: r, 70 | g: g, 71 | b: b 72 | }; 73 | }, 74 | 75 | Rgba: function(r, g, b, a) { 76 | return { 77 | type: 'rgba', 78 | r: r, 79 | g: g, 80 | b: b, 81 | a: a 82 | }; 83 | }, 84 | 85 | Property: function(left, right) { 86 | //console.log('+property'); 87 | return { 88 | type: 'property', 89 | left: left, 90 | right: right 91 | }; 92 | } 93 | } -------------------------------------------------------------------------------- /lib/output/css.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | classdef = require('classdef'), 3 | _ = require('underscore'); 4 | 5 | var errors = require('../errors'); 6 | 7 | var _cssExtensions = { 8 | 'stylus': 'styl', 9 | 'sass': 'sass', 10 | 'scss': 'scss', 11 | 'less': 'less' 12 | }; 13 | 14 | var _cssTemplates = { 15 | 'stylus': '$<%= k %> = <%= v %>', 16 | 'sass': '$<%= k %>: <%= v %>', 17 | 'scss': '$<%= k %>: <%= v %>;', 18 | 'less': '@<%= k %>: <%= v %>;' 19 | }; 20 | 21 | exports.getFileExtension = function(compilerName) { 22 | return _cssExtensions[compilerName]; 23 | }; 24 | 25 | exports.compile = function(rootScope, options) { 26 | var outPath = options.cssOut; 27 | var splitFiles = outPath.indexOf('{{ns}}') != -1; 28 | 29 | var out = []; 30 | var adapter = new BasicAdapter(options.cssTemplate || 31 | _cssTemplates[options.cssFormat]); 32 | 33 | _dumpScope(rootScope); 34 | 35 | function _dumpScope(scope) { 36 | var scopeOut = out; 37 | if (splitFiles) { 38 | scopeOut = []; 39 | } 40 | 41 | for (var v in scope.namespaces) { 42 | _dumpScope(scope.namespaces[v]); 43 | } 44 | 45 | if (!splitFiles) { 46 | scopeOut.push('\n'); 47 | scopeOut.push('// '); 48 | scopeOut.push(scope.parent == null ? 'root' : scope.path.join('.')); 49 | scopeOut.push('\n'); 50 | } 51 | 52 | for (v in scope.vars) { 53 | adapter.write(scope.vars[v], scopeOut); 54 | } 55 | 56 | if (splitFiles) { 57 | var filename = scope.parent ? scope.path.join(path.sep) : 'rosetta'; 58 | out.push({ 59 | path: outPath.replace('{{ns}}', filename), 60 | text: scopeOut.join('') 61 | }); 62 | } 63 | } 64 | 65 | return splitFiles ? out : [{ 66 | path: outPath, 67 | text: out.join('') 68 | }]; 69 | }; 70 | 71 | var BasicAdapter = classdef({ 72 | constructor: function(template) { 73 | this._t = _.template(template); 74 | }, 75 | 76 | write: function(symdef, out) { 77 | var formatter = BasicAdapter._formatters[symdef.value.type]; 78 | if (!formatter) { 79 | throw new errors.RosettaError('Unsupported output type: ' + 80 | symdef.value.type); 81 | } 82 | 83 | var formattedValue = formatter(symdef.value); 84 | 85 | out.push(this._t({ 86 | k: symdef.varname, 87 | v: formattedValue 88 | })); 89 | out.push('\n'); 90 | 91 | if (symdef.scope.parent) { // true if not root 92 | out.push(this._t({ 93 | k: symdef.scope.path.join('-') + '-' + symdef.varname, 94 | v: formattedValue 95 | })); 96 | out.push('\n'); 97 | } 98 | }, 99 | 100 | _writeNumber: function(number) { 101 | return number.unit ? number.val.toString() + number.unit : number.val.toString(); 102 | }, 103 | 104 | _writeColor: function(color) { 105 | if (color.a !== null) { 106 | return 'rgba(' + [color.r, color.g, color.b, color.a].join(',') + ')'; 107 | } else { 108 | return '#' + ('000000' + color.val.toString(16)).slice(-6); 109 | } 110 | }, 111 | 112 | _writeString: function(string) { 113 | return string.wrapChar + string.val + string.wrapChar; 114 | }, 115 | 116 | _writeUrl: function(url) { 117 | return 'url(' + url.wrapChar + url.val + url.wrapChar + ')'; 118 | }, 119 | 120 | _writeIdent: function(ident) { 121 | return ident.val; 122 | } 123 | }); 124 | 125 | BasicAdapter._formatters = { 126 | 'number': BasicAdapter.prototype._writeNumber, 127 | 'color': BasicAdapter.prototype._writeColor, 128 | 'string': BasicAdapter.prototype._writeString, 129 | 'url': BasicAdapter.prototype._writeUrl, 130 | 'ident': BasicAdapter.prototype._writeIdent 131 | }; -------------------------------------------------------------------------------- /lib/output/js.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | _ = require('underscore'), 4 | classdef = require('classdef'); 5 | 6 | var errors = require('../errors'); 7 | 8 | var _templates = { 9 | 'commonjs': __dirname + '/jstemplates/commonjs.txt', 10 | 'requirejs': __dirname + '/jstemplates/requirejs.txt', 11 | 'flat': __dirname + '/jstemplates/flat.txt' 12 | }; 13 | 14 | function _error(message) { 15 | throw new errors.RosettaError(message); 16 | } 17 | 18 | module.exports.compile = function(root, options) { 19 | 20 | var preamble = fs.readFileSync( 21 | path.join(__dirname, 'jstemplates', 'preamble.js'), 'utf8'); 22 | 23 | var template = options.jsTemplate; 24 | if (!template) { 25 | var templatePath = _templates[options.jsFormat] || 26 | _error('Unrecognized JS output format: ' + options.jsFormat); 27 | template = fs.readFileSync(templatePath, 'utf8'); 28 | } 29 | 30 | return _compile(root, options, preamble, template); 31 | }; 32 | 33 | function _compile(root, options, preamble, templateStr) { 34 | var formatter = new JsFormatter(); 35 | 36 | var blob = ['{\n', _dumpScope(root, 1), '\n}'].join(''); 37 | 38 | function _dumpScope(scope, depth) { 39 | var indent = (new Array(depth + 1).join(' ')); 40 | 41 | var entries = []; 42 | var v; 43 | 44 | for (v in scope.vars) { 45 | var symdef = scope.vars[v]; 46 | entries.push(indent + formatter.formatVarname(symdef.varname) + ': ' + 47 | formatter.formatValue(symdef.value)); 48 | } 49 | 50 | for (v in scope.namespaces) { 51 | var childScope = scope.namespaces[v]; 52 | entries.push(indent + formatter.formatVarname(childScope.name) + ': {\n' + 53 | _dumpScope(childScope, depth + 1) + '\n' + indent +'}'); 54 | } 55 | 56 | return entries.join(',\n'); 57 | } 58 | 59 | var t = _.template(templateStr); 60 | 61 | return { 62 | path: options.jsOut, 63 | text: t({ 64 | preamble: preamble, 65 | blob: blob 66 | }) 67 | }; 68 | } 69 | 70 | var JsFormatter = classdef({ 71 | 72 | formatVarname: function(value) { 73 | var p, pieces = value.split('-'); 74 | for (var a = 1; a < pieces.length; a++) { 75 | p = pieces[a]; 76 | pieces[a] = p[0].toUpperCase() + p.substr(1); 77 | } 78 | return pieces.join(''); 79 | }, 80 | 81 | formatValue: function(value) { 82 | var formatter = JsFormatter._formatters[value.type]; 83 | if (!formatter) { 84 | throw new errors.RosettaError('Unsupported output type: ' + value.type); 85 | } 86 | return formatter(value); 87 | }, 88 | 89 | _writeNumber: function(number) { 90 | return number.unit ? 91 | 'num(' + number.val + ',\'' + number.unit + '\')' : 92 | 'num(' + number.val + ')'; 93 | }, 94 | 95 | _writeColor: function(color) { 96 | return ['color(', color.val, ',', color.r, ',', color.g, ',', color.b, 97 | color.a ? ',' + color.a : '', ')'].join(''); 98 | }, 99 | 100 | _writeString: function(string) { 101 | return ['string(', string.wrapChar, string.val, string.wrapChar, 102 | ')'].join(''); 103 | }, 104 | 105 | _writeUrl: function(url) { 106 | return ['url(', url.wrapChar, url.val, url.wrapChar, ')'].join(''); 107 | }, 108 | 109 | _writeIdent: function(ident) { 110 | return ['css(', '\'', ident.val, '\'', ')'].join(''); 111 | } 112 | }); 113 | 114 | JsFormatter._formatters = { 115 | 'number': JsFormatter.prototype._writeNumber, 116 | 'color': JsFormatter.prototype._writeColor, 117 | 'string': JsFormatter.prototype._writeString, 118 | 'url': JsFormatter.prototype._writeUrl, 119 | 'ident': JsFormatter.prototype._writeIdent 120 | }; -------------------------------------------------------------------------------- /lib/output/jstemplates/commonjs.txt: -------------------------------------------------------------------------------- 1 | module.exports = <%= blob %>; 2 | 3 | <%= preamble %> -------------------------------------------------------------------------------- /lib/output/jstemplates/flat.txt: -------------------------------------------------------------------------------- 1 | var rosetta; 2 | (function() { 3 | <%= preamble %> 4 | 5 | rosetta = <%= blob %>; 6 | })(); -------------------------------------------------------------------------------- /lib/output/jstemplates/preamble.js: -------------------------------------------------------------------------------- 1 | function num(val, unit) { 2 | return {type: 'number', val: val, unit: unit}; 3 | } 4 | 5 | function string(val) { 6 | return {type: 'string', val: val}; 7 | } 8 | 9 | function color(val, r, g, b, a) { 10 | return {type: 'number', val: val, r: r, g: g, b: b, a: a || 1}; 11 | } 12 | 13 | function url(val) { 14 | return {type: 'url', val: val}; 15 | } 16 | 17 | function css(val) { 18 | return {type: 'css', val: val}; 19 | } -------------------------------------------------------------------------------- /lib/output/jstemplates/requirejs.txt: -------------------------------------------------------------------------------- 1 | define(function() { 2 | <%= preamble %> 3 | 4 | return <%= blob %>; 5 | }); -------------------------------------------------------------------------------- /lib/parser.js: -------------------------------------------------------------------------------- 1 | var Token = require('./Token'); 2 | var nodes = require('./nodes'); 3 | var errors = require('./errors'); 4 | 5 | exports.parse = function(tokens, options) { 6 | var p = new Parser(tokens, options); 7 | return p.parse(); 8 | } 9 | 10 | function error(token, msg) { 11 | throw new errors.CompilerError('ParseError', msg, token); 12 | } 13 | 14 | function compilerError(msg) { 15 | throw new errors.RosettaError(msg); 16 | } 17 | 18 | var Parser = function(tokens, options) { 19 | this.tokens = tokens; 20 | this.a = 0; 21 | this.t = tokens[this.a]; 22 | this.root = null; 23 | this.scope = null; 24 | } 25 | 26 | Parser.prototype = { 27 | constructor: Parser, 28 | 29 | parse: function() { 30 | return this.namespace(true); 31 | }, 32 | 33 | peek: function() { 34 | return this.t; 35 | }, 36 | 37 | lookahead: function(n) { 38 | return this.tokens[this.a + n]; 39 | }, 40 | 41 | goto: function(a) { 42 | this.a = a; 43 | this.t = this.tokens[a]; 44 | }, 45 | 46 | next: function() { 47 | this.a++; 48 | return this.t = this.tokens[this.a]; 49 | }, 50 | 51 | expect: function(type) { 52 | var t = this.t; 53 | if (t.type !== type) { 54 | error(t, 'Unexpected token "' + Token.typeToCharOrDesc(t.type) + 55 | '". Was expecting "' + Token.typeToCharOrDesc(type) + '"'); 56 | } 57 | this.next(); 58 | return t; 59 | }, 60 | 61 | accept: function(type) { 62 | if (this.t.type === type) { 63 | var t = this.t; 64 | this.next(); 65 | return t; 66 | } 67 | return null; 68 | }, 69 | 70 | namespace: function(isRoot) { 71 | var ns = nodes.Namespace(this.scope); 72 | this.scope = ns; 73 | 74 | if (isRoot) { 75 | ns.indentLen = 0; 76 | this.root = ns; 77 | } else { 78 | ns.name = this.namespaceName(); 79 | this.accept(Token.COMMENT); 80 | this.expect(Token.EOL); 81 | } 82 | 83 | var n, t; 84 | var currentIndent = 0; 85 | var statementStart = this.a; 86 | while(this.t) { 87 | if (t = this.accept(Token.INDENT)) { 88 | currentIndent = t.val.length; 89 | } else if (t = this.accept(Token.EOL)) { 90 | currentIndent = 0; 91 | statementStart = this.a; 92 | } else if (t = this.accept(Token.COMMENT)) { 93 | // ignore 94 | } else { 95 | if (ns.parentScope && currentIndent <= ns.parentScope.indentLen) { 96 | this.goto(statementStart); 97 | break; 98 | } 99 | 100 | if (ns.indentLen == null) { 101 | ns.indentLen = currentIndent; 102 | } else if (currentIndent !== ns.indentLen) { 103 | error(t, 'Inconsistent indentation. Expected ' + ns.indentLen + 104 | ' whitespace chars but found ' + currentIndent + '.'); 105 | } 106 | 107 | if (this.t.type === Token.IDENT) { 108 | n = this.namespace(); 109 | } else { 110 | n = this.assignment(); 111 | this.accept(Token.SEMI); 112 | this.accept(Token.COMMENT); 113 | this.expect(Token.EOL); 114 | } 115 | currentIndent = 0; 116 | statementStart = this.a; 117 | ns.nl.push(n); 118 | } 119 | } 120 | 121 | this.scope = ns.parentScope; 122 | return ns; 123 | }, 124 | 125 | assignment: function() { 126 | var varName = this.expect(Token.VARIABLE); 127 | this.expect(Token.EQ); 128 | return nodes.Assignment(varName, this.expression()); 129 | }, 130 | 131 | expression: function() { 132 | var n = this.additive(); 133 | return n; 134 | }, 135 | 136 | additive: function() { 137 | var n, t; 138 | 139 | n = this.multiplicative(); 140 | while (t = (this.accept(Token.PLUS) || this.accept(Token.MINUS))) { 141 | n = nodes.BinOp(t, n, this.multiplicative()); 142 | } 143 | return n; 144 | }, 145 | 146 | multiplicative: function() { 147 | var n, t; 148 | 149 | n = this.unary(); 150 | while (t = (this.accept(Token.MULT) || this.accept(Token.DIV))) { 151 | n = nodes.BinOp(t, n, this.unary()); 152 | } 153 | return n; 154 | }, 155 | 156 | unary: function() { 157 | var n, t; 158 | t = this.peek(); 159 | if (t.type === Token.PLUS || t.type === Token.MINUS) { 160 | this.next(); 161 | n = nodes.UnaryOp(t, this.atom()); 162 | } else { 163 | n = this.atom(); 164 | } 165 | 166 | return n; 167 | }, 168 | 169 | atom: function() { 170 | var n, t; 171 | 172 | if (this.accept(Token.LPAREN)) { 173 | n = this.expression(); 174 | this.expect(Token.RPAREN); 175 | } else { 176 | t = this.peek(); 177 | switch(t.type) { 178 | case Token.STRING_LIT: 179 | case Token.COLOR_HASH: 180 | case Token.VARIABLE: 181 | this.next(); 182 | n = nodes.Atom(t); 183 | break; 184 | case Token.NUMBER: 185 | this.next(); 186 | n = nodes.Number(t, this.accept(Token.IDENT) || this.accept(Token.PERC)); 187 | break; 188 | case Token.IDENT: 189 | n = this.ident(); 190 | break; 191 | default: 192 | error(t, 'Unexpected token: "' + Token.typeToCharOrDesc(t) + '"'); 193 | break; 194 | } 195 | } 196 | 197 | return n; 198 | }, 199 | 200 | ident: function() { 201 | var n, t, t1; 202 | 203 | t = this.peek(); 204 | if (t.val === 'url') { 205 | n = this.url(); 206 | } else if (t.val === 'rgb') { 207 | n = this.rgb(); 208 | } else if (t.val === 'rgba') { 209 | n = this.rgba(); 210 | } else { 211 | if ((t1 = this.lookahead(1), t1.type === Token.PERIOD)) { 212 | n = this.varRef(); 213 | } else { 214 | this.next(); 215 | n = nodes.Atom(t); 216 | } 217 | } 218 | 219 | return n; 220 | }, 221 | 222 | url: function() { 223 | this.next(); 224 | this.expect(Token.LPAREN); 225 | var n = nodes.Url(this.expect(Token.STRING_LIT)); 226 | this.expect(Token.RPAREN); 227 | 228 | return n; 229 | }, 230 | 231 | rgb: function() { 232 | var r, g, b; 233 | 234 | this.next(); 235 | this.expect(Token.LPAREN); 236 | r = this.expect(Token.NUMBER); 237 | this.expect(Token.COMMA); 238 | g = this.expect(Token.NUMBER); 239 | this.expect(Token.COMMA); 240 | b = this.expect(Token.NUMBER); 241 | this.expect(Token.RPAREN); 242 | 243 | return nodes.Rgb(r, g, b); 244 | }, 245 | 246 | rgba: function() { 247 | var r, g, b, a; 248 | 249 | this.next(); 250 | this.expect(Token.LPAREN); 251 | r = this.expect(Token.NUMBER); 252 | this.expect(Token.COMMA); 253 | g = this.expect(Token.NUMBER); 254 | this.expect(Token.COMMA); 255 | b = this.expect(Token.NUMBER); 256 | this.expect(Token.COMMA); 257 | a = this.expect(Token.NUMBER); 258 | this.expect(Token.RPAREN); 259 | 260 | return nodes.Rgba(r, g, b, a); 261 | }, 262 | 263 | varRef: function() { 264 | var n, t; 265 | 266 | if (t = this.accept(Token.VARIABLE)) { 267 | n = nodes.Atom(t); 268 | } else if (t = this.accept(Token.IDENT)) { 269 | this.expect(Token.PERIOD); 270 | n = nodes.Property(t, this.varRef()); 271 | } else { 272 | error(t, 'Unexpected token: "' + Token.typeToCharOrDesc(t) + '"'); 273 | } 274 | return n; 275 | }, 276 | 277 | namespaceName: function() { 278 | var n, t; 279 | t = this.expect(Token.IDENT); 280 | if (this.accept(Token.PERIOD)) { 281 | n = nodes.Property(t, this.namespaceName()); 282 | } else { 283 | n = nodes.Atom(t); 284 | this.expect(Token.COLON); 285 | } 286 | return n; 287 | } 288 | } -------------------------------------------------------------------------------- /lib/rosetta.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('underscore'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | classdef = require('classdef'), 6 | mkdirp = require('mkdirp'); 7 | 8 | 9 | var debug = require('./debug'), 10 | errors = require('./errors'), 11 | lexer = require('./lexer'), 12 | parser = require('./parser'), 13 | util = require('./util'), 14 | Resolver = require('./Resolver'); 15 | 16 | var css = require('./output/css'), 17 | js = require('./output/js'); 18 | 19 | /** 20 | * Options: 21 | * jsFormat Name of JS format. Accepts {'commonjs', 'requirejs', 'flat'}. 22 | * Default: 'commonjs'. You can also define your own format; see 23 | * 'jsTemplate'. 24 | * cssFormat Accepts {'stylus', 'sass', 'scss', 'less'}. Default: 'stylus'. 25 | * You can also define your own format; see 'cssTemplate'. 26 | * jsTemplate An underscore-formatted template string to be used when 27 | * generating the compiled JS. Must contain slots for variables 28 | * named 'preamble' and 'blob'. See lib/output/jstemplates for 29 | * an example. 30 | * cssTemplate An underscore-formatted template string that defines how a 31 | * single variable should be defined. For example, the stylus 32 | * template is '$<%= k %> = <%= v %>'. Must contain slots for 33 | * variables named 'k' (name of the variable) and 'v' (value of the 34 | * variable). 35 | * debug If true, print tons of stuff to the console. 36 | */ 37 | exports.compile = function(sources, options, cb) { 38 | if (cb) { 39 | console.error('The JS API has changed and no longer uses callbacks. ' + 40 | 'Please see the updated docks.'); 41 | return; 42 | } 43 | 44 | _.defaults(options, { 45 | jsFormat: 'commonjs', 46 | cssFormat: 'stylus', 47 | jsOut: 'rosetta.js', 48 | cssOut: 'rosetta.' + css.getFileExtension(options.cssFormat || 'stylus') || 49 | 'css' 50 | }); 51 | 52 | var resolver = new Resolver(); 53 | 54 | for (var a = 0; a < sources.length; a++) { 55 | var ast = parseFile(path.resolve(sources[a]), options); 56 | resolver.addAst(ast); 57 | } 58 | 59 | if (options.debug) { 60 | console.log('\nParse complete. Scope tree:') 61 | resolver.printScopeTree(); 62 | } 63 | var resolvedScope = resolver.resolve(); 64 | 65 | var outfiles = css.compile(resolvedScope, options); 66 | if (options.debug) { 67 | outfiles.forEach(function(outFile) { 68 | console.log(util.formatHeading(outFile.path + ':')); 69 | console.log(outFile.text); 70 | }); 71 | } 72 | 73 | var jsFile = js.compile(resolvedScope, options); 74 | if (options.debug) { 75 | _printHeading(result.path + ':'); 76 | console.log(result.text); 77 | } 78 | outfiles.push(jsFile); 79 | 80 | return outfiles; 81 | } 82 | 83 | exports.writeFiles = function(outfiles) { 84 | for (var a = 0; a < outfiles.length; a++) { 85 | var outfile = outfiles[a]; 86 | mkdirp.sync(path.dirname(outfile.path)) 87 | fs.writeFileSync(outfile.path, outfile.text, 'utf8'); 88 | } 89 | } 90 | 91 | exports.formatError = util.formatError; 92 | exports.RosettaError = errors.RosettaError; 93 | 94 | function parseFile(path, options) { 95 | var fileStr = fs.readFileSync(path, 'utf8'); 96 | var file = new SourceFile(path, fileStr); 97 | 98 | options.debug && console.log('Parsing', path); 99 | var tokens = lexer.lex(file); 100 | options.debug && console.log(debug.printTokenStream(tokens)); 101 | var ast = parser.parse(tokens); 102 | options.debug && console.log(debug.printAst(ast)); 103 | return ast; 104 | } 105 | 106 | function _getRepeatedChar(len, char) { 107 | char = char || ' '; 108 | return new Array(len + 1).join(char); 109 | } 110 | 111 | function _printHeading(heading, maxWidth) { 112 | maxWidth = maxWidth || 79; 113 | 114 | if (heading.length > maxWidth) { 115 | heading = '...' + heading.substr(heading.length - (maxWidth - 3)); 116 | } 117 | console.log('\n' + heading); 118 | console.log(_getRepeatedChar(heading.length, '-')); 119 | } 120 | 121 | var SourceFile = classdef({ 122 | constructor: function(path, text) { 123 | this.path = path; 124 | this.text = text; 125 | } 126 | }); -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var errors = require('./errors'); 2 | 3 | exports.formatError = function(e) { 4 | if (e instanceof errors.CompileError) { 5 | return _formatCompileError(e); 6 | } else if (e instanceof errors.RosettaError) { 7 | return e.message; 8 | } else { 9 | return e.toString(); 10 | } 11 | } 12 | 13 | exports.formatHeading = function(message, maxLen) { 14 | maxLen = maxLen || 80; 15 | message = _ellipsize(message, maxLen); 16 | 17 | return message + '\n' + _getRepeatedChar(message.length, '-') + '\n'; 18 | } 19 | 20 | function _formatCompileError(e) { 21 | var msg = ''; 22 | 23 | var token = e.token; 24 | var pos = token.pos; 25 | 26 | msg += e.type + ': ' + e.message; 27 | msg += '\n\n'; 28 | 29 | msg += exports.formatHeading(pos.file.path + ':', 79); 30 | 31 | var linePreamble = pos.line + '| '; 32 | msg += linePreamble; 33 | msg += _getLine(pos.line, pos.file.text); 34 | msg += '\n'; 35 | 36 | msg += _getRepeatedChar(linePreamble.length + pos.col); 37 | msg += '^\n'; 38 | 39 | return msg; 40 | } 41 | 42 | function _getLine(lineNum, source) { 43 | var start = 0; 44 | 45 | for (var a = 0; a < lineNum; a++) { 46 | start = source.indexOf('\n', start) + 1; 47 | if (start == -1) { 48 | return ''; 49 | } 50 | } 51 | 52 | var stop = source.indexOf('\n', start); 53 | if (stop == -1) { 54 | stop = source.length; 55 | } 56 | 57 | return source.substring(start, stop); 58 | } 59 | 60 | function _getRepeatedChar(len, char) { 61 | char = char || ' '; 62 | return new Array(len + 1).join(char); 63 | } 64 | 65 | function _ellipsize(line, maxWidth) { 66 | if (line.length <= maxWidth) { 67 | return line; 68 | } else { 69 | return '...' + line.substr(3 - maxWidth); 70 | } 71 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rosetta", 3 | "version": "0.3.0", 4 | "description": "Shared variables between CSS and Javascript.", 5 | "author": { 6 | "name": "Ned Burns", 7 | "email": "net7runner@gmail.com" 8 | }, 9 | "main": "lib/rosetta.js", 10 | "bin": { 11 | "rosetta": "./bin/rosetta" 12 | }, 13 | "homepage": "https://github.com/7sempra/rosetta", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/7sempra/rosetta.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/7sempra/rosetta/issues" 20 | }, 21 | "licenses": [ 22 | { 23 | "type": "MIT", 24 | "url": "http://github.com/7sempra/rosetta/blob/master/LICENSE-MIT" 25 | } 26 | ], 27 | "dependencies": { 28 | "classdef": "~1.0.1", 29 | "nopt": "~2.1.1", 30 | "glob": "~3.1.21", 31 | "underscore": "~1.4.4", 32 | "mkdirp": "~0.3.5" 33 | }, 34 | "devDependencies": { 35 | "grunt": "~0.4.0", 36 | "grunt-contrib-jshint": "~0.2.0", 37 | "grunt-contrib-nodeunit": "~0.1.2", 38 | "grunt-contrib-watch": "~0.3.1", 39 | "grunt-contrib-clean": "~0.4.0" 40 | }, 41 | "keywords": [ 42 | "css", 43 | "javascript", 44 | "shared", 45 | "variables", 46 | "stylus", 47 | "sass", 48 | "less", 49 | "gruntplugin" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /tasks/rosetta.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | var rosetta = require('../lib/rosetta'); 6 | 7 | module.exports = function(grunt) { 8 | 9 | grunt.registerMultiTask('rosetta', 'Shared variables between JS and CSS.', function() { 10 | 11 | var sources = Array.prototype.concat.apply([], 12 | grunt.util._.pluck(this.files, 'src')); 13 | 14 | var options = this.options(); 15 | 16 | try { 17 | var outfiles = rosetta.compile(sources, options); 18 | rosetta.writeFiles(outfiles); 19 | var cwd = process.cwd(); 20 | outfiles.forEach(function(ouf) { 21 | grunt.log.ok('File ' + path.relative(cwd, ouf.path) + ' written'); 22 | }); 23 | } catch (e) { 24 | if (e instanceof rosetta.RosettaError) { 25 | grint.fatal(rosetta.formatError(e)); 26 | } else { 27 | throw e; 28 | } 29 | } 30 | 31 | }); 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /test/basic.rose: -------------------------------------------------------------------------------- 1 | $bareInt = 34 2 | $bareFloat = 0.234 3 | $pxNum = 47px 4 | $percNum = 47% 5 | $colorHash = #123abc 6 | $colorRgb = rgb(234, 123, 231) 7 | $colorRgba = rgba(44, 22, 77, 0.23); 8 | $str = 'foo' 9 | $url = url('google.com'); 10 | $ident = no-wrap 11 | 12 | ns1: 13 | $foo = 47 14 | $bar = $foo 15 | $baz = $bareInt 16 | 17 | ns2: 18 | $foo = 48 19 | $bar = ns1.$foo 20 | 21 | nested: 22 | $foo = 49 23 | $bar = ns2.nested.$foo 24 | 25 | nested2: 26 | $foo = $bar -------------------------------------------------------------------------------- /test/circular_deps.rose: -------------------------------------------------------------------------------- 1 | $foo = $bar 2 | $bar = $baz 3 | $baz = $quux 4 | $quux = $foo --------------------------------------------------------------------------------