postcss-less-engine-latest
Version:
PostCSS plugin for integrating the popular Less CSS pre-processor into your PostCSS workflow
341 lines (296 loc) • 9.95 kB
JavaScript
var postcss = require('postcss')
, Input = require('postcss/lib/input')
, CssSyntaxError = require('postcss/lib/css-syntax-error')
, less = require('less');
var render = require("./lib/render")(less.ParseTree, less.transformTree);
function isFunction(f){
return typeof f === 'function';
}
function LessPlugin() {
var cacheInput;
var plugin = postcss.plugin('postcss-less-engine', function (opts) {
opts = opts || {};
return function (css, result) {
// Add PostCSS options to Less options
// TODO - outfile for source map
css.source = {};
if(opts.filename) {
css.source.input = {
file: opts.filename
};
}
else if(result.opts.from) {
css.source.input = {
file: result.opts.from
};
opts.filename = result.opts.from;
}
css.source.start = {
line: 1
, column: 1
};
// Most of Less's nodes require an object to be passed with this structure.
// It makes it hard to just stringify anything, because of Less's "context" and "output" paramaters that
// are required for many class functions.
var returnObj, context, postCssInputs = {};
// Less's importManager is like PostCSS's Input class.
// We need to convert our inputs to reference them later.
function convertImports(imports) {
for (var file in imports) {
if (imports.hasOwnProperty(file)) {
postCssInputs[file] = new Input(imports[file], file !== "input" ? { from: file } : undefined);
}
}
}
function buildNodeObject(filename, index, chunk) {
var input = postCssInputs[filename],
loc = less.utils.getLocation(index, input.css);
return {
stringValue: chunk
, source: {
// The Less parser only tracks starting indices, which should still work for sourcemaps.
start: {
line: loc.line + 1
, column: loc.column + 1
}
, input: input
}
};
}
var output = {
add: function(chunk, fileInfo, index) {
if(chunk === "") return;
if(returnObj === "" && fileInfo && index) {
return returnObj = buildNodeObject(fileInfo.filename, index, chunk);
}
returnObj.stringValue += chunk;
}
};
function getObject(lessNode, init) {
// Initialize the string
if(init)
returnObj = { stringValue: "" };
try {
if(lessNode.genCSS)
lessNode.genCSS(context, output);
}
catch(ex) {}
return returnObj;
}
var process = {
// PostCSS "at-rule"
directive: function(directive) {
var filename = directive.path
? directive.path._fileInfo.filename
: directive._fileInfo.filename;
var val, node, nodeTmp = buildNodeObject(filename, directive.index);
if(!directive.path) {
if(directive.features) {
val = getObject(directive.features, true).stringValue;
}
else {
val = getObject(directive.value, true).stringValue;
}
}
else {
val = directive.path.quote + directive.path.value + directive.path.quote;
}
node = {
type: ""
};
if(directive.type === 'Media') {
node.name = 'media';
}
else if(directive.type === 'Import') {
node.name = 'import';
}
else {
// Remove "@" for PostCSS
node.name = directive.name.replace('@','');
}
node.source = nodeTmp.source;
node.params = val;
var atrule = postcss.atRule(node);
if(directive.rules) {
atrule.nodes = [];
processRules(atrule, directive.rules);
}
return atrule;
}
// PostCSS "rule"
, ruleset: function(ruleset) {
var i = 0, selectors = [];
var val, tmpObj, node, path, pathSubCnt, paths = ruleset.paths;
// Less rulesets aren't completely flat at this stage.
// They need one more good flattening. From lib/less/tree/ruleset.js
if(paths) {
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (!(pathSubCnt = path.length)) { continue; }
if(i === 0) {
tmpObj = buildNodeObject(ruleset.selectors[0].elements[0]._fileInfo.filename, ruleset.selectors[0].elements[0].index);
node = {
type: "rule"
, nodes: []
, source: tmpObj.source
};
context.firstSelector = true;
getObject(path[0], true);
context.firstSelector = false;
}
else {
getObject(path[0], true);
}
for (j = 1; j < pathSubCnt; j++) {
getObject(path[j]);
}
selectors.push(returnObj.stringValue.trim());
}
}
else {
var selector;
for (i = 0; i < ruleset.selectors.length; i++) {
selector = ruleset.selectors[i];
if(i === 0) {
tmpObj = buildNodeObject(selector.elements[0]._fileInfo.filename, selector.elements[0].index);
node = {
type: "rule"
, nodes: []
, source: tmpObj.source
};
}
val = getObject(selector, true);
if(val.stringValue)
selectors.push(val.stringValue.trim());
i++;
}
}
node.selectors = selectors;
var rule = postcss.rule(node);
// add semicolon
rule.raws.semicolon = true;
processRules(rule, ruleset.rules);
return rule;
}
// PostCSS "decl"
, rule: function(rule) {
var node, tmpObj = buildNodeObject(rule._fileInfo.filename, rule.index);
var evalValue = getObject(rule.value, true);
node = {
type: "decl"
, source: tmpObj.source
, prop: rule.name
, value: evalValue.stringValue
};
var testImportant;
if(rule.important.indexOf("!") > -1) {
node.important = true;
}
// Currently, Less doesn't separate the important flag if it's not assigned to a variable
// This may change but if so, this should still work.
else {
testImportant = /([^!]*)(! *important)/.exec(node.value);
if(testImportant && testImportant[2] !== '') {
node.important = true;
node.value = testImportant[1].trim();
}
}
return postcss.decl(node);
}
, comment: function(comment) {
var node, tmpObj = buildNodeObject(comment._fileInfo.filename, comment.index);
node = {
type: "comment"
// TODO: Less has a bug where it wasn't setting the index for Comment nodes
// after fix is released for Less, uncomment and update tests
//, source: tmpObj.source
// PostCSS requires comments to come without comment marks
, text: comment.value.replace(/^\/\*\s*/, '').replace(/\s*\*\/$/, '')
};
return postcss.comment(node);
}
};
function processRules(container, rulesArray) {
if(rulesArray && rulesArray.length > 0) {
rulesArray.forEach(function(val) {
// a.k.a. PostCSS "rule"
if(val instanceof less.tree.Ruleset) {
var ruleset = process.ruleset(val);
if(ruleset.selector === '&' || ruleset.selector === '') {
container.append(ruleset.nodes);
}
else {
container.append(ruleset);
}
}
// a.k.a. PostCSS "decl"
else if(val instanceof less.tree.Declaration) {
container.append(process.rule(val));
}
else if(val instanceof less.tree.Comment) {
container.append(process.comment(val));
}
// a.k.a. PostCSS "atrule"
else if(val instanceof less.tree.AtRule) {
container.append(process.directive(val));
}
else if(val instanceof less.tree.Import) {
container.append(process.directive(val));
}
});
}
}
return new Promise(function (resolve, reject) {
cacheInput = cacheInput.toString();
if(!cacheInput) {
// TODO: explain the error
reject(new CssSyntaxError(
"No input is present"
));
}
render(cacheInput, opts, function(err, tree, evaldRoot, imports) {
if(err) {
// Build PostCSS error
return reject(new CssSyntaxError(
err.message
, err.line || err.line === 0 ? err.line + 1 : undefined
, err.column || err.column === 0 ? err.column + 1 : undefined
, err.extract
, err.filename
, 'postcss-less-engine'
));
}
try {
// Not sure if this is the correct context or not?
//context = tree.imports.context;
context = {};
// Convert Less AST to PostCSS AST
convertImports(imports.contents);
if(isFunction(opts.onImport)){
opts.onImport(Object.keys(postCssInputs))
}
processRules(css, evaldRoot.rules);
resolve();
}
catch(err) {
return reject(new CssSyntaxError(
err.message
, err.line || err.line === 0 ? err.line + 1 : undefined
, err.column || err.column === 0 ? err.column + 1 : undefined
, err.extract
, err.filename
, 'postcss-less-engine'
));
//return reject(err);
}
});
});
};
});
plugin.parser = function(input) {
cacheInput = input ? input : " ";
return new postcss.root();
};
return plugin;
}
module.exports = LessPlugin();