What is the best way to modify the value of a variable after the player clicks the link but before the next passage evaluates that var? (Chapbook)

I have found that if you create a JavaScript tag inside a passage, and inside that tag you code a link using the function “write”, the linked passage is automatically created and/or linked in Twine’s visual editor:

[JavaScript]
write ("[[Go left]]");
[continue]

It automatically creates a passage named “Go left” (if it yet didn’t existed) and adds a visual connection to it.

For the same reason that having Link Markup in a comment does.

eg. The “find all links within a Passage” feature doesn’t understand what JavaScript code is either, so it just sees the [[Go left]] and decides that it looks like a standard Markup based link.

Using a String variable literal that contains mark-up link like characters will also trick the feature…

/* Chapbook */
links: '[[Apple]] [[Banana]]'
--

/* Harlowe */
(set: _links to '[[Apple]] [[Banana]]')

/* SugarCube */
<<set _links to '[[Apple]] [[Banana]]'>>
1 Like

Here is a custom insert that does what you need

engine.extend('1.0.0', () => {

	engine.event.prependListener('dom-click', el => {
		let vars    = el.dataset.cbSetVars;
		if (vars) {
		  let result = engine.render(vars.replace(';',"\n")+"\n--");
		}
	});

	config.template.inserts = [{
		match: /^set\s+link/i,
		render(passage, props) {
		  if (passage) {
			return `<a href='javascript:void(0)'
					   data-cb-go="${passage}"
					   data-cb-set-vars="${props.set}">${props.label}</a>`;
		  }
		}
	}, ...config.template.inserts];
});

You can use it like a link {set link: 'Another passage', label: 'A link', set: 'foo: 2'}. The code in set is like the vars section, except that you can indicate multiple lines with a ; instead of a line break.

e.g.
{set link: 'Another passage', label: 'A link', set: 'foo: 2;bar: 4;name "hello"'}

3 Likes

This will expose the Twine passage tags in Chapbook:

window.passageTags = {}; // <== object format { passage name : tags }
let _storyData = document.getElementsByTagName( 'tw-storydata' )[0];
for ( let _child = _storyData.firstChild; _child !== null; _child = _child.nextSibling ) {
  if ( _child.tagName.toLowerCase() == 'tw-passagedata' ) {
    passageTags[ _child.getAttribute( 'name' ) ] = _child.getAttribute( 'tags' );
  }
}
window.getTags = function ( _passageName ) {
  return passageTags[ _passageName ].toLowerCase();
}

It basically grabs the Twine story data and stores the tags and then you can then do whatever you want with that data. For example, I could run:

if ( getTags( passage.name ).includes( "specific-tag-name" ) ) {
  // ==> do something if "specific-tag-name" exists in the current passage
}

You can also put that logic into a custom insert if need be.

1 Like

If I could offer alternative getTags() functions that both leverages Chapbook’s exported APIs and returns an unmodified array of tags.

Included are two versions (use only one). The code goes into the story’s JavaScript section.

Throws an error if the specified passage is nonexistent.

CODE:

/**
 * getTags(name) - Passage tags retrieval function.
 *
 * @param {string} name - Passage name.
 * @returns {Array<string>} Passage tags.
 * @throws {Error} Passage must exist.
 */
function getTags(name) {
	const passage = engine.story.passageNamed(name);

	if (!passage) {
		throw new Error(`There is no passage named "${name}".`);
	}

	return passage.tags;
}
Returns an empty array if the specified passage is nonexistent.

CODE:

/**
 * getTags(name) - Passage tags retrieval function.
 *
 * @param {string} name - Passage name.
 * @returns {Array<string>} Passage tags.
 */
function getTags(name) {
	const passage = engine.story.passageNamed(name);
	return passage ? passage.tags : [];
}

USAGE:

if (getTags('some passage').includes('some-tag')) {
	// The tag `some-tag` exists on the specified passage.
}
2 Likes

With thanks to @TheMadExile and @klembot, I’ve re-written my suggested insert above to be more elegant (and more performant)

2 Likes

Definitely, Chapbook has the best community and the most commited developers! Thanks to you all!

2 Likes

And I’ve scrapped my tag code altogether. :wink:

I didn’t think to do a console.log() of the passage object. I’m such a dummy, but I still had fun figuring it out the hard way.


@TheMadExile You wouldn’t happen to have a super secret, rough draft of the API document kicking around, would you?


We are the best! All 5 or 6 of us! High fives all around! :wink:

1 Like

This code doesn’t seem to work:
{set link: 'Another passage', label: 'A link', set: 'foo: 2;bar: 4;name "hello"'}

This is what is rendered in the browser:

The straight quotation marks are converted into curly quotation marks, and the returned value is rendered as a string, not as a link.
Its really messy to mix different types of quotation marks.

But if you set the value of the variable “name” outside the insert (fin the vars section, for example) the code works flawlessly.

1 Like

You’re missing a colon after name, I believe. Though that probably has nothing to do with the curly quotes rendering problem. I only mention it, just in case.

By the way, I’m learning so much about Chapbook from your questions and the answers from others. Thanks for that.

1 Like

You are right, even with the colon it renders improperly:

1 Like

#METOO :slightly_smiling_face:

1 Like

No, just the code in its GitHub repo. It’s …/extensibility.js file shows what it exports via the engine API for user use.

1 Like

Changing out all of the typographic (curly) quotes, both single and double, for regular quotes will probably fix the problems you’re having. Even if you do have other issues to work out, you still need to do that as typographic quotes are not valid syntax.

It’s Chapbook that’s converting all straight quotes of " and ' to the curly variety. Javier is sharing the rendered output of how Chapbook is corrupting the code, I believe. I do know that Javier submitted the bug already and Chris acknowledged it and is on it. It’s just that David suggested a solution that seems to be prone to the curly quote bug after the bug was brought to light in another thread…

Ah. Mea culpa.

Holy crap! That’s perfect! Chris really went the extra mile with his code commenting. It reads like documentation. This is awesome and overwhelming at the same time. :wink: Still have to follow the breadcrumb trail to piece it all together, but it’s there. *mind blown*

Thank you so much!

1 Like

I think this is not the same bug–issue with the code is that it’s inserting the value of set directly into HTML without escaping the quotation marks around "hello" in the example, so it returns HTML that closes the attribute too soon: <a ... data-set="name: "hello"">.

Here’s a tweak that works for me and also avoids the issue of having to use prependListener:

engine.extend('1.0.0', () => {
	engine.event.addListener('dom-click', el => {
		const setters = el.dataset.cbSetVars;
		const destination = el.dataset.cbSetVarsDestination;

		if (setters) {
			engine.render(setters.replace(/;/g, '\n') + '\n--');
		}

		if (destination) {
			go(destination);
		}
	});

  config.template.inserts = [
    {
      match: /^set\s+link/i,
      render(passage, props) {
        if (passage) {
          const link = document.createElement('a');

          link.setAttribute('href', 'javascript:void(0)');
          link.dataset.cbSetVarsDestination = passage;
          link.dataset.cbSetVars = props.set;
          link.innerText = props.label;
          return link.outerHTML;
        }
      }
    },
    ...config.template.inserts
  ];
});

3 Likes

Ahh, by creating the link directly instead of returning a textual representation of it, makes sense

1 Like

It works perfectly! Thanks!!

1 Like