├── .gitignore ├── README.md ├── package.json └── tracery.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tracery 2 | Tracery: a story-grammar generation library for javascript 3 | 4 | This is my attempt to package up [Tracery](https://github.com/galaxykate/tracery/) as a Node library. 5 | 6 | ## Installation 7 | 8 | This is hosted at npm, so it can be installed like so: 9 | 10 | ```bash 11 | $ npm install tracery-grammar --save 12 | ``` 13 | 14 | ## Example usage 15 | 16 | ```javascript 17 | var tracery = require('tracery-grammar'); 18 | 19 | var grammar = tracery.createGrammar({ 20 | 'animal': ['panda','fox','capybara','iguana'], 21 | 'emotion': ['sad','happy','angry','jealous'], 22 | 'origin':['I am #emotion.a# #animal#.'], 23 | }); 24 | 25 | grammar.addModifiers(tracery.baseEngModifiers); 26 | 27 | console.log(grammar.flatten('#origin#')); 28 | ``` 29 | 30 | Sample output: 31 | 32 | ```plaintext 33 | I am a happy iguana. 34 | I am an angry fox. 35 | I am a sad capybara. 36 | ``` 37 | 38 | ### Making Tracery deterministic 39 | 40 | By default, Tracery uses Math.random() to generate random numbers. If you need Tracery to be deterministic, you can make it use your own random number generator using: 41 | 42 | tracery.setRng(myRng); 43 | 44 | where myRng is a function that, [like Math.random()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random), returns a floating-point, pseudo-random number in the range [0, 1). 45 | 46 | By using a local random number generator that takes a seed and controlling this seed, you can make Tracery's behavior completely deterministic. 47 | 48 | 49 | ```javascript 50 | // Stable random number generator 51 | // Copied from this excellent answer on Stack Overflow: https://stackoverflow.com/a/47593316/3306 52 | function splitmix32(seed) { 53 | return function() { 54 | seed |= 0; // bitwise OR ensures this is treated as an integer internally for performance. 55 | seed = seed + 0x9e3779b9 | 0; // again, bitwise OR for performance 56 | let t = seed ^ seed >>> 16; 57 | t = Math.imul(t, 0x21f0aaad); 58 | t = t ^ t >>> 15; 59 | t = Math.imul(t, 0x735a2d97); 60 | return ((t = t ^ t >>> 15) >>> 0) / 4294967296; 61 | }; 62 | } 63 | 64 | var seed = 123456; 65 | tracery.setRng(splitmix32(seed)); 66 | 67 | console.log(grammar.flatten('#origin#')); 68 | ``` 69 | 70 | Deterministic output: 71 | 72 | ```plaintext 73 | I am an angry capybara. 74 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracery-grammar", 3 | "version": "2.7.4", 4 | "description": "npm package for Kate Compton's tracery", 5 | "main": "tracery.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/v21/tracery.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "author": "Kate Compton (packaged by v21)", 14 | "license": "ISC" 15 | } 16 | -------------------------------------------------------------------------------- /tracery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kate 3 | */ 4 | 5 | var tracery = function() { 6 | var rng = Math.random; 7 | 8 | var setRng = function setRng(newRng) { 9 | rng = newRng; 10 | } 11 | 12 | var TraceryNode = function(parent, childIndex, settings) { 13 | this.errors = []; 14 | 15 | // No input? Add an error, but continue anyways 16 | if (settings.raw === undefined) { 17 | this.errors.push("Empty input for node"); 18 | settings.raw = ""; 19 | } 20 | 21 | // If the root node of an expansion, it will have the grammar passed as the 'parent' 22 | // set the grammar from the 'parent', and set all other values for a root node 23 | if ( parent instanceof tracery.Grammar) { 24 | this.grammar = parent; 25 | this.parent = null; 26 | this.depth = 0; 27 | this.childIndex = 0; 28 | } else { 29 | this.grammar = parent.grammar; 30 | this.parent = parent; 31 | this.depth = parent.depth + 1; 32 | this.childIndex = childIndex; 33 | } 34 | 35 | this.raw = settings.raw; 36 | this.type = settings.type; 37 | this.isExpanded = false; 38 | 39 | if (!this.grammar) { 40 | this.errors.push("No grammar specified for this node " + this); 41 | } 42 | 43 | }; 44 | 45 | TraceryNode.prototype.toString = function() { 46 | return "Node('" + this.raw + "' " + this.type + " d:" + this.depth + ")"; 47 | }; 48 | 49 | // Expand the node (with the given child rule) 50 | // Make children if the node has any 51 | TraceryNode.prototype.expandChildren = function(childRule, preventRecursion) { 52 | this.children = []; 53 | this.finishedText = ""; 54 | 55 | // Set the rule for making children, 56 | // and expand it into section 57 | this.childRule = childRule; 58 | if (this.childRule !== undefined) { 59 | var sections = tracery.parse(childRule); 60 | 61 | // Add errors to this 62 | if (sections.errors.length > 0) { 63 | this.errors = this.errors.concat(sections.errors); 64 | 65 | } 66 | 67 | for (var i = 0; i < sections.length; i++) { 68 | this.children[i] = new TraceryNode(this, i, sections[i]); 69 | if (!preventRecursion) 70 | this.children[i].expand(preventRecursion); 71 | 72 | // Add in the finished text 73 | this.finishedText += this.children[i].finishedText; 74 | } 75 | } else { 76 | // In normal operation, this shouldn't ever happen 77 | this.errors.push("No child rule provided, can't expand children"); 78 | } 79 | }; 80 | 81 | // Expand this rule (possibly creating children) 82 | TraceryNode.prototype.expand = function(preventRecursion) { 83 | 84 | if (!this.isExpanded) { 85 | this.isExpanded = true; 86 | 87 | this.expansionErrors = []; 88 | 89 | // Types of nodes 90 | // -1: raw, needs parsing 91 | // 0: Plaintext 92 | // 1: Tag ("#symbol.mod.mod2.mod3#" or "#[pushTarget:pushRule]symbol.mod") 93 | // 2: Action ("[pushTarget:pushRule], [pushTarget:POP]", more in the future) 94 | 95 | switch(this.type) { 96 | // Raw rule 97 | case -1: 98 | 99 | this.expandChildren(this.raw, preventRecursion); 100 | break; 101 | 102 | // plaintext, do nothing but copy text into finsihed text 103 | case 0: 104 | this.finishedText = this.raw; 105 | break; 106 | 107 | // Tag 108 | case 1: 109 | // Parse to find any actions, and figure out what the symbol is 110 | this.preactions = []; 111 | this.postactions = []; 112 | 113 | var parsed = tracery.parseTag(this.raw); 114 | 115 | // Break into symbol actions and modifiers 116 | this.symbol = parsed.symbol; 117 | this.modifiers = parsed.modifiers; 118 | 119 | // Create all the preactions from the raw syntax 120 | for (var i = 0; i < parsed.preactions.length; i++) { 121 | this.preactions[i] = new NodeAction(this, parsed.preactions[i].raw); 122 | } 123 | for (var i = 0; i < parsed.postactions.length; i++) { 124 | // this.postactions[i] = new NodeAction(this, parsed.postactions[i].raw); 125 | } 126 | 127 | // Make undo actions for all preactions (pops for each push) 128 | for (var i = 0; i < this.preactions.length; i++) { 129 | if (this.preactions[i].type === 0) 130 | this.postactions.push(this.preactions[i].createUndo()); 131 | } 132 | 133 | // Activate all the preactions 134 | for (var i = 0; i < this.preactions.length; i++) { 135 | this.preactions[i].activate(); 136 | } 137 | 138 | this.finishedText = this.raw; 139 | 140 | // Expand (passing the node, this allows tracking of recursion depth) 141 | 142 | var selectedRule = this.grammar.selectRule(this.symbol, this, this.errors); 143 | 144 | this.expandChildren(selectedRule, preventRecursion); 145 | 146 | // Apply modifiers 147 | // TODO: Update parse function to not trigger on hashtags within parenthesis within tags, 148 | // so that modifier parameters can contain tags "#story.replace(#protagonist#, #newCharacter#)#" 149 | for (var i = 0; i < this.modifiers.length; i++) { 150 | var modName = this.modifiers[i]; 151 | var modParams = []; 152 | if (modName.indexOf("(") > 0) { 153 | var regExp = /\(([^)]+)\)/; 154 | 155 | // Todo: ignore any escaped commas. For now, commas always split 156 | var results = regExp.exec(this.modifiers[i]); 157 | if (!results || results.length < 2) { 158 | } else { 159 | var modParams = results[1].split(","); 160 | modName = this.modifiers[i].substring(0, modName.indexOf("(")); 161 | } 162 | 163 | } 164 | 165 | var mod = this.grammar.modifiers[modName]; 166 | 167 | // Missing modifier? 168 | if (!mod) { 169 | this.errors.push("Missing modifier " + modName); 170 | this.finishedText += "((." + modName + "))"; 171 | } else { 172 | this.finishedText = mod(this.finishedText, modParams); 173 | 174 | } 175 | 176 | } 177 | 178 | // Perform post-actions 179 | for (var i = 0; i < this.postactions.length; i++) { 180 | this.postactions[i].activate(); 181 | } 182 | break; 183 | case 2: 184 | 185 | // Just a bare action? Expand it! 186 | this.action = new NodeAction(this, this.raw); 187 | this.action.activate(); 188 | 189 | // No visible text for an action 190 | // TODO: some visible text for if there is a failure to perform the action? 191 | this.finishedText = ""; 192 | break; 193 | 194 | } 195 | 196 | } else { 197 | //console.warn("Already expanded " + this); 198 | } 199 | 200 | }; 201 | 202 | TraceryNode.prototype.clearEscapeChars = function() { 203 | 204 | this.finishedText = this.finishedText.replace(/\\\\/g, "DOUBLEBACKSLASH").replace(/\\/g, "").replace(/DOUBLEBACKSLASH/g, "\\"); 205 | }; 206 | 207 | // An action that occurs when a node is expanded 208 | // Types of actions: 209 | // 0 Push: [key:rule] 210 | // 1 Pop: [key:POP] 211 | // 2 function: [functionName(param0,param1)] (TODO!) 212 | function NodeAction(node, raw) { 213 | /* 214 | if (!node) 215 | console.warn("No node for NodeAction"); 216 | if (!raw) 217 | console.warn("No raw commands for NodeAction"); 218 | */ 219 | 220 | this.node = node; 221 | 222 | var sections = raw.split(":"); 223 | this.target = sections[0]; 224 | 225 | // No colon? A function! 226 | if (sections.length === 1) { 227 | this.type = 2; 228 | 229 | } 230 | 231 | // Colon? It's either a push or a pop 232 | else { 233 | this.rule = sections[1]; 234 | if (this.rule === "POP") { 235 | this.type = 1; 236 | } else { 237 | this.type = 0; 238 | } 239 | } 240 | } 241 | 242 | 243 | NodeAction.prototype.createUndo = function() { 244 | if (this.type === 0) { 245 | return new NodeAction(this.node, this.target + ":POP"); 246 | } 247 | // TODO Not sure how to make Undo actions for functions or POPs 248 | return null; 249 | }; 250 | 251 | NodeAction.prototype.activate = function() { 252 | var grammar = this.node.grammar; 253 | switch(this.type) { 254 | case 0: 255 | // split into sections (the way to denote an array of rules) 256 | this.ruleSections = this.rule.split(","); 257 | this.finishedRules = []; 258 | this.ruleNodes = []; 259 | for (var i = 0; i < this.ruleSections.length; i++) { 260 | var n = new TraceryNode(grammar, 0, { 261 | type : -1, 262 | raw : this.ruleSections[i] 263 | }); 264 | 265 | n.expand(); 266 | 267 | this.finishedRules.push(n.finishedText); 268 | } 269 | 270 | // TODO: escape commas properly 271 | grammar.pushRules(this.target, this.finishedRules, this); 272 | break; 273 | case 1: 274 | grammar.popRules(this.target); 275 | break; 276 | case 2: 277 | grammar.flatten(this.target, true); 278 | break; 279 | } 280 | 281 | }; 282 | 283 | NodeAction.prototype.toText = function() { 284 | switch(this.type) { 285 | case 0: 286 | return this.target + ":" + this.rule; 287 | case 1: 288 | return this.target + ":POP"; 289 | case 2: 290 | return "((some function))"; 291 | default: 292 | return "((Unknown Action))"; 293 | } 294 | }; 295 | 296 | // Sets of rules 297 | // Can also contain conditional or fallback sets of rulesets) 298 | function RuleSet(grammar, raw) { 299 | this.raw = raw; 300 | this.grammar = grammar; 301 | this.falloff = 1; 302 | 303 | if (Array.isArray(raw)) { 304 | this.defaultRules = raw; 305 | } else if ( typeof raw === 'string' || raw instanceof String) { 306 | this.defaultRules = [raw]; 307 | } else if (raw === 'object') { 308 | // TODO: support for conditional and hierarchical rule sets 309 | } 310 | 311 | }; 312 | 313 | RuleSet.prototype.selectRule = function(errors) { 314 | // console.log("Get rule", this.raw); 315 | // Is there a conditional? 316 | if (this.conditionalRule) { 317 | var value = this.grammar.expand(this.conditionalRule, true); 318 | // does this value match any of the conditionals? 319 | if (this.conditionalValues[value]) { 320 | var v = this.conditionalValues[value].selectRule(errors); 321 | if (v !== null && v !== undefined) 322 | return v; 323 | } 324 | // No returned value? 325 | } 326 | 327 | // Is there a ranked order? 328 | if (this.ranking) { 329 | for (var i = 0; i < this.ranking.length; i++) { 330 | var v = this.ranking.selectRule(); 331 | if (v !== null && v !== undefined) 332 | return v; 333 | } 334 | 335 | // Still no returned value? 336 | } 337 | 338 | if (this.defaultRules !== undefined) { 339 | var index = 0; 340 | // Select from this basic array of rules 341 | 342 | // Get the distribution from the grammar if there is no other 343 | var distribution = this.distribution; 344 | if (!distribution) 345 | distribution = this.grammar.distribution; 346 | 347 | switch(distribution) { 348 | case "shuffle": 349 | 350 | // create a shuffle desk 351 | if (!this.shuffledDeck || this.shuffledDeck.length === 0) { 352 | // make an array 353 | this.shuffledDeck = fyshuffle(Array.apply(null, { 354 | length : this.defaultRules.length 355 | }).map(Number.call, Number), this.falloff); 356 | 357 | } 358 | 359 | index = this.shuffledDeck.pop(); 360 | 361 | break; 362 | case "weighted": 363 | errors.push("Weighted distribution not yet implemented"); 364 | break; 365 | case "falloff": 366 | errors.push("Falloff distribution not yet implemented"); 367 | break; 368 | default: 369 | 370 | index = Math.floor(Math.pow(rng(), this.falloff) * this.defaultRules.length); 371 | break; 372 | } 373 | 374 | if (!this.defaultUses) 375 | this.defaultUses = []; 376 | this.defaultUses[index] = ++this.defaultUses[index] || 1; 377 | return this.defaultRules[index]; 378 | } 379 | 380 | errors.push("No default rules defined for " + this); 381 | return null; 382 | 383 | }; 384 | 385 | RuleSet.prototype.clearState = function() { 386 | 387 | if (this.defaultUses) { 388 | this.defaultUses = []; 389 | } 390 | }; 391 | 392 | function fyshuffle(array, falloff) { 393 | var currentIndex = array.length, 394 | temporaryValue, 395 | randomIndex; 396 | 397 | // While there remain elements to shuffle... 398 | while (0 !== currentIndex) { 399 | 400 | // Pick a remaining element... 401 | randomIndex = Math.floor(rng() * currentIndex); 402 | currentIndex -= 1; 403 | 404 | // And swap it with the current element. 405 | temporaryValue = array[currentIndex]; 406 | array[currentIndex] = array[randomIndex]; 407 | array[randomIndex] = temporaryValue; 408 | } 409 | 410 | return array; 411 | } 412 | 413 | var Symbol = function(grammar, key, rawRules) { 414 | // Symbols can be made with a single value, and array, or array of objects of (conditions/values) 415 | this.key = key; 416 | this.grammar = grammar; 417 | this.rawRules = rawRules; 418 | 419 | this.baseRules = new RuleSet(this.grammar, rawRules); 420 | this.clearState(); 421 | 422 | }; 423 | 424 | Symbol.prototype.clearState = function() { 425 | 426 | // Clear the stack and clear all ruleset usages 427 | this.stack = [this.baseRules]; 428 | 429 | this.uses = []; 430 | this.baseRules.clearState(); 431 | }; 432 | 433 | Symbol.prototype.pushRules = function(rawRules) { 434 | var rules = new RuleSet(this.grammar, rawRules); 435 | this.stack.push(rules); 436 | }; 437 | 438 | Symbol.prototype.popRules = function() { 439 | this.stack.pop(); 440 | }; 441 | 442 | Symbol.prototype.selectRule = function(node, errors) { 443 | this.uses.push({ 444 | node : node 445 | }); 446 | 447 | if (this.stack.length === 0) { 448 | errors.push("The rule stack for '" + this.key + "' is empty, too many pops?"); 449 | return "((" + this.key + "))"; 450 | } 451 | 452 | return this.stack[this.stack.length - 1].selectRule(); 453 | }; 454 | 455 | Symbol.prototype.getActiveRules = function() { 456 | if (this.stack.length === 0) { 457 | return null; 458 | } 459 | return this.stack[this.stack.length - 1].selectRule(); 460 | }; 461 | 462 | Symbol.prototype.rulesToJSON = function() { 463 | return JSON.stringify(this.rawRules); 464 | }; 465 | 466 | var Grammar = function(raw, settings) { 467 | this.modifiers = {}; 468 | this.loadFromRawObj(raw); 469 | }; 470 | 471 | Grammar.prototype.clearState = function() { 472 | var keys = Object.keys(this.symbols); 473 | for (var i = 0; i < keys.length; i++) { 474 | this.symbols[keys[i]].clearState(); 475 | } 476 | }; 477 | 478 | Grammar.prototype.addModifiers = function(mods) { 479 | 480 | // copy over the base modifiers 481 | for (var key in mods) { 482 | if (mods.hasOwnProperty(key)) { 483 | this.modifiers[key] = mods[key]; 484 | } 485 | }; 486 | 487 | }; 488 | 489 | Grammar.prototype.loadFromRawObj = function(raw) { 490 | 491 | this.raw = raw; 492 | this.symbols = {}; 493 | this.subgrammars = []; 494 | 495 | if (this.raw) { 496 | // Add all rules to the grammar 497 | for (var key in this.raw) { 498 | if (this.raw.hasOwnProperty(key)) { 499 | this.symbols[key] = new Symbol(this, key, this.raw[key]); 500 | } 501 | } 502 | } 503 | }; 504 | 505 | Grammar.prototype.createRoot = function(rule) { 506 | // Create a node and subnodes 507 | var root = new TraceryNode(this, 0, { 508 | type : -1, 509 | raw : rule, 510 | }); 511 | 512 | return root; 513 | }; 514 | 515 | Grammar.prototype.expand = function(rule, allowEscapeChars) { 516 | var root = this.createRoot(rule); 517 | root.expand(); 518 | if (!allowEscapeChars) 519 | root.clearEscapeChars(); 520 | 521 | return root; 522 | }; 523 | 524 | Grammar.prototype.flatten = function(rule, allowEscapeChars) { 525 | var root = this.expand(rule, allowEscapeChars); 526 | 527 | return root.finishedText; 528 | }; 529 | 530 | Grammar.prototype.toJSON = function() { 531 | var keys = Object.keys(this.symbols); 532 | var symbolJSON = []; 533 | for (var i = 0; i < keys.length; i++) { 534 | var key = keys[i]; 535 | symbolJSON.push(' "' + key + '" : ' + this.symbols[key].rulesToJSON()); 536 | } 537 | return "{\n" + symbolJSON.join(",\n") + "\n}"; 538 | }; 539 | 540 | // Create or push rules 541 | Grammar.prototype.pushRules = function(key, rawRules, sourceAction) { 542 | 543 | if (this.symbols[key] === undefined) { 544 | this.symbols[key] = new Symbol(this, key, rawRules); 545 | if (sourceAction) 546 | this.symbols[key].isDynamic = true; 547 | } else { 548 | this.symbols[key].pushRules(rawRules); 549 | } 550 | }; 551 | 552 | Grammar.prototype.popRules = function(key) { 553 | if (!this.symbols[key]) 554 | this.errors.push("Can't pop: no symbol for key " + key); 555 | this.symbols[key].popRules(); 556 | }; 557 | 558 | Grammar.prototype.selectRule = function(key, node, errors) { 559 | if (this.symbols[key]) { 560 | var rule = this.symbols[key].selectRule(node, errors); 561 | 562 | return rule; 563 | } 564 | 565 | // Failover to alternative subgrammars 566 | for (var i = 0; i < this.subgrammars.length; i++) { 567 | 568 | if (this.subgrammars[i].symbols[key]) 569 | return this.subgrammars[i].symbols[key].selectRule(); 570 | } 571 | 572 | // No symbol? 573 | errors.push("No symbol for '" + key + "'"); 574 | return "((" + key + "))"; 575 | }; 576 | 577 | // Parses a plaintext rule in the tracery syntax 578 | tracery = { 579 | 580 | createGrammar : function(raw) { 581 | return new Grammar(raw); 582 | }, 583 | 584 | // Parse the contents of a tag 585 | parseTag : function(tagContents) { 586 | 587 | var parsed = { 588 | symbol : undefined, 589 | preactions : [], 590 | postactions : [], 591 | modifiers : [] 592 | }; 593 | var sections = tracery.parse(tagContents); 594 | var symbolSection = undefined; 595 | for (var i = 0; i < sections.length; i++) { 596 | if (sections[i].type === 0) { 597 | if (symbolSection === undefined) { 598 | symbolSection = sections[i].raw; 599 | } else { 600 | throw ("multiple main sections in " + tagContents); 601 | } 602 | } else { 603 | parsed.preactions.push(sections[i]); 604 | } 605 | } 606 | 607 | if (symbolSection === undefined) { 608 | // throw ("no main section in " + tagContents); 609 | } else { 610 | var components = symbolSection.split("."); 611 | parsed.symbol = components[0]; 612 | parsed.modifiers = components.slice(1); 613 | } 614 | return parsed; 615 | }, 616 | 617 | parse : function(rule) { 618 | var depth = 0; 619 | var inTag = false; 620 | var sections = []; 621 | var escaped = false; 622 | 623 | var errors = []; 624 | var start = 0; 625 | 626 | var escapedSubstring = ""; 627 | var lastEscapedChar = undefined; 628 | 629 | if (rule === null) { 630 | var sections = []; 631 | sections.errors = errors; 632 | 633 | return sections; 634 | } 635 | 636 | function createSection(start, end, type) { 637 | if (end - start < 1) { 638 | if (type === 1) 639 | errors.push(start + ": empty tag"); 640 | if (type === 2) 641 | errors.push(start + ": empty action"); 642 | 643 | } 644 | var rawSubstring; 645 | if (lastEscapedChar !== undefined) { 646 | rawSubstring = escapedSubstring + "\\" + rule.substring(lastEscapedChar + 1, end); 647 | 648 | } else { 649 | rawSubstring = rule.substring(start, end); 650 | } 651 | sections.push({ 652 | type : type, 653 | raw : rawSubstring 654 | }); 655 | lastEscapedChar = undefined; 656 | escapedSubstring = ""; 657 | }; 658 | 659 | for (var i = 0; i < rule.length; i++) { 660 | 661 | if (!escaped) { 662 | var c = rule.charAt(i); 663 | 664 | switch(c) { 665 | 666 | // Enter a deeper bracketed section 667 | case '[': 668 | if (depth === 0 && !inTag) { 669 | if (start < i) 670 | createSection(start, i, 0); 671 | start = i + 1; 672 | } 673 | depth++; 674 | break; 675 | 676 | case ']': 677 | depth--; 678 | 679 | // End a bracketed section 680 | if (depth === 0 && !inTag) { 681 | createSection(start, i, 2); 682 | start = i + 1; 683 | } 684 | break; 685 | 686 | // Hashtag 687 | // ignore if not at depth 0, that means we are in a bracket 688 | case '#': 689 | if (depth === 0) { 690 | if (inTag) { 691 | createSection(start, i, 1); 692 | start = i + 1; 693 | } else { 694 | if (start < i) 695 | createSection(start, i, 0); 696 | start = i + 1; 697 | } 698 | inTag = !inTag; 699 | } 700 | break; 701 | 702 | case '\\': 703 | escaped = true; 704 | escapedSubstring = escapedSubstring + rule.substring(start, i); 705 | start = i + 1; 706 | lastEscapedChar = i; 707 | break; 708 | } 709 | } else { 710 | escaped = false; 711 | } 712 | } 713 | if (start < rule.length) 714 | createSection(start, rule.length, 0); 715 | 716 | if (inTag) { 717 | errors.push("Unclosed tag"); 718 | } 719 | if (depth > 0) { 720 | errors.push("Too many ["); 721 | } 722 | if (depth < 0) { 723 | errors.push("Too many ]"); 724 | } 725 | 726 | // Strip out empty plaintext sections 727 | 728 | sections = sections.filter(function(section) { 729 | if (section.type === 0 && section.raw.length === 0) 730 | return false; 731 | return true; 732 | }); 733 | sections.errors = errors; 734 | return sections; 735 | }, 736 | }; 737 | 738 | function isVowel(c) { 739 | var c2 = c.toLowerCase(); 740 | return (c2 === 'a') || (c2 === 'e') || (c2 === 'i') || (c2 === 'o') || (c2 === 'u'); 741 | }; 742 | 743 | function isAlphaNum(c) { 744 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); 745 | }; 746 | function escapeRegExp(str) { 747 | return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 748 | } 749 | 750 | var baseEngModifiers = { 751 | 752 | replace : function(s, params) { 753 | //http://stackoverflow.com/questions/1144783/replacing-all-occurrences-of-a-string-in-javascript 754 | return s.replace(new RegExp(escapeRegExp(params[0]), 'g'), params[1]); 755 | }, 756 | 757 | capitalizeAll : function(s) { 758 | var s2 = ""; 759 | var capNext = true; 760 | for (var i = 0; i < s.length; i++) { 761 | 762 | if (!isAlphaNum(s.charAt(i))) { 763 | capNext = true; 764 | s2 += s.charAt(i); 765 | } else { 766 | if (!capNext) { 767 | s2 += s.charAt(i); 768 | } else { 769 | s2 += s.charAt(i).toUpperCase(); 770 | capNext = false; 771 | } 772 | 773 | } 774 | } 775 | return s2; 776 | }, 777 | 778 | capitalize : function(s) { 779 | return s.charAt(0).toUpperCase() + s.substring(1); 780 | }, 781 | 782 | a : function(s) { 783 | if (s.length > 0) { 784 | if (s.charAt(0).toLowerCase() === 'u') { 785 | if (s.length > 2) { 786 | if (s.charAt(2).toLowerCase() === 'i') 787 | return "a " + s; 788 | } 789 | } 790 | 791 | if (isVowel(s.charAt(0))) { 792 | return "an " + s; 793 | } 794 | } 795 | 796 | return "a " + s; 797 | 798 | }, 799 | 800 | firstS : function(s) { 801 | console.log(s); 802 | var s2 = s.split(" "); 803 | 804 | var finished = baseEngModifiers.s(s2[0]) + " " + s2.slice(1).join(" "); 805 | console.log(finished); 806 | return finished; 807 | }, 808 | 809 | s : function(s) { 810 | switch (s.charAt(s.length -1)) { 811 | case 's': 812 | return s + "es"; 813 | break; 814 | case 'h': 815 | return s + "es"; 816 | break; 817 | case 'x': 818 | return s + "es"; 819 | break; 820 | case 'y': 821 | if (!isVowel(s.charAt(s.length - 2))) 822 | return s.substring(0, s.length - 1) + "ies"; 823 | else 824 | return s + "s"; 825 | break; 826 | default: 827 | return s + "s"; 828 | } 829 | }, 830 | ed : function(s) { 831 | switch (s.charAt(s.length -1)) { 832 | case 's': 833 | return s + "ed"; 834 | break; 835 | case 'e': 836 | return s + "d"; 837 | break; 838 | case 'h': 839 | return s + "ed"; 840 | break; 841 | case 'x': 842 | return s + "ed"; 843 | break; 844 | case 'y': 845 | if (!isVowel(s.charAt(s.length - 2))) 846 | return s.substring(0, s.length - 1) + "ied"; 847 | else 848 | return s + "d"; 849 | break; 850 | default: 851 | return s + "ed"; 852 | } 853 | } 854 | }; 855 | 856 | tracery.baseEngModifiers = baseEngModifiers; 857 | // Externalize 858 | tracery.TraceryNode = TraceryNode; 859 | 860 | tracery.Grammar = Grammar; 861 | tracery.Symbol = Symbol; 862 | tracery.RuleSet = RuleSet; 863 | 864 | tracery.setRng = setRng; 865 | 866 | return tracery; 867 | }(); 868 | 869 | module.exports = tracery; --------------------------------------------------------------------------------