Is there a SugarCube macro for building a Table of Contents for a passage?

I’m using
TweeGo v 2.1.1
SugarCube v 2.36.1

Does there exist a macro which can build a Table of Contents for a single passage?

Although the sections are “short” (well some aren’t) I’d like the reader to be able to go directly to any of the sections existing within a particular passage.

While I could manually construct a ToC as a table using HiEv’s “ScrollTo” macro (for which thanks!), it’d be nice to have an automated way to build it. It can get tedious when more than a handful of destinations are involved.

I’ve looked at HiEv’s sample code but didn’t see anything appropriate. Did I overlook something?

Does such a macro exist? Probably not, no.

Can one be built? Probably.

Build a table of contents from what specifically?

The page I want a ToC for is essentially an outline of information (the “fine print” for a contract with an imaginary company). It has a dozen or so “major points” that I’d like the reader to be able to jump to. The “outline” format itself is relatively simple: I built it using “unordered list” elements, (i.e. each element is prefixed by one or more asterisks.)

If such a macro were to exist, I’d expect it to insert a span or div with a unique ID and label at each destination point (having to decide on the label myself, with the macro deciding the unique IDs). So most of the saving would be in the construction of the ToC itself.

Theoretically, you should be able to do this with pure HTML in SugarCube, using anchor links. I tried out the example on the linked page in a SugarCube file, and it worked as intended.

note: I strongly suggest reviewing the source code of SugarCube’s own <<link>> macro as a guild to building your own ‘TOC’ macro, as it contains all the information & functionality for creating the ‘links’ you’ll need.

You didn’t supply an example of the Passage Names that will be included in the TOC, or how to distinguish those Passages from all the other Passages of the project.

A good place to start is defining the HTML element structure for your TOC, it will likely look something like the following TWEE Notation based example…

:: Start
<ul class="macro-toc">
	<li><a data-passage="First" class="link-internal link-visited">First</a></li>
	<li><a data-passage="Second" class="link-internal">Second</a></li>
	<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>

:: A non TOC passage
A non TOC passage

:: First
The First passage

:: Second
The Second passage

:: Another non TOC passage
Another non TOC passage

:: Third
The Third passage

If you Play the project you should see a basic unordered list that contains links to the First, Second, and Third passages.

Next you would create a Macro definition that constructed the above HTML structure, it would look something like the following JavaScript, and it would be placed within the Story JavaScript area of your project…

Macro.add('toc', {
	isAsync : true,
	handler() {
		/* Add argument checking if required. */

		/* Obtain list of Passages to be shown in TOC. */
		/* TODO: how to distingish them from other Passages? */
		let passages = ['First', 'Second', 'Third'];

		/* Construct the unordered list element */
		const $ul = jQuery(document.createElement('ul'))
			.addClass(`macro-${this.name}`);

		/* For each Passage in list */
		for (let passage of passages) {
			
			/* Construct the list item element for this Passsage */
			const $li = jQuery(document.createElement('li'))
				.appendTo($ul);
			
			/* Construct the anchor element for list item */
			const $link = jQuery(document.createElement('a'))
				.attr('data-passage', passage)
				.addClass('link-internal')
				.append(document.createTextNode(passage));

			/* Has this Passage been visited? */
			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}

			/* Add the onClick event handler */
			$link.ariaClick({
					namespace : '.macros',
					role      : 'link',
					one       : true
				}, this.createShadowWrapper(
					null,
					() => Engine.play(passage)
				));

			$link.appendTo($li);
		}

		/* Add unordered list structure to Node buffer. */
		$ul.appendTo(this.output);
	}
});

note: You will likely notice that the Names of the Passages to show in the TOC have been hardwired, we will come back to the sourcing of them later.

To test the new <<toc>> macro change the contents of the above Start Passage to the following, and Play the project again to see if the 2nd HTML structure looks & behaves like the 1st…

<ul class="macro-toc">
	<li><a data-passage="First" class="link-internal link-visited">First</a></li>
	<li><a data-passage="Second" class="link-internal">Second</a></li>
	<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>

<<toc>>

Now for the part where we distinguish the TOC Passages from all the others, and the simplest why to do that is to add a known Passage Tag (like toc) to all of them. So if we do that then the above TWEE Notation example would look something like the following…

:: Start
<ul class="macro-toc">
	<li><a data-passage="First" class="link-internal link-visited">First</a></li>
	<li><a data-passage="Second" class="link-internal">Second</a></li>
	<li><a data-passage="Third" class="link-internal">Third</a></li>
</ul>

<<toc>>

:: A non TOC passage
A non TOC passage

:: First [toc]
The First passage

:: Second [toc]
The Second passage

:: Another non TOC passage
Another non TOC passage

:: Third [toc]
The Third passage

So now that we can distinguish the TOC Passages we can use the Story.lookupWith() method to find them programmatically, and the <Passage>.title property of each found Passage to determine its Name.

The Macro definition would look something like the following, after the hard-wired list of Passage Names is replaced with the code to obtain them…

Macro.add('toc', {
	isAsync : true,
	handler() {
		/* Add argument checking if required. */

		/* Obtain list of Passages to be shown in TOC. */
		let passages = Story.lookupWith(function (p) {
				return p.tags.includes("toc");
			})
			.map(p => p.title);

		/* Construct the unordered list element */
		const $ul = jQuery(document.createElement('ul'))
			.addClass(`macro-${this.name}`);

		/* For each Passage in list */
		for (let passage of passages) {
			
			/* Construct the list item element for this Passsage */
			const $li = jQuery(document.createElement('li'))
				.appendTo($ul);
			
			/* Construct the anchor element for list item */
			const $link = jQuery(document.createElement('a'))
				.attr('data-passage', passage)
				.addClass('link-internal')
				.append(document.createTextNode(passage));

			/* Has this Passage been visited? */
			if (Config.addVisitedLinkClass && State.hasPlayed(passage)) {
				$link.addClass('link-visited');
			}

			/* Add the onClick event handler */
			$link.ariaClick({
					namespace : '.macros',
					role      : 'link',
					one       : true
				}, this.createShadowWrapper(
					null,
					() => Engine.play(passage)
				));

			$link.appendTo($li);
		}

		/* Add unordered list structure to Node buffer. */
		$ul.appendTo(this.output);
	}
});

warning: The Story.lookupWith() method returns the list of found Passages in alphabetical order, based on the Passage’s title, so if you want the TOC to be ordered otherwise (like alphabetically) then you will likely need to use the JavaScript <array>.sort() method to do so…

@Charm Cochran,

Anchor links are exactly what are used by @HiEv’s ScrollTo SugarCube macro ScrollTo. The macro has the advantage that you can determine whether the scrolled-to element is shown at the top or the bottom of the visible window.

See https://qjzhvmqlzvoo5lqnrvuhmg.on.drv.tw/UInv/Sample_Code.html#ScrollTo%20Macro

1 Like

@Greyelf,

Thanks for the pointer to the <<link>> source code! I’ll be reviewing it.

Also, thanks a lot for the example SugarCube code. Unfortunately, however, what I’m interested in is an automated way to build a table of contents for anchor links which are all within a single passage, not a ToC of separate passages. I’m sorry I didn’t make that more clear.

(For the moment, I generated the in-passage ToC manually. It wasn’t too tedious. :slight_smile: )

I don’t need a way to build a ToC which points to different passages because I already have a technique for doing that which is external to SugarCube. In particular, I use a bash script to invoke a programmable text editor (teco in this case),

As you suggest, I added the keyword [toc] to the desired Passage Names. The bash script is 4 lines of code (plus comments). It uses grep to find and extract the relevant Passage Names, then invokes teco to process them. The teco script consists of another 4 lines (with no comments). It extracts the passage names and surrounds them with [[ ]] . The output is a Twee passage named toc.tw which I then edit manually to be in an appropriate order. I suspect that the Linux utility awk could be used instead of teco, but regular expression matching gives me the hives :wink:

FWIW, the bash script that I use, named make_toc.sh, is

#! /usr/bin/sh -x
# cygwin bash script
#
# cleanup to avoid confusion
rm -f *.tmp toc.tw
# find all passage names with the tag toc (-a is needed for ligatures)
grep -a "::" *.tw | grep -a "\[.*toc.*\]" > toc.tmp
# create toc.tw from toc.tmp
mung make_toc.tec
# delete extra <cr>s
d2u -q toc.tw

The teco script that I use, named make_toc.tec, is invoked by the mung command.

ertoc.tmp$ewtoc.tw$a$
ji:: TOC
$<s:: $;0ki[[$s [$2rki]]
$>ex$$

Teco programs have been likened to a string of random characters. :wink: However, I’ve used that text editor since before CRT display terminals existed (oh, the memories of hours spent in front of KSR teletypes…) so I have very few problems writing in it. For a history of teco, see TECO

Another FWIW:

I’ve imagined the in-passage ToC (pair of) macros to be invoked something like this:

Somewhere near the top of the passage being indexed:

<<toc_build>>

At appropriate locations within the passage:

<<toc_entry "Appropriate string">>

“Appropriate string” would be a word or phrase and visible at the point in the text where “toc_entry” is invoked. It would have to be made visible in the ToC by “toc_build”.

toc_entry also would generate a unique (sequential?) anchor value for use with each entry in the ToC.

I dunno if this actually is feasible, though. Its possibility depends on my vague recollection that rendering happens after the text is processed.

A typical passage using this macro pair might look something like

:: Passage_with_its_own_toc

Contents:
<<toc_build>>

Below are some items of interest.

<<toc_entry "Introduction">> to this fascinating topic of interest.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ...

* <<toc_entry "Primary subject">> ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, ...
** <<toc_entry "Support">> for this argument is hard to come by.
** <<toc_entry "Additional supportl">> can be found at ....

ETA:
I’d expect the entries in the ToC itself would be shown in the same order as they’re encountered in the text. Also, being able to specify some simple form of markup for a ToC entry might be appropriate, perhaps in a second argument to the “toc_entry” macro. (like indenting or boldface, etc).