jade.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. #!/usr/bin/env node
  2. /**
  3. * Module dependencies.
  4. */
  5. var fs = require('fs')
  6. , program = require('commander')
  7. , path = require('path')
  8. , basename = path.basename
  9. , dirname = path.dirname
  10. , resolve = path.resolve
  11. , normalize = path.normalize
  12. , join = path.join
  13. , mkdirp = require('mkdirp')
  14. , jade = require('../');
  15. // jade options
  16. var options = {};
  17. // options
  18. program
  19. .version(require('../package.json').version)
  20. .usage('[options] [dir|file ...]')
  21. .option('-O, --obj <str|path>', 'JavaScript options object or JSON file containing it')
  22. .option('-o, --out <dir>', 'output the compiled html to <dir>')
  23. .option('-p, --path <path>', 'filename used to resolve includes')
  24. .option('-P, --pretty', 'compile pretty html output')
  25. .option('-c, --client', 'compile function for client-side runtime.js')
  26. .option('-n, --name <str>', 'The name of the compiled template (requires --client)')
  27. .option('-D, --no-debug', 'compile without debugging (smaller functions)')
  28. .option('-w, --watch', 'watch files for changes and automatically re-render')
  29. .option('-E, --extension <ext>', 'specify the output file extension')
  30. .option('-H, --hierarchy', 'keep directory hierarchy when a directory is specified')
  31. .option('--name-after-file', 'Name the template after the last section of the file path (requires --client and overriden by --name)')
  32. .option('--doctype <str>', 'Specify the doctype on the command line (useful if it is not specified by the template)')
  33. program.on('--help', function(){
  34. console.log(' Examples:');
  35. console.log('');
  36. console.log(' # translate jade the templates dir');
  37. console.log(' $ jade templates');
  38. console.log('');
  39. console.log(' # create {foo,bar}.html');
  40. console.log(' $ jade {foo,bar}.jade');
  41. console.log('');
  42. console.log(' # jade over stdio');
  43. console.log(' $ jade < my.jade > my.html');
  44. console.log('');
  45. console.log(' # jade over stdio');
  46. console.log(' $ echo \'h1 Jade!\' | jade');
  47. console.log('');
  48. console.log(' # foo, bar dirs rendering to /tmp');
  49. console.log(' $ jade foo bar --out /tmp ');
  50. console.log('');
  51. });
  52. program.parse(process.argv);
  53. // options given, parse them
  54. if (program.obj) {
  55. options = parseObj(program.obj);
  56. }
  57. /**
  58. * Parse object either in `input` or in the file called `input`. The latter is
  59. * searched first.
  60. */
  61. function parseObj (input) {
  62. var str, out;
  63. try {
  64. str = fs.readFileSync(program.obj);
  65. } catch (e) {
  66. return eval('(' + program.obj + ')');
  67. }
  68. // We don't want to catch exceptions thrown in JSON.parse() so have to
  69. // use this two-step approach.
  70. return JSON.parse(str);
  71. }
  72. // --path
  73. if (program.path) options.filename = program.path;
  74. // --no-debug
  75. options.compileDebug = program.debug;
  76. // --client
  77. options.client = program.client;
  78. // --pretty
  79. options.pretty = program.pretty;
  80. // --watch
  81. options.watch = program.watch;
  82. // --name
  83. if (typeof program.name === 'string') {
  84. options.name = program.name;
  85. }
  86. // --doctype
  87. options.doctype = program.doctype;
  88. // left-over args are file paths
  89. var files = program.args;
  90. // array of paths that are being watched
  91. var watchList = [];
  92. // function for rendering
  93. var render = program.watch ? tryRender : renderFile;
  94. // compile files
  95. if (files.length) {
  96. console.log();
  97. if (options.watch) {
  98. process.on('SIGINT', function() {
  99. process.exit(1);
  100. });
  101. }
  102. files.forEach(function (file) {
  103. render(file);
  104. });
  105. process.on('exit', function () {
  106. console.log();
  107. });
  108. // stdio
  109. } else {
  110. stdin();
  111. }
  112. /**
  113. * Watch for changes on path
  114. *
  115. * Renders `base` if specified, otherwise renders `path`.
  116. */
  117. function watchFile(path, base, rootPath) {
  118. path = normalize(path);
  119. if (watchList.indexOf(path) !== -1) return;
  120. console.log(" \033[90mwatching \033[36m%s\033[0m", path);
  121. fs.watchFile(path, {persistent: true, interval: 200},
  122. function (curr, prev) {
  123. // File doesn't exist anymore. Keep watching.
  124. if (curr.mtime.getTime() === 0) return;
  125. // istanbul ignore if
  126. if (curr.mtime.getTime() === prev.mtime.getTime()) return;
  127. tryRender(base || path, rootPath);
  128. });
  129. watchList.push(path);
  130. }
  131. /**
  132. * Convert error to string
  133. */
  134. function errorToString(e) {
  135. return e.stack || /* istanbul ignore next */ (e.message || e);
  136. }
  137. /**
  138. * Try to render `path`; if an exception is thrown it is printed to stderr and
  139. * otherwise ignored.
  140. *
  141. * This is used in watch mode.
  142. */
  143. function tryRender(path, rootPath) {
  144. try {
  145. renderFile(path, rootPath);
  146. } catch (e) {
  147. // keep watching when error occured.
  148. console.error(errorToString(e));
  149. }
  150. }
  151. /**
  152. * Compile from stdin.
  153. */
  154. function stdin() {
  155. var buf = '';
  156. process.stdin.setEncoding('utf8');
  157. process.stdin.on('data', function(chunk){ buf += chunk; });
  158. process.stdin.on('end', function(){
  159. var output;
  160. if (options.client) {
  161. output = jade.compileClient(buf, options);
  162. } else {
  163. var fn = jade.compile(buf, options);
  164. var output = fn(options);
  165. }
  166. process.stdout.write(output);
  167. }).resume();
  168. process.on('SIGINT', function() {
  169. process.stdout.write('\n');
  170. process.stdin.emit('end');
  171. process.stdout.write('\n');
  172. process.exit();
  173. })
  174. }
  175. var hierarchyWarned = false;
  176. /**
  177. * Process the given path, compiling the jade files found.
  178. * Always walk the subdirectories.
  179. *
  180. * @param path path of the file, might be relative
  181. * @param rootPath path relative to the directory specified in the command
  182. */
  183. function renderFile(path, rootPath) {
  184. var re = /\.jade$/;
  185. var stat = fs.lstatSync(path);
  186. // Found jade file/\.jade$/
  187. if (stat.isFile() && re.test(path)) {
  188. // Try to watch the file if needed. watchFile takes care of duplicates.
  189. if (options.watch) watchFile(path, null, rootPath);
  190. if (program.nameAfterFile) {
  191. options.name = getNameFromFileName(path);
  192. }
  193. var fn = options.client
  194. ? jade.compileFileClient(path, options)
  195. : jade.compileFile(path, options);
  196. if (options.watch && fn.dependencies) {
  197. // watch dependencies, and recompile the base
  198. fn.dependencies.forEach(function (dep) {
  199. watchFile(dep, path, rootPath);
  200. });
  201. }
  202. // --extension
  203. var extname;
  204. if (program.extension) extname = '.' + program.extension;
  205. else if (options.client) extname = '.js';
  206. else extname = '.html';
  207. // path: foo.jade -> foo.<ext>
  208. path = path.replace(re, extname);
  209. if (program.out) {
  210. // prepend output directory
  211. if (rootPath && program.hierarchy) {
  212. // replace the rootPath of the resolved path with output directory
  213. path = resolve(path).replace(new RegExp('^' + resolve(rootPath)), '');
  214. path = join(program.out, path);
  215. } else {
  216. if (rootPath && !hierarchyWarned) {
  217. console.warn('In Jade 2.0.0 --hierarchy will become the default.');
  218. hierarchyWarned = true;
  219. }
  220. // old behavior or if no rootPath handling is needed
  221. path = join(program.out, basename(path));
  222. }
  223. }
  224. var dir = resolve(dirname(path));
  225. mkdirp.sync(dir, 0755);
  226. var output = options.client ? fn : fn(options);
  227. fs.writeFileSync(path, output);
  228. console.log(' \033[90mrendered \033[36m%s\033[0m', normalize(path));
  229. // Found directory
  230. } else if (stat.isDirectory()) {
  231. var files = fs.readdirSync(path);
  232. files.map(function(filename) {
  233. return path + '/' + filename;
  234. }).forEach(function (file) {
  235. render(file, rootPath || path);
  236. });
  237. }
  238. }
  239. /**
  240. * Get a sensible name for a template function from a file path
  241. *
  242. * @param {String} filename
  243. * @returns {String}
  244. */
  245. function getNameFromFileName(filename) {
  246. var file = basename(filename, '.jade');
  247. return file.toLowerCase().replace(/[^a-z0-9]+([a-z])/g, function (_, character) {
  248. return character.toUpperCase();
  249. }) + 'Template';
  250. }