UNPKG

brogue

Version:

A Grammar based generative text library based on Tracery.

379 lines (378 loc) 13.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.postParseGrammar = exports.parseLexeme = exports.parseGrammarObject = exports.parseGrammarString = exports.parseGrammarFile = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const json5_1 = __importDefault(require("json5")); const random_seed_1 = require("random-seed"); const markov_1 = require("./markov"); function parseLexeme(data) { let i = 0; let j = -1; const whitespaceChars = { ' ': true, '\t': true, '\n': true }; function _parseError(errorString) { return new Error(`[Parsing] ${errorString}:\n${data.slice(0, 255)}...`); } function _parseExpansionModifierArgs() { let inString = false; let stringChar = ''; while (++j < data.length) { const c = data[j]; if (!inString) { // When we detect that the param list closes, json parse the contents to handle all arg types if (c === ')') { const argListJSON = data.slice(i, j); i = j + 1; return json5_1.default.parse(`[${argListJSON}]`); // Start tracking string (where we don't care about ending the argument list) } else if (c === '\'' || c === '"') { inString = true; stringChar = c; } } else { // Next character is escaped, skip it if (c === '\\') { j++; // Detected end of string } else if (c === stringChar) { inString = false; } } } throw _parseError('Reached end of string before closing argument list'); } function _parseExpansionModifier() { const modifier = { name: "", args: [] }; let didParseArgs = false; while (++j < data.length) { const c = data[j]; if (c === '(') { modifier.name = data.slice(i, j); i = j + 1; modifier.args = _parseExpansionModifierArgs(); didParseArgs = true; } else if (c === '}' || c === '.') { if (!didParseArgs) { modifier.name = data.slice(i, j); } i = j-- + 1; if (!modifier.name) { throw _parseError(`Empty modifier name`); } return modifier; } else if (c === ')') { throw _parseError(`Encountered invalid ')' character when outside arg list`); } else if (c === '{') { throw _parseError(`Encountered invalid '{' character when parsing modifier`); } } throw _parseError(`Reached end of string before before closing expansion`); } function _lookaheadIsVariable() { let k = j; while (++k < data.length) { const c = data[k]; if (whitespaceChars[c]) { return false; } else if (c === '}') { return false; } else if (c === '=') { return true; } } return false; } function _parseVariable() { let name; while (++j < data.length) { const c = data[j]; if (c === '=') { name = data.slice(i, j); if (!name?.trim()) { throw _parseError(`Variable has empty name`); } if (data.length < j + 1) { throw _parseError(`Expected space after variable name but found end of string. Example: {a= value}`); } else if (data[j + 1] !== ' ') { throw _parseError(`Expected space after variable name but found ${data[j + 1]}. Example: {a= value}`); } i = ++j + 1; return { name, lexeme: _parseLexeme('}', 'variable') }; } } throw _parseError(`Reached end of string before before closing variable`); } function _parseExpansion(lexeme) { const expansion = { name: "", modifierCalls: [], isDecorator: false }; if (data.charAt(j + 1) === '{') { expansion.isDecorator = true; i = ++j + 1; } while (++j < data.length) { const c = data[j]; if (c === '}') { lexeme.formatString += `{${lexeme.expansions.length}}`; if (expansion.modifierCalls.length === 0) { expansion.name = data.slice(i, j); } if (expansion.isDecorator && data.charAt(j + 1) !== '}') { throw _parseError(`Expected second '}' character when closing non-tracking expansion`); } if (!expansion.isDecorator) { i = j + 1; } else { i = ++j + 1; } return expansion; } else if (c === '.') { expansion.name = data.slice(i, j); i = j + 1; expansion.modifierCalls.push(_parseExpansionModifier()); } else if (c === '{' || c === '=') { throw _parseError(`Encountered invalid '${c}' character when parsing expansion`); } } throw _parseError(`Reached end of string before before closing expansion`); } function _parseLexeme(closingCharacter, scopeType) { const lexeme = { originalString: "", formatString: "", expansions: [], variables: new Map(), }; const starti = i; while (++j < data.length) { const c = data[j]; if (c === '{') { lexeme.formatString += data.slice(i, j); i = j + 1; if (_lookaheadIsVariable()) { const variable = _parseVariable(); lexeme.variables.set(variable.name, variable); } else { lexeme.expansions.push(_parseExpansion(lexeme)); } } else if (c === closingCharacter) { break; } else if (c === '}') { throw _parseError(`Encountered invalid '}' character when outside expansion`); } } if (closingCharacter && j >= data.length) { throw _parseError(`Reached end of string before before closing ${scopeType}`); } lexeme.originalString = data.slice(starti, j); lexeme.formatString += data.slice(i, j); i = j + 1; return lexeme; } return _parseLexeme(); } exports.parseLexeme = parseLexeme; function parseWeightedLexeme(data) { let lexemeString; let weight = 1.0; if (typeof data === 'string') { lexemeString = data; } else if (typeof data === 'object') { lexemeString = Object.keys(data)[0]; weight = data[lexemeString]; } else { throw new Error(`Lexeme has unexpected type: ${typeof data}`); } const lexeme = parseLexeme(lexemeString); return { lexeme, weight }; } function parseRule(name, data) { const rule = { name, totalWeight: 0.0, weightedLexemes: [] }; if (Array.isArray(data)) { for (const element of data) { const weightedLexeme = parseWeightedLexeme(element); rule.weightedLexemes.push(weightedLexeme); rule.totalWeight += weightedLexeme.weight; } } else if (typeof data === 'string' || typeof data === 'object') { const weightedLexeme = parseWeightedLexeme(data); rule.weightedLexemes.push(weightedLexeme); rule.totalWeight += weightedLexeme.weight; } else { throw new Error(`Expected value of rule ${name} to be array or string or object but was ${typeof data}`); } return rule; } function trainMarkovSymbol(markovSymbol, grammar) { const expandedSentences = markovSymbol.markov.sentences.flatMap((s) => { if (s.startsWith('{') && s.endsWith('}')) { const ruleName = s.slice(1, -1); const rule = grammar.rules.get(ruleName); if (!rule) { throw new Error(`Could not find rule ${ruleName} when training markov synbol ${markovSymbol.name}`); } return rule.weightedLexemes.map((wl) => wl.lexeme.originalString); } else { return s; } }); markovSymbol.markov.setSentences(expandedSentences); markovSymbol.markov.train(); } function parseMarkovSymbol(name, data) { let sentences; const settings = markov_1.Markov.DefaultSettings; if (typeof data === 'string') { sentences = [data]; } else if (Array.isArray(data)) { sentences = data; } else if (typeof data === 'object') { if (typeof data.sentences === 'string') { sentences = [data.sentences]; } else if (data) { sentences = data.sentences; } else { throw new Error(`Failed to parse markov state data.`); } if (data.maxCharacters) { settings.maxCharacters = data.maxCharacters; } if (data.minCharacters) { settings.minCharacters = data.minCharacters; } if (data.maxTries) { settings.maxTries = data.maxTries; } if (data.uniqueOutput) { settings.uniqueOutput = data.uniqueOutput; } if (data.order) { settings.order = data.order; } } else { throw new Error(`Expected markov state to be string or object but found type: ${typeof data}`); } return { name, markov: new markov_1.Markov(sentences, settings) }; } function _mergeGrammars(into, other) { other.rules.forEach((v, k) => { into.rules.set(k, v); }); other.variables.forEach((v, k) => { into.variables.set(k, v); }); other.modifiers.forEach((v, k) => { into.modifiers.set(k, v); }); other.markovSymbols.forEach((v, k) => { into.markovSymbols.set(k, v); }); } function readGrammarFile(fileName) { let grammarString; try { grammarString = fs_1.default.readFileSync(fileName, 'utf8'); } catch (error) { throw new Error(`Failed to load grammar file: ${fileName}`); } return grammarString; } function parseGrammarObject(obj, basePath) { const grammarDirName = basePath ? path_1.default.dirname(basePath) : process.cwd(); const grammar = { random: random_seed_1.create(), rules: new Map(), variables: new Map(), modifiers: new Map(), markovSymbols: new Map(), }; // Handle includes const includeFileNames = obj._includes; if (includeFileNames) { for (const includeFileName of includeFileNames) { const absoluteIncludeFileName = path_1.default.resolve(grammarDirName, includeFileName); const includeGrammar = parseGrammarFile(absoluteIncludeFileName); _mergeGrammars(grammar, includeGrammar); } } // Parse variables const variableStrings = obj._variables; if (variableStrings) { for (const variableString of variableStrings) { const lexeme = parseLexeme(variableString); if (lexeme.variables) { for (const [name, value] of lexeme.variables) { grammar.variables.set(name, value); } } } } // Parse markov symbols const markovSymbols = obj._markov; if (markovSymbols) { for (const [name, value] of Object.entries(markovSymbols)) { const markovSymbol = parseMarkovSymbol(name, value); grammar.markovSymbols.set(markovSymbol.name, markovSymbol); } } // Parse rules for (const [name, value] of Object.entries(obj)) { // Ignore reserved id names if (name.startsWith('_')) { continue; } const rule = parseRule(name, value); grammar.rules.set(rule.name, rule); } return grammar; } exports.parseGrammarObject = parseGrammarObject; function parseGrammarString(text, basePath) { let grammarObject; try { grammarObject = json5_1.default.parse(text); } catch (e) { throw new Error(`Failed to parse JSON5 text ${text.slice(0, 256)}...\nInternal error: ${e}`); } return parseGrammarObject(grammarObject, basePath); } exports.parseGrammarString = parseGrammarString; function parseGrammarFile(fileName) { const grammarString = readGrammarFile(fileName); return parseGrammarString(grammarString, fileName); } exports.parseGrammarFile = parseGrammarFile; function postParseGrammar(grammar) { grammar.markovSymbols.forEach((markovSymbol) => { trainMarkovSymbol(markovSymbol, grammar); }); } exports.postParseGrammar = postParseGrammar;