mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-05-18 03:12:19 +00:00
56ea0789e1
* Fix RSOD when $tw.utils.addClass receives a class string with whitespace
PR #9251 replaced the manual setAttribute("class", ...) implementation of
$tw.utils.addClass/removeClass/toggleClass with direct Element.classList
calls. Unlike setAttribute, classList.add/remove/toggle throws
InvalidCharacterError on any token containing whitespace, so callers that
pass a whole class string (e.g. modal.js passing tiddler.fields.class)
now crash.
Manual repro on tw5-com: open SampleWizard, set the `class` field to
"aaa bbb", Done, open popup -> OK -> open nested popup -> RSOD.
Fix: split the className argument on whitespace in deprecated.js and feed
individual tokens to classList. A small splitClasses() helper keeps the
three functions symmetrical.
Adds adversarial regression tests in test-utils.js covering:
- ASCII whitespace variants (space, tab, CR, LF, mixed runs, padding)
- Unicode whitespace (U+00A0 non-breaking space)
- de-duplication across single and multiple calls
- remove/toggle no-op on missing tokens
- toggle with status undefined / true / false
- silent no-op for whitespace-only / empty / non-string / null input
- silent no-op when the element has no classList
* Move new tests to their own file
* Add backwards-compat regression tests for deprecated.js
Locks in pre-5.4.0 tolerant behaviour of $:/core/modules/utils/
deprecated.js helpers that regressed in PR #9251. Each spec targets an
edge-case input the current one-line modern equivalents reject:
- repeat: negative count / null / undefined str
- startsWith / endsWith: RegExp search arg
- stringifyNumber: null / undefined
- domContains: boolean return, self-check
- hasClass: null element, classless element
- getLocationPath: query preservation, hash stripping
(browser-only; pends in Node because the TW5 sandbox has no `window`)
Also picks up the addClass/removeClass/toggleClass whitespace specs
moved out of test-utils.js by the previous commit, so all deprecated.js
coverage lives together.
Fails 8 specs on current HEAD; the follow-up deprecated.js restoration
commit turns them green.
* Restore pre-5.4.0 behaviour of deprecated.js utilities
PR #9251 replaced several helpers in $:/core/modules/utils/deprecated.js
with one-line ES2017 equivalents that diverge from the originals on
edge-case inputs. Follow-up PRs fixed the most visible cases
(getLocationHash #9622, isDate #9771, addClass empty-string #9561 and
whitespace 005e17537); this commit closes the rest:
- repeat: manual loop tolerates negative count / null / undefined str
- startsWith / endsWith: substring compare tolerates RegExp search arg
- stringifyNumber: `num + ""` coercion tolerates null / undefined
- domContains: `a !== b && a.contains(b)` returns boolean, handles self
- hasClass: null-element guard, strict-false return
- getLocationPath: `toString().split("#")[0]` preserves the query
string in permalinks (startup/story.js:214, 217) -- the most visible
user-facing regression, causing ?lang=de etc. to silently drop.
IE-only fallbacks in Math.sign, strEndsWith, domContains, and
domMatchesSelector are removed; TW5 no longer supports IE.
Covered by the regression specs added in the previous commit.
* make comments more refined
108 lines
2.8 KiB
JavaScript
108 lines
2.8 KiB
JavaScript
/*\
|
|
title: $:/core/modules/utils/deprecated.js
|
|
type: application/javascript
|
|
module-type: utils
|
|
|
|
Deprecated util functions. These preserve the pre-5.4.0 signatures and
|
|
behaviour for backwards compatibility with plugins and external scripts.
|
|
Prefer modern alternatives in new code (Array.prototype methods, classList,
|
|
Math.sign, String.prototype.repeat, etc.).
|
|
|
|
\*/
|
|
|
|
exports.logTable = (data) => console.table(data);
|
|
|
|
/*
|
|
Repeats a string
|
|
*/
|
|
exports.repeat = function(str,count) {
|
|
var result = "";
|
|
for(var t=0;t<count;t++) {
|
|
result += str;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
/*
|
|
Check if a string starts with another string
|
|
*/
|
|
exports.startsWith = function(str,search) {
|
|
return str.substring(0, search.length) === search;
|
|
};
|
|
|
|
/*
|
|
Check if a string ends with another string
|
|
*/
|
|
exports.endsWith = function(str,search) {
|
|
return str.substring(str.length - search.length) === search;
|
|
};
|
|
|
|
/*
|
|
Trim whitespace from the start and end of a string
|
|
*/
|
|
exports.trim = function(str) {
|
|
if(typeof str === "string") {
|
|
return str.trim();
|
|
} else {
|
|
return str;
|
|
}
|
|
};
|
|
|
|
exports.hopArray = (object,array) => array.some((element) => $tw.utils.hop(object,element));
|
|
|
|
exports.sign = Math.sign;
|
|
|
|
exports.strEndsWith = (str,ending,position) => str.endsWith(ending,position);
|
|
|
|
exports.stringifyNumber = function(num) {
|
|
return num + "";
|
|
};
|
|
|
|
// Returns the fully escaped CSS selector for a tag, e.g.
|
|
// "$:/tags/Stylesheet" -> "tc-tagged-\%24\%3A\%2Ftags\%2FStylesheet"
|
|
exports.tagToCssSelector = function(tagName) {
|
|
return "tc-tagged-" + encodeURIComponent(tagName).replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^`{\|}~,]/mg,function(c) {
|
|
return "\\" + c;
|
|
});
|
|
};
|
|
|
|
/*
|
|
Determines whether element 'a' contains element 'b'.
|
|
Returns false when a === b (matches the original John Resig semantics).
|
|
*/
|
|
exports.domContains = function(a,b) {
|
|
return a !== b && a.contains(b);
|
|
};
|
|
|
|
exports.domMatchesSelector = (node,selector) => node.matches(selector);
|
|
|
|
exports.hasClass = function(el,className) {
|
|
return !!(el && el.classList && el.classList.contains(className));
|
|
};
|
|
|
|
// addClass/removeClass/toggleClass split on whitespace to preserve the
|
|
// original setAttribute("class", ...) acceptance of "foo bar" as two
|
|
// classes. Regressed in #9251.
|
|
function splitClasses(className) {
|
|
return (typeof className === "string" && className.match(/\S+/g)) || [];
|
|
}
|
|
|
|
exports.addClass = function(el,className) {
|
|
if(!el.classList) return;
|
|
splitClasses(className).forEach(function(c) { el.classList.add(c); });
|
|
};
|
|
|
|
exports.removeClass = function(el,className) {
|
|
if(!el.classList) return;
|
|
splitClasses(className).forEach(function(c) { el.classList.remove(c); });
|
|
};
|
|
|
|
exports.toggleClass = function(el,className,status) {
|
|
if(!el.classList) return;
|
|
splitClasses(className).forEach(function(c) { el.classList.toggle(c,status); });
|
|
};
|
|
|
|
exports.getLocationPath = function() {
|
|
return window.location.toString().split("#")[0];
|
|
};
|