diff --git a/js/libs/css/css-builder.js b/js/libs/css/css-builder.js new file mode 100644 index 00000000..bc5b4491 --- /dev/null +++ b/js/libs/css/css-builder.js @@ -0,0 +1,251 @@ +define(['require', './normalize'], function(req, normalize) { + var nodePrint = function() {}; + if (requirejs.tools) + requirejs.tools.useLib(function(req) { + req(['node/print'], function(_nodePrint) { + nodePrint = _nodePrint; + }, function(){}); + }); + + var cssAPI = {}; + + function compress(css) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + try { + var csso = require.nodeRequire('csso'); + var csslen = css.length; + css = csso.justDoIt(css); + nodePrint('Compressed CSS output to ' + Math.round(css.length / csslen * 100) + '%.'); + return css; + } + catch(e) { + nodePrint('Compression module not installed. Use "npm install csso -g" to enable.'); + return css; + } + } + nodePrint('Compression not supported outside of nodejs environments.'); + return css; + } + + //load file code - stolen from text plugin + function loadFile(path) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + var fs = require.nodeRequire('fs'); + var file = fs.readFileSync(path, 'utf8'); + if (file.indexOf('\uFEFF') === 0) + return file.substring(1); + return file; + } + else { + var file = new java.io.File(path), + lineSeparator = java.lang.System.getProperty("line.separator"), + input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), 'utf-8')), + stringBuffer, line; + try { + stringBuffer = new java.lang.StringBuffer(); + line = input.readLine(); + if (line && line.length() && line.charAt(0) === 0xfeff) + line = line.substring(1); + stringBuffer.append(line); + while ((line = input.readLine()) !== null) { + stringBuffer.append(lineSeparator).append(line); + } + return String(stringBuffer.toString()); + } + finally { + input.close(); + } + } + } + + + function saveFile(path, data) { + if (typeof process !== "undefined" && process.versions && !!process.versions.node && require.nodeRequire) { + var fs = require.nodeRequire('fs'); + fs.writeFileSync(path, data, 'utf8'); + } + else { + var content = new java.lang.String(data); + var output = new java.io.BufferedWriter(new java.io.OutputStreamWriter(new java.io.FileOutputStream(path), 'utf-8')); + + try { + output.write(content, 0, content.length()); + output.flush(); + } + finally { + output.close(); + } + } + } + + //when adding to the link buffer, paths are normalised to the baseUrl + //when removing from the link buffer, paths are normalised to the output file path + function escape(content) { + return content.replace(/(["'\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r"); + } + + // NB add @media query support for media imports + var importRegEx = /@import\s*(url)?\s*(('([^']*)'|"([^"]*)")|\(('([^']*)'|"([^"]*)"|([^\)]*))\))\s*;?/g; + + var loadCSSFile = function(fileUrl) { + var css = loadFile(fileUrl); + + // normalize the css (except import statements) + css = normalize(css, fileUrl, baseUrl, cssBase); + + // detect all import statements in the css and normalize + var importUrls = []; + var importIndex = []; + var importLength = []; + var match; + while (match = importRegEx.exec(css)) { + var importUrl = match[4] || match[5] || match[7] || match[8] || match[9]; + + // normalize import url + if (importUrl.substr(importUrl.length - 5, 5) != '.less' && importUrl.substr(importUrl.length - 4, 4) != '.css') + importUrl += '.css'; + + // contains a protocol + if (importUrl.match(/:\/\//)) + continue; + + // relative to css base + if (importUrl.substr(0, 1) == '/' && cssBase) + importUrl = cssBase + importUrl; + else + importUrl = baseUrl + importUrl; + + importUrls.push(importUrl); + importIndex.push(importRegEx.lastIndex - match[0].length); + importLength.push(match[0].length); + } + + // load the import stylesheets and substitute into the css + for (var i = 0; i < importUrls.length; i++) + (function(i) { + var importCSS = loadCSSFile(importUrls[i]); + css = css.substr(0, importIndex[i]) + importCSS + css.substr(importIndex[i] + importLength[i]); + var lenDiff = importCSS.length - importLength[i]; + for (var j = i + 1; j < importUrls.length; j++) + importIndex[j] += lenDiff; + })(i); + + return css; + } + + + var baseUrl; + var cssBase; + var curModule; + cssAPI.load = function(name, req, load, config, parse) { + if (!baseUrl) + baseUrl = config.baseUrl; + + if (!cssBase) + cssBase = config.cssBase; + + if (config.modules) { + //run through the module list - the first one without a layer set is the current layer we are in + //allows to track the current layer number for layer-specific config + for (var i = 0; i < config.modules.length; i++) + if (config.modules[i].layer === undefined) { + curModule = i; + break; + } + } + + //store config + cssAPI.config = cssAPI.config || config; + + name += !parse ? '.css' : '.less'; + + var fileUrl = req.toUrl(name); + + //external URLS don't get added (just like JS requires) + if (fileUrl.substr(0, 7) == 'http://' || fileUrl.substr(0, 8) == 'https://') + return load(); + + //add to the buffer + _cssBuffer[name] = loadCSSFile(fileUrl); + + // parse if necessary + if (parse) + _cssBuffer[name] = parse(_cssBuffer[name]); + + load(); + } + + cssAPI.normalize = function(name, normalize) { + if (name.substr(name.length - 4, 4) == '.css') + name = name.substr(0, name.length - 4); + return normalize(name); + } + + //list of cssIds included in this layer + var _layerBuffer = []; + var _cssBuffer = []; + cssAPI.write = function(pluginName, moduleName, write, parse) { + //external URLS don't get added (just like JS requires) + if (moduleName.substr(0, 7) == 'http://' || moduleName.substr(0, 8) == 'https://' || moduleName.substr(0, 2) == '//') + return; + + var resourceName = moduleName + (!parse ? '.css' : '.less'); + _layerBuffer.push(_cssBuffer[resourceName]); + + var separateCSS = false; + if (cssAPI.config.separateCSS) + separateCSS = true; + if (typeof curModule == 'number' && cssAPI.config.modules[curModule].separateCSS !== undefined) + separateCSS = cssAPI.config.modules[curModule].separateCSS; + if (separateCSS) + write.asModule(pluginName + '!' + moduleName, 'define(function(){})'); + else + write("requirejs.s.contexts._.nextTick = function(f){f()}; require(['css'], function(css) { css.addBuffer('" + resourceName + "'); }); requirejs.s.contexts._.nextTick = requirejs.nextTick;"); + } + + cssAPI.onLayerEnd = function(write, data, parser) { + firstWrite = true; + //separateCSS parameter set either globally or as a layer setting + var separateCSS = false; + if (cssAPI.config.separateCSS) + separateCSS = true; + if (typeof curModule == 'number' && cssAPI.config.modules[curModule].separateCSS !== undefined) + separateCSS = cssAPI.config.modules[curModule].separateCSS; + curModule = null; + + //calculate layer css + var css = _layerBuffer.join(''); + + if (separateCSS) { + nodePrint('Writing CSS! file: ' + data.name + '\n'); + + //calculate the css output path for this layer + var path = this.config.appDir ? this.config.baseUrl + data.name + '.css' : cssAPI.config.out.replace(/\.js$/, '.css'); + + //renormalize the css to the output path + var output = compress(normalize(css, baseUrl, path)); + + saveFile(path, output); + } + else { + if (css == '') + return; + //write the injection and layer index into the layer + //prepare the css + css = escape(compress(css)); + + //the code below overrides async require functionality to ensure instant buffer injection + write("requirejs.s.contexts._.nextTick = function(f){f()}; require(['css'], function(css) { css.setBuffer('" + css + (parser ? "', true" : "'") + "); }); requirejs.s.contexts._.nextTick = requirejs.nextTick; "); + } + + //clear layer buffer for next layer + _layerBuffer = []; + } + + return cssAPI; +}); diff --git a/js/libs/less/less-builder.js b/js/libs/less/less-builder.js new file mode 100644 index 00000000..b296f92a --- /dev/null +++ b/js/libs/less/less-builder.js @@ -0,0 +1,42 @@ +define(['css/css-builder', './lessc-server', 'require'], function(css, lessc, req) { + var less = {}; + + var baseParts = req.toUrl('base_url').split('/'); + baseParts.pop(); + var baseUrl = baseParts.join('/'); + + // include the base url as a path + var parser = new lessc.Parser({ + paths: [baseUrl + '/'] + }); + + var parseLess = function(less) { + var CSS; + parser.parse(less, function(err, tree) { + if (err) + throw err; + CSS = tree.toCSS(); + }); + return CSS; + } + + less.normalize = function(name, normalize) { + if (name.substr(name.length - 5, 5) == '.less') + name = name.substr(0, name.length - 5); + return normalize(name); + } + + less.load = function(name, req, load, config) { + css.load(name, req, load, config, parseLess); + } + + less.write = function(pluginName, moduleName, write) { + css.write(pluginName, moduleName, write, parseLess); + } + + less.onLayerEnd = function(write, data) { + css.onLayerEnd(write, data, true); + } + + return less; +}); diff --git a/js/libs/less/lessc-server.js b/js/libs/less/lessc-server.js new file mode 100644 index 00000000..7866de45 --- /dev/null +++ b/js/libs/less/lessc-server.js @@ -0,0 +1,96 @@ +define(['./lessc'], function(less) { + + if (['node', 'rhino'].indexOf(less.mode) == -1) { + throw new Error('Environment not supported by require-less builder: ' + less.mode); + } + + var readFile = (function() { + if (less.mode === 'node') { + var fs = require.nodeRequire('fs'); + var path = require.nodeRequire('path'); + return function(pathname) { + return fs.readFileSync(pathname, 'utf-8'); + }; + } else if (less.mode === 'rhino') { + return function(pathname) { + return readFile(pathname, 'UTF-8'); + }; + } + }()); + + var checkPath = (function() { + if (less.mode === 'node') { + var fs = require.nodeRequire('fs'); + var path = require.nodeRequire('path'); + return function(pathname, file) { + try { + pathname = path.join(pathname, file); + fs.statSync(pathname); + return pathname; + } catch (e) { + return null; + } + }; + } else if (less.mode === 'rhino') { + return function(pathname, file) { + var f = new java.io.File(pathname, file); + if (f.isFile()) { + return f.getPath(); + } else { + return null; + } + }; + } + }()); + + less.Parser.importer = function (file, paths, callback, env) { + var pathname, data; + + // TODO: Undo this at some point, + // or use different approach. + var paths = [].concat(paths); + paths.push('.'); + + for (var i = 0; i < paths.length; i++) { + pathname = checkPath(paths[i], file); + if (pathname != null) { + break; + } + } + + paths = paths.slice(0, paths.length - 1); + + if (!pathname) { + if (typeof(env.errback) === "function") { + env.errback(file, paths, callback); + } else { + callback({ type: 'File', message: "'" + file + "' wasn't found.\n" }); + } + return; + } + + function parseFile(e, data) { + if (e) return callback(e); + env.contents = env.contents || {}; + env.contents[pathname] = data; // Updating top importing parser content cache. + new(less.Parser)({ + paths: [path.dirname(pathname)].concat(paths), + filename: pathname, + contents: env.contents, + files: env.files, + syncImport: env.syncImport, + dumpLineNumbers: env.dumpLineNumbers + }).parse(data, function (e, root) { + callback(e, root, pathname); + }); + }; + + try { + readFile(pathname); + parseFile(null, data); + } catch (e) { + parseFile(e); + } + } + return less; +}); \ No newline at end of file