clerestory
v0.10.3
Published
Context-free grammar tool for text generation inspired by Tracery
Downloads
7
Readme
Clerestory
Clerestory is a procedural text generation tool based on the concept of context-free grammars heavily inspired by Kate Compton's Tracery and Ian Holmes's Bracery.
The aim is to provide a simple and expressive syntax which can work well in the context of a larger project where some state is managed from outside the text-generation grammar -- what the authors of Bronco call a yielding grammar.
This is still WIP and API/expression syntax may change significantly.
Why use this
The purpose of this project is to provide a lightweight, efficient, productive tool for ad hoc generation of procedural text, ranging from person or place names to item descriptions to entire dialogues or stories. The API provides ways to guide what text is generated, such as conditional rules and different distributions.
Key concepts
Grammar
The main artefact you will interact with is a Grammar, which is simply a set of symbols. Expanding a symbol on a grammar will produce text by evaluating that symbol's rules, parsing the resulting expression, and following references to any other symbols recursively until a final text is produced.
import { Grammar } from 'clerestory';
const rules = {
pet: [ 'cat', 'dog', 'fish'],
story: 'Yesterday I bought a #pet#.'
}
const animalGrammar = new Grammar(rules);
console.log(animalGrammar.expand('story'));
// "Yesterday I bought a fish."
console.log(animalGrammar.state.pet);
// "fish"
// now that the "pet" symbol has been expanded it will keep this value
// until you change it
animalGrammar.state.pet = 'mongoose';
console.log(animalGrammar.expand('story'));
// "Yesterday I bought a mongoose."
Symbols
A symbol can be thought of as simply a set of rules for producing a string of text. The simplest possible symbol just returns a single string, but symbols have a lot of other powerful features for guiding how text gets generated. You can easily add or change symbols on a grammar by accessing its state
property. Continuing the example above:
// add a new symbol
animalGrammar.state.benefit = ['loyalty', 'personality', 'smell'];
// change an existing symbol
animalGrammar.state.story = 'I love my #pet# for its #benefit#.';
animalGrammar.expand('story');
// "I love my mongoose for its personality."
Note an important difference with Tracery: in Clerestory, symbols are "flattened" as soon as they are accessed, whether through a reference in another symbol or by outside code. Tracery differentiates between static variables and dynamic symbols that share the same namespace. Unlike Tracery, Clerestory does not have syntax for seeing variables via an expression, but does allow referencing a symbol without flattening it like #~symbol#
.
const schrodinger = new Grammar({ cat: ['alive','dead']});
console.log(schrodinger.state.cat);
// "dead"
schrodinger.state.box = 'I looked in the box and the cat was #cat#.';
console.log(schrodinger.expand('box'));
// "I looked in the box and the cat was dead."
The expand
method on a grammar or symbol will always reevaluate the symbol (though any other symbols referenced will keep their preexisting values).
console.log(schrodinger.expand('cat'));
// "alive"
You can also access the symbols on a grammar directly to modify or reset them:
schrodinger.symbols.cat.reset();
console.log(schrodinger.state.cat);
// "dead"
Rules
The possible expressions a symbol can produce are called rules. The cat
symbol above has two rules: "alive"
and "dead"
; expanding it will choose one at random. The real power of a context-free grammar comes from the expression syntax, described below, which allows you to reference other symbols, modify symbol outputs, and evaluate conditional statements within these expressions.
Distributions
You can influence which rules a symbol will return by setting a different distribution on the symbol.
random is the default and will pick a rule at random each time.
pop will select a rule at random and remove it from the stack. Note that if you run out of rules, the symbol will return an empty string.
weighted allows you to set a weight on each rule to determine its relative chance of being picked. You will need to pass rules in object syntax like [{ text: 'cat', weight: 5 },{ text: 'ferret', weight: 1 }]
. Weight should be a positive integer.
popWeighted works by creating a number of copies of each rule equal to its weight, which are removed as they are used.
shuffle acts like a "deck of cards". Rules are shuffled and one is drawn on each evaluation, and when they run out the used rules are reshuffled into a new "deck".
To specify a distribution, you need to use object-style symbol defintion:
const petGrammar = new Grammar({
pet: { rules: ['cat','dog','fish'], distribution: 'shuffle' }
});
Functional rules
A powerful feature of Clerestory is that it supports passing a function as the "rules" for a symbol. This allows you to completely bypass the built-in rule selection functionality and generate abritrary expressions in pretty much any way you wish.
const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayGrammar = new Grammar({
today: () => daysOfWeek[new Date().getDay()]
});
Expressions
Expressions are the individual bits of text produced by symbol rules and parsed by Clerestory. Expression syntax is very simple, largely inspired by Tracery.
In this expression:
Why hello there, #traveler#!
// Why hello there, Gandalf!
#traveler#
is a reference to another symbol in the same grammar. Once referenced in this way, the symbol "traveler" will persist its generated value. If you want to reference a symbol without saving the output, use #~traveler#
which will always expand the symbol without flattening it.
Bracery-style alternations are also supported:
[Hi|Hello|Yo], #traveler#!
// Hello, Gandalf!
You can even nest symbol references inside alternations:
[Hi #traveler#!|#traveler#, what's up?|Howdy!] How fares your quest?
// Gandalf, what's up? How fares your quest?
Clerestory also supports Tracery-style modifiers. These are generally simple text transformations which can be chained. A few useful defaults are included and you can add your own.
#animal#.s.uppercase
// CATS
If you want to include back-to-back symbol references without a space, you must use the |
character:
#animal.s# eat #animal#|#nutrition#
// dogs eat dogfood
Conditionals
A powerful feature that Clerestory adds is conditional expressions.