Files
biomedjs/node_modules/less/lib/less/browser.js
2014-09-14 07:04:16 -04:00

550 lines
18 KiB
JavaScript

//
// browser.js - client-side engine
//
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.length > 0 ||
isFileProtocol ? 'development'
: 'production');
// 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) {
less.tree.functions[func] = less.functions[func];
}
}
var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);
if (dumpLineNumbers) {
less.dumpLineNumbers = dumpLineNumbers[1];
}
//
// Watch mode
//
less.watch = function () {
if (!less.watchMode ){
less.env = 'development';
initRunningMode();
}
return this.watchMode = true
};
less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; };
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) {
createCSS(root.toCSS(less), sheet, env.lastModified);
}
});
}
}, less.poll);
} else {
less.optimization = 3;
}
}
if (/!watch/.test(location.hash)) {
less.watch();
}
var cache = null;
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');
var typePattern = /^text\/(x-)?less$/;
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
//
var session_cache = '';
less.modifyVars = function(record) {
var str = session_cache;
for (var name in record) {
str += ((name.slice(0,1) === '@')? '' : '@') + name +': '+
((record[name].slice(-1) === ';')? record[name] : record[name] +';');
}
new(less.Parser)(new less.tree.parseEnv(less)).parse(str, function (e, root) {
if (e) {
error(e, "session_cache");
} else {
createCSS(root.toCSS(less), less.sheets[less.sheets.length - 1]);
}
});
};
less.refresh = function (reload) {
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.");
} else {
log("parsed " + sheet.href + " successfully.");
createCSS(root.toCSS(less), sheet, env.lastModified);
}
log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms');
(env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms');
endTime = new(Date);
}, reload);
loadStyles();
};
less.refreshStyles = loadStyles;
less.refresh(less.env === 'development');
function loadStyles() {
var styles = document.getElementsByTagName('style');
for (var i = 0; i < styles.length; i++) {
if (styles[i].type.match(typePattern)) {
var env = new less.tree.parseEnv(less);
env.filename = document.location.href.replace(/#.*$/, '');
new(less.Parser)(env).parse(styles[i].innerHTML || '', function (e, cssAST) {
if (e) {
return error(e, "inline");
}
var css = cssAST.toCSS(less);
var style = styles[i];
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.innerHTML = css;
}
});
}
}
}
function loadStyleSheets(callback, reload) {
for (var i = 0; i < less.sheets.length; i++) {
loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1));
}
}
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 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-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/,
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("\\", "/").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 loadStyleSheet(sheet, callback, reload, remaining) {
// 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(sheet.href, window.location.href);
var href = hrefParts.url;
var css = cache && cache.getItem(href);
var timestamp = cache && cache.getItem(href + ':timestamp');
var styles = { css: css, timestamp: timestamp };
var env;
var newFileInfo = {
relativeUrls: less.relativeUrls,
currentDirectory: hrefParts.path,
filename: href
};
if (sheet instanceof less.tree.parseEnv) {
env = new less.tree.parseEnv(sheet);
newFileInfo.entryPath = env.currentFileInfo.entryPath;
newFileInfo.rootpath = env.currentFileInfo.rootpath;
newFileInfo.rootFilename = env.currentFileInfo.rootFilename;
} else {
env = new less.tree.parseEnv(less);
env.mime = sheet.type;
newFileInfo.entryPath = hrefParts.path;
newFileInfo.rootpath = less.rootpath || hrefParts.path;
newFileInfo.rootFilename = href;
}
if (env.relativeUrls) {
//todo - this relies on option being set on less object rather than being passed in as an option
// - need an originalRootpath
if (less.rootpath) {
newFileInfo.rootpath = extractUrlParts(less.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path;
} else {
newFileInfo.rootpath = hrefParts.path;
}
}
xhr(href, sheet.type, function (data, lastModified) {
// Store data this session
session_cache += data.replace(/@import .+?;/ig, '');
if (!reload && styles && lastModified &&
(new(Date)(lastModified).valueOf() ===
new(Date)(styles.timestamp).valueOf())) {
// Use local copy
createCSS(styles.css, sheet);
callback(null, null, data, sheet, { local: true, remaining: remaining }, href);
} else {
// Use remote copy (re-parse)
try {
env.contents[href] = data; // Updating content cache
env.paths = [hrefParts.path];
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, { local: false, lastModified: lastModified, remaining: remaining }, href);
//TODO - there must be a better way? A generic less-to-css function that can both call error
//and removeNode where appropriate
//should also add tests
if (env.currentFileInfo.rootFilename === href) {
removeNode(document.getElementById('less-error-message:' + extractId(href)));
}
} catch (e) {
callback(e, null, null, sheet);
}
});
} catch (e) {
callback(e, null, null, sheet);
}
}
}, function (status, url) {
callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, null, sheet);
});
}
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 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;
(nextEl || document.getElementsByTagName('head')[0]).parentNode.insertBefore(css, nextEl);
}
if (oldCss && keepOldCss === false) {
head.removeChild(oldCss);
}
// Don't update the local store if the file wasn't modified
if (lastModified && cache) {
log('saving ' + href + ' to cache.');
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');
}
}
}
function xhr(url, type, callback, errback) {
var xhr = getXMLHttpRequest();
var async = isFileProtocol ? less.fileAsync : less.async;
if (typeof(xhr.overrideMimeType) === 'function') {
xhr.overrideMimeType('text/css');
}
xhr.open('GET', url, async);
xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5');
xhr.send(null);
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 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);
}
}
}
function getXMLHttpRequest() {
if (window.XMLHttpRequest) {
return new(XMLHttpRequest);
} else {
try {
return new(ActiveXObject)("MSXML2.XMLHTTP.3.0");
} catch (e) {
log("browser doesn't support AJAX.");
return null;
}
}
}
function removeNode(node) {
return node && node.parentNode.removeChild(node);
}
function log(str) {
if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) }
}
function error(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, error = [];
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) {
error.push(template.replace(/\{line\}/, (parseInt(e.line) || 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>' + error.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);
}
}