The TiddlyWiki5 repository contains several scripts in the bin folder that you can use to automate common tasks, or as a useful starting point for your own scripts. See Scripts for building tiddlywiki.com for details of the scripts used to build and release http://tiddlywiki.com/.
All the scripts expect to be run from the root folder of the repository.
serve: serves tw5.com
./bin/serve.sh -h
+
Script Files
The TiddlyWiki5 repository contains several scripts in the bin folder that you can use to automate common tasks, or as a useful starting point for your own scripts. See Scripts for building tiddlywiki.com for details of the scripts used to build and release https://tiddlywiki.com/.
All the scripts expect to be run from the root folder of the repository.
This script starts TiddlyWiki5 running as an HTTP server, defaulting to the content from the tw5.com-server edition. By default, the Node.js serves on port 8080. If the optional username parameter is provided, it is used for signing edits. If the password is provided then HTTP basic authentication is used. Run the script with the -h parameter to see online help.
To experiment with this configuration, run the script and then visit http://127.0.0.1:8080 in a browser.
Changes made in the browser propagate to the server over HTTP (use the browser developer console to see these requests). The server then syncs changes to the file system (and logs each change to the screen).
test: build and run tests
This script runs the test edition of TiddlyWiki on the server to perform the server-side tests and to build test.html for running the tests in the browser.
lazy: serves tw5.com with lazily loaded images
./bin/lazy.sh <username> [<password>]
Or:
./bin/lazy.cmd <username> [<password>]
This script serves the tw5.com-server edition content with LazyLoading applied to images.
This script builds TiddlyWiki 2.6.5 from the original source and then displays the differences between them (diff is used for *nix, fc for Windows).
\ No newline at end of file
+./bin/serve.cmd [edition dir] [username] [password] [host] [port]
This script starts TiddlyWiki5 running as an HTTP server, defaulting to the content from the tw5.com-server edition. By default, the Node.js serves on port 8080. If the optional username parameter is provided, it is used for signing edits. If the password is provided then HTTP basic authentication is used. Run the script with the -h parameter to see online help.
To experiment with this configuration, run the script and then visit http://127.0.0.1:8080 in a browser.
Changes made in the browser propagate to the server over HTTP (use the browser developer console to see these requests). The server then syncs changes to the file system (and logs each change to the screen).
test: build and run tests
This script runs the test edition of TiddlyWiki on the server to perform the server-side tests and to build test.html for running the tests in the browser.
lazy: serves tw5.com with lazily loaded images
./bin/lazy.sh <username> [<password>]
Or:
./bin/lazy.cmd <username> [<password>]
This script serves the tw5.com-server edition content with LazyLoading applied to images.
Like other OpenSource projects, TiddlyWiki5 needs a signed contributor license agreement from individual contributors. This is a legal agreement that allows contributors to assert that they own the copyright of their contribution, and that they agree to license it to the UnaMesa Association (the legal entity that owns TiddlyWiki on behalf of the community).
Click the "edit" button at the top-right corner (clicking this button will fork the project so you can edit the file)
Add your name at the bottom
eg: Jeremy Ruston, @Jermolene, 2011/11/22
Below the edit box for the CLA text you should see a box labelled Propose file change
Enter a brief title to explain the change (eg, "Signing the CLA")
Click the green button labelled Propose file change
On the following screen, click the green button labelled Create pull request
The CLA documents used for this project were created using Harmony Project Templates. "HA-CLA-I-LIST Version 1.0" for "CLA-individual" and "HA-CLA-E-LIST Version 1.0" for "CLA-entity".
Like other OpenSource projects, TiddlyWiki5 needs a signed contributor license agreement from individual contributors. This is a legal agreement that allows contributors to assert that they own the copyright of their contribution, and that they agree to license it to the UnaMesa Association (the legal entity that owns TiddlyWiki on behalf of the community).
Click the "edit" button at the top-right corner (clicking this button will fork the project so you can edit the file)
Add your name at the bottom
eg: Jeremy Ruston, @Jermolene, 2011/11/22
Below the edit box for the CLA text you should see a box labelled Propose file change
Enter a brief title to explain the change (eg, "Signing the CLA")
Click the green button labelled Propose file change
On the following screen, click the green button labelled Create pull request
The CLA documents used for this project were created using Harmony Project Templates. "HA-CLA-I-LIST Version 1.0" for "CLA-individual" and "HA-CLA-E-LIST Version 1.0" for "CLA-entity".
Remarks
----—
When not owning the copyright in the entire work of authorship**
In this case, please clearly state so, since otherwise we assume that you are the legal copyright holder of the contributed work! Please provide links and additional information that clarify under which license the rest of the code is distributed.
-
This file was automatically generated by TiddlyWiki5
+
This file was automatically generated by TiddlyWiki5
Loading external text from ''<$text text={{!!_canonical_uri}}/>''
If this message doesn't disappear you may be using a browser that doesn't support external text in this configuration. See http://tiddlywiki.com/#ExternalText
+LazyLoadingWarning:
Trying to load external content from ''<$text text={{!!_canonical_uri}}/>''
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
LoginToTiddlySpace: Login to TiddlySpace
-MissingTiddler/Hint: Missing tiddler "<$text text=<>/>" - click {{$:/core/images/edit-button}} to create
+Manager/Controls/FilterByTag/None: (none)
+Manager/Controls/FilterByTag/Prompt: Filter by tag:
+Manager/Controls/Order/Prompt: Reverse order
+Manager/Controls/Search/Placeholder: Search
+Manager/Controls/Search/Prompt: Search:
+Manager/Controls/Show/Option/Tags: tags
+Manager/Controls/Show/Option/Tiddlers: tiddlers
+Manager/Controls/Show/Prompt: Show:
+Manager/Controls/Sort/Prompt: Sort by:
+Manager/Item/Colour: Colour
+Manager/Item/Fields: Fields
+Manager/Item/Icon/None: (none)
+Manager/Item/Icon: Icon
+Manager/Item/RawText: Raw text
+Manager/Item/Tags: Tags
+Manager/Item/Tools: Tools
+Manager/Item/WikifiedText: Wikified text
+MissingTiddler/Hint: Missing tiddler "<$text text=<>/>" -- click {{||$:/core/ui/Buttons/edit}} to create
No: No
OfficialPluginLibrary: Official ~TiddlyWiki Plugin Library
OfficialPluginLibrary/Hint: The official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
PluginReloadWarning: Please save {{$:/core/ui/Buttons/save-wiki}} and reload {{$:/core/ui/Buttons/refresh}} to allow changes to plugins to take effect
RecentChanges/DateFormat: DDth MMM YYYY
SystemTiddler/Tooltip: This is a system tiddler
+SystemTiddlers/Include/Prompt: Include system tiddlers
TagManager/Colour/Heading: Colour
TagManager/Count/Heading: Count
TagManager/Icon/Heading: Icon
diff --git a/core/language/en-GB/Modals/Download.tid b/core/language/en-GB/Modals/Download.tid
index 49ea95d59..884ad152d 100644
--- a/core/language/en-GB/Modals/Download.tid
+++ b/core/language/en-GB/Modals/Download.tid
@@ -1,8 +1,7 @@
title: $:/language/Modals/Download
-type: text/vnd.tiddlywiki
subtitle: Download changes
footer: <$button message="tm-close-tiddler">Close$button>
-help: http://tiddlywiki.com/static/DownloadingChanges.html
+help: https://tiddlywiki.com/static/DownloadingChanges.html
Your browser only supports manual saving.
diff --git a/core/language/en-GB/Modals/SaveInstructions.tid b/core/language/en-GB/Modals/SaveInstructions.tid
index 61f46dea0..45028fc45 100644
--- a/core/language/en-GB/Modals/SaveInstructions.tid
+++ b/core/language/en-GB/Modals/SaveInstructions.tid
@@ -1,8 +1,7 @@
title: $:/language/Modals/SaveInstructions
-type: text/vnd.tiddlywiki
subtitle: Save your work
footer: <$button message="tm-close-tiddler">Close$button>
-help: http://tiddlywiki.com/static/SavingChanges.html
+help: https://tiddlywiki.com/static/SavingChanges.html
Your changes to this wiki need to be saved as a ~TiddlyWiki HTML file.
diff --git a/core/language/en-GB/NewJournal.multids b/core/language/en-GB/NewJournal.multids
index ff566fabe..b7a3f5e62 100644
--- a/core/language/en-GB/NewJournal.multids
+++ b/core/language/en-GB/NewJournal.multids
@@ -1,4 +1,5 @@
title: $:/config/NewJournal/
Title: DDth MMM YYYY
+Text:
Tags: Journal
diff --git a/core/language/en-GB/Notifications.multids b/core/language/en-GB/Notifications.multids
index 0d9b891bd..df095b828 100644
--- a/core/language/en-GB/Notifications.multids
+++ b/core/language/en-GB/Notifications.multids
@@ -2,3 +2,5 @@ title: $:/language/Notifications/
Save/Done: Saved wiki
Save/Starting: Starting to save wiki
+CopiedToClipboard/Succeeded: Copied!
+CopiedToClipboard/Failed: Failed to copy to clipboard!
diff --git a/core/language/en-GB/Search.multids b/core/language/en-GB/Search.multids
index fd6eadc7e..2a57a6416 100644
--- a/core/language/en-GB/Search.multids
+++ b/core/language/en-GB/Search.multids
@@ -2,12 +2,13 @@ title: $:/language/Search/
DefaultResults/Caption: List
Filter/Caption: Filter
-Filter/Hint: Search via a [[filter expression|http://tiddlywiki.com/static/Filters.html]]
+Filter/Hint: Search via a [[filter expression|https://tiddlywiki.com/static/Filters.html]]
Filter/Matches: //<> matches//
Matches: //<> matches//
Matches/All: All matches:
Matches/Title: Title matches:
Search: Search
+Search/TooShort: Search text too short
Shadows/Caption: Shadows
Shadows/Hint: Search for shadow tiddlers
Shadows/Matches: //<> matches//
diff --git a/core/language/en-GB/SideBar.multids b/core/language/en-GB/SideBar.multids
index 1ae4439dd..b109f3a9b 100644
--- a/core/language/en-GB/SideBar.multids
+++ b/core/language/en-GB/SideBar.multids
@@ -3,6 +3,7 @@ title: $:/language/SideBar/
All/Caption: All
Contents/Caption: Contents
Drafts/Caption: Drafts
+Explorer/Caption: Explorer
Missing/Caption: Missing
More/Caption: More
Open/Caption: Open
diff --git a/core/language/en-GB/ThemeTweaks.multids b/core/language/en-GB/ThemeTweaks.multids
index 694d56b13..5168053c3 100644
--- a/core/language/en-GB/ThemeTweaks.multids
+++ b/core/language/en-GB/ThemeTweaks.multids
@@ -7,11 +7,12 @@ Options/SidebarLayout: Sidebar layout
Options/SidebarLayout/Fixed-Fluid: Fixed story, fluid sidebar
Options/SidebarLayout/Fluid-Fixed: Fluid story, fixed sidebar
Options/StickyTitles: Sticky titles
-Options/StickyTitles/Hint: Causes tiddler titles to "stick" to the top of the browser window. Caution: Does not work at all with Chrome, and causes some layout issues in Firefox
+Options/StickyTitles/Hint: Causes tiddler titles to "stick" to the top of the browser window
Options/CodeWrapping: Wrap long lines in code blocks
Settings: Settings
Settings/FontFamily: Font family
Settings/CodeFontFamily: Code font family
+Settings/EditorFontFamily: Editor font family
Settings/BackgroundImage: Page background image
Settings/BackgroundImageAttachment: Page background image attachment
Settings/BackgroundImageAttachment/Scroll: Scroll with tiddlers
diff --git a/core/language/en-GB/Types/application_javascript.tid b/core/language/en-GB/Types/application_javascript.tid
index fda69c6e1..28ee6ddbf 100644
--- a/core/language/en-GB/Types/application_javascript.tid
+++ b/core/language/en-GB/Types/application_javascript.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/application/javascript
description: JavaScript code
name: application/javascript
group: Developer
+group-sort: 2
diff --git a/core/language/en-GB/Types/application_json.tid b/core/language/en-GB/Types/application_json.tid
index 27b129821..7f7575599 100644
--- a/core/language/en-GB/Types/application_json.tid
+++ b/core/language/en-GB/Types/application_json.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/application/json
description: JSON data
name: application/json
group: Developer
+group-sort: 2
diff --git a/core/language/en-GB/Types/application_x_tiddler_dictionary.tid b/core/language/en-GB/Types/application_x_tiddler_dictionary.tid
index 13ab6943e..76fbec0a3 100644
--- a/core/language/en-GB/Types/application_x_tiddler_dictionary.tid
+++ b/core/language/en-GB/Types/application_x_tiddler_dictionary.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/application/x-tiddler-dictionary
description: Data dictionary
name: application/x-tiddler-dictionary
group: Developer
+group-sort: 2
diff --git a/core/language/en-GB/Types/image_gif.tid b/core/language/en-GB/Types/image_gif.tid
index bca7d7d28..559d17f56 100644
--- a/core/language/en-GB/Types/image_gif.tid
+++ b/core/language/en-GB/Types/image_gif.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/image/gif
description: GIF image
name: image/gif
group: Image
+group-sort: 1
diff --git a/core/language/en-GB/Types/image_jpeg.tid b/core/language/en-GB/Types/image_jpeg.tid
index 943f19341..5db4dc001 100644
--- a/core/language/en-GB/Types/image_jpeg.tid
+++ b/core/language/en-GB/Types/image_jpeg.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/image/jpeg
description: JPEG image
name: image/jpeg
group: Image
+group-sort: 1
diff --git a/core/language/en-GB/Types/image_png.tid b/core/language/en-GB/Types/image_png.tid
index 59fb3f865..d9e78bf0e 100644
--- a/core/language/en-GB/Types/image_png.tid
+++ b/core/language/en-GB/Types/image_png.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/image/png
description: PNG image
name: image/png
group: Image
+group-sort: 1
diff --git a/core/language/en-GB/Types/image_svg_xml.tid b/core/language/en-GB/Types/image_svg_xml.tid
index ddb6912dd..9f7c23ba3 100644
--- a/core/language/en-GB/Types/image_svg_xml.tid
+++ b/core/language/en-GB/Types/image_svg_xml.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/image/svg+xml
description: Structured Vector Graphics image
name: image/svg+xml
group: Image
+group-sort: 1
diff --git a/core/language/en-GB/Types/image_x-icon.tid b/core/language/en-GB/Types/image_x-icon.tid
index ff8d12f81..6ae32331c 100644
--- a/core/language/en-GB/Types/image_x-icon.tid
+++ b/core/language/en-GB/Types/image_x-icon.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/image/x-icon
description: ICO format icon file
name: image/x-icon
group: Image
+group-sort: 1
diff --git a/core/language/en-GB/Types/text_css.tid b/core/language/en-GB/Types/text_css.tid
index 15785f0a0..9539e21d0 100644
--- a/core/language/en-GB/Types/text_css.tid
+++ b/core/language/en-GB/Types/text_css.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/text/css
description: Static stylesheet
name: text/css
group: Developer
+group-sort: 2
diff --git a/core/language/en-GB/Types/text_html.tid b/core/language/en-GB/Types/text_html.tid
index cd8d8221e..260f3a30b 100644
--- a/core/language/en-GB/Types/text_html.tid
+++ b/core/language/en-GB/Types/text_html.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/text/html
description: HTML markup
name: text/html
group: Text
+group-sort: 0
diff --git a/core/language/en-GB/Types/text_plain.tid b/core/language/en-GB/Types/text_plain.tid
index 83da10bc8..1511e07ff 100644
--- a/core/language/en-GB/Types/text_plain.tid
+++ b/core/language/en-GB/Types/text_plain.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/text/plain
description: Plain text
name: text/plain
group: Text
+group-sort: 0
diff --git a/core/language/en-GB/Types/text_vnd.tiddlywiki.tid b/core/language/en-GB/Types/text_vnd.tiddlywiki.tid
index e946d9a4a..9546e1aed 100644
--- a/core/language/en-GB/Types/text_vnd.tiddlywiki.tid
+++ b/core/language/en-GB/Types/text_vnd.tiddlywiki.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/text/vnd.tiddlywiki
description: TiddlyWiki 5
name: text/vnd.tiddlywiki
group: Text
+group-sort: 0
diff --git a/core/language/en-GB/Types/text_x-tiddlywiki.tid b/core/language/en-GB/Types/text_x-tiddlywiki.tid
index e593430ee..e9c6c7097 100644
--- a/core/language/en-GB/Types/text_x-tiddlywiki.tid
+++ b/core/language/en-GB/Types/text_x-tiddlywiki.tid
@@ -2,3 +2,4 @@ title: $:/language/Docs/Types/text/x-tiddlywiki
description: TiddlyWiki Classic
name: text/x-tiddlywiki
group: Text
+group-sort: 0
diff --git a/core/modules/commander.js b/core/modules/commander.js
index 6c4f687e7..ad7f4b5b3 100644
--- a/core/modules/commander.js
+++ b/core/modules/commander.js
@@ -29,6 +29,24 @@ var Commander = function(commandTokens,callback,wiki,streams) {
this.outputPath = path.resolve($tw.boot.wikiPath,$tw.config.wikiOutputSubDir);
};
+/*
+Log a string if verbose flag is set
+*/
+Commander.prototype.log = function(str) {
+ if(this.verbose) {
+ this.streams.output.write(str + "\n");
+ }
+};
+
+/*
+Write a string if verbose flag is set
+*/
+Commander.prototype.write = function(str) {
+ if(this.verbose) {
+ this.streams.output.write(str);
+ }
+};
+
/*
Add a string of tokens to the command queue
*/
diff --git a/core/modules/commands/fetch.js b/core/modules/commands/fetch.js
new file mode 100644
index 000000000..8abd0b1bb
--- /dev/null
+++ b/core/modules/commands/fetch.js
@@ -0,0 +1,175 @@
+/*\
+title: $:/core/modules/commands/fetch.js
+type: application/javascript
+module-type: command
+
+Commands to fetch external tiddlers
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+exports.info = {
+ name: "fetch",
+ synchronous: false
+};
+
+var Command = function(params,commander,callback) {
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ if(this.params.length < 2) {
+ return "Missing subcommand and url";
+ }
+ switch(this.params[0]) {
+ case "raw-file":
+ return this.fetchFiles({
+ raw: true,
+ url: this.params[1],
+ transformFilter: this.params[2] || "",
+ callback: this.callback
+ });
+ break;
+ case "file":
+ return this.fetchFiles({
+ url: this.params[1],
+ importFilter: this.params[2],
+ transformFilter: this.params[3] || "",
+ callback: this.callback
+ });
+ break;
+ case "raw-files":
+ return this.fetchFiles({
+ raw: true,
+ urlFilter: this.params[1],
+ transformFilter: this.params[2] || "",
+ callback: this.callback
+ });
+ break;
+ case "files":
+ return this.fetchFiles({
+ urlFilter: this.params[1],
+ importFilter: this.params[2],
+ transformFilter: this.params[3] || "",
+ callback: this.callback
+ });
+ break;
+ }
+ return null;
+};
+
+Command.prototype.fetchFiles = function(options) {
+ var self = this;
+ // Get the list of URLs
+ var urls;
+ if(options.url) {
+ urls = [options.url]
+ } else if(options.urlFilter) {
+ urls = $tw.wiki.filterTiddlers(options.urlFilter);
+ } else {
+ return "Missing URL";
+ }
+ // Process each URL in turn
+ var next = 0;
+ var getNextFile = function(err) {
+ if(err) {
+ return options.callback(err);
+ }
+ if(next < urls.length) {
+ self.fetchFile(urls[next++],options,getNextFile);
+ } else {
+ options.callback(null);
+ }
+ };
+ getNextFile(null);
+ // Success
+ return null;
+};
+
+Command.prototype.fetchFile = function(url,options,callback,redirectCount) {
+ if(redirectCount > 10) {
+ return callback("Error too many redirects retrieving " + url);
+ }
+ var self = this,
+ lib = url.substr(0,8) === "https://" ? require("https") : require("http");
+ lib.get(url).on("response",function(response) {
+ var type = (response.headers["content-type"] || "").split(";")[0],
+ data = [];
+ self.commander.write("Reading " + url + ": ");
+ response.on("data",function(chunk) {
+ data.push(chunk);
+ self.commander.write(".");
+ });
+ response.on("end",function() {
+ self.commander.write("\n");
+ if(response.statusCode === 200) {
+ self.processBody(Buffer.concat(data),type,options,url);
+ callback(null);
+ } else {
+ 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)
+ }
+ }
+ });
+ response.on("error",function(e) {
+ console.log("Error on GET request: " + e);
+ callback(e);
+ });
+ });
+ return null;
+};
+
+Command.prototype.processBody = function(body,type,options,url) {
+ var self = this;
+ // Collect the tiddlers in a wiki
+ var incomingWiki = new $tw.Wiki();
+ if(options.raw) {
+ var typeInfo = type ? $tw.config.contentTypeInfo[type] : null,
+ encoding = typeInfo ? typeInfo.encoding : "utf8";
+ incomingWiki.addTiddler(new $tw.Tiddler({
+ title: url,
+ type: type,
+ text: body.toString(encoding)
+ }));
+ } else {
+ // Deserialise the file to extract the tiddlers
+ var tiddlers = this.commander.wiki.deserializeTiddlers(type || "text/html",body.toString("utf8"),{});
+ $tw.utils.each(tiddlers,function(tiddler) {
+ incomingWiki.addTiddler(new $tw.Tiddler(tiddler));
+ });
+ }
+ // Filter the tiddlers to select the ones we want
+ var filteredTitles = incomingWiki.filterTiddlers(options.importFilter || "[all[tiddlers]]");
+ // Import the selected tiddlers
+ var count = 0;
+ incomingWiki.each(function(tiddler,title) {
+ if(filteredTitles.indexOf(title) !== -1) {
+ var newTiddler;
+ if(options.transformFilter) {
+ var transformedTitle = (incomingWiki.filterTiddlers(options.transformFilter,null,self.commander.wiki.makeTiddlerIterator([title])) || [""])[0];
+ if(transformedTitle) {
+ self.commander.log("Importing " + title + " as " + transformedTitle)
+ newTiddler = new $tw.Tiddler(tiddler,{title: transformedTitle});
+ }
+ } else {
+ self.commander.log("Importing " + title)
+ newTiddler = tiddler;
+ }
+ self.commander.wiki.importTiddler(newTiddler);
+ count++;
+ }
+ });
+ self.commander.log("Imported " + count + " tiddlers")
+};
+
+exports.Command = Command;
+
+})();
diff --git a/core/modules/commands/import.js b/core/modules/commands/import.js
new file mode 100644
index 000000000..9465c3da1
--- /dev/null
+++ b/core/modules/commands/import.js
@@ -0,0 +1,48 @@
+/*\
+title: $:/core/modules/commands/import.js
+type: application/javascript
+module-type: command
+
+Command to import tiddlers from a file
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+exports.info = {
+ name: "import",
+ synchronous: true
+};
+
+var Command = function(params,commander,callback) {
+ this.params = params;
+ this.commander = commander;
+ this.callback = callback;
+};
+
+Command.prototype.execute = function() {
+ var self = this,
+ fs = require("fs"),
+ path = require("path");
+ if(this.params.length < 2) {
+ return "Missing parameters";
+ }
+ var filename = self.params[0],
+ deserializer = self.params[1],
+ title = self.params[2] || filename,
+ encoding = self.params[3] || "utf8",
+ text = fs.readFileSync(filename,encoding),
+ tiddlers = this.commander.wiki.deserializeTiddlers(null,text,{title: title},{deserializer: deserializer});
+ $tw.utils.each(tiddlers,function(tiddler) {
+ self.commander.wiki.importTiddler(new $tw.Tiddler(tiddler));
+ });
+ this.commander.log(tiddlers.length + " tiddler(s) imported");
+ return null;
+};
+
+exports.Command = Command;
+
+})();
diff --git a/core/modules/commands/load.js b/core/modules/commands/load.js
index 95cee01be..230a253d5 100644
--- a/core/modules/commands/load.js
+++ b/core/modules/commands/load.js
@@ -3,7 +3,7 @@ title: $:/core/modules/commands/load.js
type: application/javascript
module-type: command
-Command to load tiddlers from a file
+Command to load tiddlers from a file or directory
\*/
(function(){
@@ -30,24 +30,19 @@ Command.prototype.execute = function() {
if(this.params.length < 1) {
return "Missing filename";
}
- var ext = path.extname(self.params[0]);
- fs.readFile(this.params[0],$tw.utils.getTypeEncoding(ext),function(err,data) {
- if (err) {
- self.callback(err);
- } else {
- var fields = {title: self.params[0]},
- type = path.extname(self.params[0]);
- var tiddlers = self.commander.wiki.deserializeTiddlers(type,data,fields);
- if(!tiddlers) {
- self.callback("No tiddlers found in file \"" + self.params[0] + "\"");
- } else {
- for(var t=0; t 0);
+ this.editShowToolbar = (this.editShowToolbar === "yes") && !!(this.children && this.children.length > 0) && (!this.document.isTiddlyWikiFakeDom);
};
/*
diff --git a/core/modules/editor/operations/bitmap/rotate-left.js b/core/modules/editor/operations/bitmap/rotate-left.js
new file mode 100644
index 000000000..6e1b15d3e
--- /dev/null
+++ b/core/modules/editor/operations/bitmap/rotate-left.js
@@ -0,0 +1,24 @@
+/*\
+title: $:/core/modules/editor/operations/bitmap/rotate-left.js
+type: application/javascript
+module-type: bitmapeditoroperation
+
+Bitmap editor operation to rotate the image left by 90 degrees
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+exports["rotate-left"] = function(event) {
+ // Rotate the canvas left by 90 degrees
+ this.rotateCanvasLeft();
+ // Update the input controls
+ this.refreshToolbar();
+ // Save the image into the tiddler
+ this.saveChanges();
+};
+
+})();
diff --git a/core/modules/filters.js b/core/modules/filters.js
index 176316189..76046b828 100644
--- a/core/modules/filters.js
+++ b/core/modules/filters.js
@@ -20,7 +20,7 @@ Parses an operation (i.e. a run) within a filter string
Returns the new start position, after the parsed operation
*/
function parseFilterOperation(operators,filterString,p) {
- var operator, operand, bracketPos, curlyBracketPos;
+ var nextBracketPos, operator;
// Skip the starting square bracket
if(filterString.charAt(p++) !== "[") {
throw "Missing [ in filter expression";
@@ -33,14 +33,14 @@ function parseFilterOperation(operators,filterString,p) {
operator.prefix = filterString.charAt(p++);
}
// Get the operator name
- var nextBracketPos = filterString.substring(p).search(/[\[\{<\/]/);
+ nextBracketPos = filterString.substring(p).search(/[\[\{<\/]/);
if(nextBracketPos === -1) {
throw "Missing [ in filter expression";
}
nextBracketPos += p;
var bracket = filterString.charAt(nextBracketPos);
operator.operator = filterString.substring(p,nextBracketPos);
-
+
// Any suffix?
var colon = operator.operator.indexOf(':');
if(colon > -1) {
@@ -79,7 +79,7 @@ console.log("WARNING: Filter",operator.operator,"has a deprecated regexp operand
}
break;
}
-
+
if(nextBracketPos === -1) {
throw "Missing closing bracket in filter expression";
}
@@ -87,7 +87,7 @@ console.log("WARNING: Filter",operator.operator,"has a deprecated regexp operand
operator.operand = filterString.substring(p,nextBracketPos);
}
p = nextBracketPos + 1;
-
+
// Push this operator
operators.push(operator);
} while(filterString.charAt(p) !== "]");
diff --git a/core/modules/filters/all/tags.js b/core/modules/filters/all/tags.js
new file mode 100644
index 000000000..2aaa9dec2
--- /dev/null
+++ b/core/modules/filters/all/tags.js
@@ -0,0 +1,22 @@
+/*\
+title: $:/core/modules/filters/all/tags.js
+type: application/javascript
+module-type: allfilteroperator
+
+Filter function for [all[tags]]
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.tags = function(source,prefix,options) {
+ return Object.keys(options.wiki.getTagMap());
+};
+
+})();
diff --git a/core/modules/filters/count.js b/core/modules/filters/count.js
new file mode 100644
index 000000000..638b135df
--- /dev/null
+++ b/core/modules/filters/count.js
@@ -0,0 +1,26 @@
+/*\
+title: $:/core/modules/filters/count.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator returning the number of entries in the current list.
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.count = function(source,operator,options) {
+ var count = 0;
+ source(function(tiddler,title) {
+ count++;
+ });
+ return [count + ""];
+};
+
+})();
diff --git a/core/modules/filters/each.js b/core/modules/filters/each.js
index 25e852638..5c2c29077 100644
--- a/core/modules/filters/each.js
+++ b/core/modules/filters/each.js
@@ -18,18 +18,34 @@ Export our filter function
*/
exports.each = function(source,operator,options) {
var results =[] ,
- value,values = {},
- field = operator.operand || "title";
- if(operator.suffix !== "list-item") {
+ value,values = {},
+ field = operator.operand || "title";
+ if(operator.suffix === "value" && field === "title") {
source(function(tiddler,title) {
- if(tiddler) {
- value = (field === "title") ? title : tiddler.getFieldString(field);
- if(!$tw.utils.hop(values,value)) {
- values[value] = true;
- results.push(title);
- }
+ if(!$tw.utils.hop(values,title)) {
+ values[title] = true;
+ results.push(title);
}
});
+ } else if(operator.suffix !== "list-item") {
+ if(field === "title") {
+ source(function(tiddler,title) {
+ if(tiddler && !$tw.utils.hop(values,title)) {
+ values[title] = true;
+ results.push(title);
+ }
+ });
+ } else {
+ source(function(tiddler,title) {
+ if(tiddler) {
+ value = tiddler.getFieldString(field);
+ if(!$tw.utils.hop(values,value)) {
+ values[value] = true;
+ results.push(title);
+ }
+ }
+ });
+ }
} else {
source(function(tiddler,title) {
if(tiddler) {
diff --git a/core/modules/filters/encodings.js b/core/modules/filters/encodings.js
new file mode 100644
index 000000000..b55911cdd
--- /dev/null
+++ b/core/modules/filters/encodings.js
@@ -0,0 +1,91 @@
+/*\
+title: $:/core/modules/filters/decodeuricomponent.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator for applying decodeURIComponent() to each item.
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter functions
+*/
+
+exports.decodeuricomponent = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(decodeURIComponent(title));
+ });
+ return results;
+};
+
+exports.encodeuricomponent = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(encodeURIComponent(title));
+ });
+ return results;
+};
+
+exports.decodeuri = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(decodeURI(title));
+ });
+ return results;
+};
+
+exports.encodeuri = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(encodeURI(title));
+ });
+ return results;
+};
+
+exports.decodehtml = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push($tw.utils.htmlDecode(title));
+ });
+ return results;
+};
+
+exports.encodehtml = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push($tw.utils.htmlEncode(title));
+ });
+ return results;
+};
+
+exports.stringify = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push($tw.utils.stringify(title));
+ });
+ return results;
+};
+
+exports.jsonstringify = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push($tw.utils.jsonStringify(title));
+ });
+ return results;
+};
+
+exports.escaperegexp = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push($tw.utils.escapeRegExp(title));
+ });
+ return results;
+};
+
+})();
diff --git a/core/modules/filters/enlist.js b/core/modules/filters/enlist.js
new file mode 100644
index 000000000..c5e67d54c
--- /dev/null
+++ b/core/modules/filters/enlist.js
@@ -0,0 +1,33 @@
+/*\
+title: $:/core/modules/filters/enlist.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator returning its operand parsed as a list
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.enlist = function(source,operator,options) {
+ var list = $tw.utils.parseStringArray(operator.operand);
+ if(operator.prefix === "!") {
+ var results = [];
+ source(function(tiddler,title) {
+ if(list.indexOf(title) === -1) {
+ results.push(title);
+ }
+ });
+ return results;
+ } else {
+ return list;
+ }
+};
+
+})();
diff --git a/core/modules/filters/has.js b/core/modules/filters/has.js
index 7a1738782..d05da4113 100644
--- a/core/modules/filters/has.js
+++ b/core/modules/filters/has.js
@@ -16,19 +16,37 @@ Filter operator for checking if a tiddler has the specified field
Export our filter function
*/
exports.has = function(source,operator,options) {
- var results = [];
- if(operator.prefix === "!") {
- source(function(tiddler,title) {
- if(!tiddler || (tiddler && (!$tw.utils.hop(tiddler.fields,operator.operand) || tiddler.fields[operator.operand] === ""))) {
- results.push(title);
- }
- });
+ var results = [],
+ invert = operator.prefix === "!";
+
+ if(operator.suffix === "field") {
+ if(invert) {
+ source(function(tiddler,title) {
+ if(!tiddler || (tiddler && (!$tw.utils.hop(tiddler.fields,operator.operand)))) {
+ results.push(title);
+ }
+ });
+ } else {
+ source(function(tiddler,title) {
+ if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand)) {
+ results.push(title);
+ }
+ });
+ }
} else {
- source(function(tiddler,title) {
- if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand) && !(tiddler.fields[operator.operand] === "" || tiddler.fields[operator.operand].length === 0)) {
- results.push(title);
- }
- });
+ if(invert) {
+ source(function(tiddler,title) {
+ if(!tiddler || !$tw.utils.hop(tiddler.fields,operator.operand) || (tiddler.fields[operator.operand] === "")) {
+ results.push(title);
+ }
+ });
+ } else {
+ source(function(tiddler,title) {
+ if(tiddler && $tw.utils.hop(tiddler.fields,operator.operand) && !(tiddler.fields[operator.operand] === "" || tiddler.fields[operator.operand].length === 0)) {
+ results.push(title);
+ }
+ });
+ }
}
return results;
};
diff --git a/core/modules/filters/insertbefore.js b/core/modules/filters/insertbefore.js
new file mode 100644
index 000000000..6fa9728f9
--- /dev/null
+++ b/core/modules/filters/insertbefore.js
@@ -0,0 +1,41 @@
+/*\
+title: $:/core/modules/filters/insertbefore.js
+type: application/javascript
+module-type: filteroperator
+
+Insert an item before another item in a list
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Order a list
+*/
+exports.insertbefore = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(title);
+ });
+ var target = options.widget && options.widget.getVariable(operator.suffix || "currentTiddler");
+ if(target !== operator.operand) {
+ // Remove the entry from the list if it is present
+ var pos = results.indexOf(operator.operand);
+ if(pos !== -1) {
+ results.splice(pos,1);
+ }
+ // Insert the entry before the target marker
+ pos = results.indexOf(target);
+ if(pos !== -1) {
+ results.splice(pos,0,operator.operand);
+ } else {
+ results.push(operator.operand);
+ }
+ }
+ return results;
+};
+
+})();
diff --git a/core/modules/filters/is.js b/core/modules/filters/is.js
index 0db243044..b75943786 100644
--- a/core/modules/filters/is.js
+++ b/core/modules/filters/is.js
@@ -28,12 +28,21 @@ Export our filter function
exports.is = function(source,operator,options) {
// Dispatch to the correct isfilteroperator
var isFilterOperators = getIsFilterOperators();
- var isFilterOperator = isFilterOperators[operator.operand];
- if(isFilterOperator) {
- return isFilterOperator(source,operator.prefix,options);
+ if(operator.operand) {
+ var isFilterOperator = isFilterOperators[operator.operand];
+ if(isFilterOperator) {
+ return isFilterOperator(source,operator.prefix,options);
+ } else {
+ return [$tw.language.getString("Error/IsFilterOperator")];
+ }
} else {
- return [$tw.language.getString("Error/IsFilterOperator")];
+ // Return all tiddlers if the operand is missing
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(title);
+ });
+ return results;
}
};
-})();
+})();
\ No newline at end of file
diff --git a/core/modules/filters/listops.js b/core/modules/filters/listops.js
index 58f74f092..0c30c22e7 100644
--- a/core/modules/filters/listops.js
+++ b/core/modules/filters/listops.js
@@ -12,6 +12,23 @@ Filter operators for manipulating the current selection list
/*global $tw: false */
"use strict";
+/*
+Order a list
+*/
+exports.order = function(source,operator,options) {
+ var results = [];
+ if(operator.operand.toLowerCase() === "reverse") {
+ source(function(tiddler,title) {
+ results.unshift(title);
+ });
+ } else {
+ source(function(tiddler,title) {
+ results.push(title);
+ });
+ }
+ return results;
+};
+
/*
Reverse list
*/
@@ -27,7 +44,7 @@ exports.reverse = function(source,operator,options) {
First entry/entries in list
*/
exports.first = function(source,operator,options) {
- var count = parseInt(operator.operand) || 1,
+ var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
@@ -39,7 +56,7 @@ exports.first = function(source,operator,options) {
Last entry/entries in list
*/
exports.last = function(source,operator,options) {
- var count = parseInt(operator.operand) || 1,
+ var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
@@ -51,7 +68,7 @@ exports.last = function(source,operator,options) {
All but the first entry/entries of the list
*/
exports.rest = function(source,operator,options) {
- var count = parseInt(operator.operand) || 1,
+ var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
@@ -65,7 +82,7 @@ exports.bf = exports.rest;
All but the last entry/entries of the list
*/
exports.butlast = function(source,operator,options) {
- var count = parseInt(operator.operand) || 1,
+ var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
@@ -78,7 +95,7 @@ exports.bl = exports.butlast;
The nth member of the list
*/
exports.nth = function(source,operator,options) {
- var count = parseInt(operator.operand) || 1,
+ var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
diff --git a/core/modules/filters/lookup.js b/core/modules/filters/lookup.js
new file mode 100644
index 000000000..3ab7f42e6
--- /dev/null
+++ b/core/modules/filters/lookup.js
@@ -0,0 +1,30 @@
+/*\
+title: $:/core/modules/filters/lookup.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator that looks up values via a title prefix
+
+[lookup:[]]
+
+Prepends the prefix to the selected items and returns the specified field value
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.lookup = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ results.push(options.wiki.getTiddlerText(operator.operand + title) || options.wiki.getTiddlerText(operator.operand + operator.suffix));
+ });
+ return results;
+};
+
+})();
diff --git a/core/modules/filters/minlength.js b/core/modules/filters/minlength.js
new file mode 100644
index 000000000..d4e679bef
--- /dev/null
+++ b/core/modules/filters/minlength.js
@@ -0,0 +1,29 @@
+/*\
+title: $:/core/modules/filters/minlength.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator for filtering out titles that don't meet the minimum length in the operand
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.minlength = function(source,operator,options) {
+ var results = [],
+ minLength = parseInt(operator.operand || "",10) || 0;
+ source(function(tiddler,title) {
+ if(title.length >= minLength) {
+ results.push(title);
+ }
+ });
+ return results;
+};
+
+})();
diff --git a/core/modules/filters/sameday.js b/core/modules/filters/sameday.js
index e1700d11e..5dfc35128 100644
--- a/core/modules/filters/sameday.js
+++ b/core/modules/filters/sameday.js
@@ -20,12 +20,9 @@ exports.sameday = function(source,operator,options) {
fieldName = operator.suffix || "modified",
targetDate = (new Date($tw.utils.parseDate(operator.operand))).setHours(0,0,0,0);
// Function to convert a date/time to a date integer
- var isSameDay = function(dateField) {
- return (new Date(dateField)).setHours(0,0,0,0) === targetDate;
- };
source(function(tiddler,title) {
- if(tiddler && tiddler.fields[fieldName]) {
- if(isSameDay($tw.utils.parseDate(tiddler.fields[fieldName]))) {
+ if(tiddler) {
+ if(tiddler.getFieldDay(fieldName) === targetDate) {
results.push(title);
}
}
diff --git a/core/modules/filters/sort.js b/core/modules/filters/sort.js
index f993825df..176cd4740 100644
--- a/core/modules/filters/sort.js
+++ b/core/modules/filters/sort.js
@@ -27,6 +27,12 @@ exports.nsort = function(source,operator,options) {
return results;
};
+exports.sortan = function(source, operator, options) {
+ var results = prepare_results(source);
+ options.wiki.sortTiddlers(results, operator.operand || "title", operator.prefix === "!",false,false,true);
+ return results;
+};
+
exports.sortcs = function(source,operator,options) {
var results = prepare_results(source);
options.wiki.sortTiddlers(results,operator.operand || "title",operator.prefix === "!",true,false);
diff --git a/core/modules/filters/subtiddlerfields.js b/core/modules/filters/subtiddlerfields.js
new file mode 100644
index 000000000..681dd243d
--- /dev/null
+++ b/core/modules/filters/subtiddlerfields.js
@@ -0,0 +1,31 @@
+/*\
+title: $:/core/modules/filters/subtiddlerfields.js
+type: application/javascript
+module-type: filteroperator
+
+Filter operator for returning the names of the fields on the selected subtiddlers of the plugin named in the operand
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Export our filter function
+*/
+exports.subtiddlerfields = function(source,operator,options) {
+ var results = [];
+ source(function(tiddler,title) {
+ var subtiddler = options.wiki.getSubTiddler(operator.operand,title);
+ if(subtiddler) {
+ for(var fieldName in subtiddler.fields) {
+ $tw.utils.pushTop(results,fieldName);
+ }
+ }
+ });
+ return results;
+};
+
+})();
diff --git a/core/modules/filters/tag.js b/core/modules/filters/tag.js
index 408d7f98f..a0093f565 100644
--- a/core/modules/filters/tag.js
+++ b/core/modules/filters/tag.js
@@ -17,19 +17,31 @@ Export our filter function
*/
exports.tag = function(source,operator,options) {
var results = [];
- if(operator.prefix === "!") {
+ if((operator.suffix || "").toLowerCase() === "strict" && !operator.operand) {
+ // New semantics:
+ // Always return copy of input if operator.operand is missing
source(function(tiddler,title) {
- if(tiddler && !tiddler.hasTag(operator.operand)) {
- results.push(title);
- }
+ results.push(title);
});
} else {
- source(function(tiddler,title) {
- if(tiddler && tiddler.hasTag(operator.operand)) {
- results.push(title);
- }
- });
- results = options.wiki.sortByList(results,operator.operand);
+ // Old semantics:
+ var tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
+ if(operator.prefix === "!") {
+ // Returns a copy of the input if operator.operand is missing
+ source(function(tiddler,title) {
+ if(tiddlers.indexOf(title) === -1) {
+ results.push(title);
+ }
+ });
+ } else {
+ // Returns empty results if operator.operand is missing
+ source(function(tiddler,title) {
+ if(tiddlers.indexOf(title) !== -1) {
+ results.push(title);
+ }
+ });
+ results = options.wiki.sortByList(results,operator.operand);
+ }
}
return results;
};
diff --git a/core/modules/filters/wikiparserrules.js b/core/modules/filters/wikiparserrules.js
index ee57113c8..213298515 100644
--- a/core/modules/filters/wikiparserrules.js
+++ b/core/modules/filters/wikiparserrules.js
@@ -16,10 +16,11 @@ Filter operator for returning the names of the wiki parser rules in this wiki
Export our filter function
*/
exports.wikiparserrules = function(source,operator,options) {
- var results = [];
+ var results = [],
+ operand = operator.operand;
$tw.utils.each($tw.modules.types.wikirule,function(mod) {
var exp = mod.exports;
- if(exp.types[operator.operand]) {
+ if(!operand || exp.types[operand]) {
results.push(exp.name);
}
});
diff --git a/core/modules/filters/x-listops.js b/core/modules/filters/x-listops.js
index a20fd5646..e742e3430 100644
--- a/core/modules/filters/x-listops.js
+++ b/core/modules/filters/x-listops.js
@@ -29,7 +29,7 @@ Extended filter operators to manipulate the current list.
exports.putbefore = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
- count = parseInt(operator.suffix) || 1;
+ count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
results.slice(0, -1) :
results.slice(0, index).concat(results.slice(-count)).concat(results.slice(index, -count));
@@ -41,7 +41,7 @@ Extended filter operators to manipulate the current list.
exports.putafter = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
- count = parseInt(operator.suffix) || 1;
+ count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
results.slice(0, -1) :
results.slice(0, index + 1).concat(results.slice(-count)).concat(results.slice(index + 1, -count));
@@ -53,7 +53,7 @@ Extended filter operators to manipulate the current list.
exports.replace = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
- count = parseInt(operator.suffix) || 1;
+ count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
results.slice(0, -count) :
results.slice(0, index).concat(results.slice(-count)).concat(results.slice(index + 1, -count));
@@ -64,7 +64,7 @@ Extended filter operators to manipulate the current list.
*/
exports.putfirst = function (source, operator) {
var results = prepare_results(source),
- count = parseInt(operator.suffix) || 1;
+ count = $tw.utils.getInt(operator.suffix,1);
return results.slice(-count).concat(results.slice(0, -count));
};
@@ -73,7 +73,7 @@ Extended filter operators to manipulate the current list.
*/
exports.putlast = function (source, operator) {
var results = prepare_results(source),
- count = parseInt(operator.suffix) || 1;
+ count = $tw.utils.getInt(operator.suffix,1);
return results.slice(count).concat(results.slice(0, count));
};
@@ -83,9 +83,10 @@ Extended filter operators to manipulate the current list.
exports.move = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand),
- count = parseInt(operator.suffix) || 1,
- marker = results.splice(index, 1);
- return results.slice(0, index + count).concat(marker).concat(results.slice(index + count));
+ count = $tw.utils.getInt(operator.suffix,1),
+ marker = results.splice(index, 1),
+ offset = (index + count) > 0 ? index + count : 0;
+ return results.slice(0, offset).concat(marker).concat(results.slice(offset));
};
/*
@@ -105,7 +106,7 @@ Extended filter operators to manipulate the current list.
exports.allbefore = function (source, operator) {
var results = prepare_results(source),
index = results.indexOf(operator.operand);
- return (index <= 0) ? [] :
+ return (index < 0) ? [] :
(operator.suffix) ? results.slice(0, index + 1) :
results.slice(0, index);
};
@@ -128,7 +129,7 @@ Extended filter operators to manipulate the current list.
exports.prepend = function (source, operator) {
var prepend = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source),
- count = parseInt(operator.suffix) || prepend.length;
+ count = $tw.utils.getInt(operator.suffix,prepend.length);
return (prepend.length === 0) ? results :
(operator.prefix) ? prepend.slice(-count).concat(results) :
prepend.slice(0, count).concat(results);
diff --git a/core/modules/info/platform.js b/core/modules/info/platform.js
index 9f6097f74..ce258daea 100644
--- a/core/modules/info/platform.js
+++ b/core/modules/info/platform.js
@@ -18,6 +18,24 @@ exports.getInfoTiddlerFields = function() {
// Basics
infoTiddlerFields.push({title: "$:/info/browser", text: mapBoolean(!!$tw.browser)});
infoTiddlerFields.push({title: "$:/info/node", text: mapBoolean(!!$tw.node)});
+ if($tw.browser) {
+ // Document location
+ var setLocationProperty = function(name,value) {
+ infoTiddlerFields.push({title: "$:/info/url/" + name, text: value});
+ },
+ location = document.location;
+ setLocationProperty("full", (location.toString()).split("#")[0]);
+ setLocationProperty("host", location.host);
+ setLocationProperty("hostname", location.hostname);
+ setLocationProperty("protocol", location.protocol);
+ setLocationProperty("port", location.port);
+ setLocationProperty("pathname", location.pathname);
+ setLocationProperty("search", location.search);
+ setLocationProperty("origin", location.origin);
+ // Screen size
+ infoTiddlerFields.push({title: "$:/info/browser/screen/width", text: window.screen.width.toString()});
+ infoTiddlerFields.push({title: "$:/info/browser/screen/height", text: window.screen.height.toString()});
+ }
return infoTiddlerFields;
};
diff --git a/core/modules/macros/jsontiddler.js b/core/modules/macros/jsontiddler.js
new file mode 100644
index 000000000..509fd5559
--- /dev/null
+++ b/core/modules/macros/jsontiddler.js
@@ -0,0 +1,40 @@
+/*\
+title: $:/core/modules/macros/jsontiddler.js
+type: application/javascript
+module-type: macro
+
+Macro to output a single tiddler to JSON
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Information about this macro
+*/
+
+exports.name = "jsontiddler";
+
+exports.params = [
+ {name: "title"}
+];
+
+/*
+Run the macro
+*/
+exports.run = function(title) {
+ title = title || this.getVariable("currentTiddler");
+ var tiddler = !!title && this.wiki.getTiddler(title),
+ fields = new Object();
+ if(tiddler) {
+ for(var field in tiddler.fields) {
+ fields[field] = tiddler.getFieldString(field);
+ }
+ }
+ return JSON.stringify(fields,null,$tw.config.preferences.jsonSpaces);
+};
+
+})();
diff --git a/core/modules/parsers/binaryparser.js b/core/modules/parsers/binaryparser.js
new file mode 100644
index 000000000..ebfd2beec
--- /dev/null
+++ b/core/modules/parsers/binaryparser.js
@@ -0,0 +1,29 @@
+/*\
+title: $:/core/modules/parsers/binaryparser.js
+type: application/javascript
+module-type: parser
+
+The video parser parses a video tiddler into an embeddable HTML element
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var BINARY_WARNING_MESSAGE = "$:/core/ui/BinaryWarning";
+
+var BinaryParser = function(type,text,options) {
+ this.tree = [{
+ type: "transclude",
+ attributes: {
+ tiddler: {type: "string", value: BINARY_WARNING_MESSAGE}
+ }
+ }];
+};
+
+exports["application/octet-stream"] = BinaryParser;
+
+})();
+
diff --git a/core/modules/parsers/imageparser.js b/core/modules/parsers/imageparser.js
index 709c50ad9..c0bcd65a9 100644
--- a/core/modules/parsers/imageparser.js
+++ b/core/modules/parsers/imageparser.js
@@ -17,18 +17,11 @@ var ImageParser = function(type,text,options) {
type: "element",
tag: "img",
attributes: {}
- },
- src;
+ };
if(options._canonical_uri) {
element.attributes.src = {type: "string", value: options._canonical_uri};
- if(type === "application/pdf" || type === ".pdf") {
- element.tag = "embed";
- }
} else if(text) {
- if(type === "application/pdf" || type === ".pdf") {
- element.attributes.src = {type: "string", value: "data:application/pdf;base64," + text};
- element.tag = "embed";
- } else if(type === "image/svg+xml" || type === ".svg") {
+ if(type === "image/svg+xml" || type === ".svg") {
element.attributes.src = {type: "string", value: "data:image/svg+xml," + encodeURIComponent(text)};
} else {
element.attributes.src = {type: "string", value: "data:" + type + ";base64," + text};
@@ -42,7 +35,6 @@ exports["image/jpg"] = ImageParser;
exports["image/jpeg"] = ImageParser;
exports["image/png"] = ImageParser;
exports["image/gif"] = ImageParser;
-exports["application/pdf"] = ImageParser;
exports["image/x-icon"] = ImageParser;
})();
diff --git a/core/modules/parsers/parseutils.js b/core/modules/parsers/parseutils.js
index 97c994050..0d74355f7 100644
--- a/core/modules/parsers/parseutils.js
+++ b/core/modules/parsers/parseutils.js
@@ -218,6 +218,7 @@ exports.parseAttribute = function(source,pos) {
// Define our regexps
var reAttributeName = /([^\/\s>"'=]+)/g,
reUnquotedAttribute = /([^\/\s<>"'=]+)/g,
+ reFilteredValue = /\{\{\{(.+?)\}\}\}/g,
reIndirectValue = /\{\{([^\}]+)\}\}/g;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
@@ -243,29 +244,37 @@ exports.parseAttribute = function(source,pos) {
node.type = "string";
node.value = stringLiteral.value;
} else {
- // Look for an indirect value
- var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
- if(indirectValue) {
- pos = indirectValue.end;
- node.type = "indirect";
- node.textReference = indirectValue.match[1];
+ // Look for a filtered value
+ var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
+ if(filteredValue) {
+ pos = filteredValue.end;
+ node.type = "filtered";
+ node.filter = filteredValue.match[1];
} else {
- // Look for a unquoted value
- var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
- if(unquotedValue) {
- pos = unquotedValue.end;
- node.type = "string";
- node.value = unquotedValue.match[1];
+ // Look for an indirect value
+ var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
+ if(indirectValue) {
+ pos = indirectValue.end;
+ node.type = "indirect";
+ node.textReference = indirectValue.match[1];
} else {
- // Look for a macro invocation value
- var macroInvocation = $tw.utils.parseMacroInvocation(source,pos);
- if(macroInvocation) {
- pos = macroInvocation.end;
- node.type = "macro";
- node.value = macroInvocation;
- } else {
+ // Look for a unquoted value
+ var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
+ if(unquotedValue) {
+ pos = unquotedValue.end;
node.type = "string";
- node.value = "true";
+ node.value = unquotedValue.match[1];
+ } else {
+ // Look for a macro invocation value
+ var macroInvocation = $tw.utils.parseMacroInvocation(source,pos);
+ if(macroInvocation) {
+ pos = macroInvocation.end;
+ node.type = "macro";
+ node.value = macroInvocation;
+ } else {
+ node.type = "string";
+ node.value = "true";
+ }
}
}
}
diff --git a/core/modules/parsers/pdfparser.js b/core/modules/parsers/pdfparser.js
new file mode 100644
index 000000000..95d74ef4b
--- /dev/null
+++ b/core/modules/parsers/pdfparser.js
@@ -0,0 +1,33 @@
+/*\
+title: $:/core/modules/parsers/pdfparser.js
+type: application/javascript
+module-type: parser
+
+The PDF parser embeds a PDF viewer
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var ImageParser = function(type,text,options) {
+ var element = {
+ type: "element",
+ tag: "embed",
+ attributes: {}
+ },
+ src;
+ if(options._canonical_uri) {
+ element.attributes.src = {type: "string", value: options._canonical_uri};
+ } else if(text) {
+ element.attributes.src = {type: "string", value: "data:application/pdf;base64," + text};
+ }
+ this.tree = [element];
+};
+
+exports["application/pdf"] = ImageParser;
+
+})();
+
diff --git a/core/modules/parsers/videoparser.js b/core/modules/parsers/videoparser.js
index aaf3170c2..c0ab2d603 100644
--- a/core/modules/parsers/videoparser.js
+++ b/core/modules/parsers/videoparser.js
@@ -12,7 +12,7 @@ The video parser parses a video tiddler into an embeddable HTML element
/*global $tw: false */
"use strict";
-var AudioParser = function(type,text,options) {
+var VideoParser = function(type,text,options) {
var element = {
type: "element",
tag: "video",
@@ -29,7 +29,8 @@ var AudioParser = function(type,text,options) {
this.tree = [element];
};
-exports["video/mp4"] = AudioParser;
+exports["video/mp4"] = VideoParser;
+exports["video/quicktime"] = VideoParser;
})();
diff --git a/core/modules/parsers/wikiparser/rules/extlink.js b/core/modules/parsers/wikiparser/rules/extlink.js
index 07ddbfb88..e06f88d8d 100644
--- a/core/modules/parsers/wikiparser/rules/extlink.js
+++ b/core/modules/parsers/wikiparser/rules/extlink.js
@@ -6,7 +6,7 @@ module-type: wikirule
Wiki text inline rule for external links. For example:
```
-An external link: http://www.tiddlywiki.com/
+An external link: https://www.tiddlywiki.com/
A suppressed external link: ~http://www.tiddlyspace.com/
```
diff --git a/core/modules/parsers/wikiparser/rules/html.js b/core/modules/parsers/wikiparser/rules/html.js
index 71cdfd5dc..b92d19a69 100644
--- a/core/modules/parsers/wikiparser/rules/html.js
+++ b/core/modules/parsers/wikiparser/rules/html.js
@@ -97,10 +97,17 @@ exports.parseTag = function(source,pos,options) {
return null;
}
node.tag = token.match[1];
+ if(node.tag.slice(1).indexOf("$") !== -1) {
+ return null;
+ }
if(node.tag.charAt(0) === "$") {
node.type = node.tag.substr(1);
}
pos = token.end;
+ // Check that the tag is terminated by a space, / or >
+ if(!$tw.utils.parseWhiteSpace(source,pos) && !(source.charAt(pos) === "/") && !(source.charAt(pos) === ">") ) {
+ return null;
+ }
// Process attributes
var attribute = $tw.utils.parseAttribute(source,pos);
while(attribute) {
diff --git a/core/modules/parsers/wikiparser/rules/image.js b/core/modules/parsers/wikiparser/rules/image.js
index 4dd1855e3..6b379d9c5 100644
--- a/core/modules/parsers/wikiparser/rules/image.js
+++ b/core/modules/parsers/wikiparser/rules/image.js
@@ -6,10 +6,10 @@ module-type: wikirule
Wiki text inline rule for embedding images. For example:
```
-[img[http://tiddlywiki.com/fractalveg.jpg]]
-[img width=23 height=24 [http://tiddlywiki.com/fractalveg.jpg]]
-[img width={{!!width}} height={{!!height}} [http://tiddlywiki.com/fractalveg.jpg]]
-[img[Description of image|http://tiddlywiki.com/fractalveg.jpg]]
+[img[https://tiddlywiki.com/fractalveg.jpg]]
+[img width=23 height=24 [https://tiddlywiki.com/fractalveg.jpg]]
+[img width={{!!width}} height={{!!height}} [https://tiddlywiki.com/fractalveg.jpg]]
+[img[Description of image|https://tiddlywiki.com/fractalveg.jpg]]
[img[TiddlerTitle]]
[img[Description of image|TiddlerTitle]]
```
diff --git a/core/modules/parsers/wikiparser/rules/macrodef.js b/core/modules/parsers/wikiparser/rules/macrodef.js
index 1a0f34e01..cc76ca7ec 100644
--- a/core/modules/parsers/wikiparser/rules/macrodef.js
+++ b/core/modules/parsers/wikiparser/rules/macrodef.js
@@ -61,7 +61,7 @@ exports.parse = function() {
reEnd = /(\r?\n\\end[^\S\n\r]*(?:$|\r?\n))/mg;
} else {
// Otherwise, the end of the definition is marked by the end of the line
- reEnd = /(\r?\n)/mg;
+ reEnd = /($|\r?\n)/mg;
// Move past any whitespace
this.parser.pos = $tw.utils.skipWhiteSpace(this.parser.source,this.parser.pos);
}
@@ -84,7 +84,8 @@ exports.parse = function() {
value: {type: "string", value: text}
},
children: [],
- params: params
+ params: params,
+ isMacroDefinition: true
}];
};
diff --git a/core/modules/parsers/wikiparser/rules/prettyextlink.js b/core/modules/parsers/wikiparser/rules/prettyextlink.js
index 19b4f725b..4c497c257 100644
--- a/core/modules/parsers/wikiparser/rules/prettyextlink.js
+++ b/core/modules/parsers/wikiparser/rules/prettyextlink.js
@@ -6,8 +6,8 @@ module-type: wikirule
Wiki text inline rule for external links. For example:
```
-[ext[http://tiddlywiki.com/fractalveg.jpg]]
-[ext[Tooltip|http://tiddlywiki.com/fractalveg.jpg]]
+[ext[https://tiddlywiki.com/fractalveg.jpg]]
+[ext[Tooltip|https://tiddlywiki.com/fractalveg.jpg]]
```
\*/
diff --git a/core/modules/parsers/wikiparser/rules/syslink.js b/core/modules/parsers/wikiparser/rules/syslink.js
index 441fe2aa5..6eb2cdcd4 100644
--- a/core/modules/parsers/wikiparser/rules/syslink.js
+++ b/core/modules/parsers/wikiparser/rules/syslink.js
@@ -18,7 +18,12 @@ exports.types = {inline: true};
exports.init = function(parser) {
this.parser = parser;
// Regexp to match
- this.matchRegExp = /~?\$:\/[a-zA-Z0-9/.\-_]+/mg;
+ this.matchRegExp = new RegExp(
+ "~?\\$:\\/[" +
+ $tw.config.textPrimitives.anyLetter.substr(1,$tw.config.textPrimitives.anyLetter.length - 2) +
+ "\/._-]+",
+ "mg"
+ );
};
exports.parse = function() {
diff --git a/core/modules/parsers/wikiparser/rules/whitespace.js b/core/modules/parsers/wikiparser/rules/whitespace.js
new file mode 100644
index 000000000..e3b0c8e3f
--- /dev/null
+++ b/core/modules/parsers/wikiparser/rules/whitespace.js
@@ -0,0 +1,72 @@
+/*\
+title: $:/core/modules/parsers/wikiparser/rules/whitespace.js
+type: application/javascript
+module-type: wikirule
+
+Wiki pragma rule for whitespace specifications
+
+```
+\whitespace trim
+\whitespace notrim
+```
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+exports.name = "whitespace";
+exports.types = {pragma: true};
+
+/*
+Instantiate parse rule
+*/
+exports.init = function(parser) {
+ this.parser = parser;
+ // Regexp to match
+ this.matchRegExp = /^\\whitespace[^\S\n]/mg;
+};
+
+/*
+Parse the most recent match
+*/
+exports.parse = function() {
+ var self = this;
+ // Move past the pragma invocation
+ this.parser.pos = this.matchRegExp.lastIndex;
+ // Parse whitespace delimited tokens terminated by a line break
+ var reMatch = /[^\S\n]*(\S+)|(\r?\n)/mg,
+ tokens = [];
+ reMatch.lastIndex = this.parser.pos;
+ var match = reMatch.exec(this.parser.source);
+ while(match && match.index === this.parser.pos) {
+ this.parser.pos = reMatch.lastIndex;
+ // Exit if we've got the line break
+ if(match[2]) {
+ break;
+ }
+ // Process the token
+ if(match[1]) {
+ tokens.push(match[1]);
+ }
+ // Match the next token
+ match = reMatch.exec(this.parser.source);
+ }
+ // Process the tokens
+ $tw.utils.each(tokens,function(token) {
+ switch(token) {
+ case "trim":
+ self.parser.configTrimWhiteSpace = true;
+ break;
+ case "notrim":
+ self.parser.configTrimWhiteSpace = false;
+ break;
+ }
+ });
+ // No parse tree nodes to return
+ return [];
+};
+
+})();
diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js
index 2a55399dd..673176038 100644
--- a/core/modules/parsers/wikiparser/wikiparser.js
+++ b/core/modules/parsers/wikiparser/wikiparser.js
@@ -50,6 +50,8 @@ var WikiParser = function(type,text,options) {
this.type = type || "text/vnd.tiddlywiki";
this.source = text || "";
this.sourceLength = this.source.length;
+ // Flag for ignoring whitespace
+ this.configTrimWhiteSpace = false;
// Set current parse position
this.pos = 0;
// Instantiate the pragma parse rules
@@ -285,7 +287,7 @@ WikiParser.prototype.parseInlineRunUnterminated = function(options) {
while(this.pos < this.sourceLength && nextMatch) {
// Process the text preceding the run rule
if(nextMatch.matchIndex > this.pos) {
- tree.push({type: "text", text: this.source.substring(this.pos,nextMatch.matchIndex)});
+ this.pushTextWidget(tree,this.source.substring(this.pos,nextMatch.matchIndex));
this.pos = nextMatch.matchIndex;
}
// Process the run rule
@@ -295,7 +297,7 @@ WikiParser.prototype.parseInlineRunUnterminated = function(options) {
}
// Process the remaining text
if(this.pos < this.sourceLength) {
- tree.push({type: "text", text: this.source.substr(this.pos)});
+ this.pushTextWidget(tree,this.source.substr(this.pos));
}
this.pos = this.sourceLength;
return tree;
@@ -315,7 +317,7 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
if(terminatorMatch) {
if(!inlineRuleMatch || inlineRuleMatch.matchIndex >= terminatorMatch.index) {
if(terminatorMatch.index > this.pos) {
- tree.push({type: "text", text: this.source.substring(this.pos,terminatorMatch.index)});
+ this.pushTextWidget(tree,this.source.substring(this.pos,terminatorMatch.index));
}
this.pos = terminatorMatch.index;
if(options.eatTerminator) {
@@ -328,7 +330,7 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
if(inlineRuleMatch) {
// Preceding text
if(inlineRuleMatch.matchIndex > this.pos) {
- tree.push({type: "text", text: this.source.substring(this.pos,inlineRuleMatch.matchIndex)});
+ this.pushTextWidget(tree,this.source.substring(this.pos,inlineRuleMatch.matchIndex));
this.pos = inlineRuleMatch.matchIndex;
}
// Process the inline rule
@@ -342,12 +344,24 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
}
// Process the remaining text
if(this.pos < this.sourceLength) {
- tree.push({type: "text", text: this.source.substr(this.pos)});
+ this.pushTextWidget(tree,this.source.substr(this.pos));
}
this.pos = this.sourceLength;
return tree;
};
+/*
+Push a text widget onto an array, respecting the configTrimWhiteSpace setting
+*/
+WikiParser.prototype.pushTextWidget = function(array,text) {
+ if(this.configTrimWhiteSpace) {
+ text = $tw.utils.trim(text);
+ }
+ if(text) {
+ array.push({type: "text", text: text});
+ }
+};
+
/*
Parse zero or more class specifiers `.classname`
*/
diff --git a/core/modules/pluginswitcher.js b/core/modules/pluginswitcher.js
index 9c05d375f..ab82be933 100644
--- a/core/modules/pluginswitcher.js
+++ b/core/modules/pluginswitcher.js
@@ -18,12 +18,14 @@ wiki: wiki store to be used
pluginType: type of plugin to be switched
controllerTitle: title of tiddler used to control switching of this resource
defaultPlugins: array of default plugins to be used if nominated plugin isn't found
+onSwitch: callback when plugin is switched (single parameter is array of plugin titles)
*/
function PluginSwitcher(options) {
this.wiki = options.wiki;
this.pluginType = options.pluginType;
this.controllerTitle = options.controllerTitle;
this.defaultPlugins = options.defaultPlugins || [];
+ this.onSwitch = options.onSwitch;
// Switch to the current plugin
this.switchPlugins();
// Listen for changes to the selected plugin
@@ -64,6 +66,10 @@ PluginSwitcher.prototype.switchPlugins = function() {
var registeredTiddlers = $tw.wiki.registerPluginTiddlers(this.pluginType,plugins);
// Unpack the current theme tiddlers
$tw.wiki.unpackPluginTiddlers();
+ // Call the switch handler
+ if(this.onSwitch) {
+ this.onSwitch(plugins);
+ }
};
exports.PluginSwitcher = PluginSwitcher;
diff --git a/core/modules/saver-handler.js b/core/modules/saver-handler.js
index 4c938f7af..40747b8d6 100644
--- a/core/modules/saver-handler.js
+++ b/core/modules/saver-handler.js
@@ -37,10 +37,10 @@ function SaverHandler(options) {
// Listen out for changes to tiddlers
this.wiki.addEventListener("change",function(changes) {
// Filter the changes so that we only count changes to tiddlers that we care about
- var filteredChanges = self.filterFn.call(self.wiki,function(callback) {
+ var filteredChanges = self.filterFn.call(self.wiki,function(iterator) {
$tw.utils.each(changes,function(change,title) {
var tiddler = self.wiki.getTiddler(title);
- callback(tiddler,title);
+ iterator(tiddler,title);
});
});
// Adjust the number of changes
diff --git a/core/modules/savers/beaker.js b/core/modules/savers/beaker.js
new file mode 100644
index 000000000..dc24ef67f
--- /dev/null
+++ b/core/modules/savers/beaker.js
@@ -0,0 +1,64 @@
+/*\
+title: $:/core/modules/savers/beaker.js
+type: application/javascript
+module-type: saver
+
+Saves files using the Beaker browser's (https://beakerbrowser.com) Dat protocol (https://datproject.org/)
+Compatible with beaker >= V0.7.2
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Set up the saver
+*/
+var BeakerSaver = function(wiki) {
+ this.wiki = wiki;
+};
+
+BeakerSaver.prototype.save = function(text,method,callback) {
+ var dat = new DatArchive("" + window.location),
+ pathname = ("" + window.location.pathname).split("#")[0];
+ dat.stat(pathname).then(function(value) {
+ if(value.isDirectory()) {
+ pathname = pathname + "/index.html";
+ }
+ dat.writeFile(pathname,text,"utf8").then(function(value) {
+ callback(null);
+ },function(reason) {
+ callback("Beaker Saver Write Error: " + reason);
+ });
+ },function(reason) {
+ callback("Beaker Saver Stat Error: " + reason);
+ });
+ return true;
+};
+
+/*
+Information about this saver
+*/
+BeakerSaver.prototype.info = {
+ name: "beaker",
+ priority: 3000,
+ capabilities: ["save", "autosave"]
+};
+
+/*
+Static method that returns true if this saver is capable of working
+*/
+exports.canSave = function(wiki) {
+ return !!window.DatArchive && location.protocol==="dat:";
+};
+
+/*
+Create an instance of this saver
+*/
+exports.create = function(wiki) {
+ return new BeakerSaver(wiki);
+};
+
+})();
diff --git a/core/modules/savers/download.js b/core/modules/savers/download.js
index ea7c62f58..3f5740dcd 100644
--- a/core/modules/savers/download.js
+++ b/core/modules/savers/download.js
@@ -25,7 +25,8 @@ DownloadSaver.prototype.save = function(text,method,callback,options) {
if(!filename) {
var p = document.location.pathname.lastIndexOf("/");
if(p !== -1) {
- filename = document.location.pathname.substr(p+1);
+ // We decode the pathname because document.location is URL encoded by the browser
+ filename = decodeURIComponent(document.location.pathname.substr(p+1));
}
}
if(!filename) {
@@ -33,8 +34,6 @@ DownloadSaver.prototype.save = function(text,method,callback,options) {
}
// Set up the link
var link = document.createElement("a");
- link.setAttribute("target","_blank");
- link.setAttribute("rel","noopener noreferrer");
if(Blob !== undefined) {
var blob = new Blob([text], {type: "text/html"});
link.setAttribute("href", URL.createObjectURL(blob));
@@ -55,10 +54,19 @@ Information about this saver
*/
DownloadSaver.prototype.info = {
name: "download",
- priority: 100,
- capabilities: ["save", "download"]
+ priority: 100
};
+Object.defineProperty(DownloadSaver.prototype.info, "capabilities", {
+ get: function() {
+ var capabilities = ["save", "download"];
+ if(($tw.wiki.getTextReference("$:/config/DownloadSaver/AutoSave") || "").toLowerCase() === "yes") {
+ capabilities.push("autosave");
+ }
+ return capabilities;
+ }
+});
+
/*
Static method that returns true if this saver is capable of working
*/
diff --git a/core/modules/savers/put.js b/core/modules/savers/put.js
index 39f1ca18c..771f21c0e 100644
--- a/core/modules/savers/put.js
+++ b/core/modules/savers/put.js
@@ -15,42 +15,95 @@ to the current URL, such as a WebDAV server.
/*global $tw: false */
"use strict";
+/*
+Retrieve ETag if available
+*/
+var retrieveETag = function(self) {
+ var headers = {
+ Accept: "*/*;charset=UTF-8"
+ };
+ $tw.utils.httpRequest({
+ url: self.uri(),
+ type: "HEAD",
+ headers: headers,
+ callback: function(err,data,xhr) {
+ if(err) {
+ return;
+ }
+ var etag = xhr.getResponseHeader("ETag");
+ if(!etag) {
+ return;
+ }
+ self.etag = etag.replace(/^W\//,"");
+ }
+ });
+};
+
+
/*
Select the appropriate saver module and set it up
*/
var PutSaver = function(wiki) {
this.wiki = wiki;
var self = this;
+ var uri = this.uri();
// Async server probe. Until probe finishes, save will fail fast
// See also https://github.com/Jermolene/TiddlyWiki5/issues/2276
- var req = new XMLHttpRequest();
- req.open("OPTIONS",encodeURI(document.location.protocol + "//" + document.location.hostname + ":" + document.location.port + document.location.pathname));
- req.onload = function() {
- // Check DAV header http://www.webdav.org/specs/rfc2518.html#rfc.section.9.1
- self.serverAcceptsPuts = (this.status === 200 && !!this.getResponseHeader('dav'));
- };
- req.send();
+ $tw.utils.httpRequest({
+ url: uri,
+ type: "OPTIONS",
+ callback: function(err,data,xhr) {
+ // Check DAV header http://www.webdav.org/specs/rfc2518.html#rfc.section.9.1
+ if(!err) {
+ self.serverAcceptsPuts = xhr.status === 200 && !!xhr.getResponseHeader("dav");
+ }
+ }
+ });
+ retrieveETag(this);
};
+PutSaver.prototype.uri = function() {
+ return document.location.toString().split("#")[0];
+};
+
+// TODO: in case of edit conflict
+// Prompt: Do you want to save over this? Y/N
+// Merging would be ideal, and may be possible using future generic merge flow
PutSaver.prototype.save = function(text,method,callback) {
- if (!this.serverAcceptsPuts) {
+ if(!this.serverAcceptsPuts) {
return false;
}
- var req = new XMLHttpRequest();
- // TODO: store/check ETags if supported by server, to protect against overwrites
- // Prompt: Do you want to save over this? Y/N
- // Merging would be ideal, and may be possible using future generic merge flow
- req.onload = function() {
- if (this.status === 200 || this.status === 201) {
- callback(null); // success
- }
- else {
- callback(this.responseText); // fail
- }
+ var self = this;
+ var headers = {
+ "Content-Type": "text/html;charset=UTF-8"
};
- req.open("PUT", encodeURI(window.location.href));
- req.setRequestHeader("Content-Type", "text/html;charset=UTF-8");
- req.send(text);
+ if(this.etag) {
+ headers["If-Match"] = this.etag;
+ }
+ $tw.utils.httpRequest({
+ url: this.uri(),
+ type: "PUT",
+ headers: headers,
+ data: text,
+ callback: function(err,data,xhr) {
+ 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);
+ } else {
+ callback(err); // fail
+ }
+ } else {
+ self.etag = xhr.getResponseHeader("ETag");
+ if(self.etag == null) {
+ retrieveETag(self);
+ }
+ callback(null); // success
+ }
+ }
+ });
return true;
};
@@ -60,7 +113,7 @@ Information about this saver
PutSaver.prototype.info = {
name: "put",
priority: 2000,
- capabilities: ["save", "autosave"]
+ capabilities: ["save","autosave"]
};
/*
diff --git a/core/modules/savers/tiddlyfox.js b/core/modules/savers/tiddlyfox.js
index a729e26e3..3abe31afe 100644
--- a/core/modules/savers/tiddlyfox.js
+++ b/core/modules/savers/tiddlyfox.js
@@ -73,7 +73,7 @@ TiddlyFoxSaver.prototype.info = {
Static method that returns true if this saver is capable of working
*/
exports.canSave = function(wiki) {
- return (window.location.protocol === "file:");
+ return true;
};
/*
diff --git a/core/modules/startup/browser-messaging.js b/core/modules/startup/browser-messaging.js
index c38ba7b85..c67090464 100644
--- a/core/modules/startup/browser-messaging.js
+++ b/core/modules/startup/browser-messaging.js
@@ -29,16 +29,17 @@ function loadIFrame(url,callback) {
callback(null,iframeInfo);
} else {
// Create the iframe and save it in the list
- var iframe = document.createElement("iframe"),
- iframeInfo = {
- url: url,
- status: "loading",
- domNode: iframe
- };
+ var iframe = document.createElement("iframe");
+ iframeInfo = {
+ url: url,
+ status: "loading",
+ domNode: iframe
+ };
$tw.browserMessaging.iframeInfoMap[url] = iframeInfo;
saveIFrameInfoTiddler(iframeInfo);
// Add the iframe to the DOM and hide it
iframe.style.display = "none";
+ iframe.setAttribute("library","true");
document.body.appendChild(iframe);
// Set up onload
iframe.onload = function() {
@@ -57,6 +58,18 @@ function loadIFrame(url,callback) {
}
}
+/*
+Unload library iframe for given url
+*/
+function unloadIFrame(url){
+ $tw.utils.each(document.getElementsByTagName('iframe'), function(iframe) {
+ if(iframe.getAttribute("library") === "true" &&
+ iframe.getAttribute("src") === url) {
+ iframe.parentNode.removeChild(iframe);
+ }
+ });
+}
+
function saveIFrameInfoTiddler(iframeInfo) {
$tw.wiki.addTiddler(new $tw.Tiddler($tw.wiki.getCreationFields(),{
title: "$:/temp/ServerConnection/" + iframeInfo.url,
@@ -93,6 +106,21 @@ exports.startup = function() {
});
}
});
+ // Listen for widget messages to control unloading the plugin library
+ $tw.rootWidget.addEventListener("tm-unload-plugin-library",function(event) {
+ var paramObject = event.paramObject || {},
+ url = paramObject.url;
+ $tw.browserMessaging.iframeInfoMap[url] = undefined;
+ if(url) {
+ unloadIFrame(url);
+ $tw.utils.each(
+ $tw.wiki.filterTiddlers("[[$:/temp/ServerConnection/" + url + "]] [prefix[$:/temp/RemoteAssetInfo/" + url + "/]]"),
+ function(title) {
+ $tw.wiki.deleteTiddler(title);
+ }
+ );
+ }
+ });
$tw.rootWidget.addEventListener("tm-load-plugin-from-library",function(event) {
var paramObject = event.paramObject || {},
url = paramObject.url,
diff --git a/core/modules/startup/render.js b/core/modules/startup/render.js
index e77eb1a32..effa2dc51 100644
--- a/core/modules/startup/render.js
+++ b/core/modules/startup/render.js
@@ -57,6 +57,7 @@ exports.startup = function() {
$tw.utils.addClass($tw.pageContainer,"tc-page-container-wrapper");
document.body.insertBefore($tw.pageContainer,document.body.firstChild);
$tw.pageWidgetNode.render($tw.pageContainer,null);
+ $tw.hooks.invokeHook("th-page-refreshed");
})();
// Prepare refresh mechanism
var deferredChanges = Object.create(null),
@@ -65,6 +66,7 @@ exports.startup = function() {
// Process the refresh
$tw.pageWidgetNode.refresh(deferredChanges);
deferredChanges = Object.create(null);
+ $tw.hooks.invokeHook("th-page-refreshed");
}
// Add the change event handler
$tw.wiki.addEventListener("change",$tw.perf.report("mainRefresh",function(changes) {
diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js
index 2b5111aff..f0c3bd450 100644
--- a/core/modules/startup/rootwidget.js
+++ b/core/modules/startup/rootwidget.js
@@ -30,6 +30,10 @@ exports.startup = function() {
$tw.rootWidget.addEventListener("tm-notify",function(event) {
$tw.notifier.display(event.param,{variables: event.paramObject});
});
+ // Install the copy-to-clipboard mechanism
+ $tw.rootWidget.addEventListener("tm-copy-to-clipboard",function(event) {
+ $tw.utils.copyToClipboard(event.param);
+ });
// Install the scroller
$tw.pageScroller = new $tw.utils.PageScroller();
$tw.rootWidget.addEventListener("tm-scroll",function(event) {
@@ -38,10 +42,10 @@ exports.startup = function() {
var fullscreen = $tw.utils.getFullScreenApis();
if(fullscreen) {
$tw.rootWidget.addEventListener("tm-full-screen",function(event) {
- if(document[fullscreen._fullscreenElement]) {
- document[fullscreen._exitFullscreen]();
+ if(event.event.target.ownerDocument[fullscreen._fullscreenElement]) {
+ event.event.target.ownerDocument[fullscreen._exitFullscreen]();
} else {
- document.documentElement[fullscreen._requestFullscreen](Element.ALLOW_KEYBOARD_INPUT);
+ event.event.target.ownerDocument.documentElement[fullscreen._requestFullscreen](Element.ALLOW_KEYBOARD_INPUT);
}
});
}
diff --git a/core/modules/startup/startup.js b/core/modules/startup/startup.js
index 427e7414f..a0ecac23e 100755
--- a/core/modules/startup/startup.js
+++ b/core/modules/startup/startup.js
@@ -63,7 +63,17 @@ exports.startup = function() {
controllerTitle: "$:/language",
defaultPlugins: [
"$:/languages/en-US"
- ]
+ ],
+ onSwitch: function(plugins) {
+ if($tw.browser) {
+ var pluginTiddler = $tw.wiki.getTiddler(plugins[0]);
+ if(pluginTiddler) {
+ document.documentElement.setAttribute("dir",pluginTiddler.getFieldString("text-direction") || "auto");
+ } else {
+ document.documentElement.removeAttribute("dir");
+ }
+ }
+ }
});
// Kick off the theme manager
$tw.themeManager = new $tw.PluginSwitcher({
@@ -77,18 +87,29 @@ exports.startup = function() {
});
// Kick off the keyboard manager
$tw.keyboardManager = new $tw.KeyboardManager();
+ // Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
+ $tw.rootWidget = new widget.widget({
+ type: "widget",
+ children: []
+ },{
+ wiki: $tw.wiki,
+ document: $tw.browser ? document : $tw.fakeDocument
+ });
+ // Execute any startup actions
+ var executeStartupTiddlers = function(tag) {
+ $tw.utils.each($tw.wiki.filterTiddlers("[all[shadows+tiddlers]tag[" + tag + "]!has[draft.of]]"),function(title) {
+ $tw.rootWidget.invokeActionString($tw.wiki.getTiddlerText(title),$tw.rootWidget);
+ });
+ };
+ executeStartupTiddlers("$:/tags/StartupAction");
+ if($tw.browser) {
+ executeStartupTiddlers("$:/tags/StartupAction/Browser");
+ }
+ if($tw.node) {
+ executeStartupTiddlers("$:/tags/StartupAction/Node");
+ }
// Clear outstanding tiddler store change events to avoid an unnecessary refresh cycle at startup
$tw.wiki.clearTiddlerEventQueue();
- // Create a root widget for attaching event handlers. By using it as the parentWidget for another widget tree, one can reuse the event handlers
- if($tw.browser) {
- $tw.rootWidget = new widget.widget({
- type: "widget",
- children: []
- },{
- wiki: $tw.wiki,
- document: document
- });
- }
// Find a working syncadaptor
$tw.syncadaptor = undefined;
$tw.modules.forEachModuleOfType("syncadaptor",function(title,module) {
diff --git a/core/modules/startup/story.js b/core/modules/startup/story.js
index 987bee216..8ef57076f 100644
--- a/core/modules/startup/story.js
+++ b/core/modules/startup/story.js
@@ -53,6 +53,10 @@ exports.startup = function() {
$tw.rootWidget.addEventListener("tm-browser-refresh",function(event) {
window.location.reload(true);
});
+ // Listen for the tm-print message
+ $tw.rootWidget.addEventListener("tm-print",function(event) {
+ (event.event.view || window).print();
+ });
// Listen for the tm-home message
$tw.rootWidget.addEventListener("tm-home",function(event) {
window.location.hash = "";
diff --git a/core/modules/startup/windows.js b/core/modules/startup/windows.js
index 9ce34f842..69966f346 100644
--- a/core/modules/startup/windows.js
+++ b/core/modules/startup/windows.js
@@ -49,7 +49,10 @@ exports.startup = function() {
$tw.wiki.removeEventListener("change",refreshHandler);
},false);
// Set up the styles
- var styleWidgetNode = $tw.wiki.makeTranscludeWidget("$:/core/ui/PageStylesheet",{document: $tw.fakeDocument, variables: variables}),
+ var styleWidgetNode = $tw.wiki.makeTranscludeWidget("$:/core/ui/PageStylesheet",{
+ document: $tw.fakeDocument,
+ variables: variables,
+ importPageMacros: true}),
styleContainer = $tw.fakeDocument.createElement("style");
styleWidgetNode.render(styleContainer,null);
var styleElement = srcDocument.createElement("style");
diff --git a/core/modules/syncer.js b/core/modules/syncer.js
index d3481f69e..94975ce80 100644
--- a/core/modules/syncer.js
+++ b/core/modules/syncer.js
@@ -12,6 +12,18 @@ The syncer tracks changes to the store. If a syncadaptor is used then individual
/*global $tw: false */
"use strict";
+/*
+Defaults
+*/
+Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
+Syncer.prototype.titleUserName = "$:/status/UserName";
+Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter";
+Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done";
+Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
+Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
+Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
+Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
+
/*
Instantiate the syncer with the following options:
syncadaptor: reference to syncadaptor to be used
@@ -21,8 +33,21 @@ function Syncer(options) {
var self = this;
this.wiki = options.wiki;
this.syncadaptor = options.syncadaptor;
+ this.disableUI = !!options.disableUI;
+ this.titleIsLoggedIn = options.titleIsLoggedIn || this.titleIsLoggedIn;
+ this.titleUserName = options.titleUserName || this.titleUserName;
+ this.titleSyncFilter = options.titleSyncFilter || this.titleSyncFilter;
+ this.titleSavedNotification = options.titleSavedNotification || this.titleSavedNotification;
+ this.taskTimerInterval = options.taskTimerInterval || this.taskTimerInterval;
+ this.throttleInterval = options.throttleInterval || this.throttleInterval;
+ this.fallbackInterval = options.fallbackInterval || this.fallbackInterval;
+ this.pollTimerInterval = options.pollTimerInterval || this.pollTimerInterval;
+ this.logging = "logging" in options ? options.logging : true;
// Make a logger
- this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : ""));
+ this.logger = new $tw.utils.Logger("syncer" + ($tw.browser ? "-browser" : "") + ($tw.node ? "-server" : "") + (this.syncadaptor.name ? ("-" + this.syncadaptor.name) : ""),{
+ colour: "cyan",
+ enable: this.logging
+ });
// Compile the dirty tiddler filter
this.filterFn = this.wiki.compileFilter(this.wiki.getTiddlerText(this.titleSyncFilter));
// Record information for known tiddlers
@@ -37,7 +62,7 @@ function Syncer(options) {
self.syncToServer(changes);
});
// Browser event handlers
- if($tw.browser) {
+ if($tw.browser && !this.disableUI) {
// Set up our beforeunload handler
$tw.addUnloadTask(function(event) {
var confirmationMessage;
@@ -59,9 +84,11 @@ function Syncer(options) {
});
}
// Listen out for lazyLoad events
- this.wiki.addEventListener("lazyLoad",function(title) {
- self.handleLazyLoadEvent(title);
- });
+ if(!this.disableUI) {
+ this.wiki.addEventListener("lazyLoad",function(title) {
+ self.handleLazyLoadEvent(title);
+ });
+ }
// Get the login status
this.getStatus(function(err,isLoggedIn) {
// Do a sync from the server
@@ -69,19 +96,6 @@ function Syncer(options) {
});
}
-/*
-Constants
-*/
-Syncer.prototype.titleIsLoggedIn = "$:/status/IsLoggedIn";
-Syncer.prototype.titleUserName = "$:/status/UserName";
-Syncer.prototype.titleSyncFilter = "$:/config/SyncFilter";
-Syncer.prototype.titleSavedNotification = "$:/language/Notifications/Save/Done";
-Syncer.prototype.taskTimerInterval = 1 * 1000; // Interval for sync timer
-Syncer.prototype.throttleInterval = 1 * 1000; // Defer saving tiddlers if they've changed in the last 1s...
-Syncer.prototype.fallbackInterval = 10 * 1000; // Unless the task is older than 10s
-Syncer.prototype.pollTimerInterval = 60 * 1000; // Interval for polling for changes from the adaptor
-
-
/*
Read (or re-read) the latest tiddler info from the store
*/
@@ -127,7 +141,7 @@ Syncer.prototype.isDirty = function() {
Update the document body with the class "tc-dirty" if the wiki has unsaved/unsynced changes
*/
Syncer.prototype.updateDirtyStatus = function() {
- if($tw.browser) {
+ if($tw.browser && !this.disableUI) {
$tw.utils.toggleClass(document.body,"tc-dirty",this.isDirty());
}
};
@@ -135,7 +149,7 @@ Syncer.prototype.updateDirtyStatus = function() {
/*
Save an incoming tiddler in the store, and updates the associated tiddlerInfo
*/
-Syncer.prototype.storeTiddler = function(tiddlerFields) {
+Syncer.prototype.storeTiddler = function(tiddlerFields,hasBeenLazyLoaded) {
// Save the tiddler
var tiddler = new $tw.Tiddler(this.wiki.getTiddler(tiddlerFields.title),tiddlerFields);
this.wiki.addTiddler(tiddler);
@@ -144,7 +158,7 @@ Syncer.prototype.storeTiddler = function(tiddlerFields) {
revision: tiddlerFields.revision,
adaptorInfo: this.syncadaptor.getTiddlerInfo(tiddler),
changeCount: this.wiki.getChangeCount(tiddlerFields.title),
- hasBeenLazyLoaded: true
+ hasBeenLazyLoaded: hasBeenLazyLoaded !== undefined ? hasBeenLazyLoaded : true
};
};
@@ -164,8 +178,6 @@ Syncer.prototype.getStatus = function(callback) {
self.wiki.addTiddler({title: self.titleIsLoggedIn,text: isLoggedIn ? "yes" : "no"});
if(isLoggedIn) {
self.wiki.addTiddler({title: self.titleUserName,text: username || ""});
- } else {
- self.wiki.deleteTiddler(self.titleUserName);
}
// Invoke the callback
if(callback) {
@@ -218,7 +230,7 @@ Syncer.prototype.syncFromServer = function() {
});
} else {
// Load the skinny version of the tiddler
- self.storeTiddler(tiddlerFields);
+ self.storeTiddler(tiddlerFields,false);
}
}
}
@@ -257,13 +269,16 @@ Syncer.prototype.handleLazyLoadEvent = function(title) {
// Don't lazy load the same tiddler twice
var info = this.tiddlerInfo[title];
if(!info || !info.hasBeenLazyLoaded) {
- this.createTiddlerInfo(title);
- this.tiddlerInfo[title].hasBeenLazyLoaded = true;
- // Queue up a sync task to load this tiddler
- this.enqueueSyncTask({
- type: "load",
- title: title
- });
+ // Don't lazy load if the tiddler isn't included in the sync filter
+ if(this.filterFn.call(this.wiki).indexOf(title) !== -1) {
+ this.createTiddlerInfo(title);
+ this.tiddlerInfo[title].hasBeenLazyLoaded = true;
+ // Queue up a sync task to load this tiddler
+ this.enqueueSyncTask({
+ type: "load",
+ title: title
+ });
+ }
}
};
@@ -404,7 +419,7 @@ Process the task queue, performing the next task if appropriate
Syncer.prototype.processTaskQueue = function() {
var self = this;
// Only process a task if the sync adaptor is fully initialised and we're not already performing a task. If we are already performing a task then we'll dispatch the next one when it completes
- if(this.syncadaptor.isReady() && this.numTasksInProgress() === 0) {
+ if((!this.syncadaptor.isReady || this.syncadaptor.isReady()) && this.numTasksInProgress() === 0) {
// Choose the next task to perform
var task = this.chooseNextTask();
// Perform the task if we had one
@@ -499,7 +514,7 @@ Syncer.prototype.dispatchTask = function(task,callback) {
}
// Store the tiddler
if(tiddlerFields) {
- self.storeTiddler(tiddlerFields);
+ self.storeTiddler(tiddlerFields,true);
}
// Invoke the callback
callback(null);
diff --git a/core/modules/tiddler.js b/core/modules/tiddler.js
index f77b41108..e3382af4a 100644
--- a/core/modules/tiddler.js
+++ b/core/modules/tiddler.js
@@ -39,6 +39,24 @@ exports.getFieldString = function(field) {
}
};
+/*
+Get all the fields as a hashmap of strings. Options:
+ exclude: an array of field names to exclude
+*/
+exports.getFieldStrings = function(options) {
+ options = options || {};
+ var exclude = options.exclude || [];
+ var fields = {};
+ for(var field in this.fields) {
+ if($tw.utils.hop(this.fields,field)) {
+ if(exclude.indexOf(field) === -1) {
+ fields[field] = this.getFieldString(field);
+ }
+ }
+ }
+ return fields;
+};
+
/*
Get all the fields as a name:value block. Options:
exclude: an array of field names to exclude
@@ -108,4 +126,17 @@ exports.isEqual = function(tiddler,excludeFields) {
return differences.length === 0;
};
+exports.getFieldDay = function(field) {
+ if(this.cache && this.cache.day && $tw.utils.hop(this.cache.day,field) ) {
+ return this.cache.day[field];
+ }
+ var day = "";
+ if(this.fields[field]) {
+ day = (new Date($tw.utils.parseDate(this.fields[field]))).setHours(0,0,0,0);
+ }
+ this.cache.day = this.cache.day || {};
+ this.cache.day[field] = day;
+ return day;
+};
+
})();
diff --git a/core/modules/utils/diff-match-patch/LICENSE b/core/modules/utils/diff-match-patch/LICENSE
new file mode 100755
index 000000000..d64569567
--- /dev/null
+++ b/core/modules/utils/diff-match-patch/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/core/modules/utils/diff-match-patch/README.md b/core/modules/utils/diff-match-patch/README.md
new file mode 100755
index 000000000..7850f0820
--- /dev/null
+++ b/core/modules/utils/diff-match-patch/README.md
@@ -0,0 +1,41 @@
+The Diff Match and Patch libraries offer robust algorithms to perform the
+operations required for synchronizing plain text.
+
+1. Diff:
+ * Compare two blocks of plain text and efficiently return a list of differences.
+ * [Diff Demo](https://neil.fraser.name/software/diff_match_patch/demos/diff.html)
+2. Match:
+ * Given a search string, find its best fuzzy match in a block of plain text. Weighted for both accuracy and location.
+ * [Match Demo](https://neil.fraser.name/software/diff_match_patch/demos/match.html)
+3. Patch:
+ * Apply a list of patches onto plain text. Use best-effort to apply patch even when the underlying text doesn't match.
+ * [Patch Demo](https://neil.fraser.name/software/diff_match_patch/demos/patch.html)
+
+Originally built in 2006 to power Google Docs, this library is now available in C++, C#, Dart, Java, JavaScript, Lua, Objective C, and Python.
+
+### Reference
+
+* [API](https://github.com/google/diff-match-patch/wiki/API) - Common API across all languages.
+* [Line or Word Diffs](https://github.com/google/diff-match-patch/wiki/Line-or-Word-Diffs) - Less detailed diffs.
+* [Plain Text vs. Structured Content](https://github.com/google/diff-match-patch/wiki/Plain-Text-vs.-Structured-Content) - How to deal with data like XML.
+* [Unidiff](https://github.com/google/diff-match-patch/wiki/Unidiff) - The patch serialization format.
+* [Support](https://groups.google.com/forum/#!forum/diff-match-patch) - Newsgroup for developers.
+
+### Languages
+Although each language port of Diff Match Patch uses the same API, there are some language-specific notes.
+
+* [C++](https://github.com/google/diff-match-patch/wiki/Language:-Cpp)
+* [C#](https://github.com/google/diff-match-patch/wiki/Language:-C%23)
+* [Dart](https://github.com/google/diff-match-patch/wiki/Language:-Dart)
+* [Java](https://github.com/google/diff-match-patch/wiki/Language:-Java)
+* [JavaScript](https://github.com/google/diff-match-patch/wiki/Language:-JavaScript)
+* [Lua](https://github.com/google/diff-match-patch/wiki/Language:-Lua)
+* [Objective-C](https://github.com/google/diff-match-patch/wiki/Language:-Objective-C)
+* [Python](https://github.com/google/diff-match-patch/wiki/Language:-Python)
+
+A standardized speed test tracks the [relative performance of diffs](https://docs.google.com/spreadsheets/d/1zpZccuBpjMZTvL1nGDMKJc7rWL_m_drF4XKOJvB27Kc/edit#gid=0) in each language.
+
+### Algorithms
+This library implements [Myer's diff algorithm](https://neil.fraser.name/writing/diff/myers.pdf) which is generally considered to be the best general-purpose diff. A layer of [pre-diff speedups and post-diff cleanups](https://neil.fraser.name/writing/diff/) surround the diff algorithm, improving both performance and output quality.
+
+This library also implements a [Bitap matching algorithm](https://neil.fraser.name/writing/patch/bitap.ps) at the heart of a [flexible matching and patching strategy](https://neil.fraser.name/writing/patch/).
diff --git a/core/modules/utils/diff-match-patch/diff_match_patch.js b/core/modules/utils/diff-match-patch/diff_match_patch.js
new file mode 100755
index 000000000..879bf1e8d
--- /dev/null
+++ b/core/modules/utils/diff-match-patch/diff_match_patch.js
@@ -0,0 +1,53 @@
+function diff_match_patch(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32}var DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;
+diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[[DIFF_EQUAL,a]]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,b.length-f);a=this.diff_compute_(a,
+b,e,d);c&&a.unshift([DIFF_EQUAL,c]);g&&a.push([DIFF_EQUAL,g]);this.diff_cleanupMerge(a);return a};
+diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[[DIFF_INSERT,b]];if(!b)return[[DIFF_DELETE,a]];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[[DIFF_INSERT,e.substring(0,g)],[DIFF_EQUAL,f],[DIFF_INSERT,e.substring(g+f.length)]],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[[DIFF_DELETE,a],[DIFF_INSERT,b]]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([[DIFF_EQUAL,
+a]],c)):c&&100c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n=
+u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[[DIFF_DELETE,a],[DIFF_INSERT,b]]};
+diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)};
+diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,f=-1,g=d.length;fd?a=a.substring(c-d):c=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null;
+var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]};
+diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,[DIFF_EQUAL,c.substring(0,d)]),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,[DIFF_EQUAL,b.substring(0,e)]),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE,a[f+1][1]=b.substring(e),f++;f++}f++}};
+diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_);
+return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-
+1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
+diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;fb)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)};
+diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case DIFF_INSERT:b[g]=''+l+"";break;case DIFF_DELETE:b[g]=''+l+"";break;case DIFF_EQUAL:b[g]=""+l+""}}return b.join("")};
+diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cl)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=l);"="==f[g].charAt(0)?c[d++]=[DIFF_EQUAL,h]:c[d++]=[DIFF_DELETE,h];break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+
+f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
+diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return f.Match_Distance?e+g/f.Match_Distance:g?1:e}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h};
+diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};
+diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h,
+g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;le[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;0==e.length||e[e.length-
+1][0]!=DIFF_EQUAL?(e.push([DIFF_EQUAL,c]),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
+diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push([g,k]),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push([g,k]),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs);g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,
+this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push([DIFF_EQUAL,k]));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c} Array of diff tuples.
+ */
+diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines,
+ opt_deadline) {
+ // Set a deadline by which time the diff must be complete.
+ if (typeof opt_deadline == 'undefined') {
+ if (this.Diff_Timeout <= 0) {
+ opt_deadline = Number.MAX_VALUE;
+ } else {
+ opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000;
+ }
+ }
+ var deadline = opt_deadline;
+
+ // Check for null inputs.
+ if (text1 == null || text2 == null) {
+ throw new Error('Null input. (diff_main)');
+ }
+
+ // Check for equality (speedup).
+ if (text1 == text2) {
+ if (text1) {
+ return [[DIFF_EQUAL, text1]];
+ }
+ return [];
+ }
+
+ if (typeof opt_checklines == 'undefined') {
+ opt_checklines = true;
+ }
+ var checklines = opt_checklines;
+
+ // Trim off common prefix (speedup).
+ var commonlength = this.diff_commonPrefix(text1, text2);
+ var commonprefix = text1.substring(0, commonlength);
+ text1 = text1.substring(commonlength);
+ text2 = text2.substring(commonlength);
+
+ // Trim off common suffix (speedup).
+ commonlength = this.diff_commonSuffix(text1, text2);
+ var commonsuffix = text1.substring(text1.length - commonlength);
+ text1 = text1.substring(0, text1.length - commonlength);
+ text2 = text2.substring(0, text2.length - commonlength);
+
+ // Compute the diff on the middle block.
+ var diffs = this.diff_compute_(text1, text2, checklines, deadline);
+
+ // Restore the prefix and suffix.
+ if (commonprefix) {
+ diffs.unshift([DIFF_EQUAL, commonprefix]);
+ }
+ if (commonsuffix) {
+ diffs.push([DIFF_EQUAL, commonsuffix]);
+ }
+ this.diff_cleanupMerge(diffs);
+ return diffs;
+};
+
+
+/**
+ * Find the differences between two texts. Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {boolean} checklines Speedup flag. If false, then don't run a
+ * line-level diff first to identify the changed areas.
+ * If true, then run a faster, slightly less optimal diff.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.} Array of diff tuples.
+ * @private
+ */
+diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines,
+ deadline) {
+ var diffs;
+
+ if (!text1) {
+ // Just add some text (speedup).
+ return [[DIFF_INSERT, text2]];
+ }
+
+ if (!text2) {
+ // Just delete some text (speedup).
+ return [[DIFF_DELETE, text1]];
+ }
+
+ var longtext = text1.length > text2.length ? text1 : text2;
+ var shorttext = text1.length > text2.length ? text2 : text1;
+ var i = longtext.indexOf(shorttext);
+ if (i != -1) {
+ // Shorter text is inside the longer text (speedup).
+ diffs = [[DIFF_INSERT, longtext.substring(0, i)],
+ [DIFF_EQUAL, shorttext],
+ [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
+ // Swap insertions for deletions if diff is reversed.
+ if (text1.length > text2.length) {
+ diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+ }
+ return diffs;
+ }
+
+ if (shorttext.length == 1) {
+ // Single character string.
+ // After the previous speedup, the character can't be an equality.
+ return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+ }
+
+ // Check to see if the problem can be split in two.
+ var hm = this.diff_halfMatch_(text1, text2);
+ if (hm) {
+ // A half-match was found, sort out the return data.
+ var text1_a = hm[0];
+ var text1_b = hm[1];
+ var text2_a = hm[2];
+ var text2_b = hm[3];
+ var mid_common = hm[4];
+ // Send both pairs off for separate processing.
+ var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline);
+ var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline);
+ // Merge the results.
+ return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
+ }
+
+ if (checklines && text1.length > 100 && text2.length > 100) {
+ return this.diff_lineMode_(text1, text2, deadline);
+ }
+
+ return this.diff_bisect_(text1, text2, deadline);
+};
+
+
+/**
+ * Do a quick line-level diff on both strings, then rediff the parts for
+ * greater accuracy.
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time when the diff should be complete by.
+ * @return {!Array.} Array of diff tuples.
+ * @private
+ */
+diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) {
+ // Scan the text on a line-by-line basis first.
+ var a = this.diff_linesToChars_(text1, text2);
+ text1 = a.chars1;
+ text2 = a.chars2;
+ var linearray = a.lineArray;
+
+ var diffs = this.diff_main(text1, text2, false, deadline);
+
+ // Convert the diff back to original text.
+ this.diff_charsToLines_(diffs, linearray);
+ // Eliminate freak matches (e.g. blank lines)
+ this.diff_cleanupSemantic(diffs);
+
+ // Rediff any replacement blocks, this time character-by-character.
+ // Add a dummy entry at the end.
+ diffs.push([DIFF_EQUAL, '']);
+ var pointer = 0;
+ var count_delete = 0;
+ var count_insert = 0;
+ var text_delete = '';
+ var text_insert = '';
+ while (pointer < diffs.length) {
+ switch (diffs[pointer][0]) {
+ case DIFF_INSERT:
+ count_insert++;
+ text_insert += diffs[pointer][1];
+ break;
+ case DIFF_DELETE:
+ count_delete++;
+ text_delete += diffs[pointer][1];
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (count_delete >= 1 && count_insert >= 1) {
+ // Delete the offending records and add the merged ones.
+ diffs.splice(pointer - count_delete - count_insert,
+ count_delete + count_insert);
+ pointer = pointer - count_delete - count_insert;
+ var a = this.diff_main(text_delete, text_insert, false, deadline);
+ for (var j = a.length - 1; j >= 0; j--) {
+ diffs.splice(pointer, 0, a[j]);
+ }
+ pointer = pointer + a.length;
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete = '';
+ text_insert = '';
+ break;
+ }
+ pointer++;
+ }
+ diffs.pop(); // Remove the dummy entry at the end.
+
+ return diffs;
+};
+
+
+/**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.} Array of diff tuples.
+ * @private
+ */
+diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) {
+ // Cache the text lengths to prevent multiple calls.
+ var text1_length = text1.length;
+ var text2_length = text2.length;
+ var max_d = Math.ceil((text1_length + text2_length) / 2);
+ var v_offset = max_d;
+ var v_length = 2 * max_d;
+ var v1 = new Array(v_length);
+ var v2 = new Array(v_length);
+ // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+ // integers and undefined.
+ for (var x = 0; x < v_length; x++) {
+ v1[x] = -1;
+ v2[x] = -1;
+ }
+ v1[v_offset + 1] = 0;
+ v2[v_offset + 1] = 0;
+ var delta = text1_length - text2_length;
+ // If the total number of characters is odd, then the front path will collide
+ // with the reverse path.
+ var front = (delta % 2 != 0);
+ // Offsets for start and end of k loop.
+ // Prevents mapping of space beyond the grid.
+ var k1start = 0;
+ var k1end = 0;
+ var k2start = 0;
+ var k2end = 0;
+ for (var d = 0; d < max_d; d++) {
+ // Bail out if deadline is reached.
+ if ((new Date()).getTime() > deadline) {
+ break;
+ }
+
+ // Walk the front path one step.
+ for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+ var k1_offset = v_offset + k1;
+ var x1;
+ if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
+ x1 = v1[k1_offset + 1];
+ } else {
+ x1 = v1[k1_offset - 1] + 1;
+ }
+ var y1 = x1 - k1;
+ while (x1 < text1_length && y1 < text2_length &&
+ text1.charAt(x1) == text2.charAt(y1)) {
+ x1++;
+ y1++;
+ }
+ v1[k1_offset] = x1;
+ if (x1 > text1_length) {
+ // Ran off the right of the graph.
+ k1end += 2;
+ } else if (y1 > text2_length) {
+ // Ran off the bottom of the graph.
+ k1start += 2;
+ } else if (front) {
+ var k2_offset = v_offset + delta - k1;
+ if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
+ // Mirror x2 onto top-left coordinate system.
+ var x2 = text1_length - v2[k2_offset];
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diff_bisectSplit_(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+
+ // Walk the reverse path one step.
+ for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+ var k2_offset = v_offset + k2;
+ var x2;
+ if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
+ x2 = v2[k2_offset + 1];
+ } else {
+ x2 = v2[k2_offset - 1] + 1;
+ }
+ var y2 = x2 - k2;
+ while (x2 < text1_length && y2 < text2_length &&
+ text1.charAt(text1_length - x2 - 1) ==
+ text2.charAt(text2_length - y2 - 1)) {
+ x2++;
+ y2++;
+ }
+ v2[k2_offset] = x2;
+ if (x2 > text1_length) {
+ // Ran off the left of the graph.
+ k2end += 2;
+ } else if (y2 > text2_length) {
+ // Ran off the top of the graph.
+ k2start += 2;
+ } else if (!front) {
+ var k1_offset = v_offset + delta - k2;
+ if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
+ var x1 = v1[k1_offset];
+ var y1 = v_offset + x1 - k1_offset;
+ // Mirror x2 onto top-left coordinate system.
+ x2 = text1_length - x2;
+ if (x1 >= x2) {
+ // Overlap detected.
+ return this.diff_bisectSplit_(text1, text2, x1, y1, deadline);
+ }
+ }
+ }
+ }
+ }
+ // Diff took too long and hit the deadline or
+ // number of diffs equals number of characters, no commonality at all.
+ return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+};
+
+
+/**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} x Index of split point in text1.
+ * @param {number} y Index of split point in text2.
+ * @param {number} deadline Time at which to bail if not yet complete.
+ * @return {!Array.} Array of diff tuples.
+ * @private
+ */
+diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y,
+ deadline) {
+ var text1a = text1.substring(0, x);
+ var text2a = text2.substring(0, y);
+ var text1b = text1.substring(x);
+ var text2b = text2.substring(y);
+
+ // Compute both diffs serially.
+ var diffs = this.diff_main(text1a, text2a, false, deadline);
+ var diffsb = this.diff_main(text1b, text2b, false, deadline);
+
+ return diffs.concat(diffsb);
+};
+
+
+/**
+ * Split two texts into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {{chars1: string, chars2: string, lineArray: !Array.}}
+ * An object containing the encoded text1, the encoded text2 and
+ * the array of unique strings.
+ * The zeroth element of the array of unique strings is intentionally blank.
+ * @private
+ */
+diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) {
+ var lineArray = []; // e.g. lineArray[4] == 'Hello\n'
+ var lineHash = {}; // e.g. lineHash['Hello\n'] == 4
+
+ // '\x00' is a valid character, but various debuggers don't like it.
+ // So we'll insert a junk entry to avoid generating a null character.
+ lineArray[0] = '';
+
+ /**
+ * Split a text into an array of strings. Reduce the texts to a string of
+ * hashes where each Unicode character represents one line.
+ * Modifies linearray and linehash through being a closure.
+ * @param {string} text String to encode.
+ * @return {string} Encoded string.
+ * @private
+ */
+ function diff_linesToCharsMunge_(text) {
+ var chars = '';
+ // Walk the text, pulling out a substring for each line.
+ // text.split('\n') would would temporarily double our memory footprint.
+ // Modifying text would create many large strings to garbage collect.
+ var lineStart = 0;
+ var lineEnd = -1;
+ // Keeping our own length variable is faster than looking it up.
+ var lineArrayLength = lineArray.length;
+ while (lineEnd < text.length - 1) {
+ lineEnd = text.indexOf('\n', lineStart);
+ if (lineEnd == -1) {
+ lineEnd = text.length - 1;
+ }
+ var line = text.substring(lineStart, lineEnd + 1);
+ lineStart = lineEnd + 1;
+
+ if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
+ (lineHash[line] !== undefined)) {
+ chars += String.fromCharCode(lineHash[line]);
+ } else {
+ chars += String.fromCharCode(lineArrayLength);
+ lineHash[line] = lineArrayLength;
+ lineArray[lineArrayLength++] = line;
+ }
+ }
+ return chars;
+ }
+
+ var chars1 = diff_linesToCharsMunge_(text1);
+ var chars2 = diff_linesToCharsMunge_(text2);
+ return {chars1: chars1, chars2: chars2, lineArray: lineArray};
+};
+
+
+/**
+ * Rehydrate the text in a diff from a string of line hashes to real lines of
+ * text.
+ * @param {!Array.} diffs Array of diff tuples.
+ * @param {!Array.} lineArray Array of unique strings.
+ * @private
+ */
+diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) {
+ for (var x = 0; x < diffs.length; x++) {
+ var chars = diffs[x][1];
+ var text = [];
+ for (var y = 0; y < chars.length; y++) {
+ text[y] = lineArray[chars.charCodeAt(y)];
+ }
+ diffs[x][1] = text.join('');
+ }
+};
+
+
+/**
+ * Determine the common prefix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the start of each
+ * string.
+ */
+diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) {
+ // Quick check for common null cases.
+ if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ var pointermin = 0;
+ var pointermax = Math.min(text1.length, text2.length);
+ var pointermid = pointermax;
+ var pointerstart = 0;
+ while (pointermin < pointermid) {
+ if (text1.substring(pointerstart, pointermid) ==
+ text2.substring(pointerstart, pointermid)) {
+ pointermin = pointermid;
+ pointerstart = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+ }
+ return pointermid;
+};
+
+
+/**
+ * Determine the common suffix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of each string.
+ */
+diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) {
+ // Quick check for common null cases.
+ if (!text1 || !text2 ||
+ text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
+ return 0;
+ }
+ // Binary search.
+ // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+ var pointermin = 0;
+ var pointermax = Math.min(text1.length, text2.length);
+ var pointermid = pointermax;
+ var pointerend = 0;
+ while (pointermin < pointermid) {
+ if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
+ text2.substring(text2.length - pointermid, text2.length - pointerend)) {
+ pointermin = pointermid;
+ pointerend = pointermin;
+ } else {
+ pointermax = pointermid;
+ }
+ pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+ }
+ return pointermid;
+};
+
+
+/**
+ * Determine if the suffix of one string is the prefix of another.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of the first
+ * string and the start of the second string.
+ * @private
+ */
+diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) {
+ // Cache the text lengths to prevent multiple calls.
+ var text1_length = text1.length;
+ var text2_length = text2.length;
+ // Eliminate the null case.
+ if (text1_length == 0 || text2_length == 0) {
+ return 0;
+ }
+ // Truncate the longer string.
+ if (text1_length > text2_length) {
+ text1 = text1.substring(text1_length - text2_length);
+ } else if (text1_length < text2_length) {
+ text2 = text2.substring(0, text1_length);
+ }
+ var text_length = Math.min(text1_length, text2_length);
+ // Quick check for the worst case.
+ if (text1 == text2) {
+ return text_length;
+ }
+
+ // Start by looking for a single character match
+ // and increase length until no match is found.
+ // Performance analysis: http://neil.fraser.name/news/2010/11/04/
+ var best = 0;
+ var length = 1;
+ while (true) {
+ var pattern = text1.substring(text_length - length);
+ var found = text2.indexOf(pattern);
+ if (found == -1) {
+ return best;
+ }
+ length += found;
+ if (found == 0 || text1.substring(text_length - length) ==
+ text2.substring(0, length)) {
+ best = length;
+ length++;
+ }
+ }
+};
+
+
+/**
+ * Do the two texts share a substring which is at least half the length of the
+ * longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {Array.} Five element Array, containing the prefix of
+ * text1, the suffix of text1, the prefix of text2, the suffix of
+ * text2 and the common middle. Or null if there was no match.
+ * @private
+ */
+diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) {
+ if (this.Diff_Timeout <= 0) {
+ // Don't risk returning a non-optimal diff if we have unlimited time.
+ return null;
+ }
+ var longtext = text1.length > text2.length ? text1 : text2;
+ var shorttext = text1.length > text2.length ? text2 : text1;
+ if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+ return null; // Pointless.
+ }
+ var dmp = this; // 'this' becomes 'window' in a closure.
+
+ /**
+ * Does a substring of shorttext exist within longtext such that the substring
+ * is at least half the length of longtext?
+ * Closure, but does not reference any external variables.
+ * @param {string} longtext Longer string.
+ * @param {string} shorttext Shorter string.
+ * @param {number} i Start index of quarter length substring within longtext.
+ * @return {Array.} Five element Array, containing the prefix of
+ * longtext, the suffix of longtext, the prefix of shorttext, the suffix
+ * of shorttext and the common middle. Or null if there was no match.
+ * @private
+ */
+ function diff_halfMatchI_(longtext, shorttext, i) {
+ // Start with a 1/4 length substring at position i as a seed.
+ var seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+ var j = -1;
+ var best_common = '';
+ var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
+ while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
+ var prefixLength = dmp.diff_commonPrefix(longtext.substring(i),
+ shorttext.substring(j));
+ var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i),
+ shorttext.substring(0, j));
+ if (best_common.length < suffixLength + prefixLength) {
+ best_common = shorttext.substring(j - suffixLength, j) +
+ shorttext.substring(j, j + prefixLength);
+ best_longtext_a = longtext.substring(0, i - suffixLength);
+ best_longtext_b = longtext.substring(i + prefixLength);
+ best_shorttext_a = shorttext.substring(0, j - suffixLength);
+ best_shorttext_b = shorttext.substring(j + prefixLength);
+ }
+ }
+ if (best_common.length * 2 >= longtext.length) {
+ return [best_longtext_a, best_longtext_b,
+ best_shorttext_a, best_shorttext_b, best_common];
+ } else {
+ return null;
+ }
+ }
+
+ // First check if the second quarter is the seed for a half-match.
+ var hm1 = diff_halfMatchI_(longtext, shorttext,
+ Math.ceil(longtext.length / 4));
+ // Check again based on the third quarter.
+ var hm2 = diff_halfMatchI_(longtext, shorttext,
+ Math.ceil(longtext.length / 2));
+ var hm;
+ if (!hm1 && !hm2) {
+ return null;
+ } else if (!hm2) {
+ hm = hm1;
+ } else if (!hm1) {
+ hm = hm2;
+ } else {
+ // Both matched. Select the longest.
+ hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+ }
+
+ // A half-match was found, sort out the return data.
+ var text1_a, text1_b, text2_a, text2_b;
+ if (text1.length > text2.length) {
+ text1_a = hm[0];
+ text1_b = hm[1];
+ text2_a = hm[2];
+ text2_b = hm[3];
+ } else {
+ text2_a = hm[0];
+ text2_b = hm[1];
+ text1_a = hm[2];
+ text1_b = hm[3];
+ }
+ var mid_common = hm[4];
+ return [text1_a, text1_b, text2_a, text2_b, mid_common];
+};
+
+
+/**
+ * Reduce the number of edits by eliminating semantically trivial equalities.
+ * @param {!Array.} diffs Array of diff tuples.
+ */
+diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) {
+ var changes = false;
+ var equalities = []; // Stack of indices where equalities are found.
+ var equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ var lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ var pointer = 0; // Index of current position.
+ // Number of characters that changed prior to the equality.
+ var length_insertions1 = 0;
+ var length_deletions1 = 0;
+ // Number of characters that changed after the equality.
+ var length_insertions2 = 0;
+ var length_deletions2 = 0;
+ while (pointer < diffs.length) {
+ if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found.
+ equalities[equalitiesLength++] = pointer;
+ length_insertions1 = length_insertions2;
+ length_deletions1 = length_deletions2;
+ length_insertions2 = 0;
+ length_deletions2 = 0;
+ lastequality = diffs[pointer][1];
+ } else { // An insertion or deletion.
+ if (diffs[pointer][0] == DIFF_INSERT) {
+ length_insertions2 += diffs[pointer][1].length;
+ } else {
+ length_deletions2 += diffs[pointer][1].length;
+ }
+ // Eliminate an equality that is smaller or equal to the edits on both
+ // sides of it.
+ if (lastequality && (lastequality.length <=
+ Math.max(length_insertions1, length_deletions1)) &&
+ (lastequality.length <= Math.max(length_insertions2,
+ length_deletions2))) {
+ // Duplicate record.
+ diffs.splice(equalities[equalitiesLength - 1], 0,
+ [DIFF_DELETE, lastequality]);
+ // Change second copy to insert.
+ diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
+ // Throw away the equality we just deleted.
+ equalitiesLength--;
+ // Throw away the previous equality (it needs to be reevaluated).
+ equalitiesLength--;
+ pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
+ length_insertions1 = 0; // Reset the counters.
+ length_deletions1 = 0;
+ length_insertions2 = 0;
+ length_deletions2 = 0;
+ lastequality = null;
+ changes = true;
+ }
+ }
+ pointer++;
+ }
+
+ // Normalize the diff.
+ if (changes) {
+ this.diff_cleanupMerge(diffs);
+ }
+ this.diff_cleanupSemanticLossless(diffs);
+
+ // Find any overlaps between deletions and insertions.
+ // e.g: abcxxxxxxdef
+ // -> abcxxxdef
+ // e.g: xxxabcdefxxx
+ // -> defxxxabc
+ // Only extract an overlap if it is as big as the edit ahead or behind it.
+ pointer = 1;
+ while (pointer < diffs.length) {
+ if (diffs[pointer - 1][0] == DIFF_DELETE &&
+ diffs[pointer][0] == DIFF_INSERT) {
+ var deletion = diffs[pointer - 1][1];
+ var insertion = diffs[pointer][1];
+ var overlap_length1 = this.diff_commonOverlap_(deletion, insertion);
+ var overlap_length2 = this.diff_commonOverlap_(insertion, deletion);
+ if (overlap_length1 >= overlap_length2) {
+ if (overlap_length1 >= deletion.length / 2 ||
+ overlap_length1 >= insertion.length / 2) {
+ // Overlap found. Insert an equality and trim the surrounding edits.
+ diffs.splice(pointer, 0,
+ [DIFF_EQUAL, insertion.substring(0, overlap_length1)]);
+ diffs[pointer - 1][1] =
+ deletion.substring(0, deletion.length - overlap_length1);
+ diffs[pointer + 1][1] = insertion.substring(overlap_length1);
+ pointer++;
+ }
+ } else {
+ if (overlap_length2 >= deletion.length / 2 ||
+ overlap_length2 >= insertion.length / 2) {
+ // Reverse overlap found.
+ // Insert an equality and swap and trim the surrounding edits.
+ diffs.splice(pointer, 0,
+ [DIFF_EQUAL, deletion.substring(0, overlap_length2)]);
+ diffs[pointer - 1][0] = DIFF_INSERT;
+ diffs[pointer - 1][1] =
+ insertion.substring(0, insertion.length - overlap_length2);
+ diffs[pointer + 1][0] = DIFF_DELETE;
+ diffs[pointer + 1][1] =
+ deletion.substring(overlap_length2);
+ pointer++;
+ }
+ }
+ pointer++;
+ }
+ pointer++;
+ }
+};
+
+
+/**
+ * Look for single edits surrounded on both sides by equalities
+ * which can be shifted sideways to align the edit to a word boundary.
+ * e.g: The cat came. -> The cat came.
+ * @param {!Array.} diffs Array of diff tuples.
+ */
+diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) {
+ /**
+ * Given two strings, compute a score representing whether the internal
+ * boundary falls on logical boundaries.
+ * Scores range from 6 (best) to 0 (worst).
+ * Closure, but does not reference any external variables.
+ * @param {string} one First string.
+ * @param {string} two Second string.
+ * @return {number} The score.
+ * @private
+ */
+ function diff_cleanupSemanticScore_(one, two) {
+ if (!one || !two) {
+ // Edges are the best.
+ return 6;
+ }
+
+ // Each port of this function behaves slightly differently due to
+ // subtle differences in each language's definition of things like
+ // 'whitespace'. Since this function's purpose is largely cosmetic,
+ // the choice has been made to use each language's native features
+ // rather than force total conformity.
+ var char1 = one.charAt(one.length - 1);
+ var char2 = two.charAt(0);
+ var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_);
+ var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_);
+ var whitespace1 = nonAlphaNumeric1 &&
+ char1.match(diff_match_patch.whitespaceRegex_);
+ var whitespace2 = nonAlphaNumeric2 &&
+ char2.match(diff_match_patch.whitespaceRegex_);
+ var lineBreak1 = whitespace1 &&
+ char1.match(diff_match_patch.linebreakRegex_);
+ var lineBreak2 = whitespace2 &&
+ char2.match(diff_match_patch.linebreakRegex_);
+ var blankLine1 = lineBreak1 &&
+ one.match(diff_match_patch.blanklineEndRegex_);
+ var blankLine2 = lineBreak2 &&
+ two.match(diff_match_patch.blanklineStartRegex_);
+
+ if (blankLine1 || blankLine2) {
+ // Five points for blank lines.
+ return 5;
+ } else if (lineBreak1 || lineBreak2) {
+ // Four points for line breaks.
+ return 4;
+ } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {
+ // Three points for end of sentences.
+ return 3;
+ } else if (whitespace1 || whitespace2) {
+ // Two points for whitespace.
+ return 2;
+ } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {
+ // One point for non-alphanumeric.
+ return 1;
+ }
+ return 0;
+ }
+
+ var pointer = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < diffs.length - 1) {
+ if (diffs[pointer - 1][0] == DIFF_EQUAL &&
+ diffs[pointer + 1][0] == DIFF_EQUAL) {
+ // This is a single edit surrounded by equalities.
+ var equality1 = diffs[pointer - 1][1];
+ var edit = diffs[pointer][1];
+ var equality2 = diffs[pointer + 1][1];
+
+ // First, shift the edit as far left as possible.
+ var commonOffset = this.diff_commonSuffix(equality1, edit);
+ if (commonOffset) {
+ var commonString = edit.substring(edit.length - commonOffset);
+ equality1 = equality1.substring(0, equality1.length - commonOffset);
+ edit = commonString + edit.substring(0, edit.length - commonOffset);
+ equality2 = commonString + equality2;
+ }
+
+ // Second, step character by character right, looking for the best fit.
+ var bestEquality1 = equality1;
+ var bestEdit = edit;
+ var bestEquality2 = equality2;
+ var bestScore = diff_cleanupSemanticScore_(equality1, edit) +
+ diff_cleanupSemanticScore_(edit, equality2);
+ while (edit.charAt(0) === equality2.charAt(0)) {
+ equality1 += edit.charAt(0);
+ edit = edit.substring(1) + equality2.charAt(0);
+ equality2 = equality2.substring(1);
+ var score = diff_cleanupSemanticScore_(equality1, edit) +
+ diff_cleanupSemanticScore_(edit, equality2);
+ // The >= encourages trailing rather than leading whitespace on edits.
+ if (score >= bestScore) {
+ bestScore = score;
+ bestEquality1 = equality1;
+ bestEdit = edit;
+ bestEquality2 = equality2;
+ }
+ }
+
+ if (diffs[pointer - 1][1] != bestEquality1) {
+ // We have an improvement, save it back to the diff.
+ if (bestEquality1) {
+ diffs[pointer - 1][1] = bestEquality1;
+ } else {
+ diffs.splice(pointer - 1, 1);
+ pointer--;
+ }
+ diffs[pointer][1] = bestEdit;
+ if (bestEquality2) {
+ diffs[pointer + 1][1] = bestEquality2;
+ } else {
+ diffs.splice(pointer + 1, 1);
+ pointer--;
+ }
+ }
+ }
+ pointer++;
+ }
+};
+
+// Define some regex patterns for matching boundaries.
+diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
+diff_match_patch.whitespaceRegex_ = /\s/;
+diff_match_patch.linebreakRegex_ = /[\r\n]/;
+diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/;
+diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/;
+
+/**
+ * Reduce the number of edits by eliminating operationally trivial equalities.
+ * @param {!Array.} diffs Array of diff tuples.
+ */
+diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) {
+ var changes = false;
+ var equalities = []; // Stack of indices where equalities are found.
+ var equalitiesLength = 0; // Keeping our own length var is faster in JS.
+ /** @type {?string} */
+ var lastequality = null;
+ // Always equal to diffs[equalities[equalitiesLength - 1]][1]
+ var pointer = 0; // Index of current position.
+ // Is there an insertion operation before the last equality.
+ var pre_ins = false;
+ // Is there a deletion operation before the last equality.
+ var pre_del = false;
+ // Is there an insertion operation after the last equality.
+ var post_ins = false;
+ // Is there a deletion operation after the last equality.
+ var post_del = false;
+ while (pointer < diffs.length) {
+ if (diffs[pointer][0] == DIFF_EQUAL) { // Equality found.
+ if (diffs[pointer][1].length < this.Diff_EditCost &&
+ (post_ins || post_del)) {
+ // Candidate found.
+ equalities[equalitiesLength++] = pointer;
+ pre_ins = post_ins;
+ pre_del = post_del;
+ lastequality = diffs[pointer][1];
+ } else {
+ // Not a candidate, and can never become one.
+ equalitiesLength = 0;
+ lastequality = null;
+ }
+ post_ins = post_del = false;
+ } else { // An insertion or deletion.
+ if (diffs[pointer][0] == DIFF_DELETE) {
+ post_del = true;
+ } else {
+ post_ins = true;
+ }
+ /*
+ * Five types to be split:
+ * ABXYCD
+ * AXCD
+ * ABXC
+ * AXCD
+ * ABXC
+ */
+ if (lastequality && ((pre_ins && pre_del && post_ins && post_del) ||
+ ((lastequality.length < this.Diff_EditCost / 2) &&
+ (pre_ins + pre_del + post_ins + post_del) == 3))) {
+ // Duplicate record.
+ diffs.splice(equalities[equalitiesLength - 1], 0,
+ [DIFF_DELETE, lastequality]);
+ // Change second copy to insert.
+ diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
+ equalitiesLength--; // Throw away the equality we just deleted;
+ lastequality = null;
+ if (pre_ins && pre_del) {
+ // No changes made which could affect previous entry, keep going.
+ post_ins = post_del = true;
+ equalitiesLength = 0;
+ } else {
+ equalitiesLength--; // Throw away the previous equality.
+ pointer = equalitiesLength > 0 ?
+ equalities[equalitiesLength - 1] : -1;
+ post_ins = post_del = false;
+ }
+ changes = true;
+ }
+ }
+ pointer++;
+ }
+
+ if (changes) {
+ this.diff_cleanupMerge(diffs);
+ }
+};
+
+
+/**
+ * Reorder and merge like edit sections. Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param {!Array.} diffs Array of diff tuples.
+ */
+diff_match_patch.prototype.diff_cleanupMerge = function(diffs) {
+ diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end.
+ var pointer = 0;
+ var count_delete = 0;
+ var count_insert = 0;
+ var text_delete = '';
+ var text_insert = '';
+ var commonlength;
+ while (pointer < diffs.length) {
+ switch (diffs[pointer][0]) {
+ case DIFF_INSERT:
+ count_insert++;
+ text_insert += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_DELETE:
+ count_delete++;
+ text_delete += diffs[pointer][1];
+ pointer++;
+ break;
+ case DIFF_EQUAL:
+ // Upon reaching an equality, check for prior redundancies.
+ if (count_delete + count_insert > 1) {
+ if (count_delete !== 0 && count_insert !== 0) {
+ // Factor out any common prefixies.
+ commonlength = this.diff_commonPrefix(text_insert, text_delete);
+ if (commonlength !== 0) {
+ if ((pointer - count_delete - count_insert) > 0 &&
+ diffs[pointer - count_delete - count_insert - 1][0] ==
+ DIFF_EQUAL) {
+ diffs[pointer - count_delete - count_insert - 1][1] +=
+ text_insert.substring(0, commonlength);
+ } else {
+ diffs.splice(0, 0, [DIFF_EQUAL,
+ text_insert.substring(0, commonlength)]);
+ pointer++;
+ }
+ text_insert = text_insert.substring(commonlength);
+ text_delete = text_delete.substring(commonlength);
+ }
+ // Factor out any common suffixies.
+ commonlength = this.diff_commonSuffix(text_insert, text_delete);
+ if (commonlength !== 0) {
+ diffs[pointer][1] = text_insert.substring(text_insert.length -
+ commonlength) + diffs[pointer][1];
+ text_insert = text_insert.substring(0, text_insert.length -
+ commonlength);
+ text_delete = text_delete.substring(0, text_delete.length -
+ commonlength);
+ }
+ }
+ // Delete the offending records and add the merged ones.
+ if (count_delete === 0) {
+ diffs.splice(pointer - count_insert,
+ count_delete + count_insert, [DIFF_INSERT, text_insert]);
+ } else if (count_insert === 0) {
+ diffs.splice(pointer - count_delete,
+ count_delete + count_insert, [DIFF_DELETE, text_delete]);
+ } else {
+ diffs.splice(pointer - count_delete - count_insert,
+ count_delete + count_insert, [DIFF_DELETE, text_delete],
+ [DIFF_INSERT, text_insert]);
+ }
+ pointer = pointer - count_delete - count_insert +
+ (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
+ } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
+ // Merge this equality with the previous one.
+ diffs[pointer - 1][1] += diffs[pointer][1];
+ diffs.splice(pointer, 1);
+ } else {
+ pointer++;
+ }
+ count_insert = 0;
+ count_delete = 0;
+ text_delete = '';
+ text_insert = '';
+ break;
+ }
+ }
+ if (diffs[diffs.length - 1][1] === '') {
+ diffs.pop(); // Remove the dummy entry at the end.
+ }
+
+ // Second pass: look for single edits surrounded on both sides by equalities
+ // which can be shifted sideways to eliminate an equality.
+ // e.g: ABAC -> ABAC
+ var changes = false;
+ pointer = 1;
+ // Intentionally ignore the first and last element (don't need checking).
+ while (pointer < diffs.length - 1) {
+ if (diffs[pointer - 1][0] == DIFF_EQUAL &&
+ diffs[pointer + 1][0] == DIFF_EQUAL) {
+ // This is a single edit surrounded by equalities.
+ if (diffs[pointer][1].substring(diffs[pointer][1].length -
+ diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
+ // Shift the edit over the previous equality.
+ diffs[pointer][1] = diffs[pointer - 1][1] +
+ diffs[pointer][1].substring(0, diffs[pointer][1].length -
+ diffs[pointer - 1][1].length);
+ diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+ diffs.splice(pointer - 1, 1);
+ changes = true;
+ } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
+ diffs[pointer + 1][1]) {
+ // Shift the edit over the next equality.
+ diffs[pointer - 1][1] += diffs[pointer + 1][1];
+ diffs[pointer][1] =
+ diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+ diffs[pointer + 1][1];
+ diffs.splice(pointer + 1, 1);
+ changes = true;
+ }
+ }
+ pointer++;
+ }
+ // If shifts were made, the diff needs reordering and another shift sweep.
+ if (changes) {
+ this.diff_cleanupMerge(diffs);
+ }
+};
+
+
+/**
+ * loc is a location in text1, compute and return the equivalent location in
+ * text2.
+ * e.g. 'The cat' vs 'The big cat', 1->1, 5->8
+ * @param {!Array.} diffs Array of diff tuples.
+ * @param {number} loc Location within text1.
+ * @return {number} Location within text2.
+ */
+diff_match_patch.prototype.diff_xIndex = function(diffs, loc) {
+ var chars1 = 0;
+ var chars2 = 0;
+ var last_chars1 = 0;
+ var last_chars2 = 0;
+ var x;
+ for (x = 0; x < diffs.length; x++) {
+ if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion.
+ chars1 += diffs[x][1].length;
+ }
+ if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion.
+ chars2 += diffs[x][1].length;
+ }
+ if (chars1 > loc) { // Overshot the location.
+ break;
+ }
+ last_chars1 = chars1;
+ last_chars2 = chars2;
+ }
+ // Was the location was deleted?
+ if (diffs.length != x && diffs[x][0] === DIFF_DELETE) {
+ return last_chars2;
+ }
+ // Add the remaining character length.
+ return last_chars2 + (loc - last_chars1);
+};
+
+
+/**
+ * Convert a diff array into a pretty HTML report.
+ * @param {!Array.} diffs Array of diff tuples.
+ * @return {string} HTML representation.
+ */
+diff_match_patch.prototype.diff_prettyHtml = function(diffs) {
+ var html = [];
+ var pattern_amp = /&/g;
+ var pattern_lt = //g;
+ var pattern_para = /\n/g;
+ for (var x = 0; x < diffs.length; x++) {
+ var op = diffs[x][0]; // Operation (insert, delete, equal)
+ var data = diffs[x][1]; // Text of change.
+ var text = data.replace(pattern_amp, '&').replace(pattern_lt, '<')
+ .replace(pattern_gt, '>').replace(pattern_para, '¶ ');
+ switch (op) {
+ case DIFF_INSERT:
+ html[x] = '' + text + '';
+ break;
+ case DIFF_DELETE:
+ html[x] = '' + text + '';
+ break;
+ case DIFF_EQUAL:
+ html[x] = '' + text + '';
+ break;
+ }
+ }
+ return html.join('');
+};
+
+
+/**
+ * Compute and return the source text (all equalities and deletions).
+ * @param {!Array.} diffs Array of diff tuples.
+ * @return {string} Source text.
+ */
+diff_match_patch.prototype.diff_text1 = function(diffs) {
+ var text = [];
+ for (var x = 0; x < diffs.length; x++) {
+ if (diffs[x][0] !== DIFF_INSERT) {
+ text[x] = diffs[x][1];
+ }
+ }
+ return text.join('');
+};
+
+
+/**
+ * Compute and return the destination text (all equalities and insertions).
+ * @param {!Array.} diffs Array of diff tuples.
+ * @return {string} Destination text.
+ */
+diff_match_patch.prototype.diff_text2 = function(diffs) {
+ var text = [];
+ for (var x = 0; x < diffs.length; x++) {
+ if (diffs[x][0] !== DIFF_DELETE) {
+ text[x] = diffs[x][1];
+ }
+ }
+ return text.join('');
+};
+
+
+/**
+ * Compute the Levenshtein distance; the number of inserted, deleted or
+ * substituted characters.
+ * @param {!Array.} diffs Array of diff tuples.
+ * @return {number} Number of changes.
+ */
+diff_match_patch.prototype.diff_levenshtein = function(diffs) {
+ var levenshtein = 0;
+ var insertions = 0;
+ var deletions = 0;
+ for (var x = 0; x < diffs.length; x++) {
+ var op = diffs[x][0];
+ var data = diffs[x][1];
+ switch (op) {
+ case DIFF_INSERT:
+ insertions += data.length;
+ break;
+ case DIFF_DELETE:
+ deletions += data.length;
+ break;
+ case DIFF_EQUAL:
+ // A deletion and an insertion is one substitution.
+ levenshtein += Math.max(insertions, deletions);
+ insertions = 0;
+ deletions = 0;
+ break;
+ }
+ }
+ levenshtein += Math.max(insertions, deletions);
+ return levenshtein;
+};
+
+
+/**
+ * Crush the diff into an encoded string which describes the operations
+ * required to transform text1 into text2.
+ * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'.
+ * Operations are tab-separated. Inserted text is escaped using %xx notation.
+ * @param {!Array.} diffs Array of diff tuples.
+ * @return {string} Delta text.
+ */
+diff_match_patch.prototype.diff_toDelta = function(diffs) {
+ var text = [];
+ for (var x = 0; x < diffs.length; x++) {
+ switch (diffs[x][0]) {
+ case DIFF_INSERT:
+ text[x] = '+' + encodeURI(diffs[x][1]);
+ break;
+ case DIFF_DELETE:
+ text[x] = '-' + diffs[x][1].length;
+ break;
+ case DIFF_EQUAL:
+ text[x] = '=' + diffs[x][1].length;
+ break;
+ }
+ }
+ return text.join('\t').replace(/%20/g, ' ');
+};
+
+
+/**
+ * Given the original text1, and an encoded string which describes the
+ * operations required to transform text1 into text2, compute the full diff.
+ * @param {string} text1 Source string for the diff.
+ * @param {string} delta Delta text.
+ * @return {!Array.} Array of diff tuples.
+ * @throws {!Error} If invalid input.
+ */
+diff_match_patch.prototype.diff_fromDelta = function(text1, delta) {
+ var diffs = [];
+ var diffsLength = 0; // Keeping our own length var is faster in JS.
+ var pointer = 0; // Cursor in text1
+ var tokens = delta.split(/\t/g);
+ for (var x = 0; x < tokens.length; x++) {
+ // Each token begins with a one character parameter which specifies the
+ // operation of this token (delete, insert, equality).
+ var param = tokens[x].substring(1);
+ switch (tokens[x].charAt(0)) {
+ case '+':
+ try {
+ diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)];
+ } catch (ex) {
+ // Malformed URI sequence.
+ throw new Error('Illegal escape in diff_fromDelta: ' + param);
+ }
+ break;
+ case '-':
+ // Fall through.
+ case '=':
+ var n = parseInt(param, 10);
+ if (isNaN(n) || n < 0) {
+ throw new Error('Invalid number in diff_fromDelta: ' + param);
+ }
+ var text = text1.substring(pointer, pointer += n);
+ if (tokens[x].charAt(0) == '=') {
+ diffs[diffsLength++] = [DIFF_EQUAL, text];
+ } else {
+ diffs[diffsLength++] = [DIFF_DELETE, text];
+ }
+ break;
+ default:
+ // Blank tokens are ok (from a trailing \t).
+ // Anything else is an error.
+ if (tokens[x]) {
+ throw new Error('Invalid diff operation in diff_fromDelta: ' +
+ tokens[x]);
+ }
+ }
+ }
+ if (pointer != text1.length) {
+ throw new Error('Delta length (' + pointer +
+ ') does not equal source text length (' + text1.length + ').');
+ }
+ return diffs;
+};
+
+
+// MATCH FUNCTIONS
+
+
+/**
+ * Locate the best instance of 'pattern' in 'text' near 'loc'.
+ * @param {string} text The text to search.
+ * @param {string} pattern The pattern to search for.
+ * @param {number} loc The location to search around.
+ * @return {number} Best match index or -1.
+ */
+diff_match_patch.prototype.match_main = function(text, pattern, loc) {
+ // Check for null inputs.
+ if (text == null || pattern == null || loc == null) {
+ throw new Error('Null input. (match_main)');
+ }
+
+ loc = Math.max(0, Math.min(loc, text.length));
+ if (text == pattern) {
+ // Shortcut (potentially not guaranteed by the algorithm)
+ return 0;
+ } else if (!text.length) {
+ // Nothing to match.
+ return -1;
+ } else if (text.substring(loc, loc + pattern.length) == pattern) {
+ // Perfect match at the perfect spot! (Includes case of null pattern)
+ return loc;
+ } else {
+ // Do a fuzzy compare.
+ return this.match_bitap_(text, pattern, loc);
+ }
+};
+
+
+/**
+ * Locate the best instance of 'pattern' in 'text' near 'loc' using the
+ * Bitap algorithm.
+ * @param {string} text The text to search.
+ * @param {string} pattern The pattern to search for.
+ * @param {number} loc The location to search around.
+ * @return {number} Best match index or -1.
+ * @private
+ */
+diff_match_patch.prototype.match_bitap_ = function(text, pattern, loc) {
+ if (pattern.length > this.Match_MaxBits) {
+ throw new Error('Pattern too long for this browser.');
+ }
+
+ // Initialise the alphabet.
+ var s = this.match_alphabet_(pattern);
+
+ var dmp = this; // 'this' becomes 'window' in a closure.
+
+ /**
+ * Compute and return the score for a match with e errors and x location.
+ * Accesses loc and pattern through being a closure.
+ * @param {number} e Number of errors in match.
+ * @param {number} x Location of match.
+ * @return {number} Overall score for match (0.0 = good, 1.0 = bad).
+ * @private
+ */
+ function match_bitapScore_(e, x) {
+ var accuracy = e / pattern.length;
+ var proximity = Math.abs(loc - x);
+ if (!dmp.Match_Distance) {
+ // Dodge divide by zero error.
+ return proximity ? 1.0 : accuracy;
+ }
+ return accuracy + (proximity / dmp.Match_Distance);
+ }
+
+ // Highest score beyond which we give up.
+ var score_threshold = this.Match_Threshold;
+ // Is there a nearby exact match? (speedup)
+ var best_loc = text.indexOf(pattern, loc);
+ if (best_loc != -1) {
+ score_threshold = Math.min(match_bitapScore_(0, best_loc), score_threshold);
+ // What about in the other direction? (speedup)
+ best_loc = text.lastIndexOf(pattern, loc + pattern.length);
+ if (best_loc != -1) {
+ score_threshold =
+ Math.min(match_bitapScore_(0, best_loc), score_threshold);
+ }
+ }
+
+ // Initialise the bit arrays.
+ var matchmask = 1 << (pattern.length - 1);
+ best_loc = -1;
+
+ var bin_min, bin_mid;
+ var bin_max = pattern.length + text.length;
+ var last_rd;
+ for (var d = 0; d < pattern.length; d++) {
+ // Scan for the best match; each iteration allows for one more error.
+ // Run a binary search to determine how far from 'loc' we can stray at this
+ // error level.
+ bin_min = 0;
+ bin_mid = bin_max;
+ while (bin_min < bin_mid) {
+ if (match_bitapScore_(d, loc + bin_mid) <= score_threshold) {
+ bin_min = bin_mid;
+ } else {
+ bin_max = bin_mid;
+ }
+ bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min);
+ }
+ // Use the result from this iteration as the maximum for the next.
+ bin_max = bin_mid;
+ var start = Math.max(1, loc - bin_mid + 1);
+ var finish = Math.min(loc + bin_mid, text.length) + pattern.length;
+
+ var rd = Array(finish + 2);
+ rd[finish + 1] = (1 << d) - 1;
+ for (var j = finish; j >= start; j--) {
+ // The alphabet (s) is a sparse hash, so the following line generates
+ // warnings.
+ var charMatch = s[text.charAt(j - 1)];
+ if (d === 0) { // First pass: exact match.
+ rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
+ } else { // Subsequent passes: fuzzy match.
+ rd[j] = (((rd[j + 1] << 1) | 1) & charMatch) |
+ (((last_rd[j + 1] | last_rd[j]) << 1) | 1) |
+ last_rd[j + 1];
+ }
+ if (rd[j] & matchmask) {
+ var score = match_bitapScore_(d, j - 1);
+ // This match will almost certainly be better than any existing match.
+ // But check anyway.
+ if (score <= score_threshold) {
+ // Told you so.
+ score_threshold = score;
+ best_loc = j - 1;
+ if (best_loc > loc) {
+ // When passing loc, don't exceed our current distance from loc.
+ start = Math.max(1, 2 * loc - best_loc);
+ } else {
+ // Already passed loc, downhill from here on in.
+ break;
+ }
+ }
+ }
+ }
+ // No hope for a (better) match at greater error levels.
+ if (match_bitapScore_(d + 1, loc) > score_threshold) {
+ break;
+ }
+ last_rd = rd;
+ }
+ return best_loc;
+};
+
+
+/**
+ * Initialise the alphabet for the Bitap algorithm.
+ * @param {string} pattern The text to encode.
+ * @return {!Object} Hash of character locations.
+ * @private
+ */
+diff_match_patch.prototype.match_alphabet_ = function(pattern) {
+ var s = {};
+ for (var i = 0; i < pattern.length; i++) {
+ s[pattern.charAt(i)] = 0;
+ }
+ for (var i = 0; i < pattern.length; i++) {
+ s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1);
+ }
+ return s;
+};
+
+
+// PATCH FUNCTIONS
+
+
+/**
+ * Increase the context until it is unique,
+ * but don't let the pattern expand beyond Match_MaxBits.
+ * @param {!diff_match_patch.patch_obj} patch The patch to grow.
+ * @param {string} text Source text.
+ * @private
+ */
+diff_match_patch.prototype.patch_addContext_ = function(patch, text) {
+ if (text.length == 0) {
+ return;
+ }
+ var pattern = text.substring(patch.start2, patch.start2 + patch.length1);
+ var padding = 0;
+
+ // Look for the first and last matches of pattern in text. If two different
+ // matches are found, increase the pattern length.
+ while (text.indexOf(pattern) != text.lastIndexOf(pattern) &&
+ pattern.length < this.Match_MaxBits - this.Patch_Margin -
+ this.Patch_Margin) {
+ padding += this.Patch_Margin;
+ pattern = text.substring(patch.start2 - padding,
+ patch.start2 + patch.length1 + padding);
+ }
+ // Add one chunk for good luck.
+ padding += this.Patch_Margin;
+
+ // Add the prefix.
+ var prefix = text.substring(patch.start2 - padding, patch.start2);
+ if (prefix) {
+ patch.diffs.unshift([DIFF_EQUAL, prefix]);
+ }
+ // Add the suffix.
+ var suffix = text.substring(patch.start2 + patch.length1,
+ patch.start2 + patch.length1 + padding);
+ if (suffix) {
+ patch.diffs.push([DIFF_EQUAL, suffix]);
+ }
+
+ // Roll back the start points.
+ patch.start1 -= prefix.length;
+ patch.start2 -= prefix.length;
+ // Extend the lengths.
+ patch.length1 += prefix.length + suffix.length;
+ patch.length2 += prefix.length + suffix.length;
+};
+
+
+/**
+ * Compute a list of patches to turn text1 into text2.
+ * Use diffs if provided, otherwise compute it ourselves.
+ * There are four ways to call this function, depending on what data is
+ * available to the caller:
+ * Method 1:
+ * a = text1, b = text2
+ * Method 2:
+ * a = diffs
+ * Method 3 (optimal):
+ * a = text1, b = diffs
+ * Method 4 (deprecated, use method 3):
+ * a = text1, b = text2, c = diffs
+ *
+ * @param {string|!Array.} a text1 (methods 1,3,4) or
+ * Array of diff tuples for text1 to text2 (method 2).
+ * @param {string|!Array.} opt_b text2 (methods 1,4) or
+ * Array of diff tuples for text1 to text2 (method 3) or undefined (method 2).
+ * @param {string|!Array.} opt_c Array of diff tuples
+ * for text1 to text2 (method 4) or undefined (methods 1,2,3).
+ * @return {!Array.} Array of Patch objects.
+ */
+diff_match_patch.prototype.patch_make = function(a, opt_b, opt_c) {
+ var text1, diffs;
+ if (typeof a == 'string' && typeof opt_b == 'string' &&
+ typeof opt_c == 'undefined') {
+ // Method 1: text1, text2
+ // Compute diffs from text1 and text2.
+ text1 = /** @type {string} */(a);
+ diffs = this.diff_main(text1, /** @type {string} */(opt_b), true);
+ if (diffs.length > 2) {
+ this.diff_cleanupSemantic(diffs);
+ this.diff_cleanupEfficiency(diffs);
+ }
+ } else if (a && typeof a == 'object' && typeof opt_b == 'undefined' &&
+ typeof opt_c == 'undefined') {
+ // Method 2: diffs
+ // Compute text1 from diffs.
+ diffs = /** @type {!Array.} */(a);
+ text1 = this.diff_text1(diffs);
+ } else if (typeof a == 'string' && opt_b && typeof opt_b == 'object' &&
+ typeof opt_c == 'undefined') {
+ // Method 3: text1, diffs
+ text1 = /** @type {string} */(a);
+ diffs = /** @type {!Array.} */(opt_b);
+ } else if (typeof a == 'string' && typeof opt_b == 'string' &&
+ opt_c && typeof opt_c == 'object') {
+ // Method 4: text1, text2, diffs
+ // text2 is not used.
+ text1 = /** @type {string} */(a);
+ diffs = /** @type {!Array.} */(opt_c);
+ } else {
+ throw new Error('Unknown call format to patch_make.');
+ }
+
+ if (diffs.length === 0) {
+ return []; // Get rid of the null case.
+ }
+ var patches = [];
+ var patch = new diff_match_patch.patch_obj();
+ var patchDiffLength = 0; // Keeping our own length var is faster in JS.
+ var char_count1 = 0; // Number of characters into the text1 string.
+ var char_count2 = 0; // Number of characters into the text2 string.
+ // Start with text1 (prepatch_text) and apply the diffs until we arrive at
+ // text2 (postpatch_text). We recreate the patches one by one to determine
+ // context info.
+ var prepatch_text = text1;
+ var postpatch_text = text1;
+ for (var x = 0; x < diffs.length; x++) {
+ var diff_type = diffs[x][0];
+ var diff_text = diffs[x][1];
+
+ if (!patchDiffLength && diff_type !== DIFF_EQUAL) {
+ // A new patch starts here.
+ patch.start1 = char_count1;
+ patch.start2 = char_count2;
+ }
+
+ switch (diff_type) {
+ case DIFF_INSERT:
+ patch.diffs[patchDiffLength++] = diffs[x];
+ patch.length2 += diff_text.length;
+ postpatch_text = postpatch_text.substring(0, char_count2) + diff_text +
+ postpatch_text.substring(char_count2);
+ break;
+ case DIFF_DELETE:
+ patch.length1 += diff_text.length;
+ patch.diffs[patchDiffLength++] = diffs[x];
+ postpatch_text = postpatch_text.substring(0, char_count2) +
+ postpatch_text.substring(char_count2 +
+ diff_text.length);
+ break;
+ case DIFF_EQUAL:
+ if (diff_text.length <= 2 * this.Patch_Margin &&
+ patchDiffLength && diffs.length != x + 1) {
+ // Small equality inside a patch.
+ patch.diffs[patchDiffLength++] = diffs[x];
+ patch.length1 += diff_text.length;
+ patch.length2 += diff_text.length;
+ } else if (diff_text.length >= 2 * this.Patch_Margin) {
+ // Time for a new patch.
+ if (patchDiffLength) {
+ this.patch_addContext_(patch, prepatch_text);
+ patches.push(patch);
+ patch = new diff_match_patch.patch_obj();
+ patchDiffLength = 0;
+ // Unlike Unidiff, our patch lists have a rolling context.
+ // http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
+ // Update prepatch text & pos to reflect the application of the
+ // just completed patch.
+ prepatch_text = postpatch_text;
+ char_count1 = char_count2;
+ }
+ }
+ break;
+ }
+
+ // Update the current character count.
+ if (diff_type !== DIFF_INSERT) {
+ char_count1 += diff_text.length;
+ }
+ if (diff_type !== DIFF_DELETE) {
+ char_count2 += diff_text.length;
+ }
+ }
+ // Pick up the leftover patch if not empty.
+ if (patchDiffLength) {
+ this.patch_addContext_(patch, prepatch_text);
+ patches.push(patch);
+ }
+
+ return patches;
+};
+
+
+/**
+ * Given an array of patches, return another array that is identical.
+ * @param {!Array.} patches Array of Patch objects.
+ * @return {!Array.} Array of Patch objects.
+ */
+diff_match_patch.prototype.patch_deepCopy = function(patches) {
+ // Making deep copies is hard in JavaScript.
+ var patchesCopy = [];
+ for (var x = 0; x < patches.length; x++) {
+ var patch = patches[x];
+ var patchCopy = new diff_match_patch.patch_obj();
+ patchCopy.diffs = [];
+ for (var y = 0; y < patch.diffs.length; y++) {
+ patchCopy.diffs[y] = patch.diffs[y].slice();
+ }
+ patchCopy.start1 = patch.start1;
+ patchCopy.start2 = patch.start2;
+ patchCopy.length1 = patch.length1;
+ patchCopy.length2 = patch.length2;
+ patchesCopy[x] = patchCopy;
+ }
+ return patchesCopy;
+};
+
+
+/**
+ * Merge a set of patches onto the text. Return a patched text, as well
+ * as a list of true/false values indicating which patches were applied.
+ * @param {!Array.} patches Array of Patch objects.
+ * @param {string} text Old text.
+ * @return {!Array.>} Two element Array, containing the
+ * new text and an array of boolean values.
+ */
+diff_match_patch.prototype.patch_apply = function(patches, text) {
+ if (patches.length == 0) {
+ return [text, []];
+ }
+
+ // Deep copy the patches so that no changes are made to originals.
+ patches = this.patch_deepCopy(patches);
+
+ var nullPadding = this.patch_addPadding(patches);
+ text = nullPadding + text + nullPadding;
+
+ this.patch_splitMax(patches);
+ // delta keeps track of the offset between the expected and actual location
+ // of the previous patch. If there are patches expected at positions 10 and
+ // 20, but the first patch was found at 12, delta is 2 and the second patch
+ // has an effective expected position of 22.
+ var delta = 0;
+ var results = [];
+ for (var x = 0; x < patches.length; x++) {
+ var expected_loc = patches[x].start2 + delta;
+ var text1 = this.diff_text1(patches[x].diffs);
+ var start_loc;
+ var end_loc = -1;
+ if (text1.length > this.Match_MaxBits) {
+ // patch_splitMax will only provide an oversized pattern in the case of
+ // a monster delete.
+ start_loc = this.match_main(text, text1.substring(0, this.Match_MaxBits),
+ expected_loc);
+ if (start_loc != -1) {
+ end_loc = this.match_main(text,
+ text1.substring(text1.length - this.Match_MaxBits),
+ expected_loc + text1.length - this.Match_MaxBits);
+ if (end_loc == -1 || start_loc >= end_loc) {
+ // Can't find valid trailing context. Drop this patch.
+ start_loc = -1;
+ }
+ }
+ } else {
+ start_loc = this.match_main(text, text1, expected_loc);
+ }
+ if (start_loc == -1) {
+ // No match found. :(
+ results[x] = false;
+ // Subtract the delta for this failed patch from subsequent patches.
+ delta -= patches[x].length2 - patches[x].length1;
+ } else {
+ // Found a match. :)
+ results[x] = true;
+ delta = start_loc - expected_loc;
+ var text2;
+ if (end_loc == -1) {
+ text2 = text.substring(start_loc, start_loc + text1.length);
+ } else {
+ text2 = text.substring(start_loc, end_loc + this.Match_MaxBits);
+ }
+ if (text1 == text2) {
+ // Perfect match, just shove the replacement text in.
+ text = text.substring(0, start_loc) +
+ this.diff_text2(patches[x].diffs) +
+ text.substring(start_loc + text1.length);
+ } else {
+ // Imperfect match. Run a diff to get a framework of equivalent
+ // indices.
+ var diffs = this.diff_main(text1, text2, false);
+ if (text1.length > this.Match_MaxBits &&
+ this.diff_levenshtein(diffs) / text1.length >
+ this.Patch_DeleteThreshold) {
+ // The end points match, but the content is unacceptably bad.
+ results[x] = false;
+ } else {
+ this.diff_cleanupSemanticLossless(diffs);
+ var index1 = 0;
+ var index2;
+ for (var y = 0; y < patches[x].diffs.length; y++) {
+ var mod = patches[x].diffs[y];
+ if (mod[0] !== DIFF_EQUAL) {
+ index2 = this.diff_xIndex(diffs, index1);
+ }
+ if (mod[0] === DIFF_INSERT) { // Insertion
+ text = text.substring(0, start_loc + index2) + mod[1] +
+ text.substring(start_loc + index2);
+ } else if (mod[0] === DIFF_DELETE) { // Deletion
+ text = text.substring(0, start_loc + index2) +
+ text.substring(start_loc + this.diff_xIndex(diffs,
+ index1 + mod[1].length));
+ }
+ if (mod[0] !== DIFF_DELETE) {
+ index1 += mod[1].length;
+ }
+ }
+ }
+ }
+ }
+ }
+ // Strip the padding off.
+ text = text.substring(nullPadding.length, text.length - nullPadding.length);
+ return [text, results];
+};
+
+
+/**
+ * Add some padding on text start and end so that edges can match something.
+ * Intended to be called only from within patch_apply.
+ * @param {!Array.} patches Array of Patch objects.
+ * @return {string} The padding string added to each side.
+ */
+diff_match_patch.prototype.patch_addPadding = function(patches) {
+ var paddingLength = this.Patch_Margin;
+ var nullPadding = '';
+ for (var x = 1; x <= paddingLength; x++) {
+ nullPadding += String.fromCharCode(x);
+ }
+
+ // Bump all the patches forward.
+ for (var x = 0; x < patches.length; x++) {
+ patches[x].start1 += paddingLength;
+ patches[x].start2 += paddingLength;
+ }
+
+ // Add some padding on start of first diff.
+ var patch = patches[0];
+ var diffs = patch.diffs;
+ if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) {
+ // Add nullPadding equality.
+ diffs.unshift([DIFF_EQUAL, nullPadding]);
+ patch.start1 -= paddingLength; // Should be 0.
+ patch.start2 -= paddingLength; // Should be 0.
+ patch.length1 += paddingLength;
+ patch.length2 += paddingLength;
+ } else if (paddingLength > diffs[0][1].length) {
+ // Grow first equality.
+ var extraLength = paddingLength - diffs[0][1].length;
+ diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1];
+ patch.start1 -= extraLength;
+ patch.start2 -= extraLength;
+ patch.length1 += extraLength;
+ patch.length2 += extraLength;
+ }
+
+ // Add some padding on end of last diff.
+ patch = patches[patches.length - 1];
+ diffs = patch.diffs;
+ if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) {
+ // Add nullPadding equality.
+ diffs.push([DIFF_EQUAL, nullPadding]);
+ patch.length1 += paddingLength;
+ patch.length2 += paddingLength;
+ } else if (paddingLength > diffs[diffs.length - 1][1].length) {
+ // Grow last equality.
+ var extraLength = paddingLength - diffs[diffs.length - 1][1].length;
+ diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength);
+ patch.length1 += extraLength;
+ patch.length2 += extraLength;
+ }
+
+ return nullPadding;
+};
+
+
+/**
+ * Look through the patches and break up any which are longer than the maximum
+ * limit of the match algorithm.
+ * Intended to be called only from within patch_apply.
+ * @param {!Array.} patches Array of Patch objects.
+ */
+diff_match_patch.prototype.patch_splitMax = function(patches) {
+ var patch_size = this.Match_MaxBits;
+ for (var x = 0; x < patches.length; x++) {
+ if (patches[x].length1 <= patch_size) {
+ continue;
+ }
+ var bigpatch = patches[x];
+ // Remove the big old patch.
+ patches.splice(x--, 1);
+ var start1 = bigpatch.start1;
+ var start2 = bigpatch.start2;
+ var precontext = '';
+ while (bigpatch.diffs.length !== 0) {
+ // Create one of several smaller patches.
+ var patch = new diff_match_patch.patch_obj();
+ var empty = true;
+ patch.start1 = start1 - precontext.length;
+ patch.start2 = start2 - precontext.length;
+ if (precontext !== '') {
+ patch.length1 = patch.length2 = precontext.length;
+ patch.diffs.push([DIFF_EQUAL, precontext]);
+ }
+ while (bigpatch.diffs.length !== 0 &&
+ patch.length1 < patch_size - this.Patch_Margin) {
+ var diff_type = bigpatch.diffs[0][0];
+ var diff_text = bigpatch.diffs[0][1];
+ if (diff_type === DIFF_INSERT) {
+ // Insertions are harmless.
+ patch.length2 += diff_text.length;
+ start2 += diff_text.length;
+ patch.diffs.push(bigpatch.diffs.shift());
+ empty = false;
+ } else if (diff_type === DIFF_DELETE && patch.diffs.length == 1 &&
+ patch.diffs[0][0] == DIFF_EQUAL &&
+ diff_text.length > 2 * patch_size) {
+ // This is a large deletion. Let it pass in one chunk.
+ patch.length1 += diff_text.length;
+ start1 += diff_text.length;
+ empty = false;
+ patch.diffs.push([diff_type, diff_text]);
+ bigpatch.diffs.shift();
+ } else {
+ // Deletion or equality. Only take as much as we can stomach.
+ diff_text = diff_text.substring(0,
+ patch_size - patch.length1 - this.Patch_Margin);
+ patch.length1 += diff_text.length;
+ start1 += diff_text.length;
+ if (diff_type === DIFF_EQUAL) {
+ patch.length2 += diff_text.length;
+ start2 += diff_text.length;
+ } else {
+ empty = false;
+ }
+ patch.diffs.push([diff_type, diff_text]);
+ if (diff_text == bigpatch.diffs[0][1]) {
+ bigpatch.diffs.shift();
+ } else {
+ bigpatch.diffs[0][1] =
+ bigpatch.diffs[0][1].substring(diff_text.length);
+ }
+ }
+ }
+ // Compute the head context for the next patch.
+ precontext = this.diff_text2(patch.diffs);
+ precontext =
+ precontext.substring(precontext.length - this.Patch_Margin);
+ // Append the end context for this patch.
+ var postcontext = this.diff_text1(bigpatch.diffs)
+ .substring(0, this.Patch_Margin);
+ if (postcontext !== '') {
+ patch.length1 += postcontext.length;
+ patch.length2 += postcontext.length;
+ if (patch.diffs.length !== 0 &&
+ patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) {
+ patch.diffs[patch.diffs.length - 1][1] += postcontext;
+ } else {
+ patch.diffs.push([DIFF_EQUAL, postcontext]);
+ }
+ }
+ if (!empty) {
+ patches.splice(++x, 0, patch);
+ }
+ }
+ }
+};
+
+
+/**
+ * Take a list of patches and return a textual representation.
+ * @param {!Array.} patches Array of Patch objects.
+ * @return {string} Text representation of patches.
+ */
+diff_match_patch.prototype.patch_toText = function(patches) {
+ var text = [];
+ for (var x = 0; x < patches.length; x++) {
+ text[x] = patches[x];
+ }
+ return text.join('');
+};
+
+
+/**
+ * Parse a textual representation of patches and return a list of Patch objects.
+ * @param {string} textline Text representation of patches.
+ * @return {!Array.} Array of Patch objects.
+ * @throws {!Error} If invalid input.
+ */
+diff_match_patch.prototype.patch_fromText = function(textline) {
+ var patches = [];
+ if (!textline) {
+ return patches;
+ }
+ var text = textline.split('\n');
+ var textPointer = 0;
+ var patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/;
+ while (textPointer < text.length) {
+ var m = text[textPointer].match(patchHeader);
+ if (!m) {
+ throw new Error('Invalid patch string: ' + text[textPointer]);
+ }
+ var patch = new diff_match_patch.patch_obj();
+ patches.push(patch);
+ patch.start1 = parseInt(m[1], 10);
+ if (m[2] === '') {
+ patch.start1--;
+ patch.length1 = 1;
+ } else if (m[2] == '0') {
+ patch.length1 = 0;
+ } else {
+ patch.start1--;
+ patch.length1 = parseInt(m[2], 10);
+ }
+
+ patch.start2 = parseInt(m[3], 10);
+ if (m[4] === '') {
+ patch.start2--;
+ patch.length2 = 1;
+ } else if (m[4] == '0') {
+ patch.length2 = 0;
+ } else {
+ patch.start2--;
+ patch.length2 = parseInt(m[4], 10);
+ }
+ textPointer++;
+
+ while (textPointer < text.length) {
+ var sign = text[textPointer].charAt(0);
+ try {
+ var line = decodeURI(text[textPointer].substring(1));
+ } catch (ex) {
+ // Malformed URI sequence.
+ throw new Error('Illegal escape in patch_fromText: ' + line);
+ }
+ if (sign == '-') {
+ // Deletion.
+ patch.diffs.push([DIFF_DELETE, line]);
+ } else if (sign == '+') {
+ // Insertion.
+ patch.diffs.push([DIFF_INSERT, line]);
+ } else if (sign == ' ') {
+ // Minor equality.
+ patch.diffs.push([DIFF_EQUAL, line]);
+ } else if (sign == '@') {
+ // Start of next patch.
+ break;
+ } else if (sign === '') {
+ // Blank line? Whatever.
+ } else {
+ // WTF?
+ throw new Error('Invalid patch mode "' + sign + '" in: ' + line);
+ }
+ textPointer++;
+ }
+ }
+ return patches;
+};
+
+
+/**
+ * Class representing one patch operation.
+ * @constructor
+ */
+diff_match_patch.patch_obj = function() {
+ /** @type {!Array.} */
+ this.diffs = [];
+ /** @type {?number} */
+ this.start1 = null;
+ /** @type {?number} */
+ this.start2 = null;
+ /** @type {number} */
+ this.length1 = 0;
+ /** @type {number} */
+ this.length2 = 0;
+};
+
+
+/**
+ * Emmulate GNU diff's format.
+ * Header: @@ -382,8 +481,9 @@
+ * Indicies are printed as 1-based, not 0-based.
+ * @return {string} The GNU diff string.
+ */
+diff_match_patch.patch_obj.prototype.toString = function() {
+ var coords1, coords2;
+ if (this.length1 === 0) {
+ coords1 = this.start1 + ',0';
+ } else if (this.length1 == 1) {
+ coords1 = this.start1 + 1;
+ } else {
+ coords1 = (this.start1 + 1) + ',' + this.length1;
+ }
+ if (this.length2 === 0) {
+ coords2 = this.start2 + ',0';
+ } else if (this.length2 == 1) {
+ coords2 = this.start2 + 1;
+ } else {
+ coords2 = (this.start2 + 1) + ',' + this.length2;
+ }
+ var text = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n'];
+ var op;
+ // Escape the body of the patch with %xx notation.
+ for (var x = 0; x < this.diffs.length; x++) {
+ switch (this.diffs[x][0]) {
+ case DIFF_INSERT:
+ op = '+';
+ break;
+ case DIFF_DELETE:
+ op = '-';
+ break;
+ case DIFF_EQUAL:
+ op = ' ';
+ break;
+ }
+ text[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n';
+ }
+ return text.join('').replace(/%20/g, ' ');
+};
+
+
+// Export these global variables so that they survive Google's JS compiler.
+// In a browser, 'this' will be 'window'.
+// Users of node.js should 'require' the uncompressed version since Google's
+// JS compiler may break the following exports for non-browser environments.
+this['diff_match_patch'] = diff_match_patch;
+this['DIFF_DELETE'] = DIFF_DELETE;
+this['DIFF_INSERT'] = DIFF_INSERT;
+this['DIFF_EQUAL'] = DIFF_EQUAL;
diff --git a/core/modules/utils/diff-match-patch/tiddlywiki.files b/core/modules/utils/diff-match-patch/tiddlywiki.files
new file mode 100644
index 000000000..acfbc865f
--- /dev/null
+++ b/core/modules/utils/diff-match-patch/tiddlywiki.files
@@ -0,0 +1,14 @@
+{
+ "tiddlers": [
+ {
+ "file": "diff_match_patch.js",
+ "fields": {
+ "type": "application/javascript",
+ "title": "$:/core/modules/utils/diff-match-patch/diff_match_patch.js",
+ "module-type": "library"
+ },
+ "prefix": "(function(){",
+ "suffix": "}).call(exports);"
+ }
+ ]
+}
diff --git a/core/modules/utils/dom/dom.js b/core/modules/utils/dom/dom.js
index 118498bc3..a418197af 100644
--- a/core/modules/utils/dom/dom.js
+++ b/core/modules/utils/dom/dom.js
@@ -99,7 +99,7 @@ exports.resizeTextAreaToFit = function(domNode,minHeight) {
scrollTop = container.scrollTop;
// Measure the specified minimum height
domNode.style.height = minHeight;
- var measuredHeight = domNode.offsetHeight;
+ var measuredHeight = domNode.offsetHeight || parseInt(minHeight,10);
// Set its height to auto so that it snaps to the correct height
domNode.style.height = "auto";
// Calculate the revised height
@@ -231,4 +231,36 @@ exports.copyStyles = function(srcDomNode,dstDomNode) {
$tw.utils.setStyles(dstDomNode,$tw.utils.getComputedStyles(srcDomNode));
};
+/*
+Copy plain text to the clipboard on browsers that support it
+*/
+exports.copyToClipboard = function(text,options) {
+ options = options || {};
+ var textArea = document.createElement("textarea");
+ textArea.style.position = "fixed";
+ textArea.style.top = 0;
+ textArea.style.left = 0;
+ textArea.style.fontSize = "12pt";
+ textArea.style.width = "2em";
+ textArea.style.height = "2em";
+ textArea.style.padding = 0;
+ textArea.style.border = "none";
+ textArea.style.outline = "none";
+ textArea.style.boxShadow = "none";
+ textArea.style.background = "transparent";
+ textArea.value = text;
+ document.body.appendChild(textArea);
+ textArea.select();
+ textArea.setSelectionRange(0,text.length);
+ var succeeded = false;
+ try {
+ succeeded = document.execCommand("copy");
+ } catch (err) {
+ }
+ if(!options.doNotNotify) {
+ $tw.notifier.display(succeeded ? "$:/language/Notifications/CopiedToClipboard/Succeeded" : "$:/language/Notifications/CopiedToClipboard/Failed");
+ }
+ document.body.removeChild(textArea);
+};
+
})();
diff --git a/core/modules/utils/dom/dragndrop.js b/core/modules/utils/dom/dragndrop.js
new file mode 100644
index 000000000..5ab3831a4
--- /dev/null
+++ b/core/modules/utils/dom/dragndrop.js
@@ -0,0 +1,202 @@
+/*\
+title: $:/core/modules/utils/dom/dragndrop.js
+type: application/javascript
+module-type: utils
+
+Browser data transfer utilities, used with the clipboard and drag and drop
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Options:
+
+domNode: dom node to make draggable
+dragImageType: "pill" or "dom"
+dragTiddlerFn: optional function to retrieve the title of tiddler to drag
+dragFilterFn: optional function to retreive the filter defining a list of tiddlers to drag
+widget: widget to use as the contect for the filter
+*/
+exports.makeDraggable = function(options) {
+ var dragImageType = options.dragImageType || "dom",
+ dragImage,
+ domNode = options.domNode;
+ // Make the dom node draggable (not necessary for anchor tags)
+ if((domNode.tagName || "").toLowerCase() !== "a") {
+ domNode.setAttribute("draggable","true");
+ }
+ // Add event handlers
+ $tw.utils.addEventListeners(domNode,[
+ {name: "dragstart", handlerFunction: function(event) {
+ if(event.dataTransfer === undefined) {
+ return false;
+ }
+ // Collect the tiddlers being dragged
+ var dragTiddler = options.dragTiddlerFn && options.dragTiddlerFn(),
+ dragFilter = options.dragFilterFn && options.dragFilterFn(),
+ titles = dragTiddler ? [dragTiddler] : [],
+ startActions = options.startActions;
+ if(dragFilter) {
+ titles.push.apply(titles,options.widget.wiki.filterTiddlers(dragFilter,options.widget));
+ }
+ var titleString = $tw.utils.stringifyList(titles);
+ // Check that we've something to drag
+ if(titles.length > 0 && event.target === domNode) {
+ // Mark the drag in progress
+ $tw.dragInProgress = domNode;
+ // Set the dragging class on the element being dragged
+ $tw.utils.addClass(event.target,"tc-dragging");
+ // Invoke drag-start actions if given
+ if(startActions !== undefined) {
+ options.widget.invokeActionString(startActions,options.widget,event,{actionTiddler: titleString});
+ }
+ // Create the drag image elements
+ dragImage = options.widget.document.createElement("div");
+ dragImage.className = "tc-tiddler-dragger";
+ var inner = options.widget.document.createElement("div");
+ inner.className = "tc-tiddler-dragger-inner";
+ inner.appendChild(options.widget.document.createTextNode(
+ titles.length === 1 ?
+ titles[0] :
+ titles.length + " tiddlers"
+ ));
+ dragImage.appendChild(inner);
+ options.widget.document.body.appendChild(dragImage);
+ // Set the data transfer properties
+ var dataTransfer = event.dataTransfer;
+ // Set up the image
+ dataTransfer.effectAllowed = "all";
+ if(dataTransfer.setDragImage) {
+ if(dragImageType === "pill") {
+ dataTransfer.setDragImage(dragImage.firstChild,-16,-16);
+ } else {
+ var r = domNode.getBoundingClientRect();
+ dataTransfer.setDragImage(domNode,event.clientX-r.left,event.clientY-r.top);
+ }
+ }
+ // Set up the data transfer
+ if(dataTransfer.clearData) {
+ dataTransfer.clearData();
+ }
+ var jsonData = [];
+ if(titles.length > 1) {
+ titles.forEach(function(title) {
+ jsonData.push(options.widget.wiki.getTiddlerAsJson(title));
+ });
+ jsonData = "[" + jsonData.join(",") + "]";
+ } else {
+ jsonData = options.widget.wiki.getTiddlerAsJson(titles[0]);
+ }
+ // IE doesn't like these content types
+ if(!$tw.browser.isIE) {
+ dataTransfer.setData("text/vnd.tiddler",jsonData);
+ dataTransfer.setData("text/plain",titleString);
+ dataTransfer.setData("text/x-moz-url","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
+ }
+ dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
+ dataTransfer.setData("Text",titleString);
+ event.stopPropagation();
+ }
+ return false;
+ }},
+ {name: "dragend", handlerFunction: function(event) {
+ if(event.target === domNode) {
+ // Collect the tiddlers being dragged
+ var dragTiddler = options.dragTiddlerFn && options.dragTiddlerFn(),
+ dragFilter = options.dragFilterFn && options.dragFilterFn(),
+ titles = dragTiddler ? [dragTiddler] : [],
+ endActions = options.endActions;
+ if(dragFilter) {
+ titles.push.apply(titles,options.widget.wiki.filterTiddlers(dragFilter,options.widget));
+ }
+ var titleString = $tw.utils.stringifyList(titles);
+ $tw.dragInProgress = null;
+ // Invoke drag-end actions if given
+ if(endActions !== undefined) {
+ options.widget.invokeActionString(endActions,options.widget,event,{actionTiddler: titleString});
+ }
+ // Remove the dragging class on the element being dragged
+ $tw.utils.removeClass(event.target,"tc-dragging");
+ // Delete the drag image element
+ if(dragImage) {
+ dragImage.parentNode.removeChild(dragImage);
+ dragImage = null;
+ }
+ }
+ return false;
+ }}
+ ]);
+};
+
+exports.importDataTransfer = function(dataTransfer,fallbackTitle,callback) {
+ // Try each provided data type in turn
+ for(var t=0; t 1) {
+ alertFields.count = existingCount;
+ } else {
+ alertFields.count = undefined;
+ }
+ $tw.wiki.addTiddler(new $tw.Tiddler(alertFields));
+ // Log the alert as well
+ this.log.apply(this,Array.prototype.slice.call(arguments,0));
} else {
- alertFields = {
- title: $tw.wiki.generateNewTitle("$:/temp/alerts/alert",{prefix: ""}),
- text: text,
- tags: [ALERT_TAG],
- component: this.componentName
- };
- existingCount = 0;
- }
- alertFields.modified = new Date();
- if(++existingCount > 1) {
- alertFields.count = existingCount;
- } else {
- alertFields.count = undefined;
- }
- $tw.wiki.addTiddler(new $tw.Tiddler(alertFields));
- // Log the alert as well
- this.log.apply(this,Array.prototype.slice.call(arguments,0));
- } else {
- // Print an orange message to the console if not in the browser
- console.error("\x1b[1;33m" + text + "\x1b[0m");
+ // Print an orange message to the console if not in the browser
+ console.error("\x1b[1;33m" + text + "\x1b[0m");
+ }
}
};
diff --git a/core/modules/utils/transliterate.js b/core/modules/utils/transliterate.js
new file mode 100644
index 000000000..e69d9293f
--- /dev/null
+++ b/core/modules/utils/transliterate.js
@@ -0,0 +1,919 @@
+/*\
+title: $:/core/modules/utils/transliterate.js
+type: application/javascript
+module-type: utils
+
+Transliteration static utility functions.
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+/*
+Transliterate string to ASCII
+
+(Some pairs taken from http://semplicewebsites.com/removing-accents-javascript)
+*/
+exports.transliterationPairs = {
+ "Á":"A",
+ "Ă":"A",
+ "Ắ":"A",
+ "Ặ":"A",
+ "Ằ":"A",
+ "Ẳ":"A",
+ "Ẵ":"A",
+ "Ǎ":"A",
+ "Â":"A",
+ "Ấ":"A",
+ "Ậ":"A",
+ "Ầ":"A",
+ "Ẩ":"A",
+ "Ẫ":"A",
+ "Ä":"A",
+ "Ǟ":"A",
+ "Ȧ":"A",
+ "Ǡ":"A",
+ "Ạ":"A",
+ "Ȁ":"A",
+ "À":"A",
+ "Ả":"A",
+ "Ȃ":"A",
+ "Ā":"A",
+ "Ą":"A",
+ "Å":"A",
+ "Ǻ":"A",
+ "Ḁ":"A",
+ "Ⱥ":"A",
+ "Ã":"A",
+ "Ꜳ":"AA",
+ "Æ":"AE",
+ "Ǽ":"AE",
+ "Ǣ":"AE",
+ "Ꜵ":"AO",
+ "Ꜷ":"AU",
+ "Ꜹ":"AV",
+ "Ꜻ":"AV",
+ "Ꜽ":"AY",
+ "Ḃ":"B",
+ "Ḅ":"B",
+ "Ɓ":"B",
+ "Ḇ":"B",
+ "Ƀ":"B",
+ "Ƃ":"B",
+ "Ć":"C",
+ "Č":"C",
+ "Ç":"C",
+ "Ḉ":"C",
+ "Ĉ":"C",
+ "Ċ":"C",
+ "Ƈ":"C",
+ "Ȼ":"C",
+ "Ď":"D",
+ "Ḑ":"D",
+ "Ḓ":"D",
+ "Ḋ":"D",
+ "Ḍ":"D",
+ "Ɗ":"D",
+ "Ḏ":"D",
+ "Dz":"D",
+ "Dž":"D",
+ "Đ":"D",
+ "Ƌ":"D",
+ "DZ":"DZ",
+ "DŽ":"DZ",
+ "É":"E",
+ "Ĕ":"E",
+ "Ě":"E",
+ "Ȩ":"E",
+ "Ḝ":"E",
+ "Ê":"E",
+ "Ế":"E",
+ "Ệ":"E",
+ "Ề":"E",
+ "Ể":"E",
+ "Ễ":"E",
+ "Ḙ":"E",
+ "Ë":"E",
+ "Ė":"E",
+ "Ẹ":"E",
+ "Ȅ":"E",
+ "È":"E",
+ "Ẻ":"E",
+ "Ȇ":"E",
+ "Ē":"E",
+ "Ḗ":"E",
+ "Ḕ":"E",
+ "Ę":"E",
+ "Ɇ":"E",
+ "Ẽ":"E",
+ "Ḛ":"E",
+ "Ꝫ":"ET",
+ "Ḟ":"F",
+ "Ƒ":"F",
+ "Ǵ":"G",
+ "Ğ":"G",
+ "Ǧ":"G",
+ "Ģ":"G",
+ "Ĝ":"G",
+ "Ġ":"G",
+ "Ɠ":"G",
+ "Ḡ":"G",
+ "Ǥ":"G",
+ "Ḫ":"H",
+ "Ȟ":"H",
+ "Ḩ":"H",
+ "Ĥ":"H",
+ "Ⱨ":"H",
+ "Ḧ":"H",
+ "Ḣ":"H",
+ "Ḥ":"H",
+ "Ħ":"H",
+ "Í":"I",
+ "Ĭ":"I",
+ "Ǐ":"I",
+ "Î":"I",
+ "Ï":"I",
+ "Ḯ":"I",
+ "İ":"I",
+ "Ị":"I",
+ "Ȉ":"I",
+ "Ì":"I",
+ "Ỉ":"I",
+ "Ȋ":"I",
+ "Ī":"I",
+ "Į":"I",
+ "Ɨ":"I",
+ "Ĩ":"I",
+ "Ḭ":"I",
+ "Ꝺ":"D",
+ "Ꝼ":"F",
+ "Ᵹ":"G",
+ "Ꞃ":"R",
+ "Ꞅ":"S",
+ "Ꞇ":"T",
+ "Ꝭ":"IS",
+ "Ĵ":"J",
+ "Ɉ":"J",
+ "Ḱ":"K",
+ "Ǩ":"K",
+ "Ķ":"K",
+ "Ⱪ":"K",
+ "Ꝃ":"K",
+ "Ḳ":"K",
+ "Ƙ":"K",
+ "Ḵ":"K",
+ "Ꝁ":"K",
+ "Ꝅ":"K",
+ "Ĺ":"L",
+ "Ƚ":"L",
+ "Ľ":"L",
+ "Ļ":"L",
+ "Ḽ":"L",
+ "Ḷ":"L",
+ "Ḹ":"L",
+ "Ⱡ":"L",
+ "Ꝉ":"L",
+ "Ḻ":"L",
+ "Ŀ":"L",
+ "Ɫ":"L",
+ "Lj":"L",
+ "Ł":"L",
+ "LJ":"LJ",
+ "Ḿ":"M",
+ "Ṁ":"M",
+ "Ṃ":"M",
+ "Ɱ":"M",
+ "Ń":"N",
+ "Ň":"N",
+ "Ņ":"N",
+ "Ṋ":"N",
+ "Ṅ":"N",
+ "Ṇ":"N",
+ "Ǹ":"N",
+ "Ɲ":"N",
+ "Ṉ":"N",
+ "Ƞ":"N",
+ "Nj":"N",
+ "Ñ":"N",
+ "NJ":"NJ",
+ "Ó":"O",
+ "Ŏ":"O",
+ "Ǒ":"O",
+ "Ô":"O",
+ "Ố":"O",
+ "Ộ":"O",
+ "Ồ":"O",
+ "Ổ":"O",
+ "Ỗ":"O",
+ "Ö":"O",
+ "Ȫ":"O",
+ "Ȯ":"O",
+ "Ȱ":"O",
+ "Ọ":"O",
+ "Ő":"O",
+ "Ȍ":"O",
+ "Ò":"O",
+ "Ỏ":"O",
+ "Ơ":"O",
+ "Ớ":"O",
+ "Ợ":"O",
+ "Ờ":"O",
+ "Ở":"O",
+ "Ỡ":"O",
+ "Ȏ":"O",
+ "Ꝋ":"O",
+ "Ꝍ":"O",
+ "Ō":"O",
+ "Ṓ":"O",
+ "Ṑ":"O",
+ "Ɵ":"O",
+ "Ǫ":"O",
+ "Ǭ":"O",
+ "Ø":"O",
+ "Ǿ":"O",
+ "Õ":"O",
+ "Ṍ":"O",
+ "Ṏ":"O",
+ "Ȭ":"O",
+ "Ƣ":"OI",
+ "Ꝏ":"OO",
+ "Ɛ":"E",
+ "Ɔ":"O",
+ "Ȣ":"OU",
+ "Ṕ":"P",
+ "Ṗ":"P",
+ "Ꝓ":"P",
+ "Ƥ":"P",
+ "Ꝕ":"P",
+ "Ᵽ":"P",
+ "Ꝑ":"P",
+ "Ꝙ":"Q",
+ "Ꝗ":"Q",
+ "Ŕ":"R",
+ "Ř":"R",
+ "Ŗ":"R",
+ "Ṙ":"R",
+ "Ṛ":"R",
+ "Ṝ":"R",
+ "Ȑ":"R",
+ "Ȓ":"R",
+ "Ṟ":"R",
+ "Ɍ":"R",
+ "Ɽ":"R",
+ "Ꜿ":"C",
+ "Ǝ":"E",
+ "Ś":"S",
+ "Ṥ":"S",
+ "Š":"S",
+ "Ṧ":"S",
+ "Ş":"S",
+ "Ŝ":"S",
+ "Ș":"S",
+ "Ṡ":"S",
+ "Ṣ":"S",
+ "Ṩ":"S",
+ "Ť":"T",
+ "Ţ":"T",
+ "Ṱ":"T",
+ "Ț":"T",
+ "Ⱦ":"T",
+ "Ṫ":"T",
+ "Ṭ":"T",
+ "Ƭ":"T",
+ "Ṯ":"T",
+ "Ʈ":"T",
+ "Ŧ":"T",
+ "Ɐ":"A",
+ "Ꞁ":"L",
+ "Ɯ":"M",
+ "Ʌ":"V",
+ "Ꜩ":"TZ",
+ "Ú":"U",
+ "Ŭ":"U",
+ "Ǔ":"U",
+ "Û":"U",
+ "Ṷ":"U",
+ "Ü":"U",
+ "Ǘ":"U",
+ "Ǚ":"U",
+ "Ǜ":"U",
+ "Ǖ":"U",
+ "Ṳ":"U",
+ "Ụ":"U",
+ "Ű":"U",
+ "Ȕ":"U",
+ "Ù":"U",
+ "Ủ":"U",
+ "Ư":"U",
+ "Ứ":"U",
+ "Ự":"U",
+ "Ừ":"U",
+ "Ử":"U",
+ "Ữ":"U",
+ "Ȗ":"U",
+ "Ū":"U",
+ "Ṻ":"U",
+ "Ų":"U",
+ "Ů":"U",
+ "Ũ":"U",
+ "Ṹ":"U",
+ "Ṵ":"U",
+ "Ꝟ":"V",
+ "Ṿ":"V",
+ "Ʋ":"V",
+ "Ṽ":"V",
+ "Ꝡ":"VY",
+ "Ẃ":"W",
+ "Ŵ":"W",
+ "Ẅ":"W",
+ "Ẇ":"W",
+ "Ẉ":"W",
+ "Ẁ":"W",
+ "Ⱳ":"W",
+ "Ẍ":"X",
+ "Ẋ":"X",
+ "Ý":"Y",
+ "Ŷ":"Y",
+ "Ÿ":"Y",
+ "Ẏ":"Y",
+ "Ỵ":"Y",
+ "Ỳ":"Y",
+ "Ƴ":"Y",
+ "Ỷ":"Y",
+ "Ỿ":"Y",
+ "Ȳ":"Y",
+ "Ɏ":"Y",
+ "Ỹ":"Y",
+ "Ź":"Z",
+ "Ž":"Z",
+ "Ẑ":"Z",
+ "Ⱬ":"Z",
+ "Ż":"Z",
+ "Ẓ":"Z",
+ "Ȥ":"Z",
+ "Ẕ":"Z",
+ "Ƶ":"Z",
+ "IJ":"IJ",
+ "Œ":"OE",
+ "ᴀ":"A",
+ "ᴁ":"AE",
+ "ʙ":"B",
+ "ᴃ":"B",
+ "ᴄ":"C",
+ "ᴅ":"D",
+ "ᴇ":"E",
+ "ꜰ":"F",
+ "ɢ":"G",
+ "ʛ":"G",
+ "ʜ":"H",
+ "ɪ":"I",
+ "ʁ":"R",
+ "ᴊ":"J",
+ "ᴋ":"K",
+ "ʟ":"L",
+ "ᴌ":"L",
+ "ᴍ":"M",
+ "ɴ":"N",
+ "ᴏ":"O",
+ "ɶ":"OE",
+ "ᴐ":"O",
+ "ᴕ":"OU",
+ "ᴘ":"P",
+ "ʀ":"R",
+ "ᴎ":"N",
+ "ᴙ":"R",
+ "ꜱ":"S",
+ "ᴛ":"T",
+ "ⱻ":"E",
+ "ᴚ":"R",
+ "ᴜ":"U",
+ "ᴠ":"V",
+ "ᴡ":"W",
+ "ʏ":"Y",
+ "ᴢ":"Z",
+ "á":"a",
+ "ă":"a",
+ "ắ":"a",
+ "ặ":"a",
+ "ằ":"a",
+ "ẳ":"a",
+ "ẵ":"a",
+ "ǎ":"a",
+ "â":"a",
+ "ấ":"a",
+ "ậ":"a",
+ "ầ":"a",
+ "ẩ":"a",
+ "ẫ":"a",
+ "ä":"a",
+ "ǟ":"a",
+ "ȧ":"a",
+ "ǡ":"a",
+ "ạ":"a",
+ "ȁ":"a",
+ "à":"a",
+ "ả":"a",
+ "ȃ":"a",
+ "ā":"a",
+ "ą":"a",
+ "ᶏ":"a",
+ "ẚ":"a",
+ "å":"a",
+ "ǻ":"a",
+ "ḁ":"a",
+ "ⱥ":"a",
+ "ã":"a",
+ "ꜳ":"aa",
+ "æ":"ae",
+ "ǽ":"ae",
+ "ǣ":"ae",
+ "ꜵ":"ao",
+ "ꜷ":"au",
+ "ꜹ":"av",
+ "ꜻ":"av",
+ "ꜽ":"ay",
+ "ḃ":"b",
+ "ḅ":"b",
+ "ɓ":"b",
+ "ḇ":"b",
+ "ᵬ":"b",
+ "ᶀ":"b",
+ "ƀ":"b",
+ "ƃ":"b",
+ "ɵ":"o",
+ "ć":"c",
+ "č":"c",
+ "ç":"c",
+ "ḉ":"c",
+ "ĉ":"c",
+ "ɕ":"c",
+ "ċ":"c",
+ "ƈ":"c",
+ "ȼ":"c",
+ "ď":"d",
+ "ḑ":"d",
+ "ḓ":"d",
+ "ȡ":"d",
+ "ḋ":"d",
+ "ḍ":"d",
+ "ɗ":"d",
+ "ᶑ":"d",
+ "ḏ":"d",
+ "ᵭ":"d",
+ "ᶁ":"d",
+ "đ":"d",
+ "ɖ":"d",
+ "ƌ":"d",
+ "ı":"i",
+ "ȷ":"j",
+ "ɟ":"j",
+ "ʄ":"j",
+ "dz":"dz",
+ "dž":"dz",
+ "é":"e",
+ "ĕ":"e",
+ "ě":"e",
+ "ȩ":"e",
+ "ḝ":"e",
+ "ê":"e",
+ "ế":"e",
+ "ệ":"e",
+ "ề":"e",
+ "ể":"e",
+ "ễ":"e",
+ "ḙ":"e",
+ "ë":"e",
+ "ė":"e",
+ "ẹ":"e",
+ "ȅ":"e",
+ "è":"e",
+ "ẻ":"e",
+ "ȇ":"e",
+ "ē":"e",
+ "ḗ":"e",
+ "ḕ":"e",
+ "ⱸ":"e",
+ "ę":"e",
+ "ᶒ":"e",
+ "ɇ":"e",
+ "ẽ":"e",
+ "ḛ":"e",
+ "ꝫ":"et",
+ "ḟ":"f",
+ "ƒ":"f",
+ "ᵮ":"f",
+ "ᶂ":"f",
+ "ǵ":"g",
+ "ğ":"g",
+ "ǧ":"g",
+ "ģ":"g",
+ "ĝ":"g",
+ "ġ":"g",
+ "ɠ":"g",
+ "ḡ":"g",
+ "ᶃ":"g",
+ "ǥ":"g",
+ "ḫ":"h",
+ "ȟ":"h",
+ "ḩ":"h",
+ "ĥ":"h",
+ "ⱨ":"h",
+ "ḧ":"h",
+ "ḣ":"h",
+ "ḥ":"h",
+ "ɦ":"h",
+ "ẖ":"h",
+ "ħ":"h",
+ "ƕ":"hv",
+ "í":"i",
+ "ĭ":"i",
+ "ǐ":"i",
+ "î":"i",
+ "ï":"i",
+ "ḯ":"i",
+ "ị":"i",
+ "ȉ":"i",
+ "ì":"i",
+ "ỉ":"i",
+ "ȋ":"i",
+ "ī":"i",
+ "į":"i",
+ "ᶖ":"i",
+ "ɨ":"i",
+ "ĩ":"i",
+ "ḭ":"i",
+ "ꝺ":"d",
+ "ꝼ":"f",
+ "ᵹ":"g",
+ "ꞃ":"r",
+ "ꞅ":"s",
+ "ꞇ":"t",
+ "ꝭ":"is",
+ "ǰ":"j",
+ "ĵ":"j",
+ "ʝ":"j",
+ "ɉ":"j",
+ "ḱ":"k",
+ "ǩ":"k",
+ "ķ":"k",
+ "ⱪ":"k",
+ "ꝃ":"k",
+ "ḳ":"k",
+ "ƙ":"k",
+ "ḵ":"k",
+ "ᶄ":"k",
+ "ꝁ":"k",
+ "ꝅ":"k",
+ "ĺ":"l",
+ "ƚ":"l",
+ "ɬ":"l",
+ "ľ":"l",
+ "ļ":"l",
+ "ḽ":"l",
+ "ȴ":"l",
+ "ḷ":"l",
+ "ḹ":"l",
+ "ⱡ":"l",
+ "ꝉ":"l",
+ "ḻ":"l",
+ "ŀ":"l",
+ "ɫ":"l",
+ "ᶅ":"l",
+ "ɭ":"l",
+ "ł":"l",
+ "lj":"lj",
+ "ſ":"s",
+ "ẜ":"s",
+ "ẛ":"s",
+ "ẝ":"s",
+ "ḿ":"m",
+ "ṁ":"m",
+ "ṃ":"m",
+ "ɱ":"m",
+ "ᵯ":"m",
+ "ᶆ":"m",
+ "ń":"n",
+ "ň":"n",
+ "ņ":"n",
+ "ṋ":"n",
+ "ȵ":"n",
+ "ṅ":"n",
+ "ṇ":"n",
+ "ǹ":"n",
+ "ɲ":"n",
+ "ṉ":"n",
+ "ƞ":"n",
+ "ᵰ":"n",
+ "ᶇ":"n",
+ "ɳ":"n",
+ "ñ":"n",
+ "nj":"nj",
+ "ó":"o",
+ "ŏ":"o",
+ "ǒ":"o",
+ "ô":"o",
+ "ố":"o",
+ "ộ":"o",
+ "ồ":"o",
+ "ổ":"o",
+ "ỗ":"o",
+ "ö":"o",
+ "ȫ":"o",
+ "ȯ":"o",
+ "ȱ":"o",
+ "ọ":"o",
+ "ő":"o",
+ "ȍ":"o",
+ "ò":"o",
+ "ỏ":"o",
+ "ơ":"o",
+ "ớ":"o",
+ "ợ":"o",
+ "ờ":"o",
+ "ở":"o",
+ "ỡ":"o",
+ "ȏ":"o",
+ "ꝋ":"o",
+ "ꝍ":"o",
+ "ⱺ":"o",
+ "ō":"o",
+ "ṓ":"o",
+ "ṑ":"o",
+ "ǫ":"o",
+ "ǭ":"o",
+ "ø":"o",
+ "ǿ":"o",
+ "õ":"o",
+ "ṍ":"o",
+ "ṏ":"o",
+ "ȭ":"o",
+ "ƣ":"oi",
+ "ꝏ":"oo",
+ "ɛ":"e",
+ "ᶓ":"e",
+ "ɔ":"o",
+ "ᶗ":"o",
+ "ȣ":"ou",
+ "ṕ":"p",
+ "ṗ":"p",
+ "ꝓ":"p",
+ "ƥ":"p",
+ "ᵱ":"p",
+ "ᶈ":"p",
+ "ꝕ":"p",
+ "ᵽ":"p",
+ "ꝑ":"p",
+ "ꝙ":"q",
+ "ʠ":"q",
+ "ɋ":"q",
+ "ꝗ":"q",
+ "ŕ":"r",
+ "ř":"r",
+ "ŗ":"r",
+ "ṙ":"r",
+ "ṛ":"r",
+ "ṝ":"r",
+ "ȑ":"r",
+ "ɾ":"r",
+ "ᵳ":"r",
+ "ȓ":"r",
+ "ṟ":"r",
+ "ɼ":"r",
+ "ᵲ":"r",
+ "ᶉ":"r",
+ "ɍ":"r",
+ "ɽ":"r",
+ "ↄ":"c",
+ "ꜿ":"c",
+ "ɘ":"e",
+ "ɿ":"r",
+ "ś":"s",
+ "ṥ":"s",
+ "š":"s",
+ "ṧ":"s",
+ "ş":"s",
+ "ŝ":"s",
+ "ș":"s",
+ "ṡ":"s",
+ "ṣ":"s",
+ "ṩ":"s",
+ "ʂ":"s",
+ "ᵴ":"s",
+ "ᶊ":"s",
+ "ȿ":"s",
+ "ɡ":"g",
+ "ᴑ":"o",
+ "ᴓ":"o",
+ "ᴝ":"u",
+ "ť":"t",
+ "ţ":"t",
+ "ṱ":"t",
+ "ț":"t",
+ "ȶ":"t",
+ "ẗ":"t",
+ "ⱦ":"t",
+ "ṫ":"t",
+ "ṭ":"t",
+ "ƭ":"t",
+ "ṯ":"t",
+ "ᵵ":"t",
+ "ƫ":"t",
+ "ʈ":"t",
+ "ŧ":"t",
+ "ᵺ":"th",
+ "ɐ":"a",
+ "ᴂ":"ae",
+ "ǝ":"e",
+ "ᵷ":"g",
+ "ɥ":"h",
+ "ʮ":"h",
+ "ʯ":"h",
+ "ᴉ":"i",
+ "ʞ":"k",
+ "ꞁ":"l",
+ "ɯ":"m",
+ "ɰ":"m",
+ "ᴔ":"oe",
+ "ɹ":"r",
+ "ɻ":"r",
+ "ɺ":"r",
+ "ⱹ":"r",
+ "ʇ":"t",
+ "ʌ":"v",
+ "ʍ":"w",
+ "ʎ":"y",
+ "ꜩ":"tz",
+ "ú":"u",
+ "ŭ":"u",
+ "ǔ":"u",
+ "û":"u",
+ "ṷ":"u",
+ "ü":"u",
+ "ǘ":"u",
+ "ǚ":"u",
+ "ǜ":"u",
+ "ǖ":"u",
+ "ṳ":"u",
+ "ụ":"u",
+ "ű":"u",
+ "ȕ":"u",
+ "ù":"u",
+ "ủ":"u",
+ "ư":"u",
+ "ứ":"u",
+ "ự":"u",
+ "ừ":"u",
+ "ử":"u",
+ "ữ":"u",
+ "ȗ":"u",
+ "ū":"u",
+ "ṻ":"u",
+ "ų":"u",
+ "ᶙ":"u",
+ "ů":"u",
+ "ũ":"u",
+ "ṹ":"u",
+ "ṵ":"u",
+ "ᵫ":"ue",
+ "ꝸ":"um",
+ "ⱴ":"v",
+ "ꝟ":"v",
+ "ṿ":"v",
+ "ʋ":"v",
+ "ᶌ":"v",
+ "ⱱ":"v",
+ "ṽ":"v",
+ "ꝡ":"vy",
+ "ẃ":"w",
+ "ŵ":"w",
+ "ẅ":"w",
+ "ẇ":"w",
+ "ẉ":"w",
+ "ẁ":"w",
+ "ⱳ":"w",
+ "ẘ":"w",
+ "ẍ":"x",
+ "ẋ":"x",
+ "ᶍ":"x",
+ "ý":"y",
+ "ŷ":"y",
+ "ÿ":"y",
+ "ẏ":"y",
+ "ỵ":"y",
+ "ỳ":"y",
+ "ƴ":"y",
+ "ỷ":"y",
+ "ỿ":"y",
+ "ȳ":"y",
+ "ẙ":"y",
+ "ɏ":"y",
+ "ỹ":"y",
+ "ź":"z",
+ "ž":"z",
+ "ẑ":"z",
+ "ʑ":"z",
+ "ⱬ":"z",
+ "ż":"z",
+ "ẓ":"z",
+ "ȥ":"z",
+ "ẕ":"z",
+ "ᵶ":"z",
+ "ᶎ":"z",
+ "ʐ":"z",
+ "ƶ":"z",
+ "ɀ":"z",
+ "ff":"ff",
+ "ffi":"ffi",
+ "ffl":"ffl",
+ "fi":"fi",
+ "fl":"fl",
+ "ij":"ij",
+ "œ":"oe",
+ "st":"st",
+ "ₐ":"a",
+ "ₑ":"e",
+ "ᵢ":"i",
+ "ⱼ":"j",
+ "ₒ":"o",
+ "ᵣ":"r",
+ "ᵤ":"u",
+ "ᵥ":"v",
+ "ₓ":"x",
+ "Ё":"YO",
+ "Й":"I",
+ "Ц":"TS",
+ "У":"U",
+ "К":"K",
+ "Е":"E",
+ "Н":"N",
+ "Г":"G",
+ "Ш":"SH",
+ "Щ":"SCH",
+ "З":"Z",
+ "Х":"H",
+ "Ъ":"'",
+ "ё":"yo",
+ "й":"i",
+ "ц":"ts",
+ "у":"u",
+ "к":"k",
+ "е":"e",
+ "н":"n",
+ "г":"g",
+ "ш":"sh",
+ "щ":"sch",
+ "з":"z",
+ "х":"h",
+ "ъ":"'",
+ "Ф":"F",
+ "Ы":"I",
+ "В":"V",
+ "А":"a",
+ "П":"P",
+ "Р":"R",
+ "О":"O",
+ "Л":"L",
+ "Д":"D",
+ "Ж":"ZH",
+ "Э":"E",
+ "ф":"f",
+ "ы":"i",
+ "в":"v",
+ "а":"a",
+ "п":"p",
+ "р":"r",
+ "о":"o",
+ "л":"l",
+ "д":"d",
+ "ж":"zh",
+ "э":"e",
+ "Я":"Ya",
+ "Ч":"CH",
+ "С":"S",
+ "М":"M",
+ "И":"I",
+ "Т":"T",
+ "Ь":"'",
+ "Б":"B",
+ "Ю":"YU",
+ "я":"ya",
+ "ч":"ch",
+ "с":"s",
+ "м":"m",
+ "и":"i",
+ "т":"t",
+ "ь":"'",
+ "б":"b",
+ "ю":"yu"
+};
+
+exports.transliterate = function(str) {
+ return str.replace(/[^A-Za-z0-9\[\] ]/g,function(ch) {
+ return exports.transliterationPairs[ch] || ch
+ });
+};
+
+})();
diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js
index e322811d9..2c06cd5c3 100644
--- a/core/modules/utils/utils.js
+++ b/core/modules/utils/utils.js
@@ -12,11 +12,61 @@ Various static utility functions.
/*global $tw: false */
"use strict";
+/*
+Display a message, in colour if we're on a terminal
+*/
+exports.log = function(text,colour) {
+ console.log($tw.node ? exports.terminalColour(colour) + text + exports.terminalColour() : text);
+};
+
+exports.terminalColour = function(colour) {
+ if(!$tw.browser && $tw.node && process.stdout.isTTY) {
+ if(colour) {
+ var code = exports.terminalColourLookup[colour];
+ if(code) {
+ return "\x1b[" + code + "m";
+ }
+ } else {
+ return "\x1b[0m"; // Cancel colour
+ }
+ }
+ return "";
+};
+
+exports.terminalColourLookup = {
+ "black": "0;30",
+ "red": "0;31",
+ "green": "0;32",
+ "brown/orange": "0;33",
+ "blue": "0;34",
+ "purple": "0;35",
+ "cyan": "0;36",
+ "light gray": "0;37"
+};
+
/*
Display a warning, in colour if we're on a terminal
*/
exports.warning = function(text) {
- console.log($tw.node ? "\x1b[1;33m" + text + "\x1b[0m" : text);
+ exports.log(text,"brown/orange");
+};
+
+/*
+Return the integer represented by the str (string).
+Return the dflt (default) parameter if str is not a base-10 number.
+*/
+exports.getInt = function(str,deflt) {
+ var i = parseInt(str,10);
+ return isNaN(i) ? deflt : i;
+}
+
+/*
+Repeatedly replaces a substring within a string. Like String.prototype.replace, but without any of the default special handling of $ sequences in the replace string
+*/
+exports.replaceString = function(text,search,replace) {
+ return text.replace(search,function() {
+ return replace;
+ });
};
/*
@@ -215,11 +265,13 @@ exports.extendDeepCopy = function(object,extendedProperties) {
exports.deepFreeze = function deepFreeze(object) {
var property, key;
- Object.freeze(object);
- for(key in object) {
- property = object[key];
- if($tw.utils.hop(object,key) && (typeof property === "object") && !Object.isFrozen(property)) {
- deepFreeze(property);
+ if(object) {
+ Object.freeze(object);
+ for(key in object) {
+ property = object[key];
+ if($tw.utils.hop(object,key) && (typeof property === "object") && !Object.isFrozen(property)) {
+ deepFreeze(property);
+ }
}
}
};
@@ -256,6 +308,9 @@ exports.formatDateString = function(date,template) {
[/^0ss/, function() {
return $tw.utils.pad(date.getSeconds());
}],
+ [/^0XXX/, function() {
+ return $tw.utils.pad(date.getMilliseconds());
+ }],
[/^0DD/, function() {
return $tw.utils.pad(date.getDate());
}],
@@ -297,6 +352,9 @@ exports.formatDateString = function(date,template) {
[/^ss/, function() {
return date.getSeconds();
}],
+ [/^XXX/, function() {
+ return date.getMilliseconds();
+ }],
[/^[AP]M/, function() {
return $tw.utils.getAmPm(date).toUpperCase();
}],
@@ -313,6 +371,16 @@ exports.formatDateString = function(date,template) {
return $tw.utils.pad(date.getFullYear() - 2000);
}]
];
+ // If the user wants everything in UTC, shift the datestamp
+ // Optimize for format string that essentially means
+ // 'return raw UTC (tiddlywiki style) date string.'
+ if(t.indexOf("[UTC]") == 0 ) {
+ if(t == "[UTC]YYYY0MM0DD0hh0mm0ssXXX")
+ return $tw.utils.stringifyDate(new Date());
+ var offset = date.getTimezoneOffset() ; // in minutes
+ date = new Date(date.getTime()+offset*60*1000) ;
+ t = t.substr(5) ;
+ }
while(t.length){
var matchString = "";
$tw.utils.each(matches, function(m) {
@@ -349,7 +417,8 @@ exports.getWeek = function(date) {
d = 7; // JavaScript Sun=0, ISO Sun=7
}
dt.setTime(dt.getTime() + (4 - d) * 86400000);// shift day to Thurs of same week to calculate weekNo
- var n = Math.floor((dt.getTime()-new Date(dt.getFullYear(),0,1) + 3600000) / 86400000);
+ var x = new Date(dt.getFullYear(),0,1);
+ var n = Math.floor((dt.getTime() - x.getTime()) / 86400000);
return Math.floor(n / 7) + 1;
};
@@ -431,7 +500,7 @@ exports.entityDecode = function(s) {
e = s.substr(1,s.length-2); // Strip the & and the ;
if(e.charAt(0) === "#") {
if(e.charAt(1) === "x" || e.charAt(1) === "X") {
- return converter(parseInt(e.substr(2),16));
+ return converter(parseInt(e.substr(2),16));
} else {
return converter(parseInt(e.substr(1),10));
}
@@ -483,7 +552,24 @@ exports.stringify = function(s) {
.replace(/'/g, "\\'") // single quote character
.replace(/\r/g, '\\r') // carriage return
.replace(/\n/g, '\\n') // line feed
- .replace(/[\x80-\uFFFF]/g, exports.escape); // non-ASCII characters
+ .replace(/[\x00-\x1f\x80-\uFFFF]/g, exports.escape); // non-ASCII characters
+};
+
+// Turns a string into a legal JSON string
+// Derived from peg.js, thanks to David Majda
+exports.jsonStringify = function(s) {
+ // See http://www.json.org/
+ return (s || "")
+ .replace(/\\/g, '\\\\') // backslash
+ .replace(/"/g, '\\"') // double quote character
+ .replace(/\r/g, '\\r') // carriage return
+ .replace(/\n/g, '\\n') // line feed
+ .replace(/\x08/g, '\\b') // backspace
+ .replace(/\x0c/g, '\\f') // formfeed
+ .replace(/\t/g, '\\t') // tab
+ .replace(/[\x00-\x1f\x80-\uFFFF]/g,function(s) {
+ return '\\u' + $tw.utils.pad(s.charCodeAt(0).toString(16).toUpperCase(),4);
+ }); // non-ASCII characters
};
/*
@@ -626,7 +712,7 @@ exports.base64Decode = function(string64) {
// TODO
throw "$tw.utils.base64Decode() doesn't work in the browser";
} else {
- return (new Buffer(string64,"base64")).toString();
+ return Buffer.from(string64,"base64").toString();
}
};
@@ -647,7 +733,7 @@ High resolution microsecond timer for profiling
exports.timer = function(base) {
var m;
if($tw.node) {
- var r = process.hrtime();
+ var r = process.hrtime();
m = r[0] * 1e3 + (r[1] / 1e6);
} else if(window.performance) {
m = performance.now();
@@ -687,7 +773,6 @@ exports.tagToCssSelector = function(tagName) {
});
};
-
/*
IE does not have sign function
*/
@@ -709,7 +794,7 @@ exports.strEndsWith = function(str,ending,position) {
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > str.length) {
position = str.length;
}
- position -= str.length;
+ position -= ending.length;
var lastIndex = str.indexOf(ending, position);
return lastIndex !== -1 && lastIndex === position;
}
diff --git a/core/modules/widgets/action-createtiddler.js b/core/modules/widgets/action-createtiddler.js
new file mode 100644
index 000000000..01010b940
--- /dev/null
+++ b/core/modules/widgets/action-createtiddler.js
@@ -0,0 +1,81 @@
+/*\
+title: $:/core/modules/widgets/action-createtiddler.js
+type: application/javascript
+module-type: widget
+
+Action widget to create a new tiddler with a unique name and specified fields.
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget;
+
+var CreateTiddlerWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+/*
+Inherit from the base widget class
+*/
+CreateTiddlerWidget.prototype = new Widget();
+
+/*
+Render this widget into the DOM
+*/
+CreateTiddlerWidget.prototype.render = function(parent,nextSibling) {
+ this.computeAttributes();
+ this.execute();
+};
+
+/*
+Compute the internal state of the widget
+*/
+CreateTiddlerWidget.prototype.execute = function() {
+ this.actionBaseTitle = this.getAttribute("$basetitle");
+ this.actionSaveTitle = this.getAttribute("$savetitle");
+ this.actionTimestamp = this.getAttribute("$timestamp","yes") === "yes";
+};
+
+/*
+Refresh the widget by ensuring our attributes are up to date
+*/
+CreateTiddlerWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if($tw.utils.count(changedAttributes) > 0) {
+ this.refreshSelf();
+ return true;
+ }
+ return this.refreshChildren(changedTiddlers);
+};
+
+/*
+Invoke the action associated with this widget
+*/
+CreateTiddlerWidget.prototype.invokeAction = function(triggeringWidget,event) {
+ var title = this.wiki.generateNewTitle(this.actionBaseTitle),
+ fields = {},
+ creationFields,
+ modificationFields;
+ $tw.utils.each(this.attributes,function(attribute,name) {
+ if(name.charAt(0) !== "$") {
+ fields[name] = attribute;
+ }
+ });
+ if(this.actionTimestamp) {
+ creationFields = this.wiki.getCreationFields();
+ modificationFields = this.wiki.getModificationFields();
+ }
+ var tiddler = this.wiki.addTiddler(new $tw.Tiddler(creationFields,fields,modificationFields,{title: title}));
+ if(this.actionSaveTitle) {
+ this.wiki.setTextReference(this.actionSaveTitle,title,this.getVariable("currentTiddler"));
+ }
+ return true; // Action was invoked
+};
+
+exports["action-createtiddler"] = CreateTiddlerWidget;
+
+})();
diff --git a/core/modules/widgets/action-deletefield.js b/core/modules/widgets/action-deletefield.js
index 8e9080f91..ffe4ef5d0 100644
--- a/core/modules/widgets/action-deletefield.js
+++ b/core/modules/widgets/action-deletefield.js
@@ -57,17 +57,24 @@ Invoke the action associated with this widget
DeleteFieldWidget.prototype.invokeAction = function(triggeringWidget,event) {
var self = this,
tiddler = this.wiki.getTiddler(self.actionTiddler),
- removeFields = {};
+ removeFields = {},
+ hasChanged = false;
if(this.actionField) {
removeFields[this.actionField] = undefined;
+ if(this.actionField in tiddler.fields) {
+ hasChanged = true;
+ }
}
if(tiddler) {
$tw.utils.each(this.attributes,function(attribute,name) {
if(name.charAt(0) !== "$" && name !== "title") {
removeFields[name] = undefined;
+ hasChanged = true;
}
});
- this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getModificationFields(),tiddler,removeFields,this.wiki.getCreationFields()));
+ if(hasChanged) {
+ this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,removeFields,this.wiki.getModificationFields()));
+ }
}
return true; // Action was invoked
};
diff --git a/core/modules/widgets/action-listops.js b/core/modules/widgets/action-listops.js
index a6b0d4f62..6da84a3e5 100644
--- a/core/modules/widgets/action-listops.js
+++ b/core/modules/widgets/action-listops.js
@@ -80,9 +80,13 @@ ActionListopsWidget.prototype.invokeAction = function(triggeringWidget,
.filterTiddlers(subfilter, this)));
}
if(this.filtertags) {
- var tagfilter = "[list[" + this.target + "!!tags]] " + this.filtertags;
- this.wiki.setText(this.target, "tags", undefined, $tw.utils.stringifyList(
- this.wiki.filterTiddlers(tagfilter, this)));
+ var tiddler = this.wiki.getTiddler(this.target),
+ oldtags = tiddler ? (tiddler.fields.tags || []).slice(0) : [],
+ tagfilter = "[list[" + this.target + "!!tags]] " + this.filtertags,
+ newtags = this.wiki.filterTiddlers(tagfilter,this);
+ if($tw.utils.stringifyList(oldtags.sort()) !== $tw.utils.stringifyList(newtags.sort())) {
+ this.wiki.setText(this.target,"tags",undefined,$tw.utils.stringifyList(newtags));
+ }
}
return true; // Action was invoked
};
diff --git a/core/modules/widgets/action-sendmessage.js b/core/modules/widgets/action-sendmessage.js
index 2f1b9cae4..c53314b2d 100644
--- a/core/modules/widgets/action-sendmessage.js
+++ b/core/modules/widgets/action-sendmessage.js
@@ -78,7 +78,8 @@ SendMessageWidget.prototype.invokeAction = function(triggeringWidget,event) {
param: param,
paramObject: paramObject,
tiddlerTitle: this.getVariable("currentTiddler"),
- navigateFromTitle: this.getVariable("storyTiddler")
+ navigateFromTitle: this.getVariable("storyTiddler"),
+ event: event
});
return true; // Action was invoked
};
diff --git a/core/modules/widgets/browse.js b/core/modules/widgets/browse.js
index fca8e7467..ed57e4c33 100644
--- a/core/modules/widgets/browse.js
+++ b/core/modules/widgets/browse.js
@@ -46,17 +46,23 @@ BrowseWidget.prototype.render = function(parent,nextSibling) {
if(this.nwsaveas) {
domNode.setAttribute("nwsaveas",this.nwsaveas);
}
- // Nw.js supports "webkitdirectory" to allow a directory to be selected
+ // Nw.js supports "webkitdirectory" and "nwdirectory" to allow a directory to be selected
if(this.webkitdirectory) {
domNode.setAttribute("webkitdirectory",this.webkitdirectory);
}
+ if(this.nwdirectory) {
+ domNode.setAttribute("nwdirectory",this.nwdirectory);
+ }
// Add a click event handler
domNode.addEventListener("change",function (event) {
if(self.message) {
self.dispatchEvent({type: self.message, param: self.param, files: event.target.files});
} else {
- self.wiki.readFiles(event.target.files,function(tiddlerFieldsArray) {
- self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray)});
+ self.wiki.readFiles(event.target.files,{
+ callback: function(tiddlerFieldsArray) {
+ self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray)});
+ },
+ deserializer: self.deserializer
});
}
return false;
@@ -72,11 +78,13 @@ Compute the internal state of the widget
*/
BrowseWidget.prototype.execute = function() {
this.browseMultiple = this.getAttribute("multiple");
+ this.deserializer = this.getAttribute("deserializer");
this.message = this.getAttribute("message");
this.param = this.getAttribute("param");
this.tooltip = this.getAttribute("tooltip");
this.nwsaveas = this.getAttribute("nwsaveas");
this.webkitdirectory = this.getAttribute("webkitdirectory");
+ this.nwdirectory = this.getAttribute("nwdirectory");
};
/*
diff --git a/core/modules/widgets/button.js b/core/modules/widgets/button.js
index b53ca2e15..fdcf33caa 100644
--- a/core/modules/widgets/button.js
+++ b/core/modules/widgets/button.js
@@ -67,7 +67,7 @@ ButtonWidget.prototype.render = function(parent,nextSibling) {
// Add a click event handler
domNode.addEventListener("click",function (event) {
var handled = false;
- if(self.invokeActions(this,event)) {
+ if(self.invokeActions(self,event)) {
handled = true;
}
if(self.to) {
@@ -95,6 +95,15 @@ ButtonWidget.prototype.render = function(parent,nextSibling) {
}
return handled;
},false);
+ // Make it draggable if required
+ if(this.dragTiddler || this.dragFilter) {
+ $tw.utils.makeDraggable({
+ domNode: domNode,
+ dragTiddlerFn: function() {return self.dragTiddler;},
+ dragFilterFn: function() {return self.dragFilter;},
+ widget: this
+ });
+ }
// Insert element
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
@@ -131,12 +140,13 @@ ButtonWidget.prototype.navigateTo = function(event) {
navigateFromNode: this,
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
},
- navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1)
+ navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1),
+ event: event
});
};
ButtonWidget.prototype.dispatchMessage = function(event) {
- this.dispatchEvent({type: this.message, param: this.param, tiddlerTitle: this.getVariable("currentTiddler")});
+ this.dispatchEvent({type: this.message, param: this.param, tiddlerTitle: this.getVariable("currentTiddler"), event: event});
};
ButtonWidget.prototype.triggerPopup = function(event) {
@@ -171,6 +181,8 @@ ButtonWidget.prototype.execute = function() {
this.selectedClass = this.getAttribute("selectedClass");
this.defaultSetValue = this.getAttribute("default","");
this.buttonTag = this.getAttribute("tag");
+ this.dragTiddler = this.getAttribute("dragTiddler");
+ this.dragFilter = this.getAttribute("dragFilter");
// Make child widgets
this.makeChildWidgets();
};
@@ -180,7 +192,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
ButtonWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
- if(changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes["class"] || changedAttributes.selectedClass || changedAttributes.style || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup])) {
+ if(changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes["class"] || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup])) {
this.refreshSelf();
return true;
}
diff --git a/core/modules/widgets/checkbox.js b/core/modules/widgets/checkbox.js
index 6ec2b0372..9fa1c4889 100644
--- a/core/modules/widgets/checkbox.js
+++ b/core/modules/widgets/checkbox.js
@@ -65,7 +65,21 @@ CheckboxWidget.prototype.getValue = function() {
}
}
if(this.checkboxField) {
- var value = tiddler.fields[this.checkboxField] || this.checkboxDefault || "";
+ var value;
+ if($tw.utils.hop(tiddler.fields,this.checkboxField)) {
+ value = tiddler.fields[this.checkboxField] || "";
+ } else {
+ value = this.checkboxDefault || "";
+ }
+ if(value === this.checkboxChecked) {
+ return true;
+ }
+ if(value === this.checkboxUnchecked) {
+ return false;
+ }
+ }
+ if(this.checkboxIndex) {
+ var value = this.wiki.extractTiddlerDataItem(tiddler,this.checkboxIndex,this.checkboxDefault || "");
if(value === this.checkboxChecked) {
return true;
}
@@ -96,7 +110,8 @@ CheckboxWidget.prototype.handleChangeEvent = function(event) {
newFields = {title: this.checkboxTitle},
hasChanged = false,
tagCheck = false,
- hasTag = tiddler && tiddler.hasTag(this.checkboxTag);
+ hasTag = tiddler && tiddler.hasTag(this.checkboxTag),
+ value = checked ? this.checkboxChecked : this.checkboxUnchecked;
if(this.checkboxTag && this.checkboxInvertTag === "yes") {
tagCheck = hasTag === checked;
} else {
@@ -118,14 +133,31 @@ CheckboxWidget.prototype.handleChangeEvent = function(event) {
}
// Set the field if specified
if(this.checkboxField) {
- var value = checked ? this.checkboxChecked : this.checkboxUnchecked;
if(!tiddler || tiddler.fields[this.checkboxField] !== value) {
newFields[this.checkboxField] = value;
hasChanged = true;
}
}
+ // Set the index if specified
+ if(this.checkboxIndex) {
+ var indexValue = this.wiki.extractTiddlerDataItem(this.checkboxTitle,this.checkboxIndex);
+ if(!tiddler || indexValue !== value) {
+ hasChanged = true;
+ }
+ }
if(hasChanged) {
- this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),fallbackFields,tiddler,newFields,this.wiki.getModificationFields()));
+ if(this.checkboxIndex) {
+ this.wiki.setText(this.checkboxTitle,"",this.checkboxIndex,value);
+ } else {
+ this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),fallbackFields,tiddler,newFields,this.wiki.getModificationFields()));
+ }
+ }
+ // Trigger actions
+ if(this.checkboxActions) {
+ this.invokeActionString(this.checkboxActions,this,event);
+ }
+ if(this.checkboxUncheckActions && !checked) {
+ this.invokeActionString(this.checkboxUncheckActions,this,event);
}
};
@@ -134,9 +166,12 @@ Compute the internal state of the widget
*/
CheckboxWidget.prototype.execute = function() {
// Get the parameters from the attributes
+ this.checkboxActions = this.getAttribute("actions");
+ this.checkboxUncheckActions = this.getAttribute("uncheckactions");
this.checkboxTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
this.checkboxTag = this.getAttribute("tag");
this.checkboxField = this.getAttribute("field");
+ this.checkboxIndex = this.getAttribute("index");
this.checkboxChecked = this.getAttribute("checked");
this.checkboxUnchecked = this.getAttribute("unchecked");
this.checkboxDefault = this.getAttribute("default");
@@ -151,7 +186,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
CheckboxWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
- if(changedAttributes.tiddler || changedAttributes.tag || changedAttributes.invertTag || changedAttributes.field || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes["class"]) {
+ if(changedAttributes.tiddler || changedAttributes.tag || changedAttributes.invertTag || changedAttributes.field || changedAttributes.index || changedAttributes.checked || changedAttributes.unchecked || changedAttributes["default"] || changedAttributes["class"]) {
this.refreshSelf();
return true;
} else {
@@ -166,4 +201,4 @@ CheckboxWidget.prototype.refresh = function(changedTiddlers) {
exports.checkbox = CheckboxWidget;
-})();
\ No newline at end of file
+})();
diff --git a/core/modules/widgets/diff-text.js b/core/modules/widgets/diff-text.js
new file mode 100644
index 000000000..0dca1042c
--- /dev/null
+++ b/core/modules/widgets/diff-text.js
@@ -0,0 +1,148 @@
+/*\
+title: $:/core/modules/widgets/diff-text.js
+type: application/javascript
+module-type: widget
+
+Widget to display a diff between two texts
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget,
+ dmp = require("$:/core/modules/utils/diff-match-patch/diff_match_patch.js");
+
+var DiffTextWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+/*
+Inherit from the base widget class
+*/
+DiffTextWidget.prototype = new Widget();
+
+DiffTextWidget.prototype.invisibleCharacters = {
+ "\n": "↩︎\n",
+ "\r": "⇠",
+ "\t": "⇥\t"
+};
+
+/*
+Render this widget into the DOM
+*/
+DiffTextWidget.prototype.render = function(parent,nextSibling) {
+ this.parentDomNode = parent;
+ this.computeAttributes();
+ this.execute();
+ // Create the diff
+ var dmpObject = new dmp.diff_match_patch(),
+ diffs = dmpObject.diff_main(this.getAttribute("source"),this.getAttribute("dest"));
+ // Apply required cleanup
+ switch(this.getAttribute("cleanup","semantic")) {
+ case "none":
+ // No cleanup
+ break;
+ case "efficiency":
+ dmpObject.diff_cleanupEfficiency(diffs);
+ break;
+ default: // case "semantic"
+ dmpObject.diff_cleanupSemantic(diffs);
+ break;
+ }
+ // Create the elements
+ var domContainer = this.document.createElement("div"),
+ domDiff = this.createDiffDom(diffs);
+ parent.insertBefore(domContainer,nextSibling);
+ // Set variables
+ this.setVariable("diff-count",diffs.reduce(function(acc,diff) {
+ if(diff[0] !== dmp.DIFF_EQUAL) {
+ acc++;
+ }
+ return acc;
+ },0).toString());
+ // Render child widgets
+ this.renderChildren(domContainer,null);
+ // Render the diff
+ domContainer.appendChild(domDiff);
+ // Save our container
+ this.domNodes.push(domContainer);
+};
+
+/*
+Create DOM elements representing a list of diffs
+*/
+DiffTextWidget.prototype.createDiffDom = function(diffs) {
+ var self = this;
+ // Create the element and assign the attributes
+ var domPre = this.document.createElement("pre"),
+ domCode = this.document.createElement("code");
+ $tw.utils.each(diffs,function(diff) {
+ var tag = diff[0] === dmp.DIFF_INSERT ? "ins" : (diff[0] === dmp.DIFF_DELETE ? "del" : "span"),
+ className = diff[0] === dmp.DIFF_INSERT ? "tc-diff-insert" : (diff[0] === dmp.DIFF_DELETE ? "tc-diff-delete" : "tc-diff-equal"),
+ dom = self.document.createElement(tag),
+ text = diff[1],
+ currPos = 0,
+ re = /([\x00-\x1F])/mg,
+ match = re.exec(text),
+ span,
+ printable;
+ dom.className = className;
+ while(match) {
+ if(currPos < match.index) {
+ dom.appendChild(self.document.createTextNode(text.slice(currPos,match.index)));
+ }
+ span = self.document.createElement("span");
+ span.className = "tc-diff-invisible";
+ printable = self.invisibleCharacters[match[0]] || ("[0x" + match[0].charCodeAt(0).toString(16) + "]");
+ span.appendChild(self.document.createTextNode(printable));
+ dom.appendChild(span);
+ currPos = match.index + match[0].length;
+ match = re.exec(text);
+ }
+ if(currPos < text.length) {
+ dom.appendChild(self.document.createTextNode(text.slice(currPos)));
+ }
+ domCode.appendChild(dom);
+ });
+ domPre.appendChild(domCode);
+ return domPre;
+};
+
+/*
+Compute the internal state of the widget
+*/
+DiffTextWidget.prototype.execute = function() {
+ // Make child widgets
+ var parseTreeNodes;
+ if(this.parseTreeNode && this.parseTreeNode.children && this.parseTreeNode.children.length > 0) {
+ parseTreeNodes = this.parseTreeNode.children;
+ } else {
+ parseTreeNodes = [{
+ type: "transclude",
+ attributes: {
+ tiddler: {type: "string", value: "$:/language/Diffs/CountMessage"}
+ }
+ }];
+ }
+ this.makeChildWidgets(parseTreeNodes);
+};
+
+/*
+Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
+*/
+DiffTextWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if(changedAttributes.source || changedAttributes.dest || changedAttributes.cleanup) {
+ this.refreshSelf();
+ return true;
+ } else {
+ return this.refreshChildren(changedTiddlers);
+ }
+};
+
+exports["diff-text"] = DiffTextWidget;
+
+})();
diff --git a/core/modules/widgets/draggable.js b/core/modules/widgets/draggable.js
new file mode 100644
index 000000000..d1bdf2a16
--- /dev/null
+++ b/core/modules/widgets/draggable.js
@@ -0,0 +1,92 @@
+/*\
+title: $:/core/modules/widgets/draggable.js
+type: application/javascript
+module-type: widget
+
+Draggable widget
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget;
+
+var DraggableWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+/*
+Inherit from the base widget class
+*/
+DraggableWidget.prototype = new Widget();
+
+/*
+Render this widget into the DOM
+*/
+DraggableWidget.prototype.render = function(parent,nextSibling) {
+ var self = this;
+ // Save the parent dom node
+ this.parentDomNode = parent;
+ // Compute our attributes
+ this.computeAttributes();
+ // Execute our logic
+ this.execute();
+ // Sanitise the specified tag
+ var tag = this.draggableTag;
+ if($tw.config.htmlUnsafeElements.indexOf(tag) !== -1) {
+ tag = "div";
+ }
+ // Create our element
+ var domNode = this.document.createElement(tag);
+ // Assign classes
+ var classes = ["tc-draggable"];
+ if(this.draggableClasses) {
+ classes.push(this.draggableClasses);
+ }
+ domNode.setAttribute("class",classes.join(" "));
+ // Add event handlers
+ $tw.utils.makeDraggable({
+ domNode: domNode,
+ dragTiddlerFn: function() {return self.getAttribute("tiddler");},
+ dragFilterFn: function() {return self.getAttribute("filter");},
+ startActions: self.startActions,
+ endActions: self.endActions,
+ widget: this
+ });
+ // Insert the link into the DOM and render any children
+ parent.insertBefore(domNode,nextSibling);
+ this.renderChildren(domNode,null);
+ this.domNodes.push(domNode);
+};
+
+/*
+Compute the internal state of the widget
+*/
+DraggableWidget.prototype.execute = function() {
+ // Pick up our attributes
+ this.draggableTag = this.getAttribute("tag","div");
+ this.draggableClasses = this.getAttribute("class");
+ this.startActions = this.getAttribute("startactions");
+ this.endActions = this.getAttribute("endactions");
+ // Make the child widgets
+ this.makeChildWidgets();
+};
+
+/*
+Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
+*/
+DraggableWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if(changedTiddlers.tag || changedTiddlers["class"]) {
+ this.refreshSelf();
+ return true;
+ }
+ return this.refreshChildren(changedTiddlers);
+};
+
+exports.draggable = DraggableWidget;
+
+})();
diff --git a/core/modules/widgets/droppable.js b/core/modules/widgets/droppable.js
new file mode 100644
index 000000000..dabb4c259
--- /dev/null
+++ b/core/modules/widgets/droppable.js
@@ -0,0 +1,163 @@
+/*\
+title: $:/core/modules/widgets/droppable.js
+type: application/javascript
+module-type: widget
+
+Droppable widget
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget;
+
+var DroppableWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+/*
+Inherit from the base widget class
+*/
+DroppableWidget.prototype = new Widget();
+
+/*
+Render this widget into the DOM
+*/
+DroppableWidget.prototype.render = function(parent,nextSibling) {
+ var self = this;
+ // Remember parent
+ this.parentDomNode = parent;
+ // Compute attributes and execute state
+ this.computeAttributes();
+ this.execute();
+ var tag = this.parseTreeNode.isBlock ? "div" : "span";
+ if(this.droppableTag && $tw.config.htmlUnsafeElements.indexOf(this.droppableTag) === -1) {
+ tag = this.droppableTag;
+ }
+ // Create element and assign classes
+ var domNode = this.document.createElement(tag),
+ classes = (this["class"] || "").split(" ");
+ classes.push("tc-droppable");
+ domNode.className = classes.join(" ");
+ // Add event handlers
+ $tw.utils.addEventListeners(domNode,[
+ {name: "dragenter", handlerObject: this, handlerMethod: "handleDragEnterEvent"},
+ {name: "dragover", handlerObject: this, handlerMethod: "handleDragOverEvent"},
+ {name: "dragleave", handlerObject: this, handlerMethod: "handleDragLeaveEvent"},
+ {name: "drop", handlerObject: this, handlerMethod: "handleDropEvent"}
+ ]);
+ // Insert element
+ parent.insertBefore(domNode,nextSibling);
+ this.renderChildren(domNode,null);
+ this.domNodes.push(domNode);
+ // Stack of outstanding enter/leave events
+ this.currentlyEntered = [];
+};
+
+DroppableWidget.prototype.enterDrag = function(event) {
+ if(this.currentlyEntered.indexOf(event.target) === -1) {
+ this.currentlyEntered.push(event.target);
+ }
+ // If we're entering for the first time we need to apply highlighting
+ $tw.utils.addClass(this.domNodes[0],"tc-dragover");
+};
+
+DroppableWidget.prototype.leaveDrag = function(event) {
+ var pos = this.currentlyEntered.indexOf(event.target);
+ if(pos !== -1) {
+ this.currentlyEntered.splice(pos,1);
+ }
+ // Remove highlighting if we're leaving externally. The hacky second condition is to resolve a problem with Firefox whereby there is an erroneous dragenter event if the node being dragged is within the dropzone
+ if(this.currentlyEntered.length === 0 || (this.currentlyEntered.length === 1 && this.currentlyEntered[0] === $tw.dragInProgress)) {
+ this.currentlyEntered = [];
+ $tw.utils.removeClass(this.domNodes[0],"tc-dragover");
+ }
+};
+
+DroppableWidget.prototype.handleDragEnterEvent = function(event) {
+ this.enterDrag(event);
+ // Tell the browser that we're ready to handle the drop
+ event.preventDefault();
+ // Tell the browser not to ripple the drag up to any parent drop handlers
+ event.stopPropagation();
+ return false;
+};
+
+DroppableWidget.prototype.handleDragOverEvent = function(event) {
+ // Check for being over a TEXTAREA or INPUT
+ if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) {
+ return false;
+ }
+ // Tell the browser that we're still interested in the drop
+ event.preventDefault();
+ // Set the drop effect
+ event.dataTransfer.dropEffect = this.droppableEffect;
+ return false;
+};
+
+DroppableWidget.prototype.handleDragLeaveEvent = function(event) {
+ this.leaveDrag(event);
+ return false;
+};
+
+DroppableWidget.prototype.handleDropEvent = function(event) {
+ var self = this;
+ this.leaveDrag(event);
+ // Check for being over a TEXTAREA or INPUT
+ if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) {
+ return false;
+ }
+ var dataTransfer = event.dataTransfer;
+ // Remove highlighting
+ $tw.utils.removeClass(this.domNodes[0],"tc-dragover");
+ // Try to import the various data types we understand
+ $tw.utils.importDataTransfer(dataTransfer,null,function(fieldsArray) {
+ fieldsArray.forEach(function(fields) {
+ self.performActions(fields.title || fields.text,event);
+ });
+ });
+ // Tell the browser that we handled the drop
+ event.preventDefault();
+ // Stop the drop ripple up to any parent handlers
+ event.stopPropagation();
+ return false;
+};
+
+DroppableWidget.prototype.performActions = function(title,event) {
+ if(this.droppableActions) {
+ var modifierKey = event.ctrlKey && ! event.shiftKey ? "ctrl" : event.shiftKey && !event.ctrlKey ? "shift" :
+ event.ctrlKey && event.shiftKey ? "ctrl-shift" : "normal" ;
+ this.invokeActionString(this.droppableActions,this,event,{actionTiddler: title, modifier: modifierKey});
+ }
+};
+
+/*
+Compute the internal state of the widget
+*/
+DroppableWidget.prototype.execute = function() {
+ this.droppableActions = this.getAttribute("actions");
+ this.droppableEffect = this.getAttribute("effect","copy");
+ this.droppableTag = this.getAttribute("tag");
+ this.droppableClass = this.getAttribute("class");
+ // Make child widgets
+ this.makeChildWidgets();
+};
+
+/*
+Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
+*/
+DroppableWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if(changedAttributes["class"] || changedAttributes.tag) {
+ this.refreshSelf();
+ return true;
+ }
+ return this.refreshChildren(changedTiddlers);
+};
+
+exports.droppable = DroppableWidget;
+
+})();
diff --git a/core/modules/widgets/dropzone.js b/core/modules/widgets/dropzone.js
index 4ac8e78f4..d732fb97d 100644
--- a/core/modules/widgets/dropzone.js
+++ b/core/modules/widgets/dropzone.js
@@ -50,32 +50,35 @@ DropZoneWidget.prototype.render = function(parent,nextSibling) {
parent.insertBefore(domNode,nextSibling);
this.renderChildren(domNode,null);
this.domNodes.push(domNode);
+ // Stack of outstanding enter/leave events
+ this.currentlyEntered = [];
};
-DropZoneWidget.prototype.enterDrag = function() {
- // Check for this window being the source of the drag
- if($tw.dragInProgress) {
- return false;
+DropZoneWidget.prototype.enterDrag = function(event) {
+ if(this.currentlyEntered.indexOf(event.target) === -1) {
+ this.currentlyEntered.push(event.target);
}
- // We count enter/leave events
- this.dragEnterCount = (this.dragEnterCount || 0) + 1;
// If we're entering for the first time we need to apply highlighting
- if(this.dragEnterCount === 1) {
- $tw.utils.addClass(this.domNodes[0],"tc-dragover");
- }
+ $tw.utils.addClass(this.domNodes[0],"tc-dragover");
};
-DropZoneWidget.prototype.leaveDrag = function() {
- // Reduce the enter count
- this.dragEnterCount = (this.dragEnterCount || 0) - 1;
+DropZoneWidget.prototype.leaveDrag = function(event) {
+ var pos = this.currentlyEntered.indexOf(event.target);
+ if(pos !== -1) {
+ this.currentlyEntered.splice(pos,1);
+ }
// Remove highlighting if we're leaving externally
- if(this.dragEnterCount <= 0) {
+ if(this.currentlyEntered.length === 0) {
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
}
};
DropZoneWidget.prototype.handleDragEnterEvent = function(event) {
- this.enterDrag();
+ // Check for this window being the source of the drag
+ if($tw.dragInProgress) {
+ return false;
+ }
+ this.enterDrag(event);
// Tell the browser that we're ready to handle the drop
event.preventDefault();
// Tell the browser not to ripple the drag up to any parent drop handlers
@@ -97,11 +100,15 @@ DropZoneWidget.prototype.handleDragOverEvent = function(event) {
};
DropZoneWidget.prototype.handleDragLeaveEvent = function(event) {
- this.leaveDrag();
+ this.leaveDrag(event);
};
DropZoneWidget.prototype.handleDropEvent = function(event) {
- this.leaveDrag();
+ var self = this,
+ readFileCallback = function(tiddlerFieldsArray) {
+ self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray)});
+ };
+ this.leaveDrag(event);
// Check for being over a TEXTAREA or INPUT
if(["TEXTAREA","INPUT"].indexOf(event.target.tagName) !== -1) {
return false;
@@ -112,17 +119,19 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
}
var self = this,
dataTransfer = event.dataTransfer;
- // Reset the enter count
- this.dragEnterCount = 0;
// Remove highlighting
$tw.utils.removeClass(this.domNodes[0],"tc-dragover");
// Import any files in the drop
- var numFiles = this.wiki.readFiles(dataTransfer.files,function(tiddlerFieldsArray) {
- self.dispatchEvent({type: "tm-import-tiddlers", param: JSON.stringify(tiddlerFieldsArray)});
- });
+ var numFiles = 0;
+ if(dataTransfer.files) {
+ numFiles = this.wiki.readFiles(dataTransfer.files,{
+ callback: readFileCallback,
+ deserializer: this.dropzoneDeserializer
+ });
+ }
// Try to import the various data types we understand
if(numFiles === 0) {
- this.importData(dataTransfer);
+ $tw.utils.importDataTransfer(dataTransfer,this.wiki.generateNewTitle("Untitled"),readFileCallback);
}
// Tell the browser that we handled the drop
event.preventDefault();
@@ -130,78 +139,11 @@ DropZoneWidget.prototype.handleDropEvent = function(event) {
event.stopPropagation();
};
-DropZoneWidget.prototype.importData = function(dataTransfer) {
- // Try each provided data type in turn
- for(var t=0; t 0) {
+ domNode.setAttribute("class",classes.join(" "));
}
- domNode.setAttribute("class",classes.join(" "));
// Set an href
- var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
- wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$",
- wikiLinkText = wikiLinkTemplate.replace("$uri_encoded$",encodeURIComponent(this.to));
- wikiLinkText = wikiLinkText.replace("$uri_doubleencoded$",encodeURIComponent(encodeURIComponent(this.to)));
+ var wikilinkTransformFilter = this.getVariable("tv-filter-export-link"),
+ wikiLinkText;
+ if(wikilinkTransformFilter) {
+ // Use the filter to construct the href
+ wikiLinkText = this.wiki.filterTiddlers(wikilinkTransformFilter,this,function(iterator) {
+ iterator(self.wiki.getTiddler(self.to),self.to)
+ })[0];
+ } else {
+ // Expand the tv-wikilink-template variable to construct the href
+ var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
+ wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$";
+ wikiLinkText = $tw.utils.replaceString(wikiLinkTemplate,"$uri_encoded$",encodeURIComponent(this.to));
+ wikiLinkText = $tw.utils.replaceString(wikiLinkText,"$uri_doubleencoded$",encodeURIComponent(encodeURIComponent(this.to)));
+ }
+ // Override with the value of tv-get-export-link if defined
wikiLinkText = this.getVariable("tv-get-export-link",{params: [{name: "to",value: this.to}],defaultValue: wikiLinkText});
if(tag === "a") {
domNode.setAttribute("href",wikiLinkText);
}
+ // Set the tabindex
if(this.tabIndex) {
domNode.setAttribute("tabindex",this.tabIndex);
}
@@ -111,11 +129,13 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) {
$tw.utils.addEventListeners(domNode,[
{name: "click", handlerObject: this, handlerMethod: "handleClickEvent"},
]);
+ // Make the link draggable if required
if(this.draggable === "yes") {
- $tw.utils.addEventListeners(domNode,[
- {name: "dragstart", handlerObject: this, handlerMethod: "handleDragStartEvent"},
- {name: "dragend", handlerObject: this, handlerMethod: "handleDragEndEvent"}
- ]);
+ $tw.utils.makeDraggable({
+ domNode: domNode,
+ dragTiddlerFn: function() {return self.to;},
+ widget: this
+ });
}
// Insert the link into the DOM and render any children
parent.insertBefore(domNode,nextSibling);
@@ -133,7 +153,11 @@ LinkWidget.prototype.handleClickEvent = function(event) {
navigateFromNode: this,
navigateFromClientRect: { top: bounds.top, left: bounds.left, width: bounds.width, right: bounds.right, bottom: bounds.bottom, height: bounds.height
},
- navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1)
+ navigateSuppressNavigation: event.metaKey || event.ctrlKey || (event.button === 1),
+ metaKey: event.metaKey,
+ ctrlKey: event.ctrlKey,
+ altKey: event.altKey,
+ shiftKey: event.shiftKey
});
if(this.domNodes[0].hasAttribute("href")) {
event.preventDefault();
@@ -142,67 +166,6 @@ LinkWidget.prototype.handleClickEvent = function(event) {
return false;
};
-LinkWidget.prototype.handleDragStartEvent = function(event) {
- if(event.target === this.domNodes[0]) {
- if(this.to) {
- $tw.dragInProgress = true;
- // Set the dragging class on the element being dragged
- $tw.utils.addClass(event.target,"tc-tiddlylink-dragging");
- // Create the drag image elements
- this.dragImage = this.document.createElement("div");
- this.dragImage.className = "tc-tiddler-dragger";
- var inner = this.document.createElement("div");
- inner.className = "tc-tiddler-dragger-inner";
- inner.appendChild(this.document.createTextNode(this.to));
- this.dragImage.appendChild(inner);
- this.document.body.appendChild(this.dragImage);
- // Astoundingly, we need to cover the dragger up: http://www.kryogenix.org/code/browser/custom-drag-image.html
- var cover = this.document.createElement("div");
- cover.className = "tc-tiddler-dragger-cover";
- cover.style.left = (inner.offsetLeft - 16) + "px";
- cover.style.top = (inner.offsetTop - 16) + "px";
- cover.style.width = (inner.offsetWidth + 32) + "px";
- cover.style.height = (inner.offsetHeight + 32) + "px";
- this.dragImage.appendChild(cover);
- // Set the data transfer properties
- var dataTransfer = event.dataTransfer;
- // First the image
- dataTransfer.effectAllowed = "copy";
- if(dataTransfer.setDragImage) {
- dataTransfer.setDragImage(this.dragImage.firstChild,-16,-16);
- }
- // Then the data
- dataTransfer.clearData();
- var jsonData = this.wiki.getTiddlerAsJson(this.to),
- textData = this.wiki.getTiddlerText(this.to,""),
- title = (new RegExp("^" + $tw.config.textPrimitives.wikiLink + "$","mg")).exec(this.to) ? this.to : "[[" + this.to + "]]";
- // IE doesn't like these content types
- if(!$tw.browser.isIE) {
- dataTransfer.setData("text/vnd.tiddler",jsonData);
- dataTransfer.setData("text/plain",title);
- dataTransfer.setData("text/x-moz-url","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
- }
- dataTransfer.setData("URL","data:text/vnd.tiddler," + encodeURIComponent(jsonData));
- dataTransfer.setData("Text",title);
- event.stopPropagation();
- } else {
- event.preventDefault();
- }
- }
-};
-
-LinkWidget.prototype.handleDragEndEvent = function(event) {
- if(event.target === this.domNodes[0]) {
- $tw.dragInProgress = false;
- // Remove the dragging class on the element being dragged
- $tw.utils.removeClass(event.target,"tc-tiddlylink-dragging");
- // Delete the drag image element
- if(this.dragImage) {
- this.dragImage.parentNode.removeChild(this.dragImage);
- }
- }
-};
-
/*
Compute the internal state of the widget
*/
@@ -212,6 +175,7 @@ LinkWidget.prototype.execute = function() {
this.tooltip = this.getAttribute("tooltip");
this["aria-label"] = this.getAttribute("aria-label");
this.linkClasses = this.getAttribute("class");
+ this.overrideClasses = this.getAttribute("overrideClass");
this.tabIndex = this.getAttribute("tabindex");
this.draggable = this.getAttribute("draggable","yes");
this.linkTag = this.getAttribute("tag","a");
diff --git a/core/modules/widgets/linkcatcher.js b/core/modules/widgets/linkcatcher.js
index d279140c0..ea0629d6e 100644
--- a/core/modules/widgets/linkcatcher.js
+++ b/core/modules/widgets/linkcatcher.js
@@ -48,6 +48,8 @@ LinkCatcherWidget.prototype.execute = function() {
this.catchActions = this.getAttribute("actions");
// Construct the child widgets
this.makeChildWidgets();
+ // When executing actions we avoid trapping navigate events, so that we don't trigger ourselves recursively
+ this.executingActions = false;
};
/*
@@ -67,23 +69,35 @@ LinkCatcherWidget.prototype.refresh = function(changedTiddlers) {
Handle a tm-navigate event
*/
LinkCatcherWidget.prototype.handleNavigateEvent = function(event) {
- if(this.catchTo) {
- this.wiki.setTextReference(this.catchTo,event.navigateTo,this.getVariable("currentTiddler"));
- }
- if(this.catchMessage && this.parentWidget) {
+ if(!this.executingActions) {
+ // Execute the actions
+ if(this.catchTo) {
+ this.wiki.setTextReference(this.catchTo,event.navigateTo,this.getVariable("currentTiddler"));
+ }
+ if(this.catchMessage && this.parentWidget) {
+ this.parentWidget.dispatchEvent({
+ type: this.catchMessage,
+ param: event.navigateTo,
+ navigateTo: event.navigateTo
+ });
+ }
+ if(this.catchSet) {
+ var tiddler = this.wiki.getTiddler(this.catchSet);
+ this.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.catchSet, text: this.catchSetTo}));
+ }
+ if(this.catchActions) {
+ this.executingActions = true;
+ this.invokeActionString(this.catchActions,this,event,{navigateTo: event.navigateTo});
+ this.executingActions = false;
+ }
+ } else {
+ // This is a navigate event generated by the actions of this linkcatcher, so we don't trap it again, but just pass it to the parent
this.parentWidget.dispatchEvent({
- type: this.catchMessage,
+ type: "tm-navigate",
param: event.navigateTo,
navigateTo: event.navigateTo
});
}
- if(this.catchSet) {
- var tiddler = this.wiki.getTiddler(this.catchSet);
- this.wiki.addTiddler(new $tw.Tiddler(tiddler,{title: this.catchSet, text: this.catchSetTo}));
- }
- if(this.catchActions) {
- this.invokeActionString(this.catchActions,this);
- }
return false;
};
diff --git a/core/modules/widgets/list.js b/core/modules/widgets/list.js
index 4c5b265f2..f4981df33 100755
--- a/core/modules/widgets/list.js
+++ b/core/modules/widgets/list.js
@@ -43,6 +43,9 @@ ListWidget.prototype.render = function(parent,nextSibling) {
this.renderChildren(parent,nextSibling);
// Construct the storyview
var StoryView = this.storyViews[this.storyViewName];
+ if(this.storyViewName && !StoryView) {
+ StoryView = this.storyViews["classic"];
+ }
if(StoryView && !this.document.isTiddlyWikiFakeDom) {
this.storyview = new StoryView(this);
} else {
diff --git a/core/modules/widgets/macrocall.js b/core/modules/widgets/macrocall.js
index 02bc8cce5..23522629b 100644
--- a/core/modules/widgets/macrocall.js
+++ b/core/modules/widgets/macrocall.js
@@ -48,7 +48,9 @@ MacroCallWidget.prototype.execute = function() {
}
});
// Get the macro value
- var text = this.getVariable(this.parseTreeNode.name || this.getAttribute("$name"),{params: params}),
+ var macroName = this.parseTreeNode.name || this.getAttribute("$name"),
+ variableInfo = this.getVariableInfo(macroName,{params: params}),
+ text = variableInfo.text,
parseTreeNodes;
// Are we rendering to HTML?
if(this.renderOutput === "text/html") {
@@ -56,6 +58,21 @@ MacroCallWidget.prototype.execute = function() {
var parser = this.wiki.parseText(this.parseType,text,
{parseAsInline: !this.parseTreeNode.isBlock});
parseTreeNodes = parser ? parser.tree : [];
+ // Wrap the parse tree in a vars widget assigning the parameters to variables named "__paramname__"
+ var attributes = {};
+ $tw.utils.each(variableInfo.params,function(param) {
+ var name = "__" + param.name + "__";
+ attributes[name] = {
+ name: name,
+ type: "string",
+ value: param.value
+ };
+ });
+ parseTreeNodes = [{
+ type: "vars",
+ attributes: attributes,
+ children: parseTreeNodes
+ }];
} else {
// Otherwise, we'll render the text
var plainText = this.wiki.renderText("text/plain",this.parseType,text,{parentWidget: this});
diff --git a/core/modules/widgets/navigator.js b/core/modules/widgets/navigator.js
index 152800402..59ccfdd60 100755
--- a/core/modules/widgets/navigator.js
+++ b/core/modules/widgets/navigator.js
@@ -60,6 +60,8 @@ NavigatorWidget.prototype.execute = function() {
// Get our parameters
this.storyTitle = this.getAttribute("story");
this.historyTitle = this.getAttribute("history");
+ this.setVariable("tv-story-list",this.storyTitle);
+ this.setVariable("tv-history-list",this.historyTitle);
// Construct the child widgets
this.makeChildWidgets();
};
@@ -73,7 +75,7 @@ NavigatorWidget.prototype.refresh = function(changedTiddlers) {
this.refreshSelf();
return true;
} else {
- return this.refreshChildren(changedTiddlers);
+ return this.refreshChildren(changedTiddlers);
}
};
@@ -174,6 +176,7 @@ NavigatorWidget.prototype.addToHistory = function(title,fromPageRect) {
Handle a tm-navigate event
*/
NavigatorWidget.prototype.handleNavigateEvent = function(event) {
+ event = $tw.hooks.invokeHook("th-navigating",event);
if(event.navigateTo) {
this.addToStory(event.navigateTo,event.navigateFromTitle);
if(!event.navigateSuppressNavigation) {
@@ -208,6 +211,10 @@ NavigatorWidget.prototype.handleCloseOtherTiddlersEvent = function(event) {
// Place a tiddler in edit mode
NavigatorWidget.prototype.handleEditTiddlerEvent = function(event) {
+ var editTiddler = $tw.hooks.invokeHook("th-editing-tiddler",event);
+ if(!editTiddler) {
+ return false;
+ }
var self = this;
function isUnmodifiedShadow(title) {
return self.wiki.isShadowTiddler(title) && !self.wiki.tiddlerExists(title);
@@ -245,6 +252,7 @@ NavigatorWidget.prototype.handleDeleteTiddlerEvent = function(event) {
tiddler = this.wiki.getTiddler(title),
storyList = this.getStoryList(),
originalTitle = tiddler ? tiddler.fields["draft.of"] : "",
+ originalTiddler = originalTitle ? this.wiki.getTiddler(originalTitle) : undefined,
confirmationTitle;
if(!tiddler) {
return false;
@@ -268,10 +276,14 @@ NavigatorWidget.prototype.handleDeleteTiddlerEvent = function(event) {
}
// Delete the original tiddler
if(originalTitle) {
+ if(originalTiddler) {
+ $tw.hooks.invokeHook("th-deleting-tiddler",originalTiddler);
+ }
this.wiki.deleteTiddler(originalTitle);
this.removeTitleFromStory(storyList,originalTitle);
}
- // Delete this tiddler
+ // Invoke the hook function and delete this tiddler
+ $tw.hooks.invokeHook("th-deleting-tiddler",tiddler);
this.wiki.deleteTiddler(title);
// Remove the closed tiddler from the story
this.removeTitleFromStory(storyList,title);
@@ -349,12 +361,21 @@ NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
},this.wiki.getModificationFields());
newTiddler = $tw.hooks.invokeHook("th-saving-tiddler",newTiddler);
this.wiki.addTiddler(newTiddler);
+ // If enabled, relink references to renamed tiddler
+ var shouldRelink = this.getAttribute("relinkOnRename","no").toLowerCase().trim() === "yes";
+ if(isRename && shouldRelink && this.wiki.tiddlerExists(draftOf)) {
+console.log("Relinking '" + draftOf + "' to '" + draftTitle + "'");
+ this.wiki.relinkTiddler(draftOf,draftTitle);
+ }
// Remove the draft tiddler
this.wiki.deleteTiddler(title);
// Remove the original tiddler if we're renaming it
if(isRename) {
this.wiki.deleteTiddler(draftOf);
}
+ // #2381 always remove new title & old
+ this.removeTitleFromStory(storyList,draftTitle);
+ this.removeTitleFromStory(storyList,draftOf);
if(!event.paramObject || event.paramObject.suppressNavigation !== "yes") {
// Replace the draft in the story with the original
this.replaceFirstTitleInStory(storyList,title,draftTitle);
@@ -373,6 +394,7 @@ NavigatorWidget.prototype.handleSaveTiddlerEvent = function(event) {
// Take a tiddler out of edit mode without saving the changes
NavigatorWidget.prototype.handleCancelTiddlerEvent = function(event) {
+ event = $tw.hooks.invokeHook("th-cancelling-tiddler", event);
// Flip the specified tiddler from draft back to the original
var draftTitle = event.param || event.tiddlerTitle,
draftTiddler = this.wiki.getTiddler(draftTitle),
@@ -417,6 +439,7 @@ NavigatorWidget.prototype.handleCancelTiddlerEvent = function(event) {
//
// If a draft of the target tiddler already exists then it is reused
NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
+ event = $tw.hooks.invokeHook("th-new-tiddler", event);
// Get the story details
var storyList = this.getStoryList(),
templateTiddler, additionalFields, title, draftTitle, existingTiddler;
@@ -437,6 +460,13 @@ NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
if(additionalFields && additionalFields.title) {
title = additionalFields.title;
}
+ // Make a copy of the additional fields excluding any blank ones
+ var filteredAdditionalFields = $tw.utils.extend({},additionalFields);
+ Object.keys(filteredAdditionalFields).forEach(function(fieldName) {
+ if(filteredAdditionalFields[fieldName] === "") {
+ delete filteredAdditionalFields[fieldName];
+ }
+ });
// Generate a title if we don't have one
title = title || this.wiki.generateNewTitle($tw.language.getString("DefaultNewTiddlerTitle"));
// Find any existing draft for this tiddler
@@ -451,7 +481,7 @@ NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
// Merge the tags
var mergedTags = [];
if(existingTiddler && existingTiddler.fields.tags) {
- $tw.utils.pushTop(mergedTags,existingTiddler.fields.tags)
+ $tw.utils.pushTop(mergedTags,existingTiddler.fields.tags);
}
if(additionalFields && additionalFields.tags) {
// Merge tags
@@ -467,8 +497,9 @@ NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
"draft.title": title
},
templateTiddler,
- existingTiddler,
additionalFields,
+ existingTiddler,
+ filteredAdditionalFields,
this.wiki.getCreationFields(),
{
title: draftTitle,
@@ -482,7 +513,7 @@ NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
storyList.splice(slot + 1,0,draftTitle);
}
if(storyList.indexOf(title) !== -1) {
- storyList.splice(storyList.indexOf(title),1);
+ storyList.splice(storyList.indexOf(title),1);
}
this.saveStoryList(storyList);
// Add a new record to the top of the history stack
@@ -492,11 +523,10 @@ NavigatorWidget.prototype.handleNewTiddlerEvent = function(event) {
// Import JSON tiddlers into a pending import tiddler
NavigatorWidget.prototype.handleImportTiddlersEvent = function(event) {
- var self = this;
// Get the tiddlers
var tiddlers = [];
try {
- tiddlers = JSON.parse(event.param);
+ tiddlers = JSON.parse(event.param);
} catch(e) {
}
// Get the current $:/Import tiddler
@@ -512,6 +542,7 @@ NavigatorWidget.prototype.handleImportTiddlersEvent = function(event) {
// Process each tiddler
importData.tiddlers = importData.tiddlers || {};
$tw.utils.each(tiddlers,function(tiddlerFields) {
+ tiddlerFields.title = $tw.utils.trim(tiddlerFields.title);
var title = tiddlerFields.title;
if(title) {
incomingTiddlers.push(title);
@@ -544,12 +575,12 @@ NavigatorWidget.prototype.handleImportTiddlersEvent = function(event) {
history.push(IMPORT_TITLE);
// Save the updated story and history
this.saveStoryList(storyList);
- this.addToHistory(history);
+ this.addToHistory(history);
}
return false;
};
-//
+//
NavigatorWidget.prototype.handlePerformImportEvent = function(event) {
var self = this,
importTiddler = this.wiki.getTiddler(event.param),
@@ -560,7 +591,9 @@ NavigatorWidget.prototype.handlePerformImportEvent = function(event) {
$tw.utils.each(importData.tiddlers,function(tiddlerFields) {
var title = tiddlerFields.title;
if(title && importTiddler && importTiddler.fields["selection-" + title] !== "unchecked") {
- self.wiki.addTiddler(new $tw.Tiddler(tiddlerFields));
+ var tiddler = new $tw.Tiddler(tiddlerFields);
+ tiddler = $tw.hooks.invokeHook("th-importing-tiddler",tiddler);
+ self.wiki.addTiddler(tiddler);
importReport.push("# [[" + tiddlerFields.title + "]]");
}
});
@@ -577,8 +610,7 @@ NavigatorWidget.prototype.handlePerformImportEvent = function(event) {
};
NavigatorWidget.prototype.handleFoldTiddlerEvent = function(event) {
- var self = this,
- paramObject = event.paramObject || {};
+ var paramObject = event.paramObject || {};
if(paramObject.foldedState) {
var foldedState = this.wiki.getTiddlerText(paramObject.foldedState,"show") === "show" ? "hide" : "show";
this.wiki.setText(paramObject.foldedState,"text",null,foldedState);
@@ -613,8 +645,8 @@ NavigatorWidget.prototype.handleUnfoldAllTiddlersEvent = function(event) {
};
NavigatorWidget.prototype.handleRenameTiddlerEvent = function(event) {
- var self = this,
- paramObject = event.paramObject || {},
+ event = $tw.hooks.invokeHook("th-renaming-tiddler", event);
+ var paramObject = event.paramObject || {},
from = paramObject.from || event.tiddlerTitle,
to = paramObject.to;
$tw.wiki.renameTiddler(from,to);
diff --git a/core/modules/widgets/radio.js b/core/modules/widgets/radio.js
index 09a053c5b..90eca6bfd 100644
--- a/core/modules/widgets/radio.js
+++ b/core/modules/widgets/radio.js
@@ -3,22 +3,7 @@ title: $:/core/modules/widgets/radio.js
type: application/javascript
module-type: widget
-Radio widget
-
-Will set a field to the selected value:
-
-```
- <$radio field="myfield" value="check 1">one$radio>
- <$radio field="myfield" value="check 2">two$radio>
- <$radio field="myfield" value="check 3">three$radio>
-```
-
-|Parameter |Description |h
-|tiddler |Name of the tiddler in which the field should be set. Defaults to current tiddler |
-|field |The name of the field to be set |
-|value |The value to set |
-|class |Optional class name(s) |
-
+Set a field or index at a given tiddler via radio buttons
\*/
(function(){
@@ -48,12 +33,15 @@ RadioWidget.prototype.render = function(parent,nextSibling) {
this.computeAttributes();
// Execute our logic
this.execute();
+ var isChecked = this.getValue() === this.radioValue;
// Create our elements
this.labelDomNode = this.document.createElement("label");
- this.labelDomNode.setAttribute("class",this.radioClass);
+ this.labelDomNode.setAttribute("class",
+ "tc-radio " + this.radioClass + (isChecked ? " tc-radio-selected" : "")
+ );
this.inputDomNode = this.document.createElement("input");
this.inputDomNode.setAttribute("type","radio");
- if(this.getValue() == this.radioValue) {
+ if(isChecked) {
this.inputDomNode.setAttribute("checked","true");
}
this.labelDomNode.appendChild(this.inputDomNode);
@@ -70,12 +58,20 @@ RadioWidget.prototype.render = function(parent,nextSibling) {
};
RadioWidget.prototype.getValue = function() {
- var tiddler = this.wiki.getTiddler(this.radioTitle);
- return tiddler && tiddler.getFieldString(this.radioField);
+ var value,
+ tiddler = this.wiki.getTiddler(this.radioTitle);
+ if (this.radioIndex) {
+ value = this.wiki.extractTiddlerDataItem(this.radioTitle,this.radioIndex);
+ } else {
+ value = tiddler && tiddler.getFieldString(this.radioField);
+ }
+ return value;
};
RadioWidget.prototype.setValue = function() {
- if(this.radioField) {
+ if(this.radioIndex) {
+ this.wiki.setText(this.radioTitle,"",this.radioIndex,this.radioValue);
+ } else {
var tiddler = this.wiki.getTiddler(this.radioTitle),
addition = {};
addition[this.radioField] = this.radioValue;
@@ -96,12 +92,9 @@ RadioWidget.prototype.execute = function() {
// Get the parameters from the attributes
this.radioTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
this.radioField = this.getAttribute("field","text");
+ this.radioIndex = this.getAttribute("index");
this.radioValue = this.getAttribute("value");
this.radioClass = this.getAttribute("class","");
- if(this.radioClass !== "") {
- this.radioClass += " ";
- }
- this.radioClass += "tc-radio";
// Make the child widgets
this.makeChildWidgets();
};
@@ -111,7 +104,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
RadioWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
- if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.value || changedAttributes["class"]) {
+ if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes.value || changedAttributes["class"]) {
this.refreshSelf();
return true;
} else {
diff --git a/core/modules/widgets/range.js b/core/modules/widgets/range.js
new file mode 100644
index 000000000..591dab482
--- /dev/null
+++ b/core/modules/widgets/range.js
@@ -0,0 +1,114 @@
+/*\
+title: $:/core/modules/widgets/range.js
+type: application/javascript
+module-type: widget
+
+Range widget
+
+\*/
+(function(){
+
+/*jslint node: true, browser: true */
+/*global $tw: false */
+"use strict";
+
+var Widget = require("$:/core/modules/widgets/widget.js").widget;
+
+var RangeWidget = function(parseTreeNode,options) {
+ this.initialise(parseTreeNode,options);
+};
+
+/*
+Inherit from the base widget class
+*/
+RangeWidget.prototype = new Widget();
+
+/*
+Render this widget into the DOM
+*/
+RangeWidget.prototype.render = function(parent,nextSibling) {
+ // Save the parent dom node
+ this.parentDomNode = parent;
+ // Compute our attributes
+ this.computeAttributes();
+ // Execute our logic
+ this.execute();
+ // Create our elements
+ this.inputDomNode = this.document.createElement("input");
+ this.inputDomNode.setAttribute("type","range");
+ this.inputDomNode.setAttribute("class",this.elementClass);
+ if(this.minValue){
+ this.inputDomNode.setAttribute("min", this.minValue);
+ }
+ if(this.maxValue){
+ this.inputDomNode.setAttribute("max", this.maxValue);
+ }
+ if(this.increment){
+ this.inputDomNode.setAttribute("step", this.increment);
+ }
+ this.inputDomNode.value = this.getValue();
+
+
+ // Add a click event handler
+ $tw.utils.addEventListeners(this.inputDomNode,[
+ {name: "input", handlerObject: this, handlerMethod: "handleChangeEvent"}
+ ]);
+ // Insert the label into the DOM and render any children
+ parent.insertBefore(this.inputDomNode,nextSibling);
+ this.domNodes.push(this.inputDomNode);
+};
+
+RangeWidget.prototype.getValue = function() {
+ var tiddler = this.wiki.getTiddler(this.tiddlerTitle),
+ value = this.defaultValue;
+ if(tiddler) {
+ if($tw.utils.hop(tiddler.fields,this.tiddlerField)) {
+ value = tiddler.fields[this.tiddlerField] || "";
+ } else {
+ value = this.defaultValue || "";
+ }
+ }
+ return value;
+};
+
+RangeWidget.prototype.handleChangeEvent = function(event) {
+ this.wiki.setText(this.tiddlerTitle ,this.tiddlerField, null,this.inputDomNode.value);
+};
+
+/*
+Compute the internal state of the widget
+*/
+RangeWidget.prototype.execute = function() {
+ // Get the parameters from the attributes
+ this.tiddlerTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler"));
+ this.tiddlerField = this.getAttribute("field");
+ this.minValue = this.getAttribute("min");
+ this.maxValue = this.getAttribute("max");
+ this.increment = this.getAttribute("increment");
+ this.defaultValue = this.getAttribute("default");
+ this.elementClass = this.getAttribute("class","");
+ // Make the child widgets
+ this.makeChildWidgets();
+};
+
+/*
+Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
+*/
+RangeWidget.prototype.refresh = function(changedTiddlers) {
+ var changedAttributes = this.computeAttributes();
+ if(changedAttributes.tiddler || changedAttributes.field || changedAttributes['min'] || changedAttributes['max'] || changedAttributes['increment'] || changedAttributes["default"] || changedAttributes["class"]) {
+ this.refreshSelf();
+ return true;
+ } else {
+ var refreshed = false;
+ if(changedTiddlers[this.tiddlerTitle]) {
+ this.inputDomNode.checked = this.getValue();
+ refreshed = true;
+ }
+ return this.refreshChildren(changedTiddlers) || refreshed;
+ }
+};
+
+exports.range = RangeWidget;
+
+})();
diff --git a/core/modules/widgets/reveal.js b/core/modules/widgets/reveal.js
index 4eec55ad3..a3bc8075b 100755
--- a/core/modules/widgets/reveal.js
+++ b/core/modules/widgets/reveal.js
@@ -121,17 +121,28 @@ RevealWidget.prototype.readState = function() {
this.readPopupState(state);
break;
case "match":
- this.readMatchState(state);
+ this.isOpen = !!(this.compareStateText(state) == 0);
break;
case "nomatch":
- this.readMatchState(state);
- this.isOpen = !this.isOpen;
+ this.isOpen = !(this.compareStateText(state) == 0);
+ break;
+ case "lt":
+ this.isOpen = !!(this.compareStateText(state) < 0);
+ break;
+ case "gt":
+ this.isOpen = !!(this.compareStateText(state) > 0);
+ break;
+ case "lteq":
+ this.isOpen = !(this.compareStateText(state) > 0);
+ break;
+ case "gteq":
+ this.isOpen = !(this.compareStateText(state) < 0);
break;
}
};
-RevealWidget.prototype.readMatchState = function(state) {
- this.isOpen = state === this.text;
+RevealWidget.prototype.compareStateText = function(state) {
+ return state.localeCompare(this.text,undefined,{numeric: true,sensitivity: "case"});
};
RevealWidget.prototype.readPopupState = function(state) {
@@ -182,6 +193,7 @@ RevealWidget.prototype.refresh = function(changedTiddlers) {
Called by refresh() to dynamically show or hide the content
*/
RevealWidget.prototype.updateState = function() {
+ var self = this;
// Read the current state
this.readState();
// Construct the child nodes if needed
@@ -202,8 +214,12 @@ RevealWidget.prototype.updateState = function() {
$tw.anim.perform(this.openAnimation,domNode);
} else {
$tw.anim.perform(this.closeAnimation,domNode,{callback: function() {
- domNode.setAttribute("hidden","true");
- }});
+ //make sure that the state hasn't changed during the close animation
+ self.readState()
+ if(!self.isOpen) {
+ domNode.setAttribute("hidden","true");
+ }
+ }});
}
};
diff --git a/core/modules/widgets/select.js b/core/modules/widgets/select.js
index 4d1111730..cf094687b 100644
--- a/core/modules/widgets/select.js
+++ b/core/modules/widgets/select.js
@@ -72,7 +72,7 @@ SelectWidget.prototype.setSelectValue = function() {
var value = this.selectDefault;
// Get the value
if(this.selectIndex) {
- value = this.wiki.extractTiddlerDataItem(this.selectTitle,this.selectIndex);
+ value = this.wiki.extractTiddlerDataItem(this.selectTitle,this.selectIndex,value);
} else {
var tiddler = this.wiki.getTiddler(this.selectTitle);
if(tiddler) {
diff --git a/core/modules/widgets/setvariable.js b/core/modules/widgets/setvariable.js
index f75fa2c98..40dbadf16 100755
--- a/core/modules/widgets/setvariable.js
+++ b/core/modules/widgets/setvariable.js
@@ -40,10 +40,15 @@ SetWidget.prototype.execute = function() {
// Get our parameters
this.setName = this.getAttribute("name","currentTiddler");
this.setFilter = this.getAttribute("filter");
+ this.setSelect = this.getAttribute("select");
+ this.setTiddler = this.getAttribute("tiddler");
+ this.setSubTiddler = this.getAttribute("subtiddler");
+ this.setField = this.getAttribute("field");
+ this.setIndex = this.getAttribute("index");
this.setValue = this.getAttribute("value");
this.setEmptyValue = this.getAttribute("emptyValue");
// Set context variable
- this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params);
+ this.setVariable(this.setName,this.getValue(),this.parseTreeNode.params,!!this.parseTreeNode.isMacroDefinition);
// Construct the child widgets
this.makeChildWidgets();
};
@@ -53,10 +58,34 @@ Get the value to be assigned
*/
SetWidget.prototype.getValue = function() {
var value = this.setValue;
- if(this.setFilter) {
+ if(this.setTiddler) {
+ var tiddler;
+ if(this.setSubTiddler) {
+ tiddler = this.wiki.getSubTiddler(this.setTiddler,this.setSubTiddler);
+ } else {
+ tiddler = this.wiki.getTiddler(this.setTiddler);
+ }
+ if(!tiddler) {
+ value = this.setEmptyValue;
+ } else if(this.setField) {
+ value = tiddler.getFieldString(this.setField) || this.setEmptyValue;
+ } else if(this.setIndex) {
+ value = this.wiki.extractTiddlerDataItem(this.setTiddler,this.setIndex,this.setEmptyValue);
+ } else {
+ value = tiddler.fields.text || this.setEmptyValue ;
+ }
+ } else if(this.setFilter) {
var results = this.wiki.filterTiddlers(this.setFilter,this);
- if(!this.setValue) {
- value = $tw.utils.stringifyList(results);
+ if(this.setValue == null) {
+ var select;
+ if(this.setSelect) {
+ select = parseInt(this.setSelect,10);
+ }
+ if(select !== undefined) {
+ value = results[select] || "";
+ } else {
+ value = $tw.utils.stringifyList(results);
+ }
}
if(results.length === 0 && this.setEmptyValue !== undefined) {
value = this.setEmptyValue;
@@ -64,7 +93,7 @@ SetWidget.prototype.getValue = function() {
} else if(!value && this.setEmptyValue) {
value = this.setEmptyValue;
}
- return value;
+ return value || "";
};
/*
@@ -72,7 +101,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
SetWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
- if(changedAttributes.name || changedAttributes.filter || changedAttributes.value || changedAttributes.emptyValue ||
+ if(changedAttributes.name || changedAttributes.filter || changedAttributes.select || changedAttributes.tiddler || (this.setTiddler && changedTiddlers[this.setTiddler]) || changedAttributes.field || changedAttributes.index || changedAttributes.value || changedAttributes.emptyValue ||
(this.setFilter && this.getValue() != this.variables[this.setName].value)) {
this.refreshSelf();
return true;
diff --git a/core/modules/widgets/view.js b/core/modules/widgets/view.js
index bbe9106be..30da34147 100755
--- a/core/modules/widgets/view.js
+++ b/core/modules/widgets/view.js
@@ -51,15 +51,16 @@ ViewWidget.prototype.execute = function() {
this.viewIndex = this.getAttribute("index");
this.viewFormat = this.getAttribute("format","text");
this.viewTemplate = this.getAttribute("template","");
+ this.viewMode = this.getAttribute("mode","block");
switch(this.viewFormat) {
case "htmlwikified":
- this.text = this.getValueAsHtmlWikified();
+ this.text = this.getValueAsHtmlWikified(this.viewMode);
break;
case "plainwikified":
- this.text = this.getValueAsPlainWikified();
+ this.text = this.getValueAsPlainWikified(this.viewMode);
break;
case "htmlencodedplainwikified":
- this.text = this.getValueAsHtmlEncodedPlainWikified();
+ this.text = this.getValueAsHtmlEncodedPlainWikified(this.viewMode);
break;
case "htmlencoded":
this.text = this.getValueAsHtmlEncoded();
@@ -134,16 +135,25 @@ ViewWidget.prototype.getValueAsText = function() {
return this.getValue({asString: true});
};
-ViewWidget.prototype.getValueAsHtmlWikified = function() {
- return this.wiki.renderText("text/html","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this});
+ViewWidget.prototype.getValueAsHtmlWikified = function(mode) {
+ return this.wiki.renderText("text/html","text/vnd.tiddlywiki",this.getValueAsText(),{
+ parseAsInline: mode !== "block",
+ parentWidget: this
+ });
};
-ViewWidget.prototype.getValueAsPlainWikified = function() {
- return this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this});
+ViewWidget.prototype.getValueAsPlainWikified = function(mode) {
+ return this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{
+ parseAsInline: mode !== "block",
+ parentWidget: this
+ });
};
-ViewWidget.prototype.getValueAsHtmlEncodedPlainWikified = function() {
- return $tw.utils.htmlEncode(this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{parentWidget: this}));
+ViewWidget.prototype.getValueAsHtmlEncodedPlainWikified = function(mode) {
+ return $tw.utils.htmlEncode(this.wiki.renderText("text/plain","text/vnd.tiddlywiki",this.getValueAsText(),{
+ parseAsInline: mode !== "block",
+ parentWidget: this
+ }));
};
ViewWidget.prototype.getValueAsHtmlEncoded = function() {
diff --git a/core/modules/widgets/widget.js b/core/modules/widgets/widget.js
index cf51fcbe4..28b9f4e35 100755
--- a/core/modules/widgets/widget.js
+++ b/core/modules/widgets/widget.js
@@ -71,9 +71,10 @@ Set the value of a context variable
name: name of the variable
value: value of the variable
params: array of {name:, default:} for each parameter
+isMacroDefinition: true if the variable is set via a \define macro pragma (and hence should have variable substitution performed)
*/
-Widget.prototype.setVariable = function(name,value,params) {
- this.variables[name] = {value: value, params: params};
+Widget.prototype.setVariable = function(name,value,params,isMacroDefinition) {
+ this.variables[name] = {value: value, params: params, isMacroDefinition: !!isMacroDefinition};
};
/*
@@ -83,52 +84,76 @@ options: see below
Options include
params: array of {name:, value:} for each parameter
defaultValue: default value if the variable is not defined
+
+Returns an object with the following fields:
+
+params: array of {name:,value:} of parameters passed to wikitext variables
+text: text of variable, with parameters properly substituted
*/
-Widget.prototype.getVariable = function(name,options) {
+Widget.prototype.getVariableInfo = function(name,options) {
options = options || {};
var actualParams = options.params || [],
parentWidget = this.parentWidget;
// Check for the variable defined in the parent widget (or an ancestor in the prototype chain)
if(parentWidget && name in parentWidget.variables) {
var variable = parentWidget.variables[name],
- value = variable.value;
+ value = variable.value,
+ params = this.resolveVariableParameters(variable.params,actualParams);
// Substitute any parameters specified in the definition
- value = this.substituteVariableParameters(value,variable.params,actualParams);
- value = this.substituteVariableReferences(value);
- return value;
+ $tw.utils.each(params,function(param) {
+ value = $tw.utils.replaceString(value,new RegExp("\\$" + $tw.utils.escapeRegExp(param.name) + "\\$","mg"),param.value);
+ });
+ // Only substitute variable references if this variable was defined with the \define pragma
+ if(variable.isMacroDefinition) {
+ value = this.substituteVariableReferences(value);
+ }
+ return {
+ text: value,
+ params: params
+ };
}
// If the variable doesn't exist in the parent widget then look for a macro module
- return this.evaluateMacroModule(name,actualParams,options.defaultValue);
+ return {
+ text: this.evaluateMacroModule(name,actualParams,options.defaultValue)
+ };
};
-Widget.prototype.substituteVariableParameters = function(text,formalParams,actualParams) {
- if(formalParams) {
- var nextAnonParameter = 0, // Next candidate anonymous parameter in macro call
- paramInfo, paramValue;
- // Step through each of the parameters in the macro definition
- for(var p=0; p>
+diff-equal-background:
+diff-equal-foreground: <>
+diff-insert-background: #aaefad
+diff-insert-foreground: <>
+diff-invisible-background:
+diff-invisible-foreground: <>
dirty-indicator: #ff0000
download-background: #34c734
download-foreground: <>
diff --git a/core/sjcl-license.tid b/core/sjcl-license.tid
new file mode 100755
index 000000000..be9751832
--- /dev/null
+++ b/core/sjcl-license.tid
@@ -0,0 +1,61 @@
+title: $:/library/sjcl.js/license
+type: text/plain
+
+SJCL is open. You can use, modify and redistribute it under a BSD
+license or under the GNU GPL, version 2.0.
+
+---------------------------------------------------------------------
+
+http://opensource.org/licenses/BSD-2-Clause
+
+Copyright (c) 2009-2015, Emily Stark, Mike Hamburg and Dan Boneh at
+Stanford University. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+---------------------------------------------------------------------
+
+http://opensource.org/licenses/GPL-2.0
+
+The Stanford Javascript Crypto Library (hosted here on GitHub) is a
+project by the Stanford Computer Security Lab to build a secure,
+powerful, fast, small, easy-to-use, cross-browser library for
+cryptography in Javascript.
+
+Copyright (c) 2009-2015, Emily Stark, Mike Hamburg and Dan Boneh at
+Stanford University.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; either version 2 of the License, or (at your
+option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
\ No newline at end of file
diff --git a/core/templates/MOTW.html.tid b/core/templates/MOTW.html.tid
index 0a16bb790..b50357ee3 100644
--- a/core/templates/MOTW.html.tid
+++ b/core/templates/MOTW.html.tid
@@ -2,4 +2,4 @@ title: $:/core/templates/MOTW.html
\rules only filteredtranscludeinline transcludeinline entity
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/core/templates/canonical-uri-external-raw.tid b/core/templates/canonical-uri-external-raw.tid
new file mode 100644
index 000000000..564da18c7
--- /dev/null
+++ b/core/templates/canonical-uri-external-raw.tid
@@ -0,0 +1,8 @@
+title: $:/core/templates/canonical-uri-external-raw
+
+
+<$view field="title" format="doubleurlencoded"/>
\ No newline at end of file
diff --git a/core/templates/json-tiddler.tid b/core/templates/json-tiddler.tid
new file mode 100644
index 000000000..c9b9262bf
--- /dev/null
+++ b/core/templates/json-tiddler.tid
@@ -0,0 +1,7 @@
+title: $:/core/templates/json-tiddler
+
+<$text text=<>/>
\ No newline at end of file
diff --git a/core/templates/static.content.tid b/core/templates/static.content.tid
index 5be6cf953..2de15874b 100644
--- a/core/templates/static.content.tid
+++ b/core/templates/static.content.tid
@@ -1,8 +1,7 @@
title: $:/core/templates/static.content
-type: text/vnd.tiddlywiki
-This [[TiddlyWiki|http://tiddlywiki.com]] contains the following tiddlers:
+This [[TiddlyWiki|https://tiddlywiki.com]] contains the following tiddlers: