From dbde2bf23b2ca1b0a4c0c6493facac6a4de73c3c Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sun, 17 Mar 2013 15:28:49 +0000 Subject: [PATCH] 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. --- core/modules/commands/server.js | 45 +- core/modules/startup.js | 4 +- core/modules/syncer.js | 459 ++++++++++++++++++ core/modules/utils/dom/http.js | 65 +++ editions/clientserver/tiddlers/HelloThere.tid | 22 + editions/clientserver/tiddlywiki.info | 2 +- .../moduletypes/SyncAdaptorModules.tid | 95 ++++ plugins/tiddlywiki/tiddlyweb2/plugin.bundle | 7 + .../tiddlywiki/tiddlyweb2/tiddlywebadaptor.js | 283 +++++++++++ 9 files changed, 973 insertions(+), 9 deletions(-) create mode 100644 core/modules/syncer.js create mode 100644 core/modules/utils/dom/http.js create mode 100644 editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid create mode 100644 plugins/tiddlywiki/tiddlyweb2/plugin.bundle create mode 100644 plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js diff --git a/core/modules/commands/server.js b/core/modules/commands/server.js index e31e70a37..65821a645 100644 --- a/core/modules/commands/server.js +++ b/core/modules/commands/server.js @@ -33,7 +33,8 @@ Command.prototype.execute = function() { port = this.params[0] || "8080", rootTiddler = this.params[1] || "$:/core/templates/tiddlywiki5.template.html", renderType = this.params[2] || "text/plain", - serveType = this.params[3] || "text/html"; + serveType = this.params[3] || "text/html", + prefix; http.createServer(function(request, response) { var requestPath = url.parse(request.url).pathname, text; @@ -44,7 +45,7 @@ Command.prototype.execute = function() { data += chunk.toString(); }); request.on("end",function() { - var prefix = "/recipes/default/tiddlers/"; + prefix = "/recipes/default/tiddlers/"; if(requestPath.indexOf(prefix) === 0) { var title = decodeURIComponent(requestPath.substr(prefix.length)), fields = JSON.parse(data); @@ -64,7 +65,7 @@ Command.prototype.execute = function() { delete fields["revision"]; } 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(); response.writeHead(204, "OK",{ Etag: "\"default/" + title + "/" + changeCount + ":\"" @@ -77,11 +78,11 @@ console.log("PUT tiddler",title,fields) }); break; case "DELETE": - var prefix = "/bags/default/tiddlers/"; + prefix = "/bags/default/tiddlers/"; if(requestPath.indexOf(prefix) === 0) { var title = decodeURIComponent(requestPath.substr(prefix.length)); console.log("DELETE tiddler",title) -// self.commander.wiki.deleteTiddler(decodeURIComponent(title)); + self.commander.wiki.deleteTiddler(decodeURIComponent(title)); response.writeHead(204, "OK"); response.end(); } else { @@ -120,10 +121,40 @@ console.log("DELETE tiddler",title) text = JSON.stringify(tiddlers); response.end(text,"utf8"); } else { - response.writeHead(404); - response.end(); + prefix = "/recipes/default/tiddlers/"; + 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; + case "POST": + response.writeHead(404); + response.end(); + break; } }).listen(port); if(this.commander.verbose) { diff --git a/core/modules/startup.js b/core/modules/startup.js index ab512462d..2e32d8059 100644 --- a/core/modules/startup.js +++ b/core/modules/startup.js @@ -26,8 +26,10 @@ exports.startup = function() { $tw.modules.applyMethods("tiddlermethod",$tw.Tiddler.prototype); $tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype); $tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules); - // Set up the wiki store + // Set up the parsers $tw.wiki.initParsers(); + // Set up the syncer object + $tw.syncer = new $tw.Syncer({wiki: $tw.wiki}); // Set up the command modules $tw.Commander.initCommands(); // Get the default tiddlers diff --git a/core/modules/syncer.js b/core/modules/syncer.js new file mode 100644 index 000000000..a98857f0a --- /dev/null +++ b/core/modules/syncer.js @@ -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 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; + +})(); diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js new file mode 100644 index 000000000..fa4e87a25 --- /dev/null +++ b/core/modules/utils/dom/http.js @@ -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; +}; + +})(); diff --git a/editions/clientserver/tiddlers/HelloThere.tid b/editions/clientserver/tiddlers/HelloThere.tid index 76add9248..90f4d847a 100644 --- a/editions/clientserver/tiddlers/HelloThere.tid +++ b/editions/clientserver/tiddlers/HelloThere.tid @@ -2,3 +2,25 @@ title: HelloThere 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 + +<$reveal state="$:/status/IsLoggedIn" type="match" text="yes"> +Log out of ~TiddlyWeb: <$button message="tw-logout" class="btn btn-warning">Logout + + +<$button message="tw-server-refresh" class="btn btn-warning">Refresh + +---- + +All tiddlers: + +<$list type="all" template="TiddlerListTemplate"/> diff --git a/editions/clientserver/tiddlywiki.info b/editions/clientserver/tiddlywiki.info index fb33cbf44..e988206f9 100644 --- a/editions/clientserver/tiddlywiki.info +++ b/editions/clientserver/tiddlywiki.info @@ -1,6 +1,6 @@ { "plugins": [ - "tiddlywiki/tiddlyweb" + "tiddlywiki/tiddlyweb2" ], "parentWiki": "../tw5.com" } \ No newline at end of file diff --git a/editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid b/editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid new file mode 100644 index 000000000..39dbe270a --- /dev/null +++ b/editions/tw5.com/tiddlers/moduletypes/SyncAdaptorModules.tid @@ -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` | diff --git a/plugins/tiddlywiki/tiddlyweb2/plugin.bundle b/plugins/tiddlywiki/tiddlyweb2/plugin.bundle new file mode 100644 index 000000000..882ecb543 --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb2/plugin.bundle @@ -0,0 +1,7 @@ +{ + "title": "$:/plugins/tiddlywiki/tiddlyweb", + "description": "TiddlyWeb and TiddlySpace components", + "author": "JeremyRuston", + "version": "0.0.0", + "coreVersion": ">=5.0.0" +} diff --git a/plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js b/plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js new file mode 100644 index 000000000..943e55fce --- /dev/null +++ b/plugins/tiddlywiki/tiddlyweb2/tiddlywebadaptor.js @@ -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: + +``` +//<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; +} + +})();