diff --git a/package.json b/package.json index 2af14c5..d2b65a2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "devDependencies": { "@types/chai": "^4.3.4", "@types/lodash": "^4.14.199", - "@types/mocha": "^10.0.1", + "@types/mocha": "^10.0.2", "@types/pluralize": "^0.0.31", "@typescript-eslint/eslint-plugin": "^5.12.1", "@typescript-eslint/parser": "^5.12.1", diff --git a/src/getVariablesFromHandlebarsTemplate.ts b/src/getVariablesFromHandlebarsTemplate.ts new file mode 100644 index 0000000..889cdde --- /dev/null +++ b/src/getVariablesFromHandlebarsTemplate.ts @@ -0,0 +1,116 @@ +import handlebars from 'handlebars'; + +/** + * Parse AST for variables + * + * @param statement - Statement to parse + * @returns Variables + */ +function parseHandlebarsAst(statement: hbs.AST.Statement): { + [k in string]: unknown; +} { + // No variables + if (statement.type === 'ContentStatement') { + return {}; + } + + if (statement.type === 'PartialStatement') { + const moustacheStatement = statement as hbs.AST.PartialStatement; + const pathStatement = moustacheStatement.name as hbs.AST.PathExpression; + return { + [pathStatement.original]: 'partial', + }; + } + + // Parse variables from {{ var }} + if (statement.type === 'MustacheStatement') { + const moustacheStatement = statement as hbs.AST.MustacheStatement; + const paramsExpressionList = + moustacheStatement.params as hbs.AST.PathExpression[]; + const pathExpression = moustacheStatement.path as hbs.AST.PathExpression; + const vars = [ + ...paramsExpressionList.map(({ original }) => original), + pathExpression.original, + ].filter((x) => !!x); + return vars.reduce((acc, x) => Object.assign(acc, { [x]: null }), {}); + } + + // Parse from {{#each}} or {{#with}} + if (statement.type === 'BlockStatement' && statement) { + const moustacheStatement = statement as hbs.AST.MustacheStatement; + const paramsExpressionList = + moustacheStatement.params as hbs.AST.PathExpression[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const program = (moustacheStatement as any).program as hbs.AST.Program; + const param = paramsExpressionList[0] as unknown as + | hbs.AST.PathExpression + | hbs.AST.SubExpression; + const pathExpression = moustacheStatement.path as hbs.AST.PathExpression; + if (param.type === 'SubExpression') { + return program.body + .map(parseHandlebarsAst) + .reduce((acc, obj) => Object.assign(acc, obj), {}); + } + + if (pathExpression.original === 'each') { + return { + [param.original]: [ + program.body + .map(parseHandlebarsAst) + .reduce((acc, obj) => Object.assign(acc, obj), {}), + ], + }; + } + + return { + [param.original]: program.body + .map(parseHandlebarsAst) + .reduce((acc, obj) => Object.assign(acc, obj), {}), + }; + } + throw new Error(`Unknown statement: ${statement.type}`); +} + +/** + * Get variables from handlebars template + * + * @param template - Template + * @returns Variables + */ +export function getVariablesFromHandlebarsTemplate(template: string): { + [k in string]: unknown; +} { + const ast = handlebars.parseWithoutProcessing(template); + + const results = ast.body.map(parseHandlebarsAst); + return results.reduce((acc, data) => { + Object.entries(data).forEach(([k, v]) => { + const existing = acc[k]; + if (!existing) { + return Object.assign(acc, { [k]: v }); + } + if (Array.isArray(existing) && Array.isArray(v)) { + return Object.assign(acc, { + [k]: [ + { + ...existing[0], + ...v[0], + }, + ], + }); + } + if (typeof existing === 'object' && typeof v === 'object') { + return Object.assign(acc, { + [k]: { + ...existing, + ...v, + }, + }); + } + return Object.assign(acc, { + [k]: v, + }); + }); + return acc; + }, {}); +} diff --git a/src/index.ts b/src/index.ts index 2cf9aef..7123dc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './change-case'; export * from './createHandlebars'; +export * from './getVariablesFromHandlebarsTemplate'; diff --git a/src/tests/getVariablesFromHandlebarsTemplate.test.ts b/src/tests/getVariablesFromHandlebarsTemplate.test.ts new file mode 100644 index 0000000..9ef042e --- /dev/null +++ b/src/tests/getVariablesFromHandlebarsTemplate.test.ts @@ -0,0 +1,61 @@ +import { expect } from 'chai'; + +import { getVariablesFromHandlebarsTemplate } from '../index'; + +const TEST_HBS = ` +

+ You are an experienced project manager, constantly juggling a variety of different tasks. + Other employees at your company are very busy. + They quickly record notes in slack of things that they want to look into later. + You are tasked with reviewing those raw notes, and turning them into a structured + form that will make the notes more easily indexable by the employee when they have more time to review the information. + {{{ description }}}. Return a JSON object in the following format: + + ({{#each parameters}}{{ name }} - {{slug}}, {{/each}}clarification). +

+ +{{> promptPartialTodaysDate }} + +{{> promptPartialTranscendProducts }} + + +{{#with dog}}{{cat}} - {{fish}}{{/with}} + +{{#if (listLen extraContext)}} +{{#each extraContext}} +{{ name }} +{{/each}} +{{/if}} + +

+ The following rules define each of the input parameters: +

+

+ +{{#with dog}} {{meow}}{{/with}} + +

+ If any of the parameters are not known, it should be set to null. + If anything is not clear, return a prompt in the clarification key of the response asking for further detail +

`; + +describe('getVariablesFromHandlebars', () => { + it('should merge together', () => { + expect(getVariablesFromHandlebarsTemplate(TEST_HBS)).to.deep.equal({ + description: null, + parameters: [{ name: null, slug: null, description: null }], + dog: { cat: null, fish: null, meow: null }, + extraContext: [ + { + name: null, + }, + ], + promptPartialTodaysDate: 'partial', + promptPartialTranscendProducts: 'partial', + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 71525f2..57512f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -309,7 +309,7 @@ __metadata: "@transcend-io/type-utils": ^1.1.1 "@types/chai": ^4.3.4 "@types/lodash": ^4.14.199 - "@types/mocha": ^10.0.1 + "@types/mocha": ^10.0.2 "@types/pluralize": ^0.0.31 "@typescript-eslint/eslint-plugin": ^5.12.1 "@typescript-eslint/parser": ^5.12.1 @@ -441,7 +441,7 @@ __metadata: languageName: node linkType: hard -"@types/mocha@npm:^10.0.1": +"@types/mocha@npm:^10.0.2": version: 10.0.2 resolution: "@types/mocha@npm:10.0.2" checksum: a78a02691f102beb02f9ec435458107d21b518fc477c3b2f37c90b8e70b67bff888351715ae173bd31ede25ee5e0d688aefb0faf4284034d08ba63027c8b0c01