├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── LICENSE ├── README.md ├── SYNTAX.md ├── calyx.gemspec ├── examples ├── any_gradient.rb ├── faker.json ├── faker.rb └── tiny_woodland_bot.rb ├── lib ├── calyx.rb └── calyx │ ├── errors.rb │ ├── format.rb │ ├── grammar.rb │ ├── mapping.rb │ ├── modifiers.rb │ ├── options.rb │ ├── prefix_tree.rb │ ├── production │ ├── affix_table.rb │ ├── uniform_branch.rb │ └── weighted_branch.rb │ ├── registry.rb │ ├── result.rb │ ├── rule.rb │ ├── syntax │ ├── choices.rb │ ├── concat.rb │ ├── expression.rb │ ├── memo.rb │ ├── non_terminal.rb │ ├── paired_mapping.rb │ ├── terminal.rb │ ├── token.rb │ ├── unique.rb │ └── weighted_choices.rb │ └── version.rb └── spec ├── format ├── load_json_spec.rb ├── load_spec.rb └── samples │ ├── bad_extension.bad │ ├── bad_syntax.json │ ├── bad_weights.json │ ├── hello.json │ ├── hello_statement.json │ ├── multiple_choices.json │ ├── rule_expansion.json │ └── weighted_choices.json ├── grammar ├── affix_table_spec.rb ├── class_spec.rb ├── context_hash_spec.rb ├── error_trace_spec.rb ├── generate_spec.rb ├── instance_spec.rb ├── interpolation_spec.rb ├── mapping_spec.rb ├── memo_spec.rb ├── options_spec.rb ├── rules_dsl_spec.rb ├── strict_evaluation_spec.rb ├── symbols_spec.rb ├── unique_spec.rb └── weighted_choices_spec.rb ├── mapping_spec.rb ├── options_spec.rb ├── prefix_tree_spec.rb ├── registry_spec.rb ├── result_spec.rb ├── rule_spec.rb ├── spec_helper.rb └── syntax ├── choices_spec.rb ├── concat_spec.rb ├── expression_spec.rb ├── memo_spec.rb ├── non_terminal_spec.rb ├── terminal_spec.rb └── weighted_choices_spec.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ main ] 13 | pull_request: 14 | branches: [ main ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up Ruby 24 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 25 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 26 | uses: ruby/setup-ruby@v1 27 | #uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 28 | with: 29 | ruby-version: 2.6 30 | - name: Install dependencies 31 | run: bundle install 32 | - name: Run tests 33 | run: bundle exec rspec 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /*.gem 11 | .DS_Store 12 | NOTES* 13 | /docs/node_modules 14 | /docs/_site 15 | /docs/.sass-cache 16 | /docs/.jekyll-metadata 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [me@maetl.net](mailto:me@maetl.net). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Calyx 2 | 3 | Calyx is an open source project and contributions are welcome. 4 | 5 | ## General Contributions 6 | 7 | The best way to contribute is to use the Gem. Install it on a local project and try things out. Does it work? Does it do what you expect? Is there anything missing? 8 | 9 | It’s really helpful to contribute to discussion on open issues, reporting bugs or suggest new features. Any feedback and criticism is received with gratitude. 10 | 11 | ## Code Contributions 12 | 13 | Changes that fix bugs and improve test coverage will generally be merged on-the-spot. Larger changes that introduce new features should harmonise with the vision, goals and style of the project. If in doubt, just ask in advance. 14 | 15 | ### Submitting Changes 16 | 17 | Changes to the source code and documentation should be submitted as a pull request on GitHub, corresponding to the following process: 18 | 19 | - Fork the repo and make a new branch for your changes 20 | - Submit your branch as a pull request against the master branch 21 | 22 | If any aspects of your changes need further explanation, use the pull request description to provide further detail and context (including code samples as necessary). 23 | 24 | Please don’t bump the version as part of your pull request (this happens separately). 25 | 26 | ### Pull Request Checklist 27 | 28 | - Extraneous and trivial small commits should be squashed into larger descriptive commits 29 | - Commits should include concise and clear messages using standard formatting conventions 30 | - The test suite must be passing 31 | - Newly introduced code branches should be covered by tests 32 | - Introduce new tests if existing tests don’t support your changes 33 | - Changes to method signatures and class organisation should be annotated by doc comments 34 | 35 | ## Code of Conduct 36 | 37 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. 38 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Editorial Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Calyx 2 | 3 | [![Gem Version](http://img.shields.io/gem/v/calyx.svg)](https://rubygems.org/gems/calyx) 4 | [![Build Status](https://img.shields.io/github/workflow/status/maetl/calyx/Ruby)](https://github.com/maetl/calyx/actions/workflows/ruby.yml) 5 | 6 | Calyx provides a simple API for generating text with declarative recursive grammars. 7 | 8 | ## Install 9 | 10 | ### Command Line 11 | 12 | ```bash 13 | gem install calyx 14 | ``` 15 | 16 | ### Gemfile 17 | 18 | ```bash 19 | gem 'calyx' 20 | ``` 21 | 22 | ## Examples 23 | 24 | The best way to get started quickly is to install the gem and run the examples locally. 25 | 26 | ## Any Gradient 27 | 28 | Requires Roda and Rack to be available. 29 | 30 | ```bash 31 | gem install roda 32 | ``` 33 | 34 | Demonstrates how to use Calyx to construct SVG graphics. **Any Gradient** generates a rectangle with a linear gradient of random colours. 35 | 36 | Run as a web server and preview the output in a browser (`http://localhost:9292`): 37 | 38 | ```bash 39 | ruby examples/any_gradient.rb 40 | ``` 41 | 42 | Or generate SVG files via a command line pipe: 43 | 44 | ```bash 45 | ruby examples/any_gradient > gradient1.xml 46 | ``` 47 | 48 | ## Tiny Woodland Bot 49 | 50 | Requires the Twitter client gem and API access configured for a specific Twitter handle. 51 | 52 | ```bash 53 | gem install twitter 54 | ``` 55 | 56 | Demonstrates 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). 57 | 58 | ```bash 59 | TWITTER_CONSUMER_KEY=XXX-XXX 60 | TWITTER_CONSUMER_SECRET=XXX-XXX 61 | TWITTER_ACCESS_TOKEN=XXX-XXX 62 | TWITTER_CONSUMER_SECRET=XXX-XXX 63 | ruby examples/tiny_woodland_bot.rb 64 | ``` 65 | 66 | ## Faker 67 | 68 | [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. 69 | 70 | This example demonstrates how to use Calyx to reproduce the same functionality using custom lists defined in a YAML configuration file. 71 | 72 | ```bash 73 | ruby examples/faker.rb 74 | ``` 75 | 76 | ## Usage 77 | 78 | Require the library and inherit from `Calyx::Grammar` to construct a set of rules to generate a text. 79 | 80 | ```ruby 81 | require 'calyx' 82 | 83 | class HelloWorld < Calyx::Grammar 84 | start 'Hello world.' 85 | end 86 | ``` 87 | 88 | To generate the text itself, initialize the object and call the `generate` method. 89 | 90 | ```ruby 91 | hello = HelloWorld.new 92 | hello.generate 93 | # > "Hello world." 94 | ``` 95 | 96 | Obviously, 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. 97 | 98 | ```ruby 99 | class HelloWorld < Calyx::Grammar 100 | start '{greeting} world.' 101 | greeting 'Hello', 'Hi', 'Hey', 'Yo' 102 | end 103 | ``` 104 | 105 | Each time `#generate` runs, it evaluates the tree and randomly selects variations of rules to construct a resulting string. 106 | 107 | ```ruby 108 | hello = HelloWorld.new 109 | 110 | hello.generate 111 | # > "Hi world." 112 | 113 | hello.generate 114 | # > "Hello world." 115 | 116 | hello.generate 117 | # > "Yo world." 118 | ``` 119 | 120 | By 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. 121 | 122 | ```ruby 123 | class HelloWorld < Calyx::Grammar 124 | hello 'Hello world.' 125 | end 126 | 127 | hello = HelloWorld.new 128 | hello.generate(:hello) 129 | ``` 130 | 131 | ### Block Constructors 132 | 133 | As an alternative to subclassing, you can also construct rules unique to an instance by passing a block when initializing the class: 134 | 135 | ```ruby 136 | hello = Calyx::Grammar.new do 137 | start '{greeting} world.' 138 | greeting 'Hello', 'Hi', 'Hey', 'Yo' 139 | end 140 | 141 | hello.generate 142 | ``` 143 | 144 | ### Template Expressions 145 | 146 | Basic rule substitution uses single curly brackets as delimiters for template expressions: 147 | 148 | ```ruby 149 | fruit = Calyx::Grammar.new do 150 | start '{colour} {fruit}' 151 | colour 'red', 'green', 'yellow' 152 | fruit 'apple', 'pear', 'tomato' 153 | end 154 | 155 | 6.times { fruit.generate } 156 | # => "yellow pear" 157 | # => "red apple" 158 | # => "green tomato" 159 | # => "red pear" 160 | # => "yellow tomato" 161 | # => "green apple" 162 | ``` 163 | 164 | ### Nesting and Substitution 165 | 166 | Rules are recursive. They can be arbitrarily nested and connected to generate larger and more complex texts. 167 | 168 | ```ruby 169 | class HelloWorld < Calyx::Grammar 170 | start '{greeting} {world_phrase}.' 171 | greeting 'Hello', 'Hi', 'Hey', 'Yo' 172 | world_phrase '{happy_adj} world', '{sad_adj} world', 'world' 173 | happy_adj 'wonderful', 'amazing', 'bright', 'beautiful' 174 | sad_adj 'cruel', 'miserable' 175 | end 176 | ``` 177 | 178 | Nesting 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. 179 | 180 | ```ruby 181 | module HelloWorld 182 | class Sentiment < Calyx::Grammar 183 | start '{happy_phrase}', '{sad_phrase}' 184 | happy_phrase '{happy_greeting} {happy_adj} world.' 185 | happy_greeting 'Hello', 'Hi', 'Hey', 'Yo' 186 | happy_adj 'wonderful', 'amazing', 'bright', 'beautiful' 187 | sad_phrase '{sad_greeting} {sad_adj} world.' 188 | sad_greeting 'Goodbye', 'So long', 'Farewell' 189 | sad_adj 'cruel', 'miserable' 190 | end 191 | 192 | class Mixed < Calyx::Grammar 193 | start '{greeting} {adj} world.' 194 | greeting 'Hello', 'Hi', 'Hey', 'Yo', 'Goodbye', 'So long', 'Farewell' 195 | adj 'wonderful', 'amazing', 'bright', 'beautiful', 'cruel', 'miserable' 196 | end 197 | end 198 | ``` 199 | 200 | ### Random Sampling 201 | 202 | By 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: 203 | 204 | ```ruby 205 | grammar = Calyx::Grammar.new(seed: 12345) do 206 | # rules... 207 | end 208 | ``` 209 | 210 | Alternatively, you can pass a preconfigured instance of Ruby’s stdlib `Random` class: 211 | 212 | ```ruby 213 | random = Random.new(12345) 214 | 215 | grammar = Calyx::Grammar.new(rng: random) do 216 | # rules... 217 | end 218 | ``` 219 | 220 | When 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. 221 | 222 | ### Weighted Choices 223 | 224 | Choices can be weighted so that some rules have a greater probability of expanding than others. 225 | 226 | Weights 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. 227 | 228 | Weights can be represented as floats, integers or ranges. 229 | 230 | - Floats must be in the interval 0..1 and the given weights for a production must sum to 1. 231 | - Ranges must be contiguous and cover the entire interval from 1 to the maximum value of the largest range. 232 | - Integers (Fixnums) will produce a distribution based on the sum of all given numbers, with each number being a fraction of that sum. 233 | 234 | The following definitions produce an equivalent weighting of choices: 235 | 236 | ```ruby 237 | Calyx::Grammar.new do 238 | start 'heads' => 1, 'tails' => 1 239 | end 240 | 241 | Calyx::Grammar.new do 242 | start 'heads' => 0.5, 'tails' => 0.5 243 | end 244 | 245 | Calyx::Grammar.new do 246 | start 'heads' => 1..5, 'tails' => 6..10 247 | end 248 | 249 | Calyx::Grammar.new do 250 | start 'heads' => 50, 'tails' => 50 251 | end 252 | ``` 253 | 254 | There’s a lot of interesting things you can do with this. For example, you can model the triangular distribution produced by rolling 2d6: 255 | 256 | ```ruby 257 | Calyx::Grammar.new do 258 | start( 259 | '2' => 1, 260 | '3' => 2, 261 | '4' => 3, 262 | '5' => 4, 263 | '6' => 5, 264 | '7' => 6, 265 | '8' => 5, 266 | '9' => 4, 267 | '10' => 3, 268 | '11' => 2, 269 | '12' => 1 270 | ) 271 | end 272 | ``` 273 | 274 | Or 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): 275 | 276 | ```ruby 277 | Calyx::Grammar.new do 278 | start( 279 | :empty => 0.6, 280 | :monster => 0.1, 281 | :monster_treasure => 0.15, 282 | :special => 0.05, 283 | :trick_trap => 0.05, 284 | :treasure => 0.05 285 | ) 286 | empty 'Empty' 287 | monster 'Monster Only' 288 | monster_treasure 'Monster and Treasure' 289 | special 'Special' 290 | trick_trap 'Trick/Trap.' 291 | treasure 'Treasure' 292 | end 293 | ``` 294 | 295 | ## String Modifiers 296 | 297 | Dot-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. 298 | 299 | ```ruby 300 | greeting = Calyx::Grammar.new do 301 | start '{hello.capitalize} there.', 'Why, {hello} there.' 302 | hello 'hello', 'hi' 303 | end 304 | 305 | 4.times { greeting.generate } 306 | # => "Hello there." 307 | # => "Hi there." 308 | # => "Why, hello there." 309 | # => "Why, hi there." 310 | ``` 311 | 312 | You can also extend the grammar with custom modifiers that provide useful formatting functions. 313 | 314 | ### Filters 315 | 316 | Filters accept an input string and return the transformed output: 317 | 318 | ```ruby 319 | greeting = Calyx::Grammar.new do 320 | filter :shoutycaps do |input| 321 | input.upcase 322 | end 323 | 324 | start '{hello.shoutycaps} there.', 'Why, {hello.shoutycaps} there.' 325 | hello 'hello', 'hi' 326 | end 327 | 328 | 4.times { greeting.generate } 329 | # => "HELLO there." 330 | # => "HI there." 331 | # => "Why, HELLO there." 332 | # => "Why, HI there." 333 | ``` 334 | 335 | ### Mappings 336 | 337 | The mapping shortcut allows you to specify a map of regex patterns pointing to their resulting substitution strings: 338 | 339 | ```ruby 340 | green_bottle = Calyx::Grammar.new do 341 | mapping :pluralize, /(.+)/ => '\\1s' 342 | start 'One green {bottle}.', 'Two green {bottle.pluralize}.' 343 | bottle 'bottle' 344 | end 345 | 346 | 2.times { green_bottle.generate } 347 | # => "One green bottle." 348 | # => "Two green bottles." 349 | ``` 350 | 351 | ### Modifier Mixins 352 | 353 | In 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. 354 | 355 | Modifier 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. 356 | 357 | ```ruby 358 | module FullStop 359 | def full_stop(input) 360 | input << '.' 361 | end 362 | end 363 | 364 | hello = Calyx::Grammar.new do 365 | modifier FullStop 366 | start '{hello.capitalize.full_stop}' 367 | hello 'hello' 368 | end 369 | 370 | hello.generate 371 | # => "Hello." 372 | ``` 373 | 374 | To share custom modifiers across multiple grammars, you can include the module in `Calyx::Modifiers`. This will make the methods available to all subsequent instances: 375 | 376 | ```ruby 377 | module FullStop 378 | def full_stop(input) 379 | input << '.' 380 | end 381 | end 382 | 383 | class Calyx::Modifiers 384 | include FullStop 385 | end 386 | ``` 387 | 388 | ### Monkeypatching String 389 | 390 | Alternatively, you can combine methods from existing Gems that monkeypatch `String`: 391 | 392 | ```ruby 393 | require 'indefinite_article' 394 | 395 | module FullStop 396 | def full_stop 397 | self << '.' 398 | end 399 | end 400 | 401 | class String 402 | include FullStop 403 | end 404 | 405 | noun_articles = Calyx::Grammar.new do 406 | start '{fruit.with_indefinite_article.capitalize.full_stop}' 407 | fruit 'apple', 'orange', 'banana', 'pear' 408 | end 409 | 410 | 4.times { noun_articles.generate } 411 | # => "An apple." 412 | # => "An orange." 413 | # => "A banana." 414 | # => "A pear." 415 | ``` 416 | 417 | ### Memoized Rules 418 | 419 | Rule 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. 420 | 421 | The `@` 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. 422 | 423 | ```ruby 424 | # Without memoization 425 | grammar = Calyx::Grammar.new do 426 | start '{name} <{name.downcase}>' 427 | name 'Daenerys', 'Tyrion', 'Jon' 428 | end 429 | 430 | 3.times { grammar.generate } 431 | # => Daenerys 432 | # => Tyrion 433 | # => Jon 434 | 435 | # With memoization 436 | grammar = Calyx::Grammar.new do 437 | start '{@name} <{@name.downcase}>' 438 | name 'Daenerys', 'Tyrion', 'Jon' 439 | end 440 | 441 | 3.times { grammar.generate } 442 | # => Tyrion 443 | # => Daenerys 444 | # => Jon 445 | ``` 446 | 447 | Note that the memoization symbol can only be used on the right hand side of a production rule. 448 | 449 | ### Unique Rules 450 | 451 | Rule 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. 452 | 453 | Unique rules are marked by the `$` sigil. 454 | 455 | ```ruby 456 | grammar = Calyx::Grammar.new do 457 | start "{$medal}, {$medal}, {$medal}" 458 | medal 'Gold', 'Silver', 'Bronze' 459 | end 460 | 461 | grammar.generate 462 | # => Silver, Bronze, Gold 463 | ``` 464 | 465 | ### Dynamically Constructing Rules 466 | 467 | Template expansions can be dynamically constructed at runtime by passing a context map of rules to the `#generate` method: 468 | 469 | ```ruby 470 | class AppGreeting < Calyx::Grammar 471 | start 'Hi {username}!', 'Welcome back {username}...', 'Hola {username}' 472 | end 473 | 474 | context = { 475 | username: UserModel.username 476 | } 477 | 478 | greeting = AppGreeting.new 479 | greeting.generate(context) 480 | ``` 481 | 482 | ### External File Formats 483 | 484 | In addition to defining grammars in pure Ruby, you can load them from external JSON and YAML files: 485 | 486 | ```ruby 487 | hello = Calyx::Grammar.load('hello.yml') 488 | hello.generate 489 | ``` 490 | 491 | The format requires a flat map with keys representing the left-hand side named symbols and the values representing the right hand side substitution rules. 492 | 493 | In JSON: 494 | 495 | ```json 496 | { 497 | "start": "{greeting} world.", 498 | "greeting": ["Hello", "Hi", "Hey", "Yo"] 499 | } 500 | ``` 501 | 502 | In YAML: 503 | 504 | ```yaml 505 | --- 506 | start: "{greeting} world." 507 | greeting: 508 | - Hello 509 | - Hi 510 | - Hey 511 | - Yo 512 | ``` 513 | 514 | ### Accessing the Raw Generated Tree 515 | 516 | Calling `#evaluate` on the grammar instance will give you access to the raw generated tree structure before it gets flattened into a string. 517 | 518 | The 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. 519 | 520 | This 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. 521 | 522 | ```ruby 523 | grammar = Calyx::Grammar.new do 524 | start 'Riddle me ree.' 525 | end 526 | 527 | grammar.evaluate 528 | # => [:start, [:choice, [:concat, [[:atom, "Riddle me ree."]]]]] 529 | ``` 530 | 531 | ## Roadmap 532 | 533 | Rough plan for stabilising the API and features for a `1.0` release. 534 | 535 | | Version | Features planned | 536 | |---------|------------------| 537 | | `0.6` | ~~block constructor~~ | 538 | | `0.7` | ~~support for template context map passed to generate~~ | 539 | | `0.8` | ~~method missing metaclass API~~ | 540 | | `0.9` | ~~return grammar tree from `#evaluate`, with flattened string from `#generate` being separate~~ | 541 | | `0.10` | ~~inject custom string functions for parameterised rules, transforms and mappings~~ | 542 | | `0.11` | ~~support YAML format (and JSON?)~~ | 543 | | `0.12` | ~~API documentation~~ | 544 | | `0.13` | ~~Support for unique rules~~ | 545 | | `0.14` | ~~Support for Ruby 2.4~~ | 546 | | `0.15` | ~~Options config and ‘strict mode’ error handling~~ | 547 | | `0.16` | ~~Improve representation of weighted probability selection~~ | 548 | | `0.17` | ~~Return result object from `#generate` calls~~ | 549 | 550 | ## Credits 551 | 552 | ### Author & Maintainer 553 | 554 | - [Mark Rickerby](https://github.com/maetl) 555 | 556 | ### Contributors 557 | 558 | - [Tariq Ali](https://github.com/tra38) 559 | 560 | ## License 561 | 562 | Calyx is open source and provided under the terms of the MIT license. Copyright (c) 2015-2017 [Editorial Technology](http://editorial.technology/). 563 | 564 | See the `LICENSE` file [included with the project distribution](https://github.com/maetl/calyx/blob/master/LICENSE) for more information. 565 | -------------------------------------------------------------------------------- /SYNTAX.md: -------------------------------------------------------------------------------- 1 | # Calyx Syntax Specification 2 | 3 | > An ad-hoc, informally specified, bug-ridden, etc... etc... 4 | 5 | ## Background 6 | 7 | Since `v0.11`, Calyx has supported loading grammars from external JSON files—a very similar format to Tracery[1][1]—but the precise syntax and structure used by these files was never properly documented or defined in a schema[2][2]. 8 | 9 | This is worth documenting for several reasons: 10 | 11 | 1) It’s rather obvious that having good documentation will make it easier for new users to get started and for advanced users to learn about the limits of what they can do with the tool. 12 | 2) A well-defined schema reduces ambiguity and helps focus on authoring concerns, rather than drifting towards implementation concerns. This is currently a particular risk in Calyx because of the impedance mismatch between the Ruby DSL and JSON data. 13 | 3) A well-defined schema opens up potential for collaboration with authors of other similar tools and could help provide a future foundation for a standard data format that enables sharing grammars across languages and tools. This would be of particular benefit to authors, making it easier to build up reusable content libraries. It could also provide a foundation for new innovations in authoring UIs that aren’t tied to a specific language or tool. 14 | 15 | ## Format 16 | 17 | ### Files 18 | 19 | External grammars are defined in JSON files. They must be encoded as `utf-8`, have a `.json` extension and conform to standard JSON syntax rules. 20 | 21 | ### Structure 22 | 23 | #### Top Level 24 | 25 | The top-level structure of the grammar must be a map/object-literal with each key representing a single left-hand rule symbol and the value representing the grammar productions for that rule: 26 | 27 | ```json 28 | { 29 | "start": "Colorless green ideas sleep furiously." 30 | } 31 | ``` 32 | 33 | Empty grammars should be represented by an empty object: 34 | 35 | ```json 36 | {} 37 | ``` 38 | 39 | #### Production Rules 40 | 41 | Left hand side rules must be string symbols conforming to the following pattern: 42 | 43 | ```ruby 44 | /^[A-Za-z0-9_\-]+$/ 45 | ``` 46 | 47 | Grammars are not context-sensitive[3][3]. The left-hand side rules must be a direct symbol reference, not a production that can be expanded. 48 | 49 | Right-hand side productions can be either single strings, arrays of strings or weighted probability objects. 50 | 51 | Strings represent the template for a single choice that the production will always resolve to: 52 | 53 | ```json 54 | { 55 | "start": "Colorless green ideas sleep furiously." 56 | } 57 | ``` 58 | 59 | Arrays of strings represent multiple choices that can produce any one of the possible output strings. Each string should have a (roughly) equal chance of being selected to expand to a result. 60 | 61 | ```json 62 | { 63 | "start": ["red", "green", "blue"] 64 | } 65 | ``` 66 | 67 | Weighted probability objects represent a mapping of possible output strings to their probability of expanding to a result. The keys represent the possible output strings, and the values represent their probability of the string being selected. 68 | 69 | Supported intervals are: 70 | 71 | - 0..1 (`Number`) 72 | 73 | The following example shows `red` with a 50% chance of being selected; `green` and `blue` with 25% chances: 74 | 75 | ```json 76 | { 77 | "start": { 78 | "red": 0.5, 79 | "green": 0.25, 80 | "blue": 0.25 81 | } 82 | } 83 | ``` 84 | 85 | #### Template Expansions 86 | 87 | Productions can be recursively expanded by embedding rules using the template expression syntax, with the expressions delimited by `{` and `}` characters. Everything outside of the delimiters is treated as literal text. 88 | 89 | Basic syntax: 90 | 91 | ```json 92 | "{weather}" 93 | ``` 94 | 95 | Expanding a simple rule: 96 | 97 | ```json 98 | { 99 | "start": "The sky was {weather}.", 100 | "weather": ["cloudy", "dark", "clear", "bright"] 101 | } 102 | ``` 103 | 104 | A chain of nested expansions: 105 | 106 | ```json 107 | { 108 | "start": "{best} {worst}", 109 | "best": "{twas} the {best_adj} of times.", 110 | "worst": "{twas} the {worst_adj} of times.", 111 | "twas": ["It was", "'Twas"], 112 | "best_adj": ["best", "greatest"], 113 | "worst_adj": ["worst", "most insufferable"] 114 | } 115 | ``` 116 | 117 | #### Expression Modifiers 118 | 119 | There are two different forms of expression modifiers—**Selection Modifiers** and **Output Modifiers**. 120 | 121 | Selection modifiers apply to the grammar production itself, influencing how the rule is expanded. They are defined by prefixing a rule expression with a sigil that defines the behaviour of the selection. 122 | 123 | ```json 124 | "{$unique_rule}" 125 | "{@memoized_rule}" 126 | ``` 127 | 128 | Output modifiers format the string that is generated by the grammar production. They are defined by a chain of `.` separated references following the rule. 129 | 130 | ```json 131 | "{formatted_rule.upcase}" 132 | "{formatted_rule.downcase.capitalize}" 133 | ``` 134 | 135 | #### Unique Choices 136 | 137 | Unique choices are prefixed with the `$` sigil in an expression. 138 | 139 | This ensures that multiple references to the same production will always result in a unique value being chosen (until the choices in the production are exhausted). 140 | 141 | ```json 142 | { 143 | "start": "{$medal}. {$medal}. {$medal}.", 144 | "medal": ["Gold", "Silver", "Bronze"] 145 | } 146 | ``` 147 | 148 | ```json 149 | { 150 | "start": "It was the {$adj} of times; it was the {$adj} of times.", 151 | "adj": ["best", "worst"] 152 | } 153 | ``` 154 | 155 | #### Memoized Choices 156 | 157 | Memoized choices are prefixed with the `@` sigil in an expression. 158 | 159 | This ensures that multiple references to the same production will always result in the first selected value being repeated. 160 | 161 | ```json 162 | { 163 | "start": "The {@pet} ran to join the other {@pet}s.", 164 | "pet": ["cat", "dog"] 165 | } 166 | ``` 167 | 168 | #### Output Modifiers 169 | 170 | Due to their dependency on Ruby string methods and Calyx internals, output modifiers are currently a bit of a nightmare for interoperability. 171 | 172 | All basic Ruby string formatting methods with arity 0 are supported by default[4][4]. 173 | 174 | ```json 175 | "{my_rule.downcase}" 176 | "{my_rule.upcase}" 177 | "{my_rule.capitalize}" 178 | "{my_rule.reverse}" 179 | "{my_rule.swapcase}" 180 | "{my_rule.strip}" 181 | "{my_rule.lstrip}" 182 | "{my_rule.rstrip}" 183 | "{my_rule.succ}" 184 | "{my_rule.chop}" 185 | "{my_rule.chomp}" 186 | ``` 187 | 188 | The Ruby DSL provides a variety of methods for extending the supported range of modifiers. This behaviour currently won’t work at all when grammars are defined in JSON. 189 | 190 | ## References 191 | 192 | [1]: http://tracery.io/ 193 | [2]: http://json-schema.org/ 194 | [3]: https://en.wikipedia.org/wiki/Context-sensitive_grammar 195 | [4]: https://ruby-doc.org/core-2.4.0/String.html 196 | 197 | 1) http://tracery.io/ 198 | 2) http://json-schema.org/ 199 | 3) https://en.wikipedia.org/wiki/Context-sensitive_grammar 200 | 4) https://ruby-doc.org/core-2.4.0/String.html 201 | -------------------------------------------------------------------------------- /calyx.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'calyx/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'calyx' 8 | spec.version = Calyx::VERSION 9 | spec.authors = ['Mark Rickerby'] 10 | spec.email = ['me@maetl.net'] 11 | 12 | spec.summary = %q{Generate text with declarative recursive grammars} 13 | spec.description = %q{Calyx provides a simple API for generating text with declarative recursive grammars.} 14 | spec.homepage = 'https://github.com/maetl/calyx' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) } 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 2.0' 22 | spec.add_development_dependency 'rake', '~> 13.0' 23 | spec.add_development_dependency 'rspec', '~> 3.4' 24 | end 25 | -------------------------------------------------------------------------------- /examples/any_gradient.rb: -------------------------------------------------------------------------------- 1 | require 'roda' 2 | require 'calyx' 3 | 4 | class AnyGradient < Calyx::Grammar 5 | html '{svg}' 6 | xml '{svg}' 7 | svg ' 8 | 13 | {defs} 14 | {rect} 15 | 16 | ' 17 | defs ' 18 | 19 | {linear_gradient} 20 | 21 | ' 22 | linear_gradient ' 23 | 24 | {stop_fixed} 25 | {stop_offset} 26 | 27 | ' 28 | stop_fixed '' 29 | stop_offset '' 30 | stop_color '#{hex_num}{hex_num}{hex_num}' 31 | hex_num '{hex_val}{hex_val}' 32 | hex_val '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' 33 | offset '{tens}{ones}%' 34 | tens 3,4,5,6 35 | ones 1,2,3,4,5,6,7,8,9,0 36 | rect ' 37 | 45 | ' 46 | end 47 | 48 | class Server < Roda 49 | route do |r| 50 | r.root do 51 | AnyGradient.new.generate(:html) 52 | end 53 | end 54 | end 55 | 56 | if STDOUT.tty? 57 | Rack::Server.start :app => Server 58 | else 59 | STDOUT.puts AnyGradient.new.generate(:xml) 60 | end 61 | -------------------------------------------------------------------------------- /examples/faker.json: -------------------------------------------------------------------------------- 1 | {"email":"{username}@{domain}","full_name":["{first_name} {last_name}","{first_name} {first_name.chars.first} {last_name}"],"username":["{first_name.downcase}","{first_name.downcase}{number}"],"first_name":["Aaliyah","Aaron","Abagail","Abbey","Abbie","Abbigail","Abby","Abdiel","Abdul","Abdullah","Abe","Abel","Abelardo","Abigail","Abigale","Abigayle","Abner","Abraham","Ada","Adah","Adalberto","Adaline","Adam","Adan","Addie","Addison","Adela","Adelbert","Adele","Adelia","Adeline","Adell","Adella","Adelle","Aditya","Adolf","Adolfo","Adolph","Adolphus","Adonis","Adrain","Adrian","Adriana","Adrianna","Adriel","Adrien","Adrienne","Afton","Aglae","Agnes","Agustin","Agustina","Ahmad","Ahmed","Aida","Aidan","Aiden","Aileen","Aimee","Aisha","Aiyana","Akeem","Al","Alaina","Alan","Alana","Alanis","Alanna","Alayna","Alba","Albert","Alberta","Albertha","Alberto","Albin","Albina","Alda","Alden","Alec","Aleen","Alejandra","Alejandrin","Alek","Alena","Alene","Alessandra","Alessandro","Alessia","Aletha","Alex","Alexa","Alexander","Alexandra","Alexandre","Alexandrea","Alexandria","Alexandrine","Alexandro","Alexane","Alexanne","Alexie","Alexis","Alexys","Alexzander","Alf","Alfonso","Alfonzo","Alford","Alfred","Alfreda","Alfredo","Ali","Alia","Alice","Alicia","Alisa","Alisha","Alison","Alivia","Aliya","Aliyah","Aliza","Alize","Allan","Allen","Allene","Allie","Allison","Ally","Alphonso","Alta","Althea","Alva","Alvah","Alvena","Alvera","Alverta","Alvina","Alvis","Alyce","Alycia","Alysa","Alysha","Alyson","Alysson","Amalia","Amanda","Amani","Amara","Amari","Amaya","Amber","Ambrose","Amelia","Amelie","Amely","America","Americo","Amie","Amina","Amir","Amira","Amiya","Amos","Amparo","Amy","Amya","Ana","Anabel","Anabelle","Anahi","Anais","Anastacio","Anastasia","Anderson","Andre","Andreane","Andreanne","Andres","Andrew","Andy","Angel","Angela","Angelica","Angelina","Angeline","Angelita","Angelo","Angie","Angus","Anibal","Anika","Anissa","Anita","Aniya","Aniyah","Anjali","Anna","Annabel","Annabell","Annabelle","Annalise","Annamae","Annamarie","Anne","Annetta","Annette","Annie","Ansel","Ansley","Anthony","Antoinette","Antone","Antonetta","Antonette","Antonia","Antonietta","Antonina","Antonio","Antwan","Antwon","Anya","April","Ara","Araceli","Aracely","Arch","Archibald","Ardella","Arden","Ardith","Arely","Ari","Ariane","Arianna","Aric","Ariel","Arielle","Arjun","Arlene","Arlie","Arlo","Armand","Armando","Armani","Arnaldo","Arne","Arno","Arnold","Arnoldo","Arnulfo","Aron","Art","Arthur","Arturo","Arvel","Arvid","Arvilla","Aryanna","Asa","Asha","Ashlee","Ashleigh","Ashley","Ashly","Ashlynn","Ashton","Ashtyn","Asia","Assunta","Astrid","Athena","Aubree","Aubrey","Audie","Audra","Audreanne","Audrey","August","Augusta","Augustine","Augustus","Aurelia","Aurelie","Aurelio","Aurore","Austen","Austin","Austyn","Autumn","Ava","Avery","Avis","Axel","Ayana","Ayden","Ayla","Aylin","Baby","Bailee","Bailey","Barbara","Barney","Baron","Barrett","Barry","Bart","Bartholome","Barton","Baylee","Beatrice","Beau","Beaulah","Bell","Bella","Belle","Ben","Benedict","Benjamin","Bennett","Bennie","Benny","Benton","Berenice","Bernadette","Bernadine","Bernard","Bernardo","Berneice","Bernhard","Bernice","Bernie","Berniece","Bernita","Berry","Bert","Berta","Bertha","Bertram","Bertrand","Beryl","Bessie","Beth","Bethany","Bethel","Betsy","Bette","Bettie","Betty","Bettye","Beulah","Beverly","Bianka","Bill","Billie","Billy","Birdie","Blair","Blaise","Blake","Blanca","Blanche","Blaze","Bo","Bobbie","Bobby","Bonita","Bonnie","Boris","Boyd","Brad","Braden","Bradford","Bradley","Bradly","Brady","Braeden","Brain","Brandi","Brando","Brandon","Brandt","Brandy","Brandyn","Brannon","Branson","Brant","Braulio","Braxton","Brayan","Breana","Breanna","Breanne","Brenda","Brendan","Brenden","Brendon","Brenna","Brennan","Brennon","Brent","Bret","Brett","Bria","Brian","Briana","Brianne","Brice","Bridget","Bridgette","Bridie","Brielle","Brigitte","Brionna","Brisa","Britney","Brittany","Brock","Broderick","Brody","Brook","Brooke","Brooklyn","Brooks","Brown","Bruce","Bryana","Bryce","Brycen","Bryon","Buck","Bud","Buddy","Buford","Bulah","Burdette","Burley","Burnice","Buster","Cade","Caden","Caesar","Caitlyn","Cale","Caleb","Caleigh","Cali","Calista","Callie","Camden","Cameron","Camila","Camilla","Camille","Camren","Camron","Camryn","Camylle","Candace","Candelario","Candice","Candida","Candido","Cara","Carey","Carissa","Carlee","Carleton","Carley","Carli","Carlie","Carlo","Carlos","Carlotta","Carmel","Carmela","Carmella","Carmelo","Carmen","Carmine","Carol","Carolanne","Carole","Carolina","Caroline","Carolyn","Carolyne","Carrie","Carroll","Carson","Carter","Cary","Casandra","Casey","Casimer","Casimir","Casper","Cassandra","Cassandre","Cassidy","Cassie","Catalina","Caterina","Catharine","Catherine","Cathrine","Cathryn","Cathy","Cayla","Ceasar","Cecelia","Cecil","Cecile","Cecilia","Cedrick","Celestine","Celestino","Celia","Celine","Cesar","Chad","Chadd","Chadrick","Chaim","Chance","Chandler","Chanel","Chanelle","Charity","Charlene","Charles","Charley","Charlie","Charlotte","Chase","Chasity","Chauncey","Chaya","Chaz","Chelsea","Chelsey","Chelsie","Chesley","Chester","Chet","Cheyanne","Cheyenne","Chloe","Chris","Christ","Christa","Christelle","Christian","Christiana","Christina","Christine","Christop","Christophe","Christopher","Christy","Chyna","Ciara","Cicero","Cielo","Cierra","Cindy","Citlalli","Clair","Claire","Clara","Clarabelle","Clare","Clarissa","Clark","Claud","Claude","Claudia","Claudie","Claudine","Clay","Clemens","Clement","Clementina","Clementine","Clemmie","Cleo","Cleora","Cleta","Cletus","Cleve","Cleveland","Clifford","Clifton","Clint","Clinton","Clotilde","Clovis","Cloyd","Clyde","Coby","Cody","Colby","Cole","Coleman","Colin","Colleen","Collin","Colt","Colten","Colton","Columbus","Concepcion","Conner","Connie","Connor","Conor","Conrad","Constance","Constantin","Consuelo","Cooper","Cora","Coralie","Corbin","Cordelia","Cordell","Cordia","Cordie","Corene","Corine","Cornelius","Cornell","Corrine","Cortez","Cortney","Cory","Coty","Courtney","Coy","Craig","Crawford","Creola","Cristal","Cristian","Cristina","Cristobal","Cristopher","Cruz","Crystal","Crystel","Cullen","Curt","Curtis","Cydney","Cynthia","Cyril","Cyrus","Dagmar","Dahlia","Daija","Daisha","Daisy","Dakota","Dale","Dallas","Dallin","Dalton","Damaris","Dameon","Damian","Damien","Damion","Damon","Dan","Dana","Dandre","Dane","D'angelo","Dangelo","Danial","Daniela","Daniella","Danielle","Danika","Dannie","Danny","Dante","Danyka","Daphne","Daphnee","Daphney","Darby","Daren","Darian","Dariana","Darien","Dario","Darion","Darius","Darlene","Daron","Darrel","Darrell","Darren","Darrick","Darrin","Darrion","Darron","Darryl","Darwin","Daryl","Dashawn","Dasia","Dave","David","Davin","Davion","Davon","Davonte","Dawn","Dawson","Dax","Dayana","Dayna","Dayne","Dayton","Dean","Deangelo","Deanna","Deborah","Declan","Dedric","Dedrick","Dee","Deion","Deja","Dejah","Dejon","Dejuan","Delaney","Delbert","Delfina","Delia","Delilah","Dell","Della","Delmer","Delores","Delpha","Delphia","Delphine","Delta","Demarco","Demarcus","Demario","Demetris","Demetrius","Demond","Dena","Denis","Dennis","Deon","Deondre","Deontae","Deonte","Dereck","Derek","Derick","Deron","Derrick","Deshaun","Deshawn","Desiree","Desmond","Dessie","Destany","Destin","Destinee","Destiney","Destini","Destiny","Devan","Devante","Deven","Devin","Devon","Devonte","Devyn","Dewayne","Dewitt","Dexter","Diamond","Diana","Dianna","Diego","Dillan","Dillon","Dimitri","Dina","Dino","Dion","Dixie","Dock","Dolly","Dolores","Domenic","Domenica","Domenick","Domenico","Domingo","Dominic","Dominique","Don","Donald","Donato","Donavon","Donna","Donnell","Donnie","Donny","Dora","Dorcas","Dorian","Doris","Dorothea","Dorothy","Dorris","Dortha","Dorthy","Doug","Douglas","Dovie","Doyle","Drake","Drew","Duane","Dudley","Dulce","Duncan","Durward","Dustin","Dusty","Dwight","Dylan","Earl","Earlene","Earline","Earnest","Earnestine","Easter","Easton","Ebba","Ebony","Ed","Eda","Edd","Eddie","Eden","Edgar","Edgardo","Edison","Edmond","Edmund","Edna","Eduardo","Edward","Edwardo","Edwin","Edwina","Edyth","Edythe","Effie","Efrain","Efren","Eileen","Einar","Eino","Eladio","Elaina","Elbert","Elda","Eldon","Eldora","Eldred","Eldridge","Eleanora","Eleanore","Eleazar","Electa","Elena","Elenor","Elenora","Eleonore","Elfrieda","Eli","Elian","Eliane","Elias","Eliezer","Elijah","Elinor","Elinore","Elisa","Elisabeth","Elise","Eliseo","Elisha","Elissa","Eliza","Elizabeth","Ella","Ellen","Ellie","Elliot","Elliott","Ellis","Ellsworth","Elmer","Elmira","Elmo","Elmore","Elna","Elnora","Elody","Eloisa","Eloise","Elouise","Eloy","Elroy","Elsa","Else","Elsie","Elta","Elton","Elva","Elvera","Elvie","Elvis","Elwin","Elwyn","Elyse","Elyssa","Elza","Emanuel","Emelia","Emelie","Emely","Emerald","Emerson","Emery","Emie","Emil","Emile","Emilia","Emiliano","Emilie","Emilio","Emily","Emma","Emmalee","Emmanuel","Emmanuelle","Emmet","Emmett","Emmie","Emmitt","Emmy","Emory","Ena","Enid","Enoch","Enola","Enos","Enrico","Enrique","Ephraim","Era","Eriberto","Eric","Erica","Erich","Erick","Ericka","Erik","Erika","Erin","Erling","Erna","Ernest","Ernestina","Ernestine","Ernesto","Ernie","Ervin","Erwin","Eryn","Esmeralda","Esperanza","Esta","Esteban","Estefania","Estel","Estell","Estella","Estelle","Estevan","Esther","Estrella","Etha","Ethan","Ethel","Ethelyn","Ethyl","Ettie","Eudora","Eugene","Eugenia","Eula","Eulah","Eulalia","Euna","Eunice","Eusebio","Eva","Evalyn","Evan","Evangeline","Evans","Eve","Eveline","Evelyn","Everardo","Everett","Everette","Evert","Evie","Ewald","Ewell","Ezekiel","Ezequiel","Ezra","Fabian","Fabiola","Fae","Fannie","Fanny","Fatima","Faustino","Fausto","Favian","Fay","Faye","Federico","Felicia","Felicita","Felicity","Felipa","Felipe","Felix","Felton","Fermin","Fern","Fernando","Ferne","Fidel","Filiberto","Filomena","Finn","Fiona","Flavie","Flavio","Fleta","Fletcher","Flo","Florence","Florencio","Florian","Florida","Florine","Flossie","Floy","Floyd","Ford","Forest","Forrest","Foster","Frances","Francesca","Francesco","Francis","Francisca","Francisco","Franco","Frank","Frankie","Franz","Fred","Freda","Freddie","Freddy","Frederic","Frederick","Frederik","Frederique","Fredrick","Fredy","Freeda","Freeman","Freida","Frida","Frieda","Friedrich","Fritz","Furman","Gabe","Gabriel","Gabriella","Gabrielle","Gaetano","Gage","Gail","Gardner","Garett","Garfield","Garland","Garnet","Garnett","Garret","Garrett","Garrick","Garrison","Garry","Garth","Gaston","Gavin","Gay","Gayle","Gaylord","Gene","General","Genesis","Genevieve","Gennaro","Genoveva","Geo","Geoffrey","George","Georgette","Georgiana","Georgianna","Geovanni","Geovanny","Geovany","Gerald","Geraldine","Gerard","Gerardo","Gerda","Gerhard","Germaine","German","Gerry","Gerson","Gertrude","Gia","Gianni","Gideon","Gilbert","Gilberto","Gilda","Giles","Gillian","Gina","Gino","Giovani","Giovanna","Giovanni","Giovanny","Gisselle","Giuseppe","Gladyce","Gladys","Glen","Glenda","Glenna","Glennie","Gloria","Godfrey","Golda","Golden","Gonzalo","Gordon","Grace","Gracie","Graciela","Grady","Graham","Grant","Granville","Grayce","Grayson","Green","Greg","Gregg","Gregoria","Gregorio","Gregory","Greta","Gretchen","Greyson","Griffin","Grover","Guadalupe","Gudrun","Guido","Guillermo","Guiseppe","Gunnar","Gunner","Gus","Gussie","Gust","Gustave","Guy","Gwen","Gwendolyn","Hadley","Hailee","Hailey","Hailie","Hal","Haleigh","Haley","Halie","Halle","Hallie","Hank","Hanna","Hannah","Hans","Hardy","Harley","Harmon","Harmony","Harold","Harrison","Harry","Harvey","Haskell","Hassan","Hassie","Hattie","Haven","Hayden","Haylee","Hayley","Haylie","Hazel","Hazle","Heath","Heather","Heaven","Heber","Hector","Heidi","Helen","Helena","Helene","Helga","Hellen","Helmer","Heloise","Henderson","Henri","Henriette","Henry","Herbert","Herman","Hermann","Hermina","Herminia","Herminio","Hershel","Herta","Hertha","Hester","Hettie","Hilario","Hilbert","Hilda","Hildegard","Hillard","Hillary","Hilma","Hilton","Hipolito","Hiram","Hobart","Holden","Hollie","Hollis","Holly","Hope","Horace","Horacio","Hortense","Hosea","Houston","Howard","Howell","Hoyt","Hubert","Hudson","Hugh","Hulda","Humberto","Hunter","Hyman","Ian","Ibrahim","Icie","Ida","Idell","Idella","Ignacio","Ignatius","Ike","Ila","Ilene","Iliana","Ima","Imani","Imelda","Immanuel","Imogene","Ines","Irma","Irving","Irwin","Isaac","Isabel","Isabell","Isabella","Isabelle","Isac","Isadore","Isai","Isaiah","Isaias","Isidro","Ismael","Isobel","Isom","Israel","Issac","Itzel","Iva","Ivah","Ivory","Ivy","Izabella","Izaiah","Jabari","Jace","Jacey","Jacinthe","Jacinto","Jack","Jackeline","Jackie","Jacklyn","Jackson","Jacky","Jaclyn","Jacquelyn","Jacques","Jacynthe","Jada","Jade","Jaden","Jadon","Jadyn","Jaeden","Jaida","Jaiden","Jailyn","Jaime","Jairo","Jakayla","Jake","Jakob","Jaleel","Jalen","Jalon","Jalyn","Jamaal","Jamal","Jamar","Jamarcus","Jamel","Jameson","Jamey","Jamie","Jamil","Jamir","Jamison","Jammie","Jan","Jana","Janae","Jane","Janelle","Janessa","Janet","Janice","Janick","Janie","Janis","Janiya","Jannie","Jany","Jaquan","Jaquelin","Jaqueline","Jared","Jaren","Jarod","Jaron","Jarred","Jarrell","Jarret","Jarrett","Jarrod","Jarvis","Jasen","Jasmin","Jason","Jasper","Jaunita","Javier","Javon","Javonte","Jay","Jayce","Jaycee","Jayda","Jayde","Jayden","Jaydon","Jaylan","Jaylen","Jaylin","Jaylon","Jayme","Jayne","Jayson","Jazlyn","Jazmin","Jazmyn","Jazmyne","Jean","Jeanette","Jeanie","Jeanne","Jed","Jedediah","Jedidiah","Jeff","Jefferey","Jeffery","Jeffrey","Jeffry","Jena","Jenifer","Jennie","Jennifer","Jennings","Jennyfer","Jensen","Jerad","Jerald","Jeramie","Jeramy","Jerel","Jeremie","Jeremy","Jermain","Jermaine","Jermey","Jerod","Jerome","Jeromy","Jerrell","Jerrod","Jerrold","Jerry","Jess","Jesse","Jessica","Jessie","Jessika","Jessy","Jessyca","Jesus","Jett","Jettie","Jevon","Jewel","Jewell","Jillian","Jimmie","Jimmy","Jo","Joan","Joana","Joanie","Joanne","Joannie","Joanny","Joany","Joaquin","Jocelyn","Jodie","Jody","Joe","Joel","Joelle","Joesph","Joey","Johan","Johann","Johanna","Johathan","John","Johnathan","Johnathon","Johnnie","Johnny","Johnpaul","Johnson","Jolie","Jon","Jonas","Jonatan","Jonathan","Jonathon","Jordan","Jordane","Jordi","Jordon","Jordy","Jordyn","Jorge","Jose","Josefa","Josefina","Joseph","Josephine","Josh","Joshua","Joshuah","Josiah","Josiane","Josianne","Josie","Josue","Jovan","Jovani","Jovanny","Jovany","Joy","Joyce","Juana","Juanita","Judah","Judd","Jude","Judge","Judson","Judy","Jules","Julia","Julian","Juliana","Julianne","Julie","Julien","Juliet","Julio","Julius","June","Junior","Junius","Justen","Justice","Justina","Justine","Juston","Justus","Justyn","Juvenal","Juwan","Kacey","Kaci","Kacie","Kade","Kaden","Kadin","Kaela","Kaelyn","Kaia","Kailee","Kailey","Kailyn","Kaitlin","Kaitlyn","Kale","Kaleb","Kaleigh","Kaley","Kali","Kallie","Kameron","Kamille","Kamren","Kamron","Kamryn","Kane","Kara","Kareem","Karelle","Karen","Kari","Kariane","Karianne","Karina","Karine","Karl","Karlee","Karley","Karli","Karlie","Karolann","Karson","Kasandra","Kasey","Kassandra","Katarina","Katelin","Katelyn","Katelynn","Katharina","Katherine","Katheryn","Kathleen","Kathlyn","Kathryn","Kathryne","Katlyn","Katlynn","Katrina","Katrine","Kattie","Kavon","Kay","Kaya","Kaycee","Kayden","Kayla","Kaylah","Kaylee","Kayleigh","Kayley","Kayli","Kaylie","Kaylin","Keagan","Keanu","Keara","Keaton","Keegan","Keeley","Keely","Keenan","Keira","Keith","Kellen","Kelley","Kelli","Kellie","Kelly","Kelsi","Kelsie","Kelton","Kelvin","Ken","Kendall","Kendra","Kendrick","Kenna","Kennedi","Kennedy","Kenneth","Kennith","Kenny","Kenton","Kenya","Kenyatta","Kenyon","Keon","Keshaun","Keshawn","Keven","Kevin","Kevon","Keyon","Keyshawn","Khalid","Khalil","Kian","Kiana","Kianna","Kiara","Kiarra","Kiel","Kiera","Kieran","Kiley","Kim","Kimberly","King","Kip","Kira","Kirk","Kirsten","Kirstin","Kitty","Kobe","Koby","Kody","Kolby","Kole","Korbin","Korey","Kory","Kraig","Kris","Krista","Kristian","Kristin","Kristina","Kristofer","Kristoffer","Kristopher","Kristy","Krystal","Krystel","Krystina","Kurt","Kurtis","Kyla","Kyle","Kylee","Kyleigh","Kyler","Kylie","Kyra","Lacey","Lacy","Ladarius","Lafayette","Laila","Laisha","Lamar","Lambert","Lamont","Lance","Landen","Lane","Laney","Larissa","Laron","Larry","Larue","Laura","Laurel","Lauren","Laurence","Lauretta","Lauriane","Laurianne","Laurie","Laurine","Laury","Lauryn","Lavada","Lavern","Laverna","Laverne","Lavina","Lavinia","Lavon","Lavonne","Lawrence","Lawson","Layla","Layne","Lazaro","Lea","Leann","Leanna","Leanne","Leatha","Leda","Lee","Leif","Leila","Leilani","Lela","Lelah","Leland","Lelia","Lempi","Lemuel","Lenna","Lennie","Lenny","Lenora","Lenore","Leo","Leola","Leon","Leonard","Leonardo","Leone","Leonel","Leonie","Leonor","Leonora","Leopold","Leopoldo","Leora","Lera","Lesley","Leslie","Lesly","Lessie","Lester","Leta","Letha","Letitia","Levi","Lew","Lewis","Lexi","Lexie","Lexus","Lia","Liam","Liana","Libbie","Libby","Lila","Lilian","Liliana","Liliane","Lilla","Lillian","Lilliana","Lillie","Lilly","Lily","Lilyan","Lina","Lincoln","Linda","Lindsay","Lindsey","Linnea","Linnie","Linwood","Lionel","Lisa","Lisandro","Lisette","Litzy","Liza","Lizeth","Lizzie","Llewellyn","Lloyd","Logan","Lois","Lola","Lolita","Loma","Lon","London","Lonie","Lonnie","Lonny","Lonzo","Lora","Loraine","Loren","Lorena","Lorenz","Lorenza","Lorenzo","Lori","Lorine","Lorna","Lottie","Lou","Louie","Louisa","Lourdes","Louvenia","Lowell","Loy","Loyal","Loyce","Lucas","Luciano","Lucie","Lucienne","Lucile","Lucinda","Lucio","Lucious","Lucius","Lucy","Ludie","Ludwig","Lue","Luella","Luigi","Luis","Luisa","Lukas","Lula","Lulu","Luna","Lupe","Lura","Lurline","Luther","Luz","Lyda","Lydia","Lyla","Lynn","Lyric","Lysanne","Mabel","Mabelle","Mable","Mac","Macey","Maci","Macie","Mack","Mackenzie","Macy","Madaline","Madalyn","Maddison","Madeline","Madelyn","Madelynn","Madge","Madie","Madilyn","Madisen","Madison","Madisyn","Madonna","Madyson","Mae","Maegan","Maeve","Mafalda","Magali","Magdalen","Magdalena","Maggie","Magnolia","Magnus","Maia","Maida","Maiya","Major","Makayla","Makenna","Makenzie","Malachi","Malcolm","Malika","Malinda","Mallie","Mallory","Malvina","Mandy","Manley","Manuel","Manuela","Mara","Marc","Marcel","Marcelina","Marcelino","Marcella","Marcelle","Marcellus","Marcelo","Marcia","Marco","Marcos","Marcus","Margaret","Margarete","Margarett","Margaretta","Margarette","Margarita","Marge","Margie","Margot","Margret","Marguerite","Maria","Mariah","Mariam","Marian","Mariana","Mariane","Marianna","Marianne","Mariano","Maribel","Marie","Mariela","Marielle","Marietta","Marilie","Marilou","Marilyne","Marina","Mario","Marion","Marisa","Marisol","Maritza","Marjolaine","Marjorie","Marjory","Mark","Markus","Marlee","Marlen","Marlene","Marley","Marlin","Marlon","Marques","Marquis","Marquise","Marshall","Marta","Martin","Martina","Martine","Marty","Marvin","Mary","Maryam","Maryjane","Maryse","Mason","Mateo","Mathew","Mathias","Mathilde","Matilda","Matilde","Matt","Matteo","Mattie","Maud","Maude","Maudie","Maureen","Maurice","Mauricio","Maurine","Maverick","Mavis","Max","Maxie","Maxime","Maximilian","Maximillia","Maximillian","Maximo","Maximus","Maxine","Maxwell","May","Maya","Maybell","Maybelle","Maye","Maymie","Maynard","Mayra","Mazie","Mckayla","Mckenna","Mckenzie","Meagan","Meaghan","Meda","Megane","Meggie","Meghan","Mekhi","Melany","Melba","Melisa","Melissa","Mellie","Melody","Melvin","Melvina","Melyna","Melyssa","Mercedes","Meredith","Merl","Merle","Merlin","Merritt","Mertie","Mervin","Meta","Mia","Micaela","Micah","Michael","Michaela","Michale","Micheal","Michel","Michele","Michelle","Miguel","Mikayla","Mike","Mikel","Milan","Miles","Milford","Miller","Millie","Milo","Milton","Mina","Minerva","Minnie","Miracle","Mireille","Mireya","Misael","Missouri","Misty","Mitchel","Mitchell","Mittie","Modesta","Modesto","Mohamed","Mohammad","Mohammed","Moises","Mollie","Molly","Mona","Monica","Monique","Monroe","Monserrat","Monserrate","Montana","Monte","Monty","Morgan","Moriah","Morris","Mortimer","Morton","Mose","Moses","Moshe","Mossie","Mozell","Mozelle","Muhammad","Muriel","Murl","Murphy","Murray","Mustafa","Mya","Myah","Mylene","Myles","Myra","Myriam","Myrl","Myrna","Myron","Myrtice","Myrtie","Myrtis","Myrtle","Nadia","Nakia","Name","Nannie","Naomi","Naomie","Napoleon","Narciso","Nash","Nasir","Nat","Natalia","Natalie","Natasha","Nathan","Nathanael","Nathanial","Nathaniel","Nathen","Nayeli","Neal","Ned","Nedra","Neha","Neil","Nelda","Nella","Nelle","Nellie","Nels","Nelson","Neoma","Nestor","Nettie","Neva","Newell","Newton","Nia","Nicholas","Nicholaus","Nichole","Nick","Nicklaus","Nickolas","Nico","Nicola","Nicolas","Nicole","Nicolette","Nigel","Nikita","Nikki","Nikko","Niko","Nikolas","Nils","Nina","Noah","Noble","Noe","Noel","Noelia","Noemi","Noemie","Noemy","Nola","Nolan","Nona","Nora","Norbert","Norberto","Norene","Norma","Norris","Norval","Norwood","Nova","Novella","Nya","Nyah","Nyasia","Obie","Oceane","Ocie","Octavia","Oda","Odell","Odessa","Odie","Ofelia","Okey","Ola","Olaf","Ole","Olen","Oleta","Olga","Olin","Oliver","Ollie","Oma","Omari","Omer","Ona","Onie","Opal","Ophelia","Ora","Oral","Oran","Oren","Orie","Orin","Orion","Orland","Orlando","Orlo","Orpha","Orrin","Orval","Orville","Osbaldo","Osborne","Oscar","Osvaldo","Oswald","Oswaldo","Otha","Otho","Otilia","Otis","Ottilie","Ottis","Otto","Ova","Owen","Ozella","Ozzie","Pablo","Paige","Palma","Pamela","Pansy","Paolo","Paris","Parker","Pascale","Pasquale","Pat","Patience","Patricia","Patrick","Patsy","Pattie","Paul","Paula","Pauline","Paxton","Payton","Pearl","Pearlie","Pearline","Pedro","Peggie","Penelope","Percival","Percy","Perry","Pete","Peter","Petra","Peyton","Philip","Phoebe","Phyllis","Pierce","Pierre","Pietro","Pink","Pinkie","Piper","Polly","Porter","Precious","Presley","Preston","Price","Prince","Princess","Priscilla","Providenci","Prudence","Queen","Queenie","Quentin","Quincy","Quinn","Quinten","Quinton","Rachael","Rachel","Rachelle","Rae","Raegan","Rafael","Rafaela","Raheem","Rahsaan","Rahul","Raina","Raleigh","Ralph","Ramiro","Ramon","Ramona","Randal","Randall","Randi","Randy","Ransom","Raoul","Raphael","Raphaelle","Raquel","Rashad","Rashawn","Rasheed","Raul","Raven","Ray","Raymond","Raymundo","Reagan","Reanna","Reba","Rebeca","Rebecca","Rebeka","Rebekah","Reece","Reed","Reese","Regan","Reggie","Reginald","Reid","Reilly","Reina","Reinhold","Remington","Rene","Renee","Ressie","Reta","Retha","Retta","Reuben","Reva","Rex","Rey","Reyes","Reymundo","Reyna","Reynold","Rhea","Rhett","Rhianna","Rhiannon","Rhoda","Ricardo","Richard","Richie","Richmond","Rick","Rickey","Rickie","Ricky","Rico","Rigoberto","Riley","Rita","River","Robb","Robbie","Robert","Roberta","Roberto","Robin","Robyn","Rocio","Rocky","Rod","Roderick","Rodger","Rodolfo","Rodrick","Rodrigo","Roel","Rogelio","Roger","Rogers","Rolando","Rollin","Roma","Romaine","Roman","Ron","Ronaldo","Ronny","Roosevelt","Rory","Rosa","Rosalee","Rosalia","Rosalind","Rosalinda","Rosalyn","Rosamond","Rosanna","Rosario","Roscoe","Rose","Rosella","Roselyn","Rosemarie","Rosemary","Rosendo","Rosetta","Rosie","Rosina","Roslyn","Ross","Rossie","Rowan","Rowena","Rowland","Roxane","Roxanne","Roy","Royal","Royce","Rozella","Ruben","Rubie","Ruby","Rubye","Rudolph","Rudy","Rupert","Russ","Russel","Russell","Rusty","Ruth","Ruthe","Ruthie","Ryan","Ryann","Ryder","Rylan","Rylee","Ryleigh","Ryley","Sabina","Sabrina","Sabryna","Sadie","Sadye","Sage","Saige","Sallie","Sally","Salma","Salvador","Salvatore","Sam","Samanta","Samantha","Samara","Samir","Sammie","Sammy","Samson","Sandra","Sandrine","Sandy","Sanford","Santa","Santiago","Santina","Santino","Santos","Sarah","Sarai","Sarina","Sasha","Saul","Savanah","Savanna","Savannah","Savion","Scarlett","Schuyler","Scot","Scottie","Scotty","Seamus","Sean","Sebastian","Sedrick","Selena","Selina","Selmer","Serena","Serenity","Seth","Shad","Shaina","Shakira","Shana","Shane","Shanel","Shanelle","Shania","Shanie","Shaniya","Shanna","Shannon","Shanny","Shanon","Shany","Sharon","Shaun","Shawn","Shawna","Shaylee","Shayna","Shayne","Shea","Sheila","Sheldon","Shemar","Sheridan","Sherman","Sherwood","Shirley","Shyann","Shyanne","Sibyl","Sid","Sidney","Sienna","Sierra","Sigmund","Sigrid","Sigurd","Silas","Sim","Simeon","Simone","Sincere","Sister","Skye","Skyla","Skylar","Sofia","Soledad","Solon","Sonia","Sonny","Sonya","Sophia","Sophie","Spencer","Stacey","Stacy","Stan","Stanford","Stanley","Stanton","Stefan","Stefanie","Stella","Stephan","Stephania","Stephanie","Stephany","Stephen","Stephon","Sterling","Steve","Stevie","Stewart","Stone","Stuart","Summer","Sunny","Susan","Susana","Susanna","Susie","Suzanne","Sven","Syble","Sydnee","Sydney","Sydni","Sydnie","Sylvan","Sylvester","Sylvia","Tabitha","Tad","Talia","Talon","Tamara","Tamia","Tania","Tanner","Tanya","Tara","Taryn","Tate","Tatum","Tatyana","Taurean","Tavares","Taya","Taylor","Teagan","Ted","Telly","Terence","Teresa","Terrance","Terrell","Terrence","Terrill","Terry","Tess","Tessie","Tevin","Thad","Thaddeus","Thalia","Thea","Thelma","Theo","Theodora","Theodore","Theresa","Therese","Theresia","Theron","Thomas","Thora","Thurman","Tia","Tiana","Tianna","Tiara","Tierra","Tiffany","Tillman","Timmothy","Timmy","Timothy","Tina","Tito","Titus","Tobin","Toby","Tod","Tom","Tomas","Tomasa","Tommie","Toney","Toni","Tony","Torey","Torrance","Torrey","Toy","Trace","Tracey","Tracy","Travis","Travon","Tre","Tremaine","Tremayne","Trent","Trenton","Tressa","Tressie","Treva","Trever","Trevion","Trevor","Trey","Trinity","Trisha","Tristian","Tristin","Triston","Troy","Trudie","Trycia","Trystan","Turner","Twila","Tyler","Tyra","Tyree","Tyreek","Tyrel","Tyrell","Tyrese","Tyrique","Tyshawn","Tyson","Ubaldo","Ulices","Ulises","Una","Unique","Urban","Uriah","Uriel","Ursula","Vada","Valentin","Valentina","Valentine","Valerie","Vallie","Van","Vance","Vanessa","Vaughn","Veda","Velda","Vella","Velma","Velva","Vena","Verda","Verdie","Vergie","Verla","Verlie","Vern","Verna","Verner","Vernice","Vernie","Vernon","Verona","Veronica","Vesta","Vicenta","Vicente","Vickie","Vicky","Victor","Victoria","Vida","Vidal","Vilma","Vince","Vincent","Vincenza","Vincenzo","Vinnie","Viola","Violet","Violette","Virgie","Virgil","Virginia","Virginie","Vita","Vito","Viva","Vivian","Viviane","Vivianne","Vivien","Vivienne","Vladimir","Wade","Waino","Waldo","Walker","Wallace","Walter","Walton","Wanda","Ward","Warren","Watson","Wava","Waylon","Wayne","Webster","Weldon","Wellington","Wendell","Wendy","Werner","Westley","Weston","Whitney","Wilber","Wilbert","Wilburn","Wiley","Wilford","Wilfred","Wilfredo","Wilfrid","Wilhelm","Wilhelmine","Will","Willa","Willard","William","Willie","Willis","Willow","Willy","Wilma","Wilmer","Wilson","Wilton","Winfield","Winifred","Winnifred","Winona","Winston","Woodrow","Wyatt","Wyman","Xander","Xavier","Xzavier","Yadira","Yasmeen","Yasmin","Yasmine","Yazmin","Yesenia","Yessenia","Yolanda","Yoshiko","Yvette","Yvonne","Zachariah","Zachary","Zachery","Zack","Zackary","Zackery","Zakary","Zander","Zane","Zaria","Zechariah","Zelda","Zella","Zelma","Zena","Zetta","Zion","Zita","Zoe","Zoey","Zoie","Zoila","Zola","Zora","Zula"],"last_name":["Abbott","Abernathy","Abshire","Adams","Altenwerth","Anderson","Ankunding","Armstrong","Auer","Aufderhar","Bahringer","Bailey","Balistreri","Barrows","Bartell","Bartoletti","Barton","Bashirian","Batz","Bauch","Baumbach","Bayer","Beahan","Beatty","Bechtelar","Becker","Bednar","Beer","Beier","Berge","Bergnaum","Bergstrom","Bernhard","Bernier","Bins","Blanda","Blick","Block","Bode","Boehm","Bogan","Bogisich","Borer","Bosco","Botsford","Boyer","Boyle","Bradtke","Brakus","Braun","Breitenberg","Brekke","Brown","Bruen","Buckridge","Carroll","Carter","Cartwright","Casper","Cassin","Champlin","Christiansen","Cole","Collier","Collins","Conn","Connelly","Conroy","Considine","Corkery","Cormier","Corwin","Cremin","Crist","Crona","Cronin","Crooks","Cruickshank","Cummerata","Cummings","Dach","D'Amore","Daniel","Dare","Daugherty","Davis","Deckow","Denesik","Dibbert","Dickens","Dicki","Dickinson","Dietrich","Donnelly","Dooley","Douglas","Doyle","DuBuque","Durgan","Ebert","Effertz","Eichmann","Emard","Emmerich","Erdman","Ernser","Fadel","Fahey","Farrell","Fay","Feeney","Feest","Feil","Ferry","Fisher","Flatley","Frami","Franecki","Friesen","Fritsch","Funk","Gaylord","Gerhold","Gerlach","Gibson","Gislason","Gleason","Gleichner","Glover","Goldner","Goodwin","Gorczany","Gottlieb","Goyette","Grady","Graham","Grant","Green","Greenfelder","Greenholt","Grimes","Gulgowski","Gusikowski","Gutkowski","Gutmann","Haag","Hackett","Hagenes","Hahn","Haley","Halvorson","Hamill","Hammes","Hand","Hane","Hansen","Harber","Harris","Hartmann","Harvey","Hauck","Hayes","Heaney","Heathcote","Hegmann","Heidenreich","Heller","Herman","Hermann","Hermiston","Herzog","Hessel","Hettinger","Hickle","Hilll","Hills","Hilpert","Hintz","Hirthe","Hodkiewicz","Hoeger","Homenick","Hoppe","Howe","Howell","Hudson","Huel","Huels","Hyatt","Jacobi","Jacobs","Jacobson","Jakubowski","Jaskolski","Jast","Jenkins","Jerde","Johns","Johnson","Johnston","Jones","Kassulke","Kautzer","Keebler","Keeling","Kemmer","Kerluke","Kertzmann","Kessler","Kiehn","Kihn","Kilback","King","Kirlin","Klein","Kling","Klocko","Koch","Koelpin","Koepp","Kohler","Konopelski","Koss","Kovacek","Kozey","Krajcik","Kreiger","Kris","Kshlerin","Kub","Kuhic","Kuhlman","Kuhn","Kulas","Kunde","Kunze","Kuphal","Kutch","Kuvalis","Labadie","Lakin","Lang","Langosh","Langworth","Larkin","Larson","Leannon","Lebsack","Ledner","Leffler","Legros","Lehner","Lemke","Lesch","Leuschke","Lind","Lindgren","Littel","Little","Lockman","Lowe","Lubowitz","Lueilwitz","Luettgen","Lynch","Macejkovic","MacGyver","Maggio","Mann","Mante","Marks","Marquardt","Marvin","Mayer","Mayert","McClure","McCullough","McDermott","McGlynn","McKenzie","McLaughlin","Medhurst","Mertz","Metz","Miller","Mills","Mitchell","Moen","Mohr","Monahan","Moore","Morar","Morissette","Mosciski","Mraz","Mueller","Muller","Murazik","Murphy","Murray","Nader","Nicolas","Nienow","Nikolaus","Nitzsche","Nolan","Oberbrunner","O'Connell","O'Conner","O'Hara","O'Keefe","O'Kon","Okuneva","Olson","Ondricka","O'Reilly","Orn","Ortiz","Osinski","Pacocha","Padberg","Pagac","Parisian","Parker","Paucek","Pfannerstill","Pfeffer","Pollich","Pouros","Powlowski","Predovic","Price","Prohaska","Prosacco","Purdy","Quigley","Quitzon","Rath","Ratke","Rau","Raynor","Reichel","Reichert","Reilly","Reinger","Rempel","Renner","Reynolds","Rice","Rippin","Ritchie","Robel","Roberts","Rodriguez","Rogahn","Rohan","Rolfson","Romaguera","Roob","Rosenbaum","Rowe","Ruecker","Runolfsdottir","Runolfsson","Runte","Russel","Rutherford","Ryan","Sanford","Satterfield","Sauer","Sawayn","Schaden","Schaefer","Schamberger","Schiller","Schimmel","Schinner","Schmeler","Schmidt","Schmitt","Schneider","Schoen","Schowalter","Schroeder","Schulist","Schultz","Schumm","Schuppe","Schuster","Senger","Shanahan","Shields","Simonis","Sipes","Skiles","Smith","Smitham","Spencer","Spinka","Sporer","Stamm","Stanton","Stark","Stehr","Steuber","Stiedemann","Stokes","Stoltenberg","Stracke","Streich","Stroman","Strosin","Swaniawski","Swift","Terry","Thiel","Thompson","Tillman","Torp","Torphy","Towne","Toy","Trantow","Tremblay","Treutel","Tromp","Turcotte","Turner","Ullrich","Upton","Vandervort","Veum","Volkman","Von","VonRueden","Waelchi","Walker","Walsh","Walter","Ward","Waters","Watsica","Weber","Wehner","Weimann","Weissnat","Welch","West","White","Wiegand","Wilderman","Wilkinson","Will","Williamson","Willms","Windler","Wintheiser","Wisoky","Wisozk","Witting","Wiza","Wolf","Wolff","Wuckert","Wunsch","Wyman","Yost","Yundt","Zboncak","Zemlak","Ziemann","Zieme","Zulauf"],"number":["777","69","34","{number}4"],"domain":["{noun}{tld}","{noun}{noun}{tld}"],"noun":["abc","acorn","adze","aphx","bat","ball","beach","bell","cat","chalk","dog","dent","elp"],"tld":[".com",".com.au",".co",".co.nz",".org",".info",".co.uk",".tech",".io",".xyz"]} -------------------------------------------------------------------------------- /examples/faker.rb: -------------------------------------------------------------------------------- 1 | require 'calyx' 2 | 3 | faker = Calyx::Grammar.load(__dir__ + '/faker.json') 4 | 5 | puts faker.generate(:full_name) 6 | puts faker.generate(:email) 7 | puts faker.generate(:username) 8 | -------------------------------------------------------------------------------- /examples/tiny_woodland_bot.rb: -------------------------------------------------------------------------------- 1 | require "calyx" 2 | require "twitter" 3 | require "logger" 4 | 5 | tiny_woodland = Calyx::Grammar.new do 6 | start :field 7 | field (0..7).map { "{row}{br}" }.join 8 | row (0..12).map { "{point}" }.join 9 | point trees: 0.6, foliage: 0.35, flowers: 0.05 10 | trees "🌲", "🌳" 11 | foliage "🌿", "🌱" 12 | flowers "🌷", "🌻", "🌼" 13 | br "\n" 14 | end 15 | 16 | twitter_client = Twitter::REST::Client.new do |config| 17 | config.consumer_key = ENV["TWITTER_CONSUMER_KEY"] 18 | config.consumer_secret = ENV["TWITTER_CONSUMER_SECRET"] 19 | config.access_token = ENV["TWITTER_ACCESS_TOKEN"] 20 | config.access_token_secret = ENV["TWITTER_CONSUMER_SECRET"] 21 | end 22 | 23 | logger = Logger.new(STDOUT) 24 | 25 | begin 26 | tweet = twitter_client.update(tiny_woodland.generate) 27 | logger.info("Posted tweet: https://twitter.com/#{tweet.user.name}/status/#{tweet.id}") 28 | rescue Exception => e 29 | logger.error(e) 30 | end 31 | -------------------------------------------------------------------------------- /lib/calyx.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems/deprecate' 2 | require 'calyx/version' 3 | require 'calyx/options' 4 | require 'calyx/rule' 5 | require 'calyx/result' 6 | require 'calyx/grammar' 7 | require 'calyx/errors' 8 | require 'calyx/format' 9 | require 'calyx/registry' 10 | require 'calyx/modifiers' 11 | require 'calyx/prefix_tree' 12 | require 'calyx/mapping' 13 | require 'calyx/production/affix_table' 14 | require 'calyx/syntax/token' 15 | require 'calyx/syntax/memo' 16 | require 'calyx/syntax/unique' 17 | require 'calyx/syntax/choices' 18 | require 'calyx/syntax/concat' 19 | require 'calyx/syntax/expression' 20 | require 'calyx/syntax/non_terminal' 21 | require 'calyx/syntax/terminal' 22 | require 'calyx/syntax/weighted_choices' 23 | -------------------------------------------------------------------------------- /lib/calyx/errors.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Library-specific error types. 3 | module Errors 4 | # Only rules that exist in the registry can be evaluated. When a 5 | # non-existent rule is referenced, this error is raised. 6 | # 7 | # grammar = Calyx::Grammar.new do 8 | # start :blank 9 | # end 10 | # 11 | # grammar.evaluate 12 | # # => Calyx::Errors::UndefinedRule: :blank is not defined 13 | class UndefinedRule < RuntimeError 14 | def initialize(rule, symbol) 15 | @trace = if rule 16 | rule.trace 17 | else 18 | trace_api_boundary(caller_locations) 19 | end 20 | 21 | super("undefined rule :#{symbol} in #{@trace.path}:#{@trace.lineno}:`#{source_line}`") 22 | end 23 | 24 | def trace_api_boundary(trace) 25 | (trace.count - 1).downto(0) do |index| 26 | if trace[index].to_s.include?('lib/calyx/') 27 | return trace[index + 1] 28 | end 29 | end 30 | end 31 | 32 | def source_line 33 | File.open(@trace.absolute_path) do |source| 34 | (@trace.lineno-1).times { source.gets } 35 | source.gets 36 | end.strip 37 | end 38 | end 39 | 40 | # Raised when a rule passed in via a context map conflicts with an existing 41 | # rule in the grammar. 42 | # 43 | # grammar = Calyx::Grammar.new do 44 | # start :priority 45 | # priority "(A)" 46 | # end 47 | # 48 | # grammar.evaluate(priority: "(B)") 49 | # # => Calyx::Errors::DuplicateRule: :priority is already registered 50 | class DuplicateRule < ArgumentError 51 | def initialize(msg) 52 | super(":#{msg} is already registered") 53 | end 54 | end 55 | 56 | # Raised when the client attempts to load a grammar with an unsupported file 57 | # extension. Only `.json` and `.yml` are valid. 58 | # 59 | # Calyx::Grammar.load("grammar.toml") 60 | # # => Calyx::Errors::UnsupportedFormat: grammar.toml is not a valid JSON or YAML file 61 | class UnsupportedFormat < ArgumentError 62 | def initialize(msg) 63 | super("#{File.basename(msg)} is not a valid JSON or YAML file") 64 | end 65 | end 66 | 67 | # Raised when a rule defined in a grammar is invalid. This will prevent 68 | # the grammar from compiling correctly. 69 | # 70 | # Calyx::Grammar.new do 71 | # start '40%' => 0.4, '30%' => 0.3 72 | # end 73 | # 74 | # # => Calyx::Errors::InvalidDefinition: Weights must sum to 1 75 | # 76 | class InvalidDefinition < ArgumentError 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/calyx/format.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | module Calyx 4 | # Helper methods for loading and initializing grammars from static files 5 | # on disk. 6 | module Format 7 | class Trace 8 | def initialize(match_symbol, filename, contents) 9 | @match_symbol = match_symbol 10 | @filename = Pathname.new(filename) 11 | @contents = contents 12 | end 13 | 14 | def path 15 | @filename.basename 16 | end 17 | 18 | def absolute_path 19 | @filename.expand_path 20 | end 21 | 22 | def lineno 23 | line_number = 0 24 | @contents.each_line do |line| 25 | line_number += 1 26 | return line_number if line =~ @match_symbol 27 | end 28 | end 29 | end 30 | 31 | class JSONGrammar 32 | def initialize(filename) 33 | require 'json' 34 | @filename = filename 35 | @contents = File.read(@filename) 36 | @rules = JSON.parse(@contents) 37 | end 38 | 39 | def each_rule(&block) 40 | @rules.each do |rule, productions| 41 | yield rule, productions, Trace.new(/("#{rule}")(\s*)(:)/, @filename, @contents) 42 | end 43 | end 44 | end 45 | 46 | class YAMLGrammar 47 | def initialize(filename) 48 | warn [ 49 | "NOTE: Loading grammars defined in YAML is deprecated. ", 50 | "Use the JSON format instead: `Calyx::Format.load(\"hello.json\")`" 51 | ].join 52 | 53 | require 'yaml' 54 | @filename = filename 55 | @contents = File.read(@filename) 56 | @rules = YAML.load(@contents) 57 | end 58 | 59 | def each_rule(&block) 60 | @rules.each do |rule, productions| 61 | yield rule, productions, Trace.new(/#{rule}:/, @filename, @contents) 62 | end 63 | end 64 | end 65 | 66 | # Reads a file and parses its format, based on the given extension. 67 | # 68 | # Accepts a JSON or YAML file path, identified by its extension (`.json` 69 | # or `.yml`). 70 | # 71 | # @param [String] filename 72 | # @return [Calyx::Grammar] 73 | def self.load(filename) 74 | extension = File.extname(filename) 75 | 76 | if extension == ".yml" 77 | self.load_yml(filename) 78 | elsif extension == ".json" 79 | self.load_json(filename) 80 | else 81 | raise Errors::UnsupportedFormat.new(filename) 82 | end 83 | end 84 | 85 | # Converts the given string of YAML data to a grammar instance. 86 | # 87 | # @param [String] filename 88 | # @return [Calyx::Format::YAMLGrammar] 89 | def self.load_yml(filename) 90 | self.build_grammar(YAMLGrammar.new(filename)) 91 | end 92 | 93 | # Converts the given string of JSON data to a grammar instance. 94 | # 95 | # @param [String] filename 96 | # @return [Calyx::Format::JSONGrammar] 97 | def self.load_json(filename) 98 | self.build_grammar(JSONGrammar.new(filename)) 99 | end 100 | 101 | private 102 | 103 | def self.build_grammar(grammar_format) 104 | Calyx::Grammar.new do 105 | grammar_format.each_rule do |label, productions, trace| 106 | productions = [productions] unless productions.is_a?(Enumerable) 107 | define_rule(label, trace, productions) 108 | end 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /lib/calyx/grammar.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # The main public interface to Calyx. Grammars represent the concept of a 3 | # template grammar defined by a set of production rules that can be chained 4 | # and nested from a given starting rule. 5 | # 6 | # Calyx works like a traditional phrase-structured grammar in reverse. Instead 7 | # of recognising strings based on a union of possible matches, it generates 8 | # strings by representing the union as a choice and randomly picking one 9 | # of the options each time the grammar runs. 10 | class Grammar 11 | class << self 12 | # Access the registry belonging to this grammar class. 13 | # 14 | # Constructs a new registry if it isn't already available. 15 | # 16 | # @return [Calyx::Registry] 17 | def registry 18 | @registry ||= Registry.new 19 | end 20 | 21 | # Load a grammar instance from the given file. 22 | # 23 | # Accepts a JSON or YAML file path, identified by its extension (`.json` 24 | # or `.yml`). 25 | # 26 | # @param [String] filename 27 | # @return [Calyx::Grammar] 28 | def load(filename) 29 | Format.load(filename) 30 | end 31 | 32 | # DSL helper method for registering a modifier module with the grammar. 33 | # 34 | # @param [Module] module_name 35 | def modifier(module_name) 36 | warn [ 37 | "NOTE: Loading modifiers via grammar class methods is deprecated.", 38 | "Alternative API TBD. For now this method still works." 39 | ].join 40 | registry.modifier(module_name) 41 | end 42 | 43 | # DSL helper method for registering a paired mapping regex. 44 | # 45 | # @param [Symbol] name 46 | # @param [Hash] pairs 47 | def mapping(name, pairs) 48 | warn [ 49 | "NOTE: The fixed `mapping` class method is deprecated.", 50 | "This still works but will be replaced with a new mapping format." 51 | ].join 52 | registry.mapping(name, pairs) 53 | end 54 | 55 | # DSL helper method for registering the given block as a string filter. 56 | # 57 | # @param [Symbol] name 58 | # @yieldparam [String] the input string to be processed by the filter 59 | # @yieldreturn [String] the processed output string 60 | def filter(name, &block) 61 | warn [ 62 | "NOTE: The fixed `filter` class method is deprecated.", 63 | "This will be removed in 0.22. Use the API for modifers instead." 64 | ].join 65 | registry.filter(name, &block) 66 | end 67 | 68 | # DSL helper method for registering a new grammar rule. 69 | # 70 | # Not usually used directly, as the method missing API is less verbose. 71 | # 72 | # @param [Symbol] name 73 | # @param [Array] productions 74 | def rule(name, *productions) 75 | registry.define_rule(name, caller_locations.first, productions) 76 | end 77 | 78 | # Augument the grammar with a method missing hook that treats class 79 | # method calls as declarations of a new rule. 80 | # 81 | # This must be bypassed by calling `#rule` directly if the name of the 82 | # desired rule clashes with an existing helper method. 83 | # 84 | # @param [Symbol] name 85 | # @param [Array] productions 86 | def method_missing(name, *productions) 87 | registry.define_rule(name, caller_locations.first, productions) 88 | end 89 | 90 | # Hook for combining the registry of a parent grammar into the child that 91 | # inherits from it. 92 | # 93 | # @param [Calyx::Registry] child_registry 94 | def inherit_registry(child_registry) 95 | registry.combine(child_registry) unless child_registry.nil? 96 | end 97 | 98 | # Hook for combining the rules from a parent grammar into the child that 99 | # inherits from it. 100 | # 101 | # This is automatically called by the Ruby engine. 102 | # 103 | # @param [Class] subclass 104 | def inherited(subclass) 105 | subclass.inherit_registry(registry) 106 | end 107 | end 108 | 109 | # Create a new grammar instance, passing in a random seed if needed. 110 | # 111 | # Grammar rules can be constructed on the fly when the passed-in block is 112 | # evaluated. 113 | # 114 | # @param [Numeric, Random, Hash] options 115 | def initialize(options={}, &block) 116 | unless options.is_a?(Hash) 117 | config_opts = {} 118 | if options.is_a?(Numeric) 119 | warn [ 120 | "NOTE: Passing a numeric seed arg directly is deprecated. ", 121 | "Use the options hash instead: `Calyx::Grammar.new(seed: 1234)`" 122 | ].join 123 | config_opts[:seed] = options 124 | elsif options.is_a?(Random) 125 | warn [ 126 | "NOTE: Passing a Random object directly is deprecated. ", 127 | "Use the options hash instead: `Calyx::Grammar.new(rng: Random.new)`" 128 | ].join 129 | config_opts[:rng] = options 130 | end 131 | else 132 | config_opts = options 133 | end 134 | 135 | @options = Options.new(config_opts) 136 | 137 | if block_given? 138 | @registry = Registry.new 139 | @registry.instance_eval(&block) 140 | else 141 | @registry = self.class.registry 142 | end 143 | 144 | @registry.options(@options) 145 | end 146 | 147 | # Produces a string as an output of the grammar. 148 | # 149 | # @overload generate(start_symbol) 150 | # @param [Symbol] start_symbol 151 | # @overload generate(rules_map) 152 | # @param [Hash] rules_map 153 | # @overload generate(start_symbol, rules_map) 154 | # @param [Symbol] start_symbol 155 | # @param [Hash] rules_map 156 | # @return [String] 157 | def generate(*args) 158 | result = generate_result(*args) 159 | result.text 160 | end 161 | 162 | # Produces a syntax tree of nested list nodes as an output of the grammar. 163 | # 164 | # @deprecated Please use {#generate_result} instead. 165 | def evaluate(*args) 166 | warn <<~DEPRECATION 167 | [DEPRECATION] `evaluate` is deprecated and will be removed in 1.0. 168 | Please use #generate_result instead. 169 | See https://github.com/maetl/calyx/issues/23 for more details. 170 | DEPRECATION 171 | 172 | result = generate_result(*args) 173 | result.tree 174 | end 175 | 176 | # Produces a generated result from evaluating the grammar. 177 | # 178 | # @see Calyx::Result 179 | # @overload generate_result(start_symbol) 180 | # @param [Symbol] start_symbol 181 | # @overload generate_result(rules_map) 182 | # @param [Hash] rules_map 183 | # @overload generate_result(start_symbol, rules_map) 184 | # @param [Symbol] start_symbol 185 | # @param [Hash] rules_map 186 | # @return [Calyx::Result] 187 | def generate_result(*args) 188 | start_symbol, rules_map = map_default_args(*args) 189 | 190 | Result.new(@registry.evaluate(start_symbol, rules_map)) 191 | end 192 | 193 | private 194 | 195 | def map_default_args(*args) 196 | start_symbol = :start 197 | rules_map = {} 198 | 199 | args.each do |arg| 200 | start_symbol = arg if arg.class == Symbol 201 | rules_map = arg if arg.class == Hash 202 | end 203 | 204 | [start_symbol, rules_map] 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/calyx/mapping.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | class Mapping 3 | def initialize 4 | @key = "key" 5 | @value = "value" 6 | end 7 | 8 | def call(input) 9 | if @key == input then @value 10 | else 11 | "" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/calyx/modifiers.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Applies modifiers to the output of a rule in a template substitution. 3 | class Modifiers 4 | # Transforms an output string by delegating to the given output function. 5 | # 6 | # If a registered modifier method is not found, then delegate to the given 7 | # string function. 8 | # 9 | # If an invalid modifier function is given, returns the raw input string. 10 | # 11 | # @param [Symbol] name 12 | # @param [String] value 13 | # @return [String] 14 | def transform(name, value) 15 | if respond_to?(name) 16 | send(name, value) 17 | elsif value.respond_to?(name) 18 | value.send(name) 19 | else 20 | value 21 | end 22 | end 23 | 24 | def upper(value) 25 | value.upcase 26 | end 27 | 28 | def lower(value) 29 | value.downcase 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/calyx/options.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Provides access to configuration options while evaluating a grammar. 3 | class Options 4 | # These options are used by default if not explicitly provided during 5 | # initialization of the grammar. 6 | DEFAULTS = { 7 | strict: true 8 | } 9 | 10 | # Constructs a new options instance, merging the passed in options with the 11 | # defaults. 12 | # 13 | # @param [Hash, Calyx::Options] options 14 | def initialize(options={}) 15 | @options = DEFAULTS.merge(options) 16 | end 17 | 18 | # Returns the internal random number generator instance. If a seed or random 19 | # instance is not passed-in directly, a new instance of `Random` is 20 | # initialized by default. 21 | # 22 | # @return [Random] 23 | def rng 24 | unless @options[:rng] 25 | @options[:rng] = if @options[:seed] 26 | Random.new(@options[:seed]) 27 | else 28 | Random.new 29 | end 30 | end 31 | 32 | @options[:rng] 33 | end 34 | 35 | # Returns the next pseudo-random number in the sequence defined by the 36 | # internal random number generator state. 37 | # 38 | # The value returned is a floating point number between 0.0 and 1.0, 39 | # including 0.0 and excluding 1.0. 40 | # 41 | # @return [Float] 42 | def rand 43 | rng.rand 44 | end 45 | 46 | # True if the strict mode option is enabled. This option defines whether or 47 | # not to raise an error on missing rules. When set to false, missing rules 48 | # are skipped over when the production is concatenated. 49 | # 50 | # @return [TrueClass, FalseClass] 51 | def strict? 52 | @options[:strict] 53 | end 54 | 55 | # Merges two instances together and returns a new instance. 56 | # 57 | # @param [Calyx::Options] options 58 | # @return [Calyx::Options] 59 | def merge(options) 60 | Options.new(@options.merge(options.to_h)) 61 | end 62 | 63 | # Serializes instance data to a hash. 64 | # 65 | # @return [Hash] 66 | def to_h 67 | @options.dup 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/calyx/prefix_tree.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | PrefixNode = Struct.new(:children, :index) 3 | PrefixEdge = Struct.new(:node, :label, :wildcard?) 4 | PrefixMatch = Struct.new(:label, :index, :captured) 5 | 6 | class PrefixTree 7 | def initialize 8 | @root = PrefixNode.new([], nil) 9 | end 10 | 11 | def insert(label, index) 12 | if @root.children.empty? 13 | @root.children << PrefixEdge.new(PrefixNode.new([], index), label, false) 14 | end 15 | end 16 | 17 | def add_all(elements) 18 | elements.each_with_index { |el, i| add(el, i) } 19 | end 20 | 21 | def add(label, index) 22 | parts = label.split(/(%)/).reject { |p| p.empty? } 23 | parts_count = parts.count 24 | 25 | # Can’t use more than one capture symbol which gives the following splits: 26 | # - ["literal"] 27 | # - ["%", "literal"] 28 | # - ["literal", "%"] 29 | # - ["literal", "%", "literal"] 30 | if parts_count > 3 31 | raise "Too many capture patterns: #{label}" 32 | end 33 | 34 | current_node = @root 35 | 36 | parts.each_with_index do |part, i| 37 | index_slot = (i == parts_count - 1) ? index : nil 38 | is_wildcard = part == "%" 39 | matched_prefix = false 40 | 41 | current_node.children.each_with_index do |edge, j| 42 | prefix = common_prefix(edge.label, part) 43 | unless prefix.empty? 44 | matched_prefix = true 45 | 46 | if prefix == edge.label 47 | # Current prefix matches the edge label so we can continue down the 48 | # tree without mutating the current branch 49 | next_node = PrefixNode.new([], index_slot) 50 | current_node.children << PrefixEdge.new(next_node, label.delete_prefix(prefix), is_wildcard) 51 | else 52 | # We have a partial match on current edge so replace it with the new 53 | # prefix then rejoin the remaining suffix to the existing branch 54 | edge.label = edge.label.delete_prefix(prefix) 55 | prefix_node = PrefixNode.new([edge], nil) 56 | next_node = PrefixNode.new([], index_slot) 57 | prefix_node.children << PrefixEdge.new(next_node, label.delete_prefix(prefix), is_wildcard) 58 | current_node.children[j] = PrefixEdge.new(prefix_node, prefix, is_wildcard) 59 | end 60 | 61 | current_node = next_node 62 | break 63 | end 64 | end 65 | 66 | # No existing edges have a common prefix so push a new branch onto the tree 67 | # at the current level 68 | unless matched_prefix 69 | next_edge = PrefixEdge.new(PrefixNode.new([], index_slot), part, is_wildcard) 70 | current_node.children << next_edge 71 | current_node = next_edge.node 72 | end 73 | end 74 | end 75 | 76 | # This was basically ported from the pseudocode found on Wikipedia to Ruby, 77 | # with a lot of extra internal state tracking that is totally absent from 78 | # most algorithmic descriptions. This ends up making a real mess of the 79 | # expression of the algorithm, mostly due to choices and conflicts between 80 | # whether to go with the standard iterative and procedural flow of statements 81 | # or use a more functional style. A mangle that speaks to the questions 82 | # around portability between different languages. Is this codebase a design 83 | # prototype? Is it an evolving example that should guide implementations in 84 | # other languages? 85 | # 86 | # The problem with code like this is that it’s a bit of a maintenance burden 87 | # if not structured compactly and precisely enough to not matter and having 88 | # enough tests passing that it lasts for a few years without becoming a 89 | # nuisance or leading to too much nonsense. 90 | # 91 | # There are several ways to implement this, some of these may work better or 92 | # worse, and this might be quite different across multiple languages so what 93 | # goes well in one place could suck in other places. The only way to make a 94 | # good decision around it is to learn via testing and experiments. 95 | # 96 | # Alternative possible implementations: 97 | # - Regex compilation on registration, use existing legacy mapping code 98 | # - Prefix tree, trie, radix tree/trie, compressed bitpatterns, etc 99 | # - Split string flip, imperative list processing hacks 100 | # (easier for more people to contribute?) 101 | def lookup(label) 102 | current_node = @root 103 | chars_consumed = 0 104 | chars_captured = nil 105 | label_length = label.length 106 | 107 | # Traverse the tree until reaching a leaf node or all input characters are consumed 108 | while current_node != nil && !current_node.children.empty? && chars_consumed < label_length 109 | # Candidate edge pointing to the next node to check 110 | candidate_edge = nil 111 | 112 | # Traverse from the current node down the tree looking for candidate edges 113 | current_node.children.each do |edge| 114 | # Generate a suffix based on the prefix already consumed 115 | sub_label = label[chars_consumed, label_length] 116 | 117 | # If this edge is a wildcard we check the next level of the tree 118 | if edge.wildcard? 119 | # Wildcard pattern is anchored to the end of the string so we can 120 | # consume all remaining characters and pick this as an edge candidate 121 | if edge.node.children.empty? 122 | chars_captured = label[chars_consumed, sub_label.length] 123 | chars_consumed += sub_label.length 124 | candidate_edge = edge 125 | break 126 | end 127 | 128 | # The wildcard is anchored to the start or embedded in the middle of 129 | # the string so we traverse this edge and scan the next level of the 130 | # tree with a greedy lookahead. This means we will always match as 131 | # much of the wildcard string as possible when there is a trailing 132 | # suffix that could be repeated several times within the characters 133 | # consumed by the wildcard pattern. 134 | # 135 | # For example, we expect `"te%s"` to match on `"tests"` rather than 136 | # bail out after matching the first three characters `"tes"`. 137 | edge.node.children.each do |lookahead_edge| 138 | prefix = sub_label.rindex(lookahead_edge.label) 139 | if prefix 140 | chars_captured = label[chars_consumed, prefix] 141 | chars_consumed += prefix + lookahead_edge.label.length 142 | candidate_edge = lookahead_edge 143 | break 144 | end 145 | end 146 | # We found a candidate so no need to continue checking edges 147 | break if candidate_edge 148 | else 149 | # Look for a common prefix on this current edge label and the remaining suffix 150 | if edge.label == common_prefix(edge.label, sub_label) 151 | chars_consumed += edge.label.length 152 | candidate_edge = edge 153 | break 154 | end 155 | end 156 | end 157 | 158 | if candidate_edge 159 | # Traverse to the node our edge candidate points to 160 | current_node = candidate_edge.node 161 | else 162 | # We didn’t find a possible edge candidate so bail out of the loop 163 | current_node = nil 164 | end 165 | end 166 | 167 | # In order to return a match, the following postconditions must be true: 168 | # - We are pointing to a leaf node 169 | # - We have consumed all the input characters 170 | if current_node != nil and current_node.index != nil and chars_consumed == label_length 171 | PrefixMatch.new(label, current_node.index, chars_captured) 172 | else 173 | nil 174 | end 175 | end 176 | 177 | def common_prefix(a, b) 178 | selected_prefix = "" 179 | min_index_length = a < b ? a.length : b.length 180 | index = 0 181 | 182 | until index == min_index_length 183 | return selected_prefix if a[index] != b[index] 184 | selected_prefix += a[index] 185 | index += 1 186 | end 187 | 188 | selected_prefix 189 | end 190 | end 191 | end 192 | -------------------------------------------------------------------------------- /lib/calyx/production/affix_table.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Production 3 | # A type of production rule representing a bidirectional dictionary of 4 | # mapping pairs that can be used as a substitution table in template 5 | # expressions. 6 | class AffixTable 7 | def self.parse(productions, registry) 8 | # TODO: handle wildcard expressions 9 | self.new(productions) 10 | end 11 | 12 | # %es 13 | # prefix: nil, suffix: 'es' 14 | # match: 'buses' -> ends_with(suffix) 15 | 16 | # %y 17 | # prefix: nil, suffix: 'ies' 18 | 19 | def initialize(mapping) 20 | @lhs_index = PrefixTree.new 21 | @rhs_index = PrefixTree.new 22 | 23 | @lhs_list = mapping.keys 24 | @rhs_list = mapping.values 25 | 26 | @lhs_index.add_all(@lhs_list) 27 | @rhs_index.add_all(@rhs_list) 28 | end 29 | 30 | def value_for(key) 31 | match = @lhs_index.lookup(key) 32 | result = @rhs_list[match.index] 33 | 34 | if match.captured 35 | result.sub("%", match.captured) 36 | else 37 | result 38 | end 39 | end 40 | 41 | def key_for(value) 42 | match = @rhs_index.lookup(value) 43 | result = @lhs_list[match.index] 44 | 45 | if match.captured 46 | result.sub("%", match.captured) 47 | else 48 | result 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/calyx/production/uniform_branch.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Productions 3 | class UniformBranch 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/calyx/production/weighted_branch.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Productions 3 | class WeightedBranch 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/calyx/registry.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Lookup table of all the available rules in the grammar. 3 | class Registry 4 | attr_reader :rules, :dicts, :transforms, :modifiers 5 | 6 | # Construct an empty registry. 7 | def initialize 8 | @options = Options.new({}) 9 | @rules = {} 10 | @dicts = {} 11 | @transforms = {} 12 | @modifiers = Modifiers.new 13 | end 14 | 15 | # Applies additional config options to this instance. 16 | # 17 | # @param [Options] opts 18 | def options(opts) 19 | @options = @options.merge(opts) 20 | end 21 | 22 | # Attaches a modifier module to this instance. 23 | # 24 | # @param [Module] name 25 | def modifier(name) 26 | modifiers.extend(name) 27 | end 28 | 29 | # Registers a paired mapping regex. 30 | # 31 | # @param [Symbol] name 32 | # @param [Hash] pairs 33 | def mapping(name, pairs) 34 | transforms[name.to_sym] = construct_mapping(pairs) 35 | end 36 | 37 | # Registers the given block as a string filter. 38 | # 39 | # @param [Symbol] name 40 | # @yield [String] 41 | # @yieldreturn [String] 42 | def filter(name, callable=nil, &block) 43 | if block_given? 44 | transforms[name.to_sym] = block 45 | else 46 | transforms[name.to_sym] = callable 47 | end 48 | end 49 | 50 | # Registers a new grammar rule without explicitly calling the `#rule` method. 51 | # 52 | # @param [Symbol] name 53 | # @param [Array] productions 54 | def method_missing(name, *productions) 55 | define_rule(name, caller_locations.first, productions) 56 | end 57 | 58 | # Registers a new grammar rule. 59 | # 60 | # @param [Symbol] name 61 | # @param [Array] productions 62 | def rule(name, *productions) 63 | define_rule(name, caller_locations.first, productions) 64 | end 65 | 66 | # Defines a static rule in the grammar. 67 | # 68 | # @param [Symbol] name 69 | # @param [Array] productions 70 | def define_rule(name, trace, productions) 71 | symbol = name.to_sym 72 | 73 | # TODO: this could be tidied up by consolidating parsing in a single class 74 | branch = Rule.build_ast(productions, self) 75 | 76 | # If the static rule is a map of k=>v pairs then add it to the lookup dict 77 | if branch.is_a?(Production::AffixTable) 78 | dicts[symbol] = branch 79 | else 80 | rules[symbol] = Rule.new(symbol, branch, trace) 81 | end 82 | end 83 | 84 | # Defines a rule in the temporary evaluation context. 85 | # 86 | # @param [Symbol] name 87 | # @param [Array] productions 88 | def define_context_rule(name, trace, productions) 89 | productions = [productions] unless productions.is_a?(Enumerable) 90 | context[name.to_sym] = Rule.new(name.to_sym, Rule.build_ast(productions, self), trace) 91 | end 92 | 93 | # Expands the given symbol to its rule. 94 | # 95 | # @param [Symbol] symbol 96 | # @return [Calyx::Rule] 97 | def expand(symbol) 98 | expansion = rules[symbol] || context[symbol] 99 | 100 | if expansion.nil? 101 | if @options.strict? 102 | raise Errors::UndefinedRule.new(@last_expansion, symbol) 103 | else 104 | expansion = Syntax::Terminal.new('') 105 | end 106 | end 107 | 108 | @last_expansion = expansion 109 | expansion 110 | end 111 | 112 | # Applies the given modifier function to the given value to filter it. 113 | # 114 | # @param [Symbol] name 115 | # @param [String] value 116 | # @return [String] 117 | def expand_filter(name, value) 118 | if transforms.key?(name) 119 | transforms[name].call(value) 120 | else 121 | modifiers.transform(name, value) 122 | end 123 | end 124 | 125 | # Applies a modifier to substitute the value with a bidirectional map 126 | # lookup. 127 | # 128 | # @param [Symbol] name 129 | # @param [String] value 130 | # @param [Symbol] direction :left or :right 131 | # @return [String] 132 | def expand_map(name, value, direction) 133 | map_lookup = dicts[name] 134 | 135 | if direction == :left 136 | map_lookup.key_for(value) 137 | else 138 | map_lookup.value_for(value) 139 | end 140 | end 141 | 142 | # Expands a memoized rule symbol by evaluating it and storing the result 143 | # for later. 144 | # 145 | # @param [Symbol] symbol 146 | def memoize_expansion(symbol) 147 | memos[symbol] ||= expand(symbol).evaluate(@options) 148 | end 149 | 150 | # Expands a unique rule symbol by evaluating it and checking that it hasn't 151 | # previously been selected. 152 | # 153 | # @param [Symbol] symbol 154 | def unique_expansion(symbol) 155 | pending = true 156 | uniques[symbol] = [] if uniques[symbol].nil? 157 | 158 | while pending 159 | if uniques[symbol].size == expand(symbol).size 160 | uniques[symbol] = [] 161 | pending = false 162 | end 163 | 164 | result = expand(symbol).evaluate(@options) 165 | 166 | unless uniques[symbol].include?(result) 167 | uniques[symbol] << result 168 | pending = false 169 | end 170 | end 171 | 172 | result 173 | end 174 | 175 | # Merges the given registry instance with the target registry. 176 | # 177 | # This is only needed at compile time, so that child classes can easily 178 | # inherit the set of rules decared by their parent. 179 | # 180 | # @param [Calyx::Registry] registry 181 | def combine(registry) 182 | @rules = rules.merge(registry.rules) 183 | end 184 | 185 | # Evaluates the grammar defined in this registry, combining it with rules 186 | # from the passed in context. 187 | # 188 | # Produces a syntax tree of nested list nodes. 189 | # 190 | # @param [Symbol] start_symbol 191 | # @param [Hash] rules_map 192 | # @return [Array] 193 | def evaluate(start_symbol=:start, rules_map={}) 194 | reset_evaluation_context 195 | 196 | rules_map.each do |key, value| 197 | if rules.key?(key.to_sym) 198 | raise Errors::DuplicateRule.new(key) 199 | end 200 | 201 | define_context_rule(key, caller_locations.last, value) 202 | end 203 | 204 | [start_symbol, expand(start_symbol).evaluate(@options)] 205 | end 206 | 207 | private 208 | 209 | attr_reader :memos, :context, :uniques 210 | 211 | def reset_evaluation_context 212 | @context = {} 213 | @memos = {} 214 | @uniques = {} 215 | end 216 | 217 | def construct_mapping(pairs) 218 | mapper = -> (input) { 219 | match, target = pairs.detect { |match, target| input =~ match } 220 | 221 | if match && target 222 | input.gsub(match, target) 223 | else 224 | input 225 | end 226 | } 227 | end 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /lib/calyx/result.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Value object representing a generated grammar result. 3 | class Result 4 | def initialize(expression) 5 | @expression = expression.freeze 6 | end 7 | 8 | # Produces a syntax tree of nested nodes as the output of the grammar. Each 9 | # syntax node represents the production rules that were evaluated at each 10 | # step of the generator. 11 | # 12 | # @return [Array] 13 | def tree 14 | @expression 15 | end 16 | 17 | alias_method :to_exp, :tree 18 | 19 | # Produces a text string as the output of the grammar. 20 | # 21 | # @return [String] 22 | def text 23 | @expression.flatten.reject do |obj| 24 | obj.is_a?(Symbol) 25 | end.join 26 | end 27 | 28 | alias_method :to_s, :text 29 | 30 | # Produces a symbol as the output of the grammar. 31 | # 32 | # @return [Symbol] 33 | def symbol 34 | text.to_sym 35 | end 36 | 37 | alias_method :to_sym, :symbol 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/calyx/rule.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # Represents a named rule connected to a tree of productions that can be 3 | # evaluated and a trace which represents where the rule was declared. 4 | class Rule 5 | def self.build_ast(productions, registry) 6 | if productions.first.is_a?(Hash) 7 | # TODO: test that key is a string 8 | 9 | if productions.first.first.last.is_a?(String) 10 | # If value of the production is a strings then this is a 11 | # paired mapping production. 12 | Production::AffixTable.parse(productions.first, registry) 13 | else 14 | # Otherwise, we assume this is a weighted choice declaration and 15 | # convert the hash to an array 16 | Syntax::WeightedChoices.parse(productions.first.to_a, registry) 17 | end 18 | elsif productions.first.is_a?(Enumerable) 19 | # TODO: this needs to change to support attributed/tagged grammars 20 | Syntax::WeightedChoices.parse(productions, registry) 21 | else 22 | Syntax::Choices.parse(productions, registry) 23 | end 24 | end 25 | 26 | attr_reader :name, :tree, :trace 27 | 28 | def initialize(name, productions, trace) 29 | @name = name.to_sym 30 | @tree = productions 31 | @trace = trace 32 | end 33 | 34 | def size 35 | tree.size 36 | end 37 | 38 | def evaluate(options) 39 | tree.evaluate(options) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/calyx/syntax/choices.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | # A type of production rule representing a list of possible rules, one of 3 | # which will chosen each time the grammar runs. 4 | module Syntax 5 | class Choices 6 | # Parse a list of productions and return a choice node which is the head 7 | # of a syntax tree of child nodes. 8 | # 9 | # @param [Array] productions 10 | # @param [Calyx::Registry] registry 11 | def self.parse(productions, registry) 12 | choices = productions.map do |choice| 13 | if choice.is_a?(String) 14 | Concat.parse(choice, registry) 15 | elsif choice.is_a?(Integer) 16 | Terminal.new(choice.to_s) 17 | elsif choice.is_a?(Symbol) 18 | if choice[0] == Memo::SIGIL 19 | Memo.new(choice, registry) 20 | elsif choice[0] == Unique::SIGIL 21 | Unique.new(choice, registry) 22 | else 23 | NonTerminal.new(choice, registry) 24 | end 25 | end 26 | end 27 | self.new(choices) 28 | end 29 | 30 | # Initialize a new choice with a list of child nodes. 31 | # 32 | # @param [Array] collection 33 | def initialize(collection) 34 | @collection = collection 35 | end 36 | 37 | # The number of possible choices available for this rule. 38 | # 39 | # @return [Integer] 40 | def size 41 | @collection.size 42 | end 43 | 44 | # Evaluate the choice by randomly picking one of its possible options. 45 | # 46 | # @param [Calyx::Options] options 47 | # @return [Array] 48 | def evaluate(options) 49 | [:choice, @collection.sample(random: options.rng).evaluate(options)] 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/calyx/syntax/concat.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule representing a string combining both template 4 | # substitutions and raw content. 5 | class Concat 6 | EXPRESSION = /(\{[A-Za-z0-9_@$<>\.]+\})/.freeze 7 | DEREF_OP = /([<>\.])/.freeze 8 | START_TOKEN = '{'.freeze 9 | END_TOKEN = '}'.freeze 10 | 11 | # Parses an interpolated string into fragments combining terminal strings 12 | # and non-terminal rules. 13 | # 14 | # Returns a concat node which is the head of a tree of child nodes. 15 | # 16 | # @param [String] production 17 | # @param [Calyx::Registry] registry 18 | def self.parse(production, registry) 19 | expressions = production.split(EXPRESSION).map do |atom| 20 | if atom.is_a?(String) 21 | if atom.chars.first == START_TOKEN && atom.chars.last == END_TOKEN 22 | head, *tail = atom.slice(1, atom.length-2).split(DEREF_OP) 23 | if tail.any? 24 | ExpressionChain.parse(head, tail, registry) 25 | else 26 | Expression.parse(head, registry) 27 | end 28 | else 29 | Terminal.new(atom) 30 | end 31 | end 32 | end 33 | 34 | self.new(expressions) 35 | end 36 | 37 | # Initialize the concat node with an expansion of terminal and 38 | # non-terminal fragments. 39 | # 40 | # @param [Array] expansion 41 | def initialize(expressions) 42 | @expressions = expressions 43 | end 44 | 45 | # Evaluate all the child nodes of this node and concatenate each expansion 46 | # together into a single result. 47 | # 48 | # @param [Calyx::Options] options 49 | # @return [Array] 50 | def evaluate(options) 51 | expansion = @expressions.reduce([]) do |exp, atom| 52 | exp << atom.evaluate(options) 53 | end 54 | 55 | #[:expansion, expansion] 56 | # TODO: fix this along with a git rename 57 | # Commented out because of a lot of tests depending on :concat symbol 58 | [:concat, expansion] 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/calyx/syntax/expression.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A symbolic expression representing a single template substitution. 4 | class Expression 5 | def self.parse(symbol, registry) 6 | if symbol[0] == Memo::SIGIL 7 | Memo.new(symbol, registry) 8 | elsif symbol[0] == Unique::SIGIL 9 | Unique.new(symbol, registry) 10 | else 11 | NonTerminal.new(symbol, registry) 12 | end 13 | end 14 | end 15 | 16 | class Modifier < Struct.new(:type, :name, :map_dir) 17 | def self.filter(name) 18 | new(:filter, name, nil) 19 | end 20 | 21 | def self.map_left(name) 22 | new(:map, name, :left) 23 | end 24 | 25 | def self.map_right(name) 26 | new(:map, name, :right) 27 | end 28 | end 29 | 30 | # Handles filter chains that symbolic expressions can pass through to 31 | # generate a custom substitution. 32 | class ExpressionChain 33 | def self.parse(production, production_chain, registry) 34 | modifier_chain = production_chain.each_slice(2).map do |op_token, target| 35 | rule = target.to_sym 36 | case op_token 37 | when Token::EXPR_FILTER then Modifier.filter(rule) 38 | when Token::EXPR_MAP_LEFT then Modifier.map_left(rule) 39 | when Token::EXPR_MAP_RIGHT then Modifier.map_right(rule) 40 | else 41 | # Should not end up here because the regex excludes it but this 42 | # could be a place to add a helpful parse error on any weird 43 | # chars used by the expression—current behaviour is to pass 44 | # the broken expression through to the result as part of the 45 | # text, as if that is what the author meant. 46 | raise("unreachable") 47 | end 48 | end 49 | 50 | expression = Expression.parse(production, registry) 51 | 52 | self.new(expression, modifier_chain, registry) 53 | end 54 | 55 | # @param [#evaluate] production 56 | # @param [Array] modifiers 57 | # @param [Calyx::Registry] registry 58 | def initialize(production, modifiers, registry) 59 | @production = production 60 | @modifiers = modifiers 61 | @registry = registry 62 | end 63 | 64 | # Evaluate the expression by expanding the non-terminal to produce a 65 | # terminal string, then passing it through the given modifier chain and 66 | # returning the transformed result. 67 | # 68 | # @param [Calyx::Options] options 69 | # @return [Array] 70 | def evaluate(options) 71 | expanded = @production.evaluate(options).flatten.reject { |o| o.is_a?(Symbol) }.join 72 | chain = [] 73 | 74 | expression = @modifiers.reduce(expanded) do |value, modifier| 75 | case modifier.type 76 | when :filter 77 | @registry.expand_filter(modifier.name, value) 78 | when :map 79 | @registry.expand_map(modifier.name, value, modifier.map_dir) 80 | end 81 | end 82 | 83 | [:expression, expression] 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/calyx/syntax/memo.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule representing a memoized subsitution which 4 | # returns the first value selected on all subsequent lookups. 5 | class Memo 6 | SIGIL = '@'.freeze 7 | 8 | # Construct a memoized rule, given the symbol to lookup and the registry 9 | # to look it up in. 10 | # 11 | # @param [Symbol] symbol 12 | # @param [Calyx::Registry] registry 13 | def initialize(symbol, registry) 14 | @symbol = symbol.slice(1, symbol.length-1).to_sym 15 | @registry = registry 16 | end 17 | 18 | # Evaluate the memo, using the registry to handle the expansion. 19 | # 20 | # @param [Calyx::Options] options 21 | # @return [Array] 22 | def evaluate(options) 23 | [@symbol, @registry.memoize_expansion(@symbol)] 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/calyx/syntax/non_terminal.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule that represents a non-terminal expansion, 4 | # linking one rule to another. 5 | class NonTerminal 6 | # Construct a non-terminal node, given the symbol to lookup and the 7 | # registry to look it up in. 8 | # 9 | # @param [Symbol] symbol 10 | # @param [Calyx::Registry] registry 11 | def initialize(symbol, registry) 12 | @symbol = symbol.to_sym 13 | @registry = registry 14 | end 15 | 16 | # Evaluate the non-terminal, using the registry to handle the expansion. 17 | # 18 | # @param [Calyx::Options] options 19 | # @return [Array] 20 | def evaluate(options) 21 | [@symbol, @registry.expand(@symbol).evaluate(options)] 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/calyx/syntax/paired_mapping.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule representing a bidirectional dictionary of 4 | # mapping pairs that can be used as a substitution table in template 5 | # expressions. 6 | class PairedMapping 7 | def self.parse(productions, registry) 8 | # TODO: handle wildcard expressions 9 | self.new(productions) 10 | end 11 | 12 | # %es 13 | # prefix: nil, suffix: 'es' 14 | # match: 'buses' -> ends_with(suffix) 15 | 16 | # %y 17 | # prefix: nil, suffix: 'ies' 18 | 19 | def initialize(mapping) 20 | @lhs_index = PrefixTree.new 21 | @rhs_index = PrefixTree.new 22 | 23 | @lhs_list = mapping.keys 24 | @rhs_list = mapping.values 25 | 26 | @lhs_index.add_all(@lhs_list) 27 | @rhs_index.add_all(@rhs_list) 28 | end 29 | 30 | def value_for(key) 31 | match = @lhs_index.lookup(key) 32 | result = @rhs_list[match.index] 33 | 34 | if match.captured 35 | result.sub("%", match.captured) 36 | else 37 | result 38 | end 39 | end 40 | 41 | def key_for(value) 42 | match = @rhs_index.lookup(value) 43 | result = @lhs_list[match.index] 44 | 45 | if match.captured 46 | result.sub("%", match.captured) 47 | else 48 | result 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/calyx/syntax/terminal.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule that terminates with a resulting string atom. 4 | class Terminal 5 | # Construct a terminal node with the given value. 6 | # 7 | # @param [#to_s] atom 8 | def initialize(atom) 9 | @atom = atom 10 | end 11 | 12 | # Evaluate the terminal by returning its identity directly. 13 | # 14 | # @param [Calyx::Options] options 15 | # @return [Array] 16 | def evaluate(options) 17 | [:atom, @atom] 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/calyx/syntax/token.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | module Token 4 | EXPR_MAP_LEFT = "<" 5 | EXPR_MAP_RIGHT = ">" 6 | EXPR_FILTER = "." 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/calyx/syntax/unique.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule representing a unique substitution which only 4 | # returns values that have not previously been selected. The probability 5 | # that a given rule will be selected increases as more selections are made 6 | # and the list grows smaller. 7 | class Unique 8 | SIGIL = '$'.freeze 9 | 10 | # Construct a unique rule, given the symbol to lookup and the registry 11 | # to look it up in. 12 | # 13 | # @param [Symbol] symbol 14 | # @param [Calyx::Registry] registry 15 | def initialize(symbol, registry) 16 | @symbol = symbol.slice(1, symbol.length-1).to_sym 17 | @registry = registry 18 | end 19 | 20 | # Evaluate the unique rule, using the registry to handle the expansion 21 | # and keep track of previous selections. 22 | # 23 | # @param [Calyx::Options] options 24 | # @return [Array] 25 | def evaluate(options) 26 | [@symbol, @registry.unique_expansion(@symbol)] 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/calyx/syntax/weighted_choices.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | module Syntax 3 | # A type of production rule representing a map of possible rules with 4 | # associated weights that define the expected probability of a rule 5 | # being chosen. 6 | class WeightedChoices 7 | # Parse a given list or hash of productions into a syntax tree of weighted 8 | # choices. Supports weights specified as Range, Fixnum and Float types. 9 | # 10 | # All weights get normalized to a set of values in the 0..1 interval that 11 | # sum to 1. 12 | # 13 | # @param [Array, Hash<#to_s, Float>] productions 14 | # @param [Calyx::Registry] registry 15 | # @return [Calyx::Syntax::WeightedChoices] 16 | def self.parse(productions, registry) 17 | if productions.first.last.is_a?(Range) 18 | range_max = productions.max { |a,b| a.last.max <=> b.last.max }.last.max 19 | 20 | weights_sum = productions.reduce(0) do |memo, choice| 21 | memo += choice.last.size 22 | end 23 | 24 | if range_max != weights_sum 25 | raise Errors::InvalidDefinition, "Weights must sum to total: #{range_max}" 26 | end 27 | 28 | normalized_productions = productions.map do |choice| 29 | weight = choice.last.size / range_max.to_f 30 | [choice.first, weight] 31 | end 32 | else 33 | weights_sum = productions.reduce(0) do |memo, choice| 34 | memo += choice.last 35 | end 36 | 37 | if productions.first.last.is_a?(Float) 38 | raise Errors::InvalidDefinition, 'Weights must sum to 1' if weights_sum != 1.0 39 | normalized_productions = productions 40 | else 41 | normalized_productions = productions.map do |choice| 42 | weight = choice.last.to_f / weights_sum.to_f * 1.0 43 | [choice.first, weight] 44 | end 45 | end 46 | end 47 | 48 | choices = normalized_productions.map do |choice, weight| 49 | if choice.is_a?(String) 50 | [Concat.parse(choice, registry), weight] 51 | elsif choice.is_a?(Symbol) 52 | [NonTerminal.new(choice, registry), weight] 53 | end 54 | end 55 | 56 | self.new(choices) 57 | end 58 | 59 | # Initialize a new choice with a list of child nodes. 60 | # 61 | # @param [Array] collection 62 | def initialize(collection) 63 | @collection = collection 64 | end 65 | 66 | # The number of possible choices available for this rule. 67 | # 68 | # @return [Integer] 69 | def size 70 | @collection.size 71 | end 72 | 73 | # Evaluate the choice by randomly picking one of its possible options, 74 | # balanced according to the given weights. 75 | # 76 | # The method for selecting weighted probabilities is based on a snippet 77 | # of code recommended in the Ruby standard library documentation. 78 | # 79 | # @param [Calyx::Options] options 80 | # @return [Array] 81 | def evaluate(options) 82 | choice = @collection.max_by do |_, weight| 83 | options.rand ** (1.0 / weight) 84 | end.first 85 | 86 | [:weighted_choice, choice.evaluate(options)] 87 | end 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/calyx/version.rb: -------------------------------------------------------------------------------- 1 | module Calyx 2 | VERSION = '0.22.0'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /spec/format/load_json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe '#load_json' do 4 | def sample_path(filename) 5 | "#{__dir__}/samples/#{filename}.json" 6 | end 7 | 8 | specify 'generate with recursive rules' do 9 | grammar = Calyx::Format.load_json(sample_path('hello_statement')) 10 | expect(grammar.generate).to eq('Hello World') 11 | end 12 | 13 | specify 'generate with multiple choices' do 14 | grammar = Calyx::Format.load_json(sample_path('multiple_choices')) 15 | expect(grammar.generate).to match(/Dragon Wins|Hero Wins/) 16 | end 17 | 18 | specify 'generate with rule expansion' do 19 | grammar = Calyx::Format.load_json(sample_path('rule_expansion')) 20 | expect(grammar.generate).to match(/apple|orange/) 21 | end 22 | 23 | specify 'generate with weighted choices' do 24 | grammar = Calyx::Format.load_json(sample_path('weighted_choices')) 25 | expect(grammar.generate).to match(/40%|60%/) 26 | end 27 | 28 | specify 'raise error if weighted choices do not sum to 1' do 29 | expect { Calyx::Format.load_json(sample_path('bad_weights')) }.to raise_error('Weights must sum to 1') 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/format/load_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'json' 3 | 4 | describe '#load' do 5 | def sample(filename) 6 | "#{__dir__}/samples/#{filename}" 7 | end 8 | 9 | specify 'can load a JSON file properly' do 10 | grammar = Calyx::Format.load(sample('hello.json')) 11 | expect(grammar.generate).to eq('Hello World') 12 | end 13 | 14 | specify 'raises error if given a file that it cannot parse' do 15 | expect { Calyx::Format.load(sample('bad_syntax.json')) }.to raise_error(JSON::ParserError) 16 | end 17 | 18 | specify 'raises error with bad file extension' do 19 | expect { Calyx::Format.load(sample('bad_extension.bad')) }.to raise_error(Calyx::Errors::UnsupportedFormat) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/format/samples/bad_extension.bad: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maetl/calyx/eaf49eabf06dad9ba343756549be688f4d03bc43/spec/format/samples/bad_extension.bad -------------------------------------------------------------------------------- /spec/format/samples/bad_syntax.json: -------------------------------------------------------------------------------- 1 | { 2 | "rule": hanging 3 | } 4 | -------------------------------------------------------------------------------- /spec/format/samples/bad_weights.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": [["90%", 0.9], ["80%", 0.8]] 3 | } 4 | -------------------------------------------------------------------------------- /spec/format/samples/hello.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "Hello World" 3 | } 4 | -------------------------------------------------------------------------------- /spec/format/samples/hello_statement.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "{hello_world}", 3 | "hello_world": "{statement}", 4 | "statement": "Hello World" 5 | } 6 | -------------------------------------------------------------------------------- /spec/format/samples/multiple_choices.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": ["{dragon_wins}", "{hero_wins}"], 3 | "dragon_wins": "Dragon Wins", 4 | "hero_wins": "Hero Wins" 5 | } 6 | -------------------------------------------------------------------------------- /spec/format/samples/rule_expansion.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": "{fruit}", 3 | "fruit": ["apple", "orange"] 4 | } 5 | -------------------------------------------------------------------------------- /spec/format/samples/weighted_choices.json: -------------------------------------------------------------------------------- 1 | { 2 | "start": { 3 | "60%": 0.6, 4 | "40%": 0.4 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /spec/grammar/affix_table_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Production::AffixTable do 4 | let(:registry) do 5 | Calyx::Registry.new 6 | end 7 | 8 | describe 'literal match' do 9 | let(:paired_map) do 10 | Calyx::Production::AffixTable.parse({ 11 | 'atom' => 'atoms', 12 | 'molecule' => 'molecules' 13 | }, registry) 14 | end 15 | 16 | specify 'lookup from key to value' do 17 | expect(paired_map.value_for('atom')).to eq('atoms') 18 | expect(paired_map.value_for('molecule')).to eq('molecules') 19 | end 20 | 21 | specify 'lookup from value to key' do 22 | expect(paired_map.key_for('atoms')).to eq('atom') 23 | expect(paired_map.key_for('molecules')).to eq('molecule') 24 | end 25 | end 26 | 27 | describe 'wildcard match' do 28 | let(:paired_map) do 29 | Calyx::Production::AffixTable.parse({ 30 | "%y" => "%ies", 31 | "%s" => "%ses", 32 | "%" => "%s" 33 | }, registry) 34 | end 35 | 36 | specify 'lookup from key to value' do 37 | expect(paired_map.value_for('ferry')).to eq('ferries') 38 | expect(paired_map.value_for('bus')).to eq('buses') 39 | expect(paired_map.value_for('car')).to eq('cars') 40 | end 41 | 42 | specify 'lookup from value to key' do 43 | expect(paired_map.key_for('ferries')).to eq('ferry') 44 | expect(paired_map.key_for('buses')).to eq('bus') 45 | expect(paired_map.key_for('cars')).to eq('car') 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/grammar/class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'Class' do 5 | it 'combines rules from inheritance chain in subclass' do 6 | class ParentRule < Calyx::Grammar 7 | rule :one, 'One' 8 | end 9 | 10 | class ChildRule < ParentRule 11 | rule :two, 'Two' 12 | end 13 | 14 | class GrandchildRule < ChildRule 15 | rule :three, 'Three' 16 | end 17 | 18 | class GreatGrandchildRule < GrandchildRule 19 | start '{one}. {two}. {three}.' 20 | end 21 | 22 | grammar = GreatGrandchildRule.new 23 | expect(grammar.generate).to eq('One. Two. Three.') 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/grammar/context_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'context hash' do 5 | specify 'construct dynamic rules with context hash of values' do 6 | class TemplateStringValues < Calyx::Grammar 7 | start '{one}{two}{three}' 8 | end 9 | 10 | grammar = TemplateStringValues.new 11 | expect(grammar.generate({one: 1, two: 2, three: 3})).to eq('123') 12 | end 13 | 14 | specify 'construct dynamic rules with context hash of expansion strings' do 15 | class TemplateStringExpansions < Calyx::Grammar 16 | start '{how}' 17 | rule :a, 'piece of string?' 18 | end 19 | 20 | grammar = TemplateStringExpansions.new 21 | expect(grammar.generate({how: '{long}', long: '{is}', is: '{a}'})).to eq('piece of string?') 22 | end 23 | 24 | specify 'construct dynamic rules with context hash of choices' do 25 | class TemplateStringChoices < Calyx::Grammar 26 | start '{fruit}' 27 | end 28 | 29 | grammar = TemplateStringChoices.new 30 | expect(grammar.generate({fruit: ['apple', 'orange']})).to match(/apple|orange/) 31 | end 32 | 33 | 34 | specify 'construct dynamic rules with two context hashes' do 35 | class StockReport < Calyx::Grammar 36 | start 'You should buy shares in {company}.' 37 | end 38 | 39 | grammar = StockReport.new 40 | cyberdyne_context_hash = {company: 'Cyberdyne' } 41 | bridgestone_context_hash = {company: 'Bridgestone' } 42 | 43 | expect(grammar.generate(cyberdyne_context_hash)).to eq('You should buy shares in Cyberdyne.') 44 | expect(grammar.generate(bridgestone_context_hash)).to eq('You should buy shares in Bridgestone.') 45 | end 46 | 47 | specify 'raise error when duplicate rule is passed' do 48 | grammar = Calyx::Grammar.new do 49 | start :priority 50 | priority '(A)' 51 | end 52 | 53 | expect { grammar.generate(priority: '(B)') }.to raise_error(Calyx::Errors::DuplicateRule) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /spec/grammar/error_trace_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Grammar do 2 | describe 'error trace' do 3 | specify 'undefined rule in block constructor' do 4 | grammar = Calyx::Grammar.new do 5 | start '{next_rule}' 6 | end 7 | 8 | expect { 9 | grammar.generate 10 | }.to raise_error(Calyx::Errors::UndefinedRule, /:next_rule/) 11 | end 12 | 13 | specify 'undefined rule in class definition' do 14 | class UndefinedRules < Calyx::Grammar 15 | start '{next_rule}' 16 | end 17 | 18 | grammar = UndefinedRules.new 19 | 20 | expect { 21 | grammar.generate 22 | }.to raise_error(Calyx::Errors::UndefinedRule, /:next_rule/) 23 | end 24 | 25 | specify 'undefined start symbol in block constructor' do 26 | grammar = Calyx::Grammar.new 27 | 28 | expect { 29 | grammar.generate 30 | }.to raise_error(Calyx::Errors::UndefinedRule, /:start/) 31 | end 32 | 33 | specify 'undefined start symbol in block constructor' do 34 | grammar = Calyx::Grammar.new 35 | 36 | expect { 37 | grammar.generate 38 | }.to raise_error(Calyx::Errors::UndefinedRule, /:start/) 39 | end 40 | 41 | specify 'undefined rule in context hash' do 42 | grammar = Calyx::Grammar.new do 43 | start :hello 44 | end 45 | 46 | expect { 47 | grammar.generate(hello: :world) 48 | }.to raise_error(Calyx::Errors::UndefinedRule, /:world/) 49 | end 50 | 51 | specify 'error trace contains details of source line' do 52 | grammar = Calyx::Grammar.new do 53 | start '{hello}' 54 | end 55 | 56 | expect { grammar.generate }.to raise_error { |error| 57 | expect(error.message).to match(/error_trace_spec.rb:53/) 58 | expect(error.message).to match(/start '{hello}'/) 59 | expect(error.source_line).to eq("start '{hello}'") 60 | } 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/grammar/generate_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe '#generate' do 5 | specify 'generate with explicit start symbol' do 6 | grammar = Calyx::Grammar.new do 7 | alt_start 'alt_start' 8 | start 'start' 9 | end 10 | 11 | expect(grammar.generate(:alt_start)).to eq('alt_start') 12 | end 13 | 14 | specify 'generate with explicit start symbol and context hash' do 15 | grammar = Calyx::Grammar.new do 16 | alt_start '{alt_start_var}' 17 | start 'start' 18 | end 19 | 20 | expect(grammar.generate(:alt_start, { alt_start_var: 'alt_start_var' })).to eq('alt_start_var') 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/grammar/instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'Instance' do 5 | it 'constructs a grammar in block with #rule' do 6 | hue = Calyx::Grammar.new do 7 | rule :start, 'rgb({r},{g},{b})' 8 | rule :r, '255' 9 | rule :g, '0' 10 | rule :b, '0' 11 | end 12 | 13 | expect(hue.generate).to eq('rgb(255,0,0)') 14 | end 15 | 16 | it 'constructs a grammar in block with #method_missing' do 17 | hue = Calyx::Grammar.new do 18 | start 'rgb({r},{g},{b})' 19 | r '255' 20 | g '0' 21 | b '0' 22 | end 23 | 24 | expect(hue.generate).to eq('rgb(255,0,0)') 25 | end 26 | 27 | it 'returns an empty grammar when block not given' do 28 | empty = Calyx::Grammar.new 29 | 30 | expect(empty.generate(start: 'START')).to eq('START') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/grammar/interpolation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'string interpolation' do 5 | it 'substitutes multiple rules in a string' do 6 | class OneTwo < Calyx::Grammar 7 | start '{one}. {two}.' 8 | rule :one, 'One' 9 | rule :two, 'Two' 10 | end 11 | 12 | grammar = OneTwo.new 13 | expect(grammar.generate).to eq('One. Two.') 14 | end 15 | 16 | it 'calls a formatting modifier in a string substitution' do 17 | class StringFormatters < Calyx::Grammar 18 | start :hello_world 19 | rule :hello_world, '{hello.capitalize} world' 20 | rule :hello, 'hello' 21 | end 22 | 23 | grammar = StringFormatters.new(seed: 12345) 24 | expect(grammar.generate).to eq('Hello world') 25 | end 26 | 27 | it 'calls a chain of formatting functions in a string substitution' do 28 | class StringFormatters < Calyx::Grammar 29 | start :hello_world 30 | rule :hello_world, '{hello.upcase.rstrip}.' 31 | rule :hello, 'hello world ' 32 | end 33 | 34 | grammar = StringFormatters.new 35 | expect(grammar.generate).to eq('HELLO WORLD.') 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/grammar/mapping_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'custom mappings' do 5 | it 'modifies expressions with custom mappings defined in the grammar' do 6 | grammar = Calyx::Grammar.new do 7 | mapping :trim_e, /(.+)(e$)/ => "\\1" 8 | ism_without_e '{structural.trim_e}ism' 9 | ism_with_e '{narrative.trim_e}ism' 10 | structural 'structural' 11 | narrative 'narrative' 12 | end 13 | 14 | expect(grammar.generate(:ism_without_e)).to eq('structuralism') 15 | expect(grammar.generate(:ism_with_e)).to eq('narrativism') 16 | end 17 | 18 | it 'modifies expressions with methods from an included module' do 19 | module Pluralisation 20 | def pluralise(input) 21 | "#{input}s" 22 | end 23 | end 24 | 25 | grammar = Calyx::Grammar.new do 26 | modifier Pluralisation 27 | start '{noun.pluralise}' 28 | noun 'noun' 29 | end 30 | 31 | expect(grammar.generate).to eq('nouns') 32 | end 33 | 34 | it 'only includes modifiers on the instance scope' do 35 | grammar = Calyx::Grammar.new do 36 | start '{noun.pluralise}' 37 | noun 'noun' 38 | end 39 | 40 | expect(grammar.generate).to eq('noun') 41 | end 42 | 43 | it 'responds to builtin modifiers' do 44 | grammar = Calyx::Grammar.new do 45 | start '{noun_l.upper} & {noun_u.lower}' 46 | noun_l 'lower' 47 | noun_u 'UPPER' 48 | end 49 | 50 | expect(grammar.generate).to eq('LOWER & upper') 51 | end 52 | 53 | it "handles modifier chains with bidirectional mapping" do 54 | grammar = Calyx::Grammar.new(seed: 12345) do 55 | animal "Snowball", "Santa’s Little Helper" 56 | posessive "her", "his" 57 | 58 | animal_to_pronoun({ 59 | "Snowball" => "her", 60 | "Santa’s Little Helper" => "his" 61 | }) 62 | 63 | map_left "{@posessiveanimal_to_pronoun} tail" 65 | end 66 | 67 | expect(grammar.generate(:map_left)).to eq("Snowball licks her tail") 68 | expect(grammar.generate(:map_right)).to eq("Santa’s Little Helper licks his tail") 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/grammar/memo_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'set' 3 | 4 | describe Calyx::Grammar do 5 | describe 'memoized rules' do 6 | specify 'memoized rule mapped with symbol prefix' do 7 | grammar = Calyx::Grammar.new do 8 | rule :start, '{@tramp}:{@tramp}' 9 | rule :tramp, :@character 10 | rule :character, 'Vladimir', 'Estragon' 11 | end 12 | 13 | actual = grammar.generate.split(':') 14 | expect(actual.first).to eq(actual.last) 15 | end 16 | 17 | specify 'memoized rule mapped with template expression' do 18 | grammar = Calyx::Grammar.new do 19 | rule :start, :pupper 20 | rule :pupper, '{@spitz}:{@spitz}' 21 | rule :spitz, 'pomeranian', 'samoyed', 'shiba inu' 22 | end 23 | 24 | actual = grammar.generate.split(':') 25 | expect(actual.first).to eq(actual.last) 26 | end 27 | 28 | specify 'memoized rules are reset between multiple runs' do 29 | grammar = Calyx::Grammar.new do 30 | rule :start, '{flower}{flower}{flower}' 31 | rule :flower, :@flowers 32 | rule :flowers, '🌷', '🌻', '🌼' 33 | end 34 | 35 | generations = Set.new 36 | 37 | while generations.size < 3 38 | generation = grammar.generate 39 | expect(generation).to match(/🌷🌷🌷|🌻🌻🌻|🌼🌼🌼/) 40 | generations << generation 41 | end 42 | end 43 | 44 | specify 'memoized rules capture nested expansions' do 45 | grammar = Calyx::Grammar.new do 46 | rule :start, '{@chain}:{@chain}' 47 | rule :chain, '{one}{one}', '{two}{two}', '{three}{three}' 48 | rule :one, '{a}' 49 | rule :two, '{b}' 50 | rule :three, '{c}' 51 | rule :a, 'a', 'A' 52 | rule :b, 'b', 'B' 53 | rule :c, 'c', 'C' 54 | end 55 | 56 | actual = grammar.generate.split(':') 57 | expect(actual.first).to eq(actual.last) 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/grammar/options_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rspec/expectations' 2 | 3 | RSpec::Matchers.define :eq_per_platform do |expected| 4 | match do |actual| 5 | key = if expected.key?(RUBY_ENGINE.to_sym) 6 | RUBY_ENGINE.to_sym 7 | else 8 | :default 9 | end 10 | 11 | actual == expected[key] 12 | end 13 | end 14 | 15 | describe Calyx::Grammar do 16 | describe 'options' do 17 | class Metallurgy < Calyx::Grammar 18 | start 'platinum', 'titanium', 'tungsten' 19 | end 20 | 21 | describe ':seed' do 22 | describe 'deprecation' do 23 | before do 24 | @orig_stderr = $stderr 25 | $stderr = StringIO.new 26 | end 27 | 28 | it 'deprecates seed number in the legacy constructor format' do 29 | Metallurgy.new(43210) 30 | $stderr.rewind 31 | expect($stderr.string.chomp).to match(/Passing a numeric seed arg directly is deprecated./) 32 | end 33 | 34 | after do 35 | $stderr = @orig_stderr 36 | end 37 | end 38 | 39 | it 'accepts a seed option' do 40 | grammar = Metallurgy.new(seed: 43210) 41 | 42 | expect(grammar.generate).to eq_per_platform(jruby: 'platinum', default: 'platinum') 43 | end 44 | end 45 | 46 | describe ':rng' do 47 | describe 'deprecation' do 48 | before do 49 | @orig_stderr = $stderr 50 | $stderr = StringIO.new 51 | end 52 | 53 | it 'deprecates random instance in the legacy constructor format' do 54 | Metallurgy.new(Random.new(43210)) 55 | $stderr.rewind 56 | expect($stderr.string.chomp).to match(/Passing a Random object directly is deprecated./) 57 | end 58 | 59 | after do 60 | $stderr = @orig_stderr 61 | end 62 | end 63 | 64 | it 'accepts a random instance as an option' do 65 | grammar = Metallurgy.new(rng: Random.new(43210)) 66 | 67 | expect(grammar.generate).to eq_per_platform(jruby: 'platinum', default: 'platinum') 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/grammar/rules_dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'rules dsl' do 5 | specify 'rule symbols from named instance methods' do 6 | grammar = Calyx::Grammar.new do 7 | start '{hello} {world}.' 8 | hello 'Hallo' 9 | world 'Welt' 10 | end 11 | 12 | expect(grammar.generate).to eq('Hallo Welt.') 13 | end 14 | 15 | specify 'rule symbols from named class methods' do 16 | class HalloWelt < Calyx::Grammar 17 | start '{hello} {world}.' 18 | hello 'Hallo' 19 | world 'Welt' 20 | end 21 | grammar = HalloWelt.new 22 | 23 | expect(grammar.generate).to eq('Hallo Welt.') 24 | end 25 | 26 | specify 'class definitions behave the same as instance definitions' do 27 | class ClassDef < Calyx::Grammar 28 | start '.1.' 29 | end 30 | 31 | instance_def = Calyx::Grammar.new do 32 | start '.1.' 33 | end 34 | 35 | class_def = ClassDef.new 36 | expect(class_def.generate).to eq(instance_def.generate) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/grammar/strict_evaluation_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Grammar do 2 | describe 'strict evaluation' do 3 | it 'raises undefined rule errors by default' do 4 | grammar = Calyx::Grammar.new do 5 | start '{missing_rule}' 6 | end 7 | 8 | expect { grammar.generate }.to raise_error(Calyx::Errors::UndefinedRule, /{missing_rule}/) 9 | end 10 | 11 | it 'raises undefined rule errors when :strict => true' do 12 | grammar = Calyx::Grammar.new(strict: true) do 13 | start '{missing_rule}' 14 | end 15 | 16 | expect { grammar.generate }.to raise_error(Calyx::Errors::UndefinedRule, /{missing_rule}/) 17 | end 18 | 19 | it 'concats empty strings for undefined rules when :strict => false' do 20 | grammar = Calyx::Grammar.new(strict: false) do 21 | start '{missing_rule}' 22 | end 23 | 24 | expect(grammar.generate).to eq('') 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/grammar/symbols_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'recursive symbols' do 5 | it 'substitutes a chain of rules with symbols' do 6 | class RuleSymbols < Calyx::Grammar 7 | start :rule_symbol 8 | rule :rule_symbol, :terminal_symbol 9 | rule :terminal_symbol, 'OK' 10 | end 11 | 12 | grammar = RuleSymbols.new 13 | expect(grammar.generate).to eq('OK') 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/grammar/unique_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Grammar do 2 | describe 'unique rules' do 3 | specify 'unique rule mapped with symbol prefix' do 4 | grammar = Calyx::Grammar.new do 5 | rule :start, '{$tramp}:{$tramp}' 6 | rule :tramp, :$character 7 | rule :character, 'Vladimir', 'Estragon' 8 | end 9 | 10 | actual = grammar.generate.split(':') 11 | expect(actual.first).to_not eq(actual.last) 12 | end 13 | 14 | specify 'unique rules never repeat the same choice' do 15 | grammar = Calyx::Grammar.new do 16 | rule :start, '{flower}{flower}{flower}' 17 | rule :flower, :$flowers 18 | rule :flowers, '🌷', '🌻', '🌼' 19 | end 20 | 21 | expect(grammar.generate).to match(/🌷🌻🌼|🌷🌼🌻|🌻🌷🌼|🌻🌼🌷|🌼🌻🌷|🌼🌷🌻/) 22 | end 23 | 24 | specify 'unique rules cycle once sequence is consumed' do 25 | grammar = Calyx::Grammar.new do 26 | rule :start, '{pet}{pet}{pet}' 27 | rule :pet, :$pets 28 | rule :pets, '🐱', '🐶' 29 | end 30 | 31 | expect(grammar.generate).to match(/🐱🐶🐶|🐱🐶🐱|🐶🐱🐱|🐶🐱🐶/) 32 | end 33 | 34 | specify 'unique rules can merge from execution context' do 35 | grammar = Calyx::Grammar.new do 36 | start "{$num}{$num}" 37 | end 38 | 39 | expect(grammar.generate(num: ["¶", "§"])).to match(/¶§|§¶/) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/grammar/weighted_choices_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Grammar do 4 | describe 'weighted choices' do 5 | it 'raises error if weighted choices do not sum to 1' do 6 | expect do 7 | class BadWeights < Calyx::Grammar 8 | start ['10%', 0.1], ['70%', 0.7] 9 | end 10 | end.to raise_error('Weights must sum to 1') 11 | end 12 | 13 | it 'selects rules with a weighted choice' do 14 | class WeightedChoices < Calyx::Grammar 15 | start ['20%', 0.2], ['80%', 0.8] 16 | end 17 | 18 | grammar = WeightedChoices.new(seed: 12345) 19 | expect(grammar.generate).to eq('20%') 20 | end 21 | 22 | it 'selects rules with hash choices' do 23 | grammar = Calyx::Grammar.new(seed: 12345) do 24 | start '20%' => 0.2, '80%' => 0.8 25 | end 26 | 27 | expect(grammar.generate).to eq('20%') 28 | end 29 | 30 | it 'raises error if ranges do not sum to range.max' do 31 | expect do 32 | Calyx::Grammar.new(seed: 12345) do 33 | start 'first' => 0..1, 'last' => 5..7 34 | end 35 | end.to raise_error('Weights must sum to total: 7') 36 | end 37 | 38 | it 'selects rules with range weights' do 39 | grammar = Calyx::Grammar.new(seed: 12345) do 40 | start '20%' => 1..2, '80%' => 3..10 41 | end 42 | 43 | expect(grammar.generate).to eq('20%') 44 | end 45 | 46 | it 'selects rules with fixnum weights' do 47 | grammar = Calyx::Grammar.new(seed: 12345) do 48 | start '20%' => 20, '80%' => 80 49 | end 50 | 51 | expect(grammar.generate).to eq('20%') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/mapping_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Mapping do 2 | it "is substitutable with callable procs" do 3 | mapping = Calyx::Mapping.new 4 | 5 | expect(mapping.call("key")).to eq("value") 6 | expect(mapping.call("key!")).to eq("") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/options_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Options do 2 | describe :rng do 3 | it 'returns a random generator from given numeric seed' do 4 | options = Calyx::Options.new(seed: 1234) 5 | expect(options.rng.seed).to eq(1234) 6 | end 7 | 8 | it 'returns a random generator from given random instance' do 9 | random = Random.new(5678) 10 | options = Calyx::Options.new(rng: random) 11 | expect(options.rng).to be(random) 12 | end 13 | 14 | it 'returns a new generator instance when none is provided' do 15 | options = Calyx::Options.new 16 | expect(options.rng).to be_a(Random) 17 | end 18 | end 19 | 20 | describe :strict do 21 | it 'returns true by default' do 22 | options = Calyx::Options.new 23 | expect(options.strict?).to eq(true) 24 | end 25 | 26 | it 'returns false when option is set' do 27 | options = Calyx::Options.new(strict: false) 28 | expect(options.strict?).to eq(false) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/prefix_tree_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::PrefixTree do 2 | specify 'longest common prefix of strings' do 3 | tree = Calyx::PrefixTree.new 4 | expect(tree.common_prefix("a", "b")).to eq("") 5 | expect(tree.common_prefix("aaaaa", "aab")).to eq("aa") 6 | expect(tree.common_prefix("aa", "ab")).to eq("a") 7 | expect(tree.common_prefix("ababababahahahaha", "ababafgfgbaba")).to eq("ababa") 8 | expect(tree.common_prefix("abab", "abab")).to eq("abab") 9 | end 10 | 11 | specify "insert single value" do 12 | tree = Calyx::PrefixTree.new 13 | tree.add("one", 0) 14 | 15 | expect(tree.lookup("one").index).to eq(0) 16 | expect(tree.lookup("one!!")).to be_falsey 17 | expect(tree.lookup("two")).to be_falsey 18 | end 19 | 20 | specify "lookup with literal string data" do 21 | tree = Calyx::PrefixTree.new 22 | tree.add("test", 2) 23 | tree.add("team", 3) 24 | 25 | expect(tree.lookup("test").index).to eq(2) 26 | expect(tree.lookup("team").index).to eq(3) 27 | expect(tree.lookup("teal")).to be_falsey 28 | end 29 | 30 | specify "lookup with leading wildcard data" do 31 | tree = Calyx::PrefixTree.new 32 | tree.add("%es", 111) 33 | 34 | expect(tree.lookup("buses").index).to eq(111) 35 | expect(tree.lookup("bus")).to be_falsey 36 | expect(tree.lookup("train")).to be_falsey 37 | expect(tree.lookup("bushes").index).to eq(111) 38 | end 39 | 40 | specify "lookup with trailing wildcard data" do 41 | tree = Calyx::PrefixTree.new 42 | tree.add("te%", 222) 43 | 44 | expect(tree.lookup("test").index).to eq(222) 45 | expect(tree.lookup("total")).to be_falsey 46 | expect(tree.lookup("rubbish")).to be_falsey 47 | expect(tree.lookup("team").index).to eq(222) 48 | end 49 | 50 | specify "lookup with anchored wildcard data" do 51 | tree = Calyx::PrefixTree.new 52 | tree.add("te%s", 333) 53 | 54 | expect(tree.lookup("tests").index).to eq(333) 55 | expect(tree.lookup("total")).to be_falsey 56 | expect(tree.lookup("test")).to be_falsey 57 | expect(tree.lookup("team")).to be_falsey 58 | expect(tree.lookup("teams").index).to eq(333) 59 | end 60 | 61 | specify "lookup with catch all wildcard data" do 62 | tree = Calyx::PrefixTree.new 63 | tree.add("%", 444) 64 | 65 | expect(tree.lookup("tests").index).to eq(444) 66 | expect(tree.lookup("total").index).to eq(444) 67 | expect(tree.lookup("test").index).to eq(444) 68 | expect(tree.lookup("team").index).to eq(444) 69 | expect(tree.lookup("teams").index).to eq(444) 70 | end 71 | 72 | specify "lookup with cascading wildcard data" do 73 | tree = Calyx::PrefixTree.new 74 | tree.add("%y", 555) 75 | tree.add("%s", 666) 76 | tree.add("%", 777) 77 | 78 | expect(tree.lookup("ferry").index).to eq(555) 79 | expect(tree.lookup("bus").index).to eq(666) 80 | expect(tree.lookup("car").index).to eq(777) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Registry do 4 | let(:registry) do 5 | Calyx::Registry.new 6 | end 7 | 8 | specify 'registry evaluates the start rule' do 9 | registry.start('atom') 10 | expect(registry.evaluate).to eq([:start, [:choice, [:concat, [[:atom, 'atom']]]]]) 11 | end 12 | 13 | specify 'registry evaluates recursive rules' do 14 | registry.start(:atom) 15 | registry.rule(:atom, 'atom') 16 | expect(registry.evaluate).to eq([:start, [:choice, [:atom, [:choice, [:concat, [[:atom, 'atom']]]]]]]) 17 | end 18 | 19 | specify 'evaluate from context if rule not found' do 20 | registry.start(:atom) 21 | expect(registry.evaluate(:start, atom: 'atom')).to eq([:start, [:choice, [:atom, [:choice, [:concat, [[:atom, 'atom']]]]]]]) 22 | end 23 | 24 | specify 'evaluate concatenated production' do 25 | registry.start('Hello, {name}.') 26 | registry.rule(:name, 'Joe') 27 | expect(registry.evaluate).to eq([:start, [:choice, [:concat, [[:atom, 'Hello, '], [:name, [:choice, [:concat, [[:atom, 'Joe']]]]], [:atom, '.']]]]]) 28 | end 29 | 30 | specify 'define rules with error traces' do 31 | registry.define_rule(:start, 'Registry#define_rule', ['Hello, {name}.']) 32 | registry.define_rule(:name, 'Registry#define_rule', ['Joe']) 33 | expect(registry.evaluate).to eq([:start, [:choice, [:concat, [[:atom, 'Hello, '], [:name, [:choice, [:concat, [[:atom, 'Joe']]]]], [:atom, '.']]]]]) 34 | end 35 | 36 | specify 'define rules with method missing' do 37 | registry.one('1.') 38 | expect(registry.evaluate(:one)).to eq([:one, [:choice, [:concat, [[:atom, "1."]]]]]) 39 | end 40 | 41 | specify 'transform a value using core string API' do 42 | expect(registry.expand_filter(:upcase, 'derive')).to eq('DERIVE') 43 | end 44 | 45 | specify 'transform a value using custom string transformation' do 46 | registry.mapping(:past_tensify, /(.+e)$/ => '\1d') 47 | expect(registry.expand_filter(:past_tensify, 'derive')).to eq('derived') 48 | end 49 | 50 | specify 'unregistered transform returns the identity passed to it' do 51 | expect(registry.expand_filter(:null, 'derive')). to eq('derive') 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/result_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Result do 4 | describe '#tree' do 5 | specify 'wraps expression tree' do 6 | tree = Calyx::Result.new([:root, [:leaf, 'atom']]).tree 7 | expect(tree).to eq([:root, [:leaf, 'atom']]) 8 | end 9 | 10 | specify 'expression tree is immutable' do 11 | tree = Calyx::Result.new([:root, [:leaf, 'atom']]).tree 12 | expect { tree << [:leaf, 'atom'] }.to raise_error(RuntimeError) 13 | end 14 | 15 | specify '#to_exp aliases #tree' do 16 | result = Calyx::Result.new([:root, [:leaf, 'atom']]) 17 | expect(result.to_exp).to eq(result.tree) 18 | end 19 | end 20 | 21 | describe '#text' do 22 | specify 'flattens expression tree to string' do 23 | expr = [:root, [:branch, [:leaf, 'one'], [:leaf, ' '], [:leaf, 'two']]] 24 | text = Calyx::Result.new(expr).text 25 | expect(text).to eq('one two') 26 | end 27 | 28 | specify '#to_s aliases #text' do 29 | result = Calyx::Result.new([:root, [:leaf, 'atom']]) 30 | expect(result.to_s).to eq(result.text) 31 | end 32 | 33 | specify '#to_s interpolates automatically' do 34 | result = Calyx::Result.new([:root, [:leaf, 'atom']]) 35 | expect("#{result}").to eq(result.text) 36 | end 37 | end 38 | 39 | describe '#symbol' do 40 | specify 'flattens expression tree to symbol' do 41 | symbol = Calyx::Result.new([:root, [:leaf, 'atom']]).symbol 42 | expect(symbol).to eq(:atom) 43 | end 44 | 45 | specify '#to_sym aliases #symbol' do 46 | result = Calyx::Result.new([:root, [:leaf, 'atom']]) 47 | expect(result.to_sym).to eq(result.symbol) 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /spec/rule_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Rule do 4 | describe '#build_ast' do 5 | before(:all) do 6 | @registry = Calyx::Registry.new 7 | end 8 | 9 | specify 'ε' do 10 | tree = Calyx::Rule.build_ast([], @registry) 11 | expect(tree).to be_a(Calyx::Syntax::Choices) 12 | expect(tree.size).to eq(0) 13 | end 14 | 15 | specify 'choices list' do 16 | tree = Calyx::Rule.build_ast(["One", "Two"], @registry) 17 | expect(tree).to be_a(Calyx::Syntax::Choices) 18 | expect(tree.size).to eq(2) 19 | end 20 | 21 | specify 'weighted choices list' do 22 | tree = Calyx::Rule.build_ast([["One", 80], ["Two", 20]], @registry) 23 | expect(tree).to be_a(Calyx::Syntax::WeightedChoices) 24 | expect(tree.size).to eq(2) 25 | end 26 | 27 | specify 'weighted choices hash' do 28 | tree = Calyx::Rule.build_ast({"One" => 80, "Two" => 20}, @registry) 29 | expect(tree).to be_a(Calyx::Syntax::WeightedChoices) 30 | expect(tree.size).to eq(2) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'calyx' 4 | -------------------------------------------------------------------------------- /spec/syntax/choices_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Syntax::Choices do 4 | specify 'construct choices from strings' do 5 | rule = Calyx::Syntax::Choices.parse(['atom', 'atom', 'atom'], Calyx::Registry.new) 6 | expect(rule.evaluate(Calyx::Options.new)).to eq([:choice, [:concat, [[:atom, 'atom']]]]) 7 | end 8 | 9 | specify 'construct choices from numbers' do 10 | rule = Calyx::Syntax::Choices.parse([123, 123, 123], Calyx::Registry.new) 11 | expect(rule.evaluate(Calyx::Options.new)).to eq([:choice, [:atom, '123']]) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/syntax/concat_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Syntax::Concat do 2 | it 'treats input with no delimiters as a concatenated production with a single atom' do 3 | registry = double('registry') 4 | concat = Calyx::Syntax::Concat.parse('one two three', registry) 5 | expect(concat.evaluate(Calyx::Options.new)).to eq([:concat, [[:atom, 'one two three']]]) 6 | end 7 | 8 | it 'treats input with delimiters as a concatenated production with an expansion' do 9 | registry = double('registry') 10 | allow(registry).to receive(:expand).and_return(Calyx::Syntax::Terminal.new('ONE')) 11 | concat = Calyx::Syntax::Concat.parse('{one} two three', registry) 12 | expect(concat.evaluate(Calyx::Options.new)).to eq([:concat, [[:atom, ''], [:one, [:atom, 'ONE']], [:atom, ' two three']]]) 13 | end 14 | 15 | # it 'treats input with chained expression in delimiters as a concatenated production' do 16 | # registry = double('registry') 17 | # concat = Calyx::Syntax::Concat.parse('{one} two three', registry) 18 | # expect(concat.evaluate(Calyx::Options.new)).to eq([:concat, [[:atom, ''], [:one, [:atom, 'ONE']], [:atom, ' two three']]]) 19 | # end 20 | end 21 | -------------------------------------------------------------------------------- /spec/syntax/expression_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Syntax::Modifier do 4 | specify "#filter" do 5 | modifier = Calyx::Syntax::Modifier.filter(:lower) 6 | expect(modifier.type).to eq(:filter) 7 | expect(modifier.name).to eq(:lower) 8 | end 9 | 10 | specify "#map_left" do 11 | modifier = Calyx::Syntax::Modifier.map_left(:pronoun) 12 | expect(modifier.type).to eq(:map) 13 | expect(modifier.name).to eq(:pronoun) 14 | expect(modifier.map_dir).to eq(:left) 15 | end 16 | 17 | specify "#map_right" do 18 | modifier = Calyx::Syntax::Modifier.map_right(:pronoun) 19 | expect(modifier.type).to eq(:map) 20 | expect(modifier.name).to eq(:pronoun) 21 | expect(modifier.map_dir).to eq(:right) 22 | end 23 | end 24 | 25 | describe Calyx::Syntax::Expression do 26 | describe "parser" do 27 | # let(:registry) do 28 | # registry = double(:registry) 29 | # allow(registry).to receive(:expand).with(:one).and_return("tahi") 30 | # allow(registry).to receive(:memoize_expansion).with(:two).and_return("rua") 31 | # allow(registry).to receive(:unique_expansion).with(:three).and_return("toru") 32 | # registry 33 | # end 34 | 35 | let(:options) do 36 | Calyx::Options.new 37 | end 38 | 39 | specify "non terminal expansion" do 40 | tahi = double(:tahi) 41 | allow(tahi).to receive(:evaluate).with(options).and_return("tahi") 42 | registry = double(:registry) 43 | allow(registry).to receive(:expand).with(:one).and_return(tahi) 44 | 45 | expr = Calyx::Syntax::Expression.parse("one", registry) 46 | expect(expr).to be_a(Calyx::Syntax::NonTerminal) 47 | expect(expr.evaluate(options)).to eq([:one, 'tahi']) 48 | end 49 | 50 | specify "memoized expansion" do 51 | registry = double(:registry) 52 | expect(registry).to receive(:memoize_expansion).with(:two).and_return("rua") 53 | 54 | expr = Calyx::Syntax::Expression.parse("@two", registry) 55 | expect(expr).to be_a(Calyx::Syntax::Memo) 56 | expect(expr.evaluate(options)).to eq([:two, 'rua']) 57 | end 58 | 59 | specify "unique expansion" do 60 | registry = double(:registry) 61 | allow(registry).to receive(:unique_expansion).with(:three).and_return("toru") 62 | 63 | expr = Calyx::Syntax::Expression.parse("$three", registry) 64 | expect(expr).to be_a(Calyx::Syntax::Unique) 65 | expect(expr.evaluate(options)).to eq([:three, 'toru']) 66 | end 67 | end 68 | end 69 | 70 | describe Calyx::Syntax::ExpressionChain do 71 | let(:atom) do 72 | atom = double(:node) 73 | allow(atom).to receive(:evaluate).and_return([:atom, 'HELLO']) 74 | atom 75 | end 76 | 77 | let(:registry) do 78 | registry = double(:registry) 79 | allow(registry).to receive(:expand).with(:lookup).and_return(atom) 80 | allow(registry).to receive(:expand_filter).with(:lower, 'HELLO').and_return('hello') 81 | registry 82 | end 83 | 84 | let(:lower_modifier) do 85 | modifier = double(:lower_modifier) 86 | allow(modifier).to receive(:type).and_return(:filter) 87 | allow(modifier).to receive(:name).and_return(:lower) 88 | modifier 89 | end 90 | 91 | let(:options) do 92 | Calyx::Options.new 93 | end 94 | 95 | describe "parser" do 96 | it "constructs a modifier chain from expression syntax" do 97 | chain = Calyx::Syntax::ExpressionChain.parse("lookup", [".", "lower"], registry) 98 | expect(chain.evaluate(options)).to eq([:expression, "hello"]) 99 | end 100 | end 101 | 102 | describe "instance" do 103 | it "constructs a modifier chain from initialized productions" do 104 | chain = Calyx::Syntax::ExpressionChain.new(atom, [lower_modifier], registry) 105 | expect(chain.evaluate(options)).to eq([:expression, "hello"]) 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/syntax/memo_spec.rb: -------------------------------------------------------------------------------- 1 | describe Calyx::Syntax::Memo do 2 | it 'uses the registry to memoize expansions' do 3 | registry = double('registry') 4 | allow(registry).to receive(:memoize_expansion).with(:one).and_return('ONE') 5 | memo = Calyx::Syntax::Memo.new(:@one, registry) 6 | expect(memo.evaluate(Calyx::Options.new)).to eq([:one, 'ONE']) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/syntax/non_terminal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Syntax::NonTerminal do 4 | specify 'construct non-terminal production rule' do 5 | registry = double('registry') 6 | expect(registry).to receive(:expand).and_return(Calyx::Syntax::Terminal.new(:atom)) 7 | rule = Calyx::Syntax::NonTerminal.new(:statement, registry) 8 | rule.evaluate(Calyx::Options.new) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/syntax/terminal_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Syntax::Terminal do 4 | specify 'construct terminal production rule' do 5 | atom = Calyx::Syntax::Terminal.new(:terminal) 6 | expect(atom.evaluate(Calyx::Options.new)).to eq([:atom, :terminal]) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/syntax/weighted_choices_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Calyx::Syntax::WeightedChoices do 4 | let(:options) do 5 | Calyx::Options.new(seed: 2) 6 | end 7 | 8 | specify 'construct choices from array of floats' do 9 | rule = Calyx::Syntax::WeightedChoices.parse([['atom', 0.5], ['molecule', 0.5]], Calyx::Registry.new) 10 | expect(rule.evaluate(options)).to eq([:weighted_choice, [:concat, [[:atom, 'atom']]]]) 11 | end 12 | 13 | specify 'construct choices from hash of floats' do 14 | rule = Calyx::Syntax::WeightedChoices.parse({'atom' => 0.4, 'molecule' => 0.6}, Calyx::Registry.new) 15 | expect(rule.evaluate(options)).to eq([:weighted_choice, [:concat, [[:atom, 'atom']]]]) 16 | end 17 | 18 | specify 'construct choices from hash of fixnums' do 19 | rule = Calyx::Syntax::WeightedChoices.parse({'atom' => 1, 'molecule' => 4}, Calyx::Registry.new) 20 | expect(rule.evaluate(options)).to eq([:weighted_choice, [:concat, [[:atom, 'atom']]]]) 21 | end 22 | end 23 | --------------------------------------------------------------------------------