Skip to content

Conversation

@yaisog
Copy link
Contributor

@yaisog yaisog commented Dec 11, 2025

This PR explores some ideas that were discussed in #9471. I propose to keep the non-technical discussion about the feature in that issue.

Specifically, an that is the biggest change, multi-valued variables must be explicitly declared during their assignment by using the variable=((( filter expression ))) syntax, i.e. by using parentheses instead of curly brackets.

If so defined, the usual filter syntax [<variable>] will return a list of all items, while for variables assigned using curly braces nothing changes.

Additionally, the TranscludeWidget has been extended to use the attribute=((( filter expression ))) syntax to pass multi-valued variables to procedures and functions where they create multi-valued variables within the scope of the procedure / function.

The :let filter run prefix will always create multi-valued variables. Within a filter expression, this would in almost all cases be the intended behavior.

An intersection[] filter operator has been added that can take multi-valued variables as a parameter to return the intersection with the input list.

The Filtered Transclusion syntax has been extended to also use ((( filter expression ))) to output the complete result list. It is currently synonymous with {{{ filter expression }}} in this regard. It is proposed to change the behavior of the latter to only output the first item, to be consistent with the curly brackets behavior elsewhere. This would be a break in backwards compatibility. It should be possible to write a script as part of the PR to find occurences of {{{ filter expression }}} and modify these to keep the output unchanged.

I had to remove some tests that were failing because the behavior is intentionally changed.

Note that any filters with the #8972 syntax [(name)] will cause the same filter parse error messages that they would have in v5.3.8.

  • Test custom widgets more thoroughly
  • Decide if enlist filter should work with MVVs as currently implemented
  • Extend documentation
  • Update getvariable operator
  • Update and restore tests
  • Update changenote

Documentation

Please refer to the tiddler [[Multi-Valued Variables]] in the PR preview for an overview of all features and syntax.


Syntax consistency

It is difficult to find another consistent syntax to distinguish between ordinary variables (SVV) and multi-valued ones (MVV) that hasn't been used in other context and uses ASCII characters only. Also see #9471.

Feature v5.3.8 PR #8972 this PR
SVV in WikiText <<name>> <<name>> <<name>>
MVV in WikiText N/A N/A (not useful) N/A (not useful)
SVV in attributes attribute=<<name>> attribute=<<name>> attribute=<<name>>
MVV in attributes N/A not implemented
possibly attribute=((name))
attribute=<<name>>
SVV in substituted attributes `$(name)$` `$(name)$` `$(name)$`
MVV in substituted attributes* N/A not implemented not implemented
SVV in macros $name$
$(name)$
<<__name__>>
deprecated deprecated
SVV in filter runs [<name>]
[operator<name>]
[<name>]
[operator<name>]
[<name>]
[operator<name>]
MVV in filter runs N/A [(name)]
[operator(name)]**
[<name>]
[operator<name>]**
SVV filtered attributes attribute={{{ filter }}} attribute={{{ filter }}} attribute={{{ filter }}}
MVV filtered attributes N/A attribute={{{ filter }}} attribute=((( filter )))
SVV filtered
transclusion
N/A N/A {{{ filter }}} (proposed)
MVV filtered
transclusion
{{{ filter }}} {{{ filter }}} ((( filter )))
SVV in filtered substituted attributes `${ filter }$` `${ filter }$` `${ filter }$`
MVV in filtered substituted attributes* N/A not implemented not implemented

* A useful application would be the extension of the substitute[] operator to handle multiple inputs and parameters to create a list of unique substitutions at once
** Only title[] and function[] (+ intersection[])

@netlify
Copy link

netlify bot commented Dec 11, 2025

Deploy Preview for tiddlywiki-previews ready!

Name Link
🔨 Latest commit 79527dd
🔍 Latest deploy log https://app.netlify.com/projects/tiddlywiki-previews/deploys/696345cc9edaee00087ecae4
😎 Deploy Preview https://deploy-preview-9487--tiddlywiki-previews.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link

github-actions bot commented Dec 11, 2025

📊 Build Size Comparison: empty.html

Branch Size
Base (master) 2451.4 KB
PR 2460.3 KB

Diff: ⬆️ Increase: +8.9 KB


✅ Change Note Status

All change notes are properly formatted and validated!

📝 $:/changenotes/5.4.0/#9487

Type: feature | Category: internal
Release: 5.4.0

TBD

🔗 #9487

👥 Contributors: yaisog


📖 Change Note Guidelines

Change notes help track and communicate changes effectively. See the full documentation for details.

@github-actions
Copy link

Confirmed: yaisog has already signed the Contributor License Agreement (see contributing.md)

Copy link
Contributor Author

@yaisog yaisog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not happy about this one. There are 2 wikitext syntax variants now, that do the exact same thing.

As I wrote in the PR description (and #9471), I would like to change {{{ filter }}} to return only the first result, to make the syntax consistent. I did not want to implement this without discussion, though.

Also note that I removed the tooltip and CSS feature parsing from the rule as these were never used by the generated ListWidget.

@pmario
Copy link
Member

pmario commented Dec 12, 2025

Also note that I removed the tooltip and CSS feature parsing from the rule as these were never used by the generated ListWidget.

They are currently not used, but there are plans to enhance that functionality in the future. So for consistency reasons this would need to be added to the triple braces ((())) too.

That would mean, that there are 2 different syntaxes, that do exactly the same thing, with a minor difference.

The existing {{{}}} regexp contains a bug, which is most likely added to this PR too, if it is similar.

See this PR: Fix 7701 filtered transclusion fails if "}" follows somewhere in the text #7822

@yaisog
Copy link
Contributor Author

yaisog commented Dec 12, 2025

They are currently not used, but there are plans to enhance that functionality in the future.

That wouldn't get my vote. Tooltips and styles for Filtered Transclusions are super useless. Nobody uses this anyway except for debugging, maybe, because it's so inflexible and just gives you a plain list. The template makes at least some sense and any styles can be put in there. Personally, I would put resources elsewhere and accept the backwards-compatibility break here.

I'll keep the bug in mind but won't act on it until some decisions are made. This is a very minor feature of this PR anyway, which is primarily about the ((( ))) syntax and adapting the TranscludeWidget. Also, the regexp has been completely rewritten. Have you checked if the bug still exists?

That would mean, that there are 2 different syntaxes, that do exactly the same thing, with a minor difference.

This is exactly the point of this PR: Have two syntaxes that are subtly different, with new features reserved for the new syntax. You could likewise complain in the original PR #8972 implementation that [(variable)] and [<variable>] are two syntaxes that do the same thing with a minor difference.

@Jermolene
Copy link
Member

Thanks @yaisog there is some proximity between the changes here and those in #9055. It might be worth a review to explore whether it would be helpful to merge them, but I suspect that it might be more practical for me to suspend work on #9055 while we get this completed and merged. (The last time I updated that PR from master was a bit of a nightmare due to other changes we've made in the same areas).

@pmario
Copy link
Member

pmario commented Dec 21, 2025

I am absolutely concerned. If we really use the same filter syntax [<v>] for single variables and MVVs. Our filter syntax gets very close to brainfuck

@pmario
Copy link
Member

pmario commented Dec 22, 2025

  1. "The code above is completely unreadable" - Are you trying to say it's a bad example?

Your examples are good, because you did choose the right variable names. But I am not happy with the new syntax. (I have lost context somewhere in the middle of #9471 and here. So I am basically unable to use MVVs at all.)

If there is some code where MVVs are used, you can not see if they are MVVs or SVVs. So we have to go and search where the variable has been assigned to see what they are. The only way to make it clear is good variable names. But naming variables is hard and most users get it wrong.

  1. This is not a feature for "standard users" and has never been meant to be.

IMO That's not a good argument. Our users will use it and imo they will have problems, which causes a lot of maintenance.


What do I need to do, If I have a MVV and I only want to assign the first value?

@yaisog
Copy link
Contributor Author

yaisog commented Dec 22, 2025

What do I need to do, If I have a MVV and I only want to assign the first value?

Wou would use <$let variable={{{ [<function>] }}} > instead of <$let variable=((( [<function>] ))) > or <$let variable=<<function>> >. The use of {{{ }}} would also mark this clearly as single-valued variable in the code, helping readability.

Also note that, like I mentioned in the issue, the documentation does not explicitly state the only the first result will be assigned in the direct or attribute assignment, at least not in the [[Functions]] tiddler.

Previously, empty filter results were being converted to arrays containing an empty string [""] instead of truly empty arrays []. This change removes the .map() that was adding empty strings and preserves empty arrays as-is, which is correct behavior for multi-valued variables.
Unified empty array → empty string conversion for all attribute types

Changed string type to return [undefined] instead of [] when attribute value is undefined, making it consistent with missing macro behavior
Modified the getvariable operator to use getVariableInfo() and its
resultList property instead of getVariable(). This allows the operator
to return all values from multi-valued variables rather than just the
first value.

Non-existent variables still return "" and empty multi-valued variables are preserved as empty (no output)
These will be replaced by proper unit tests later
The LogWidget now handles mixed variable types more effectively:

- Regular variables are displayed in a table with full column width
- Multi-valued variables show as placeholders (e.g., "[Array: 3 values (see below)]")
  in the main table
- Each multi-valued variable's values are shown in a separate collapsible
  console group with a dedicated table

This prevents multi-valued variables from creating wide tables with many columns
that squeeze regular variables into narrow, hard-to-read spaces.
- Add new ((( ))) syntax as separate parser rules (filteredlisttransclude*)
  with simplified parameters (filter and template only)
- Maintain {{{ }}} syntax with full parameters (filter, tooltip, template,
  style, classes) for backward compatibility with existing plugins (Relink)
- Fixes parsing of filters with indirect operands like {$:/temp/Tiddler}

The separation into distinct parser rules makes future deprecation of the
{{{ }}} syntax straightforward while ensuring existing functionality remains
intact.
Delegate to the title filter when operands is a multi-value variable for backwards compatibility.
This ensures existing multi-value operand usage continues to work while the single-operand behavior is preserved.
@Jermolene
Copy link
Member

Thank you @yaisog for your persistence. I'm catching up after the break, if you've time it would be very useful if you could very briefly summarise where you've got to, and any outstanding questions.

Copy link
Contributor Author

@yaisog yaisog left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @yaisog for your persistence. I'm catching up after the break, if you've time it would be very useful if you could very briefly summarise where you've got to, and any outstanding questions.

Hi @Jermolene, I did have some time to work on this between the holidays, so there were a bunch of commits.

What is currently implemented:

  • The intersection[] operator outputs the intersection of its input and its (MVV) operand.
  • The getvariable[]operator was extended to output all of an MVV's values as a list.
  • The enlist[] operator accepts MVV operands and falls back to title[] in that case (this made the transition less error-prone for me, at least)
  • The substitute[] operator was extended in #9510 to work with MVV operands. This could be included here.
  • computeAttribute(s) in widget.js was updated heavily
  • I made separate parser rules for the ((( ))) syntax. Otherwise the regex was too unwieldy. Also, this way {{{ }}} remains functionally unchanged (I had problems with Relink when removing the tooltip and classes code).
  • The syntax attribute=<<.function>> makes attribute an MVV if the function returns more than one result. This is a backward compatibility break, but very, very useful in my opinion. It seems logical, too.
  • I found it useful for <% if %> to make its filter result available as an MVV to replace a lot of format:titlelist / enlist instances. So within its scope conditionList is now available. The ListWidget had to be extended to accept an additional parameter listVariable which is the name of an MVV that contains the complete filter result.
  • I rewrote the ActionLogWidget for better display of MVVs. In the overview table they are listed with their number of items, and these items are listed for each MVV in a separate table after the overview.

Open questions:

  • Do you agree with the above or should anything be changed / reverted? I pretty much went ahead and implemented my ideas as they occured to me, so it boils down to identifying where I overshot the target.

I tried to update the overview tiddler [[Multi-Valued Variables]] as I went along. You can check it out if you need more details than this short summary above.

@pmario
Copy link
Member

pmario commented Jan 4, 2026

In general I am OK with the additions. The only problem I have is the difference for MVVs in the filter syntax.

I think if MVVs in filter runs are indistinguishable from SVVs it will cause problems. -- Just my thoughts.

@yaisog ... What was the reasoning bind this change?

Feature v5.3.8 PR #8972 this PR
MVV in filter runs N/A [(name)]
[operator(name)]
[<name>]
[operator<name>]

@yaisog
Copy link
Contributor Author

yaisog commented Jan 4, 2026

@yaisog ... What was the reasoning bind this change?

Pretty much what I wrote in #9471 (comment)

[...] using ((( ))) to declare multi-valued variables. This obviates the need for a special syntax to use them, so there is no conflict with keyboard shortcuts. In my opinion, this is more natural. Once a variable is defined as MVV, the TranscludeWidget will now also pass it to procedures and functions as such.

This was inspired by @Jermolene's comment in #9471 (comment)

If the difference between the attribute assignments a=<<b>> and a=((b)) is whether the full result list is assigned or just a single value, then there is a case for a={{{ filter }}} also only assigning a single value, and requiring the use of a=((( filter ))) to assign the full result list.

Mostly, I didn't like that we need another syntax to assign MVVs to attributes, where the natural candidate a=((b)) conflicts somewhat with already existing (but exotic) usage. Also, typing a variable when defining it feels more "right" than coercing it into a type when using it, at least to me.

The listVariable was not being properly updated when the filter output
changed, particularly when list items were reused during refresh.

Changes:
- Move listVariable assignment from ListItemWidget to ListWidget for
  better efficiency (set once instead of per item)
- Update listVariable in handleListChanges() before refreshing children
  to ensure they see the new value
- Remove fullList and listVariableName from parseTreeNode since they're
  no longer needed
- Simplify ListItemWidget.refresh() by removing listVariable handling

This makes the listVariable feature more efficient and ensures it
correctly reflects the current filter results in all cases.
@yaisog
Copy link
Contributor Author

yaisog commented Jan 18, 2026

Hi @Jermolene, do we also need to adapt makeTranscludeWidget() and makeWidget() in wiki.js? I'm not sure where and how these are used...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants