56 changes: 38 additions & 18 deletions core/modules/parsers/wikiparser/wikiparser.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ Attributes are stored as hashmaps of the following objects:
/*global $tw: false */
"use strict";

/*
type: content type of text
text: text to be parsed
options: see below:
parseAsInline: true to parse text as inline instead of block
wiki: reference to wiki to use
_canonical_uri: optional URI of content if text is missing or empty
*/
var WikiParser = function(type,text,options) {
this.wiki = options.wiki;
var self = this;
Expand All @@ -33,19 +41,6 @@ var WikiParser = function(type,text,options) {
this.loadRemoteTiddler(options._canonical_uri);
text = $tw.language.getRawString("LazyLoadingWarning");
}
// Initialise the classes if we don't have them already
if(!this.pragmaRuleClasses) {
WikiParser.prototype.pragmaRuleClasses = $tw.modules.createClassesFromModules("wikirule","pragma",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.pragmaRuleClasses,"$:/config/WikiParserRules/Pragmas/");
}
if(!this.blockRuleClasses) {
WikiParser.prototype.blockRuleClasses = $tw.modules.createClassesFromModules("wikirule","block",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.blockRuleClasses,"$:/config/WikiParserRules/Block/");
}
if(!this.inlineRuleClasses) {
WikiParser.prototype.inlineRuleClasses = $tw.modules.createClassesFromModules("wikirule","inline",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.inlineRuleClasses,"$:/config/WikiParserRules/Inline/");
}
// Save the parse text
this.type = type || "text/vnd.tiddlywiki";
this.source = text || "";
Expand All @@ -54,13 +49,38 @@ var WikiParser = function(type,text,options) {
this.configTrimWhiteSpace = false;
// Set current parse position
this.pos = 0;
// Start with empty output
this.tree = [];
// Assemble the rule classes we're going to use
var pragmaRuleClasses, blockRuleClasses, inlineRuleClasses;
if(options.rules) {
pragmaRuleClasses = options.rules.pragma;
blockRuleClasses = options.rules.block;
inlineRuleClasses = options.rules.inline;
} else {
// Setup the rule classes if we don't have them already
if(!this.pragmaRuleClasses) {
WikiParser.prototype.pragmaRuleClasses = $tw.modules.createClassesFromModules("wikirule","pragma",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.pragmaRuleClasses,"$:/config/WikiParserRules/Pragmas/");
}
pragmaRuleClasses = this.pragmaRuleClasses;
if(!this.blockRuleClasses) {
WikiParser.prototype.blockRuleClasses = $tw.modules.createClassesFromModules("wikirule","block",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.blockRuleClasses,"$:/config/WikiParserRules/Block/");
}
blockRuleClasses = this.blockRuleClasses;
if(!this.inlineRuleClasses) {
WikiParser.prototype.inlineRuleClasses = $tw.modules.createClassesFromModules("wikirule","inline",$tw.WikiRuleBase);
this.setupRules(WikiParser.prototype.inlineRuleClasses,"$:/config/WikiParserRules/Inline/");
}
inlineRuleClasses = this.inlineRuleClasses;
}
// Instantiate the pragma parse rules
this.pragmaRules = this.instantiateRules(this.pragmaRuleClasses,"pragma",0);
this.pragmaRules = this.instantiateRules(pragmaRuleClasses,"pragma",0);
// Instantiate the parser block and inline rules
this.blockRules = this.instantiateRules(this.blockRuleClasses,"block",0);
this.inlineRules = this.instantiateRules(this.inlineRuleClasses,"inline",0);
this.blockRules = this.instantiateRules(blockRuleClasses,"block",0);
this.inlineRules = this.instantiateRules(inlineRuleClasses,"inline",0);
// Parse any pragmas
this.tree = [];
var topBranch = this.parsePragmas();
// Parse the text into inline runs or blocks
if(options.parseAsInline) {
Expand Down Expand Up @@ -358,7 +378,7 @@ WikiParser.prototype.pushTextWidget = function(array,text) {
text = $tw.utils.trim(text);
}
if(text) {
array.push({type: "text", text: text});
array.push({type: "text", text: text});
}
};

Expand Down
3 changes: 2 additions & 1 deletion core/modules/saver-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ SaverHandler.prototype.saveWiki = function(options) {
return false;
}
var variables = options.variables || {},
template = options.template || "$:/core/save/all",
template = (options.template ||
this.wiki.getTiddlerText("$:/config/SaveWikiButton/Template","$:/core/save/all")).trim(),
downloadType = options.downloadType || "text/plain",
text = this.wiki.renderTiddler(downloadType,template,options),
callback = function(err) {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/andtidwiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ AndTidWiki.prototype.save = function(text,method,callback,options) {
window.twi.saveWiki(text);
} else {
// Get the pathname of this document
var pathname = decodeURIComponent(document.location.toString().split("#")[0]);
var pathname = $tw.utils.decodeURIComponentSafe(document.location.toString().split("#")[0]);
// Strip the file://
if(pathname.indexOf("file://") === 0) {
pathname = pathname.substr(7);
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ DownloadSaver.prototype.save = function(text,method,callback,options) {
var p = document.location.pathname.lastIndexOf("/");
if(p !== -1) {
// We decode the pathname because document.location is URL encoded by the browser
filename = decodeURIComponent(document.location.pathname.substr(p+1));
filename = $tw.utils.decodeURIComponentSafe(document.location.pathname.substr(p+1));
}
}
if(!filename) {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/gitea.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ GiteaSaver.prototype.save = function(text,method,callback) {
}
}
var data = {
message: $tw.language.getRawString("ControlPanel/Saving/GitService/CommitMessage"),
message: $tw.language.getString("ControlPanel/Saving/GitService/CommitMessage"),
content: $tw.utils.base64Encode(text),
sha: sha
};
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ GitHubSaver.prototype.save = function(text,method,callback) {
});
}
var data = {
message: $tw.language.getRawString("ControlPanel/Saving/GitService/CommitMessage"),
message: $tw.language.getString("ControlPanel/Saving/GitService/CommitMessage"),
content: $tw.utils.base64Encode(text),
branch: branch,
sha: sha
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ GitLabSaver.prototype.save = function(text,method,callback) {
});
}
var data = {
commit_message: $tw.language.getRawString("ControlPanel/Saving/GitService/CommitMessage"),
commit_message: $tw.language.getString("ControlPanel/Saving/GitService/CommitMessage"),
content: text,
branch: branch,
sha: sha
Expand Down
9 changes: 6 additions & 3 deletions core/modules/savers/put.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,12 @@ PutSaver.prototype.save = function(text,method,callback) {
if(err) {
// response is textual: "XMLHttpRequest error code: 412"
var status = Number(err.substring(err.indexOf(':') + 2, err.length))
if(status === 412) { // edit conflict
var message = $tw.language.getString("Error/EditConflict");
callback(message);
if(status === 412) { // file changed on server
callback($tw.language.getString("Error/PutEditConflict"));
} else if(status === 401) { // authentication required
callback($tw.language.getString("Error/PutUnauthorized"));
} else if(status === 403) { // permission denied
callback($tw.language.getString("Error/PutForbidden"));
} else {
callback(err); // fail
}
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/tiddlyfox.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ TiddlyFoxSaver.prototype.save = function(text,method,callback) {
}
// Create the message element and put it in the message box
var message = document.createElement("div");
message.setAttribute("data-tiddlyfox-path",decodeURIComponent(pathname));
message.setAttribute("data-tiddlyfox-path",$tw.utils.decodeURIComponentSafe(pathname));
message.setAttribute("data-tiddlyfox-content",text);
messageBox.appendChild(message);
// Add an event handler for when the file has been saved
Expand Down
2 changes: 1 addition & 1 deletion core/modules/savers/twedit.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ TWEditSaver.prototype.save = function(text,method,callback) {
return false;
}
// Get the pathname of this document
var pathname = decodeURIComponent(document.location.pathname);
var pathname = $tw.utils.decodeURIComponentSafe(document.location.pathname);
// Strip any query or location part
var p = pathname.indexOf("?");
if(p !== -1) {
Expand Down
18 changes: 15 additions & 3 deletions core/modules/savers/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,29 @@ UploadSaver.prototype.save = function(text,method,callback) {
password = $tw.utils.getPassword("upload"),
uploadDir = this.wiki.getTextReference("$:/UploadDir") || ".",
uploadFilename = this.wiki.getTextReference("$:/UploadFilename") || "index.html",
uploadWithUrlOnly = this.wiki.getTextReference("$:/UploadWithUrlOnly") || "no",
url = this.wiki.getTextReference("$:/UploadURL");
// Bail out if we don't have the bits we need
if(!username || username.toString().trim() === "" || !password || password.toString().trim() === "") {
return false;
if (uploadWithUrlOnly === "yes") {
// The url is good enough. No need for a username and password.
// Assume the server uses some other kind of auth mechanism.
if(!url || url.toString().trim() === "") {
return false;
}
}
else {
// Require username and password to be present.
// Assume the server uses the standard UploadPlugin username/password.
if(!username || username.toString().trim() === "" || !password || password.toString().trim() === "") {
return false;
}
}
// Construct the url if not provided
if(!url) {
url = "http://" + username + ".tiddlyspot.com/store.cgi";
}
// Assemble the header
var boundary = "---------------------------" + "AaB03x";
var boundary = "---------------------------" + "AaB03x";
var uploadFormName = "UploadPlugin";
var head = [];
head.push("--" + boundary + "\r\nContent-disposition: form-data; name=\"UploadPlugin\"\r\n");
Expand Down
2 changes: 1 addition & 1 deletion core/modules/server/routes/delete-tiddler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.method = "DELETE";
exports.path = /^\/bags\/default\/tiddlers\/(.+)$/;

exports.handler = function(request,response,state) {
var title = decodeURIComponent(state.params[0]);
var title = $tw.utils.decodeURIComponentSafe(state.params[0]);
state.wiki.deleteTiddler(title);
response.writeHead(204, "OK", {
"Content-Type": "text/plain"
Expand Down
3 changes: 1 addition & 2 deletions core/modules/server/routes/get-favicon.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ exports.method = "GET";
exports.path = /^\/favicon.ico$/;

exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "image/x-icon"});
var buffer = state.wiki.getTiddlerText("$:/favicon.ico","");
response.end(buffer,"base64");
state.sendResponse(200,{"Content-Type": "image/x-icon"},buffer,"base64");
};

}());
38 changes: 21 additions & 17 deletions core/modules/server/routes/get-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@ exports.handler = function(request,response,state) {
var path = require("path"),
fs = require("fs"),
util = require("util"),
suppliedFilename = decodeURIComponent(state.params[0]),
filename = path.resolve(state.boot.wikiPath,"files",suppliedFilename),
suppliedFilename = $tw.utils.decodeURIComponentSafe(state.params[0]),
baseFilename = path.resolve(state.boot.wikiPath,"files"),
filename = path.resolve(baseFilename,suppliedFilename),
extension = path.extname(filename);
fs.readFile(filename,function(err,content) {
var status,content,type = "text/plain";
if(err) {
console.log("Error accessing file " + filename + ": " + err.toString());
status = 404;
content = "File '" + suppliedFilename + "' not found";
} else {
status = 200;
content = content;
type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream");
}
response.writeHead(status,{
"Content-Type": type
// Check that the filename is inside the wiki files folder
if(path.relative(baseFilename,filename).indexOf("..") !== 0) {
// Send the file
fs.readFile(filename,function(err,content) {
var status,content,type = "text/plain";
if(err) {
console.log("Error accessing file " + filename + ": " + err.toString());
status = 404;
content = "File '" + suppliedFilename + "' not found";
} else {
status = 200;
content = content;
type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream");
}
state.sendResponse(status,{"Content-Type": type},content);
});
response.end(content);
});
} else {
state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found");
}
};

}());
24 changes: 1 addition & 23 deletions core/modules/server/routes/get-index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,16 @@ GET /
/*global $tw: false */
"use strict";

var zlib = require("zlib");

exports.method = "GET";

exports.path = /^\/$/;

exports.handler = function(request,response,state) {
var acceptEncoding = request.headers["accept-encoding"];
if(!acceptEncoding) {
acceptEncoding = "";
}
var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")),
responseHeaders = {
"Content-Type": state.server.get("root-serve-type")
};
/*
If the gzip=yes flag for `listen` is set, check if the user agent permits
compression. If so, compress our response. Note that we use the synchronous
functions from zlib to stay in the imperative style. The current `Server`
doesn't depend on this, and we may just as well use the async versions.
*/
if(state.server.enableGzip) {
if (/\bdeflate\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "deflate";
text = zlib.deflateSync(text);
} else if (/\bgzip\b/.test(acceptEncoding)) {
responseHeaders["Content-Encoding"] = "gzip";
text = zlib.gzipSync(text);
}
}
response.writeHead(200,responseHeaders);
response.end(text);
state.sendResponse(200,responseHeaders,text);
};

}());
5 changes: 3 additions & 2 deletions core/modules/server/routes/get-login-basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ exports.handler = function(request,response,state) {
response.writeHead(401,{
"WWW-Authenticate": 'Basic realm="Please provide your username and password to login to ' + state.server.servername + '"'
});
response.end();
response.end();
} else {
// Redirect to the root wiki if login worked
var location = ($tw.syncadaptor && $tw.syncadaptor.host)? $tw.syncadaptor.host: "/";
response.writeHead(302,{
Location: "/"
Location: location
});
response.end();
}
Expand Down
3 changes: 1 addition & 2 deletions core/modules/server/routes/get-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ exports.method = "GET";
exports.path = /^\/status$/;

exports.handler = function(request,response,state) {
response.writeHead(200, {"Content-Type": "application/json"});
var text = JSON.stringify({
username: state.authenticatedUsername || state.server.get("anon-username") || "",
anonymous: !state.authenticatedUsername,
Expand All @@ -27,7 +26,7 @@ exports.handler = function(request,response,state) {
},
tiddlywiki_version: $tw.version
});
response.end(text,"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};

}());
6 changes: 3 additions & 3 deletions core/modules/server/routes/get-tiddler-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.method = "GET";
exports.path = /^\/([^\/]+)$/;

exports.handler = function(request,response,state) {
var title = decodeURIComponent(state.params[0]),
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title);
if(tiddler) {
var renderType = tiddler.getFieldString("_render_type"),
Expand All @@ -32,9 +32,9 @@ exports.handler = function(request,response,state) {
renderTemplate = renderTemplate || state.server.get("tiddler-render-template");
}
var text = state.wiki.renderTiddler(renderType,renderTemplate,{parseAsInline: true, variables: {currentTiddler: title}});

// Naughty not to set a content-type, but it's the easiest way to ensure the browser will see HTML pages as HTML, and accept plain text tiddlers as CSS or JS
response.writeHead(200);
response.end(text,"utf8");
state.sendResponse(200,{},text,"utf8");
} else {
response.writeHead(404);
response.end();
Expand Down
5 changes: 2 additions & 3 deletions core/modules/server/routes/get-tiddler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.method = "GET";
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;

exports.handler = function(request,response,state) {
var title = decodeURIComponent(state.params[0]),
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
tiddler = state.wiki.getTiddler(title),
tiddlerFields = {},
knownFields = [
Expand All @@ -36,8 +36,7 @@ exports.handler = function(request,response,state) {
tiddlerFields.revision = state.wiki.getChangeCount(title);
tiddlerFields.bag = "default";
tiddlerFields.type = tiddlerFields.type || "text/vnd.tiddlywiki";
response.writeHead(200, {"Content-Type": "application/json"});
response.end(JSON.stringify(tiddlerFields),"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},JSON.stringify(tiddlerFields),"utf8");
} else {
response.writeHead(404);
response.end();
Expand Down
3 changes: 1 addition & 2 deletions core/modules/server/routes/get-tiddlers-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ exports.handler = function(request,response,state) {
}
var excludeFields = (state.queryParameters.exclude || "text").split(","),
titles = state.wiki.filterTiddlers(filter);
response.writeHead(200, {"Content-Type": "application/json"});
var tiddlers = [];
$tw.utils.each(titles,function(title) {
var tiddler = state.wiki.getTiddler(title);
Expand All @@ -45,7 +44,7 @@ exports.handler = function(request,response,state) {
}
});
var text = JSON.stringify(tiddlers);
response.end(text,"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};

}());
2 changes: 1 addition & 1 deletion core/modules/server/routes/put-tiddler.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.method = "PUT";
exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/;

exports.handler = function(request,response,state) {
var title = decodeURIComponent(state.params[0]),
var title = $tw.utils.decodeURIComponentSafe(state.params[0]),
fields = JSON.parse(state.data);
// Pull up any subfields in the `fields` object
if(fields.fields) {
Expand Down
81 changes: 76 additions & 5 deletions core/modules/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ if($tw.node) {
fs = require("fs"),
url = require("url"),
path = require("path"),
querystring = require("querystring");
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
}

/*
Expand All @@ -40,13 +42,15 @@ function Server(options) {
if(options.variables[variable]) {
this.variables[variable] = options.variables[variable];
}
}
}
}
$tw.utils.extend({},this.defaultVariables,options.variables);
// Initialise CSRF
this.csrfDisable = this.get("csrf-disable") === "yes";
// Initialize Gzip compression
this.enableGzip = this.get("gzip") === "yes";
// Initialize browser-caching
this.enableBrowserCache = this.get("use-browser-cache") === "yes";
// Initialise authorization
var authorizedUserName = (this.get("username") && this.get("password")) ? this.get("username") : "(anon)";
this.authorizationPrincipals = {
Expand Down Expand Up @@ -78,6 +82,71 @@ function Server(options) {
this.transport = require(this.protocol);
}

/*
Send a response to the client. This method checks if the response must be sent
or if the client alrady has the data cached. If that's the case only a 304
response will be transmitted and the browser will use the cached data.
Only requests with status code 200 are considdered for caching.
request: request instance passed to the handler
response: response instance passed to the handler
statusCode: stauts code to send to the browser
headers: response headers (they will be augmented with an `Etag` header)
data: the data to send (passed to the end method of the response instance)
encoding: the encoding of the data to send (passed to the end method of the response instance)
*/
function sendResponse(request,response,statusCode,headers,data,encoding) {
if(this.enableBrowserCache && (statusCode == 200)) {
var hash = crypto.createHash('md5');
// Put everything into the hash that could change and invalidate the data that
// the browser already stored. The headers the data and the encoding.
hash.update(data);
hash.update(JSON.stringify(headers));
if(encoding) {
hash.update(encoding);
}
var contentDigest = hash.digest("hex");
// RFC 7232 section 2.3 mandates for the etag to be enclosed in quotes
headers["Etag"] = '"' + contentDigest + '"';
headers["Cache-Control"] = "max-age=0, must-revalidate";
// Check if any of the hashes contained within the if-none-match header
// matches the current hash.
// If one matches, do not send the data but tell the browser to use the
// cached data.
// We do not implement "*" as it makes no sense here.
var ifNoneMatch = request.headers["if-none-match"];
if(ifNoneMatch) {
var matchParts = ifNoneMatch.split(",").map(function(etag) {
return etag.replace(/^[ "]+|[ "]+$/g, "");
});
if(matchParts.indexOf(contentDigest) != -1) {
response.writeHead(304,headers);
response.end();
return;
}
}
}
/*
If the gzip=yes is set, check if the user agent permits compression. If so,
compress our response if the raw data is bigger than 2k. Compressing less
data is inefficient. Note that we use the synchronous functions from zlib
to stay in the imperative style. The current `Server` doesn't depend on
this, and we may just as well use the async versions.
*/
if(this.enableGzip && (data.length > 2048)) {
var acceptEncoding = request.headers["accept-encoding"] || "";
if(/\bdeflate\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "deflate";
data = zlib.deflateSync(data);
} else if(/\bgzip\b/.test(acceptEncoding)) {
headers["Content-Encoding"] = "gzip";
data = zlib.gzipSync(data);
}
}

response.writeHead(statusCode,headers);
response.end(data,encoding);
}

Server.prototype.defaultVariables = {
port: "8080",
host: "127.0.0.1",
Expand All @@ -89,7 +158,8 @@ Server.prototype.defaultVariables = {
"system-tiddler-render-type": "text/plain",
"system-tiddler-render-template": "$:/core/templates/wikified-tiddler",
"debug-level": "none",
"gzip": "no"
"gzip": "no",
"use-browser-cache": "no"
};

Server.prototype.get = function(name) {
Expand Down Expand Up @@ -167,13 +237,14 @@ Server.prototype.requestHandler = function(request,response,options) {
state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
state.sendResponse = sendResponse.bind(self,request,response);
// Get the principals authorized to access this resource
var authorizationType = this.methodMappings[request.method] || "readers";
// Check for the CSRF header if this is a write
if(!this.csrfDisable && authorizationType === "writers" && request.headers["x-requested-with"] !== "TiddlyWiki") {
response.writeHead(403,"'X-Requested-With' header required to login to '" + this.servername + "'");
response.end();
return;
return;
}
// Check whether anonymous access is granted
state.allowAnon = this.isAuthorized(authorizationType,null);
Expand All @@ -182,7 +253,7 @@ Server.prototype.requestHandler = function(request,response,options) {
if(!this.authenticators[0].authenticateRequest(request,response,state)) {
// Bail if we failed (the authenticator will have sent the response)
return;
}
}
}
// Authorize with the authenticated username
if(!this.isAuthorized(authorizationType,state.authenticatedUsername)) {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/startup/favicon.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports.name = "favicon";
exports.platforms = ["browser"];
exports.after = ["startup"];
exports.synchronous = true;

// Favicon tiddler
var FAVICON_TITLE = "$:/favicon.ico";

Expand Down
3 changes: 3 additions & 0 deletions core/modules/startup/load-modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ exports.startup = function() {
if($tw.node) {
$tw.modules.applyMethods("utils-node",$tw.utils);
}
if($tw.browser) {
$tw.modules.applyMethods("utils-browser",$tw.utils);
}
$tw.modules.applyMethods("global",$tw);
$tw.modules.applyMethods("config",$tw.config);
$tw.Tiddler.fieldModules = $tw.modules.getModulesByTypeAsHashmap("tiddlerfield");
Expand Down
2 changes: 1 addition & 1 deletion core/modules/startup/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ exports.startup = function() {
var onlyThrottledTiddlersHaveChanged = true;
for(var title in changes) {
var tiddler = $tw.wiki.getTiddler(title);
if(!tiddler || !(tiddler.hasField("draft.of") || tiddler.hasField("throttle.refresh"))) {
if(!$tw.wiki.isVolatileTiddler(title) && (!tiddler || !(tiddler.hasField("draft.of") || tiddler.hasField("throttle.refresh")))) {
onlyThrottledTiddlersHaveChanged = false;
}
}
Expand Down
9 changes: 5 additions & 4 deletions core/modules/startup/rootwidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ exports.startup = function() {
});
$tw.rootWidget.addEventListener("tm-show-switcher",function(event) {
$tw.modal.display("$:/core/ui/SwitcherModal",{variables: event.paramObject, event: event});
});
});
// Install the notification mechanism
$tw.notifier = new $tw.utils.Notifier($tw.wiki);
$tw.rootWidget.addEventListener("tm-notify",function(event) {
Expand All @@ -40,9 +40,10 @@ exports.startup = function() {
// Install the tm-focus-selector message
$tw.rootWidget.addEventListener("tm-focus-selector",function(event) {
var selector = event.param || "",
element;
element,
doc = event.event && event.event.target ? event.event.target.ownerDocument : document;
try {
element = document.querySelector(selector);
element = doc.querySelector(selector);
} catch(e) {
console.log("Error in selector: ",selector)
}
Expand All @@ -68,7 +69,7 @@ exports.startup = function() {
fullScreenDocument[fullscreen._exitFullscreen]();
} else {
fullScreenDocument.documentElement[fullscreen._requestFullscreen](Element.ALLOW_KEYBOARD_INPUT);
}
}
}
});
}
Expand Down
5 changes: 2 additions & 3 deletions core/modules/startup/startup.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ var PERFORMANCE_INSTRUMENTATION_CONFIG_TITLE = "$:/config/Performance/Instrument
var widget = require("$:/core/modules/widgets/widget.js");

exports.startup = function() {
var modules,n,m,f;
// Minimal browser detection
if($tw.browser) {
$tw.browser.isIE = (/msie|trident/i.test(navigator.userAgent));
Expand Down Expand Up @@ -66,10 +65,10 @@ exports.startup = function() {
// Execute any startup actions
$tw.rootWidget.invokeActionsByTag("$:/tags/StartupAction");
if($tw.browser) {
$tw.rootWidget.invokeActionsByTag("$:/tags/StartupAction/Browser");
$tw.rootWidget.invokeActionsByTag("$:/tags/StartupAction/Browser");
}
if($tw.node) {
$tw.rootWidget.invokeActionsByTag("$:/tags/StartupAction/Node");
$tw.rootWidget.invokeActionsByTag("$:/tags/StartupAction/Node");
}
// Kick off the language manager and switcher
$tw.language = new $tw.Language();
Expand Down
10 changes: 5 additions & 5 deletions core/modules/startup/story.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ exports.startup = function() {
updateHistory: $tw.wiki.getTiddlerText(CONFIG_UPDATE_HISTORY,"no").trim(),
targetTiddler: event.param || event.tiddlerTitle,
copyToClipboard: $tw.wiki.getTiddlerText(CONFIG_PERMALINKVIEW_COPY_TO_CLIPBOARD,"yes").trim() === "yes" ? "permaview" : "none"
});
});
});
}
};
Expand All @@ -120,10 +120,10 @@ function openStartupTiddlers(options) {
var hash = $tw.locationHash.substr(1),
split = hash.indexOf(":");
if(split === -1) {
target = decodeURIComponent(hash.trim());
target = $tw.utils.decodeURIComponentSafe(hash.trim());
} else {
target = decodeURIComponent(hash.substr(0,split).trim());
storyFilter = decodeURIComponent(hash.substr(split + 1).trim());
target = $tw.utils.decodeURIComponentSafe(hash.substr(0,split).trim());
storyFilter = $tw.utils.decodeURIComponentSafe(hash.substr(split + 1).trim());
}
}
// If the story wasn't specified use the current tiddlers or a blank story
Expand Down Expand Up @@ -165,7 +165,7 @@ function openStartupTiddlers(options) {
story.addToHistory(target);
} else if(storyList.length > 0) {
story.addToHistory(storyList[0]);
}
}
}
}

Expand Down
10 changes: 5 additions & 5 deletions core/modules/storyviews/classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ ClassicStoryView.prototype.navigateTo = function(historyInfo) {
var listItemWidget = this.listWidget.children[listElementIndex],
targetElement = listItemWidget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
if(duration) {
// Scroll the node into view
this.listWidget.dispatchEvent({type: "tm-scroll", target: targetElement});
this.listWidget.dispatchEvent({type: "tm-scroll", target: targetElement});
} else {
targetElement.scrollIntoView();
}
Expand All @@ -43,7 +43,7 @@ ClassicStoryView.prototype.insert = function(widget) {
if(duration) {
var targetElement = widget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
// Get the current height of the tiddler
Expand Down Expand Up @@ -83,7 +83,7 @@ ClassicStoryView.prototype.remove = function(widget) {
widget.removeChildDomNodes();
};
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
removeElement();
return;
}
Expand Down Expand Up @@ -118,4 +118,4 @@ ClassicStoryView.prototype.remove = function(widget) {

exports.classic = ClassicStoryView;

})();
})();
6 changes: 3 additions & 3 deletions core/modules/storyviews/pop.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ PopStoryView.prototype.navigateTo = function(historyInfo) {
var listItemWidget = this.listWidget.children[listElementIndex],
targetElement = listItemWidget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
// Scroll the node into view
Expand All @@ -35,7 +35,7 @@ PopStoryView.prototype.insert = function(widget) {
var targetElement = widget.findFirstDomNode(),
duration = $tw.utils.getAnimationDuration();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
// Reset once the transition is over
Expand Down Expand Up @@ -77,7 +77,7 @@ PopStoryView.prototype.remove = function(widget) {
}
};
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
removeElement();
return;
}
Expand Down
6 changes: 3 additions & 3 deletions core/modules/storyviews/zoomin.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ ZoominListView.prototype.navigateTo = function(historyInfo) {
var listItemWidget = this.listWidget.children[listElementIndex],
targetElement = listItemWidget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
// Make the new tiddler be position absolute and visible so that we can measure it
Expand Down Expand Up @@ -130,7 +130,7 @@ function findTitleDomNode(widget,targetClass) {
ZoominListView.prototype.insert = function(widget) {
var targetElement = widget.findFirstDomNode();
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
return;
}
// Make the newly inserted node position absolute and hidden
Expand All @@ -147,7 +147,7 @@ ZoominListView.prototype.remove = function(widget) {
widget.removeChildDomNodes();
};
// Abandon if the list entry isn't a DOM element (it might be a text node)
if(!(targetElement instanceof Element)) {
if(!targetElement || targetElement.nodeType === Node.TEXT_NODE) {
removeElement();
return;
}
Expand Down
8 changes: 2 additions & 6 deletions core/modules/syncer.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ Syncer.prototype.getStatus = function(callback) {
// Get login status
this.syncadaptor.getStatus(function(err,isLoggedIn,username,isReadOnly,isAnonymous) {
if(err) {
self.logger.alert(err);
self.displayError("Get Status Error",err);
} else {
// Set the various status tiddlers
self.wiki.addTiddler({title: self.titleIsReadOnly,text: isReadOnly ? "yes" : "no"});
Expand Down Expand Up @@ -472,7 +472,7 @@ Syncer.prototype.handleLogoutEvent = function() {
if(this.syncadaptor.logout) {
this.syncadaptor.logout(function(err) {
if(err) {
self.logger.alert(err);
self.displayError("Logout Error",err);
} else {
self.getStatus();
}
Expand Down Expand Up @@ -633,10 +633,6 @@ DeleteTiddlerTask.prototype.run = function(callback) {
}
// Remove the info stored about this tiddler
delete self.syncer.tiddlerInfo[self.title];
if($tw.boot.files){
// Remove the tiddler from $tw.boot.files
delete $tw.boot.files[self.title];
}
// Invoke the callback
callback(null);
},{
Expand Down
11 changes: 5 additions & 6 deletions core/modules/upgraders/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Upgrader module that suppresses certain system tiddlers that shouldn't be import
/*global $tw: false */
"use strict";

var DONT_IMPORT_LIST = ["$:/StoryList","$:/HistoryList"],
DONT_IMPORT_PREFIX_LIST = ["$:/temp/","$:/state/","$:/Import"],
var DONT_IMPORT_LIST = ["$:/Import"],
UNSELECT_PREFIX_LIST = ["$:/temp/","$:/state/","$:/StoryList","$:/HistoryList"],
WARN_IMPORT_PREFIX_LIST = ["$:/core/modules/"];

exports.upgrade = function(wiki,titles,tiddlers) {
Expand All @@ -26,11 +26,10 @@ exports.upgrade = function(wiki,titles,tiddlers) {
tiddlers[title] = Object.create(null);
messages[title] = $tw.language.getString("Import/Upgrader/System/Suppressed");
} else {
for(var t=0; t<DONT_IMPORT_PREFIX_LIST.length; t++) {
var prefix = DONT_IMPORT_PREFIX_LIST[t];
for(var t=0; t<UNSELECT_PREFIX_LIST.length; t++) {
var prefix = UNSELECT_PREFIX_LIST[t];
if(title.substr(0,prefix.length) === prefix) {
tiddlers[title] = Object.create(null);
messages[title] = $tw.language.getString("Import/Upgrader/State/Suppressed");
messages[title] = $tw.language.getString("Import/Upgrader/Tiddler/Unselected");
}
}
for(var t=0; t<WARN_IMPORT_PREFIX_LIST.length; t++) {
Expand Down
2 changes: 1 addition & 1 deletion core/modules/utils/csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ exports.parseCsvStringWithHeader = function(text,options) {
var columnName = headings[column];
columnResult[columnName] = $tw.utils.trim(columns[column] || "");
}
results.push(columnResult);
results.push(columnResult);
}
return results;
}
Expand Down
8 changes: 6 additions & 2 deletions core/modules/utils/dom/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ exports.domContains = function(a,b) {
!!(a.compareDocumentPosition(b) & 16);
};

exports.domMatchesSelector = function(node,selector) {
return node.matches ? node.matches(selector) : node.msMatchesSelector(selector);
};

exports.removeChildren = function(node) {
while(node.hasChildNodes()) {
node.removeChild(node.firstChild);
Expand Down Expand Up @@ -65,7 +69,7 @@ Get the first parent element that has scrollbars or use the body as fallback.
*/
exports.getScrollContainer = function(el) {
var doc = el.ownerDocument;
while(el.parentNode) {
while(el.parentNode) {
el = el.parentNode;
if(el.scrollTop) {
return el;
Expand Down Expand Up @@ -204,7 +208,7 @@ exports.addEventListeners = function(domNode,events) {
if(eventInfo.handlerMethod) {
handler = function(event) {
eventInfo.handlerObject[eventInfo.handlerMethod].call(eventInfo.handlerObject,event);
};
};
} else {
handler = eventInfo.handlerObject;
}
Expand Down
25 changes: 20 additions & 5 deletions core/modules/utils/dom/dragndrop.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Browser data transfer utilities, used with the clipboard and drag and drop
Options:
domNode: dom node to make draggable
dragImageType: "pill" or "dom"
dragImageType: "pill", "blank" or "dom" (the default)
dragTiddlerFn: optional function to retrieve the title of tiddler to drag
dragFilterFn: optional function to retreive the filter defining a list of tiddlers to drag
widget: widget to use as the contect for the filter
Expand All @@ -27,7 +27,7 @@ exports.makeDraggable = function(options) {
domNode = options.domNode;
// Make the dom node draggable (not necessary for anchor tags)
if((domNode.tagName || "").toLowerCase() !== "a") {
domNode.setAttribute("draggable","true");
domNode.setAttribute("draggable","true");
}
// Add event handlers
$tw.utils.addEventListeners(domNode,[
Expand Down Expand Up @@ -73,14 +73,17 @@ exports.makeDraggable = function(options) {
if(dataTransfer.setDragImage) {
if(dragImageType === "pill") {
dataTransfer.setDragImage(dragImage.firstChild,-16,-16);
} else if (dragImageType === "blank") {
dragImage.removeChild(dragImage.firstChild);
dataTransfer.setDragImage(dragImage,0,0);
} else {
var r = domNode.getBoundingClientRect();
dataTransfer.setDragImage(domNode,event.clientX-r.left,event.clientY-r.top);
}
}
// Set up the data transfer
if(dataTransfer.clearData) {
dataTransfer.clearData();
dataTransfer.clearData();
}
var jsonData = [];
if(titles.length > 1) {
Expand Down Expand Up @@ -164,7 +167,7 @@ var importDataTypes = [
}},
{type: "URL", IECompatible: true, toTiddlerFieldsArray: function(data,fallbackTitle) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
var match = $tw.utils.decodeURIComponentSafe(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return parseJSONTiddlers(match[1],fallbackTitle);
} else {
Expand All @@ -173,7 +176,7 @@ var importDataTypes = [
}},
{type: "text/x-moz-url", IECompatible: false, toTiddlerFieldsArray: function(data,fallbackTitle) {
// Check for tiddler data URI
var match = decodeURIComponent(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
var match = $tw.utils.decodeURIComponentSafe(data).match(/^data\:text\/vnd\.tiddler,(.*)/i);
if(match) {
return parseJSONTiddlers(match[1],fallbackTitle);
} else {
Expand Down Expand Up @@ -205,4 +208,16 @@ function parseJSONTiddlers(json,fallbackTitle) {
return data;
};

exports.dragEventContainsFiles = function(event) {
if(event.dataTransfer.types) {
for(var i=0; i<event.dataTransfer.types.length; i++) {
if(event.dataTransfer.types[i] === "Files") {
return true;
break;
}
}
}
return false;
};

})();
19 changes: 18 additions & 1 deletion core/modules/utils/dom/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ exports.httpRequest = function(options) {
});
return result;
},
getHeader = function(targetHeader) {
return headers[targetHeader] || headers[targetHeader.toLowerCase()];
},
isSimpleRequest = function(type,headers) {
if(["GET","HEAD","POST"].indexOf(type) === -1) {
return false;
}
for(var header in headers) {
if(["accept","accept-language","content-language","content-type"].indexOf(header.toLowerCase()) === -1) {
return false;
}
}
if(hasHeader("Content-Type") && ["application/x-www-form-urlencoded","multipart/form-data","text/plain"].indexOf(getHeader["Content-Type"]) === -1) {
return false;
}
return true;
},
returnProp = options.returnProp || "responseText",
request = new XMLHttpRequest(),
data = "",
Expand Down Expand Up @@ -76,7 +93,7 @@ exports.httpRequest = function(options) {
if(data && !hasHeader("Content-Type")) {
request.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8");
}
if(!hasHeader("X-Requested-With")) {
if(!hasHeader("X-Requested-With") && !isSimpleRequest(type,headers)) {
request.setRequestHeader("X-Requested-With","TiddlyWiki");
}
try {
Expand Down
5 changes: 3 additions & 2 deletions core/modules/utils/dom/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Modal.prototype.display = function(title,options) {
options = options || {};
this.srcDocument = options.variables && (options.variables.rootwindow === "true" ||
options.variables.rootwindow === "yes") ? document :
(options.event.event && options.event.event.target ? options.event.event.target.ownerDocument : document);
(options.event && options.event.event && options.event.event.target ? options.event.event.target.ownerDocument : document);
this.srcWindow = this.srcDocument.defaultView;
var self = this,
refreshHandler,
Expand Down Expand Up @@ -105,7 +105,7 @@ Modal.prototype.display = function(title,options) {
parentWidget: $tw.rootWidget
});
navigatorWidgetNode.render(modalBody,null);

// Render the title of the message
var headerWidgetNode = this.wiki.makeTranscludeWidget(title,{
field: "subtitle",
Expand Down Expand Up @@ -243,6 +243,7 @@ Modal.prototype.adjustPageClass = function() {
if(windowContainer) {
$tw.utils.toggleClass(windowContainer,"tc-modal-displayed",this.modalCount > 0);
}
$tw.utils.toggleClass(this.srcDocument.body,"tc-modal-prevent-scroll",this.modalCount > 0);
};

exports.Modal = Modal;
Expand Down
2 changes: 1 addition & 1 deletion core/modules/utils/dom/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ Popup.prototype.show = function(options) {
}
// Add the click handler if we have any popups
if(this.popups.length > 0) {
this.rootElement.addEventListener("click",this,true);
this.rootElement.addEventListener("click",this,true);
}
};

Expand Down
6 changes: 3 additions & 3 deletions core/modules/utils/dom/scroller.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ PageScroller.prototype.handleEvent = function(event) {
if(event.paramObject && event.paramObject.selector) {
this.scrollSelectorIntoView(null,event.paramObject.selector);
} else {
this.scrollIntoView(event.target);
this.scrollIntoView(event.target);
}
return false; // Event was handled
}
Expand Down Expand Up @@ -103,7 +103,7 @@ PageScroller.prototype.scrollIntoView = function(element,callback) {
if(duration <= 0) {
t = 1;
} else {
t = ((Date.now()) - self.startTime) / duration;
t = ((Date.now()) - self.startTime) / duration;
}
if(t >= 1) {
self.cancelScroll(srcWindow);
Expand All @@ -126,7 +126,7 @@ PageScroller.prototype.scrollSelectorIntoView = function(baseElement,selector,ca
baseElement = baseElement || document.body;
var element = baseElement.querySelector(selector);
if(element) {
this.scrollIntoView(element,callback);
this.scrollIntoView(element,callback);
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,28 @@
/*\
title: $:/core/modules/startup/CSSescape.js
title: $:/core/modules/utils/escapecss.js
type: application/javascript
module-type: startup
module-type: utils
Polyfill for CSS.escape()
Provides CSS.escape() functionality.
\*/
(function(root,factory){
(function(){

/*jslint node: true, browser: true */
/*global $tw: false */
/*global $tw: false, window: false */
"use strict";

// Export name and synchronous status
exports.name = "css-escape";
exports.platforms = ["browser"];
exports.after = ["startup"];
exports.synchronous = true;

/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
// https://github.com/umdjs/umd/blob/master/returnExports.js
exports.startup = factory(root);
}(typeof global != 'undefined' ? global : this, function(root) {

if (root.CSS && root.CSS.escape) {
return;
exports.escapeCSS = (function() {
// use browser's native CSS.escape() function if available
if ($tw.browser && window.CSS && window.CSS.escape) {
return window.CSS.escape;
}

// https://drafts.csswg.org/cssom/#serialize-an-identifier
var cssEscape = function(value) {
// otherwise, a utility method is provided
// see also https://drafts.csswg.org/cssom/#serialize-an-identifier

/*! https://mths.be/cssescape v1.5.1 by @mathias | MIT license */
return function(value) {
if (arguments.length == 0) {
throw new TypeError('`CSS.escape` requires an argument.');
}
Expand Down Expand Up @@ -104,11 +99,6 @@ exports.startup = factory(root);
}
return result;
};
})();

if (!root.CSS) {
root.CSS = {};
}

root.CSS.escape = cssEscape;

}));
})();
2 changes: 1 addition & 1 deletion core/modules/utils/fakedom.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ Object.defineProperty(TW_Element.prototype, "innerHTML", {
if(node instanceof TW_Element) {
b.push(node.outerHTML);
} else if(node instanceof TW_TextNode) {
b.push($tw.utils.htmlEncode(node.textContent));
b.push($tw.utils.htmlTextEncode(node.textContent));
}
});
return b.join("");
Expand Down
141 changes: 89 additions & 52 deletions core/modules/utils/filesystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,13 @@ Options include:
extFilters: optional array of filters to be used to generate the base path
wiki: optional wiki for evaluating the pathFilters,
fileInfo: an existing fileInfo to check against
originalpath: a preferred filepath if no pathFilters match
*/
exports.generateTiddlerFileInfo = function(tiddler,options) {
var fileInfo = {}, metaExt;
// Propagate the isEditableFile flag
if(options.fileInfo) {
fileInfo.isEditableFile = options.fileInfo.isEditableFile || false;
if(options.fileInfo && !!options.fileInfo.isEditableFile) {
fileInfo.isEditableFile = true;
fileInfo.originalpath = options.fileInfo.originalpath;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
Expand All @@ -228,6 +228,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
hasUnsafeFields = hasUnsafeFields || /[\x00-\x1F]/mg.test(value);
hasUnsafeFields = hasUnsafeFields || ($tw.utils.trim(value) !== value);
}
hasUnsafeFields = hasUnsafeFields || /:/mg.test(fieldName);
});
// Check for field values
if(hasUnsafeFields) {
Expand All @@ -247,12 +248,12 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.hasMetaFile = true;
}
if(options.extFilters) {
// Check for extension override
// Check for extension overrides
metaExt = $tw.utils.generateTiddlerExtension(tiddler.fields.title,{
extFilters: options.extFilters,
wiki: options.wiki
});
if(metaExt){
if(metaExt) {
if(metaExt === ".tid") {
// Overriding to the .tid extension needs special handling
fileInfo.type = "application/x-tiddler";
Expand All @@ -279,8 +280,7 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
directory: options.directory,
pathFilters: options.pathFilters,
wiki: options.wiki,
fileInfo: options.fileInfo,
originalpath: options.originalpath
fileInfo: options.fileInfo
});
return fileInfo;
};
Expand All @@ -292,8 +292,7 @@ Options include:
wiki: optional wiki for evaluating the extFilters
*/
exports.generateTiddlerExtension = function(title,options) {
var self = this,
extension;
var extension;
// Check if any of the extFilters applies
if(options.extFilters && options.wiki) {
$tw.utils.each(options.extFilters,function(filter) {
Expand All @@ -319,11 +318,10 @@ Options include:
fileInfo: an existing fileInfo object to check against
*/
exports.generateTiddlerFilepath = function(title,options) {
var self = this,
directory = options.directory || "",
var directory = options.directory || "",
extension = options.extension || "",
originalpath = options.originalpath || "",
filepath;
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
filepath;
// Check if any of the pathFilters applies
if(options.pathFilters && options.wiki) {
$tw.utils.each(options.pathFilters,function(filter) {
Expand All @@ -336,34 +334,46 @@ exports.generateTiddlerFilepath = function(title,options) {
}
});
}
if(!filepath && originalpath !== "") {
if(!filepath && !!originalpath) {
//Use the originalpath without the extension
var ext = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - ext.length);
} else if(!filepath) {
filepath = title;
// If the filepath already ends in the extension then remove it
if(filepath.substring(filepath.length - extension.length) === extension) {
filepath = filepath.substring(0,filepath.length - extension.length);
}
// Remove any forward or backward slashes so we don't create directories
filepath = filepath.replace(/\/|\\/g,"_");
}
//If the path does not start with "." or ".." and a path seperator, then
// Replace any Windows control codes
filepath = filepath.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
// Replace any leading spaces with the same number of underscores
filepath = filepath.replace(/^ +/,function (u) { return u.replace(/ /g, "_")});
//If the path does not start with "." or ".." && a path seperator, then
if(!/^\.{1,2}[/\\]/g.test(filepath)) {
// Don't let the filename start with any dots because such files are invisible on *nix
filepath = filepath.replace(/^\.+/g,"_");
filepath = filepath.replace(/^\.+/g,function (u) { return u.replace(/\./g, "_")});
}
// Remove any characters that can't be used in cross-platform filenames
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any dots or spaces at the end of the extension with the same number of underscores
extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_")});
// Truncate the extension if it is too long
if(extension.length > 32) {
extension = extension.substr(0,32);
}
// If the filepath already ends in the extension then remove it
if(filepath.substring(filepath.length - extension.length) === extension) {
filepath = filepath.substring(0,filepath.length - extension.length);
}
// Truncate the filename if it is too long
if(filepath.length > 200) {
filepath = filepath.substr(0,200);
}
// If the resulting filename is blank (eg because the title is just punctuation characters)
if(!filepath) {
// If the resulting filename is blank (eg because the title is just punctuation)
if(!filepath || /^_+$/g.test(filepath)) {
// ...then just use the character codes of the title
filepath = "";
filepath = "";
$tw.utils.each(title.split(""),function(char) {
if(filepath) {
filepath += "-";
Expand All @@ -382,22 +392,21 @@ exports.generateTiddlerFilepath = function(title,options) {
count++;
} while(fs.existsSync(fullPath));
// If the last write failed with an error, or if path does not start with:
// the resolved options.directory, the resolved wikiPath directory, or the wikiTiddlersPath directory,
// then encodeURIComponent() and resolve to tiddler directory
var newPath = fullPath,
// the resolved options.directory, the resolved wikiPath directory, the wikiTiddlersPath directory,
// or the 'originalpath' directory, then encodeURIComponent() and resolve to tiddler directory.
var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath,fullPath),
encode = (options.fileInfo || {writeError: false}).writeError == true;
if(!encode){
encode = !(fullPath.indexOf(path.resolve(directory)) == 0 ||
fullPath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
fullPath.indexOf($tw.boot.wikiTiddlersPath) == 0);
if(!encode) {
encode = !(writePath.indexOf($tw.boot.wikiTiddlersPath) == 0 ||
writePath.indexOf(path.resolve(directory)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiPath)) == 0 ||
writePath.indexOf(path.resolve($tw.boot.wikiTiddlersPath,originalpath)) == 0 );
}
if(encode){
fullPath = path.resolve(directory, encodeURIComponent(fullPath));
if(encode) {
writePath = path.resolve(directory,encodeURIComponent(fullPath));
}
// Call hook to allow plugins to modify the final path
fullPath = $tw.hooks.invokeHook("th-make-tiddler-path", newPath, fullPath);
// Return the full path to the file
return fullPath;
return writePath;
};

/*
Expand All @@ -411,18 +420,33 @@ exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
fs.writeFile(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding,function(err) {
fs.writeFile(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding,function(err) {
if(err) {
return callback(err);
}
fs.writeFile(fileInfo.filepath + ".meta",tiddler.getFieldStringBlock({exclude: ["text","bag"]}),"utf8",callback);
fs.writeFile(fileInfo.filepath + ".meta",tiddler.getFieldStringBlock({exclude: ["text","bag"]}),"utf8",function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
});
} else {
// Save the tiddler as a self contained templated file
if(fileInfo.type === "application/x-tiddler") {
fs.writeFile(fileInfo.filepath,tiddler.getFieldStringBlock({exclude: ["text","bag"]}) + (!!tiddler.fields.text ? "\n\n" + tiddler.fields.text : ""),"utf8",callback);
fs.writeFile(fileInfo.filepath,tiddler.getFieldStringBlock({exclude: ["text","bag"]}) + (!!tiddler.fields.text ? "\n\n" + tiddler.fields.text : ""),"utf8",function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
} else {
fs.writeFile(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8",callback);
fs.writeFile(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8",function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
}
}
};
Expand All @@ -438,7 +462,7 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
if(fileInfo.hasMetaFile) {
// Save the tiddler as a separate body and meta file
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text,typeInfo.encoding);
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding);
fs.writeFileSync(fileInfo.filepath + ".meta",tiddler.getFieldStringBlock({exclude: ["text","bag"]}),"utf8");
} else {
// Save the tiddler as a self contained templated file
Expand All @@ -448,31 +472,44 @@ exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
fs.writeFileSync(fileInfo.filepath,JSON.stringify([tiddler.getFieldStrings({exclude: ["bag"]})],null,$tw.config.preferences.jsonSpaces),"utf8");
}
}
return fileInfo;
};

/*
Delete a file described by the fileInfo if it exits
*/
exports.deleteTiddlerFile = function(fileInfo, callback) {
exports.deleteTiddlerFile = function(fileInfo,callback) {
//Only attempt to delete files that exist on disk
if(!fileInfo.filepath || !fs.existsSync(fileInfo.filepath)) {
return callback(null);
//For some reason, the tiddler is only in memory or we can't modify the file at this path
$tw.syncer.displayError("Server deleteTiddlerFile task failed for filepath: "+fileInfo.filepath);
return callback(null,fileInfo);
}
// Delete the file
fs.unlink(fileInfo.filepath,function(err) {
if(err) {
return callback(err);
}
}
// Delete the metafile if present
if(fileInfo.hasMetaFile && fs.existsSync(fileInfo.filepath + ".meta")) {
fs.unlink(fileInfo.filepath + ".meta",function(err) {
if(err) {
return callback(err);
}
return $tw.utils.deleteEmptyDirs(path.dirname(fileInfo.filepath),callback);
return $tw.utils.deleteEmptyDirs(path.dirname(fileInfo.filepath),function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
});
} else {
return $tw.utils.deleteEmptyDirs(path.dirname(fileInfo.filepath),callback);
return $tw.utils.deleteEmptyDirs(path.dirname(fileInfo.filepath),function(err) {
if(err) {
return callback(err);
}
return callback(null,fileInfo);
});
}
});
};
Expand All @@ -482,25 +519,25 @@ Cleanup old files on disk, by comparing the options values:
adaptorInfo from $tw.syncer.tiddlerInfo
bootInfo from $tw.boot.files
*/
exports.cleanupTiddlerFiles = function(options, callback) {
exports.cleanupTiddlerFiles = function(options,callback) {
var adaptorInfo = options.adaptorInfo || {},
bootInfo = options.bootInfo || {},
title = options.title || "undefined";
if(adaptorInfo.filepath && bootInfo.filepath && adaptorInfo.filepath !== bootInfo.filepath) {
return $tw.utils.deleteTiddlerFile(adaptorInfo, function(err){
$tw.utils.deleteTiddlerFile(adaptorInfo,function(err) {
if(err) {
if ((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "unlink") {
// Error deleting the previous file on disk, should fail gracefully
$tw.syncer.displayError("Server desynchronized. Error cleaning up previous file for tiddler: "+title, err);
return callback(null);
$tw.syncer.displayError("Server desynchronized. Error cleaning up previous file for tiddler: \""+title+"\"",err);
return callback(null,bootInfo);
} else {
return callback(err);
}
}
return callback(null);
return callback(null,bootInfo);
});
} else {
return callback(null);
return callback(null,bootInfo);
}
};

Expand Down
200 changes: 143 additions & 57 deletions core/modules/utils/linked-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,105 +14,191 @@ function LinkedList() {
};

LinkedList.prototype.clear = function() {
this.index = Object.create(null);
// LinkedList performs the duty of both the head and tail node
this.next = this;
this.prev = this;
this.next = Object.create(null);
this.prev = Object.create(null);
this.first = undefined;
this.last = undefined;
this.length = 0;
};

LinkedList.prototype.remove = function(value) {
if($tw.utils.isArray(value)) {
for(var t=0; t<value.length; t++) {
_assertString(value[t]);
}
for(var t=0; t<value.length; t++) {
_removeOne(this,value[t]);
}
} else {
_assertString(value);
_removeOne(this,value);
}
};

/*
Push behaves like array.push and accepts multiple string arguments. But it also
accepts a single array argument too, to be consistent with its other methods.
*/
LinkedList.prototype.push = function(/* values */) {
for(var i = 0; i < arguments.length; i++) {
var value = arguments[i];
var node = {value: value};
var preexistingNode = this.index[value];
_linkToEnd(this,node);
if(preexistingNode) {
// We want to keep pointing to the first instance, but we want
// to have that instance (or chain of instances) point to the
// new one.
while (preexistingNode.copy) {
preexistingNode = preexistingNode.copy;
}
preexistingNode.copy = node;
} else {
this.index[value] = node;
}
var values = arguments;
if($tw.utils.isArray(values[0])) {
values = values[0];
}
for(var i = 0; i < values.length; i++) {
_assertString(values[i]);
}
for(var i = 0; i < values.length; i++) {
_linkToEnd(this,values[i]);
}
return this.length;
};

LinkedList.prototype.pushTop = function(value) {
if($tw.utils.isArray(value)) {
for (var t=0; t<value.length; t++) {
_assertString(value[t]);
}
for(var t=0; t<value.length; t++) {
_removeOne(this,value[t]);
}
this.push.apply(this,value);
} else {
var node = _removeOne(this,value);
if(!node) {
node = {value: value};
this.index[value] = node;
} else {
// Put this node at the end of the copy chain.
var preexistingNode = node;
while(preexistingNode.copy) {
preexistingNode = preexistingNode.copy;
}
// The order of these three statements is important,
// because sometimes preexistingNode == node.
preexistingNode.copy = node;
this.index[value] = node.copy;
node.copy = undefined;
for(var t=0; t<value.length; t++) {
_linkToEnd(this,value[t]);
}
_linkToEnd(this,node);
} else {
_assertString(value);
_removeOne(this,value);
_linkToEnd(this,value);
}
};

LinkedList.prototype.each = function(callback) {
for(var ptr = this.next; ptr !== this; ptr = ptr.next) {
callback(ptr.value);
var visits = Object.create(null),
value = this.first;
while(value !== undefined) {
callback(value);
var next = this.next[value];
if(typeof next === "object") {
var i = visits[value] || 0;
visits[value] = i+1;
value = next[i];
} else {
value = next;
}
}
};

LinkedList.prototype.toArray = function() {
var output = [];
for(var ptr = this.next; ptr !== this; ptr = ptr.next) {
output.push(ptr.value);
}
var output = new Array(this.length),
index = 0;
this.each(function(value) { output[index++] = value; });
return output;
};

LinkedList.prototype.makeTiddlerIterator = function(wiki) {
var self = this;
return function(callback) {
self.each(function(title) {
callback(wiki.getTiddler(title),title);
});
};
};

function _removeOne(list,value) {
var node = list.index[value];
if(node) {
node.prev.next = node.next;
node.next.prev = node.prev;
list.length -= 1;
// Point index to the next instance of the same value, maybe nothing.
list.index[value] = node.copy;
var prevEntry = list.prev[value],
nextEntry = list.next[value],
prev = prevEntry,
next = nextEntry;
if(typeof nextEntry === "object") {
next = nextEntry[0];
prev = prevEntry[0];
}
return node;
// Relink preceding element.
if(list.first === value) {
list.first = next
} else if(prev !== undefined) {
if(typeof list.next[prev] === "object") {
if(next === undefined) {
// Must have been last, and 'i' would be last element.
list.next[prev].pop();
} else {
var i = list.next[prev].indexOf(value);
list.next[prev][i] = next;
}
} else {
list.next[prev] = next;
}
} else {
return;
}
// Now relink following element
// Check "next !== undefined" rather than "list.last === value" because
// we need to know if the FIRST value is the last in the list, not the last.
if(next !== undefined) {
if(typeof list.prev[next] === "object") {
if(prev === undefined) {
// Must have been first, and 'i' would be 0.
list.prev[next].shift();
} else {
var i = list.prev[next].indexOf(value);
list.prev[next][i] = prev;
}
} else {
list.prev[next] = prev;
}
} else {
list.last = prev;
}
// Delink actual value. If it uses arrays, just remove first entries.
if(typeof nextEntry === "object") {
nextEntry.shift();
prevEntry.shift();
} else {
list.next[value] = undefined;
list.prev[value] = undefined;
}
list.length -= 1;
};

function _linkToEnd(list,node) {
// Sticks the given node onto the end of the list.
list.prev.next = node;
node.prev = list.prev;
list.prev = node;
node.next = list;
// Sticks the given node onto the end of the list.
function _linkToEnd(list,value) {
if(list.first === undefined) {
list.first = value;
} else {
// Does it already exists?
if(list.first === value || list.prev[value] !== undefined) {
if(typeof list.next[value] === "string") {
list.next[value] = [list.next[value]];
list.prev[value] = [list.prev[value]];
} else if(typeof list.next[value] === "undefined") {
// list.next[value] must be undefined.
// Special case. List already has 1 value. It's at the end.
list.next[value] = [];
list.prev[value] = [list.prev[value]];
}
list.prev[value].push(list.last);
// We do NOT append a new value onto "next" list. Iteration will
// figure out it must point to End-of-List on its own.
} else {
list.prev[value] = list.last;
}
// Make the old last point to this new one.
if(typeof list.next[list.last] === "object") {
list.next[list.last].push(value);
} else {
list.next[list.last] = value;
}
}
list.last = value;
list.length += 1;
};

function _assertString(value) {
if(typeof value !== "string") {
throw "Linked List only accepts string values, not " + value;
}
};

exports.LinkedList = LinkedList;

})();
4 changes: 2 additions & 2 deletions core/modules/utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Logger.prototype.log = function(/* args */) {
self.saveBufferLogger.buffer += " " + arg;
});
this.saveBufferLogger.buffer += "\n";
this.saveBufferLogger.buffer = this.saveBufferLogger.buffer.slice(-this.saveBufferLogger.saveLimit);
this.saveBufferLogger.buffer = this.saveBufferLogger.buffer.slice(-this.saveBufferLogger.saveLimit);
}
if(console !== undefined && console.log !== undefined) {
return Function.apply.call(console.log, console, [$tw.utils.terminalColour(this.colour),this.componentName + ":"].concat(Array.prototype.slice.call(arguments,0)).concat($tw.utils.terminalColour()));
Expand Down Expand Up @@ -111,7 +111,7 @@ Logger.prototype.alert = function(/* args */) {
} else {
// Print an orange message to the console if not in the browser
console.error("\x1b[1;33m" + text + "\x1b[0m");
}
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion core/modules/utils/performance.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function Performance(enabled) {

Performance.prototype.showGreeting = function() {
if($tw.browser) {
this.logger.log("Execute $tw.perf.log(); to see filter execution timings");
this.logger.log("Execute $tw.perf.log(); to see filter execution timings");
}
};

Expand Down
9 changes: 8 additions & 1 deletion core/modules/utils/transliterate.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ exports.transliterationPairs = {
"Nj":"N",
"Ñ":"N",
"NJ":"NJ",
"ð":"d",
"Ð":"D",
"Ó":"O",
"Ŏ":"O",
"Ǒ":"O",
Expand Down Expand Up @@ -265,6 +267,8 @@ exports.transliterationPairs = {
"Ɽ":"R",
"Ꜿ":"C",
"Ǝ":"E",
"ß":"ss",
"ẞ":"SS",
"Ś":"S",
"Ṥ":"S",
"Š":"S",
Expand All @@ -275,6 +279,8 @@ exports.transliterationPairs = {
"Ṡ":"S",
"Ṣ":"S",
"Ṩ":"S",
"þ": "th",
"Þ": "TH",
"Ť":"T",
"Ţ":"T",
"Ṱ":"T",
Expand Down Expand Up @@ -907,7 +913,8 @@ exports.transliterationPairs = {
"т":"t",
"ь":"'",
"б":"b",
"ю":"yu"
"ю":"yu",
"…":"..."
};

exports.transliterate = function(str) {
Expand Down
106 changes: 98 additions & 8 deletions core/modules/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Convert a string to title case (ie capitalise each initial letter)
exports.toTitleCase = function(str) {
return (str || "").replace(/(^|\s)\S/g, function(c) {return c.toUpperCase();});
}

/*
Find the line break preceding a given position in a string
Returns position immediately after that line break, or the start of the string
Expand Down Expand Up @@ -223,6 +223,7 @@ exports.removeArrayEntries = function(array,value) {
array.splice(p,1);
}
}
return array;
};

/*
Expand Down Expand Up @@ -294,6 +295,47 @@ exports.slowInSlowOut = function(t) {
return (1 - ((Math.cos(t * Math.PI) + 1) / 2));
};

exports.formatTitleString = function(template,options) {
var base = options.base || "",
separator = options.separator || "",
counter = options.counter || "";
var result = "",
t = template,
matches = [
[/^\$basename\$/i, function() {
return base;
}],
[/^\$count:(\d+)\$/i, function(match) {
return $tw.utils.pad(counter,match[1]);
}],
[/^\$separator\$/i, function() {
return separator;
}],
[/^\$count\$/i, function() {
return counter + "";
}]
];
while(t.length){
var matchString = "";
$tw.utils.each(matches, function(m) {
var match = m[0].exec(t);
if(match) {
matchString = m[1].call(null,match);
t = t.substr(match[0].length);
return false;
}
});
if(matchString) {
result += matchString;
} else {
result += t.charAt(0);
t = t.substr(1);
}
}
result = result.replace(/\\(.)/g,"$1");
return result;
};

exports.formatDateString = function(date,template) {
var result = "",
t = template,
Expand Down Expand Up @@ -341,6 +383,15 @@ exports.formatDateString = function(date,template) {
[/^0WW/, function() {
return $tw.utils.pad($tw.utils.getWeek(date));
}],
[/^0ddddd/, function() {
return $tw.utils.pad(Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24),3);
}],
[/^ddddd/, function() {
return Math.floor((date - new Date(date.getFullYear(), 0, 0)) / 1000 / 60 / 60 / 24);
}],
[/^dddd/, function() {
return [7,1,2,3,4,5,6][date.getDay()];
}],
[/^ddd/, function() {
return $tw.language.getString("Date/Short/Day/" + date.getDay());
}],
Expand Down Expand Up @@ -515,6 +566,15 @@ exports.htmlEncode = function(s) {
}
};

// Converts like htmlEncode, but forgets the double quote for brevity
exports.htmlTextEncode = function(s) {
if(s) {
return s.toString().replace(/&/mg,"&amp;").replace(/</mg,"&lt;").replace(/>/mg,"&gt;");
} else {
return "";
}
};

// Converts all HTML entities to their character equivalents
exports.entityDecode = function(s) {
var converter = String.fromCodePoint || String.fromCharCode,
Expand Down Expand Up @@ -618,7 +678,7 @@ exports.nextTick = function(fn) {
/*global window: false */
if(typeof process === "undefined") {
// Apparently it would be faster to use postMessage - http://dbaron.org/log/20100309-faster-timeouts
window.setTimeout(fn,4);
window.setTimeout(fn,0);
} else {
process.nextTick(fn);
}
Expand Down Expand Up @@ -690,9 +750,8 @@ exports.isValidFieldName = function(name) {
if(!name || typeof name !== "string") {
return false;
}
name = name.toLowerCase().trim();
var fieldValidatorRegEx = /^[a-z0-9\-\._]+$/mg;
return fieldValidatorRegEx.test(name);
// Since v5.2.x, there are no restrictions on characters in field names
return name;
};

/*
Expand Down Expand Up @@ -792,7 +851,7 @@ exports.makeDataUri = function(text,type,_canonical_uri) {
parts.push(type);
parts.push(isBase64 ? ";base64" : "");
parts.push(",");
parts.push(isBase64 ? text : encodeURIComponent(text));
parts.push(isBase64 ? text : encodeURIComponent(text));
}
return parts.join("");
};
Expand Down Expand Up @@ -867,7 +926,9 @@ exports.stringifyNumber = function(num) {

exports.makeCompareFunction = function(type,options) {
options = options || {};
var gt = options.invert ? -1 : +1,
// set isCaseSensitive to true if not defined in options
var isCaseSensitive = (options.isCaseSensitive === false) ? false : true,
gt = options.invert ? -1 : +1,
lt = options.invert ? +1 : -1,
compare = function(a,b) {
if(a > b) {
Expand All @@ -886,7 +947,11 @@ exports.makeCompareFunction = function(type,options) {
return compare($tw.utils.parseInt(a),$tw.utils.parseInt(b));
},
"string": function(a,b) {
return compare("" + a,"" +b);
if(!isCaseSensitive) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return compare("" + a,"" + b);
},
"date": function(a,b) {
var dateA = $tw.utils.parseDate(a),
Expand All @@ -901,9 +966,34 @@ exports.makeCompareFunction = function(type,options) {
},
"version": function(a,b) {
return $tw.utils.compareVersions(a,b);
},
"alphanumeric": function(a,b) {
if(!isCaseSensitive) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return options.invert ? b.localeCompare(a,undefined,{numeric: true,sensitivity: "base"}) : a.localeCompare(b,undefined,{numeric: true,sensitivity: "base"});
}
};
return (types[type] || types[options.defaultType] || types.number);
};

exports.decodeURIComponentSafe = function(str) {
var value = str;
try {
value = decodeURIComponent(str);
} catch(e) {
}
return value;
};

exports.decodeURISafe = function(str) {
var value = str;
try {
value = decodeURI(str);
} catch(e) {
}
return value;
};

})();
7 changes: 4 additions & 3 deletions core/modules/widgets/action-confirm.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ Invoke the action associated with this widget
*/
ConfirmWidget.prototype.invokeAction = function(triggeringWidget,event) {
var invokeActions = true,
handled = true;
handled = true,
win = event && event.event && event.event.view ? event.event.view : window;
if(this.prompt) {
invokeActions = confirm(this.message);
invokeActions = win.confirm(this.message);
}
if(invokeActions) {
handled = this.invokeActions(triggeringWidget,event);
Expand All @@ -74,4 +75,4 @@ ConfirmWidget.prototype.allowActionPropagation = function() {

exports["action-confirm"] = ConfirmWidget;

})();
})();
17 changes: 12 additions & 5 deletions core/modules/widgets/action-createtiddler.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ CreateTiddlerWidget.prototype = new Widget();
Render this widget into the DOM
*/
CreateTiddlerWidget.prototype.render = function(parent,nextSibling) {
this.parentDomNode = parent;
this.computeAttributes();
this.execute();
// Render children
this.renderChildren(parent,nextSibling);
};

/*
Expand All @@ -44,7 +47,8 @@ CreateTiddlerWidget.prototype.execute = function() {
this.actionTemplate = this.getAttribute("$template");
this.useTemplate = !!this.actionTemplate;
this.actionOverwrite = this.getAttribute("$overwrite","no");

// Construct the child widgets
this.makeChildWidgets();
};

/*
Expand Down Expand Up @@ -86,18 +90,21 @@ CreateTiddlerWidget.prototype.invokeAction = function(triggeringWidget,event) {
if (!this.hasBase && this.useTemplate) {
title = this.wiki.generateNewTitle(this.actionTemplate);
} else if (!this.hasBase && !this.useTemplate) {
// If NO $basetitle AND NO $template use initial title
// DON'T overwrite any stuff
// If no $basetitle and no $template then use initial title
title = this.wiki.generateNewTitle(title);
}
var templateTiddler = this.wiki.getTiddler(this.actionTemplate) || {};
var tiddler = this.wiki.addTiddler(new $tw.Tiddler(templateTiddler.fields,creationFields,fields,modificationFields,{title: title}));
this.wiki.addTiddler(new $tw.Tiddler(templateTiddler.fields,creationFields,fields,modificationFields,{title: title}));
var draftTitle = this.wiki.generateDraftTitle(title);
if(this.actionSaveTitle) {
this.wiki.setTextReference(this.actionSaveTitle,title,this.getVariable("currentTiddler"));
}
if(this.actionSaveDraftTitle) {
this.wiki.setTextReference(this.actionSaveDraftTitle,this.wiki.generateDraftTitle(title),this.getVariable("currentTiddler"));
this.wiki.setTextReference(this.actionSaveDraftTitle,draftTitle,this.getVariable("currentTiddler"));
}
this.setVariable("createTiddler-title",title);
this.setVariable("createTiddler-draftTitle",draftTitle);
this.refreshChildren();
return true; // Action was invoked
};

Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/action-deletefield.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ DeleteFieldWidget.prototype.invokeAction = function(triggeringWidget,event) {
}
});
if(hasChanged) {
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,removeFields,this.wiki.getModificationFields()));
this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,removeFields,this.wiki.getModificationFields()));
}
}
return true; // Action was invoked
Expand Down
17 changes: 6 additions & 11 deletions core/modules/widgets/action-listops.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ ActionListopsWidget.prototype.execute = function() {
*/
ActionListopsWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.$tiddler || changedAttributes.$filter ||
changedAttributes.$subfilter || changedAttributes.$field ||
changedAttributes.$index || changedAttributes.$tags) {
if($tw.utils.count(changedAttributes) > 0) {
this.refreshSelf();
return true;
}
Expand All @@ -60,12 +58,10 @@ ActionListopsWidget.prototype.invokeAction = function(triggeringWidget,
//Apply the specified filters to the lists
var field = this.listField,
index,
type = "!!",
list = this.listField;
if(this.listIndex) {
field = undefined;
index = this.listIndex;
type = "##";
list = this.listIndex;
}
if(this.filter) {
Expand All @@ -74,18 +70,17 @@ ActionListopsWidget.prototype.invokeAction = function(triggeringWidget,
.filterTiddlers(this.filter, this)));
}
if(this.subfilter) {
var subfilter = "[list[" + this.target + type + list + "]] " + this.subfilter;
this.wiki.setText(this.target, field, index, $tw.utils.stringifyList(
this.wiki
.filterTiddlers(subfilter, this)));
var inputList = this.wiki.getTiddlerList(this.target,field,index),
subfilter = $tw.utils.stringifyList(inputList) + " " + this.subfilter;
this.wiki.setText(this.target, field, index, $tw.utils.stringifyList(this.wiki.filterTiddlers(subfilter,this)));
}
if(this.filtertags) {
var tiddler = this.wiki.getTiddler(this.target),
oldtags = tiddler ? (tiddler.fields.tags || []).slice(0) : [],
tagfilter = "[list[" + this.target + "!!tags]] " + this.filtertags,
tagfilter = $tw.utils.stringifyList(oldtags) + " " + this.filtertags,
newtags = this.wiki.filterTiddlers(tagfilter,this);
if($tw.utils.stringifyList(oldtags.sort()) !== $tw.utils.stringifyList(newtags.sort())) {
this.wiki.setText(this.target,"tags",undefined,$tw.utils.stringifyList(newtags));
this.wiki.setText(this.target,"tags",undefined,$tw.utils.stringifyList(newtags));
}
}
return true; // Action was invoked
Expand Down
6 changes: 3 additions & 3 deletions core/modules/widgets/action-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@ LogWidget.prototype.log = function() {
$tw.utils.each(this.attributes,function(attribute,name) {
if(name.substring(0,2) !== "$$") {
data[name] = attribute;
}
}
});

for(var v in this.variables) {
allVars[v] = this.getVariable(v,{defaultValue:""});
}
}
if(this.filter) {
filteredVars = this.wiki.compileFilter(this.filter).call(this.wiki,this.wiki.makeTiddlerIterator(allVars));
$tw.utils.each(filteredVars,function(name) {
data[name] = allVars[name];
});
});
}
dataCount = $tw.utils.count(data);

Expand Down
13 changes: 12 additions & 1 deletion core/modules/widgets/action-navigate.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,18 @@ NavigateWidget.prototype.invokeAction = function(triggeringWidget,event) {
navigateFromNode: triggeringWidget,
navigateFromClientRect: bounds && { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
},
navigateSuppressNavigation: suppressNavigation
navigateFromClientTop: bounds && bounds.top,
navigateFromClientLeft: bounds && bounds.left,
navigateFromClientWidth: bounds && bounds.width,
navigateFromClientRight: bounds && bounds.right,
navigateFromClientBottom: bounds && bounds.bottom,
navigateFromClientHeight: bounds && bounds.height,
navigateSuppressNavigation: suppressNavigation,
metaKey: event.metaKey,
ctrlKey: event.ctrlKey,
altKey: event.altKey,
shiftKey: event.shiftKey,
event: event
});
return true; // Action was invoked
};
Expand Down
4 changes: 3 additions & 1 deletion core/modules/widgets/action-popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Compute the internal state of the widget
ActionPopupWidget.prototype.execute = function() {
this.actionState = this.getAttribute("$state");
this.actionCoords = this.getAttribute("$coords");
this.floating = this.getAttribute("$floating","no") === "yes";
};

/*
Expand Down Expand Up @@ -68,7 +69,8 @@ ActionPopupWidget.prototype.invokeAction = function(triggeringWidget,event) {
height: parseFloat(match[4])
},
title: this.actionState,
wiki: this.wiki
wiki: this.wiki,
floating: this.floating
});
} else {
$tw.popup.cancel(0);
Expand Down
5 changes: 4 additions & 1 deletion core/modules/widgets/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ ButtonWidget.prototype.render = function(parent,nextSibling) {
if(this["aria-label"]) {
domNode.setAttribute("aria-label",this["aria-label"]);
}
if(this.popup || this.popupTitle) {
domNode.setAttribute("aria-expanded",isPoppedUp ? "true" : "false");
}
// Set the tabindex
if(this.tabIndex) {
domNode.setAttribute("tabindex",this.tabIndex);
Expand Down Expand Up @@ -224,7 +227,7 @@ ButtonWidget.prototype.execute = function() {
ButtonWidget.prototype.updateDomNodeClasses = function() {
var domNodeClasses = this.domNode.className.split(" "),
oldClasses = this.class.split(" "),
newClasses;
newClasses;
this["class"] = this.getAttribute("class","");
newClasses = this.class.split(" ");
//Remove classes assigned from the old value of class attribute
Expand Down
2 changes: 2 additions & 0 deletions core/modules/widgets/draggable.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ DraggableWidget.prototype.render = function(parent,nextSibling) {
dragFilterFn: function() {return self.getAttribute("filter");},
startActions: self.startActions,
endActions: self.endActions,
dragImageType: self.dragImageType,
widget: this
});
// Insert the link into the DOM and render any children
Expand All @@ -71,6 +72,7 @@ DraggableWidget.prototype.execute = function() {
this.draggableClasses = this.getAttribute("class");
this.startActions = this.getAttribute("startactions");
this.endActions = this.getAttribute("endactions");
this.dragImageType = this.getAttribute("dragimagetype");
// Make the child widgets
this.makeChildWidgets();
};
Expand Down
4 changes: 2 additions & 2 deletions core/modules/widgets/droppable.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ DroppableWidget.prototype.render = function(parent,nextSibling) {
{name: "dragover", handlerObject: this, handlerMethod: "handleDragOverEvent"},
{name: "dragleave", handlerObject: this, handlerMethod: "handleDragLeaveEvent"},
{name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"}
]);
]);
} else {
$tw.utils.addClass(this.domNode,this.disabledClass);
}
Expand Down Expand Up @@ -155,7 +155,7 @@ DroppableWidget.prototype.execute = function() {
DroppableWidget.prototype.assignDomNodeClasses = function() {
var classes = this.getAttribute("class","").split(" ");
classes.push("tc-droppable");
this.domNode.className = classes.join(" ");
this.domNode.className = classes.join(" ");
};

/*
Expand Down
144 changes: 120 additions & 24 deletions core/modules/widgets/dropzone.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Dropzone widget
/*global $tw: false */
"use strict";

var IMPORT_TITLE = "$:/Import";

var Widget = require("$:/core/modules/widgets/widget.js").widget;

var DropZoneWidget = function(parseTreeNode,options) {
Expand All @@ -35,6 +37,7 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
this.execute();
// Create element
var domNode = this.document.createElement("div");
this.domNode = domNode;
domNode.className = this.dropzoneClass || "tc-dropzone";
// Add event handlers
if(this.dropzoneEnable) {
Expand All @@ -45,10 +48,8 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
{name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"},
{name: "paste", handlerObject: this, handlerMethod: "handlePasteEvent"},
{name: "dragend", handlerObject: this, handlerMethod: "handleDragEndEvent"}
]);
]);
}
domNode.addEventListener("click",function (event) {
},false);
// Insert element
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
Expand All @@ -57,12 +58,46 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
this.currentlyEntered = [];
};

// Handler for transient event listeners added when the dropzone has an active drag in progress
DropZoneWidget.prototype.handleEvent = function(event) {
if(event.type === "click") {
if(this.currentlyEntered.length) {
this.resetState();
}
} else if(event.type === "dragenter") {
if(event.target && event.target !== this.domNode && !$tw.utils.domContains(this.domNode,event.target)) {
this.resetState();
}
} else if(event.type === "dragleave") {
// Check if drag left the window
if(event.relatedTarget === null || (event.relatedTarget && event.relatedTarget.nodeName === "HTML")) {
this.resetState();
}
}
};

// Reset the state of the dropzone after a drag has ended
DropZoneWidget.prototype.resetState = function() {
$tw.utils.removeClass(this.domNode,"tc-dragover");
this.currentlyEntered = [];
this.document.body.removeEventListener("click",this,true);
this.document.body.removeEventListener("dragenter",this,true);
this.document.body.removeEventListener("dragleave",this,true);
this.dragInProgress = false;
};

DropZoneWidget.prototype.enterDrag = function(event) {
if(this.currentlyEntered.indexOf(event.target) === -1) {
this.currentlyEntered.push(event.target);
}
// If we're entering for the first time we need to apply highlighting
$tw.utils.addClass(this.domNodes[0],"tc-dragover");
if(!this.dragInProgress) {
this.dragInProgress = true;
// If we're entering for the first time we need to apply highlighting
$tw.utils.addClass(this.domNodes[0],"tc-dragover");
this.document.body.addEventListener("click",this,true);
this.document.body.addEventListener("dragenter",this,true);
this.document.body.addEventListener("dragleave",this,true);
}
};

DropZoneWidget.prototype.leaveDrag = function(event) {
Expand All @@ -72,15 +107,17 @@ DropZoneWidget.prototype.leaveDrag = function(event) {
}
// Remove highlighting if we're leaving externally
if(this.currentlyEntered.length === 0) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
}
};

DropZoneWidget.prototype.handleDragEnterEvent = function(event) {
// Check for this window being the source of the drag
if($tw.dragInProgress) {
return false;
}
if(this.filesOnly && !$tw.utils.dragEventContainsFiles(event)) {
return false;
}
this.enterDrag(event);
// Tell the browser that we're ready to handle the drop
event.preventDefault();
Expand All @@ -99,21 +136,52 @@ DropZoneWidget.prototype.handleDragOverEvent = function(event) {
}
// Tell the browser that we're still interested in the drop
event.preventDefault();
event.dataTransfer.dropEffect = "copy"; // Explicitly show this is a copy
// Check if this is a synthetic event, IE does not allow accessing dropEffect outside of original event handler
if(event.isTrusted) {
event.dataTransfer.dropEffect = "copy"; // Explicitly show this is a copy
}
};

DropZoneWidget.prototype.handleDragLeaveEvent = function(event) {
this.leaveDrag(event);
};

DropZoneWidget.prototype.handleDragEndEvent = function(event) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
};

DropZoneWidget.prototype.filterByContentTypes = function(tiddlerFieldsArray) {
var filteredTypes,
filtered = [],
types = [];
$tw.utils.each(tiddlerFieldsArray,function(tiddlerFields) {
types.push(tiddlerFields.type || "");
});
filteredTypes = this.wiki.filterTiddlers(this.contentTypesFilter,this,this.wiki.makeTiddlerIterator(types));
$tw.utils.each(tiddlerFieldsArray,function(tiddlerFields) {
if(filteredTypes.indexOf(tiddlerFields.type) !== -1) {
filtered.push(tiddlerFields);
}
});
return filtered;
};

DropZoneWidget.prototype.readFileCallback = function(tiddlerFieldsArray) {
if(this.contentTypesFilter) {
tiddlerFieldsArray = this.filterByContentTypes(tiddlerFieldsArray);
}
if(tiddlerFieldsArray.length) {
this.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: this.autoOpenOnImport, importTitle: this.importTitle});
if(this.actions) {
this.invokeActionString(this.actions,this,event,{importTitle: this.importTitle});
}
}
};

DropZoneWidget.prototype.handleDropEvent = function(event) {
var self = this,
readFileCallback = function(tiddlerFieldsArray) {
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
self.readFileCallback(tiddlerFieldsArray);
};
this.leaveDrag(event);
// Check for being over a TEXTAREA or INPUT
Expand All @@ -127,7 +195,7 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
var self = this,
dataTransfer = event.dataTransfer;
// Remove highlighting
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
this.resetState();
// Import any files in the drop
var numFiles = 0;
if(dataTransfer.files) {
Expand All @@ -138,7 +206,23 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
}
// Try to import the various data types we understand
if(numFiles === 0) {
$tw.utils.importDataTransfer(dataTransfer,this.wiki.generateNewTitle("Untitled"),readFileCallback);
var fallbackTitle = self.wiki.generateNewTitle("Untitled");
//Use the deserializer specified if any
if(this.dropzoneDeserializer) {
for(var t= 0; t<dataTransfer.items.length; t++) {
var item = dataTransfer.items[t];
if(item.kind === "string") {
item.getAsString(function(str){
var tiddlerFields = self.wiki.deserializeTiddlers(null,str,{title: fallbackTitle},{deserializer:self.dropzoneDeserializer});
if(tiddlerFields && tiddlerFields.length) {
readFileCallback(tiddlerFields);
}
})
}
}
} else {
$tw.utils.importDataTransfer(dataTransfer,fallbackTitle,readFileCallback);
}
}
// Tell the browser that we handled the drop
event.preventDefault();
Expand All @@ -149,7 +233,7 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
DropZoneWidget.prototype.handlePasteEvent = function(event) {
var self = this,
readFileCallback = function(tiddlerFieldsArray) {
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
self.readFileCallback(tiddlerFieldsArray);
};
// Let the browser handle it if we're in a textarea or input box
if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) == -1 && !event.target.isContentEditable) {
Expand All @@ -166,17 +250,26 @@ DropZoneWidget.prototype.handlePasteEvent = function(event) {
});
} else if(item.kind === "string") {
// Create tiddlers from string items
var type = item.type;
var tiddlerFields,
type = item.type;
item.getAsString(function(str) {
var tiddlerFields = {
title: self.wiki.generateNewTitle("Untitled"),
text: str,
type: type
};
if($tw.log.IMPORT) {
console.log("Importing string '" + str + "', type: '" + type + "'");
// Use the deserializer specified if any
if(self.dropzoneDeserializer) {
tiddlerFields = self.wiki.deserializeTiddlers(null,str,{title: self.wiki.generateNewTitle("Untitled")},{deserializer:self.dropzoneDeserializer});
if(tiddlerFields && tiddlerFields.length) {
readFileCallback(tiddlerFields);
}
} else {
tiddlerFields = {
title: self.wiki.generateNewTitle("Untitled"),
text: str,
type: type
};
if($tw.log.IMPORT) {
console.log("Importing string '" + str + "', type: '" + type + "'");
}
readFileCallback([tiddlerFields]);
}
self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify([tiddlerFields]), autoOpenOnImport: self.autoOpenOnImport, importTitle: self.importTitle});
});
}
}
Expand All @@ -194,7 +287,10 @@ DropZoneWidget.prototype.execute = function() {
this.dropzoneDeserializer = this.getAttribute("deserializer");
this.dropzoneEnable = (this.getAttribute("enable") || "yes") === "yes";
this.autoOpenOnImport = this.getAttribute("autoOpenOnImport");
this.importTitle = this.getAttribute("importTitle");
this.importTitle = this.getAttribute("importTitle",IMPORT_TITLE);
this.actions = this.getAttribute("actions");
this.contentTypesFilter = this.getAttribute("contentTypesFilter");
this.filesOnly = this.getAttribute("filesOnly","no") === "yes";
// Make child widgets
this.makeChildWidgets();
};
Expand All @@ -204,7 +300,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
DropZoneWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.enable || changedAttributes.autoOpenOnImport || changedAttributes.importTitle || changedAttributes.deserializer || changedAttributes.class) {
if($tw.utils.count(changedAttributes) > 0) {
this.refreshSelf();
return true;
}
Expand Down
12 changes: 6 additions & 6 deletions core/modules/widgets/edit-bitmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,6 @@ var LINE_WIDTH_TITLE = "$:/config/BitmapEditor/LineWidth",
var Widget = require("$:/core/modules/widgets/widget.js").widget;

var EditBitmapWidget = function(parseTreeNode,options) {
// Initialise the editor operations if they've not been done already
if(!this.editorOperations) {
EditBitmapWidget.prototype.editorOperations = {};
$tw.modules.applyMethods("bitmapeditoroperation",this.editorOperations);
}
this.initialise(parseTreeNode,options);
};

Expand All @@ -43,6 +38,11 @@ Render this widget into the DOM
*/
EditBitmapWidget.prototype.render = function(parent,nextSibling) {
var self = this;
// Initialise the editor operations if they've not been done already
if(!this.editorOperations) {
EditBitmapWidget.prototype.editorOperations = {};
$tw.modules.applyMethods("bitmapeditoroperation",this.editorOperations);
}
// Save the parent dom node
this.parentDomNode = parent;
// Compute our attributes
Expand Down Expand Up @@ -156,7 +156,7 @@ EditBitmapWidget.prototype.loadCanvas = function() {
};
// Get the current bitmap into an image object
if(tiddler && tiddler.fields.type && tiddler.fields.text) {
currImage.src = "data:" + tiddler.fields.type + ";base64," + tiddler.fields.text;
currImage.src = "data:" + tiddler.fields.type + ";base64," + tiddler.fields.text;
} else {
currImage.width = DEFAULT_IMAGE_WIDTH;
currImage.height = DEFAULT_IMAGE_HEIGHT;
Expand Down
6 changes: 3 additions & 3 deletions core/modules/widgets/edit-shortcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ EditShortcutWidget.prototype.render = function(parent,nextSibling) {
this.inputNode = this.document.createElement("input");
// Assign classes
if(this.shortcutClass) {
this.inputNode.className = this.shortcutClass;
this.inputNode.className = this.shortcutClass;
}
// Assign other attributes
if(this.shortcutStyle) {
Expand Down Expand Up @@ -117,7 +117,7 @@ EditShortcutWidget.prototype.handleKeydownEvent = function(event) {
// Ignore the keydown if it was already handled
event.preventDefault();
event.stopPropagation();
return true;
return true;
} else {
return false;
}
Expand Down Expand Up @@ -145,7 +145,7 @@ EditShortcutWidget.prototype.refresh = function(changedTiddlers) {
this.updateInputNode();
return true;
} else {
return false;
return false;
}
};

Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ ElementWidget.prototype.refresh = function(changedTiddlers) {
if(hasChangedAttributes) {
if(!this.isReplaced) {
// Update our attributes
this.assignAttributes(this.domNodes[0],{excludeEventAttributes: true});
this.assignAttributes(this.domNodes[0],{excludeEventAttributes: true});
} else {
// If we were replaced then completely refresh ourselves
return this.refreshSelf();
Expand Down
2 changes: 1 addition & 1 deletion core/modules/widgets/entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ EntityWidget.prototype.refresh = function(changedTiddlers) {
this.refreshSelf();
return true;
} else {
return false;
return false;
}
};

Expand Down
85 changes: 55 additions & 30 deletions core/modules/widgets/eventcatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,48 +37,62 @@ EventWidget.prototype.render = function(parent,nextSibling) {
var tag = this.parseTreeNode.isBlock ? "div" : "span";
if(this.elementTag && $tw.config.htmlUnsafeElements.indexOf(this.elementTag) === -1) {
tag = this.elementTag;
}
}
var domNode = this.document.createElement(tag);
this.domNode = domNode;
// Assign classes
this.assignDomNodeClasses();
this.assignDomNodeClasses();
// Add our event handler
$tw.utils.each(this.types,function(type) {
domNode.addEventListener(type,function(event) {
var selector = self.getAttribute("selector"),
actions = self.getAttribute("actions-"+type),
actions = self.getAttribute("$"+type) || self.getAttribute("actions-"+type),
stopPropagation = self.getAttribute("stopPropagation","onaction"),
selectedNode = event.target,
selectedNodeRect,
catcherNodeRect,
variables = {};
// Firefox can fire dragover and dragenter events on text nodes instead of their parents
if(selectedNode.nodeType === 3) {
selectedNode = selectedNode.parentNode;
}
if(selector) {
// Search ancestors for a node that matches the selector
while(!selectedNode.matches(selector) && selectedNode !== domNode) {
while(!$tw.utils.domMatchesSelector(selectedNode,selector) && selectedNode !== domNode) {
selectedNode = selectedNode.parentNode;
}
// If we found one, copy the attributes as variables, otherwise exit
if(selectedNode.matches(selector)) {
$tw.utils.each(selectedNode.attributes,function(attribute) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
//Add a variable with a popup coordinate string for the selected node
variables["tv-popup-coords"] = "(" + selectedNode.offsetLeft + "," + selectedNode.offsetTop +"," + selectedNode.offsetWidth + "," + selectedNode.offsetHeight + ")";

//Add variables for offset of selected node
variables["tv-selectednode-posx"] = selectedNode.offsetLeft.toString();
variables["tv-selectednode-posy"] = selectedNode.offsetTop.toString();
variables["tv-selectednode-width"] = selectedNode.offsetWidth.toString();
variables["tv-selectednode-height"] = selectedNode.offsetHeight.toString();
if($tw.utils.domMatchesSelector(selectedNode,selector)) {
// Only set up variables if we have actions to invoke
if(actions) {
$tw.utils.each(selectedNode.attributes,function(attribute) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
//Add a variable with a popup coordinate string for the selected node
variables["tv-popup-coords"] = "(" + selectedNode.offsetLeft + "," + selectedNode.offsetTop +"," + selectedNode.offsetWidth + "," + selectedNode.offsetHeight + ")";

//Add variables for event X and Y position relative to selected node
selectedNodeRect = selectedNode.getBoundingClientRect();
variables["event-fromselected-posx"] = (event.clientX - selectedNodeRect.left).toString();
variables["event-fromselected-posy"] = (event.clientY - selectedNodeRect.top).toString();
//Add variables for offset of selected node
variables["tv-selectednode-posx"] = selectedNode.offsetLeft.toString();
variables["tv-selectednode-posy"] = selectedNode.offsetTop.toString();
variables["tv-selectednode-width"] = selectedNode.offsetWidth.toString();
variables["tv-selectednode-height"] = selectedNode.offsetHeight.toString();

if(event.clientX && event.clientY) {
//Add variables for event X and Y position relative to selected node
selectedNodeRect = selectedNode.getBoundingClientRect();
variables["event-fromselected-posx"] = (event.clientX - selectedNodeRect.left).toString();
variables["event-fromselected-posy"] = (event.clientY - selectedNodeRect.top).toString();

//Add variables for event X and Y position relative to event catcher node
catcherNodeRect = self.domNode.getBoundingClientRect();
variables["event-fromcatcher-posx"] = (event.clientX - catcherNodeRect.left).toString();
variables["event-fromcatcher-posy"] = (event.clientY - catcherNodeRect.top).toString();
//Add variables for event X and Y position relative to event catcher node
catcherNodeRect = self.domNode.getBoundingClientRect();
variables["event-fromcatcher-posx"] = (event.clientX - catcherNodeRect.left).toString();
variables["event-fromcatcher-posy"] = (event.clientY - catcherNodeRect.top).toString();

//Add variables for event X and Y position relative to the viewport
variables["event-fromviewport-posx"] = event.clientX.toString();
variables["event-fromviewport-posy"] = event.clientY.toString();
}
}
} else {
return false;
}
Expand Down Expand Up @@ -106,6 +120,8 @@ EventWidget.prototype.render = function(parent,nextSibling) {
variables["event-detail"] = event.detail.toString();
}
self.invokeActionString(actions,self,event,variables);
}
if((actions && stopPropagation === "onaction") || stopPropagation === "always") {
event.preventDefault();
event.stopPropagation();
return true;
Expand All @@ -125,7 +141,15 @@ Compute the internal state of the widget
EventWidget.prototype.execute = function() {
var self = this;
// Get attributes that require a refresh on change
this.types = this.getAttribute("events","").split(" ");
this.types = [];
$tw.utils.each(this.attributes,function(value,key) {
if(key.charAt(0) === "$") {
self.types.push(key.slice(1));
}
});
if(!this.types.length) {
this.types = this.getAttribute("events","").split(" ");
}
this.elementTag = this.getAttribute("tag");
// Make child widgets
this.makeChildWidgets();
Expand All @@ -134,19 +158,20 @@ EventWidget.prototype.execute = function() {
EventWidget.prototype.assignDomNodeClasses = function() {
var classes = this.getAttribute("class","").split(" ");
classes.push("tc-eventcatcher");
this.domNode.className = classes.join(" ");
this.domNode.className = classes.join(" ");
};

/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
*/
EventWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes["events"] || changedAttributes["tag"]) {
var changedAttributes = this.computeAttributes(),
changedAttributesCount = $tw.utils.count(changedAttributes);
if(changedAttributesCount === 1 && changedAttributes["class"]) {
this.assignDomNodeClasses();
} else if(changedAttributesCount > 0) {
this.refreshSelf();
return true;
} else if(changedAttributes["class"]) {
this.assignDomNodeClasses();
}
return this.refreshChildren(changedTiddlers);
};
Expand Down
Loading