diff --git a/Composer/packages/client/src/utils/lgUtil.ts b/Composer/packages/client/src/utils/lgUtil.ts index 6f3e47618d..db0e5a40a7 100644 --- a/Composer/packages/client/src/utils/lgUtil.ts +++ b/Composer/packages/client/src/utils/lgUtil.ts @@ -133,25 +133,6 @@ export function getTemplate(content: string, templateName: string): LGTemplate | return resource.Templates.find(t => t.Name === templateName); } -/** - * - * @param text string - * -[Greeting], I'm a fancy bot, [Bye] ---> ['Greeting', 'Bye'] - * - */ -export function extractTemplateNames(text: string): string[] { - const templateNames: string[] = []; - // match a template name match a temlate func e.g. `showDate()` - // eslint-disable-next-line security/detect-unsafe-regex - const reg = /\[([A-Za-z_][-\w]+)(\(.*\))?\]/g; - let matchResult; - while ((matchResult = reg.exec(text)) !== null) { - const templateName = matchResult[1]; - templateNames.push(templateName); - } - return templateNames; -} - export function removeTemplate(content: string, templateName: string): string { const resource = LGParser.parse(content); return resource.deleteTemplate(templateName).toString(); diff --git a/Composer/packages/extensions/obiformeditor/src/Form/fields/TableField.tsx b/Composer/packages/extensions/obiformeditor/src/Form/fields/TableField.tsx index a693b0fd32..19ace0c609 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/fields/TableField.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/fields/TableField.tsx @@ -96,6 +96,7 @@ function ItemActions(props: ItemActionsProps) { const item = formData[index]; // @ts-ignore if (item.$type === 'Microsoft.SendActivity' && item.activity && item.activity.indexOf('bfdactivity-') !== -1) { + // TODO: (ze) 'removeLgTemplate' -> 'removeLgTemplateRef', it should accept inputs like '[bfdactivity-1234]' // @ts-ignore formContext.shellApi.removeLgTemplate('common', item.activity.slice(1, item.activity.length - 1)); } diff --git a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx index 923aaa5905..a446baa40b 100644 --- a/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx +++ b/Composer/packages/extensions/obiformeditor/src/Form/widgets/LgEditorWidget.tsx @@ -3,6 +3,7 @@ import React, { useState, useMemo } from 'react'; import { LgEditor } from '@bfc/code-editor'; +import { LgMetaData, LgTemplateRef } from '@bfc/shared'; import get from 'lodash/get'; import debounce from 'lodash/debounce'; @@ -14,16 +15,24 @@ const lspServerPath = '/lg-language-server'; const LG_HELP = 'https://github.com/microsoft/BotBuilder-Samples/blob/master/experimental/language-generation/docs/lg-file-format.md'; +const tryGetLgMetaDataType = (lgText: string): string | null => { + const lgRef = LgTemplateRef.parse(lgText); + if (lgRef === null) return null; + + const lgMetaData = LgMetaData.parse(lgRef.name); + if (lgMetaData === null) return null; + + return lgMetaData.type; +}; + const getInitialTemplate = (fieldName: string, formData?: string): string => { - let newTemplate = formData || '- '; + const lgText = formData || ''; - if (newTemplate.includes(`bfd${fieldName}-`)) { + // Field content is already a ref created by composer. + if (tryGetLgMetaDataType(lgText) === fieldName) { return ''; - } else if (newTemplate && !newTemplate.startsWith('-')) { - newTemplate = `-${newTemplate}`; } - - return newTemplate; + return lgText.startsWith('-') ? lgText : `- ${lgText}`; }; interface LgEditorWidgetProps { @@ -37,7 +46,7 @@ interface LgEditorWidgetProps { export const LgEditorWidget: React.FC = props => { const { formContext, name, value, height = 250 } = props; const [errorMsg, setErrorMsg] = useState(''); - const lgId = `bfd${name}-${formContext.dialogId}`; + const lgName = new LgMetaData(name, formContext.dialogId || '').toString(); const lgFileId = formContext.currentDialog.lgFile || 'common'; const lgFile = formContext.lgFiles && formContext.lgFiles.find(file => file.id === lgFileId); @@ -45,19 +54,19 @@ export const LgEditorWidget: React.FC = props => { () => debounce((body: string) => { formContext.shellApi - .updateLgTemplate(lgFileId, lgId, body) + .updateLgTemplate(lgFileId, lgName, body) .then(() => setErrorMsg('')) .catch(error => setErrorMsg(error)); }, 500), - [lgId, lgFileId] + [lgName, lgFileId] ); const template = (lgFile && lgFile.templates && lgFile.templates.find(template => { - return template.Name === lgId; + return template.Name === lgName; })) || { - Name: lgId, + Name: lgName, Body: getInitialTemplate(name, value), }; @@ -73,10 +82,10 @@ export const LgEditorWidget: React.FC = props => { if (formContext.dialogId) { if (body) { updateLgTemplate(body); - props.onChange(`[${lgId}]`); + props.onChange(new LgTemplateRef(lgName).toString()); } else { updateLgTemplate.flush(); - formContext.shellApi.removeLgTemplate(lgFileId, lgId); + formContext.shellApi.removeLgTemplate(lgFileId, lgName); props.onChange(); } } diff --git a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx index e71ab326cf..b061cb56a0 100644 --- a/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx +++ b/Composer/packages/extensions/visual-designer/src/editors/ObiEditor.tsx @@ -5,7 +5,7 @@ import { jsx } from '@emotion/core'; import { useContext, FC, useEffect, useState, useRef } from 'react'; import { MarqueeSelection, Selection } from 'office-ui-fabric-react/lib/MarqueeSelection'; -import { deleteAction, deleteActions } from '@bfc/shared'; +import { deleteAction, deleteActions, LgTemplateRef, LgMetaData } from '@bfc/shared'; import { NodeEventTypes } from '../constants/NodeEventTypes'; import { KeyboardCommandTypes, KeyboardPrimaryTypes } from '../constants/KeyboardCommandTypes'; @@ -49,12 +49,10 @@ export const ObiEditor: FC = ({ ); const deleteLgTemplates = (lgTemplates: string[]) => { - const lgPattern = /\[(bfd\w+-\d+)\]/; const normalizedLgTemplates = lgTemplates .map(x => { - const matches = lgPattern.exec(x); - if (matches && matches.length === 2) return matches[1]; - return ''; + const lgTemplateRef = LgTemplateRef.parse(x); + return lgTemplateRef ? lgTemplateRef.name : ''; }) .filter(x => !!x); return removeLgTemplates('common', normalizedLgTemplates); @@ -89,16 +87,19 @@ export const ObiEditor: FC = ({ if (eventData.$type === 'PASTE') { handler = e => { // TODO: clean this along with node deletion. - const copyLgTemplateToNewNode = async (lgTemplateName: string, newNodeId: string) => { - const matches = /\[(bfd\w+-(\d+))\]/.exec(lgTemplateName); - if (Array.isArray(matches) && matches.length === 3) { - const originLgId = matches[1]; - const originNodeId = matches[2]; - const newLgId = originLgId.replace(originNodeId, newNodeId); - await copyLgTemplate('common', originLgId, newLgId); - return `[${newLgId}]`; - } - return lgTemplateName; + const copyLgTemplateToNewNode = async (lgText: string, newNodeId: string) => { + const inputLgRef = LgTemplateRef.parse(lgText); + if (!inputLgRef) return lgText; + + const inputLgMetaData = LgMetaData.parse(inputLgRef.name); + if (!inputLgMetaData) return lgText; + + inputLgMetaData.designerId = newNodeId; + const newLgName = inputLgMetaData.toString(); + const newLgTemplateRefString = new LgTemplateRef(newLgName).toString(); + + await copyLgTemplate('common', inputLgRef.name, newLgName); + return newLgTemplateRefString; }; pasteNodes(data, e.id, e.position, clipboardActions, copyLgTemplateToNewNode).then(dialog => { onChange(dialog); diff --git a/Composer/packages/extensions/visual-designer/src/utils/hooks.ts b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts index eddd11f52d..901b6dbb23 100644 --- a/Composer/packages/extensions/visual-designer/src/utils/hooks.ts +++ b/Composer/packages/extensions/visual-designer/src/utils/hooks.ts @@ -3,33 +3,18 @@ import { useContext, useState, useEffect, useRef } from 'react'; import debounce from 'lodash/debounce'; +import { LgTemplateRef } from '@bfc/shared'; import { NodeRendererContext } from '../store/NodeRendererContext'; -// matches [bfd-123456] -const TEMPLATE_PATTERN = /^\[(bfd.+-\d{6})\]$/; - -const getTemplateId = (str?: string): string | null => { - if (!str) { - return null; - } - - const match = TEMPLATE_PATTERN.exec(str); - - if (!match || !match[1]) { - return null; - } - - return match[1]; -}; - export const useLgTemplate = (str?: string, dialogId?: string) => { const { getLgTemplates } = useContext(NodeRendererContext); const [templateText, setTemplateText] = useState(''); let cancelled = false; const updateTemplateText = async () => { - const templateId = getTemplateId(str); + const lgTemplateRef = LgTemplateRef.parse(str || ''); + const templateId = lgTemplateRef ? lgTemplateRef.name : ''; if (templateId && dialogId) { // this is an LG template, go get it's content diff --git a/Composer/packages/lib/indexers/package.json b/Composer/packages/lib/indexers/package.json index 497ecdb788..67184bfa1b 100644 --- a/Composer/packages/lib/indexers/package.json +++ b/Composer/packages/lib/indexers/package.json @@ -28,6 +28,7 @@ "ts-jest": "^24.1.0" }, "dependencies": { + "@bfc/shared": "*", "botbuilder-expression-parser": "^4.5.11", "botbuilder-lg": "https://botbuilder.myget.org/F/botbuilder-declarative/npm/botbuilder-lg/-/4.7.0-preview2.tgz", "lodash": "^4.17.15", diff --git a/Composer/packages/lib/indexers/src/dialogIndexer.ts b/Composer/packages/lib/indexers/src/dialogIndexer.ts index ec235de7ae..fb7aa816a6 100644 --- a/Composer/packages/lib/indexers/src/dialogIndexer.ts +++ b/Composer/packages/lib/indexers/src/dialogIndexer.ts @@ -3,6 +3,7 @@ import has from 'lodash/has'; import uniq from 'lodash/uniq'; +import { extractLgTemplateRefs } from '@bfc/shared'; import { ITrigger, DialogInfo, FileInfo } from './type'; import { DialogChecker } from './utils/dialogChecker'; @@ -37,14 +38,7 @@ function ExtractLgTemplates(dialog): string[] { return true; } targets.forEach(target => { - // match a template name match a temlate func e.g. `showDate()` - // eslint-disable-next-line security/detect-unsafe-regex - const reg = /\[([A-Za-z_][-\w]+)(\(.*\))?\]/g; - let matchResult; - while ((matchResult = reg.exec(target)) !== null) { - const templateName = matchResult[1]; - templates.push(templateName); - } + templates.push(...extractLgTemplateRefs(target).map(x => x.name)); }); } return false; diff --git a/Composer/packages/lib/package.json b/Composer/packages/lib/package.json index 6e6604e56c..ce957b0aa9 100644 --- a/Composer/packages/lib/package.json +++ b/Composer/packages/lib/package.json @@ -10,7 +10,7 @@ "build:code-editor": "cd code-editor && yarn build", "build:shared": "cd shared && yarn build", "build:indexers": "cd indexers && yarn build", - "build:all": "concurrently --kill-others-on-fail \"yarn:build:code-editor\" \"yarn:build:shared\" \"yarn:build:indexers\"" + "build:all": "yarn build:shared && concurrently --kill-others-on-fail \"yarn:build:code-editor\" \"yarn:build:indexers\"" }, "author": "", "license": "ISC" diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/models/LgMetaData.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgMetaData.test.ts new file mode 100644 index 0000000000..1d0061c5f1 --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgMetaData.test.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgMetaData } from '../../../src'; + +describe('LgMetaData', () => { + it('can construct an instance via constructor', () => { + const instance = new LgMetaData('activity', '123456'); + + expect(instance.type).toEqual('activity'); + expect(instance.designerId).toEqual('123456'); + expect(instance.toString).toBeDefined(); + }); + + it('can generate correct output strings', () => { + const instance = new LgMetaData('activity', '123456'); + + expect(instance.toString()).toEqual('bfdactivity-123456'); + }); + + it('can construct instance via `parse()` method', () => { + expect(LgMetaData.parse('bfdactivity-123456')).toBeInstanceOf(LgMetaData); + }); +}); diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts new file mode 100644 index 0000000000..972e42438b --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/lgUtils/models/LgTemplateRef.test.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgTemplateRef } from '../../../src'; + +describe('LgTemplateRef#', () => { + it('can construct an instance via constructor', () => { + const a = new LgTemplateRef('a', undefined); + expect(a.name).toEqual('a'); + expect(a.parameters).toEqual([]); + + const b = new LgTemplateRef('b', []); + expect(b.name).toEqual('b'); + expect(b.parameters).toEqual([]); + + const c = new LgTemplateRef('c', ['1', '2']); + expect(c.name).toEqual('c'); + expect(c.parameters).toEqual(['1', '2']); + }); + + it('can output correct strings', () => { + const a = new LgTemplateRef('a', undefined); + expect(a.toString()).toEqual('[a()]'); + + const b = new LgTemplateRef('b', []); + expect(b.toString()).toEqual('[b()]'); + + const c = new LgTemplateRef('c', ['1', '2']); + expect(c.toString()).toEqual('[c(1,2)]'); + }); + + describe('parse()', () => { + it('should return null when inputs are invalid', () => { + expect(LgTemplateRef.parse('')).toEqual(null); + expect(LgTemplateRef.parse('xxx')).toEqual(null); + expect(LgTemplateRef.parse('[0]')).toEqual(null); + }); + + it('should return LgTemplateRef when inputs are valid', () => { + const a = LgTemplateRef.parse('[bfdactivity-123456]'); + expect(a).toEqual(new LgTemplateRef('bfdactivity-123456')); + + const a2 = LgTemplateRef.parse('[bfdactivity-123456()]'); + expect(a2).toEqual(new LgTemplateRef('bfdactivity-123456')); + + const b = LgTemplateRef.parse('[greeting(1,2)]'); + expect(b).toEqual(new LgTemplateRef('greeting', ['1', '2'])); + }); + }); +}); diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgParamString.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgParamString.test.ts new file mode 100644 index 0000000000..1330259054 --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgParamString.test.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import parseLgParamString from '../../../src/lgUtils/parsers/parseLgParamString'; + +describe('parseLgParamString', () => { + it('should return undefined when no params detected', () => { + expect(parseLgParamString('')).toBeUndefined(); + expect(parseLgParamString('xxx')).toBeUndefined(); + }); + + it('should return params array when input valid strings', () => { + expect(parseLgParamString('()')).toEqual([]); + expect(parseLgParamString('(a,b)')).toEqual(['a', 'b']); + }); +}); diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateName.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateName.test.ts new file mode 100644 index 0000000000..2ab93d2d6d --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateName.test.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgMetaData } from '../../../src'; +import parseLgTemplateName from '../../../src/lgUtils/parsers/parseLgTemplateName'; + +describe('parseLgTemplateName', () => { + it('should return null when inputs are invalid', () => { + expect(parseLgTemplateName('')).toEqual(null); + expect(parseLgTemplateName('xxx')).toEqual(null); + }); + + it('should return LgMetaData when inputs are valid', () => { + const result = parseLgTemplateName('bfdactivity-123456'); + expect(result).toBeInstanceOf(LgMetaData); + expect((result as LgMetaData).designerId).toEqual('123456'); + expect((result as LgMetaData).type).toEqual('activity'); + }); +}); diff --git a/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts new file mode 100644 index 0000000000..15dc759e62 --- /dev/null +++ b/Composer/packages/lib/shared/__tests__/lgUtils/parsers/parseLgTemplateRef.test.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgTemplateRef } from '../../../src'; +import parseLgTemplateRef, { extractLgTemplateRefs } from '../../../src/lgUtils/parsers/parseLgTemplateRef'; + +describe('parseLgTemplateRef', () => { + it('should return null when inputs are invalid', () => { + expect(parseLgTemplateRef('')).toEqual(null); + expect(parseLgTemplateRef('xxx')).toEqual(null); + expect(parseLgTemplateRef('[0]')).toEqual(null); + expect(parseLgTemplateRef('hi, [greeting]. [greeting]')).toEqual(null); + }); + + it('should return LgTemplateRef when inputs are valid', () => { + const a = parseLgTemplateRef('[bfdactivity-123456]'); + expect(a).toEqual(new LgTemplateRef('bfdactivity-123456')); + + const b = parseLgTemplateRef('[greeting(1,2)]'); + expect(b).toEqual(new LgTemplateRef('greeting', ['1', '2'])); + }); +}); + +describe('extractLgTemplateRefs', () => { + it('can extract lg refs from input string', () => { + expect(extractLgTemplateRefs('Hi')).toEqual([]); + expect(extractLgTemplateRefs('[bfdactivity-123456]')).toEqual([new LgTemplateRef('bfdactivity-123456')]); + expect(extractLgTemplateRefs(`-[Greeting], I'm a fancy bot, [Bye]`)).toEqual([ + new LgTemplateRef('Greeting'), + new LgTemplateRef('Bye'), + ]); + }); +}); diff --git a/Composer/packages/lib/shared/src/index.ts b/Composer/packages/lib/shared/src/index.ts index 36c5633392..0c0d75b91e 100644 --- a/Composer/packages/lib/shared/src/index.ts +++ b/Composer/packages/lib/shared/src/index.ts @@ -12,3 +12,4 @@ export * from './promptTabs'; export * from './appschema'; export * from './types'; export * from './constant'; +export * from './lgUtils'; diff --git a/Composer/packages/lib/shared/src/lgUtils/index.ts b/Composer/packages/lib/shared/src/lgUtils/index.ts new file mode 100644 index 0000000000..34aa3f9dde --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/index.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +/** models */ +export { default as LgMetaData } from './models/LgMetaData'; +export { default as LgTemplateRef } from './models/LgTemplateRef'; + +/** parsers */ +export { extractLgTemplateRefs } from './parsers/parseLgTemplateRef'; diff --git a/Composer/packages/lib/shared/src/lgUtils/models/LgMetaData.ts b/Composer/packages/lib/shared/src/lgUtils/models/LgMetaData.ts new file mode 100644 index 0000000000..81bf2df7eb --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/models/LgMetaData.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import parseLgTemplateName from '../parsers/parseLgTemplateName'; +import buildLgTemplateName from '../stringBuilders/buildLgTemplateName'; + +import { LgTemplateName } from './stringTypes'; + +/** + * LgMetaData can be converted from & to Lg name. Such as 'bfdactivity-1234'. + * It's created by Composer, contains designerId and filed type. + */ +export default class LgMetaData { + type: string; + designerId: string; + + constructor(lgType: string, designerId: string) { + this.type = lgType; + this.designerId = designerId; + } + + static parse(lgTemplateName: LgTemplateName): LgMetaData | null { + return parseLgTemplateName(lgTemplateName); + } + + toString(): LgTemplateName { + return buildLgTemplateName(this); + } +} diff --git a/Composer/packages/lib/shared/src/lgUtils/models/LgTemplateRef.ts b/Composer/packages/lib/shared/src/lgUtils/models/LgTemplateRef.ts new file mode 100644 index 0000000000..b784d199a4 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/models/LgTemplateRef.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import parseLgTemplateRef from '../parsers/parseLgTemplateRef'; +import buildLgTemplateRefString from '../stringBuilders/buildLgTemplateRefString'; + +import { LgTemplateName, LgTemplateRefString } from './stringTypes'; + +export default class LgTemplateRef { + name: LgTemplateName; + + parameters: string[]; + + constructor(name: LgTemplateName, parameters: string[] = []) { + this.name = name; + this.parameters = parameters; + } + + static parse(input: LgTemplateRefString): LgTemplateRef | null { + return parseLgTemplateRef(input); + } + + toString(): LgTemplateRefString { + return buildLgTemplateRefString(this); + } +} diff --git a/Composer/packages/lib/shared/src/lgUtils/models/stringTypes.ts b/Composer/packages/lib/shared/src/lgUtils/models/stringTypes.ts new file mode 100644 index 0000000000..2bf9e52061 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/models/stringTypes.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +/** + * example: 'hello' | '[bfdactivity-123456]' + */ +export type LgText = string; + +/** + * example: '[greetings()]' | '[bfdactivity-123456()]' + */ +export type LgTemplateRefString = string; + +/** + * example: 'greeting' | 'bfdactivity-123456' + */ +export type LgTemplateName = string; + +/** + * How we understand LG strings: + 1. LgText + { + "activity": "....", // LgText + "prompt": "....", // LgText + "invalidPrompt": "..." // LgText + } + + 2. LgTemplateRef + '[bfdactivity-1234]' + '[greeting]' + '[greeting(1)]' + + 3. LgTemplateName + 'bfdactivity-1234' in '[bfdactivity-1234]' + 'greeting' in '[greeting(1)]' + + 4. LgMetaData (Composer-only, can be converted to LgTemplateName) + 'bfdactivity-1234' => { type: 'activity', designerId: '1234' } // LgMetaData + 'greeting' => NO meta data since its not created by Composer +*/ diff --git a/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts b/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts new file mode 100644 index 0000000000..0e28a41ca5 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/parsers/lgPatterns.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +export const LgNamePattern = `bfd(\\w+)-(\\d+)`; + +export const LgTemplateRefPattern = `\\[([A-Za-z_][-\\w]+)(\\(.*\\))?\\]`; diff --git a/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgParamString.ts b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgParamString.ts new file mode 100644 index 0000000000..040cdfbe11 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgParamString.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +/** + * @param input '(1,2,3)' + * @returns ['1', '2', '3'] + */ +export default function parseLgParamString(input: string): string[] | undefined { + if (!input) return undefined; + + const results = /^\((.*)\)$/.exec(input); + if (Array.isArray(results) && results.length === 2) { + return results[1] ? results[1].split(',') : []; + } + return undefined; +} diff --git a/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateName.ts b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateName.ts new file mode 100644 index 0000000000..22ac4f8daa --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateName.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgTemplateName } from '../models/stringTypes'; +import LgMetaData from '../models/LgMetaData'; + +import { LgNamePattern } from './lgPatterns'; + +export default function parseLgTemplateName(lgTemplateName: LgTemplateName): LgMetaData | null { + if (!lgTemplateName) return null; + + const results = lgTemplateName.match(LgNamePattern); + if (Array.isArray(results) && results.length === 3) { + return new LgMetaData(results[1], results[2]); + } + return null; +} diff --git a/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateRef.ts b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateRef.ts new file mode 100644 index 0000000000..51f60823c3 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/parsers/parseLgTemplateRef.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import { LgTemplateRefString } from '../models/stringTypes'; +import LgTemplateRef from '../models/LgTemplateRef'; + +import { LgTemplateRefPattern } from './lgPatterns'; +import parseLgParamString from './parseLgParamString'; + +const mapMatchResultToTemplateRef = (matchResult: RegExpMatchArray): LgTemplateRef | null => { + if (matchResult.length !== 3) { + return null; + } + + const name = matchResult[1]; + const lgParams = parseLgParamString(matchResult[2]); + + return new LgTemplateRef(name, lgParams); +}; + +/** + * '[greetings()]' => { name: greetings, parameters: []} + * 'hi [greetings()]' => null + */ +export default function parseLgTemplateRef(inputString: LgTemplateRefString): LgTemplateRef | null { + if (typeof inputString !== 'string') return null; + + const BoundariedLgTemplateRefPattern = `^${LgTemplateRefPattern}$`; + const matchResult = inputString.match(BoundariedLgTemplateRefPattern); + if (!matchResult) return null; + + return mapMatchResultToTemplateRef(matchResult); +} + +/** + * + * @param text string + * '-[Greeting], I'm a fancy bot, [Bye]' => [ LgTemplateRef('Greeting'), LgTemplateRef('Bye') ] + */ +export function extractLgTemplateRefs(text: string): LgTemplateRef[] { + const templateRefs: LgTemplateRef[] = []; + + // eslint-disable-next-line security/detect-non-literal-regexp + const reg = new RegExp(LgTemplateRefPattern, 'g'); + + let matchResult; + while ((matchResult = reg.exec(text)) !== null) { + const ref = mapMatchResultToTemplateRef(matchResult); + if (!ref) continue; + + templateRefs.push(ref); + } + return templateRefs; +} diff --git a/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgParamString.ts b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgParamString.ts new file mode 100644 index 0000000000..e25412808b --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgParamString.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +/** + * ['1', '2'] => '(1, 2)' + */ +export default function buildLgParamString(parameters: string[]): string { + if (Array.isArray(parameters)) { + return `(${parameters.join(',')})`; + } else { + return '()'; + } +} diff --git a/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateName.ts b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateName.ts new file mode 100644 index 0000000000..cd1c35cb50 --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateName.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import LgMetaData from '../models/LgMetaData'; +import { LgTemplateName } from '../models/stringTypes'; + +/** + * { type: 'activity', designerId: '1234' } => 'bfdactivity-1234' + * + * @param lgMetaData input metadata + * @returns toString() result of the input object. + */ +export default function buildLgTemplateNameFromLgMetaData(lgMetaData: LgMetaData): LgTemplateName { + const { type, designerId } = lgMetaData; + return `bfd${type}-${designerId}`; +} diff --git a/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts new file mode 100644 index 0000000000..1ef13c74df --- /dev/null +++ b/Composer/packages/lib/shared/src/lgUtils/stringBuilders/buildLgTemplateRefString.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License + +import LgTemplateRef from '../models/LgTemplateRef'; +import { LgTemplateRefString } from '../models/stringTypes'; + +import buildLgParamString from './buildLgParamString'; + +/** + * { name: 'greeting', parameters: ['1'] } => '[greeting(1)]' + */ +export default function buildLgTemplateRefString(lgTemplateRef: LgTemplateRef): LgTemplateRefString { + const { name, parameters } = lgTemplateRef; + return `[${name}${buildLgParamString(parameters)}]`; +}