Creating your own Twine 2 Story Format: Part 2: Reading Story and Passage Data

Creating your own Twine 2 Story Format

Part 2: Reading Story and Passage Data

Story formats are JavaScript code that read, parse, and change the storage area of a Twine 2 story (<tw-storydata>) into different visual representations for players. The first step in writing a new story format is reading the attributes of <tw-storydata> element to find the following:

  • Name of the story
  • Starting passage of the story

Reviewing HTML Structures

In the previous part, the structure of a Twine 2 story was examined. It starts with a <tw-storydata> element, which can contain Story Stylesheet (<style>), Story JavaScript (<script>), and each passage’s own data as separate <tw-passagedata> elements.

Consider the following reduced example code:

<tw-storydata 
   name="Example HTML"
   startnode="1" hidden>
   <tw-passagedata 
     pid="1" 
     name="Start">
     Some content.</tw-passagedata>
</tw-storydata>

Based on the above code, the name of the story is an attribute of the <tw-storydata> element. So, too, is the startnode attribute.

Note: In the above code, the <tw-storydata> element has the attribute hidden. This “hides” the element and any of its children from rendering by the browser.

Accessing Attributes in JavaScript

Each Element in JavaScript has an attributes property through which its attributes and any possible values can be accessed.

Using the querySelector() function, elements can be found within the DOM. Based on the found element, its attributes property can be used to access the startnode attribute and then save its value.

// Find the 'tw-storydata' element.
let storydata = document.querySelector('tw-storydata');
// Get the 'startnode' attribute. Save its 'value'.
let startnode = storydata.attributes['startnode'].value;

After the startnode is known, the passage matching it must also be found. This can also be done through using querySelector(). However, instead of looking for an element, an attribute selector can be used to directly find an element on the page with an pid matching the value found.

In the selector format, it might be something like the following:

[pid="1"]

Note that it has quotation marks around the value to look for and, since it is looking for an attribute, also uses open and closing square brackets. Moving over to the code, it might look like the following:

 // Find the element with a 'pid' of the startnode
 let passagedata = document.querySelector(`[pid="${startnode}"]`);

Note: The above code uses a template literal. Instead of gluing — concatenating — a string together, expressions can be used within literals to get the value of a variable.

The current code, then, would be the following:

// 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}"]`);

Showing Passage Data

Once the passage has been found that matching the startnode value, it is time to move to the second step: showing its content to the user. This can be done through a few different ways, but one of the easier ways is to create a new element in the DOM: <tw-passage>.

// Create a new 'tw-passage' element
let passage = document.createElement('tw-passage');

This new element will contain the contents of the passage matching the startnode value. For now, it is easiest to simply set their innerHTML equal to each other, copying over any text or HTML found.

 // Set the 'innerHTML' of the passagedata to the 'tw-passage' element
passage.innerHTML = passagedata.innerHTML;

Finally, the last step is to append the new element to the existing <body>, adding the content of the passage to the page and showing the user the content.

// Append the child element to the HTML <body>
document.body.appendChild(passage);

Putting all the code together would be the following:

// 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' of the passagedata to the 'tw-passage' element
passage.innerHTML = passagedata.innerHTML;
// Append the child element to the HTML <body>
document.body.appendChild(passage);

With the JavaScript written to read the first passage and then show a visual representation, it needs to be added to a HTML file and incorporated with the existing structures.

Consider the combination of the reduced Twine 2 story HTML with the new JavaScript written to read from the <tw-storydata> element and then show the content of the first passage of the story to the user.

<html>
    <body>
        <tw-storydata 
            name="Example HTML"
            startnode="1" hidden>
        <tw-passagedata 
            pid="1" 
            name="Start">
            Some content.</tw-passagedata>
        </tw-storydata>
    
        <script title="Twine engine code" data-main="example">
                // 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' of the passagedata to 'tw-passage' element
                passage.innerHTML = passagedata.innerHTML;
                // Append the child element to the HTML <body>
                document.body.appendChild(passage);
        </script>
    </body>
</html>

Wrapping Enclosure

By default, any variables created outside of the scope of a function or class are global to that page. To prevent that, and to protect the code from accidentally being called in another section of JavaScript, they can be written within a JavaScript closure.

Through wrapping the existing code within a function, the scope of the variables will be limited to that function. (This is a common packaging technique used by many tools, and will be re-used in a later part of this series to package the story format.)

<script title="Twine engine code" data-main="example">
            (function(){
                // 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' of the passagedata to 'tw-passage' element
                passage.innerHTML = passagedata.innerHTML;
                // Append the child element to the HTML <body>
                document.body.appendChild(passage);
            }())
        </script>

Adding in the closure, the final code is the following:

<html>
    <body>
        <tw-storydata 
            name="Example HTML"
            startnode="1" hidden>
        <tw-passagedata 
            pid="1" 
            name="Start">
            Some content.</tw-passagedata>
        </tw-storydata>
    
        <script title="Twine engine code" data-main="example">
            (function(){
                // 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' of the passagedata to 'tw-passage' element
                passage.innerHTML = passagedata.innerHTML;
                // Append the child element to the HTML <body>
                document.body.appendChild(passage);
            }())
        </script>
    </body>
</html>

Reviewing

Through combining querySelector() with the property attributes, the startnode of the <tw-storydata> element can be found and saved. A passage matching this value can be found through using an attribute selector (‘[pid=”<number>”‘]) to find the matching passage.

A new element (<tw-passage>) can be found and its innerHTML property set to that of the found passage. This will copy over any text or HTML within the first storage passage to the visual one.

Finally, the new visual passage can be appended to the <body> of the page, thus moving the content from the storage area into something a user can see and potentially interact with, thus creating a basic story format.