rule.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. var path = require('path')
  2. , fs = require('fs')
  3. , Task = require('./task/task').Task
  4. , Matcher
  5. , rule = {}
  6. , Rule;
  7. // Define a helper object with some utility functions
  8. Matcher = new (function () {
  9. // Split a task to two parts, name space and task name.
  10. // For example, given 'foo:bin/a%.c', return an object with
  11. // - 'ns' : foo
  12. // - 'name' : bin/a%.c
  13. this.split = function(task) {
  14. var parts = task.split(':')
  15. , name = parts.pop()
  16. , ns = this.resolveNS( parts );
  17. return {
  18. 'name' : name,
  19. 'ns' : ns
  20. };
  21. };
  22. // Return the namespace based on an array of names.
  23. // For example, given ['foo', 'baz' ], return the namespace
  24. //
  25. // default -> foo -> baz
  26. //
  27. // where default is the global root namespace
  28. // and -> means child namespace.
  29. this.resolveNS = function(parts) {
  30. var ns = jake.defaultNamespace;
  31. for(var i = 0, l = parts.length; ns && i < l; i++) {
  32. ns = ns.childNamespaces[parts[i]];
  33. }
  34. return ns;
  35. };
  36. // Given a pattern p, say 'foo:bin/a%.c'
  37. // Return an object with
  38. // - 'ns' : foo
  39. // - 'dir' : bin
  40. // - 'prefix' : a
  41. // - 'suffix' : .c
  42. this.resolve = function(p) {
  43. var task = this.split(p),
  44. name = task.name,
  45. ns = task.ns;
  46. var split = path.basename(name).split('%');
  47. return {
  48. ns: ns
  49. , dir: path.dirname(name)
  50. , prefix: split[0]
  51. , suffix: split[1]
  52. };
  53. };
  54. // Test whether string a is a suffix of string b
  55. this.stringEndWith = function (a,b) {
  56. var l;
  57. return (l = b.lastIndexOf(a)) == -1 ? false : l + a.length == b.length;
  58. };
  59. // Replace the suffix a of the string s with b.
  60. // Note that, it is assumed a is a suffix of s.
  61. this.stringReplaceSuffix = function (s, a, b) {
  62. return s.slice(0,s.lastIndexOf(a)) + b;
  63. };
  64. // Test wether the a prerequisite matchs the pattern.
  65. // The arg 'pattern' does not have namespace as prefix.
  66. // For example, the following tests are true
  67. //
  68. // pattern | name
  69. // bin/%.o | bin/main.o
  70. // bin/%.o | foo:bin/main.o
  71. //
  72. // The following tests are false (trivally)
  73. //
  74. // pattern | name
  75. // bin/%.o | foobin/main.o
  76. // bin/%.o | bin/main.oo
  77. this.match = function(pattern, name) {
  78. var p
  79. , task
  80. , ns
  81. , obj
  82. , filename;
  83. if (pattern instanceof RegExp) {
  84. return pattern.test(name);
  85. }
  86. else if (pattern.indexOf('%') == -1) {
  87. // No Pattern. No Folder. No Namespace.
  88. // A Simple Suffix Rule. Just test suffix
  89. return this.stringEndWith(pattern, name);
  90. }
  91. else {
  92. // Resolve the dir, prefix and suffix of pattern
  93. p = this.resolve(pattern);
  94. // Resolve the namespace and task-name
  95. task = this.split(name);
  96. name = task.name;
  97. ns = task.ns;
  98. // Set the objective as the task-name
  99. obj = name;
  100. // Namespace is already matched.
  101. // Check dir
  102. if (path.dirname(obj) != p.dir) {
  103. return false;
  104. }
  105. filename = path.basename(obj);
  106. // Check file name length
  107. if ((p.prefix.length + p.suffix.length + 1) > filename.length) {
  108. // Length does not match.
  109. return false;
  110. }
  111. // Check prefix
  112. if (filename.indexOf(p.prefix) !== 0) {
  113. return false;
  114. }
  115. // Check suffix
  116. if (!this.stringEndWith(p.suffix, filename)) {
  117. return false;
  118. }
  119. // OK. Find a match.
  120. return true;
  121. }
  122. };
  123. // Generate the source based on
  124. // - name name for the synthesized task
  125. // - pattern pattern for the objective
  126. // - source pattern for the source
  127. //
  128. // Return the source with properties
  129. // - dep the prerequisite of source
  130. // (with the namespace)
  131. //
  132. // - file the file name of source
  133. // (without the namespace)
  134. //
  135. // For example, given
  136. //
  137. // - name foo:bin/main.o
  138. // - pattern bin/%.o
  139. // - source src/%.c
  140. //
  141. // return 'foo:src/main.c',
  142. //
  143. this.getSource = function(name, pattern, source) {
  144. var dep
  145. , pat
  146. , match
  147. , file
  148. , src;
  149. // Regex pattern -- use to look up the extension
  150. if (pattern instanceof RegExp) {
  151. match = pattern.exec(name);
  152. if (match) {
  153. if (typeof source == 'function') {
  154. src = source(name);
  155. }
  156. else {
  157. src = this.stringReplaceSuffix(name, match[0], source);
  158. }
  159. }
  160. }
  161. // Assume string
  162. else {
  163. // Simple string suffix replacement
  164. if (pattern.indexOf('%') == -1) {
  165. if (typeof source == 'function') {
  166. src = source(name);
  167. }
  168. else {
  169. src = this.stringReplaceSuffix(name, pattern, source);
  170. }
  171. }
  172. // Percent-based substitution
  173. else {
  174. pat = pattern.replace('%', '(.*?)');
  175. pat = new RegExp(pat);
  176. match = pat.exec(name);
  177. if (match) {
  178. if (typeof source == 'function') {
  179. src = source(name);
  180. }
  181. else {
  182. file = match[1];
  183. file = source.replace('%', file);
  184. dep = match[0];
  185. src = name.replace(dep, file);
  186. }
  187. }
  188. }
  189. }
  190. return src;
  191. };
  192. })();
  193. Rule = function (opts) {
  194. this.pattern = opts.pattern;
  195. this.source = opts.source;
  196. this.prereqs = opts.prereqs;
  197. this.action = opts.action;
  198. this.opts = opts.opts;
  199. this.desc = opts.desc;
  200. this.ns = opts.ns;
  201. };
  202. Rule.prototype = new (function () {
  203. // Create a file task based on this rule for the specified
  204. // task-name
  205. // ======
  206. // FIXME: Right now this just throws away any passed-in args
  207. // for the synthsized task (taskArgs param)
  208. // ======
  209. this.createTask = function (fullName, level) {
  210. var self = this
  211. , pattern
  212. , source
  213. , action
  214. , opts
  215. , prereqs
  216. , parts
  217. , valid
  218. , src
  219. , tNs
  220. , createdTask
  221. , name = Task.getBaseTaskName(fullName)
  222. , nsPath = Task.getBaseNamespacePath(fullName)
  223. , ns = this.ns.resolveNamespace(nsPath);
  224. pattern = this.pattern;
  225. source = this.source;
  226. if (typeof source == 'string') {
  227. src = Matcher.getSource(name, pattern, source);
  228. }
  229. else {
  230. src = source(name);
  231. }
  232. // TODO: Write a utility function that appends a
  233. // taskname to a namespace path
  234. src = nsPath.split(':').filter(function (item) {
  235. return !!item;
  236. }).concat(src).join(':');
  237. // Generate the prerequisite for the matching task.
  238. // It is the original prerequisites plus the prerequisite
  239. // representing source file, i.e.,
  240. //
  241. // rule( '%.o', '%.c', ['some.h'] ...
  242. //
  243. // If the objective is main.o, then new task should be
  244. //
  245. // file( 'main.o', ['main.c', 'some.h' ] ...
  246. prereqs = this.prereqs.slice(); // Get a copy to work with
  247. prereqs.unshift(src);
  248. // Prereq should be:
  249. // 1. an existing task
  250. // 2. an existing file on disk
  251. // 3. a valid rule (i.e., not at too deep a level)
  252. valid = prereqs.some(function (p) {
  253. var ns = self.ns;
  254. return ns.resolveTask(p) ||
  255. fs.existsSync(Task.getBaseTaskName(p)) ||
  256. jake.attemptRule(p, ns, level + 1);
  257. });
  258. // If any of the prereqs aren't valid, the rule isn't valid
  259. if (!valid) {
  260. return null;
  261. }
  262. // Otherwise, hunky-dory, finish creating the task for the rule
  263. else {
  264. // Create the action for the task
  265. action = function () {
  266. var task = this;
  267. self.action.apply(task);
  268. };
  269. opts = this.opts;
  270. // Insert the file task into Jake
  271. //
  272. // Since createTask function stores the task as a child task
  273. // of currentNamespace. Here we temporariliy switch the namespace.
  274. // FIXME: Should allow optional ns passed in instead of this hack
  275. tNs = jake.currentNamespace;
  276. jake.currentNamespace = ns;
  277. createdTask = jake.createTask('file', name, prereqs, action, opts);
  278. createdTask.source = src.split(':').pop();
  279. jake.currentNamespace = tNs;
  280. return createdTask;
  281. }
  282. };
  283. this.match = function (name) {
  284. return Matcher.match(this.pattern, name);
  285. };
  286. })();
  287. rule.Rule = Rule;
  288. rule.Matcher = Matcher;
  289. module.exports = rule;