Merge branch 'master' into testcase-widget

This commit is contained in:
Jeremy Ruston 2023-11-21 12:39:45 +00:00
commit 2c07310cd3
26 changed files with 501 additions and 113 deletions

View File

@ -60,6 +60,7 @@ jobs:
TW5_BUILD_TIDDLYWIKI: "./node_modules/tiddlywiki/tiddlywiki.js"
TW5_BUILD_MAIN_EDITION: "./editions/tw5.com"
TW5_BUILD_OUTPUT: "./output"
TW5_BUILD_ARCHIVE: "./output"
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1

View File

@ -84,10 +84,27 @@ echo -e -n "title: $:/build\ncommit: $TW5_BUILD_COMMIT\n\n$TW5_BUILD_DETAILS\n"
######################################################
#
# Core distribution
# Core distributions
#
######################################################
# Conditionally build archive if $TW5_BUILD_ARCHIVE variable is set, otherwise do nothing
#
# /archive/Empty-TiddlyWiki-<version>.html Empty archived version
# /archive/TiddlyWiki-<version>.html Full archived version
if [ -n "$TW5_BUILD_ARCHIVE" ]; then
node $TW5_BUILD_TIDDLYWIKI \
$TW5_BUILD_MAIN_EDITION \
--verbose \
--version \
--load $TW5_BUILD_OUTPUT/build.tid \
--output $TW5_BUILD_ARCHIVE \
--build archive \
|| exit 1
fi
# /index.html Main site
# /favicon.ico Favicon for main site
# /static.html Static rendering of default tiddlers
@ -95,6 +112,7 @@ echo -e -n "title: $:/build\ncommit: $TW5_BUILD_COMMIT\n\n$TW5_BUILD_DETAILS\n"
# /static/* Static single tiddlers
# /static/static.css Static stylesheet
# /static/favicon.ico Favicon for static pages
node $TW5_BUILD_TIDDLYWIKI \
$TW5_BUILD_MAIN_EDITION \
--verbose \

View File

@ -2,9 +2,6 @@
# test TiddlyWiki5 for tiddlywiki.com
npm install playwright @playwright/test
npx playwright install chromium firefox --with-deps
node ./tiddlywiki.js \
./editions/test \
--verbose \
@ -13,4 +10,7 @@ node ./tiddlywiki.js \
--test \
|| exit 1
npm install playwright @playwright/test
npx playwright install chromium firefox --with-deps
npx playwright test

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

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

View File

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

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

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

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

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 {

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

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

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 {

View File

@ -54,7 +54,7 @@ $:/config/EditTemplateFields/Visibility/$(currentField)$
\whitespace trim
<$vars name={{{ [<newFieldNameTiddler>get[text]] }}}>
<$reveal type="nomatch" text="" default=<<name>>>
<$button tooltip=<<lingo Fields/Add/Button/Hint>>>
<$button tooltip={{$:/language/EditTemplate/Fields/Add/Button/Hint}}>
<$action-sendmessage $message="tm-add-field"
$name=<<name>>
$value={{{ [subfilter<get-field-value-tiddler-filter>get[text]] }}}/>

View File

@ -4,17 +4,17 @@ tags: $:/tags/Macro
\define list-links(filter,type:"ul",subtype:"li",class:"",emptyMessage,field:"caption")
\whitespace trim
<$genesis $type=<<__type__>> class=<<__class__>>>
<$list filter=<<__filter__>> emptyMessage=<<__emptyMessage__>>>
<$genesis $type=<<__subtype__>>>
<$link to={{!!title}}>
<$let tv-wikilinks="no">
<$transclude field=<<__field__>>>
<$view field="title"/>
</$transclude>
</$let>
</$link>
</$genesis>
</$list>
<$list filter=<<__filter__>> emptyMessage=<<__emptyMessage__>>>
<$genesis $type=<<__subtype__>>>
<$link to={{!!title}}>
<$let tv-wikilinks="no">
<$transclude field=<<__field__>>>
<$view field="title"/>
</$transclude>
</$let>
</$link>
</$genesis>
</$list>
</$genesis>
\end
@ -25,34 +25,42 @@ tags: $:/tags/Macro
\define list-links-draggable(tiddler,field:"list",emptyMessage,type:"ul",subtype:"li",class:"",itemTemplate)
\whitespace trim
<span class="tc-links-draggable-list">
<$vars targetTiddler="""$tiddler$""" targetField="""$field$""">
<$genesis $type=<<__type__>> class="$class$">
<$list filter="[list[$tiddler$!!$field$]]" emptyMessage=<<__emptyMessage__>>>
<$droppable actions=<<list-links-draggable-drop-actions>> tag="""$subtype$""" enable=<<tv-enable-drag-and-drop>>>
<div class="tc-droppable-placeholder"/>
<div>
<$transclude tiddler="""$itemTemplate$""">
<$link to={{!!title}}>
<$let tv-wikilinks="no">
<$transclude field="caption">
<$view field="title"/>
</$transclude>
</$let>
</$link>
</$transclude>
</div>
</$droppable>
</$list>
<$tiddler tiddler="">
<$droppable actions=<<list-links-draggable-drop-actions>> tag="div" enable=<<tv-enable-drag-and-drop>>>
<div class="tc-droppable-placeholder">
{{$:/core/images/blank}}
</div>
<div style="height:0.5em;"/>
</$droppable>
</$tiddler>
</$genesis>
</$vars>
<$vars targetTiddler="""$tiddler$""" targetField="""$field$""">
<$genesis $type=<<__type__>> class="$class$">
<$list filter="[list[$tiddler$!!$field$]]" emptyMessage=<<__emptyMessage__>>>
<$droppable
actions=<<list-links-draggable-drop-actions>>
tag="""$subtype$"""
enable=<<tv-enable-drag-and-drop>>
>
<div class="tc-droppable-placeholder"/>
<div>
<$transclude tiddler="""$itemTemplate$""">
<$link to={{!!title}}>
<$let tv-wikilinks="no">
<$transclude field="caption">
<$view field="title"/>
</$transclude>
</$let>
</$link>
</$transclude>
</div>
</$droppable>
</$list>
<$tiddler tiddler="">
<$droppable
actions=<<list-links-draggable-drop-actions>>
tag="div"
enable=<<tv-enable-drag-and-drop>>
>
<div class="tc-droppable-placeholder">
{{$:/core/images/blank}}
</div>
<div style="height:0.5em;"/>
</$droppable>
</$tiddler>
</$genesis>
</$vars>
</span>
\end
@ -60,50 +68,59 @@ tags: $:/tags/Macro
\whitespace trim
<!-- Save the current ordering of the tiddlers with this tag -->
<$set name="order" filter="[<__tag__>tagging[]]">
<!-- Remove any list-after or list-before fields from the tiddlers with this tag -->
<$list filter="[<__tag__>tagging[]]">
<$action-deletefield $field="list-before"/>
<$action-deletefield $field="list-after"/>
</$list>
<!-- Save the new order to the Tag Tiddler -->
<$action-listops $tiddler=<<__tag__>> $field="list" $filter="+[enlist<order>] +[insertbefore<actionTiddler>,<currentTiddler>]"/>
<!-- Make sure the newly added item has the right tag -->
<!-- Removing this line makes dragging tags within the dropdown work as intended -->
<!--<$action-listops $tiddler=<<actionTiddler>> $tags=<<__tag__>>/>-->
<!-- Using the following 5 lines as replacement makes dragging titles from outside into the dropdown apply the tag -->
<$list filter="[<actionTiddler>!contains:tags<__tag__>]">
<$fieldmangler tiddler=<<actionTiddler>>>
<$action-sendmessage $message="tm-add-tag" $param=<<__tag__>>/>
</$fieldmangler>
</$list>
<!-- Remove any list-after or list-before fields from the tiddlers with this tag -->
<$list filter="[<__tag__>tagging[]]">
<$action-deletefield $field="list-before"/>
<$action-deletefield $field="list-after"/>
</$list>
<!-- Save the new order to the Tag Tiddler -->
<$action-listops $tiddler=<<__tag__>> $field="list" $filter="+[enlist<order>] +[insertbefore<actionTiddler>,<currentTiddler>]"/>
<!-- Make sure the newly added item has the right tag -->
<!-- Removing this line makes dragging tags within the dropdown work as intended -->
<!--<$action-listops $tiddler=<<actionTiddler>> $tags=<<__tag__>>/>-->
<!-- Using the following 5 lines as replacement makes dragging titles from outside into the dropdown apply the tag -->
<$list filter="[<actionTiddler>!contains:tags<__tag__>]">
<$fieldmangler tiddler=<<actionTiddler>>>
<$action-sendmessage $message="tm-add-tag" $param=<<__tag__>>/>
</$fieldmangler>
</$list>
</$set>
\end
\define list-tagged-draggable(tag,subFilter,emptyMessage,itemTemplate,elementTag:"div",storyview:"")
\whitespace trim
<span class="tc-tagged-draggable-list">
<$set name="tag" value=<<__tag__>>>
<$list filter="[<__tag__>tagging[]$subFilter$]" emptyMessage=<<__emptyMessage__>> storyview=<<__storyview__>>>
<$genesis $type=<<__elementTag__>> class="tc-menu-list-item">
<$droppable actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" enable=<<tv-enable-drag-and-drop>>>
<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/>
<$genesis $type=<<__elementTag__>>>
<$transclude tiddler="""$itemTemplate$""">
<$link to={{!!title}}>
<$view field="title"/>
</$link>
</$transclude>
</$genesis>
</$droppable>
</$genesis>
</$list>
<$tiddler tiddler="">
<$droppable actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>""" enable=<<tv-enable-drag-and-drop>>>
<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/>
<$genesis $type=<<__elementTag__>> style="height:0.5em;">
</$genesis>
</$droppable>
</$tiddler>
</$set>
<$set name="tag" value=<<__tag__>>>
<$list
filter="[<__tag__>tagging[]$subFilter$]"
emptyMessage=<<__emptyMessage__>>
storyview=<<__storyview__>>
>
<$genesis $type=<<__elementTag__>> class="tc-menu-list-item">
<$droppable
actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>"""
enable=<<tv-enable-drag-and-drop>>
>
<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/>
<$genesis $type=<<__elementTag__>>>
<$transclude tiddler="""$itemTemplate$""">
<$link to={{!!title}}>
<$view field="title"/>
</$link>
</$transclude>
</$genesis>
</$droppable>
</$genesis>
</$list>
<$tiddler tiddler="">
<$droppable
actions="""<$macrocall $name="list-tagged-draggable-drop-actions" tag=<<__tag__>>/>"""
enable=<<tv-enable-drag-and-drop>>
>
<$genesis $type=<<__elementTag__>> class="tc-droppable-placeholder"/>
<$genesis $type=<<__elementTag__>> style="height:0.5em;"/>
</$droppable>
</$tiddler>
</$set>
</span>
\end

View File

@ -0,0 +1,3 @@
title: $:/my-scroll-position
scroll-left: 0
scroll-top: 100

View File

@ -0,0 +1,20 @@
title: Transclude/CustomWidget/CodeblockOverride-TextParser
description: Test that overriding codeblock widget does not impact text parser
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
\widget $codeblock(code)
<$transclude $variable="copy-to-clipboard" src=<<code>>/>
<$genesis $type="$codeblock" $remappable="no" code=<<code>>/>
\end
\procedure myvariable() hello
<$transclude $variable="myvariable" $type="text/plain" $output="text/plain"/>
+
title: ExpectedResult
<p>hello</p>

View File

@ -53,6 +53,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-5]]")).toEqual(["five"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-4]]")).toEqual(["six"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-3]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-2]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-1]]")).toEqual(["null"]);
});
it("should support the jsonextract operator", function() {
@ -70,6 +75,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-5]]")).toEqual(['"five"']);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-4]]")).toEqual(['"six"']);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-3]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-2]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-1]]")).toEqual(["null"]);
});
it("should support the jsonindexes operator", function() {
@ -85,6 +95,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[2]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[3]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[4]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-5]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-4]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-3]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-2]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-1]]")).toEqual([]);
});
it("should support the jsontype operator", function() {
@ -101,6 +116,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-5]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-4]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-1]]")).toEqual(["null"]);
});
it("should support the jsonset operator", function() {
@ -115,6 +135,7 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonset:null[id]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":null}']);
expect(wiki.filterTiddlers("[{First}jsonset:array[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,[]]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:object[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,{}]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:number[d],[f],[-1],[42]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,42]}}']);
expect(wiki.filterTiddlers("[{First}jsonset[missing],[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:json[\"Antelope\"]]")).toEqual(['"Antelope"']);
expect(wiki.filterTiddlers("[{First}jsonset:json[id],[{\"a\":313}]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":{"a":313}}']);

View File

@ -40,6 +40,22 @@ There are technical restrictions on the legal characters in an URL fragment. To
Both the target tiddler title and the story filter should be URL encoded (but not the separating colon). TiddlyWiki generates properly encoded URLs which can look quite ugly. However, in practice browsers will usually perfectly happily process arbitrary characters in URL fragments. Thus when creating permalinks manually you can choose to ignore URL encoding.
!! Simpler URLS
<<.from-version "5.3.2">> The URLs generated are simplified from the hard-to-read percent encoding when feasible. Spaces are replaced with underscores (`_`), many punctuation characters are allowed to remain unencoded, and permaview filters receive a simpler encoding. For example the tiddler "Hard Linebreaks with CSS - Example", which percent-encoded would look like
> @@font-family:monospace;#Hard%20Linebreaks%20with%20CSS%20-%20Example@@
instead looks like
> @@font-family:monospace;#Hard_Linebreaks_with_CSS_-_Example@@
Existing story filter URLs like
> @@font-family:monospace;#:[tag[Features]]%20+[limit[5]]@@
will continue to work.
! Permalink Behaviour
Two important aspects of TiddlyWiki's behaviour with permalinks can be controlled via options in the [[control panel|$:/ControlPanel]] <<.icon $:/core/images/options-button>> ''Settings'' tab:

View File

@ -53,6 +53,14 @@ The <<.op jsonextract>> operator uses multiple operands to specify the indexes o
[<jsondata>jsonextract[d],[g]] --> {"x":"max","y":"may","z":"maize"}
```
<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:
```
[<jsondata>jsonextract[d],[f],[-1]] --> null
[<jsondata>jsonextract[d],[f],[-2]] --> false
[<jsondata>jsonextract[d],[f],[-4]] --> "six"
```
Indexes can be dynamically composed from variables and transclusions:
```

View File

@ -51,6 +51,14 @@ The <<.op jsonget>> operator uses multiple operands to specify the indexes of th
[<jsondata>jsonget[d],[f],[0]] --> "five"
```
<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:
```
[<jsondata>jsonget[d],[f],[-1]] --> null
[<jsondata>jsonget[d],[f],[-2]] --> false
[<jsondata>jsonget[d],[f],[-4]] --> "six"
```
Indexes can be dynamically composed from variables and transclusions:
```

View File

@ -51,6 +51,14 @@ The <<.op jsonset>> operator uses multiple operands to specify the indexes of th
[<jsondata>jsonset[d],[f],[Panther]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": "Panther","g": {"x": "max","y": "may","z": "maize"}}"}
```
Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:
```
[<jsondata>jsonset[d],[f],[-1],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,false,"Elephant"],"g": {"x": "max","y": "may","z": "maize"}}"}
[<jsondata>jsonset[d],[f],[-2],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,"Elephant",null],"g": {"x": "max","y": "may","z": "maize"}}"}
[<jsondata>jsonset[d],[f],[-4],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","Elephant",true,false,null],"g": {"x": "max","y": "may","z": "maize"}}"}
```
Indexes can be dynamically composed from variables and transclusions:
```

View File

@ -61,6 +61,14 @@ The <<.op jsontype>> operator uses multiple operands to specify the indexes of t
[<jsondata>jsontype[d],[f],[2]] --> "boolean"
```
<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:
```
[<jsondata>jsontype[d],[f],[-1]] --> "null"
[<jsondata>jsontype[d],[f],[-2]] --> "boolean"
[<jsondata>jsontype[d],[f],[-4]] --> "string"
```
Indexes can be dynamically composed from variables and transclusions:
```

View File

@ -1,6 +1,6 @@
caption: image
created: 20140416160234142
modified: 20220721102303815
modified: 20231121114351165
tags: Widgets
title: ImageWidget
type: text/vnd.tiddlywiki
@ -21,6 +21,7 @@ Any content of the `<$image>` widget is ignored.
|alt |The alternative text to be associated with the image |
|class |CSS classes to be assigned to the `<img>` element |
|loading|<<.from-version "5.2.3">>Optional. Set to `lazy` to enable lazy loading of images loaded from an external URI |
|usemap|<<.from-version "5.3.2">>Optional usemap attribute to be assigned to the `<img>` element for use with HTML image maps |
The width and the height can be specified as pixel values (eg "23" or "23px") or percentages (eg "23%"). They are both optional; if not provided the browser will use CSS rules to size the image.

View File

@ -1,6 +1,6 @@
caption: scrollable
created: 20140324223413403
modified: 20220620115347910
modified: 20230731100903977
tags: Widgets
title: ScrollableWidget
type: text/vnd.tiddlywiki
@ -16,12 +16,15 @@ The content of the `<$scrollable>` widget is displayed within a pair of wrapper
|!Attribute |!Description |
|class |The CSS class(es) to be applied to the outer DIV |
|fallthrough |See below |
|bind |<<.from-version "5.3.2">> Optional title of tiddler to which the scroll position should be bound |
Binding the scroll position to a tiddler automatically copies the scroll coordinates into the `scroll-left` and `scroll-top` fields as scrolling occurs. Conversely, setting those field values will automatically cause the scrollable to scroll if it can.
<$macrocall $name=".note" _="""If a scrollable widget can't handle the `tm-scroll` message because the inner DIV fits within the outer DIV, then by default the message falls through to the parent widget. Setting the ''fallthrough'' attribute to `no` prevents this behaviour."""/>
! Examples
This example requires the following CSS definitions from [[$:/_tw5.com-styles]]:
These examples require the following CSS definitions from [[$:/_tw5.com-styles]]:
```
.tc-scrollable-demo {
@ -33,6 +36,8 @@ This example requires the following CSS definitions from [[$:/_tw5.com-styles]]:
}
```
!! Simple Usage
This wiki text shows how to display a list within the scrollable widget:
<<wikitext-example-without-html "<$scrollable class='tc-scrollable-demo'>
@ -46,3 +51,23 @@ This wiki text shows how to display a list within the scrollable widget:
</$scrollable>
">>
!! Binding scroll position to a tiddler
[[Current scroll position|$:/my-scroll-position]]: {{$:/my-scroll-position!!scroll-left}}, {{$:/my-scroll-position!!scroll-top}}
<$button>
<$action-setfield $tiddler="$:/my-scroll-position" scroll-left="100" scroll-top="100"/>
Set current scroll position to 100,100
</$button>
<<wikitext-example-without-html "<$scrollable class='tc-scrollable-demo' bind='$:/my-scroll-position'>
<$list filter='[tag[Reference]]'>
<$view field='title'/>: <$list filter='[all[current]links[]sort[title]]' storyview='pop'>
<$link><$view field='title'/></$link>
</$list>
</$list>
</$scrollable>
">>

View File

@ -55,7 +55,12 @@
"--render","$:/core/templates/static.template.css","static/static.css","text/plain"],
"external-js": [
"--render","$:/core/save/offline-external-js","[[external-]addsuffix<version>addsuffix[.html]]","text/plain",
"--render","$:/core/templates/tiddlywiki5.js","[[tiddlywikicore-]addsuffix<version>addsuffix[.js]]","text/plain"]
"--render","$:/core/templates/tiddlywiki5.js","[[tiddlywikicore-]addsuffix<version>addsuffix[.js]]","text/plain"],
"archive":[
"--render","$:/core/save/all","[[archive/TiddlyWiki-]addsuffix<version>addsuffix[.html]]","text/plain",
"--render","$:/editions/tw5.com/download-empty","[[archive/Empty-TiddlyWiki-]addsuffix<version>addsuffix[.html]]","text/plain",
"--render","[[TiddlyWiki Archive]]","archive/index.html","text/plain","$:/core/templates/static.tiddler.html",
"--render","$:/core/templates/static.template.css","archive/static.css","text/plain"]
},
"config": {
"retain-original-tiddler-path": true