From 856057be81ecad3d2e5b90e9292fe73ea928dd69 Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Sun, 12 Jul 2026 09:20:31 +0200 Subject: [PATCH 01/18] Add 8972 field names with parens impact note (#9875) --- .../#8972 - parenthesised-field-names.tid | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972 - parenthesised-field-names.tid diff --git a/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972 - parenthesised-field-names.tid b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972 - parenthesised-field-names.tid new file mode 100644 index 0000000000..f15df5f1de --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.4.0/#8972 - parenthesised-field-names.tid @@ -0,0 +1,20 @@ +title: $:/changenotes/5.4.0/#8972/impacts/parenthesised-field-names +changenote: $:/changenotes/5.4.0/#8972 +created: 20260609193636000 +modified: 20260609193636000 +tags: $:/tags/ImpactNote +impact-type: compatibility-break +description: A field name containing round brackets can no longer be used as a filter operator suffix, because `(` now starts a multi value variable operand. + +Two filter operators take a field name in the suffix position: `regexp` and `search`. Up to 5.3.8 such a field name could contain round brackets, for example: + +``` +[regexp:_cd-work(s)[(?i)suite]] +[search:_cd-work(s)[suite]] +``` + +In 5.4.0 the new `(varname)` operand syntax for [[Multi-Valued Variables]] makes `(` start an operand. The parser now splits such a field name at the `(`, so the filter no longer matches. + +This will not be fixed. The two readings of `field(x)`, a field named `field(x)` versus the field `field` with the operand `(x)`, are genuinely ambiguous, so `(` and `)` are reserved in filter operator names and suffixes from 5.4.0 onwards. + +Workaround: do not use `(` or `)` in field names that are referenced as a filter operator suffix. Rename the field, for example `_cd-works`. From a30854a4c7e016955096b6e501928a07d5e0b198 Mon Sep 17 00:00:00 2001 From: superuser-does <32982579+superuser-does@users.noreply.github.com> Date: Sun, 12 Jul 2026 08:48:53 +0100 Subject: [PATCH 02/18] [DOCS] Corrected Description Lists tiddler (#9819) * [DOCS] Corrected Description Lists tiddler * Correct modified, remove trailing whitespace --- .../tiddlers/wikitext/Description Lists in WikiText.tid | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editions/tw5.com/tiddlers/wikitext/Description Lists in WikiText.tid b/editions/tw5.com/tiddlers/wikitext/Description Lists in WikiText.tid index 6aa4be904f..619be20bab 100644 --- a/editions/tw5.com/tiddlers/wikitext/Description Lists in WikiText.tid +++ b/editions/tw5.com/tiddlers/wikitext/Description Lists in WikiText.tid @@ -7,7 +7,7 @@ type: text/vnd.tiddlywiki !! Basic syntax -HTML description lists (AKA definition lists) are created with this syntax: +HTML description lists (AKA definition lists) are created with this syntax: <AKA definition lists) !! Multiple terms and descriptions -You can create multiple descriptions for a term, or multiple terms for a single description: +You can create multiple descriptions for a term, or multiple terms with a single description: <> +">> \ No newline at end of file From 9a0bbf831b7ddab210b71160ec25de4044c302e1 Mon Sep 17 00:00:00 2001 From: vuktw Date: Sun, 12 Jul 2026 20:37:43 +0300 Subject: [PATCH 03/18] Signing the CLA (#9923) * Signing the CLA * Apply suggestion from @saqimtiaz * Apply suggestion from @saqimtiaz * Apply suggestion from @saqimtiaz --------- Co-authored-by: Saq Imtiaz --- licenses/cla-individual.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/licenses/cla-individual.md b/licenses/cla-individual.md index 10f2b5a495..ba6fad2ebc 100644 --- a/licenses/cla-individual.md +++ b/licenses/cla-individual.md @@ -650,4 +650,5 @@ Rishu kumar, @rishu-7549, 2025/10/25 Himmel, @NotHimmel, 2026/03/19 -@sean-clayton, 2026/05/16 \ No newline at end of file + +@vuktw, 2026/07/12 From 4c29dae4af545cb375f20722f970074eafdbb5e4 Mon Sep 17 00:00:00 2001 From: XLBilly Date: Mon, 13 Jul 2026 21:57:04 +0800 Subject: [PATCH 04/18] Replace some wikify widget with call dynamic attribute syntax (#9816) * Replace some wikify widget with call dynamic attribute syntax * Apply suggestions * Add change note --- core/ui/Actions/new-journal.tid | 8 ++-- core/ui/EditTemplate/tags.tid | 39 ++++++++----------- core/ui/TagPickerTagTemplate.tid | 6 +-- core/ui/ViewToolbar/new-journal-here.tid | 7 ++-- .../tiddlers/releasenotes/5.5.0/#9816.tid | 10 +++++ 5 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9816.tid diff --git a/core/ui/Actions/new-journal.tid b/core/ui/Actions/new-journal.tid index 9d663e537c..c885880f4e 100644 --- a/core/ui/Actions/new-journal.tid +++ b/core/ui/Actions/new-journal.tid @@ -4,13 +4,15 @@ description: create a new journal tiddler \whitespace trim \function get-tags() [] [] +[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=<>/>"> +<$let journalTitle=<> + textFieldTags={{$:/config/NewJournal/Tags}} + tagsFieldTags={{$:/config/NewJournal/Tags!!tags}} + journalText={{$:/config/NewJournal/Text}} +> <$reveal type="nomatch" state=<> text=""> <$action-sendmessage $message="tm-new-tiddler" title=<> tags=<> text={{{ [get[]] }}}/> <$reveal type="match" state=<> text=""> <$action-sendmessage $message="tm-new-tiddler" title=<> tags=<> text=<>/> - diff --git a/core/ui/EditTemplate/tags.tid b/core/ui/EditTemplate/tags.tid index bbd3d301eb..20e1cad8e5 100644 --- a/core/ui/EditTemplate/tags.tid +++ b/core/ui/EditTemplate/tags.tid @@ -6,30 +6,23 @@ 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=<> - fallbackTarget=<> - colourA=<> - colourB=<>/> - """ -> - <$let backgroundColor=<> > - > - style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;` +<$let foregroundColor=<> fallbackTarget=<> colourA=<> colourB=<> >> + backgroundColor=<> +> + > + style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;` + > + <$transclude tiddler=<>/> + <$view field="title" format="text"/> + <$button class="tc-btn-invisible tc-remove-tag-button" + style.fill=<> > - <$transclude tiddler=<>/> - <$view field="title" format="text"/> - <$button class="tc-btn-invisible tc-remove-tag-button" - style.fill=<> - > - <$action-listops $tiddler=<> $field=<> $subfilter="-[{!!title}]"/> - {{$:/core/images/close-button}} - - - - + <$action-listops $tiddler=<> $field=<> $subfilter="-[{!!title}]"/> + {{$:/core/images/close-button}} + + + \end \procedure tag-body(colour,palette,icon,tagField:"tags") diff --git a/core/ui/TagPickerTagTemplate.tid b/core/ui/TagPickerTagTemplate.tid index 9e86891539..6083609f96 100644 --- a/core/ui/TagPickerTagTemplate.tid +++ b/core/ui/TagPickerTagTemplate.tid @@ -16,15 +16,13 @@ title: $:/core/ui/TagPickerTagTemplate <$set name="backgroundColor" value={{{ [] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}} > - <$wikify name="foregroundColor" - text="""<$macrocall $name="contrastcolour" target=<> fallbackTarget=<> colourA=<> colourB=<>/>""" - > + <$let foregroundColor=<> fallbackTarget=<> colourA=<> colourB=<> >> > > data-tag-title=<> > {{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/> - + diff --git a/core/ui/ViewToolbar/new-journal-here.tid b/core/ui/ViewToolbar/new-journal-here.tid index 2df7a2a4e6..cba1889b7d 100644 --- a/core/ui/ViewToolbar/new-journal-here.tid +++ b/core/ui/ViewToolbar/new-journal-here.tid @@ -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=<>> -<$wikify name="journalTitle" text="""<$transclude $variable="now" format=<>/>"""> <$action-sendmessage $message="tm-new-tiddler" title=<> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/> <%if [match[yes]] %> {{$:/core/images/new-journal-button}} @@ -16,9 +15,11 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}} <$text text={{$:/language/Buttons/NewJournalHere/Caption}}/> <%endif%> - \end -<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<>> +<$let journalTitle=<> + journalTags={{$:/config/NewJournal/Tags}} + currentTiddlerTag=<> +> <> diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9816.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9816.tid new file mode 100644 index 0000000000..9703761ca3 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9816.tid @@ -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. \ No newline at end of file From 521e530e11a966fe6b16e43d0ae28203aa087a3a Mon Sep 17 00:00:00 2001 From: saqimtiaz Date: Tue, 14 Jul 2026 07:55:48 +0200 Subject: [PATCH 05/18] docs: added size comparison of empty.html from prerelease and latest release --- ...TiddlyWiki Pre-release Size Comparison.tid | 94 +++++++++++++++++++ .../system/TiddlyWiki Pre-release.tid | 2 + 2 files changed, 96 insertions(+) create mode 100644 editions/prerelease/tiddlers/system/TiddlyWiki Pre-release Size Comparison.tid diff --git a/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release Size Comparison.tid b/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release Size Comparison.tid new file mode 100644 index 0000000000..57f14d8e2a --- /dev/null +++ b/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release Size Comparison.tid @@ -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) [abs[]compare:number:gteq[1048576]bytes.to.mib] :else[abs[]compare:number:gteq[1024]bytes.to.kib] :else[addsuffix[ bytes]] + + +\procedure get-wiki-filesize(filepath) + \procedure completion-get-json() + + <$list filter="[compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore"> + <$action-log msg="completed" size={{{ [jsonget[size]] }}}/> + <$action-setfield $tiddler=`$:/temp/file-size-comparison/$(filepath)$` text={{{ [jsonget[size]] }}} /> + + \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=<> + var-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=<> >> + + +\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={{{ [sign[]match[-1]then[Size increased]] :else[sign[]match[1]then[Size decreased]] :else[[Size has not changed]] }}} +> + + + <$text text=<>/>: <$text text={{{ [format.file.size] }}} /> + + + + + + + + + + + + + + + + + + + <$text text=<>/> + + + + <$text text={{{ [format.file.size] }}} /> + + + + +; 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 + +--- + + +<%else %> +<$button actions=<> class="tc-btn-big-green"> +Compare size of empty.html + +<%endif%> + + + diff --git a/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release.tid b/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release.tid index 7483128e20..4bf8ead5c2 100644 --- a/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release.tid +++ b/editions/prerelease/tiddlers/system/TiddlyWiki Pre-release.tid @@ -13,6 +13,8 @@ The pre-release is also available as an [[empty wiki|https://tiddlywiki.com/prer +{{ TiddlyWiki Pre-release Size Comparison }} + <$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">

<$text text=<>/>

From 7ececf9e0fc29156cbbc6e3e9b0081573e0fecfc Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 14 Jul 2026 12:22:45 +0200 Subject: [PATCH 06/18] 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 --- core/modules/utils/fakedom.js | 15 +++++- editions/test/tiddlers/tests/test-fakedom.js | 50 +++++++++++++++++++ .../5.5.0/#9843-fakedom-tagname-spec.tid | 13 +++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid diff --git a/core/modules/utils/fakedom.js b/core/modules/utils/fakedom.js index f1f0cacfb6..b3611ae2d1 100755 --- a/core/modules/utils/fakedom.js +++ b/core/modules/utils/fakedom.js @@ -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; } }); diff --git a/editions/test/tiddlers/tests/test-fakedom.js b/editions/test/tiddlers/tests/test-fakedom.js index dd5c797937..c58fa8fa78 100644 --- a/editions/test/tiddlers/tests/test-fakedom.js +++ b/editions/test/tiddlers/tests/test-fakedom.js @@ -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. diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid new file mode 100644 index 0000000000..33993cae46 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9843-fakedom-tagname-spec.tid @@ -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 From 304858c7c5be4af4eacde35c96516dc5347576bd Mon Sep 17 00:00:00 2001 From: Mario Pietsch Date: Tue, 14 Jul 2026 12:53:23 +0200 Subject: [PATCH 07/18] Make Click to generate wiki info button translatable (#9737) * Make Click to generate wiki info button translatable * Add ChangeNote --- core/language/en-GB/ControlPanel.multids | 3 ++- core/ui/WikiInformation.tid | 6 +++++- .../5.5.0/#9737-wiki-info-translatable.tid | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9737-wiki-info-translatable.tid diff --git a/core/language/en-GB/ControlPanel.multids b/core/language/en-GB/ControlPanel.multids index 28534bd1ee..f7f5a7f111 100644 --- a/core/language/en-GB/ControlPanel.multids +++ b/core/language/en-GB/ControlPanel.multids @@ -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 \ No newline at end of file +WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki +WikiInformation/Generate/Caption: Click to generate wiki information report \ No newline at end of file diff --git a/core/ui/WikiInformation.tid b/core/ui/WikiInformation.tid index 3cfee54e95..b0bab1bddd 100644 --- a/core/ui/WikiInformation.tid +++ b/core/ui/WikiInformation.tid @@ -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 [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"> diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9737-wiki-info-translatable.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9737-wiki-info-translatable.tid new file mode 100644 index 0000000000..6a7ce33202 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9737-wiki-info-translatable.tid @@ -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]]) From 86a4e181343c22ed4ecebf11cab274ec91a16209 Mon Sep 17 00:00:00 2001 From: Mario Pietsch <pmariojo@gmail.com> Date: Tue, 14 Jul 2026 13:04:50 +0200 Subject: [PATCH 08/18] =?UTF-8?q?diff-text=20change=20newline=20to=20?= =?UTF-8?q?=E2=86=B2=20(#9736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * diff-text change newline to ↲ * Add ChangeNote --- core/modules/widgets/diff-text.js | 2 +- .../5.5.0/#9736-difftext-newline-glyph.tid | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9736-difftext-newline-glyph.tid diff --git a/core/modules/widgets/diff-text.js b/core/modules/widgets/diff-text.js index d91d4b96ad..3c32c91138 100644 --- a/core/modules/widgets/diff-text.js +++ b/core/modules/widgets/diff-text.js @@ -22,7 +22,7 @@ Inherit from the base widget class DiffTextWidget.prototype = new Widget(); DiffTextWidget.prototype.invisibleCharacters = { - "\n": "↩︎\n", + "\n": "↲\n", "\r": "⇠", "\t": "⇥\t" }; diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9736-difftext-newline-glyph.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9736-difftext-newline-glyph.tid new file mode 100644 index 0000000000..207a307dc5 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9736-difftext-newline-glyph.tid @@ -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]]) From 7f7f36d9869a095a08c74bf09e899e5e67877314 Mon Sep 17 00:00:00 2001 From: Mario Pietsch <pmariojo@gmail.com> Date: Tue, 14 Jul 2026 13:10:21 +0200 Subject: [PATCH 09/18] 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 --- core/modules/indexers/back-indexer.js | 5 +- .../test/tiddlers/tests/test-back-indexer.js | 162 ++++++++++++++++++ ...16-back-indexer-system-tiddler-sources.tid | 12 ++ 3 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 editions/test/tiddlers/tests/test-back-indexer.js create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid diff --git a/core/modules/indexers/back-indexer.js b/core/modules/indexers/back-indexer.js index 62df3f3d97..8b3ec93d5f 100644 --- a/core/modules/indexers/back-indexer.js +++ b/core/modules/indexers/back-indexer.js @@ -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); } diff --git a/editions/test/tiddlers/tests/test-back-indexer.js b/editions/test/tiddlers/tests/test-back-indexer.js new file mode 100644 index 0000000000..98d71f560c --- /dev/null +++ b/editions/test/tiddlers/tests/test-back-indexer.js @@ -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"); + }); + }); +}); diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid new file mode 100644 index 0000000000..fc92ece7b2 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid @@ -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/9916 +release: 5.5.0 +tags: $:/tags/ChangeNote +title: $:/changenotes/5.5.0/#9916 +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 From d27716c1e05a3f6ac81048f9635d39acf95248e8 Mon Sep 17 00:00:00 2001 From: saqimtiaz <saq.imtiaz@gmail.com> Date: Tue, 14 Jul 2026 13:17:58 +0200 Subject: [PATCH 10/18] chore: fixed erroneous changenote number --- ...rces.tid => #9917-back-indexer-system-tiddler-sources.tid} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename editions/tw5.com/tiddlers/releasenotes/5.5.0/{#9916-back-indexer-system-tiddler-sources.tid => #9917-back-indexer-system-tiddler-sources.tid} (94%) diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9917-back-indexer-system-tiddler-sources.tid similarity index 94% rename from editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid rename to editions/tw5.com/tiddlers/releasenotes/5.5.0/#9917-back-indexer-system-tiddler-sources.tid index fc92ece7b2..2e251e4967 100644 --- a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9916-back-indexer-system-tiddler-sources.tid +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9917-back-indexer-system-tiddler-sources.tid @@ -3,10 +3,10 @@ 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/9916 +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9917 release: 5.5.0 tags: $:/tags/ChangeNote -title: $:/changenotes/5.5.0/#9916 +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 From 04cd3081e438f86e722678a06ef2ad7fb548cb3a Mon Sep 17 00:00:00 2001 From: Bram Chen <bram.chen@gmail.com> Date: Tue, 14 Jul 2026 22:50:37 +0800 Subject: [PATCH 11/18] Update chinese language files (#9933) * Update chinese language files * Add WikiInformation/Generate/Caption in ControlPanel.multids * Add ChangeNote --- editions/tw5.com/tiddlers/releasenotes/5.5.0/#9933.tid | 10 ++++++++++ languages/zh-Hans/ControlPanel.multids | 3 ++- languages/zh-Hant/ControlPanel.multids | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9933.tid diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9933.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9933.tid new file mode 100644 index 0000000000..48a2b7756b --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9933.tid @@ -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` diff --git a/languages/zh-Hans/ControlPanel.multids b/languages/zh-Hans/ControlPanel.multids index 3d2875f5b4..76888aa568 100644 --- a/languages/zh-Hans/ControlPanel.multids +++ b/languages/zh-Hans/ControlPanel.multids @@ -256,4 +256,5 @@ ViewTemplateTags/Caption: 查看模板标签区 ViewTemplateTags/Hint: 默认的查看模板使用此规则级联,动态选择模板以显示条目的标签区域。 WikiInformation/Caption: 维基信息 WikiInformation/Hint: 此页面总结了有关此 ~TiddlyWiki 配置的高阶信息。旨在让用户能够快速与他人共享其 ~TiddlyWiki 配置的相关信息,例如,在其中一个论坛中寻求帮助时。不包含隐私或个人信息,没有明确复制和粘贴在其他地方的情况 -WikiInformation/Drag/Caption: 拖动此链接可将此工具复制到另一个维基 \ No newline at end of file +WikiInformation/Drag/Caption: 拖动此链接可将此工具复制到另一个维基 +WikiInformation/Generate/Caption: 生成维基信息报告 \ No newline at end of file diff --git a/languages/zh-Hant/ControlPanel.multids b/languages/zh-Hant/ControlPanel.multids index a43522ce5b..799b962f9b 100644 --- a/languages/zh-Hant/ControlPanel.multids +++ b/languages/zh-Hant/ControlPanel.multids @@ -256,4 +256,5 @@ ViewTemplateTags/Caption: 檢視範本標籤 ViewTemplateTags/Hint: 預設的檢視範本使用此規則級聯,動態選擇範本以顯示條目的標籤。 WikiInformation/Caption: 維基資訊 WikiInformation/Hint: 此頁面總結了有關此 ~TiddlyWiki 配置的高階資訊。旨在讓使用者能夠快速與他人共享其 ~TiddlyWiki 配置的相關資訊,例如,在其中一個論壇中尋求幫助時。不包含隱私或個人資訊,沒有明確複製和粘貼在其他地方的情況 -WikiInformation/Drag/Caption: 拖動此連結可將此工具複製到另一個維基 \ No newline at end of file +WikiInformation/Drag/Caption: 拖動此連結可將此工具複製到另一個維基 +WikiInformation/Generate/Caption: 產生維基資訊報告 \ No newline at end of file From 4bf7e9d1920870ce614b15b475f71b241c01fda4 Mon Sep 17 00:00:00 2001 From: Joshua Fontany <joshua.fontany@gmail.com> Date: Tue, 14 Jul 2026 08:26:59 -0700 Subject: [PATCH 12/18] [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> --- core/modules/utils/csv.js | 3 +- editions/test/tiddlers/tests/test-csv.js | 41 +++++++++++++++++++ .../tiddlers/releasenotes/5.5.0/#9919.tid | 13 ++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 editions/test/tiddlers/tests/test-csv.js create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9919.tid diff --git a/core/modules/utils/csv.js b/core/modules/utils/csv.js index 74f90faa85..f583ce55a1 100644 --- a/core/modules/utils/csv.js +++ b/core/modules/utils/csv.js @@ -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]; } diff --git a/editions/test/tiddlers/tests/test-csv.js b/editions/test/tiddlers/tests/test-csv.js new file mode 100644 index 0000000000..81798bce4b --- /dev/null +++ b/editions/test/tiddlers/tests/test-csv.js @@ -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"}]); + }); +}); diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9919.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9919.tid new file mode 100644 index 0000000000..4d11c71e82 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9919.tid @@ -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. From 7516d96eeda8f62e13f631487027c99f184c6017 Mon Sep 17 00:00:00 2001 From: vuktw <vukmd@proton.me> Date: Sat, 18 Jul 2026 12:31:42 +0300 Subject: [PATCH 13/18] 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> --- .../Installing TiddlyWiki on Node.js.tid | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid b/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid index fc9e0f8f42..01fd5edad2 100644 --- a/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid +++ b/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid @@ -4,7 +4,7 @@ created: 20131219100608529 delivery: DIY description: Flexible hosting on your own machine or in the cloud method: sync -modified: 20221115230831173 +modified: 20260712162730421 tags: Saving [[TiddlyWiki on Node.js]] Windows Mac Linux title: Installing TiddlyWiki on Node.js type: text/vnd.tiddlywiki @@ -39,4 +39,39 @@ The `-g` flag causes TiddlyWiki to be installed globally. Without it, TiddlyWiki <<.warning "If you are using Debian or Debian-based Linux and you are receiving a `node: command not found` error though node.js package is installed, you may need to create a symbolic link between `nodejs` and `node`. Consult your distro's manual and `whereis` to correctly create a link. See github [[issue 1434|http://github.com/TiddlyWiki/TiddlyWiki5/issues/1434]]. <br><br>Example Debian v8.0: `sudo ln -s /usr/bin/nodejs /usr/bin/node`">> <br> -<<.tip "You can also install prior versions like this: <br><code> npm install -g tiddlywiki@5.1.13</code>">> +<<.tip "You can also install prior versions like this: <br><code> npm install -g tiddlywiki@5.1.13</code><br>Note: this will overwrite the installed version rather than installing it alongside.">> + +!! Installing multiple Node.js verions alongside + +There are multiple options: + +!!! Local install + +``` +mkdir -p /home/user/opt/tiddlywiki-5.1.13 && cd "$_" # works in Bash +npm install tiddlywiki@5.1.13 +``` + +This installs the given version in the current directory, at the expense of taking some extra disk space. The binary is in `node_modules/.bin/tiddlywiki` (relative to the current directory). For convenience, it is possible to create a symlink (in Linux) containing explicit version number in the name, like this: + +``` +ln -s /home/user/opt/tiddlywiki-5.1.13/node_modules/.bin/tiddlywiki /home/user/bin/tiddlywiki-5.1.13 +``` + +Having `~/bin` in `PATH` environment variable (usually at the beginning of it), makes it possible to start this version of TiddlyWiki as simple as `tiddlywiki-5.1.13`. + +!!! Use `npx` + +To run another version without overwriting the currently installed version, use npx: + +``` +npx tiddlywiki@5.3.8 --version +``` + +<<.warning "This comes with a security risk, since it downloads and executes a package from the internet. See https://talk.tiddlywiki.org/t/tiddlywiki-docs-gettingstarted-node-js/15423/4">> + +!!! Run directly from the source code repository + +# clone https://github.com/TiddlyWiki/TiddlyWiki5/ locally +# checkout any version (using `git checkout` or `git switch` for modern versions of Git) +# set up a link or a shell script for it From 2ececf21c7de0e774bbdeac0ac3dbe35a5d9883d Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Sat, 18 Jul 2026 11:36:37 +0100 Subject: [PATCH 14/18] Cherry pick docs update from master --- .../Installing TiddlyWiki on Node.js.tid | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid b/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid index fc9e0f8f42..01fd5edad2 100644 --- a/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid +++ b/editions/tw5.com/tiddlers/nodejs/Installing TiddlyWiki on Node.js.tid @@ -4,7 +4,7 @@ created: 20131219100608529 delivery: DIY description: Flexible hosting on your own machine or in the cloud method: sync -modified: 20221115230831173 +modified: 20260712162730421 tags: Saving [[TiddlyWiki on Node.js]] Windows Mac Linux title: Installing TiddlyWiki on Node.js type: text/vnd.tiddlywiki @@ -39,4 +39,39 @@ The `-g` flag causes TiddlyWiki to be installed globally. Without it, TiddlyWiki <<.warning "If you are using Debian or Debian-based Linux and you are receiving a `node: command not found` error though node.js package is installed, you may need to create a symbolic link between `nodejs` and `node`. Consult your distro's manual and `whereis` to correctly create a link. See github [[issue 1434|http://github.com/TiddlyWiki/TiddlyWiki5/issues/1434]]. <br><br>Example Debian v8.0: `sudo ln -s /usr/bin/nodejs /usr/bin/node`">> <br> -<<.tip "You can also install prior versions like this: <br><code> npm install -g tiddlywiki@5.1.13</code>">> +<<.tip "You can also install prior versions like this: <br><code> npm install -g tiddlywiki@5.1.13</code><br>Note: this will overwrite the installed version rather than installing it alongside.">> + +!! Installing multiple Node.js verions alongside + +There are multiple options: + +!!! Local install + +``` +mkdir -p /home/user/opt/tiddlywiki-5.1.13 && cd "$_" # works in Bash +npm install tiddlywiki@5.1.13 +``` + +This installs the given version in the current directory, at the expense of taking some extra disk space. The binary is in `node_modules/.bin/tiddlywiki` (relative to the current directory). For convenience, it is possible to create a symlink (in Linux) containing explicit version number in the name, like this: + +``` +ln -s /home/user/opt/tiddlywiki-5.1.13/node_modules/.bin/tiddlywiki /home/user/bin/tiddlywiki-5.1.13 +``` + +Having `~/bin` in `PATH` environment variable (usually at the beginning of it), makes it possible to start this version of TiddlyWiki as simple as `tiddlywiki-5.1.13`. + +!!! Use `npx` + +To run another version without overwriting the currently installed version, use npx: + +``` +npx tiddlywiki@5.3.8 --version +``` + +<<.warning "This comes with a security risk, since it downloads and executes a package from the internet. See https://talk.tiddlywiki.org/t/tiddlywiki-docs-gettingstarted-node-js/15423/4">> + +!!! Run directly from the source code repository + +# clone https://github.com/TiddlyWiki/TiddlyWiki5/ locally +# checkout any version (using `git checkout` or `git switch` for modern versions of Git) +# set up a link or a shell script for it From c0b3403f879ea09631a8ba5b3aea7e9c99f587fc Mon Sep 17 00:00:00 2001 From: Jeremy Ruston <jeremy@jermolene.com> Date: Tue, 21 Jul 2026 12:11:14 +0100 Subject: [PATCH 15/18] Remove extraneous github ribbon in the dev edition --- editions/dev/tiddlers/system/github-fork-ribbon.tid | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 editions/dev/tiddlers/system/github-fork-ribbon.tid diff --git a/editions/dev/tiddlers/system/github-fork-ribbon.tid b/editions/dev/tiddlers/system/github-fork-ribbon.tid deleted file mode 100644 index eefba8be30..0000000000 --- a/editions/dev/tiddlers/system/github-fork-ribbon.tid +++ /dev/null @@ -1,6 +0,0 @@ -title: $:/editions/dev/github-fork-ribbon -tags: $:/tags/PageTemplate -caption: ~GitHub ribbon -description: ~GitHub ribbon for tw5.com/dev - -<div class="github-fork-ribbon-wrapper right" style><div class="github-fork-ribbon" style="background-color:#DF4848;"><a href="https://github.com/TiddlyWiki/TiddlyWiki5" target="_blank" rel="noopener noreferrer">Find me on ~GitHub</a></div></div> \ No newline at end of file From e1e34f49071356bd7c45071ea26a0d5a1b1ee797 Mon Sep 17 00:00:00 2001 From: lin onetwo <linonetwo012@gmail.com> Date: Wed, 22 Jul 2026 00:16:33 +0800 Subject: [PATCH 16/18] 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. --- core/modules/background-actions.js | 2 +- editions/tw5.com/tiddlers/releasenotes/5.5.0/#9891.tid | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9891.tid diff --git a/core/modules/background-actions.js b/core/modules/background-actions.js index 9317bbbe44..e3829056fa 100644 --- a/core/modules/background-actions.js +++ b/core/modules/background-actions.js @@ -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, diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9891.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9891.tid new file mode 100644 index 0000000000..263fc22408 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9891.tid @@ -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. From 97e27d0dadeab3664cd78c0d1c28669b0ff5fa41 Mon Sep 17 00:00:00 2001 From: Mario Pietsch <pmariojo@gmail.com> Date: Wed, 22 Jul 2026 13:12:55 +0200 Subject: [PATCH 17/18] 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 --- .../parsers/wikiparser/rules/codeblock.js | 2 +- .../tiddlers/tests/test-codeblock-parser.js | 69 +++++++++++++++++++ .../5.5.0/#9739-empty-codeblock.tid | 13 ++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 editions/test/tiddlers/tests/test-codeblock-parser.js create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9739-empty-codeblock.tid diff --git a/core/modules/parsers/wikiparser/rules/codeblock.js b/core/modules/parsers/wikiparser/rules/codeblock.js index 1171704b37..744c2ff46f 100644 --- a/core/modules/parsers/wikiparser/rules/codeblock.js +++ b/core/modules/parsers/wikiparser/rules/codeblock.js @@ -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 diff --git a/editions/test/tiddlers/tests/test-codeblock-parser.js b/editions/test/tiddlers/tests/test-codeblock-parser.js new file mode 100644 index 0000000000..905dcf799b --- /dev/null +++ b/editions/test/tiddlers/tests/test-codeblock-parser.js @@ -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"); + }); +}); diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9739-empty-codeblock.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9739-empty-codeblock.tid new file mode 100644 index 0000000000..83a5ae79b4 --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9739-empty-codeblock.tid @@ -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 From abf8ffdbca225798973ff6c56991484d5390e863 Mon Sep 17 00:00:00 2001 From: Mario Pietsch <pmariojo@gmail.com> Date: Wed, 22 Jul 2026 13:45:25 +0200 Subject: [PATCH 18/18] 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> --- core-server/filesystem.js | 20 ++++- .../test/tiddlers/tests/test-filesystem.js | 83 +++++++++++++++++++ .../5.5.0/#9815-filesystempaths-backslash.tid | 13 +++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 editions/test/tiddlers/tests/test-filesystem.js create mode 100644 editions/tw5.com/tiddlers/releasenotes/5.5.0/#9815-filesystempaths-backslash.tid diff --git a/core-server/filesystem.js b/core-server/filesystem.js index e24e3fa67b..165291444b 100644 --- a/core-server/filesystem.js +++ b/core-server/filesystem.js @@ -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]), @@ -352,8 +364,10 @@ 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,"_")); + } // 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 diff --git a/editions/test/tiddlers/tests/test-filesystem.js b/editions/test/tiddlers/tests/test-filesystem.js new file mode 100644 index 0000000000..23f6f0a7e6 --- /dev/null +++ b/editions/test/tiddlers/tests/test-filesystem.js @@ -0,0 +1,83 @@ +/*\ +title: test-filesystem.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests for $:/core-server filesystem utilities. + +\*/ +"use strict"; + +if($tw.node) { + + var path = require("path"); + + describe("generateTiddlerFilepath", function() { + + var directory = path.resolve("/tmp/tw5-test-filesystem"); + + // Characters stripped by the cross-platform-filename regex at + // core-server/filesystem.js:356. Each entry is [char, description]. + // The forbidden set includes all characters disallowed by Windows + // (< > : " | ? *), backslash (directory separator on Windows), + // tilde (legacy 8.3 short-name marker), and caret (disallowed in + // some shell/FS contexts). + var forbiddenChars = [ + ["<","less-than"], + [">","greater-than"], + ["~","tilde"], + [":","colon"], + ["\"","double-quote"], + ["|","pipe"], + ["?","question-mark"], + ["*","asterisk"], + ["^","caret"], + ["\\","backslash"] + ]; + + // Use originalpath so we exercise the line-356 regex directly. + // The title-branch at line ~342 pre-strips "/" and "\" before the + // main regex runs, which would mask the backslash case. + function filepathFromOriginalpath(title,extension) { + return $tw.utils.generateTiddlerFilepath(title,{ + extension: extension, + directory: directory, + fileInfo: { + overwrite: true, + originalpath: title + extension + } + }); + } + + forbiddenChars.forEach(function(entry) { + var char = entry[0], name = entry[1]; + it("should replace " + name + " (" + char + ") with underscore", function() { + var result = filepathFromOriginalpath("a" + char + "b",".tid"); + expect(path.dirname(result)).toBe(directory); + expect(path.basename(result)).toBe("a_b.tid"); + }); + }); + + it("should replace every forbidden char in a dense string", function() { + // Prefix with "x" so the sanitized result isn't all underscores, + // which would trigger the charcode-fallback branch at line ~371. + var title = "x" + forbiddenChars.map(function(e) { return e[0]; }).join(""); + var expected = "x" + forbiddenChars.map(function() { return "_"; }).join(""); + var result = filepathFromOriginalpath(title,".tid"); + expect(path.dirname(result)).toBe(directory); + expect(path.basename(result)).toBe(expected + ".tid"); + }); + + it("should sanitize real-world 'Pragma: \\define' title without creating a subdirectory", function() { + // Issue for editions/tw5.com titles like "Pragma: \define". + // Before the fix, backslash survived sanitization and path.resolve + // treated it as a separator on Windows, producing a "Pragma_ " + // subdir containing "define.tid". + var result = filepathFromOriginalpath("Pragma: \\define",".tid"); + expect(path.dirname(result)).toBe(directory); + expect(path.basename(result)).toBe("Pragma_ _define.tid"); + }); + + }); + +} diff --git a/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9815-filesystempaths-backslash.tid b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9815-filesystempaths-backslash.tid new file mode 100644 index 0000000000..d7255e3a9c --- /dev/null +++ b/editions/tw5.com/tiddlers/releasenotes/5.5.0/#9815-filesystempaths-backslash.tid @@ -0,0 +1,13 @@ +change-category: nodejs +change-type: bugfix +created: 20260711175727000 +description: Tiddler titles containing a backslash no longer break generated file paths +github-contributors: pmario +github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9815 +release: 5.5.0 +tags: $:/tags/ChangeNote +title: $:/changenotes/5.5.0/#9815 +type: text/vnd.tiddlywiki + +* Titles containing a backslash, such as `pragma: \define`, get the backslash replaced like other forbidden filename characters instead of it acting 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 filename sanitising