mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Basic setup for MacroParser + initial tests
This commit is contained in:
@@ -244,6 +244,7 @@ import { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/Slash
|
|||||||
import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
|
import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||||
import { MacroLexer } from './scripts/macros/MacroLexer.js';
|
import { MacroLexer } from './scripts/macros/MacroLexer.js';
|
||||||
import { MacroEngine } from './scripts/macros/MacroEngine.js';
|
import { MacroEngine } from './scripts/macros/MacroEngine.js';
|
||||||
|
import { MacroParser } from './scripts/macros/MacroParser.js';
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
@@ -7971,7 +7972,7 @@ window['SillyTavern'].getContext = function () {
|
|||||||
POPUP_RESULT: POPUP_RESULT,
|
POPUP_RESULT: POPUP_RESULT,
|
||||||
macros: {
|
macros: {
|
||||||
MacroLexer,
|
MacroLexer,
|
||||||
MacrosParser,
|
MacroParser,
|
||||||
MacroEngine,
|
MacroEngine,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -16,11 +16,38 @@ class MacroParser extends CstParser {
|
|||||||
/** @private */
|
/** @private */
|
||||||
constructor() {
|
constructor() {
|
||||||
super(MacroLexer.def);
|
super(MacroLexer.def);
|
||||||
|
const Tokens = MacroLexer.tokens;
|
||||||
|
|
||||||
// const $ = this;
|
const $ = this;
|
||||||
|
|
||||||
|
this.macro = $.RULE("macro", () => {
|
||||||
|
$.CONSUME(Tokens.Macro.Start);
|
||||||
|
$.CONSUME(Tokens.Macro.Identifier);
|
||||||
|
$.OPTION(() => $.SUBRULE($.arguments));
|
||||||
|
$.CONSUME(Tokens.Macro.End);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.arguments = $.RULE("arguments", () => {
|
||||||
|
$.CONSUME(Tokens.Identifier);
|
||||||
|
});
|
||||||
|
|
||||||
this.performSelfAnalysis();
|
this.performSelfAnalysis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test(input) {
|
||||||
|
const lexingResult = MacroLexer.tokenize(input);
|
||||||
|
// "input" is a setter which will reset the parser's state.
|
||||||
|
this.input = lexingResult.tokens;
|
||||||
|
const node = this.macro();
|
||||||
|
|
||||||
|
|
||||||
|
if (this.errors.length > 0) {
|
||||||
|
console.error('Parser errors', this.errors);
|
||||||
|
throw new Error('Parser errors found\n' + this.errors.map(x => x.message).join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instance = MacroParser.instance;
|
instance = MacroParser.instance;
|
||||||
|
@@ -2,13 +2,13 @@
|
|||||||
/** @typedef {import('../../public/lib/chevrotain.js').ILexingError} ILexingError */
|
/** @typedef {import('../../public/lib/chevrotain.js').ILexingError} ILexingError */
|
||||||
/** @typedef {{type: string, text: string}} TestableToken */
|
/** @typedef {{type: string, text: string}} TestableToken */
|
||||||
|
|
||||||
|
// Those tests ar evaluating via puppeteer, the need more time to run and finish
|
||||||
|
jest.setTimeout(10_000);
|
||||||
|
|
||||||
describe('MacroLexer', () => {
|
describe('MacroLexer', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await page.goto(global.ST_URL);
|
await page.goto(global.ST_URL);
|
||||||
await page.waitForFunction('document.getElementById("preloader") === null', { timeout: 0 });
|
await page.waitForFunction('document.getElementById("preloader") === null', { timeout: 0 });
|
||||||
|
|
||||||
// Those tests ar evaluating via puppeteer, the need more time to run and finish
|
|
||||||
jest.setTimeout(10_000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('General Macro', () => {
|
describe('General Macro', () => {
|
||||||
@@ -1114,19 +1114,18 @@ async function runLexerGetTokensAndErrors(input) {
|
|||||||
return result;
|
return result;
|
||||||
}, input);
|
}, input);
|
||||||
|
|
||||||
return getTestableTokens(result);
|
return simplifyTokens(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Simplify the lexer tokens result into an easily testable format.
|
||||||
*
|
*
|
||||||
* @param {ILexingResult} result The result from the lexer
|
* @param {ILexingResult} result The result from the lexer
|
||||||
* @returns {{tokens: TestableToken[], errors: ILexingError[]}} The tokens
|
* @returns {{tokens: TestableToken[], errors: ILexingError[]}} The tokens
|
||||||
*/
|
*/
|
||||||
function getTestableTokens(result) {
|
function simplifyTokens(result) {
|
||||||
const errors = result.errors;
|
const errors = result.errors;
|
||||||
const tokens = result.tokens
|
const tokens = result.tokens
|
||||||
// Filter out the mode popper. We don't care aobut that for testing
|
|
||||||
//.filter(token => !['ModePopper', 'BeforeEnd'].includes(token.tokenType.name))
|
|
||||||
// Extract relevant properties from tokens for comparison
|
// Extract relevant properties from tokens for comparison
|
||||||
.map(token => ({
|
.map(token => ({
|
||||||
type: token.tokenType.name,
|
type: token.tokenType.name,
|
||||||
|
120
tests/frontend/MacroParser.test.js
Normal file
120
tests/frontend/MacroParser.test.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/** @typedef {import('../../public/lib/chevrotain.js').CstNode} CstNode */
|
||||||
|
|
||||||
|
// Those tests ar evaluating via puppeteer, the need more time to run and finish
|
||||||
|
jest.setTimeout(10_000);
|
||||||
|
|
||||||
|
describe('MacroParser', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await page.goto(global.ST_URL);
|
||||||
|
await page.waitForFunction('document.getElementById("preloader") === null', { timeout: 0 });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('General Macro', () => {
|
||||||
|
// {{user}}
|
||||||
|
it('should parse a simple macro', async () => {
|
||||||
|
const input = '{{user}}';
|
||||||
|
const cst = await runParser(input);
|
||||||
|
|
||||||
|
const expectedCst = {
|
||||||
|
'Macro.Start': '{{',
|
||||||
|
'Macro.Identifier': 'user',
|
||||||
|
'Macro.End': '}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cst).toEqual(expectedCst);
|
||||||
|
});
|
||||||
|
// {{ user }}
|
||||||
|
it('should generally handle whitespaces', async () => {
|
||||||
|
const input = '{{ user }}';
|
||||||
|
const cst = await runParser(input);
|
||||||
|
|
||||||
|
const expectedCst = {
|
||||||
|
'Macro.Start': '{{',
|
||||||
|
'Macro.Identifier': 'user',
|
||||||
|
'Macro.End': '}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cst).toEqual(expectedCst);
|
||||||
|
});
|
||||||
|
// {{ macro value }}
|
||||||
|
it('should only read one identifier and treat the rest as arguments', async () => {
|
||||||
|
const input = '{{ macro value }}';
|
||||||
|
const cst = await runParser(input);
|
||||||
|
|
||||||
|
const expectedCst = {
|
||||||
|
'Macro.Start': '{{',
|
||||||
|
'Macro.Identifier': 'macro',
|
||||||
|
'arguments': { 'Identifier': 'value' },
|
||||||
|
'Macro.End': '}}',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(cst).toEqual(expectedCst);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Error Cases (General Macro', () => {
|
||||||
|
// {{}}
|
||||||
|
it('[Error] should throw an error for empty macro', () => {
|
||||||
|
// TODO:
|
||||||
|
});
|
||||||
|
// {{§!#&blah}}
|
||||||
|
it('[Error] should throw an error for invalid identifier', () => {
|
||||||
|
// TODO:
|
||||||
|
});
|
||||||
|
// {{user
|
||||||
|
it('[Error] should throw an error for incomplete macro', () => {
|
||||||
|
// TODO:
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the input through the MacroParser and returns the result.
|
||||||
|
*
|
||||||
|
* @param {string} input - The input string to be parsed.
|
||||||
|
* @return {Promise<TestableCstNode>} A promise that resolves to the result of the MacroParser.
|
||||||
|
*/
|
||||||
|
async function runParser(input) {
|
||||||
|
const cst = await page.evaluate(async (input) => {
|
||||||
|
/** @type {import('../../public/scripts/macros/MacroParser.js')} */
|
||||||
|
const { MacroParser } = await import('./scripts/macros/MacroParser.js');
|
||||||
|
|
||||||
|
const cst = MacroParser.test(input);
|
||||||
|
return cst;
|
||||||
|
}, input);
|
||||||
|
|
||||||
|
return simplifyCstNode(cst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @typedef {{[tokenName: string]: (string|string[]|TestableCstNode|TestableCstNode[])}} TestableCstNode */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simplify the parser syntax tree result into an easily testable format.
|
||||||
|
*
|
||||||
|
* @param {CstNode} result The result from the parser
|
||||||
|
* @returns {TestableCstNode} The testable syntax tree
|
||||||
|
*/
|
||||||
|
function simplifyCstNode(cst) {
|
||||||
|
/** @returns {TestableCstNode} @param {CstNode} node */
|
||||||
|
function simplifyNode(node) {
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
// Single-element arrays are converted to a single string
|
||||||
|
if (node.length === 1) {
|
||||||
|
return node[0].image || simplifyNode(node[0]);
|
||||||
|
}
|
||||||
|
// For multiple elements, return an array of simplified nodes
|
||||||
|
return node.map(simplifyNode);
|
||||||
|
}
|
||||||
|
if (node.children) {
|
||||||
|
const simplifiedChildren = {};
|
||||||
|
for (const key in node.children) {
|
||||||
|
simplifiedChildren[key] = simplifyNode(node.children[key]);
|
||||||
|
}
|
||||||
|
return simplifiedChildren;
|
||||||
|
}
|
||||||
|
return node.image;
|
||||||
|
}
|
||||||
|
|
||||||
|
return simplifyNode(cst);
|
||||||
|
}
|
Reference in New Issue
Block a user