Refactor the Tiddler object to enforce immutability

And in the process move the fields out of the fields member
This commit is contained in:
Jeremy Ruston 2012-01-17 13:01:55 +00:00
parent 2ff603da0e
commit b898afe3e5
12 changed files with 120 additions and 88 deletions

View File

@ -16,7 +16,7 @@ var BitmapParseTree = function() {
BitmapParseTree.prototype.compile = function(type) {
if(type === "text/html") {
return "(function (tiddler,store,utils) {return '<img src=\"data:' + tiddler.fields.type + ';base64,' + tiddler.fields.text + '\">';})";
return "(function (tiddler,store,utils) {return '<img src=\"data:' + tiddler.type + ';base64,' + tiddler.text + '\">';})";
} else {
return null;
}

View File

@ -132,7 +132,7 @@ Recipe.prototype.loadTiddlerFiles = function(recipeLine) {
filename = path.basename(filepath), // eg *.js
posStar = filename.indexOf("*");
if(posStar !== -1) {
var fileRegExp = new RegExp("^" + filename.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace("*",".*") + "$");
var fileRegExp = new RegExp("^" + filename.replace(/[\-\[\]{}()+?.,\\\^$|#\s]/g, "\\$&").replace("*",".*") + "$");
var files = fs.readdirSync(path.resolve(path.dirname(recipeLine.contextPath),filedir));
for(var f=0; f<files.length; f++) {
if(fileRegExp.test(files[f])) {
@ -341,7 +341,7 @@ Recipe.tiddlerOutputter = {
// Lines starting with //# are removed from javascript tiddlers
for(var t=0; t<tiddlers.length; t++) {
var tid = this.store.getTiddler(tiddlers[t]),
text = tid.fields.text;
text = tid.text;
// 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");
@ -370,7 +370,7 @@ Recipe.tiddlerOutputter = {
tid = this.store.getTiddler(title);
out.push("<" + "script type=\"application/javascript\">");
out.push("define(\"" + title + "\",function(require,exports,module) {");
out.push(tid.fields.text);
out.push(tid.text);
out.push("});");
out.push("</" + "script>");
}
@ -388,24 +388,24 @@ Recipe.prototype.cookRss = function() {
return title.indexOf(" ") == -1 ? title : "[[" + title + "]]";
},
tiddlerToRssItem = function(tiddler,uri) {
var s = "<title" + ">" + utils.htmlEncode(tiddler.fields.title) + "</title" + ">\n";
s += "<description>" + utils.htmlEncode(me.store.renderTiddler("text/html",tiddler.fields.title)) + "</description>\n";
var s = "<title" + ">" + utils.htmlEncode(tiddler.title) + "</title" + ">\n";
s += "<description>" + utils.htmlEncode(me.store.renderTiddler("text/html",tiddler.title)) + "</description>\n";
var i;
if(tiddler.fields.tags) {
for(i=0; i<tiddler.fields.tags.length; i++) {
s += "<category>" + tiddler.fields.tags[i] + "</category>\n";
if(tiddler.tags) {
for(i=0; i<tiddler.tags.length; i++) {
s += "<category>" + tiddler.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";
s += "<link>" + uri + "#" + encodeURIComponent(encodeTiddlyLink(tiddler.title)) + "</link>\n";
if(tiddler.modified) {
s +="<pubDate>" + tiddler.modified.toUTCString() + "</pubDate>\n";
}
return s;
},
getRssTiddlers = function(sortField,excludeTag) {
var r = [];
me.store.forEachTiddler(sortField,excludeTag,function(title,tiddler) {
if(!tiddler.hasTag(excludeTag) && tiddler.fields.modified !== undefined) {
if(!tiddler.hasTag(excludeTag) && tiddler.modified !== undefined) {
r.push(tiddler);
}
});

View File

@ -16,7 +16,7 @@ var SVGParseTree = function() {
SVGParseTree.prototype.compile = function(type) {
if(type === "text/html") {
return "(function (tiddler,store,utils) {return tiddler.fields.text;})";
return "(function (tiddler,store,utils) {return tiddler.text;})";
} else {
return null;
}

View File

@ -21,25 +21,57 @@ var utils = require("./Utils.js"),
ArgParser = require("./ArgParser.js").ArgParser;
var Tiddler = function(/* tiddler,fields */) {
this.parseTree = null; // Caches the parse tree for the tiddler
this.renderers = {}; // Caches rendering functions for this tiddler (indexed by MIME type)
this.renditions = {}; // Caches the renditions produced by those functions (indexed by MIME type)
this.fields = {};
for(var c=0; c<arguments.length; c++) {
var arg = arguments[c],
src = null;
this.cache = {}; // Expose the cache object
var fields = {}, // Keep the fields private, later we'll expose getters for them
tags, // Keep the tags separately because they're the only Array field
f,t,c,arg,src;
// Accumulate the supplied fields
for(c=0; c<arguments.length; c++) {
arg = arguments[c];
src = null;
if(arg instanceof Tiddler) {
src = arg.fields;
} else {
src = arg;
}
for(var t in src) {
var f = this.parseTiddlerField(t,src[t]);
for(t in src) {
f = Tiddler.parseTiddlerField(t,src[t]);
if(f !== null) {
this.fields[t] = f;
fields[t] = f;
}
}
}
// Pull out the tags
if(fields.tags) {
tags = fields.tags;
delete fields.tags;
}
// Expose the fields as read only properties
for(f in fields) {
Object.defineProperty(this,f,{value: fields[f], writeable: false});
}
// Expose the tags as a getter
Object.defineProperty(this,"tags",{get: function() {return tags ? tags.slice(0) : [];}});
// Other methods that need access to the fields
this.getFields = function() {
var r = {};
for(var f in fields) {
var v = fields[f];
if(v instanceof Array) {
r[f] = v.slice(0);
} else {
r[f] = v;
}
}
if(tags) {
r.tags = tags;
}
return r;
};
};
Tiddler.prototype.hasTag = function(tag) {
return this.tags.indexOf(tag) !== -1;
};
Tiddler.standardFields = {
@ -57,18 +89,21 @@ Tiddler.isStandardField = function(name) {
return name in Tiddler.standardFields;
};
Tiddler.prototype.hasTag = function(tag) {
if(this.tags) {
for(var t=0; t<this.tags.length; t++) {
if(this.tags[t] === tag) {
return true;
}
Tiddler.compareTiddlerFields = function(a,b,sortField) {
var aa = a[sortField] || 0,
bb = b[sortField] || 0;
if(aa < bb) {
return -1;
} else {
if(aa > bb) {
return 1;
} else {
return 0;
}
}
return false;
};
Tiddler.prototype.parseTiddlerField = function(name,value) {
Tiddler.parseTiddlerField = function(name,value) {
var type = Tiddler.specialTiddlerFields[name];
if(type) {
return Tiddler.specialTiddlerFieldParsers[type](value);

View File

@ -34,29 +34,30 @@ var outputTiddler = function(tid) {
var result = [],
outputAttribute = function(name,value) {
result.push(name + ": " + value + "\n");
};
for(var t in tid.fields) {
},
fields = tid.getFields();
for(var t in fields) {
switch(t) {
case "text":
// Ignore the text field
break;
case "tags":
// Output tags as a list
outputAttribute(t,tiddlerOutput.stringifyTags(tid.fields.tags));
outputAttribute(t,tiddlerOutput.stringifyTags(fields.tags));
break;
case "modified":
case "created":
// Output dates in YYYYMMDDHHMM
outputAttribute(t,utils.convertToYYYYMMDDHHMM(tid.fields[t]));
outputAttribute(t,utils.convertToYYYYMMDDHHMM(fields[t]));
break;
default:
// Output other attributes raw
outputAttribute(t,tid.fields[t]);
outputAttribute(t,fields[t]);
break;
}
}
result.push("\n");
result.push(tid.fields.text);
result.push(fields.text);
return result.join("");
};
@ -68,23 +69,19 @@ The fields are in the order title, creator, modifier, created, modified, tags, f
*/
var outputTiddlerDiv = function(tid) {
var result = [],
attributes = {},
outputAttribute = function(name,transform,dontDelete) {
if(name in attributes) {
var value = attributes[name];
fields = tid.getFields(),
text = fields.text,
outputAttribute = function(name,transform) {
if(name in fields) {
var value = fields[name];
if(transform)
value = transform(value);
result.push(" " + name + "=\"" + value + "\"");
if(!dontDelete) {
delete attributes[name];
}
delete fields[name];
}
};
for(var t in tid.fields) {
attributes[t] = tid.fields[t];
}
if(attributes.text) {
delete attributes.text;
if(fields.text) {
delete fields.text;
}
result.push("<div");
// Output the standard attributes in the correct order
@ -95,11 +92,11 @@ var outputTiddlerDiv = function(tid) {
outputAttribute("modified", function(v) {return utils.convertToYYYYMMDDHHMM(v);});
outputAttribute("tags", function(v) {return stringifyTags(v);});
// Output any other attributes
for(t in attributes) {
for(var t in fields) {
outputAttribute(t,null,true);
}
result.push(">\n<pre>");
result.push(utils.htmlEncode(tid.fields.text));
result.push(utils.htmlEncode(text));
result.push("</pre>\n</div>");
return result.join("");
};

View File

@ -1,6 +1,12 @@
/*\
title: js/WikiStore.js
WikiStore uses the .cache member of tiddlers to store the following information:
parseTree: Caches the parse tree for the tiddler
renderers: Caches rendering functions for this tiddler (indexed by MIME type)
renditions: Caches the renditions produced by those functions (indexed by MIME type)
\*/
(function(){
@ -125,7 +131,7 @@ WikiStore.prototype.getTiddler = function(title) {
WikiStore.prototype.getTiddlerText = function(title) {
var t = this.getTiddler(title);
return t instanceof Tiddler ? t.fields.text : null;
return t instanceof Tiddler ? t.text : null;
};
WikiStore.prototype.deleteTiddler = function(title) {
@ -143,8 +149,8 @@ WikiStore.prototype.tiddlerExists = function(title) {
};
WikiStore.prototype.addTiddler = function(tiddler) {
this.tiddlers[tiddler.fields.title] = tiddler;
this.touchTiddler("modified",tiddler.fields.title);
this.tiddlers[tiddler.title] = tiddler;
this.touchTiddler("modified",tiddler.title);
};
WikiStore.prototype.forEachTiddler = function(/* [sortField,[excludeTag,]]callback */) {
@ -159,22 +165,10 @@ WikiStore.prototype.forEachTiddler = function(/* [sortField,[excludeTag,]]callba
for(t in this.tiddlers) {
tiddlers.push(this.tiddlers[t]);
}
tiddlers.sort(function (a,b) {
var aa = a.fields[sortField] || 0,
bb = b.fields[sortField] || 0;
if(aa < bb) {
return -1;
} else {
if(aa > bb) {
return 1;
} else {
return 0;
}
}
});
tiddlers.sort(function (a,b) {return Tiddler.compareTiddlerFields(a,b,sortField);});
for(t=0; t<tiddlers.length; t++) {
if(!tiddlers[t].hasTag(excludeTag)) {
callback.call(this,tiddlers[t].fields.title,tiddlers[t]);
callback.call(this,tiddlers[t].title,tiddlers[t]);
}
}
} else {
@ -286,10 +280,10 @@ WikiStore.prototype.parseTiddler = function(title) {
var tiddler = this.getTiddler(title);
if(tiddler) {
// Check the cache
if(!tiddler.parseTree) {
tiddler.parseTree = this.parseText(tiddler.fields.type,tiddler.fields.text);
if(!tiddler.cache.parseTree) {
tiddler.cache.parseTree = this.parseText(tiddler.type,tiddler.text);
}
return tiddler.parseTree;
return tiddler.cache.parseTree;
} else {
return null;
}
@ -311,11 +305,14 @@ WikiStore.prototype.compileTiddler = function(title,type) {
/*jslint evil: true */
var tiddler = this.getTiddler(title);
if(tiddler) {
if(!tiddler.renderers[type]) {
var tree = this.parseTiddler(title);
tiddler.renderers[type] = eval(tree.compile(type));
if(!tiddler.cache.renderers) {
tiddler.cache.renderers = {};
}
return tiddler.renderers[type];
if(!tiddler.cache.renderers[type]) {
var tree = this.parseTiddler(title);
tiddler.cache.renderers[type] = eval(tree.compile(type));
}
return tiddler.cache.renderers[type];
} else {
return null;
}
@ -343,10 +340,13 @@ WikiStore.prototype.renderTiddler = function(targetType,title,asTitle) {
var asTiddler = this.getTiddler(asTitle);
return fn(asTiddler,this,utils);
} else {
if(!tiddler.renditions[targetType]) {
tiddler.renditions[targetType] = fn(tiddler,this,utils);
if(!tiddler.cache.renditions) {
tiddler.cache.renditions = {};
}
return tiddler.renditions[targetType];
if(!tiddler.cache.renditions[targetType]) {
tiddler.cache.renditions[targetType] = fn(tiddler,this,utils);
}
return tiddler.cache.renditions[targetType];
}
}
return null;

View File

@ -16,7 +16,7 @@ exports.macro = {
},
handler: function(type,tiddler,store,params) {
var encoder = type === "text/html" ? utils.htmlEncode : function(x) {return x;},
parseTree = store.parseTiddler(tiddler.fields.title);
parseTree = store.parseTiddler(tiddler.title);
if(parseTree) {
var r = [];
var d = parseTree.dependencies;

View File

@ -52,8 +52,8 @@ exports.macro = {
handler,
t;
if(template) {
templateType = template.fields.type;
templateText = template.fields.text;
templateType = template.type;
templateText = template.text;
}
handler = handlers[params.type];
handler = handler || handlers.all;

View File

@ -21,13 +21,13 @@ exports.macro = {
if(params["with"]) {
// Parameterised transclusion
var targetTiddler = store.getTiddler(params.target),
text = targetTiddler.fields.text;
text = targetTiddler.text;
var withTokens = [params["with"]];
for(var t=0; t<withTokens.length; t++) {
var placeholderRegExp = new RegExp("\\$"+(t+1),"mg");
text = text.replace(placeholderRegExp,withTokens[t]);
}
return store.renderText(targetTiddler.fields.type,text,type,tiddler.fields.title);
return store.renderText(targetTiddler.type,text,type,tiddler.title);
} else {
// There's no parameterisation, so we can just render the target tiddler directly
return store.renderTiddler(type,params.target);

View File

@ -18,14 +18,14 @@ exports.macro = {
template: {byPos: 2, type: "text", optional: true}
},
handler: function(type,tiddler,store,params) {
var v = tiddler.fields[params.field],
var v = tiddler[params.field],
encoder = type === "text/html" ? utils.htmlEncode : function(x) {return x;};
if(v) {
switch(params.format) {
case "link":
return store.renderMacro("link",type,tiddler,{target: v},encoder(v));
case "wikified":
return store.renderTiddler(type,tiddler.fields.title);
return store.renderTiddler(type,tiddler.title);
case "date":
var template = params.template || "DD MMM YYYY";
return encoder(utils.formatDateString(v,template));

View File

@ -2,6 +2,6 @@ title: SeventhTiddler
<<echo {{2+2}}>>
<<echo {{tiddler.fields.title}}>>
<<echo {{tiddler.title}}>>
<<echo {{"window"}}>>

View File

@ -135,7 +135,7 @@ var commandLineSwitches = {
handler: function(args,callback) {
var recipe = [];
app.store.forEachTiddler(function(title,tiddler) {
var filename = encodeURIComponent(tiddler.fields.title.replace(/ /g,"_")) + ".tid";
var filename = encodeURIComponent(tiddler.title.replace(/ /g,"_")) + ".tid";
fs.writeFileSync(path.resolve(args[0],filename),tiddlerOutput.outputTiddler(tiddler),"utf8");
recipe.push("tiddler: " + filename + "\n");
});