raw.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. var path = require('path')
  2. var fs = require('fs')
  3. var TerraformError = exports.TerraformError = require("../error").TerraformError
  4. /**
  5. * This is our processor map for all the helpers.
  6. *
  7. * This is our map. This ideally this would be dynamically created by
  8. * some sort of plugin architecture but lets not let perfect be the
  9. * enemy of good :)
  10. *
  11. */
  12. var processors = exports.processors = {
  13. "html": ["jade", "ejs", "md", "nunjucks", "njk"],
  14. "css" : ["styl", "less", "scss", "sass"],
  15. "js" : ["coffee"]
  16. }
  17. /**
  18. * Build Priority List
  19. *
  20. * returns a priority list of files to look for on a given request.
  21. *
  22. * `css` and `html` are special extensions that will add `less` and `jade`
  23. * to the priority list (respectively).
  24. *
  25. * e.g
  26. *
  27. * buildPriorityList("foobar")
  28. * => ["foobar", "foobar.html", "foobar.jade", "foobar.html.jade"]
  29. *
  30. * buildPriorityList("foobar.css")
  31. * => ["foobar.css", "foobar.less", "foobar.css.less"]
  32. *
  33. * buildPriorityList("foobar.html")
  34. * => ["foobar.html", "foobar.jade", "foobar.html.jade"]
  35. *
  36. * buildPriorityList("foobar.jade")
  37. * => ["foobar.jade"]
  38. *
  39. * buildPriorityList("foobar.html.jade.html")
  40. * => ["foobar.html.jade.html", "foobar.html.jade.jade", "foobar.html.jade.html.jade"]
  41. *
  42. * buildPriorityList("hello/foobar")
  43. * => ["hello/foobar", "hello/foobar.html", "hello/foobar.jade", "hello/foobar.html.jade"]
  44. *
  45. */
  46. var buildPriorityList = exports.buildPriorityList = function(filePath){
  47. var list = []
  48. /**
  49. * get extension
  50. */
  51. var ext = path.extname(filePath).replace(/^\./, '')
  52. var processor = processors[ext]
  53. if(processor){
  54. // foo.html => foo.jade
  55. processor.forEach(function(p){
  56. var regexp = new RegExp(ext + '$')
  57. list.push(filePath.replace(regexp, p))
  58. })
  59. // foo.html => foo.html.jade
  60. processor.forEach(function(p){
  61. list.push(filePath + '.' + p)
  62. })
  63. }else{
  64. // assume template when unknown processor
  65. if(processors['html'].indexOf(ext) !== -1){
  66. list.push(filePath)
  67. }else{
  68. // foo.xml => foo.xml.jade
  69. processors['html'].forEach(function(p){
  70. list.push(filePath + '.' + p)
  71. })
  72. }
  73. }
  74. // remove leading and trailing slashes
  75. var list = list.map(function(item){ return item.replace(/^\/|^\\|\/$/g, '') })
  76. return list
  77. }
  78. /**
  79. * Find First File
  80. *
  81. * Takes a directory and an array of files. Returns the first file in the list that exists.
  82. *
  83. * findFirstFile("/path/to/public", ["foo.html", "foo.jade", "foo.html.jade"])
  84. * => "foo.jade"
  85. *
  86. * returns null if no file is found.
  87. *
  88. */
  89. var findFirstFile = exports.findFirstFile = function(dir, arr) {
  90. var dirPath = path.dirname(path.join(dir, arr[0]))
  91. var fullPath = path.resolve(dirPath)
  92. try{
  93. var list = fs.readdirSync(fullPath)
  94. }catch(e){
  95. var list = []
  96. }
  97. var first = null
  98. if(list){
  99. arr.reverse().map(function(item){
  100. var fileName = path.basename(item)
  101. if(list.indexOf(fileName) !== -1){
  102. first = item
  103. }
  104. })
  105. }
  106. return first
  107. }
  108. var layoutPriorityList = buildPriorityList("_layout.html")
  109. /**
  110. * Find Nearest Layout
  111. *
  112. * Walks up the tree to find the nearest _layout file. Returns relative path to root
  113. *
  114. * findNearestLayout("/path/to/public", "/path/to/public/nested/dir")
  115. * => "_layout.jade"
  116. *
  117. * returns null if no layout is found.
  118. *
  119. */
  120. var findNearestLayout = exports.findNearestLayout = function(rootPath, dirPath) {
  121. // lets make sure we are working with an absolute path
  122. var dirPath = path.resolve(rootPath, (dirPath || ""))
  123. var layout = findFirstFile(dirPath, layoutPriorityList)
  124. // if we have a layout we return relative path
  125. if(layout !== null)
  126. return path.relative(rootPath, path.join(dirPath, layout))
  127. return path.relative(rootPath, dirPath) !== ''
  128. ? findNearestLayout(rootPath, path.resolve(dirPath, ".."))
  129. : null // we reached the root
  130. }
  131. /**
  132. * Find Default Layout
  133. *
  134. * returns null if outputing json
  135. */
  136. var findDefaultLayout = exports.findDefaultLayout = function(rootPath, filePath) {
  137. var arr = filePath.split(".")
  138. if (arr.length > 2 && arr[1] == "json"){
  139. return null
  140. } else {
  141. return findNearestLayout(rootPath, path.dirname(filePath))
  142. }
  143. }
  144. /**
  145. * Is Empty
  146. *
  147. * Checks if Object is empty & returns true or false.
  148. *
  149. */
  150. var isEmpty = function(obj) {
  151. for(var prop in obj) {
  152. if(Object.prototype.hasOwnProperty.call(obj,prop))
  153. return false;
  154. }
  155. return true;
  156. }
  157. /**
  158. *
  159. * Walks directory and build the data object.
  160. *
  161. * If we call the dataTree on the public dir
  162. *
  163. * public/
  164. * |- _data.json
  165. * |- articles/
  166. * | `- _data.json
  167. * `- people
  168. * `- _data.json
  169. *
  170. * We get the following...
  171. *
  172. * {
  173. * "_data": {...},
  174. * "articles": {
  175. * "_data": {...}
  176. * },
  177. * "people": {
  178. * "_data": {...}
  179. * }
  180. * }
  181. *
  182. */
  183. var dataTree = exports.dataTree = function (filename) {
  184. var dirPath = path.resolve(filename)
  185. try{
  186. var list = fs.readdirSync(dirPath)
  187. }catch(e){
  188. e.source = "Config"
  189. e.dest = "Config"
  190. e.lineno = -1
  191. e.filename = filename
  192. e.stack = null
  193. throw new TerraformError(e)
  194. }
  195. var obj = {}
  196. obj._contents = []
  197. try{
  198. var dataPath = path.resolve(dirPath, "_data")
  199. obj._data = require(dataPath)
  200. delete require.cache[require.resolve(dataPath)]
  201. }catch(e){
  202. if(e.code && e.code === "MODULE_NOT_FOUND"){
  203. // data file failed or does not exist
  204. }else{
  205. e.source = "Data"
  206. e.dest = "Globals"
  207. e.lineno = -1
  208. e.filename = require.resolve(dataPath)
  209. e.stack = fs.readFileSync(e.filename)
  210. throw new TerraformError(e)
  211. }
  212. //console.log(e.code)
  213. }
  214. list.forEach(function(file){
  215. var filePath = path.resolve(dirPath, file)
  216. var stat = fs.statSync(filePath)
  217. if(stat.isDirectory()){
  218. if(['_', '.'].indexOf(file[0]) === -1){
  219. var d = dataTree(filePath)
  220. if(!isEmpty(d))
  221. obj[file] = d
  222. }
  223. }else{
  224. if(["_", "."].indexOf(file[0]) === -1 ) obj._contents.push(outputPath(file))
  225. }
  226. })
  227. if(obj._contents.length == 0)
  228. delete obj._contents
  229. return obj
  230. }
  231. /**
  232. *
  233. * Get Current
  234. *
  235. * returns current object based on the path of source file
  236. *
  237. * getCurrent("foo/bar/baz.jade")
  238. * => { path: ["foo", "bar", "baz"], source: "baz" }
  239. *
  240. * getCurrent("index.html")
  241. * => { path: ["index"], source: "index" }
  242. *
  243. */
  244. exports.getCurrent = function(sourcePath){
  245. // windows
  246. sourcePath = sourcePath.replace(/\\/g,'/')
  247. // this could be a tad smarter
  248. var namespace = sourcePath.split('.').slice(0, -1).join('.').split('/')
  249. return {
  250. source: namespace[namespace.length -1],
  251. path: namespace
  252. }
  253. }
  254. /**
  255. *
  256. * Source Type
  257. *
  258. * Returns processor based on file path.
  259. *
  260. * sourceType("foobar.jade") => "jade"
  261. * sourceType("foobar.less") => "less"
  262. * sourceType("foobar") => null
  263. *
  264. */
  265. exports.sourceType = function(sourcePath){
  266. return path.extname(sourcePath).replace(/^\./, '')
  267. }
  268. /**
  269. *
  270. * Walk Data Tree
  271. *
  272. * Recursive function that returns the data object accociated with path.
  273. *
  274. * var globals = {
  275. * "public": {
  276. * "articles": {
  277. * "_data": {
  278. * "hello-world": "You Found Me!"
  279. * }
  280. * }
  281. * }
  282. * }
  283. *
  284. * walkData(["public", "articles", "hello-world"], globals) => "You Found Me!"
  285. */
  286. var walkData = exports.walkData = function(tail, obj){
  287. var tail = tail.slice(0) // clone array.
  288. var head = tail.shift()
  289. if(obj.hasOwnProperty(head)){
  290. return walkData(tail, obj[head])
  291. }else if(obj.hasOwnProperty("_data")){
  292. return obj["_data"][head]
  293. ? obj["_data"][head]
  294. : null
  295. }else{
  296. return null
  297. }
  298. }
  299. /**
  300. *
  301. * Output Path
  302. *
  303. * Returns output path output for given source file.
  304. *
  305. * allowAlternateExtensions: true. May be turned off for one less RegExp execution
  306. * (and faster performance)
  307. *
  308. * eg.
  309. * foo.jade => foo.html
  310. * foo.html.jade => foo.html
  311. */
  312. var outputPath = exports.outputPath = function(source, allowAlternateExtensions){
  313. if (allowAlternateExtensions == undefined) {
  314. allowAlternateExtensions = true;
  315. }
  316. for(var targetExtension in processors){ // .html, .css, .js
  317. if (processors.hasOwnProperty(targetExtension)) {
  318. processors[targetExtension].forEach(function(sourceExtension){ // .jade, .ejs, .md
  319. if (allowAlternateExtensions && targetExtension !== sourceExtension) { // Don’t bother if it’s .js to .js
  320. // Search for a alternate extension before the known source extension e.g. foobar.bar.jade
  321. var alternateFileExtension = new RegExp("^.*\\.(\\w{3,4})\\." + sourceExtension + "$")
  322. var match = alternateFileExtension.exec(source)
  323. if (match) {
  324. // If found, replace the target extension with the alternate
  325. targetExtension = match[1]
  326. }
  327. }
  328. // Let's try to match known sourceExtensions. Optionally, capture the targetExtension BEFORE.
  329. var regexp = new RegExp('(\\.' + targetExtension + ')?\\.' + sourceExtension + '$')
  330. // If found, will replace the whole extension with target extension, e.g.:
  331. // .html.jade -> .html (matches)
  332. // .jade -> .html (matches)
  333. // .html -> .html (doesn't matches - doesn't matter)
  334. // .foo -> .foo (doesn't matches - doesn't matter)
  335. source = source.replace(regexp, "." + targetExtension)
  336. })
  337. }
  338. }
  339. return source
  340. }
  341. /**
  342. *
  343. * Output Type
  344. *
  345. * Returns output type source file.
  346. *
  347. * eg.
  348. * foo.jade => html
  349. * foo.html.jade => html
  350. */
  351. var outputType = exports.outputType = function(source){
  352. var op = outputPath(source)
  353. return path.extname(op).replace(/^\./, '')
  354. }
  355. /**
  356. *
  357. * Should Ignore
  358. *
  359. * Checks to see if path should be ignored.
  360. *
  361. * eg.
  362. * shouldIgnore('_foo.html') => true
  363. * shouldIgnore('foo_.html') => false
  364. * shouldIgnore('_foo/bar.html') => true
  365. * shouldIgnore('foo/_bar.html') => true
  366. * shouldIgnore('foo/_bar/baz.html') => true
  367. */
  368. exports.shouldIgnore = function(filePath){
  369. // remove starting and trailing slashed
  370. filePath = filePath.replace(/^\/|\/$/g, '')
  371. // create array out of path
  372. var arr = filePath.split(path.sep)
  373. // test for starting underscore, .git, .gitignore
  374. var map = arr.map(function(item){
  375. return item[0] === "_" || item.indexOf(".git") === 0
  376. })
  377. // return if any item starts with underscore
  378. return map.indexOf(true) !== -1
  379. }
  380. /**
  381. *
  382. * isTemplate
  383. *
  384. * returns true if file is a template file
  385. *
  386. * eg.
  387. * isTemplate('foo.jade') => true
  388. * isTemplate('foo.md') => true
  389. * isTemplate('foo.html') => false
  390. * isTemplate('foo/bar.jade') => true
  391. * isTemplate('foo/bar.md') => true
  392. * isTemplate('foo/bar.html') => false
  393. * isTemplate('foo.less') => false
  394. * isTemplate('foo.css') => false
  395. * isTemplate('foo.bar.baz.jade') => true
  396. */
  397. exports.isTemplate = function(filePath){
  398. var ext = path.extname(filePath).replace(/^\./, '')
  399. return processors["html"].indexOf(ext) !== -1
  400. }
  401. /**
  402. *
  403. * isStylesheet
  404. *
  405. * returns true if file is a pre-processor stylesheet file
  406. *
  407. * eg.
  408. * isTemplate('foo.less') => true
  409. * isTemplate('foo.md') => false
  410. * isTemplate('foo.css') => false
  411. * isTemplate('foo.bar.baz.less') => true
  412. */
  413. exports.isStylesheet = function(filePath){
  414. var ext = path.extname(filePath).replace(/^\./, '')
  415. return processors["css"].indexOf(ext) !== -1
  416. }
  417. /**
  418. * isJavaScript(filePath)
  419. *
  420. * returns true if file is a pre-processor javascript file
  421. *
  422. * eg.
  423. * isJavaScript('foo.coffee') => true
  424. * isJavaScript('foo.md') => false
  425. * isJavaScript('foo.css') => false
  426. * isJavaScript('foo.bar.baz.coffee') => true
  427. */
  428. exports.isJavaScript = function(filePath){
  429. var ext = path.extname(filePath).replace(/^\./, '')
  430. return processors["js"].indexOf(ext) !== -1
  431. }
  432. /**
  433. * needsBrowserify
  434. *
  435. * returns true if the code uses require, exports or module but doesn't declare them
  436. */
  437. exports.needsBrowserify = function(source) {
  438. return /^[^#\/'"*]*(require|module|exports)\b/m.test(source)
  439. && !(/\b(function|var|global) +(require|module|exports)\b|\b(module|require) *=[^=]/.test(source))
  440. }
  441. /**
  442. * shouldApplyLayout
  443. *
  444. * returns true if an HTML template engine does not include a partial/layout system and terraform should handle that
  445. */
  446. exports.shouldApplyLayout = function(ext) {
  447. var extsWithLayout = ["nunjucks", "njk"]
  448. return (extsWithLayout.indexOf(ext) < 0)
  449. }