const builder = require('xmlbuilder'); const convert = (jsonStr) => { const inputData = JSON.parse(jsonStr); const printableNumber = n => (n > 0) ? "+" + n : n; const capitalize = (s) => { if (typeof s !== 'string') return '' return s.charAt(0).toUpperCase() + s.slice(1) }; const ordinalize = (i) => { const j = i % 10, k = i % 100; if (j == 1 && k != 11) { return i + "st"; } if (j == 2 && k != 12) { return i + "nd"; } if (j == 3 && k != 13) { return i + "rd"; } return i + "th"; }; const replaceSpecialValues = (text) => { text = text.replace(/\{@hit (\d+?)\}/g, (match, p1) => printableNumber(p1)) text = text.replace(/\{@h\}(\d+?)/g, '$1'); text = text.replace(/\{@damage (.*?)\}/g, '$1'); text = text.replace(/\{@dc (\d+?)\}/g, 'DC $1'); text = text.replace(/\{@condition (\w+)\}/g, '$1'); text = text.replace(/\{@skill (\w+)\}/g, '$1'); text = text.replace(/\{@dice (.+?)\}/g, '$1'); text = text.replace(/\{@spell (.+?)\}/g, (match, p1) => capitalize(p1)); text = text.replace(/\{@creature (.+?)\}/g, (match, p1) => capitalize(p1)); text = text.replace(/\{@atk (.+?)\}/g, (match, p1) => { p1 = p1.replace(/mw/g, 'Melee Weapon Attack'); p1 = p1.replace(/ms/g, 'Melee Spell Attack'); p1 = p1.replace(/rw/g, 'Ranged Weapon Attack'); p1 = p1.replace(/rs/g, 'Ranged Spell Attack'); p1 = p1.replace(/\,\s*/g, ' or '); return `${p1}:`; }); return text; }; const xml = builder.create('root', { encoding: 'utf-8' }) .ele('compendium', { version: '5' }) const parseToXML = (obj) => { const monster = xml.ele('monster'); monster.ele('name', obj.name) monster.ele('size', obj.size) monster.ele('type', typeof obj.type === 'object' ? obj.type.type : obj.type) monster.ele('source', obj.source) monster.ele('alignment', obj.alignment && obj.alignment.length ? obj.alignment.join('') : null) monster.ele('ac', obj.ac && obj.ac.length ? obj.ac[0].ac : null) monster.ele('hp', `${obj.hp.average} (${obj.hp.formula})`) monster.ele('cr', obj.cr) monster.ele('passive', obj.passive) monster.ele('str', obj.str) monster.ele('dex', obj.dex) monster.ele('con', obj.con) monster.ele('int', obj.int) monster.ele('wis', obj.wis) monster.ele('cha', obj.cha) monster.ele('languages', obj.languages && obj.languages.length ? obj.languages.join(', ') : null ); monster.ele('senses', obj.senses && obj.senses.length ? obj.senses.join(', ') : null ); monster.ele('immune', obj.immune && obj.immune.length && obj.immune.map((item) => { if (typeof item === 'string') { return item; } else { let res = ''; if (item.preNote) { res += item.preNote; } if (item.immune) { res += item.immune.join(', '); } if (item.postNote) { res += item.postNote; } return res; } }).join(', ')); const speedTypes = ['walk', 'burrow', 'climb', 'fly', 'swim']; monster.ele('speed', obj.speed && speedTypes.map((key) => { const value = obj.speed[key]; const type = key === 'walk' ? '' : key; if (typeof value === 'object') { const num = value.number; const cond = value.condition || ''; return `${type} ${num} ft. ${cond}`.trim(); } else if (!!value) { const num = obj.speed[key]; return `${type} ${num} ft.`; } else { return undefined; } }).filter(a => !!a).join(', ').trim()); monster.ele('save', obj.save && Object.keys(obj.save).map((key) => { return `${key} ${obj.save[key]}`; }).join(', ')); monster.ele('skill', obj.skill && Object.keys(obj.skill).map((key) => { return `${key} ${obj.skill[key]}`; }).join(', ')); obj.trait && obj.trait.forEach((trait) => { const e = monster.ele('trait'); e.ele('name', trait.name); trait.entries.forEach((entry) => { const text = replaceSpecialValues(entry); e.ele('text', text); }); }); obj.spellcasting && obj.spellcasting.forEach((spellcasting) => { const e = monster.ele('trait'); e.ele('name', spellcasting.name); spellcasting.headerEntries && spellcasting.headerEntries.length && spellcasting.headerEntries.forEach((header) => { e.ele('text', replaceSpecialValues(header)); }); spellcasting.spells && Object.keys(spellcasting.spells).forEach((level) => { const spell = spellcasting.spells[level]; const slots = spell.slots; const lst = spell.spells; const levelNum = parseInt(level, 10); const ordinal = ordinalize(levelNum); let pre = levelNum !== 0 ? ordinal + ' level' : 'Cantrips'; pre += slots ? ` (${slots} slot${slots > 0 ? 's' : ''})` : ''; pre = pre.trim(); const text = `${pre}: ${lst.join(', ')}`; e.ele('text', replaceSpecialValues(text)); }); if (spellcasting.will) { const pre = 'At will' const lst = spellcasting.will; const text = `${pre}: ${lst.join(', ')}`; e.ele('text', replaceSpecialValues(text)); } spellcasting.daily && Object.keys(spellcasting.daily).forEach((count) => { const lst = spellcasting.daily[count]; let pre = `• ${count}/day`; const text = `${pre}: ${lst.join(', ')}`; e.ele('text', replaceSpecialValues(text)); }); }); obj.legendary && obj.legendary.forEach((legendary) => { const e = monster.ele('legendary'); e.ele('name', legendary.name); legendary.entries.forEach((entry) => { const text = replaceSpecialValues(entry); e.ele('text', text); }); }); obj.action && obj.action.forEach((action) => { const e = monster.ele('action'); e.ele('name', action.name); action.entries.forEach((entry) => { const text = replaceSpecialValues(entry); e.ele('text', text); }); }) } const processInputData = () => { if (inputData.length) { // array inputData.forEach((obj) => { parseToXML(obj); }); } else if (inputData.monster) { // .monster is an array inputData.monster.forEach((obj) => { parseToXML(obj); }); } else { parseToXML(inputData); } } processInputData(); xml.end({ pretty: true, newline: '\n', }); return xml.toString(); }; module.exports = { convert, };