import { parseStringPromise } from 'xml2js' import xmllint from 'xmllint' import schema from './schema' import packageJson from '../package.json' import { Template, Root, Person as PersonElement, Location as LocationElement, } from './elements' import { NumberGenerator, TextGenerator, PersonGenerator, LocationGenerator } from './generators' import { resolveInlineTags, resolveTemplateRefs, loopExpander, textTagSampler, resolvePeopleRefs, resolveLocationRefs } from './preprocessor' export type LoreMLOptions = { seed?: number locale?: string debug?: boolean refDate?: string | Date | number } export type ParseOptions = { validate?: boolean } export default class LoreML { private seed: number private locale: string private debug: boolean private refDate?: string | Date | number constructor(opts?: LoreMLOptions) { this.seed = opts?.seed ?? Date.now() ^ (Math.random() * 0x100000000) this.locale = opts?.locale ?? 'en' this.debug = opts?.debug ?? false this.refDate = opts?.refDate } public async parse(xml: string, opts?: ParseOptions): Promise { const validate = opts?.validate ?? true if (validate) { const validation = this.validate(xml) if (!validation.isValid) { validation.errors.forEach(err => console.error(err)) return '' } } const rootAttributes = await this.loadRootAttributes(xml) const seed = rootAttributes.seed !== undefined ? rootAttributes.seed : this.seed const locale = rootAttributes.locale ?? this.locale const debug = rootAttributes.debug ?? this.debug const refDate = rootAttributes.refDate ?? this.refDate const rng = new NumberGenerator(seed) const rtg = new TextGenerator({ seed, locale, refDate }) const preProcessedXml = await this.preProcess(rng, rtg, xml) const parsed = await parseStringPromise(preProcessedXml, { explicitArray: false, preserveChildrenOrder: true, }) const root = new Root(parsed.Root) let debugHeader = '' if (debug) { debugHeader = `--------------------- DEBUG INFO ---------------------\nversion: ${packageJson.version} | seed: ${seed} | locale: ${locale}\n-------------------------------------------------------\n\n` } return debugHeader + root.toString() } public validate(xml: string): { isValid: boolean, errors: string[] } { const errors: string[] = xmllint.validateXML({xml, schema })?.errors ?? [] return { isValid: errors.length === 0, errors } } // gets the attributes from the Root element private async loadRootAttributes(xml: string) { const parsed = await parseStringPromise(xml, { explicitArray: false, }) const root = new Root(parsed.Root) as Root if (!root) { throw new Error('Root element not found') } return { seed: root?.seed, locale: root?.locale, debug: root?.debug, refDate: root?.refDate, } } private async loadTemplates(xml: string): Promise | undefined> { const parsed = await parseStringPromise(xml, { explicitArray: false, preserveChildrenOrder: true, }) const templates = parsed.Root.Template as Template | Template[] | undefined if (!templates) { return } const acc: Record = {}; (Array.isArray(templates) ? templates : [templates]).forEach((tmpl) => { acc[tmpl.$.name] = new Template(tmpl) }) return acc } private async loadPeople(rtg: TextGenerator, xml: string): Promise | undefined> { const parsed = await parseStringPromise(xml, { explicitArray: false, preserveChildrenOrder: true, }) const people = parsed.Root.Person as PersonElement | PersonElement[] | undefined if (!people) { return } const acc: Record = {}; (Array.isArray(people) ? people : [people]).forEach((person) => { acc[person.$.ref] = new PersonGenerator(rtg.generator, person.$) }) return acc } private async loadLocations(rtg: TextGenerator, xml: string): Promise | undefined> { const parsed = await parseStringPromise(xml, { explicitArray: false, preserveChildrenOrder: true, }) const locations = parsed.Root.Location as LocationElement | LocationElement[] | undefined if (!locations) { return } const acc: Record = {}; (Array.isArray(locations) ? locations : [locations]).forEach( location => { acc[location.$.ref] = new LocationGenerator(rtg.generator) }) return acc } private async preProcess(rng: NumberGenerator, rtg: TextGenerator, xml: string): Promise { const templates = await this.loadTemplates(xml) const people = await this.loadPeople(rtg, xml) const locations = await this.loadLocations(rtg, xml) // 1. expand loops const expanded = loopExpander(xml) // 2. resolve person references const resolvedPeople = resolvePeopleRefs(expanded, people) const resolvedLocations = resolveLocationRefs(resolvedPeople, locations) // 3. resolve inline expressions const inlined = resolveInlineTags(resolvedLocations, rng, rtg) // 4. resolve template references const resolvedTemplates = resolveTemplateRefs(inlined, templates) // 5. Apply random sampling const sampled = textTagSampler(rng, resolvedTemplates) return sampled } }