You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
810 lines
18 KiB
810 lines
18 KiB
#!/usr/bin/env node |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var fs = require('fs') |
|
, stylus = require('../lib/stylus') |
|
, basename = require('path').basename |
|
, dirname = require('path').dirname |
|
, extname = require('path').extname |
|
, resolve = require('path').resolve |
|
, join = require('path').join |
|
, isWindows = process.platform === 'win32'; |
|
|
|
/** |
|
* Arguments. |
|
*/ |
|
|
|
var args = process.argv.slice(2); |
|
|
|
/** |
|
* Compare flag. |
|
*/ |
|
|
|
var compare = false; |
|
|
|
/** |
|
* Compress flag. |
|
*/ |
|
|
|
var compress = false; |
|
|
|
/** |
|
* CSS conversion flag. |
|
*/ |
|
|
|
var convertCSS = false; |
|
|
|
/** |
|
* Line numbers flag. |
|
*/ |
|
|
|
var linenos = false; |
|
|
|
/** |
|
* CSS class prefix. |
|
*/ |
|
var prefix = ''; |
|
|
|
/** |
|
* Print to stdout flag. |
|
*/ |
|
var print = false; |
|
|
|
/** |
|
* Firebug flag |
|
*/ |
|
|
|
var firebug = false; |
|
|
|
/** |
|
* Sourcemap flag |
|
*/ |
|
|
|
var sourcemap = false; |
|
|
|
/** |
|
* Files to processes. |
|
*/ |
|
|
|
var files = []; |
|
|
|
/** |
|
* Import paths. |
|
*/ |
|
|
|
var paths = []; |
|
|
|
/** |
|
* Destination directory. |
|
*/ |
|
|
|
var dest; |
|
|
|
/** |
|
* Watcher hash. |
|
*/ |
|
|
|
var watchers; |
|
|
|
/** |
|
* Enable REPL. |
|
*/ |
|
|
|
var interactive; |
|
|
|
/** |
|
* Plugins. |
|
*/ |
|
|
|
var plugins = []; |
|
|
|
/** |
|
* Optional url() function. |
|
*/ |
|
|
|
var urlFunction = false; |
|
|
|
/** |
|
* Include CSS on import. |
|
*/ |
|
|
|
var includeCSS = false; |
|
|
|
/** |
|
* Set file imports. |
|
*/ |
|
|
|
var imports = []; |
|
|
|
/** |
|
* Resolve relative urls |
|
*/ |
|
|
|
var resolveURL = false; |
|
|
|
/** |
|
* Disable cache. |
|
*/ |
|
|
|
var disableCache = false; |
|
|
|
/** |
|
* Display dependencies flag. |
|
*/ |
|
|
|
var deps = false; |
|
|
|
/** |
|
* Hoist at-rules. |
|
*/ |
|
|
|
var hoist = false; |
|
|
|
/** |
|
* Usage docs. |
|
*/ |
|
|
|
var usage = [ |
|
'' |
|
, ' Usage: stylus [options] [command] [< in [> out]]' |
|
, ' [file|dir ...]' |
|
, '' |
|
, ' Commands:' |
|
, '' |
|
, ' help [<type>:]<prop> Opens help info at MDN for <prop> in' |
|
, ' your default browser. Optionally' |
|
, ' searches other resources of <type>:' |
|
, ' safari opera w3c ms caniuse quirksmode' |
|
, '' |
|
, ' Options:' |
|
, '' |
|
, ' -i, --interactive Start interactive REPL' |
|
, ' -u, --use <path> Utilize the Stylus plugin at <path>' |
|
, ' -U, --inline Utilize image inlining via data URI support' |
|
, ' -w, --watch Watch file(s) for changes and re-compile' |
|
, ' -o, --out <dir> Output to <dir> when passing files' |
|
, ' -C, --css <src> [dest] Convert CSS input to Stylus' |
|
, ' -I, --include <path> Add <path> to lookup paths' |
|
, ' -c, --compress Compress CSS output' |
|
, ' -d, --compare Display input along with output' |
|
, ' -f, --firebug Emits debug infos in the generated CSS that' |
|
, ' can be used by the FireStylus Firebug plugin' |
|
, ' -l, --line-numbers Emits comments in the generated CSS' |
|
, ' indicating the corresponding Stylus line' |
|
, ' -m, --sourcemap Generates a sourcemap in sourcemaps v3 format' |
|
, ' --sourcemap-inline Inlines sourcemap with full source text in base64 format' |
|
, ' --sourcemap-root <url> "sourceRoot" property of the generated sourcemap' |
|
, ' --sourcemap-base <path> Base <path> from which sourcemap and all sources are relative' |
|
, ' -P, --prefix [prefix] prefix all css classes' |
|
, ' -p, --print Print out the compiled CSS' |
|
, ' --import <file> Import stylus <file>' |
|
, ' --include-css Include regular CSS on @import' |
|
, ' -D, --deps Display dependencies of the compiled file' |
|
, ' --disable-cache Disable caching' |
|
, ' --hoist-atrules Move @import and @charset to the top' |
|
, ' -r, --resolve-url Resolve relative urls inside imports' |
|
, ' --resolve-url-nocheck Like --resolve-url but without file existence check' |
|
, ' -V, --version Display the version of Stylus' |
|
, ' -h, --help Display help information' |
|
, '' |
|
].join('\n'); |
|
|
|
/** |
|
* Handle arguments. |
|
*/ |
|
|
|
var arg; |
|
while (args.length) { |
|
arg = args.shift(); |
|
switch (arg) { |
|
case '-h': |
|
case '--help': |
|
console.error(usage); |
|
process.exit(0); |
|
case '-d': |
|
case '--compare': |
|
compare = true; |
|
break; |
|
case '-c': |
|
case '--compress': |
|
compress = true; |
|
break; |
|
case '-C': |
|
case '--css': |
|
convertCSS = true; |
|
break; |
|
case '-f': |
|
case '--firebug': |
|
firebug = true; |
|
break; |
|
case '-l': |
|
case '--line-numbers': |
|
linenos = true; |
|
break; |
|
case '-m': |
|
case '--sourcemap': |
|
sourcemap = {}; |
|
break; |
|
case '--sourcemap-inline': |
|
sourcemap = sourcemap || {}; |
|
sourcemap.inline = true; |
|
break; |
|
case '--sourcemap-root': |
|
var url = args.shift(); |
|
if (!url) throw new Error('--sourcemap-root <url> required'); |
|
sourcemap = sourcemap || {}; |
|
sourcemap.sourceRoot = url; |
|
break; |
|
case '--sourcemap-base': |
|
var path = args.shift(); |
|
if (!path) throw new Error('--sourcemap-base <path> required'); |
|
sourcemap = sourcemap || {}; |
|
sourcemap.basePath = path; |
|
break; |
|
case '-P': |
|
case '--prefix': |
|
prefix = args.shift(); |
|
if (!prefix) throw new Error('--prefix <prefix> required'); |
|
break; |
|
case '-p': |
|
case '--print': |
|
print = true; |
|
break; |
|
case '-V': |
|
case '--version': |
|
console.log(stylus.version); |
|
process.exit(0); |
|
break; |
|
case '-o': |
|
case '--out': |
|
dest = args.shift(); |
|
if (!dest) throw new Error('--out <dir> required'); |
|
break; |
|
case 'help': |
|
var name = args.shift() |
|
, browser = name.split(':'); |
|
if (browser.length > 1) { |
|
name = [].slice.call(browser, 1).join(':'); |
|
browser = browser[0]; |
|
} else { |
|
name = browser[0]; |
|
browser = ''; |
|
} |
|
if (!name) throw new Error('help <property> required'); |
|
help(name); |
|
break; |
|
case '--include-css': |
|
includeCSS = true; |
|
break; |
|
case '--disable-cache': |
|
disableCache = true; |
|
break; |
|
case '--hoist-atrules': |
|
hoist = true; |
|
break; |
|
case '-i': |
|
case '--repl': |
|
case '--interactive': |
|
interactive = true; |
|
break; |
|
case '-I': |
|
case '--include': |
|
var path = args.shift(); |
|
if (!path) throw new Error('--include <path> required'); |
|
paths.push(path); |
|
break; |
|
case '-w': |
|
case '--watch': |
|
watchers = {}; |
|
break; |
|
case '-U': |
|
case '--inline': |
|
args.unshift('--use', 'url'); |
|
break; |
|
case '-u': |
|
case '--use': |
|
var options; |
|
var path = args.shift(); |
|
if (!path) throw new Error('--use <path> required'); |
|
|
|
// options |
|
if ('--with' == args[0]) { |
|
args.shift(); |
|
options = args.shift(); |
|
if (!options) throw new Error('--with <options> required'); |
|
options = eval('(' + options + ')'); |
|
} |
|
|
|
// url support |
|
if ('url' == path) { |
|
urlFunction = options || {}; |
|
} else { |
|
paths.push(dirname(path)); |
|
plugins.push({ path: path, options: options }); |
|
} |
|
break; |
|
case '--import': |
|
var file = args.shift(); |
|
if (!file) throw new Error('--import <file> required'); |
|
imports.push(file); |
|
break; |
|
case '-r': |
|
case '--resolve-url': |
|
resolveURL = {}; |
|
break; |
|
case '--resolve-url-nocheck': |
|
resolveURL = { nocheck: true }; |
|
break; |
|
case '-D': |
|
case '--deps': |
|
deps = true; |
|
break; |
|
default: |
|
files.push(arg); |
|
} |
|
} |
|
|
|
// if --watch is used, assume we are |
|
// not working with stdio |
|
|
|
if (watchers && !files.length) { |
|
files = fs.readdirSync(process.cwd()) |
|
.filter(function(file){ |
|
return file.match(/\.styl$/); |
|
}); |
|
} |
|
|
|
// --sourcemap flag is not working with stdio |
|
if (sourcemap && !files.length) sourcemap = false; |
|
|
|
/** |
|
* Open the default browser to the CSS property `name`. |
|
* |
|
* @param {String} name |
|
*/ |
|
|
|
function help(name) { |
|
var url |
|
, exec = require('child_process').exec |
|
, command; |
|
|
|
name = encodeURIComponent(name); |
|
|
|
switch (browser) { |
|
case 'safari': |
|
case 'webkit': |
|
url = 'https://developer.apple.com/library/safari/search/?q=' + name; |
|
break; |
|
case 'opera': |
|
url = 'http://dev.opera.com/search/?term=' + name; |
|
break; |
|
case 'w3c': |
|
url = 'http://www.google.com/search?q=site%3Awww.w3.org%2FTR+' + name; |
|
break; |
|
case 'ms': |
|
url = 'http://social.msdn.microsoft.com/search/en-US/ie?query=' + name + '&refinement=59%2c61'; |
|
break; |
|
case 'caniuse': |
|
url = 'http://caniuse.com/#search=' + name; |
|
break; |
|
case 'quirksmode': |
|
url = 'http://www.google.com/search?q=site%3Awww.quirksmode.org+' + name; |
|
break; |
|
default: |
|
url = 'https://developer.mozilla.org/en/CSS/' + name; |
|
} |
|
|
|
switch (process.platform) { |
|
case 'linux': command = 'x-www-browser'; break; |
|
default: command = 'open'; |
|
} |
|
|
|
exec(command + ' "' + url + '"', function(){ |
|
process.exit(0); |
|
}); |
|
} |
|
|
|
// Compilation options |
|
|
|
if (files.length > 1 && extname(dest) === '.css') { |
|
dest = dirname(dest); |
|
} |
|
|
|
var options = { |
|
filename: 'stdin' |
|
, compress: compress |
|
, firebug: firebug |
|
, linenos: linenos |
|
, sourcemap: sourcemap |
|
, paths: [process.cwd()].concat(paths) |
|
, prefix: prefix |
|
, dest: dest |
|
, 'hoist atrules': hoist |
|
}; |
|
|
|
// Buffer stdin |
|
|
|
var str = ''; |
|
|
|
// Convert CSS to Stylus |
|
|
|
if (convertCSS) { |
|
switch (files.length) { |
|
case 2: |
|
compileCSSFile(files[0], files[1]); |
|
break; |
|
case 1: |
|
var file = files[0]; |
|
compileCSSFile(file, join(dirname(file), basename(file, extname(file))) + '.styl'); |
|
break; |
|
default: |
|
var stdin = process.openStdin(); |
|
stdin.setEncoding('utf8'); |
|
stdin.on('data', function(chunk){ str += chunk; }); |
|
stdin.on('end', function(){ |
|
var out = stylus.convertCSS(str); |
|
console.log(out); |
|
}); |
|
} |
|
} else if (interactive) { |
|
repl(); |
|
} else if (deps) { |
|
// if --deps is used, just display list of the dependencies |
|
// not working with stdio and dirs |
|
displayDeps(); |
|
} else { |
|
if (files.length) { |
|
compileFiles(files); |
|
} else { |
|
compileStdio(); |
|
} |
|
} |
|
|
|
/** |
|
* Start Stylus REPL. |
|
*/ |
|
|
|
function repl() { |
|
var options = { cache: false, filename: 'stdin', imports: [join(__dirname, '..', 'lib', 'functions')] } |
|
, parser = new stylus.Parser('', options) |
|
, evaluator = new stylus.Evaluator(parser.parse(), options) |
|
, rl = require('readline') |
|
, repl = rl.createInterface(process.stdin, process.stdout, autocomplete) |
|
, global = evaluator.global.scope; |
|
|
|
// expose BIFs |
|
evaluator.evaluate(); |
|
|
|
// readline |
|
repl.setPrompt('> '); |
|
repl.prompt(); |
|
|
|
// HACK: flat-list auto-complete |
|
function autocomplete(line){ |
|
var out = process.stdout |
|
, keys = Object.keys(global.locals) |
|
, len = keys.length |
|
, words = line.split(/\s+/) |
|
, word = words.pop() |
|
, names = [] |
|
, name |
|
, node |
|
, key; |
|
|
|
// find words that match |
|
for (var i = 0; i < len; ++i) { |
|
key = keys[i]; |
|
if (0 == key.indexOf(word)) { |
|
node = global.lookup(key); |
|
switch (node.nodeName) { |
|
case 'function': |
|
names.push(node.toString()); |
|
break; |
|
default: |
|
names.push(key); |
|
} |
|
} |
|
} |
|
|
|
return [names, line]; |
|
}; |
|
|
|
repl.on('line', function(line){ |
|
if (!line.trim().length) return repl.prompt(); |
|
parser = new stylus.Parser(line, options); |
|
parser.state.push('expression'); |
|
evaluator.return = true; |
|
try { |
|
var expr = parser.parse(); |
|
evaluator.root = expr; |
|
var ret = evaluator.evaluate(); |
|
var node; |
|
while (node = ret.nodes.pop()) { |
|
if (!node.suppress) { |
|
var str = node.toString(); |
|
if ('(' == str[0]) str = str.replace(/^\(|\)$/g, ''); |
|
console.log('\033[90m=> \033[0m' + highlight(str)); |
|
break; |
|
} |
|
} |
|
repl.prompt(); |
|
} catch (err) { |
|
console.error('\033[31merror: %s\033[0m', err.message || err.stack); |
|
repl.prompt(); |
|
} |
|
}); |
|
|
|
repl.on('SIGINT', function(){ |
|
console.log(); |
|
process.exit(0); |
|
}); |
|
} |
|
|
|
/** |
|
* Highlight the given string of Stylus. |
|
*/ |
|
|
|
function highlight(str) { |
|
return str |
|
.replace(/(#)?(\d+(\.\d+)?)/g, function($0, $1, $2){ |
|
return $1 ? $0 : '\033[36m' + $2 + '\033[0m'; |
|
}) |
|
.replace(/(#[\da-fA-F]+)/g, '\033[33m$1\033[0m') |
|
.replace(/('.*?'|".*?")/g, '\033[32m$1\033[0m'); |
|
} |
|
|
|
/** |
|
* Convert a CSS file to a Styl file |
|
*/ |
|
|
|
function compileCSSFile(file, fileOut) { |
|
fs.stat(file, function(err, stat){ |
|
if (err) throw err; |
|
if (stat.isFile()) { |
|
fs.readFile(file, 'utf8', function(err, str){ |
|
if (err) throw err; |
|
var styl = stylus.convertCSS(str); |
|
fs.writeFile(fileOut, styl, function(err){ |
|
if (err) throw err; |
|
}); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Compile with stdio. |
|
*/ |
|
|
|
function compileStdio() { |
|
process.stdin.setEncoding('utf8'); |
|
process.stdin.on('data', function(chunk){ str += chunk; }); |
|
process.stdin.on('end', function(){ |
|
// Compile to css |
|
var style = stylus(str, options); |
|
if (includeCSS) style.set('include css', true); |
|
if (disableCache) style.set('cache', false); |
|
usePlugins(style); |
|
importFiles(style); |
|
style.render(function(err, css){ |
|
if (err) throw err; |
|
if (compare) { |
|
console.log('\n\x1b[1mInput:\x1b[0m'); |
|
console.log(str); |
|
console.log('\n\x1b[1mOutput:\x1b[0m'); |
|
} |
|
console.log(css); |
|
if (compare) console.log(); |
|
}); |
|
}).resume(); |
|
} |
|
|
|
/** |
|
* Compile the given files. |
|
*/ |
|
|
|
function compileFiles(files) { |
|
files.forEach(compileFile); |
|
} |
|
|
|
/** |
|
* Display dependencies of the compiled files. |
|
*/ |
|
|
|
function displayDeps() { |
|
files.forEach(function(file){ |
|
// ensure file exists |
|
fs.stat(file, function(err, stat){ |
|
if (err) throw err; |
|
fs.readFile(file, 'utf8', function(err, str){ |
|
if (err) throw err; |
|
options.filename = file; |
|
var style = stylus(str, options); |
|
|
|
usePlugins(style); |
|
importFiles(style); |
|
console.log(style.deps().join('\n')); |
|
}); |
|
}); |
|
}); |
|
} |
|
|
|
/** |
|
* Compile the given file. |
|
*/ |
|
|
|
function compileFile(file) { |
|
// ensure file exists |
|
fs.stat(file, function(err, stat){ |
|
if (err) throw err; |
|
// file |
|
if (stat.isFile()) { |
|
fs.readFile(file, 'utf8', function(err, str){ |
|
if (err) throw err; |
|
options.filename = file; |
|
options._imports = []; |
|
var style = stylus(str, options); |
|
if (includeCSS) style.set('include css', true); |
|
if (disableCache) style.set('cache', false); |
|
if (sourcemap) style.set('sourcemap', sourcemap); |
|
|
|
usePlugins(style); |
|
importFiles(style); |
|
style.render(function(err, css){ |
|
watchImports(file, options._imports); |
|
if (err) { |
|
if (watchers) { |
|
console.error(err.stack || err.message); |
|
} else { |
|
throw err; |
|
} |
|
} else { |
|
writeFile(file, css); |
|
// write sourcemap |
|
if (sourcemap && !sourcemap.inline) { |
|
writeSourcemap(file, style.sourcemap); |
|
} |
|
} |
|
}); |
|
}); |
|
// directory |
|
} else if (stat.isDirectory()) { |
|
fs.readdir(file, function(err, files){ |
|
if (err) throw err; |
|
files.filter(function(path){ |
|
return path.match(/\.styl$/); |
|
}).map(function(path){ |
|
return join(file, path); |
|
}).forEach(compileFile); |
|
}); |
|
} |
|
}); |
|
} |
|
|
|
/** |
|
* Write the given CSS output. |
|
*/ |
|
|
|
function createPath(file, sourceMap) { |
|
var out; |
|
if (files.length === 1 && extname(dest) === '.css') { |
|
return [dest, sourceMap ? '.map' : ''].join(''); |
|
} |
|
// --out support |
|
out = [basename(file, extname(file)), sourceMap ? '.css.map' : '.css'].join(''); |
|
return dest |
|
? join(dest, out) |
|
: join(dirname(file), out); |
|
} |
|
|
|
function writeFile(file, css) { |
|
// --print support |
|
if (print) return process.stdout.write(css); |
|
var path = createPath(file); |
|
fs.writeFile(path, css, function(err){ |
|
if (err) throw err; |
|
console.log(' \033[90mcompiled\033[0m %s', path); |
|
// --watch support |
|
watch(file, file); |
|
}); |
|
} |
|
|
|
/** |
|
* Write the given sourcemap. |
|
*/ |
|
|
|
function writeSourcemap(file, sourcemap) { |
|
var path = createPath(file, true); |
|
fs.writeFile(path, JSON.stringify(sourcemap), function(err){ |
|
if (err) throw err; |
|
// don't output log message if --print is present |
|
if (!print) console.log(' \033[90mgenerated\033[0m %s', path); |
|
}); |
|
} |
|
|
|
/** |
|
* Watch the given `file` and recompiling `rootFile` when modified. |
|
*/ |
|
|
|
function watch(file, rootFile) { |
|
// not watching |
|
if (!watchers) return; |
|
|
|
// already watched |
|
if (watchers[file]) { |
|
watchers[file][rootFile] = true; |
|
return; |
|
} |
|
|
|
// watch the file itself |
|
watchers[file] = {}; |
|
watchers[file][rootFile] = true; |
|
if (print) { |
|
console.error('Stylus CLI Error: Watch and print cannot be used together'); |
|
process.exit(1); |
|
} |
|
console.log(' \033[90mwatching\033[0m %s', file); |
|
// if is windows use fs.watch api instead |
|
// TODO: remove watchFile when fs.watch() works on osx etc |
|
if (isWindows) { |
|
fs.watch(file, function(event) { |
|
if (event === 'change') compile(); |
|
}); |
|
} else { |
|
fs.watchFile(file, { interval: 300 }, function(curr, prev) { |
|
if (curr.mtime > prev.mtime) compile(); |
|
}); |
|
} |
|
|
|
function compile() { |
|
for (var rootFile in watchers[file]) { |
|
compileFile(rootFile); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Watch `imports`, re-compiling `file` when they change. |
|
*/ |
|
|
|
function watchImports(file, imports) { |
|
imports.forEach(function(imported){ |
|
if (!imported.path) return; |
|
watch(imported.path, file); |
|
}); |
|
} |
|
|
|
/** |
|
* Utilize plugins. |
|
*/ |
|
|
|
function usePlugins(style) { |
|
plugins.forEach(function(plugin){ |
|
var path = plugin.path; |
|
var options = plugin.options; |
|
fn = require(/^\.+\//.test(path) ? resolve(path) : path); |
|
if ('function' != typeof fn) { |
|
throw new Error('plugin ' + path + ' does not export a function'); |
|
} |
|
style.use(fn(options)); |
|
}); |
|
|
|
if (urlFunction) { |
|
style.define('url', stylus.url(urlFunction)); |
|
} else if (resolveURL) { |
|
style.define('url', stylus.resolver(resolveURL)); |
|
} |
|
} |
|
|
|
/** |
|
* Imports the indicated files. |
|
*/ |
|
|
|
function importFiles(style) { |
|
imports.forEach(function(file) { |
|
style.import(file); |
|
}); |
|
}
|
|
|