├── .gitignore ├── scss ├── deeper │ └── _some.scss ├── _importable.scss ├── example.scss └── demo.scss ├── grunt-tasks ├── mocha-test.js ├── clean.js ├── mkdir.js ├── jshint.js ├── shell.js ├── file-size.js ├── concat.js └── versions.js ├── .editorconfig ├── dist ├── versions.json ├── file-size.json └── sass.js ├── libsass ├── Makefile.conf.patch ├── prepare.sh ├── emscripten_wrapper.hpp ├── build.sh ├── Makefile.patch ├── empterpreter_whitelist.json └── emscripten_wrapper.cpp ├── CONTRIBUTING.md ├── .jshintrc ├── maps ├── drublic-sass-mixins.js └── bourbon.js ├── test ├── test.comments.js ├── test.import.js ├── test.compile.js ├── test.filesystem.js └── test.importer.js ├── bower.json ├── bin └── benchmark.js ├── LICENSE ├── src ├── sass.util.js ├── sass.configure.path.js ├── sass.worker.js ├── sass.importer.js ├── sass.options.js ├── sass.js └── sass.api.js ├── package.json ├── sass.sync.html ├── sass.source.html ├── Gruntfile.js ├── sass.worker.html ├── CHANGELOG.md └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | libsass/libsass 4 | -------------------------------------------------------------------------------- /scss/deeper/_some.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | font-family: monospace; 3 | } -------------------------------------------------------------------------------- /scss/_importable.scss: -------------------------------------------------------------------------------- 1 | @import "deeper/some"; 2 | 3 | .green { 4 | background-color: lime; 5 | } -------------------------------------------------------------------------------- /grunt-tasks/mocha-test.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileMochaTest(grunt) { 2 | 'use strict'; 3 | 4 | grunt.config('mochaTest', { 5 | src: ['test/**/test.*.js'] 6 | }); 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /grunt-tasks/clean.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileClean(grunt) { 2 | 'use strict'; 3 | 4 | grunt.config('clean', { 5 | libsass: ['libsass/libsass'], 6 | dist: ['dist'] 7 | }); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /grunt-tasks/mkdir.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileMkdir(grunt) { 2 | 'use strict'; 3 | 4 | grunt.config('mkdir', { 5 | dist: { 6 | options: { 7 | create: ['dist'] 8 | } 9 | } 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /dist/versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "emscripten": { 3 | "version": "1.36.1", 4 | "commit": "d5085ed" 5 | }, 6 | "libsass": { 7 | "version": "3.3.6", 8 | "commit": "3ae9a20" 9 | }, 10 | "sassjs": { 11 | "version": "0.9.10", 12 | "commit": "9a781bf", 13 | "branch": "master" 14 | } 15 | } -------------------------------------------------------------------------------- /dist/file-size.json: -------------------------------------------------------------------------------- 1 | { 2 | "dist/sass.js": { 3 | "normal": "5 KB", 4 | "compressed": "2 KB" 5 | }, 6 | "dist/sass.sync.js": { 7 | "normal": "2682 KB", 8 | "compressed": "571 KB" 9 | }, 10 | "dist/sass.worker.js": { 11 | "normal": "2682 KB", 12 | "compressed": "571 KB" 13 | } 14 | } -------------------------------------------------------------------------------- /libsass/Makefile.conf.patch: -------------------------------------------------------------------------------- 1 | --- Makefile.conf.orig 2015-09-09 14:47:04.000000000 +0300 2 | +++ Makefile.conf 2015-09-09 14:47:23.000000000 +0300 3 | @@ -42,6 +42,7 @@ 4 | units.cpp \ 5 | utf8_string.cpp \ 6 | values.cpp \ 7 | - util.cpp 8 | + util.cpp \ 9 | + emscripten_wrapper.cpp 10 | 11 | CSOURCES = cencode.c 12 | -------------------------------------------------------------------------------- /scss/example.scss: -------------------------------------------------------------------------------- 1 | @import "importable.scss"; 2 | 3 | @mixin border-radius($radius) { 4 | -webkit-border-radius: $radius; 5 | -moz-border-radius: $radius; 6 | -ms-border-radius: $radius; 7 | -o-border-radius: $radius; 8 | border-radius: $radius; 9 | } 10 | 11 | .box { 12 | @extend .green; 13 | @include border-radius(10px); 14 | border: 20px solid lime; 15 | } -------------------------------------------------------------------------------- /grunt-tasks/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileShell(grunt) { 2 | 'use strict'; 3 | 4 | var jshintOptions = grunt.file.readJSON('.jshintrc'); 5 | jshintOptions.reporter = require('jshint-stylish'); 6 | 7 | grunt.config('jshint', { 8 | options: jshintOptions, 9 | target: [ 10 | 'Gruntfile.js', 11 | 'grunt-tasks/**/*.js', 12 | 'src/**/*.js', 13 | 'test/**/*.js' 14 | ] 15 | }); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /grunt-tasks/shell.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileShell(grunt) { 2 | 'use strict'; 3 | 4 | grunt.config('shell', { 5 | prepareLibsass: { 6 | command: '(cd libsass && /bin/bash ./prepare.sh "<%= pkg.libsass %>")', 7 | }, 8 | buildLibsass: { 9 | command: '(cd libsass && /bin/bash ./build.sh "<%= pkg.libsass %>")', 10 | }, 11 | buildLibsassDebug: { 12 | command: '(cd libsass && /bin/bash ./build.sh "<%= pkg.libsass %>" debug)', 13 | }, 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | Sass.js is a convenience API for libsass.js which is emscripted from [libsass](https://github.com/sass/libsass/). That means that the issues pertaining to parsing SCSS are *not* scope of Sass.js. They need to be reported in the [libsass](https://github.com/sass/libsass/) repository. 4 | 5 | In other words, if you can reproduce your issue with either [node-sass](https://github.com/sass/node-sass) or [sassc](https://github.com/sass/sassc/) please report your problems to the [libsass](https://github.com/sass/libsass/) repository. 6 | 7 | -------------------------------------------------------------------------------- /libsass/prepare.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | # USAGE: 6 | # prepare.sh 7 | 8 | # accept parameter, default to master 9 | LIBSASS_VERSION=${1:-"master"} 10 | echo "Preparing libsass version ${LIBSASS_VERSION}" 11 | 12 | # clean 13 | echo " cleaning target directory" 14 | rm -rf ./libsass 15 | 16 | # download 17 | echo " downloading repository to target directory" 18 | git clone https://github.com/sass/libsass.git libsass 19 | echo " checking out branch/tag and initializing submodules" 20 | (cd libsass && git checkout ${LIBSASS_VERSION} && git submodule init && git submodule update) 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": false, 5 | "bitwise": false, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "expr": true, 22 | "globals": { 23 | "describe": false, 24 | "it": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "after": false, 28 | "afterEach": false, 29 | "define": false 30 | } 31 | } -------------------------------------------------------------------------------- /maps/drublic-sass-mixins.js: -------------------------------------------------------------------------------- 1 | (function(Sass) { 2 | 'use strict'; 3 | 4 | // make sure the namespace is available 5 | !Sass.maps && (Sass.maps = {}); 6 | 7 | // files map for drublic's Sass-Mixins v0.11.0 - https://github.com/drublic/Sass-Mixins 8 | Sass.maps['drublic-sass-mixins'] = { 9 | // make the source file available in "drublic/mixins.scss" 10 | directory: 'drublic', 11 | // https://github.com/drublic/Sass-Mixins/blob/v0.11.0/ 12 | // using rawgit to directly access the github repository via CORS 13 | // NOTE: that this will only work for preloading, as lazyloading throws security exceptions 14 | base: 'https://cdn.rawgit.com/drublic/Sass-Mixins/v0.11.0/', 15 | files: [ 16 | 'mixins.scss', 17 | ] 18 | }; 19 | 20 | })(Sass); -------------------------------------------------------------------------------- /test/test.comments.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Sass = require('../dist/sass.sync.js'); 5 | 6 | describe('option.comments', function() { 7 | 8 | it('should add line comments', function(done) { 9 | var source = '@import "testfile";\n\n$foo:123px;\n\n.m {\n width:$foo;\n}'; 10 | var expected = '/* line 1, /sass/testfile.scss */\n.imported {\n content: "testfile"; }\n\n/* line 5, stdin */\n.m {\n width: 123px; }\n'; 11 | 12 | Sass.options('defaults'); 13 | Sass.options({comments: true}); 14 | 15 | Sass.writeFile('testfile.scss', '.imported { content: "testfile"; }'); 16 | 17 | Sass.compile(source, function(result) { 18 | expect(result.text).to.equal(expected); 19 | 20 | done(); 21 | }); 22 | }); 23 | 24 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass.js", 3 | "version": "0.9.10", 4 | "main": [ 5 | "./dist/sass.js", 6 | "./dist/sass.worker.js" 7 | ], 8 | "ignore": [ 9 | "**/.*", 10 | "*.html", 11 | "*.md", 12 | "*.js", 13 | "*.json", 14 | "bin", 15 | "scss", 16 | "libsass", 17 | "grunt-tasks", 18 | "bin", 19 | "src", 20 | "test" 21 | ], 22 | "keywords": [ 23 | "libsass", 24 | "sass", 25 | "scss", 26 | "css" 27 | ], 28 | "authors": [ 29 | "Rodney Rehm (http://rodneyrehm.de)", 30 | "Sebastian Golasch (http://asciidisco.com)", 31 | "Christian Kruse (http://http://wwwtech.de)" 32 | ], 33 | "moduleType": [ 34 | "amd", 35 | "globals", 36 | "node" 37 | ], 38 | "license": "MIT" 39 | } -------------------------------------------------------------------------------- /libsass/emscripten_wrapper.hpp: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _EMSCRIPTEN_WRAPPER_H 3 | #define _EMSCRIPTEN_WRAPPER_H 4 | 5 | #ifdef __cplusplus 6 | extern "C" { 7 | using namespace std; 8 | #endif 9 | 10 | void sass_compile_emscripten( 11 | char *source_string, 12 | char *include_paths, 13 | bool compile_file, 14 | bool custom_importer, 15 | int output_style, 16 | int precision, 17 | bool source_comments, 18 | bool is_indented_syntax_src, 19 | bool source_map_contents, 20 | bool source_map_embed, 21 | bool omit_source_map_url, 22 | char *source_map_root, 23 | char *source_map_file, 24 | char *input_path, 25 | char *output_path, 26 | char *indent, 27 | char *linefeed 28 | ); 29 | 30 | struct Sass_Import** sass_importer_emscripten( 31 | const char* cur_path, Sass_Importer_Entry cb, 32 | struct Sass_Compiler* comp 33 | ); 34 | 35 | #ifdef __cplusplus 36 | } 37 | #endif 38 | 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /libsass/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -u 4 | 5 | # USAGE: 6 | # build.sh 7 | # build.sh debug 8 | 9 | # accept parameter, default to master 10 | LIBSASS_VERSION=${1:-"master"} 11 | echo "Building libsass version ${LIBSASS_VERSION} ${2:-}" 12 | 13 | # clean 14 | echo " resetting target directory to git HEAD" 15 | (cd libsass && git checkout --force ${LIBSASS_VERSION}) 16 | 17 | # patch 18 | echo " patching Makefile" 19 | patch ./libsass/Makefile < ./Makefile.patch 20 | patch ./libsass/Makefile.conf < ./Makefile.conf.patch 21 | echo " copying emscripten_wrapper" 22 | cp ./emscripten_wrapper.cpp ./libsass/src/emscripten_wrapper.cpp 23 | cp ./emscripten_wrapper.hpp ./libsass/src/emscripten_wrapper.hpp 24 | 25 | # build 26 | echo " initializing emscripten" 27 | if [ "${2:-}" = "debug" ]; then 28 | (cd libsass && emmake make js-debug) 29 | else 30 | (cd libsass && emmake make js) 31 | fi 32 | -------------------------------------------------------------------------------- /bin/benchmark.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var Benchmark = require('benchmark'); 3 | var nodeSass = require('node-sass'); 4 | var Sass = require('../dist/sass.sync.js'); 5 | var source = fs.readFileSync(__dirname + '/../scss/demo.scss', {encoding: 'utf8'}); 6 | 7 | var suite = new Benchmark.Suite(); 8 | // add tests 9 | suite.add('sass.js', function(deferred) { 10 | Sass.compile(source, function() { 11 | deferred.resolve(); 12 | }); 13 | }, { 14 | defer: true, 15 | }) 16 | .add('node-sass', function(deferred) { 17 | nodeSass.render({ 18 | data: source 19 | }, function(err, result) { 20 | deferred.resolve(); 21 | }); 22 | }, { 23 | defer: true, 24 | }) 25 | // add listeners 26 | .on('cycle', function(event) { 27 | console.log(String(event.target)); 28 | }) 29 | .on('complete', function() { 30 | console.log('Fastest is ' + this.filter('fastest')[0].name); 31 | }) 32 | // run async 33 | .run({ 'async': true }); 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rodney Rehm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/sass.util.js: -------------------------------------------------------------------------------- 1 | /*global Module*/ 2 | /*jshint strict:false, unused:false*/ 3 | 4 | function noop(){} 5 | 6 | 7 | function stripLeadingSlash(text) { 8 | return text.slice(0, 1) === '/' ? text.slice(1) : text; 9 | } 10 | 11 | function addLeadingSlash(text) { 12 | return text.slice(0, 1) !== '/' ? ('/' + text) : text; 13 | } 14 | 15 | function stripTrailingSlash(text) { 16 | return text.slice(-1) === '/' ? text.slice(0, -1) : text; 17 | } 18 | 19 | function addTrailingSlash(text) { 20 | return text.slice(-1) !== '/' ? (text + '/') : text; 21 | } 22 | 23 | 24 | function pointerToString(pointer) { 25 | /*jshint camelcase:false*/ 26 | return pointer && Module.Pointer_stringify(pointer) || null; 27 | } 28 | 29 | function stringToPointer(text) { 30 | var buffer = Module._malloc(text.length + 1); 31 | Module.writeStringToMemory(text, buffer); 32 | return buffer; 33 | } 34 | 35 | function pointerToJson(pointer) { 36 | var test = pointerToString(pointer); 37 | return test && JSON.parse(test) || null; 38 | } 39 | 40 | function pointerToStringArray(pointer) { 41 | var list = []; 42 | if (!pointer) { 43 | return list; 44 | } 45 | 46 | // TODO: are we limited to 32bit? 47 | for (var i=0; true; i+=4) { 48 | var _pointer = Module.getValue(pointer + i, '*'); 49 | if (!_pointer) { 50 | break; 51 | } 52 | 53 | var _item = pointerToString(_pointer); 54 | _item && list.push(_item); 55 | } 56 | 57 | return list; 58 | } 59 | -------------------------------------------------------------------------------- /libsass/Makefile.patch: -------------------------------------------------------------------------------- 1 | --- Makefile.orig 2015-09-09 14:30:05.000000000 +0300 2 | +++ Makefile 2015-09-09 14:36:59.000000000 +0300 3 | @@ -208,6 +208,33 @@ 4 | debug-shared: CXXFLAGS := -g -DDEBUG -DDEBUG_LVL="$(DEBUG_LVL)" $(filter-out -O2,$(CXXFLAGS)) 5 | debug-shared: shared 6 | 7 | +js: static 8 | + emcc lib/libsass.a -o lib/libsass.js \ 9 | + -O3 \ 10 | + -s EXPORTED_FUNCTIONS="['_sass_compile_emscripten']" \ 11 | + -s DISABLE_EXCEPTION_CATCHING=0 \ 12 | + -s ALLOW_MEMORY_GROWTH=1 \ 13 | + -s EMTERPRETIFY=1 \ 14 | + -s EMTERPRETIFY_ASYNC=1 \ 15 | + -s EMTERPRETIFY_WHITELIST=@../empterpreter_whitelist.json \ 16 | + --memory-init-file 0 17 | + 18 | +js-debug: static 19 | + emcc lib/libsass.a -o lib/libsass.js \ 20 | + -O0 \ 21 | + -s EXPORTED_FUNCTIONS="['_sass_compile_emscripten']" \ 22 | + -s DISABLE_EXCEPTION_CATCHING=0 \ 23 | + -s ALLOW_MEMORY_GROWTH=1 \ 24 | + -s EMTERPRETIFY=1 \ 25 | + -s EMTERPRETIFY_ASYNC=1 \ 26 | + -s EMTERPRETIFY_WHITELIST=@../empterpreter_whitelist.json \ 27 | + -s ASSERTIONS=2 \ 28 | + -s SAFE_HEAP=1 \ 29 | + -s DEMANGLE_SUPPORT=1 \ 30 | + --profiling-funcs \ 31 | + --minify 0 \ 32 | + --memory-init-file 0 33 | + 34 | lib: 35 | $(MKDIR) lib 36 | 37 | @@ -299,7 +326,7 @@ 38 | $(RUBY_BIN) $(SASS_SPEC_PATH)/sass-spec.rb -c $(SASSC_BIN) $(LOG_FLAGS) $(SASS_SPEC_PATH)/spec/issues 39 | 40 | clean-objects: lib 41 | - -$(RM) lib/*.a lib/*.so lib/*.dll lib/*.la 42 | + -$(RM) lib/*.a lib/*.so lib/*.dll lib/*.la lib/*.js 43 | -$(RMDIR) lib 44 | clean: clean-objects 45 | $(RM) $(CLEANUPS) 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sass.js", 3 | "version": "0.9.10", 4 | "libsass": "3.3.6", 5 | "title": "Sass.js - API for emscripted libsass", 6 | "description": "Sass.js is a convenience API for the JavaScript libsass (compiled with Emscripten)", 7 | "homepage": "http://medialize.github.com/sass.js/", 8 | "author": "Rodney Rehm (mail+github@rodneyrehm.de) ", 9 | "contributors": [ 10 | { 11 | "name": "Sebastian Golasch", 12 | "url": "http://asciidisco.com/" 13 | }, 14 | { 15 | "name": "Christian Kruse", 16 | "url": "http://http://wwwtech.de/" 17 | } 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/medialize/sass.js.git" 22 | }, 23 | "license": "MIT", 24 | "keywords": [ 25 | "libsass", 26 | "sass", 27 | "scss", 28 | "css" 29 | ], 30 | "main": "./dist/sass.sync.js", 31 | "scripts": { 32 | "build": "grunt build", 33 | "test": "grunt test", 34 | "lint": "grunt lint" 35 | }, 36 | "files": [ 37 | "dist/", 38 | "maps/" 39 | ], 40 | "devDependencies": { 41 | "benchmark": "^2.0.0", 42 | "chai": "^3.5.0", 43 | "grunt": "^1.0.1", 44 | "grunt-contrib-clean": "^1.0.0", 45 | "grunt-contrib-concat": "^1.0.0", 46 | "grunt-contrib-jshint": "^1.0.0", 47 | "grunt-mkdir": "^1.0.0", 48 | "grunt-mocha-test": "~0.12.7", 49 | "grunt-shell": "^1.2.1", 50 | "jit-grunt": "^0.10.0", 51 | "jshint-stylish": "^2.1.0", 52 | "node-sass": "^3.4.2", 53 | "time-grunt": "^1.1.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /sass.sync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sass.js Sync Console 6 | 7 | 8 |

Sass.js Sync Console

9 |

This document loads the synchronous Sass.js. Please open your browser's DevTools to use the API.

10 | 11 |

Try the following example:

12 |
Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }', function() {
13 |   console.log('wrote "testfile.scss"');
14 | });
15 | Sass.writeFile('sub/deeptest.scss', '.deeptest { content: "loaded"; }', function() {
16 |   console.log('wrote "sub/deeptest.scss"');
17 | });
18 | Sass.options({ style: Sass.style.expanded }, function(result) {
19 |   console.log("set options");
20 | });
21 | Sass.compile('@import "testfile";', function(result) {
22 |   console.log("compiled", result.text);
23 | });
24 | 25 | 26 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/sass.configure.path.js: -------------------------------------------------------------------------------- 1 | /*global document*/ 2 | // identify the path sass.js is located at in case we're loaded by a simple 3 | // 4 | // this path can be used to identify the location of 5 | // * sass.worker.js from sass.js 6 | // * libsass.js.mem from sass.sync.js 7 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 8 | // see https://github.com/medialize/sass.js/issues/33 9 | var SASSJS_RELATIVE_PATH = (function() { 10 | 'use strict'; 11 | 12 | // in Node things are rather simple 13 | if (typeof __dirname !== 'undefined') { 14 | return __dirname; 15 | } 16 | 17 | // we can only run this test in the browser, 18 | // so make sure we actually have a DOM to work with. 19 | if (typeof document === 'undefined' || !document.getElementsByTagName) { 20 | return null; 21 | } 22 | 23 | // http://www.2ality.com/2014/05/current-script.html 24 | var currentScript = document.currentScript || (function() { 25 | var scripts = document.getElementsByTagName('script'); 26 | return scripts[scripts.length - 1]; 27 | })(); 28 | 29 | var path = currentScript && currentScript.src; 30 | if (!path) { 31 | return null; 32 | } 33 | 34 | // [worker] make sure we're not running in some concatenated thing 35 | if (path.slice(-8) === '/sass.js') { 36 | return path.slice(0, -8); 37 | } 38 | 39 | // [sync] make sure we're not running in some concatenated thing 40 | if (path.slice(-13) === '/sass.sync.js') { 41 | return path.slice(0, -13); 42 | } 43 | 44 | return null; 45 | })() || '.'; 46 | -------------------------------------------------------------------------------- /sass.source.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sass.js Source Console 6 | 7 | 8 |

Sass.js Source Console

9 |

This document loads the source Sass.js. You have to compile libsass using grunt libsass:prepare libsass:build before this will work. Please open your browser's DevTools to use the API.

10 | 11 | 12 |

Try the following example:

13 |
Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }', function() {
14 |   console.log('wrote "testfile.scss"');
15 | });
16 | Sass.writeFile('sub/deeptest.scss', '.deeptest { content: "loaded"; }', function() {
17 |   console.log('wrote "sub/deeptest.scss"');
18 | });
19 | Sass.options({ style: Sass.style.expanded }, function(result) {
20 |   console.log("set options");
21 | });
22 | Sass.compile('@import "testfile";', function(result) {
23 |   console.log("compiled", result.text);
24 | });
25 | 26 | 27 | 28 | 29 | 30 | 31 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | 4 | require('time-grunt')(grunt); 5 | require('jit-grunt')(grunt, { 6 | // mappings 7 | }); 8 | 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('package.json'), 11 | }); 12 | 13 | grunt.loadTasks('grunt-tasks'); 14 | 15 | // aliases to build libsass from git using emscripten 16 | grunt.registerTask('libsass:prepare', ['shell:prepareLibsass']); 17 | grunt.registerTask('libsass:build', [ 18 | 'shell:buildLibsass', 19 | 'mkdir:dist', 20 | ]); 21 | grunt.registerTask('libsass:debug', [ 22 | 'shell:buildLibsassDebug', 23 | 'mkdir:dist', 24 | ]); 25 | 26 | // concatenate source files and libsass.js 27 | grunt.registerTask('build:worker', ['concat:worker']); 28 | grunt.registerTask('build:sass', ['concat:sass']); 29 | grunt.registerTask('build:sync', ['concat:sync']); 30 | 31 | // full build pipeline 32 | grunt.registerTask('build', [ 33 | 'clean:dist', 34 | 'mkdir:dist', 35 | 'libsass:prepare', 36 | 'versions', 37 | 'libsass:build', 38 | 'build:sass', 39 | 'build:worker', 40 | 'build:sync', 41 | 'file-size', 42 | ]); 43 | grunt.registerTask('build:debug', [ 44 | 'clean:dist', 45 | 'mkdir:dist', 46 | 'libsass:prepare', 47 | 'versions', 48 | 'libsass:debug', 49 | 'build:sass', 50 | 'build:worker', 51 | 'build:sync', 52 | 'file-size', 53 | ]); 54 | // simplifications for development 55 | grunt.registerTask('rebuild', [ 56 | 'versions', 57 | 'libsass:build', 58 | 'build:sass', 59 | 'build:worker', 60 | 'build:sync', 61 | 'file-size', 62 | ]); 63 | grunt.registerTask('rebuild:debug', [ 64 | 'versions', 65 | 'libsass:debug', 66 | 'build:sass', 67 | 'build:worker', 68 | 'build:sync', 69 | 'file-size', 70 | ]); 71 | 72 | grunt.registerTask('lint', 'jshint'); 73 | grunt.registerTask('test', 'mochaTest'); 74 | }; 75 | -------------------------------------------------------------------------------- /libsass/empterpreter_whitelist.json: -------------------------------------------------------------------------------- 1 | [ 2 | "_ZN4Sass9To_StringC1EPNS_7ContextEbb", 3 | "__Z20sass_compile_contextP12Sass_ContextN4Sass7Context4DataE", 4 | "__Z20sass_compile_contextP12Sass_ContextPN4Sass7ContextE", 5 | "__ZN4Sass12Data_Context5parseEv", 6 | "__ZN4Sass6Parser12parse_importEv", 7 | "__ZN4Sass6Parser15add_single_fileEPNS_6ImportENSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE", 8 | "__ZN4Sass6Parser16parse_block_nodeEb", 9 | "__ZN4Sass6Parser17parse_block_nodesEb", 10 | "__ZN4Sass6Parser18import_single_fileEPNS_6ImportENSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE", 11 | "__ZN4Sass6Parser5parseEv", 12 | "__ZN4Sass6Parser9do_importERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEPNS_6ImportENS1_6vectorIP13Sass_ImporterNS5_ISE_EEEEb", 13 | "__ZN4Sass7Context10import_urlEPNS_6ImportENSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEERKS9_", 14 | "__ZN4Sass7Context10parse_fileEv", 15 | "__ZN4Sass7Context11call_loaderERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEPKcRNS_11ParserStateEPNS_6ImportENS1_6vectorIP13Sass_ImporterNS5_ISI_EEEEb", 16 | "__ZN4Sass7Context11load_importERKNS_8ImporterENS_11ParserStateE", 17 | "__ZN4Sass7Context12parse_stringEv", 18 | "__ZN4Sass7Context14call_importersERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEPKcRNS_11ParserStateEPNS_6ImportE", 19 | "__ZN4Sass7Context17register_resourceERKNS_7IncludeERKNS_8ResourceE", 20 | "__ZN4Sass7Context17register_resourceERKNS_7IncludeERKNS_8ResourceEPNS_11ParserStateE", 21 | "__ZN4Sass7Context19process_queue_entryERNS_11Sass_QueuedEj", 22 | "__ZN4Sass7Context8add_fileERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEES9_NS_11ParserStateE", 23 | "__ZN4Sass7Context8add_fileERKNSt3__112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEb", 24 | "_sass_compile_data_context", 25 | "_sass_compile_emscripten", 26 | "_sass_compiler_parse", 27 | "_sass_importer_emscripten", 28 | "_emscripten_sleep" 29 | ] -------------------------------------------------------------------------------- /src/sass.worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*global Sass, postMessage, onmessage:true, importScripts*/ 3 | importScripts('libsass.js', 'sass.js'); 4 | 5 | var _importerDone; 6 | var _importerInit = function(request, done) { 7 | _importerDone = done; 8 | postMessage({ 9 | command: '_importerInit', 10 | args: [request] 11 | }); 12 | }; 13 | 14 | var methods = { 15 | _importerFinish: function(result) { 16 | _importerDone && _importerDone(result); 17 | _importerDone = null; 18 | }, 19 | 20 | importer: function(callback) { 21 | // an importer was un/set 22 | // we need to register a callback that will pipe 23 | // things through the worker 24 | Sass.importer(callback ? _importerInit : null); 25 | }, 26 | }; 27 | 28 | onmessage = function (event) { 29 | 30 | function done(result) { 31 | try { 32 | // may throw DataCloneError: Failed to execute 'postMessage' on 'WorkerGlobalScope': An object could not be cloned. 33 | // because of Error instances not being clonable (wtf?) 34 | postMessage({ 35 | id: event.data.id, 36 | result: result 37 | }); 38 | } catch (e) { 39 | if (!result.error) { 40 | // unless we're dealing with a DataCloneError because of an Error instance, 41 | // we have no idea what is going on, so give up. 42 | throw e; 43 | } else { 44 | // for whatever reason Error instances may not always be serializable, 45 | // in which case we simply return the error data as a plain object 46 | result.error = { 47 | code: result.error.code, 48 | message: result.error.message, 49 | stack: result.error.stack, 50 | }; 51 | } 52 | 53 | postMessage({ 54 | id: event.data.id, 55 | result: result 56 | }); 57 | } 58 | } 59 | 60 | var method = methods[event.data.command] || Sass[event.data.command]; 61 | 62 | if (!method) { 63 | return done({ 64 | line: 0, 65 | message: 'Unknown command ' + event.action 66 | }); 67 | } 68 | 69 | method.apply(Sass, (event.data.args || []).concat([done])); 70 | }; 71 | -------------------------------------------------------------------------------- /test/test.import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Sass = require('../dist/sass.sync.js'); 5 | 6 | describe('@import', function() { 7 | 8 | it('should read from FS', function(done) { 9 | var source = '@import "testfile";'; 10 | var expected = '.imported {\n content: "testfile"; }\n'; 11 | 12 | Sass.options('defaults'); 13 | 14 | Sass.writeFile('testfile.scss', '.imported { content: "testfile"; }'); 15 | 16 | Sass.compile(source, function(result) { 17 | expect(result.text).to.equal(expected); 18 | 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should allow directories', function(done) { 24 | var source = '@import "some-dir/testfile";'; 25 | var expected = '.imported {\n content: "testfile"; }\n'; 26 | 27 | Sass.options('defaults'); 28 | 29 | Sass.writeFile('some-dir/testfile.scss', '.imported { content: "testfile"; }'); 30 | 31 | Sass.compile(source, function(result) { 32 | expect(result.text).to.equal(expected); 33 | 34 | done(); 35 | }); 36 | }); 37 | 38 | it('should resolve nested imports', function(done) { 39 | var source = '@import "some-dir/testfile";'; 40 | var expected = '.imported {\n content: "bar"; }\n\n.imported {\n content: "testfile"; }\n'; 41 | 42 | Sass.options('defaults'); 43 | 44 | Sass.writeFile('some-dir/testfile.scss', '@import "foo/bar";.imported { content: "testfile"; }'); 45 | Sass.writeFile('some-dir/foo/bar.scss', '.imported { content: "bar"; }'); 46 | 47 | Sass.compile(source, function(result) { 48 | expect(result.text).to.equal(expected); 49 | 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should fail unknown files', function(done) { 55 | var source = '@import "unknown-file";'; 56 | 57 | Sass.options('defaults'); 58 | 59 | Sass.compile(source, function(result) { 60 | expect(result).to.be.a('object'); 61 | expect(result.line).to.equal(1); 62 | expect(result.message).to.equal('File to import not found or unreadable: unknown-file\nParent style sheet: stdin'); 63 | 64 | done(); 65 | }); 66 | }); 67 | 68 | }); -------------------------------------------------------------------------------- /sass.worker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sass.js Worker Console 6 | 7 | 8 |

Sass.js Worker Console

9 |

This document loads the worker Sass.js. Please open your browser's DevTools to use the API.

10 | 11 |

Try the following example:

12 |
// when loading the worker api via script[src$="/sass.js"] it's a safe bet the worker
13 | // will be found relative to that file. In any other case you'd have to define where
14 | // the worker is located before initializing Sass:
15 | // Sass.setWorkerUrl('dist/sass.worker.js');
16 | var sass = new Sass();
17 | 
18 | sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }', function() {
19 |   console.log('wrote "testfile.scss"');
20 | });
21 | sass.writeFile('sub/deeptest.scss', '.deeptest { content: "loaded"; }', function() {
22 |   console.log('wrote "sub/deeptest.scss"');
23 | });
24 | sass.options({ style: Sass.style.expanded }, function(result) {
25 |   console.log("set options");
26 | });
27 | sass.compile('@import "testfile";', function(result) {
28 |   console.log("compiled", result.text);
29 | });
30 | 31 | 32 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /grunt-tasks/file-size.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileVersions(grunt) { 2 | 'use strict'; 3 | 4 | var childProcess = require('child_process'); 5 | var PATH = require('path'); 6 | var fs = require('fs'); 7 | 8 | function getFileSize(path, done) { 9 | var command = 'cp ' + path + ' ' + path + '.tmp && gzip ' + path + '.tmp'; 10 | var cwd = PATH.join(__dirname, '..'); 11 | var target = path + '.tmp.gz'; 12 | 13 | childProcess.exec(command, {cwd: cwd}, function(err, stdout, stderr) { 14 | if (err) { 15 | grunt.log.error('compressing ' + path + ' failed: ' + err.code + '\n' + stderr); 16 | return; 17 | } 18 | 19 | fs.stat(path, function(error, inputStats) { 20 | if (err) { 21 | grunt.log.error('inspecting ' + target + ' failed: ' + err.code + '\n' + stderr); 22 | return; 23 | } 24 | 25 | fs.stat(target, function(error, compressedStats) { 26 | if (err) { 27 | grunt.log.error('inspecting ' + target + ' failed: ' + err.code + '\n' + stderr); 28 | return; 29 | } 30 | 31 | fs.unlink(target, function(error) { 32 | if (err) { 33 | grunt.log.error('removing ' + target + ' failed: ' + err.code + '\n' + stderr); 34 | return; 35 | } 36 | 37 | done( 38 | Math.round(inputStats.size / 1024) + ' KB', 39 | Math.round(compressedStats.size / 1024) + ' KB' 40 | ); 41 | }); 42 | }); 43 | }); 44 | }); 45 | } 46 | 47 | grunt.registerTask('file-size', function() { 48 | var done = this.async(); 49 | var sizes = {}; 50 | getFileSize('dist/sass.js', function(normal, compressed) { 51 | sizes['dist/sass.js'] = { 52 | normal: normal, 53 | compressed: compressed 54 | }; 55 | 56 | getFileSize('dist/sass.sync.js', function(normal, compressed) { 57 | sizes['dist/sass.sync.js'] = { 58 | normal: normal, 59 | compressed: compressed 60 | }; 61 | 62 | getFileSize('dist/sass.worker.js', function(normal, compressed) { 63 | sizes['dist/sass.worker.js'] = { 64 | normal: normal, 65 | compressed: compressed 66 | }; 67 | 68 | var _sizes = JSON.stringify(sizes, null, 2); 69 | fs.writeFile('dist/file-size.json', _sizes, done); 70 | }); 71 | }); 72 | }); 73 | }); 74 | }; -------------------------------------------------------------------------------- /grunt-tasks/concat.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileConcat(grunt) { 2 | 'use strict'; 3 | 4 | /*jshint laxbreak:true */ 5 | var banner = '/*! <%= pkg.name %> - v<%= pkg.version %> (<%= versions.sassjs.commit %>) - built <%= grunt.template.today("yyyy-mm-dd") %>\n' 6 | + ' providing libsass <%= versions.libsass.version %> (<%= versions.libsass.commit %>)\n' 7 | + ' via emscripten <%= versions.emscripten.version %> (<%= versions.emscripten.commit %>)\n */\n'; 8 | var umdHeader = ['(function (root, factory) {', 9 | ' \'use strict\';', 10 | ' if (typeof define === \'function\' && define.amd) {', 11 | ' define([], factory);', 12 | ' } else if (typeof exports === \'object\') {', 13 | ' module.exports = factory();', 14 | ' } else {', 15 | ' root.Sass = factory();', 16 | ' }', 17 | '}(this, function () {'].join('\n'); 18 | var umdFooter = 'return Sass;\n}));'; 19 | /*jshint laxbreak:false */ 20 | 21 | grunt.config('concat', { 22 | sass: { 23 | dest: 'dist/sass.js', 24 | src: [ 25 | 'src/sass.configure.path.js', 26 | 'src/sass.js' 27 | ], 28 | options: { 29 | banner: banner + '\n' + umdHeader, 30 | footer: umdFooter, 31 | } 32 | }, 33 | worker: { 34 | dest: 'dist/sass.worker.js', 35 | src: [ 36 | 'libsass/libsass/lib/libsass.js', 37 | 'src/sass.util.js', 38 | 'src/sass.options.js', 39 | 'src/sass.importer.js', 40 | 'src/sass.api.js', 41 | 'src/sass.worker.js' 42 | ], 43 | options: { 44 | banner: banner, 45 | process: function (content) { 46 | return content 47 | // prevent emscripted libsass from exporting itself 48 | .replace(/module\['exports'\] = Module;/, '') 49 | // libsass and sass API are inlined, so no need to load them 50 | .replace(/importScripts\((['"])libsass.js\1,\s*\1sass.js\1\);/, ''); 51 | } 52 | } 53 | }, 54 | sync: { 55 | dest: 'dist/sass.sync.js', 56 | src: [ 57 | 'src/sass.configure.path.js', 58 | 'libsass/libsass/lib/libsass.js', 59 | 'src/sass.util.js', 60 | 'src/sass.options.js', 61 | 'src/sass.importer.js', 62 | 'src/sass.api.js' 63 | ], 64 | options: { 65 | banner: banner + '\n' + umdHeader, 66 | footer: umdFooter, 67 | process: function (content) { 68 | // prevent emscripted libsass from exporting itself 69 | return content.replace(/module\['exports'\] = Module;/, ''); 70 | } 71 | } 72 | }, 73 | }); 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /src/sass.importer.js: -------------------------------------------------------------------------------- 1 | /*global FS, PATH, Sass, stringToPointer*/ 2 | /*jshint strict:false*/ 3 | 4 | var Importer = { 5 | _running: false, 6 | _result: null, 7 | 8 | find: function(current, previous) { 9 | if (!Sass._importer) { 10 | Importer._running = false; 11 | return; 12 | } 13 | 14 | Importer._running = true; 15 | Importer._result = null; 16 | 17 | var resolved = PATH.resolve(previous === 'stdin' ? Sass._path : PATH.dirname(previous), current); 18 | var found = Importer._resolvePath(resolved); 19 | var done = function done(result) { 20 | Importer._result = result; 21 | Importer._running = false; 22 | }; 23 | 24 | try { 25 | Sass._importer({ 26 | current: current, 27 | previous: previous, 28 | resolved: resolved, 29 | path: found, 30 | options: Sass._options.importer || null, 31 | }, done); 32 | } catch(e) { 33 | // allow emscripten to resume libsass, 34 | // if only to have it stop gracefully 35 | done({ error: e.message }); 36 | // but don't just swallow the JS error 37 | console.error(e.stack); 38 | } 39 | }, 40 | 41 | finished: function() { 42 | return !Importer._running; 43 | }, 44 | 45 | path: function() { 46 | return Importer._resultPointer('path'); 47 | }, 48 | 49 | content: function() { 50 | return Importer._resultPointer('content'); 51 | }, 52 | 53 | error: function() { 54 | return Importer._resultPointer('error'); 55 | }, 56 | 57 | _resultPointer: function(key) { 58 | return Importer._result && Importer._result[key] && stringToPointer(Importer._result[key]) || 0; 59 | }, 60 | 61 | _libsassPathVariations: function(path) { 62 | // [importer,include_path] this is where we would add the ability to 63 | // examine the include_path (if we ever use that in Sass.js) 64 | path = PATH.normalize(path); 65 | var directory = PATH.dirname(path); 66 | var basename = PATH.basename(path); 67 | var extensions = ['.scss', '.sass', '.css']; 68 | // basically what is done by resolve_and_load() in file.cpp 69 | // Resolution order for ambiguous imports: 70 | return [ 71 | // (1) filename as given 72 | path, 73 | // (2) underscore + given 74 | PATH.resolve(directory, '_' + basename) 75 | ].concat(extensions.map(function(extension) { 76 | // (3) underscore + given + extension 77 | return PATH.resolve(directory, '_' + basename + extension); 78 | })).concat(extensions.map(function(extension) { 79 | // (4) given + extension 80 | return PATH.resolve(directory, basename + extension); 81 | })); 82 | }, 83 | 84 | _resolvePath: function(path) { 85 | return Importer._libsassPathVariations(path).reduce(function(found, path) { 86 | if (found) { 87 | return found; 88 | } 89 | 90 | try { 91 | FS.stat(path); 92 | return path; 93 | } catch(e) { 94 | return null; 95 | } 96 | }, null); 97 | }, 98 | 99 | }; -------------------------------------------------------------------------------- /test/test.compile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Sass = require('../dist/sass.sync.js'); 5 | 6 | describe('Sass.compile()', function() { 7 | 8 | it('should return CSS', function(done) { 9 | var source = '$foo:123px;\n\n.m {\n width:$foo;\n}'; 10 | var expected = '.m {\n width: 123px; }\n'; 11 | 12 | Sass.options('defaults'); 13 | 14 | Sass.compile(source, function(result) { 15 | expect(result).to.be.a('object'); 16 | expect(result.map).to.be.a('object'); 17 | expect(result.text).to.equal(expected); 18 | 19 | done(); 20 | }); 21 | }); 22 | 23 | it('should return parse errors', function(done) { 24 | var source = '$foo:123px;\n\n.m {\n width:$foo;\n}\n\nfoobar'; 25 | 26 | Sass.options('defaults'); 27 | 28 | Sass.compile(source, function(result) { 29 | expect(result).to.be.a('object'); 30 | expect(result.line).to.equal(7); 31 | expect(result.message).to.include('Invalid CSS after "foobar":'); 32 | 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should accept options', function(done) { 38 | var source = '$foo:123.4px;\n\n.m {\n width:$foo;\n}'; 39 | var expected = '.m{width:123.4px}\n'; 40 | var expected2 = '.m {\n width: 123.4px; }\n'; 41 | 42 | Sass.options('defaults'); 43 | Sass.options({precision: 1}); 44 | 45 | Sass.compile(source, {style: Sass.style.compressed}, function(result) { 46 | expect(result).to.be.a('object'); 47 | expect(result.map).to.be.a('object'); 48 | expect(result.text).to.equal(expected); 49 | 50 | Sass.compile(source, function(result) { 51 | expect(result).to.be.a('object'); 52 | expect(result.map).to.be.a('object'); 53 | expect(result.text).to.equal(expected2); 54 | 55 | done(); 56 | }); 57 | }); 58 | }); 59 | 60 | it('should fail unnkown options', function(done) { 61 | var source = '$foo:123.4px;\n\n.m {\n width:$foo;\n}'; 62 | 63 | Sass.options('defaults'); 64 | Sass.options({precision: 1}); 65 | 66 | Sass.compile(source, {bla: 1}, function(result) { 67 | expect(result).to.be.a('object'); 68 | expect(result.status).to.equal(99); 69 | expect(result.message).to.equal('Unknown option "bla"'); 70 | 71 | done(); 72 | }); 73 | }); 74 | 75 | it('should queue compile() calls', function(done) { 76 | var source = '$foo:123px;\n\n.m {\n width:$foo;\n}'; 77 | var expected = '.m {\n width: 123px; }\n'; 78 | var _counter = 2; 79 | 80 | Sass.options('defaults'); 81 | 82 | Sass.compile(source, function(result) { 83 | expect(result).to.be.a('object'); 84 | expect(result.map).to.be.a('object'); 85 | expect(result.text).to.equal(expected); 86 | 87 | _counter--; 88 | if (!_counter) { 89 | done(); 90 | } 91 | }); 92 | Sass.compile(source, function(result) { 93 | expect(result).to.be.a('object'); 94 | expect(result.map).to.be.a('object'); 95 | expect(result.text).to.equal(expected); 96 | 97 | _counter--; 98 | if (!_counter) { 99 | done(); 100 | } 101 | }); 102 | }); 103 | }); -------------------------------------------------------------------------------- /src/sass.options.js: -------------------------------------------------------------------------------- 1 | /*jshint strict:false, unused:false*/ 2 | 3 | var BooleanNumber = function(input) { 4 | // in emscripten you pass booleans as integer 0 and 1 5 | return Number(Boolean(input)); 6 | }; 7 | 8 | // map of arguments required by the emscripten wrapper (order relevant!) 9 | // to not have to touch various different spots in this file, 10 | // everything is defined here and registered in the appropriate places. 11 | var options = [ 12 | { 13 | // int output_style, 14 | type: 'number', 15 | // Output style for the generated css code 16 | // using Sass.style.* 17 | key: 'style', 18 | initial: 0, 19 | coerce: Number, 20 | }, 21 | { 22 | // int precision, 23 | type: 'number', 24 | // Precision for outputting fractional numbers 25 | // 0: use libsass default 26 | key: 'precision', 27 | initial: -1, 28 | coerce: Number, 29 | }, 30 | { 31 | // bool source_comments, 32 | type: 'number', 33 | // If you want inline source comments 34 | key: 'comments', 35 | initial: 0, 36 | coerce: BooleanNumber, 37 | }, 38 | { 39 | // bool is_indented_syntax_src, 40 | type: 'number', 41 | // Treat source_string as SASS (as opposed to SCSS) 42 | key: 'indentedSyntax', 43 | initial: 0, 44 | coerce: BooleanNumber, 45 | }, 46 | { 47 | // bool source_map_contents, 48 | type: 'number', 49 | // embed include contents in maps 50 | key: 'sourceMapContents', 51 | initial: 1, 52 | coerce: BooleanNumber, 53 | }, 54 | { 55 | // bool source_map_embed, 56 | type: 'number', 57 | // embed sourceMappingUrl as data uri 58 | key: 'sourceMapEmbed', 59 | initial: 0, 60 | coerce: BooleanNumber, 61 | }, 62 | { 63 | // bool omit_source_map_url, 64 | type: 'number', 65 | // Disable sourceMappingUrl in css output 66 | key: 'sourceMapOmitUrl', 67 | initial: 1, 68 | coerce: BooleanNumber, 69 | }, 70 | { 71 | // char *source_map_root, 72 | type: 'string', 73 | // Pass-through as sourceRoot property 74 | key: 'sourceMapRoot', 75 | initial: 'root', 76 | coerce: String, 77 | }, 78 | { 79 | // char *source_map_file, 80 | type: 'string', 81 | // Path to source map file 82 | // Enables the source map generating 83 | // Used to create sourceMappingUrl 84 | key: 'sourceMapFile', 85 | initial: 'file', 86 | coerce: String, 87 | }, 88 | { 89 | // char *input_path, 90 | type: 'string', 91 | // The input path is used for source map generation. 92 | // It can be used to define something with string 93 | // compilation or to overload the input file path. 94 | // It is set to "stdin" for data contexts 95 | // and to the input file on file contexts. 96 | key: 'inputPath', 97 | initial: 'stdin', 98 | coerce: String, 99 | }, 100 | { 101 | // char *output_path, 102 | type: 'string', 103 | // The output path is used for source map generation. 104 | // Libsass will not write to this file, it is just 105 | // used to create information in source-maps etc. 106 | key: 'outputPath', 107 | initial: 'stdout', 108 | coerce: String, 109 | }, 110 | { 111 | // char *indent, 112 | type: 'string', 113 | // String to be used for indentation 114 | key: 'indent', 115 | initial: ' ', 116 | coerce: String, 117 | }, 118 | { 119 | // char *linefeed, 120 | type: 'string', 121 | // String to be used to for line feeds 122 | key: 'linefeed', 123 | initial: '\n', 124 | coerce: String, 125 | }, 126 | ]; 127 | -------------------------------------------------------------------------------- /maps/bourbon.js: -------------------------------------------------------------------------------- 1 | (function(Sass) { 2 | 'use strict'; 3 | 4 | // make sure the namespace is available 5 | !Sass.maps && (Sass.maps = {}); 6 | 7 | // files map for bourbon v4.2.1 - http://bourbon.io/ 8 | Sass.maps.bourbon = { 9 | // make the source file available in "bourbon/_bourbon.scss" 10 | directory: 'bourbon', 11 | // https://github.com/thoughtbot/bourbon/blob/v4.2.1/app/assets/stylesheets/ 12 | // using rawgit to directly access the github repository via CORS 13 | // NOTE: that this will only work for preloading, as lazyloading throws security exceptions 14 | base: 'https://cdn.rawgit.com/thoughtbot/bourbon/v4.2.1/app/assets/stylesheets/', 15 | files: [ 16 | '_bourbon.scss', 17 | '_bourbon-deprecated-upcoming.scss', 18 | 'addons/_border-color.scss', 19 | 'addons/_border-radius.scss', 20 | 'addons/_border-style.scss', 21 | 'addons/_border-width.scss', 22 | 'addons/_buttons.scss', 23 | 'addons/_clearfix.scss', 24 | 'addons/_ellipsis.scss', 25 | 'addons/_font-stacks.scss', 26 | 'addons/_hide-text.scss', 27 | 'addons/_margin.scss', 28 | 'addons/_padding.scss', 29 | 'addons/_position.scss', 30 | 'addons/_prefixer.scss', 31 | 'addons/_retina-image.scss', 32 | 'addons/_size.scss', 33 | 'addons/_text-inputs.scss', 34 | 'addons/_timing-functions.scss', 35 | 'addons/_triangle.scss', 36 | 'addons/_word-wrap.scss', 37 | 'css3/_animation.scss', 38 | 'css3/_appearance.scss', 39 | 'css3/_backface-visibility.scss', 40 | 'css3/_background-image.scss', 41 | 'css3/_background.scss', 42 | 'css3/_border-image.scss', 43 | 'css3/_calc.scss', 44 | 'css3/_columns.scss', 45 | 'css3/_filter.scss', 46 | 'css3/_flex-box.scss', 47 | 'css3/_font-face.scss', 48 | 'css3/_font-feature-settings.scss', 49 | 'css3/_hidpi-media-query.scss', 50 | 'css3/_hyphens.scss', 51 | 'css3/_image-rendering.scss', 52 | 'css3/_keyframes.scss', 53 | 'css3/_linear-gradient.scss', 54 | 'css3/_perspective.scss', 55 | 'css3/_placeholder.scss', 56 | 'css3/_radial-gradient.scss', 57 | 'css3/_selection.scss', 58 | 'css3/_text-decoration.scss', 59 | 'css3/_transform.scss', 60 | 'css3/_transition.scss', 61 | 'css3/_user-select.scss', 62 | 'functions/_assign-inputs.scss', 63 | 'functions/_contains-falsy.scss', 64 | 'functions/_contains.scss', 65 | 'functions/_is-length.scss', 66 | 'functions/_is-light.scss', 67 | 'functions/_is-number.scss', 68 | 'functions/_is-size.scss', 69 | 'functions/_modular-scale.scss', 70 | 'functions/_px-to-em.scss', 71 | 'functions/_px-to-rem.scss', 72 | 'functions/_shade.scss', 73 | 'functions/_strip-units.scss', 74 | 'functions/_tint.scss', 75 | 'functions/_transition-property-name.scss', 76 | 'functions/_unpack.scss', 77 | 'helpers/_convert-units.scss', 78 | 'helpers/_directional-values.scss', 79 | 'helpers/_font-source-declaration.scss', 80 | 'helpers/_gradient-positions-parser.scss', 81 | 'helpers/_linear-angle-parser.scss', 82 | 'helpers/_linear-gradient-parser.scss', 83 | 'helpers/_linear-positions-parser.scss', 84 | 'helpers/_linear-side-corner-parser.scss', 85 | 'helpers/_radial-arg-parser.scss', 86 | 'helpers/_radial-gradient-parser.scss', 87 | 'helpers/_radial-positions-parser.scss', 88 | 'helpers/_render-gradients.scss', 89 | 'helpers/_shape-size-stripper.scss', 90 | 'helpers/_str-to-num.scss', 91 | 'settings/_asset-pipeline.scss', 92 | 'settings/_prefixer.scss', 93 | 'settings/_px-to-em.scss', 94 | ] 95 | }; 96 | 97 | })(Sass); -------------------------------------------------------------------------------- /grunt-tasks/versions.js: -------------------------------------------------------------------------------- 1 | module.exports = function GruntfileVersions(grunt) { 2 | 'use strict'; 3 | 4 | var childProcess = require('child_process'); 5 | var fs = require('fs'); 6 | 7 | function getGitMeta(path, done) { 8 | // https://github.com/damkraw/grunt-gitinfo 9 | var data = { 10 | version: null, 11 | commit: null, 12 | }; 13 | 14 | // git rev-parse --short HEAD 15 | // f82a41b 16 | childProcess.exec('git rev-parse --short HEAD', {cwd: path}, function(err, stdout, stderr) { 17 | if (err) { 18 | grunt.log.error('obtaining git commit failed with: ' + err.code + '\n' + stderr); 19 | return; 20 | } 21 | 22 | data.commit = stdout.slice(0, -1); 23 | // git symbolic-ref HEAD 24 | // refs/heads/master 25 | childProcess.exec('git symbolic-ref HEAD', {cwd: path}, function(err, stdout /*, stderr*/) { 26 | if (err) { 27 | // git describe --tags 28 | // 3.2.0-beta.5-32-g0dd6543 (last tag contained) 29 | // 3.2.0-beta.5 (immediate tag) 30 | childProcess.exec('git describe --tags', {cwd: path}, function(err, stdout, stderr) { 31 | if (err) { 32 | grunt.log.error('obtaining git tag failed with: ' + err.code + '\n' + stderr); 33 | return; 34 | } 35 | 36 | data.version = stdout.slice(0, -1); 37 | done(data); 38 | }); 39 | 40 | return; 41 | } 42 | 43 | data.version = stdout.replace('refs/heads/', '').slice(0, -1); 44 | done(data); 45 | }); 46 | }); 47 | } 48 | 49 | grunt.registerTask('get-emscripten-version', function() { 50 | var done = this.async(); 51 | 52 | if (!grunt.config.data.versions) { 53 | grunt.config.data.versions = {}; 54 | } 55 | 56 | var versions = grunt.config.data.versions; 57 | childProcess.exec('emcc --version', function(err, stdout, stderr) { 58 | if (err) { 59 | grunt.log.error('`emcc --version` failed with: ' + err.code + '\n' + stderr); 60 | return; 61 | } 62 | 63 | var line = stdout.split('\n')[0]; 64 | // "emcc (Emscripten GCC-like replacement) 1.30.2 (commit dac9f88335dd74b377bedbc2ad7d3b64f0c9bb15)" 65 | // "emcc (Emscripten GCC-like replacement) 1.32.0 ()" 66 | var tokens = line.match(/emcc \([^)]+\) ([\d.]+)(?: \(commit ([^)]+)\))?/); 67 | 68 | versions.emscripten = { 69 | version: tokens[1], 70 | commit: tokens[2] && tokens[2].slice(0, 7) || null, 71 | }; 72 | 73 | done(); 74 | }); 75 | }); 76 | 77 | grunt.registerTask('get-libsass-version', function() { 78 | var done = this.async(); 79 | 80 | if (!grunt.config.data.versions) { 81 | grunt.config.data.versions = {}; 82 | } 83 | 84 | var versions = grunt.config.data.versions; 85 | getGitMeta('libsass/libsass/', function(data) { 86 | versions.libsass = data; 87 | done(); 88 | }); 89 | }); 90 | 91 | grunt.registerTask('get-sassjs-version', function() { 92 | var done = this.async(); 93 | 94 | if (!grunt.config.data.versions) { 95 | grunt.config.data.versions = {}; 96 | } 97 | 98 | var versions = grunt.config.data.versions; 99 | getGitMeta('.', function(data) { 100 | data.branch = data.version; 101 | data.version = grunt.config.data.pkg.version; 102 | versions.sassjs = data; 103 | done(); 104 | }); 105 | }); 106 | 107 | grunt.registerTask('save-versions', function() { 108 | var done = this.async(); 109 | var _versions = JSON.stringify(grunt.config.data.versions, null, 2); 110 | fs.writeFile('dist/versions.json', _versions, done); 111 | }); 112 | 113 | grunt.registerTask('versions', [ 114 | 'get-emscripten-version', 115 | 'get-libsass-version', 116 | 'get-sassjs-version', 117 | 'mkdir:dist', 118 | 'save-versions', 119 | ]); 120 | 121 | }; 122 | -------------------------------------------------------------------------------- /src/sass.js: -------------------------------------------------------------------------------- 1 | /*global Worker, SASSJS_RELATIVE_PATH*/ 2 | 'use strict'; 3 | 4 | var noop = function(){}; 5 | var slice = [].slice; 6 | // defined upon first Sass.initialize() call 7 | var globalWorkerUrl; 8 | 9 | function Sass(workerUrl) { 10 | if (!workerUrl && !globalWorkerUrl) { 11 | /*jshint laxbreak:true */ 12 | throw new Error( 13 | 'Sass needs to be initialized with the URL of sass.worker.js - ' 14 | + 'either via Sass.setWorkerUrl(url) or by new Sass(url)' 15 | ); 16 | /*jshint laxbreak:false */ 17 | } 18 | 19 | if (!globalWorkerUrl) { 20 | globalWorkerUrl = workerUrl; 21 | } 22 | 23 | // bind all functions 24 | // we're doing this because we used to have a single hard-wired instance that allowed 25 | // [].map(Sass.removeFile) and we need to maintain that for now (at least until 1.0.0) 26 | for (var key in this) { 27 | if (typeof this[key] === 'function') { 28 | this[key] = this[key].bind(this); 29 | } 30 | } 31 | 32 | this._callbacks = {}; 33 | this._worker = new Worker(workerUrl || globalWorkerUrl); 34 | this._worker.addEventListener('message', this._handleWorkerMessage, false); 35 | } 36 | 37 | // allow setting the workerUrl before the first Sass instance is initialized, 38 | // where registering the global workerUrl would've happened automatically 39 | Sass.setWorkerUrl = function(workerUrl) { 40 | globalWorkerUrl = workerUrl; 41 | }; 42 | 43 | Sass.style = { 44 | nested: 0, 45 | expanded: 1, 46 | compact: 2, 47 | compressed: 3 48 | }; 49 | 50 | Sass.comments = { 51 | 'none': 0, 52 | 'default': 1 53 | }; 54 | 55 | Sass.prototype = { 56 | style: Sass.style, 57 | comments: Sass.comments, 58 | 59 | destroy: function() { 60 | this._worker && this._worker.terminate(); 61 | this._worker = null; 62 | this._callbacks = {}; 63 | this._importer = null; 64 | }, 65 | 66 | _handleWorkerMessage: function(event) { 67 | if (event.data.command) { 68 | this[event.data.command](event.data.args); 69 | } 70 | 71 | this._callbacks[event.data.id] && this._callbacks[event.data.id](event.data.result); 72 | delete this._callbacks[event.data.id]; 73 | }, 74 | 75 | _dispatch: function(options, callback) { 76 | if (!this._worker) { 77 | throw new Error('Sass worker has been terminated'); 78 | } 79 | 80 | options.id = 'cb' + Date.now() + Math.random(); 81 | this._callbacks[options.id] = callback; 82 | this._worker.postMessage(options); 83 | }, 84 | 85 | _importerInit: function(args) { 86 | // importer API done callback pushing results 87 | // back to the worker 88 | var done = function done(result) { 89 | this._worker.postMessage({ 90 | command: '_importerFinish', 91 | args: [result] 92 | }); 93 | }.bind(this); 94 | 95 | try { 96 | this._importer(args[0], done); 97 | } catch(e) { 98 | done({ error: e.message }); 99 | throw e; 100 | } 101 | }, 102 | 103 | importer: function(importerCallback, callback) { 104 | if (typeof importerCallback !== 'function' && importerCallback !== null) { 105 | throw new Error('importer callback must either be a function or null'); 106 | } 107 | 108 | // callback is executed in the main EventLoop 109 | this._importer = importerCallback; 110 | // tell worker to activate importer callback 111 | this._worker.postMessage({ 112 | command: 'importer', 113 | args: [Boolean(importerCallback)] 114 | }); 115 | 116 | callback && callback(); 117 | }, 118 | }; 119 | 120 | var commands = 'writeFile readFile listFiles removeFile clearFiles lazyFiles preloadFiles options compile compileFile'; 121 | commands.split(' ').forEach(function(command) { 122 | Sass.prototype[command] = function() { 123 | var callback = slice.call(arguments, -1)[0]; 124 | var args = slice.call(arguments, 0, -1); 125 | if (typeof callback !== 'function') { 126 | args.push(callback); 127 | callback = noop; 128 | } 129 | 130 | this._dispatch({ 131 | command: command, 132 | args: args 133 | }, callback); 134 | }; 135 | }); 136 | 137 | // automatically set the workerUrl in case we're loaded by a simple 138 | // 139 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 140 | Sass.setWorkerUrl(SASSJS_RELATIVE_PATH + '/sass.worker.js'); 141 | -------------------------------------------------------------------------------- /test/test.filesystem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Sass = require('../dist/sass.sync.js'); 5 | 6 | describe('filesystem', function() { 7 | 8 | it('should write file', function(done) { 9 | Sass.clearFiles(); 10 | 11 | Sass.writeFile('hello.scss', 'content'); 12 | expect(Sass._files['/sass/hello.scss']).to.be.a('string'); 13 | expect(Sass.FS.stat('/sass/hello.scss')).to.be.a('object'); 14 | 15 | Sass.writeFile('sub/hello.scss', 'content'); 16 | expect(Sass._files['/sass/sub/hello.scss']).to.be.a('string'); 17 | expect(Sass.FS.stat('/sass/sub/hello.scss')).to.be.a('object'); 18 | 19 | done(); 20 | }); 21 | 22 | it('should read file', function(done) { 23 | Sass.clearFiles(); 24 | 25 | Sass.writeFile('hello.scss', 'my-content'); 26 | Sass.readFile('hello.scss', function(content) { 27 | expect(content).to.equal('my-content'); 28 | done(); 29 | }); 30 | 31 | }); 32 | 33 | it('should remove file', function(done) { 34 | Sass.clearFiles(); 35 | 36 | Sass.writeFile('hello.scss', 'my-content'); 37 | expect(Sass._files['/sass/hello.scss']).to.be.a('string'); 38 | expect(Sass.FS.stat('/sass/hello.scss')).to.be.a('object'); 39 | 40 | Sass.removeFile('hello.scss'); 41 | expect(Sass._files['/sass/hello.scss']).to.equal(undefined); 42 | try { 43 | Sass.FS.stat('/sass/hello.scss'); 44 | expect(false).to.equal(true); 45 | } catch(e) {} 46 | 47 | done(); 48 | }); 49 | 50 | it('should list files', function(done) { 51 | Sass.clearFiles(); 52 | 53 | Sass.writeFile('hello.scss', 'my-content'); 54 | Sass.writeFile('world.scss', 'my-content'); 55 | 56 | Sass.listFiles(function(list) { 57 | list.sort(); 58 | expect(list.join(',')).to.equal('hello.scss,world.scss'); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should clear files', function(done) { 64 | Sass.clearFiles(); 65 | 66 | Sass.writeFile('hello.scss', 'my-content'); 67 | Sass.writeFile('world.scss', 'my-content'); 68 | 69 | Sass.listFiles(function(list) { 70 | list.sort(); 71 | expect(list.join(',')).to.equal('hello.scss,world.scss'); 72 | }); 73 | 74 | Sass.clearFiles(); 75 | 76 | Sass.listFiles(function(list) { 77 | list.sort(); 78 | expect(list.join(',')).to.equal(''); 79 | }); 80 | 81 | done(); 82 | }); 83 | 84 | it('should write multiple files', function(done) { 85 | Sass.clearFiles(); 86 | 87 | Sass.writeFile({ 88 | 'first.scss': 'my-first-content', 89 | 'sub/second.scss': 'my-second-content', 90 | }, function(result) { 91 | expect(result['first.scss']).to.equal(true); 92 | expect(result['sub/second.scss']).to.equal(true); 93 | 94 | expect(Sass._files['/sass/first.scss']).to.be.a('string'); 95 | expect(Sass.FS.stat('/sass/first.scss')).to.be.a('object'); 96 | 97 | expect(Sass._files['/sass/sub/second.scss']).to.be.a('string'); 98 | expect(Sass.FS.stat('/sass/sub/second.scss')).to.be.a('object'); 99 | 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should read multiple files', function(done) { 105 | Sass.clearFiles(); 106 | 107 | var files = ['first.scss', 'second.scss', 'third.scss']; 108 | Sass.writeFile('first.scss', 'my-first-content'); 109 | Sass.writeFile('second.scss', 'my-second-content'); 110 | Sass.readFile(files, function(result) { 111 | expect(result['first.scss']).to.equal('my-first-content'); 112 | expect(result['second.scss']).to.equal('my-second-content'); 113 | expect(result['third.scss']).to.equal(undefined); 114 | 115 | done(); 116 | }); 117 | 118 | }); 119 | 120 | it('should remove multiple files', function(done) { 121 | Sass.clearFiles(); 122 | 123 | var files = ['first.scss', 'second.scss', 'third.scss']; 124 | Sass.writeFile('first.scss', 'my-first-content'); 125 | Sass.writeFile('second.scss', 'my-second-content'); 126 | expect(Sass._files['/sass/first.scss']).to.be.a('string'); 127 | expect(Sass._files['/sass/second.scss']).to.be.a('string'); 128 | expect(Sass.FS.stat('/sass/first.scss')).to.be.a('object'); 129 | expect(Sass.FS.stat('/sass/second.scss')).to.be.a('object'); 130 | 131 | Sass.removeFile(files, function(result) { 132 | try { 133 | expect(result['first.scss']).to.equal(true); 134 | expect(result['second.scss']).to.equal(true); 135 | expect(result['third.scss']).to.equal(false); 136 | 137 | files.forEach(function(file) { 138 | expect(Sass._files['/sass/' + file]).to.equal(undefined); 139 | try { 140 | Sass.FS.stat('/sass/' + file); 141 | expect(false).to.equal(true); 142 | } catch(e) {} 143 | }); 144 | done(); 145 | } catch(e) { 146 | done(e); 147 | } 148 | }); 149 | }); 150 | 151 | it('should compile file', function(done) { 152 | Sass.clearFiles(); 153 | 154 | Sass.writeFile({ 155 | 'hello.scss': '@import "sub/world"; .hello { content: "file"; }', 156 | 'sub/world.scss': '.sub-world { content: "file"; }', 157 | }); 158 | 159 | var expected = '.sub-world{content:"file"}.hello{content:"file"}\n'; 160 | 161 | Sass.options('defaults'); 162 | Sass.options({style: Sass.style.compressed}); 163 | 164 | Sass.compileFile('hello.scss', function(result) { 165 | expect(result).to.be.a('object'); 166 | expect(result.map).to.be.a('object'); 167 | expect(result.text).to.equal(expected); 168 | 169 | done(); 170 | }); 171 | }); 172 | 173 | it.skip('should preload files', function(done) { 174 | done(); 175 | }); 176 | 177 | it.skip('should lazy load files', function(done) { 178 | done(); 179 | }); 180 | 181 | }); -------------------------------------------------------------------------------- /dist/sass.js: -------------------------------------------------------------------------------- 1 | /*! sass.js - v0.9.10 (9a781bf) - built 2016-04-24 2 | providing libsass 3.3.6 (3ae9a20) 3 | via emscripten 1.36.1 (d5085ed) 4 | */ 5 | 6 | (function (root, factory) { 7 | 'use strict'; 8 | if (typeof define === 'function' && define.amd) { 9 | define([], factory); 10 | } else if (typeof exports === 'object') { 11 | module.exports = factory(); 12 | } else { 13 | root.Sass = factory(); 14 | } 15 | }(this, function () {/*global document*/ 16 | // identify the path sass.js is located at in case we're loaded by a simple 17 | // 18 | // this path can be used to identify the location of 19 | // * sass.worker.js from sass.js 20 | // * libsass.js.mem from sass.sync.js 21 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 22 | // see https://github.com/medialize/sass.js/issues/33 23 | var SASSJS_RELATIVE_PATH = (function() { 24 | 'use strict'; 25 | 26 | // in Node things are rather simple 27 | if (typeof __dirname !== 'undefined') { 28 | return __dirname; 29 | } 30 | 31 | // we can only run this test in the browser, 32 | // so make sure we actually have a DOM to work with. 33 | if (typeof document === 'undefined' || !document.getElementsByTagName) { 34 | return null; 35 | } 36 | 37 | // http://www.2ality.com/2014/05/current-script.html 38 | var currentScript = document.currentScript || (function() { 39 | var scripts = document.getElementsByTagName('script'); 40 | return scripts[scripts.length - 1]; 41 | })(); 42 | 43 | var path = currentScript && currentScript.src; 44 | if (!path) { 45 | return null; 46 | } 47 | 48 | // [worker] make sure we're not running in some concatenated thing 49 | if (path.slice(-8) === '/sass.js') { 50 | return path.slice(0, -8); 51 | } 52 | 53 | // [sync] make sure we're not running in some concatenated thing 54 | if (path.slice(-13) === '/sass.sync.js') { 55 | return path.slice(0, -13); 56 | } 57 | 58 | return null; 59 | })() || '.'; 60 | 61 | /*global Worker, SASSJS_RELATIVE_PATH*/ 62 | 'use strict'; 63 | 64 | var noop = function(){}; 65 | var slice = [].slice; 66 | // defined upon first Sass.initialize() call 67 | var globalWorkerUrl; 68 | 69 | function Sass(workerUrl) { 70 | if (!workerUrl && !globalWorkerUrl) { 71 | /*jshint laxbreak:true */ 72 | throw new Error( 73 | 'Sass needs to be initialized with the URL of sass.worker.js - ' 74 | + 'either via Sass.setWorkerUrl(url) or by new Sass(url)' 75 | ); 76 | /*jshint laxbreak:false */ 77 | } 78 | 79 | if (!globalWorkerUrl) { 80 | globalWorkerUrl = workerUrl; 81 | } 82 | 83 | // bind all functions 84 | // we're doing this because we used to have a single hard-wired instance that allowed 85 | // [].map(Sass.removeFile) and we need to maintain that for now (at least until 1.0.0) 86 | for (var key in this) { 87 | if (typeof this[key] === 'function') { 88 | this[key] = this[key].bind(this); 89 | } 90 | } 91 | 92 | this._callbacks = {}; 93 | this._worker = new Worker(workerUrl || globalWorkerUrl); 94 | this._worker.addEventListener('message', this._handleWorkerMessage, false); 95 | } 96 | 97 | // allow setting the workerUrl before the first Sass instance is initialized, 98 | // where registering the global workerUrl would've happened automatically 99 | Sass.setWorkerUrl = function(workerUrl) { 100 | globalWorkerUrl = workerUrl; 101 | }; 102 | 103 | Sass.style = { 104 | nested: 0, 105 | expanded: 1, 106 | compact: 2, 107 | compressed: 3 108 | }; 109 | 110 | Sass.comments = { 111 | 'none': 0, 112 | 'default': 1 113 | }; 114 | 115 | Sass.prototype = { 116 | style: Sass.style, 117 | comments: Sass.comments, 118 | 119 | destroy: function() { 120 | this._worker && this._worker.terminate(); 121 | this._worker = null; 122 | this._callbacks = {}; 123 | this._importer = null; 124 | }, 125 | 126 | _handleWorkerMessage: function(event) { 127 | if (event.data.command) { 128 | this[event.data.command](event.data.args); 129 | } 130 | 131 | this._callbacks[event.data.id] && this._callbacks[event.data.id](event.data.result); 132 | delete this._callbacks[event.data.id]; 133 | }, 134 | 135 | _dispatch: function(options, callback) { 136 | if (!this._worker) { 137 | throw new Error('Sass worker has been terminated'); 138 | } 139 | 140 | options.id = 'cb' + Date.now() + Math.random(); 141 | this._callbacks[options.id] = callback; 142 | this._worker.postMessage(options); 143 | }, 144 | 145 | _importerInit: function(args) { 146 | // importer API done callback pushing results 147 | // back to the worker 148 | var done = function done(result) { 149 | this._worker.postMessage({ 150 | command: '_importerFinish', 151 | args: [result] 152 | }); 153 | }.bind(this); 154 | 155 | try { 156 | this._importer(args[0], done); 157 | } catch(e) { 158 | done({ error: e.message }); 159 | throw e; 160 | } 161 | }, 162 | 163 | importer: function(importerCallback, callback) { 164 | if (typeof importerCallback !== 'function' && importerCallback !== null) { 165 | throw new Error('importer callback must either be a function or null'); 166 | } 167 | 168 | // callback is executed in the main EventLoop 169 | this._importer = importerCallback; 170 | // tell worker to activate importer callback 171 | this._worker.postMessage({ 172 | command: 'importer', 173 | args: [Boolean(importerCallback)] 174 | }); 175 | 176 | callback && callback(); 177 | }, 178 | }; 179 | 180 | var commands = 'writeFile readFile listFiles removeFile clearFiles lazyFiles preloadFiles options compile compileFile'; 181 | commands.split(' ').forEach(function(command) { 182 | Sass.prototype[command] = function() { 183 | var callback = slice.call(arguments, -1)[0]; 184 | var args = slice.call(arguments, 0, -1); 185 | if (typeof callback !== 'function') { 186 | args.push(callback); 187 | callback = noop; 188 | } 189 | 190 | this._dispatch({ 191 | command: command, 192 | args: args 193 | }, callback); 194 | }; 195 | }); 196 | 197 | // automatically set the workerUrl in case we're loaded by a simple 198 | // 199 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 200 | Sass.setWorkerUrl(SASSJS_RELATIVE_PATH + '/sass.worker.js'); 201 | return Sass; 202 | })); -------------------------------------------------------------------------------- /libsass/emscripten_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "sass/context.h" 4 | #include "emscripten_wrapper.hpp" 5 | #include 6 | 7 | void sass_compile_emscripten( 8 | char *source_string, 9 | char *include_paths, 10 | bool compile_file, 11 | bool custom_importer, 12 | int output_style, 13 | int precision, 14 | bool source_comments, 15 | bool is_indented_syntax_src, 16 | bool source_map_contents, 17 | bool source_map_embed, 18 | bool omit_source_map_url, 19 | char *source_map_root, 20 | char *source_map_file, 21 | char *input_path, 22 | char *output_path, 23 | char *indent, 24 | char *linefeed 25 | ) { 26 | // transform input 27 | Sass_Output_Style sass_output_style = (Sass_Output_Style)output_style; 28 | 29 | // initialize context 30 | struct Sass_Data_Context* data_ctx; 31 | struct Sass_File_Context* file_ctx; 32 | struct Sass_Context* ctx; 33 | 34 | if (compile_file) { 35 | file_ctx = sass_make_file_context(strdup(source_string)); 36 | ctx = sass_file_context_get_context(file_ctx); 37 | } else { 38 | data_ctx = sass_make_data_context(strdup(source_string)); 39 | ctx = sass_data_context_get_context(data_ctx); 40 | } 41 | 42 | struct Sass_Options* ctx_opt = sass_context_get_options(ctx); 43 | 44 | // configure context 45 | if (precision > -1) { 46 | // if we set a precision libsass will use that blindly, with 47 | // 0 being a valid precision - i.e. the precision of "integer" 48 | sass_option_set_precision(ctx_opt, precision); 49 | } 50 | sass_option_set_output_style(ctx_opt, sass_output_style); 51 | sass_option_set_source_comments(ctx_opt, source_comments); 52 | sass_option_set_source_map_embed(ctx_opt, source_map_embed); 53 | sass_option_set_source_map_contents(ctx_opt, source_map_contents); 54 | sass_option_set_omit_source_map_url(ctx_opt, omit_source_map_url); 55 | sass_option_set_is_indented_syntax_src(ctx_opt, is_indented_syntax_src); 56 | sass_option_set_indent(ctx_opt, indent); 57 | sass_option_set_linefeed(ctx_opt, linefeed); 58 | sass_option_set_input_path(ctx_opt, input_path); 59 | sass_option_set_output_path(ctx_opt, output_path); 60 | // void sass_option_set_plugin_path (struct Sass_Options* options, const char* plugin_path); 61 | sass_option_set_include_path(ctx_opt, include_paths); 62 | sass_option_set_source_map_file(ctx_opt, source_map_file); 63 | sass_option_set_source_map_root(ctx_opt, source_map_root); 64 | // void sass_option_set_c_functions (struct Sass_Options* options, Sass_C_Function_List c_functions); 65 | 66 | if (custom_importer) { 67 | double priority = 0; 68 | void* cookie = 0; 69 | Sass_Importer_List importers = sass_make_importer_list(1); 70 | Sass_Importer_Entry importer = sass_make_importer(sass_importer_emscripten, priority, cookie); 71 | sass_importer_set_list_entry(importers, 0, importer); 72 | sass_option_set_c_importers(ctx_opt, importers); 73 | } 74 | 75 | // compile 76 | int status; 77 | if (compile_file) { 78 | status = sass_compile_file_context(file_ctx); 79 | } else { 80 | status = sass_compile_data_context(data_ctx); 81 | } 82 | 83 | // returning data to JS via callback rather than regular function return value and C pointer fun, 84 | // because emscripten does not inform JavaScript when an (empterpreter) async function is done. 85 | // Since (char *) is a pointer (int) we can abuse EM_ASM_INT() to pass that back to JavaScript. 86 | // NOTE: Because we're performing tasks *after* we informed the JavaScript of success/error, 87 | // we need to make sure that those callbacks don't mess with the stack or prematurely 88 | // unblock anything vital to the still running C function 89 | // see https://github.com/kripken/emscripten/issues/3307#issuecomment-90999205 90 | if (status == 0) { 91 | EM_ASM_INT({ 92 | Sass._sassCompileEmscriptenSuccess( 93 | pointerToString($0), 94 | pointerToJson($1), 95 | pointerToStringArray($2) 96 | ); 97 | }, 98 | sass_context_get_output_string(ctx), 99 | sass_context_get_source_map_string(ctx), 100 | sass_context_get_included_files(ctx) 101 | ); 102 | } else { 103 | EM_ASM_INT({ 104 | Sass._sassCompileEmscriptenError( 105 | pointerToJson($0), 106 | pointerToString($1) 107 | ); 108 | }, 109 | sass_context_get_error_json(ctx), 110 | sass_context_get_error_message(ctx) 111 | ); 112 | } 113 | 114 | // clean up 115 | if (compile_file) { 116 | sass_delete_file_context(file_ctx); 117 | } else { 118 | sass_delete_data_context(data_ctx); 119 | } 120 | } 121 | 122 | Sass_Import_List sass_importer_emscripten( 123 | const char* cur_path, 124 | Sass_Importer_Entry cb, 125 | struct Sass_Compiler* comp 126 | ) { 127 | struct Sass_Import* previous = sass_compiler_get_last_import(comp); 128 | const char* prev_base = sass_import_get_abs_path(previous); 129 | // struct Sass_Context* ctx = sass_compiler_get_context(comp); 130 | // struct Sass_Options* opts = sass_context_get_options(ctx); 131 | 132 | // flag denoting if JS importer callback has completed 133 | bool done = false; 134 | 135 | // kick off the (potentially) asynchronous JS importer callback 136 | // NOTE: there can only be one importer running concurrently 137 | EM_ASM_INT({ 138 | Importer.find( 139 | pointerToString($0), 140 | pointerToString($1) 141 | ); 142 | }, cur_path, prev_base); 143 | 144 | 145 | // check if the JS importer callback has already finished, 146 | // otherwise abuse emscripten_sleep() to interrupt the 147 | // otherwise synchronous execution of the C function for 148 | // 20ms to give the (potentially) asynchronous JS importer 149 | // callback a chance to breathe. 150 | // see https://github.com/kripken/emscripten/wiki/Emterpreter#emterpreter-async-run-synchronous-code 151 | while (true) { 152 | done = (bool)EM_ASM_INT({ 153 | return Number(Importer.finished()); 154 | }, 0); 155 | 156 | if (done) { 157 | break; 158 | } 159 | 160 | emscripten_sleep(20); 161 | } 162 | 163 | // the JS importer callback could have reported an unrecoverable error 164 | char *error = (char *)EM_ASM_INT({ 165 | return Number(Importer.error()); 166 | }, 0); 167 | 168 | if (error) { 169 | Sass_Import_List list = sass_make_import_list(1); 170 | list[0] = sass_make_import_entry(cur_path, 0, 0); 171 | sass_import_set_error(list[0], strdup(error), 0, 0); 172 | return list; 173 | } 174 | 175 | // obtain path and or content provided by the JS importer callback 176 | char *path = (char *)EM_ASM_INT({ 177 | return Number(Importer.path()); 178 | }, 0); 179 | char *content = (char *)EM_ASM_INT({ 180 | return Number(Importer.content()); 181 | }, 0); 182 | 183 | if (content || path) { 184 | Sass_Import_List list = sass_make_import_list(1); 185 | // TODO: figure out if strdup() is really required 186 | list[0] = sass_make_import_entry(strdup(path ? path : cur_path), content ? strdup(content) : 0, 0); 187 | return list; 188 | } 189 | 190 | // JS importer callback did not find anything, nor did it report an error, 191 | // have the next importer (likely libsass default file loader) determine 192 | // how to proceed 193 | return NULL; 194 | } 195 | -------------------------------------------------------------------------------- /test/test.importer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var expect = require('chai').expect; 4 | var Sass = require('../dist/sass.sync.js'); 5 | 6 | describe('importer', function() { 7 | 8 | it('should noop for no content', function(done) { 9 | var source = '@import "testfile";'; 10 | var expected = '.deeptest{content:"loaded"}.testfile{content:"loaded"}\n'; 11 | var expectedFiles = [ 12 | '/sass/sub/deeptest.scss', 13 | '/sass/testfile.scss' 14 | ]; 15 | var expectedBuffer = [ 16 | {'current':'testfile', 'previous':'stdin', 'path':'/sass/testfile.scss'}, 17 | {'current':'sub/deeptest', 'previous':'/sass/testfile.scss', 'path':'/sass/sub/deeptest.scss'} 18 | ]; 19 | var buffer = []; 20 | 21 | Sass.clearFiles(); 22 | Sass.importer(function(request, done) { 23 | buffer.push({ 24 | current: request.current, 25 | previous: request.previous, 26 | path: request.path, 27 | }); 28 | 29 | done(); 30 | }); 31 | 32 | Sass.options('defaults'); 33 | Sass.options({ style: Sass.style.compressed }); 34 | 35 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 36 | Sass.writeFile('sub/deeptest.scss', '.deeptest { content: "loaded"; }'); 37 | 38 | Sass.compile(source, function(result) { 39 | expect(result.text).to.equal(expected); 40 | expect(JSON.stringify(buffer)).to.equal(JSON.stringify(expectedBuffer)); 41 | expect(JSON.stringify(result.files)).to.equal(JSON.stringify(expectedFiles)); 42 | 43 | done(); 44 | }); 45 | }); 46 | 47 | it('should rewrite paths', function(done) { 48 | var source = '@import "testfile";'; 49 | var expected = '.rewritten{content:"loaded"}.testfile{content:"loaded"}\n'; 50 | var expectedFiles = [ 51 | '/sass/sub/rewritten.scss', 52 | '/sass/testfile.scss' 53 | ]; 54 | 55 | Sass.clearFiles(); 56 | Sass.importer(function(request, done) { 57 | var result = {}; 58 | if (request.current === 'sub/deeptest') { 59 | result.path = '/sass/sub/rewritten.scss'; 60 | } 61 | 62 | done(result); 63 | }); 64 | 65 | Sass.options('defaults'); 66 | Sass.options({ style: Sass.style.compressed }); 67 | 68 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 69 | Sass.writeFile('sub/rewritten.scss', '.rewritten { content: "loaded"; }'); 70 | 71 | Sass.compile(source, function(result) { 72 | expect(result.text).to.equal(expected); 73 | expect(JSON.stringify(result.files)).to.equal(JSON.stringify(expectedFiles)); 74 | 75 | done(); 76 | }); 77 | }); 78 | 79 | it('should rewrite content', function(done) { 80 | var source = '@import "testfile";'; 81 | var expected = '.yolo{content:"injected"}.testfile{content:"loaded"}\n'; 82 | var expectedFiles = [ 83 | '/sass/sub/yolo.scss', 84 | '/sass/testfile.scss' 85 | ]; 86 | 87 | Sass.clearFiles(); 88 | Sass.importer(function(request, done) { 89 | var result = {}; 90 | if (request.current === 'sub/deeptest') { 91 | result.path = '/sass/sub/yolo.scss'; 92 | result.content = '.yolo { content: "injected"; }'; 93 | } 94 | 95 | done(result); 96 | }); 97 | 98 | Sass.options('defaults'); 99 | Sass.options({ style: Sass.style.compressed }); 100 | 101 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 102 | Sass.writeFile('sub/yolo.scss', '.yolo { content: "loaded"; }'); 103 | 104 | Sass.compile(source, function(result) { 105 | expect(result.text).to.equal(expected); 106 | // https://github.com/sass/libsass/issues/1040 107 | expect(JSON.stringify(result.files)).to.equal(JSON.stringify(expectedFiles)); 108 | 109 | done(); 110 | }); 111 | }); 112 | 113 | it('should transport errors', function(done) { 114 | var source = '@import "testfile";'; 115 | 116 | Sass.clearFiles(); 117 | Sass.importer(function(request, done) { 118 | var result = {}; 119 | if (request.current === 'sub/deeptest') { 120 | result.error = 'nope nope nope'; 121 | } 122 | 123 | done(result); 124 | }); 125 | 126 | Sass.options('defaults'); 127 | Sass.options({ style: Sass.style.compressed }); 128 | 129 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 130 | Sass.writeFile('sub/yolo.scss', '.yolo { content: "loaded"; }'); 131 | 132 | Sass.compile(source, function(result) { 133 | expect(result.state).not.to.equal(0); 134 | expect(result.message).to.equal('nope nope nope'); 135 | expect(result.file).to.equal('/sass/testfile.scss'); 136 | expect(result.line).to.equal(1); 137 | expect(result.column).to.equal(9); 138 | 139 | done(); 140 | }); 141 | }); 142 | 143 | it('should catch errors', function(done) { 144 | var source = '@import "testfile";'; 145 | 146 | Sass.clearFiles(); 147 | Sass.importer(function(request, done) { 148 | if (request.current === 'sub/deeptest') { 149 | throw new Error('nope nope nope'); 150 | } 151 | 152 | done(); 153 | }); 154 | 155 | Sass.options('defaults'); 156 | Sass.options({ style: Sass.style.compressed }); 157 | 158 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 159 | Sass.writeFile('sub/yolo.scss', '.yolo { content: "loaded"; }'); 160 | 161 | Sass.compile(source, function(result) { 162 | expect(result.state).not.to.equal(0); 163 | expect(result.message).to.equal('nope nope nope'); 164 | expect(result.file).to.equal('/sass/testfile.scss'); 165 | expect(result.line).to.equal(1); 166 | expect(result.column).to.equal(9); 167 | 168 | done(); 169 | }); 170 | }); 171 | 172 | it('should be async', function(done) { 173 | var source = '@import "testfile";'; 174 | var expected = '.rewritten{content:"loaded"}.testfile{content:"loaded"}\n'; 175 | var expectedFiles = [ 176 | '/sass/sub/rewritten.scss', 177 | '/sass/testfile.scss' 178 | ]; 179 | 180 | Sass.clearFiles(); 181 | Sass.importer(function(request, done) { 182 | var result = {}; 183 | if (request.current === 'sub/deeptest') { 184 | result.path = '/sass/sub/rewritten.scss'; 185 | } 186 | 187 | setTimeout(function() { 188 | done(result); 189 | }, 100); 190 | }); 191 | 192 | Sass.options('defaults'); 193 | Sass.options({ style: Sass.style.compressed }); 194 | 195 | Sass.writeFile('testfile.scss', '@import "sub/deeptest";\n.testfile { content: "loaded"; }'); 196 | Sass.writeFile('sub/rewritten.scss', '.rewritten { content: "loaded"; }'); 197 | 198 | Sass.compile(source, function(result) { 199 | expect(result.text).to.equal(expected); 200 | expect(JSON.stringify(result.files)).to.equal(JSON.stringify(expectedFiles)); 201 | 202 | done(); 203 | }); 204 | }); 205 | 206 | it('should accept options', function(done) { 207 | var source = '@import "testfile";'; 208 | var expected = '.hello{content:"world"}\n'; 209 | 210 | Sass.importer(function(request, done) { 211 | done({ 212 | content: request.options.gustav, 213 | }); 214 | }); 215 | 216 | Sass.options('defaults'); 217 | Sass.options({ style: Sass.style.compressed }); 218 | 219 | var options = { 220 | importer: { 221 | gustav: '.hello { content: "world" }', 222 | }, 223 | }; 224 | 225 | Sass.compile(source, options, function(result) { 226 | expect(result.text).to.equal(expected); 227 | done(); 228 | }); 229 | }); 230 | 231 | it('should use correct options', function(done) { 232 | var source = '@import "testfile";'; 233 | var alpha = '.alpha{content:"alpha"}\n'; 234 | var bravo = '.bravo{content:"bravo"}\n'; 235 | var charlie = '.charlie{content:"charlie"}\n'; 236 | 237 | var remaining = 4; 238 | var _done = function() { 239 | remaining--; 240 | if (!remaining) { 241 | done(); 242 | } 243 | }; 244 | 245 | Sass.options('defaults'); 246 | Sass.options({ style: Sass.style.compressed }); 247 | 248 | Sass.importer(function(request, done) { 249 | done({ 250 | content: request.options, 251 | }); 252 | }); 253 | 254 | Sass.options({ importer: '.alpha { content: "alpha"; }' }); 255 | Sass.compile(source, function(result) { 256 | expect(result.text).to.equal(alpha, 'alpha'); 257 | _done(); 258 | }); 259 | 260 | Sass.options({ importer: '.bravo { content: "bravo"; }' }); 261 | Sass.compile(source, function(result) { 262 | expect(result.text).to.equal(bravo, 'first bravo'); 263 | _done(); 264 | }); 265 | 266 | Sass.compile(source, { importer: '.charlie { content: "charlie"; }' }, function(result) { 267 | expect(result.text).to.equal(charlie, 'charlie'); 268 | _done(); 269 | }); 270 | 271 | Sass.compile(source, function(result) { 272 | expect(result.text).to.equal(bravo, 'second bravo'); 273 | _done(); 274 | }); 275 | }); 276 | 277 | }); -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Sass.js Changelog 2 | 3 | ## 0.9.10 (April 20th 2016) ## 4 | 5 | * upgrading to [libsass 3.3.6](https://github.com/sass/libsass/releases/tag/3.3.6) 6 | 7 | ## 0.9.9 (April 20th 2016) ## 8 | 9 | * upgrading to [libsass 3.3.5](https://github.com/sass/libsass/releases/tag/3.3.5) 10 | 11 | ## 0.9.8 (April 7th 2016) ## 12 | 13 | * upgrading to [libsass 3.3.4](https://github.com/sass/libsass/releases/tag/3.3.4) 14 | * upgrading to [emscripten 1.36.1](https://github.com/kripken/emscripten/releases/tag/1.36.1) 15 | 16 | ## 0.9.7 (February 4th 2016) ## 17 | 18 | * refactoring sass.js initialization when emscripten is ready 19 | 20 | ## 0.9.6 (February 1st 2016) ## 21 | 22 | * adding option `importer` to provide data to [importer callbacks](https://github.com/medialize/sass.js/#importer-callback-function) - ([Issue #43](https://github.com/medialize/sass.js/issues/43)) 23 | 24 | ## 0.9.5 (January 23rd 2016) ## 25 | 26 | * upgrading to [libsass 3.3.3](https://github.com/sass/libsass/releases/tag/3.3.3) 27 | * upgrading to [emscripten 1.35.22](https://github.com/kripken/emscripten/releases/tag/1.35.22) 28 | * inline memory init file and drop `libsass.js.mem` - ([Issue #42](https://github.com/medialize/sass.js/issues/42)) 29 | 30 | ## 0.9.4 (October 27th 2015) ## 31 | 32 | * upgrading to [libsass 3.3.1](https://github.com/sass/libsass/releases/tag/3.3.1) 33 | * upgrading to [emscripten 1.35.4](https://github.com/kripken/emscripten/releases/tag/1.35.4) 34 | 35 | ## 0.9.3 (October 24th 2015) ## 36 | 37 | * upgrading to [libsass 3.3.0](https://github.com/sass/libsass/releases/tag/3.3.0) 38 | * upgrading to [emscripten 1.35.2](https://github.com/kripken/emscripten/releases/tag/1.35.2) 39 | 40 | ## 0.9.2 (June 14th 2015) ## 41 | 42 | * upgrading to [libsass 3.2.5](https://github.com/sass/libsass/releases/tag/3.2.5) 43 | 44 | ## 0.9.1 (May 30th 2015) ## 45 | 46 | * fixing automatic path detection (for loading `sass.worker.js` and `libsass.js.mem`), to make `sass.sync.js` load `libsass.js.mem` relative to itself (Node and browser) - ([Issue #33](https://github.com/medialize/sass.js/issues/33)) 47 | 48 | ## 0.9.0 (May 21st 2015) ## 49 | 50 | **NOTE:** This release contains breaking changes! 51 | 52 | * upgrading to [libsass 3.2.4](https://github.com/sass/libsass/releases/tag/3.2.4) 53 | * fixing worker API to avoid throwing `DataCloneError` because `postMessage` can't handle `Error` instances 54 | * improving worker API to find `sass.worker.js` itself when loaded through simple ` 26 | 32 | ``` 33 | 34 | If you load `sass.js` by other means than then ` 62 | 69 | ``` 70 | 71 | ### Synchronous API (NodeJS) 72 | 73 | While you probably want to use [node-sass](https://github.com/sass/node-sass) for performance reasons, Sass.js also runs on NodeJS. You can use the synchronous API (as in "executed in the main EventLoop") as follows: 74 | 75 | ```js 76 | Sass = require('sass.js'); 77 | var scss = '$someVar: 123px; .some-selector { width: $someVar; }'; 78 | Sass.compile(scss, function(result) { 79 | console.log(result); 80 | }); 81 | ``` 82 | 83 | [webworker-threads](https://www.npmjs.com/package/webworker-threads) could be the key to running Sass.js in a non-blocking fashion, but it (currently) has its problems with [typed arrays](https://github.com/audreyt/node-webworker-threads/issues/18#issuecomment-92098583). 84 | 85 | ### Synchronous API From Source (browser) 86 | 87 | After cloning this repository you can run `grunt libsass:prepare libsass:build` (explained below) and then run Sass.js off its source files by running [`sass.source.html`](sass.source.html) 88 | 89 | ```html 90 | 91 | 92 | 93 | 94 | 95 | 96 | 102 | ``` 103 | 104 | --- 105 | 106 | 107 | ## Using the Sass.js API 108 | 109 | ```js 110 | // initialize a Sass instance 111 | // Note: this is not necessary in the synchronous API 112 | var sass = new Sass('path/to/sass.worker.js'); 113 | 114 | // destruct/destroy/clean up a Sass instance 115 | // Note: this is not necessary in the synchronous API 116 | sass.destroy(); 117 | 118 | // globally set the URL where the the sass worker file is located 119 | // so it does not have to be supplied to every constructor 120 | Sass.setWorkerUrl('path/to/sass.worker.js'); 121 | var sass = new Sass(); 122 | 123 | 124 | // compile text to SCSS 125 | sass.compile(text, function callback(result) { 126 | // (object) result compilation result 127 | // (number) result.status success status (0 in success case) 128 | // (string) result.text compiled CSS string 129 | // (object) result.map SourceMap 130 | // (array) result.files list of files used during compilation 131 | // ------------------------------- 132 | // (number) result.status success status (not 0 in error case) 133 | // (string) result.file the file path the occurred in 134 | // (number) result.line the line the error occurred on 135 | // (number) result.column the character offset in that line the error began with 136 | // (string) result.message the error message 137 | // (string) result.formatted human readable error message containing all details 138 | }); 139 | // it is possible to set options for a specific compile() call, 140 | // rather than "gobally" for all compile() calls. 141 | // see Sass.options() for details 142 | sass.compile(text, options, callback); 143 | 144 | // compile file to SCSS 145 | sass.compileFile(filename, callback); 146 | sass.compileFile(filename, options, callback); 147 | 148 | 149 | // set libsass compile options 150 | // (provided options are merged onto previously set options) 151 | sass.options({ 152 | // Format output: nested, expanded, compact, compressed 153 | style: Sass.style.nested, 154 | // Precision for outputting fractional numbers 155 | // (-1 will use the libsass default, which currently is 5) 156 | precision: -1, 157 | // If you want inline source comments 158 | comments: false, 159 | // Treat source_string as SASS (as opposed to SCSS) 160 | indentedSyntax: false, 161 | // String to be used for indentation 162 | indent: ' ', 163 | // String to be used to for line feeds 164 | linefeed: '\n', 165 | 166 | // data passed through to the importer callback 167 | // must be serializable to JSON 168 | importer: {}, 169 | 170 | // Path to source map file 171 | // Enables the source map generating 172 | // Used to create sourceMappingUrl 173 | sourceMapFile: 'file', 174 | // Pass-through as sourceRoot property 175 | sourceMapRoot: 'root', 176 | // The input path is used for source map generation. 177 | // It can be used to define something with string 178 | // compilation or to overload the input file path. 179 | // It is set to "stdin" for data contexts 180 | // and to the input file on file contexts. 181 | inputPath: 'stdin', 182 | // The output path is used for source map generation. 183 | // Libsass will not write to this file, it is just 184 | // used to create information in source-maps etc. 185 | outputPath: 'stdout', 186 | // Embed included contents in maps 187 | sourceMapContents: true, 188 | // Embed sourceMappingUrl as data uri 189 | sourceMapEmbed: false, 190 | // Disable sourceMappingUrl in css output 191 | sourceMapOmitUrl: true, 192 | }, function callback(){}); 193 | // reset options to Sass.js defaults (listed above) 194 | sass.options('defaults', function callback(){}); 195 | 196 | 197 | // register a file to be available for @import 198 | sass.writeFile(filename, text, function callback(success) { 199 | // (boolean) success is 200 | // `true` when the write was OK, 201 | // `false` when it failed 202 | }); 203 | // register multiple files 204 | sass.writeFile({ 205 | 'filename-1.scss': 'content-1', 206 | 'filename-2.scss': 'content-2', 207 | }, function callback(result) { 208 | // (object) result is 209 | // result['filename-1.scss']: success 210 | // result['filename-2.scss']: success 211 | // (boolean) success is 212 | // `true` when the write was OK, 213 | // `false` when it failed 214 | }); 215 | 216 | // remove a file 217 | sass.removeFile(filename, function callback(success) { 218 | // (boolean) success is 219 | // `true` when deleting the file was OK, 220 | // `false` when it failed 221 | }); 222 | // remove multiple files 223 | sass.removeFile([filename1, filename2], function callback(result) { 224 | // (object) result is 225 | // result[filename1]: success 226 | // result[filename2]: success 227 | // (boolean) success is 228 | // `true` when deleting the file was OK, 229 | // `false` when it failed 230 | }); 231 | 232 | // get a file's content 233 | sass.readFile(filename, function callback(content) { 234 | // (string) content is the file's content, 235 | // `undefined` when the read failed 236 | }); 237 | // read multiple files 238 | sass.readFile([filename1, filename2], function callback(result) { 239 | // (object) result is 240 | // result[filename1]: content 241 | // result[filename2]: content 242 | // (string) content is the file's content, 243 | // `undefined` when the read failed 244 | }); 245 | 246 | // list all files (regardless of directory structure) 247 | sass.listFiles(function callback(list) { 248 | // (array) list contains the paths of all registered files 249 | }); 250 | 251 | // remove all files 252 | sass.clearFiles(function callback() {}); 253 | 254 | // preload a set of files 255 | // see chapter »Working With Files« below 256 | sass.preloadFiles(remoteUrlBase, localDirectory, filesMap, function callback() {}); 257 | 258 | // register a set of files to be (synchronously) loaded when required 259 | // see chapter »Working With Files« below 260 | // Note: this method is not available in the synchronous API 261 | sass.lazyFiles(remoteUrlBase, localDirectory, filesMap, function callback() {}); 262 | 263 | // intercept file loading requests from libsass 264 | sass.importer(function(request, done) { 265 | // (object) request 266 | // (string) request.current path libsass wants to load (content of »@import "";«) 267 | // (string) request.previous absolute path of previously imported file ("stdin" if first) 268 | // (string) request.resolved currentPath resolved against previousPath 269 | // (string) request.path absolute path in file system, null if not found 270 | // (mixed) request.options the value of options.importer 271 | // ------------------------------- 272 | // (object) result 273 | // (string) result.path the absolute path to load from file system 274 | // (string) result.content the content to use instead of loading a file 275 | // (string) result.error the error message to print and abort the compilation 276 | 277 | // asynchronous callback 278 | done(result); 279 | }); 280 | ``` 281 | 282 | ### `compile()` Response Object 283 | 284 | Compiling the following source: 285 | 286 | ```scss 287 | $someVar: 123px; .some-selector { width: $someVar; } 288 | ``` 289 | 290 | Yields the following `result` object: 291 | 292 | ```js 293 | { 294 | // status 0 means everything is ok, 295 | // any other value means an error occured 296 | "status": 0, 297 | // the compiled CSS 298 | "text": ".some-selector {\n width: 123px; }\n", 299 | // the SourceMap for this compilation 300 | "map": { 301 | "version": 3, 302 | "sourceRoot": "root", 303 | "file": "stdout", 304 | "sources": [ 305 | "stdin" 306 | ], 307 | "sourcesContent": [ 308 | "$someVar: 123px; .some-selector { width: $someVar; }" 309 | ], 310 | "mappings": "AAAiB,cAAc,CAAC;EAAE,KAAK,EAA7B,KAAK,GAAkB", 311 | "names": [] 312 | }, 313 | // the files that were used during the compilation 314 | "files": [] 315 | } 316 | ``` 317 | 318 | Compiling the following (invalid) source: 319 | 320 | ```scss 321 | $foo: 123px; 322 | 323 | .bar { 324 | width: $bar; 325 | } 326 | 327 | bad-token-test 328 | ``` 329 | 330 | Yields the following `result` object: 331 | 332 | ```js 333 | { 334 | // status other than 0 means an error occured 335 | "status": 1, 336 | // the file the problem occurred in 337 | "file": "stdin", 338 | // the line the problem occurred on 339 | "line": 7, 340 | // the character on the line the problem started with 341 | "column": 1, 342 | // the problem description 343 | "message": "invalid top-level expression", 344 | // human readable formatting of the error 345 | "formatted": "Error: invalid top-level expression\n on line 7 of stdin\n>> bad-token-test\n ^\n" 346 | } 347 | ``` 348 | 349 | where the `formatted` properties contains a human readable presentation of the problem: 350 | 351 | ``` 352 | Error: invalid top-level expression 353 | on line 7 of stdin 354 | >> bad-token-test 355 | ^ 356 | ``` 357 | 358 | ### Compiling Concurrently 359 | 360 | Using the worker API, multiple Sass instances can be initialized to compile sources in *parallel*, rather than in *sequence*: 361 | 362 | ``` 363 | // compile sources in sequence (default behavior) 364 | var sass = new Sass('path/to/sass.worker.js'); 365 | sass.compile(source1, callback1); 366 | sass.compile(source2, callback2); 367 | 368 | // compile sources in parallel 369 | Sass.setWorkerUrl('path/to/sass.worker.js') 370 | var sass1 = new Sass(); 371 | var sass2 = new Sass(); 372 | sass1.compile(source1, callback1); 373 | sass2.compile(source2, callback2); 374 | ``` 375 | 376 | ### Working With Files 377 | 378 | Chances are you want to use one of the readily available Sass mixins (e.g. [drublic/sass-mixins](https://github.com/drublic/Sass-Mixins) or [Bourbon](https://github.com/thoughtbot/bourbon)). While Sass.js doesn't feature a full-blown "loadBurbon()", registering individual files is possible: 379 | 380 | ```js 381 | Sass.writeFile('one.scss', '.one { width: 123px; }'); 382 | Sass.writeFile('some-dir/two.scss', '.two { width: 123px; }'); 383 | Sass.compile('@import "one"; @import "some-dir/two";', function(result) { 384 | console.log(result.text); 385 | }); 386 | ``` 387 | 388 | outputs 389 | 390 | ```css 391 | .one { 392 | width: 123px; } 393 | 394 | .two { 395 | width: 123px; } 396 | ``` 397 | 398 | To make things somewhat more comfortable, Sass.js provides 2 methods to load batches of files. `Sass.lazyFiles()` registers the files and only loads them when they're loaded by libsass - the catch is this HTTP request has to be made synchronously (and thus only works within the WebWorker). `Sass.preloadFiles()` downloads the registered files immediately (asynchronously, also working in the synchronous API): 399 | 400 | ```js 401 | // HTTP requests are made relative to worker 402 | var base = '../scss/'; 403 | // equals 'http://medialize.github.io/sass.js/scss/' 404 | 405 | // the directory files should be made available in 406 | var directory = ''; 407 | 408 | // the files to load (relative to both base and directory) 409 | var files = [ 410 | 'demo.scss', 411 | 'example.scss', 412 | '_importable.scss', 413 | 'deeper/_some.scss', 414 | ]; 415 | 416 | // register the files to load when necessary 417 | Sass.lazyFiles(base, directory, files, function() { console.log('files registered, not loaded') }); 418 | 419 | // download the files immediately 420 | Sass.preloadFiles(base, directory, files, function() { console.log('files loaded') }); 421 | ``` 422 | 423 | Note that `Sass.lazyFiles()` can slow down the perceived performance of `Sass.compile()` because of the synchronous HTTP requests. They're made in sequence, not in parallel. 424 | 425 | While Sass.js does not plan on providing file maps to SASS projects, it contains two mappings to serve as an example how your project can approach the problem: [`maps/bourbon.js`](maps/bourbon.js) and [`maps/drublic-sass-mixins.js`](maps/drublic-sass-mixins.js). 426 | 427 | 428 | ### Importer Callback Function 429 | 430 | Since libsass 3.2 a callback allows us to hook into the compilation process. The callback allows us to intercept libsass' attempts to process `@import "";` declarations. The request object contains the `` to be imported in `request.current` and the fully qualified path of the file containing the `@import` statement in `request.previous`. For convenience `request.resolved` contains a relative resolution of `current` against `previous`. To allow importer callbacks to overwrite *everything*, but not have to deal with any of libsass' default file resolution `request.path` contains the file's path found in the file system (including the variations like `@import "foo";` resolving to `…/_foo.scss`). 431 | 432 | ```js 433 | // register a custom importer callback 434 | Sass.importer(function(request, done) { 435 | if (request.path) { 436 | // Sass.js already found a file, 437 | // we probably want to just load that 438 | done(); 439 | } else if (request.current === 'content') { 440 | // provide a specific content 441 | // (e.g. downloaded on demand) 442 | done({ 443 | content: '.some { content: "from anywhere"; }' 444 | }) 445 | } else if (request.current === 'redirect') { 446 | // provide a specific content 447 | done({ 448 | path: '/sass/to/some/other.scss' 449 | }) 450 | } else if (request.current === 'error') { 451 | // provide content directly 452 | // note that there is no cache 453 | done({ 454 | error: 'import failed because bacon.' 455 | }) 456 | } else { 457 | // let libsass handle the import 458 | done(); 459 | } 460 | }); 461 | // unregister custom reporter callback 462 | Sass.importer(null); 463 | ``` 464 | 465 | 466 | --- 467 | 468 | 469 | ## Building Sass.js ## 470 | 471 | To compile libsass to JS you need [emscripten](http://emscripten.org), to build Sass.js additionally need [grunt](http://gruntjs.com/). 472 | 473 | ```bash 474 | grunt build 475 | # destination: 476 | # dist/sass.js 477 | # dist/sass.min.js 478 | # dist/sass.worker.js 479 | # dist/versions.json 480 | # dist/worker.js 481 | # dist/worker.min.js 482 | 483 | ``` 484 | 485 | ### Building Sass.js in emscripten debug mode ### 486 | 487 | ```bash 488 | grunt build:debug 489 | # destination: 490 | # dist/sass.js 491 | # dist/sass.min.js 492 | # dist/sass.worker.js 493 | # dist/versions.json 494 | # dist/worker.js 495 | # dist/worker.min.js 496 | ``` 497 | 498 | ### Building only libsass.js ### 499 | 500 | ```bash 501 | # import libsass repository 502 | grunt libsass:prepare 503 | # invoke emscripten 504 | grunt libsass:build 505 | # invoke emscripten in debug mode 506 | grunt libsass:debug 507 | 508 | # destination: 509 | # libsass/libsass/lib/libsass.js 510 | ``` 511 | 512 | If you don't like grunt, run with the shell: 513 | 514 | ```bash 515 | LIBSASS_VERSION="3.1.0" 516 | # import libsass repository 517 | (cd libsass && ./prepare.sh ${LIBSASS_VERSION}) 518 | # invoke emscripten 519 | (cd libsass && ./build.sh ${LIBSASS_VERSION}) 520 | # invoke emscripten in debug mode 521 | (cd libsass && ./build.sh ${LIBSASS_VERSION} debug) 522 | 523 | # destination: 524 | # libsass/libsass/lib/libsass.js 525 | ``` 526 | 527 | --- 528 | 529 | ## Changelog 530 | 531 | * [see CHANGELOG.md](./CHANGELOG.md) 532 | 533 | ## Authors 534 | 535 | * [Christian Kruse](https://github.com/ckruse) - [@cjk101010](https://twitter.com/cjk101010) 536 | * [Sebastian Golasch](https://github.com/asciidisco) - [@asciidisco](https://twitter.com/asciidisco) 537 | * [Rodney Rehm](http://rodneyrehm.de/en/) - [@rodneyrehm](https://twitter.com/rodneyrehm) 538 | 539 | ## Credits 540 | 541 | * the [sass group](https://github.com/sass), especially [team libsass](https://github.com/sass/libsass) 542 | * team [emscripten](https://github.com/kripken/emscripten), especially [Alon Zakai](https://github.com/kripken) 543 | 544 | ## License 545 | 546 | Sass.js is - as [libsass](https://github.com/sass/libsass) and [emscripten](https://github.com/kripken/emscripten/) are - published under the [MIT License](http://opensource.org/licenses/mit-license). 547 | --------------------------------------------------------------------------------