Boundless is a React-based Story Format for Twine Its built upon the Unified ecosystem.
Boundless is compatible with Twine 2.3.0 and above.
Go to the Boundless releases page
and expand the assets for the latest release. Copy the link of the format.js
.
Open Twine and click on the Twine
menu
Then click on Story Formats
Then click on + Add
menu item and paste in the URL of the format.js
file.
If done correctly, the dialog should say Boundless <version> will be added
.
Next, click the + Add
button beneath the text field.
After you have installed Boundless you will need to specify the story format.
Open the story you wish to use Boundless with and click on the Story
menu.
Then click the Details
tab. This will open a dialog in the bottom right corner
of the screen.
In the dialog box use the Story Format
dropdown to select the version of Boundless
you want to use.
Boundless supports GitHub Flavored Markdown and a few additional features.
Passage links are defined using the [[
and ]]
syntax. Boundless supports the
following link formats:
Syntax | Example |
---|---|
[[link target]] |
[[store]] |
[[link text->link target]] |
[[go to the store->store]] |
[[link target<-link text]] |
[[store<-go to the store]] |
[[link text|link target]] |
[[go to the store|store]] |
Boundless exposes a global zustand
store called App
. It is attached
to the window
object and can be accessed anywhere in the application.
Returns the value of the specified key in the application state. If no key is specified, the entire application state is returned.
App.get('key') // => value of `key`
App.get() // => { key: value }
Sets the value of the specified key in the application state. If no value is specified and the key is an object, the new state is merged with the existing state.
App.set('key', 'value') // => { key: 'value' }
App.set({ key: 'value2', key2: 'value2' }) // => { key: 'value2', key2: 'value2' }
Boundless supports Remark's implementation of Markdown directives
(via remark-directive
),
which are used to add HTML elements not normally supported by Markdown.
Directives are only meant to supplement Markdown, not supersede it.
When choosing between Markdown or Directives, it is recommended to use
the Markdown syntax whenever possible. For instance, if you want to create
a link, use [link text](https://www.test.com)
instead of :::a{href=https://www.test.com}
.
Directives come in three flavors: container directives, leaf directives, and text directives.
Container directives start with :::tag
(where tag
is
an HTML tag) and end with :::
, both of which must appear
on their own separate line. For example:
:::section
This is a section.
:::
Will render as:
<section>
<p>This is a section.</p>
</section>
Note: nesting leaf and text directives should work fine, but nesting container directives, while possible, is not predictable.
Leaf directives are similar to container directives, but they do not have a closing tag. For example:
test
:::br
test
Will render as:
<p>test<br>test</p>
If a leaf directive contains text, you can place the content between two
square brackets. For example, the text of an option
would be written as:
:::select{name=selectExample required}
::option[Red]{value=red}
::option[White]{value=white}
::option[Blue]{value=blue}
:::
Text directives are inline and can be used anywhere in a paragraph.
The content affected by a text directive is indicated by the square
brackets ([]
). For example:
For example:
This is a paragraph with a :em[emphasized content].
Will render as:
<p>This is a paragraph with a <em>emphasized content</em>.</p>
You can also add attributes to the container directive by adding
{key="value"}
after the tag name.
:::section{style="color: red"}
Here is some content
:::
Will produce:
<section style="color: red">
<p>Here is some content</p>
</section>
Note that the double quotes are optional if the value does not contain any spaces.
:::section{id=test}
Here is some content
:::
Will produce:
<section id="test">
Here is some content
</section>
Any attributes that starts with a .
will be added to the
container's class list.
:::section{.red .bold}
Here is some content
:::
Will produce:
<section class="red bold">
Here is some content
</section>
The :::if[condition]
container directive is used to conditionally render content. The
content inside the directive will only be rendered if the condition is true
.
:::if[2>1]
This will be rendered
:::
:::if[2<1]
This will not be rendered
:::
Using a single word as the condition is a shorthand way of checking the App
state. Therefore, the following two examples are equivalent:
:::if[key]
This will be rendered assuming the `key` exists in the application state and is truthy.
:::
Is equivalent to:
:::if[App.get('key')]
This will be rendered assuming the `key` exists in the application state and is truthy.
:::
You can also negate the condition by prefixing it with !
.
:::if[!key]
This will be rendered assuming the `key` does not exist in the application state or is
falsy.
:::
Is equivalent to:
:::if[!App.get('key')]
This will be rendered assuming the `key` does not exist in the application state or is
falsy.
:::
The :::script
container directive is used to add JavaScript to the passage.
The code is executed as soon as the passage loads (see "Application State" below for
information on how to access application variables).
:::script
console.log("Hello, world!")
:::
The :::ejs
container directive is used to render complex content using the EJS templating
language.
:::script
App.set('name', 'Bob')
App.set('numbers', [1, 2, 3])
:::
:::ejs
<p>Hello, <%= App.get('name') %>!</p>
<ul>
<% if (App.get().numbers.length > 0) { %>
<% App.get().numbers.forEach(function (number) { %>
<li><%= number %></li>
<% }) %>
<% } %>
</ul>
:::
Will render as:
<p>Hello, Bob!</p>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
::include[passageName]
is a leaf directive used to include other passages in the
current passage. For example, let's say you have two passages: welcome
and store
.
Here is the contents of welcome
:
Welcome to the store!
Here is the contents of store
:
::include[welcome]
These are the items in the store:
- content
- content
...
Will produce:
Welcome to the store!
These are the items in the store:
- content
- content
...
The :state[key]
leaf directive is used to access the value of the specified key
in the application state. It can be used anywhere in a passage.
:::script
App.set({test: "Hello World!"})
:::
This is the value of the `test` variable: :state[test]
It can also be used as an attribute value:
:::script
App.set({test: "Hello World!"})
:::
:::section{data-state=":state[test]"}
Here is some content
:::
Will produce:
<section data-state="Hello World!">
Here is some content
</section>
You can also use state values as class names:
:::script
App.set({test: "red"})
:::
:::section{.bold .:state[test]}
Here is some content
:::
Will produce:
<section class="bold red">
Here is some content
</section>
The :state[key]
directive also works within :::ejs
directives. However, using
App.get([key])
is recommended instead because directives cannot be used in
logic.
:::script
App.set({test: "Hello World!"})
:::
:::ejs
<div>
This is some <strong>HTML</strong> content.
<p>Here is a state variable: :state[test]</p>
</div>
:::
Will produce:
<div>
This is some <strong>HTML</strong> content.
<p>Here is a state variable: Hello World!</p>
</div>
:eval[statement]
is a text directive used to evaluate JavaScript statements.
The current time is :eval[new Date().toLocaleTimeString()]
Will produce:
<p>The current time is 12:00:00 PM</p>
Another use is to display computations based on the application state:
:::script
App.set({hitPoints: 13, damage: 2})
:::
You have :eval[App.get('hitPoints') - App.get('damage')] hit points left
Will produce:
<p>You have 11 hit points left</p>