diff --git a/lib/file_types/code.js b/lib/file_types/code.js new file mode 100644 index 0000000..3b10fd2 --- /dev/null +++ b/lib/file_types/code.js @@ -0,0 +1,19 @@ +sc_require('../mixins/content_filters'); + +BT.CodeFile = BT.File.extend(BT.ContentFiltersMixin, +{ + isCode: true, + + content: null, + + contentObservers: null, + + updateContent: function() + { + var raw = this.get('rawContent'); + this.set('content', raw ? this.filterContent(raw.toString()) : null); + + var deps = this.getPath('contentObservers.content'); + if(deps) for(var i = 0, len = deps.get('length'); i < len; ++i) deps[i].updateContent(); + }.observes('rawContent'), +}) diff --git a/lib/file_types/scss.js b/lib/file_types/scss.js new file mode 100644 index 0000000..b414021 --- /dev/null +++ b/lib/file_types/scss.js @@ -0,0 +1,107 @@ +sc_require('code'); + +BT.SCSSFile = BT.CodeFile.extend( +{ + extension: 'scss', + isSCSSFile: true, + isStylesheet: true, + contentType: 'text/css', + language: 'any', + + contentFilters: [ + 'filterStopIfPartial', + 'filterParseSASS', + ], + + _sass_importer: function(path, from, done) + { + var pathlib = require('path'); + + var pathBasename = pathlib.basename(path); + if('_' !== pathBasename.charAt(0)) pathBasename = '_' + pathBasename; + if('.scss' !== pathBasename.substr(-5).toLowerCase()) pathBasename = pathBasename + '.scss'; + path = pathlib.join(pathlib.dirname(path), pathBasename); + + var fullpath = ('stdin' === from) + ? pathlib.join(pathlib.dirname(this.get('path')), path) + : pathlib.join(pathlib.dirname(from), path); + var file = this.getPath('framework.files.stylesheets').findProperty('path', fullpath) + if(file && file.isSCSSFile) + { + var observers = file.contentObservers; + if(!observers) observers = file.contentObservers = BT.DependenciesController.create(); + if(!observers.contains(this)) observers.addObject(this); + } + + return null; + }, + + _sass_sc_static_handler: function(url) + { + var sass = require('node-sass'); + var SassString = sass.types.String; + var className = SC._object_className(this.constructor); + + var res = this.get('framework').findResourceFor(url); + if(!res || 0 === res.length) + { + BT.Logger.warn(className + "#_sass_sc_static_handler: found no files for %@ in file %@".fmt(url, this.get('path'))); + return new SassString('url(/* ' + url + ' */)'); + } + + var file = res[0]; + if(res.length > 1) + { + BT.Logger.warn(className + "#_sass_sc_static_handler: found multiple files for %@ in file %@, taking the first (%@)".fmt(url, this.get('path'), file.get('path'))); + } + + var deps = this.resourceDependencies; + if(!deps.contains(file)) deps.addObject(file); + + var ret = this.getPath('framework.belongsTo.doRelativeBuild') + ? file.get('relativeUrl') + : file.get('url') + + return new SassString('url(' + ret + ')'); + }, + + /** + Parses an .scss file. + Paths in sc_static() or static_url() are relative to the calling file. + */ + filterParseSASS: function(content) + { + var self = this; + var ret = null + + try + { + ret = require('node-sass').renderSync({ + data: content, + includePaths: [require('path').dirname(this.get('path'))], + functions: { + 'sc_static($url)': function(url) { return self._sass_sc_static_handler(url.getValue()) }, + 'static_url($url)': function(url) { return self._sass_sc_static_handler(url.getValue()) }, + }, + importer: function(path, from, done) { return self._sass_importer(path, from, done) }, + }).css; + } + catch(e) { BT.Logger.warn("node-sass: " + e.formatted) } + return ret; + }, + + /** + Stops the processing of content because partials should not produce any output, + but should be imported from usual SCSS files. + */ + filterStopIfPartial: function(content) + { + return this.get('isPartial') ? null : content; + }, + + isPartial: function() + { + return '_' === require('path').basename(this.get('path')).charAt(0); + }.property('path').cacheable(), + +}) diff --git a/lib/filetypes.js b/lib/filetypes.js index 8e8f9df..d4ede85 100644 --- a/lib/filetypes.js +++ b/lib/filetypes.js @@ -3,6 +3,7 @@ sc_require('file_types/script'); sc_require('file_types/json'); sc_require('file_types/css'); +sc_require('file_types/scss'); sc_require('file_types/image'); sc_require('file_types/module_script'); sc_require('file_types/appcache'); @@ -13,6 +14,7 @@ sc_require('file_types/html'); BT.projectManager.registerFileClass("js", BT.ScriptFile); BT.projectManager.registerFileClass("json", BT.JSONFile); BT.projectManager.registerFileClass("css", BT.CSSFile); +BT.projectManager.registerFileClass("scss", BT.SCSSFile); BT.projectManager.registerFileClass("ejs", BT.TemplateFile); BT.projectManager.registerFileClass("html", BT.HTMLFile); diff --git a/lib/mixins/content_filters.js b/lib/mixins/content_filters.js new file mode 100644 index 0000000..c329d62 --- /dev/null +++ b/lib/mixins/content_filters.js @@ -0,0 +1,55 @@ +BT.ContentFiltersMixin = +{ + hasContentFiltersSupport: true, + + /** + Array of content filters. + Example filter configuration: + ```contentFilters: [ + function(content) { ... }, // anonymous function will be called + 'methodName', // this.methodName(content) method will be called + ['methodName', param1, param2, ...], // this.methodName(content, param1, param2) will be called with arbitrary number of parameters + [['SC.Object', 'methodName'], param1, param2, ...] // SC.Object.methodName(content) + [[SC.Object, function(content, param1) {}], param1, ...], + SC.Object.create(BT.ContentFilterMixin), // Apply filters from another object + ],``` + A filter may return `null` or an empty string to stop further filtering. + */ + contentFilters: [], + + filterContent: function(content) + { + var filters = this.get('contentFilters'); + for(var i = 0, len = filters.get('length'); i < len; ++i) + { + var filter = filters[i]; + if(filter.hasContentFiltersSupport) + { + content = filter.filterContent(content); + } + else + { + switch(SC.typeOf(filter)) + { + case SC.T_STRING: content = this[filter](content); break; + case SC.T_FUNCTION: content = filter.call(this, content); break; + case SC.T_ARRAY: { + var target = this, handler = filter.shift(); + if(SC.T_ARRAY === SC.typeOf(handler)) + { + target = handler.shift(); + handler = handler.shift(); + } + if(SC.T_STRING === SC.typeOf(target)) target = SC.objectForPropertyPath(target); + filter.unshift(content); + if(SC.T_STRING === SC.typeOf(handler)) content = target[handler].apply(target, filter); + else handler.apply(target, filter); + } + break; + } + } + if(SC.empty(content)) break; + } + return content; + }, +}