Fun with WikiText: Shortest Breadcrumb Path

There has been one or the other discussion about how to implement breadcrumb navigation here on Talk, e.g. Can Hierarchical Navigation Benefit TW Documentation?. What has been missing (I think) is, how to find the “right” path. Since the hierarchy is tag-based and each tiddler can have multiple tags, there will be multiple paths to one or more top-level tiddlers.

As an exercise, I tried to find a recursive method to find all possible paths for a given tiddler, and then sort them by length. Usually, the “right” one would be shortest one, I guess, or maybe the one that contains the most open tiddlers. Maybe we want to show the others paths in a tooltip.

Here’s what I came up with:
image

Tiddlywiki.com uses some tags that don’t exist as tiddlers and thus cannot have tags themselves and therefore no upwards hierarchy. To exclude these, we simply need to add a is[tiddler] in the right place, though.

There is no true check for circular references, but a configurable counter that currently stops after 10 steps.

To play around with it, simply copy-paste this WikiText into a new tiddler on tiddlywiki.com and select any doc tiddler from the dropdown.

\define separator() »»
\define max-depth() 10

\function .escape-separator() [search-replace:g:regexp[»],[-»]]
\function .unescape-separator() [search-replace:g:regexp[-»],[»]]

\function .check-length() [all[current]split<separator>count[]compare:number:lteq<max-depth>then<currentTiddler>]
\function .get-step-inner() [all[current].check-length[]split<separator>last[]tags[]!is[system].escape-separator[]addprefix<separator>addprefix<currentTiddler>.get-step[]] :else[all[]]
\function .get-step() [all[]] :map:flat[<currentTiddler>.get-step-inner[]]

<$select tiddler=<<qualify '$:/temp/breadcrumbs-demo'>> default=<<currentTiddler>> >
	<$list filter='[all[tiddlers]!is[system]sort[title]]'>
		<option value=<<currentTiddler>>>
			<$view field='title'/>
		</option>
	</$list>
</$select>

<$let start={{{ [<qualify '$:/temp/breadcrumbs-demo'>get[text].escape-separator[]] }}}
	  paths={{{ [<start>.get-step[]] :sort[split<separator>count[]] +[format:titlelist[]join[ ]] }}}>
	<$let shortestPath={{{ [enlist<paths>first[]] }}} >
		{{$:/core/images/home-button}} → 
		<$list filter="[<shortestPath>split<separator>.unescape-separator[]reverse[]butlast[]]" variable="node" join=" → ">
			<$link to=<<node>> />
		</$list>
	</$let>
	<br>
	<% if [enlist<paths>count[]compare:number:gt[1]] %>
		<br>
		Alternative paths:<br>

		<ul>
			<$list filter="[enlist<paths>butfirst[]]" variable="path">
				<li>
					{{$:/core/images/home-button}} → 
					<$list filter="[<path>split<separator>.unescape-separator[]reverse[]butlast[]]" variable="node" join=" → ">
						<$link to=<<node>> />
					</$list>
				</li>
			</$list>
		</ul>
	<% endif %>
</$let>

The choice of the “right” path is all yours. Choose wisely!

1 Like

That’s very interesting. For my trails-plugin, I do use the first “path” in the tags list, which are alphabetically sorted.

I just use the ancestors filter

<$list filter="[<currentTiddler>ancestors[]is[tiddler]reverse[]]" join="->">
	<$link><<currentTiddler>></$link>
</$list>

Interesting that I always treated my structure as a tree. Yet it is indeed true that generally it can be a graph and multiple paths exist. I have to think more about this and maybe try the code snippet above on a minimal example crafted to have multiple paths, to see what happens.

TW has no ancestors filter operator.

That’s great to see.

At the time we had that discussion, I did a similar recursive algorithm to find them all. But my solution was just a proof-of-concept JavaScript technique. I couldn’t then have even considered trying this in wikitext, and even now, it would be daunting. I’m going to study this!

Displaying only the shortest path does seem like a good option, but I did consider something else, either in that thread or elsewhere around that time: show them all. :wink:

The idea was, to find them all, as you do, then choose the shortest or other default, but add a way to click on the list and choose a different hierarchy, not actually a <select> widget but something functionally akin to one:

Again, thank you for sharing!

$:/plugins/yaisog/ancestors-filter (I think)

That will indeed give all the same tiddlers, but not in a usable “breadcrumb” order. Both have their uses. The ancestors output can become quite large in a thoroughly-tagged wiki.

The breadcrumbs-y output could be used to look for circular references or to generally optimize the tag structure, e.g. in my own wikis I would not put “Filters” under the TOC, then again under “Reference” and once more under “Reference → Concepts”.

Funnily enough, the bare-bones code is just the two functions .get-step and .get-step-inner. Most of the rest is the backstop for circular references (.check-length), safeguarding against the separator appearing in a tiddler title (.escape-separator and .unescape-separator), etc.

None of this would have even be remotely possible without the debug-log filter, as usual.

1 Like

That’s a great debug tool. We should try again to get it into the core. Is there an issue at GH still open? If not you should create a new one.

Did you try it in combination with the new multi-variables filter syntax extension?

GitHub PR #7190 was closed two years ago in favor of a planned filter debugging UI by @jeremyruston.

Unfortunately, I don’t think many folks outside TalkTW have ever heard of the debug-log filter, and probably not many within it, either. Without it, I don’t think many of my projects would have been successful.

Since we only pass a variable name as parameter, there is currently no way for the filter to know if it’s an MVV. It’ll show the first element only. I’ll look into implementing an MVV check once the discussion about naming the various parts has settled and the PR goes ahead.

UPDATE: After slight changes to the code, the debug-log filter (version 0.9.2) now works with MVVs .

1 Like

Cool project!

I’m not sure I would want the [is[tiddler]] filter step to be baked in to any breadcrumb solution…

All my wikis have view templates that can display at “nodes” (potential tiddler name-space strings whenever such nodes are opened in the story river, even if there’s no tiddler there. (These nodes typically include a map of things tagging to that string, since selection from a tag pill is the most common way a node is opened in the story river despite not “existing” and not being explicitly pointed-to with a missing link… but my view templates also scan for mentions of that string in other fields, such as modifier fields, alias fields, keywords fields, etc.)

In other words, lots of my tag-nodes display “what’s connected to here” resources, even though they’re technically not tiddlers. There’d be no reason to prevent people from navigating to those nodes within a breadcrumb system.

(I still do like to have a way to see immediately which nodes are tiddlers, and which are mere “holographs” built from templates. So I make strings italic (even inside tag pills) if there’s no explicit tiddler-content at that location…)

Of course, if I’m the only one making extensive use of these “virtual/navigational tiddler nodes” then I can retrofit your solution. But I think TiddlyWiki is essential ripe for this kind of use, so why not allow folks to toggle a setting to “avoid missing tiddlers”? There may be other layers of detail some folks would want to “include” or “exclude” with a toggle…

Heck, why not just offer a configurable filter-space for what ought to show up as a breadcrumb? Maybe one likes to display a limited set of breadcrumbs to read-only visitors, and a more robust set for authoring purposes?

1 Like

Hi @Yaisog prompted by this comment I’ve put together a quick proof of concept of the idea.

It is based on a new operator [inspect<filter>] that evaluates a filter and returns a block of JSON that contains the overall input and output of the filter along with data about all the intermediate steps, including the intermediate result lists. Also included is a first pass at integrating this new operator with the “Advanced Search” → “Filter” tab.

I have too many open PRs already so I would be very happy if @Yaisog or anyone else would like to help with this.

1 Like

Man, could I have used this two years ago! I’m getting better at debugging without it now, but it will clearly make that much easier!

Thank you