Skip to content
/ bracery Public

Procedural text generator, somewhat compatible with Tracery

License

Notifications You must be signed in to change notification settings

ihh/bracery

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

License Build Status Coverage Status

Bracery

Bracery (bracery.org) is a small language for procedural text generation. Its purpose is to enable quick and fluid writing of text with random elements, allowing for user extensions that may include calls to a server (e.g. for synonyms, rhymes, or other functions).

Bracery aims...

  • to keep out of the writer's way, looking mostly like a markup language and close to plain text;
  • to be easy to work with, presenting simply whether you're a casual user or an experienced programmer;
  • to avoid plundering the keyboard for syntax, especially common punctuation, like quotes;
  • to allow real programming, with variables, functions, and lists, but without forcing the writer to learn (or care) about all that;
  • to be usable offline by default, but also readily connectable to online generative text servers;
  • to be secure running random code from the internet, including limits on recursion and network/CPU usage;
  • to be compatible as much as possible with previous work, especially @galaxykate's Tracery.

Bracery combines a few different tricks well-known to computational linguistics and adjacent fields, such as variable manipulation syntax from Tracery, alternations from regular expressions, natural language processing from the compromise library (and, optionally, rhymes and phonemes from RiTa), parsing algorithms from bioinformatics, and lists from Scheme.

Procedural text repository

A writable repository of Bracery scripts is available at bracery.org. Edit Bracery in the web browser; save and share immediately; deploy as a Twitter bot with a single click.

Bracery can also be run from the command line, in the browser, etc. The Bracery command-line client will use the bracery.org server to resolve symbol definitions if the -w switch is specified from the command line, e.g.

bin/bracery -w -e '~common_animal'

will generate a sample from this page. You can also type bin/bracery -wr to enter text interactively from the command-line and have expansions returned by the server (or, alternatively, just point your web browser at bracery.org, click "Edit", and type into the box).

The micro-wiki repository is serverless, built using AWS Lambda functions available in the lambda/ directory.

Programmer's tl;dr

If you're a programmer, or not, you may want the summary: Bracery is a text markup language oriented around probabilistic context-free grammars that uses dollars signs $ for variables (as in $x), equals for assignment ($x=value), curly-brace delimiters ($x={longer value}), square-bracket alternations ([first option|another option|third option]), ampersands for built-in function names (&add, &subtract, &plural, etc.), conditionals (&if), lists (&map, &join, &reduce), access to several parsers (&eval, &quote, &syntax, &parse), and other language-oriented features; with an engine that runs asynchronously and can (optionally) connect to an online wiki for procedural content, where each page is its own symbol in the grammar.

Usage

The following Bracery code generates lines like how goes it, magician of Middle Earth and well met, magus of the world

[hello|well met|how goes it|greetings], [wizard|witch|mage|magus|magician|sorcerer|enchanter] of [earthsea|Earth|Middle Earth|the planet|the world]
Try this

Here's the same example, but using variables to keep track of the choices:

$greetings=[hello|well met|how goes it|greetings]
$wizard=[wizard|witch|mage|magus|magician|sorcerer|enchanter]
$earthsea=[earthsea|Earth|Middle Earth|the planet|the world]
$greetings, $wizard of $earthsea
Try this

You can also use variables to store Bracery code itself, for later expansion:

$greetings=&quote{[hello|well met|how goes it|greetings]}

$wizard=&quote{[wizard|witch|mage|magus|magician|sorcerer|enchanter]}

$earthsea=&quote{[earthsea|Earth|Middle Earth|the planet|the world]}

&eval{$greetings}, &eval{$wizard} of &eval{$earthsea}
Try this

The above example uses dynamic evaluation. Here's the same code with some syntactic sugar for the way variables are assigned and expanded:

[greetings=>hello|well met|how goes it|greetings]
[wizard=>wizard|witch|mage|magus|magician|sorcerer|enchanter]
[earthsea=>earthsea|Earth|Middle Earth|the planet|the world]
#greetings#, #wizard# of #earthsea#
Try this

Programmers may recognize this kind of thing too (lambdas):

$greetings=[hello|well met|how goes it|greetings]
$wizard=[wizard|witch|mage|magus|magician|sorcerer|enchanter]
$earthsea=[earthsea|Earth|Middle Earth|the planet|the world]

$sentence=&function{$name}{$greetings, $name}

&$sentence{$wizard of $earthsea}
Try this

And maybe this as well (lists):

$greetings=[hello|well met|how goes it|greetings]
$wizard=[wizard|witch|mage|magus|magician|sorcerer|enchanter]
$earthsea=[earthsea|Earth|Middle Earth|the planet|the world]

$sentence={$greetings, $wizard of $earthsea}

&join{&shuffle{&split{$sentence}}}
Try this

which gives jumbled-up output like

witch the hello, of world

There is a built-in (limited) rhyming engine, using RiTa or the CMU Pronouncing Dictionary for text-to-phoneme conversion. If you supply two grammar templates, Bracery will make a (limited) effort to find two expansions that rhyme:

&rhyme{The [stupid|foolish|hungry] [cat|dog], }{the [lonely|fateful] [hat|log]}
Try this

Note that the rhyming algorithm is essentially "brute-force": Bracery generates a few different possibilities, and preferentially selects the ones that rhyme. So, this could take a while if the rhyme templates are complex, and it won't always work (and it will tend to work best if you choose templates that you know will contain some rhymes, as in this example).

Bracery's alternations form a context-free grammar, and Bracery includes a limited parser. In other words, if you have some text that you think might have been generated by a particular Bracery program, then you can reconstruct how that program could have generated that output. This is like running the program backwards! And it only works if the program is very simple (e.g. no functions can be used, nor can variables be modified while the program is running).

Here's an example, using the syntactically ambiguous phrase "fruit flies like a banana":

[sentence=>[#singular_noun# #singular_verb#|#plural_noun# #plural_verb#] #noun_phrase#]
[noun_phrase=>#noun#|#preposition# #noun#]
[noun=>#plural_noun#|#singular_noun#]
[singular_noun=>fruit|a banana]
[singular_verb=>flies|likes|nears]
[plural_noun=>fruit flies|bananas]
[plural_verb=>fly|like|near]
[preposition=>like|near]
&json&parse#sentence#{fruit flies like a banana}
Try this

This should output one of two different parses of the phrase. One parse has "fruit flies" as the noun, and "like" as the verb:

[["root",["#sentence#",["alt",["#plural_noun#",["alt","fruit flies"]]," ",["#plural_verb#",["alt","like"]]," ",["#prep_or_noun#",["alt",["#noun#",["alt",["#singular_noun#",["alt","a banana"]]]]]]]]]]

The other parse has "fruit" as the noun, "flies" as the verb, and "like" as a preposition:

[["root",["#sentence#",["alt",["#singular_noun#",["alt","fruit"]]," ",["#singular_verb#",["alt","flies"]]," ",["#prep_or_noun#",["alt",["#prep#",["alt","like"]]," ",["#noun#",["alt",["#singular_noun#",["alt","a banana"]]]]]]]]]]

Bracery's &parse function is stochastic: if multiple valid parses exist, it will return a random parse, sampled proportionally to the probability that it's the correct parse.

Finally, note that you don't need to use any of these programmer-oriented features, if you just want to write generative text. Just start typing and go!

Web usage

The "wizard of earthsea" example is available as a web demo (source).

Here's another example, taken from Kate Compton's online tutorial to Tracery:

[name=>Arjun|Yuuma|Darcy|Mia|Chiaki|Izzi|Azra|Lina]
[animal=>unicorn|raven|sparrow|scorpion|coyote|eagle|owl|lizard|zebra|duck|kitten]
[mood=>vexed|indignant|impassioned|wistful|astute|courteous]
[story=>#hero# traveled with her pet #heroPet#.  #hero# was never #mood#, for the #heroPet# was always too #mood#.]
[origin=>#[hero:#name#][heroPet:#animal#]story#]
#origin#
Try this

This example generates lines like the following:

Darcy traveled with her pet kitten.  Darcy was never wistful, for the kitten was always too astute.

Alternate formats

There are several other ways you can specify the definitions. For example, you can use Tracery-style JSON:

{
 "name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"],
 "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"],
 "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"],
 "story": ["#hero# traveled with her pet #heroPet#.  #hero# was never #mood#, for the #heroPet# was always too #mood#."],
 "origin": ["#[hero:#name#][heroPet:#animal#]story#"]
}

Here is a web demo (source) using the Tracery-style JSON symbol definitions. These can also be found, along with other examples from Kate's online tutorial, in the examples directory of this repository.

Command-line usage

Give Bracery some text to expand:

bracery -e '[hello|hi] [world|planet]!'

Or specify a definitions file and play with command-line options. For example, starting with a Tracery-format file, travel.json, which you can grab as follows

curl -O https://raw.githubusercontent.com/ihh/bracery/master/examples/travel.json

Then do a few things with it

bracery -d travel.json
bracery -d travel.json -n5
bracery -d travel.json -n5 --eval '#origin# And then they met #name#.'
bracery -d travel.json -n5 --eval '#origin# And they had [fun|trouble|no luck], until they met #name#.'
bracery -d travel.json --tree
bracery -d travel.json --repl
bracery -d travel.json -n5 --async

These examples all load the Tracery travel.json rules as user extensions, using the -d option. If you specify the -b option, the command-line tool will convert and output the Tracery JSON to Bracery code.

You can run the tool in client/server mode (NB this is a very light implementation, mostly just a toy example to demonstrate networked symbol expansion):

bracery -d travel.json -S 8000 &
bracery -C http://localhost:8000/ -e '#origin#'

You can also connect directly to the repository at https://bracery.org/

bin/bracery -w -e '~common_animal'
bin/bracery -w -e '&xget~common_animal'

To get a list of available options

bracery --help

From NodeJS

Same example from @galaxykate's online tutorial

var bracery = require('bracery')

var b = new bracery.Bracery
({"name": ["Arjun","Yuuma","Darcy","Mia","Chiaki","Izzi","Azra","Lina"],
  "animal": ["unicorn","raven","sparrow","scorpion","coyote","eagle","owl","lizard","zebra","duck","kitten"],
  "mood": ["vexed","indignant","impassioned","wistful","astute","courteous"],
  "story": ["#hero# traveled with her pet #heroPet#.  #hero# was never #mood#, for the #heroPet# was always too #mood#."],
  "origin": ["#[hero:#name#][heroPet:#animal#]story#"]})

console.log (b.expand().text)

You should see an output like

Lina traveled with her pet owl.  Lina was never wistful, for the owl was always too courteous.

The expand() method tries to guess the starting nonterminal, but you can override this, or add other stuff like variable bindings, e.g.

console.log (b.expand('#origin#',{vars:{name:'Berenice'}}))
console.log (b.expand('#origin# And then they met #name#.'))

and so on.

Dynamic bindings

When using the node API, tilde-prefixed symbols like ~name can be bound to JavaScript functions:

var bracery = require('../bracery')

var b = new bracery.Bracery
  ({"percentage": function (config) { return Math.round (config.random() * 100) + ' percent' }})

console.log (b.expand('I [love|hate|like] you ~percentage!').text)

If a callback is specified, the functions can return promises:

var bracery = require('../bracery')

var b = new bracery.Bracery ({ percentage: function (config) {
  return new Promise (function (resolve, reject) {
    setTimeout (function() {
      resolve (Math.round (config.random() * 100) + ' percent')
    }, 1000)
  })
}})

console.log ('Calculating...')
b.expand ('I [love|hate|like] you ~percentage!',
          { callback: function (expansion) { console.log (expansion.text) } })

Technical details

Namespaces

Bracery defines three separate namespaces, distinguished by the prefix character. You can ignore these and just use the Tracery syntax #name# if you want, but for a deeper understanding of what's going on:

  • $name refers to a variable
  • &name refers to a core library function or macro
  • ~name refers to a user extension (local or remote)
  • #name# means "expand variable $name if defined, otherwise call user extension ~name

Limits on program complexity

Bracery was designed to be run on unfiltered user input. Since it is capable of general programming, it must also include configurable constraints on the amount of resources a program is allowed to consume, otherwise a user program could easily send it into an infinite loop or otherwise hog CPU. Another reason to impose limits is that recursion in Bracery is implemented using recursion in JavaScript, with no tail call optimization, so heavily recursive Bracery code can quickly max out the JavaScript stack.

The main constraints that Bracery enforces are maximum parse tree depth, parse tree node count, recursion depth, and output length. For the &parse function, constraints on the parsed sequence and subsequence lengths are also enforced.

Comparison with Markdown and Twine

The syntax &link{hint}{destination} will be rendered in an implementation-determined way (the default implementation just passes it through unchanged); the same is true of &reveal{hint}{revelation}, except that revelation is expanded whereas destination is quoted. At bracery.org, both are modeled as anchor elements: destination is Bracery source text that replaces the current Bracery source text when the hint is clicked, so that an interactive fiction-style pattern is &link{Doorway description}{~next_room} where the tilde-prefixed symbol ~next_room plays a similar role to a Twine passage; whereas revelation is pre-expanded text that is revealed when the hint is clicked (replacing the clicked element).

The Markdown-inspired syntax [Next room.]{You enter the next room... #next_room#} is a shortcut for &link{Next room.}{You enter the next room... #next_room#}. (Note that Bracery is orthogonal to most of Markdown; a Markdown postprocessing step is a common addition.)

The Twine-inspired syntax [[Next room.]] is a shortcut for &link{Next room.}{#next_room#}. (The symbol name is derived from the link text by lower-casing and replacing some interior characters with underscores.)

Comparison with Tracery

In Tracery, variables and symbols share the same namespace, as part of the design. For example, #sentence# is the syntax to expand the nonterminal symbol sentence, and it is also the syntax for retrieving and expanding the value of the variable named sentence. If the variable has been specified in the local context of the running program (i.e. the text up to that point), then that specified value overrides the original nonterminal symbol definition (if there was one).

Bracery keeps faith with this aspect of Tracery's design, expanding #sentence# the same way as Tracery does, with locally-specified variables overriding globally-specified symbol definitions. However, Bracery also has syntax allowing programmers to access the local variable's value directly (as $sentence) or expand the original global nonterminal (as ~sentence). It also introduces dynamic evaluation and conditional primitives, which are required to connect the above elements (#sentence#, ~sentence and $sentence), but are also quite powerful in their own right.

Distinction between symbols and variables

As well as the flanking hash-character notation that Tracery uses for symbol expansions from the grammar, #symbol#, Bracery allows the tilde character prefix, ~symbol. The Bracery variant carries the additional, specific nuance that you want to use the original symbol definitions file (or other authority) to expand the symbol, as opposed to any subsequently defined variables.

Thus, if b is the Bracery object with the example grammar defined in the NodeJS section above, then b.expand('[name:PERRY] #name# ').text will always give the result PERRY , but b.expand('[name:PERRY] ~name ').text will give Arjun , or Yuuma , or Darcy and so on, according to the example grammar.

If you just want the variable value, you can use the dollar character prefix, $name, which will evaluate to the empty string if the variable has not been defined. So, b.expand('[name:PERRY] $name ').text. will always be PERRY , again, but b.expand('$name').text will be the empty string.

Regex-like shorthands for procedural grammars

Bracery also allows other ways of generating repetitive, regex-like grammars, such as alternations

console.log (b.expand ('[hello|hallo|hullo]').text)

which should give hello, hallo or hullo, and repetitions

console.log (b.expand ('&rep{hello }{3,5}').text)

which should yield from three to five hello's, with a space after each.

See tests for more examples using the JavaScript API.

Built-in functions

Bracery also offers a number of built-in functions for processing text (e.g. case, tense, plurals) and lists. These are described under Syntax.

Rationale

Bracery works just fine as a synchronous library, running from a local symbol definitions file, like Tracery (this is the default when running from the command-line, or using the node API). However, Bracery was specifically designed to work well for asynchronous applications where the client is decoupled from the symbol definition store.

In asynchronous mode, the symbol expansion code can run on a server somewhere remote from the client (i.e. the place where procedural text generation is happening, such as the user's web browser). This means that, for example, the set of definitions can potentially be very big (including a "standard library"), or can be continually updated, or collaboratively edited.

In order to allow programmers to write efficient code in this framework, Bracery's syntax distinguishes between expansions that can be performed on the client, from those that must be performed by the server. The former (client expansions) are called variables and the latter (server expansions) are called symbols.

Syntax

The formal grammar for Bracery is in src/rhs.peg.js (specified using PEG.js)

Language features include

  • named nonterminals:
    • Tracery-style
      • #symbol_name# to expand a variable, falling back to an external definition (i.e. user extension)
    • Bracery-style
      • &$symbol_name or &${symbol_name} to expand a variable
      • ~symbol_name or ~{symbol_name} to expand an externally-defined symbol (user extension)
  • alternations (anonymous nonterminals):
    • [option1|option 2|Option number three|Some other option...]
    • can be nested: [option1|option 2|3rd opt|4th|more [options|nested options]...]
  • variables:
    • Tracery-style
      • [variable_name:value] to assign
        • [variable_name=>value] to quote-assign (this is a Bracery-specific extension)
      • #variable_name# to retrieve and expand, defaulting to externally-defined symbol ~name
        • all names are case-insensitive
    • Bracery-style
      • $variable_name={value} to assign
        • braces can be omitted if value has no whitespace or punctuation
      • $variable_name or ${variable_name} to retrieve (without expanding)
      • &eval{$variable_name} or &$variable_name to retrieve and expand
    • the Tracery-style syntax #name# is equivalent to &$name if variable $name is defined, otherwise falls back to calling user extension ~name
  • built-in text-processing functions:
    • &plural{...} (plural), &a{...} ("a" or "an")
    • &cap{...} (Capitalize), &lc{...} and &uc{...} (lower- & UPPER-case)
    • selected natural language-processing functions from compromise including
      • (for nouns) &singular and &topic
      • (for verbs) &past, &present, &future, &infinitive, &adjective, &negative
    • natural language-friendly arithmetic using compromise:
      • &add{2}{4} gives 6
      • &add{two}{4} gives six, &add{two cats}{4} gives six cats
        • form of result is determined by first argument, so &add{4}{two} and &add{4}{two cats} both evaluate to 6
      • &subtract{x}{y} behaves like &add
      • &multiply{x}{y}, &divide{x}{y}, &pow{x}{y} return digits only: &multiply{ten cats}{two dogs} is 20
      • &math{($x+$y*$z)/$a} defines a context that allows infix arithmetic operators
      • &ordinal{3} is 3rd, &cardinal{3rd} is 3
      • &dignum{3} is 3, &wordnum{three} is three
      • &random{n}, &floor{x}, &ceil{x}, &round{x} do what you probably expect
        • &prob{p}{succeed}{fail} expands to succeed with probability p, and fail otherwise
      • &eq{x}{y}, &neq{x}{y}, &gt{x}{y}, &geq{x}{y}, &lt{x}{y}, &leq{x}{y} also fairly predictable
      • similarly &inc{$x}, &dec{$x}, $x++, ++$x, $x--, --$y
        • and $x+=1, $x-=2, $x*=3, $x/=4
    • regular expressions:
      • &match/regex/flags{text}{expr} returns a list of expr evaluations ($$1, $$2, etc are bound to matching groups)
      • &replace/regex/flags{text}{replacement} returns a string
      • &split/regex/flags{text} or just &split{text} returns a list
  • special functions:
    • repetition:
      • &rep{x}{3} expands to xxx
      • &rep{x}{3,5} expands to xxx, xxxx, or xxxxx
    • conditionals:
      • &if{testExpr}then{trueExpr}else{falseExpr}
      • Evaluates to trueExpr if testExpr contains any non-whitespace characters, and falseExpr otherwise
      • The falseExpr clause is optional and defaults to the empty string
      • The then and else keywords are optional; you can write &if{testExpr}{trueExpr}{falseExpr} or &if{testExpr}{trueExpr}
      • The conditional test (testExpr) can use arithmetic operators &eq, &neq, &gt, &lt, &geq, &leq
        • also string comparison &same{x}{y} and boolean operators &and{x}{y}, &or{x}{y}, &not{x}
    • dynamic evaluation
      • &eval{expr} parses expr as Bracery and dynamically expands it
        • conversely, &quote{expr} returns expr as a text string, without doing any expansions
        • &quote{...}, &unquote{...}, &strictquote{...} work pretty much like quasiquote/unquote/quote in Scheme
        • &\{...}, &,{...}, &'{...}` are the corresponding shorthand equivalents
      • &eval{&quote{expr}} is the same as expr, although...
        • there is a configurable limit on the number of dynamic evaluations that an expression can use, to guard against infinite recursion or hammering the server
      • &quotify{expr} wraps a string or (nested) list with &quote and &list (shorthand is &q)
    • locally scoped variables:
      • Tracery-style #[x:value1][y:value2]symbol_name# (what Tracery calls "actions")
      • Bracery-style &let$x={value1}$y={value2}{something involving x and y}
    • first-class functions (or, at the very least, frequent-flyer functions that got an upgrade)
      • &call{expr}{arg1}{arg2}{arg3...} binds $$1 to arg1, $$2 to arg2, $$3 to arg3... before expanding expr
        • in other words, &let$$1={arg1}{&let$$2={arg2}{&let$$3={arg3}{...}}}
        • &$x is short for &call{$x}
      • &apply{expr}{args} is the same but the arguments are in list form
      • &function$arg1$arg2$arg3{...} is exactly the same as &quote{&let$arg1={$$1}{&let$arg2={$$2}{&let$arg3={$$3}{...}}}}
      • you can also pass args to user extensions e.g. &~extension{arg1}{arg2}{arg3}
        • &~extension is short for &xcall{~extension}
        • the 'apply' form of this is &xapply{~extension}{arglist}
      • the implementation may optionally allow retrieval of the Bracery code behind an extension symbol, using the syntax &xget{~extension}, but this is not guaranteed
        • specifically, extensions don't have to be implemented in Bracery themselves
      • for those that are, however, it's useful to be able to retrieve the code in order to do syntactic analysis of the underlying context-free grammar
      • the &parse function uses this feature
  • lists:
    • &list{...} or just &{...} creates an explicit nested list context, vs the default string context
      • e.g. $a=&list{&quote{1}&quote{2}&quote{3}} creates a list, as does $b=&list{1&,2&,3} or $c=&{1&,2&,3} or $d=&makelist{1}{2}{3}
      • &{} is the empty list, equivalent to &list{}
      • beginning a string context with {} (or any other list) makes it a list context
      • beginning a string context with a string, or wrapping it in &quote{...} makes it a string context
    • &islist{x} returns true if, and only if, x is a list
    • list-coercing functions:
      • &prepend{item}{list}, &append{list}{item} return lists
      • &first{list}, &last{list} return individual list items (can be strings or nested lists)
      • &notfirst{list}, &notlast{list} return lists
      • &nth{index}{list} returns item number index (0-based) from list
      • &indexof{item}{list} returns index of item in list, or the (falsy) empty string if item is not in list
      • &cat{list1}{list2} returns a list
      • &join{list}{item} returns a string
      • &map$varname:{list}{expr} and &filter$varname:{list}{expr} return lists
      • &reduce$varname:{list}$result={init}{expr} can return list or string
      • &for:$varname{list}{expr} returns the empty list, discarding the expansions of expr (but keeping the side-effects)
      • &shuffle{list} returns a shuffled list
        • &rotate{list} returns a rotated list (first element moved to back)
        • &bump{list} returns a pseudo-rotated list (first element moved to random place in back half)
      • &makelist{a}{b}{c}{d}{etc} returns a list and is equivalent to &list{&value{a}&value{b}&value{c}&value{d}&value{etc}}
      • &quotelist{$a}{$b}{$c}{$d}{etc} returns a list and is equivalent to &list{&quote{$a}&quote{$b}&quote{$c}&quote{$d}&quote{etc}}
      • &numsort$varname{list}{weightExpr} and &lexsort$varname{list}{tagExpr} return lists, numerically- or lexically-sorted (respectively) by the corresponding mapped expression
    • when coerced into a list context by one of the above functions, the empty string becomes the empty list and any nonempty string becomes a single-element list
    • when coerced into a string context (i.e. most contexts), a list is invisibly joined/flattened as if by &join{list}{}
    • The special constructs &cycle and &queue are useful for deterministically (vs stochastically) iterating through a range of options each time the code is called:
      • &cycle$x{list} sets $x to list the first time it's called, then rotates $x thereafter
      • &queue$x{list} sets $x to list the first time it's called, then shifts elements off $x thereafter
      • unlike &cycle, &queue does not repeat; when all elements have been shifted off, $x is the empty list, which is a non-truthy value
      • the return value of both &cycle$x{list} and &queue$x{list} is not the new value of $x, but rather &eval{&first{$x}}; $x itself represents the series of options to be iterated through, and you generally only want to display the first of these at any given time
      • these constructs are useful with &makelist and &quotelist, e.g. [tick=>You feel &cycle$mood&makelist{happy}{sad}{bored}.] #tick# #tick# #tick# #tick# expands to You feel happy. You feel sad. You feel bored. You feel happy.
    • Similarly, &playlist$x{list} cycles through list in a non-repeating random order, using &shuffle and &bump
  • functions, alternations, repetitions, variable assignments, and conditionals can be arbitrarily nested
  • everything can occur asynchronously, so symbols can be resolved and expanded from a remote store
    • but if you have a synchronously resolvable store (i.e. a local Tracery object), everything can work synchronously too
  • access to the parser (disabled by default, for performance guarantees; to enable, set { enableParse: true } in the configuration object for the expand method)
    • &syntax{...} returns the parse tree for the given Bracery expression
    • &parse{source}{expr} returns a parse tree by which Bracery expression source might have generated expr, or the empty string if no parse exists
      • The parse is probabilistic, not deterministic: if multiple valid parses exist, then the parse tree is sampled from the posterior probability distribution of valid parse trees
      • The source expression source (and any other Bracery code that it indirectly invokes) may not contain any function calls or variable assignments, only symbol references and variable lookups/expansions. This makes it a strict context-free grammar
      • The parser is not guaranteed to work correctly if the grammar contains null cycles (i.e. a series of transformations that leads back to the original symbol, with no other sequence generated) - &grammar{source} returns the (almost) Chomsky normal-form grammar used by &parse, obtained by syntactic analysis of the source expression . An example of such a null cycle is [a=>#b#|x] [b=>#a#]
      • The parser uses a variant of the Inside algorithm to sample from the posterior distribution of valid parses
      • The full Inside algorithm takes time O(L^3) and memory O(L^2) where L is the string length. This is rather expensive for long strings, so Bracery's implementation restricts the maximum subclause length to be K (via the maxSubsequenceLength config parameter), leading to memory O(KL) and time O(LK^2)
      • NB the &syntax function uses Parsing Expression Grammars (via PEG.js) which is much faster than the Inside algorithm, but is unsuited to stochastic grammars such as those specified by a Bracery program
  • syntactic sugar/hacks/apologies
    • the Tracery-style expression #name# is parsed and implemented as &if{$name}then{&eval{$name}}else{~name}. Tracery overloads the same namespace for symbol and variable names, and uses the variable if it's defined; this quasi-macro reproduces that behavior (almost)
    • braces around single-argument functions or symbols can be omitted, e.g. $currency=&cap&plural~name means the same as $currency={&cap{&plural{~name}}}
    • variable and symbol names are case-insensitive
      • the case used when a variable is referenced can be a shorthand for capitalization: you can use ~Nonterminal_name as a shorthand for &cap{~nonterminal_name}, and $Variable_name for &cap{$variable_name}
      • similarly, ~NONTERMINAL_NAME is a shorthand for &uc{~nonterminal_name}, and $VARIABLE_NAME for &uc{$variable_name}
    • some Tracery modifier syntax works, e.g. #symbol_name.capitalize# instead of &cap{#symbol_name#}
    • the syntax [name=>value1|value2|value3|...] is shorthand for $name={&quote{[value1|value2|value3|...]} and ensures that every occurrence of #name# (or &eval{$name}) will be expanded from an independently-sampled one of the values

Most/all of these features are exercised in the file test/basic.js.

Advanced features

Bracery contains some advanced features designed to allow Bracery messages to be sequenced (as in a Markov chain). These are described in the accompanying file MESSAGES.md.

About

Procedural text generator, somewhat compatible with Tracery

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published