Browser now syncs changes with server which syncs with the file system

A bit rough and ready, but this gives us basic support for editting
tiddlers in the browser and updating the original file on the server
This commit is contained in:
Jeremy Ruston 2012-04-05 12:21:49 +01:00
parent 495ef1ada5
commit 0ac55688c4
8 changed files with 249 additions and 94 deletions

View File

@ -10,6 +10,7 @@ This is the main() function in the browser
"use strict"; "use strict";
var WikiStore = require("./WikiStore.js").WikiStore, var WikiStore = require("./WikiStore.js").WikiStore,
HttpSync = require("./HttpSync.js").HttpSync,
Tiddler = require("./Tiddler.js").Tiddler, Tiddler = require("./Tiddler.js").Tiddler,
tiddlerInput = require("./TiddlerInput.js"), tiddlerInput = require("./TiddlerInput.js"),
tiddlerOutput = require("./TiddlerOutput.js"), tiddlerOutput = require("./TiddlerOutput.js"),
@ -81,6 +82,8 @@ var App = function() {
var shadowArea = document.getElementById("shadowArea"); var shadowArea = document.getElementById("shadowArea");
this.store.shadows.addTiddlers(this.store.deserializeTiddlers("(DOM)",shadowArea)); this.store.shadows.addTiddlers(this.store.deserializeTiddlers("(DOM)",shadowArea));
} }
// Reset pending events on the store so that we don't get events for the initial load
this.store.clearEvents();
// Bit of a hack to set up the macros // Bit of a hack to set up the macros
this.store.installMacro(require("./macros/chooser.js").macro); this.store.installMacro(require("./macros/chooser.js").macro);
this.store.installMacro(require("./macros/command.js").macro); this.store.installMacro(require("./macros/command.js").macro);
@ -102,8 +105,10 @@ var App = function() {
linkInfo.target = encodeURIComponent(linkInfo.target); linkInfo.target = encodeURIComponent(linkInfo.target);
} }
}; };
// Set up navigation if we're in the browser // Set up for the browser
if(this.isBrowser) { if(this.isBrowser) {
// Set up HttpSync
this.httpSync = new HttpSync(this.store);
// Open the PageTemplate // Open the PageTemplate
var renderer = this.store.renderMacro("tiddler",{target: "PageTemplate"}); var renderer = this.store.renderMacro("tiddler",{target: "PageTemplate"});
renderer.renderInDom(document.body); renderer.renderInDom(document.body);
@ -118,18 +123,19 @@ var App = function() {
titleRenderer.refresh(changes); titleRenderer.refresh(changes);
document.title = titleRenderer.render("text/plain"); document.title = titleRenderer.render("text/plain");
}); });
// Set up a timer to change the value of a tiddler
var me = this;
window.setInterval(function() {
me.store.addTiddler(new Tiddler({
title: "ClockTiddler",
text: "The time was recently " + (new Date()).toString()
}));
},3000);
// Listen for navigate events that weren't caught // Listen for navigate events that weren't caught
document.addEventListener("tw-navigate",function (event) { document.addEventListener("tw-navigate",function (event) {
renderer.broadcastEvent(event); renderer.broadcastEvent(event);
},false); },false);
// Set up a timer to change the value of a tiddler
var me = this,
s = setInterval || window.setInterval;
s(function() {
me.store.addTiddler(new Tiddler({
title: "ClockTiddler",
text: "The time was recently " + (new Date()).toString()
}));
},3000);
} }
}; };

35
js/HttpSync.js Normal file
View File

@ -0,0 +1,35 @@
/*\
title: js/HttpSync.js
\*/
(function(){
/*jslint node: true */
"use strict";
function HttpSync(store) {
this.store = store;
this.changeCounts = {};
store.addEventListener("",function(changes) {
for(var title in changes) {
var tiddler = store.getTiddler(title);
if(tiddler) {
var fieldStrings = tiddler.getFieldStrings(),
fields = {},
t;
for(t=0; t<fieldStrings.length; t++) {
fields[fieldStrings[t].name] = fieldStrings[t].value;
}
fields.text = tiddler.text;
var x = new XMLHttpRequest();
x.open("PUT",window.location.toString() + encodeURIComponent(title),true);
x.setRequestHeader("Content-type", "application/json");
x.send(JSON.stringify(fields));
}
}
});
}
exports.HttpSync = HttpSync;
})();

View File

@ -1,5 +1,5 @@
/*\ /*\
title: js/FileStore.js title: js/LocalFileSync.js
\*/ \*/
(function(){ (function(){
@ -8,17 +8,18 @@ title: js/FileStore.js
"use strict"; "use strict";
var retrieveFile = require("./FileRetriever.js").retrieveFile, var retrieveFile = require("./FileRetriever.js").retrieveFile,
utils = require("./Utils.js"),
fs = require("fs"), fs = require("fs"),
path = require("path"), path = require("path"),
url = require("url"), url = require("url"),
util = require("util"), util = require("util"),
async = require("async"); async = require("async");
function FileStore(dirpath,store,callback) { function LocalFileSync(dirpath,store,callback) {
this.dirpath = dirpath; this.dirpath = dirpath;
this.store = store; this.store = store;
this.callback = callback; this.callback = callback;
this.sources = {}; // A hashmap of <tiddlername>: <srcpath> this.changeCounts = {}; // A hashmap of <tiddlername>: <changeCount>
var self = this; var self = this;
// Set up a queue for loading tiddler files // Set up a queue for loading tiddler files
this.loadQueue = async.queue(function(task,callback) { this.loadQueue = async.queue(function(task,callback) {
@ -68,8 +69,8 @@ function FileStore(dirpath,store,callback) {
var loadCallback = function(task,tiddlers) { var loadCallback = function(task,tiddlers) {
for(var t=0; t<tiddlers.length; t++) { for(var t=0; t<tiddlers.length; t++) {
var tiddler = tiddlers[t]; var tiddler = tiddlers[t];
self.sources[tiddler.title] = task.filepath;
self.store.addTiddler(tiddler); self.store.addTiddler(tiddler);
self.changeCounts[tiddler.title] = self.store.getChangeCount(tiddler.title);
} }
}; };
for(var t=0; t<files.length; t++) { for(var t=0; t<files.length; t++) {
@ -83,8 +84,42 @@ function FileStore(dirpath,store,callback) {
} }
} }
}); });
// Set up a queue for saving tiddler files
this.saveQueue = async.queue(function(task,callback) {
var data = task.data,
encoding = "utf8";
if(task.binary) {
data = new Buffer(task.data,"base64").toString("binary"),
encoding = "binary";
}
fs.writeFile(self.dirpath + "/" + task.name,data,encoding,function(err) {
callback(err);
});
},10);
// Install our event listener to listen out for tiddler changes
this.store.addEventListener("",function(changes) {
for(var title in changes) {
// Get the information about the tiddler
var tiddler = self.store.getTiddler(title),
changeCount = self.store.getChangeCount(title),
lastChangeCount = self.changeCounts[title],
files = [];
// Construct a changecount record if we don't have one
if(!lastChangeCount) {
lastChangeCount = 0;
self.changeCounts[title] = lastChangeCount;
}
// Save the tiddler if the changecount has increased
if(changeCount > lastChangeCount) {
files = self.store.serializeTiddlers([tiddler],"application/x-tiddler");
for(var t=0; t<files.length; t++) {
self.saveQueue.push(files[t]);
}
}
}
});
} }
exports.FileStore = FileStore; exports.LocalFileSync = LocalFileSync;
})(); })();

View File

@ -380,7 +380,7 @@ Recipe.tiddlerOutputter = {
// Ordinary tiddlers are output as a <DIV> // Ordinary tiddlers are output as a <DIV>
for(var t=0; t<tiddlers.length; t++) { for(var t=0; t<tiddlers.length; t++) {
var tid = this.store.getTiddler(tiddlers[t]); var tid = this.store.getTiddler(tiddlers[t]);
out.push(this.store.serializeTiddler("application/x-tiddler-html-div",tid),"\n"); out.push(this.store.serializeTiddlers([tid],"application/x-tiddler-html-div")[0].data,"\n");
} }
}, },
javascript: function(out,tiddlers) { javascript: function(out,tiddlers) {
@ -404,7 +404,7 @@ Recipe.tiddlerOutputter = {
for(var t=0; t<tiddlers.length; t++) { for(var t=0; t<tiddlers.length; t++) {
var title = tiddlers[t], var title = tiddlers[t],
tid = this.store.shadows.getTiddler(title); tid = this.store.shadows.getTiddler(title);
out.push(this.store.serializeTiddler("application/x-tiddler-html-div",tid),"\n"); out.push(this.store.serializeTiddlers([tid],"application/x-tiddler-html-div")[0].data,"\n");
} }
}, },
title: function(out,tiddlers) { title: function(out,tiddlers) {

View File

@ -1,7 +1,24 @@
/*\ /*\
title: js/TiddlerOutput.js title: js/TiddlerOutput.js
Functions concerned with parsing representations of tiddlers Serializers that output tiddlers in a variety of formats.
store.serializeTiddlers(tiddlers,type)
tiddlers: An array of tiddler objects
type: The target output type as a file extension like `.tid` or a MIME type like `application/x-tiddler`. If `null` or `undefined` then the best type is chosen automatically
The serializer returns an array of information defining one or more files containing the tiddlers:
[
{name: "title.tid", type: "application/x-tiddler", ext: ".tid", data: "xxxxx"},
{name: "title.jpg", type: "image/jpeg", ext: ".jpg", binary: true, data: "xxxxx"},
{name: "title.jpg.meta", type: "application/x-tiddler-metadata", ext: ".meta", data: "xxxxx"}
]
Notes:
* The `type` field is the type of the file, which is not necessrily the same as the type of the tiddler.
* The `binary` field may be omitted if it is not `true`
\*/ \*/
(function(){ (function(){
@ -14,96 +31,105 @@ var utils = require("./Utils.js"),
var tiddlerOutput = exports; var tiddlerOutput = exports;
// Utility function to convert a tags string array into a TiddlyWiki-style quoted tags string var outputMetaDataBlock = function(tiddler) {
var stringifyTags = function(tags) { var result = [],
var results = []; fields = tiddler.getFieldStrings(),
for(var t=0; t<tags.length; t++) { t;
if(tags[t].indexOf(" ") !== -1) { for(t=0; t<fields.length; t++) {
results.push("[[" + tags[t] + "]]"); result.push(fields[t].name + ": " + fields[t].value);
} else {
results.push(tags[t]);
}
} }
return results.join(" "); return result.join("\n");
}; };
/* /*
Output a tiddler as a .tid file Output tiddlers as separate files in their native formats (ie. `.tid` or `.jpg`/`.jpg.meta`)
*/ */
var outputTiddler = function(tid) { var outputTiddlers = function(tiddlers) {
var result = [], var result = [];
outputAttribute = function(name,value) { for(var t=0; t<tiddlers.length; t++) {
result.push(name + ": " + value + "\n"); var tiddler = tiddlers[t],
}, extension,
fields = tid.getFields(); binary = false;
for(var t in fields) { switch(tiddler.type) {
switch(t) { case "image/jpeg":
case "text": extension = ".jpg";
// Ignore the text field binary = true;
break; break;
case "tags": case "image/gif":
// Output tags as a list extension = ".gif";
outputAttribute(t,stringifyTags(fields.tags)); binary = true;
break; break;
case "modified": case "image/png":
case "created": extension = ".png";
// Output dates in YYYYMMDDHHMM binary = true;
outputAttribute(t,utils.convertToYYYYMMDDHHMM(fields[t])); break;
case "image/svg+xml":
extension = ".svg";
break; break;
default: default:
// Output other attributes raw extension = ".tid";
outputAttribute(t,fields[t]);
break; break;
} }
if(extension === ".tid") {
result.push({
name: tiddler.title + ".tid",
type: "application/x-tiddler",
extension: ".tid",
data: outputMetaDataBlock(tiddler) + "\n\n" + tiddler.text,
binary: false
});
} else {
result.push({
name: tiddler.title,
type: tiddler.type,
extension: extension,
data: tiddler.text,
binary: binary
});
result.push({
name: tiddler.title + ".meta",
type: "application/x-tiddler-metadata",
extension: ".meta",
data: outputMetaDataBlock(tiddler),
binary: false
});
}
} }
result.push("\n"); return result;
result.push(fields.text);
return result.join("");
}; };
/* /*
Output a tiddler as an HTML <DIV> Output an array of tiddlers as HTML <DIV>s
out - array to push the output strings out - array to push the output strings
tid - the tiddler to be output tid - the tiddler to be output
The fields are in the order title, creator, modifier, created, modified, tags, followed by any others The fields are in the order title, creator, modifier, created, modified, tags, followed by any others
*/ */
var outputTiddlerDiv = function(tid) { var outputTiddlerDivs = function(tiddlers) {
var result = [], var result = [];
fields = tid.getFields(), for(var t=0; t<tiddlers.length; t++) {
text = fields.text, var tiddler = tiddlers[t],
outputAttribute = function(name,transform) { output = [],
if(name in fields) { fieldStrings = tiddler.getFieldStrings();
var value = fields[name]; output.push("<div");
if(transform) for(var f=0; f<fieldStrings.length; f++) {
value = transform(value); output.push(" " + fieldStrings[f].name + "=\"" + fieldStrings[f].value + "\"");
result.push(" " + name + "=\"" + value + "\""); }
delete fields[name]; output.push(">\n<pre>");
} output.push(utils.htmlEncode(tiddler.text));
}; output.push("</pre>\n</div>");
if(fields.text) { result.push({
delete fields.text; name: tiddler.title,
type: "application/x-tiddler-html-div",
extension: ".tiddler",
data: output.join("")
});
} }
result.push("<div"); return result;
// Output the standard attributes in the correct order
outputAttribute("title");
outputAttribute("creator");
outputAttribute("modifier");
outputAttribute("created", function(v) {return utils.convertToYYYYMMDDHHMM(v);});
outputAttribute("modified", function(v) {return utils.convertToYYYYMMDDHHMM(v);});
outputAttribute("tags", function(v) {return stringifyTags(v);});
// Output any other attributes
for(var t in fields) {
outputAttribute(t,null,true);
}
result.push(">\n<pre>");
result.push(utils.htmlEncode(text));
result.push("</pre>\n</div>");
return result.join("");
}; };
tiddlerOutput.register = function(store) { tiddlerOutput.register = function(store) {
store.registerTiddlerSerializer(".tid","application/x-tiddler",outputTiddler); store.registerTiddlerSerializer(".tid","application/x-tiddler",outputTiddlers);
store.registerTiddlerSerializer(".tiddler","application/x-tiddler-html-div",outputTiddlerDiv); store.registerTiddlerSerializer(".tiddler","application/x-tiddler-html-div",outputTiddlerDivs);
}; };
})(); })();

View File

@ -28,6 +28,7 @@ var WikiStore = function WikiStore(options) {
this.parsers = {}; // Hashmap of parsers by accepted MIME type this.parsers = {}; // Hashmap of parsers by accepted MIME type
this.macros = {}; // Hashmap of macros by macro name this.macros = {}; // Hashmap of macros by macro name
this.caches = {}; // Hashmap of cache objects by tiddler title, each is a hashmap of named caches this.caches = {}; // Hashmap of cache objects by tiddler title, each is a hashmap of named caches
this.changeCount = {}; // Hashmap of integer changecount (>1) for each tiddler; persistent across deletions
this.tiddlerSerializers = {}; // Hashmap of serializers by target MIME type this.tiddlerSerializers = {}; // Hashmap of serializers by target MIME type
this.tiddlerDeserializers = {}; // Hashmap of deserializers by accepted MIME type this.tiddlerDeserializers = {}; // Hashmap of deserializers by accepted MIME type
this.eventListeners = []; // Array of {filter:,listener:} this.eventListeners = []; // Array of {filter:,listener:}
@ -38,6 +39,22 @@ var WikiStore = function WikiStore(options) {
}); });
}; };
WikiStore.prototype.incChangeCount = function(title) {
if(this.changeCount.hasOwnProperty(title)) {
this.changeCount[title]++;
} else {
this.changeCount[title] = 1;
}
};
WikiStore.prototype.getChangeCount = function(title) {
if(this.changeCount.hasOwnProperty(title)) {
return this.changeCount[title];
} else {
return 0;
}
};
WikiStore.prototype.registerParser = function(type,parser) { WikiStore.prototype.registerParser = function(type,parser) {
if(type instanceof Array) { if(type instanceof Array) {
for(var t=0; t<type.length; t++) { for(var t=0; t<type.length; t++) {
@ -96,6 +113,10 @@ WikiStore.prototype.touchTiddler = function(type,title) {
this.triggerEvents(); this.triggerEvents();
}; };
WikiStore.prototype.clearEvents = function() {
this.changedTiddlers = {};
};
/* /*
Trigger the execution of the event dispatcher at the next tick, if it is not already triggered Trigger the execution of the event dispatcher at the next tick, if it is not already triggered
*/ */
@ -133,6 +154,7 @@ WikiStore.prototype.getTiddlerText = function(title,defaultText) {
}; };
WikiStore.prototype.deleteTiddler = function(title) { WikiStore.prototype.deleteTiddler = function(title) {
this.incChangeCount(title);
delete this.tiddlers[title]; delete this.tiddlers[title];
this.clearCache(title); this.clearCache(title);
this.touchTiddler("deleted",title); this.touchTiddler("deleted",title);
@ -153,6 +175,7 @@ WikiStore.prototype.addTiddler = function(tiddler) {
if(!(tiddler instanceof Tiddler)) { if(!(tiddler instanceof Tiddler)) {
tiddler = new Tiddler(tiddler); tiddler = new Tiddler(tiddler);
} }
this.incChangeCount(tiddler.title);
var status = tiddler.title in this.tiddlers ? "modified" : "created"; var status = tiddler.title in this.tiddlers ? "modified" : "created";
this.clearCache(tiddler.title); this.clearCache(tiddler.title);
this.tiddlers[tiddler.title] = tiddler; this.tiddlers[tiddler.title] = tiddler;
@ -213,10 +236,11 @@ WikiStore.prototype.getShadowTitles = function() {
return this.shadows ? this.shadows.getTitles() : []; return this.shadows ? this.shadows.getTitles() : [];
}; };
WikiStore.prototype.serializeTiddler = function(type,tiddler) { WikiStore.prototype.serializeTiddlers = function(tiddlers,type) {
type = type || "application/x-tiddler";
var serializer = this.tiddlerSerializers[type]; var serializer = this.tiddlerSerializers[type];
if(serializer) { if(serializer) {
return serializer(tiddler); return serializer(tiddlers);
} else { } else {
return null; return null;
} }

View File

@ -45,7 +45,7 @@ exports.macro = {
for(var t=0; t<story.tiddlers.length; t++) { for(var t=0; t<story.tiddlers.length; t++) {
var storyRecord = story.tiddlers[t]; var storyRecord = story.tiddlers[t];
if(storyRecord.title === event.tiddlerTitle && storyRecord.template !== template) { if(storyRecord.title === event.tiddlerTitle && storyRecord.template !== template) {
storyRecord.title = "Draft of " + event.tiddlerTitle + " at " + (new Date()); storyRecord.title = "Draft " + (new Date()) + " of " + event.tiddlerTitle;
storyRecord.template = template; storyRecord.template = template;
var tiddler = this.store.getTiddler(event.tiddlerTitle); var tiddler = this.store.getTiddler(event.tiddlerTitle);
this.store.addTiddler(new Tiddler( this.store.addTiddler(new Tiddler(

View File

@ -9,7 +9,7 @@ TiddlyWiki command line interface
var App = require("./js/App.js").App, var App = require("./js/App.js").App,
WikiStore = require("./js/WikiStore.js").WikiStore, WikiStore = require("./js/WikiStore.js").WikiStore,
FileStore = require("./js/FileStore.js").FileStore, LocalFileSync = require("./js/LocalFileSync.js").LocalFileSync,
Tiddler = require("./js/Tiddler.js").Tiddler, Tiddler = require("./js/Tiddler.js").Tiddler,
Recipe = require("./js/Recipe.js").Recipe, Recipe = require("./js/Recipe.js").Recipe,
tiddlerInput = require("./js/TiddlerInput.js"), tiddlerInput = require("./js/TiddlerInput.js"),
@ -47,8 +47,9 @@ var parseOptions = function(args,defaultSwitch) {
}; };
var switches = parseOptions(Array.prototype.slice.call(process.argv,2),"dummy"), var switches = parseOptions(Array.prototype.slice.call(process.argv,2),"dummy"),
verbose = false,
recipe = null, recipe = null,
fileStore = null, localFileSync = null,
lastRecipeFilepath = null, lastRecipeFilepath = null,
currSwitch = 0; currSwitch = 0;
@ -111,7 +112,7 @@ var commandLineSwitches = {
store: { store: {
args: {min: 1, max: 1}, args: {min: 1, max: 1},
handler: function(args,callback) { handler: function(args,callback) {
fileStore = new FileStore(args[0],app.store,function() { localFileSync = new LocalFileSync(args[0],app.store,function() {
callback(null); callback(null);
}); });
} }
@ -149,7 +150,7 @@ var commandLineSwitches = {
recipe = []; recipe = [];
app.store.forEachTiddler(function(title,tiddler) { app.store.forEachTiddler(function(title,tiddler) {
var filename = encodeURIComponent(tiddler.title.replace(/ /g,"_")) + ".tid"; var filename = encodeURIComponent(tiddler.title.replace(/ /g,"_")) + ".tid";
fs.writeFileSync(path.resolve(outdir,filename),app.store.serializeTiddler("application/x-tiddler",tiddler),"utf8"); fs.writeFileSync(path.resolve(outdir,filename),app.store.serializeTiddlers([tiddler],"application/x-tiddler")[0].data,"utf8");
recipe.push("tiddler: " + filename + "\n"); recipe.push("tiddler: " + filename + "\n");
}); });
fs.writeFileSync(path.join(args[0],"split.recipe"),recipe.join("")); fs.writeFileSync(path.join(args[0],"split.recipe"),recipe.join(""));
@ -174,11 +175,33 @@ var commandLineSwitches = {
callback("--servewiki must be preceded by a --recipe"); callback("--servewiki must be preceded by a --recipe");
} }
var port = args.length > 0 ? args[0] : 8000; var port = args.length > 0 ? args[0] : 8000;
// Dumbly, this implementation wastes the recipe processing that happened on the --recipe switch
http.createServer(function(request, response) { http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/html"}); var path = url.parse(request.url).pathname;
response.end(recipe.cook(), "utf8"); switch(request.method) {
case "PUT":
var data = "";
request.on("data",function(chunk) {
data += chunk.toString();
});
request.on("end",function() {
var title = decodeURIComponent(path.substr(1));
app.store.addTiddler(new Tiddler(JSON.parse(data),{title: title}));
response.writeHead(204, "OK");
response.end();
});
break;
case "GET":
if(path === "/") {
response.writeHead(200, {"Content-Type": "text/html"});
response.end(recipe.cook(), "utf8");
} else {
response.writeHead(404);
response.end();
}
break;
}
}).listen(port); }).listen(port);
process.nextTick(function() {callback(null);});
} }
}, },
servetiddlers: { servetiddlers: {
@ -196,11 +219,13 @@ var commandLineSwitches = {
response.end(); response.end();
} }
}).listen(port); }).listen(port);
process.nextTick(function() {callback(null);});
} }
}, },
verbose: { verbose: {
args: {min: 0, max: 0}, args: {min: 0, max: 0},
handler: function(args,callback) { handler: function(args,callback) {
verbose = true;
process.nextTick(function() {callback(null);}); process.nextTick(function() {callback(null);});
} }
}, },
@ -245,6 +270,7 @@ var commandLineSwitches = {
} }
} }
} }
process.nextTick(function() {callback(null);});
} }
} }
}; };
@ -259,6 +285,9 @@ var processNextSwitch = function() {
if(s.args.length > csw.args.max) { if(s.args.length > csw.args.max) {
throw "Command line switch --" + s.switchName + " should have a maximum of " + csw.args.max + " arguments"; throw "Command line switch --" + s.switchName + " should have a maximum of " + csw.args.max + " arguments";
} }
if(verbose) {
console.log("Processing --" + s.switchName + " " + s.args.join(" "));
}
csw.handler(s.args,function (err) { csw.handler(s.args,function (err) {
if(err) { if(err) {
throw "Error while executing option '--" + s.switchName + "' was:\n" + err; throw "Error while executing option '--" + s.switchName + "' was:\n" + err;