Compare commits

...
Author SHA1 Message Date
Théophile DesmedtandGitHub 8c599e64c0 Doc fixes following PR #10019 (#10022)
* add examples using the new templates $:/core/templates/plain-text and $:/core/templates/rendered-text

* Fix broken links, use documentation macro and correct the system tag given in the example

* fix version number

* fix version number

* use the tid documentation macro
2026-09-11 16:52:01 +01:00
Jeremy Ruston 2b60da0dcf Updated Catalan translation 2026-09-10 14:00:07 +01:00
Jeremy Ruston 5d3f2685a3 Merge branch 'tiddlywiki-com' 2026-09-08 11:40:21 +01:00
Théophile DesmedtandGitHub 3aec096a07 Add $:/core/templates/plain-text and $:/core/templates/rendered-text (#10017)
* Add $:/core/templates/plain-text core template

This template renders a single `text` variable as plain text using the
<$text> widget. It enables exporting and downloading dynamic strings
via messages like `tm-download-file` using the `variables` paramObject,
eliminating the need to create temporary tiddlers in the wiki store
purely for rendering purposes.

* add release note

* add rendered text template

* update change note
2026-09-08 09:38:27 +01:00
Théophile DesmedtandGitHub ddc59ee77b add examples using the new templates $:/core/templates/plain-text and $:/core/templates/rendered-text (#10019) 2026-09-08 09:38:05 +01:00
Zaid AlmomaniandGitHub e0e6ab69b1 Fix LinkWidget href fallback when tv-filter-export-link returns empty (#10009) 2026-09-08 09:31:24 +01:00
Théophile DesmedtandGitHub 47806feb85 Fix plugin library iframes hanging if onload never fires by adding readiness handshake (#10003)
* Fix race condition in plugin library iframe message handling

Introduce a PLUGIN-LIBRARY-READY handshake verb for plugin library iframes. 
This ensures that postMessage commands are not prematurely dropped before 
the iframe's internal script has fully bound its message listeners. 

Retains iframe.onload as a backward-compatible fallback for legacy or 
external libraries that do not emit the ready signal.

* add changenote

* feat(plugin-library): add PLUGIN-LIBRARY-READY handshake to plugin library iframe

    Add PLUGIN-LIBRARY-READY message emission in libraryserver.js

    Signals to parent window when iframe is ready to receive messages

    Complements browser-messaging.js PLUGIN-LIBRARY-READY handler

    Includes comprehensive tests for callback queueing and handshake flow

    Fixes race condition in plugin library iframe message handling

* fix(plugin-library): emit PLUGIN-LIBRARY-READY message on iframe initialization

Emit the PLUGIN-LIBRARY-READY message after the message listener is bound,

signaling to the parent window that the iframe is ready to receive postMessage

commands. This completes the handshake implementation to eliminate the race

condition where messages could be lost before the iframe's script initialization

completes.

* fix indentation

* remove extra space

* remove typo

* refactor(test-browser-messaging): update test descriptions and remove unused code

* lint fix

* fix: correct the update note
2026-09-08 09:28:15 +01:00
Mario PietschandGitHub 5577a3d07b Allow colons in variable names in call syntax (#10015)
Accept a colon in the variable name of a `<<...>>` call and of an
`((...))` attribute reference, restoring the v5.3.8 name character
class. Call Syntax already describes the callee name as any
non-whitespace character other than `(` or `>`, and the colon is only
meaningful at the parameter name position.

* Add test cases for a colon name standalone, as an attribute value, as
  an =-separated parameter value, and with named parameters
* Add the 5.5.0 change note
2026-09-08 09:12:02 +01:00
Théophile DesmedtandGitHub d62b75cceb Fix #10004 iframe DOM leak in unloadIFrame loop (#10018)
* Fix unloadIFrame loop condition for proper iframe unloading

* add changenote for #10018

* add missing field to changenote
2026-09-08 12:11:06 +04:00
30a51be168 Bump @humanfs/node from 0.16.7 to 0.16.8 (#10008)
Bumps [@humanfs/node](https://github.com/humanwhocodes/humanfs/tree/HEAD/packages/node) from 0.16.7 to 0.16.8.
- [Release notes](https://github.com/humanwhocodes/humanfs/releases)
- [Changelog](https://github.com/humanwhocodes/humanfs/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/humanwhocodes/humanfs/commits/node-v0.16.8/packages/node)

---
updated-dependencies:
- dependency-name: "@humanfs/node"
  dependency-version: 0.16.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-07 15:00:46 +04:00
Jeremy Ruston 38fa4486e4 Merge branch 'tiddlywiki-com' 2026-09-05 14:58:11 +01:00
Jeremy Ruston 52c60be3a0 Merge branch 'tiddlywiki-com' 2026-08-25 17:24:38 +01:00
Jeremy Ruston 516135e1b2 Merge branch 'master' of https://github.com/TiddlyWiki/TiddlyWiki5 2026-08-24 22:13:04 +01:00
Jeremy Ruston 6845f1c47e Merge branch 'tiddlywiki-com' 2026-08-24 22:12:30 +01:00
Simon HuberandGitHub 5e5937838b Fix: #9879 change release note for version 5.5.0 (#9968) 2026-08-14 12:56:09 +02:00
Simon HuberandGitHub 6c8ac9b6a7 Refactor class attributes for edit field wrapper (#9879)
* Refactor class attributes for edit field wrapper

The problem here is, that if we have an existing field with name - say - "field1" and we want to add a field with name "field12345", at the moment we insert "field1" the edit-text widget re-renders because its classes change. The `tc-edit-field-exists` class must go outside to the container div.

* Add changenote for bugfix in field-name inputs

Fixes the field-name input re-rendering issue that caused loss of focus when inserting existing field-names.
2026-08-14 08:31:20 +02:00
Simon HuberandGitHub 0c60e1fa7a Add class assignment to DOM node in editors (#9965)
* Add class assignment to DOM node in editor

* Fix indentation in assignDomNodeClasses method

* Fix conditional check for changedAttributes class

* Improve class assignment in EditTextWidget

Refactor class assignment logic to avoid duplicates and handle whitespace.

* Refactor DOM class assignment in EditTextWidget

* Refactor updateDomNodeClasses for clarity and efficiency

* Add changenote for enhancement in editor refresh behavior
2026-08-13 12:21:20 +02:00
Mario PietschandGitHub b40b6de3ae Make sort order using sortby more consistent (#9747)
* make sort order more consistent

* Fix tests, as they are to be different due to the change.

* Add ChangeNote

* sortby: add "end" operand to place unlisted last

Give the sortby filter operator an optional second operand `end` that
places input titles missing from the reference list after the listed
ones, while the default still keeps them at the start.

* x-listops: read a second operand; `end` sorts unlisted titles to the
  end of the list, the default (no operand) leaves them at the start
* Document the optional `place` parameter on the sortby operator and
  rename the operator and examples tiddlers to match their titles
* Add a `sortby[...],[end]` example to the operator examples
* Replace the 5.5.0 change note with one describing the new `end`
  operand and its unchanged default
* Extend the sortby tests to cover both default (unlisted first) and
  `end` (unlisted last) placement

* Make the sortby end flag a suffix

Specify the placement of unlisted titles with an operator suffix.
Eg: `sortby:end[a b c]`

* Read the flag from `operator.suffix`
* Move the example and the change note to the suffix syntax
* Switch the sortby tests to the suffix syntax

* Cover the sortby guard and unlisted ordering

Close gaps found by mutating the sortby operator

* Sort two titles to catch an off-by-one in the length guard
* Keep unlisted titles in input order under the `end` suffix to catch a
  comparator tie break
* Return a single title untouched, exercising the early return
2026-08-13 11:18:38 +01:00
aac08a6252 Bump js-yaml from 4.3.0 to 4.3.1 (#9966)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-12 15:19:57 +02:00
Simon HuberandGitHub 40b353bd0e Touch a temp tiddler when removing a tag (#9889)
* Touch a temp tiddler when removing a tag

Like that removing a tag doesn't make TiddlyWiki5 wait for the Typing timeout to remove it but removes it instantly, because also a non-draft Tiddler has changed

* Add release notes for bugfix in Edit Mode tags

Fixes an issue where tags of Tiddlers in Edit Mode don't animate instantly when they get removed.

* Add inline comment

* Update release version to 5.5.0 in changenote
2026-08-11 10:50:55 +02:00
Jeremy Ruston 8e65fdc06a Merge branch 'tiddlywiki-com' 2026-08-06 10:50:33 +01:00
Simon HuberandGitHub 2f578589a0 Fix typo in story width variable name (#9958)
* Fix typo in story width variable name

* Add change note for release 5.5.0

Fixes a typo in the CSS custom properties
2026-08-06 11:20:13 +02:00
916df90cc9 Fix $tw.utils.pulseElement() (#9741)
* Fix $tw.utils.pulseElement() by replacing missing $tw.browser.animationEnd

Use $tw.utils.convertEventName("animationEnd") and add pulse CSS animation

Fixes #6835

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add ChangeNote

* add tc- prefix to pulse class

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-07-30 06:15:38 +02:00
ea18c85a4c Bump brace-expansion from 1.1.12 to 1.1.16 (#9953)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.12 to 1.1.16.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v1.1.12...v1.1.16)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.16
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 21:53:27 +02:00
Mario PietschandGitHub abf134afe4 Fix regression introduced by PR #9815 (#9944)
* Keep retained tiddler subdirectories on save

Make generateTiddlerFilepath keep a retained tiddler's recorded subdirectory
and produce a valid filename for every path segment.

* Normalise originalpath separators to "/" before the sanitiser, so recorded
  subdirectories survive
* Validate each path segment (reserved names, trailing dots or spaces), so
  "sub/CON" is valid on every OS
* Run that pass after transliteration, which can itself create a reserved name
* Add filesystem and adversarial specs, covering generateTiddlerFileInfo and
  generateTiddlerExtension

* update changenote
2026-07-28 11:34:09 +01:00
Saq ImtiazandGitHub 45d2ca948a Optimizes filterrunprefixes & filters using ES2017 (#9943)
* refactor: use ES2017 to optimize filter run prefixes

* chore: eslint

* chore: eslint

* fix: added missing file

* refactor: prefix, suffix and subfilter operators

* refactor: further improvements using new String methods

* docs: added changenote

* chore: renamed docs tiddler

* chore: renamed docs tiddler
2026-07-28 12:32:48 +02:00
Mario PietschandGitHub 94b046b8cb Fix button widget selection state with setTitle (#9951)
Make the button widget apply selectedClass correctly when the state is
addressed with setTitle instead of set.

Issue #9949, the button could not be rendered at all

* Guard the setField read in isSelected(), which dereferenced the result of
  getTiddler() directly. A missing state tiddler threw a TypeError, and that
  tiddler is normally only created by the first click

Issue #9950, every button of a set reported itself as selected

* Rewrite isSelected() as an if/else. The nested ternary mixed `||` with
  `?:`, so the setTitle branch returned a fallback value instead of a
  comparison
* Watch the setTitle state tiddler in refresh(), so the selected class
  follows the state as it already does for set

Documentation and tests

* Document that setField takes preference over setIndex. That has always
  been the behaviour, it was never written down
* Document that selectedClass and default apply to setTitle as well as to set
* Add wiki based test cases for the set, setTitle, setField and setIndex
  selection paths. Six of the seven fail against v5.4.1, the one that passes
  uses set, which was never broken

Fixes #9949
Fixes #9950
2026-07-28 11:19:12 +01:00
Saq ImtiazandGitHub c710a56b21 Optimizes x-listops using ES2017 (#9942)
* refactor: use ES2017 to optimize x-listops

* chore: eslint

* refactor: optimize listops.js using ES2017

* refactor: prefix operator

* docs: added changenote

* feat: added tests
2026-07-28 11:12:35 +01:00
saqimtiaz e681096127 fix: remove duplicate widget evaluation for multi-valued variables 2026-07-24 21:04:54 +02:00
24b433211d feat: add JSON output support to makepatches operator (#9940)
* feat: enhance makepatches function to support multiple suffixes and JSON output format

* feat: add release notes for makepatches JSON output enhancement

* doc: update makepatches operator documentation to document new json output

* doc: add JSON output examples to makepatches and applypatches documentation

* chore: convert space indentations to tabs

* test: add tests for makepatches operator with JSON output support

* chore: change space to tab indentation

* test: add DiffMergePatch4 integration test for testing makepatches json output

* fix: remove superfluous heading character

* refactor the parsing of the makepatches suffixes

Co-authored-by: Saq Imtiaz <saq.imtiaz@gmail.com>

* refactor: improve makepatches function for better readability and performance

* chore: apply style guide by collapsing consecutive variable assignments

* chore: remove extraneous tabs

---------

Co-authored-by: Saq Imtiaz <saq.imtiaz@gmail.com>
2026-07-24 16:17:34 +02:00
abf8ffdbca Fix 9814 file system paths "pragma: \ define" tiddler (#9815)
* New test for all "forbidden" directory characters

* Fix for 9814 Pragma: \define creates dir problem

* improve handling of tiddlers in root/community directory

* Add ChangeNote

* Apply suggestion from @saqimtiaz

* Apply suggestion from @saqimtiaz

---------

Co-authored-by: Saq Imtiaz <saq.imtiaz@gmail.com>
2026-07-22 13:45:25 +02:00
Mario PietschandGitHub 97e27d0dad Fix regex for code block end detection in wikiparser (#9739)
* Fix regex for code block end detection in wikiparser

* Anchor the codeblock end fence to its own line

Close an empty code block without consuming the tiddler, while a fence
only counts when it stands alone on its line.

* Match the end fence with (^|\r?\n) so the empty block case closes at
  the line start and a code line ending in ``` stays inside the block
* Add test-codeblock-parser.js covering empty blocks with language and
  CRLF, delimiter newline handling, mid-line fences and a missing fence
2026-07-22 13:12:55 +02:00
Jeremy Ruston 855dc2fbb9 Merge branch 'master' of https://github.com/TiddlyWiki/TiddlyWiki5 2026-07-21 17:24:39 +01:00
lin onetwoandGitHub e1e34f4907 Move background action console.log inside platforms check (#9891)
* Move background action log inside platforms check

* Add change note for #9891

* Shorten change note

* Release notes: move #9891 from 5.4.1 to 5.5.0

PR #9891 is still open, so the background-action log change is planned for 5.5.0 instead of 5.4.1.

* Fix change-category for #9891 release note

 is not a valid category; use  instead.
2026-07-21 18:16:33 +02:00
Jeremy Ruston 7196b9ee9b Merge branch 'tiddlywiki-com' 2026-07-21 12:11:44 +01:00
Jeremy Ruston ed71c8262a Merge branch 'tiddlywiki-com' 2026-07-18 11:36:50 +01:00
7516d96eed Update docs: running a custom Node.js TiddlyWiki version with npx (#9886)
* Update docs: running a custom Node.js TiddlyWiki version with npx

* Update docs: document different options of running multiple TW versions

---------

Co-authored-by: shadow <shadow@darkstar.localdomain>
Co-authored-by: me <me@localhost>
2026-07-18 10:31:42 +01:00
4bf7e9d192 [Bug] Fix the CSV parser blanking a table (#9919)
* Fix the CSV parser blanking a table

getCellInfo read the loop variable i before its declaration, so hoisting made
it undefined and charAt(undefined) read the first character of the whole text.
Any CSV opening with a separator therefore returned every cell empty.

- Test the cell against the start of the cell rather than the hoisted i
- Keep a separator inside a quoted cell literal
- Cover the parser with tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add a change note for the CSV fix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:26:59 +02:00
Bram ChenandGitHub 04cd3081e4 Update chinese language files (#9933)
* Update chinese language files

* Add WikiInformation/Generate/Caption in ControlPanel.multids

* Add ChangeNote
2026-07-14 15:50:37 +01:00
saqimtiaz d27716c1e0 chore: fixed erroneous changenote number 2026-07-14 13:17:58 +02:00
Mario PietschandGitHub 7f7f36d986 Skip system tiddlers in back-indexer updates (#9917)
* Skip system tiddlers in back-indexer updates

Fix #9916: editing a system tiddler that links or transcludes another
tiddler made it a backlinks[]/backtranscludes[] source until the next
index rebuild (shipped since v5.3.4).

* Guard BackSubIndexer.update() with isSystemTiddler() on both sides,
  matching the initial scan; this also skips parsing $:/StoryList on
  every navigation
* Add test-back-indexer.js: regression guards plus adversarial probes
  for shadow reveals and hostile __proto__ titles

* Add ChangeNote
2026-07-14 13:10:21 +02:00
Mario PietschandGitHub 86a4e18134 diff-text change newline to ↲ (#9736)
* diff-text change newline to ↲

* Add ChangeNote
2026-07-14 13:04:50 +02:00
Mario PietschandGitHub 304858c7c5 Make Click to generate wiki info button translatable (#9737)
* Make Click to generate wiki info button translatable

* Add ChangeNote
2026-07-14 12:53:23 +02:00
Mario PietschandGitHub 7ececf9e0f Align fakedom tagname with HTML spec (#9843)
* Fakedom: uppercase tagName for HTML elements per DOM spec

Browsers uppercase tagName for HTML elements. Other namespaces (SVG,
MathML, XML) preserve case. Fakedom now matches, so widget code can use
the natural `node.tagName === "OPTGROUP"` form in both environments.

https://dom.spec.whatwg.org/#dom-element-tagname

* Tests: cover fakedom tagName spec compliance

Three specs pin the DOM contract for tagName: uppercase for HTML
elements, case preservation for other namespaces, and non-mutating
reads. Includes the OPTGROUP predicate the select widget will rely on
once it can drop its defensive .toUpperCase() call.

https://dom.spec.whatwg.org/#dom-element-tagname

* Fakedom: empty or null namespace creates a no-namespace element

Per DOM spec, createElementNS("") and createElementNS(null) have to produce
elements with no namespace. Fakedom previously defaults both to HTML,
which made tagName incorrectly uppercase non-HTML elements.
createElement without arguments still defaults to HTML.

https://dom.spec.whatwg.org/#dom-document-createelementns

* Add ChangeNote
2026-07-14 12:22:45 +02:00
saqimtiaz 521e530e11 docs: added size comparison of empty.html from prerelease and latest release 2026-07-14 07:55:48 +02:00
XLBillyandGitHub 4c29dae4af Replace some wikify widget with call dynamic attribute syntax (#9816)
* Replace some wikify widget with call dynamic attribute syntax

* Apply suggestions

* Add change note
2026-07-13 15:57:04 +02:00
Mario PietschandGitHub f5317dc225 Fix Node.js deprecated warning (#9742)
* Fix Node.js deprecated warning

* Add ChangeNote

* Change deprecation to bugfix for DEP0169 warning

Updated change type from 'deprecation' to 'bugfix' for the Node.js server's handling of the DEP0169 warning.
2026-07-13 13:43:07 +02:00
lin onetwoandGitHub c952450c2e fix(filter): optimize tag[] and !tag[] with Set for O(1) lookup (#9715)
* fix(filter): optimize tag[] and !tag[] with Set for O(1) lookup, resolves performance gap vs search:tags[]

* useless test

* note

* note
2026-07-13 13:37:05 +02:00
Rob HoelzandGitHub 1eb7ec4402 Convert deeply nested if/else to do/while(false) with breaks for parseAttribute (#9770)
01d3cde96 refactored parseMacroParameterAsAttribute to use a
do/while(false) construct with break statements - this updates
parseAttribute with an analogous refactoring for consistency
2026-07-13 08:15:26 +02:00
869557f7d1 Bump js-yaml from 4.1.1 to 4.3.0 (#9903)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-12 09:22:08 +02:00
saqimtiaz c9f1154643 chore: preparing for v5.5.0 2026-07-10 12:41:53 +02:00
142 changed files with 2667 additions and 752 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
# Default to the current version number for building the plugin library
if [ -z "$TW5_BUILD_VERSION" ]; then
TW5_BUILD_VERSION=v5.4.1.
TW5_BUILD_VERSION=v5.5.0.
fi
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
+33 -5
View File
@@ -217,6 +217,11 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
fileInfo.isEditableFile = true;
fileInfo.originalpath = options.fileInfo.originalpath;
}
// Propagate the pinFilepath flag (so generateTiddlerFilepath can short-circuit
// FileSystemPaths and write back to the original location)
if(options.fileInfo && !!options.fileInfo.pinFilepath) {
fileInfo.pinFilepath = true;
}
// Check if the tiddler has any unsafe fields that can't be expressed in a .tid or .meta file: containing control characters, or leading/trailing whitespace
var hasUnsafeFields = false;
$tw.utils.each(tiddler.getFieldStrings(),function(value,fieldName) {
@@ -319,9 +324,16 @@ exports.generateTiddlerFilepath = function(title,options) {
extension = options.extension || "",
originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "",
overwrite = options.fileInfo && options.fileInfo.overwrite || false,
pinFilepath = !!(options.fileInfo && options.fileInfo.pinFilepath),
filepath;
// If the tiddler's filepath is pinned via tiddlywiki.files, the originalpath
// wins and the path filters are skipped entirely.
if(pinFilepath && originalpath) {
var pinnedExt = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - pinnedExt.length);
}
// Check if any of the pathFilters applies
if(options.pathFilters && options.wiki) {
if(!filepath && options.pathFilters && options.wiki) {
$tw.utils.each(options.pathFilters,function(filter) {
if(!filepath) {
var source = options.wiki.makeTiddlerIterator([title]),
@@ -336,13 +348,14 @@ exports.generateTiddlerFilepath = function(title,options) {
//Use the originalpath without the extension
var ext = path.extname(originalpath);
filepath = originalpath.substring(0,originalpath.length - ext.length);
// normalise "\" to "/" so the sanitiser keeps subdirectories
// (it strips "\", not "/") instead of flattening them.
filepath = filepath.split(path.sep).join("/");
} else if(!filepath) {
filepath = title;
// Remove any forward or backward slashes so we don't create directories
filepath = filepath.replace(/\/|\\/g,"_");
}
// Replace any Windows control codes
filepath = filepath.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
// Replace any leading spaces with the same number of underscores
filepath = filepath.replace(/^ +/,function (u) { return u.replace(/ /g, "_");});
//If the path does not start with "." or ".." && a path seperator, then
@@ -352,8 +365,23 @@ exports.generateTiddlerFilepath = function(title,options) {
}
// Replace any Unicode control codes
filepath = filepath.replace(/[\x00-\x1f\x80-\x9f]/g,"_");
// Replace any characters that can't be used in cross-platform filenames
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^/g,"_"));
// Replace any characters that can't be used in cross-platform filenames.
if(!pinFilepath) {
filepath = $tw.utils.transliterate(filepath.replace(/<|>|~|\:|\"|\||\?|\*|\^|\\/g,"_"));
}
// Per segment (catches "sub/CON"), MUST be after transliterate which can create a
// reserved name. Reserved names are wrapped on every path incl pinned;
// trailing dots/spaces (Windows strips) fixed for generated names, not pinned.
filepath = filepath.split("/").map(function(segment) {
if(segment === "" || segment === "." || segment === "..") {
return segment;
}
segment = segment.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i,"_$1_");
if(!pinFilepath) {
segment = segment.replace(/[. ]+$/,function(u) { return u.replace(/[. ]/g,"_"); });
}
return segment;
}).join("/");
// Replace any dots or spaces at the end of the extension with the same number of underscores
extension = extension.replace(/[\. ]+$/, function (u) { return u.replace(/[\. ]/g, "_");});
// Truncate the extension if it is too long
+4 -5
View File
@@ -9,15 +9,14 @@ Serve tiddlers over http
"use strict";
let fs, url, path, querystring, crypto, zlib;
let fs, path, crypto, zlib, URL;
if($tw.node) {
fs = require("fs"),
url = require("url"),
path = require("path"),
querystring = require("querystring"),
crypto = require("crypto"),
zlib = require("zlib");
URL = require("url").URL;
}
/*
@@ -260,8 +259,8 @@ Server.prototype.requestHandler = function(request,response,options) {
state.wiki = options.wiki || self.wiki;
state.boot = options.boot || self.boot;
state.server = self;
state.urlInfo = url.parse(request.url);
state.queryParameters = querystring.parse(state.urlInfo.query);
state.urlInfo = new URL(request.url, "http://localhost");
state.queryParameters = Object.fromEntries(state.urlInfo.searchParams);
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
// Enable CORS
if(this.corsEnable) {
+2 -1
View File
@@ -255,4 +255,5 @@ ViewTemplateTags/Caption: View Template Tags
ViewTemplateTags/Hint: This rule cascade is used by the default view template to dynamically choose the template for displaying the tags area of a tiddler.
WikiInformation/Caption: Wiki Information
WikiInformation/Hint: This page summarises high level information about the configuration of this ~TiddlyWiki. It is designed to enable users to quickly share relevant aspects of the configuration of their ~TiddlyWiki with others, for example when seeking help in one of the forums. No private or personal information is included, and nothing is shared without being explicitly copied and pasted elsewhere
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
WikiInformation/Generate/Caption: Click to generate wiki information report
+1 -1
View File
@@ -78,7 +78,6 @@ class BackgroundActionTracker {
fnProcess: (changes) => {
if(this.hasChanged) {
this.hasChanged = false;
console.log("Processing background action", this.title);
const tiddler = this.wiki.getTiddler(this.title);
let doActions = true;
if(tiddler && tiddler.fields.platforms) {
@@ -89,6 +88,7 @@ class BackgroundActionTracker {
}
}
if(doActions) {
console.log("Processing background action", this.title);
this.wiki.invokeActionString(
this.actions,
null,
+27 -1
View File
@@ -210,6 +210,29 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
this.editShowToolbar = this.wiki.getTiddlerText(ENABLE_TOOLBAR_TITLE,"yes");
this.editShowToolbar = (this.editShowToolbar === "yes") && !!(this.children && this.children.length > 0) && (!this.document.isTiddlyWikiFakeDom);
};
EditTextWidget.prototype.updateDomNodeClasses = function() {
var domNodeClasses = this.engine.domNode.className.split(/\s+/).filter(Boolean),
oldClasses = this.editClass.split(/\s+/).filter(Boolean),
newClasses;
this.editClass = this.getAttribute("class","");
newClasses = this.editClass.split(/\s+/).filter(Boolean);
// Remove classes assigned from the old value of the class attribute
domNodeClasses = domNodeClasses.filter(function(className) {
return !oldClasses.includes(className);
});
// Add new classes from the updated class attribute
domNodeClasses = domNodeClasses.concat(
newClasses.filter(function(className) {
return !domNodeClasses.includes(className);
})
);
this.engine.domNode.className = domNodeClasses.join(" ");
};
/*
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
@@ -217,7 +240,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
EditTextWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Completely rerender if any of our attributes have changed
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes["default"] || changedAttributes.placeholder || changedAttributes.size || changedAttributes.autoHeight || changedAttributes.minHeight || changedAttributes.focusPopup || changedAttributes.rows || changedAttributes.tabindex || changedAttributes.cancelPopups || changedAttributes.inputActions || changedAttributes.refreshTitle || changedAttributes.autocomplete || changedTiddlers[HEIGHT_MODE_TITLE] || changedTiddlers[ENABLE_TOOLBAR_TITLE] || changedTiddlers["$:/palette"] || changedAttributes.disabled || changedAttributes.fileDrop) {
this.refreshSelf();
return true;
} else if(changedTiddlers[this.editRefreshTitle]) {
@@ -226,6 +249,9 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) {
var editInfo = this.getEditInfo();
this.updateEditor(editInfo.value,editInfo.type);
}
if(changedAttributes["class"]) {
this.updateDomNodeClasses();
}
this.engine.fixHeight();
if(this.editShowToolbar) {
return this.refreshChildren(changedTiddlers);
@@ -13,14 +13,15 @@ Export our filter prefix function
exports.intersection = function(operationSubFunction) {
return function(results,source,widget) {
if(results.length !== 0) {
var secondRunResults = operationSubFunction(source,widget);
var firstRunResults = results.toArray();
const secondRunResults = operationSubFunction(source,widget),
secondRunSet = new Set(secondRunResults),
firstRunResults = results.toArray();
results.clear();
$tw.utils.each(firstRunResults,function(title) {
if(secondRunResults.indexOf(title) !== -1) {
firstRunResults.forEach((title) => {
if(secondRunSet.has(title)) {
results.push(title);
}
});
}
};
};
};
+15 -20
View File
@@ -13,33 +13,28 @@ Export our filter prefix function
exports.sort = function(operationSubFunction,options) {
return function(results,source,widget) {
if(results.length > 0) {
var suffixes = options.suffixes,
const suffixes = options.suffixes,
sortType = (suffixes[0] && suffixes[0][0]) ? suffixes[0][0] : "string",
invert = suffixes[1] ? (suffixes[1].indexOf("reverse") !== -1) : false,
isCaseSensitive = suffixes[1] ? (suffixes[1].indexOf("casesensitive") !== -1) : false,
invert = suffixes[1] ? suffixes[1].includes("reverse") : false,
isCaseSensitive = suffixes[1] ? suffixes[1].includes("casesensitive") : false,
inputTitles = results.toArray(),
sortKeys = [],
indexes = new Array(inputTitles.length),
compareFn;
results.each(function(title) {
var key = operationSubFunction(options.wiki.makeTiddlerIterator([title]),widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
"..currentTiddler": widget.getVariable("currentTiddler",{defaultValue:""})
}));
compareFn = $tw.utils.makeCompareFunction(sortType,{defaultType: "string", invert:invert, isCaseSensitive:isCaseSensitive});
results.each((title) => {
const key = operationSubFunction(
options.wiki.makeTiddlerIterator([title]),
widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
"..currentTiddler": widget.getVariable("currentTiddler",{defaultValue:""})
})
);
sortKeys.push(key[0] || "");
});
results.clear();
// Prepare an array of indexes to sort
for(var t=0; t<inputTitles.length; t++) {
indexes[t] = t;
}
// Sort the indexes
compareFn = $tw.utils.makeCompareFunction(sortType,{defaultType: "string", invert:invert, isCaseSensitive:isCaseSensitive});
indexes = indexes.sort(function(a,b) {
return compareFn(sortKeys[a],sortKeys[b]);
});
// Add to results in correct order
$tw.utils.each(indexes,function(index) {
let indexes = Array.from(inputTitles.keys());
indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
indexes.forEach((index) => {
results.push(inputTitles[index]);
});
}
+1 -1
View File
@@ -296,7 +296,7 @@ exports.compileFilter = function(filterString) {
var varTree = $tw.utils.parseFilterVariable(operand.text);
var resultList = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source});
if((resultList.length > 0 && resultList[0] !== undefined) || resultList.length === 0) {
operand.multiValue = widgetClass.evaluateVariable(widget,varTree.name,{params: varTree.params, source: source}) || [];
operand.multiValue = resultList || [];
operand.value = operand.multiValue[0] || "";
} else {
operand.value = "";
+30 -54
View File
@@ -9,69 +9,56 @@ Filter operators for manipulating the current selection list
"use strict";
/*
Fetch titles from the current list
*/
const prepare_results = (source) => {
const results = [];
source((tiddler,title) => results.push(title));
return results;
};
/*
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;
const results = prepare_results(source);
return operator.operand.toLowerCase() === "reverse" ?
results.reverse() :
results;
};
/*
Reverse list
*/
exports.reverse = function(source,operator,options) {
var results = [];
source(function(tiddler,title) {
results.unshift(title);
});
return results;
return prepare_results(source).reverse();
};
/*
First entry/entries in list
*/
exports.first = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(0,count);
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(0,count);
};
/*
Last entry/entries in list
*/
exports.last = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,1),
results = [];
if(count === 0) return results;
source(function(tiddler,title) {
results.push(title);
});
return results.slice(-count);
const count = $tw.utils.getInt(operator.operand,1);
return count === 0 ?
[] :
prepare_results(source).slice(-count);
};
/*
All but the first entry/entries of the list
*/
exports.rest = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count);
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(count);
};
exports.butfirst = exports.rest;
exports.bf = exports.rest;
@@ -80,12 +67,9 @@ exports.bf = exports.rest;
All but the last entry/entries of the list
*/
exports.butlast = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
var index = count === 0 ? results.length : -count;
const count = $tw.utils.getInt(operator.operand,1),
results = prepare_results(source),
index = count === 0 ? results.length : -count;
return results.slice(0,index);
};
exports.bl = exports.butlast;
@@ -94,22 +78,14 @@ exports.bl = exports.butlast;
The nth member of the list
*/
exports.nth = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,1),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count - 1,count);
const count = $tw.utils.getInt(operator.operand,1);
return prepare_results(source).slice(count - 1,count);
};
/*
The zero based nth member of the list
*/
exports.zth = function(source,operator,options) {
var count = $tw.utils.getInt(operator.operand,0),
results = [];
source(function(tiddler,title) {
results.push(title);
});
return results.slice(count,count + 1);
};
const count = $tw.utils.getInt(operator.operand,0);
return prepare_results(source).slice(count,count + 1);
};
+17 -32
View File
@@ -13,37 +13,22 @@ Filter operator for checking if a title starts with a prefix
Export our filter function
*/
exports.prefix = function(source,operator,options) {
var results = [],
suffixes = (operator.suffixes || [])[0] || [];
if(suffixes.indexOf("caseinsensitive") !== -1) {
var operand = operator.operand.toLowerCase();
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.toLowerCase().substr(0,operand.length) !== operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.toLowerCase().substr(0,operand.length) === operand) {
results.push(title);
}
});
const results = [],
suffixes = (operator.suffixes || [])[0] || [],
caseInsensitive = suffixes.includes("caseinsensitive"),
negate = operator.prefix === "!";
const operand = caseInsensitive ?
operator.operand.toLowerCase() :
operator.operand;
source((tiddler,title) => {
const value = caseInsensitive ? title.toLowerCase() : title,
matches = value.startsWith(operand);
if(negate ? !matches : matches) {
results.push(title);
}
} else {
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.substr(0,operator.operand.length) !== operator.operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.substr(0,operator.operand.length) === operator.operand) {
results.push(title);
}
});
}
}
});
return results;
};
};
+7 -12
View File
@@ -14,13 +14,13 @@ Export our filter function
*/
exports.sortsub = function(source,operator,options) {
// Compile the subfilter
var filterFn = options.wiki.compileFilter(operator.operand);
let filterFn = options.wiki.compileFilter(operator.operand);
// Collect the input titles and the corresponding sort keys
var inputTitles = [],
let inputTitles = [],
sortKeys = [];
source(function(tiddler,title) {
inputTitles.push(title);
var r = filterFn.call(options.wiki,function(iterator) {
let r = filterFn.call(options.wiki,function(iterator) {
iterator(options.wiki.getTiddler(title),title);
},options.widget.makeFakeWidgetWithVariables({
"currentTiddler": "" + title,
@@ -29,17 +29,12 @@ exports.sortsub = function(source,operator,options) {
sortKeys.push(r[0] || "");
});
// Rather than sorting the titles array, we'll sort the indexes so that we can consult both arrays
var indexes = new Array(inputTitles.length);
for(var t=0; t<inputTitles.length; t++) {
indexes[t] = t;
}
let indexes = Array.from(inputTitles.keys());
// Sort the indexes
var compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
indexes = indexes.sort(function(a,b) {
return compareFn(sortKeys[a],sortKeys[b]);
});
let compareFn = $tw.utils.makeCompareFunction(operator.suffix,{defaultType: "string",invert: operator.prefix === "!"});
indexes = indexes.sort((a,b) => compareFn(sortKeys[a],sortKeys[b]));
// Make the results array in order
var results = [];
let results = [];
$tw.utils.each(indexes,function(index) {
results.push(inputTitles[index]);
});
+26 -13
View File
@@ -91,22 +91,35 @@ function diffLineWordMode(text1,text2,mode) {
return diffs;
}
exports.makepatches = function(source,operator,options) {
var suffix = operator.suffix || "",
result = [];
source(function(tiddler,title) {
let diffs, patches;
if(suffix === "lines" || suffix === "words") {
diffs = diffLineWordMode(title,operator.operand,suffix);
patches = dmp.patchMake(title,diffs);
exports.makepatches = function(source, operator, options) {
const suffixes = operator.suffixes || [],
[modeArg = [], formatArg = []] = suffixes,
modeSuffix = modeArg[0] || operator.suffix || "",
mode = ["lines", "words"].includes(modeSuffix) ? modeSuffix : "",
isJson = formatArg[0] === "json",
results = [];
source((tiddler, title) => {
if (isJson) {
const diffs = (mode === "lines" || mode === "words")
? diffLineWordMode(title, operator.operand, mode)
: dmp.diffMain(title, operator.operand);
const jsonOutput = diffs.map(([typeCode, text]) => ({
type: typeCode === 1 ? "insert" : (typeCode === -1 ? "delete" : "equal"),
text
}));
results.push(JSON.stringify(jsonOutput));
} else {
patches = dmp.patchMake(title,operator.operand);
const patches = (mode === "lines" || mode === "words")
? dmp.patchMake(title, diffLineWordMode(title, operator.operand, mode))
: dmp.patchMake(title, operator.operand);
results.push(dmp.patchToText(patches));
}
Array.prototype.push.apply(result,[dmp.patchToText(patches)]);
});
return result;
return results;
};
exports.applypatches = makeStringBinaryOperator(
@@ -235,4 +248,4 @@ exports.charcode = function(source,operator,options) {
}
});
return [chars.join("")];
};
};
+5 -4
View File
@@ -13,11 +13,12 @@ Filter operator returning its operand evaluated as a filter
Export our filter function
*/
exports.subfilter = function(source,operator,options) {
var list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
const list = options.wiki.filterTiddlers(operator.operand,options.widget,source);
if(operator.prefix === "!") {
var results = [];
source(function(tiddler,title) {
if(list.indexOf(title) === -1) {
const results = [],
listSet = new Set(list);
source((tiddler,title) => {
if(!listSet.has(title)) {
results.push(title);
}
});
+17 -31
View File
@@ -13,41 +13,27 @@ Filter operator for checking if a title ends with a suffix
Export our filter function
*/
exports.suffix = function(source,operator,options) {
var results = [],
const results = [],
suffixes = (operator.suffixes || [])[0] || [];
if(!operator.operand) {
source(function(tiddler,title) {
source((tiddler,title) => {
results.push(title);
});
} else if(suffixes.indexOf("caseinsensitive") !== -1) {
var operand = operator.operand.toLowerCase();
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.toLowerCase().substr(-operand.length) !== operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.toLowerCase().substr(-operand.length) === operand) {
results.push(title);
}
});
}
} else {
if(operator.prefix === "!") {
source(function(tiddler,title) {
if(title.substr(-operator.operand.length) !== operator.operand) {
results.push(title);
}
});
} else {
source(function(tiddler,title) {
if(title.substr(-operator.operand.length) === operator.operand) {
results.push(title);
}
});
}
return results;
}
const caseInsensitive = suffixes.indexOf("caseinsensitive") !== -1,
negate = operator.prefix === "!",
operand = caseInsensitive ? operator.operand.toLowerCase() : operator.operand;
source((tiddler,title) => {
const value = caseInsensitive ? title.toLowerCase() : title,
matches = value.endsWith(operand);
if(negate ? !matches : matches) {
results.push(title);
}
});
return results;
};
+4 -5
View File
@@ -22,12 +22,11 @@ exports.tag = function(source,operator,options) {
});
} else {
// Old semantics:
var tiddlers;
if(operator.prefix === "!") {
// Returns a copy of the input if operator.operand is missing
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
const excludeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
source(function(tiddler,title) {
if(tiddlers.indexOf(title) === -1) {
if(!excludeTagSet.has(title)) {
results.push(title);
}
});
@@ -39,9 +38,9 @@ exports.tag = function(source,operator,options) {
return indexedResults;
}
} else {
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
const includeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
source(function(tiddler,title) {
if(tiddlers.indexOf(title) !== -1) {
if(includeTagSet.has(title)) {
results.push(title);
}
});
+76 -81
View File
@@ -11,20 +11,18 @@ Extended filter operators to manipulate the current list.
/*
Fetch titles from the current list
*/
var prepare_results = function (source) {
var results = [];
source(function (tiddler, title) {
results.push(title);
});
*/
const prepare_results = (source) => {
const results = [];
source((tiddler,title) => results.push(title));
return results;
};
/*
Moves a number of items from the tail of the current list before the item named in the operand
*/
exports.putbefore = function (source, operator) {
var results = prepare_results(source),
*/
exports.putbefore = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -34,9 +32,9 @@ exports.putbefore = function (source, operator) {
/*
Moves a number of items from the tail of the current list after the item named in the operand
*/
exports.putafter = function (source, operator) {
var results = prepare_results(source),
*/
exports.putafter = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -46,9 +44,9 @@ exports.putafter = function (source, operator) {
/*
Replaces the item named in the operand with a number of items from the tail of the current list
*/
exports.replace = function (source, operator) {
var results = prepare_results(source),
*/
exports.replace = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1);
return (index === -1) ?
@@ -58,39 +56,39 @@ exports.replace = function (source, operator) {
/*
Moves a number of items from the tail of the current list to the head of the list
*/
exports.putfirst = function (source, operator) {
var results = prepare_results(source),
*/
exports.putfirst = function(source,operator) {
const results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,1);
return results.slice(-count).concat(results.slice(0, -count));
return [...results.slice(-count), ...results.slice(0, -count)];
};
/*
Moves a number of items from the head of the current list to the tail of the list
*/
exports.putlast = function (source, operator) {
var results = prepare_results(source),
*/
exports.putlast = function(source,operator) {
const results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,1);
return results.slice(count).concat(results.slice(0, count));
return [...results.slice(count), ...results.slice(0, count)];
};
/*
Moves the item named in the operand a number of places forward or backward in the list
*/
exports.move = function (source, operator) {
var results = prepare_results(source),
*/
exports.move = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand),
count = $tw.utils.getInt(operator.suffix,1),
marker = results.splice(index, 1),
offset = (index + count) > 0 ? index + count : 0;
offset = (index + count) > 0 ? index + count : 0;
return results.slice(0, offset).concat(marker).concat(results.slice(offset));
};
/*
Returns the items from the current list that are after the item named in the operand
*/
exports.allafter = function (source, operator) {
var results = prepare_results(source),
*/
exports.allafter = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand);
return (index === -1) ? [] :
(operator.suffix) ? results.slice(index) :
@@ -99,9 +97,9 @@ exports.allafter = function (source, operator) {
/*
Returns the items from the current list that are before the item named in the operand
*/
exports.allbefore = function (source, operator) {
var results = prepare_results(source),
*/
exports.allbefore = function(source,operator) {
const results = prepare_results(source),
index = results.indexOf(operator.operand);
return (index === -1) ? [] :
(operator.suffix) ? results.slice(0, index + 1) :
@@ -110,9 +108,9 @@ exports.allbefore = function (source, operator) {
/*
Appends the items listed in the operand array to the tail of the current list
*/
exports.append = function (source, operator) {
var append = $tw.utils.parseStringArray(operator.operand, "true"),
*/
exports.append = function(source,operator) {
const append = $tw.utils.parseStringArray(operator.operand,"true"),
results = prepare_results(source),
count = parseInt(operator.suffix) || append.length;
return (append.length === 0) ? results :
@@ -122,9 +120,9 @@ exports.append = function (source, operator) {
/*
Prepends the items listed in the operand array to the head of the current list
*/
exports.prepend = function (source, operator) {
var prepend = $tw.utils.parseStringArray(operator.operand, "true"),
*/
exports.prepend = function(source,operator) {
const prepend = $tw.utils.parseStringArray(operator.operand,"true"),
results = prepare_results(source),
count = $tw.utils.getInt(operator.suffix,prepend.length);
return (prepend.length === 0) ? results :
@@ -134,21 +132,19 @@ exports.prepend = function (source, operator) {
/*
Returns all items from the current list except the items listed in the operand array
*/
exports.remove = function (source, operator) {
var array = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source),
count = parseInt(operator.suffix) || array.length,
p,
len,
index;
len = array.length - 1;
for(p = 0; p < count; ++p) {
if(operator.prefix) {
index = results.indexOf(array[len - p]);
} else {
index = results.indexOf(array[p]);
}
*/
exports.remove = function(source, operator) {
const array = $tw.utils.parseStringArray(operator.operand, "true"),
results = prepare_results(source);
if(array.length === 0) {
return results;
}
const count = parseInt(operator.suffix, 10) || array.length,
targetItems = operator.prefix ? array.slice(-count).reverse() : array.slice(0, count);
for(const item of targetItems) {
const index = results.indexOf(item);
if(index !== -1) {
results.splice(index, 1);
}
@@ -158,40 +154,38 @@ exports.remove = function (source, operator) {
/*
Returns all items from the current list sorted in the order of the items in the operand array
*/
exports.sortby = function (source, operator) {
var results = prepare_results(source);
*/
exports.sortby = function(source,operator) {
const results = prepare_results(source);
if(!results || results.length < 2) {
return results;
}
var lookup = $tw.utils.parseStringArray(operator.operand, "true");
results.sort(function (a, b) {
return lookup.indexOf(a) - lookup.indexOf(b);
});
return results;
const lookup = $tw.utils.parseStringArray(operator.operand,"true"),
// The "end" suffix places unlisted titles last, the default is first
unlisted = operator.suffix === "end" ? lookup.length : -1,
position = (title) => {
const index = lookup.indexOf(title);
return index === -1 ? unlisted : index;
};
return results.sort((a,b) => position(a) - position(b));
};
/*
Removes all duplicate items from the current list
*/
exports.unique = function (source, operator) {
var results = prepare_results(source);
var set = results.reduce(function (a, b) {
if(a.indexOf(b) < 0) {
a.push(b);
}
return a;
}, []);
return set;
*/
exports.unique = function(source, operator) {
return Array.from(new Set(prepare_results(source)));
};
var cycleValueInArray = function(results,operands,stepSize) {
var resultsIndex,
const cycleValueInArray = function(results,operands,stepSize) {
let resultsIndex,
step = stepSize || 1,
i = 0,
opLength = operands.length,
nextOperandIndex;
for(i; i < opLength; i++) {
const opLength = operands.length;
for(; i < opLength; i++) {
resultsIndex = results.indexOf(operands[i]);
if(resultsIndex !== -1) {
break;
@@ -213,18 +207,19 @@ var cycleValueInArray = function(results,operands,stepSize) {
/*
Toggles an item in the current list.
*/
*/
exports.toggle = function(source,operator) {
return cycleValueInArray(prepare_results(source),operator.operands);
};
exports.cycle = function(source,operator) {
var results = prepare_results(source),
operands = (operator.operand.length ? $tw.utils.parseStringArray(operator.operand, "true") : [""]),
step = $tw.utils.getInt(operator.operands[1]||"",1);
const results = prepare_results(source),
operands = operator.operand.length ? $tw.utils.parseStringArray(operator.operand,"true") : [""];
let step = $tw.utils.getInt(operator.operands[1] || "",1);
if(step < 0) {
operands.reverse();
step = Math.abs(step);
}
return cycleValueInArray(results,operands,step);
};
};
+3 -2
View File
@@ -88,10 +88,11 @@ BackSubIndexer.prototype.update = function(updateDescriptor) {
var newTargets = [],
oldTargets = [],
self = this;
if(updateDescriptor.old.exists) {
// System tiddlers are never indexed as sources, matching the _init() scan
if(updateDescriptor.old.exists && !this.wiki.isSystemTiddler(updateDescriptor.old.tiddler.fields.title)) {
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
}
if(updateDescriptor.new.exists) {
if(updateDescriptor.new.exists && !this.wiki.isSystemTiddler(updateDescriptor.new.tiddler.fields.title)) {
newTargets = this._getTarget(updateDescriptor.new.tiddler);
}
+68 -56
View File
@@ -221,7 +221,7 @@ exports.parseMacroInvocationAsTransclusion = function(source,pos) {
orderedAttributes: []
};
// Define our regexps
var reVarName = /([^\s>"'=:]+)/y;
var reVarName = /([^\s>"'=]+)/y;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a double opening angle bracket
@@ -269,7 +269,7 @@ exports.parseMVVReferenceAsTransclusion = function(source,pos) {
orderedAttributes: []
};
// Define our regexps
var reVarName = /([^\s>"'=:)]+)/y;
var reVarName = /([^\s>"'=)]+)/y;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a double opening parenthesis
@@ -528,67 +528,79 @@ exports.parseAttribute = function(source,pos) {
pos = token.end;
// Skip whitespace
pos = $tw.utils.skipWhiteSpace(source,pos);
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
} else {
do {
// Look for a string literal
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
if(stringLiteral) {
pos = stringLiteral.end;
node.type = "string";
node.value = stringLiteral.value;
break;
}
// 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 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.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
} else {
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
} else {
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
} 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];
} else if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
} else {
node.type = "string";
node.value = "true";
}
}
}
}
}
break;
}
}
// 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];
break;
}
// Look for a macro invocation value
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
if(macroInvocation) {
pos = macroInvocation.end;
node.type = "macro";
node.value = macroInvocation;
break;
}
// Look for an MVV reference value
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
if(mvvReference) {
pos = mvvReference.end;
node.type = "macro";
node.value = mvvReference;
node.isMVV = true;
break;
}
// Look for a substituted value
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
if(substitutedValue) {
pos = substitutedValue.end;
node.type = "substituted";
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
break;
}
// 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];
break;
}
if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
return null;
}
node.type = "string";
node.value = "true";
} while(false);
} else {
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
node.type = "string";
@@ -25,7 +25,7 @@ exports.init = function(parser) {
};
exports.parse = function() {
var reEnd = /(\r?\n```$)/mg;
var reEnd = /(^|\r?\n)```$/mg;
var languageStart = this.parser.pos + 3,
languageEnd = languageStart + this.match[1].length;
// Move past the match
+29 -9
View File
@@ -4,7 +4,6 @@ type: application/javascript
module-type: startup
Browser message handling
\*/
"use strict";
@@ -18,6 +17,20 @@ exports.synchronous = true;
/*
Load a specified url as an iframe and call the callback when it is loaded. If the url is already loaded then the existing iframe instance is used
*/
// Ensure the callback fires once, whether by PLUGIN-LIBRARY-READY or onload (legacy)
function flushCallbacks(iframeInfo, err) {
if(iframeInfo.status !== "loaded") {
iframeInfo.status = err ? "error" : "loaded";
saveIFrameInfoTiddler(iframeInfo);
var cb;
while((cb = iframeInfo.callbacks.shift())) {
cb(err, iframeInfo);
}
}
}
function loadIFrame(url,callback) {
// Check if iframe already exists
var iframeInfo = $tw.browserMessaging.iframeInfoMap[url];
@@ -30,7 +43,8 @@ function loadIFrame(url,callback) {
iframeInfo = {
url: url,
status: "loading",
domNode: iframe
domNode: iframe,
callbacks: [callback]
};
$tw.browserMessaging.iframeInfoMap[url] = iframeInfo;
saveIFrameInfoTiddler(iframeInfo);
@@ -38,19 +52,18 @@ function loadIFrame(url,callback) {
iframe.style.display = "none";
iframe.setAttribute("library","true");
document.body.appendChild(iframe);
// Set up onload
// Set up onload. Legacy fallback: if PLUGIN-LIBRARY-READY never arrives, onload triggers the flush later
iframe.onload = function() {
iframeInfo.status = "loaded";
saveIFrameInfoTiddler(iframeInfo);
callback(null,iframeInfo);
flushCallbacks(iframeInfo);
};
iframe.onerror = function() {
callback("Cannot load iframe");
flushCallbacks(iframeInfo, "Cannot load iframe");
};
try {
iframe.src = url;
} catch(ex) {
callback(ex);
flushCallbacks(iframeInfo, ex);
}
}
}
@@ -60,7 +73,7 @@ Unload library iframe for given url
*/
function unloadIFrame(url){
var iframes = document.getElementsByTagName("iframe");
for(var t=iframes.length-1; t--; t>=0) {
for(var t = iframes.length - 1; t >= 0; t--) {
var iframe = iframes[t];
if(iframe.getAttribute("library") === "true" &&
iframe.getAttribute("src") === url) {
@@ -147,6 +160,13 @@ exports.startup = function() {
// console.log("browser-messaging: Received message from",event.origin);
// console.log("browser-messaging: Message content",event.data);
switch(event.data.verb) {
case "PLUGIN-LIBRARY-READY":
$tw.utils.each($tw.browserMessaging.iframeInfoMap, function(info) {
if(info && info.domNode && info.domNode.contentWindow === event.source) {
flushCallbacks(info);
}
});
break;
case "GET-RESPONSE":
if(event.data.status.charAt(0) === "2") {
if(event.data.cookies) {
+2 -1
View File
@@ -15,7 +15,8 @@ var getCellInfo = function(text, start, length, SEPARATOR) {
var isCellQuoted = text.charAt(start) === QUOTE;
var cellStart = isCellQuoted ? start + 1 : start;
if(text.charAt(i) === SEPARATOR) {
// A quote licenses a separator inside the cell, so only an unquoted cell reads an immediate separator as empty
if(!isCellQuoted && text.charAt(cellStart) === SEPARATOR) {
return [cellStart, cellStart, false];
}
+6 -5
View File
@@ -164,15 +164,16 @@ exports.forceLayout = function(element) {
Pulse an element for debugging purposes
*/
exports.pulseElement = function(element) {
var eventName = $tw.utils.convertEventName("animationEnd");
// Event handler to remove the class at the end
element.addEventListener($tw.browser.animationEnd,function handler(event) {
element.removeEventListener($tw.browser.animationEnd,handler,false);
$tw.utils.removeClass(element,"pulse");
element.addEventListener(eventName,function handler(event) {
element.removeEventListener(eventName,handler,false);
$tw.utils.removeClass(element,"tc-pulse");
},false);
// Apply the pulse class
$tw.utils.removeClass(element,"pulse");
$tw.utils.removeClass(element,"tc-pulse");
$tw.utils.forceLayout(element);
$tw.utils.addClass(element,"pulse");
$tw.utils.addClass(element,"tc-pulse");
};
/*
+13 -2
View File
@@ -115,7 +115,9 @@ var TW_Element = function(tag, namespace) {
this.children = [];
this._style = {}; // Internal style object
this.style = new TW_Style(this); // Proxy for style management
this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml";
// createElementNS with empty-string or null normalises to null (no namespace) per spec.
// https://dom.spec.whatwg.org/#dom-document-createelementns
this.namespaceURI = namespace !== undefined ? (namespace || null) : "http://www.w3.org/1999/xhtml";
};
@@ -206,7 +208,16 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
Object.defineProperty(TW_Element.prototype, "tagName", {
get: function() {
return this.tag || "";
if(!this.tag) {
return "";
}
// HTML elements report uppercase tagName per DOM spec. Other namespaces
// preserve case. Fakedom only models HTML documents.
// https://dom.spec.whatwg.org/#dom-element-tagname
if(this.namespaceURI === "http://www.w3.org/1999/xhtml") {
return this.tag.toUpperCase();
}
return this.tag;
}
});
+19 -5
View File
@@ -151,10 +151,24 @@ ButtonWidget.prototype.getBoundingClientRect = function() {
};
ButtonWidget.prototype.isSelected = function() {
return this.setTitle ? (this.setField ? this.wiki.getTiddler(this.setTitle).getFieldString(this.setField) === this.setTo :
(this.setIndex ? this.wiki.extractTiddlerDataItem(this.setTitle,this.setIndex) === this.setTo :
this.wiki.getTiddlerText(this.setTitle))) || this.defaultSetValue || this.getVariable("currentTiddler") :
this.wiki.getTextReference(this.set,this.defaultSetValue,this.getVariable("currentTiddler")) === this.setTo;
var currentValue;
if(this.setTitle) {
if(this.setField) {
// The state tiddler usually does not exist until the button is first clicked
var tiddler = this.wiki.getTiddler(this.setTitle);
currentValue = tiddler ? tiddler.getFieldString(this.setField) : undefined;
} else if(this.setIndex) {
currentValue = this.wiki.extractTiddlerDataItem(this.setTitle,this.setIndex);
} else {
currentValue = this.wiki.getTiddlerText(this.setTitle);
}
if(!currentValue) {
currentValue = this.defaultSetValue || this.getVariable("currentTiddler");
}
} else {
currentValue = this.wiki.getTextReference(this.set,this.defaultSetValue,this.getVariable("currentTiddler"));
}
return currentValue === this.setTo;
};
ButtonWidget.prototype.isPoppedUp = function() {
@@ -267,7 +281,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.tooltip || changedAttributes.actions || changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.popup && changedTiddlers[this.popup]) || (this.popupTitle && changedTiddlers[this.popupTitle]) || changedAttributes.popupAbsCoords || changedAttributes.setTitle || changedAttributes.setField || changedAttributes.setIndex || changedAttributes.popupTitle || changedAttributes.disabled || changedAttributes["default"]) {
if(changedAttributes.tooltip || changedAttributes.actions || changedAttributes.to || changedAttributes.message || changedAttributes.param || changedAttributes.set || changedAttributes.setTo || changedAttributes.popup || changedAttributes.hover || changedAttributes.selectedClass || changedAttributes.style || changedAttributes.dragFilter || changedAttributes.dragTiddler || (this.set && changedTiddlers[this.set]) || (this.setTitle && changedTiddlers[this.setTitle]) || (this.popup && changedTiddlers[this.popup]) || (this.popupTitle && changedTiddlers[this.popupTitle]) || changedAttributes.popupAbsCoords || changedAttributes.setTitle || changedAttributes.setField || changedAttributes.setIndex || changedAttributes.popupTitle || changedAttributes.disabled || changedAttributes["default"]) {
this.refreshSelf();
return true;
} else {
+1 -1
View File
@@ -22,7 +22,7 @@ Inherit from the base widget class
DiffTextWidget.prototype = new Widget();
DiffTextWidget.prototype.invisibleCharacters = {
"\n": "↩︎\n",
"\n": "\n",
"\r": "⇠",
"\t": "⇥\t"
};
+2 -1
View File
@@ -99,7 +99,8 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) {
wikiLinkText = this.wiki.filterTiddlers(wikilinkTransformFilter,this,function(iterator) {
iterator(self.wiki.getTiddler(self.to),self.to);
})[0];
} else {
}
if(!wikiLinkText) {
// Expand the tv-wikilink-template variable to construct the href
var wikiLinkTemplateMacro = this.getVariable("tv-wikilink-template"),
wikiLinkTemplate = wikiLinkTemplateMacro ? wikiLinkTemplateMacro.trim() : "#$uri_encoded$";
+3 -3
View File
@@ -1,6 +1,6 @@
title: $:/core/stylesheets/custom-properties
\rules only transcludeinline macrocallinline html transcludeblock
\rules only transcludeinline macrocallinline html transcludeblock filteredtranscludeinline
/* Tiddlywiki's CSS properties */
@@ -21,10 +21,10 @@ title: $:/core/stylesheets/custom-properties
--tp-story-left: {{$:/themes/tiddlywiki/vanilla/metrics/storyleft}};
--tp-story-top: {{$:/themes/tiddlywiki/vanilla/metrics/storytop}};
--tp-story-right: {{$:/themes/tiddlywiki/vanilla/metrics/storyright}};
--tp-story-width: {{$:/themes/tiddlywiki/vanilla/metrics/storyrwidth}};
--tp-story-width: {{$:/themes/tiddlywiki/vanilla/metrics/storywidth}};
--tp-tiddler-width: {{$:/themes/tiddlywiki/vanilla/metrics/tiddlerwidth}};
--tp-sidebar-breakpoint: {{$:/themes/tiddlywiki/vanilla/metrics/sidebarbreakpoint}};
--tp-sidebar-width: {{$:/themes/tiddlywiki/vanilla/metrics/sidebarwidth}};
--tp-animation-duration: {{{ [{$:/config/AnimationDuration}addsuffix[ms]] }}};
}
}
+3
View File
@@ -0,0 +1,3 @@
title: $:/core/templates/plain-text
<$text text=<<text>>/>
+3
View File
@@ -0,0 +1,3 @@
title: $:/core/templates/rendered-text
<<text>>
+5 -3
View File
@@ -4,13 +4,15 @@ description: create a new journal tiddler
\whitespace trim
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} textFieldTags={{$:/config/NewJournal/Tags}} tagsFieldTags={{$:/config/NewJournal/Tags!!tags}} journalText={{$:/config/NewJournal/Text}}>
<$wikify name="journalTitle" text="<$transclude $variable='now' format=<<journalTitleTemplate>>/>">
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
textFieldTags={{$:/config/NewJournal/Tags}}
tagsFieldTags={{$:/config/NewJournal/Tags!!tags}}
journalText={{$:/config/NewJournal/Text}}
>
<$reveal type="nomatch" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text={{{ [<journalTitle>get[]] }}}/>
</$reveal>
<$reveal type="match" state=<<journalTitle>> text="">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text=<<journalText>>/>
</$reveal>
</$wikify>
</$let>
+3 -3
View File
@@ -108,10 +108,10 @@ tags: $:/tags/EditTemplate
storeTitle=<<newFieldNameInputTiddler>>
searchListState=<<newFieldNameSelectionTiddler>>
>
<div class="tc-edit-field-add-name-wrapper">
<div class=`tc-edit-field-add-name-wrapper ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`>
<$transclude $variable="keyboard-driven-input"
cancelPopups="yes"
class=`tc-edit-texteditor tc-popup-handle ${ [<newFieldNameTiddler>get[text]] :intersection[<storyTiddler>fields[]] :then[[tc-edit-field-exists]] }$`
class="tc-edit-texteditor tc-popup-handle"
configTiddlerFilter="[[$:/config/EditMode/fieldname-filter]]"
default=""
focus={{{ [{!!draft.of}is[tiddler]then{$:/config/AutoFocusEdit}match[fields]then[true]] :else[{$:/config/AutoFocus}match[fields]then[true]] :else[[false]] }}}
@@ -178,4 +178,4 @@ tags: $:/tags/EditTemplate
</$let>
</$let>
</div>
</$let>
</$let>
+18 -23
View File
@@ -6,30 +6,25 @@ tags: $:/tags/EditTemplate
\procedure lingo-base() $:/language/EditTemplate/
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour"
target=<<colour>>
fallbackTarget=<<fallbackTarget>>
colourA=<<colourA>>
colourB=<<colourB>>/>
"""
>
<$let backgroundColor=<<colour>> >
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
<$let foregroundColor=<<contrastcolour target=<<colour>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >>
backgroundColor=<<colour>>
>
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
data-tag-title=<<currentTiddler>>
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
>
<$transclude tiddler=<<icon>>/>
<$view field="title" format="text"/>
<$button class="tc-btn-invisible tc-remove-tag-button"
style.fill=<<foregroundColor>>
>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
</$wikify>
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
<!-- Touch a temp tiddler so that the tag remove animation fires instantly. Otherwise it's delayed by the typing timeout duration -->
<$action-setfield $tiddler="$:/temp/tags-edit-refresh" text=<<currentTiddler>>/>
{{$:/core/images/close-button}}
</$button>
</span>
</$let>
\end
\procedure tag-body(colour,palette,icon,tagField:"tags")
+2 -4
View File
@@ -16,15 +16,13 @@ title: $:/core/ui/TagPickerTagTemplate
<$set name="backgroundColor"
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
>
<$wikify name="foregroundColor"
text="""<$macrocall $name="contrastcolour" target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>>/>"""
>
<$let foregroundColor=<<contrastcolour target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >> >
<span class="tc-tag-label tc-btn-invisible"
style=<<tag-pill-styles>>
data-tag-title=<<currentTiddler>>
>
{{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/>
</span>
</$wikify>
</$let>
</$set>
</$button>
+4 -3
View File
@@ -6,7 +6,6 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
\whitespace trim
\procedure journalButton()
<$button tooltip={{$:/language/Buttons/NewJournalHere/Hint}} aria-label={{$:/language/Buttons/NewJournalHere/Caption}} class=<<tv-config-toolbar-class>>>
<$wikify name="journalTitle" text="""<$transclude $variable="now" format=<<journalTitleTemplate>>/>""">
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/>
<%if [<tv-config-toolbar-icons>match[yes]] %>
{{$:/core/images/new-journal-button}}
@@ -16,9 +15,11 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
</span>
<%endif%>
</$wikify>
</$button>
\end
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<<currentTiddler>>>
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
journalTags={{$:/config/NewJournal/Tags}}
currentTiddlerTag=<<currentTiddler>>
>
<<journalButton>>
</$let>
+5 -1
View File
@@ -17,6 +17,10 @@ This page summarises high level information about the configuration of this ~Tid
Drag this link to copy this tool to another wiki
\end intrinsic-lingo-Drag/Caption
\procedure intrinsic-lingo-Generate/Caption()
Click to generate wiki information report
\end intrinsic-lingo-Generate/Caption
\procedure lingo(title,mode:"inline")
<%if [<title>addprefix<lingo-base>is[shadow]] %>
<$transclude $tiddler={{{ [<title>addprefix<lingo-base>] }}} $mode=<<mode>>/>
@@ -105,7 +109,7 @@ Drag this link to copy this tool to another wiki
<$button>
<<display-wiki-info-modal>>
Click to generate wiki information report
<<lingo title:"Generate/Caption">>
</$button>
<$link to="$:/core/ui/ControlPanel/WikiInformation">
+1 -1
View File
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/library/v5.4.1/index.html
url: https://tiddlywiki.com/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}}
{{$:/language/OfficialPluginLibrary/Hint}}
@@ -1,6 +1,6 @@
title: $:/config/LocalPluginLibrary
tags: $:/tags/PluginLibrary
url: http://127.0.0.1:8080/prerelease/library/v5.4.1/index.html
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
@@ -1,6 +1,6 @@
title: $:/config/OfficialPluginLibrary
tags: $:/tags/PluginLibrary
url: https://tiddlywiki.com/prerelease/library/v5.4.1/index.html
url: https://tiddlywiki.com/prerelease/library/v5.5.0/index.html
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
@@ -0,0 +1,94 @@
created: 20260714055110474
modified: 20260714055129225
tags:
title: TiddlyWiki Pre-release Size Comparison
\function bytes.to.mib(size) [abs[]divide[1048576]fixed[2]addsuffix[ MiB]]
\function bytes.to.kib(size) [abs[]divide[1024]fixed[2]addsuffix[ KiB]]
\function format.file.size(size) [<size>abs[]compare:number:gteq[1048576]bytes.to.mib<size>] :else[<size>abs[]compare:number:gteq[1024]bytes.to.kib<size>] :else[<size>addsuffix[ bytes]]
\procedure get-wiki-filesize(filepath)
\procedure completion-get-json()
<!-- Success -->
<$list filter="[<status>compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore">
<$action-log msg="completed" size={{{ [<data>jsonget[size]] }}}/>
<$action-setfield $tiddler=`$:/temp/file-size-comparison/$(filepath)$` text={{{ [<data>jsonget[size]] }}} />
</$list>
\end completion-get-json
<$action-sendmessage
$message="tm-http-request"
url=`https://api.github.com/repos/TiddlyWiki/tiddlywiki.com-gh-pages/contents/$(filepath)$?ref=master`
method="GET"
oncompletion=<<completion-get-json>>
var-filepath=<<filepath>>
/>
\end get-wiki-filesize
\procedure get-wiki-filesizes()
<$list filter="empty.html prerelease/empty.html" variable="filepath">
<$action-log />
<<$transclude $variable="get-wiki-filesize" filepath=<<filepath>> >>
</$list>
\end get-wiki-filesizes
<%if [[$:/temp/file-size-comparison/empty.html]is[tiddler]] [[$:/temp/file-size-comparison/prerelease/empty.html]is[tiddler]] :and[count[]match[2]]%>
<$let delta={{{ [{$:/temp/file-size-comparison/empty.html}subtract{$:/temp/file-size-comparison/prerelease/empty.html}] }}}
message={{{ [<delta>sign[]match[-1]then[Size increased]] :else[<delta>sign[]match[1]then[Size decreased]] :else[[Size has not changed]] }}}
>
<svg xmlns="http://www.w3.org/2000/svg" width="158" height="20" role="img" style.zoom="1.5" aria-label="<$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /> bytes">
<title><$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /></title>
<filter id="blur">
<feGaussianBlur stdDeviation="16"/>
</filter>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r">
<rect width="158" height="20" rx="3"/>
</clipPath>
<g clip-path="url(#r)">
<rect width="93" height="20" fill="#555"/>
<rect x="93" width="65" height="20" fill="#67ac09"/>
<rect width="158" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830"><$text text=<<message>>/></text>
<text x="475" y="150" fill-opacity=".3" textLength="830"><$text text=<<message>>/></text>
</g>
<text x="475" y="140" textLength="830"><$text text=<<message>>/></text>
</g>
<g transform="scale(.1)">
<g aria-hidden="true" fill="#010101">
<text x="1245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
<text x="1245" y="150" fill-opacity=".3" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
<text x="1245" y="140" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
</g>
</g>
</svg>
; Pre-release empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/prerelease/empty.html}] }}} /> bytes
; <$text text={{{ [tag[ReleaseNotes]sort[released]last[]] }}} /> empty.html
: <$text text={{{ [{$:/temp/file-size-comparison/empty.html}] }}} /> bytes
---
</$let>
<%else %>
<$button actions=<<get-wiki-filesizes>> class="tc-btn-big-green">
Compare size of empty.html
</$button>
<%endif%>
@@ -13,6 +13,8 @@ The pre-release is also available as an [[empty wiki|https://tiddlywiki.com/prer
</div>
{{ TiddlyWiki Pre-release Size Comparison }}
<$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">
<div class="tc-titlebar">
<h2 class="tc-title"><$text text=<<currentTiddler>>/></h2>
@@ -0,0 +1,12 @@
title: ButtonSelection/DefaultSelection
description: The default attribute selects a setTitle button while the state tiddler is missing
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" default="Beta" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" default="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -0,0 +1,12 @@
title: ButtonSelection/MissingStateTiddler
description: A setField button renders when its state tiddler does not exist
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
<$button setTitle="$:/state/demo" setField="selection" setTo="Alpha" selectedClass="sel">Alpha</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button></p>
@@ -0,0 +1,15 @@
title: ButtonSelection/SetFieldSelection
description: Only the setField button whose setTo matches the state field is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
selection: Beta
+
title: Output
<$button setTitle="$:/state/demo" setField="selection" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setField="selection" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -0,0 +1,17 @@
title: ButtonSelection/SetIndexSelection
description: Only the setIndex button whose setTo matches the state index is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
type: application/json
{"selection": "Beta"}
+
title: Output
<$button setTitle="$:/state/demo" setIndex="selection" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setIndex="selection" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -0,0 +1,16 @@
title: ButtonSelection/SetSelection
description: Only the set button whose setTo matches the state is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button set="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button set="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="true" class=" sel">Alpha</button><button aria-checked="false" class="">Beta</button></p>
@@ -0,0 +1,20 @@
title: ButtonSelection/SetTitleRefresh
description: The setTitle selection follows the state tiddler when it changes
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: Actions
<$action-setfield $tiddler="$:/state/demo" text="Beta"/>
+
title: ExpectedResult
<p><button aria-checked="false" class="">Alpha</button><button aria-checked="true" class=" sel">Beta</button></p>
@@ -0,0 +1,16 @@
title: ButtonSelection/SetTitleSelection
description: Only the setTitle button whose setTo matches the state is selected
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: $:/state/demo
Alpha
+
title: Output
<$button setTitle="$:/state/demo" setTo="Alpha" selectedClass="sel">Alpha</$button><$button setTitle="$:/state/demo" setTo="Beta" selectedClass="sel">Beta</$button>
+
title: ExpectedResult
<p><button aria-checked="true" class=" sel">Alpha</button><button aria-checked="false" class="">Beta</button></p>
@@ -0,0 +1,22 @@
title: Filters/DiffMergePatch4
description: Tests for diff-merge-patch derived operators
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
\define text1()
The quick brown fox
\end
\define text2()
The fast brown fox
\end
<$text text={{{ [<text1>makepatches::json<text2>] }}}/>
+
title: ExpectedResult
[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}]
@@ -0,0 +1,14 @@
title: Filters/ListOps
description: Test listops operators
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
(<$text text={{{ =[[E]] =[[A]] =[[B]] =[[C]] =[[C]] =[[D]] =[[C]] +[unique[]join[]] }}}/>)
+
title: ExpectedResult
<p>(EABCD)</p>
@@ -0,0 +1,15 @@
title: MultiValuedVariables/AttributeColonVarName
description: ((var)) on a widget attribute may name a variable containing a colon (#10013)
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\whitespace trim
<$let my:list={{{ [range[2]] }}}>
<$text text=((my:list))/>
</$let>
+
title: ExpectedResult
<p>1</p>
@@ -0,0 +1,16 @@
title: Parse/BackCompat/MacrocallColonVarNameAndParam
description: A colon-named procedure or macro accepts named parameters with either separator (#10013)
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\procedure object:method(p) COLONPROC(<<p>>)
\define object:macro(p) COLONMACRO($p$)
<<object:method p:"V">>
<<object:method p="V">>
<<object:macro p:"V">>
+
title: ExpectedResult
<p>COLONPROC(V)</p><p>COLONPROC(V)</p><p>COLONMACRO(V)</p>
@@ -0,0 +1,16 @@
title: Parse/BackCompat/MacrocallColonVarNameAsValue
description: A colon-named procedure or macro may be an =-separated parameter value (#10013)
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\procedure object:method() COLONPROC
\define object:macro() COLONMACRO
\procedure host(a) [a=<<a>>]
<<host a=<<object:method>>>>
<<host a=<<object:macro>>>>
+
title: ExpectedResult
<p>[a=COLONPROC]</p><p>[a=COLONMACRO]</p>
@@ -0,0 +1,13 @@
title: Parse/BackCompat/MacrocallColonVarName
description: Macro call variable name may contain a colon (#10013)
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\procedure object:method() VAL
<<object:method>>
+
title: ExpectedResult
<p>VAL</p>
@@ -0,0 +1,13 @@
title: Parse/BackCompat/WidgetAttrMacroColonVarName
description: Widget attribute macro value may name a variable containing a colon (#10013)
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Output
\procedure object:method() VAL
<$text text=<<object:method>>/>
+
title: ExpectedResult
<p>VAL</p>
@@ -0,0 +1,162 @@
/*\
title: test-back-indexer.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Regression tests for #9916: the back-indexer must never record system
tiddlers as backlink or backtransclude sources, neither when the index is
first built nor when it is incrementally updated.
\*/
"use strict";
describe("Back-indexer system source tests (#9916)", function() {
function setupWiki() {
// Create a wiki with indexers and one primed backlink pair
var wiki = new $tw.Wiki();
wiki.addIndexersToWiki();
wiki.addTiddler({
title: "TestIncoming",
text: ""});
wiki.addTiddler({
title: "TestOutgoing",
text: "A link to [[TestIncoming]]"});
return wiki;
}
it("should never report a system tiddler as a backlink source", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"}))
// and run the filter again; $:/temp/demo must not appear.
var wiki = setupWiki();
// The first lookup builds the lazy index; its initial scan skips system tiddlers
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
// The incremental update() must skip them too
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should keep backlinks stable while a linking system tiddler is modified and deleted", function() {
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// change its text, $tw.wiki.deleteTiddler("$:/temp/demo"); the filter result never changes.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
// Modify: both the old and the new side of the index update are system tiddlers
wiki.addTiddler({
title: "$:/temp/system-source",
text: "Links to [[TestIncoming]] and [[TestOutgoing]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.deleteTiddler("$:/temp/system-source");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should drop the backlink when its source is renamed to a system title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[HelloThere]]"})),
// confirm Demo is in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then
// $tw.wiki.renameTiddler("Demo","$:/Demo"); neither Demo nor $:/Demo remains a source.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.renameTiddler("TestOutgoing","$:/TestOutgoing");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("");
});
it("should gain the backlink when a linking system tiddler is renamed to a normal title", function() {
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
// then $tw.wiki.renameTiddler("$:/temp/demo","DemoVisible");
// DemoVisible now appears in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]").
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "$:/temp/system-source",
text: "A link to [[TestIncoming]]"});
wiki.renameTiddler("$:/temp/system-source","VisibleSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,VisibleSource");
});
it("should still report normal sources for a system tiddler target", function() {
// Only sources are filtered, targets are not. Browser console:
// $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[$:/config/NewJournal/Tags]]"}));
// $tw.wiki.filterTiddlers("[[$:/config/NewJournal/Tags]backlinks[]]") contains Demo.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "TestSystemLinker",
text: "A link to [[$:/config/Target]]"});
expect(wiki.filterTiddlers("[[$:/config/Target]backlinks[]]").join(",")).toBe("TestSystemLinker");
});
describe("Adversarial probes", function() {
it("should never index a shadow tiddler as a source, even when revealed by deleting its override", function() {
// Goes red when a refactor of BackSubIndexer.update() checks the tiddler instead of
// the exists flag:
// if(updateDescriptor["new"].tiddler) { ... } // broken: goes red here
// if(updateDescriptor["new"].exists) { ... } // correct: stays green
// After deleteTiddler() on an override, boot.js fills new.tiddler via getTiddler(),
// which falls back to the revealed shadow, while new.exists stays false because
// only the real store counts. The broken variant indexes the shadow's links and
// the final expect fails with "TestOutgoing,ShadowSource".
// The first expect also goes red if _init() ever starts scanning shadows; the
// middle expect pins that a real override IS indexed, so this probe cannot be
// satisfied by indexing nothing at all.
// Browser console: override a plugin shadow with text [[HelloThere]], check it appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then $tw.wiki.deleteTiddler(title);
// the title disappears from the filter result even though the shadow still renders.
var wiki = setupWiki();
wiki.addTiddler({
title: "$:/plugins/test/shadow-plugin",
type: "application/json",
"plugin-type": "plugin",
text: JSON.stringify({tiddlers: {
"ShadowSource": {title: "ShadowSource", text: "A shadow link to [[TestIncoming]]"}
}})});
wiki.readPluginInfo();
wiki.registerPluginTiddlers("plugin");
wiki.unpackPluginTiddlers();
// The initial scan sees only real tiddlers, not the shadow
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "ShadowSource",
text: "An overriding link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,ShadowSource");
// Deleting the override reveals the shadow; it must not enter the index
wiki.deleteTiddler("ShadowSource");
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
});
it("should handle hostile titles like __proto__ as source and target", function() {
// Goes red when any title-keyed hashmap on the backlinks path is created like this:
// this.index = {}; // broken: goes red here
// instead of:
// this.index = Object.create(null); // correct: stays green
// this.index = new Map(); // a Map/Set refactor also stays green
// The same applies to the boot tiddler store and to the per-target source maps
// (self.index[target] = ...). On a plain {} the assignment index["__proto__"] = x
// stores no key; it silently replaces the object's prototype. Depending on which
// map regresses, the __proto__ tiddler never registers as a source (second expect),
// or its target entry lands in the shared prototype, where it pollutes every other
// lookup and lookup("__proto__") returns garbage (third expect).
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "__proto__",
// text: "[[HelloThere]]"})); __proto__ appears in
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") and
// $tw.wiki.filterTiddlers("[[__proto__]backlinks[]]") lists tiddlers linking to it.
var wiki = setupWiki();
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
wiki.addTiddler({
title: "__proto__",
text: "A link to [[TestIncoming]]"});
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,__proto__");
wiki.addTiddler({
title: "ProtoLinker",
text: "A link to [[__proto__]]"});
expect(wiki.filterTiddlers("[[__proto__]backlinks[]]").join(",")).toBe("ProtoLinker");
});
});
});
@@ -0,0 +1,140 @@
/*\
title: test-browser-messaging.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the PLUGIN-LIBRARY-READY handshake and legacy onload fallback.
\*/
"use strict";
describe("Browser Messaging", function() {
it("should declare itself as a synchronous browser-only startup module", function() {
var startup = require("$:/core/modules/browser-messaging.js");
expect(startup.name).toBe("browser-messaging");
expect(startup.platforms).toEqual(["browser"]);
expect(startup.synchronous).toBe(true);
expect(typeof startup.startup).toBe("function");
});
// loadIFrame()/flushCallbacks() are private to browser-messaging.js, so the only way
// to exercise them for real is through $tw.rootWidget events and real postMessage
// traffic with a real iframe, rather than re-implementing their logic here.
describe("plugin library iframe handshake", function() {
var urlsToUnload;
beforeEach(function() {
if(!$tw.browser) { return; }
urlsToUnload = [];
});
afterEach(function() {
if(!$tw.browser) { return; }
urlsToUnload.forEach(function(url) {
// Grab the DOM node before unloading clears the map entry, as a
// belt-and-braces removal (unloadIFrame's own removal has an
// unrelated off-by-one loop bug for small iframe counts).
var info = $tw.browserMessaging.iframeInfoMap[url];
$tw.rootWidget.dispatchEvent({type: "tm-unload-plugin-library", paramObject: {url: url}});
if(info && info.domNode && info.domNode.parentNode) {
info.domNode.parentNode.removeChild(info.domNode);
}
URL.revokeObjectURL(url);
});
});
function makeChildUrl(script) {
var blob = new Blob(["<script>" + script + "<\/script>"],{type: "text/html"});
var url = URL.createObjectURL(blob);
urlsToUnload.push(url);
return url;
}
function waitUntil(predicate,done,description) {
var attempts = 0;
(function check() {
if(predicate()) {
done();
} else if(++attempts > 400) {
done.fail("Timed out waiting for: " + description);
} else {
setTimeout(check,20);
}
})();
}
it("should complete the real GET / GET-RESPONSE round trip once the child signals PLUGIN-LIBRARY-READY", function(done) {
if(!$tw.browser) { pending("browser-only: requires a real iframe and postMessage - run in browser"); return; }
// Mirrors plugins/tiddlywiki/pluginlibrary/libraryserver.js's real handshake and GET handling:
// listener registration and the ready postMessage both happen synchronously, before <body> parses,
// so onload (the legacy fallback below) cannot win this race for a well-behaved child.
var url = makeChildUrl(
"window.addEventListener('message',function(e){" +
"if(e.data && e.data.verb === 'GET' && e.data.url === 'recipes/library/tiddlers.json'){" +
"e.source.postMessage({verb:'GET-RESPONSE',status:'200',cookies:e.data.cookies,url:e.data.url," +
"type:'application/json',body:JSON.stringify([{title:'ExamplePlugin',type:'application/json',text:'{}'}])},'*');" +
"}});" +
"window.parent.postMessage({verb:'PLUGIN-LIBRARY-READY'},'*');"
);
var expectedTitle = "$:/temp/RemoteAssetInfo/" + url + "/ExamplePlugin";
$tw.rootWidget.dispatchEvent({
type: "tm-load-plugin-library",
paramObject: {url: url}
});
waitUntil(function() {
return !!$tw.wiki.getTiddler(expectedTitle);
},done,"tiddler " + expectedTitle);
},10000);
it("should still resolve via the legacy iframe.onload fallback when the child never sends PLUGIN-LIBRARY-READY", function(done) {
if(!$tw.browser) { pending("browser-only: requires a real iframe - run in browser"); return; }
// A blank document that never sends the handshake exercises the onload-only legacy path.
var url = makeChildUrl("");
$tw.rootWidget.dispatchEvent({
type: "tm-load-plugin-library",
paramObject: {url: url}
});
waitUntil(function() {
var info = $tw.browserMessaging.iframeInfoMap[url];
return !!info && info.status === "loaded";
},done,"iframe status to become 'loaded' via onload");
},10000);
it("should complete the round trip via PLUGIN-LIBRARY-READY even when the child's own load is aborted and onload can never fire", function(done) {
if(!$tw.browser) { pending("browser-only: requires a real iframe and postMessage - run in browser"); return; }
// window.stop() simulates onload never firing despite the library being ready,
// so only the PLUGIN-LIBRARY-READY message can complete this round trip.
var url = makeChildUrl(
"window.addEventListener('message',function(e){" +
"if(e.data && e.data.verb === 'GET' && e.data.url === 'recipes/library/tiddlers.json'){" +
"e.source.postMessage({verb:'GET-RESPONSE',status:'200',cookies:e.data.cookies,url:e.data.url," +
"type:'application/json',body:JSON.stringify([{title:'ExamplePlugin',type:'application/json',text:'{}'}])},'*');" +
"}});" +
"window.parent.postMessage({verb:'PLUGIN-LIBRARY-READY'},'*');" +
"window.stop();"
);
var expectedTitle = "$:/temp/RemoteAssetInfo/" + url + "/ExamplePlugin";
$tw.rootWidget.dispatchEvent({
type: "tm-load-plugin-library",
paramObject: {url: url}
});
waitUntil(function() {
return !!$tw.wiki.getTiddler(expectedTitle);
},done,"tiddler " + expectedTitle);
},10000);
});
});
@@ -0,0 +1,69 @@
/*\
title: test-codeblock-parser.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the codeblock wikitext rule (#9047): an empty code block must parse
cleanly, and a closing fence only counts when it stands alone on its line.
\*/
"use strict";
describe("codeblock parser tests (#9047)", function() {
var wiki = new $tw.Wiki();
function parse(text) {
return wiki.parseText("text/vnd.tiddlywiki",text).tree;
}
it("parses an empty code block with a language", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\n```").tree
// Expected: one codeblock node, code "", language "bash".
var tree = parse("```bash\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("");
expect(tree[0].attributes.language.value).toBe("bash");
});
it("handles CRLF line endings around both fences", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\r\nfoo\r\n```").tree
// Expected: one codeblock node with code "foo"; neither the CRLF after the
// opening fence nor the one before the closing fence is part of the code.
var tree = parse("```bash\r\nfoo\r\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
it("keeps the content of a block and drops the delimiting newlines", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```\nfoo\n```").tree
// Expected: one codeblock node with code "foo"; the newlines around the
// delimiter lines are not part of the code.
var tree = parse("```\nfoo\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
it("only closes the block when the fence stands alone on its line", function() {
// A content line ending in ``` is code, not a closing fence, e.g. nested
// markdown fences or template literals quoted inside a code block.
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```\nabc```\ndef\n```").tree
// Expected: one codeblock containing "abc```\ndef"; nothing leaks out as wikitext.
var tree = parse("```\nabc```\ndef\n```");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("abc```\ndef");
});
it("swallows the rest of the tiddler when no closing fence exists", function() {
// Browser console: $tw.wiki.parseText("text/vnd.tiddlywiki","```bash\nfoo").tree
// Expected: one codeblock node with code "foo", extending to the end of the text.
var tree = parse("```bash\nfoo");
expect(tree.length).toBe(1);
expect(tree[0].type).toBe("codeblock");
expect(tree[0].attributes.code.value).toBe("foo");
});
});
+41
View File
@@ -0,0 +1,41 @@
/*\
title: test-csv.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests the CSV parser.
\*/
"use strict";
describe("CSV tests", function() {
it("parses a simple table", function() {
expect($tw.utils.parseCsvString("a,b,c\n1,2,3")).toEqual([["a","b","c"],["1","2","3"]]);
});
it("parses a table whose first cell reads empty", function() {
expect($tw.utils.parseCsvString(",b,c\n1,2,3")).toEqual([["","b","c"],["1","2","3"]]);
});
it("parses an empty cell in the middle of a row", function() {
expect($tw.utils.parseCsvString("a,,c")).toEqual([["a","","c"]]);
});
it("parses quoted cells", function() {
expect($tw.utils.parseCsvString('a,"b,still b",c')).toEqual([["a","b,still b","c"]]);
});
it("parses a quoted cell holding an escaped quote", function() {
expect($tw.utils.parseCsvString('a,"b ""quoted""",c')).toEqual([["a",'b "quoted"',"c"]]);
});
it("honours a custom separator", function() {
expect($tw.utils.parseCsvString("a;b;c",{separator: ";"})).toEqual([["a","b","c"]]);
});
it("parses a table with a header row into hashmaps", function() {
expect($tw.utils.parseCsvStringWithHeader("a,b\n1,2")).toEqual([{a: "1", b: "2"}]);
});
});
@@ -19,6 +19,56 @@ describe("fakedom tests", function() {
expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3);
});
// Per DOM spec, tagName returns the HTML-uppercased qualified name for HTML
// elements. Other namespaces preserve case.
// https://dom.spec.whatwg.org/#dom-element-tagname
var HTML_NS = "http://www.w3.org/1999/xhtml";
var SVG_NS = "http://www.w3.org/2000/svg";
it("tagName uppercases for HTML namespace", function() {
// Default namespace is HTML
expect($tw.fakeDocument.createElement("div").tagName).toBe("DIV");
// The exact predicate the select widget relies on (#9839)
expect($tw.fakeDocument.createElement("optgroup").tagName === "OPTGROUP").toBe(true);
// Already-uppercase input stays uppercase
expect($tw.fakeDocument.createElement("OPTGROUP").tagName).toBe("OPTGROUP");
// Mixed-case input is normalised
expect($tw.fakeDocument.createElement("Div").tagName).toBe("DIV");
// Hyphenated custom-element names uppercase whole tag, hyphens survive
expect($tw.fakeDocument.createElement("my-button").tagName).toBe("MY-BUTTON");
// Empty tag returns empty string
expect($tw.fakeDocument.createElement("").tagName).toBe("");
// Explicit HTML namespace via createElementNS uppercases the same way
expect($tw.fakeDocument.createElementNS(HTML_NS,"Div").tagName).toBe("DIV");
});
it("tagName preserves case for non-HTML namespaces", function() {
// SVG: lowercase preserved
expect($tw.fakeDocument.createElementNS(SVG_NS,"circle").tagName).toBe("circle");
// SVG: camelCase preserved (linearGradient is the canonical example)
expect($tw.fakeDocument.createElementNS(SVG_NS,"linearGradient").tagName).toBe("linearGradient");
// SVG: already-uppercase input is also preserved (NOT lowercased)
expect($tw.fakeDocument.createElementNS(SVG_NS,"DIV").tagName).toBe("DIV");
// Empty namespace string is "no namespace", not HTML. Case preserved.
expect($tw.fakeDocument.createElementNS("","div").tagName).toBe("div");
});
it("tagName reflects current state without mutating it", function() {
// Reading tagName must not overwrite the internal `tag` field
var el = $tw.fakeDocument.createElement("div");
expect(el.tagName).toBe("DIV");
expect(el.tag).toBe("div");
// Idempotent: two reads return identical values
var first = el.tagName, second = el.tagName;
expect(first).toBe(second);
// Dynamic namespace change is reflected. The getter must read current
// state, not a value cached at construction time.
var dynamic = $tw.fakeDocument.createElement("foo");
expect(dynamic.tagName).toBe("FOO");
dynamic.namespaceURI = SVG_NS;
expect(dynamic.tagName).toBe("foo");
});
// Real CSSStyleDeclaration returns undefined for Symbol property keys.
// Without a guard, the TW_Style Proxy throws on Symbol access. This bites
// in practice when Jasmine pretty-prints fakedom elements on failure.
@@ -0,0 +1,173 @@
/*\
title: test-filesystem-adversarial.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Adversarial tests for generateTiddlerFilepath: hostile titles and recorded
originalpaths (path traversal, absolute paths, control codes, reserved device
names, over-long and non-ASCII input) must still yield a filename valid on every
supported OS, without throwing or leaking a forbidden character. Only the
filename is sanitised; the tiddler title itself is never altered.
generateTiddlerFilepath is node-only (utils-node), so reproduce from the CLI, not
the browser. Save as probe.js in the repo root and run `node probe.js`:
var path = require("path");
var $tw = require("./boot/boot.js").TiddlyWiki();
$tw.boot.argv = ["./editions/test"];
$tw.boot.boot(function() {
var dir = path.resolve("/tmp/tw5-probe");
function base(t) { return path.basename($tw.utils.generateTiddlerFilepath(t,{extension: ".tid", directory: dir, fileInfo: {}})); }
console.log(base("con")); // expected: _con_.tid (reserved device name wrapped)
console.log(base("a<b>c")); // expected: a_b_c.tid (forbidden chars replaced)
console.log(base("trailing ")); // expected: trailing_.tid (trailing space replaced)
process.exit(0);
});
Control codes in the specs are built with String.fromCharCode so this source
stays pure ASCII. All path-logic tests use the real-save default overwrite:false.
\*/
"use strict";
if($tw.node) {
var fs = require("fs");
var path = require("path");
describe("generateTiddlerFilepath (adversarial)", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem-adv");
beforeEach(function() {
fs.rmSync(directory,{recursive: true, force: true});
fs.mkdirSync(directory,{recursive: true});
});
afterAll(function() {
fs.rmSync(directory,{recursive: true, force: true});
});
function fromTitle(title) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
fileInfo: {}
});
}
function fromOriginalpath(originalpath) {
return $tw.utils.generateTiddlerFilepath("plain-title",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: originalpath}
});
}
// A saved path segment must contain no character Windows forbids in a
// filename and no control code. "/" and "\" are separators between
// segments; "." and ".." are dot segments, not filenames.
function hasControlCode(seg) {
for(var i = 0; i < seg.length; i++) {
if(seg.charCodeAt(i) < 0x20) {
return true;
}
}
return false;
}
function eachSegment(result,callback) {
path.relative(directory,result).split(/[\\/]/).forEach(function(seg) {
if(seg !== "" && seg !== "." && seg !== "..") {
callback(seg);
}
});
}
function expectCleanSegments(result) {
eachSegment(result,function(seg) {
expect(seg).not.toMatch(/[<>:"|?*]/); // Windows-forbidden chars
expect(hasControlCode(seg)).toBe(false); // control codes
expect(seg).not.toMatch(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i); // reserved device name
expect(seg).not.toMatch(/[. ]$/); // trailing dot or space
});
}
// Control codes are built at runtime so the source stays pure ASCII.
var NUL = String.fromCharCode(0), BEL = String.fromCharCode(7), US = String.fromCharCode(0x1f);
// Battery of hostile inputs, fed through both the title and originalpath
// channels.
var hostile = [
"../../../etc/passwd",
"..\\..\\evil",
"a<b>c:d\"e|f?g*h",
"a" + NUL + "b" + BEL + "c" + US + "d",
"con", "PRN", "nul", "COM1", "LPT9",
"a/nul/b",
" leading",
"trailing ",
"dots...",
"café naïve",
"中文テスト",
new Array(400).join("z"),
"<>:\"|?*",
"//..//..//",
"a/b\\c/d",
"",
".",
"..",
"::::",
" "
];
it("never leaks a forbidden character or throws, for any hostile input", function() {
hostile.forEach(function(input) {
[fromTitle, function(t) { return fromOriginalpath(t + ".tid"); }].forEach(function(fn) {
var result;
expect(function() { result = fn(input); }).not.toThrow();
expect(typeof result).toBe("string");
expect(result.length).toBeGreaterThan(0);
expectCleanSegments(result);
});
});
});
it("replaces control codes and NUL in the filename with underscore", function() {
expect(path.basename(fromTitle("a" + NUL + "b" + US + "c"))).toBe("a_b_c.tid");
});
it("wraps a reserved Windows device name used as the whole title", function() {
expect(path.basename(fromTitle("CON"))).toBe("_CON_.tid");
expect(path.basename(fromTitle("nul"))).toBe("_nul_.tid");
expect(path.basename(fromTitle("COM1"))).toBe("_COM1_.tid");
});
it("bounds the filename length for an over-long title", function() {
var result = fromTitle(new Array(400).join("z"));
expect(path.basename(result).length).toBeLessThanOrEqual(200 + ".tid".length);
});
it("transliterates non-ASCII to ASCII in every segment", function() {
expect(path.basename(fromTitle("café"))).toBe("cafe.tid");
eachSegment(fromOriginalpath("café/naïve.tid"),function(seg) {
for(var i = 0; i < seg.length; i++) {
expect(seg.charCodeAt(i)).toBeLessThan(128);
}
});
});
it("gives dots-only titles a valid, distinct, non-empty filename", function() {
// "." and ".." are legal tiddler titles but not legal bare filenames.
// The contract is a non-empty, forbidden-char-free name that is not a
// hidden dotfile, and two different titles must not map to one file.
// The exact fallback encoding is an implementation detail, not asserted.
var dot = path.basename(fromTitle("."));
var dotdot = path.basename(fromTitle(".."));
[dot,dotdot].forEach(function(name) {
expect(name.length).toBeGreaterThan(".tid".length);
expect(name).not.toMatch(/[<>:"|?*]/);
expect(name).not.toMatch(/^\.+/);
});
expect(dot).not.toBe(dotdot);
});
});
}
@@ -0,0 +1,362 @@
/*\
title: test-filesystem.js
type: application/javascript
tags: [[$:/tags/test-spec]]
Tests for the $:/core-server filesystem utilities: generateTiddlerFilepath,
generateTiddlerFileInfo and generateTiddlerExtension. These are node-only
(module-type utils-node), so they run under `npm test` and are absent in the
browser: reproduce them from the CLI, not the F12 console.
Reproduce a case by hand. Save as probe.js in the repo root, run `node probe.js`,
and swap in the input and expected value from any spec below:
var path = require("path");
var $tw = require("./boot/boot.js").TiddlyWiki();
$tw.boot.argv = ["./editions/test"];
$tw.boot.boot(function() {
var dir = path.resolve("/tmp/tw5-probe");
var r = $tw.utils.generateTiddlerFilepath("a>b",{extension: ".tid", directory: dir, fileInfo: {}});
console.log(path.basename(r)); // expected: a_b.tid
process.exit(0);
});
Reproduce the real client/server save (the retain-original-tiddler-path case).
From the repo root run:
node ./tiddlywiki.js ./editions/tw5.com-server --listen
then in the browser edit a tiddler whose file lives in a subfolder, for example
examples/ButtonWidget/Popup.tid, save, and confirm on disk that the file stays in
examples/ButtonWidget/ rather than being flattened to the tiddlers root.
\*/
"use strict";
if($tw.node) {
var fs = require("fs");
var path = require("path");
describe("generateTiddlerFilepath", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem");
// Real saves use overwrite:false, which runs the uniquifier and probes the
// filesystem. Start every test from an empty directory so the computed
// filenames are deterministic. Only the dedicated overwrite test below
// passes overwrite:true.
beforeEach(function() {
fs.rmSync(directory,{recursive: true, force: true});
fs.mkdirSync(directory,{recursive: true});
});
afterAll(function() {
fs.rmSync(directory,{recursive: true, force: true});
});
// Characters illegal in a filename on at least one supported OS (Windows
// forbids < > : " / \ | ? *). generateTiddlerFilepath replaces each with
// "_" so a tiddler file committed on one OS still checks out on every
// other: an unsanitised "a>b.tid" would break `git checkout` on Windows.
// Only the filename is sanitised; the tiddler title keeps the character.
var forbiddenChars = [
["<","less-than"],
[">","greater-than"],
["~","tilde"],
[":","colon"],
["\"","double-quote"],
["|","pipe"],
["?","question-mark"],
["*","asterisk"],
["^","caret"],
["\\","backslash"]
];
// Title channel: a title is plain text, so its slashes and backslashes are
// not directory separators and every forbidden character is replaced.
function filepathFromTitle(title) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
fileInfo: {}
});
}
// OTP channel: originalpath ($:/config/OriginalTiddlerPaths) is a real
// recorded relative location. Its separators are preserved; a forbidden
// character inside a segment is still replaced. A distinct title proves the
// filename derives from originalpath, not the title.
function filepathFromOriginalpath(originalpath) {
return $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: originalpath}
});
}
// FileSystemPaths channel: a $:/config/FileSystemPaths filter builds the
// path from the title, so a backslash leaking in from the title must be
// sanitised. "[[...]]" echoes the literal.
function filepathFromPathFilter(title,filter) {
return $tw.utils.generateTiddlerFilepath(title,{
extension: ".tid",
directory: directory,
wiki: $tw.wiki,
pathFilters: [filter],
fileInfo: {}
});
}
forbiddenChars.forEach(function(entry) {
var char = entry[0], name = entry[1];
it("replaces " + name + " (" + char + ") in a title with underscore", function() {
var result = filepathFromTitle("a" + char + "b");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("a_b.tid");
});
});
it("replaces every forbidden char in a dense title", function() {
// Prefix "x" so the result is not all underscores, which would trigger
// the charcode-fallback branch in generateTiddlerFilepath.
var title = "x" + forbiddenChars.map(function(e) { return e[0]; }).join("");
var expected = "x" + forbiddenChars.map(function() { return "_"; }).join("");
var result = filepathFromTitle(title);
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe(expected + ".tid");
});
it("does not overwrite a different tiddler that sanitises to the same filename", function() {
// "a>b" and "a/b" are distinct titles that both sanitise to "a_b.tid".
// With overwrite off (the real-save default) the second file gets a
// uniquifier ("_1") rather than clobbering the first.
fs.writeFileSync(path.resolve(directory,"a_b.tid"),"");
var result = filepathFromTitle("a/b");
expect(path.basename(result)).toBe("a_b_1.tid");
});
it("reuses the target filename when overwrite is set (the --save path)", function() {
// overwrite:true (used by the --save command) writes a specific file
// even if it already exists, so no uniquifier is added. This is the
// only test allowed to pass overwrite:true.
fs.writeFileSync(path.resolve(directory,"a_b.tid"),"");
var result = $tw.utils.generateTiddlerFilepath("a/b",{
extension: ".tid",
directory: directory,
fileInfo: {overwrite: true}
});
expect(path.basename(result)).toBe("a_b.tid");
});
it("sanitises a backslash from a FileSystemPaths filter without making a subdir (#9814)", function() {
// A title such as "Pragma: \define" reaching the filename via a
// FileSystemPaths filter must not act as a Windows path separator.
var result = filepathFromPathFilter("Pragma: \\define","[[Pragma: \\define]]");
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).toBe("Pragma_ _define.tid");
});
it("keeps the subdirectory of a retained originalpath (native separators)", function() {
// Regression: on Windows path.relative yields backslash separators, so
// originalpath is e.g. "examples\ButtonWidget\Popup.tid". They must be
// preserved so the tiddler saves back to its folder rather than being
// flattened to "examples_ButtonWidget_Popup.tid" at the tiddlers root.
var original = ["examples","ButtonWidget","Popup"].join(path.sep) + ".tid";
var result = filepathFromOriginalpath(original);
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("keeps the subdirectory of a retained originalpath (forward slashes)", function() {
var result = filepathFromOriginalpath("examples/ButtonWidget/Popup.tid");
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("still sanitises a forbidden char inside an originalpath segment", function() {
// ">" is a legal filename char on Linux and macOS but forbidden on
// Windows. The sanitiser always targets the strictest OS, so a file
// committed on Linux still checks out on Windows: an unsanitised
// "a>b.tid" would break `git checkout` there and brick the clone.
// The ">" is replaced while the "sub/" directory is kept.
var result = filepathFromOriginalpath("sub/a>b.tid");
expect(path.dirname(result)).toBe(path.resolve(directory,"sub"));
expect(path.basename(result)).toBe("a_b.tid");
});
it("replaces trailing dots and spaces in a filename", function() {
// Windows silently strips a trailing dot or space from a filename, so
// they are replaced to keep the name stable across platforms, in a
// title and inside an originalpath segment alike.
expect(path.basename(filepathFromTitle("foo."))).toBe("foo_.tid");
expect(path.basename(filepathFromTitle("foo "))).toBe("foo_.tid");
expect(path.basename(filepathFromOriginalpath("sub/foo..tid"))).toBe("foo_.tid");
});
it("uses a tiddlywiki.files pinned path verbatim, skipping sanitisation", function() {
// A pinFilepath (tiddlywiki.files) path is author-controlled and
// trusted: separators are kept and the forbidden-char sanitiser is
// skipped, so a tilde (which it would otherwise replace) survives.
var result = $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
fileInfo: {pinFilepath: true, originalpath: "pinned/a~b.tid"}
});
expect(path.dirname(result)).toBe(path.resolve(directory,"pinned"));
expect(path.basename(result)).toBe("a~b.tid");
});
it("falls back to originalpath when no FileSystemPaths filter matches", function() {
// A filter that selects nothing for this tiddler leaves the recorded
// originalpath in charge of the location.
var result = $tw.utils.generateTiddlerFilepath("WrongIfUsed",{
extension: ".tid",
directory: directory,
wiki: $tw.wiki,
pathFilters: ["[tag[__no_such_tag__]]"],
fileInfo: {originalpath: "examples/ButtonWidget/Popup.tid"}
});
expect(path.dirname(result)).toBe(path.resolve(directory,"examples","ButtonWidget"));
expect(path.basename(result)).toBe("Popup.tid");
});
it("replaces leading spaces in a title with underscores", function() {
expect(path.basename(filepathFromTitle(" foo"))).toBe("__foo.tid");
});
it("replaces leading dots so the file is not hidden on unix", function() {
expect(path.basename(filepathFromTitle(".hidden"))).toBe("_hidden.tid");
expect(path.basename(filepathFromTitle("..twodots"))).toBe("__twodots.tid");
});
it("replaces trailing dots or spaces in the extension", function() {
var result = $tw.utils.generateTiddlerFilepath("foo",{
extension: ".tid.",
directory: directory,
fileInfo: {}
});
expect(path.basename(result)).toBe("foo.tid_");
});
it("truncates an over-long extension to 32 characters", function() {
var longExt = "." + new Array(50).join("x");
var result = $tw.utils.generateTiddlerFilepath("foo",{
extension: longExt,
directory: directory,
fileInfo: {}
});
expect(path.basename(result)).toBe("foo" + longExt.substr(0,32));
});
it("does not double the extension when the title already ends in it", function() {
// "notes.tid" with extension ".tid" must yield "notes.tid", not
// "notes.tid.tid".
expect(path.basename(filepathFromTitle("notes.tid"))).toBe("notes.tid");
});
it("keeps its own filename when an existing tiddler is re-saved", function() {
// The tiddler already owns "a_b.tid" (fileInfo.filepath). Re-saving
// reuses it instead of appending a "_1" uniquifier, which would
// otherwise orphan a copy on every save.
var owned = path.resolve(directory,"a_b.tid");
fs.writeFileSync(owned,"");
var result = $tw.utils.generateTiddlerFilepath("a/b",{
extension: ".tid",
directory: directory,
fileInfo: {filepath: owned}
});
expect(path.basename(result)).toBe("a_b.tid");
});
it("URI-encodes the filename after a write error", function() {
// fileInfo.writeError forces the encoded fallback so a retry can write
// a name the filesystem will accept.
var result = $tw.utils.generateTiddlerFilepath("plain",{
extension: ".tid",
directory: directory,
fileInfo: {writeError: true}
});
expect(path.dirname(result)).toBe(directory);
expect(path.basename(result)).not.toBe("plain.tid");
expect(path.basename(result)).toContain("plain.tid");
});
it("folds a path that escapes all allowed roots back into the directory", function() {
// An originalpath resolving outside directory / wikiTiddlersPath is
// encoded into a single filename inside directory rather than escaping.
var result = $tw.utils.generateTiddlerFilepath("plain",{
extension: ".tid",
directory: directory,
fileInfo: {originalpath: "../../../outside/evil.tid"}
});
expect(path.dirname(result)).toBe(directory);
});
});
describe("generateTiddlerFileInfo", function() {
var directory = path.resolve("/tmp/tw5-test-filesystem");
function fileInfoFor(fields,existing) {
return $tw.utils.generateTiddlerFileInfo(new $tw.Tiddler(fields),{
directory: directory,
wiki: $tw.wiki,
fileInfo: existing || {}
});
}
it("saves a wikitext tiddler as a single .tid file", function() {
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/vnd.tiddlywiki"});
expect(fi.type).toBe("application/x-tiddler");
expect(fi.hasMetaFile).toBe(false);
});
it("saves a non-wikitext tiddler as a body file plus a .meta file", function() {
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/plain"});
expect(fi.type).toBe("text/plain");
expect(fi.hasMetaFile).toBe(true);
});
it("saves a tiddler with an unsafe field value as JSON", function() {
// A field value with leading whitespace cannot round-trip through a
// .tid header, so the whole tiddler is written as JSON.
var fi = fileInfoFor({title: "Foo", text: "body", type: "text/vnd.tiddlywiki", custom: " leading"});
expect(fi.type).toBe("application/json");
expect(fi.hasMetaFile).toBe(false);
});
it("keeps a tiddler with a _canonical_uri as a .tid file", function() {
var fi = fileInfoFor({title: "Img", type: "image/png", _canonical_uri: "images/x.png"});
expect(fi.type).toBe("application/x-tiddler");
});
it("propagates isEditableFile and originalpath from the existing fileInfo", function() {
var fi = fileInfoFor({title: "Foo", text: "b", type: "text/vnd.tiddlywiki"},{isEditableFile: true, originalpath: "sub/Foo.tid"});
expect(fi.isEditableFile).toBe(true);
expect(fi.originalpath).toBe("sub/Foo.tid");
});
});
describe("generateTiddlerExtension", function() {
it("returns the extension from the first matching extFilter", function() {
var ext = $tw.utils.generateTiddlerExtension("Foo",{
extFilters: ["[[Foo]then[.md]]"],
wiki: $tw.wiki
});
expect(ext).toBe(".md");
});
it("returns undefined when no extFilter matches", function() {
var ext = $tw.utils.generateTiddlerExtension("Foo",{
extFilters: ["[tag[__no_such_tag__]then[.md]]"],
wiki: $tw.wiki
});
expect(ext).toBeUndefined();
});
});
}
+11 -1
View File
@@ -940,10 +940,20 @@ describe("Filter tests", function() {
it("should handle the sortby operator", function() {
expect(wiki.filterTiddlers("a b c +[sortby[d e]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("a b c +[sortby[b c a]]").join(",")).toBe("b,c,a");
// By default titles missing from the reference list sort to the start
expect(wiki.filterTiddlers("aa a b c +[sortby[b c a cc]]").join(",")).toBe("aa,b,c,a");
expect(wiki.filterTiddlers("a bb b c +[sortby[b c a cc]]").join(",")).toBe("bb,b,c,a");
expect(wiki.filterTiddlers("a bb cc b c +[sortby[b c a cc]]").join(",")).toBe("bb,b,c,a,cc");
// The "end" suffix places missing titles after the listed ones
expect(wiki.filterTiddlers("aa a b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,aa");
expect(wiki.filterTiddlers("a bb b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,bb");
expect(wiki.filterTiddlers("a bb cc b c +[sortby:end[b c a cc]]").join(",")).toBe("b,c,a,cc,bb");
// Missing titles keep their input order. Avoid a repeated title here, it would be moved to the end of the input
expect(wiki.filterTiddlers("zz a yy b +[sortby:end[b a]]").join(",")).toBe("b,a,zz,yy");
// Two titles are the shortest input that is sorted rather than returned untouched
expect(wiki.filterTiddlers("a b +[sortby[b a]]").join(",")).toBe("b,a");
expect(wiki.filterTiddlers("a +[sortby:end[b a]]").join(",")).toBe("a");
expect(wiki.filterTiddlers("b a b c +[sortby[]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("b a b c +[sortby[a b b c]]").join(",")).toBe("a,b,c");
expect(wiki.filterTiddlers("b a b c +[sortby[b a c b]]").join(",")).toBe("b,a,c");
@@ -173,5 +173,20 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}format:json[ ]]")).toEqual(["{\n \"a\": \"one\",\n \"b\": \"\",\n \"c\": 1.618,\n \"d\": {\n \"e\": \"four\",\n \"f\": [\n \"five\",\n \"six\",\n true,\n false,\n null\n ]\n }\n}"]);
});
it("should support the makepatches operator with json output", function() {
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches::json[The fast brown fox]]")).toEqual([
'[{"type":"equal","text":"The "},{"type":"delete","text":"quick"},{"type":"insert","text":"fast"},{"type":"equal","text":" brown fox"}]'
]);
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words:json[The fast brown fox]]")).toEqual([
'[{"type":"equal","text":"The "},{"type":"delete","text":"quick "},{"type":"insert","text":"fast "},{"type":"equal","text":"brown fox"}]'
]);
// Safely ignores missing/invalid second suffix and returns a standard patch string instead of JSON
expect(wiki.filterTiddlers("[[The quick brown fox]makepatches:words[The fast brown fox]]")[0]).not.toContain(
'"type":"equal"'
);
});
});
+26 -3
View File
@@ -1,5 +1,5 @@
created: 20160107225427489
modified: 20211221102625141
modified: 20260907204047567
tags: Features
title: Modals
type: text/vnd.tiddlywiki
@@ -19,6 +19,29 @@ Note that the footer and subtitle fields are not limited to plain text, and wiki
Modals are displayed with the [[WidgetMessage: tm-modal]].
<$button message="tm-modal" param="SampleWizard">Open demo modal</$button>
<<.tip """<$macrocall $name=".from-version" version="5.2.4"/> allow using "mask-closable" field""">>
!! Examples
<<wikitext-example-without-html """<$button message="tm-modal" param="SampleWizard">Open demo modal</$button>""">>
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/rendered-text>> to display a modal if you do not want to create a separate tiddler:
<<wikitext-example-without-html """\procedure text()
<style class="headless-modal">
.tc-modal:has(.tc-modal-body .headless-modal) .tc-modal-header{display:none}
</style>
!! Motovun Jack
{{Motovun Jack.jpg}}
<$button message="tm-modal" param="SampleWizard">Open nested modal</$button>
\end
<$button>
<$action-sendmessage $message="tm-modal" $param="$:/core/templates/rendered-text" text=<<text>>/>
Click me!
</$button>""">>
@@ -1,5 +1,5 @@
created: 20230304160331362
modified: 20230304160332927
modified: 20260724082228670
tags: [[makepatches Operator]] [[applypatches Operator]] [[Operator Examples]]
title: makepatches and applypatches Operator (Examples)
type: text/vnd.tiddlywiki
@@ -40,4 +40,12 @@ The `lines` mode doesn't work as well in this application:
It is better suited as a very fast algorithm to detect line-wise incremental changes to texts and store only the changes instead of multiple versions of the whole texts.
The `json` suffix outputs a structured JSON array representing the differences instead of a patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (`::`).
<<.operator-example 8 "[{Hamlet##Shakespeare-old}makepatches::json{Hamlet##Shakespeare-new}]">>
The `json` suffix can also be combined with the `words` or `lines` modes:
<<.operator-example 9 "[{Hamlet##Shakespeare-old}makepatches:words:json{Hamlet##Shakespeare-new}]">>
</div>
@@ -9,3 +9,4 @@ type: text/vnd.tiddlywiki
<<.operator-example 1 "10 6 4 9 3 2 8 +[sortby[1 2 3 4 5 6 7 8 9 10]]">>
<<.operator-example 2 "Friday Tuesday Monday Thursday Sunday +[sortby{Days of the Week!!list}]">>
<<.operator-example 3 "1 Mon 5 Fri 4 Tue Sun 2 +[sortby{Days of the Week!!short}]">>
<<.operator-example 4 "1 Mon 5 Fri 4 Tue Sun 2 +[sortby:end{Days of the Week!!short}]">>
@@ -1,23 +1,28 @@
caption: makepatches
created: 20230304122354967
modified: 20230304122400128
op-purpose: returns a set of patches that transform the input to a given string
modified: 20260724081502905
op-input: a [[selection of titles|Title Selection]]
op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>>, or a structured JSON array representing the differences if the `json` suffix is used
op-parameter: a string of characters
op-parameter-name: S
op-output: a set of patch instructions per input title to be used by the [[applypatches Operator]] to transform the input title(s) into the string <<.place S>>
op-suffix: `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode. See notes below.
op-suffix-name: T
op-purpose: returns a set of patches that transform the input to a given string
op-suffix: optional suffixes specifying the diff mode and output format
op-suffix-name: T:F
tags: [[Filter Operators]] [[String Operators]]
title: makepatches Operator
type: text/vnd.tiddlywiki
<<.from-version "5.2.6">>
The <<.op makepatches>> operator uses up to two suffixes:
* <<.place T>> (diff mode): `lines` to operate in line mode, `words` to operate in word mode. If omitted (default), the algorithm operates in character mode.
* <<.place F>> (output format): <<.from-version "5.5.0">> `json` to output a structured JSON array of differences instead of a standard patch string. To use the JSON output with the default character mode, skip the first suffix with a double colon (e.g., `makepatches::json`).
The difference algorithm operates in character mode by default. This produces the most detailed diff possible. In `words` mode, each word in the input text is transformed into a meta-character, upon which the algorithm then operates. In the default character mode, the filter would find two patches between "ActionWidget" and "Action-Widgets" (the hyphen and the plural s), while in `words` mode, the whole word is found to be changed. In `lines` mode, the meta-character is formed from the whole line, delimited by newline characters, and is found to be changed independent of the number of changes within the line.
The different modes influence the result when the patches are applied to texts other than the original, as well as the runtime.
<<.tip "The calculation in `words` mode is roughly 10 times faster than the default character mode, while `lines` mode can be more than 100 times faster than the default.">>
<<.operator-examples "makepatches and applypatches">>
<<.operator-examples "makepatches and applypatches">>
@@ -1,11 +1,12 @@
caption: sortby
created: 20151017145021839
modified: 20151108052142057
modified: 20260811120000000
op-input: a list of items
op-output: all items sorted by lookup list
op-output: all items sorted by the order list, with unlisted items placed first, or last with the `end` suffix
op-parameter: a list specifying the order in which to sort the current list
op-parameter-name: order
op-purpose: sort the current list in the order of the list referenced in the parameter
op-suffix: <<.from-version "5.5.0">> optional: `end` to place items missing from <<.place order>> after the listed ones. If omitted they are placed first
tags: [[Filter Operators]] [[Order Operators]] [[Listops Operators]]
title: sortby Operator
type: text/vnd.tiddlywiki
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 50 KiB

@@ -1,6 +1,6 @@
caption: tm-download-file
created: 20140811112201235
modified: 20230723214745520
modified: 20260910134915210
tags: Messages
title: WidgetMessage: tm-download-file
type: text/vnd.tiddlywiki
@@ -19,4 +19,41 @@ The following variable names have special behaviour:
The download file message is usually generated with the ButtonWidget.
The download file message is handled by the TiddlyWiki core SyncMechanism which invokes the current [[SaverModule|SaverModules]].
The download file message is handled by the TiddlyWiki core [[SyncMechanism|https://tiddlywiki.com/dev/#SyncAdaptorModules]] which invokes the current [[SaverModule|https://tiddlywiki.com/dev/#Saver]].
! Examples
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/plain-text>> to create a custom download configuration without creating additional tiddlers:
<<wikitext-example-without-html """\function field.value() [<pluginTitle>get{!!title}]
\function json.plugin()
[<pluginTitle>fields[]]:reduce[<accumulator>jsonset{!!title},<field.value>]=>pluginData
"[]"+[jsonset:json[0],<pluginData>]
\end
\define externalPlugin()
{
const tw = globalThis.$tw ??= Object.create(null);
tw.preloadTiddlers = (tw.preloadTiddlers ?? []).concat($(json.plugin)$);
}
\end
<$let pluginTitle="$:/plugins/tiddlywiki/menubar" text=<<externalPlugin>> >
<$button tooltip="Export standalone plugin">{{$:/core/images/export-button}} export <<pluginTitle>> as standalone plugin
<$action-sendmessage
$message="tm-download-file"
$param="$:/core/templates/plain-text"
text=<<text>>
filename={{{ [<pluginTitle>search-replace:g[/],[_]addsuffix[.js]] }}}
type="text/javascript"
/>
</$button>
You can load the exported plugin by creating a tiddler with the tag <<.tag $:/tags/RawMarkupWikified/TopHead>> containing this:
```
`<script src="URL_OF_YOUR_PLUGIN_HERE" onerror="alert('Error: Cannot load YOUR_PLUGIN located at URL_OF_YOUR_PLUGIN_HERE');"></script>`
```""">>
@@ -1,6 +1,6 @@
caption: tm-notify
created: 20140811112304772
modified: 20230723220728382
modified: 20260907202408431
tags: Messages
title: WidgetMessage: tm-notify
type: text/vnd.tiddlywiki
@@ -12,3 +12,28 @@ The notify message briefly displays a specified tiddler as a small alert in the
|//{any other params}// |Any other parameters are made available as variables to the notify message. |
The notify message is handled by the TiddlyWiki core.
! Examples
<<.from-version "5.5.0">> you can use the template <<.tid $:/core/templates/plain-text>> to display simple text in a notification:
<<wikitext-example-without-html """\procedure notify()
<$action-sendmessage $message="tm-notify" $param="$:/core/templates/plain-text" text={{!!count}} />
\end notify
<$button set="!!count" setTo={{{ [{!!count}add[1]] }}} tooltip="count" actions="<<notify>>" >
Count
</$button>""">>
You can also use the template $:/core/templates/rendered-text to display wikitext:
<<wikitext-example-without-html """\procedure notify()
\procedure template()
Current time is <<now>>
\end template
<$action-sendmessage $message="tm-notify" $param="$:/core/templates/rendered-text" text=<<template>> />
\end notify
<$button actions="<<notify>>" >
What time is it?
</$button>""">>
@@ -0,0 +1,8 @@
title: $:/changenotes/5.4.0/#9715
change-type: performance
change-category: filters
tags: $:/tags/ChangeNote
github-contributors: linonetwo
release: 5.4.0
description: Optimized tag[] and !tag[] filter operators to use Set for O(1) lookup, matching search:tags[] performance.
github-links: [[https://github.com/Jermolene/TiddlyWiki5/pull/9715]]
@@ -0,0 +1,22 @@
title: $:/changenotes/5.5.0/#10003
description: Fix race condition in plugin library iframe message handling
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: plugin
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10003
github-contributors: DesignThinkerer
Introduce a ''PLUGIN-LIBRARY-READY'' handshake verb for plugin library iframes.
This ensures that postMessage commands are not prematurely dropped before the iframe's
internal script has fully bound its message listeners.
Retains iframe.onload as a backward-compatible fallback for legacy or external libraries
that do not emit the ready signal.
Previously, the browser-messaging.js relied entirely on iframe.onload to trigger the initial
postMessage call to a plugin library. However, if the iframe's connection or load is aborted
after its internal script has already run and bound window.addEventListener("message"), onload
never fires at all, so the initial postMessage call is never sent and the request is silently
lost with no error. The new handshake eliminates this by having the iframe actively confirm
readiness as soon as its listener is bound, independent of whether onload ever fires.
@@ -0,0 +1,34 @@
title: $:/changenotes/5.5.0/#10014
created: 20260906223932000
modified: 20260906223932000
description: Variable names may contain a colon in call and multi-valued variable syntax
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: hackability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10014
github-contributors: pmario
A variable name may contain a colon in the `<<...>>` call syntax: standalone, with named parameters using either separator, as a nested call, and as a widget attribute value.
```
\procedure obj:plain() Hello
\procedure greet:person(name) Hello <<name>>
\procedure obj:host(a) [<<a>>]
<<obj:plain>>
<<greet:person name:"world">>
<<greet:person name="world">>
<<obj:host a=<<obj:plain>>>>
<div data-x=<<obj:plain>> ></div>
```
An `((...))` multi-valued variable reference used as a widget attribute value accepts the same names as the inline syntax:
```
<$let my:list={{{ [range[3]] }}}>
<$text text=((my:list))/>
</$let>
```
Fixed issue: [[#10013|https://github.com/TiddlyWiki/TiddlyWiki5/issues/10013]]
@@ -0,0 +1,12 @@
change-category: internal
change-type: feature
created: 20260906234108933
description: Added new $:/core/templates/plain-text core template for rendering raw variables
github-contributors: DesignThinkerer
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10017
modified: 20260906234439533
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#10017
Added two new core template, `$:/core/templates/plain-text` and `$:/core/templates/rendered-text` that renders respectively a single `text` variable as plain text or wikitext. This allows to export and download dynamic strings via messages like `tm-download-file`, without creating temporary tiddlers in the wiki store.
@@ -0,0 +1,12 @@
change-category: internal
change-type: bugfix
created: 20260907175854854
description: Fixed a DOM leak when closing the plugin library
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/10018
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#10018
type: text/vnd.tiddlywiki
github-contributors: DesignThinkerer
Fixed a syntax error in the `unloadIFrame` loop that permanently left a hidden `<iframe library="true">` attached to the DOM every time the plugin library modal was closed.
@@ -0,0 +1,12 @@
change-category: widget
change-type: enhancement
created: 20260711020921000
description: The diff-text widget shows removed or added newlines with a visible ↲ glyph
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9736
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9736
type: text/vnd.tiddlywiki
* The diff-text widget marks newline characters in a diff with the visible `↲` glyph (U+21B2) instead of an invisible character, so line break changes are recognisable in the rendered diff (fixes [[Issue #9461|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9461]])
@@ -0,0 +1,12 @@
change-category: translation
change-type: enhancement
created: 20260711020729000
description: The "Click to generate wiki info" button in the control panel is now translatable
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9737
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9737
type: text/vnd.tiddlywiki
* The "Click to generate wiki info" button on the control panel's wiki information tab uses a language string instead of hard coded English text, so translators can localise it (fixes [[Issue #9654|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9654]])
@@ -0,0 +1,13 @@
change-category: hackability
change-type: bugfix
created: 20260711181412000
description: Empty code blocks parse cleanly; a closing fence only counts alone on its line
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9739
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9739
type: text/vnd.tiddlywiki
* An empty code block such as ```` ```bash ```` directly followed by the closing fence parses as an empty codeblock instead of swallowing the rest of the tiddler (fixes [[Issue #9047|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9047]])
* A closing fence only ends the block when it stands alone on its line, so code lines ending in three backticks stay inside the block
@@ -0,0 +1,12 @@
change-category: hackability
change-type: bugfix
created: 20260711185128000
description: $tw.utils.pulseElement() works again and the vanilla theme defines the tc-pulse class
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9741
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9741
type: text/vnd.tiddlywiki
* `$tw.utils.pulseElement()` listens for the standard `animationend` event instead of the removed `$tw.browser.animationEnd`, so the pulse animation runs and cleans up its CSS class again, and the vanilla theme defines the missing `tc-pulse` class and animation (fixes [[Issue #6835|https://github.com/TiddlyWiki/TiddlyWiki5/issues/6835]])
@@ -0,0 +1,12 @@
change-category: nodejs
change-type: bugfix
created: 20260711020057000
description: The Node.js server no longer emits the DEP0169 url.parse() deprecation warning
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9742
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9742
type: text/vnd.tiddlywiki
* The server module parses request URLs with the WHATWG `URL` API instead of the deprecated `url.parse()`, so recent Node.js versions no longer print the DEP0169 deprecation warning on startup (fixes [[Issue #9628|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9628]])
@@ -0,0 +1,12 @@
change-category: filters
change-type: enhancement
created: 20260711015656000
description: sortby accepts an optional end suffix to place titles missing from the reference list after the listed ones
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9747
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9747
type: text/vnd.tiddlywiki
* The [[sortby|sortby Operator]] filter operator now accepts an optional `end` suffix that places input titles missing from the reference list after the listed ones. The default is unchanged, so with a list `Alpha Beta Gamma` the input `Delta Epsilon Alpha Beta Gamma` still sorts to `Delta Epsilon Alpha Beta Gamma`, while `sortby:end[...]` sorts it to `Alpha Beta Gamma Delta Epsilon` (addresses [[Issue #8342|https://github.com/TiddlyWiki/TiddlyWiki5/issues/8342]])
@@ -0,0 +1,14 @@
change-category: nodejs
change-type: bugfix
created: 20260711175727000
description: Keep retained subdirectory tiddlers and sanitise file names per path segment
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9815 https://github.com/TiddlyWiki/TiddlyWiki5/pull/9944
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9815
type: text/vnd.tiddlywiki
* A tiddler saved with `retain-original-tiddler-path` keeps its recorded subdirectory instead of being flattened to the tiddlers root on Windows
* File names are sanitised per path segment, so a forbidden character or reserved device name is replaced at every level (for example `sub/CON`), and a title containing a backslash such as `pragma: \define` no longer acts as a Windows path separator (fixes [[Issue #9814|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9814]])
* Tiddler files declared in `tiddlywiki.files` are pinned to their original location and skip the `$:/config/FileSystemPaths` filters and forbidden-character sanitising
@@ -0,0 +1,10 @@
title: $:/changenotes/5.5.0/#9816
description: Replaces some wikify widget with call dynamic syntax
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9816
github-contributors: Leilei332
Replaces unnecessary wikify widget usage with call dynamic attribute syntax.
@@ -0,0 +1,13 @@
change-category: internal
change-type: bugfix
created: 20260711014241000
description: fakedom tagName is uppercase for HTML elements, matching the DOM spec
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9843
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9843
type: text/vnd.tiddlywiki
* `$tw.fakeDocument` elements now report `tagName` in uppercase for HTML elements, matching the DOM specification and real browsers; code that compares fakedom `tagName` against lowercase strings must be updated
* Creating an element with an empty or null namespace now produces a plain element without a namespace
@@ -0,0 +1,10 @@
title: $:/changenotes/5.5.0/#9879
description: Fix field-name inputs re-render
tags: $:/tags/ChangeNote
release: 5.5.0
change-type: bugfix
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9879
github-contributors: BurningTreeC
Fixes the field-name input re-rendering and therefor loosing focus when inserting a field-name that already exists
@@ -0,0 +1,10 @@
title: $:/changenotes/5.4.1/#9889
description: Make tags in Edit Mode animate instantly when removing
tags: $:/tags/ChangeNote
release: 5.5.0
change-type: bugfix
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9889
github-contributors: BurningTreeC
Fixes an issue where tags of Tiddlers in Edit Mode don't animate instantly when they get removed
@@ -0,0 +1,10 @@
title: $:/changenotes/5.5.0/#9891
description: Move background action log inside platforms check to avoid spurious server-side logs
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9891
github-contributors: linonetwo
Background actions with `platforms: browser` were logging on the server even though execution was correctly skipped. The log now only prints when the action actually runs.
@@ -0,0 +1,12 @@
change-category: filters
change-type: bugfix
created: 20260711011920000
description: backlinks[] and backtranscludes[] no longer report edited system tiddlers as sources
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9917
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9917
type: text/vnd.tiddlywiki
* Editing a system tiddler that links or transcludes another tiddler no longer adds it as a [[backlinks|backlinks Operator]] or [[backtranscludes|backtranscludes Operator]] source; the incremental back-index update now skips system tiddlers like the initial scan always did
@@ -0,0 +1,13 @@
title: $:/changenotes/5.5.0/#9919
created: 20260712000000000
modified: 20260712000000000
description: Fix the CSV parser returning every cell empty
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: bugfix
change-category: internal
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9919
github-contributors: joshuafontany
type: text/vnd.tiddlywiki
`getCellInfo` read its loop variable before the loop declared it, so hoisting made it undefined and the parser tested the first character of the whole text rather than the first character of the cell. Any CSV opening with a separator therefore returned every cell empty, and the table rendered blank.
@@ -0,0 +1,10 @@
title: $:/changenotes/5.5.0/#9933
description: Update Chinese translations
release: 5.5.0
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: translation
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9933
github-contributors: BramChen
* Add `WikiInformation/Generate/Caption` in `ControlPanel.multids`
@@ -0,0 +1,12 @@
change-category: filters
change-type: enhancement
created: 20260724080519370
description: The makepatches operator can now output structured JSON
github-contributors: DesignThinkerer
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9940
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9940
type: text/vnd.tiddlywiki
* The [[makepatches|makepatches Operator]] operator now supports a `:json` suffix (e.g., `makepatches::json`) that outputs a structured JSON array of the differences, enabling easier and robust parsing with WikiText
@@ -0,0 +1,13 @@
change-category: filters
change-type: enhancement
created: 20260726124618748
description: Optimizes listops filter operators
github-contributors: saqimtiaz
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9942
modified: 20260726125232992
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9942
type: text/vnd.tiddlywiki
* The listops filter operators have been rewritten using more modern JavaScript (ES2017). The `unique` and `remove` operators have improved performance in certain circumstances.
@@ -0,0 +1,13 @@
change-category: filters
change-type: enhancement
created: 20260726124618748
description: Optimizes filterrun prefixes and select filters using modern JavaScript.
github-contributors: saqimtiaz
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9943
modified: 20260726130207664
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9943
type: text/vnd.tiddlywiki
* The `intersection` and `sort` filterrun prefixes, and the `prefix`, `sortsub`, `subfilter` and `suffix` operators have been optimized using more modern JavaScript (ES2017).
@@ -0,0 +1,26 @@
change-category: widget
change-type: bugfix
created: 20260727171657000
description: The button widget applies selectedClass when the state is addressed with setTitle
github-contributors: pmario
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9951
release: 5.5.0
tags: $:/tags/ChangeNote
title: $:/changenotes/5.5.0/#9951
type: text/vnd.tiddlywiki
* Buttons sharing a state tiddler through `setTitle` highlight the selected one. Only the button whose `setTo` matches the state gets the `selectedClass`
* The highlight follows the state as it changes
* `default` picks the highlighted button while the state tiddler is still missing
* The state is the tiddler text. Use `setField` or `setIndex` to address a field or an index instead
```
<div class="tc-tab-buttons">
<$button setTitle="$:/state/tab" setTo="Alpha" default="Alpha" selectedClass="tc-tab-selected">Alpha</$button>
<$button setTitle="$:/state/tab" setTo="Beta" default="Alpha" selectedClass="tc-tab-selected">Beta</$button>
</div>
```
The ButtonWidget documentation gained two missing rules. `setField` takes preference over `setIndex`. `selectedClass` and `default` apply to `setTitle`, not only to `set`.
Fixed issues: [[#9949|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9949]] and [[#9950|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9950]]
@@ -0,0 +1,10 @@
title: $:/changenotes/5.4.1/#9958
description: Fix typo in CSS custom properties
tags: $:/tags/ChangeNote
release: 5.5.0
change-type: bugfix
change-category: usability
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9958
github-contributors: BurningTreeC
Fixes a typo in the CSS custom properties
@@ -0,0 +1,14 @@
title: $:/changenotes/5.5.0/#9965
created: 20260812171534862
modified: 20260812171534862
tags: $:/tags/ChangeNote
change-type: enhancement
change-category: usability
description: Change of the class attribute of editors does no more completely refresh the widget
release: 5.5.0
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9965
github-contributors: BurningTreeC
type: text/vnd.tiddlywiki
This pull request changes the way editors refresh when their `class` attribute changes.
They did refresh the widget completely. Now they just assign the new classes.
@@ -0,0 +1,13 @@
caption: 5.5.0
created: 20260710103826404
modified: 20260710103826404
tags: ReleaseNotes
title: Release 5.5.0
type: text/vnd.tiddlywiki
description: Under development
\procedure release-introduction()
Release v5.5.0 is under development.
\end release-introduction
<<releasenote 5.5.0>>
@@ -30,12 +30,12 @@ The content of the `<$button>` widget is displayed within the button.
|param |The optional parameter to the message |
|set |A TextReference to which a new value will be assigned |
|setTitle |A title to which a new value will be assigned, ''without'' TextReference. Gets preferred over <<.attr set>> |
|setField |A ''field name'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Defaults to the ''text'' field |
|setIndex |An ''index'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present |
|setField |A ''field name'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Defaults to the ''text'' field. Takes preference over <<.attr setIndex>> |
|setIndex |An ''index'' to which the new value will be assigned, if the attribute <<.attr setTitle>> is present. Ignored if <<.attr setField>> is also present |
|setTo |The new value to assign to the TextReference identified in the `set` attribute or the text field / the field specified through <<.attr setField>> / the index specified through <<.attr setIndex>> of the title given through <<.attr setTitle>> |
|selectedClass |An optional additional CSS class to be assigned if the popup is triggered or the tiddler specified in <<.attr set>> already has the value specified in <<.attr setTo>> |
|selectedClass |An optional additional CSS class to be assigned if the popup is triggered or the state identified by <<.attr set>> or <<.attr setTitle>> already has the value specified in <<.attr setTo>> |
|selectedAria |<<.from-version "5.4.0">> An ARIA attribute to be set to `true` or `false` when <<.attr selectedClass>> is defined. Allowed values are `aria-checked` (default), `aria-selected` and `aria-pressed` |
|default |Default value if <<.attr set>> tiddler is missing for testing against <<.attr setTo>> to determine <<.attr selectedClass>> |
|default |Default value if the state identified by <<.attr set>> or <<.attr setTitle>> is missing for testing against <<.attr setTo>> to determine <<.attr selectedClass>> |
|popup |Title of a state tiddler for a popup that is toggled when the button is clicked. See PopupMechanism for details |
|popupTitle |Title of a state tiddler for a popup that is toggled when the button is clicked. In difference to the <<.attr popup>> attribute, ''no'' TextReference is used. See PopupMechanism for details |
|popupAbsCoords |<<.from-version "5.2.4">> If set to ''yes'' writes absolute coordinates to the tiddler referenced by the <<.attr popup>>. If set to ''no'' (the default) uses relative coordinates. See [[Coordinate Systems]] for details |

Some files were not shown because too many files have changed in this diff Show More