story-grammar
v1.2.3
Published
A combinatorial grammar for narrative-based projects
Maintainers
Readme
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.
Table of Contents
- Story Grammar
- Table of Contents
- Interactive Examples
- Overview
- Features
- Quick Start
- TypeScript Usage
- Examples
- Built-in Modifiers
- Modifier 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)
- English Articles (
- Performance and Utility Features
- Enhanced Error Handling
- Build and Deployment
- API Reference
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 againNote: 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 + 1possible 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 cleanupBasic English Modifiers
import { Parser, BasicEnglishModifiers } from 'story-grammar';
const parser = new Parser();
// Load only core modifiers for performance
parser.loadModifiers(BasicEnglishModifiers);
// Includes: articles, pluralization, ordinalsNamespace 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 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.
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 modifierremoveModifier(name)- Remove a modifier by nameclearModifiers()- Remove all modifiershasModifier(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:
- Priority 10: Article correction (a/an)
- Priority 9: Pluralization (many/several/three/etc.)
- Priority 8: Ordinal conversion (1st/2nd/3rd/etc.)
- Priority 7: Capitalization fixes
- 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 textsVariation 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 buildThis 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:allThis 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 valuesaddRules(rules: Grammar)- Add multiple static rules at onceremoveRule(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 ruleremoveFunctionRule(key: string): boolean- Remove a function rulehasFunctionRule(key: string): boolean- Check if function rule existsclearFunctionRules(): void- Clear all function rules
Weighted Rule Methods
addWeightedRule(key: string, values: string[], weights: number[]): void- Add a weighted probability ruleremoveWeightedRule(key: string): boolean- Remove a weighted rulehasWeightedRule(key: string): boolean- Check if weighted rule existsclearWeightedRules(): void- Clear all weighted rules
Conditional Rule Methods
addConditionalRule(key: string, condition: ConditionalRule): void- Add a context-aware conditional ruleremoveConditionalRule(key: string): boolean- Remove a conditional rulehasConditionalRule(key: string): boolean- Check if conditional rule existsclearConditionalRules(): void- Clear all conditional rules
Sequential Rule Methods
addSequentialRule(key: string, values: string[]): void- Add a sequential rule that cycles through valuesresetSequentialRule(key: string): void- Reset sequential rule to first valueremoveSequentialRule(key: string): boolean- Remove a sequential rulehasSequentialRule(key: string): boolean- Check if sequential rule existsclearSequentialRules(): void- Clear all sequential rules
Range Rule Methods
addRangeRule(key: string, min: number, max: number, isInteger?: boolean): void- Add a numeric range ruleremoveRangeRule(key: string): boolean- Remove a range rulehasRangeRule(key: string): boolean- Check if range rule existsclearRangeRules(): void- Clear all range rules
Template Rule Methods
addTemplateRule(key: string, template: string, slots: string[]): void- Add a template rule with variable slotsremoveTemplateRule(key: string): boolean- Remove a template rulehasTemplateRule(key: string): boolean- Check if template rule existsclearTemplateRules(): void- Clear all template rules
Reference Rule Methods
addReferenceRule(key: string, referenceKey: string): void- Add a rule that references previously generated valuesremoveReferenceRule(key: string): boolean- Remove a reference rulehasReferenceRule(key: string): boolean- Check if reference rule existsclearReferenceRules(): void- Clear all reference rules
Parsing
parse(text: string): string- Parse text and expand all variablesfindVariables(text: string): string[]- Find all variables in textvalidate(): ValidationResult- Validate grammar for missing rules and circular references
Modifiers
addModifier(modifier: Modifier): void- Add a text transformation modifierremoveModifier(name: string): boolean- Remove a modifierhasModifier(name: string): boolean- Check if modifier existsgetModifiers(): Modifier[]- Get all modifiers sorted by priorityclearModifiers(): void- Clear all modifiers
Modifier Loading Methods
loadModifier(modifier: Modifier)- Load a single modifierloadModifiers(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 depthsetRandomSeed(seed: number): void- Set random seed for deterministic resultsclearRandomSeed(): void- Clear random seed and return to Math.random()getRandomSeed(): number | null- Get current random seed or nullclearAll(): 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;
}