npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

story-grammar

v1.2.3

Published

A combinatorial grammar for narrative-based projects

Readme

Story Grammar

CI Security Audit npm version License: MIT

A combinatorial grammar for generative and narrative-based projects.

This project is heavily inspired by the great work Kate Compton did and continues to do with Tracery.

Table of Contents

Interactive Examples

Visit the docs/ folder for interactive examples demonstrating the Story Grammar library with the new modular modifier API:

  • Tarot Three-Card Spread - Generate mystical three-card tarot readings with Past • Present • Future spreads using Story Grammar's combinatorial rules. Features a complete 78-card tarot deck, dynamic card combinations and interpretations, and 474,552 possible combinations.

  • Weapon Loot Table Generator - Generate RPG weapon loot with authentic rarity distribution using Story Grammar's weighted rules system. Includes 17 weapon types, realistic drop rates (Common 38.06%, Magic 50%, Rare 10.44%, Unique 1.5%), dynamic stats and special effects by rarity, and a color-coded rarity system with visual effects.

Overview

The Story Grammar Parser allows you to create complex, dynamic text generation systems using a simple key-value grammar with variable substitution.

Key Features

Features

  • Simple Grammar Definition: Define rules using key-value pairs
  • Variable Expansion: Use %variable% syntax for rule expansion
  • Nested Variables: Support for deeply nested rule references
  • Function Rules: Dynamic rule generation using JavaScript functions
  • Weighted Rules: Probability-based selection with custom weights
  • Conditional Rules: Context-aware selection based on previous values
  • Sequential Rules: Ordered cycling through values with reset capability
  • Range Rules: Numeric range generation (integers and floats)
  • Template Rules: Structured multi-variable combinations
  • Reference Rules: Reuse previously generated values for consistency
  • Seeded Randomness: Deterministic results for testing and reproducibility
  • Modifier System: Apply text transformations during generation
  • Circular Reference Detection: Automatic validation to prevent infinite loops
  • TypeScript Support: Full type definitions included
  • Complexity Analysis: Calculate the generative potential of rule collections
  • Probability Analysis: Determine the likelihood of different outcomes
  • Zero Dependencies: Pure TypeScript implementation

Quick Start

import { Parser } from 'story-grammar';

const parser = new Parser();

// Define simple rules
parser.addRule('flowers', ['roses', 'daisies', 'tulips']);
parser.addRule('colors', ['red', 'blue', 'yellow']);

// Define complex rules with variables
parser.addRule('colored_flowers', ['%colors% %flowers%']);

// Generate text
const result = parser.parse('I see beautiful %colored_flowers% in the garden.');
console.log(result); // "I see beautiful red roses in the garden."

TypeScript Usage

Story Grammar includes full TypeScript support with comprehensive type definitions. All interfaces are exported for type-safe development:

import { 
  Parser, 
  Grammar, 
  FunctionRule, 
  ConditionalRule, 
  Modifier,
  ParserStats
} from 'story-grammar';

// Type-safe grammar definition
const grammar: Grammar = {
  protagonist: ['brave knight', 'clever wizard'],
  action: ['rescued', 'discovered'],
  treasure: ['ancient scroll', 'magical sword']
};

// Function rules with proper typing
const dynamicRule: FunctionRule = () => {
  return ['dynamically generated value'];
};

// Conditional rules with typed context
const contextualRule: ConditionalRule = {
  conditions: [
    {
      if: (context: { [key: string]: string }) => context.mood === 'happy',
      then: ['Great!', 'Wonderful!']
    },
    {
      default: ['Okay', 'Sure']
    }
  ]
};

// Custom modifiers with type safety
const customModifier: Modifier = {
  name: 'custom',
  condition: (text: string) => text.includes('test'),
  transform: (text: string) => text.toUpperCase(),
  priority: 5
};

const parser = new Parser();
parser.addRules(grammar);
parser.addFunctionRule('dynamic', dynamicRule);
parser.addConditionalRule('contextual', contextualRule);
parser.addModifier(customModifier);

// Type-safe parsing and statistics
const result: string = parser.parse('%protagonist% %action% %treasure%');
const stats: ParserStats = parser.getStats();

See typescript-usage-example.ts for a complete working example.

Examples

Basic Usage

import { Parser } from 'story-grammar';

const parser = new Parser();

parser.addRule('flowers', ['roses', 'daisies', 'tulips']);
parser.addRule('colors', ['red', 'blue', 'pink']);

const text = 'I see a random %colors% %flowers%.';
console.log(parser.parse(text));
// Output: "I see a random blue roses." (randomized)

Complex Nested Variables

### Nested Variables

Variables can reference other variables:

```typescript
parser.addRule('greeting', ['Hello %name%!', 'Hi there %name%!']);
parser.addRule('name', ['Alice', 'Bob', 'Charlie']);
parser.addRule('farewell', ['Goodbye %name%', 'See you later %name%']);

console.log(parser.parse('%greeting% %farewell%'));
// Output: "Hello Alice! See you later Bob"

Function Rules

Create dynamic rules that generate values at runtime:

// Add a function rule that returns random numbers
parser.addFunctionRule('randomNumber', () => {
    const num = Math.floor(Math.random() * 100) + 1;
    return [num.toString()];
});

// Add a function rule for dice rolls
parser.addFunctionRule('diceRoll', () => {
    const roll = Math.floor(Math.random() * 20) + 1;
    return [`${roll} (d20)`];
});

// Add a function rule for current time
parser.addFunctionRule('timestamp', () => {
    return [new Date().toLocaleTimeString()];
});

console.log(parser.parse('Player rolls %diceRoll% at %timestamp%'));
// Output: "Player rolls 15 (d20) at 3:45:21 PM"

console.log(parser.parse('Random encounter strength: %randomNumber%'));
// Output: "Random encounter strength: 73"

Function rules are perfect for:

  • Random numbers and dice rolls
  • Current date/time values
  • Dynamic calculations
  • External API data (when used with async patterns)
  • Any content that changes each time it's generated

Weighted Rules

Create rules where some values are more likely than others using probability weights:

// Equal probability (default behavior)
parser.addRule('color', ['red', 'green', 'blue']); // Each has 33.33% chance

// Weighted probability - weights must sum to 1.0
parser.addWeightedRule('rarity', 
  ['common', 'uncommon', 'rare', 'epic', 'legendary'], 
  [0.50, 0.30, 0.15, 0.04, 0.01]
);

// More realistic treasure distribution
parser.addWeightedRule('treasure', 
  ['coins', 'jewelry', 'weapon', 'armor', 'artifact'], 
  [0.40, 0.25, 0.20, 0.10, 0.05]
);

console.log(parser.parse('You found %rarity% %treasure%!'));
// Output: "You found common coins!" (most likely)
// Output: "You found legendary artifact!" (very rare - 0.05% chance)

Weighted rules are ideal for:

  • Realistic item rarity in games (common items more frequent than legendary)
  • Weather patterns (sunny days more common than storms)
  • Character traits (normal attributes more common than exceptional ones)
  • Any scenario where natural distribution isn't uniform

Conditional Rules

Create context-aware rules that select values based on previously generated content:

parser.addRule('character_type', ['warrior', 'mage', 'rogue']);
parser.addConditionalRule('weapon', {
  conditions: [
    {
      if: (context) => context.character_type === 'warrior',
      then: ['sword', 'axe', 'hammer']
    },
    {
      if: (context) => context.character_type === 'mage', 
      then: ['staff', 'wand', 'orb']
    },
    {
      default: ['dagger', 'bow'] // Fallback for any other case
    }
  ]
});

console.log(parser.parse('A %character_type% wielding a %weapon%'));
// Output: "A warrior wielding a sword" (weapon matches character type)

Sequential Rules

Generate values in a specific order, with optional cycling:

// Cycling sequence (repeats after end)
parser.addSequentialRule('day', ['Monday', 'Tuesday', 'Wednesday'], { cycle: true });

// Non-cycling sequence (stops at last value)  
parser.addSequentialRule('countdown', ['3', '2', '1', 'GO!'], { cycle: false });

console.log(parser.parse('%day%')); // Monday
console.log(parser.parse('%day%')); // Tuesday
console.log(parser.parse('%day%')); // Wednesday
console.log(parser.parse('%day%')); // Monday (cycles back)

// Reset a sequential rule to start over
parser.resetSequentialRule('countdown');

Range Rules

Generate numeric values within specified ranges:

// Integer ranges
parser.addRangeRule('age', { min: 18, max: 65, type: 'integer' });

// Float ranges with custom steps
parser.addRangeRule('height', { min: 5.0, max: 6.5, step: 0.1, type: 'float' });

// Percentage scores
parser.addRangeRule('score', { min: 0, max: 100, type: 'integer' });

console.log(parser.parse('Character: age %age%, height %height%ft, score %score%'));
// Output: "Character: age 34, height 5.7ft, score 87"

Template Rules

Create structured combinations with their own variable sets:

parser.addTemplateRule('npc', {
  template: '%name% the %adjective% %profession%',
  variables: {
    name: ['Aldric', 'Brina', 'Caius'],
    adjective: ['brave', 'wise', 'cunning'],
    profession: ['knight', 'merchant', 'scholar']
  }
});

console.log(parser.parse('Meet %npc%'));
// Output: "Meet Brina the cunning merchant"

Reference Rules

Reuse previously generated values for consistency:

parser.addRule('hero', ['Alice', 'Bob', 'Charlie']);
parser.addRule('quest', ['rescue the princess', 'slay the dragon']);

// Use @ prefix to reference previously generated values
const story = parser.parse(
  '%hero% begins to %quest%. Later, %@hero% succeeds and %@hero% becomes legendary.',
  true  // preserveContext = true
);
// Output: "Alice begins to slay the dragon. Later, Alice succeeds and Alice becomes legendary."

Advanced Rule Combinations:

All rule types can work together seamlessly:

parser.addConditionalRule('spell_power', {
  conditions: [
    { if: (ctx) => ctx.character_type === 'mage', then: ['devastating', 'reality-bending'] },
    { default: ['weak', 'fizzling'] }
  ]
});

parser.parse('%character_type% %@character_type% casts a %spell_power% spell', true);
// Output: "mage mage casts a devastating spell" (consistent character, appropriate power)

Seeded Randomness

For testing and reproducible results, you can seed the random number generator:

const parser = new Parser();
parser.addRule('character', ['Alice', 'Bob', 'Charlie']);
parser.addWeightedRule('rarity', ['common', 'rare'], [0.8, 0.2]);

// Set a seed for deterministic results
parser.setRandomSeed(12345);

console.log(parser.parse('%character% finds %rarity% treasure'));
// Will always produce the same result with the same seed

// Generate multiple consistent results
for (let i = 0; i < 3; i++) {
  console.log(parser.parse('%character% finds %rarity% treasure'));
}

// Reset to same seed to reproduce the exact same sequence
parser.setRandomSeed(12345);
console.log(parser.parse('%character% finds %rarity% treasure')); // Same as first result

// Clear seed to return to true randomness
parser.clearRandomSeed();
console.log(parser.parse('%character% finds %rarity% treasure')); // Random again

Note: Seeded randomness affects the parser's internal random selection for static and weighted rules. Function rules that use Math.random() internally will remain random unless you implement seeding within your functions.

Seeded randomness is perfect for:

  • Unit testing with predictable outcomes
  • Debugging complex grammar combinations
  • Generating reproducible procedural content
  • Creating consistent examples for documentation

Story Generation

parser.addRules({
  characters: ['princess', 'knight', 'dragon', 'wizard'],
  locations: ['castle', 'forest', 'mountain', 'village'],
  actions: ['discovered', 'protected', 'explored', 'enchanted'],
  objects: ['treasure', 'magic sword', 'ancient book', 'crystal'],
  
  story_elements: ['%characters%', '%locations%', '%objects%'],
  story_template: [
    'The %characters% %actions% a %objects% in the %locations%.',
    'Once upon a time, a %characters% lived in a %locations%.',
    'A brave %characters% went to the %locations% seeking %objects%.'
  ]
});

// Generate multiple story variations
for (let i = 0; i < 3; i++) {
  console.log(parser.parse('%story_template%'));
}

Error Handling

// Handle undefined variables gracefully
const result = parser.parse('Unknown %variable% stays unchanged');
console.log(result); // "Unknown %variable% stays unchanged"

// Prevent infinite recursion
parser.addRule('infinite', ['%infinite% loop']);
try {
  parser.parse('This is %infinite%');
} catch (error) {
  console.log(error.message); // "Maximum recursion depth exceeded..."
}

// Validate grammar
const validation = parser.validate();
if (!validation.isValid) {
  console.log('Missing rules:', validation.missingRules);
  console.log('Circular references:', validation.circularReferences);
}

Complexity Analysis

Analyze the generative potential of your grammar rules:

// Simple complexity calculation
parser.addRule('colors', ['red', 'blue', 'green']);
parser.addRule('animals', ['cat', 'dog']);
parser.addRule('description', ['The %colors% %animals%']);

// Calculate complexity for a specific rule
const ruleComplexity = parser.calculateRuleComplexity('description');
console.log(ruleComplexity.complexity); // 6 (3 colors × 2 animals)
console.log(ruleComplexity.variables); // ['colors', 'animals']
console.log(ruleComplexity.ruleType); // 'static'

// Calculate total complexity across all rules
const totalComplexity = parser.calculateTotalComplexity();
console.log(totalComplexity.totalComplexity); // 11 (3 + 2 + 6)
console.log(totalComplexity.averageComplexity); // 3.67
console.log(totalComplexity.mostComplexRules[0].ruleName); // 'description'

Complexity Features:

  • Individual Rule Analysis: Calculate how many possible outcomes a single rule can produce
  • Total Grammar Analysis: Get comprehensive statistics about your entire grammar
  • Circular Reference Detection: Identifies and handles circular dependencies gracefully
  • Infinite Complexity Detection: Detects function rules and other infinite-complexity scenarios
  • Detailed Warnings: Provides insights about missing rules, depth limits, and potential issues

Rule Type Complexities:

  • Static Rules: Sum of all value possibilities (accounting for nested variables)
  • Weighted Rules: Same as static (weights don't affect possibility count)
  • Range Rules: (max - min) / step + 1 possible values
  • Template Rules: Product of all template variable possibilities
  • Sequential Rules: Number of values in the sequence
  • Conditional Rules: Sum of possibilities across all conditions
  • Function Rules: Marked as infinite complexity
// Complex nested example
parser.addRule('adjectives', ['big', 'small']);
parser.addRule('materials', ['wooden', 'metal', 'glass']);
parser.addRule('objects', ['%adjectives% %materials% box']);
parser.addRangeRule('quantity', { min: 1, max: 5, type: 'integer' });
parser.addTemplateRule('inventory', {
  template: '%quantity% %objects%',
  variables: {
    // uses external rules for objects and quantity
  }
});

const analysis = parser.calculateTotalComplexity();
console.log(`Total possible combinations: ${analysis.totalComplexity}`);
// Outputs combinations across all interconnected rules

// Detect potential issues
if (analysis.warnings.length > 0) {
  console.log('Warnings:', analysis.warnings);
}
if (analysis.circularReferences.length > 0) {
  console.log('Circular references found:', analysis.circularReferences);
}

Probability Analysis

Analyze the probability distribution of your grammar rules to understand outcome likelihood:

// Basic probability analysis
parser.addWeightedRule('rarity', ['common', 'rare', 'legendary'], [0.7, 0.2, 0.1]);
parser.addRule('items', ['sword', 'shield']);
parser.addRule('loot', ['%rarity% %items%']);

// Calculate probability distribution for a rule
const analysis = parser.calculateProbabilities('loot');
console.log(`Total possible outcomes: ${analysis.totalOutcomes}`); // 6
console.log(`Entropy (randomness): ${analysis.entropy.toFixed(2)}`); // Measure of uncertainty

// Most probable outcomes
console.log('Most likely outcomes:');
analysis.mostProbable.forEach(outcome => {
  console.log(`${outcome.outcome}: ${(outcome.probability * 100).toFixed(1)}%`);
});
// Output:
// common sword: 35.0%
// common shield: 35.0%
// rare sword: 10.0%
// rare shield: 10.0%
// legendary sword: 5.0%

// Least probable outcomes
console.log('Rarest outcomes:');
analysis.leastProbable.forEach(outcome => {
  console.log(`${outcome.outcome}: ${(outcome.probability * 100).toFixed(1)}%`);
});

Quick Access Methods:

// Get single most/least probable outcomes
const mostProbable = parser.getMostProbableOutcome('loot');
console.log(`Most likely: ${mostProbable.outcome} (${(mostProbable.probability * 100).toFixed(1)}%)`);

const leastProbable = parser.getLeastProbableOutcome('loot');
console.log(`Rarest: ${leastProbable.outcome} (${(leastProbable.probability * 100).toFixed(1)}%)`);

Probability Features:

  • Weighted Rule Analysis: Respects probability weights from weighted rules
  • Nested Probability Calculation: Handles complex nested variable dependencies
  • Entropy Calculation: Measures the randomness/uncertainty of outcomes
  • Probability Trees: Shows the probability chain for complex expansions
  • Multiple Rule Type Support: Works with all rule types (static, weighted, range, template, etc.)

Rule Type Probabilities:

  • Static Rules: Equal probability (1/n) for each value
  • Weighted Rules: Uses specified probability weights
  • Range Rules: Uniform distribution across the range
  • Template Rules: Product of component variable probabilities
  • Conditional Rules: Assumes equal probability for each condition
  • Function Rules: Marked as dynamic (cannot calculate exact probabilities)

Advanced Probability Analysis:

// Complex nested probability analysis
parser.addRule('adjectives', ['big', 'small']);
parser.addWeightedRule('colors', ['red', 'blue'], [0.7, 0.3]);
parser.addRule('objects', ['%adjectives% %colors% box']);

const objectAnalysis = parser.calculateProbabilities('objects');

// Check specific outcome probabilities
const bigRedBox = objectAnalysis.outcomes.find(o => o.outcome === 'big red box');
console.log(`Big red box probability: ${(bigRedBox.probability * 100).toFixed(1)}%`); // 35.0%

// Analyze probability distribution
if (objectAnalysis.entropy > 1.5) {
  console.log('High randomness - outcomes are fairly distributed');
} else {
  console.log('Low randomness - some outcomes are much more likely');
}

// Examine probability trees for complex rules
objectAnalysis.outcomes.forEach(outcome => {
  console.log(`${outcome.outcome}:`);
  outcome.probabilityTree.forEach(node => {
    console.log(`  ${node.ruleName}: ${node.value} (${node.probability})`);
  });
});

Performance Considerations:

// Control analysis scope for large grammars
const limitedAnalysis = parser.calculateProbabilities('complexRule', 
  50,    // maxDepth: prevent deep recursion
  1000   // maxOutcomes: limit total outcomes calculated
);

if (limitedAnalysis.warnings.length > 0) {
  console.log('Analysis warnings:', limitedAnalysis.warnings);
}

Built-in Modifiers

The parser uses a modular modifier system that allows loading language-specific modifiers as needed:

Loading Individual Modifiers

import { Parser, EnglishArticleModifier, EnglishPluralizationModifier } from 'story-grammar';

const parser = new Parser();

// Load specific modifiers
parser.loadModifier(EnglishArticleModifier);
parser.loadModifier(EnglishPluralizationModifier);

// Automatically corrects "a" to "an" before vowel sounds
parser.addRule('items', ['a elephant', 'a umbrella', 'a house']);
console.log(parser.parse('%items%')); 
// Outputs: "an elephant", "an umbrella", "a house"

// Automatically pluralizes nouns with quantity indicators
parser.addRule('animals', ['cat', 'dog', 'mouse', 'child']);
console.log(parser.parse('I saw many %animals%')); 
// Outputs: "I saw many cats", "I saw many dogs", "I saw many mice", "I saw many children"

Loading All English Modifiers

import { Parser, AllEnglishModifiers } from 'story-grammar';

const parser = new Parser();

// Load all English modifiers at once
parser.loadModifiers(AllEnglishModifiers);

// Now all English language features are available:
// - Article correction (a/an)
// - Pluralization (many cats)
// - Ordinals (1st, 2nd, 3rd)
// - Capitalization (sentence starts)
// - Possessives (John's car)
// - Verb agreement (he is, they are)
// - Punctuation cleanup

Basic English Modifiers

import { Parser, BasicEnglishModifiers } from 'story-grammar';

const parser = new Parser();

// Load only core modifiers for performance
parser.loadModifiers(BasicEnglishModifiers);
// Includes: articles, pluralization, ordinals

Namespace Structure (New in v1.0.5)

Story Grammar now organizes modifiers in a clean namespace structure for better organization and future multi-language support:

import { Parser, Modifiers } from 'story-grammar';

const parser = new Parser();

// Import from English namespace
parser.loadModifier(Modifiers.English.ArticleModifier);
parser.loadModifier(Modifiers.English.PluralizationModifier);
parser.loadModifier(Modifiers.English.CapitalizationModifier);

// Or load all English modifiers at once
parser.loadModifiers(Modifiers.English.AllEnglishModifiers);

// Individual imports are also available for convenience
parser.loadModifier(Modifiers.ArticleModifier);
parser.loadModifiers(Modifiers.AllEnglishModifiers);

Namespace Imports

// Import specific language namespace
import { English } from 'story-grammar/modifiers';

// Import all modifiers
import { Modifiers } from 'story-grammar';

// Future support for additional languages:
// import { Spanish, French } from 'story-grammar/modifiers';

Available English Modifiers in Namespace

  • Modifiers.English.ArticleModifier - Handles a/an article correction
  • Modifiers.English.PluralizationModifier - Pluralizes nouns with quantity words
  • Modifiers.English.OrdinalModifier - Converts numbers to ordinals (1st, 2nd, 3rd)
  • Modifiers.English.CapitalizationModifier - Capitalizes sentence starts
  • Modifiers.English.PossessiveModifier - Handles possessive forms (John's, cats')
  • Modifiers.English.VerbAgreementModifier - Basic subject-verb agreement
  • Modifiers.English.PunctuationCleanupModifier - Cleans up spacing around punctuation

Note: The original exports remain available for backward compatibility.

Modifier System

The Story Grammar parser includes a powerful modifier system that allows you to apply transformations to generated text after variable expansion.

Modifier Features

  • Conditional Application: Modifiers only apply when their condition is met
  • Priority System: Modifiers are applied in priority order (higher numbers first)
  • Built-in Article Modifier: Automatically handles English "a/an" articles
  • Built-in Pluralization Modifier: Automatically pluralizes nouns with quantity words
  • Built-in Ordinal Modifier: Converts cardinal numbers to ordinal format (1st, 2nd, 3rd, etc.)
  • Chainable: Multiple modifiers can be applied to the same text

Adding Custom Modifiers

parser.addModifier({
  name: 'emphasize',
  condition: (text: string) => text.includes('important'),
  transform: (text: string) => text.replace(/important/g, 'IMPORTANT'),
  priority: 5
});

Example with Multiple Modifiers

With the article modifier enabled:

Input:  "I found a %adjective% %noun%"
Rules:  adjective: ['old', 'ancient'], noun: ['apple', 'elephant']
Output: "I found an old apple" or "I found an ancient elephant"

Modifier Interface

interface Modifier {
  name: string;
  condition: ModifierFunction;
  transform: ModifierFunction;
  priority: number;
}

Management Methods

  • addModifier(modifier) - Add a new modifier
  • removeModifier(name) - Remove a modifier by name
  • clearModifiers() - Remove all modifiers
  • hasModifier(name) - Check if a modifier exists

Priority System

Modifiers with higher priority numbers are applied first. This allows you to control the order of transformations. For example:

  1. Priority 10: Article correction (a/an)
  2. Priority 9: Pluralization (many/several/three/etc.)
  3. Priority 8: Ordinal conversion (1st/2nd/3rd/etc.)
  4. Priority 7: Capitalization fixes
  5. Priority 1: Punctuation cleanup

This ensures that language-specific transformations (articles, plurals, ordinals) are handled before stylistic transformations.

Built-in Modifiers Reference

All English modifiers are available as separate imports and can be loaded individually or in groups:

import { 
  EnglishArticleModifier,
  EnglishPluralizationModifier,
  EnglishOrdinalModifier,
  EnglishCapitalizationModifier,
  EnglishPossessiveModifier,
  EnglishVerbAgreementModifier,
  PunctuationCleanupModifier,
  AllEnglishModifiers,
  BasicEnglishModifiers
} from 'story-grammar';

English Articles (EnglishArticleModifier)

  • Priority: 10
  • Function: Converts "a" to "an" before vowel sounds
  • Examples: "a elephant" → "an elephant", "a umbrella" → "an umbrella"

English Pluralization (EnglishPluralizationModifier)

  • Priority: 9
  • Function: Pluralizes nouns when quantity indicators are present
  • Triggers: "many", "several", "multiple", "few", numbers > 1, written numbers
  • Rules:
    • Regular: "cat" → "cats"
    • S/X/Z/CH/SH endings: "box" → "boxes"
    • Consonant+Y: "fly" → "flies"
    • F/FE endings: "leaf" → "leaves"
    • Irregular: "child" → "children", "mouse" → "mice"
  • Examples: "three cat" → "three cats", "many child" → "many children"

English Ordinals (EnglishOrdinalModifier)

  • Priority: 8
  • Function: Converts cardinal numbers to ordinal format
  • Triggers: Any standalone number (digits)
  • Rules:
    • Numbers ending in 1: "1" → "1st", "21" → "21st"
    • Numbers ending in 2: "2" → "2nd", "22" → "22nd"
    • Numbers ending in 3: "3" → "3rd", "33" → "33rd"
    • Exception - 11, 12, 13: "11" → "11th", "112" → "112th"
    • All others: "4" → "4th", "100" → "100th"
  • Examples: "1 place" → "1st place", "22 floor" → "22nd floor"

English Capitalization (EnglishCapitalizationModifier)

  • Priority: 7
  • Function: Capitalizes words after sentence-ending punctuation
  • Triggers: Lowercase letters following periods, exclamation marks, or question marks
  • Examples: "hello. world" → "hello. World", "what? yes!" → "what? Yes!"

English Possessives (EnglishPossessiveModifier)

  • Priority: 6
  • Function: Handles English possessive forms
  • Triggers: "possessive" marker and malformed possessives
  • Rules:
    • Regular nouns: "John possessive" → "John's"
    • Plural nouns: "boys possessive" → "boys'"
    • Fix doubles: "John's's" → "John's"
  • Examples: "cat possessive toy" → "cat's toy"

English Verb Agreement (EnglishVerbAgreementModifier)

  • Priority: 5
  • Function: Fixes basic subject-verb agreement
  • Triggers: Mismatched subjects and verbs (is/are, has/have)
  • Rules:
    • Singular subjects: "he are" → "he is", "she have" → "she has"
    • Plural/quantified subjects: "they is" → "they are", "many has" → "many have"
  • Examples: "he are happy" → "he is happy", "many is here" → "many are here"

Punctuation Cleanup (PunctuationCleanupModifier)

  • Priority: 1
  • Function: Fixes common punctuation and spacing issues
  • Triggers: Multiple spaces, incorrect punctuation spacing
  • Rules:
    • Multiple spaces → single space
    • Space before punctuation → removed
    • Missing space after punctuation → added
    • Trim leading/trailing whitespace
  • Examples: "hello , world" → "hello, world"

Performance and Utility Features

Batch Processing

Process multiple texts efficiently with shared context:

const texts = [
  'I saw a %animal%',
  'The %animal% was %color%',
  'It ran %direction%'
];

const results = parser.parseBatch(texts, true); // preserve context
// Results will use consistent values across all texts

Variation Generation

Generate multiple variations for testing or options:

// Generate 5 variations with consistent seed
const variations = parser.generateVariations('%greeting% %name%!', 5, 12345);
console.log(variations);
// ["Hello Alice!", "Hi Bob!", "Hey Charlie!", "Hello David!", "Hi Eve!"]

Performance Monitoring

Monitor parsing performance with detailed timing:

const result = parser.parseWithTiming('%complex_rule%');
console.log(`Total: ${result.timing.totalMs}ms`);
console.log(`Expansion: ${result.timing.expansionMs}ms`);
console.log(`Modifiers: ${result.timing.modifierMs}ms`);

Parser Analysis and Optimization

Analyze your parser for optimization opportunities:

// Get statistics
const stats = parser.getStats();
console.log(`Total rules: ${stats.totalRules}`);
console.log(`Rule breakdown:`, stats.rulesByType);

// Analyze complexity
const analysis = parser.analyzeRules();
console.log(`Most complex rules:`, analysis.mostComplex);
console.log(`Suggestions:`, analysis.suggestions);

// Get optimization recommendations
const optimization = parser.optimize();
if (!optimization.optimized) {
  console.log('Warnings:', optimization.warnings);
  console.log('Suggestions:', optimization.suggestions);
}

Enhanced Error Handling

Safe Parsing

Parse with automatic error recovery and detailed diagnostics:

const result = parser.safeParse('%potentially_problematic%', {
  validateFirst: true,    // Validate rules before parsing
  maxAttempts: 3,        // Retry with reduced complexity
  preserveContext: false
});

if (result.success) {
  console.log('Result:', result.result);
  console.log('Attempts needed:', result.attempts);
} else {
  console.log('Error:', result.error);
  if (result.validation) {
    console.log('Missing rules:', result.validation.missingRules);
  }
}

Rule Analysis

Analyze individual rules for complexity and issues:

// Analyze specific rule
const ruleAnalysis = parser.analyzeRules('complex_rule');
console.log('Complexity score:', ruleAnalysis.ruleDetails?.complexity);
console.log('Variables used:', ruleAnalysis.ruleDetails?.variables);
console.log('Nesting depth:', ruleAnalysis.ruleDetails?.depth);

Helpful Error Messages

Get detailed error explanations with actionable suggestions:

try {
  parser.parse('%problematic_rule%');
} catch (error) {
  const helpfulMessage = parser.getHelpfulError(error, {
    text: '%problematic_rule%',
    ruleName: 'problematic_rule'
  });
  
  console.log(helpfulMessage);
  // Includes suggestions, validation issues, and troubleshooting tips
}

Build and Deployment

TypeScript Build

Build the library for Node.js environments:

npm run build

This creates:

  • TypeScript declaration files in the types/ directory
  • JavaScript modules in the dist/ directory

The separated structure keeps type definitions cleanly organized from compiled code.

Webpack Bundle

Build the library for browser environments:

# Production build (minified)
npm run build:webpack

# Development build (unminified with source maps)
npm run build:webpack:dev

# Build both TypeScript and Webpack
npm run build:all

This creates:

  • dist/story-grammar.bundle.js - Production browser bundle (minified)
  • dist/story-grammar.dev.bundle.js - Development browser bundle
  • Source maps for debugging

Browser Usage

Include the webpack bundle in your HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Story Grammar Example</title>
</head>
<body>
    <script src="dist/story-grammar.bundle.js"></script>
    <script>
        // Create parser instance
        const parser = new StoryGrammar.Parser();
        
        // Add rules
        parser.addRule('colors', ['red', 'blue', 'green']);
        parser.addRule('animals', ['cat', 'dog', 'bird']);
        
        // Add modifiers
        parser.loadModifier(EnglishArticleModifier);
        parser.loadModifier(EnglishPluralizationModifier);
        
        // Generate text
        const story = parser.parse('I saw many %colors% %animals%');
        console.log(story); // "I saw many red cats"
    </script>
</body>
</html>

The library is exposed as StoryGrammar global object with the Parser class available as StoryGrammar.Parser.

API Reference

Parser Class

Static Rules

  • addRule(key: string, values: string[]) - Add a static rule with fixed values
  • addRules(rules: Grammar) - Add multiple static rules at once
  • removeRule(key: string): boolean - Remove any rule (static or function)
  • hasRule(key: string): boolean - Check if any rule exists (static or function)
  • clear() - Clear all rules (static and function)
  • getGrammar(): Grammar - Get copy of static rules only

Function Rule Methods

  • addFunctionRule(key: string, fn: FunctionRule): void - Add a dynamic function rule
  • removeFunctionRule(key: string): boolean - Remove a function rule
  • hasFunctionRule(key: string): boolean - Check if function rule exists
  • clearFunctionRules(): void - Clear all function rules

Weighted Rule Methods

  • addWeightedRule(key: string, values: string[], weights: number[]): void - Add a weighted probability rule
  • removeWeightedRule(key: string): boolean - Remove a weighted rule
  • hasWeightedRule(key: string): boolean - Check if weighted rule exists
  • clearWeightedRules(): void - Clear all weighted rules

Conditional Rule Methods

  • addConditionalRule(key: string, condition: ConditionalRule): void - Add a context-aware conditional rule
  • removeConditionalRule(key: string): boolean - Remove a conditional rule
  • hasConditionalRule(key: string): boolean - Check if conditional rule exists
  • clearConditionalRules(): void - Clear all conditional rules

Sequential Rule Methods

  • addSequentialRule(key: string, values: string[]): void - Add a sequential rule that cycles through values
  • resetSequentialRule(key: string): void - Reset sequential rule to first value
  • removeSequentialRule(key: string): boolean - Remove a sequential rule
  • hasSequentialRule(key: string): boolean - Check if sequential rule exists
  • clearSequentialRules(): void - Clear all sequential rules

Range Rule Methods

  • addRangeRule(key: string, min: number, max: number, isInteger?: boolean): void - Add a numeric range rule
  • removeRangeRule(key: string): boolean - Remove a range rule
  • hasRangeRule(key: string): boolean - Check if range rule exists
  • clearRangeRules(): void - Clear all range rules

Template Rule Methods

  • addTemplateRule(key: string, template: string, slots: string[]): void - Add a template rule with variable slots
  • removeTemplateRule(key: string): boolean - Remove a template rule
  • hasTemplateRule(key: string): boolean - Check if template rule exists
  • clearTemplateRules(): void - Clear all template rules

Reference Rule Methods

  • addReferenceRule(key: string, referenceKey: string): void - Add a rule that references previously generated values
  • removeReferenceRule(key: string): boolean - Remove a reference rule
  • hasReferenceRule(key: string): boolean - Check if reference rule exists
  • clearReferenceRules(): void - Clear all reference rules

Parsing

  • parse(text: string): string - Parse text and expand all variables
  • findVariables(text: string): string[] - Find all variables in text
  • validate(): ValidationResult - Validate grammar for missing rules and circular references

Modifiers

  • addModifier(modifier: Modifier): void - Add a text transformation modifier
  • removeModifier(name: string): boolean - Remove a modifier
  • hasModifier(name: string): boolean - Check if modifier exists
  • getModifiers(): Modifier[] - Get all modifiers sorted by priority
  • clearModifiers(): void - Clear all modifiers

Modifier Loading Methods

  • loadModifier(modifier: Modifier) - Load a single modifier
  • loadModifiers(modifiers: Modifier[]) - Load multiple modifiers

Available English Modifiers

All modifiers are available as separate imports:

import {
  EnglishArticleModifier,       // Fix a/an articles
  EnglishPluralizationModifier, // Handle English plurals  
  EnglishOrdinalModifier,       // Convert to ordinals (1st, 2nd)
  EnglishCapitalizationModifier,// Capitalize after sentences
  EnglishPossessiveModifier,    // Handle possessives ('s)
  EnglishVerbAgreementModifier, // Fix subject-verb agreement
  PunctuationCleanupModifier,   // Fix spacing/punctuation
  AllEnglishModifiers,          // All modifiers array
  BasicEnglishModifiers         // Core modifiers only
} from 'story-grammar';

Configuration

  • setMaxDepth(depth: number): void - Set maximum recursion depth (default: 100)
  • getMaxDepth(): number - Get current maximum recursion depth
  • setRandomSeed(seed: number): void - Set random seed for deterministic results
  • clearRandomSeed(): void - Clear random seed and return to Math.random()
  • getRandomSeed(): number | null - Get current random seed or null
  • clearAll(): void - Clear all rules and modifiers

Types

interface FunctionRule {
  (): string[];
}

interface WeightedRule {
  values: string[];
  weights: number[];
  cumulativeWeights: number[];
}

interface ConditionalRule {
  (context: Map<string, string>): string;
}

interface SequentialRule {
  values: string[];
  currentIndex: number;
}

interface RangeRule {
  min: number;
  max: number;
  isInteger: boolean;
}

interface TemplateRule {
  template: string;
  slots: string[];
}

interface ReferenceRule {
  referenceKey: string;
}

interface Grammar {
  [key: string]: string[];
}

interface Modifier {
  name: string;
  condition: (text: string, context?: ModifierContext) => boolean;
  transform: (text: string, context?: ModifierContext) => string;
  priority?: number;
}