Merge branch 'master' into external-tasks

This commit is contained in:
Jeremy Ruston 2021-10-27 12:27:35 +01:00 committed by GitHub
commit 979f079c7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
697 changed files with 12718 additions and 4772 deletions

View File

@ -5,7 +5,7 @@
# Default to the current version number for building the plugin library
if [ -z "$TW5_BUILD_VERSION" ]; then
TW5_BUILD_VERSION=v5.1.24
TW5_BUILD_VERSION=v5.2.0
fi
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"

View File

@ -5,52 +5,52 @@ Optimise the SVGs in ./core/images using SVGO from https://github.com/svg/svgo
Install SVGO with the following command in the root of the repo:
npm install svgo
npm install svgo@2.3.0
*/
"use strict";
var fs = require("fs"),
path = require("path"),
SVGO = require("svgo"),
svgo = new SVGO({
{ optimize } = require("svgo"),
config = {
plugins: [
{cleanupAttrs: true},
{removeDoctype: true},
{removeXMLProcInst: true},
{removeComments: true},
{removeMetadata: true},
{removeTitle: true},
{removeDesc: true},
{removeUselessDefs: true},
{removeEditorsNSData: true},
{removeEmptyAttrs: true},
{removeHiddenElems: true},
{removeEmptyText: true},
{removeEmptyContainers: true},
{removeViewBox: false},
{cleanupEnableBackground: true},
{convertStyleToAttrs: true},
{convertColors: true},
{convertPathData: true},
{convertTransform: true},
{removeUnknownsAndDefaults: true},
{removeNonInheritableGroupAttrs: true},
{removeUselessStrokeAndFill: true},
{removeUnusedNS: true},
{cleanupIDs: true},
{cleanupNumericValues: true},
{moveElemsAttrsToGroup: true},
{moveGroupAttrsToElems: true},
{collapseGroups: true},
{removeRasterImages: false},
{mergePaths: true},
{convertShapeToPath: true},
{sortAttrs: true},
{removeDimensions: false},
{removeAttrs: {attrs: "(stroke|fill)"}}
'cleanupAttrs',
'removeDoctype',
'removeXMLProcInst',
'removeComments',
'removeMetadata',
'removeTitle',
'removeDesc',
'removeUselessDefs',
'removeEditorsNSData',
'removeEmptyAttrs',
'removeHiddenElems',
'removeEmptyText',
'removeEmptyContainers',
// 'removeViewBox',
'cleanupEnableBackground',
'convertStyleToAttrs',
'convertColors',
'convertPathData',
'convertTransform',
'removeUnknownsAndDefaults',
'removeNonInheritableGroupAttrs',
'removeUselessStrokeAndFill',
'removeUnusedNS',
'cleanupIDs',
'cleanupNumericValues',
'moveElemsAttrsToGroup',
'moveGroupAttrsToElems',
'collapseGroups',
// 'removeRasterImages',
'mergePaths',
'convertShapeToPath',
'sortAttrs',
//'removeDimensions',
{name: 'removeAttrs', params: { attrs: '(stroke|fill)' } }
]
});
};
var basepath = "./core/images/",
files = fs.readdirSync(basepath).sort();
@ -66,12 +66,14 @@ files.forEach(function(filename) {
fakeSVG = body.join("\n");
// A hack to make the new-journal-button work
fakeSVG = fakeSVG.replace("<<now \"DD\">>","&lt;&lt;now &quot;DD&quot;&gt;&gt;");
svgo.optimize(fakeSVG, {path: filepath}).then(function(result) {
config.path = filepath;
var result = optimize(fakeSVG,config);
if(result) {
var newSVG = header.join("\n") + "\n\n" + result.data.replace("&lt;&lt;now &quot;DD&quot;&gt;&gt;","<<now \"DD\">>");
fs.writeFileSync(filepath,newSVG);
},function(err) {
} else {
console.log("Error " + err + " with " + filename)
process.exit();
});
};
}
});

View File

@ -256,6 +256,28 @@ $tw.utils.deepDefaults = function(object /*, sourceObjectList */) {
return object;
};
/*
Convert a URIComponent encoded string to a string safely
*/
$tw.utils.decodeURIComponentSafe = function(s) {
var v = s;
try {
v = decodeURIComponent(s);
} catch(e) {}
return v;
};
/*
Convert a URI encoded string to a string safely
*/
$tw.utils.decodeURISafe = function(s) {
var v = s;
try {
v = decodeURI(s);
} catch(e) {}
return v;
};
/*
Convert "&amp;" to &, "&nbsp;" to nbsp, "&lt;" to <, "&gt;" to > and "&quot;" to "
*/
@ -1154,7 +1176,7 @@ $tw.Wiki = function(options) {
var index = tiddlerTitles.indexOf(title);
if(index !== -1) {
tiddlerTitles.splice(index,1);
}
}
}
// Record the new tiddler state
updateDescriptor["new"] = {
@ -1212,8 +1234,12 @@ $tw.Wiki = function(options) {
index,titlesLength,title;
for(index = 0, titlesLength = titles.length; index < titlesLength; index++) {
title = titles[index];
var shadowInfo = shadowTiddlers[title];
callback(shadowInfo.tiddler,title);
if(tiddlers[title]) {
callback(tiddlers[title],title);
} else {
var shadowInfo = shadowTiddlers[title];
callback(shadowInfo.tiddler,title);
}
}
};
@ -1301,7 +1327,7 @@ $tw.Wiki = function(options) {
}
} else {
if(pluginInfo[title]) {
delete pluginInfo[title];
delete pluginInfo[title];
results.deletedPlugins.push(title);
}
}
@ -1602,8 +1628,8 @@ $tw.modules.define("$:/boot/tiddlerdeserializer/json","tiddlerdeserializer",{
}
for(var f in data) {
if($tw.utils.hop(data,f)) {
// Check field name doesn't contain whitespace or control characters
if(typeof(data[f]) !== "string" || /[\x00-\x1F\s]/.test(f)) {
// Check field name doesn't contain control characters
if(typeof(data[f]) !== "string" || /[\x00-\x1F]/.test(f)) {
return false;
}
}
@ -1618,7 +1644,12 @@ $tw.modules.define("$:/boot/tiddlerdeserializer/json","tiddlerdeserializer",{
}
return true;
},
data = JSON.parse(text);
data = {};
try {
data = JSON.parse(text);
} catch(e) {
// Ignore JSON parse errors
}
if($tw.utils.isArray(data) && isTiddlerArrayValid(data)) {
return data;
} else if(isTiddlerValid(data)) {
@ -1724,13 +1755,20 @@ $tw.modules.define("$:/boot/tiddlerdeserializer/dom","tiddlerdeserializer",{
},
t,result = [];
if(node) {
for(t = 0; t < node.childNodes.length; t++) {
var type = (node.getAttribute && node.getAttribute("type")) || null;
if(type) {
// A new-style container with an explicit deserialization type
result = $tw.wiki.deserializeTiddlers(type,node.textContent);
} else {
// An old-style container of classic DIV-based tiddlers
for(t = 0; t < node.childNodes.length; t++) {
var childNode = node.childNodes[t],
tiddlers = extractTextTiddlers(childNode);
tiddlers = tiddlers || extractModuleTiddlers(childNode);
if(tiddlers) {
result.push.apply(result,tiddlers);
}
}
}
}
return result;
@ -1739,17 +1777,23 @@ $tw.modules.define("$:/boot/tiddlerdeserializer/dom","tiddlerdeserializer",{
$tw.loadTiddlersBrowser = function() {
// In the browser, we load tiddlers from certain elements
var containerIds = [
"libraryModules",
"modules",
"bootKernelPrefix",
"bootKernel",
"styleArea",
"storeArea",
"systemArea"
var containerSelectors = [
// IDs for old-style v5.1.x tiddler stores
"#libraryModules",
"#modules",
"#bootKernelPrefix",
"#bootKernel",
"#styleArea",
"#storeArea",
"#systemArea",
// Classes for new-style v5.2.x JSON tiddler stores
"script.tiddlywiki-tiddler-store"
];
for(var t=0; t<containerIds.length; t++) {
$tw.wiki.addTiddlers($tw.wiki.deserializeTiddlers("(DOM)",document.getElementById(containerIds[t])));
for(var t=0; t<containerSelectors.length; t++) {
var nodes = document.querySelectorAll(containerSelectors[t]);
for(var n=0; n<nodes.length; n++) {
$tw.wiki.addTiddlers($tw.wiki.deserializeTiddlers("(DOM)",nodes[n]));
}
}
};
@ -1872,13 +1916,13 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
value = path.basename(filename);
break;
case "filename-uri-decoded":
value = decodeURIComponent(path.basename(filename));
value = $tw.utils.decodeURIComponentSafe(path.basename(filename));
break;
case "basename":
value = path.basename(filename,path.extname(filename));
break;
case "basename-uri-decoded":
value = decodeURIComponent(path.basename(filename,path.extname(filename)));
value = $tw.utils.decodeURIComponentSafe(path.basename(filename,path.extname(filename)));
break;
case "extname":
value = path.extname(filename);
@ -1906,6 +1950,20 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
tiddlers.push({tiddlers: fileTiddlers});
}
};
// Helper to recursively search subdirectories
var getAllFiles = function(dirPath, recurse, arrayOfFiles) {
recurse = recurse || false;
arrayOfFiles = arrayOfFiles || [];
var files = fs.readdirSync(dirPath);
files.forEach(function(file) {
if (recurse && fs.statSync(dirPath + path.sep + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + path.sep + file, recurse, arrayOfFiles);
} else if(fs.statSync(dirPath + path.sep + file).isFile()){
arrayOfFiles.push(path.join(dirPath, path.sep, file));
}
});
return arrayOfFiles;
}
// Process the listed tiddlers
$tw.utils.each(filesInfo.tiddlers,function(tidInfo) {
if(tidInfo.prefix && tidInfo.suffix) {
@ -1929,18 +1987,19 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
// Process directory specifier
var dirPath = path.resolve(filepath,dirSpec.path);
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
var files = fs.readdirSync(dirPath),
var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
metaRegExp = /^.*\.meta$/;
for(var t=0; t<files.length; t++) {
var filename = files[t];
var thisPath = path.relative(filepath, files[t]),
filename = path.basename(thisPath);
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
processFile(dirPath + path.sep + filename,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile);
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile);
}
}
} else {
console.log("Warning: a directory in a tiddlywiki.files file does not exist.");
console.log("dirPath: " + dirPath);
console.log("dirPath: " + dirPath);
console.log("tiddlywiki.files location: " + filepath);
}
}
@ -1985,7 +2044,7 @@ $tw.loadPluginFolder = function(filepath,excludeRegExp) {
pluginInfo.dependents = pluginInfo.dependents || [];
pluginInfo.type = "application/json";
// Set plugin text
pluginInfo.text = JSON.stringify({tiddlers: pluginInfo.tiddlers},null,4);
pluginInfo.text = JSON.stringify({tiddlers: pluginInfo.tiddlers});
delete pluginInfo.tiddlers;
// Deserialise array fields (currently required for the dependents field)
for(var field in pluginInfo) {
@ -2199,7 +2258,7 @@ $tw.loadTiddlersNode = function() {
type = parts[0];
if(parts.length === 3 && ["plugins","themes","languages"].indexOf(type) !== -1) {
$tw.loadPlugins([parts[1] + "/" + parts[2]],$tw.config[type + "Path"],$tw.config[type + "EnvVar"]);
}
}
}
});
// Load the tiddlers from the wiki directory
@ -2467,16 +2526,29 @@ $tw.boot.executeNextStartupTask = function(callback) {
};
/*
Returns true if we are running on one platforms specified in a task modules `platforms` array
Returns true if we are running on one of the platforms specified in taskModule's
`platforms` array; or if `platforms` property is not defined.
*/
$tw.boot.doesTaskMatchPlatform = function(taskModule) {
var platforms = taskModule.platforms;
if(platforms) {
for(var t=0; t<platforms.length; t++) {
if((platforms[t] === "browser" && !$tw.browser) || (platforms[t] === "node" && !$tw.node)) {
return false;
switch (platforms[t]) {
case "browser":
if ($tw.browser) {
return true;
}
break;
case "node":
if ($tw.node) {
return true;
}
break;
default:
$tw.utils.error("Module " + taskModule.name + ": '" + platforms[t] + "' in export.platforms invalid");
}
}
return false;
}
return true;
};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
title: $:/core/images/minus-button
tags: $:/tags/Image
<svg width="22pt" height="22pt" class="tc-image-minus-button tc-image-button" viewBox="0 0 128 128"><path d="M64 0c35.346 0 64 28.654 64 64 0 35.346-28.654 64-64 64-35.346 0-64-28.654-64-64C0 28.654 28.654 0 64 0zm.332 16c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48z"/><rect width="80" height="16" x="24" y="56" rx="8"/></svg>

View File

@ -0,0 +1,4 @@
title: $:/core/images/plus-button
tags: $:/tags/Image
<svg width="22pt" height="22pt" class="tc-image-plus-button tc-image-button" viewBox="0 0 128 128"><path d="M64-.333c35.346 0 64 28.654 64 64 0 35.346-28.654 64-64 64-35.346 0-64-28.654-64-64 0-35.346 28.654-64 64-64zM64 16c-26.51 0-48 21.49-48 48s21.49 48 48 48 48-21.49 48-48-21.49-48-48-48z"/><rect width="80" height="16" x="24" y="56" rx="8"/><rect width="16" height="80" x="56" y="24" rx="8"/></svg>

View File

@ -123,12 +123,12 @@ Saving/TiddlySpot/BackupDir: Backup Directory
Saving/TiddlySpot/ControlPanel: ~TiddlySpot Control Panel
Saving/TiddlySpot/Backups: Backups
Saving/TiddlySpot/Caption: ~TiddlySpot Saver
Saving/TiddlySpot/Description: These settings are only used when saving to http://tiddlyspot.com or a compatible remote server
Saving/TiddlySpot/Description: These settings are only used when saving to [[TiddlySpot|http://tiddlyspot.com]], [[TiddlyHost|https://tiddlyhost.com]], or a compatible remote server. See [[here|https://github.com/simonbaird/tiddlyhost/wiki/TiddlySpot-Saver-configuration-for-Tiddlyhost-and-Tiddlyspot]] for information on ~TiddlySpot and ~TiddlyHost saving configuration.
Saving/TiddlySpot/Filename: Upload Filename
Saving/TiddlySpot/Heading: ~TiddlySpot
Saving/TiddlySpot/Hint: //The server URL defaults to `http://<wikiname>.tiddlyspot.com/store.cgi` and can be changed to use a custom server address, e.g. `http://example.com/store.php`.//
Saving/TiddlySpot/Password: Password
Saving/TiddlySpot/ReadOnly: The ~TiddlySpot service is currently only available in read-only form. Please see http://tiddlyspot.com/ for the latest details. The ~TiddlySpot saver can still be used to save to compatible servers.
Saving/TiddlySpot/ReadOnly: Note that [[TiddlySpot|http://tiddlyspot.com]] no longer allows the creation of new sites. For new sites, you can use [[TiddlyHost|https://tiddlyhost.com]], a new hosting service that replaces ~TiddlySpot.
Saving/TiddlySpot/ServerURL: Server URL
Saving/TiddlySpot/UploadDir: Upload Directory
Saving/TiddlySpot/UserName: Wiki Name

View File

@ -3,6 +3,8 @@ title: $:/language/EditTemplate/
Body/External/Hint: This tiddler shows content stored outside of the main TiddlyWiki file. You can edit the tags and fields but cannot directly edit the content itself
Body/Placeholder: Type the text for this tiddler
Body/Preview/Type/Output: output
Body/Preview/Type/DiffShadow: differences from shadow (if any)
Body/Preview/Type/DiffCurrent: differences from current
Field/Remove/Caption: remove field
Field/Remove/Hint: Remove field
Field/Dropdown/Caption: field list

View File

@ -19,10 +19,9 @@ All parameters are optional with safe defaults, and can be specified in any orde
* ''username'' - optional username for basic authentication
* ''password'' - optional password for basic authentication
* ''authenticated-user-header'' - optional name of header to be used for trusted authentication
* ''readers'' - comma separated list of principals allowed to read from this wiki
* ''writers'' - comma separated list of principals allowed to write to this wiki
* ''readers'' - comma-separated list of principals allowed to read from this wiki
* ''writers'' - comma-separated list of principals allowed to write to this wiki
* ''csrf-disable'' - set to "yes" to disable CSRF checks (defaults to "no")
* ''sse-enabled'' - set to "yes" to enable Server-sent events (defaults to "no")
* ''root-tiddler'' - the tiddler to serve at the root (defaults to "$:/core/save/all")
* ''root-render-type'' - the content type to which the root tiddler should be rendered (defaults to "text/plain")
* ''root-serve-type'' - the content type with which the root tiddler should be served (defaults to "text/html")
@ -30,6 +29,7 @@ All parameters are optional with safe defaults, and can be specified in any orde
* ''tls-key'' - pathname of TLS key file (relative to wiki folder)
* ''debug-level'' - optional debug level; set to "debug" to view request details (defaults to "none")
* ''gzip'' - set to "yes" to enable gzip compression for some http endpoints (defaults to "no")
* ''use-browser-cache'' - set to "yes" to allow the browser to cache responses to save bandwidth (defaults to "no")
For information on opening up your instance to the entire local network, and possible security concerns, see the WebServer tiddler at TiddlyWiki.com.

View File

@ -24,7 +24,7 @@ Notes:
* The output directory is not cleared of any existing files
* Any missing directories in the path to the filename are automatically created.
* When referring to a tiddler with spaces in its title, take care to use both the quotes required by your shell and also TiddlyWiki's double square brackets : `--render "[[Motovun Jack.jpg]]"`
* When referring to a tiddler with spaces in its title, take care to use both the quotes required by your shell and also TiddlyWiki's double square brackets: `--render "[[Motovun Jack.jpg]]"`
* The filename filter is evaluated with the selected items being set to the title of the tiddler currently being rendered, allowing the title to be used as the basis for computing the filename. For example `[encodeuricomponent[]addprefix[static/]]` applies URI encoding to each title, and then adds the prefix `static/`
* Multiple ''name''/''value'' pairs can be used to pass more than one variable
* The `--render` command is a more flexible replacement for both the `--rendertiddler` and `--rendertiddlers` commands, which are deprecated

View File

@ -16,7 +16,7 @@ Notes:
* The output directory is not cleared of any existing files
* Any missing directories in the path to the filename are automatically created.
* When saving a tiddler with spaces in its title, take care to use both the quotes required by your shell and also TiddlyWiki's double square brackets : `--save "[[Motovun Jack.jpg]]"`
* When saving a tiddler with spaces in its title, take care to use both the quotes required by your shell and also TiddlyWiki's double square brackets: `--save "[[Motovun Jack.jpg]]"`
* The filename filter is evaluated with the selected items being set to the title of the tiddler currently being saved, allowing the title to be used as the basis for computing the filename. For example `[encodeuricomponent[]addprefix[static/]]` applies URI encoding to each title, and then adds the prefix `static/`
* The `--save` command is a more flexible replacement for both the `--savetiddler` and `--savetiddlers` commands, which are deprecated

View File

@ -1,7 +1,9 @@
title: $:/language/Import/
Editor/Import/Heading: Import images and insert them into the editor.
Imported/Hint: The following tiddlers were imported:
Listing/Cancel/Caption: Cancel
Listing/Cancel/Warning: Do you wish to cancel the import?
Listing/Hint: These tiddlers are ready to import:
Listing/Import/Caption: Import
Listing/Select/Caption: Select
@ -22,7 +24,11 @@ Upgrader/Plugins/Suppressed/Incompatible: Blocked incompatible or obsolete plugi
Upgrader/Plugins/Suppressed/Version: Blocked plugin (due to incoming <<incoming>> not being newer than existing <<existing>>).
Upgrader/Plugins/Upgraded: Upgraded plugin from <<incoming>> to <<upgraded>>.
Upgrader/State/Suppressed: Blocked temporary state tiddler.
Upgrader/System/Disabled: Disabled system tiddler.
Upgrader/System/Suppressed: Blocked system tiddler.
Upgrader/System/Warning: Core module tiddler.
Upgrader/System/Alert: You are about to import a tiddler that will overwrite a core module tiddler. This is not recommended as it may make the system unstable.
Upgrader/ThemeTweaks/Created: Migrated theme tweak from <$text text=<<from>>/>.
Upgrader/Tiddler/Disabled: Disabled tiddler.
Upgrader/Tiddler/Selected: Selected tiddler.
Upgrader/Tiddler/Unselected: Unselected tiddler.

View File

@ -24,7 +24,6 @@ Encryption/RepeatPassword: Repeat password
Encryption/PasswordNoMatch: Passwords do not match
Encryption/SetPassword: Set password
Error/Caption: Error
Error/EditConflict: File changed on server
Error/Filter: Filter error
Error/FilterSyntax: Syntax error in filter expression
Error/FilterRunPrefix: Filter Error: Unknown prefix for filter run
@ -32,6 +31,9 @@ Error/IsFilterOperator: Filter Error: Unknown operand for the 'is' filter operat
Error/FormatFilterOperator: Filter Error: Unknown suffix for the 'format' filter operator
Error/LoadingPluginLibrary: Error loading plugin library
Error/NetworkErrorAlert: `<h2>''Network Error''</h2>It looks like the connection to the server has been lost. This may indicate a problem with your network connection. Please attempt to restore network connectivity before continuing.<br><br>''Any unsaved changes will be automatically synchronised when connectivity is restored''.`
Error/PutEditConflict: File changed on server
Error/PutForbidden: Permission denied
Error/PutUnauthorized: Authentication required
Error/RecursiveTransclusion: Recursive transclusion error in transclude widget
Error/RetrievingSkinny: Error retrieving skinny tiddler list
Error/SavingToTWEdit: Error saving to TWEdit
@ -39,7 +41,6 @@ Error/WhileSaving: Error while saving
Error/XMLHttpRequest: XMLHttpRequest error code
InternalJavaScriptError/Title: Internal JavaScript Error
InternalJavaScriptError/Hint: Well, this is embarrassing. It is recommended that you restart TiddlyWiki by refreshing your browser
InvalidFieldName: Illegal characters in field name "<$text text=<<fieldName>>/>". Fields can only contain lowercase letters, digits and the characters underscore (`_`), hyphen (`-`) and period (`.`)
LayoutSwitcher/Description: Open the layout switcher
LazyLoadingWarning: <p>Trying to load external content from ''<$text text={{!!_canonical_uri}}/>''</p><p>If this message doesn't disappear, either the tiddler content type doesn't match the type of the external content, or you may be using a browser that doesn't support external content for wikis loaded as standalone files. See https://tiddlywiki.com/#ExternalText</p>
LoginToTiddlySpace: Login to TiddlySpace

View File

@ -154,7 +154,7 @@ Commander.prototype.extractNamedParameters = function(params,mandatoryParameters
if(errors.length > 0) {
return errors.join(" and\n");
} else {
return paramsByName;
return paramsByName;
}
};

View File

@ -115,7 +115,7 @@ Command.prototype.fetchFile = function(url,options,callback,redirectCount) {
if(response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {
return self.fetchFile(response.headers.location,options,callback,redirectCount + 1);
} else {
return callback("Error " + response.statusCode + " retrieving " + url)
return callback("Error " + response.statusCode + " retrieving " + url)
}
}
});

View File

@ -8,58 +8,59 @@ Render individual tiddlers and save the results to the specified files
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var widget = require("$:/core/modules/widgets/widget.js");
exports.info = {
name: "render",
synchronous: true
};
var Command = function(params,commander,callback) {
this.params = params;
this.commander = commander;
this.callback = callback;
};
Command.prototype.execute = function() {
if(this.params.length < 1) {
return "Missing tiddler filter";
}
var self = this,
fs = require("fs"),
path = require("path"),
wiki = this.commander.wiki,
tiddlerFilter = this.params[0],
filenameFilter = this.params[1] || "[is[tiddler]addsuffix[.html]]",
type = this.params[2] || "text/html",
template = this.params[3],
variableList = this.params.slice(4),
tiddlers = wiki.filterTiddlers(tiddlerFilter),
variables = Object.create(null);
while(variableList.length >= 2) {
variables[variableList[0]] = variableList[1];
variableList = variableList.slice(2);
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
var widget = require("$:/core/modules/widgets/widget.js");
exports.info = {
name: "render",
synchronous: true
};
var Command = function(params,commander,callback) {
this.params = params;
this.commander = commander;
this.callback = callback;
};
Command.prototype.execute = function() {
if(this.params.length < 1) {
return "Missing tiddler filter";
}
$tw.utils.each(tiddlers,function(title) {
var parser = wiki.parseTiddler(template || title);
var widgetNode = wiki.makeWidget(parser,{variables: $tw.utils.extend({},variables,{currentTiddler: title})}),
container = $tw.fakeDocument.createElement("div");
widgetNode.render(container,null);
var text = type === "text/html" ? container.innerHTML : container.textContent,
filepath = path.resolve(self.commander.outputPath,wiki.filterTiddlers(filenameFilter,$tw.rootWidget,wiki.makeTiddlerIterator([title]))[0]);
if(self.commander.verbose) {
console.log("Rendering \"" + title + "\" to \"" + filepath + "\"");
}
$tw.utils.createFileDirectories(filepath);
fs.writeFileSync(filepath,text,"utf8");
});
return null;
};
exports.Command = Command;
})();
var self = this,
fs = require("fs"),
path = require("path"),
wiki = this.commander.wiki,
tiddlerFilter = this.params[0],
filenameFilter = this.params[1] || "[is[tiddler]addsuffix[.html]]",
type = this.params[2] || "text/html",
template = this.params[3],
variableList = this.params.slice(4),
tiddlers = wiki.filterTiddlers(tiddlerFilter),
variables = Object.create(null);
while(variableList.length >= 2) {
variables[variableList[0]] = variableList[1];
variableList = variableList.slice(2);
}
$tw.utils.each(tiddlers,function(title) {
var filepath = path.resolve(self.commander.outputPath,wiki.filterTiddlers(filenameFilter,$tw.rootWidget,wiki.makeTiddlerIterator([title]))[0]);
if(self.commander.verbose) {
console.log("Rendering \"" + title + "\" to \"" + filepath + "\"");
}
var parser = wiki.parseTiddler(template || title),
widgetNode = wiki.makeWidget(parser,{variables: $tw.utils.extend({},variables,{currentTiddler: title})}),
container = $tw.fakeDocument.createElement("div");
widgetNode.render(container,null);
var text = type === "text/html" ? container.innerHTML : container.textContent;
$tw.utils.createFileDirectories(filepath);
fs.writeFileSync(filepath,text,"utf8");
});
return null;
};
exports.Command = Command;
})();

View File

@ -36,7 +36,7 @@ Command.prototype.execute = function() {
filter = this.params[0],
template = this.params[1],
outputPath = this.commander.outputPath,
pathname = path.resolve(outputPath,this.params[2]),
pathname = path.resolve(outputPath,this.params[2]),
type = this.params[3] || "text/html",
extension = this.params[4] || ".html",
deleteDirectory = (this.params[5] || "").toLowerCase() !== "noclean",

View File

@ -97,7 +97,7 @@ WikiFolderMaker.prototype.save = function() {
// A custom plugin
self.log("Processing custom plugin: " + title);
self.saveCustomPlugin(tiddler);
}
}
} else {
// Ordinary tiddler
self.saveTiddler("tiddlers",tiddler);

View File

@ -34,7 +34,7 @@ exports.htmlEntities = {quot:34, amp:38, apos:39, lt:60, gt:62, nbsp:160, iexcl:
exports.htmlVoidElements = "area,base,br,col,command,embed,hr,img,input,keygen,link,meta,param,source,track,wbr".split(",");
exports.htmlBlockElements = "address,article,aside,audio,blockquote,canvas,dd,div,dl,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,noscript,ol,output,p,pre,section,table,tfoot,ul,video".split(",");
exports.htmlBlockElements = "address,article,aside,audio,blockquote,canvas,dd,details,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,hr,li,nav,ol,p,pre,section,summary,table,tfoot,ul,video".split(",");
exports.htmlUnsafeElements = "script".split(",");

View File

@ -12,6 +12,124 @@ Functions to deserialise tiddlers from a block of text
/*global $tw: false */
"use strict";
exports["application/x-tiddler-html-div"] = function(text,fields) {
return [deserializeTiddlerDiv(text,fields)];
};
exports["application/json"] = function(text,fields) {
var incoming,
results = [];
try {
incoming = JSON.parse(text);
} catch(e) {
incoming = [{
title: "JSON error: " + e,
text: ""
}]
}
if(!$tw.utils.isArray(incoming)) {
incoming = [incoming];
}
for(var t=0; t<incoming.length; t++) {
var incomingFields = incoming[t],
fields = {};
for(var f in incomingFields) {
if(typeof incomingFields[f] === "string") {
fields[f] = incomingFields[f];
}
}
results.push(fields);
}
return results;
};
/*
Parse an HTML file into tiddlers. There are three possibilities:
# A TiddlyWiki classic HTML file containing `text/x-tiddlywiki` tiddlers
# A TiddlyWiki5 HTML file containing `text/vnd.tiddlywiki` tiddlers
# An ordinary HTML file
*/
exports["text/html"] = function(text,fields) {
var results = [];
// Check if we've got an old-style store area
var storeAreaMarkerRegExp = /<div id=["']?storeArea['"]?( style=["']?display:none;["']?)?>/gi,
storeAreaMatch = storeAreaMarkerRegExp.exec(text);
if(storeAreaMatch) {
// If so, we've got tiddlers in classic TiddlyWiki format or unencrypted old-style TW5 format
results.push.apply(results,deserializeStoreArea(text,storeAreaMarkerRegExp.lastIndex,!!storeAreaMatch[1],fields));
}
// Check for new-style store areas
var newStoreAreaMarkerRegExp = /<script class="tiddlywiki-tiddler-store" type="([^"]*)">/gi,
newStoreAreaMatch = newStoreAreaMarkerRegExp.exec(text),
haveHadNewStoreArea = !!newStoreAreaMatch;
while(newStoreAreaMatch) {
results.push.apply(results,deserializeNewStoreArea(text,newStoreAreaMarkerRegExp.lastIndex,newStoreAreaMatch[1],fields));
newStoreAreaMatch = newStoreAreaMarkerRegExp.exec(text);
}
// Return if we had either an old-style or a new-style store area
if(storeAreaMatch || haveHadNewStoreArea) {
return results;
}
// Otherwise, check whether we've got an encrypted file
var encryptedStoreArea = $tw.utils.extractEncryptedStoreArea(text);
if(encryptedStoreArea) {
// If so, attempt to decrypt it using the current password
return $tw.utils.decryptStoreArea(encryptedStoreArea);
} else {
// It's not a TiddlyWiki so we'll return the entire HTML file as a tiddler
return deserializeHtmlFile(text,fields);
}
};
function deserializeHtmlFile(text,fields) {
var result = {};
$tw.utils.each(fields,function(value,name) {
result[name] = value;
});
result.text = text;
result.type = "text/html";
return [result];
}
function deserializeNewStoreArea(text,storeAreaEnd,type,fields) {
var endOfScriptRegExp = /<\/script>/gi;
endOfScriptRegExp.lastIndex = storeAreaEnd;
var match = endOfScriptRegExp.exec(text);
if(match) {
var scriptContent = text.substring(storeAreaEnd,match.index);
return $tw.wiki.deserializeTiddlers(type,scriptContent);
} else {
return [];
}
}
function deserializeStoreArea(text,storeAreaEnd,isTiddlyWiki5,fields) {
var results = [],
endOfDivRegExp = /(<\/div>\s*)/gi,
startPos = storeAreaEnd,
defaultType = isTiddlyWiki5 ? undefined : "text/x-tiddlywiki";
endOfDivRegExp.lastIndex = startPos;
var match = endOfDivRegExp.exec(text);
while(match) {
var endPos = endOfDivRegExp.lastIndex,
tiddlerFields = deserializeTiddlerDiv(text.substring(startPos,endPos),fields,{type: defaultType});
if(!tiddlerFields) {
break;
}
$tw.utils.each(tiddlerFields,function(value,name) {
if(typeof value === "string") {
tiddlerFields[name] = $tw.utils.htmlDecode(value);
}
});
if(tiddlerFields.text !== null) {
results.push(tiddlerFields);
}
startPos = endPos;
match = endOfDivRegExp.exec(text);
}
return results;
}
/*
Utility function to parse an old-style tiddler DIV in a *.tid file. It looks like this:
@ -24,7 +142,7 @@ Note that the field attributes are HTML encoded, but that the body of the <PRE>
When these tiddler DIVs are encountered within a TiddlyWiki HTML file then the body is encoded in the usual way.
*/
var parseTiddlerDiv = function(text /* [,fields] */) {
var deserializeTiddlerDiv = function(text /* [,fields] */) {
// Slot together the default results
var result = {};
if(arguments.length > 1) {
@ -67,106 +185,4 @@ var parseTiddlerDiv = function(text /* [,fields] */) {
return undefined;
};
exports["application/x-tiddler-html-div"] = function(text,fields) {
return [parseTiddlerDiv(text,fields)];
};
exports["application/json"] = function(text,fields) {
var incoming,
results = [];
try {
incoming = JSON.parse(text);
} catch(e) {
incoming = [{
title: "JSON error: " + e,
text: ""
}]
}
if(!$tw.utils.isArray(incoming)) {
incoming = [incoming];
}
for(var t=0; t<incoming.length; t++) {
var incomingFields = incoming[t],
fields = {};
for(var f in incomingFields) {
if(typeof incomingFields[f] === "string") {
fields[f] = incomingFields[f];
}
}
results.push(fields);
}
return results;
};
/*
Parse an HTML file into tiddlers. There are three possibilities:
# A TiddlyWiki classic HTML file containing `text/x-tiddlywiki` tiddlers
# A TiddlyWiki5 HTML file containing `text/vnd.tiddlywiki` tiddlers
# An ordinary HTML file
*/
exports["text/html"] = function(text,fields) {
// Check if we've got a store area
var storeAreaMarkerRegExp = /<div id=["']?storeArea['"]?( style=["']?display:none;["']?)?>/gi,
match = storeAreaMarkerRegExp.exec(text);
if(match) {
// If so, it's either a classic TiddlyWiki file or an unencrypted TW5 file
// First read the normal tiddlers
var results = deserializeTiddlyWikiFile(text,storeAreaMarkerRegExp.lastIndex,!!match[1],fields);
// Then any system tiddlers
var systemAreaMarkerRegExp = /<div id=["']?systemArea['"]?( style=["']?display:none;["']?)?>/gi,
sysMatch = systemAreaMarkerRegExp.exec(text);
if(sysMatch) {
results.push.apply(results,deserializeTiddlyWikiFile(text,systemAreaMarkerRegExp.lastIndex,!!sysMatch[1],fields));
}
return results;
} else {
// Check whether we've got an encrypted file
var encryptedStoreArea = $tw.utils.extractEncryptedStoreArea(text);
if(encryptedStoreArea) {
// If so, attempt to decrypt it using the current password
return $tw.utils.decryptStoreArea(encryptedStoreArea);
} else {
// It's not a TiddlyWiki so we'll return the entire HTML file as a tiddler
return deserializeHtmlFile(text,fields);
}
}
};
function deserializeHtmlFile(text,fields) {
var result = {};
$tw.utils.each(fields,function(value,name) {
result[name] = value;
});
result.text = text;
result.type = "text/html";
return [result];
}
function deserializeTiddlyWikiFile(text,storeAreaEnd,isTiddlyWiki5,fields) {
var results = [],
endOfDivRegExp = /(<\/div>\s*)/gi,
startPos = storeAreaEnd,
defaultType = isTiddlyWiki5 ? undefined : "text/x-tiddlywiki";
endOfDivRegExp.lastIndex = startPos;
var match = endOfDivRegExp.exec(text);
while(match) {
var endPos = endOfDivRegExp.lastIndex,
tiddlerFields = parseTiddlerDiv(text.substring(startPos,endPos),fields,{type: defaultType});
if(!tiddlerFields) {
break;
}
$tw.utils.each(tiddlerFields,function(value,name) {
if(typeof value === "string") {
tiddlerFields[name] = $tw.utils.htmlDecode(value);
}
});
if(tiddlerFields.text !== null) {
results.push(tiddlerFields);
}
startPos = endPos;
match = endOfDivRegExp.exec(text);
}
return results;
}
})();

View File

@ -78,7 +78,7 @@ function FramedEngine(options) {
}
if(this.widget.isDisabled === "yes") {
this.domNode.setAttribute("disabled",true);
}
}
// Copy the styles from the dummy textarea
this.copyStyles();
// Add event listeners
@ -88,6 +88,18 @@ function FramedEngine(options) {
{name: "keydown",handlerObject: this.widget,handlerMethod: "handleKeydownEvent"},
{name: "focus",handlerObject: this,handlerMethod: "handleFocusEvent"}
]);
// Add drag and drop event listeners if fileDrop is enabled
if(this.widget.isFileDropEnabled) {
$tw.utils.addEventListeners(this.domNode,[
{name: "dragenter",handlerObject: this.widget,handlerMethod: "handleDragEnterEvent"},
{name: "dragover",handlerObject: this.widget,handlerMethod: "handleDragOverEvent"},
{name: "dragleave",handlerObject: this.widget,handlerMethod: "handleDragLeaveEvent"},
{name: "dragend",handlerObject: this.widget,handlerMethod: "handleDragEndEvent"},
{name: "drop", handlerObject: this.widget,handlerMethod: "handleDropEvent"},
{name: "paste", handlerObject: this.widget,handlerMethod: "handlePasteEvent"},
{name: "click",handlerObject: this.widget,handlerMethod: "handleClickEvent"}
]);
}
// Insert the element into the DOM
this.iframeDoc.body.appendChild(this.domNode);
}
@ -123,7 +135,11 @@ FramedEngine.prototype.setText = function(text,type) {
Update the DomNode with the new text
*/
FramedEngine.prototype.updateDomNodeText = function(text) {
this.domNode.value = text;
try {
this.domNode.value = text;
} catch(e) {
// Ignore
}
};
/*
@ -170,7 +186,7 @@ Handle a focus event
*/
FramedEngine.prototype.handleFocusEvent = function(event) {
if(this.widget.editCancelPopups) {
$tw.popup.cancel(0);
$tw.popup.cancel(0);
}
};

View File

@ -85,7 +85,11 @@ SimpleEngine.prototype.setText = function(text,type) {
Update the DomNode with the new text
*/
SimpleEngine.prototype.updateDomNodeText = function(text) {
this.domNode.value = text;
try {
this.domNode.value = text;
} catch(e) {
// Ignore
}
};
/*

View File

@ -103,7 +103,11 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
var tiddler = this.wiki.getTiddler(this.editTitle);
if(tiddler) {
// If we've got a tiddler, the value to display is the field string value
value = tiddler.getFieldString(this.editField);
if(tiddler.hasField(this.editField)) {
value = tiddler.getFieldString(this.editField);
} else {
value = this.editDefault || "";
}
if(this.editField === "text") {
type = tiddler.fields.type || "text/vnd.tiddlywiki";
}
@ -182,6 +186,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
this.editRefreshTitle = this.getAttribute("refreshTitle");
this.editAutoComplete = this.getAttribute("autocomplete");
this.isDisabled = this.getAttribute("disabled","no");
this.isFileDropEnabled = this.getAttribute("fileDrop","no") === "yes";
// Get the default editor element tag and type
var tag,type;
if(this.editField === "text") {
@ -213,7 +218,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
EditTextWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Completely rerender if any of our attributes have changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedAttributes.disabled) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedAttributes.disabled || changedAttributes.fileDrop) {
this.refreshSelf();
return true;
} else if (changedTiddlers[this.editRefreshTitle]) {
@ -293,21 +298,89 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
Propogate keydown events to our container for the keyboard widgets benefit
*/
EditTextWidget.prototype.propogateKeydownEvent = function(event) {
var newEvent = this.document.createEventObject ? this.document.createEventObject() : this.document.createEvent("Events");
if(newEvent.initEvent) {
newEvent.initEvent("keydown", true, true);
}
newEvent.keyCode = event.keyCode;
newEvent.which = event.which;
newEvent.metaKey = event.metaKey;
newEvent.ctrlKey = event.ctrlKey;
newEvent.altKey = event.altKey;
newEvent.shiftKey = event.shiftKey;
var newEvent = this.cloneEvent(event,["keyCode","which","metaKey","ctrlKey","altKey","shiftKey"]);
return !this.parentDomNode.dispatchEvent(newEvent);
};
return EditTextWidget;
EditTextWidget.prototype.cloneEvent = function(event,propertiesToCopy) {
var propertiesToCopy = propertiesToCopy || [],
newEvent = this.document.createEventObject ? this.document.createEventObject() : this.document.createEvent("Events");
if(newEvent.initEvent) {
newEvent.initEvent(event.type, true, true);
}
$tw.utils.each(propertiesToCopy,function(prop){
newEvent[prop] = event[prop];
});
return newEvent;
};
EditTextWidget.prototype.dispatchDOMEvent = function(newEvent) {
var dispatchNode = this.engine.iframeNode || this.engine.parentNode;
return dispatchNode.dispatchEvent(newEvent);
};
/*
Propogate drag and drop events with File data to our container for the dropzone widgets benefit.
If there are no Files, let the browser handle it.
*/
EditTextWidget.prototype.handleDropEvent = function(event) {
if(event.dataTransfer.files.length) {
event.preventDefault();
event.stopPropagation();
this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
};
EditTextWidget.prototype.handlePasteEvent = function(event) {
if(event.clipboardData && event.clipboardData.files && event.clipboardData.files.length) {
event.preventDefault();
event.stopPropagation();
this.dispatchDOMEvent(this.cloneEvent(event,["clipboardData"]));
}
};
EditTextWidget.prototype.handleDragEnterEvent = function(event) {
if($tw.utils.dragEventContainsFiles(event)) {
// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
if( event.relatedTarget && (event.relatedTarget.nodeType === 3 || event.target === event.relatedTarget)) {
return true;
}
event.preventDefault();
return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
return true;
};
EditTextWidget.prototype.handleDragOverEvent = function(event) {
if($tw.utils.dragEventContainsFiles(event)) {
// Call preventDefault() in browsers that default to not allowing drop events on textarea
if($tw.browser.isFirefox || $tw.browser.isIE) {
event.preventDefault();
}
event.dataTransfer.dropEffect = "copy";
return this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
}
return true;
};
EditTextWidget.prototype.handleDragLeaveEvent = function(event) {
// Ignore excessive events fired by FF when entering and leaving text nodes in a text area.
if(event.relatedTarget && ((event.relatedTarget.nodeType === 3) || (event.target === event.relatedTarget))) {
return true;
}
event.preventDefault();
this.dispatchDOMEvent(this.cloneEvent(event,["dataTransfer"]));
};
EditTextWidget.prototype.handleDragEndEvent = function(event) {
this.dispatchDOMEvent(this.cloneEvent(event));
};
EditTextWidget.prototype.handleClickEvent = function(event) {
return !this.dispatchDOMEvent(this.cloneEvent(event));
};
return EditTextWidget;
}
exports.editTextWidgetFactory = editTextWidgetFactory;

View File

@ -0,0 +1,17 @@
/*\
title: $:/core/modules/editor/operations/text/focus-editor.js
type: application/javascript
module-type: texteditoroperation
Simply focus the Text editor
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports["focus-editor"] = function(event,operation) {
operation = null;
};
})();

View File

@ -0,0 +1,23 @@
/*\
title: $:/core/modules/editor/operations/text/insert-text.js
type: application/javascript
module-type: texteditoroperation
Text editor operation insert text at the caret position. If there is a selection it is replaced.
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports["insert-text"] = function(event,operation) {
operation.replacement = event.paramObject.text;
operation.cutStart = operation.selStart;
operation.cutEnd = operation.selEnd;
operation.newSelStart = operation.selStart + operation.replacement.length;
operation.newSelEnd = operation.newSelStart;
};
})();

View File

@ -17,10 +17,21 @@ exports.filter = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var resultsToRemove = [];
results.each(function(result) {
var filtered = operationSubFunction(options.wiki.makeTiddlerIterator([result]),widget);
results.each(function(title) {
var filtered = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
default:
return widget.getVariable(name);
}
}
});
if(filtered.length === 0) {
resultsToRemove.push(result);
resultsToRemove.push(title);
}
});
results.remove(resultsToRemove);

View File

@ -0,0 +1,39 @@
/*\
title: $:/core/modules/filterrunprefixes/map.js
type: application/javascript
module-type: filterrunprefix
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter prefix function
*/
exports.map = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var inputTitles = results.toArray();
results.clear();
$tw.utils.each(inputTitles,function(title) {
var filtered = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
default:
return widget.getVariable(name);
}
}
});
results.push(filtered[0] || "");
});
}
}
};
})();

View File

@ -19,30 +19,32 @@ exports.reduce = function(operationSubFunction,options) {
var index = 0;
results.each(function(title) {
var list = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "accumulator":
return "" + accumulator;
case "index":
return "" + index;
case "revIndex":
return "" + (results.length - 1 - index);
case "length":
return "" + results.length;
default:
return widget.getVariable(name);
}
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
case "accumulator":
return "" + accumulator;
case "index":
return "" + index;
case "revIndex":
return "" + (results.length - 1 - index);
case "length":
return "" + results.length;
default:
return widget.getVariable(name);
}
});
}
});
if(list.length > 0) {
accumulator = "" + list[0];
}
++index;
});
results.clear();
results.push(accumulator);
results.push(accumulator);
}
}
};

View File

@ -0,0 +1,60 @@
/*\
title: $:/core/modules/filterrunprefixes/sort.js
type: application/javascript
module-type: filterrunprefix
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter prefix function
*/
exports.sort = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var suffixes = options.suffixes,
sortType = (suffixes[0] && suffixes[0][0]) ? suffixes[0][0] : "string",
invert = suffixes[1] ? (suffixes[1].indexOf("reverse") !== -1) : false,
isCaseSensitive = suffixes[1] ? (suffixes[1].indexOf("casesensitive") !== -1) : false,
inputTitles = results.toArray(),
sortKeys = [],
indexes = new Array(inputTitles.length),
compareFn;
results.each(function(title) {
var key = operationSubFunction(options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return widget.getVariable("currentTiddler");
default:
return widget.getVariable(name);
}
}
});
sortKeys.push(key[0] || "");
});
results.clear();
// Prepare an array of indexes to sort
for(var t=0; t<inputTitles.length; t++) {
indexes[t] = t;
}
// Sort the indexes
compareFn = $tw.utils.makeCompareFunction(sortType,{defaultType: "string", invert:invert, isCaseSensitive:isCaseSensitive});
indexes = indexes.sort(function(a,b) {
return compareFn(sortKeys[a],sortKeys[b]);
});
// Add to results in correct order
$tw.utils.each(indexes,function(index) {
results.push(inputTitles[index]);
});
}
}
};
})();

View File

@ -101,10 +101,10 @@ function parseFilterOperation(operators,filterString,p) {
}
p = nextBracketPos + 1;
}
p = nextBracketPos + 1;
parseOperand(bracket);
// Check for multiple operands
while(filterString.charAt(p) === ",") {
p++;
@ -116,7 +116,7 @@ function parseFilterOperation(operators,filterString,p) {
throw "Missing [ in filter expression";
}
}
// Push this operator
operators.push(operator);
} while(filterString.charAt(p) !== "]");
@ -137,7 +137,7 @@ exports.parseFilter = function(filterString) {
p = 0, // Current position in the filter string
match;
var whitespaceRegExp = /(\s+)/mg,
operandRegExp = /((?:\+|\-|~|=|\:(\w+))?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg;
operandRegExp = /((?:\+|\-|~|=|\:(\w+)(?:\:([\w\:, ]*))?)?)(?:(\[)|(?:"([^"]*)")|(?:'([^']*)')|([^\s\[\]]+))/mg;
while(p < filterString.length) {
// Skip any whitespace
whitespaceRegExp.lastIndex = p;
@ -162,15 +162,27 @@ exports.parseFilter = function(filterString) {
if(match[2]) {
operation.namedPrefix = match[2];
}
if(match[3]) {
operation.suffixes = [];
$tw.utils.each(match[3].split(":"),function(subsuffix) {
operation.suffixes.push([]);
$tw.utils.each(subsuffix.split(","),function(entry) {
entry = $tw.utils.trim(entry);
if(entry) {
operation.suffixes[operation.suffixes.length -1].push(entry);
}
});
});
}
}
if(match[3]) { // Opening square bracket
if(match[4]) { // Opening square bracket
p = parseFilterOperation(operation.operators,filterString,p);
} else {
p = match.index + match[0].length;
}
if(match[4] || match[5] || match[6]) { // Double quoted string, single quoted string or unquoted title
if(match[5] || match[6] || match[7]) { // Double quoted string, single quoted string or unquoted title
operation.operators.push(
{operator: "title", operands: [{text: match[4] || match[5] || match[6]}]}
{operator: "title", operands: [{text: match[5] || match[6] || match[7]}]}
);
}
results.push(operation);
@ -236,12 +248,13 @@ exports.compileFilter = function(filterString) {
} else {
operatorFunction = filterOperators[operator.operator];
}
$tw.utils.each(operator.operands,function(operand) {
if(operand.indirect) {
operand.value = self.getTextReference(operand.text,"",currTiddlerTitle);
} else if(operand.variable) {
operand.value = widget.getVariable(operand.text,{defaultValue: ""});
var varTree = $tw.utils.parseFilterVariable(operand.text);
operand.value = widget.getVariable(varTree.name,{params:varTree.params,defaultValue: ""});
} else {
operand.value = operand.text;
}
@ -280,7 +293,7 @@ exports.compileFilter = function(filterString) {
var filterRunPrefixes = self.getFilterRunPrefixes();
// Wrap the operator functions in a wrapper function that depends on the prefix
operationFunctions.push((function() {
var options = {wiki: self};
var options = {wiki: self, suffixes: operation.suffixes || []};
switch(operation.prefix || "") {
case "": // No prefix means that the operation is unioned into the result
return filterRunPrefixes["or"](operationSubFunction, options);
@ -311,6 +324,9 @@ exports.compileFilter = function(filterString) {
} else if(typeof source === "object") { // Array or hashmap
source = self.makeTiddlerIterator(source);
}
if(!widget) {
widget = $tw.rootWidget;
}
var results = new $tw.utils.LinkedList();
$tw.utils.each(operationFunctions,function(operationFunction) {
operationFunction(results,source,widget);

View File

@ -52,7 +52,7 @@ exports.all = function(source,operator,options) {
results.pushTop(subop(source,operator.prefix,options));
}
}
return results.toArray();
return results.makeTiddlerIterator(options.wiki);
};
})();

View File

@ -16,11 +16,11 @@ Filter operator for returning all the backlinks from a tiddler
Export our filter function
*/
exports.backlinks = function(source,operator,options) {
var results = [];
var results = new $tw.utils.LinkedList();
source(function(tiddler,title) {
$tw.utils.pushTop(results,options.wiki.getTiddlerBacklinks(title));
results.pushTop(options.wiki.getTiddlerBacklinks(title));
});
return results;
return results.makeTiddlerIterator(options.wiki);
};
})();

View File

@ -17,7 +17,7 @@ Export our filter function
*/
exports.contains = function(source,operator,options) {
var results = [],
fieldname = (operator.suffix || "list").toLowerCase();
fieldname = operator.suffix || "list";
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(tiddler) {

View File

@ -0,0 +1,27 @@
/*\
title: $:/core/modules/filters/deserializers.js
type: application/javascript
module-type: filteroperator
Filter operator for returning the names of the deserializers in this wiki
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter function
*/
exports.deserializers = function(source,operator,options) {
var results = [];
$tw.utils.each($tw.Wiki.tiddlerDeserializerModules,function(deserializer,type) {
results.push(type);
});
results.sort();
return results;
};
})();

View File

@ -22,7 +22,7 @@ exports.editiondescription = function(source,operator,options) {
if(editionInfo) {
source(function(tiddler,title) {
if($tw.utils.hop(editionInfo,title)) {
results.push(editionInfo[title].description || "");
results.push(editionInfo[title].description || "");
}
});
}

View File

@ -19,12 +19,7 @@ Export our filter functions
exports.decodeuricomponent = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var value = title;
try {
value = decodeURIComponent(title);
} catch(e) {
}
results.push(value);
results.push($tw.utils.decodeURIComponentSafe(title));
});
return results;
};
@ -40,12 +35,7 @@ exports.encodeuricomponent = function(source,operator,options) {
exports.decodeuri = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var value = title;
try {
value = decodeURI(title);
} catch(e) {
}
results.push(value);
results.push($tw.utils.decodeURISafe(title));
});
return results;
};
@ -102,7 +92,7 @@ exports.escapecss = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
// escape any character with a special meaning in CSS using CSS.escape()
results.push(CSS.escape(title));
results.push($tw.utils.escapeCSS(title));
});
return results;
};

View File

@ -17,7 +17,7 @@ Export our filter function
*/
exports.field = function(source,operator,options) {
var results = [],indexedResults,
fieldname = (operator.suffix || operator.operator || "title").toLowerCase();
fieldname = operator.suffix || operator.operator || "title";
if(operator.prefix === "!") {
if(operator.regexp) {
source(function(tiddler,title) {

View File

@ -20,7 +20,7 @@ exports.fields = function(source,operator,options) {
fieldName,
suffixes = (operator.suffixes || [])[0] || [],
operand = $tw.utils.parseStringArray(operator.operand);
source(function(tiddler,title) {
if(tiddler) {
if(suffixes.indexOf("include") !== -1) {

View File

@ -20,7 +20,18 @@ exports.filter = function(source,operator,options) {
results = [],
target = operator.prefix !== "!";
source(function(tiddler,title) {
var list = filterFn.call(options.wiki,options.wiki.makeTiddlerIterator([title]));
var list = filterFn.call(options.wiki,options.wiki.makeTiddlerIterator([title]),{
getVariable: function(name) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
default:
return options.widget.getVariable(name);
}
}
});
if((list.length > 0) === target) {
results.push(title);
}

View File

@ -13,13 +13,13 @@ module-type: formatfilteroperator
Export our filter function
*/
exports.date = function(source,operand,options) {
var results = [];
var results = [];
source(function(tiddler,title) {
var value = $tw.utils.parseDate(title);
if(value && $tw.utils.isDate(value) && value.toString() !== "Invalid Date") {
results.push($tw.utils.formatDateString(value,operand || "YYYY MM DD 0hh:0mm"));
}
});
});
return results;
};

View File

@ -13,13 +13,13 @@ module-type: formatfilteroperator
Export our filter function
*/
exports.relativedate = function(source,operand,options) {
var results = [];
var results = [];
source(function(tiddler,title) {
var value = $tw.utils.parseDate(title);
if(value && $tw.utils.isDate(value) && value.toString() !== "Invalid Date") {
results.push($tw.utils.getRelativeDate((new Date()) - (new Date(value))).description);
}
});
});
return results;
};

View File

@ -0,0 +1,25 @@
/*\
title: $:/core/modules/filters/format/titlelist.js
type: application/javascript
module-type: formatfilteroperator
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter function
*/
exports.titlelist = function(source,operand,options) {
var results = [];
source(function(tiddler,title) {
if(title && title.length) {
results.push($tw.utils.stringifyList([title]));
}
});
return results;
};
})();

View File

@ -61,7 +61,7 @@ exports.has = function(source,operator,options) {
if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand) && (tiddler.fields[operator.operand].length !== 0)) {
results.push(title);
}
});
});
}
}
return results;

View File

@ -28,7 +28,7 @@ exports.draft = function(source,prefix,options) {
if(tiddler && $tw.utils.hop(tiddler.fields,"draft.of") && (tiddler.fields["draft.of"].length !== 0)) {
results.push(title);
}
});
});
}
return results;
};

View File

@ -20,7 +20,7 @@ exports.links = function(source,operator,options) {
source(function(tiddler,title) {
results.pushTop(options.wiki.getTiddlerLinks(title));
});
return results.toArray();
return results.makeTiddlerIterator(options.wiki);
};
})();

View File

@ -125,6 +125,83 @@ exports.minall = makeNumericReducingOperator(
Infinity // Initial value
);
exports.median = makeNumericArrayOperator(
function(values) {
var len = values.length, median;
values.sort();
if(len % 2) {
// Odd, return the middle number
median = values[(len - 1) / 2];
} else {
// Even, return average of two middle numbers
median = (values[len / 2 - 1] + values[len / 2]) / 2;
}
return [median];
}
);
exports.average = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0, // Initial value
function(finalValue,numberOfValues) {
return finalValue/numberOfValues;
}
);
exports.variance = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0,
function(finalValue,numberOfValues,originalValues) {
return getVarianceFromArray(originalValues,finalValue/numberOfValues);
}
);
exports["standard-deviation"] = makeNumericReducingOperator(
function(accumulator,value) {return accumulator + value},
0,
function(finalValue,numberOfValues,originalValues) {
var variance = getVarianceFromArray(originalValues,finalValue/numberOfValues);
return Math.sqrt(variance);
}
);
//trigonometry
exports.cos = makeNumericBinaryOperator(
function(a) {return Math.cos(a)}
);
exports.sin = makeNumericBinaryOperator(
function(a) {return Math.sin(a)}
);
exports.tan = makeNumericBinaryOperator(
function(a) {return Math.tan(a)}
);
exports.acos = makeNumericBinaryOperator(
function(a) {return Math.acos(a)}
);
exports.asin = makeNumericBinaryOperator(
function(a) {return Math.asin(a)}
);
exports.atan = makeNumericBinaryOperator(
function(a) {return Math.atan(a)}
);
exports.atan2 = makeNumericBinaryOperator(
function(a,b) {return Math.atan2(a,b)}
);
//Calculate the variance of a population of numbers in an array given its mean
function getVarianceFromArray(values,mean) {
var deviationTotal = values.reduce(function(accumulator,value) {
return accumulator + Math.pow(value - mean, 2);
},0);
return deviationTotal/values.length;
};
function makeNumericBinaryOperator(fnCalc) {
return function(source,operator,options) {
var result = [],
@ -134,19 +211,37 @@ function makeNumericBinaryOperator(fnCalc) {
});
return result;
};
}
};
function makeNumericReducingOperator(fnCalc,initialValue) {
function makeNumericReducingOperator(fnCalc,initialValue,fnFinal) {
initialValue = initialValue || 0;
return function(source,operator,options) {
var result = [];
source(function(tiddler,title) {
result.push(title);
result.push($tw.utils.parseNumber(title));
});
return [$tw.utils.stringifyNumber(result.reduce(function(accumulator,currentValue) {
return fnCalc(accumulator,$tw.utils.parseNumber(currentValue));
},initialValue))];
var value = result.reduce(function(accumulator,currentValue) {
return fnCalc(accumulator,currentValue);
},initialValue);
if(fnFinal) {
value = fnFinal(value,result.length,result);
}
return [$tw.utils.stringifyNumber(value)];
};
}
};
function makeNumericArrayOperator(fnCalc) {
return function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
results.push($tw.utils.parseNumber(title));
});
results = fnCalc(results);
$tw.utils.each(results,function(value,index) {
results[index] = $tw.utils.stringifyNumber(value);
});
return results;
};
};
})();

View File

@ -0,0 +1,30 @@
/*\
title: $:/core/modules/filters/moduleproperty.js
type: application/javascript
module-type: filteroperator
Filter [[module-name]moduleproperty[name]] retrieve a module property
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
Export our filter function
*/
exports.moduleproperty = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
var value = require(title)[operator.operand || ""];
if(value !== undefined) {
results.push(value);
}
});
results.sort();
return results;
};
})();

View File

@ -17,11 +17,23 @@ Export our filter function
*/
exports.modules = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
$tw.utils.each($tw.modules.types[title],function(moduleInfo,moduleName) {
results.push(moduleName);
if(operator.operands.length >= 2) {
// Return the modules that have the module property specified in the first operand with the value in the second operand
source(function(tiddler,title) {
$tw.utils.each($tw.modules.types[title],function(moduleInfo,moduleName) {
if(require(moduleName)[operator.operands[0]] === operator.operands[1]) {
results.push(moduleName);
}
});
});
});
} else {
// Return all the module names without filtering
source(function(tiddler,title) {
$tw.utils.each($tw.modules.types[title],function(moduleInfo,moduleName) {
results.push(moduleName);
});
});
}
results.sort();
return results;
};

View File

@ -17,9 +17,13 @@ Export our filter function
*/
exports.range = function(source,operator,options) {
var results = [];
// Split the operand into numbers delimited by these symbols
var parts = operator.operand.split(/[,:;]/g),
beg, end, inc, i, fixed = 0;
// For backwards compatibility, if there is only one operand, try to split it using one of the delimiters
var parts = operator.operands || [];
if(parts.length === 1) {
parts = operator.operand.split(/[,:;]/g);
}
// Process the parts
var beg, end, inc, i, fixed = 0;
for (i=0; i<parts.length; i++) {
// Validate real number
if(!/^\s*[+-]?((\d+(\.\d*)?)|(\.\d+))\s*$/.test(parts[i])) {

View File

@ -31,6 +31,8 @@ exports.reduce = function(source,operator,options) {
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
case "accumulator":
return "" + accumulator;
case "index":

View File

@ -17,7 +17,7 @@ Export our filter function
*/
exports.regexp = function(source,operator,options) {
var results = [],
fieldname = (operator.suffix || "title").toLowerCase(),
fieldname = operator.suffix || "title",
regexpString, regexp, flags = "", match,
getFieldString = function(tiddler,title) {
if(tiddler) {

View File

@ -27,10 +27,13 @@ exports.sortsub = function(source,operator,options) {
iterator(options.wiki.getTiddler(title),title);
},{
getVariable: function(name) {
if(name === "currentTiddler") {
return title;
} else {
return options.widget.getVariable(name);
switch(name) {
case "currentTiddler":
return "" + title;
case "..currentTiddler":
return options.widget.getVariable("currentTiddler");
default:
return options.widget.getVariable(name);
}
}
});

View File

@ -105,13 +105,13 @@ exports.splitregexp = function(source,operator,options) {
flags = (suffix.indexOf("m") !== -1 ? "m" : "") + (suffix.indexOf("i") !== -1 ? "i" : ""),
regExp;
try {
regExp = new RegExp(operator.operand || "",flags);
regExp = new RegExp(operator.operand || "",flags);
} catch(ex) {
return ["RegExp error: " + ex];
}
source(function(tiddler,title) {
Array.prototype.push.apply(result,title.split(regExp));
});
});
return result;
};
@ -119,11 +119,11 @@ exports["search-replace"] = function(source,operator,options) {
var results = [],
suffixes = operator.suffixes || [],
flagSuffix = (suffixes[0] ? (suffixes[0][0] || "") : ""),
flags = (flagSuffix.indexOf("g") !== -1 ? "g" : "") + (flagSuffix.indexOf("i") !== -1 ? "i" : ""),
flags = (flagSuffix.indexOf("g") !== -1 ? "g" : "") + (flagSuffix.indexOf("i") !== -1 ? "i" : "") + (flagSuffix.indexOf("m") !== -1 ? "m" : ""),
isRegExp = (suffixes[1] && suffixes[1][0] === "regexp") ? true : false,
searchTerm,
regExp;
source(function(tiddler,title) {
if(title && (operator.operands.length > 1)) {
//Escape regexp characters if the operand is not a regular expression
@ -156,7 +156,7 @@ exports.pad = function(source,operator,options) {
var padString = "",
padStringLength = targetLength - title.length;
while (padStringLength > padString.length) {
padString += fill;
padString += fill;
}
//make sure we do not exceed the specified length
padString = padString.slice(0,padStringLength);
@ -172,4 +172,14 @@ exports.pad = function(source,operator,options) {
return results;
}
exports.charcode = function(source,operator,options) {
var chars = [];
$tw.utils.each(operator.operands,function(operand) {
if(operand !== "") {
chars.push(String.fromCharCode($tw.utils.parseInt(operand)));
}
});
return [chars.join("")];
};
})();

View File

@ -50,7 +50,7 @@ exports.tag = function(source,operator,options) {
});
results = options.wiki.sortByList(results,operator.operand);
}
}
}
}
return results;
};

View File

@ -16,20 +16,13 @@ Filter operator returning all the selected tiddlers that are untagged
Export our filter function
*/
exports.untagged = function(source,operator,options) {
var results = [];
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(tiddler && $tw.utils.isArray(tiddler.fields.tags) && tiddler.fields.tags.length > 0) {
$tw.utils.pushTop(results,title);
}
});
} else {
source(function(tiddler,title) {
if(!tiddler || !tiddler.hasField("tags") || ($tw.utils.isArray(tiddler.fields.tags) && tiddler.fields.tags.length === 0)) {
$tw.utils.pushTop(results,title);
}
});
}
var results = [],
expected = (operator.prefix === "!");
source(function(tiddler,title) {
if((tiddler && $tw.utils.isArray(tiddler.fields.tags) && tiddler.fields.tags.length > 0) === expected) {
results.push(title);
}
});
return results;
};

View File

@ -193,7 +193,7 @@ Extended filter operators to manipulate the current list.
step = stepSize || 1,
i = 0,
opLength = operands.length,
nextOperandIndex;
nextOperandIndex;
for(i; i < opLength; i++) {
resultsIndex = results.indexOf(operands[i]);
if(resultsIndex !== -1) {
@ -211,12 +211,12 @@ Extended filter operators to manipulate the current list.
} else {
results.push(operands[0]);
}
return results;
return results;
}
/*
Toggles an item in the current list.
*/
*/
exports.toggle = function(source,operator) {
return cycleValueInArray(prepare_results(source),operator.operands);
}
@ -228,8 +228,8 @@ Extended filter operators to manipulate the current list.
if(step < 0) {
operands.reverse();
step = Math.abs(step);
}
}
return cycleValueInArray(results,operands,step);
}
})();

View File

@ -121,7 +121,7 @@ FieldIndexer.prototype.update = function(updateDescriptor) {
indexEntry[value].push(updateDescriptor["new"].tiddler.fields.title);
}
}
});
});
}
};

View File

@ -65,7 +65,7 @@ TagSubIndexer.prototype.rebuild = function() {
} else {
self.index[tag].titles.push(title);
}
});
});
});
};
@ -83,7 +83,7 @@ TagSubIndexer.prototype.lookup = function(tag) {
if(!indexRecord.isSorted) {
if(this.indexer.wiki.sortByList) {
indexRecord.titles = this.indexer.wiki.sortByList(indexRecord.titles,tag);
}
}
indexRecord.isSorted = true;
}
return indexRecord.titles;

View File

@ -22,7 +22,7 @@ exports.getInfoTiddlerFields = function(updateInfoTiddlersCallback) {
if($tw.browser) {
// Document location
var setLocationProperty = function(name,value) {
infoTiddlerFields.push({title: "$:/info/url/" + name, text: value});
infoTiddlerFields.push({title: "$:/info/url/" + name, text: value});
},
location = document.location;
setLocationProperty("full", (location.toString()).split("#")[0]);

View File

@ -179,7 +179,7 @@ Key descriptors have the following format:
ctrl+enter
ctrl+shift+alt+A
*/
KeyboardManager.prototype.parseKeyDescriptor = function(keyDescriptor) {
KeyboardManager.prototype.parseKeyDescriptor = function(keyDescriptor,options) {
var components = keyDescriptor.split(/\+|\-/),
info = {
keyCode: 0,
@ -206,6 +206,9 @@ KeyboardManager.prototype.parseKeyDescriptor = function(keyDescriptor) {
info.keyCode = this.namedKeys[s];
}
}
if(options.keyDescriptor) {
info.keyDescriptor = options.keyDescriptor;
}
if(info.keyCode) {
return info;
} else {
@ -237,6 +240,7 @@ KeyboardManager.prototype.parseKeyDescriptors = function(keyDescriptors,options)
lookupName = function(configName) {
var keyDescriptors = wiki.getTiddlerText("$:/config/" + configName + "/" + name);
if(keyDescriptors) {
options.keyDescriptor = keyDescriptor;
result.push.apply(result,self.parseKeyDescriptors(keyDescriptors,options));
}
};
@ -245,7 +249,7 @@ KeyboardManager.prototype.parseKeyDescriptors = function(keyDescriptors,options)
});
}
} else {
result.push(self.parseKeyDescriptor(keyDescriptor));
result.push(self.parseKeyDescriptor(keyDescriptor,options));
}
});
return result;
@ -276,16 +280,20 @@ KeyboardManager.prototype.checkKeyDescriptor = function(event,keyInfo) {
};
KeyboardManager.prototype.checkKeyDescriptors = function(event,keyInfoArray) {
return (this.getMatchingKeyDescriptor(event,keyInfoArray) !== null);
};
KeyboardManager.prototype.getMatchingKeyDescriptor = function(event,keyInfoArray) {
for(var t=0; t<keyInfoArray.length; t++) {
if(this.checkKeyDescriptor(event,keyInfoArray[t])) {
return true;
return keyInfoArray[t];
}
}
return false;
return null;
};
KeyboardManager.prototype.getEventModifierKeyDescriptor = function(event) {
return event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey ? "ctrl" :
return event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey ? "ctrl" :
event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey ? "shift" :
event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey ? "ctrl-shift" :
event.altKey && !event.shiftKey && !event.ctrlKey && !event.metaKey ? "alt" :
@ -295,7 +303,7 @@ KeyboardManager.prototype.getEventModifierKeyDescriptor = function(event) {
event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey ? "meta" :
event.metaKey && event.ctrlKey && !event.shiftKey && !event.altKey ? "meta-ctrl" :
event.metaKey && event.ctrlKey && event.shiftKey && !event.altKey ? "meta-ctrl-shift" :
event.metaKey && event.ctrlKey & event.shiftKey && event.altKey ? "meta-ctrl-alt-shift" : "normal";
event.metaKey && event.ctrlKey && event.shiftKey && event.altKey ? "meta-ctrl-alt-shift" : "normal";
};
KeyboardManager.prototype.getShortcutTiddlerList = function() {
@ -324,7 +332,7 @@ KeyboardManager.prototype.handleKeydownEvent = function(event) {
if(key !== undefined) {
event.preventDefault();
event.stopPropagation();
$tw.rootWidget.invokeActionString(action,$tw.rootWidget);
$tw.rootWidget.invokeActionString(action,$tw.rootWidget,event);
return true;
}
return false;

View File

@ -2,6 +2,7 @@
title: $:/core/modules/macros/unusedtitle.js
type: application/javascript
module-type: macro
Macro to return a new title that is unused in the wiki. It can be given a name as a base.
\*/
(function(){
@ -10,25 +11,25 @@ Macro to return a new title that is unused in the wiki. It can be given a name a
/*global $tw: false */
"use strict";
/*
Information about this macro
*/
exports.name = "unusedtitle";
exports.params = [
{name: "baseName"},
{name: "options"}
{name: "separator"},
{name: "template"}
];
/*
Run the macro
*/
exports.run = function(baseName, options) {
exports.run = function(baseName,separator,template) {
separator = separator || " ";
if(!baseName) {
baseName = $tw.language.getString("DefaultNewTiddlerTitle");
}
return this.wiki.generateNewTitle(baseName, options);
// $tw.wiki.generateNewTitle = function(baseTitle,options)
// options.prefix must be a string!
return this.wiki.generateNewTitle(baseName, {"prefix": separator, "template": template});
};
})();

View File

@ -23,10 +23,12 @@ var HtmlParser = function(type,text,options) {
type: "element",
tag: "iframe",
attributes: {
src: {type: "string", value: src},
sandbox: {type: "string", value: ""}
src: {type: "string", value: src}
}
}];
if($tw.wiki.getTiddlerText("$:/config/HtmlParser/DisableSandbox","no") !== "yes") {
this.tree[0].attributes.sandbox = {type: "string", value: $tw.wiki.getTiddlerText("$:/config/HtmlParser/SandboxTokens","")};
}
};
exports["text/html"] = HtmlParser;

View File

@ -123,6 +123,19 @@ exports.parseStringLiteral = function(source,pos) {
}
};
exports.parseMacroParameters = function(node,source,pos) {
// Process parameters
var parameter = $tw.utils.parseMacroParameter(source,pos);
while(parameter) {
node.params.push(parameter);
pos = parameter.end;
// Get the next parameter
parameter = $tw.utils.parseMacroParameter(source,pos);
}
node.end = pos;
return node;
}
/*
Look for a macro invocation parameter. Returns null if not found, or {type: "macro-parameter", name:, value:, start:, end:}
*/
@ -187,14 +200,8 @@ exports.parseMacroInvocation = function(source,pos) {
}
node.name = name.match[1];
pos = name.end;
// Process parameters
var parameter = $tw.utils.parseMacroParameter(source,pos);
while(parameter) {
node.params.push(parameter);
pos = parameter.end;
// Get the next parameter
parameter = $tw.utils.parseMacroParameter(source,pos);
}
node = $tw.utils.parseMacroParameters(node,source,pos);
pos = node.end;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a double greater than sign
@ -208,6 +215,29 @@ exports.parseMacroInvocation = function(source,pos) {
return node;
};
exports.parseFilterVariable = function(source) {
var node = {
name: "",
params: [],
},
pos = 0,
reName = /([^\s"']+)/g;
// If there is no whitespace or it is an empty string then there are no macro parameters
if(/^\S*$/.test(source)) {
node.name = source;
return node;
}
// Get the variable name
var nameMatch = $tw.utils.parseTokenRegExp(source,pos,reName);
if(nameMatch) {
node.name = nameMatch.match[1];
pos = nameMatch.end;
node = $tw.utils.parseMacroParameters(node,source,pos);
delete node.end;
}
return node;
};
/*
Look for an HTML attribute definition. Returns null if not found, otherwise returns {type: "attribute", name:, valueType: "string|indirect|macro", value:, start:, end:,}
*/

View File

@ -7,6 +7,12 @@ Wiki text block rule for HTML comments. For example:
```
<!-- This is a comment -->
\define macroX()
<!-- This is a comment -->
xxxx
\end
<!-- This is a comment -->
```
Note that the syntax for comments is simplified to an opening "<!--" sequence and a closing "-->" sequence -- HTML itself implements a more complex format (see http://ostermiller.org/findhtmlcomment.html)
@ -19,7 +25,7 @@ Note that the syntax for comments is simplified to an opening "<!--" sequence an
"use strict";
exports.name = "commentblock";
exports.types = {block: true};
exports.types = {block:true, pragma:true};
exports.init = function(parser) {
this.parser = parser;

View File

@ -66,7 +66,7 @@ exports.parse = function() {
};
/*
Look for an HTML tag. Returns null if not found, otherwise returns {type: "element", name:, attributes: [], isSelfClosing:, start:, end:,}
Look for an HTML tag. Returns null if not found, otherwise returns {type: "element", name:, attributes: {}, orderedAttributes: [], isSelfClosing:, start:, end:,}
*/
exports.parseTag = function(source,pos,options) {
options = options || {};
@ -74,7 +74,8 @@ exports.parseTag = function(source,pos,options) {
node = {
type: "element",
start: pos,
attributes: {}
attributes: {},
orderedAttributes: []
};
// Define our regexps
var reTagName = /([a-zA-Z0-9\-\$]+)/g;
@ -106,6 +107,7 @@ exports.parseTag = function(source,pos,options) {
// Process attributes
var attribute = $tw.utils.parseAttribute(source,pos);
while(attribute) {
node.orderedAttributes.push(attribute);
node.attributes[attribute.name] = attribute;
pos = attribute.end;
// Get the next attribute

View File

@ -36,7 +36,7 @@ exports.parse = function() {
// Move past the pragma invocation
this.parser.pos = this.matchRegExp.lastIndex;
// Parse the filter terminated by a line break
var reMatch = /(.*)(\r?\n)|$/mg;
var reMatch = /(.*)(?:$|\r?\n)/mg;
reMatch.lastIndex = this.parser.pos;
var match = reMatch.exec(this.parser.source);
this.parser.pos = reMatch.lastIndex;

View File

@ -161,7 +161,7 @@ exports.parse = function() {
// Move the caption to the first row if it isn't already
if(table.children.length !== 1) {
table.children.pop(); // Take rowContainer out of the children array
table.children.splice(0,0,rowContainer); // Insert it at the bottom
table.children.splice(0,0,rowContainer); // Insert it at the bottom
}
// Set the alignment - TODO: figure out why TW did this
// rowContainer.attributes.align = rowCount === 0 ? "top" : "bottom";

View File

@ -231,7 +231,10 @@ WikiParser.prototype.parseBlock = function(terminatorRegExpString) {
return nextMatch.rule.parse();
}
// Treat it as a paragraph if we didn't find a block rule
return [{type: "element", tag: "p", children: this.parseInlineRun(terminatorRegExp)}];
var start = this.pos;
var children = this.parseInlineRun(terminatorRegExp);
var end = this.pos;
return [{type: "element", tag: "p", children: children, start: start, end: end }];
};
/*
@ -307,7 +310,7 @@ WikiParser.prototype.parseInlineRunUnterminated = function(options) {
while(this.pos < this.sourceLength && nextMatch) {
// Process the text preceding the run rule
if(nextMatch.matchIndex > this.pos) {
this.pushTextWidget(tree,this.source.substring(this.pos,nextMatch.matchIndex));
this.pushTextWidget(tree,this.source.substring(this.pos,nextMatch.matchIndex),this.pos,nextMatch.matchIndex);
this.pos = nextMatch.matchIndex;
}
// Process the run rule
@ -317,7 +320,7 @@ WikiParser.prototype.parseInlineRunUnterminated = function(options) {
}
// Process the remaining text
if(this.pos < this.sourceLength) {
this.pushTextWidget(tree,this.source.substr(this.pos));
this.pushTextWidget(tree,this.source.substr(this.pos),this.pos,this.sourceLength);
}
this.pos = this.sourceLength;
return tree;
@ -337,7 +340,7 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
if(terminatorMatch) {
if(!inlineRuleMatch || inlineRuleMatch.matchIndex >= terminatorMatch.index) {
if(terminatorMatch.index > this.pos) {
this.pushTextWidget(tree,this.source.substring(this.pos,terminatorMatch.index));
this.pushTextWidget(tree,this.source.substring(this.pos,terminatorMatch.index),this.pos,terminatorMatch.index);
}
this.pos = terminatorMatch.index;
if(options.eatTerminator) {
@ -350,7 +353,7 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
if(inlineRuleMatch) {
// Preceding text
if(inlineRuleMatch.matchIndex > this.pos) {
this.pushTextWidget(tree,this.source.substring(this.pos,inlineRuleMatch.matchIndex));
this.pushTextWidget(tree,this.source.substring(this.pos,inlineRuleMatch.matchIndex),this.pos,inlineRuleMatch.matchIndex);
this.pos = inlineRuleMatch.matchIndex;
}
// Process the inline rule
@ -364,7 +367,7 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
}
// Process the remaining text
if(this.pos < this.sourceLength) {
this.pushTextWidget(tree,this.source.substr(this.pos));
this.pushTextWidget(tree,this.source.substr(this.pos),this.pos,this.sourceLength);
}
this.pos = this.sourceLength;
return tree;
@ -373,12 +376,12 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
/*
Push a text widget onto an array, respecting the configTrimWhiteSpace setting
*/
WikiParser.prototype.pushTextWidget = function(array,text) {
WikiParser.prototype.pushTextWidget = function(array,text,start,end) {
if(this.configTrimWhiteSpace) {
text = $tw.utils.trim(text);
}
if(text) {
array.push({type: "text", text: text});
array.push({type: "text", text: text, start: start, end: end});
}
};

View File

@ -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) {

View File

@ -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);

View File

@ -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) {

View File

@ -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
};

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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) {

View File

@ -50,7 +50,7 @@ UploadSaver.prototype.save = function(text,method,callback) {
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");

View File

@ -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"

View File

@ -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");
};
}());

View File

@ -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");
}
};
}());

View File

@ -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);
};
}());

View File

@ -22,7 +22,7 @@ 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: "/";

View File

@ -17,18 +17,16 @@ 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,
read_only: !state.server.isAuthorized("writers",state.authenticatedUsername),
sse_enabled: state.server.get("sse-enabled") === "yes",
space: {
recipe: "default"
},
tiddlywiki_version: $tw.version
});
response.end(text,"utf8");
state.sendResponse(200,{"Content-Type": "application/json"},text,"utf8");
};
}());

View File

@ -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"),
@ -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();

View File

@ -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 = [
@ -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();

View File

@ -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);
@ -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");
};
}());

View File

@ -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) {
@ -30,7 +30,7 @@ exports.handler = function(request,response,state) {
if(fields.revision) {
delete fields.revision;
}
state.wiki.addTiddler(new $tw.Tiddler(state.wiki.getCreationFields(),fields,{title: title},state.wiki.getModificationFields()));
state.wiki.addTiddler(new $tw.Tiddler(fields,{title: title}));
var changeCount = state.wiki.getChangeCount(title).toString();
response.writeHead(204, "OK",{
Etag: "\"default/" + encodeURIComponent(title) + "/" + changeCount + ":\"",

View File

@ -1,70 +0,0 @@
/*\
title: $:/core/modules/server/server-sent-events.js
type: application/javascript
module-type: library
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
/*
parameters:
prefix - usually the plugin path, such as `plugins/tiddlywiki/tiddlyweb`. The
route will match `/events/${prefix}` exactly.
handler - a function that will be called each time a request comes in with the
request and state from the route and an emit function to call.
*/
var ServerSentEvents = function ServerSentEvents(prefix, handler) {
this.handler = handler;
this.prefix = prefix;
};
ServerSentEvents.prototype.getExports = function() {
return {
bodyFormat: "stream",
method: "GET",
path: new RegExp("^/events/" + this.prefix + "$"),
handler: this.handleEventRequest.bind(this)
};
};
ServerSentEvents.prototype.handleEventRequest = function(request,response,state) {
if(ServerSentEvents.prototype.isEventStreamRequest(request)) {
response.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive"
});
this.handler(request,state,this.emit.bind(this,response),this.end.bind(this,response));
} else {
response.writeHead(406,"Not Acceptable",{});
response.end();
}
};
ServerSentEvents.prototype.isEventStreamRequest = function(request) {
return request.headers.accept &&
request.headers.accept.match(/^text\/event-stream/);
};
ServerSentEvents.prototype.emit = function(response,event,data) {
if(typeof event !== "string" || event.indexOf("\n") !== -1) {
throw new Error("Type must be a single-line string");
}
if(typeof data !== "string" || data.indexOf("\n") !== -1) {
throw new Error("Data must be a single-line string");
}
response.write("event: " + event + "\ndata: " + data + "\n\n", "utf8");
};
ServerSentEvents.prototype.end = function(response) {
response.end();
};
exports.ServerSentEvents = ServerSentEvents;
})();

View File

@ -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");
}
/*
@ -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 = {
@ -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",
@ -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) {
@ -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);
@ -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)) {

View File

@ -17,7 +17,7 @@ exports.name = "favicon";
exports.platforms = ["browser"];
exports.after = ["startup"];
exports.synchronous = true;
// Favicon tiddler
var FAVICON_TITLE = "$:/favicon.ico";

View File

@ -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;
}
}

View File

@ -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) {
@ -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)
}
@ -68,7 +69,7 @@ exports.startup = function() {
fullScreenDocument[fullscreen._exitFullscreen]();
} else {
fullScreenDocument.documentElement[fullscreen._requestFullscreen](Element.ALLOW_KEYBOARD_INPUT);
}
}
}
});
}

View File

@ -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"
});
});
});
}
};
@ -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
@ -165,7 +165,7 @@ function openStartupTiddlers(options) {
story.addToHistory(target);
} else if(storyList.length > 0) {
story.addToHistory(storyList[0]);
}
}
}
}

View File

@ -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();
}
@ -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
@ -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;
}
@ -118,4 +118,4 @@ ClassicStoryView.prototype.remove = function(widget) {
exports.classic = ClassicStoryView;
})();
})();

View File

@ -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
@ -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
@ -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;
}

View File

@ -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
@ -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
@ -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;
}

View File

@ -20,7 +20,6 @@ Syncer.prototype.titleIsAnonymous = "$:/status/IsAnonymous";
Syncer.prototype.titleIsReadOnly = "$:/status/IsReadOnly";
Syncer.prototype.titleUserName = "$:/status/UserName";
Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter";
Syncer.prototype.titleSyncDisablePolling = "$:/config/SyncDisablePolling";
Syncer.prototype.titleSyncPollingInterval = "$:/config/SyncPollingInterval";
Syncer.prototype.titleSyncDisableLazyLoading = "$:/config/SyncDisableLazyLoading";
Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done";
@ -90,7 +89,7 @@ function Syncer(options) {
if(filteredChanges.length > 0) {
self.processTaskQueue();
} else {
// Look for deletions of tiddlers we're already syncing
// Look for deletions of tiddlers we're already syncing
var outstandingDeletion = false
$tw.utils.each(changes,function(change,title,object) {
if(change.deleted && $tw.utils.hop(self.tiddlerInfo,title)) {
@ -122,7 +121,7 @@ function Syncer(options) {
self.login(username,password,function() {});
} else {
// No username and password, so we display a prompt
self.handleLoginEvent();
self.handleLoginEvent();
}
});
$tw.rootWidget.addEventListener("tm-logout",function() {
@ -144,7 +143,7 @@ function Syncer(options) {
if(!this.disableUI && this.wiki.getTiddlerText(this.titleSyncDisableLazyLoading) !== "yes") {
this.wiki.addEventListener("lazyLoad",function(title) {
self.handleLazyLoadEvent(title);
});
});
}
// Get the login status
this.getStatus(function(err,isLoggedIn) {
@ -179,8 +178,8 @@ Syncer.prototype.getTiddlerRevision = function(title) {
if(this.syncadaptor && this.syncadaptor.getTiddlerRevision) {
return this.syncadaptor.getTiddlerRevision(title);
} else {
return this.wiki.getTiddler(title).fields.revision;
}
return this.wiki.getTiddler(title).fields.revision;
}
};
/*
@ -273,9 +272,9 @@ Syncer.prototype.getStatus = function(callback) {
// Mark us as not logged in
this.wiki.addTiddler({title: this.titleIsLoggedIn,text: "no"});
// Get login status
this.syncadaptor.getStatus(function(err,isLoggedIn,username,isReadOnly,isAnonymous,isPollingDisabled) {
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"});
@ -284,9 +283,6 @@ Syncer.prototype.getStatus = function(callback) {
if(isLoggedIn) {
self.wiki.addTiddler({title: self.titleUserName,text: username || ""});
}
if(isPollingDisabled) {
self.wiki.addTiddler({title: self.titleSyncDisablePolling, text: "yes"});
}
}
// Invoke the callback
if(callback) {
@ -310,15 +306,12 @@ Syncer.prototype.syncFromServer = function() {
}
},
triggerNextSync = function() {
if(pollingEnabled) {
self.pollTimerId = setTimeout(function() {
self.pollTimerId = null;
self.syncFromServer.call(self);
},self.pollTimerInterval);
}
self.pollTimerId = setTimeout(function() {
self.pollTimerId = null;
self.syncFromServer.call(self);
},self.pollTimerInterval);
},
syncSystemFromServer = (self.wiki.getTiddlerText("$:/config/SyncSystemTiddlersFromServer") === "yes"),
pollingEnabled = (self.wiki.getTiddlerText(self.titleSyncDisablePolling) !== "yes");
syncSystemFromServer = (self.wiki.getTiddlerText("$:/config/SyncSystemTiddlersFromServer") === "yes" ? true : false);
if(this.syncadaptor && this.syncadaptor.getUpdatedTiddlers) {
this.logger.log("Retrieving updated tiddler list");
cancelNextSync();
@ -341,7 +334,7 @@ Syncer.prototype.syncFromServer = function() {
});
if(updates.modifications.length > 0 || updates.deletions.length > 0) {
self.processTaskQueue();
}
}
}
});
} else if(this.syncadaptor && this.syncadaptor.getSkinnyTiddlers) {
@ -484,7 +477,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();
}
@ -521,7 +514,7 @@ Syncer.prototype.processTaskQueue = function() {
} else {
self.updateDirtyStatus();
// Process the next task
self.processTaskQueue.call(self);
self.processTaskQueue.call(self);
}
});
} else {
@ -529,11 +522,11 @@ Syncer.prototype.processTaskQueue = function() {
this.updateDirtyStatus();
// And trigger a timeout if there is a pending task
if(task === true) {
this.triggerTimeout();
this.triggerTimeout();
}
}
} else {
this.updateDirtyStatus();
this.updateDirtyStatus();
}
};
@ -567,7 +560,7 @@ Syncer.prototype.chooseNextTask = function() {
isReadyToSave = !tiddlerInfo || !tiddlerInfo.timestampLastSaved || tiddlerInfo.timestampLastSaved < thresholdLastSaved;
if(hasChanged) {
if(isReadyToSave) {
return new SaveTiddlerTask(this,title);
return new SaveTiddlerTask(this,title);
} else {
havePending = true;
}

View File

@ -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) {
@ -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++) {

View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More