/*** |''Name''|TiddlyWebAdaptor| |''Description''|adaptor for interacting with TiddlyWeb| |''Author:''|FND| |''Contributors''|Chris Dent, Martin Budden| |''Version''|1.2.0| |''Status''|stable| |''Source''|http://svn.tiddlywiki.org/Trunk/association/adaptors/TiddlyWebAdaptor.js| |''CodeRepository''|http://svn.tiddlywiki.org/Trunk/association/| |''License''|[[BSD|http://www.opensource.org/licenses/bsd-license.php]]| |''CoreVersion''|2.5| |''Keywords''|serverSide TiddlyWeb| !Notes This plugin includes [[jQuery JSON|http://code.google.com/p/jquery-json/]]. !To Do * createWorkspace * document custom/optional context attributes (e.g. filters, query, revision) and tiddler fields (e.g. server.title, origin) !Code ***/ //{{{ (function($) { var adaptor; adaptor = config.adaptors.tiddlyweb = function() {}; //# set up alias adaptor.prototype = new AdaptorBase(); adaptor.serverType = "tiddlyweb"; adaptor.serverLabel = "TiddlyWeb"; adaptor.mimeType = "application/json"; adaptor.parsingErrorMessage = "Error parsing result from server"; adaptor.locationIDErrorMessage = "no bag or recipe specified for tiddler"; // TODO: rename // retrieve current status (requires TiddlyWeb status plugin) adaptor.prototype.getStatus = function(context, userParams, callback) { context = this.setContext(context, userParams, callback); var uriTemplate = "%0/status"; var uri = uriTemplate.format([context.host]); var req = httpReq("GET", uri, adaptor.getStatusCallback, context, null, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getStatusCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { context.serverStatus = $.evalJSON(responseText); // XXX: error handling!? } if(context.callback) { context.callback(context, context.userParams); } }; // retrieve a list of workspaces adaptor.prototype.getWorkspaceList = function(context, userParams, callback) { context = this.setContext(context, userParams, callback); context.workspaces = []; var uriTemplate = "%0/recipes"; // XXX: bags? var uri = uriTemplate.format([context.host]); var req = httpReq("GET", uri, adaptor.getWorkspaceListCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getWorkspaceListCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { try { var workspaces = $.evalJSON(responseText); } catch(ex) { context.status = false; // XXX: correct? context.statusText = exceptionText(ex, adaptor.parsingErrorMessage); if(context.callback) { context.callback(context, context.userParams); } return; } context.workspaces = workspaces.map(function(itm) { return { title: itm }; }); } if(context.callback) { context.callback(context, context.userParams); } }; // retrieve a list of tiddlers adaptor.prototype.getTiddlerList = function(context, userParams, callback) { context = this.setContext(context, userParams, callback); var uriTemplate = "%0/%1/%2/tiddlers%3"; var params = context.filters ? "?filter=" + context.filters : ""; if(context.format) { params = context.format + params; } var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([context.host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), params]); var req = httpReq("GET", uri, adaptor.getTiddlerListCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getTiddlerListCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { context.tiddlers = []; try { var tiddlers = $.evalJSON(responseText); //# N.B.: not actual tiddler instances } catch(ex) { context.status = false; // XXX: correct? context.statusText = exceptionText(ex, adaptor.parsingErrorMessage); if(context.callback) { context.callback(context, context.userParams); } return; } for(var i = 0; i < tiddlers.length; i++) { var t = tiddlers[i]; var tiddler = new Tiddler(t.title); t.created = Date.convertFromYYYYMMDDHHMM(t.created); t.modified = Date.convertFromYYYYMMDDHHMM(t.modified); tiddler.assign(t.title, t.text, t.modifier, t.modified, t.tags, t.created, t.fields); tiddler.fields["server.type"] = adaptor.serverType; tiddler.fields["server.host"] = AdaptorBase.minHostName(context.host); tiddler.fields["server.workspace"] = context.workspace; tiddler.fields["server.page.revision"] = t.revision; context.tiddlers.push(tiddler); } } if(context.callback) { context.callback(context, context.userParams); } }; // perform global search adaptor.prototype.getSearchResults = function(context, userParams, callback) { context = this.setContext(context, userParams, callback); var uriTemplate = "%0/search?q=%1%2"; var filterString = context.filters ? ";filter=" + context.filters : ""; var uri = uriTemplate.format([context.host, context.query, filterString]); // XXX: parameters need escaping? var req = httpReq("GET", uri, adaptor.getSearchResultsCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getSearchResultsCallback = function(status, context, responseText, uri, xhr) { adaptor.getTiddlerListCallback(status, context, responseText, uri, xhr); // XXX: use apply? }; // retrieve a particular tiddler's revisions adaptor.prototype.getTiddlerRevisionList = function(title, limit, context, userParams, callback) { context = this.setContext(context, userParams, callback); var uriTemplate = "%0/%1/%2/tiddlers/%3/revisions"; var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([context.host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(title)]); var req = httpReq("GET", uri, adaptor.getTiddlerRevisionListCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getTiddlerRevisionListCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { context.revisions = []; try { var tiddlers = $.evalJSON(responseText); //# N.B.: not actual tiddler instances } catch(ex) { context.status = false; // XXX: correct? context.statusText = exceptionText(ex, adaptor.parsingErrorMessage); if(context.callback) { context.callback(context, context.userParams); } return; } for(var i = 0; i < tiddlers.length; i++) { var t = tiddlers[i]; var tiddler = new Tiddler(t.title); tiddler.assign(t.title, null, t.modifier, Date.convertFromYYYYMMDDHHMM(t.modified), t.tags, Date.convertFromYYYYMMDDHHMM(t.created), t.fields); tiddler.fields["server.type"] = adaptor.serverType; tiddler.fields["server.host"] = AdaptorBase.minHostName(context.host); tiddler.fields["server.page.revision"] = t.revision; tiddler.fields["server.workspace"] = "bags/" + t.bag; context.revisions.push(tiddler); } var sortField = "server.page.revision"; context.revisions.sort(function(a, b) { return a.fields[sortField] < b.fields[sortField] ? 1 : (a.fields[sortField] == b.fields[sortField] ? 0 : -1); }); } if(context.callback) { context.callback(context, context.userParams); } }; // retrieve an individual tiddler revision -- XXX: breaks with standard arguments list -- XXX: convenience function; simply use getTiddler? adaptor.prototype.getTiddlerRevision = function(title, revision, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.revision = revision; return this.getTiddler(title, context, userParams, callback); }; // retrieve an individual tiddler //# context is an object with members host and workspace //# callback is passed the new context and userParams adaptor.prototype.getTiddler = function(title, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = title; if(context.revision) { var uriTemplate = "%0/%1/%2/tiddlers/%3/revisions/%4"; } else { uriTemplate = "%0/%1/%2/tiddlers/%3"; } if(!context.tiddler) { context.tiddler = new Tiddler(title); } context.tiddler.fields["server.type"] = adaptor.serverType; context.tiddler.fields["server.host"] = AdaptorBase.minHostName(context.host); context.tiddler.fields["server.title"] = title; //# required for detecting renames context.tiddler.fields["server.workspace"] = context.workspace; var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([context.host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(title), context.revision]); var req = httpReq("GET", uri, adaptor.getTiddlerCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getTiddlerCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { try { var t = $.evalJSON(responseText); //# N.B.: not an actual tiddler instance } catch(ex) { context.status = false; // XXX: correct? context.statusText = exceptionText(ex, adaptor.parsingErrorMessage); if(context.callback) { context.callback(context, context.userParams); } return; } context.tiddler.assign(context.tiddler.title, t.text, t.modifier, Date.convertFromYYYYMMDDHHMM(t.modified), t.tags || [], Date.convertFromYYYYMMDDHHMM(t.created), context.tiddler.fields); // XXX: merge extended fields!? context.tiddler.fields["server.workspace"] = t.bag ? "bags/" + t.bag : "recipes/" + t.recipe; // XXX: bag is always supplied!? context.tiddler.fields["server.page.revision"] = t.revision; context.tiddler.fields["server.permissions"] = t.permissions.join(", "); } if(context.callback) { context.callback(context, context.userParams); } }; // retrieve tiddler chronicle (all revisions) adaptor.prototype.getTiddlerChronicle = function(title, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = title; var uriTemplate = "%0/%1/%2/tiddlers/%3/revisions?fat=1"; var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([context.host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(title)]); var req = httpReq("GET", uri, adaptor.getTiddlerChronicleCallback, context, { accept: adaptor.mimeType }, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.getTiddlerChronicleCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { context.responseText = responseText; } if(context.callback) { context.callback(context, context.userParams); } }; // store an individual tiddler adaptor.prototype.putTiddler = function(tiddler, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = tiddler.title; context.tiddler = tiddler; context.host = context.host || this.fullHostName(tiddler.fields["server.host"]); if(!tiddler.fields["server.title"]) { tiddler.fields["server.title"] = tiddler.title; //# required for detecting subsequent renames } else if(tiddler.title != tiddler.fields["server.title"]) { return this.moveTiddler({ title: tiddler.fields["server.title"] }, { title: tiddler.title }, context, userParams, callback); } var uriTemplate = "%0/%1/%2/tiddlers/%3"; try { var workspace = adaptor.resolveWorkspace(tiddler.fields["server.workspace"]); } catch(ex) { return adaptor.locationIDErrorMessage; } var uri = uriTemplate.format([context.host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(tiddler.title)]); var etag = adaptor.generateETag(workspace, tiddler); var headers = etag ? { "If-Match": '"' + etag + '"' } : null; var payload = { title: tiddler.title, text: tiddler.text, modifier: tiddler.modifier, tags: tiddler.tags, fields: $.extend({}, tiddler.fields) }; delete payload.fields.changecount; payload = $.toJSON(payload); var req = httpReq("PUT", uri, adaptor.putTiddlerCallback, context, headers, payload, adaptor.mimeType, null, null, true); return typeof req == "string" ? req : true; }; adaptor.putTiddlerCallback = function(status, context, responseText, uri, xhr) { context.status = [204, 1223].contains(xhr.status); context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(context.status) { context.adaptor.getTiddler(context.tiddler.title, context, context.userParams, context.callback); } else if(context.callback) { context.callback(context, context.userParams); } }; // store a tiddler chronicle adaptor.prototype.putTiddlerChronicle = function(revisions, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = revisions[0].title; var headers = null; var uriTemplate = "%0/%1/%2/tiddlers/%3/revisions"; var host = context.host || this.fullHostName(tiddler.fields["server.host"]); var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(context.title)]); if(workspace.type == "bag") { // generate ETag var etag = [adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(context.title), 0].join("/"); //# zero-revision prevents overwriting existing contents headers = { "If-Match": '"' + etag + '"' }; } var payload = $.toJSON(revisions); var req = httpReq("POST", uri, adaptor.putTiddlerChronicleCallback, context, headers, payload, adaptor.mimeType, null, null, true); return typeof req == "string" ? req : true; }; adaptor.putTiddlerChronicleCallback = function(status, context, responseText, uri, xhr) { context.status = [204, 1223].contains(xhr.status); context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(context.callback) { context.callback(context, context.userParams); } }; // store a collection of tiddlers (import TiddlyWiki HTML store) adaptor.prototype.putTiddlerStore = function(store, context, userParams, callback) { context = this.setContext(context, userParams, callback); var uriTemplate = "%0/%1/%2/tiddlers"; var host = context.host; var workspace = adaptor.resolveWorkspace(context.workspace); var uri = uriTemplate.format([host, workspace.type + "s", adaptor.normalizeTitle(workspace.name)]); var req = httpReq("POST", uri, adaptor.putTiddlerStoreCallback, context, null, store, "text/x-tiddlywiki", null, null, true); return typeof req == "string" ? req : true; }; adaptor.putTiddlerStoreCallback = function(status, context, responseText, uri, xhr) { context.status = [204, 1223].contains(xhr.status); context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(context.callback) { context.callback(context, context.userParams); } }; // rename an individual tiddler or move it to a different workspace -- TODO: make {from|to}.title optional //# from and to are objects with members title and workspace (bag; optional), //# representing source and target tiddler, respectively adaptor.prototype.moveTiddler = function(from, to, context, userParams, callback) { // XXX: rename parameters (old/new)? var _this = this; var newTiddler = store.getTiddler(from.title) || store.getTiddler(to.title); //# local rename might already have occurred var oldTiddler = $.extend(true, {}, newTiddler); //# required for eventual deletion oldTiddler.title = from.title; //# required for original tiddler's ETag var _getTiddlerChronicle = function(title, context, userParams, callback) { return _this.getTiddlerChronicle(title, context, userParams, callback); }; var _putTiddlerChronicle = function(context, userParams) { if(!context.status) { return callback(context, userParams); } var revisions = $.evalJSON(context.responseText); // XXX: error handling? // change current title while retaining previous location for(var i = 0; i < revisions.length; i++) { if(!revisions[i].fields.origin) { // N.B.: origin = "<workspace>/<title>" revisions[i].fields.origin = ["bags", revisions[i].bag, revisions[i].title].join("/"); } revisions[i].title = to.title; } // add new revision var rev = $.extend({}, revisions[0]); rev.title = to.title; $.each(newTiddler, function(i, item) { if(!$.isFunction(item)) { rev[i] = item; } }); rev.revision++; rev.created = rev.created.convertToYYYYMMDDHHMM(); rev.modified = new Date().convertToYYYYMMDDHHMM(); delete rev.fields.changecount; revisions.unshift(rev); if(to.workspace) { context.workspace = to.workspace; } else if(context.workspace.substring(0, 4) != "bags") { // N.B.: target workspace must be a bag context.workspace = "bags/" + rev.bag; } var subCallback = function(context, userparams) { var rev = "server.page.revision"; newTiddler.fields[rev] = parseInt(newTiddler.fields[rev], 10) + 1; // XXX: extended fields' values should be strings!? newTiddler.fields["server.title"] = to.title; _deleteTiddler(context, userparams); }; return _this.putTiddlerChronicle(revisions, context, context.userParams, subCallback); }; var _deleteTiddler = function(context, userParams) { if(!context.status) { return callback(context, userParams); } context.callback = null; return _this.deleteTiddler(oldTiddler, context, context.userParams, callback); }; callback = callback || function() {}; context = this.setContext(context, userParams); context.host = context.host || oldTiddler.fields["server.host"]; context.workspace = from.workspace || oldTiddler.fields["server.workspace"]; return _getTiddlerChronicle(from.title, context, userParams, _putTiddlerChronicle); }; // delete an individual tiddler adaptor.prototype.deleteTiddler = function(tiddler, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = tiddler.title; // XXX: not required!? var uriTemplate = "%0/%1/%2/tiddlers/%3"; var host = context.host || this.fullHostName(tiddler.fields["server.host"]); try { var workspace = adaptor.resolveWorkspace(tiddler.fields["server.workspace"]); } catch(ex) { return adaptor.locationIDErrorMessage; } var uri = uriTemplate.format([host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(tiddler.title)]); var etag = adaptor.generateETag(workspace, tiddler); var headers = etag ? { "If-Match": '"' + etag + '"' } : null; var req = httpReq("DELETE", uri, adaptor.deleteTiddlerCallback, context, headers, null, null, null, null, true); return typeof req == "string" ? req : true; }; adaptor.deleteTiddlerCallback = function(status, context, responseText, uri, xhr) { context.status = [204, 1223].contains(xhr.status); context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(context.callback) { context.callback(context, context.userParams); } }; // compare two revisions of a tiddler (requires TiddlyWeb differ plugin) //# if context.rev1 is not specified, the latest revision will be used for comparison //# if context.rev2 is not specified, the local revision will be sent for comparison //# context.format is a string as determined by the TiddlyWeb differ plugin adaptor.prototype.getTiddlerDiff = function(title, context, userParams, callback) { context = this.setContext(context, userParams, callback); context.title = title; var tiddler = store.getTiddler(title); try { var workspace = adaptor.resolveWorkspace(tiddler.fields["server.workspace"]); } catch(ex) { return adaptor.locationIDErrorMessage; } var tiddlerRef = [workspace.type + "s", workspace.name, tiddler.title].join("/"); var rev1 = context.rev1 ? [tiddlerRef, context.rev1].join("/") : tiddlerRef; var rev2 = context.rev2 ? [tiddlerRef, context.rev2].join("/") : null; var uriTemplate = "%0/diff?rev1=%1"; if(rev2) { uriTemplate += "&rev2=%2"; } if(context.format) { uriTemplate += "&format=%3"; } var host = context.host || this.fullHostName(tiddler.fields["server.host"]); var uri = uriTemplate.format([host, adaptor.normalizeTitle(rev1), adaptor.normalizeTitle(rev2), context.format]); if(rev2) { var req = httpReq("GET", uri, adaptor.getTiddlerDiffCallback, context, null, null, null, null, null, true); } else { var payload = { title: tiddler.title, text: tiddler.text, modifier: tiddler.modifier, tags: tiddler.tags, fields: $.extend({}, tiddler.fields) }; // XXX: missing attributes!? payload = $.toJSON(payload); req = httpReq("POST", uri, adaptor.getTiddlerDiffCallback, context, null, payload, adaptor.mimeType, null, null, true); } return typeof req == "string" ? req : true; }; adaptor.getTiddlerDiffCallback = function(status, context, responseText, uri, xhr) { context.status = status; context.statusText = xhr.statusText; context.httpStatus = xhr.status; if(status) { context.diff = responseText; } if(context.callback) { context.callback(context, context.userParams); } }; // generate tiddler information adaptor.prototype.generateTiddlerInfo = function(tiddler) { var info = {}; var uriTemplate = "%0/%1/%2/tiddlers/%3"; var host = this.host || tiddler.fields["server.host"]; // XXX: this.host obsolete? host = this.fullHostName(host); var workspace = adaptor.resolveWorkspace(tiddler.fields["server.workspace"]); info.uri = uriTemplate.format([host, workspace.type + "s", adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(tiddler.title)]); return info; }; adaptor.resolveWorkspace = function(workspace) { var components = workspace.split("/"); return { type: components[0] == "bags" ? "bag" : "recipe", name: components[1] || components[0] }; }; adaptor.generateETag = function(workspace, tiddler) { var etag = null; if(workspace.type == "bag") { var revision = tiddler.fields["server.page.revision"]; if(typeof revision == "undefined") { revision = "0"; } etag = [adaptor.normalizeTitle(workspace.name), adaptor.normalizeTitle(tiddler.title), revision].join("/"); } return etag; }; adaptor.normalizeTitle = function(title) { return encodeURIComponent(title); }; })(jQuery); /* * jQuery JSON Plugin * version: 1.3 * source: http://code.google.com/p/jquery-json/ * license: MIT (http://www.opensource.org/licenses/mit-license.php) */ (function($){function toIntegersAtLease(n) {return n<10?'0'+n:n;} Date.prototype.toJSON=function(date) {return this.getUTCFullYear()+'-'+ toIntegersAtLease(this.getUTCMonth())+'-'+ toIntegersAtLease(this.getUTCDate());};var escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};$.quoteString=function(string) {if(escapeable.test(string)) {return'"'+string.replace(escapeable,function(a) {var c=meta[a];if(typeof c==='string'){return c;} c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"';} return'"'+string+'"';};$.toJSON=function(o,compact) {var type=typeof(o);if(type=="undefined") return"undefined";else if(type=="number"||type=="boolean") return o+"";else if(o===null) return"null";if(type=="string") {return $.quoteString(o);} if(type=="object"&&typeof o.toJSON=="function") return o.toJSON(compact);if(type!="function"&&typeof(o.length)=="number") {var ret=[];for(var i=0;i<o.length;i++){ret.push($.toJSON(o[i],compact));} if(compact) return"["+ret.join(",")+"]";else return"["+ret.join(", ")+"]";} if(type=="function"){throw new TypeError("Unable to convert object of type 'function' to json.");} var ret=[];for(var k in o){var name;type=typeof(k);if(type=="number") name='"'+k+'"';else if(type=="string") name=$.quoteString(k);else continue;var val=$.toJSON(o[k],compact);if(typeof(val)!="string"){continue;} if(compact) ret.push(name+":"+val);else ret.push(name+": "+val);} return"{"+ret.join(", ")+"}";};$.compactJSON=function(o) {return $.toJSON(o,true);};$.evalJSON=function(src) {return eval("("+src+")");};$.secureEvalJSON=function(src) {var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered)) return eval("("+src+")");else throw new SyntaxError("Error parsing JSON, source is not valid.");};})(jQuery); //}}}