// // browser.js - client-side engine //
global less, window, document, XMLHttpRequest, location
var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);
less.env = less.env || (location.hostname == '127.0.0.1' ||
location.hostname == '0.0.0.0' || location.hostname == 'localhost' || (location.port && location.port.length > 0) || isFileProtocol ? 'development' : 'production');
var logLevel = {
debug: 3, info: 2, errors: 1, none: 0
};
// The amount of logging in the javascript console. // 3 - Debug, information and errors // 2 - Information and errors // 1 - Errors // 0 - None // Defaults to 2 less.logLevel = typeof(less.logLevel) != 'undefined' ? less.logLevel : (less.env === 'development' ? logLevel.debug : logLevel.errors);
// Load styles asynchronously (default: false) // // This is set to `false` by default, so that the body // doesn't start loading before the stylesheets are parsed. // Setting this to `true` can result in flickering. // less.async = less.async || false; less.fileAsync = less.fileAsync || false;
// Interval between watch polls less.poll = less.poll || (isFileProtocol ? 1000 : 1500);
//Setup user functions if (less.functions) {
for(var func in less.functions) { if (less.functions.hasOwnProperty(func)) { less.tree.functions[func] = less.functions[func]; } }
}
var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash); if (dumpLineNumbers) {
less.dumpLineNumbers = dumpLineNumbers[1];
}
var typePattern = /^text/(x-)?less$/; var cache = null; var fileCache = {};
function log(str, level) {
if (typeof(console) !== 'undefined' && less.logLevel >= level) { console.log('less: ' + str); }
}
function extractId(href) {
return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' ) // Remove protocol & domain .replace(/^\//, '' ) // Remove root / .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension .replace(/[^\.\w-]+/g, '-') // Replace illegal characters .replace(/\./g, ':'); // Replace dots with colons(for valid id)
}
function errorConsole(e, rootHref) {
var template = '{line} {content}'; var filename = e.filename || rootHref; var errors = []; var content = (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + " in " + filename + " "; var errorline = function (e, i, classname) { if (e.extract[i] !== undefined) { errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1)) .replace(/\{class\}/, classname) .replace(/\{content\}/, e.extract[i])); } }; if (e.extract) { errorline(e, 0, ''); errorline(e, 1, 'line'); errorline(e, 2, ''); content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n' + errors.join('\n'); } else if (e.stack) { content += e.stack; } log(content, logLevel.errors);
}
function createCSS(styles, sheet, lastModified) {
// Strip the query-string var href = sheet.href || ''; // If there is no title set, use the filename, minus the extension var id = 'less:' + (sheet.title || extractId(href)); // If this has already been inserted into the DOM, we may need to replace it var oldCss = document.getElementById(id); var keepOldCss = false; // Create a new stylesheet node for insertion or (if necessary) replacement var css = document.createElement('style'); css.setAttribute('type', 'text/css'); if (sheet.media) { css.setAttribute('media', sheet.media); } css.id = id; if (css.styleSheet) { // IE try { css.styleSheet.cssText = styles; } catch (e) { throw new(Error)("Couldn't reassign styleSheet.cssText."); } } else { css.appendChild(document.createTextNode(styles)); // If new contents match contents of oldCss, don't replace oldCss keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 && oldCss.firstChild.nodeValue === css.firstChild.nodeValue); } var head = document.getElementsByTagName('head')[0]; // If there is no oldCss, just append; otherwise, only append if we need // to replace oldCss with an updated stylesheet if (oldCss === null || keepOldCss === false) { var nextEl = sheet && sheet.nextSibling || null; if (nextEl) { nextEl.parentNode.insertBefore(css, nextEl); } else { head.appendChild(css); } } if (oldCss && keepOldCss === false) { oldCss.parentNode.removeChild(oldCss); } // Don't update the local store if the file wasn't modified if (lastModified && cache) { log('saving ' + href + ' to cache.', logLevel.info); try { cache.setItem(href, styles); cache.setItem(href + ':timestamp', lastModified); } catch(e) { //TODO - could do with adding more robust error handling log('failed to save', logLevel.errors); } }
}
function postProcessCSS(styles) {
if (less.postProcessor && typeof less.postProcessor === 'function') { styles = less.postProcessor.call(styles, styles) || styles; } return styles;
}
function errorHTML(e, rootHref) {
var id = 'less-error-message:' + extractId(rootHref || ""); var template = '<li><label>{line}</label><pre class="{class}">{content}</pre></li>'; var elem = document.createElement('div'), timer, content, errors = []; var filename = e.filename || rootHref; var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; elem.id = id; elem.className = "less-error-message"; content = '<h3>' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + '</h3>' + '<p>in <a href="' + filename + '">' + filenameNoPath + "</a> "; var errorline = function (e, i, classname) { if (e.extract[i] !== undefined) { errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1)) .replace(/\{class\}/, classname) .replace(/\{content\}/, e.extract[i])); } }; if (e.extract) { errorline(e, 0, ''); errorline(e, 1, 'line'); errorline(e, 2, ''); content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':</p>' + '<ul>' + errors.join('') + '</ul>'; } else if (e.stack) { content += '<br/>' + e.stack.split('\n').slice(1).join('<br/>'); } elem.innerHTML = content; // CSS for error messages createCSS([ '.less-error-message ul, .less-error-message li {', 'list-style-type: none;', 'margin-right: 15px;', 'padding: 4px 0;', 'margin: 0;', '}', '.less-error-message label {', 'font-size: 12px;', 'margin-right: 15px;', 'padding: 4px 0;', 'color: #cc7777;', '}', '.less-error-message pre {', 'color: #dd6666;', 'padding: 4px 0;', 'margin: 0;', 'display: inline-block;', '}', '.less-error-message pre.line {', 'color: #ff0000;', '}', '.less-error-message h3 {', 'font-size: 20px;', 'font-weight: bold;', 'padding: 15px 0 5px 0;', 'margin: 0;', '}', '.less-error-message a {', 'color: #10a', '}', '.less-error-message .error {', 'color: red;', 'font-weight: bold;', 'padding-bottom: 2px;', 'border-bottom: 1px dashed red;', '}' ].join('\n'), { title: 'error-message' }); elem.style.cssText = [ "font-family: Arial, sans-serif", "border: 1px solid #e00", "background-color: #eee", "border-radius: 5px", "-webkit-border-radius: 5px", "-moz-border-radius: 5px", "color: #e00", "padding: 15px", "margin-bottom: 15px" ].join(';'); if (less.env == 'development') { timer = setInterval(function () { if (document.body) { if (document.getElementById(id)) { document.body.replaceChild(elem, document.getElementById(id)); } else { document.body.insertBefore(elem, document.body.firstChild); } clearInterval(timer); } }, 10); }
}
function error(e, rootHref) {
if (!less.errorReporting || less.errorReporting === "html") { errorHTML(e, rootHref); } else if (less.errorReporting === "console") { errorConsole(e, rootHref); } else if (typeof less.errorReporting === 'function') { less.errorReporting("add", e, rootHref); }
}
function removeErrorHTML(path) {
var node = document.getElementById('less-error-message:' + extractId(path)); if (node) { node.parentNode.removeChild(node); }
}
function removeErrorConsole(path) {
//no action
}
function removeError(path) {
if (!less.errorReporting || less.errorReporting === "html") { removeErrorHTML(path); } else if (less.errorReporting === "console") { removeErrorConsole(path); } else if (typeof less.errorReporting === 'function') { less.errorReporting("remove", path); }
}
function loadStyles(modifyVars) {
var styles = document.getElementsByTagName('style'), style; for (var i = 0; i < styles.length; i++) { style = styles[i]; if (style.type.match(typePattern)) { var env = new less.tree.parseEnv(less), lessText = style.innerHTML || ''; env.filename = document.location.href.replace(/#.*$/, ''); if (modifyVars || less.globalVars) { env.useFileCache = true; } jshint loopfunc:true // use closure to store current value of i var callback = (function(style) { return function (e, cssAST) { if (e) { return error(e, "inline"); } var css = cssAST.toCSS(less); style.type = 'text/css'; if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.innerHTML = css; } }; })(style); new(less.Parser)(env).parse(lessText, callback, {globalVars: less.globalVars, modifyVars: modifyVars}); } }
}
function extractUrlParts(url, baseUrl) {
// urlParts[1] = protocol&hostname || / // urlParts[2] = / if path relative to host base // urlParts[3] = directories // urlParts[4] = filename // urlParts[5] = parameters var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i, urlParts = url.match(urlPartsRegex), returner = {}, directories = [], i, baseUrlParts; if (!urlParts) { throw new Error("Could not parse sheet href - '"+url+"'"); } // Stylesheets in IE don't always return the full path if (!urlParts[1] || urlParts[2]) { baseUrlParts = baseUrl.match(urlPartsRegex); if (!baseUrlParts) { throw new Error("Could not parse page url - '"+baseUrl+"'"); } urlParts[1] = urlParts[1] || baseUrlParts[1] || ""; if (!urlParts[2]) { urlParts[3] = baseUrlParts[3] + urlParts[3]; } } if (urlParts[3]) { directories = urlParts[3].replace(/\\/g, "/").split("/"); // extract out . before .. so .. doesn't absorb a non-directory for(i = 0; i < directories.length; i++) { if (directories[i] === ".") { directories.splice(i, 1); i -= 1; } } for(i = 0; i < directories.length; i++) { if (directories[i] === ".." && i > 0) { directories.splice(i-1, 2); i -= 2; } } } returner.hostPart = urlParts[1]; returner.directories = directories; returner.path = urlParts[1] + directories.join("/"); returner.fileUrl = returner.path + (urlParts[4] || ""); returner.url = returner.fileUrl + (urlParts[5] || ""); return returner;
}
function pathDiff(url, baseUrl) {
// diff between two paths to create a relative path var urlParts = extractUrlParts(url), baseUrlParts = extractUrlParts(baseUrl), i, max, urlDirectories, baseUrlDirectories, diff = ""; if (urlParts.hostPart !== baseUrlParts.hostPart) { return ""; } max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); for(i = 0; i < max; i++) { if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } } baseUrlDirectories = baseUrlParts.directories.slice(i); urlDirectories = urlParts.directories.slice(i); for(i = 0; i < baseUrlDirectories.length-1; i++) { diff += "../"; } for(i = 0; i < urlDirectories.length-1; i++) { diff += urlDirectories[i] + "/"; } return diff;
}
function getXMLHttpRequest() {
if (window.XMLHttpRequest && (window.location.protocol !== "file:" || !window.ActiveXObject)) { return new XMLHttpRequest(); } else { try { /*global ActiveXObject */ return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { log("browser doesn't support AJAX.", logLevel.errors); return null; } }
}
function doXHR(url, type, callback, errback) {
var xhr = getXMLHttpRequest(); var async = isFileProtocol ? less.fileAsync : less.async; if (typeof(xhr.overrideMimeType) === 'function') { xhr.overrideMimeType('text/css'); } log("XHR: Getting '" + url + "'", logLevel.debug); xhr.open('GET', url, async); xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); xhr.send(null); function handleResponse(xhr, callback, errback) { if (xhr.status >= 200 && xhr.status < 300) { callback(xhr.responseText, xhr.getResponseHeader("Last-Modified")); } else if (typeof(errback) === 'function') { errback(xhr.status, url); } } if (isFileProtocol && !less.fileAsync) { if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { callback(xhr.responseText); } else { errback(xhr.status, url); } } else if (async) { xhr.onreadystatechange = function () { if (xhr.readyState == 4) { handleResponse(xhr, callback, errback); } }; } else { handleResponse(xhr, callback, errback); }
}
function loadFile(originalHref, currentFileInfo, callback, env, modifyVars) {
if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\//.test(originalHref)) { originalHref = currentFileInfo.currentDirectory + originalHref; } // sheet may be set to the stylesheet for the initial load or a collection of properties including // some env variables for imports var hrefParts = extractUrlParts(originalHref, window.location.href); var href = hrefParts.url; var newFileInfo = { currentDirectory: hrefParts.path, filename: href }; if (currentFileInfo) { newFileInfo.entryPath = currentFileInfo.entryPath; newFileInfo.rootpath = currentFileInfo.rootpath; newFileInfo.rootFilename = currentFileInfo.rootFilename; newFileInfo.relativeUrls = currentFileInfo.relativeUrls; } else { newFileInfo.entryPath = hrefParts.path; newFileInfo.rootpath = less.rootpath || hrefParts.path; newFileInfo.rootFilename = href; newFileInfo.relativeUrls = env.relativeUrls; } if (newFileInfo.relativeUrls) { if (env.rootpath) { newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path; } else { newFileInfo.rootpath = hrefParts.path; } } if (env.useFileCache && fileCache[href]) { try { var lessText = fileCache[href]; callback(null, lessText, href, newFileInfo, { lastModified: new Date() }); } catch (e) { callback(e, null, href); } return; } doXHR(href, env.mime, function (data, lastModified) { // per file cache fileCache[href] = data; // Use remote copy (re-parse) try { callback(null, data, href, newFileInfo, { lastModified: lastModified }); } catch (e) { callback(e, null, href); } }, function (status, url) { callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, href); });
}
function loadStyleSheet(sheet, callback, reload, remaining, modifyVars) {
var env = new less.tree.parseEnv(less); env.mime = sheet.type; if (modifyVars || less.globalVars) { env.useFileCache = true; } loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) { if (webInfo) { webInfo.remaining = remaining; var css = cache && cache.getItem(path), timestamp = cache && cache.getItem(path + ':timestamp'); if (!reload && timestamp && webInfo.lastModified && (new(Date)(webInfo.lastModified).valueOf() === new(Date)(timestamp).valueOf())) { // Use local copy createCSS(css, sheet); webInfo.local = true; callback(null, null, data, sheet, webInfo, path); return; } } //TODO add tests around how this behaves when reloading removeError(path); if (data) { env.currentFileInfo = newFileInfo; new(less.Parser)(env).parse(data, function (e, root) { if (e) { return callback(e, null, null, sheet); } try { callback(e, root, data, sheet, webInfo, path); } catch (e) { callback(e, null, null, sheet); } }, {modifyVars: modifyVars, globalVars: less.globalVars}); } else { callback(e, null, null, sheet, webInfo, path); } }, env, modifyVars);
}
function loadStyleSheets(callback, reload, modifyVars) {
for (var i = 0; i < less.sheets.length; i++) { loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), modifyVars); }
}
function initRunningMode(){
if (less.env === 'development') { less.optimization = 0; less.watchTimer = setInterval(function () { if (less.watchMode) { loadStyleSheets(function (e, root, _, sheet, env) { if (e) { error(e, sheet.href); } else if (root) { var styles = root.toCSS(less); styles = postProcessCSS(styles); createCSS(styles, sheet, env.lastModified); } }); } }, less.poll); } else { less.optimization = 3; }
}
// // Watch mode // less.watch = function () {
if (!less.watchMode ){ less.env = 'development'; initRunningMode(); } this.watchMode = true; return true;
};
less.unwatch = function () {clearInterval(less.watchTimer); this.watchMode = false; return false; };
if (/!watch/.test(location.hash)) {
less.watch();
}
if (less.env != 'development') {
try { cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; } catch (_) {}
}
// // Get all <link> tags with the 'rel' attribute set to “stylesheet/less” // var links = document.getElementsByTagName('link');
less.sheets = [];
for (var i = 0; i < links.length; i++) {
if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && (links[i].type.match(typePattern)))) { less.sheets.push(links[i]); }
}
// // With this function, it's possible to alter variables and re-render // CSS without reloading less-files // less.modifyVars = function(record) {
less.refresh(false, record);
};
less.refresh = function (reload, modifyVars) {
var startTime, endTime; startTime = endTime = new Date(); loadStyleSheets(function (e, root, _, sheet, env) { if (e) { return error(e, sheet.href); } if (env.local) { log("loading " + sheet.href + " from cache.", logLevel.info); } else { log("parsed " + sheet.href + " successfully.", logLevel.debug); var styles = root.toCSS(less); styles = postProcessCSS(styles); createCSS(styles, sheet, env.lastModified); } log("css for " + sheet.href + " generated in " + (new Date() - endTime) + 'ms', logLevel.info); if (env.remaining === 0) { log("less has finished. css generated in " + (new Date() - startTime) + 'ms', logLevel.info); } endTime = new Date(); }, reload, modifyVars); loadStyles(modifyVars);
};
less.refreshStyles = loadStyles;
less.Parser.fileLoader = loadFile;
less.refresh(less.env === 'development');