2011-12-13 12:30:09 +00:00
|
|
|
/*\
|
|
|
|
title: js/Recipe.js
|
2011-12-12 10:52:04 +00:00
|
|
|
|
2011-12-13 12:30:09 +00:00
|
|
|
FileRetriever can asynchronously retrieve files from HTTP URLs or the local file system
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe processing is in four parts:
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
1) The recipe file is parsed and any subrecipe files loaded recursively into this structure:
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
this.recipe = [
|
|
|
|
{marker: <marker>, filepath: <filepath>, contextPath: <contextPath>},
|
|
|
|
...
|
|
|
|
{marker: <marker>, filepath: <filepath>, contextPath: <contextPath>},
|
|
|
|
[
|
|
|
|
{marker: <marker>, filepath: <filepath>, contextPath: <contextPath>},
|
|
|
|
...
|
|
|
|
{marker: <marker>, filepath: <filepath>, contextPath: <contextPath>},
|
|
|
|
]
|
|
|
|
];
|
|
|
|
|
|
|
|
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 = {
|
|
|
|
<marker>: [<tiddler title>,<tiddler title>,...],
|
|
|
|
<marker>: [<tiddler title>,<tiddler title>,...],
|
|
|
|
...
|
|
|
|
}
|
|
|
|
|
|
|
|
4) Finally, the template is processed by replacing the markers with the text of the associated tiddlers
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-13 12:30:09 +00:00
|
|
|
\*/
|
|
|
|
(function(){
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-09 16:34:02 +00:00
|
|
|
/*jslint node: true */
|
2011-11-30 17:27:00 +00:00
|
|
|
"use strict";
|
|
|
|
|
2011-11-27 09:46:02 +00:00
|
|
|
var Tiddler = require("./Tiddler.js").Tiddler,
|
2011-11-22 17:42:03 +00:00
|
|
|
utils = require("./Utils.js"),
|
2011-11-28 13:47:38 +00:00
|
|
|
retrieveFile = require("./FileRetriever.js").retrieveFile,
|
2011-11-22 14:29:29 +00:00
|
|
|
fs = require("fs"),
|
|
|
|
path = require("path"),
|
2011-12-02 14:40:18 +00:00
|
|
|
util = require("util"),
|
|
|
|
async = require("async");
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-11 14:51:48 +00:00
|
|
|
var Recipe = function(options,callback) {
|
2011-12-02 14:40:18 +00:00
|
|
|
var me = this;
|
2011-12-11 14:51:48 +00:00
|
|
|
this.filepath = options.filepath;
|
|
|
|
this.store = options.store;
|
|
|
|
this.tiddlerConverters = options.tiddlerConverters;
|
2011-12-11 18:28:09 +00:00
|
|
|
this.textProcessors = options.textProcessors;
|
2011-11-28 13:47:38 +00:00
|
|
|
this.callback = callback;
|
2011-12-02 14:40:18 +00:00
|
|
|
this.recipe = [];
|
|
|
|
this.markers = {};
|
|
|
|
this.recipeQueue = async.queue(function(task,callback) {
|
|
|
|
retrieveFile(task.filepath,task.contextPath,function(err,data) {
|
|
|
|
if(err) {
|
|
|
|
callback(err);
|
|
|
|
} else {
|
|
|
|
me.processRecipeFile(task.recipe,data.text,data.path);
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},1);
|
|
|
|
this.tiddlerQueue = async.queue(function(task,callback) {
|
|
|
|
me.readTiddlerFile(task.filepath,task.contextPath,function(err,data) {
|
|
|
|
if(err) {
|
|
|
|
callback(err);
|
|
|
|
} else {
|
|
|
|
task.recipeLine.tiddlers = data;
|
|
|
|
callback(null);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},1);
|
|
|
|
this.recipeQueue.drain = function() {
|
|
|
|
me.loadTiddlerFiles(me.recipe);
|
|
|
|
};
|
|
|
|
this.tiddlerQueue.drain = function() {
|
|
|
|
me.chooseTiddlers(me.recipe);
|
2011-12-03 12:01:43 +00:00
|
|
|
me.sortTiddlersForMarker("tiddler");
|
2011-12-11 14:51:48 +00:00
|
|
|
me.callback(null);
|
2011-12-02 14:40:18 +00:00
|
|
|
};
|
2011-12-11 14:51:48 +00:00
|
|
|
this.recipeQueue.push({filepath: this.filepath,
|
2011-12-02 14:40:18 +00:00
|
|
|
contextPath: process.cwd(),
|
|
|
|
recipe: this.recipe});
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.prototype.loadTiddlerFiles = function(recipe) {
|
|
|
|
for(var r=0; r<recipe.length; r++) {
|
|
|
|
var recipeLine = recipe[r];
|
|
|
|
if(recipeLine instanceof Array) {
|
|
|
|
this.loadTiddlerFiles(recipeLine);
|
|
|
|
} else {
|
|
|
|
this.tiddlerQueue.push({filepath: recipeLine.filepath, contextPath: recipeLine.contextPath, recipeLine: recipeLine});
|
|
|
|
}
|
|
|
|
}
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-28 13:47:38 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.prototype.chooseTiddlers = function(recipe) {
|
|
|
|
for(var r=0; r<recipe.length; r++) {
|
|
|
|
var recipeLine = recipe[r];
|
|
|
|
if(recipeLine instanceof Array) {
|
|
|
|
this.chooseTiddlers(recipeLine);
|
|
|
|
} else {
|
2011-12-02 16:13:17 +00:00
|
|
|
var store = recipeLine.marker === "shadow" ? this.store.shadows : this.store,
|
|
|
|
markerArray = this.markers[recipeLine.marker];
|
2011-12-02 14:40:18 +00:00
|
|
|
if(markerArray === undefined) {
|
|
|
|
this.markers[recipeLine.marker] = [];
|
|
|
|
markerArray = this.markers[recipeLine.marker];
|
|
|
|
}
|
|
|
|
for(var t=0; t<recipeLine.tiddlers.length; t++) {
|
|
|
|
// Only add the tiddler to the marker if it isn't already there
|
|
|
|
var found = false;
|
|
|
|
for(var m=0; m<markerArray.length; m++) {
|
|
|
|
if(markerArray[m] === recipeLine.tiddlers[t].title) {
|
|
|
|
found = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!found) {
|
|
|
|
markerArray.push(recipeLine.tiddlers[t].title);
|
2011-12-02 16:13:17 +00:00
|
|
|
}
|
|
|
|
store.addTiddler(new Tiddler(recipeLine.tiddlers[t]));
|
2011-12-02 14:40:18 +00:00
|
|
|
}
|
|
|
|
}
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-03 12:01:43 +00:00
|
|
|
Recipe.prototype.sortTiddlersForMarker = function(marker) {
|
|
|
|
if(this.markers[marker]) {
|
|
|
|
this.markers[marker].sort();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-11-22 14:29:29 +00:00
|
|
|
// Process the contents of a recipe file
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.prototype.processRecipeFile = function(recipe,text,contextPath) {
|
2011-11-28 15:15:35 +00:00
|
|
|
var me = this;
|
2011-12-02 14:40:18 +00:00
|
|
|
text.split("\n").forEach(function(line) {
|
|
|
|
var p = line.indexOf(":"),
|
|
|
|
insertionPoint;
|
2011-11-22 14:29:29 +00:00
|
|
|
if(p !== -1) {
|
|
|
|
var marker = line.substr(0, p).trim(),
|
|
|
|
value = line.substr(p+1).trim();
|
|
|
|
if(marker === "recipe") {
|
2011-12-02 14:40:18 +00:00
|
|
|
insertionPoint = recipe.push([]) - 1;
|
|
|
|
me.recipeQueue.push({filepath: value, contextPath: contextPath, recipe: recipe[insertionPoint]});
|
2011-11-22 14:29:29 +00:00
|
|
|
} else {
|
2011-12-02 14:40:18 +00:00
|
|
|
recipe.push({marker: marker, filepath: value, contextPath: contextPath});
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
// Read a tiddler file and callback with an array of hashmaps of tiddler fields. For single
|
2011-11-30 16:06:34 +00:00
|
|
|
// tiddler files it also looks for an accompanying .meta file
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.prototype.readTiddlerFile = function(filepath,contextPath,callback) {
|
2011-11-30 11:41:26 +00:00
|
|
|
var me = this;
|
2011-11-22 14:29:29 +00:00
|
|
|
// Read the tiddler file
|
2011-12-02 14:40:18 +00:00
|
|
|
retrieveFile(filepath,contextPath,function(err,data) {
|
2011-11-28 13:47:38 +00:00
|
|
|
if (err) throw err;
|
2011-11-30 11:41:26 +00:00
|
|
|
var fields = {
|
2011-12-02 14:40:18 +00:00
|
|
|
title: data.path
|
2011-11-30 11:41:26 +00:00
|
|
|
};
|
2011-12-11 14:51:48 +00:00
|
|
|
var tiddlers = me.tiddlerConverters.deserialize(data.extname,data.text,fields);
|
2011-11-28 13:47:38 +00:00
|
|
|
// Check for the .meta file
|
2011-12-02 14:40:18 +00:00
|
|
|
if(data.extname !== ".json" && tiddlers.length === 1) {
|
2011-11-30 16:06:34 +00:00
|
|
|
var metafile = filepath + ".meta";
|
|
|
|
retrieveFile(metafile,contextPath,function(err,data) {
|
|
|
|
if(err && err.code !== "ENOENT" && err.code !== "404") {
|
2011-12-02 14:40:18 +00:00
|
|
|
callback(err);
|
|
|
|
} else {
|
|
|
|
var fields = tiddlers[0];
|
|
|
|
if(!err) {
|
2011-12-11 14:51:48 +00:00
|
|
|
var text = data.text.split("\n\n")[0];
|
|
|
|
if(text) {
|
|
|
|
fields = me.tiddlerConverters.deserialize("application/x-tiddler",text,fields)[0];
|
|
|
|
}
|
2011-12-02 14:40:18 +00:00
|
|
|
}
|
|
|
|
callback(null,[fields]);
|
2011-11-30 16:06:34 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
2011-12-02 14:40:18 +00:00
|
|
|
callback(null,tiddlers);
|
2011-11-30 16:06:34 +00:00
|
|
|
}
|
2011-11-28 13:47:38 +00:00
|
|
|
});
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
|
|
|
// Return a string of the cooked recipe
|
|
|
|
Recipe.prototype.cook = function() {
|
2011-12-02 14:40:18 +00:00
|
|
|
var template = this.markers.template ? this.store.getTiddlerText(this.markers.template[0]) : "",
|
2011-11-28 17:04:39 +00:00
|
|
|
out = [],
|
|
|
|
me = this;
|
2011-11-22 14:29:29 +00:00
|
|
|
template.split("\n").forEach(function(line) {
|
|
|
|
var templateRegExp = /^(?:<!--@@(.*)@@-->)|(?:<!--@@(.*)@@-->)$/gi;
|
|
|
|
var match = templateRegExp.exec(line);
|
|
|
|
if(match) {
|
|
|
|
var marker = match[1] === undefined ? match[2] : match[1];
|
2011-12-02 14:40:18 +00:00
|
|
|
me.outputTiddlersForMarker(out,marker);
|
2011-11-22 14:29:29 +00:00
|
|
|
} else {
|
|
|
|
out.push(line);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return out.join("\n");
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
|
|
|
// Output all the tiddlers in the recipe with a particular marker
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.prototype.outputTiddlersForMarker = function(out,marker) {
|
|
|
|
var tiddlers = this.markers[marker],
|
|
|
|
outputType = Recipe.tiddlerOutputMapper[marker] || "raw",
|
|
|
|
outputter = Recipe.tiddlerOutputter[outputType];
|
2011-12-10 11:46:37 +00:00
|
|
|
if(!tiddlers) {
|
|
|
|
tiddlers = [];
|
|
|
|
}
|
|
|
|
if(outputter) {
|
2011-12-02 14:40:18 +00:00
|
|
|
outputter.call(this,out,tiddlers);
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
2011-12-01 10:19:21 +00:00
|
|
|
};
|
2011-11-22 14:29:29 +00:00
|
|
|
|
|
|
|
// Allows for specialised processing for certain markers
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.tiddlerOutputMapper = {
|
2011-11-22 14:29:29 +00:00
|
|
|
tiddler: "div",
|
|
|
|
js: "javascript",
|
|
|
|
jsdeprecated: "javascript",
|
|
|
|
jquery: "javascript",
|
2011-12-10 11:46:37 +00:00
|
|
|
shadow: "shadow",
|
2011-12-13 12:30:09 +00:00
|
|
|
title: "title",
|
|
|
|
jsmodule: "jsmodule"
|
2011-11-22 14:29:29 +00:00
|
|
|
};
|
|
|
|
|
2011-12-02 14:40:18 +00:00
|
|
|
Recipe.tiddlerOutputter = {
|
|
|
|
raw: function(out,tiddlers) {
|
2011-11-22 14:29:29 +00:00
|
|
|
// The default is just to output the raw text of the tiddler, ignoring any metadata
|
2011-12-02 14:40:18 +00:00
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
2011-11-22 14:29:29 +00:00
|
|
|
// For compatibility with cook.rb, remove one trailing \n from tiddler
|
2011-12-02 14:40:18 +00:00
|
|
|
var text = this.store.getTiddlerText(tiddlers[t]);
|
2011-11-22 14:29:29 +00:00
|
|
|
text = text.charAt(text.length-1) === "\n" ? text.substr(0,text.length-1) : text;
|
|
|
|
out.push(text);
|
|
|
|
}
|
|
|
|
},
|
2011-12-02 14:40:18 +00:00
|
|
|
div: function(out,tiddlers) {
|
2011-11-22 14:29:29 +00:00
|
|
|
// Ordinary tiddlers are output as a <DIV>
|
2011-12-02 14:40:18 +00:00
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
|
|
|
var tid = this.store.getTiddler(tiddlers[t]);
|
2011-12-11 14:51:48 +00:00
|
|
|
out.push(this.tiddlerConverters.serialize("application/x-tiddler-html-div",tid));
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
|
|
|
},
|
2011-12-02 14:40:18 +00:00
|
|
|
javascript: function(out,tiddlers) {
|
2011-11-22 14:29:29 +00:00
|
|
|
// Lines starting with //# are removed from javascript tiddlers
|
2011-12-02 14:40:18 +00:00
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
|
|
|
var tid = this.store.getTiddler(tiddlers[t]),
|
2011-11-28 17:04:39 +00:00
|
|
|
text = tid.fields.text;
|
2011-11-22 14:29:29 +00:00
|
|
|
// For compatibility with cook.rb, remove one trailing \n from tiddler
|
|
|
|
text = text.charAt(text.length-1) === "\n" ? text.substr(0,text.length-1) : text;
|
|
|
|
var lines = text.split("\n");
|
|
|
|
for(var line=0; line<lines.length; line++) {
|
|
|
|
var commentRegExp = /^\s*\/\/#/gi;
|
|
|
|
if(!commentRegExp.test(lines[line])) {
|
|
|
|
out.push(lines[line]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2011-12-02 14:40:18 +00:00
|
|
|
shadow: function(out,tiddlers) {
|
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
|
|
|
var title = tiddlers[t],
|
2011-12-02 16:13:17 +00:00
|
|
|
tid = this.store.shadows.getTiddler(title);
|
2011-12-11 14:51:48 +00:00
|
|
|
out.push(this.tiddlerConverters.serialize("application/x-tiddler-html-div",tid));
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
2011-12-10 11:46:37 +00:00
|
|
|
},
|
|
|
|
title: function(out,tiddlers) {
|
2011-12-11 18:28:09 +00:00
|
|
|
out.push(this.store.renderTiddler("text/plain","WindowTitle"));
|
2011-12-13 12:30:09 +00:00
|
|
|
},
|
|
|
|
jsmodule: function(out,tiddlers) {
|
|
|
|
// JavaScript modules are output as a special script tag
|
|
|
|
for(var t=0; t<tiddlers.length; t++) {
|
|
|
|
var title = tiddlers[t],
|
|
|
|
tid = this.store.getTiddler(title);
|
|
|
|
out.push("<" + "script type=\"application/x-js-module\" title=\"" + title + "\">");
|
|
|
|
out.push(tid.fields.text);
|
|
|
|
out.push("</" + "script>");
|
|
|
|
}
|
2011-11-22 14:29:29 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-12-08 16:20:11 +00:00
|
|
|
// Cook an RSS file of the most recent 20 tiddlers
|
|
|
|
Recipe.prototype.cookRss = function()
|
|
|
|
{
|
|
|
|
var me = this,
|
|
|
|
numRssItems = 20,
|
|
|
|
s = [],
|
|
|
|
d = new Date(),
|
2011-12-11 18:28:09 +00:00
|
|
|
u = this.store.renderTiddler("text/plain","SiteUrl"),
|
2011-12-08 16:20:11 +00:00
|
|
|
encodeTiddlyLink = function(title) {
|
|
|
|
return title.indexOf(" ") == -1 ? title : "[[" + title + "]]";
|
|
|
|
},
|
|
|
|
tiddlerToRssItem = function(tiddler,uri) {
|
|
|
|
var s = "<title" + ">" + utils.htmlEncode(tiddler.fields.title) + "</title" + ">\n";
|
2011-12-12 08:59:28 +00:00
|
|
|
s += "<description>" + utils.htmlEncode(me.store.renderTiddler("text/html",tiddler.fields.title)) + "</description>\n";
|
2011-12-08 16:20:11 +00:00
|
|
|
var i;
|
|
|
|
if(tiddler.fields.tags) {
|
|
|
|
for(i=0; i<tiddler.fields.tags.length; i++) {
|
|
|
|
s += "<category>" + tiddler.fields.tags[i] + "</category>\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
s += "<link>" + uri + "#" + encodeURIComponent(encodeTiddlyLink(tiddler.fields.title)) + "</link>\n";
|
|
|
|
if(tiddler.fields.modified) {
|
|
|
|
s +="<pubDate>" + tiddler.fields.modified.toUTCString() + "</pubDate>\n";
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
},
|
|
|
|
getRssTiddlers = function(sortField,excludeTag) {
|
|
|
|
var r = [];
|
2011-12-12 08:59:28 +00:00
|
|
|
me.store.forEachTiddler(sortField,excludeTag,function(title,tiddler) {
|
2011-12-08 16:20:11 +00:00
|
|
|
if(!tiddler.hasTag(excludeTag)) {
|
|
|
|
r.push(tiddler);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return r;
|
|
|
|
};
|
|
|
|
// Assemble the header
|
|
|
|
s.push("<" + "?xml version=\"1.0\"?" + ">");
|
|
|
|
s.push("<rss version=\"2.0\">");
|
|
|
|
s.push("<channel>");
|
2011-12-11 18:28:09 +00:00
|
|
|
s.push("<title" + ">" + utils.htmlEncode(this.store.renderTiddler("text/plain","SiteTitle")) + "</title" + ">");
|
2011-12-08 16:20:11 +00:00
|
|
|
if(u)
|
|
|
|
s.push("<link>" + utils.htmlEncode(u) + "</link>");
|
2011-12-11 18:28:09 +00:00
|
|
|
s.push("<description>" + utils.htmlEncode(this.store.renderTiddler("text/plain","SiteSubtitle")) + "</description>");
|
2011-12-08 16:20:11 +00:00
|
|
|
//s.push("<language>" + config.locale + "</language>");
|
|
|
|
s.push("<pubDate>" + d.toUTCString() + "</pubDate>");
|
|
|
|
s.push("<lastBuildDate>" + d.toUTCString() + "</lastBuildDate>");
|
|
|
|
s.push("<docs>http://blogs.law.harvard.edu/tech/rss</docs>");
|
|
|
|
s.push("<generator>https://github.com/Jermolene/cook.js</generator>");
|
|
|
|
// 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("<item>\n" + tiddlerToRssItem(tiddlers[i],u) + "\n</item>");
|
|
|
|
}
|
|
|
|
// And footer
|
|
|
|
s.push("</channel>");
|
|
|
|
s.push("</rss>");
|
|
|
|
// Save it all
|
|
|
|
return s.join("\n");
|
2011-12-09 16:34:02 +00:00
|
|
|
};
|
2011-12-08 16:20:11 +00:00
|
|
|
|
2011-11-22 14:29:29 +00:00
|
|
|
exports.Recipe = Recipe;
|
|
|
|
|
2011-12-12 10:52:04 +00:00
|
|
|
})();
|