{"id":13747382,"url":"https://github.com/maetl/calyx","last_synced_at":"2026-03-11T00:33:39.224Z","repository":{"id":56164552,"uuid":"45290994","full_name":"maetl/calyx","owner":"maetl","description":"A Ruby library for generating text with recursive template grammars.","archived":false,"fork":false,"pushed_at":"2022-11-01T12:02:13.000Z","size":413,"stargazers_count":60,"open_issues_count":10,"forks_count":5,"subscribers_count":5,"default_branch":"main","last_synced_at":"2024-04-25T09:44:02.943Z","etag":null,"topics":["dsl","generative-art","grammars","ruby","syntax-tree","text"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/maetl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-10-31T06:10:11.000Z","updated_at":"2024-03-16T16:45:08.000Z","dependencies_parsed_at":"2022-08-15T14:00:14.559Z","dependency_job_id":null,"html_url":"https://github.com/maetl/calyx","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maetl%2Fcalyx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maetl%2Fcalyx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maetl%2Fcalyx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maetl%2Fcalyx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maetl","download_url":"https://codeload.github.com/maetl/calyx/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221582685,"owners_count":16847409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","generative-art","grammars","ruby","syntax-tree","text"],"created_at":"2024-08-03T06:01:26.994Z","updated_at":"2026-03-11T00:33:34.201Z","avatar_url":"https://github.com/maetl.png","language":"Ruby","funding_links":[],"categories":["Ruby","Templating Languages"],"sub_categories":[],"readme":"# Calyx\n\n[![Gem Version](http://img.shields.io/gem/v/calyx.svg)](https://rubygems.org/gems/calyx)\n[![Build Status](https://img.shields.io/github/workflow/status/maetl/calyx/Ruby)](https://github.com/maetl/calyx/actions/workflows/ruby.yml)\n\nCalyx provides a simple API for generating text with declarative recursive grammars.\n\n## Install\n\n### Command Line\n\n```bash\ngem install calyx\n```\n\n### Gemfile\n\n```bash\ngem 'calyx'\n```\n\n## Examples\n\nThe best way to get started quickly is to install the gem and run the examples locally.\n\n## Any Gradient\n\nRequires Roda and Rack to be available.\n\n```bash\ngem install roda\n```\n\nDemonstrates how to use Calyx to construct SVG graphics. **Any Gradient** generates a rectangle with a linear gradient of random colours.\n\nRun as a web server and preview the output in a browser (`http://localhost:9292`):\n\n```bash\nruby examples/any_gradient.rb\n```\n\nOr generate SVG files via a command line pipe:\n\n```bash\nruby examples/any_gradient \u003e gradient1.xml\n```\n\n## Tiny Woodland Bot\n\nRequires the Twitter client gem and API access configured for a specific Twitter handle.\n\n```bash\ngem install twitter\n```\n\nDemonstrates how to use Calyx to make a minimal Twitter bot that periodically posts unique tweets. See [@tiny_woodland on Twitter](https://twitter.com/tiny_woodland) and the [writeup here](http://maetl.net/notes/storyboard/tiny-woodlands).\n\n```bash\nTWITTER_CONSUMER_KEY=XXX-XXX\nTWITTER_CONSUMER_SECRET=XXX-XXX\nTWITTER_ACCESS_TOKEN=XXX-XXX\nTWITTER_CONSUMER_SECRET=XXX-XXX\nruby examples/tiny_woodland_bot.rb\n```\n\n## Faker\n\n[Faker](https://github.com/stympy/faker) is a popular library for generating fake names and associated sample data like internet addresses, company names and locations.\n\nThis example demonstrates how to use Calyx to reproduce the same functionality using custom lists defined in a YAML configuration file.\n\n```bash\nruby examples/faker.rb\n```\n\n## Usage\n\nRequire the library and inherit from `Calyx::Grammar` to construct a set of rules to generate a text.\n\n```ruby\nrequire 'calyx'\n\nclass HelloWorld \u003c Calyx::Grammar\n  start 'Hello world.'\nend\n```\n\nTo generate the text itself, initialize the object and call the `generate` method.\n\n```ruby\nhello = HelloWorld.new\nhello.generate\n# \u003e \"Hello world.\"\n```\n\nObviously, this hardcoded sentence isn’t very interesting by itself. Possible variations can be added to the text by adding additional rules which provide a named set of text strings. The rule delimiter syntax (`{}`) can be used to substitute the generated content of other rules.\n\n```ruby\nclass HelloWorld \u003c Calyx::Grammar\n  start '{greeting} world.'\n  greeting 'Hello', 'Hi', 'Hey', 'Yo'\nend\n```\n\nEach time `#generate` runs, it evaluates the tree and randomly selects variations of rules to construct a resulting string.\n\n```ruby\nhello = HelloWorld.new\n\nhello.generate\n# \u003e \"Hi world.\"\n\nhello.generate\n# \u003e \"Hello world.\"\n\nhello.generate\n# \u003e \"Yo world.\"\n```\n\nBy convention, the `start` rule specifies the default starting point for generating the final text. You can start from any other named rule by passing it explicitly to the generate method.\n\n```ruby\nclass HelloWorld \u003c Calyx::Grammar\n  hello 'Hello world.'\nend\n\nhello = HelloWorld.new\nhello.generate(:hello)\n```\n\n### Block Constructors\n\nAs an alternative to subclassing, you can also construct rules unique to an instance by passing a block when initializing the class:\n\n```ruby\nhello = Calyx::Grammar.new do\n  start '{greeting} world.'\n  greeting 'Hello', 'Hi', 'Hey', 'Yo'\nend\n\nhello.generate\n```\n\n### Template Expressions\n\nBasic rule substitution uses single curly brackets as delimiters for template expressions:\n\n```ruby\nfruit = Calyx::Grammar.new do\n  start '{colour} {fruit}'\n  colour 'red', 'green', 'yellow'\n  fruit 'apple', 'pear', 'tomato'\nend\n\n6.times { fruit.generate }\n# =\u003e \"yellow pear\"\n# =\u003e \"red apple\"\n# =\u003e \"green tomato\"\n# =\u003e \"red pear\"\n# =\u003e \"yellow tomato\"\n# =\u003e \"green apple\"\n```\n\n### Nesting and Substitution\n\nRules are recursive. They can be arbitrarily nested and connected to generate larger and more complex texts.\n\n```ruby\nclass HelloWorld \u003c Calyx::Grammar\n  start '{greeting} {world_phrase}.'\n  greeting 'Hello', 'Hi', 'Hey', 'Yo'\n  world_phrase '{happy_adj} world', '{sad_adj} world', 'world'\n  happy_adj 'wonderful', 'amazing', 'bright', 'beautiful'\n  sad_adj 'cruel', 'miserable'\nend\n```\n\nNesting and hierarchy can be manipulated to balance consistency with novelty. The exact same word atoms can be combined in a variety of ways to produce strikingly different resulting texts.\n\n```ruby\nmodule HelloWorld\n  class Sentiment \u003c Calyx::Grammar\n    start '{happy_phrase}', '{sad_phrase}'\n    happy_phrase '{happy_greeting} {happy_adj} world.'\n    happy_greeting 'Hello', 'Hi', 'Hey', 'Yo'\n    happy_adj 'wonderful', 'amazing', 'bright', 'beautiful'\n    sad_phrase '{sad_greeting} {sad_adj} world.'\n    sad_greeting 'Goodbye', 'So long', 'Farewell'\n    sad_adj 'cruel', 'miserable'\n  end\n\n  class Mixed \u003c Calyx::Grammar\n    start '{greeting} {adj} world.'\n    greeting 'Hello', 'Hi', 'Hey', 'Yo', 'Goodbye', 'So long', 'Farewell'\n    adj 'wonderful', 'amazing', 'bright', 'beautiful', 'cruel', 'miserable'\n  end\nend\n```\n\n### Random Sampling\n\nBy default, the outcomes of generated rules are selected with Ruby’s built-in pseudorandom number generator (as seen in methods like `Kernel.rand` and `Array.sample`). To seed the random number generator, pass in an integer seed value as the first argument to the constructor:\n\n```ruby\ngrammar = Calyx::Grammar.new(seed: 12345) do\n  # rules...\nend\n```\n\nAlternatively, you can pass a preconfigured instance of Ruby’s stdlib `Random` class:\n\n```ruby\nrandom = Random.new(12345)\n\ngrammar = Calyx::Grammar.new(rng: random) do\n  # rules...\nend\n```\n\nWhen a random seed isn’t supplied, `Time.new.to_i` is used as the default seed, which makes each run of the generator relatively unique.\n\n### Weighted Choices\n\nChoices can be weighted so that some rules have a greater probability of expanding than others.\n\nWeights are defined by passing a hash instead of a list of rules where the keys are strings or symbols representing the grammar rules and the values are weights.\n\nWeights can be represented as floats, integers or ranges.\n\n- Floats must be in the interval 0..1 and the given weights for a production must sum to 1.\n- Ranges must be contiguous and cover the entire interval from 1 to the maximum value of the largest range.\n- Integers (Fixnums) will produce a distribution based on the sum of all given numbers, with each number being a fraction of that sum.\n\nThe following definitions produce an equivalent weighting of choices:\n\n```ruby\nCalyx::Grammar.new do\n  start 'heads' =\u003e 1, 'tails' =\u003e 1\nend\n\nCalyx::Grammar.new do\n  start 'heads' =\u003e 0.5, 'tails' =\u003e 0.5\nend\n\nCalyx::Grammar.new do\n  start 'heads' =\u003e 1..5, 'tails' =\u003e 6..10\nend\n\nCalyx::Grammar.new do\n  start 'heads' =\u003e 50, 'tails' =\u003e 50\nend\n```\n\nThere’s a lot of interesting things you can do with this. For example, you can model the triangular distribution produced by rolling 2d6:\n\n```ruby\nCalyx::Grammar.new do\n  start(\n    '2' =\u003e 1,\n    '3' =\u003e 2,\n    '4' =\u003e 3,\n    '5' =\u003e 4,\n    '6' =\u003e 5,\n    '7' =\u003e 6,\n    '8' =\u003e 5,\n    '9' =\u003e 4,\n    '10' =\u003e 3,\n    '11' =\u003e 2,\n    '12' =\u003e 1\n  )\nend\n```\n\nOr reproduce Gary Gygax’s famous generation table from the original [Dungeon Master’s Guide](https://en.wikipedia.org/wiki/Dungeon_Master%27s_Guide#Advanced_Dungeons_.26_Dragons) (page 171):\n\n```ruby\nCalyx::Grammar.new do\n  start(\n    :empty =\u003e 0.6,\n    :monster =\u003e 0.1,\n    :monster_treasure =\u003e 0.15,\n    :special =\u003e 0.05,\n    :trick_trap =\u003e 0.05,\n    :treasure =\u003e 0.05\n  )\n  empty 'Empty'\n  monster 'Monster Only'\n  monster_treasure 'Monster and Treasure'\n  special 'Special'\n  trick_trap 'Trick/Trap.'\n  treasure 'Treasure'\nend\n```\n\n## String Modifiers\n\nDot-notation is supported in template expressions, allowing you to call any available method on the `String` object returned from a rule. Formatting methods can be chained arbitrarily and will execute in the same way as they would in native Ruby code.\n\n```ruby\ngreeting = Calyx::Grammar.new do\n  start '{hello.capitalize} there.', 'Why, {hello} there.'\n  hello 'hello', 'hi'\nend\n\n4.times { greeting.generate }\n# =\u003e \"Hello there.\"\n# =\u003e \"Hi there.\"\n# =\u003e \"Why, hello there.\"\n# =\u003e \"Why, hi there.\"\n```\n\nYou can also extend the grammar with custom modifiers that provide useful formatting functions.\n\n### Filters\n\nFilters accept an input string and return the transformed output:\n\n```ruby\ngreeting = Calyx::Grammar.new do\n  filter :shoutycaps do |input|\n    input.upcase\n  end\n\n  start '{hello.shoutycaps} there.', 'Why, {hello.shoutycaps} there.'\n  hello 'hello', 'hi'\nend\n\n4.times { greeting.generate }\n# =\u003e \"HELLO there.\"\n# =\u003e \"HI there.\"\n# =\u003e \"Why, HELLO there.\"\n# =\u003e \"Why, HI there.\"\n```\n\n### Mappings\n\nThe mapping shortcut allows you to specify a map of regex patterns pointing to their resulting substitution strings:\n\n```ruby\ngreen_bottle = Calyx::Grammar.new do\n  mapping :pluralize, /(.+)/ =\u003e '\\\\1s'\n  start 'One green {bottle}.', 'Two green {bottle.pluralize}.'\n  bottle 'bottle'\nend\n\n2.times { green_bottle.generate }\n# =\u003e \"One green bottle.\"\n# =\u003e \"Two green bottles.\"\n```\n\n### Modifier Mixins\n\nIn order to use more intricate rewriting and formatting methods in a modifier chain, you can add methods to a module and embed it in a grammar using the `modifier` classmethod.\n\nModifier methods accept a single argument representing the input string from the previous step in the expression chain and must return a string, representing the modified output.\n\n```ruby\nmodule FullStop\n  def full_stop(input)\n    input \u003c\u003c '.'\n  end\nend\n\nhello = Calyx::Grammar.new do\n  modifier FullStop\n  start '{hello.capitalize.full_stop}'\n  hello 'hello'\nend\n\nhello.generate\n# =\u003e \"Hello.\"\n```\n\nTo share custom modifiers across multiple grammars, you can include the module in `Calyx::Modifiers`. This will make the methods available to all subsequent instances:\n\n```ruby\nmodule FullStop\n  def full_stop(input)\n    input \u003c\u003c '.'\n  end\nend\n\nclass Calyx::Modifiers\n  include FullStop\nend\n```\n\n### Monkeypatching String\n\nAlternatively, you can combine methods from existing Gems that monkeypatch `String`:\n\n```ruby\nrequire 'indefinite_article'\n\nmodule FullStop\n  def full_stop\n    self \u003c\u003c '.'\n  end\nend\n\nclass String\n  include FullStop\nend\n\nnoun_articles = Calyx::Grammar.new do\n  start '{fruit.with_indefinite_article.capitalize.full_stop}'\n  fruit 'apple', 'orange', 'banana', 'pear'\nend\n\n4.times { noun_articles.generate }\n# =\u003e \"An apple.\"\n# =\u003e \"An orange.\"\n# =\u003e \"A banana.\"\n# =\u003e \"A pear.\"\n```\n\n### Memoized Rules\n\nRule expansions can be ‘memoized’ so that multiple references to the same rule return the same value. This is useful for picking a noun from a list and reusing it in multiple places within a text.\n\nThe `@` sigil is used to mark memoized rules. This evaluates the rule and stores it in memory the first time it’s referenced. All subsequent references to the memoized rule use the same stored value.\n\n```ruby\n# Without memoization\ngrammar = Calyx::Grammar.new do\n  start '{name} \u003c{name.downcase}\u003e'\n  name 'Daenerys', 'Tyrion', 'Jon'\nend\n\n3.times { grammar.generate }\n# =\u003e Daenerys \u003cjon\u003e\n# =\u003e Tyrion \u003cdaenerys\u003e\n# =\u003e Jon \u003ctyrion\u003e\n\n# With memoization\ngrammar = Calyx::Grammar.new do\n  start '{@name} \u003c{@name.downcase}\u003e'\n  name 'Daenerys', 'Tyrion', 'Jon'\nend\n\n3.times { grammar.generate }\n# =\u003e Tyrion \u003ctyrion\u003e\n# =\u003e Daenerys \u003cdaenerys\u003e\n# =\u003e Jon \u003cjon\u003e\n```\n\nNote that the memoization symbol can only be used on the right hand side of a production rule.\n\n### Unique Rules\n\nRule expansions can be marked as ‘unique’, meaning that multiple references to the same rule always return a different value. This is useful for situations where the same result appearing twice would appear awkward and messy.\n\nUnique rules are marked by the `$` sigil.\n\n```ruby\ngrammar = Calyx::Grammar.new do\n  start \"{$medal}, {$medal}, {$medal}\"\n  medal 'Gold', 'Silver', 'Bronze'\nend\n\ngrammar.generate\n# =\u003e Silver, Bronze, Gold\n```\n\n### Dynamically Constructing Rules\n\nTemplate expansions can be dynamically constructed at runtime by passing a context map of rules to the `#generate` method:\n\n```ruby\nclass AppGreeting \u003c Calyx::Grammar\n  start 'Hi {username}!', 'Welcome back {username}...', 'Hola {username}'\nend\n\ncontext = {\n  username: UserModel.username\n}\n\ngreeting = AppGreeting.new\ngreeting.generate(context)\n```\n\n### External File Formats\n\nIn addition to defining grammars in pure Ruby, you can load them from external JSON and YAML files:\n\n```ruby\nhello = Calyx::Grammar.load('hello.yml')\nhello.generate\n```\n\nThe format requires a flat map with keys representing the left-hand side named symbols and the values representing the right hand side substitution rules.\n\nIn JSON:\n\n```json\n{\n  \"start\": \"{greeting} world.\",\n  \"greeting\": [\"Hello\", \"Hi\", \"Hey\", \"Yo\"]\n}\n```\n\nIn YAML:\n\n```yaml\n---\nstart: \"{greeting} world.\"\ngreeting:\n  - Hello\n  - Hi\n  - Hey\n  - Yo\n```\n\n### Accessing the Raw Generated Tree\n\nCalling `#evaluate` on the grammar instance will give you access to the raw generated tree structure before it gets flattened into a string.\n\nThe tree is encoded as an array of nested arrays, with the leading symbols labeling the choices and rules selected, and the trailing terminal leaves encoding string values.\n\nThis may not make a lot of sense unless you’re familiar with the concept of [s-expressions](https://en.wikipedia.org/wiki/S-expression). It’s a fairly speculative feature at this stage, but it leads to some interesting possibilities.\n\n```ruby\ngrammar = Calyx::Grammar.new do\n  start 'Riddle me ree.'\nend\n\ngrammar.evaluate\n# =\u003e [:start, [:choice, [:concat, [[:atom, \"Riddle me ree.\"]]]]]\n```\n\n## Roadmap\n\nRough plan for stabilising the API and features for a `1.0` release.\n\n| Version | Features planned |\n|---------|------------------|\n| `0.6`   | ~~block constructor~~ |\n| `0.7`   | ~~support for template context map passed to generate~~ |\n| `0.8`   | ~~method missing metaclass API~~ |\n| `0.9`   | ~~return grammar tree from `#evaluate`, with flattened string from `#generate` being separate~~ |\n| `0.10`  | ~~inject custom string functions for parameterised rules, transforms and mappings~~ |\n| `0.11`  | ~~support YAML format (and JSON?)~~ |\n| `0.12`   | ~~API documentation~~ |\n| `0.13`   | ~~Support for unique rules~~ |\n| `0.14`   | ~~Support for Ruby 2.4~~ |\n| `0.15`   | ~~Options config and ‘strict mode’ error handling~~ |\n| `0.16`   | ~~Improve representation of weighted probability selection~~ |\n| `0.17`   | ~~Return result object from `#generate` calls~~ |\n\n## Credits\n\n### Author \u0026 Maintainer\n\n- [Mark Rickerby](https://github.com/maetl)\n\n### Contributors\n\n- [Tariq Ali](https://github.com/tra38)\n\n## License\n\nCalyx is open source and provided under the terms of the MIT license. Copyright (c) 2015-2017 [Editorial Technology](http://editorial.technology/).\n\nSee the `LICENSE` file [included with the project distribution](https://github.com/maetl/calyx/blob/master/LICENSE) for more information.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaetl%2Fcalyx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaetl%2Fcalyx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaetl%2Fcalyx/lists"}