/*\ title: js/Recipe.js Recipe processing is in four parts: 1) The recipe file is parsed and any subrecipe files loaded recursively into this structure: this.recipe = [ {marker: , filepath: , baseDir: }, ... {marker: , filepath: , baseDir: }, [ {marker: , filepath: , baseDir: }, ... {marker: , filepath: , baseDir: }, ] ]; 2) The tiddler files referenced by the recipe structure are loaded into it as an additional 'tiddlers' member that contains an array of hashmaps of tiddler field values. 3) The recipe is scanned to create a hashmap of markers and their associated tiddlers. In cases where more than one tiddler with the same title is assigned to a marker, the one that is later in the recipe file wins. At this point tiddlers are placed in the store so that they can be referenced by title this.markers = { : [,,...], : [,,...], ... } 4) Finally, to actually cook the recipe, the template is processed by replacing the markers with the text of the associated tiddlers \*/ (function(){ /*jslint node: true */ "use strict"; var Tiddler = require("./Tiddler.js").Tiddler, utils = require("./Utils.js"), retrieveFile = require("./FileRetriever.js").retrieveFile, fs = require("fs"), path = require("path"), util = require("util"), async = require("async"); /* Load a recipe file. Arguments are: options: See below callback: Function to be called when the recipe has been loaded as callback(err), null === success Options include: filepath: The filepath of the recipe file to load. Can be a local path or an HTTP URL store: Indicates the WikiStore to use to store the tiddlers (mandatory) */ var Recipe = function(options,callback) { var me = this; this.filepath = options.filepath; this.store = options.store; this.callback = callback; this.recipe = []; this.markers = {}; // A task queue for loading recipe files this.recipeQueue = async.queue(function(task,callback) { retrieveFile(task.filepath,task.baseDir,function(err,data) { if(err) { me.callback(err); } else { callback(me.processRecipeFile(task.recipe,data.text,data.path)); } }); },1); // A task queue for loading tiddler files this.tiddlerQueue = async.queue(function(task,callback) { me.readTiddlerFile(task.filepath,task.baseDir,function(err,data) { if(err) { me.callback(err); } else { if(data.length === 0) { callback("Tiddler file '" + task.filepath + "' does not contain any tiddlers"); } else { if(task.recipeLine.fields) { for(var t=0; t 0) { return "Unexpected indentation in recipe file '" + recipePath + "'"; } if(match.marker === "recipe") { var insertionPoint = recipe.push([]) - 1; this.recipeQueue.push({ filepath: match.value, baseDir: path.dirname(recipePath), recipe: recipe[insertionPoint] }); } else { var fieldLines = [], fieldMatch = matchLine(lines[line]); while(fieldMatch && fieldMatch.indent.length > 0) { fieldLines.push(lines[line++]); fieldMatch = matchLine(lines[line]); } var fields = {}; if(fieldLines.length > 0) { fields = this.store.deserializeTiddlers("application/x-tiddler",fieldLines.join("\n"),{})[0]; } recipe.push({ marker: match.marker, filepath: match.value, baseDir: path.dirname(recipePath), fields: fields}); } } } return null; }; /* Read a tiddler file and callback with an array of hashmaps of tiddler fields. For single tiddler files it also looks for an accompanying .meta file filepath: the filepath to the tiddler file (possibly relative) baseDir: the base directory from which the filepath is taken callback: called on completion as callback(err,data) where data is an array of tiddler fields */ Recipe.prototype.readTiddlerFile = function(filepath,baseDir,callback) { var me = this; // Read the tiddler file retrieveFile(filepath,baseDir,function(err,data) { if (err) { callback(err); return; } // Use the filepath as the default title for the tiddler var fields = { title: data.path }; var tiddlers = me.store.deserializeTiddlers(data.extname,data.text,fields); // Check for the .meta file if(data.extname !== ".json" && tiddlers.length === 1) { var metafile = filepath + ".meta"; retrieveFile(metafile,baseDir,function(err,data) { if(err && err.code !== "ENOENT" && err.code !== "404") { callback(err); } else { var fields = tiddlers[0]; if(!err) { var text = data.text.split("\n\n")[0]; if(text) { fields = me.store.deserializeTiddlers("application/x-tiddler",text,fields)[0]; } } callback(null,[fields]); } }); } else { callback(null,tiddlers); } }); }; // Return a string of the cooked recipe Recipe.prototype.cook = function() { var template = this.markers.template ? this.store.getTiddlerText(this.markers.template[0]) : "", out = [], templateLines = template.split("\n"); for(var line=0; line)|(?:<!--@@(.*)@@-->)$/gi; var match = templateRegExp.exec(templateLines[line]); if(match) { var marker = match[1] === undefined ? match[2] : match[1]; this.outputTiddlersForMarker(out,marker); } else { if(line !== templateLines.length-1) { out.push(templateLines[line],"\n"); } } } return out.join(""); }; // Output all the tiddlers in the recipe with a particular marker Recipe.prototype.outputTiddlersForMarker = function(out,marker) { var tiddlers = [], outputType = Recipe.tiddlerOutputMapper[marker] || "raw", outputter = Recipe.tiddlerOutputter[outputType]; if(this.markers[marker]) { tiddlers = this.markers[marker]; } if(marker === "tiddler") { this.store.forEachTiddler(function(title,tiddler) { if(tiddlers.indexOf(title) === -1) { tiddlers.push(title); } }); } if(outputter) { if((out.length > 1) && (Recipe.compatibilityCheats[marker] === "suppressLeadingNewline")) { var lastLine = out[out.length-1]; if(lastLine.substr(-1) === "\n") { out[out.length-1] = lastLine.substr(0,lastLine.length-1); } } outputter.call(this,out,tiddlers); if(Recipe.compatibilityCheats[marker] === "addTrailingNewline") { out.push("\n"); } } }; // Allows for specialised processing for certain markers Recipe.tiddlerOutputMapper = { tiddler: "div", js: "javascript", jslib: "javascript", jsdeprecated: "javascript", jquery: "javascript", shadow: "shadow", title: "title", jsmodule: "jsmodule", pluginmodule: "pluginmodule", base64ie: "base64ie" }; Recipe.compatibilityCheats = { "prehead": "addTrailingNewline", "posthead": "addTrailingNewline", "prebody": "addTrailingNewline", "postscript": "addTrailingNewline", "title": "suppressLeadingNewline" }; Recipe.tiddlerOutputter = { raw: function(out,tiddlers) { // The default is just to output the raw text of the tiddler, ignoring any metadata for(var t=0; t for(var t=0; t"); out.push("define(\"" + title + "\",function(require,exports,module) {"); out.push(tid.text); out.push("});"); out.push(""); } }, pluginmodule: function(out,tiddlers) { // plugin modules are output as a special script tag for(var t=0; t"); out.push("$tw.defineModule(\"" + title + "\",\"" + tid.module + "\",function(module,exports,require) {"); out.push(tid.text); out.push("});"); out.push(""); } }, base64ie: function(out,tiddlers) { // For IE, we output binary tiddlers in MHTML format (http://www.phpied.com/mhtml-when-you-need-data-uris-in-ie7-and-under/) if(tiddlers.length) { out.push("\n"); } } }; // Cook an RSS file of the most recent 20 tiddlers Recipe.prototype.cookRss = function() { var me = this, numRssItems = 20, s = [], d = new Date(), u = this.store.renderTiddler("text/plain","SiteUrl"), encodeTiddlyLink = function(title) { return title.indexOf(" ") == -1 ? title : "[[" + title + "]]"; }, tiddlerToRssItem = function(tiddler,uri) { var s = "" + utils.htmlEncode(tiddler.title) + "\n"; s += "" + utils.htmlEncode(me.store.renderTiddler("text/html",tiddler.title)) + "\n"; var i; if(tiddler.tags) { for(i=0; i\n"; } } s += "" + uri + "#" + encodeURIComponent(encodeTiddlyLink(tiddler.title)) + "\n"; if(tiddler.modified) { s +="" + tiddler.modified.toUTCString() + "\n"; } return s; }, getRssTiddlers = function(sortField,excludeTag) { var r = []; me.store.forEachTiddler(sortField,excludeTag,function(title,tiddler) { if(!tiddler.hasTag(excludeTag) && tiddler.modified !== undefined) { r.push(tiddler); } }); return r; }; // Assemble the header s.push("<" + "?xml version=\"1.0\"?" + ">"); s.push(""); s.push(""); s.push("" + utils.htmlEncode(this.store.renderTiddler("text/plain","SiteTitle")) + ""); if(u) s.push("" + utils.htmlEncode(u) + ""); s.push("" + utils.htmlEncode(this.store.renderTiddler("text/plain","SiteSubtitle")) + ""); //s.push("" + config.locale + ""); s.push("" + d.toUTCString() + ""); s.push("" + d.toUTCString() + ""); s.push("http://blogs.law.harvard.edu/tech/rss"); s.push("https://github.com/Jermolene/cook.js"); // The body var tiddlers = getRssTiddlers("modified","excludeLists"); var i,n = numRssItems > tiddlers.length ? 0 : tiddlers.length-numRssItems; for(i=tiddlers.length-1; i>=n; i--) { s.push("\n" + tiddlerToRssItem(tiddlers[i],u) + "\n"); } // And footer s.push(""); s.push(""); // Save it all return s.join("\n"); }; exports.Recipe = Recipe; })();