The changes in this fork have been merged into a branch of the original bondage.js project.
This project is a runner for Yarn dialogues, attempting compliance with the 2.0 language specification.
API improvements and additional features are present in YarnBound, which uses this package under the hood.
Known Deviations from Yarn 2.0 spec
- Reading from a .yarn file is left to the user; dialogues should be supplied to bondage.js as a text string or array of node objects.
- Some minutia about what unicode characters define a string has not been considered.
There are features in the Yarn docs that are not present in the Yarn language spec. Known examples are:
-
Character: some text
annotation [b]Markup[/b]
These exist in YarnBound but not here.
Usage
Install with npm i -S bondage
or grab bondage.js
from the /dist/
folder.
For information on how to write Yarn, visit the official documentation.
The examples below illustrate how bondage.js
in particular works:
Basic Dialogue
import bondage from 'bondage';
// or node:
// const bondage = require('bondage')
// or in a script tag:
// <script src="path-to-file/bondage.min.js"></script>
// bondage.js strips empty lines, but make sure lines have
// no leading whitespace (besides indentation)!
const dialogue = `
# someFiletag
title: StartingNode
someTag: someTag
---
This is a line of text.#someHashtag
This is another line of text.
===
`
const runner = new bondage.Runner()
runner.load(dialogue)
const generator = runner.run('StartingNode')
let node = generator.next().value
console.log('node', node)
When we log out node
above, we will see this object structure:
{
"text": "This is a line of text.",
"hashtags": ['someHashtag'],
"metadata": {
"title": "StartingNode",
"someTag": "someTag",
"filetags": [
"someFiletag"
]
}
}
Notice that hashtags at the end of a line go in a hashtags
array.
to continue, we call
node = generator.next().value
again, and if we log the new node, we see:
{
"text": "This is another line of text.",
"hashtags": [],
"metadata": {
"title": "StartingNode",
"someTag": "someTag",
"filetags": [
"someFiletag"
]
}
}
If we had jumped, we would see the new node's title and header tags under the metadata
property (along with the same fileTags).
Options
Given this dialogue:
# someFiletag
title: StartingNode
someTag: someTag
---
What color do you like?
-> Red
You picked Red!
-> Blue
You picked Blue!
===
We can start the dialogue runner like above.
const runner = new bondage.Runner()
runner.load(dialogue)
const generator = runner.run('StartingNode')
let node = generator.next().value
which will give us a text result like the last example. However, the next node we get from calling generator.next().value
will be:
{
"options": [
{
"text": "Red",
"isAvailable": true,
"hashtags": []
},
{
"text": "Blue",
"isAvailable": true,
"hashtags": []
}
],
"metadata": {
"title": "StartingNode",
"someTag": "someTag",
"filetags": [
"someFiletag"
]
}
}
In order to continue the dialogue, you will need to call
node.select(0);
node = generator.next().value
in order to move to the line with text, "You picked Red!"
But how will your view layer know whether you're looking at a text result or an options result? Use instanceof
:
node instanceof bondage.TextResult
node instanceof bondage.OptionsResult
node instanceof bondage.CommandResult
Speaking of CommandResult...
Commands
The third and last result type you need to know about is CommandResult. Given this dialogue:
# someFiletag
title: StartingNode
someTag: someTag
---
Sending a command...
<<someCommand with spaces>>
===
You will see a "Sending a command..." TextResult, but the next node will look like this:
{
"name": "someCommand with spaces",
"hashtags": [],
"metadata": {
"title": "StartingNode",
"someTag": "someTag",
"filetags": [
"someFiletag"
]
}
}
Your program can do what it wants with that, then call generator.next().value
to get the next node, as usual.
Custom Variable Storage
Bondage keeps track of variables internally. Optionally, you can supply your own variableStorage. variableStorage is an object with get() and set() methods defined.
const customStorage = new Map()
customStorage.set('hello', 1)
const runner = new bondage.Runner()
runner.setVariableStorage(customStorage)
runner.load(dialogue)
Call setVariableStorage BEFORE loading a dialogue with runner.load
. This is because declare
commands will resolve when the dialogue loads (as opposed to when runner.run()
is called)
Above, we set an initial value for the hello
variable, so if a line of dialogue contains {$hello}
, it will show the number 1
, no need to call <<set $hello = 1>>
.
Simple dialogues can probably just use the built-in storage.
Functions
You can also register functions to be used in your dialogue.
runner.registerFunction('sayHello', () => 'hello')
If a line of dialogue contains {sayHello()}
, it will show hello
.
Object Input Format
In addition to the regular yarn format as a string, bondage also accepts a javascript object. This is an intermediary format exported by some utilities. The text format is nicer to work with, so it should be preferred. For reference,
#someFiletag
#someOtherFiletag
title: SomeNode
tags: hello
arbitraryKey: arbitraryValue
---
This is a line of text
<<jump SomeOtherNode>>
===
title: SomeOtherNode
---
This is another line of text.
===
is equivalent to:
[
{
"title": "SomeNode",
"tags": "hello",
"arbitraryKey": "arbitraryValue",
"body": "This is a line of text\n<<jump SomeOtherNode>>\n",
"filetags": [
"someFiletag",
"someOtherFiletag"
]
},
{
"title": "SomeOtherNode",
"body": "This is another line of text.\n",
"filetags": [
"someFiletag",
"someOtherFiletag"
]
}
]
Other included versions
A minified version exists at bondage/dist/bondage.min.js
.
If you want to transpile for yourself, use import bondage from 'bondage/src/index'
and make sure it's being included by your build system.
If you need compatibility with internet explorer, you can transpile for yourself or use bondage/dist/bondage.ie.js
.
Development
The parser is compiled ahead of time, so after making changes to the grammar you will need to run node src/parser/make-parser
. This is done automatically during npm run build
.