Filter/Macro/Plugin to get Duplicate Links from Tiddler?

I am building a cookbook/meal planning wiki where I have Recipes in their own tiddlers, a Meal Plan that includes links to those tiddlers, and a Shopping List that shows the ingredients from each recipe. The problem I’m having is [[Meal Plan]links()] is returning only one instance of a recipe even if it’s linked multiple times. Before I consider creating tiddlers for each entry in the meal plan to get around this, is there a filter, macro, plugin, or other way to get a list of links to other tiddlers including duplicates?

I’m hosting this publicly while I try to work it out here:

It’s a little inefficient, but you could try something like [[Meal Plan]get[text]enlist-input:raw[]is[tiddler]].

This will technically return any single word or [[bracketed multiword title]] that corresponds to an extant tiddler’s title, so it may throw some false positives. You could add a final filter step like tag[Recipes] in place of is[tiddler] to eliminate results you don’t want.

This works brilliantly, thank you!

Adding tag[Recipes] would make is[tiddler] unnecesary, right? (Oops, you did say that!)

{{{ [[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]] }}}

1 Like

Yes, I said “in place of” but I realize in retrospect that was too easily overlooked. Thank you for spelling it out!

You’re right, I just tried it and it gave me the same results. I’d like to better understand how enlist-input:raw[] can output tiddler names to the input of the tag[Recipes] filter. The way @etardiff explained it made it sound like it would chop up all the text on the tiddler to pass to is[tiddler].

That’s correct! My suggestion takes advantage of the fact that the wikitext link syntax is the same as the syntax used for multi-word values in title list format. When we enlist a field (either with the enlist operator, as in [enlist{!!tags}], or with enlist-input, as in [{!!tags}enlist-input[]] or [<currentTiddler>get[tags]enlist-input[]] — all of which have the same results) what we’re saying is, “Treat the contents of this field as if it is a title list — a list of space-separated values.”

Of course, your text field isn’t really using title list format, but it does contains strings of values separated by spaces, and at least a few bracketed [[multi-word values]] — your links. And crucially, the enlist-input operator lets us use the :raw suffix to prevent deduplication, which (as you discovered) the links operator does not. So we can take advantage of this overlap in syntax and use enlist-input:raw in a way it wasn’t necessarily intended.

However… enlist-input doesn’t know anything about links, so [[Meal Plan]get[text]enlist-input:raw[]] will return every space-separated value in your tiddler. (Note that the links visible in the preview are generated by the {{{ }}} syntax, not your original wikitext links.)
image

Obviously we only want a subset of these results, so we need an extra filter step to discard unwanted values. I suggested is[tiddler] because I assumed it was unlikely that you’d have a “Wednesday:” tiddler.

But let’s say you do, and you don’t want it to show up in your list of recipes. In that case, if you want a list of recipes linked in the “Meal Plan” text, you can use tag[Recipes] instead of is[tiddler]. A hypothetical “Wednesday:” tiddler won’t have the “Recipes” tag, so it will be discarded; a non-existent “*” tiddler doesn’t have a tags field at all, so it’s discarded by default.

3 Likes

@etardiff Thank you so much for your detailed response. I am new to the community here but already loving it and thinking I made the right choice to try building this tool on top of TW5.

2 Likes

Do come back with questions as they occur to you!

Quite often, while developing a solution, I reach a point where I realize that my initial design could have been more elegant or efficient all along, if I had just explained my task/idea to someone who really understands TiddlyWiki.

It takes quite a bit of practice to understand the full power of fields, filters, and transclusions… Depending on what your intended workflow is like, that power could sometimes be unnecessary, but other times a bit of extra care in the design makes a “whole 'nother level” come into view. :slight_smile:

For example, you mention that you considered making tiddlers for each day that feeds into your menu planning. If you realize that you’d like to have something like an archive of what your meals have been (for whatever reason) — or if you sometimes want to start planning in advance for a certain holiday or party even though it’s not in the current week — then having a tiddler for each date would make lots of sense.

You could also have “slots” for entrees and for appetizers / desserts, etc., but only if that really matters to how your meals work!

The good news is, you can get up and running with a perfectly functional version of your solution like you’re doing now, and then decide whether and when to tinker (adding depth and interconnected features) as your needs continue to emerge.

Enjoy!

2 Likes

@Springer I would LOVE to explain the premise of my meal planning cookbook, I will start a new thread to gain feedback. Thanks!

Started new thread here:

I’m getting the duplicate tiddlers as I want, but is it possible to create a filter to give me the ingredient tiddlers (including their duplicates). Right now I have:

<ul>
<$list filter="[[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]]">
 <li>{{!!title}}</li>
 <ul>
 <$list filter="[field:recipe-parent<currentTiddler>]">
   <li>{{!!amount}} {{!!measurement}} {{!!ingredient}}</li>
 </$list>
 </ul>
</$list>
</ul>

But what I’d like is something like this:

<$list filter="[[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]  <-- something here that pulls the ingredient tiddlers for all the tiddlers fed into it -->  ]">

I think with that I can probably do some magic on grouping and adding the quantities.

I keep thinking about this in RDBMS terms where I would just do an INNER JOIN on the query of meals. I think once I understand how TW5 is like (and is NOT like) an RDBMS in terms of filters vs SQL I will have a much easier time with it but it’s a tough hill to climb!

Sure. Try this:

\function get.meals() [[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]]
\function get.ingredients() [all[tiddlers]field:recipe-parent<currentTiddler>]

{{{ [get.meals[]] :map:flat[get.ingredients[]] +[sort[]] }}}

Here I’ve used functions for legibility/potential reusability, but this is all equivalent to

{{{ [[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]]
	:map:flat[all[tiddlers]field:recipe-parent<currentTiddler>] +[sort[]] }}}
  • The map filter run prefix is the key element here!
  • If you do use functions, the \function definition pragmas need to be at the top of the tiddler (i.e., before any non-pragma wikitext.)

The rest of this post is musings not directly related to your question; feel free to ignore!

An alternate approach

I can’t help thinking that it’s a bit unwieldy to have a separate tiddler for each Recipe:Ingredient combo: you’ll end up with a lot of ingredient tiddlers very quickly, and it’s more difficult to add and update ingredient lists since you need to modify tiddlers outside the recipe you’re currently editing. I experimented a little with including an ingredients block directly in the recipe tiddler:


I used the wikitext style block marker @@ to delimit the ingredients block, with the class .ingredients to make it easy to filter for this style block in particular. As a bonus, you can also define an .ingredients class in CSS and easily apply whatever styling you like to the list.

Conventions I chose:

  • each ingredient gets its own bullet point
  • format of each line:
    • [[Ingredient]], quantity (preparation notes)

I rewrote the Kolachi and African Chicken recipes in this format.

With all the ingredients listed directly in the recipe, it’s a bit easier to sort and filter values. Here’s some sample code I used to generate a list of all the ingredients from the meal plan and the quantities/recipes in which they’re used:

\function get.meals() [[Meal Plan]get[text]enlist-input:raw[]tag[Recipes]]
\function get.ingredients() [get[text]split[@@.ingredients]butfirst[]split[@@]first[]splitregexp[\n]removeprefix[* ]]
\function all.ingredients() [get.meals[]] :map:flat[get.ingredients[]]
\function get.amount() [<recipe>get.ingredients[]removeprefix<ingredient>trim[, ]]

<ul style="columns: 30em;">
<$list filter="[all.ingredients[]] :map[split[,]first[]] +[unique[]sort[]]" variable=ingredient>
<li style="break-inside: avoid;">
	<<ingredient>>
	<ul>
	<$list filter="[get.meals[]search<ingredient>]" variable=recipe>
		<li>
			<<get.amount>>
			(<$link to=<<recipe>> />)
		</li>
	</$list>
	</ul>
</li>
</$list>
</ul>

Demo:

This is just a general proof of concept; you may have different display needs (combining quantities of the same ingredient where appropriate?). I’m sure there are some unfamiliar filter patterns in there, so do feel free to ask if you’re interested.

Here’s a package containing the two rewritten recipes, the altered Meal Plan, and the filter testing tiddler: recipe filter demo.json (3.0 KB) You can import it into your wiki if you’d like to test it yourself.

2 Likes

I agree completely. I had wrongly assumed that in order to get the amounts I would have to store them as fields on a tiddler per ingredient. I had just planned to have a custom editor that would handle creating the per-ingredient tiddlers when adding a new recipe. Your solution is much simpler, and may make it possible to use all or part of the Maple plugin mentioned in my other thread.

1 Like