Creating your own Twine 2 Story Format
- Part 1: Understanding Twine 2 HTML Structures
- Part 2: Reading Story and Passage Data
- Part 3: Parsing Links
- Part 4: Adding Functionality
- Part 5: Packaging a Story Format
Part 3: Parsing Links
While different story format use different syntax, all of them supply the same functionality: converting passage link syntax into hyperlinks to other passages. This allows for a user to click a link and for the story format to load the next passage in sequence.
Instead of writing new code, it is possible to pull from an existing story format, Snowman, for how to parse and create hyperlinks. In Snowman, the text of each passage is parsed and its contents replaced or changed before finally copying the new results over to the passage element. The code for creating hyperlinks can easily be dropped into another project and used for the same purpose.
Breaking the code out and into its own function, then, it would look like the following:
function createHyperlinks(result) {
/* Classic [[links]] */
result = result.replace(/\[\[(.*?)\]\]/g, function (match, target) {
var display = target;
/* display|target format */
var barIndex = target.indexOf('|');
if (barIndex !== -1) {
display = target.substr(0, barIndex);
target = target.substr(barIndex + 1);
} else {
/* display->target format */
var rightArrIndex = target.indexOf('->');
if (rightArrIndex !== -1) {
display = target.substr(0, rightArrIndex);
target = target.substr(rightArrIndex + 2);
} else {
/* target<-display format */
var leftArrIndex = target.indexOf('<-');
if (leftArrIndex !== -1) {
display = target.substr(leftArrIndex + 2);
target = target.substr(0, leftArrIndex);
}
}
}
return '<a href="javascript:void(0)" data-passage="' +
target + '">' + display + '</a>';
});
return result;
}
Updating Basic Story Format
In the previous part, code was added to read from the <tw-storydata> element and use its startnode attribute to find the correct first passage to read. From there, the passage was found using its pid and its innerHTML content was set to the <tw-passage> created within the same code.
In order to move between passages, the extra code needs to be updated to more than one passage and using passage link syntax to connect them.
<tw-storydata
name="Example HTML"
startnode="1" hidden>
<tw-passagedata
pid="1"
name="Start">
[[Another Passage]]</tw-passagedata>
<tw-passagedata
pid="2"
name="Another Passage">
[[Start]]</tw-passagedata>
</tw-storydata>
In the above code, the passage “Start” now has a passage link to another, “Another Passage”. From there, it has a link back to “Start”. Once the hyperlink system is in place, it should be possible to ‘move’ from one passage to another and back again.
Parsing Initial Links
With the addition of new passages and a function to parse links, the code can be updated to read the innerHTML of a passage and switch any usages of passage link syntax into hyperlinks.
<html>
<body>
<tw-storydata
name="Example HTML"
startnode="1" hidden>
<tw-passagedata
pid="1"
name="Start">
[[Another Passage]]</tw-passagedata>
<tw-passagedata
pid="2"
name="Another Passage">
[[Start]]</tw-passagedata>
</tw-storydata>
<script title="Twine engine code" data-main="example">
(function(){
function createHyperlinks(result) {
/* Classic [[links]] */
result = result.replace(/\[\[(.*?)\]\]/g, function (match, target) {
var display = target;
/* display|target format */
var barIndex = target.indexOf('|');
if (barIndex !== -1) {
display = target.substr(0, barIndex);
target = target.substr(barIndex + 1);
} else {
/* display->target format */
var rightArrIndex = target.indexOf('->');
if (rightArrIndex !== -1) {
display = target.substr(0, rightArrIndex);
target = target.substr(rightArrIndex + 2);
} else {
/* target<-display format */
var leftArrIndex = target.indexOf('<-');
if (leftArrIndex !== -1) {
display = target.substr(leftArrIndex + 2);
target = target.substr(0, leftArrIndex);
}
}
}
return '<a href="javascript:void(0)" data-passage="' +
target + '">' + display + '</a>';
});
return result;
}
// Find the 'tw-storydata' element.
let storydata = document.querySelector('tw-storydata');
// Get the 'startnode' attribute. Save its 'value'.
let startnode = storydata.attributes['startnode'].value;
// Find the element with a 'pid' of the startnode
let passagedata = document.querySelector(`[pid="${startnode}"]`);
// Create a new 'tw-passage' element
let passage = document.createElement('tw-passage');
// Set the 'innerHTML' to text with hyperlinks
passage.innerHTML = createHyperlinks(passagedata.innerHTML);
// Append the child element to the HTML <body>
document.body.appendChild(passage);
}())
</script>
</body>
</html>
Now, the initial passage is parsed, but this does not complete the story format. There needs to be an ability to click a link and have the code know to load a passage as a result. For that, the existing code needs to become more generalized.
findPassage*() Functions
The most common action for a passage will be to find and return it for parsing. To help with that, its two main attributes, pid and name, can be used to create separate functions: findPassageByPid() and findPassageByName().
function findPassageByPid(pid) {
// Find the element by its 'pid'
return document.querySelector(`[pid="${pid}"]`);
}
function findPassageByName(name) {
// Find the element by its 'name'
return document.querySelector(`[name="${name}"]`);
}
showPassage() Function
Another common action will be to show a passage after it has been found. For this, another function can be written whose purpose is to call the previous function, createHyperlinks(), and to swap the contents of the <tw-passage>element.
Because code in the previous part created the element, a short conditional test could be written to test to see if the element exists. If it does not, this is the first time and the element should be created. If it does, change its contents. Either way, append the child. (If it is already appended, it will not be added a second time.)
function showPassage(passagedata) {
// Look for the 'tw-passage'
let passage = document.querySelector('tw-passage');
// If it is null, it does not exist yet
if(passage === null) {
// Create it
passage = document.createElement('tw-passage');
}
// Set the 'innerHTML' to text with hyperlinks
passage.innerHTML = createHyperlinks(passagedata.innerHTML);
// Append the child element to the HTML <body>
document.body.appendChild(passage);
}
Adding in Event Listeners
There is one last step. The JavaScript code also needs to listen for all click events and then load the next passage based on the data-passage attribute of the link. (The data-passage attribute is created by the createHyperlink() function when parsing passage link syntax.)
For each time the showPassage() function is called, it also makes sense to add in new event listeners. The last step of the function could be to look through the existing elements with the data-passage attribute and set event listeners.
For any click event, the name of the target should be pulled from the element’s attributes. Using the name, it should be found using an attribute selector. Once found, it should be passed to showPassage() to show it to the user, and the whole processing cycle started again.
// Find all links in the current passage
let links = passage.querySelectorAll('a[data-passage]');
// For each link, add an event listener
for(let link of links) {
// If a link is clicked...
link.addEventListener('click', function() {
// Get the name of the passage to load.
let passagename = this.attributes["data-passage"].value;
// Find the passage by its name
let passagedata = findPassageByName(passagename);
// Show the passage
showPassage(passagedata);
});
}
Putting it All Together
Using event listeners and new functions to show passages once found, the new code will load the first passage and setup new event listeners for every passage shown. It will also parse the contents for passage syntax links and change them into hyperlinks.
When a user clicks on a hyperlink, it will load the target passage. This will continue for as long as their are hyperlinks for the user to click in the story.
<html>
<body>
<tw-storydata
name="Example HTML"
startnode="1" hidden>
<tw-passagedata
pid="1"
name="Start">
[[Another Passage]]</tw-passagedata>
<tw-passagedata
pid="2"
name="Another Passage">
[[Start]]</tw-passagedata>
</tw-storydata>
<script title="Twine engine code" data-main="example">
(function(){
function createHyperlinks(result) {
/* Classic [[links]] */
result = result.replace(/\[\[(.*?)\]\]/g, function (match, target) {
var display = target;
/* display|target format */
var barIndex = target.indexOf('|');
if (barIndex !== -1) {
display = target.substr(0, barIndex);
target = target.substr(barIndex + 1);
} else {
/* display->target format */
var rightArrIndex = target.indexOf('->');
if (rightArrIndex !== -1) {
display = target.substr(0, rightArrIndex);
target = target.substr(rightArrIndex + 2);
} else {
/* target<-display format */
var leftArrIndex = target.indexOf('<-');
if (leftArrIndex !== -1) {
display = target.substr(leftArrIndex + 2);
target = target.substr(0, leftArrIndex);
}
}
}
return '<a href="javascript:void(0)" data-passage="' +
target + '">' + display + '</a>';
});
return result;
}
function findPassageByPid(pid) {
// Find the element by its 'pid'
return document.querySelector(`[pid="${pid}"]`);
}
function findPassageByName(name) {
// Find the element by its 'name'
return document.querySelector(`[name="${name}"]`);
}
function showPassage(passagedata) {
// Look for the 'tw-passage'
let passage = document.querySelector('tw-passage');
// If it is null, it does not exist yet
if(passage === null) {
// Create it
passage = document.createElement('tw-passage');
}
// Set the 'innerHTML' to text with hyperlinks
passage.innerHTML = createHyperlinks(passagedata.innerHTML);
// Find all links in the current passage
let links = passage.querySelectorAll('a[data-passage]');
// For each link, add an event listener
for(let link of links) {
// If a link is clicked...
link.addEventListener('click', function() {
// Get the name of the passage to load.
let passagename = this.attributes["data-passage"].value;
// Find the passage by its name
let passagedata = findPassageByName(passagename);
// Show the passage
showPassage(passagedata);
});
}
// Append the child element to the HTML <body>
// (If it already exists, it will not be appended.)
document.body.appendChild(passage);
}
// Find the 'tw-storydata' element.
let storydata = document.querySelector('tw-storydata');
// Get the 'startnode' attribute. Save its 'value'.
let startnode = storydata.attributes['startnode'].value;
// Find the element with a 'pid' of the startnode
let passagedata = findPassageByPid(startnode);
// Show the passage matching the startnode
showPassage(passagedata);
}())
</script>
</body>
</html>
Reviewing
Existing code can be found to parse passage link syntax into hyperlinks. Using this code, an example can be put together that loads passages based on the target name encoded in an attribute like data-passage. Through parsing a passage’s content, new event listeners can be added so that a click on a link with the attribute data-passage will find a passage of the same name from the existing Twine 2 HTML structure, parse its content, and continue the cycle. For any new passage loaded, this process will repeat.