Merge branch 'master' into testcase-widget

This commit is contained in:
Jeremy Ruston
2023-11-21 12:39:45 +00:00
26 changed files with 501 additions and 113 deletions
+19 -5
View File
@@ -213,6 +213,18 @@ function getDataItemType(data,indexes) {
}
}
function getItemAtIndex(item,index) {
if($tw.utils.hop(item,index)) {
return item[index];
} else if($tw.utils.isArray(item)) {
index = $tw.utils.parseInt(index);
if(index < 0) { index = index + item.length };
return item[index]; // Will be undefined if index was out-of-bounds
} else {
return undefined;
}
}
/*
Given a JSON data structure and an array of index strings, return the value at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
@@ -225,7 +237,7 @@ function getDataItem(data,indexes) {
for(var i=0; i<indexes.length; i++) {
if(item !== undefined) {
if(item !== null && ["number","string","boolean"].indexOf(typeof item) === -1) {
item = item[indexes[i]];
item = getItemAtIndex(item,indexes[i]);
} else {
item = undefined;
}
@@ -249,16 +261,18 @@ function setDataItem(data,indexes,value) {
// Traverse the JSON data structure using the index chain
var current = data;
for(var i = 0; i < indexes.length - 1; i++) {
var index = indexes[i];
if($tw.utils.hop(current,index)) {
current = current[index];
} else {
current = getItemAtIndex(current,indexes[i]);
if(current === undefined) {
// Return the original JSON data structure if any of the index strings are invalid
return data;
}
}
// Add the value to the end of the index chain
var lastIndex = indexes[indexes.length - 1];
if($tw.utils.isArray(current)) {
lastIndex = $tw.utils.parseInt(lastIndex);
if(lastIndex < 0) { lastIndex = lastIndex + current.length };
}
current[lastIndex] = value;
return data;
}
+5 -4
View File
@@ -14,10 +14,12 @@ The plain text parser processes blocks of source text into a degenerate parse tr
var TextParser = function(type,text,options) {
this.tree = [{
type: "codeblock",
type: "genesis",
attributes: {
code: {type: "string", value: text},
language: {type: "string", value: type}
$type: {name: "$type", type: "string", value: "$codeblock"},
code: {name: "code", type: "string", value: text},
language: {name: "language", type: "string", value: type},
$remappable: {name: "$remappable", type:"string", value: "no"}
}
}];
this.source = text;
@@ -32,4 +34,3 @@ exports["text/css"] = TextParser;
exports["application/x-tiddler-dictionary"] = TextParser;
})();
@@ -194,6 +194,7 @@ Parse any pragmas at the beginning of a block of parse text
WikiParser.prototype.parsePragmas = function() {
var currentTreeBranch = this.tree;
while(true) {
var savedPos = this.pos;
// Skip whitespace
this.skipWhitespace();
// Check for the end of the text
@@ -204,6 +205,7 @@ WikiParser.prototype.parsePragmas = function() {
var nextMatch = this.findNextMatch(this.pragmaRules,this.pos);
// If not, just exit
if(!nextMatch || nextMatch.matchIndex !== this.pos) {
this.pos = savedPos;
break;
}
// Process the pragma rule
+7 -7
View File
@@ -122,10 +122,10 @@ function openStartupTiddlers(options) {
var hash = $tw.locationHash.substr(1),
split = hash.indexOf(":");
if(split === -1) {
target = $tw.utils.decodeURIComponentSafe(hash.trim());
target = $tw.utils.decodeTWURITarget(hash.trim());
} else {
target = $tw.utils.decodeURIComponentSafe(hash.substr(0,split).trim());
storyFilter = $tw.utils.decodeURIComponentSafe(hash.substr(split + 1).trim());
target = $tw.utils.decodeTWURITarget(hash.substr(0,split).trim());
storyFilter = $tw.utils.decodeTWURIList(hash.substr(split + 1).trim());
}
}
// If the story wasn't specified use the current tiddlers or a blank story
@@ -198,19 +198,19 @@ function updateLocationHash(options) {
// Assemble the location hash
switch(options.updateAddressBar) {
case "permalink":
$tw.locationHash = "#" + encodeURIComponent(targetTiddler);
$tw.locationHash = "#" + $tw.utils.encodeTiddlerTitle(targetTiddler);
break;
case "permaview":
$tw.locationHash = "#" + encodeURIComponent(targetTiddler) + ":" + encodeURIComponent($tw.utils.stringifyList(storyList));
$tw.locationHash = "#" + $tw.utils.encodeTiddlerTitle(targetTiddler) + ":" + $tw.utils.encodeFilterPath($tw.utils.stringifyList(storyList));
break;
}
// Copy URL to the clipboard
switch(options.copyToClipboard) {
case "permalink":
$tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + encodeURIComponent(targetTiddler));
$tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + $tw.utils.encodeTiddlerTitle(targetTiddler));
break;
case "permaview":
$tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + encodeURIComponent(targetTiddler) + ":" + encodeURIComponent($tw.utils.stringifyList(storyList)));
$tw.utils.copyToClipboard($tw.utils.getLocationPath() + "#" + $tw.utils.encodeTiddlerTitle(targetTiddler) + ":" + $tw.utils.encodeFilterPath($tw.utils.stringifyList(storyList)));
break;
}
// Only change the location hash if we must, thus avoiding unnecessary onhashchange events
+3 -3
View File
@@ -313,7 +313,7 @@ exports.collectDOMVariables = function(selectedNode,domNode,event) {
variables["dom-" + attribute.name] = attribute.value.toString();
});
if(selectedNode.offsetLeft) {
if("offsetLeft" in selectedNode) {
// Add variables with a (relative and absolute) popup coordinate string for the selected node
var nodeRect = {
left: selectedNode.offsetLeft,
@@ -338,12 +338,12 @@ exports.collectDOMVariables = function(selectedNode,domNode,event) {
}
}
if(domNode && domNode.offsetWidth) {
if(domNode && ("offsetWidth" in domNode)) {
variables["tv-widgetnode-width"] = domNode.offsetWidth.toString();
variables["tv-widgetnode-height"] = domNode.offsetHeight.toString();
}
if(event && event.clientX && event.clientY) {
if(event && ("clientX" in event) && ("clientY" in event)) {
if(selectedNode) {
// Add variables for event X and Y position relative to selected node
selectedNodeRect = selectedNode.getBoundingClientRect();
+128
View File
@@ -0,0 +1,128 @@
/*\
title: $:/core/modules/utils/twuri-encoding.js
type: application/javascript
module-type: utils
Utility functions related to permalink/permaview encoding/decoding.
\*/
(function(){
// The character that will substitute for a space in the URL
var SPACE_SUBSTITUTE = "_";
// The character added to the end to avoid ending with `.`, `?`, `!` or the like
var TRAILER = "_";
// The character that will separate out the list elements in the URL
var CONJUNCTION = ";";
// Those of the allowed url characters claimed by TW
var CLAIMED = [SPACE_SUBSTITUTE, ":", CONJUNCTION];
// Non-alphanumeric characters allowed in a URL fragment
// More information at https://www.rfc-editor.org/rfc/rfc3986#appendix-A
var VALID_IN_URL_FRAGMENT = "-._~!$&'()*+,;=:@/?".split("");
// The subset of the pchars we will not percent-encode in permalinks/permaviews
var SUBSTITUTES = []
$tw.utils.each(VALID_IN_URL_FRAGMENT, function(c) {
if (CLAIMED.indexOf(c) === -1) {
SUBSTITUTES.push(c)
}
});
// A regex to match the percent-encoded characters we will want to replace.
// Something similar to the following, depending on SPACE and CONJUNCTION
// /(%2D|%2E|%7E|%21|%24|%26|%27|%28|%29|%2A|%2B|%3B|%3D|%40|%2F|%3F)/g
var CHAR_MATCH_STR = []
$tw.utils.each(SUBSTITUTES, function(c) {
CHAR_MATCH_STR.push("%" + c.charCodeAt(0).toString(16).toUpperCase())
})
var CHAR_MATCH = new RegExp("(" + CHAR_MATCH_STR.join("|") + ")", "g");
// A regex to match the SPACE_SUBSTITUTE character
var SPACE_MATCH = new RegExp("(\\" + SPACE_SUBSTITUTE + ")", "g");
// A regex to match URLs ending with sentence-ending punctuation
var SENTENCE_ENDING = new RegExp("(\\.|\\!|\\?|\\" + TRAILER + ")$", "g");
// A regex to match URLs ending with sentence-ending punctuation plus the TRAILER
var SENTENCE_TRAILING = new RegExp("(\\.|\\!|\\?|\\" + TRAILER + ")\\" + TRAILER + "$", "g");
// An object mapping the percent encodings back to their source characters
var PCT_CHAR_MAP = SUBSTITUTES.reduce(function (a, c) {
a["%" + c.charCodeAt(0).toString(16).toUpperCase()] = c
return a
}, {});
// Convert a URI List Component encoded string (with the `SPACE_SUBSTITUTE`
// value as an allowed replacement for the space character) to a string
exports.decodeTWURIList = function(s) {
var parts = s.replace(SENTENCE_TRAILING, "$1").split(CONJUNCTION);
var withSpaces = []
$tw.utils.each(parts, function(s) {
withSpaces.push(s.replace(SPACE_MATCH, " "))
});
var withBrackets = []
$tw.utils.each(withSpaces, function(s) {
withBrackets .push(s.indexOf(" ") >= 0 ? "[[" + s + "]]" : s)
});
return $tw.utils.decodeURIComponentSafe(withBrackets.join(" "));
};
// Convert a URI Target Component encoded string (with the `SPACE_SUBSTITUTE`
// value as an allowed replacement for the space character) to a string
exports.decodeTWURITarget = function(s) {
return $tw.utils.decodeURIComponentSafe(
s.replace(SENTENCE_TRAILING, "$1").replace(SPACE_MATCH, " ")
)
};
// Convert a URIComponent encoded title string (with the `SPACE_SUBSTITUTE`
// value as an allowed replacement for the space character) to a string
exports.encodeTiddlerTitle = function(s) {
var extended = s.replace(SENTENCE_ENDING, "$1" + TRAILER)
var encoded = encodeURIComponent(extended);
var substituted = encoded.replace(/\%20/g, SPACE_SUBSTITUTE);
return substituted.replace(CHAR_MATCH, function(_, c) {
return PCT_CHAR_MAP[c];
});
};
// Convert a URIComponent encoded filter string (with the `SPACE_SUBSTITUTE`
// value as an allowed replacement for the space character) to a string
exports.encodeFilterPath = function(s) {
var parts = s.replace(SENTENCE_ENDING, "$1" + TRAILER)
.replace(/\[\[(.+?)\]\]/g, function (_, t) {return t.replace(/ /g, SPACE_SUBSTITUTE )})
.split(" ");
var nonEmptyParts = []
$tw.utils.each(parts, function(p) {
if (p) {
nonEmptyParts.push (p)
}
});
var trimmed = [];
$tw.utils.each(nonEmptyParts, function(s) {
trimmed.push(s.trim())
});
var encoded = [];
$tw.utils.each(trimmed, function(s) {
encoded.push(encodeURIComponent(s))
});
var substituted = [];
$tw.utils.each(encoded, function(s) {
substituted.push(s.replace(/\%20/g, SPACE_SUBSTITUTE))
});
var replaced = []
$tw.utils.each(substituted, function(s) {
replaced.push(s.replace(CHAR_MATCH, function(_, c) {
return PCT_CHAR_MAP[c];
}))
});
return replaced.join(CONJUNCTION);
};
})();
+5 -1
View File
@@ -100,6 +100,9 @@ ImageWidget.prototype.render = function(parent,nextSibling) {
if(this.imageClass) {
domNode.setAttribute("class",this.imageClass);
}
if(this.imageUsemap) {
domNode.setAttribute("usemap",this.imageUsemap);
}
if(this.imageWidth) {
domNode.setAttribute("width",this.imageWidth);
}
@@ -139,6 +142,7 @@ ImageWidget.prototype.execute = function() {
this.imageWidth = this.getAttribute("width");
this.imageHeight = this.getAttribute("height");
this.imageClass = this.getAttribute("class");
this.imageUsemap = this.getAttribute("usemap");
this.imageTooltip = this.getAttribute("tooltip");
this.imageAlt = this.getAttribute("alt");
this.lazyLoading = this.getAttribute("loading");
@@ -149,7 +153,7 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
ImageWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.source || changedAttributes.width || changedAttributes.height || changedAttributes["class"] || changedAttributes.tooltip || changedTiddlers[this.imageSource]) {
if(changedAttributes.source || changedAttributes.width || changedAttributes.height || changedAttributes["class"] || changedAttributes.usemap || changedAttributes.tooltip || changedTiddlers[this.imageSource]) {
this.refreshSelf();
return true;
} else {
+40 -4
View File
@@ -28,6 +28,18 @@ Inherit from the base widget class
*/
ListWidget.prototype = new Widget();
ListWidget.prototype.initialise = function(parseTreeNode,options) {
// Bail if parseTreeNode is undefined, meaning that the ListWidget constructor was called without any arguments so that it can be subclassed
if(parseTreeNode === undefined) {
return;
}
// First call parent constructor to set everything else up
Widget.prototype.initialise.call(this,parseTreeNode,options);
// Now look for <$list-template> and <$list-empty> widgets as immediate child widgets
// This is safe to do during initialization because parse trees never change after creation
this.findExplicitTemplates();
}
/*
Render this widget into the DOM
*/
@@ -68,8 +80,6 @@ ListWidget.prototype.execute = function() {
this.counterName = this.getAttribute("counter");
this.storyViewName = this.getAttribute("storyview");
this.historyTitle = this.getAttribute("history");
// Look for <$list-template> and <$list-empty> widgets as immediate child widgets
this.findExplicitTemplates();
// Compose the list elements
this.list = this.getTiddlerList();
var members = [],
@@ -92,6 +102,7 @@ ListWidget.prototype.findExplicitTemplates = function() {
var self = this;
this.explicitListTemplate = null;
this.explicitEmptyTemplate = null;
this.hasTemplateInBody = false;
var searchChildren = function(childNodes) {
$tw.utils.each(childNodes,function(node) {
if(node.type === "list-template") {
@@ -100,6 +111,8 @@ ListWidget.prototype.findExplicitTemplates = function() {
self.explicitEmptyTemplate = node.children;
} else if(node.type === "element" && node.tag === "p") {
searchChildren(node.children);
} else {
self.hasTemplateInBody = true;
}
});
};
@@ -160,11 +173,11 @@ ListWidget.prototype.makeItemTemplate = function(title,index) {
// Check for a <$list-item> widget
if(this.explicitListTemplate) {
templateTree = this.explicitListTemplate;
} else if (!this.explicitEmptyTemplate) {
} else if(this.hasTemplateInBody) {
templateTree = this.parseTreeNode.children;
}
}
if(!templateTree) {
if(!templateTree || templateTree.length === 0) {
// Default template is a link to the title
templateTree = [{type: "element", tag: this.parseTreeNode.isBlock ? "div" : "span", children: [{type: "link", attributes: {to: {type: "string", value: title}}, children: [
{type: "text", text: title}
@@ -414,4 +427,27 @@ ListItemWidget.prototype.refresh = function(changedTiddlers) {
exports.listitem = ListItemWidget;
/*
Make <$list-template> and <$list-empty> widgets that do nothing
*/
var ListTemplateWidget = function(parseTreeNode,options) {
// Main initialisation inherited from widget.js
this.initialise(parseTreeNode,options);
};
ListTemplateWidget.prototype = new Widget();
ListTemplateWidget.prototype.render = function() {}
ListTemplateWidget.prototype.refresh = function() { return false; }
exports["list-template"] = ListTemplateWidget;
var ListEmptyWidget = function(parseTreeNode,options) {
// Main initialisation inherited from widget.js
this.initialise(parseTreeNode,options);
};
ListEmptyWidget.prototype = new Widget();
ListEmptyWidget.prototype.render = function() {}
ListEmptyWidget.prototype.refresh = function() { return false; }
exports["list-empty"] = ListEmptyWidget;
})();
+44
View File
@@ -171,6 +171,46 @@ ScrollableWidget.prototype.render = function(parent,nextSibling) {
parent.insertBefore(this.outerDomNode,nextSibling);
this.renderChildren(this.innerDomNode,null);
this.domNodes.push(this.outerDomNode);
// If the scroll position is bound to a tiddler
if(this.scrollableBind) {
// After a delay for rendering, scroll to the bound position
setTimeout(this.updateScrollPositionFromBoundTiddler.bind(this),50);
// Save scroll position on DOM scroll event
this.outerDomNode.addEventListener("scroll",function(event) {
var existingTiddler = self.wiki.getTiddler(self.scrollableBind),
newTiddlerFields = {
title: self.scrollableBind,
"scroll-left": self.outerDomNode.scrollLeft.toString(),
"scroll-top": self.outerDomNode.scrollTop.toString()
};
if(!existingTiddler || (existingTiddler.fields["scroll-left"] !== newTiddlerFields["scroll-left"] || existingTiddler.fields["scroll-top"] !== newTiddlerFields["scroll-top"])) {
self.wiki.addTiddler(new $tw.Tiddler(existingTiddler,newTiddlerFields));
}
});
}
};
ScrollableWidget.prototype.updateScrollPositionFromBoundTiddler = function() {
// Bail if we're running on the fakedom
if(!this.outerDomNode.scrollTo) {
return;
}
var tiddler = this.wiki.getTiddler(this.scrollableBind);
if(tiddler) {
var scrollLeftTo = this.outerDomNode.scrollLeft;
if(parseFloat(tiddler.fields["scroll-left"]).toString() === tiddler.fields["scroll-left"]) {
scrollLeftTo = parseFloat(tiddler.fields["scroll-left"]);
}
var scrollTopTo = this.outerDomNode.scrollTop;
if(parseFloat(tiddler.fields["scroll-top"]).toString() === tiddler.fields["scroll-top"]) {
scrollTopTo = parseFloat(tiddler.fields["scroll-top"]);
}
this.outerDomNode.scrollTo({
top: scrollTopTo,
left: scrollLeftTo,
behavior: "instant"
})
}
};
/*
@@ -178,6 +218,7 @@ Compute the internal state of the widget
*/
ScrollableWidget.prototype.execute = function() {
// Get attributes
this.scrollableBind = this.getAttribute("bind");
this.fallthrough = this.getAttribute("fallthrough","yes");
this["class"] = this.getAttribute("class");
// Make child widgets
@@ -193,6 +234,9 @@ ScrollableWidget.prototype.refresh = function(changedTiddlers) {
this.refreshSelf();
return true;
}
if(changedAttributes.bind || changedTiddlers[this.getAttribute("bind")]) {
this.updateScrollPositionFromBoundTiddler();
}
return this.refreshChildren(changedTiddlers);
};
+2 -2
View File
@@ -1287,7 +1287,7 @@ exports.search = function(text,options) {
console.log("Regexp error parsing /(" + text + ")/" + flags + ": ",e);
}
} else if(options.some) {
terms = text.trim().split(/ +/);
terms = text.trim().split(/[^\S\xA0]+/);
if(terms.length === 1 && terms[0] === "") {
searchTermsRegExps = null;
} else {
@@ -1298,7 +1298,7 @@ exports.search = function(text,options) {
searchTermsRegExps.push(new RegExp("(" + regExpStr + ")",flags));
}
} else { // default: words
terms = text.split(/ +/);
terms = text.split(/[^\S\xA0]+/);
if(terms.length === 1 && terms[0] === "") {
searchTermsRegExps = null;
} else {