A combinatorial grammar for narrative-based projects
npm install story-grammar



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.
- Story Grammar
- Table of Contents
- Interactive Examples
- Overview
- Key Features
- Features
- Quick Start
- TypeScript Usage
- Examples
- Basic Usage
- Complex Nested Variables
- Function Rules
- Weighted Rules
- Conditional Rules
- Sequential Rules
- Range Rules
- Template Rules
- Reference Rules
- Seeded Randomness
- Story Generation
- Error Handling
- Complexity Analysis
- Probability Analysis
- Built-in Modifiers
- Loading Individual Modifiers
- Loading All English Modifiers
- Basic English Modifiers
- Modifier System
- Modifier Features
- Adding Custom Modifiers
- Example with Multiple Modifiers
- Modifier Interface
- Management Methods
- Priority System
- Built-in Modifiers Reference
- English Articles (EnglishArticleModifier)
- English Pluralization (EnglishPluralizationModifier)
- English Ordinals (EnglishOrdinalModifier)
- English Capitalization (EnglishCapitalizationModifier)
- English Possessives (EnglishPossessiveModifier)
- English Verb Agreement (EnglishVerbAgreementModifier)
- Punctuation Cleanup (PunctuationCleanupModifier)
- Performance and Utility Features
- Batch Processing
- Variation Generation
- Performance Monitoring
- Parser Analysis and Optimization
- Enhanced Error Handling
- Safe Parsing
- Rule Analysis
- Helpful Error Messages
- Build and Deployment
- TypeScript Build
- Webpack Bundle
- Browser Usage
- API Reference
- Parser Class
- Static Rules
- Function Rule Methods
- Weighted Rule Methods
- Conditional Rule Methods
- Sequential Rule Methods
- Range Rule Methods
- Template Rule Methods
- Reference Rule Methods
- Parsing
- Modifiers
- Modifier Loading Methods
- Available English Modifiers
- Configuration
- Types
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.
The Story Grammar Parser allows you to create complex, dynamic text generation systems using a simple key-value grammar with variable substitution.
- 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
``typescript
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."
`
Story Grammar includes full TypeScript support with comprehensive type definitions. All interfaces are exported for type-safe development:
`typescript
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.
`typescript
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)
`
`typescript$3
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"
`
Create dynamic rules that generate values at runtime:
`typescript
// 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
Create rules where some values are more likely than others using probability weights:
`typescript
// 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
Create context-aware rules that select values based on previously generated content:
`typescript
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)
`
Generate values in a specific order, with optional cycling:
`typescript
// 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');
`
Generate numeric values within specified ranges:
`typescript
// 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"
`
Create structured combinations with their own variable sets:
`typescript
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"
`
Reuse previously generated values for consistency:
`typescript
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:
`typescript
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)
`
For testing and reproducible results, you can seed the random number generator:
`typescript
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
`typescript
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%'));
}
`
`typescript
// 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);
}
`
Analyze the generative potential of your grammar rules:
`typescript
// 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
`typescript
// 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);
}
`
Analyze the probability distribution of your grammar rules to understand outcome likelihood:
`typescript
// 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}); // 6Entropy (randomness): ${analysis.entropy.toFixed(2)}
console.log(); // 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:
`typescriptMost likely: ${mostProbable.outcome} (${(mostProbable.probability * 100).toFixed(1)}%)
// Get single most/least probable outcomes
const mostProbable = parser.getMostProbableOutcome('loot');
console.log();
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:
`typescript
// 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}:); ${node.ruleName}: ${node.value} (${node.probability})
outcome.probabilityTree.forEach(node => {
console.log();`
});
});
Performance Considerations:
`typescript
// 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);
}
`
The parser uses a modular modifier system that allows loading language-specific modifiers as needed:
`typescript
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"
`
`typescript
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
`
`typescript
import { Parser, BasicEnglishModifiers } from 'story-grammar';
const parser = new Parser();
// Load only core modifiers for performance
parser.loadModifiers(BasicEnglishModifiers);
// Includes: articles, pluralization, ordinals
`
Story Grammar now organizes modifiers in a clean namespace structure for better organization and future multi-language support:
`typescript
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
`typescript
// 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 correctionModifiers.English.PluralizationModifier
- - Pluralizes nouns with quantity wordsModifiers.English.OrdinalModifier
- - Converts numbers to ordinals (1st, 2nd, 3rd)Modifiers.English.CapitalizationModifier
- - Capitalizes sentence startsModifiers.English.PossessiveModifier
- - Handles possessive forms (John's, cats')Modifiers.English.VerbAgreementModifier
- - Basic subject-verb agreementModifiers.English.PunctuationCleanupModifier
- - Cleans up spacing around punctuation
Note: The original exports remain available for backward compatibility.
The Story Grammar parser includes a powerful modifier system that allows you to apply transformations to generated text after variable expansion.
- 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
`typescript`
parser.addModifier({
name: 'emphasize',
condition: (text: string) => text.includes('important'),
transform: (text: string) => text.replace(/important/g, 'IMPORTANT'),
priority: 5
});
With the article modifier enabled:
`text`
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"
`typescript`
interface Modifier {
name: string;
condition: ModifierFunction;
transform: ModifierFunction;
priority: number;
}
- addModifier(modifier) - Add a new modifierremoveModifier(name)
- - Remove a modifier by nameclearModifiers()
- - Remove all modifiershasModifier(name)
- - Check if a modifier exists
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.
All English modifiers are available as separate imports and can be loaded individually or in groups:
`typescript`
import {
EnglishArticleModifier,
EnglishPluralizationModifier,
EnglishOrdinalModifier,
EnglishCapitalizationModifier,
EnglishPossessiveModifier,
EnglishVerbAgreementModifier,
PunctuationCleanupModifier,
AllEnglishModifiers,
BasicEnglishModifiers
} from 'story-grammar';
- Priority: 10
- Function: Converts "a" to "an" before vowel sounds
- Examples: "a elephant" → "an elephant", "a umbrella" → "an umbrella"
- 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"
- 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"
- 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!"
- 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"
- 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"
- 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"
Process multiple texts efficiently with shared context:
`typescript
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
`
Generate multiple variations for testing or options:
`typescript`
// 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!"]
Monitor parsing performance with detailed timing:
`typescriptTotal: ${result.timing.totalMs}ms
const result = parser.parseWithTiming('%complex_rule%');
console.log();Expansion: ${result.timing.expansionMs}ms
console.log();Modifiers: ${result.timing.modifierMs}ms
console.log();`
Analyze your parser for optimization opportunities:
`typescriptTotal rules: ${stats.totalRules}
// Get statistics
const stats = parser.getStats();
console.log();Rule breakdown:
console.log(, stats.rulesByType);
// Analyze complexity
const analysis = parser.analyzeRules();
console.log(Most complex rules:, analysis.mostComplex);Suggestions:
console.log(, analysis.suggestions);
// Get optimization recommendations
const optimization = parser.optimize();
if (!optimization.optimized) {
console.log('Warnings:', optimization.warnings);
console.log('Suggestions:', optimization.suggestions);
}
`
Parse with automatic error recovery and detailed diagnostics:
`typescript
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);
}
}
`
Analyze individual rules for complexity and issues:
`typescript`
// 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);
Get detailed error explanations with actionable suggestions:
`typescript`
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 the library for Node.js environments:
`bash`
npm run build
This creates:
- TypeScript declaration files in the types/ directorydist/
- JavaScript modules in the directory
The separated structure keeps type definitions cleanly organized from compiled code.
Build the library for browser environments:
`bashProduction build (minified)
npm run build:webpack
This creates:
-
dist/story-grammar.bundle.js - Production browser bundle (minified)
- dist/story-grammar.dev.bundle.js - Development browser bundle
- Source maps for debugging$3
Include the webpack bundle in your HTML:
`html
Story Grammar Example
`The library is exposed as
StoryGrammar global object with the Parser class available as StoryGrammar.Parser.API Reference
$3
#### 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:
`typescript
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$3
`typescript
interface FunctionRule {
(): string[];
}interface WeightedRule {
values: string[];
weights: number[];
cumulativeWeights: number[];
}
interface ConditionalRule {
(context: Map): 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;
}
``