mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-09-20 16:42:02 +00:00
Add support for JSON-formatted tiddler store, and make it the default (#5708)
* Add support for JSON-formatted tiddler store, and make it the default The change to `getTiddlersAsJson()` is to allow experimentation * Move JSON tiddlers into their own store area, and fix support for encrypted tiddlers Also add a dummy old-style store area for backwards compatibility The current arrangement is that JSON tiddlers will always override old-style tiddlers. * Use the deserialiser mechanism to decode the content * Refactor $:/core/modules/deserializers.js before we start extending it Cleaning up the helper function names and ordering * Drop support for the "systemArea" div It was only used in really old v5.0.x * Update deserializer to support JSON store format and add some tests * Life UI restrictions on characters in fieldnames * Add another test case * Correct mis-merge * Remove toLowerCase() methods applied to fieldnames * Insert line breaks in output of getTiddlersAsJson (#5786) Rather than have the entire store on one line, insert a line break after each tiddler. * Refactor #5786 for backwards compatibility * Only read .tiddlywiki-tiddler-store blocks from script tags Prompted by @simonbaird's comment here: https://github.com/Jermolene/TiddlyWiki5/pull/5708#discussion_r648833367 * Clean up escaping of unsafe script characters It seems that escaping `<` is sufficient * Add docs from @saqimtiaz Thanks @saqimtiaz * Docs tweaks * Remove excess whitespace Thanks @simonbaird * Fix templates for lazy loading * Remove obsolete item from release note * Clean up whitespace * Docs for the jsontiddler widget * Fix whitespace Fixes #5840 * Comments * Fix newlines in JSON store area * Remove obsolete docs change Co-authored-by: Simon Baird <simon.baird@gmail.com>
This commit is contained in:
co-authored by
Simon Baird
parent
155525708b
commit
d455072f13
@@ -12,63 +12,8 @@ Functions to deserialise tiddlers from a block of text
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
/*
|
||||
Utility function to parse an old-style tiddler DIV in a *.tid file. It looks like this:
|
||||
|
||||
<div title="Title" creator="JoeBloggs" modifier="JoeBloggs" created="201102111106" modified="201102111310" tags="myTag [[my long tag]]">
|
||||
<pre>The text of the tiddler (without the expected HTML encoding).
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Note that the field attributes are HTML encoded, but that the body of the <PRE> tag is not encoded.
|
||||
|
||||
When these tiddler DIVs are encountered within a TiddlyWiki HTML file then the body is encoded in the usual way.
|
||||
*/
|
||||
var parseTiddlerDiv = function(text /* [,fields] */) {
|
||||
// Slot together the default results
|
||||
var result = {};
|
||||
if(arguments.length > 1) {
|
||||
for(var f=1; f<arguments.length; f++) {
|
||||
var fields = arguments[f];
|
||||
for(var t in fields) {
|
||||
result[t] = fields[t];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse the DIV body
|
||||
var startRegExp = /^\s*<div\s+([^>]*)>(\s*<pre>)?/gi,
|
||||
endRegExp,
|
||||
match = startRegExp.exec(text);
|
||||
if(match) {
|
||||
// Old-style DIVs don't have the <pre> tag
|
||||
if(match[2]) {
|
||||
endRegExp = /<\/pre>\s*<\/div>\s*$/gi;
|
||||
} else {
|
||||
endRegExp = /<\/div>\s*$/gi;
|
||||
}
|
||||
var endMatch = endRegExp.exec(text);
|
||||
if(endMatch) {
|
||||
// Extract the text
|
||||
result.text = text.substring(match.index + match[0].length,endMatch.index);
|
||||
// Process the attributes
|
||||
var attrRegExp = /\s*([^=\s]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
|
||||
attrMatch;
|
||||
do {
|
||||
attrMatch = attrRegExp.exec(match[1]);
|
||||
if(attrMatch) {
|
||||
var name = attrMatch[1];
|
||||
var value = attrMatch[2] !== undefined ? attrMatch[2] : attrMatch[3];
|
||||
result[name] = value;
|
||||
}
|
||||
} while(attrMatch);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
exports["application/x-tiddler-html-div"] = function(text,fields) {
|
||||
return [parseTiddlerDiv(text,fields)];
|
||||
return [deserializeTiddlerDiv(text,fields)];
|
||||
};
|
||||
|
||||
exports["application/json"] = function(text,fields) {
|
||||
@@ -105,30 +50,34 @@ Parse an HTML file into tiddlers. There are three possibilities:
|
||||
# An ordinary HTML file
|
||||
*/
|
||||
exports["text/html"] = function(text,fields) {
|
||||
// Check if we've got a store area
|
||||
var results = [];
|
||||
// Check if we've got an old-style store area
|
||||
var storeAreaMarkerRegExp = /<div id=["']?storeArea['"]?( style=["']?display:none;["']?)?>/gi,
|
||||
match = storeAreaMarkerRegExp.exec(text);
|
||||
if(match) {
|
||||
// If so, it's either a classic TiddlyWiki file or an unencrypted TW5 file
|
||||
// First read the normal tiddlers
|
||||
var results = deserializeTiddlyWikiFile(text,storeAreaMarkerRegExp.lastIndex,!!match[1],fields);
|
||||
// Then any system tiddlers
|
||||
var systemAreaMarkerRegExp = /<div id=["']?systemArea['"]?( style=["']?display:none;["']?)?>/gi,
|
||||
sysMatch = systemAreaMarkerRegExp.exec(text);
|
||||
if(sysMatch) {
|
||||
results.push.apply(results,deserializeTiddlyWikiFile(text,systemAreaMarkerRegExp.lastIndex,!!sysMatch[1],fields));
|
||||
}
|
||||
storeAreaMatch = storeAreaMarkerRegExp.exec(text);
|
||||
if(storeAreaMatch) {
|
||||
// If so, we've got tiddlers in classic TiddlyWiki format or unencrypted old-style TW5 format
|
||||
results.push.apply(results,deserializeStoreArea(text,storeAreaMarkerRegExp.lastIndex,!!storeAreaMatch[1],fields));
|
||||
}
|
||||
// Check for new-style store areas
|
||||
var newStoreAreaMarkerRegExp = /<script class="tiddlywiki-tiddler-store" type="([^"]*)">/gi,
|
||||
newStoreAreaMatch = newStoreAreaMarkerRegExp.exec(text),
|
||||
haveHadNewStoreArea = !!newStoreAreaMatch;
|
||||
while(newStoreAreaMatch) {
|
||||
results.push.apply(results,deserializeNewStoreArea(text,newStoreAreaMarkerRegExp.lastIndex,newStoreAreaMatch[1],fields));
|
||||
newStoreAreaMatch = newStoreAreaMarkerRegExp.exec(text);
|
||||
}
|
||||
// Return if we had either an old-style or a new-style store area
|
||||
if(storeAreaMatch || haveHadNewStoreArea) {
|
||||
return results;
|
||||
}
|
||||
// Otherwise, check whether we've got an encrypted file
|
||||
var encryptedStoreArea = $tw.utils.extractEncryptedStoreArea(text);
|
||||
if(encryptedStoreArea) {
|
||||
// If so, attempt to decrypt it using the current password
|
||||
return $tw.utils.decryptStoreArea(encryptedStoreArea);
|
||||
} else {
|
||||
// Check whether we've got an encrypted file
|
||||
var encryptedStoreArea = $tw.utils.extractEncryptedStoreArea(text);
|
||||
if(encryptedStoreArea) {
|
||||
// If so, attempt to decrypt it using the current password
|
||||
return $tw.utils.decryptStoreArea(encryptedStoreArea);
|
||||
} else {
|
||||
// It's not a TiddlyWiki so we'll return the entire HTML file as a tiddler
|
||||
return deserializeHtmlFile(text,fields);
|
||||
}
|
||||
// It's not a TiddlyWiki so we'll return the entire HTML file as a tiddler
|
||||
return deserializeHtmlFile(text,fields);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,7 +91,19 @@ function deserializeHtmlFile(text,fields) {
|
||||
return [result];
|
||||
}
|
||||
|
||||
function deserializeTiddlyWikiFile(text,storeAreaEnd,isTiddlyWiki5,fields) {
|
||||
function deserializeNewStoreArea(text,storeAreaEnd,type,fields) {
|
||||
var endOfScriptRegExp = /<\/script>/gi;
|
||||
endOfScriptRegExp.lastIndex = storeAreaEnd;
|
||||
var match = endOfScriptRegExp.exec(text);
|
||||
if(match) {
|
||||
var scriptContent = text.substring(storeAreaEnd,match.index);
|
||||
return $tw.wiki.deserializeTiddlers(type,scriptContent);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function deserializeStoreArea(text,storeAreaEnd,isTiddlyWiki5,fields) {
|
||||
var results = [],
|
||||
endOfDivRegExp = /(<\/div>\s*)/gi,
|
||||
startPos = storeAreaEnd,
|
||||
@@ -151,7 +112,7 @@ function deserializeTiddlyWikiFile(text,storeAreaEnd,isTiddlyWiki5,fields) {
|
||||
var match = endOfDivRegExp.exec(text);
|
||||
while(match) {
|
||||
var endPos = endOfDivRegExp.lastIndex,
|
||||
tiddlerFields = parseTiddlerDiv(text.substring(startPos,endPos),fields,{type: defaultType});
|
||||
tiddlerFields = deserializeTiddlerDiv(text.substring(startPos,endPos),fields,{type: defaultType});
|
||||
if(!tiddlerFields) {
|
||||
break;
|
||||
}
|
||||
@@ -169,4 +130,59 @@ function deserializeTiddlyWikiFile(text,storeAreaEnd,isTiddlyWiki5,fields) {
|
||||
return results;
|
||||
}
|
||||
|
||||
/*
|
||||
Utility function to parse an old-style tiddler DIV in a *.tid file. It looks like this:
|
||||
|
||||
<div title="Title" creator="JoeBloggs" modifier="JoeBloggs" created="201102111106" modified="201102111310" tags="myTag [[my long tag]]">
|
||||
<pre>The text of the tiddler (without the expected HTML encoding).
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
Note that the field attributes are HTML encoded, but that the body of the <PRE> tag is not encoded.
|
||||
|
||||
When these tiddler DIVs are encountered within a TiddlyWiki HTML file then the body is encoded in the usual way.
|
||||
*/
|
||||
var deserializeTiddlerDiv = function(text /* [,fields] */) {
|
||||
// Slot together the default results
|
||||
var result = {};
|
||||
if(arguments.length > 1) {
|
||||
for(var f=1; f<arguments.length; f++) {
|
||||
var fields = arguments[f];
|
||||
for(var t in fields) {
|
||||
result[t] = fields[t];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Parse the DIV body
|
||||
var startRegExp = /^\s*<div\s+([^>]*)>(\s*<pre>)?/gi,
|
||||
endRegExp,
|
||||
match = startRegExp.exec(text);
|
||||
if(match) {
|
||||
// Old-style DIVs don't have the <pre> tag
|
||||
if(match[2]) {
|
||||
endRegExp = /<\/pre>\s*<\/div>\s*$/gi;
|
||||
} else {
|
||||
endRegExp = /<\/div>\s*$/gi;
|
||||
}
|
||||
var endMatch = endRegExp.exec(text);
|
||||
if(endMatch) {
|
||||
// Extract the text
|
||||
result.text = text.substring(match.index + match[0].length,endMatch.index);
|
||||
// Process the attributes
|
||||
var attrRegExp = /\s*([^=\s]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
|
||||
attrMatch;
|
||||
do {
|
||||
attrMatch = attrRegExp.exec(match[1]);
|
||||
if(attrMatch) {
|
||||
var name = attrMatch[1];
|
||||
var value = attrMatch[2] !== undefined ? attrMatch[2] : attrMatch[3];
|
||||
result[name] = value;
|
||||
}
|
||||
} while(attrMatch);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
})();
|
||||
|
||||
@@ -17,7 +17,7 @@ Export our filter function
|
||||
*/
|
||||
exports.contains = function(source,operator,options) {
|
||||
var results = [],
|
||||
fieldname = (operator.suffix || "list").toLowerCase();
|
||||
fieldname = operator.suffix || "list";
|
||||
if(operator.prefix === "!") {
|
||||
source(function(tiddler,title) {
|
||||
if(tiddler) {
|
||||
|
||||
@@ -17,7 +17,7 @@ Export our filter function
|
||||
*/
|
||||
exports.field = function(source,operator,options) {
|
||||
var results = [],indexedResults,
|
||||
fieldname = (operator.suffix || operator.operator || "title").toLowerCase();
|
||||
fieldname = operator.suffix || operator.operator || "title";
|
||||
if(operator.prefix === "!") {
|
||||
if(operator.regexp) {
|
||||
source(function(tiddler,title) {
|
||||
|
||||
@@ -17,7 +17,7 @@ Export our filter function
|
||||
*/
|
||||
exports.regexp = function(source,operator,options) {
|
||||
var results = [],
|
||||
fieldname = (operator.suffix || "title").toLowerCase(),
|
||||
fieldname = operator.suffix || "title",
|
||||
regexpString, regexp, flags = "", match,
|
||||
getFieldString = function(tiddler,title) {
|
||||
if(tiddler) {
|
||||
|
||||
@@ -741,9 +741,8 @@ exports.isValidFieldName = function(name) {
|
||||
if(!name || typeof name !== "string") {
|
||||
return false;
|
||||
}
|
||||
name = name.toLowerCase().trim();
|
||||
var fieldValidatorRegEx = /^[a-z0-9\-\._]+$/mg;
|
||||
return fieldValidatorRegEx.test(name);
|
||||
// Since v5.2.x, there are no restrictions on characters in field names
|
||||
return name;
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -73,26 +73,12 @@ FieldManglerWidget.prototype.handleRemoveFieldEvent = function(event) {
|
||||
FieldManglerWidget.prototype.handleAddFieldEvent = function(event) {
|
||||
var tiddler = this.wiki.getTiddler(this.mangleTitle),
|
||||
addition = this.wiki.getModificationFields(),
|
||||
hadInvalidFieldName = false,
|
||||
addField = function(name,value) {
|
||||
var trimmedName = name.toLowerCase().trim();
|
||||
if(!$tw.utils.isValidFieldName(trimmedName)) {
|
||||
if(!hadInvalidFieldName) {
|
||||
alert($tw.language.getString(
|
||||
"InvalidFieldName",
|
||||
{variables:
|
||||
{fieldName: trimmedName}
|
||||
}
|
||||
));
|
||||
hadInvalidFieldName = true;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if(!value && tiddler) {
|
||||
value = tiddler.fields[trimmedName];
|
||||
}
|
||||
addition[trimmedName] = value || "";
|
||||
var trimmedName = name.trim();
|
||||
if(!value && tiddler) {
|
||||
value = tiddler.fields[trimmedName];
|
||||
}
|
||||
addition[trimmedName] = value || "";
|
||||
return;
|
||||
};
|
||||
addition.title = this.mangleTitle;
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*\
|
||||
title: $:/core/modules/widgets/jsontiddler.js
|
||||
type: application/javascript
|
||||
module-type: widget
|
||||
|
||||
Render a tiddler as JSON text
|
||||
|
||||
\*/
|
||||
(function(){
|
||||
|
||||
/*jslint node: true, browser: true */
|
||||
/*global $tw: false */
|
||||
"use strict";
|
||||
|
||||
var Widget = require("$:/core/modules/widgets/widget.js").widget;
|
||||
|
||||
var JSONTiddlerWidget = function(parseTreeNode,options) {
|
||||
this.initialise(parseTreeNode,options);
|
||||
};
|
||||
|
||||
/*
|
||||
Inherit from the base widget class
|
||||
*/
|
||||
JSONTiddlerWidget.prototype = new Widget();
|
||||
|
||||
/*
|
||||
Render this widget into the DOM
|
||||
*/
|
||||
JSONTiddlerWidget.prototype.render = function(parent,nextSibling) {
|
||||
var self = this;
|
||||
this.parentDomNode = parent;
|
||||
this.computeAttributes();
|
||||
this.execute();
|
||||
// Collect the fields from the optional base tiddler
|
||||
var fields = {};
|
||||
if(this.attTiddler) {
|
||||
var tiddler = this.wiki.getTiddler(this.attTiddler);
|
||||
if(tiddler) {
|
||||
fields = tiddler.getFieldStrings({exclude: this.attExclude.split(" ")});
|
||||
}
|
||||
}
|
||||
// Add custom fields specified in attributes starting with $
|
||||
$tw.utils.each(this.attributes,function(attribute,name) {
|
||||
if(name.charAt(0) === "$") {
|
||||
fields[name.slice(1)] = attribute;
|
||||
}
|
||||
});
|
||||
// JSONify
|
||||
var json = JSON.stringify(fields);
|
||||
// Escape unsafe script characters
|
||||
if(this.attEscapeUnsafeScriptChars) {
|
||||
json = json.replace(/</g,"\\u003C");
|
||||
}
|
||||
// Update the DOM
|
||||
var textNode = this.document.createTextNode(json);
|
||||
parent.insertBefore(textNode,nextSibling);
|
||||
this.domNodes.push(textNode);
|
||||
};
|
||||
|
||||
/*
|
||||
Compute the internal state of the widget
|
||||
*/
|
||||
JSONTiddlerWidget.prototype.execute = function() {
|
||||
this.attTiddler = this.getAttribute("tiddler");
|
||||
this.attExclude = this.getAttribute("exclude","");
|
||||
this.attEscapeUnsafeScriptChars = this.getAttribute("escapeUnsafeScriptChars","no") === "yes";
|
||||
};
|
||||
|
||||
/*
|
||||
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
|
||||
*/
|
||||
JSONTiddlerWidget.prototype.refresh = function(changedTiddlers) {
|
||||
var changedAttributes = this.computeAttributes();
|
||||
if($tw.utils.count(changedAttributes) > 0 || (this.attTiddler && changedTiddlers[this.attTiddler])) {
|
||||
this.refreshSelf();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
exports.jsontiddler = JSONTiddlerWidget;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user