import { isFinite, toNumber } from 'lodash'; import { TypeScriptUtils } from '../../utils'; import { AbilityAccessors } from '../Ability'; import { CharacterDerivers } from '../Character'; import { DiceRenderers } from '../Dice'; import { FormatUtils } from '../Format'; import { LimitedUseDerivers } from '../LimitedUse'; import { SpellDerivers } from '../Spell'; import { SnippetAbilityKeyEnum, SnippetContentChunkTypeEnum, SnippetMathOperatorEnum, SnippetPostProcessTypeEnum, SnippetSymbolEnum, SnippetTagDataTypeEnum, SnippetTagValueTypeEnum, SnippetValueModifierTypeEnum, } from './constants'; const snippetTagValues = Object.keys(SnippetTagValueTypeEnum) .map((tagValueType) => SnippetTagValueTypeEnum[tagValueType]) .join('|') .toLowerCase(); const groupFragmentRegExpr = new RegExp(['\\(([^\\(\\)]*?)\\)(?:', SnippetSymbolEnum.VALUE_MODIFIER, '([^+*\\-\\/]+)?)?'].join('')); const tagValueRegExpr = new RegExp([ '((?:', snippetTagValues, ')(?:\\', SnippetSymbolEnum.PARAMETER, '[^', SnippetSymbolEnum.VALUE_MODIFIER, '+*\\-\\/]+)?)(?:', SnippetSymbolEnum.VALUE_MODIFIER, '([^+*\\-\\/]+)?)?', ].join('')); /** * * @param key */ export function validateAbilityKey(key) { if (!isSnippetAbilityKeyEnum(key)) { throw new Error(`Invalid Ability Key: ${key}`); } } /** * * @param key */ export function isSnippetAbilityKeyEnum(key) { switch (key) { case SnippetAbilityKeyEnum.STRENGTH: case SnippetAbilityKeyEnum.DEXTERITY: case SnippetAbilityKeyEnum.CONSTITUTION: case SnippetAbilityKeyEnum.INTELLIGENCE: case SnippetAbilityKeyEnum.WISDOM: case SnippetAbilityKeyEnum.CHARISMA: return true; default: // not implemented } return false; } /** * * @param key */ export function isSnippetValueModifierTypeEnum(key) { switch (key) { case SnippetValueModifierTypeEnum.MAX: case SnippetValueModifierTypeEnum.MIN: case SnippetValueModifierTypeEnum.ROUND_DOWN: case SnippetValueModifierTypeEnum.ROUND_UP: return true; default: // not implemented } return false; } /** * * @param key */ export function isSnippetPostProcessTypeEnum(key) { switch (key) { case SnippetPostProcessTypeEnum.MAX: case SnippetPostProcessTypeEnum.MIN: case SnippetPostProcessTypeEnum.ROUND_DOWN: case SnippetPostProcessTypeEnum.ROUND_UP: case SnippetPostProcessTypeEnum.SIGNED_NUMBER: case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER: return true; default: // not implemented } return false; } /** * * @param key */ export function isSnippetTagValueTypeEnum(key) { switch (key) { case SnippetTagValueTypeEnum.SCALE_VALUE: case SnippetTagValueTypeEnum.LIMITED_USE: case SnippetTagValueTypeEnum.CLASS_LEVEL: case SnippetTagValueTypeEnum.ABILITY_SCORE: case SnippetTagValueTypeEnum.CHARACTER_LEVEL: case SnippetTagValueTypeEnum.MAX_HP: case SnippetTagValueTypeEnum.PROFICIENCY: case SnippetTagValueTypeEnum.SPELL_ATTACK: case SnippetTagValueTypeEnum.SAVE_DC: case SnippetTagValueTypeEnum.MODIFIER: case SnippetTagValueTypeEnum.FIXED_VALUE: return true; default: // not implemented } return false; } /** * * @param type */ export function isSignedTagValueType(type) { switch (type) { case SnippetTagValueTypeEnum.SPELL_ATTACK: case SnippetTagValueTypeEnum.MODIFIER: return true; default: // not implemented } return false; } /** * * @param contextData */ export function isLevelScaleFixedValue(contextData) { if (!contextData.levelScale) { return false; } return contextData.levelScale.fixedValue !== null && contextData.levelScale.dice === null; } /** * * @param rawSnippetTag * @param contextData */ export function isValidStringTagValueType(rawSnippetTag, contextData) { const tagValueStringTypesRegExp = new RegExp(['^(', [SnippetTagValueTypeEnum.SCALE_VALUE].join('|').toLowerCase(), ')$'].join('')); let match = rawSnippetTag.match(tagValueStringTypesRegExp); if (match) { switch (match[1]) { case SnippetTagValueTypeEnum.SCALE_VALUE: return !isLevelScaleFixedValue(contextData); default: // not implemented } } return false; } /** * * @param type */ export function isLastPostProcessType(type) { switch (type) { case SnippetPostProcessTypeEnum.SIGNED_NUMBER: case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER: return true; } return false; } /** * * @param numberString */ export function isParsableNumber(numberString) { return !isNaN(toNumber(numberString)) || numberString === '-' || numberString === '.'; } /** * * @param expression * @param operator */ export function hasOperator(expression, operator) { return expression.indexOf(operator) > -1; } /** * * @param str */ export function throwMismatchedParens(str) { let level = 0; for (let i = 0; i < str.length; i++) { let char = str[i]; if (char === '(') { level += 1; } if (char === ')') { if (level === 0) { throw new Error('Mismatching parenthesis'); } level -= 1; } } if (level !== 0) { throw new Error('Mismatching parenthesis'); } } /** * * @param str */ export function throwInvalidSnippetCharacters(str) { const keywordMatches = str.match(/[a-z]+/g); keywordMatches === null || keywordMatches === void 0 ? void 0 : keywordMatches.forEach((keyword) => { if (isSnippetTagValueTypeEnum(keyword) || isSnippetAbilityKeyEnum(keyword) || isSnippetValueModifierTypeEnum(keyword) || isSnippetPostProcessTypeEnum(keyword)) { return; } else { throw new Error(`Invalid keyword: ${keyword}`); } }); const invalidSymbolMatches = str.match(/[^a-z\d+*\-\/#@:,()]/); if (invalidSymbolMatches) { throw new Error(`Invalid symbol: ${invalidSymbolMatches[0]}`); } const invalidValueModifierMatches = str.match(/[^a-z@)]+(@[a-z,:\d]*)/); if (invalidValueModifierMatches) { throw new Error(`Value modifier cannot be used at current location: ${invalidValueModifierMatches[1]}`); } } /** * * @param str */ export function deriveFormattedTag(str) { return str .toLowerCase() .replace(/\s+/g, '') .replace(/-\(/g, '-1*(') .replace(/\)\(/g, ')*(') .replace(/-/g, '+-') .replace(/--/g, '+') .replace(/\+\+/g, '+') .replace(/\(\+/g, '(') .replace(/(\d)\(/g, '$1*(') .replace(/^\++/, ''); } /** * * @param str */ export function deriveListOptions(str) { return str.split(SnippetSymbolEnum.LIST_OPTION); } /** * * @param stats * @param snippetData */ export function deriveStatModifierValue(stats, snippetData) { const modifierValues = stats .map((statKey) => { validateAbilityKey(statKey); return AbilityAccessors.getModifier(snippetData.abilityKeyLookup[statKey]); }) .filter(TypeScriptUtils.isNotNullOrUndefined); return Math.max(...modifierValues); } /** * * @param stats * @param snippetData */ export function deriveAbilityScoreValue(stats, snippetData) { const modifierValues = stats .map((statKey) => { validateAbilityKey(statKey); return AbilityAccessors.getScore(snippetData.abilityKeyLookup[statKey]); }) .filter(TypeScriptUtils.isNotNullOrUndefined); return Math.max(...modifierValues); } /** * * @param contextData */ export function deriveLevelScaleDiceValue(contextData) { if (!contextData.levelScale) { throw new Error('No level scale data available'); } if (contextData.levelScale.dice === null) { throw new Error('Has level scale, but no dice value available'); } return DiceRenderers.renderDice(contextData.levelScale.dice); } /** * * @param contextData */ export function deriveLevelScaleFixedValue(contextData) { if (!contextData.levelScale) { throw new Error('No level scale data available'); } if (contextData.levelScale.fixedValue === null) { throw new Error('Has level scale, but no numeric value available'); } return contextData.levelScale.fixedValue; } /** * * @param tagValue * @param contextData */ export function deriveStringTagValue(tagValue, contextData) { const { type, params } = tagValue; switch (type) { case SnippetTagValueTypeEnum.SCALE_VALUE: return deriveLevelScaleDiceValue(contextData); } throw new Error('Unknown string tag'); } /** * * @param tagValue * @param contextData * @param snippetData * @param proficiencyBonus */ export function deriveNumericTagValue(tagValue, contextData, snippetData, proficiencyBonus) { const { type, params } = tagValue; let value = 0; switch (type) { case SnippetTagValueTypeEnum.SAVE_DC: if (params.length < 1) { throw new Error('Save DC missing ability key'); } return CharacterDerivers.deriveAttackSaveValue(snippetData.proficiencyBonus, deriveStatModifierValue(deriveListOptions(params[0]), snippetData)); case SnippetTagValueTypeEnum.SPELL_ATTACK: if (params.length < 1) { throw new Error('Spell attack missing ability key'); } return SpellDerivers.deriveSpellAttackModifier(snippetData.proficiencyBonus, deriveStatModifierValue(deriveListOptions(params[0]), snippetData)); case SnippetTagValueTypeEnum.MODIFIER: { if (params.length < 1) { throw new Error('Modifier missing ability key'); } return deriveStatModifierValue(deriveListOptions(params[0]), snippetData); } case SnippetTagValueTypeEnum.ABILITY_SCORE: { if (params.length < 1) { throw new Error('Ability score missing ability key'); } return deriveAbilityScoreValue(deriveListOptions(params[0]), snippetData); } case SnippetTagValueTypeEnum.SCALE_VALUE: return deriveLevelScaleFixedValue(contextData); case SnippetTagValueTypeEnum.FIXED_VALUE: if (params.length < 1) { throw new Error('Fixed value is missing'); } value = toNumber(params[0]); if (!isFinite(value)) { throw new Error('Fixed value is not a number'); } return value; case SnippetTagValueTypeEnum.CLASS_LEVEL: { if (!contextData.classLevel) { throw new Error('No class level data available'); } return contextData.classLevel; } case SnippetTagValueTypeEnum.CHARACTER_LEVEL: { return snippetData.xpInfo.currentLevel; } case SnippetTagValueTypeEnum.PROFICIENCY: { return snippetData.proficiencyBonus; } case SnippetTagValueTypeEnum.MAX_HP: { return snippetData.hitPointInfo.totalHp; } case SnippetTagValueTypeEnum.LIMITED_USE: { if (!contextData.limitedUse) { throw new Error('No limited use data available'); } return LimitedUseDerivers.deriveMaxUses(contextData.limitedUse, snippetData.abilityLookup, snippetData.ruleData, proficiencyBonus); } } } /** * * @param str */ export function parseSnippetParams(str) { if (!str.length) { throw new Error('Invalid parameter'); } const [key, ...params] = str.split(SnippetSymbolEnum.PARAMETER); if (!key.length) { throw new Error('Invalid parameter key'); } return { key, params: params.filter(Boolean), }; } /** * * @param str */ export function parseSnippetPostProcessParams(str) { const { key, params } = parseSnippetParams(str); if (!isSnippetPostProcessTypeEnum(key)) { throw new Error(`Unknown post process type: ${key}`); } return { type: key, params, }; } /** * * @param str */ export function parseSnippetTagValue(str) { const { key, params } = parseSnippetParams(str); if (!isSnippetTagValueTypeEnum(key)) { throw new Error(`Unknown value type: ${key}`); } return { type: key, params, }; } /** * * @param str */ export function parseSnippetValueModifierParams(str) { const { key, params } = parseSnippetParams(str); if (!isSnippetValueModifierTypeEnum(key)) { throw new Error(`Unknown value modifier type: ${key}`); } return { type: key, params, }; } /** * * @param str */ export function parseRawTagInfo(str) { if (!str.length) { throw new Error('Invalid tag'); } const [expression, rawPostProcess] = str.split(SnippetSymbolEnum.POST_PROCESS); if (!expression.length) { throw new Error('Missing tag expression'); } let postProcess = rawPostProcess; if (rawPostProcess === undefined || rawPostProcess === '') { postProcess = null; } return { expression, postProcess, }; } /** * * @param value * @param valueModifierString */ export function applyValueModifierString(value, valueModifierString) { let modifiedValue = value; const valueModifiers = deriveListOptions(valueModifierString); valueModifiers.forEach((valueModifierString) => { const { type, params } = parseSnippetValueModifierParams(valueModifierString); modifiedValue = deriveModifiedValue(modifiedValue, type, params); }); return modifiedValue; } /** * * @param value * @param postProcessesString * @param intrinsicSignedNumber */ export function applyPostProcessString(value, postProcessesString, intrinsicSignedNumber = false) { if (typeof value === 'string' && !value) { return value; } let processedValue = value; let postProcesses = postProcessesString === null || !postProcessesString.length ? [] : deriveListOptions(postProcessesString); if (intrinsicSignedNumber && !postProcesses.includes(SnippetPostProcessTypeEnum.SIGNED_NUMBER) && !postProcesses.includes(SnippetPostProcessTypeEnum.UNSIGNED_NUMBER)) { postProcesses = [...postProcesses, SnippetPostProcessTypeEnum.SIGNED_NUMBER]; } postProcesses.forEach((postProcessString, idx) => { const { type, params } = parseSnippetPostProcessParams(postProcessString); const isLast = idx + 1 === postProcesses.length; if (isLastPostProcessType(type) && !isLast) { throw new Error(`"${type}" post process must be the last to run`); } processedValue = derivePostProcessedValue(processedValue, type, params); }); return processedValue; } /** * * @param value * @param modifierType * @param params */ export function deriveModifiedValue(value, modifierType, params) { switch (modifierType) { case SnippetValueModifierTypeEnum.MAX: if (params.length < 1) { throw new Error('Max value modifier is missing a number'); } const maxValue = toNumber(params[0]); if (!isFinite(maxValue)) { throw new Error('Max value modifier is not a number'); } return Math.min(maxValue, value); case SnippetValueModifierTypeEnum.MIN: if (params.length < 1) { throw new Error('Min value modifier is missing a number'); } const minValue = toNumber(params[0]); if (!isFinite(minValue)) { throw new Error('Min value modifier is not a number'); } return Math.max(minValue, value); case SnippetValueModifierTypeEnum.ROUND_UP: return Math.ceil(value); case SnippetValueModifierTypeEnum.ROUND_DOWN: return Math.floor(value); } } /** * * @param value * @param type * @param params */ export function derivePostProcessedValue(value, type, params) { switch (type) { case SnippetPostProcessTypeEnum.MAX: if (typeof value !== 'number') { throw new Error('Max post process must be used on number value'); } if (params.length < 1) { throw new Error('Max post process is missing a number'); } const maxValue = toNumber(params[0]); return Math.min(maxValue, value); case SnippetPostProcessTypeEnum.MIN: if (typeof value !== 'number') { throw new Error('Min post process must be used on number value'); } if (params.length < 1) { throw new Error('Min post process is missing a number'); } const minValue = toNumber(params[0]); return Math.max(minValue, value); case SnippetPostProcessTypeEnum.ROUND_DOWN: if (typeof value !== 'number') { throw new Error('Post process value being rounded is not a number'); } return Math.floor(value); case SnippetPostProcessTypeEnum.ROUND_UP: if (typeof value !== 'number') { throw new Error('Post process value being rounded is not a number'); } return Math.ceil(value); case SnippetPostProcessTypeEnum.SIGNED_NUMBER: if (typeof value !== 'number') { return value; } return FormatUtils.renderSignedNumber(value); case SnippetPostProcessTypeEnum.UNSIGNED_NUMBER: return value; } } /** * * @param haystack * @param operatorIdx * @param direction */ export function deriveExpressionValueFragmentSide(haystack, operatorIdx, direction) { const limit = direction == -1 ? 0 : haystack.length; let i = operatorIdx + direction; let term = ''; while (i * direction <= limit) { if (isParsableNumber(haystack[i])) { if (direction == 1) { term = term + haystack[i]; } else { term = haystack[i] + term; } i += direction; } else { return term; } } return term; } /** * * @param expression * @param operator */ export function deriveExpressionValueFragment(expression, operator) { if (hasOperator(expression, operator)) { const middleIndex = expression.indexOf(operator); const left = deriveExpressionValueFragmentSide(expression, middleIndex, -1); const right = deriveExpressionValueFragmentSide(expression, middleIndex, 1); const value = deriveExpressionValue(left, right, operator); return expression.replace(left + operator + right, value.toString()); } return expression; } /** * * @param left * @param right * @param operator */ export function deriveExpressionValue(left, right, operator) { switch (operator) { case SnippetMathOperatorEnum.ADDITION: return parseFloat(left) + parseFloat(right); case SnippetMathOperatorEnum.MULTIPLICATION: return parseFloat(left) * parseFloat(right); case SnippetMathOperatorEnum.DIVISION: return parseFloat(left) / parseFloat(right); case SnippetMathOperatorEnum.SUBTRACTION: return parseFloat(left) - parseFloat(right); } } /** * * @param expression */ export function deriveExpression(expression) { while (hasOperator(expression, SnippetMathOperatorEnum.MULTIPLICATION) || hasOperator(expression, SnippetMathOperatorEnum.DIVISION)) { let multiply = true; if (expression.indexOf(SnippetMathOperatorEnum.MULTIPLICATION) < expression.indexOf(SnippetMathOperatorEnum.DIVISION)) { multiply = hasOperator(expression, SnippetMathOperatorEnum.MULTIPLICATION); } else { multiply = !hasOperator(expression, SnippetMathOperatorEnum.DIVISION); } if (multiply) { expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.MULTIPLICATION); } else { expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.DIVISION); } } while (hasOperator(expression, SnippetMathOperatorEnum.ADDITION)) { expression = deriveExpressionValueFragment(expression, SnippetMathOperatorEnum.ADDITION); } return toNumber(expression); } /** * * @param tagExpression * @param contextData * @param snippetData * @param proficiencyBonus */ export function generateFragmentExpression(tagExpression, contextData, snippetData, proficiencyBonus) { let hasSignedValue = false; let updatedExpression = tagExpression; let match; while ((match = updatedExpression.match(tagValueRegExpr))) { const tagRaw = match[0]; const tagValue = match[1]; const tagValueModifier = match[2]; const snippetTagValue = parseSnippetTagValue(tagValue); if (!hasSignedValue) { hasSignedValue = isSignedTagValueType(snippetTagValue.type); } let value = deriveNumericTagValue(snippetTagValue, contextData, snippetData, proficiencyBonus); if (tagValueModifier) { value = applyValueModifierString(value, tagValueModifier); } updatedExpression = updatedExpression.replace(tagRaw, value.toString()); } return { original: tagExpression, expression: updatedExpression, hasSignedValue, }; } /** * * @param rawSnippetTag * @param contextData * @param snippetData * @param proficiencyBonus */ export function generateSnippetTag(rawSnippetTag, contextData, snippetData, proficiencyBonus) { const { expression, postProcess } = parseRawTagInfo(deriveFormattedTag(rawSnippetTag)); const snippetBaseTag = { raw: rawSnippetTag, }; if (isValidStringTagValueType(rawSnippetTag, contextData)) { const snippetTagValue = parseSnippetTagValue(rawSnippetTag); return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.STRING, value: deriveStringTagValue(snippetTagValue, contextData) }); } throwMismatchedParens(rawSnippetTag); throwInvalidSnippetCharacters(rawSnippetTag); let hasSignedValue = false; let updatedExpression = expression; // solve grouped parens first let match; while ((match = updatedExpression.match(groupFragmentRegExpr))) { let groupFragmentRaw = match[0]; let groupFragmentExpression = match[1]; let groupFragmentValueModifier = match[2]; let tagFragmentExpression = generateFragmentExpression(groupFragmentExpression, contextData, snippetData, proficiencyBonus); if (!hasSignedValue) { hasSignedValue = tagFragmentExpression.hasSignedValue; } let value = deriveExpression(tagFragmentExpression.expression); if (groupFragmentValueModifier) { value = applyValueModifierString(value, groupFragmentValueModifier); } updatedExpression = updatedExpression.replace(groupFragmentRaw, value.toString()); } let fragmentExpression = generateFragmentExpression(updatedExpression, contextData, snippetData, proficiencyBonus); let value = deriveExpression(fragmentExpression.expression); if (!hasSignedValue) { hasSignedValue = fragmentExpression.hasSignedValue; } value = applyPostProcessString(value, postProcess, hasSignedValue); if (typeof value === 'number') { return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.NUMBER, value }); } else { return Object.assign(Object.assign({}, snippetBaseTag), { type: SnippetTagDataTypeEnum.STRING, value }); } } /** * * @param id */ export function generateSnippetTagPlaceholder(id) { return `__SNIPPET_PLACEHOLDER_${id}__`; } /** * * @param snippetTag */ export function generateSnippetContentTagChunk(snippetTag) { return { type: SnippetContentChunkTypeEnum.TAG, data: snippetTag, }; } /** * * @param str */ export function generateSnippetContentTextChunk(str) { return { type: SnippetContentChunkTypeEnum.TEXT, data: str, }; } /** * * @param snippetString * @param placeholders * @param placeholderLookup */ export function generateSnippetContentChunks(snippetString, placeholders, placeholderLookup) { let chunks = []; placeholders.forEach((placeholder) => { let strIdx = snippetString.indexOf(placeholder); if (strIdx > -1) { let prePlaceholder = snippetString.slice(0, strIdx); let postPlaceholder = snippetString.slice(strIdx + placeholder.length); if (prePlaceholder) { chunks = [...generateSnippetContentChunks(prePlaceholder, placeholders, placeholderLookup)]; } chunks = [...chunks, generateSnippetContentTagChunk(placeholderLookup[placeholder])]; if (postPlaceholder) { chunks = [...chunks, ...generateSnippetContentChunks(postPlaceholder, placeholders, placeholderLookup)]; } } }); if (!chunks.length) { chunks.push(generateSnippetContentTextChunk(snippetString)); } return chunks; } /** * * @param snippetString * @param contextData * @param snippetData * @param proficiencyBonus */ export function generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus) { let match; let placeholderSnippetString = snippetString; let placeholders = []; let rawSnippetTags = []; // find all snippet tags and put placeholders while ((match = placeholderSnippetString.match(/\{\{([^\}]*)\}\}/))) { const rawSnippetTag = match[0]; const rawSnippetTagContent = match[1]; const placeholder = generateSnippetTagPlaceholder(rawSnippetTags.length); rawSnippetTags.push(rawSnippetTagContent); placeholders.push(placeholder); placeholderSnippetString = placeholderSnippetString.split(rawSnippetTag).join(placeholder); } // parse all snippet tags that were found let placeholderLookup = {}; rawSnippetTags.forEach((rawSnippetTag, idx) => { const key = generateSnippetTagPlaceholder(idx); try { placeholderLookup[key] = generateSnippetTag(rawSnippetTag, contextData, snippetData, proficiencyBonus); } catch (error) { const errorSnippetTag = { type: SnippetTagDataTypeEnum.ERROR, raw: rawSnippetTag, value: error.message, }; placeholderLookup[key] = errorSnippetTag; } }); // break apart the original string into chunks return generateSnippetContentChunks(placeholderSnippetString, placeholders, placeholderLookup); } /** * * @param snippetString * @param contextData * @param snippetData * @param outputHtml * @param proficiencyBonus * @deprecated this will be moving to character component Snippet * */ export function convertSnippetToHtml(snippetString, contextData, snippetData, outputHtml = true, proficiencyBonus) { if (snippetString === null) { return null; } const snippetChunks = generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus); const htmlChunkString = snippetChunks .map((contentChunk) => { switch (contentChunk.type) { case SnippetContentChunkTypeEnum.TAG: switch (contentChunk.data.type) { case SnippetTagDataTypeEnum.ERROR: const errorMessage = `${contentChunk.data.raw} - ${contentChunk.data.value}`; return `${errorMessage}`; case SnippetTagDataTypeEnum.NUMBER: return `${contentChunk.data.value.toLocaleString()}`; case SnippetTagDataTypeEnum.STRING: default: return `${contentChunk.data.value}`; } case SnippetContentChunkTypeEnum.TEXT: default: return contentChunk.data; } }) .join(''); return `

${htmlChunkString.replace(/\s*\n+?\s*\n*/g, '

')}

`; } /** * * @param snippetString * @param contextData * @param snippetData * @param proficiencyBonus */ export function convertSnippetToText(snippetString, contextData, snippetData, proficiencyBonus) { if (snippetString === null) { return null; } const snippetChunks = generateSnippetChunks(snippetString, contextData, snippetData, proficiencyBonus); return snippetChunks .map((contentChunk) => { switch (contentChunk.type) { case SnippetContentChunkTypeEnum.TAG: switch (contentChunk.data.type) { case SnippetTagDataTypeEnum.ERROR: let messageParts = []; if (contentChunk.data.raw.trim()) { messageParts.push(contentChunk.data.raw); } messageParts.push(contentChunk.data.value); return messageParts.join(' - '); case SnippetTagDataTypeEnum.NUMBER: return contentChunk.data.value.toLocaleString(); case SnippetTagDataTypeEnum.STRING: default: return contentChunk.data.value; } case SnippetContentChunkTypeEnum.TEXT: return contentChunk.data; default: // not implemented } }) .join(''); }