mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2025-02-14 18:10:02 +00:00
Started refactoring TiddlyWeb syncer into generic syncer + TiddlyWeb adaptor
The refactored plugin is `tiddlyweb2` for the moment. The idea is to be able to use the same syncer with a different adaptor for syncing changes to the local file system.
This commit is contained in:
parent
e78c161c63
commit
dbde2bf23b
@ -33,7 +33,8 @@ Command.prototype.execute = function() {
|
|||||||
port = this.params[0] || "8080",
|
port = this.params[0] || "8080",
|
||||||
rootTiddler = this.params[1] || "$:/core/templates/tiddlywiki5.template.html",
|
rootTiddler = this.params[1] || "$:/core/templates/tiddlywiki5.template.html",
|
||||||
renderType = this.params[2] || "text/plain",
|
renderType = this.params[2] || "text/plain",
|
||||||
serveType = this.params[3] || "text/html";
|
serveType = this.params[3] || "text/html",
|
||||||
|
prefix;
|
||||||
http.createServer(function(request, response) {
|
http.createServer(function(request, response) {
|
||||||
var requestPath = url.parse(request.url).pathname,
|
var requestPath = url.parse(request.url).pathname,
|
||||||
text;
|
text;
|
||||||
@ -44,7 +45,7 @@ Command.prototype.execute = function() {
|
|||||||
data += chunk.toString();
|
data += chunk.toString();
|
||||||
});
|
});
|
||||||
request.on("end",function() {
|
request.on("end",function() {
|
||||||
var prefix = "/recipes/default/tiddlers/";
|
prefix = "/recipes/default/tiddlers/";
|
||||||
if(requestPath.indexOf(prefix) === 0) {
|
if(requestPath.indexOf(prefix) === 0) {
|
||||||
var title = decodeURIComponent(requestPath.substr(prefix.length)),
|
var title = decodeURIComponent(requestPath.substr(prefix.length)),
|
||||||
fields = JSON.parse(data);
|
fields = JSON.parse(data);
|
||||||
@ -64,7 +65,7 @@ Command.prototype.execute = function() {
|
|||||||
delete fields["revision"];
|
delete fields["revision"];
|
||||||
}
|
}
|
||||||
console.log("PUT tiddler",title,fields)
|
console.log("PUT tiddler",title,fields)
|
||||||
// self.commander.wiki.addTiddler(new $tw.Tiddler(JSON.parse(data),{title: title}));
|
self.commander.wiki.addTiddler(new $tw.Tiddler(JSON.parse(data),{title: title}));
|
||||||
var changeCount = self.commander.wiki.getChangeCount(title).toString();
|
var changeCount = self.commander.wiki.getChangeCount(title).toString();
|
||||||
response.writeHead(204, "OK",{
|
response.writeHead(204, "OK",{
|
||||||
Etag: "\"default/" + title + "/" + changeCount + ":\""
|
Etag: "\"default/" + title + "/" + changeCount + ":\""
|
||||||
@ -77,11 +78,11 @@ console.log("PUT tiddler",title,fields)
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
var prefix = "/bags/default/tiddlers/";
|
prefix = "/bags/default/tiddlers/";
|
||||||
if(requestPath.indexOf(prefix) === 0) {
|
if(requestPath.indexOf(prefix) === 0) {
|
||||||
var title = decodeURIComponent(requestPath.substr(prefix.length));
|
var title = decodeURIComponent(requestPath.substr(prefix.length));
|
||||||
console.log("DELETE tiddler",title)
|
console.log("DELETE tiddler",title)
|
||||||
// self.commander.wiki.deleteTiddler(decodeURIComponent(title));
|
self.commander.wiki.deleteTiddler(decodeURIComponent(title));
|
||||||
response.writeHead(204, "OK");
|
response.writeHead(204, "OK");
|
||||||
response.end();
|
response.end();
|
||||||
} else {
|
} else {
|
||||||
@ -120,10 +121,40 @@ console.log("DELETE tiddler",title)
|
|||||||
text = JSON.stringify(tiddlers);
|
text = JSON.stringify(tiddlers);
|
||||||
response.end(text,"utf8");
|
response.end(text,"utf8");
|
||||||
} else {
|
} else {
|
||||||
response.writeHead(404);
|
prefix = "/recipes/default/tiddlers/";
|
||||||
response.end();
|
if(requestPath.indexOf(prefix) === 0) {
|
||||||
|
var title = decodeURIComponent(requestPath.substr(prefix.length)),
|
||||||
|
tiddler = $tw.wiki.getTiddler(title),
|
||||||
|
tiddlerFields = {},
|
||||||
|
knownFields = [
|
||||||
|
"bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri"
|
||||||
|
];
|
||||||
|
if(tiddler) {
|
||||||
|
$tw.utils.each(tiddler.fields,function(field,name) {
|
||||||
|
var value = tiddler.getFieldString(name)
|
||||||
|
if(knownFields.indexOf(name) !== -1) {
|
||||||
|
tiddlerFields[name] = value;
|
||||||
|
} else {
|
||||||
|
tiddlerFields.fields = tiddlerFields.fields || {};
|
||||||
|
tiddlerFields.fields[name] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
response.writeHead(200, {"Content-Type": "application/json"});
|
||||||
|
response.end(JSON.stringify(tiddlerFields),"utf8");
|
||||||
|
} else {
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "POST":
|
||||||
|
response.writeHead(404);
|
||||||
|
response.end();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}).listen(port);
|
}).listen(port);
|
||||||
if(this.commander.verbose) {
|
if(this.commander.verbose) {
|
||||||
|
@ -26,8 +26,10 @@ exports.startup = function() {
|
|||||||
$tw.modules.applyMethods("tiddlermethod",$tw.Tiddler.prototype);
|
$tw.modules.applyMethods("tiddlermethod",$tw.Tiddler.prototype);
|
||||||
$tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype);
|
$tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype);
|
||||||
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
||||||
// Set up the wiki store
|
// Set up the parsers
|
||||||
$tw.wiki.initParsers();
|
$tw.wiki.initParsers();
|
||||||
|
// Set up the syncer object
|
||||||
|
$tw.syncer = new $tw.Syncer({wiki: $tw.wiki});
|
||||||
// Set up the command modules
|
// Set up the command modules
|
||||||
$tw.Commander.initCommands();
|
$tw.Commander.initCommands();
|
||||||
// Get the default tiddlers
|
// Get the default tiddlers
|
||||||
|
459
core/modules/syncer.js
Normal file
459
core/modules/syncer.js
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/core/modules/syncer.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: global
|
||||||
|
|
||||||
|
The syncer transfers content to and from data sources using syncadaptor modules.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
(function(){
|
||||||
|
|
||||||
|
/*jslint node: true, browser: true */
|
||||||
|
/*global $tw: false */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
Instantiate the syncer with the following options:
|
||||||
|
wiki: wiki to be synced
|
||||||
|
*/
|
||||||
|
function Syncer(options) {
|
||||||
|
var self = this;
|
||||||
|
this.wiki = options.wiki;
|
||||||
|
// Find a working syncadaptor
|
||||||
|
this.syncadaptor = undefined;
|
||||||
|
$tw.modules.forEachModuleOfType("syncadaptor",function(title,module) {
|
||||||
|
if(!self.syncadaptor && module.adaptorClass) {
|
||||||
|
self.syncadaptor = new module.adaptorClass(self);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Only do anything if we've got a syncadaptor
|
||||||
|
if(this.syncadaptor) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Error handling
|
||||||
|
*/
|
||||||
|
Syncer.prototype.showError = function(error) {
|
||||||
|
alert("Syncer error: " + error);
|
||||||
|
$tw.utils.log("Syncer error: " + error);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Message logging
|
||||||
|
*/
|
||||||
|
Syncer.prototype.log = function(/* arguments */) {
|
||||||
|
var args = Array.prototype.slice.call(arguments,0);
|
||||||
|
args[0] = "Syncer: " + args[0];
|
||||||
|
$tw.utils.log.apply(null,args);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constants
|
||||||
|
*/
|
||||||
|
Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
|
||||||
|
Syncer.prototype.titleUserName = "$:/status/UserName";
|
||||||
|
Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
|
||||||
|
Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
|
||||||
|
Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
|
||||||
|
Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialise the syncer
|
||||||
|
*/
|
||||||
|
Syncer.prototype.init = function() {
|
||||||
|
var self = this;
|
||||||
|
// Hashmap by title of {revision:,changeCount:,adaptorInfo:}
|
||||||
|
this.tiddlerInfo = {};
|
||||||
|
// Record information for known tiddlers
|
||||||
|
this.wiki.forEachTiddler(function(title,tiddler) {
|
||||||
|
if(tiddler.fields["revision"]) {
|
||||||
|
self.tiddlerInfo[title] = {
|
||||||
|
revision: tiddler.fields["revision"],
|
||||||
|
adaptorInfo: self.syncadaptor.getTiddlerInfo(tiddler),
|
||||||
|
changeCount: self.wiki.getChangeCount(title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Tasks are {type: "load"/"save"/"delete", title:, queueTime:, lastModificationTime:}
|
||||||
|
this.taskQueue = {}; // Hashmap of tasks to be performed
|
||||||
|
this.taskInProgress = {}; // Hash of tasks in progress
|
||||||
|
this.taskTimerId = null; // Timer for task dispatch
|
||||||
|
this.pollTimerId = null; // Timer for polling server
|
||||||
|
// Mark us as not logged in
|
||||||
|
this.wiki.addTiddler({title: this.titleIsLoggedIn,text: "no"});
|
||||||
|
// Listen out for changes to tiddlers
|
||||||
|
this.wiki.addEventListener("change",function(changes) {
|
||||||
|
self.syncToServer(changes);
|
||||||
|
});
|
||||||
|
// Listen out for lazyLoad events
|
||||||
|
this.wiki.addEventListener("lazyLoad",function(title) {
|
||||||
|
self.handleLazyLoadEvent(title);
|
||||||
|
});
|
||||||
|
// Listen our for login/logout/refresh events
|
||||||
|
document.addEventListener("tw-login",function(event) {
|
||||||
|
self.handleLoginEvent(event);
|
||||||
|
},false);
|
||||||
|
document.addEventListener("tw-logout",function(event) {
|
||||||
|
self.handleLogoutEvent(event);
|
||||||
|
},false);
|
||||||
|
document.addEventListener("tw-server-refresh",function(event) {
|
||||||
|
self.handleRefreshEvent(event);
|
||||||
|
},false);
|
||||||
|
// Get the login status
|
||||||
|
this.getStatus(function (err,isLoggedIn) {
|
||||||
|
if(isLoggedIn) {
|
||||||
|
// Do a sync from the server
|
||||||
|
self.syncFromServer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Save an incoming tiddler in the store, and updates the associated tiddlerInfo
|
||||||
|
*/
|
||||||
|
Syncer.prototype.storeTiddler = function(tiddlerFields) {
|
||||||
|
// Save the tiddler
|
||||||
|
var tiddler = new $tw.Tiddler(this.wiki.getTiddler(tiddlerFields.title),tiddlerFields);
|
||||||
|
this.wiki.addTiddler(tiddler);
|
||||||
|
// Save the tiddler revision and changeCount details
|
||||||
|
this.tiddlerInfo[tiddlerFields.title] = {
|
||||||
|
revision: tiddlerFields.revision,
|
||||||
|
adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler),
|
||||||
|
changeCount: this.wiki.getChangeCount(tiddlerFields.title)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Syncer.prototype.getStatus = function(callback) {
|
||||||
|
var self = this;
|
||||||
|
this.syncadaptor.getStatus(function(err,isLoggedIn,userName) {
|
||||||
|
if(err) {
|
||||||
|
self.showError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Set the various status tiddlers
|
||||||
|
self.wiki.addTiddler({title: self.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"});
|
||||||
|
if(isLoggedIn) {
|
||||||
|
self.wiki.addTiddler({title: self.titleUserName,text: userName});
|
||||||
|
} else {
|
||||||
|
self.wiki.deleteTiddler(self.titleUserName);
|
||||||
|
}
|
||||||
|
// Invoke the callback
|
||||||
|
if(callback) {
|
||||||
|
callback(err,isLoggedIn,userName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Synchronise from the server by reading the skinny tiddler list and queuing up loads for any tiddlers that we don't already have up to date
|
||||||
|
*/
|
||||||
|
Syncer.prototype.syncFromServer = function() {
|
||||||
|
this.log("Retrieving skinny tiddler list");
|
||||||
|
var self = this;
|
||||||
|
if(this.pollTimerId) {
|
||||||
|
clearTimeout(this.pollTimerId);
|
||||||
|
this.pollTimerId = null;
|
||||||
|
}
|
||||||
|
this.syncadaptor.getSkinnyTiddlers(function(err,tiddlers) {
|
||||||
|
// Trigger another sync
|
||||||
|
self.pollTimerId = window.setTimeout(function() {
|
||||||
|
self.pollTimerId = null;
|
||||||
|
self.syncFromServer.call(self);
|
||||||
|
},self.pollTimerInterval);
|
||||||
|
// Check for errors
|
||||||
|
if(err) {
|
||||||
|
self.log("Error retrieving skinny tiddler list:",err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Process each incoming tiddler
|
||||||
|
for(var t=0; t<tiddlers.length; t++) {
|
||||||
|
// Get the incoming tiddler fields, and the existing tiddler
|
||||||
|
var tiddlerFields = tiddlers[t],
|
||||||
|
incomingRevision = tiddlerFields.revision.toString(),
|
||||||
|
tiddler = self.wiki.getTiddler(tiddlerFields.title),
|
||||||
|
tiddlerInfo = self.tiddlerInfo[tiddlerFields.title],
|
||||||
|
currRevision = tiddlerInfo ? tiddlerInfo.revision : null;
|
||||||
|
// Ignore the incoming tiddler if it's the same as the revision we've already got
|
||||||
|
if(currRevision !== incomingRevision) {
|
||||||
|
// Do a full load if we've already got a fat version of the tiddler
|
||||||
|
if(tiddler && tiddler.fields.text !== undefined) {
|
||||||
|
// Do a full load of this tiddler
|
||||||
|
self.enqueueSyncTask({
|
||||||
|
type: "load",
|
||||||
|
title: tiddlerFields.title
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Load the skinny version of the tiddler
|
||||||
|
self.storeTiddler(tiddlerFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Synchronise a set of changes to the server
|
||||||
|
*/
|
||||||
|
Syncer.prototype.syncToServer = function(changes) {
|
||||||
|
var self = this,
|
||||||
|
now = new Date();
|
||||||
|
$tw.utils.each(changes,function(change,title,object) {
|
||||||
|
// Queue a task to sync this tiddler
|
||||||
|
self.enqueueSyncTask({
|
||||||
|
type: change.deleted ? "delete" : "save",
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Lazily load a skinny tiddler if we can
|
||||||
|
*/
|
||||||
|
Syncer.prototype.handleLazyLoadEvent = function(title) {
|
||||||
|
// Queue up a sync task to load this tiddler
|
||||||
|
this.enqueueSyncTask({
|
||||||
|
type: "load",
|
||||||
|
title: title
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dispay a password prompt and allow the user to login
|
||||||
|
*/
|
||||||
|
Syncer.prototype.handleLoginEvent = function() {
|
||||||
|
var self = this;
|
||||||
|
this.getStatus(function(err,isLoggedIn,userName) {
|
||||||
|
if(!isLoggedIn) {
|
||||||
|
$tw.passwordPrompt.createPrompt({
|
||||||
|
serviceName: "Login to TiddlySpace",
|
||||||
|
callback: function(data) {
|
||||||
|
self.login(data.username,data.password,function(err,isLoggedIn) {
|
||||||
|
self.syncFromServer();
|
||||||
|
});
|
||||||
|
return true; // Get rid of the password prompt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Attempt to login to TiddlyWeb.
|
||||||
|
username: username
|
||||||
|
password: password
|
||||||
|
callback: invoked with arguments (err,isLoggedIn)
|
||||||
|
*/
|
||||||
|
Syncer.prototype.login = function(username,password,callback) {
|
||||||
|
this.log("Attempting to login as",username);
|
||||||
|
var self = this;
|
||||||
|
this.syncadaptor.login(username,password,function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
self.getStatus(function(err,isLoggedIn,userName) {
|
||||||
|
if(callback) {
|
||||||
|
callback(null,isLoggedIn);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Attempt to log out of TiddlyWeb
|
||||||
|
*/
|
||||||
|
Syncer.prototype.handleLogoutEvent = function() {
|
||||||
|
this.log("Attempting to logout");
|
||||||
|
var self = this;
|
||||||
|
this.syncadaptor.logout(function(err) {
|
||||||
|
if(err) {
|
||||||
|
self.showError(err);
|
||||||
|
} else {
|
||||||
|
self.getStatus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Immediately refresh from the server
|
||||||
|
*/
|
||||||
|
Syncer.prototype.handleRefreshEvent = function() {
|
||||||
|
this.syncFromServer();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Queue up a sync task. If there is already a pending task for the tiddler, just update the last modification time
|
||||||
|
*/
|
||||||
|
Syncer.prototype.enqueueSyncTask = function(task) {
|
||||||
|
var self = this,
|
||||||
|
now = new Date();
|
||||||
|
// Set the timestamps on this task
|
||||||
|
task.queueTime = now;
|
||||||
|
task.lastModificationTime = now;
|
||||||
|
// Fill in some tiddlerInfo if the tiddler is one we haven't seen before
|
||||||
|
if(!$tw.utils.hop(this.tiddlerInfo,task.title)) {
|
||||||
|
this.tiddlerInfo[task.title] = {
|
||||||
|
revision: "0",
|
||||||
|
adaptorInfo: {},
|
||||||
|
changeCount: -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bail if this is a save and the tiddler is already at the changeCount that the server has
|
||||||
|
if(task.type === "save" && this.wiki.getChangeCount(task.title) <= this.tiddlerInfo[task.title].changeCount) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Check if this tiddler is already in the queue
|
||||||
|
if($tw.utils.hop(this.taskQueue,task.title)) {
|
||||||
|
this.log("Re-queueing up sync task with type:",task.type,"title:",task.title);
|
||||||
|
var existingTask = this.taskQueue[task.title];
|
||||||
|
// If so, just update the last modification time
|
||||||
|
existingTask.lastModificationTime = task.lastModificationTime;
|
||||||
|
// If the new task is a save then we upgrade the existing task to a save. Thus a pending load is turned into a save if the tiddler changes locally in the meantime. But a pending save is not modified to become a load
|
||||||
|
if(task.type === "save" || task.type === "delete") {
|
||||||
|
existingTask.type = task.type;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.log("Queuing up sync task with type:",task.type,"title:",task.title);
|
||||||
|
// If it is not in the queue, insert it
|
||||||
|
this.taskQueue[task.title] = task;
|
||||||
|
}
|
||||||
|
// Process the queue
|
||||||
|
$tw.utils.nextTick(function() {self.processTaskQueue.call(self);});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Return the number of tasks in progress
|
||||||
|
*/
|
||||||
|
Syncer.prototype.numTasksInProgress = function() {
|
||||||
|
return $tw.utils.count(this.taskInProgress);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Return the number of tasks in the queue
|
||||||
|
*/
|
||||||
|
Syncer.prototype.numTasksInQueue = function() {
|
||||||
|
return $tw.utils.count(this.taskQueue);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Trigger a timeout if one isn't already outstanding
|
||||||
|
*/
|
||||||
|
Syncer.prototype.triggerTimeout = function() {
|
||||||
|
var self = this;
|
||||||
|
if(!this.taskTimerId) {
|
||||||
|
this.taskTimerId = window.setTimeout(function() {
|
||||||
|
self.taskTimerId = null;
|
||||||
|
self.processTaskQueue.call(self);
|
||||||
|
},self.taskTimerInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Process the task queue, performing the next task if appropriate
|
||||||
|
*/
|
||||||
|
Syncer.prototype.processTaskQueue = function() {
|
||||||
|
var self = this;
|
||||||
|
// Only process a task if we're not already performing a task. If we are already performing a task then we'll dispatch the next one when it completes
|
||||||
|
if(this.numTasksInProgress() === 0) {
|
||||||
|
// Choose the next task to perform
|
||||||
|
var task = this.chooseNextTask();
|
||||||
|
// Perform the task if we had one
|
||||||
|
if(task) {
|
||||||
|
// Remove the task from the queue and add it to the in progress list
|
||||||
|
delete this.taskQueue[task.title];
|
||||||
|
this.taskInProgress[task.title] = task;
|
||||||
|
// Dispatch the task
|
||||||
|
this.dispatchTask(task,function(err) {
|
||||||
|
// Mark that this task is no longer in progress
|
||||||
|
delete self.taskInProgress[task.title];
|
||||||
|
// Process the next task
|
||||||
|
self.processTaskQueue.call(self);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Make sure we've set a time if there wasn't a task to perform, but we've still got tasks in the queue
|
||||||
|
if(this.numTasksInQueue() > 0) {
|
||||||
|
this.triggerTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Choose the next applicable task
|
||||||
|
*/
|
||||||
|
Syncer.prototype.chooseNextTask = function() {
|
||||||
|
var self = this,
|
||||||
|
candidateTask = null,
|
||||||
|
now = new Date();
|
||||||
|
// Select the best candidate task
|
||||||
|
$tw.utils.each(this.taskQueue,function(task,title) {
|
||||||
|
// Exclude the task if there's one of the same name in progress
|
||||||
|
if($tw.utils.hop(self.taskInProgress,title)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exclude the task if it is a save and the tiddler has been modified recently, but not hit the fallback time
|
||||||
|
if(task.type === "save" && (now - task.lastModificationTime) < self.throttleInterval &&
|
||||||
|
(now - task.queueTime) < self.fallbackInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Exclude the task if it is newer than the current best candidate
|
||||||
|
if(candidateTask && candidateTask.queueTime < task.queueTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Now this is our best candidate
|
||||||
|
candidateTask = task;
|
||||||
|
});
|
||||||
|
return candidateTask;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dispatch a task and invoke the callback
|
||||||
|
*/
|
||||||
|
Syncer.prototype.dispatchTask = function(task,callback) {
|
||||||
|
var self = this;
|
||||||
|
if(task.type === "save") {
|
||||||
|
var changeCount = this.wiki.getChangeCount(task.title),
|
||||||
|
tiddler = this.wiki.getTiddler(task.title);
|
||||||
|
this.log("Dispatching 'save' task:",task.title);
|
||||||
|
this.syncadaptor.saveTiddler(tiddler,function(err,adaptorInfo,revision) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Adjust the info stored about this tiddler
|
||||||
|
self.tiddlerInfo[task.title] = {
|
||||||
|
changeCount: changeCount,
|
||||||
|
adaptorInfo: adaptorInfo,
|
||||||
|
revision: revision
|
||||||
|
};
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
} else if(task.type === "load") {
|
||||||
|
// Load the tiddler
|
||||||
|
this.log("Dispatching 'load' task:",task.title);
|
||||||
|
this.syncadaptor.loadTiddler(task.title,function(err,tiddlerFields) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Store the tiddler
|
||||||
|
self.storeTiddler(tiddlerFields);
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
} else if(task.type === "delete") {
|
||||||
|
// Delete the tiddler
|
||||||
|
this.log("Dispatching 'delete' task:",task.title);
|
||||||
|
this.syncadaptor.deleteTiddler(task.title,function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Syncer = Syncer;
|
||||||
|
|
||||||
|
})();
|
65
core/modules/utils/dom/http.js
Normal file
65
core/modules/utils/dom/http.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/core/modules/utils/dom/http.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: utils
|
||||||
|
|
||||||
|
Browser HTTP support
|
||||||
|
|
||||||
|
\*/
|
||||||
|
(function(){
|
||||||
|
|
||||||
|
/*jslint node: true, browser: true */
|
||||||
|
/*global $tw: false */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
/*
|
||||||
|
A quick and dirty HTTP function; to be refactored later. Options are:
|
||||||
|
url: URL to retrieve
|
||||||
|
type: GET, PUT, POST etc
|
||||||
|
callback: function invoked with (err,data)
|
||||||
|
*/
|
||||||
|
exports.httpRequest = function(options) {
|
||||||
|
var type = options.type || "GET",
|
||||||
|
headers = options.headers || {accept: "application/json"},
|
||||||
|
request = new XMLHttpRequest(),
|
||||||
|
data = "",
|
||||||
|
f,results;
|
||||||
|
// Massage the data hashmap into a string
|
||||||
|
if(options.data) {
|
||||||
|
if(typeof options.data === "string") { // Already a string
|
||||||
|
data = options.data;
|
||||||
|
} else { // A hashmap of strings
|
||||||
|
results = [];
|
||||||
|
$tw.utils.each(options.data,function(dataItem,dataItemTitle) {
|
||||||
|
results.push(dataItemTitle + "=" + encodeURIComponent(dataItem));
|
||||||
|
});
|
||||||
|
data = results.join("&");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set up the state change handler
|
||||||
|
request.onreadystatechange = function() {
|
||||||
|
if(this.readyState === 4) {
|
||||||
|
if(this.status === 200 || this.status === 204) {
|
||||||
|
// Success!
|
||||||
|
options.callback(null,this.responseText,this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Something went wrong
|
||||||
|
options.callback(new Error("XMLHttpRequest error: " + this.status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Make the request
|
||||||
|
request.open(type,options.url,true);
|
||||||
|
if(headers) {
|
||||||
|
$tw.utils.each(headers,function(header,headerTitle,object) {
|
||||||
|
request.setRequestHeader(headerTitle,header);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(data && !$tw.utils.hop(headers,"Content-type")) {
|
||||||
|
request.setRequestHeader("Content-type","application/x-www-form-urlencoded; charset=UTF-8");
|
||||||
|
}
|
||||||
|
request.send(data);
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
})();
|
@ -2,3 +2,25 @@ title: HelloThere
|
|||||||
|
|
||||||
Experimental clientserver edition of TiddlyWiki5.
|
Experimental clientserver edition of TiddlyWiki5.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Current [[login status|$:/status/IsLoggedIn]]: {{$:/status/IsLoggedIn}}
|
||||||
|
|
||||||
|
Current [[username|$:/status/UserName]]: {{$:/status/UserName}}
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
<$reveal state="$:/status/IsLoggedIn" type="nomatch" text="yes">
|
||||||
|
Log in to ~TiddlyWeb: <$button message="tw-login" class="btn btn-info">Login</$button>
|
||||||
|
</$reveal>
|
||||||
|
<$reveal state="$:/status/IsLoggedIn" type="match" text="yes">
|
||||||
|
Log out of ~TiddlyWeb: <$button message="tw-logout" class="btn btn-warning">Logout</$button>
|
||||||
|
</$reveal>
|
||||||
|
|
||||||
|
<$button message="tw-server-refresh" class="btn btn-warning">Refresh</$button>
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
All tiddlers:
|
||||||
|
|
||||||
|
<$list type="all" template="TiddlerListTemplate"/>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"tiddlywiki/tiddlyweb"
|
"tiddlywiki/tiddlyweb2"
|
||||||
],
|
],
|
||||||
"parentWiki": "../tw5.com"
|
"parentWiki": "../tw5.com"
|
||||||
}
|
}
|
95
editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid
Normal file
95
editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
title: SyncAdaptorModules
|
||||||
|
tags: internals
|
||||||
|
|
||||||
|
! Introduction
|
||||||
|
|
||||||
|
SyncAdaptorModules encapsulate storage mechanisms that can be used by the SyncMechanism. Two examples are:
|
||||||
|
|
||||||
|
* The TiddlyWebAdaptor interfaces with servers compatible with TiddlyWeb's HTTP API, such as TiddlyWeb itself and TiddlyWiki5's built-in ServerMechanism.
|
||||||
|
|
||||||
|
* The LocalFileAdaptor interfaces with file systems with an API compatible with node.js's `fs` module
|
||||||
|
|
||||||
|
SyncAdaptorModules are represented as JavaScript tiddlers with the field `module-type` set to `syncadaptor`.
|
||||||
|
|
||||||
|
! Exports
|
||||||
|
|
||||||
|
The following properties should be exposed via the `exports` object:
|
||||||
|
|
||||||
|
|!Property |!Description |
|
||||||
|
|adaptorClass |The JavaScript class for the adaptor |
|
||||||
|
|
||||||
|
Nothing should be exported if the adaptor detects that it isn't capable of operating successfully (eg, because it only runs on either the browser or the server, or because a dependency is missing).
|
||||||
|
|
||||||
|
! Adaptor Module Methods
|
||||||
|
|
||||||
|
Adaptor modules must handle the following methods.
|
||||||
|
|
||||||
|
!! `Constructor(syncer)`
|
||||||
|
|
||||||
|
Initialises a new adaptor instance.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|syncer |Syncer object that is using this adaptor |
|
||||||
|
|
||||||
|
!! `getTiddlerInfo(tiddler)`
|
||||||
|
|
||||||
|
Gets the supplemental information that the adaptor needs to keep track of for a particular tiddler. For example, the TiddlyWeb adaptor includes a `bag` field indicating the original bag of the tiddler.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|tiddler |Target tiddler |
|
||||||
|
|
||||||
|
Returns an object storing any additional information required by the adaptor.
|
||||||
|
|
||||||
|
!! `getStatus(callback)`
|
||||||
|
|
||||||
|
Retrieves status information from the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|callback |Callback function invoked with parameters `err,isLoggedIn,userName` |
|
||||||
|
|
||||||
|
!! `login(username,password,callback)`
|
||||||
|
|
||||||
|
Attempts to login to the server with specified credentials.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|username |Username |
|
||||||
|
|password |Password |
|
||||||
|
|callback |Callback function invoked with parameter `err` |
|
||||||
|
|
||||||
|
!! `logout(callback)`
|
||||||
|
|
||||||
|
Attempts to logout of the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|callback |Callback function invoked with parameter `err` |
|
||||||
|
|
||||||
|
!! `getSkinnyTiddlers(callback)`
|
||||||
|
|
||||||
|
Retrieves a list of skinny tiddlers from the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|callback |Callback function invoked with parameter `err,tiddlers`, where `tiddlers` is an array of tiddler field objects |
|
||||||
|
|
||||||
|
!! `saveTiddler(tiddler,callback)`
|
||||||
|
|
||||||
|
Saves a tiddler to the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|tiddler |Tiddler to be saved |
|
||||||
|
|callback |Callback function invoked with parameter `err,adaptorInfo,revision` |
|
||||||
|
|
||||||
|
!! `loadTiddler(title,callback)`
|
||||||
|
|
||||||
|
Loads a tiddler from the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|title |Title of tiddler to be retrieved |
|
||||||
|
|callback |Callback function invoked with parameter `err,tiddlerFields` |
|
||||||
|
|
||||||
|
!! `deleteTiddler(title,callback)`
|
||||||
|
|
||||||
|
Delete a tiddler from the server.
|
||||||
|
|
||||||
|
|!Parameter |!Description |
|
||||||
|
|title |Title of tiddler to be deleted |
|
||||||
|
|callback |Callback function invoked with parameter `err` |
|
7
plugins/tiddlywiki/tiddlyweb2/plugin.bundle
Normal file
7
plugins/tiddlywiki/tiddlyweb2/plugin.bundle
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"title": "$:/plugins/tiddlywiki/tiddlyweb",
|
||||||
|
"description": "TiddlyWeb and TiddlySpace components",
|
||||||
|
"author": "JeremyRuston",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"coreVersion": ">=5.0.0"
|
||||||
|
}
|
283
plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js
Normal file
283
plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/plugins/tiddlywiki/tiddlyweb/tiddlywebadaptor.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: syncadaptor
|
||||||
|
|
||||||
|
A sync adaptor module for synchronising with TiddlyWeb compatible servers
|
||||||
|
|
||||||
|
\*/
|
||||||
|
(function(){
|
||||||
|
|
||||||
|
/*jslint node: true, browser: true */
|
||||||
|
/*global $tw: false */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function TiddlyWebAdaptor(syncer) {
|
||||||
|
this.syncer = syncer;
|
||||||
|
this.host = document.location.protocol + "//" + document.location.host + "/";
|
||||||
|
this.recipe = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
TiddlyWebAdaptor.prototype.getTiddlerInfo = function(tiddler) {
|
||||||
|
return {
|
||||||
|
bag: tiddler.fields["bag"]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the current status of the TiddlyWeb connection
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.getStatus = function(callback) {
|
||||||
|
// Get status
|
||||||
|
var self = this,
|
||||||
|
wiki = self.syncer.wiki;
|
||||||
|
this.syncer.log("Getting status");
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "status",
|
||||||
|
callback: function(err,data) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Decode the status JSON
|
||||||
|
var json = null,
|
||||||
|
isLoggedIn = false;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
if(json) {
|
||||||
|
// Record the recipe
|
||||||
|
if(json.space) {
|
||||||
|
self.recipe = json.space.recipe;
|
||||||
|
}
|
||||||
|
// Check if we're logged in
|
||||||
|
isLoggedIn = json.username !== "GUEST";
|
||||||
|
}
|
||||||
|
// Invoke the callback if present
|
||||||
|
if(callback) {
|
||||||
|
callback(null,isLoggedIn,json.username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Attempt to login and invoke the callback(err)
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.login = function(username,password,callback) {
|
||||||
|
var self = this;
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "challenge/tiddlywebplugins.tiddlyspace.cookie_form",
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
user: username,
|
||||||
|
password: password,
|
||||||
|
tiddlyweb_redirect: "/status" // workaround to marginalize automatic subsequent GET
|
||||||
|
},
|
||||||
|
callback: function(err) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.logout = function(callback) {
|
||||||
|
var self = this;
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "logout",
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
csrf_token: this.getCsrfToken(),
|
||||||
|
tiddlyweb_redirect: "/status" // workaround to marginalize automatic subsequent GET
|
||||||
|
},
|
||||||
|
callback: function(err,data) {
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retrieve the CSRF token from its cookie
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.getCsrfToken = function() {
|
||||||
|
var regex = /^(?:.*; )?csrf_token=([^(;|$)]*)(?:;|$)/,
|
||||||
|
match = regex.exec(document.cookie),
|
||||||
|
csrf = null;
|
||||||
|
if (match && (match.length === 2)) {
|
||||||
|
csrf = match[1];
|
||||||
|
}
|
||||||
|
return csrf;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get an array of skinny tiddler fields from the server
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.getSkinnyTiddlers = function(callback) {
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "recipes/" + this.recipe + "/tiddlers.json",
|
||||||
|
callback: function(err,data) {
|
||||||
|
// Check for errors
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Invoke the callback with the skinny tiddlers
|
||||||
|
callback(null,JSON.parse(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Save a tiddler and invoke the callback with (err,adaptorInfo,revision)
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.saveTiddler = function(tiddler,callback) {
|
||||||
|
var self = this;
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(tiddler.fields.title),
|
||||||
|
type: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-type": "application/json"
|
||||||
|
},
|
||||||
|
data: this.convertTiddlerToTiddlyWebFormat(tiddler),
|
||||||
|
callback: function(err,data,request) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Save the details of the new revision of the tiddler
|
||||||
|
var etagInfo = self.parseEtag(request.getResponseHeader("Etag"));
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null,{
|
||||||
|
bag: etagInfo.bag
|
||||||
|
}, etagInfo.revision);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Load a tiddler and invoke the callback with (err,tiddlerFields)
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.loadTiddler = function(title,callback) {
|
||||||
|
var self = this;
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "recipes/" + encodeURIComponent(this.recipe) + "/tiddlers/" + encodeURIComponent(title),
|
||||||
|
callback: function(err,data,request) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null,self.convertTiddlerFromTiddlyWebFormat(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Delete a tiddler and invoke the callback with (err)
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.deleteTiddler = function(title,callback) {
|
||||||
|
var self = this,
|
||||||
|
bag = this.syncer.tiddlerInfo[title].adaptorInfo.bag;
|
||||||
|
$tw.utils.httpRequest({
|
||||||
|
url: this.host + "bags/" + encodeURIComponent(bag) + "/tiddlers/" + encodeURIComponent(title),
|
||||||
|
type: "DELETE",
|
||||||
|
callback: function(err,data,request) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
// Invoke the callback
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert a tiddler to a field set suitable for PUTting to TiddlyWeb
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.convertTiddlerToTiddlyWebFormat = function(tiddler) {
|
||||||
|
var result = {},
|
||||||
|
knownFields = [
|
||||||
|
"bag", "created", "creator", "modified", "modifier", "permissions", "recipe", "revision", "tags", "text", "title", "type", "uri"
|
||||||
|
];
|
||||||
|
if(tiddler) {
|
||||||
|
$tw.utils.each(tiddler.fields,function(fieldValue,fieldName) {
|
||||||
|
var fieldString = fieldName === "tags" ?
|
||||||
|
tiddler.fields.tags :
|
||||||
|
tiddler.getFieldString(fieldName); // Tags must be passed as an array, not a string
|
||||||
|
|
||||||
|
if(knownFields.indexOf(fieldName) !== -1) {
|
||||||
|
// If it's a known field, just copy it across
|
||||||
|
result[fieldName] = fieldString;
|
||||||
|
} else {
|
||||||
|
// If it's unknown, put it in the "fields" field
|
||||||
|
result.fields = result.fields || {};
|
||||||
|
result.fields[fieldName] = fieldString;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Convert the type "text/x-tiddlywiki" into null
|
||||||
|
if(result.type === "text/x-tiddlywiki") {
|
||||||
|
result.type = null;
|
||||||
|
}
|
||||||
|
return JSON.stringify(result,null,$tw.config.preferences.jsonSpaces);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert a field set in TiddlyWeb format into ordinary TiddlyWiki5 format
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.convertTiddlerFromTiddlyWebFormat = function(data) {
|
||||||
|
var tiddlerFields = JSON.parse(data),
|
||||||
|
self = this,
|
||||||
|
result = {};
|
||||||
|
// Transfer the fields, pulling down the `fields` hashmap
|
||||||
|
$tw.utils.each(tiddlerFields,function(element,title,object) {
|
||||||
|
if(title === "fields") {
|
||||||
|
$tw.utils.each(element,function(element,subTitle,object) {
|
||||||
|
result[subTitle] = element;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result[title] = tiddlerFields[title];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Some unholy freaking of content types
|
||||||
|
if(result.type === "text/javascript") {
|
||||||
|
result.type = "application/javascript";
|
||||||
|
} else if(!result.type || result.type === "None") {
|
||||||
|
result.type = "text/x-tiddlywiki";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Split a TiddlyWeb Etag into its constituent parts. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
"system-images_public/unsyncedIcon/946151:9f11c278ccde3a3149f339f4a1db80dd4369fc04"
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the value includes the opening and closing double quotes.
|
||||||
|
|
||||||
|
The parts are:
|
||||||
|
|
||||||
|
```
|
||||||
|
<bag>/<title>/<revision>:<hash>
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
TiddlyWebAdaptor.prototype.parseEtag = function(etag) {
|
||||||
|
var firstSlash = etag.indexOf("/"),
|
||||||
|
lastSlash = etag.lastIndexOf("/"),
|
||||||
|
colon = etag.lastIndexOf(":");
|
||||||
|
if(firstSlash === -1 || lastSlash === -1 || colon === -1) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
bag: decodeURIComponent(etag.substring(1,firstSlash)),
|
||||||
|
title: decodeURIComponent(etag.substring(firstSlash + 1,lastSlash)),
|
||||||
|
revision: etag.substring(lastSlash + 1,colon)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if($tw.browser) {
|
||||||
|
exports.adaptorClass = TiddlyWebAdaptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
Loading…
x
Reference in New Issue
Block a user