l0bsterssg

node.js static responsive blog post generator
Log | Files | Refs | README

index.js (13900B)


      1 var path = require('path');
      2 var wordwrap = require('wordwrap');
      3 
      4 /*  Hack an instance of Argv with process.argv into Argv
      5     so people can do
      6         require('optimist')(['--beeble=1','-z','zizzle']).argv
      7     to parse a list of args and
      8         require('optimist').argv
      9     to get a parsed version of process.argv.
     10 */
     11 
     12 var inst = Argv(process.argv.slice(2));
     13 Object.keys(inst).forEach(function (key) {
     14     Argv[key] = typeof inst[key] == 'function'
     15         ? inst[key].bind(inst)
     16         : inst[key];
     17 });
     18 
     19 var exports = module.exports = Argv;
     20 function Argv (args, cwd) {
     21     var self = {};
     22     if (!cwd) cwd = process.cwd();
     23     
     24     self.$0 = process.argv
     25         .slice(0,2)
     26         .map(function (x) {
     27             var b = rebase(cwd, x);
     28             return x.match(/^\//) && b.length < x.length
     29                 ? b : x
     30         })
     31         .join(' ')
     32     ;
     33     
     34     if (process.env._ != undefined && process.argv[1] == process.env._) {
     35         self.$0 = process.env._.replace(
     36             path.dirname(process.execPath) + '/', ''
     37         );
     38     }
     39     
     40     var flags = { bools : {}, strings : {} };
     41     
     42     self.boolean = function (bools) {
     43         if (!Array.isArray(bools)) {
     44             bools = [].slice.call(arguments);
     45         }
     46         
     47         bools.forEach(function (name) {
     48             flags.bools[name] = true;
     49         });
     50         
     51         return self;
     52     };
     53     
     54     self.string = function (strings) {
     55         if (!Array.isArray(strings)) {
     56             strings = [].slice.call(arguments);
     57         }
     58         
     59         strings.forEach(function (name) {
     60             flags.strings[name] = true;
     61         });
     62         
     63         return self;
     64     };
     65     
     66     var aliases = {};
     67     self.alias = function (x, y) {
     68         if (typeof x === 'object') {
     69             Object.keys(x).forEach(function (key) {
     70                 self.alias(key, x[key]);
     71             });
     72         }
     73         else if (Array.isArray(y)) {
     74             y.forEach(function (yy) {
     75                 self.alias(x, yy);
     76             });
     77         }
     78         else {
     79             var zs = (aliases[x] || []).concat(aliases[y] || []).concat(x, y);
     80             aliases[x] = zs.filter(function (z) { return z != x });
     81             aliases[y] = zs.filter(function (z) { return z != y });
     82         }
     83         
     84         return self;
     85     };
     86     
     87     var demanded = {};
     88     self.demand = function (keys) {
     89         if (typeof keys == 'number') {
     90             if (!demanded._) demanded._ = 0;
     91             demanded._ += keys;
     92         }
     93         else if (Array.isArray(keys)) {
     94             keys.forEach(function (key) {
     95                 self.demand(key);
     96             });
     97         }
     98         else {
     99             demanded[keys] = true;
    100         }
    101         
    102         return self;
    103     };
    104     
    105     var usage;
    106     self.usage = function (msg, opts) {
    107         if (!opts && typeof msg === 'object') {
    108             opts = msg;
    109             msg = null;
    110         }
    111         
    112         usage = msg;
    113         
    114         if (opts) self.options(opts);
    115         
    116         return self;
    117     };
    118     
    119     function fail (msg) {
    120         self.showHelp();
    121         if (msg) console.error(msg);
    122         process.exit(1);
    123     }
    124     
    125     var checks = [];
    126     self.check = function (f) {
    127         checks.push(f);
    128         return self;
    129     };
    130     
    131     var defaults = {};
    132     self.default = function (key, value) {
    133         if (typeof key === 'object') {
    134             Object.keys(key).forEach(function (k) {
    135                 self.default(k, key[k]);
    136             });
    137         }
    138         else {
    139             defaults[key] = value;
    140         }
    141         
    142         return self;
    143     };
    144     
    145     var descriptions = {};
    146     self.describe = function (key, desc) {
    147         if (typeof key === 'object') {
    148             Object.keys(key).forEach(function (k) {
    149                 self.describe(k, key[k]);
    150             });
    151         }
    152         else {
    153             descriptions[key] = desc;
    154         }
    155         return self;
    156     };
    157     
    158     self.parse = function (args) {
    159         return Argv(args).argv;
    160     };
    161     
    162     self.option = self.options = function (key, opt) {
    163         if (typeof key === 'object') {
    164             Object.keys(key).forEach(function (k) {
    165                 self.options(k, key[k]);
    166             });
    167         }
    168         else {
    169             if (opt.alias) self.alias(key, opt.alias);
    170             if (opt.demand) self.demand(key);
    171             if (typeof opt.default !== 'undefined') {
    172                 self.default(key, opt.default);
    173             }
    174             
    175             if (opt.boolean || opt.type === 'boolean') {
    176                 self.boolean(key);
    177             }
    178             if (opt.string || opt.type === 'string') {
    179                 self.string(key);
    180             }
    181             
    182             var desc = opt.describe || opt.description || opt.desc;
    183             if (desc) {
    184                 self.describe(key, desc);
    185             }
    186         }
    187         
    188         return self;
    189     };
    190     
    191     var wrap = null;
    192     self.wrap = function (cols) {
    193         wrap = cols;
    194         return self;
    195     };
    196     
    197     self.showHelp = function (fn) {
    198         if (!fn) fn = console.error;
    199         fn(self.help());
    200     };
    201     
    202     self.help = function () {
    203         var keys = Object.keys(
    204             Object.keys(descriptions)
    205             .concat(Object.keys(demanded))
    206             .concat(Object.keys(defaults))
    207             .reduce(function (acc, key) {
    208                 if (key !== '_') acc[key] = true;
    209                 return acc;
    210             }, {})
    211         );
    212         
    213         var help = keys.length ? [ 'Options:' ] : [];
    214         
    215         if (usage) {
    216             help.unshift(usage.replace(/\$0/g, self.$0), '');
    217         }
    218         
    219         var switches = keys.reduce(function (acc, key) {
    220             acc[key] = [ key ].concat(aliases[key] || [])
    221                 .map(function (sw) {
    222                     return (sw.length > 1 ? '--' : '-') + sw
    223                 })
    224                 .join(', ')
    225             ;
    226             return acc;
    227         }, {});
    228         
    229         var switchlen = longest(Object.keys(switches).map(function (s) {
    230             return switches[s] || '';
    231         }));
    232         
    233         var desclen = longest(Object.keys(descriptions).map(function (d) { 
    234             return descriptions[d] || '';
    235         }));
    236         
    237         keys.forEach(function (key) {
    238             var kswitch = switches[key];
    239             var desc = descriptions[key] || '';
    240             
    241             if (wrap) {
    242                 desc = wordwrap(switchlen + 4, wrap)(desc)
    243                     .slice(switchlen + 4)
    244                 ;
    245             }
    246             
    247             var spadding = new Array(
    248                 Math.max(switchlen - kswitch.length + 3, 0)
    249             ).join(' ');
    250             
    251             var dpadding = new Array(
    252                 Math.max(desclen - desc.length + 1, 0)
    253             ).join(' ');
    254             
    255             var type = null;
    256             
    257             if (flags.bools[key]) type = '[boolean]';
    258             if (flags.strings[key]) type = '[string]';
    259             
    260             if (!wrap && dpadding.length > 0) {
    261                 desc += dpadding;
    262             }
    263             
    264             var prelude = '  ' + kswitch + spadding;
    265             var extra = [
    266                 type,
    267                 demanded[key]
    268                     ? '[required]'
    269                     : null
    270                 ,
    271                 defaults[key] !== undefined
    272                     ? '[default: ' + JSON.stringify(defaults[key]) + ']'
    273                     : null
    274                 ,
    275             ].filter(Boolean).join('  ');
    276             
    277             var body = [ desc, extra ].filter(Boolean).join('  ');
    278             
    279             if (wrap) {
    280                 var dlines = desc.split('\n');
    281                 var dlen = dlines.slice(-1)[0].length
    282                     + (dlines.length === 1 ? prelude.length : 0)
    283                 
    284                 body = desc + (dlen + extra.length > wrap - 2
    285                     ? '\n'
    286                         + new Array(wrap - extra.length + 1).join(' ')
    287                         + extra
    288                     : new Array(wrap - extra.length - dlen + 1).join(' ')
    289                         + extra
    290                 );
    291             }
    292             
    293             help.push(prelude + body);
    294         });
    295         
    296         help.push('');
    297         return help.join('\n');
    298     };
    299     
    300     Object.defineProperty(self, 'argv', {
    301         get : parseArgs,
    302         enumerable : true,
    303     });
    304     
    305     function parseArgs () {
    306         var argv = { _ : [], $0 : self.$0 };
    307         Object.keys(flags.bools).forEach(function (key) {
    308             setArg(key, defaults[key] || false);
    309         });
    310         
    311         function setArg (key, val) {
    312             var num = Number(val);
    313             var value = typeof val !== 'string' || isNaN(num) ? val : num;
    314             if (flags.strings[key]) value = val;
    315             
    316             setKey(argv, key.split('.'), value);
    317             
    318             (aliases[key] || []).forEach(function (x) {
    319                 argv[x] = argv[key];
    320             });
    321         }
    322         
    323         for (var i = 0; i < args.length; i++) {
    324             var arg = args[i];
    325             
    326             if (arg === '--') {
    327                 argv._.push.apply(argv._, args.slice(i + 1));
    328                 break;
    329             }
    330             else if (arg.match(/^--.+=/)) {
    331                 // Using [\s\S] instead of . because js doesn't support the
    332                 // 'dotall' regex modifier. See:
    333                 // http://stackoverflow.com/a/1068308/13216
    334                 var m = arg.match(/^--([^=]+)=([\s\S]*)$/);
    335                 setArg(m[1], m[2]);
    336             }
    337             else if (arg.match(/^--no-.+/)) {
    338                 var key = arg.match(/^--no-(.+)/)[1];
    339                 setArg(key, false);
    340             }
    341             else if (arg.match(/^--.+/)) {
    342                 var key = arg.match(/^--(.+)/)[1];
    343                 var next = args[i + 1];
    344                 if (next !== undefined && !next.match(/^-/)
    345                 && !flags.bools[key]
    346                 && (aliases[key] ? !flags.bools[aliases[key]] : true)) {
    347                     setArg(key, next);
    348                     i++;
    349                 }
    350                 else if (/^(true|false)$/.test(next)) {
    351                     setArg(key, next === 'true');
    352                     i++;
    353                 }
    354                 else {
    355                     setArg(key, true);
    356                 }
    357             }
    358             else if (arg.match(/^-[^-]+/)) {
    359                 var letters = arg.slice(1,-1).split('');
    360                 
    361                 var broken = false;
    362                 for (var j = 0; j < letters.length; j++) {
    363                     if (letters[j+1] && letters[j+1].match(/\W/)) {
    364                         setArg(letters[j], arg.slice(j+2));
    365                         broken = true;
    366                         break;
    367                     }
    368                     else {
    369                         setArg(letters[j], true);
    370                     }
    371                 }
    372                 
    373                 if (!broken) {
    374                     var key = arg.slice(-1)[0];
    375                     
    376                     if (args[i+1] && !args[i+1].match(/^-/)
    377                     && !flags.bools[key]
    378                     && (aliases[key] ? !flags.bools[aliases[key]] : true)) {
    379                         setArg(key, args[i+1]);
    380                         i++;
    381                     }
    382                     else if (args[i+1] && /true|false/.test(args[i+1])) {
    383                         setArg(key, args[i+1] === 'true');
    384                         i++;
    385                     }
    386                     else {
    387                         setArg(key, true);
    388                     }
    389                 }
    390             }
    391             else {
    392                 var n = Number(arg);
    393                 argv._.push(flags.strings['_'] || isNaN(n) ? arg : n);
    394             }
    395         }
    396         
    397         Object.keys(defaults).forEach(function (key) {
    398             if (!(key in argv)) {
    399                 argv[key] = defaults[key];
    400                 if (key in aliases) {
    401                     argv[aliases[key]] = defaults[key];
    402                 }
    403             }
    404         });
    405         
    406         if (demanded._ && argv._.length < demanded._) {
    407             fail('Not enough non-option arguments: got '
    408                 + argv._.length + ', need at least ' + demanded._
    409             );
    410         }
    411         
    412         var missing = [];
    413         Object.keys(demanded).forEach(function (key) {
    414             if (!argv[key]) missing.push(key);
    415         });
    416         
    417         if (missing.length) {
    418             fail('Missing required arguments: ' + missing.join(', '));
    419         }
    420         
    421         checks.forEach(function (f) {
    422             try {
    423                 if (f(argv) === false) {
    424                     fail('Argument check failed: ' + f.toString());
    425                 }
    426             }
    427             catch (err) {
    428                 fail(err)
    429             }
    430         });
    431         
    432         return argv;
    433     }
    434     
    435     function longest (xs) {
    436         return Math.max.apply(
    437             null,
    438             xs.map(function (x) { return x.length })
    439         );
    440     }
    441     
    442     return self;
    443 };
    444 
    445 // rebase an absolute path to a relative one with respect to a base directory
    446 // exported for tests
    447 exports.rebase = rebase;
    448 function rebase (base, dir) {
    449     var ds = path.normalize(dir).split('/').slice(1);
    450     var bs = path.normalize(base).split('/').slice(1);
    451     
    452     for (var i = 0; ds[i] && ds[i] == bs[i]; i++);
    453     ds.splice(0, i); bs.splice(0, i);
    454     
    455     var p = path.normalize(
    456         bs.map(function () { return '..' }).concat(ds).join('/')
    457     ).replace(/\/$/,'').replace(/^$/, '.');
    458     return p.match(/^[.\/]/) ? p : './' + p;
    459 };
    460 
    461 function setKey (obj, keys, value) {
    462     var o = obj;
    463     keys.slice(0,-1).forEach(function (key) {
    464         if (o[key] === undefined) o[key] = {};
    465         o = o[key];
    466     });
    467     
    468     var key = keys[keys.length - 1];
    469     if (o[key] === undefined || typeof o[key] === 'boolean') {
    470         o[key] = value;
    471     }
    472     else if (Array.isArray(o[key])) {
    473         o[key].push(value);
    474     }
    475     else {
    476         o[key] = [ o[key], value ];
    477     }
    478 }