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";
var WikiStore = require("./WikiStore.js").WikiStore,
HttpSync = require("./HttpSync.js").HttpSync,
Tiddler = require("./Tiddler.js").Tiddler,
tiddlerInput = require("./TiddlerInput.js"),
tiddlerOutput = require("./TiddlerOutput.js"),
@ -81,6 +82,8 @@ var App = function() {
var shadowArea = document.getElementById("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
this.store.installMacro(require("./macros/chooser.js").macro);
this.store.installMacro(require("./macros/command.js").macro);
@ -102,8 +105,10 @@ var App = function() {
linkInfo.target = encodeURIComponent(linkInfo.target);
}
};
// Set up navigation if we're in the browser
// Set up for the browser
if(this.isBrowser) {
// Set up HttpSync
this.httpSync = new HttpSync(this.store);
// Open the PageTemplate
var renderer = this.store.renderMacro("tiddler",{target: "PageTemplate"});
renderer.renderInDom(document.body);
@ -118,18 +123,19 @@ var App = function() {
titleRenderer.refresh(changes);
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
document.addEventListener("tw-navigate",function (event) {
renderer.broadcastEvent(event);
},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(){
@ -8,17 +8,18 @@ title: js/FileStore.js
"use strict";
var retrieveFile = require("./FileRetriever.js").retrieveFile,
utils = require("./Utils.js"),
fs = require("fs"),
path = require("path"),
url = require("url"),
util = require("util"),
async = require("async");
function FileStore(dirpath,store,callback) {
function LocalFileSync(dirpath,store,callback) {
this.dirpath = dirpath;
this.store = store;
this.callback = callback;
this.sources = {}; // A hashmap of <tiddlername>: <srcpath>
this.changeCounts = {}; // A hashmap of <tiddlername>: <changeCount>
var self = this;
// Set up a queue for loading tiddler files
this.loadQueue = async.queue(function(task,callback) {
@ -68,8 +69,8 @@ function FileStore(dirpath,store,callback) {
var loadCallback = function(task,tiddlers) {
for(var t=0; t<tiddlers.length; t++) {
var tiddler = tiddlers[t];
self.sources[tiddler.title] = task.filepath;
self.store.addTiddler(tiddler);
self.changeCounts[tiddler.title] = self.store.getChangeCount(tiddler.title);
}
};
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>
for(var t=0; t<tiddlers.length; 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) {
@ -404,7 +404,7 @@ Recipe.tiddlerOutputter = {
for(var t=0; t<tiddlers.length; t++) {
var title = tiddlers[t],
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) {

View File

@ -1,7 +1,24 @@
/*\
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(){
@ -14,96 +31,105 @@ var utils = require("./Utils.js"),
var tiddlerOutput = exports;
// Utility function to convert a tags string array into a TiddlyWiki-style quoted tags string
var stringifyTags = function(tags) {
var results = [];
for(var t=0; t<tags.length; t++) {
if(tags[t].indexOf(" ") !== -1) {
results.push("[[" + tags[t] + "]]");
} else {
results.push(tags[t]);
}
var outputMetaDataBlock = function(tiddler) {
var result = [],
fields = tiddler.getFieldStrings(),
t;
for(t=0; t<fields.length; t++) {
result.push(fields[t].name + ": " + fields[t].value);
}
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 result = [],
outputAttribute = function(name,value) {
result.push(name + ": " + value + "\n");
},
fields = tid.getFields();
for(var t in fields) {
switch(t) {
case "text":
// Ignore the text field
var outputTiddlers = function(tiddlers) {
var result = [];
for(var t=0; t<tiddlers.length; t++) {
var tiddler = tiddlers[t],
extension,
binary = false;
switch(tiddler.type) {
case "image/jpeg":
extension = ".jpg";
binary = true;
break;
case "tags":
// Output tags as a list
outputAttribute(t,stringifyTags(fields.tags));
case "image/gif":
extension = ".gif";
binary = true;
break;
case "modified":
case "created":
// Output dates in YYYYMMDDHHMM
outputAttribute(t,utils.convertToYYYYMMDDHHMM(fields[t]));
case "image/png":
extension = ".png";
binary = true;
break;
case "image/svg+xml":
extension = ".svg";
break;
default:
// Output other attributes raw
outputAttribute(t,fields[t]);
extension = ".tid";
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");
result.push(fields.text);
return result.join("");
return result;
};
/*
Output a tiddler as an HTML <DIV>
Output an array of tiddlers as HTML <DIV>s
out - array to push the output strings
tid - the tiddler to be output
The fields are in the order title, creator, modifier, created, modified, tags, followed by any others
*/
var outputTiddlerDiv = function(tid) {
var result = [],
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 + "\"");
delete fields[name];
}
};
if(fields.text) {
delete fields.text;
var outputTiddlerDivs = function(tiddlers) {
var result = [];
for(var t=0; t<tiddlers.length; t++) {
var tiddler = tiddlers[t],
output = [],
fieldStrings = tiddler.getFieldStrings();
output.push("<div");
for(var f=0; f<fieldStrings.length; f++) {
output.push(" " + fieldStrings[f].name + "=\"" + fieldStrings[f].value + "\"");
}
output.push(">\n<pre>");
output.push(utils.htmlEncode(tiddler.text));
output.push("</pre>\n</div>");
result.push({
name: tiddler.title,
type: "application/x-tiddler-html-div",
extension: ".tiddler",
data: output.join("")
});
}
result.push("<div");
// 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("");
return result;
};
tiddlerOutput.register = function(store) {
store.registerTiddlerSerializer(".tid","application/x-tiddler",outputTiddler);
store.registerTiddlerSerializer(".tiddler","application/x-tiddler-html-div",outputTiddlerDiv);
store.registerTiddlerSerializer(".tid","application/x-tiddler",outputTiddlers);
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.macros = {}; // Hashmap of macros by macro name
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.tiddlerDeserializers = {}; // Hashmap of deserializers by accepted MIME type
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) {
if(type instanceof Array) {
for(var t=0; t<type.length; t++) {
@ -96,6 +113,10 @@ WikiStore.prototype.touchTiddler = function(type,title) {
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
*/
@ -133,6 +154,7 @@ WikiStore.prototype.getTiddlerText = function(title,defaultText) {
};
WikiStore.prototype.deleteTiddler = function(title) {
this.incChangeCount(title);
delete this.tiddlers[title];
this.clearCache(title);
this.touchTiddler("deleted",title);
@ -153,6 +175,7 @@ WikiStore.prototype.addTiddler = function(tiddler) {
if(!(tiddler instanceof Tiddler)) {
tiddler = new Tiddler(tiddler);
}
this.incChangeCount(tiddler.title);
var status = tiddler.title in this.tiddlers ? "modified" : "created";
this.clearCache(tiddler.title);
this.tiddlers[tiddler.title] = tiddler;
@ -213,10 +236,11 @@ WikiStore.prototype.getShadowTitles = function() {
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];
if(serializer) {
return serializer(tiddler);
return serializer(tiddlers);
} else {
return null;
}

View File

@ -45,7 +45,7 @@ exports.macro = {
for(var t=0; t<story.tiddlers.length; t++) {
var storyRecord = story.tiddlers[t];
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;
var tiddler = this.store.getTiddler(event.tiddlerTitle);
this.store.addTiddler(new Tiddler(

View File

@ -9,7 +9,7 @@ TiddlyWiki command line interface
var App = require("./js/App.js").App,
WikiStore = require("./js/WikiStore.js").WikiStore,
FileStore = require("./js/FileStore.js").FileStore,
LocalFileSync = require("./js/LocalFileSync.js").LocalFileSync,
Tiddler = require("./js/Tiddler.js").Tiddler,
Recipe = require("./js/Recipe.js").Recipe,
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"),
verbose = false,
recipe = null,
fileStore = null,
localFileSync = null,
lastRecipeFilepath = null,
currSwitch = 0;
@ -111,7 +112,7 @@ var commandLineSwitches = {
store: {
args: {min: 1, max: 1},
handler: function(args,callback) {
fileStore = new FileStore(args[0],app.store,function() {
localFileSync = new LocalFileSync(args[0],app.store,function() {
callback(null);
});
}
@ -149,7 +150,7 @@ var commandLineSwitches = {
recipe = [];
app.store.forEachTiddler(function(title,tiddler) {
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");
});
fs.writeFileSync(path.join(args[0],"split.recipe"),recipe.join(""));
@ -174,11 +175,33 @@ var commandLineSwitches = {
callback("--servewiki must be preceded by a --recipe");
}
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) {
response.writeHead(200, {"Content-Type": "text/html"});
response.end(recipe.cook(), "utf8");
var path = url.parse(request.url).pathname;
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);
process.nextTick(function() {callback(null);});
}
},
servetiddlers: {
@ -196,11 +219,13 @@ var commandLineSwitches = {
response.end();
}
}).listen(port);
process.nextTick(function() {callback(null);});
}
},
verbose: {
args: {min: 0, max: 0},
handler: function(args,callback) {
verbose = true;
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) {
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) {
if(err) {
throw "Error while executing option '--" + s.switchName + "' was:\n" + err;