|
|
import { test, expect } from '@playwright/test'; |
|
|
import { testSetup } from './frontent-test-utils.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_FLATTEN_KEYS = [ |
|
|
'arguments.Args.DoubleColon', |
|
|
]; |
|
|
const DEFAULT_IGNORE_KEYS = [ |
|
|
|
|
|
]; |
|
|
|
|
|
test.describe('MacroParser', () => { |
|
|
|
|
|
test.beforeEach(testSetup.goST); |
|
|
|
|
|
test.describe('General Macro', () => { |
|
|
|
|
|
test('should parse a simple macro', async ({ page }) => { |
|
|
const input = '{{user}}'; |
|
|
const macroCst = await runParser(page, input); |
|
|
|
|
|
const expectedCst = { |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'user', |
|
|
'Macro.End': '}}', |
|
|
}; |
|
|
|
|
|
expect(macroCst).toEqual(expectedCst); |
|
|
}); |
|
|
|
|
|
test('should generally handle whitespaces', async ({ page }) => { |
|
|
const input = '{{ user }}'; |
|
|
const macroCst = await runParser(page, input); |
|
|
|
|
|
const expectedCst = { |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'user', |
|
|
'Macro.End': '}}', |
|
|
}; |
|
|
|
|
|
expect(macroCst).toEqual(expectedCst); |
|
|
}); |
|
|
|
|
|
test.describe('Error Cases (General Macro)', () => { |
|
|
|
|
|
test('[Error] should throw an error for empty macro', async ({ page }) => { |
|
|
const input = '{{}}'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
const expectedErrors = [ |
|
|
{ name: 'NoViableAltException' }, |
|
|
]; |
|
|
const expectedMessage = /Expecting: one of these possible Token sequences:(.*?)\[Macro\.Identifier\](.*?)but found: '}}'/gs; |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toMatchObject(expectedErrors); |
|
|
expect(errors[0].message).toMatch(expectedMessage); |
|
|
}); |
|
|
|
|
|
test('[Error] should throw an error for invalid identifier', async ({ page }) => { |
|
|
const input = '{{§!#&blah}}'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
const expectedErrors = [ |
|
|
{ name: 'NoViableAltException' }, |
|
|
]; |
|
|
const expectedMessage = /Expecting: one of these possible Token sequences:(.*?)\[Macro\.Identifier\](.*?)but found: '!'/gs; |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toMatchObject(expectedErrors); |
|
|
expect(errors[0].message).toMatch(expectedMessage); |
|
|
}); |
|
|
|
|
|
test('[Error] should throw an error for incomplete macro', async ({ page }) => { |
|
|
const input = '{{user'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
const expectedErrors = [ |
|
|
{ name: 'MismatchedTokenException', message: 'Expecting token of type --> Macro.End <-- but found --> \'\' <--' }, |
|
|
]; |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toEqual(expectedErrors); |
|
|
}); |
|
|
|
|
|
|
|
|
test('[Error] for testing purposes, macros need to start at the beginning of the string', async ({ page }) => { |
|
|
const input = 'something{{user}}'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
const expectedErrors = [ |
|
|
{ name: 'MismatchedTokenException', message: 'Expecting token of type --> Macro.Start <-- but found --> \'something\' <--' }, |
|
|
]; |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toEqual(expectedErrors); |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
test.describe('Arguments Handling', () => { |
|
|
|
|
|
test('should parse macros with double-colon argument', async ({ page }) => { |
|
|
const input = '{{getvar::myvar}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'getvar', |
|
|
'arguments': { |
|
|
'separator': '::', |
|
|
'argument': 'myvar', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse macros with single colon argument', async ({ page }) => { |
|
|
const input = '{{roll:3d20}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'roll', |
|
|
'arguments': { |
|
|
'separator': ':', |
|
|
'argument': '3d20', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse macros with multiple double-colon arguments', async ({ page }) => { |
|
|
const input = '{{setvar::myvar::value}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
ignoreKeys: ['arguments.Args.DoubleColon'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'setvar', |
|
|
'arguments': { |
|
|
'separator': '::', |
|
|
'argument': ['myvar', 'value'], |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should strip spaces around arguments', async ({ page }) => { |
|
|
const input = '{{something:: spaced }}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
ignoreKeys: ['arguments.separator', 'arguments.Args.DoubleColon'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'something', |
|
|
'arguments': { 'argument': 'spaced' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should treat single colons as part of the argument with double-colon separator', async ({ page }) => { |
|
|
const input = '{{something::with:single:colons}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
ignoreKeys: ['arguments.Args.DoubleColon'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'something', |
|
|
'arguments': { |
|
|
'separator': '::', |
|
|
'argument': 'with:single:colons', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should treat single colons as part of the argument even with colon separator', async ({ page }) => { |
|
|
const input = '{{legacy:something:else}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
ignoreKeys: ['arguments.separator', 'arguments.Args.Colon'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'legacy', |
|
|
'arguments': { 'argument': 'something:else' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse double-colon with an empty argument value', async ({ page }) => { |
|
|
const input = '{{something::}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'something', |
|
|
'arguments': { |
|
|
'separator': '::', |
|
|
'argument': '', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
test.describe('Legacy Macros', () => { |
|
|
|
|
|
test('should parse legacy roll macro with whitespace separator', async ({ page }) => { |
|
|
const input = '{{roll 1d5}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'roll', |
|
|
'arguments': { 'argument': '1d5' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse legacy roll macro with explicit colon separator', async ({ page }) => { |
|
|
const input = '{{roll:2d20}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'roll', |
|
|
'arguments': { |
|
|
'separator': ':', |
|
|
'argument': '2d20', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse legacy roll macro with numeric argument', async ({ page }) => { |
|
|
const input = '{{roll 20}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'roll', |
|
|
'arguments': { 'argument': '20' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse reverse legacy macro with colon argument', async ({ page }) => { |
|
|
const input = '{{reverse:something}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'reverse', |
|
|
'arguments': { |
|
|
'separator': ':', |
|
|
'argument': 'something', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse legacy single colon argument that allows double colons inside the argument', async ({ page }) => { |
|
|
const input = '{{reverse:this contains::double::colons}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'reverse', |
|
|
'arguments': { |
|
|
'separator': ':', |
|
|
'argument': 'this contains::double::colons', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
test('should parse legacy comment macro', async ({ page }) => { |
|
|
const input = '{{//comment-style macro}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'arguments': { 'argument': 'comment-style macro' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse legacy datetime format macro', async ({ page }) => { |
|
|
const input = '{{datetimeformat HH:mm}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'datetimeformat', |
|
|
'arguments': { 'argument': 'HH:mm' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should parse legacy banned macro with quoted argument', async ({ page }) => { |
|
|
const input = '{{banned "abannedword"}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'banned', |
|
|
'arguments': { 'argument': '"abannedword"' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse legacy macro with empty quoted argument', async ({ page }) => { |
|
|
const input = '{{banned ""}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'banned', |
|
|
'arguments': { 'argument': '""' }, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should allow legacy setvar with empty value argument', async ({ page }) => { |
|
|
const input = '{{setvar::myvar::}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
|
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'setvar', |
|
|
'arguments': { |
|
|
'separator': '::', |
|
|
'argument': ['myvar', ''], |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
}); |
|
|
|
|
|
test.describe('Comment Macros', () => { |
|
|
|
|
|
test('should parse comment macro without whitespace', async ({ page }) => { |
|
|
const input = '{{//comment}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'Macro.End': '}}', |
|
|
'arguments': { |
|
|
'argument': 'comment', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse comment macro with whitespace', async ({ page }) => { |
|
|
const input = '{{// comment}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'Macro.End': '}}', |
|
|
'arguments': { |
|
|
'argument': 'comment', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
test('should parse comment macro with special characters', async ({ page }) => { |
|
|
const input = '{{//!@#$%^&*()_+}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'Macro.End': '}}', |
|
|
'arguments': { |
|
|
'argument': '!@#$%^&*()_+', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
test('should parse comment macro starting with flags', async ({ page }) => { |
|
|
const input = '{{//!@flags}}'; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'Macro.End': '}}', |
|
|
'arguments': { |
|
|
'argument': '!@flags', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test('should parse multiline comments', async ({ page }) => { |
|
|
const input = `{{// This is a multiline comment. |
|
|
This is the second line |
|
|
}}`; |
|
|
const macroCst = await runParser(page, input, { |
|
|
flattenKeys: ['arguments.argument'], |
|
|
}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': '//', |
|
|
'Macro.End': '}}', |
|
|
'arguments': { |
|
|
'argument': 'This is a multiline comment.\nThis is the second line', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
}); |
|
|
|
|
|
test.describe('Nested Macros', () => { |
|
|
|
|
|
test('should parse nested macros inside arguments', async ({ page }) => { |
|
|
const input = '{{outer::word {{inner}}}}'; |
|
|
const macroCst = await runParser(page, input, {}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'outer', |
|
|
'arguments': { |
|
|
'argument': { |
|
|
'Identifier': 'word', |
|
|
'macro': { |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'inner', |
|
|
'Macro.End': '}}', |
|
|
}, |
|
|
}, |
|
|
'separator': '::', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
test('should parse two nested macros next to each other inside an argument', async ({ page }) => { |
|
|
const input = '{{outer::word {{inner1}}{{inner2}}}}'; |
|
|
const macroCst = await runParser(page, input, {}); |
|
|
expect(macroCst).toEqual({ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'outer', |
|
|
'arguments': { |
|
|
'argument': { |
|
|
'Identifier': 'word', |
|
|
'macro': [ |
|
|
{ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'inner1', |
|
|
'Macro.End': '}}', |
|
|
}, |
|
|
{ |
|
|
'Macro.Start': '{{', |
|
|
'Macro.identifier': 'inner2', |
|
|
'Macro.End': '}}', |
|
|
}, |
|
|
], |
|
|
}, |
|
|
'separator': '::', |
|
|
}, |
|
|
'Macro.End': '}}', |
|
|
}); |
|
|
}); |
|
|
|
|
|
test.describe('Error Cases (Nested Macros)', () => { |
|
|
|
|
|
test('[Error] should throw when there is a nested macro instead of an identifier', async ({ page }) => { |
|
|
const input = '{{{{macroindentifier}}::value}}'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toHaveLength(1); |
|
|
}); |
|
|
|
|
|
|
|
|
test('[Error] should throw when there is a macro inside an identifier', async ({ page }) => { |
|
|
const input = '{{inside{{macro}}me}}'; |
|
|
const { macroCst, errors } = await runParserAndGetErrors(page, input); |
|
|
|
|
|
expect(macroCst).toBeUndefined(); |
|
|
expect(errors).toHaveLength(1); |
|
|
}); |
|
|
|
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function runParser(page, input, options = {}) { |
|
|
const { cst, errors } = await runParserAndGetErrors(page, input, options); |
|
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) { |
|
|
throw new Error('Parser errors found\n' + errors.map(x => x.message).join('\n')); |
|
|
} |
|
|
|
|
|
return cst; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function runParserAndGetErrors(page, input, options = {}) { |
|
|
const params = { input, options }; |
|
|
const { result } = await page.evaluate(async ({ input, options }) => { |
|
|
|
|
|
const { MacroParser } = await import('./scripts/macros/engine/MacroParser.js'); |
|
|
const result = MacroParser.test(input); |
|
|
return { result }; |
|
|
}, params); |
|
|
return { cst: simplifyCstNode(result.cst, input, options), errors: simplifyErrors(result.errors) }; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function simplifyCstNode(cst, input, { flattenKeys = [], ignoreKeys = [], ignoreDefaultFlattenKeys = false, ignoreDefaultIgnoreKeys = false } = {}) { |
|
|
if (!ignoreDefaultFlattenKeys) flattenKeys = [...flattenKeys, ...DEFAULT_FLATTEN_KEYS]; |
|
|
if (!ignoreDefaultIgnoreKeys) ignoreKeys = [...ignoreKeys, ...DEFAULT_IGNORE_KEYS]; |
|
|
|
|
|
|
|
|
function simplifyNode(node, path = []) { |
|
|
if (!node) return node; |
|
|
if (Array.isArray(node)) { |
|
|
|
|
|
if (node.length === 1) { |
|
|
return node[0].image || simplifyNode(node[0], path.concat('[]')); |
|
|
} |
|
|
|
|
|
return node.map(child => simplifyNode(child, path.concat('[]'))); |
|
|
} |
|
|
if (node.children) { |
|
|
const simplifiedChildren = {}; |
|
|
for (const key in node.children) { |
|
|
function simplifyChildNode(childNode, path) { |
|
|
if (Array.isArray(childNode)) { |
|
|
|
|
|
if (childNode.length === 1) { |
|
|
return simplifyChildNode(childNode[0], path.concat('[]')); |
|
|
} |
|
|
return childNode.map(child => simplifyChildNode(child, path.concat('[]'))); |
|
|
} |
|
|
|
|
|
const flattenKey = path.filter(x => x !== '[]').join('.'); |
|
|
if (ignoreKeys.includes(flattenKey)) { |
|
|
return null; |
|
|
} else if (flattenKeys.includes(flattenKey)) { |
|
|
if (!childNode.location) return null; |
|
|
const startOffset = childNode.location.startOffset; |
|
|
const endOffset = childNode.location.endOffset; |
|
|
return input.slice(startOffset, endOffset + 1); |
|
|
} else { |
|
|
return simplifyNode(childNode, path); |
|
|
} |
|
|
} |
|
|
|
|
|
const simplifiedValue = simplifyChildNode(node.children[key], path.concat(key)); |
|
|
if (simplifiedValue !== null) simplifiedChildren[key] = simplifiedValue; |
|
|
} |
|
|
if (Object.values(simplifiedChildren).length === 0) return null; |
|
|
return simplifiedChildren; |
|
|
} |
|
|
return node.image; |
|
|
} |
|
|
|
|
|
return simplifyNode(cst); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function simplifyErrors(errors) { |
|
|
return errors.map(exception => ({ |
|
|
name: exception.name, |
|
|
message: exception.message, |
|
|
})); |
|
|
} |
|
|
|