diff --git a/libraries/botbuilder-core/src/activityFactory.ts b/libraries/botbuilder-core/src/activityFactory.ts new file mode 100644 index 0000000000..a261afcdf3 --- /dev/null +++ b/libraries/botbuilder-core/src/activityFactory.ts @@ -0,0 +1,618 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Activity, SuggestedActions, Attachment, ActivityTypes, ActionTypes, CardAction } from 'botframework-schema'; +import { MessageFactory } from './messageFactory'; +import { CardFactory } from './cardFactory'; + +/** + * The ActivityFactory + * to generate text and then uses simple markdown semantics like chatdown to create Activity. + */ +export class ActivityFactory { + + private static readonly lgType = 'lgType'; + private static readonly errorPrefix = '[ERROR]'; + private static readonly warningPrefix = '[WARNING]'; + private static adaptiveCardType: string = CardFactory.contentTypes.adaptiveCard; + + private static readonly genericCardTypeMapping: Map = new Map + ([ + [ 'herocard', CardFactory.contentTypes.heroCard ], + [ 'thumbnailcard', CardFactory.contentTypes.thumbnailCard ], + [ 'audiocard', CardFactory.contentTypes.audioCard ], + [ 'videocard', CardFactory.contentTypes.videoCard ], + [ 'animationcard', CardFactory.contentTypes.animationCard ], + [ 'signincard', CardFactory.contentTypes.signinCard ], + [ 'oauthcard', CardFactory.contentTypes.oauthCard ], + [ 'receiptcard', CardFactory.contentTypes.receiptCard ], + ]); + + private static readonly activityProperties: string[] = ['type','id','timestamp','localTimestamp','localTimezone','callerId', + 'serviceUrl','channelId','from','conversation','recipient','textFormat','attachmentLayout','membersAdded', + 'membersRemoved','reactionsAdded','reactionsRemoved','topicName','historyDisclosed','locale','text','speak', + 'inputHint','summary','suggestedActions','attachments','entities','channelData','action','replyToId','label', + 'valueType','value','name','typrelatesToe','code','expiration','importance','deliveryMode','listenFor', + 'textHighlights','semanticAction']; + + private static readonly cardActionProperties: string[] = ['type','title','image','text','displayText','value','channelData']; + + /** + * Generate the activity. + * @param lgResult string result from languageGenerator. + */ + public static fromObject(lgResult: any): Partial { + const diagnostics: string[] = this.checkLGResult(lgResult); + const errors: string[] = diagnostics.filter((u: string): boolean => u.startsWith(this.errorPrefix)); + if (errors !== undefined && errors.length > 0) { + throw new Error(`${ errors.join('\n') }`); + } + + if (typeof lgResult === 'string') { + const structuredLGResult: any = this.parseStructuredLGResult(lgResult.trim()); + return structuredLGResult === undefined ? + this.buildActivityFromText(lgResult.trim()) + :this.buildActivityFromLGStructuredResult(lgResult); + } + + return this.buildActivityFromLGStructuredResult(lgResult); + } + + /** + * check the LG result before generate an Activity. + * @param lgResult lg output. + * @returns Diagnostic list. + */ + public static checkLGResult(lgResult: any): string[] { + if (lgResult === undefined) { + return [this.buildDiagnostic('LG output is empty', false)]; + } + + if (typeof lgResult === 'string') { + if (!lgResult.startsWith('{') || !lgResult.endsWith('}')) { + return [this.buildDiagnostic('LG output is not a json object, and will fallback to string format.', false)]; + } + + let lgStructuredResult: any = undefined; + + try { + lgStructuredResult = JSON.parse(lgResult); + } catch (error) { + return [this.buildDiagnostic('LG output is not a json object, and will fallback to string format.', false)]; + } + + return this.checkStructuredResult(lgStructuredResult); + } else { + return this.checkStructuredResult(lgResult); + } + } + + /** + * Given a lg result, create a text activity. This method will create a MessageActivity from text. + * @param text lg text output. + */ + private static buildActivityFromText(text: string): Partial { + const msg: Partial = { + type: ActivityTypes.Message, + text: text, + speak: text + }; + + return msg; + } + + /** + * Given a structured lg result, create an activity. This method will create an MessageActivity from object + * @param lgValue lg output. + */ + private static buildActivityFromLGStructuredResult(lgValue: any): Partial { + let activity: Partial = {}; + + const type: string = this.getStructureType(lgValue); + if (this.genericCardTypeMapping.has(type) || type === 'attachment') { + activity = MessageFactory.attachment(this.getAttachment(lgValue)); + } else if (type === 'activity') { + activity = this.buildActivity(lgValue); + } + + return activity; + } + + private static buildActivity(messageValue: any): Partial { + let activity: Partial = { type: ActivityTypes.Message }; + for (const key of Object.keys(messageValue)) { + const property: string = key.trim(); + if (property === this.lgType) { + continue; + } + + const value: any = messageValue[key]; + + switch (property.toLowerCase()) { + case 'attachments': + activity.attachments = this.getAttachments(value); + break; + case 'suggestedactions': + activity.suggestedActions = this.getSuggestions(value); + break; + default: + var properties = this.activityProperties.map((u: string): string => u.toLowerCase()); + if (properties.includes(property.toLowerCase())) + { + var realPropertyName = this.activityProperties[properties.indexOf(property.toLowerCase())]; + activity[realPropertyName] = value; + } else { + activity[property.toLowerCase()] = value; + } + break; + } + } + + return activity; + } + + private static getSuggestions(suggestionsValue: any): SuggestedActions { + const actions: any[] = this.normalizedToList(suggestionsValue); + + const suggestedActions: SuggestedActions = { + actions : this.getCardActions(actions), + to: [] + }; + + return suggestedActions; + } + + private static getButtons(buttonsValue: any): CardAction[] { + const actions: any[] = this.normalizedToList(buttonsValue); + return this.getCardActions(actions); + } + + private static getCardActions(actions: any[]): CardAction[] { + return actions.map((u: any): CardAction => this.getCardAction(u)); + } + + private static getCardAction(action: any): CardAction + { + let cardAction: CardAction; + if (typeof action === 'string') { + cardAction = { type: ActionTypes.ImBack, value: action, title: action, channelData: undefined }; + } else { + const type: string = this.getStructureType(action); + cardAction = { + type: ActionTypes.ImBack, + title: '', + value: '' + }; + + if (type === 'cardaction') { + for (const key of Object.keys(action)) { + const property: string = key.trim(); + if (property === this.lgType) { + continue; + } + + const value: any = action[key]; + + switch (property.toLowerCase()) { + case 'displaytext': + cardAction.displayText = value; + break; + case 'channeldata': + cardAction.channelData = value; + break; + default: + cardAction[property.toLowerCase()] = value; + break; + } + } + } + } + + return cardAction; + } + + + + + private static getAttachments(input: any): Attachment[] { + const attachments: Attachment[] = []; + const attachmentsJsonList: any[] = this.normalizedToList(input); + + for (const attachmentsJson of attachmentsJsonList) { + if (typeof attachmentsJson === 'object') { + attachments.push(this.getAttachment(attachmentsJson)); + } + } + + return attachments; + } + + private static getAttachment(input: any): Attachment { + let attachment: Attachment = { + contentType: '' + }; + const type: string = this.getStructureType(input); + if (this.genericCardTypeMapping.has(type)) { + attachment = this.getCardAttachment(this.genericCardTypeMapping.get(type), input); + } else if (type === 'adaptivecard') { + attachment = CardFactory.adaptiveCard(input); + } else if (type === 'attachment') { + attachment = this.getNormalAttachment(input); + } else { + attachment = {contentType: type, content: input}; + } + + return attachment; + } + + private static getNormalAttachment(input: any): Attachment { + const attachment: Attachment = {contentType:''}; + + for (const key of Object.keys(input)) { + const property: string = key.trim(); + const value: any = input[key]; + + switch (property.toLowerCase()) { + case 'contenttype': + const type: string = value.toString().toLowerCase(); + if (this.genericCardTypeMapping.has(type)) { + attachment.contentType = this.genericCardTypeMapping.get(type); + } else if (type === 'adaptivecard') { + attachment.contentType = this.adaptiveCardType; + } else { + attachment.contentType = type; + } + break; + case 'contenturl': + attachment.contentUrl = value; + break; + case 'thumbnailurl': + attachment.thumbnailUrl = value; + break; + default: + attachment[property.toLowerCase()] = value; + break; + } + } + + return attachment; + } + + private static getCardAttachment(type: string, input: any): Attachment { + const card: any = {}; + + for (const key of Object.keys(input)) { + const property: string = key.trim().toLowerCase(); + const value: any = input[key]; + + switch (property) { + case 'tap': + card[property] = this.getCardAction(value); + break; + case 'image': + case 'images': + if (type === CardFactory.contentTypes.heroCard || type === CardFactory.contentTypes.thumbnailCard) { + if (!('images' in card)) { + card['images'] = []; + } + + const imageList: string[] = this.normalizedToList(value).map((u): string => u.toString()); + imageList.forEach( (u): any => card['images'].push({url : u})); + } else { + card['image'] = {url: value.toString()}; + } + break; + case 'media': + if (!('media' in card)) { + card['media'] = []; + } + + const mediaList: string[] = this.normalizedToList(value).map((u): string => u.toString()); + mediaList.forEach( (u): any => card['media'].push({url : u})); + break; + case 'buttons': + if (!('buttons' in card)) { + card['buttons'] = []; + } + + const buttons: any[] = this.getButtons(value); + buttons.forEach( (u): any => card[property].push(u)); + break; + case 'autostart': + case 'shareable': + case 'autoloop': + const boolValue: boolean = this.getValidBooleanValue(value.toString()); + if (boolValue !== undefined) { + card[property] = boolValue; + } + break; + case 'connectionname': + card['connectionName'] = value; + break; + default: + card[property.toLowerCase()] = value; + break; + } + } + + const attachment: Attachment = { + contentType: type, + content: card + }; + + return attachment; + } + + private static normalizedToList(item: any): any[] { + if (item === undefined) { + return []; + } else if (Array.isArray(item)) { + return item; + } else { + return [item]; + } + } + + private static parseStructuredLGResult(lgStringResult: string): any + { + let lgStructuredResult: any = undefined; + if (lgStringResult === undefined || lgStringResult === '') { + return undefined; + } + + lgStringResult = lgStringResult.trim(); + + if (lgStringResult === '' || !lgStringResult.startsWith('{') || !lgStringResult.endsWith('}')) { + return undefined; + } + + try { + lgStructuredResult = JSON.parse(lgStringResult); + } catch (error) { + return undefined; + } + + return lgStructuredResult; + } + + private static checkStructuredResult(input: any): string[] { + const result: string[] = []; + const type: string = this.getStructureType(input); + if (this.genericCardTypeMapping.has(type) || type === 'attachment') { + result.push(...this.checkAttachment(input)); + } else if (type === 'activity') { + result.push(...this.checkActivity(input)); + } else { + const diagnosticMessage: string = (type === undefined || type === '') ? + `'${ this.lgType }' does not exist in lg output json object.` + : `Type '${ type }' is not supported currently.`; + result.push(this.buildDiagnostic(diagnosticMessage)); + } + + return result; + } + + private static checkActivity(input: any): string[] { + const result: string[] = []; + let activityType: string = undefined; + if ('type' in input) { + activityType = input['type'].toString().trim(); + } + + result.push(...this.checkActivityType(activityType)); + result.push(...this.checkActivityPropertyName(input)); + result.push(...this.checkActivityProperties(input)); + + return result; + } + + private static checkActivityType(activityType: string): string[] { + if (activityType !== undefined) { + if (!Object.values(ActivityTypes).map((u: string): string => u.toLowerCase()).includes(activityType.toLowerCase())) { + return [this.buildDiagnostic(`'${ activityType }' is not a valid activity type.`)]; + } + } + return []; + } + + private static checkActivityPropertyName(input: any): string[] { + const invalidProperties: string[] = []; + for (const property of Object.keys(input)) { + if (property === this.lgType) { + continue; + } + if (!this.activityProperties.map((u: string): string => u.toLowerCase()).includes(property.toLowerCase())) { + invalidProperties.push(property); + } + } + if (invalidProperties.length > 0) { + return [this.buildDiagnostic(`'${ invalidProperties.join(',') }' not support in Activity.`, false)]; + } + + return []; + } + + private static checkActivityProperties(input: any): string[] { + const result: string[] = []; + for (const key of Object.keys(input)) { + const property: string = key.trim(); + const value: any = input[key]; + + switch (property.toLowerCase()) { + case 'attachments': + result.push(...this.checkAttachments(value)); + break; + case 'suggestedactions': + result.push(...this.checkSuggestions(value)); + break; + default: + break; + } + } + + return result; + } + + private static checkSuggestions(value: any): string[] { + const actions: any[] = this.normalizedToList(value); + return this.checkCardActions(actions); + } + + private static checkButtons(value: any): string[] { + const actions: any[] = this.normalizedToList(value); + return this.checkCardActions(actions); + } + + private static checkCardActions(actions: any[]): string[] { + const result: string[] = []; + actions.forEach((u: any): void => { result.push(...this.checkCardAction(u)); }); + return result; + } + + private static checkCardAction(value: any): string[] { + const result: string[] = []; + if (typeof value === 'string') { + return result; + } + + if (typeof value === 'object') { + const type: string = this.getStructureType(value); + if (type !== 'cardaction') { + result.push(this.buildDiagnostic(`'${ type }' is not card action type.`, false)); + } else { + result.push(...this.checkCardActionPropertyName(value)); + if ('type' in value) { + result.push(...this.checkCardActionType(value['type'])); + } + } + } else { + result.push(this.buildDiagnostic(`'${ value }' is not a valid card action format.`, false)); + } + + return result; + } + + + private static checkCardActionPropertyName(input: any): string[] { + const invalidProperties: string[] = []; + for (const property of Object.keys(input)) { + if (property === this.lgType) { + continue; + } + if (!this.cardActionProperties.map((u: string): string => u.toLowerCase()).includes(property.toLowerCase())) { + invalidProperties.push(property); + } + } + if (invalidProperties.length > 0) { + return [this.buildDiagnostic(`'${ invalidProperties.join(',') }' not support in card action.`, false)]; + } + + return []; + } + + private static checkCardActionType(cardActionType: string): string[] { + const result: string[] = []; + if (!cardActionType) { + return result; + } + + if (!Object.values(ActionTypes).map((u: string): string => u.toLowerCase()).includes(cardActionType.toLowerCase())) { + return [this.buildDiagnostic(`'${ cardActionType }' is not a valid card action type.`)]; + } + + return result; + } + + private static checkAttachments(value: any): string[] { + const result: string[] = []; + const attachmentsJsonList: any[] = this.normalizedToList(value); + + for (const attachmentsJson of attachmentsJsonList) { + if (typeof attachmentsJson === 'object') { + result.push(...this.checkAttachment(attachmentsJson)); + } + } + + return result; + } + + private static checkAttachment(value: any): string[] { + const result: string[] = []; + const type: string = this.getStructureType(value); + if (this.genericCardTypeMapping.has(type)) { + result.push(...this.checkCardAttachment(value)); + } else if (type === 'adaptivecard') { + // TODO + // check adaptivecard format + } else if (type === 'attachment') { + // TODO + // Check attachment format + } else { + result.push(this.buildDiagnostic(`'${ type }' is not an attachment type.`, false)); + } + + return result; + } + + private static checkCardAttachment(input: any): string[] { + const result: string[] = []; + for (const key of Object.keys(input)) { + const property: string = key.trim().toLowerCase(); + const value: any = input[key]; + + switch (property) { + case 'buttons': + result.push(...this.checkButtons(value)); + break; + case 'autostart': + case 'shareable': + case 'autoloop': + const boolValue: boolean = this.getValidBooleanValue(value.toString()); + if (boolValue === undefined) { + result.push(this.buildDiagnostic(`'${ value.toString() }' is not a boolean value.`)); + } + break; + default: + break; + } + } + + return result; + } + + private static getStructureType(input: any): string { + let result = ''; + + if (input && typeof input === 'object') { + if (this.lgType in input) { + result = input[this.lgType].toString(); + } else if ('type' in input) { + // Adaptive card type + result = input['type'].toString(); + } + } + + return result.trim().toLowerCase(); + } + + + private static getValidBooleanValue(boolValue: string): boolean{ + if (boolValue.toLowerCase() === 'true') + { + return true; + } + else if (boolValue.toLowerCase() === 'false') + { + return false; + } + + return undefined; + } + + private static buildDiagnostic(message: string, isError: boolean = true): string { + message = message === undefined ? '' : message; + return isError ? this.errorPrefix + message : this.warningPrefix + message; + } +} \ No newline at end of file diff --git a/libraries/botbuilder-core/src/index.ts b/libraries/botbuilder-core/src/index.ts index 815326f5ed..a0d1c29140 100644 --- a/libraries/botbuilder-core/src/index.ts +++ b/libraries/botbuilder-core/src/index.ts @@ -7,6 +7,7 @@ */ export * from 'botframework-schema'; +export * from './activityFactory'; export * from './appCredentials'; export * from './activityHandler'; export * from './activityHandlerBase'; diff --git a/libraries/botbuilder-lg/README.MD b/libraries/botbuilder-lg/README.MD index 97b48d3899..84d03d0197 100644 --- a/libraries/botbuilder-lg/README.MD +++ b/libraries/botbuilder-lg/README.MD @@ -94,19 +94,19 @@ For NodeJS, add botbuilder-lg Load the template manager with your .lg file ```typescript - let lgFile = new LGParser.parseFile(filePath, importResolver?, expressionParser?); + let templates = new Templates.parseFile(filePath, importResolver?, expressionParser?); ``` -When you need template expansion, call the LGFile and pass in the relevant template name +When you need template expansion, call the templates and pass in the relevant template name ```typescript - await turnContext.sendActivity(lgFile.evaluateTemplate("", entitiesCollection)); + await turnContext.sendActivity(templates.evaluate("", entitiesCollection)); ``` If your template needs specific entity values to be passed for resolution/ expansion, you can pass them in on the call to `evaluateTemplate` ```typescript - await turnContext.sendActivity(lgFile.evaluateTemplate("WordGameReply", { GameName = "MarcoPolo" } )); + await turnContext.sendActivity(templates.evaluate("WordGameReply", { GameName = "MarcoPolo" } )); ``` [1]:https://github.com/Microsoft/BotBuilder/blob/master/specs/botframework-activity/botframework-activity.md diff --git a/libraries/botbuilder-lg/src/activityChecker.ts b/libraries/botbuilder-lg/src/activityChecker.ts deleted file mode 100644 index 1fed950a41..0000000000 --- a/libraries/botbuilder-lg/src/activityChecker.ts +++ /dev/null @@ -1,320 +0,0 @@ -/** - * @module botbuilder-lg - */ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { ActivityTypes, ActionTypes } from 'botbuilder-core'; -import { CardFactory } from 'botbuilder-core'; -import { Diagnostic, DiagnosticSeverity } from './diagnostic'; -import { Range } from './range'; -import { Position } from './position'; -import { Evaluator } from './evaluator'; - -/** - * Structure LG result checker. - */ -export class ActivityChecker { - public static readonly genericCardTypeMapping: Map = new Map - ([ - [ 'herocard', CardFactory.contentTypes.heroCard ], - [ 'thumbnailcard', CardFactory.contentTypes.thumbnailCard ], - [ 'audiocard', CardFactory.contentTypes.audioCard ], - [ 'videocard', CardFactory.contentTypes.videoCard ], - [ 'animationcard', CardFactory.contentTypes.animationCard ], - [ 'signincard', CardFactory.contentTypes.signinCard ], - [ 'oauthcard', CardFactory.contentTypes.oauthCard ], - [ 'receiptcard', CardFactory.contentTypes.receiptCard ], - ]); - - public static readonly activityProperties: string[] = ['type','id','timestamp','localTimestamp','localTimezone','callerId', - 'serviceUrl','channelId','from','conversation','recipient','textFormat','attachmentLayout','membersAdded', - 'membersRemoved','reactionsAdded','reactionsRemoved','topicName','historyDisclosed','locale','text','speak', - 'inputHint','summary','suggestedActions','attachments','entities','channelData','action','replyToId','label', - 'valueType','value','name','typrelatesToe','code','expiration','importance','deliveryMode','listenFor', - 'textHighlights','semanticAction']; - - public static readonly cardActionProperties: string[] = ['type','title','image','text','displayText','value','channelData']; - - /** - * check the LG result before generate an Activity. - * @param lgResult lg output. - * @returns Diagnostic list. - */ - public static check(lgResult: any): Diagnostic[] { - if (lgResult === undefined) { - return [this.buildDiagnostic('LG output is empty', false)]; - } - - if (typeof lgResult === 'string') { - if (!lgResult.startsWith('{') || !lgResult.endsWith('}')) { - return [this.buildDiagnostic('LG output is not a json object, and will fallback to string format.', false)]; - } - - let lgStructuredResult: any = undefined; - - try { - lgStructuredResult = JSON.parse(lgResult); - } catch (error) { - return [this.buildDiagnostic('LG output is not a json object, and will fallback to string format.', false)]; - } - - return this.checkStructuredResult(lgStructuredResult); - } else { - return this.checkStructuredResult(lgResult); - } - } - - public static checkStructuredResult(input: any): Diagnostic[] { - const result: Diagnostic[] = []; - const type: string = this.getStructureType(input); - if (ActivityChecker.genericCardTypeMapping.has(type) || type === 'attachment') { - result.push(...this.checkAttachment(input)); - } else if (type === 'activity') { - result.push(...this.checkActivity(input)); - } else { - const diagnosticMessage: string = (type === undefined || type === '') ? - `'${ Evaluator.LGType }' does not exist in lg output json object.` - : `Type '${ type }' is not supported currently.`; - result.push(this.buildDiagnostic(diagnosticMessage)); - } - - return result; - } - - private static checkActivity(input: any): Diagnostic[] { - const result: Diagnostic[] = []; - let activityType: string = undefined; - if ('type' in input) { - activityType = input['type'].toString().trim(); - } - - result.push(...this.checkActivityType(activityType)); - result.push(...this.checkActivityPropertyName(input)); - result.push(...this.checkActivityProperties(input)); - - return result; - } - - private static checkActivityType(activityType: string): Diagnostic[] { - if (activityType !== undefined) { - if (!Object.values(ActivityTypes).map((u: string): string => u.toLowerCase()).includes(activityType.toLowerCase())) { - return [this.buildDiagnostic(`'${ activityType }' is not a valid activity type.`)]; - } - } - return []; - } - - private static checkActivityPropertyName(input: any): Diagnostic[] { - const invalidProperties: string[] = []; - for (const property of Object.keys(input)) { - if (property === Evaluator.LGType) { - continue; - } - if (!ActivityChecker.activityProperties.map((u: string): string => u.toLowerCase()).includes(property.toLowerCase())) { - invalidProperties.push(property); - } - } - if (invalidProperties.length > 0) { - return [this.buildDiagnostic(`'${ invalidProperties.join(',') }' not support in Activity.`, false)]; - } - - return []; - } - - private static checkActivityProperties(input: any): Diagnostic[] { - const result: Diagnostic[] = []; - for (const key of Object.keys(input)) { - const property: string = key.trim(); - const value: any = input[key]; - - switch (property.toLowerCase()) { - case 'attachments': - result.push(...this.checkAttachments(value)); - break; - case 'suggestedactions': - result.push(...this.checkSuggestions(value)); - break; - default: - break; - } - } - - return result; - } - - private static checkSuggestions(value: any): Diagnostic[] { - const actions: any[] = this.normalizedToList(value); - return this.checkCardActions(actions); - } - - private static checkButtons(value: any): Diagnostic[] { - const actions: any[] = this.normalizedToList(value); - return this.checkCardActions(actions); - } - - private static checkCardActions(actions: any[]): Diagnostic[] { - const result: Diagnostic[] = []; - actions.forEach((u: any): void => { result.push(...this.checkCardAction(u)); }); - return result; - } - - private static checkCardAction(value: any): Diagnostic[] { - const result: Diagnostic[] = []; - if (typeof value === 'string') { - return result; - } - - if (typeof value === 'object') { - const type: string = this.getStructureType(value); - if (type !== 'cardaction') { - result.push(this.buildDiagnostic(`'${ type }' is not card action type.`, false)); - } else { - result.push(...this.checkCardActionPropertyName(value)); - if ('type' in value) { - result.push(...this.checkCardActionType(value['type'])); - } - } - } else { - result.push(this.buildDiagnostic(`'${ value }' is not a valid card action format.`, false)); - } - - return result; - } - - - private static checkCardActionPropertyName(input: any): Diagnostic[] { - const invalidProperties: string[] = []; - for (const property of Object.keys(input)) { - if (property === Evaluator.LGType) { - continue; - } - if (!ActivityChecker.cardActionProperties.map((u: string): string => u.toLowerCase()).includes(property.toLowerCase())) { - invalidProperties.push(property); - } - } - if (invalidProperties.length > 0) { - return [this.buildDiagnostic(`'${ invalidProperties.join(',') }' not support in card action.`, false)]; - } - - return []; - } - - private static checkCardActionType(cardActionType: string): Diagnostic[] { - const result: Diagnostic[] = []; - if (!cardActionType) { - return result; - } - - if (!Object.values(ActionTypes).map((u: string): string => u.toLowerCase()).includes(cardActionType.toLowerCase())) { - return [this.buildDiagnostic(`'${ cardActionType }' is not a valid card action type.`)]; - } - - return result; - } - - private static checkAttachments(value: any): Diagnostic[] { - const result: Diagnostic[] = []; - const attachmentsJsonList: any[] = this.normalizedToList(value); - - for (const attachmentsJson of attachmentsJsonList) { - if (typeof attachmentsJson === 'object') { - result.push(...this.checkAttachment(attachmentsJson)); - } - } - - return result; - } - - private static checkAttachment(value: any): Diagnostic[] { - const result: Diagnostic[] = []; - const type: string = this.getStructureType(value); - if (ActivityChecker.genericCardTypeMapping.has(type)) { - result.push(...this.checkCardAttachment(value)); - } else if (type === 'adaptivecard') { - // TODO - // check adaptivecard format - } else if (type === 'attachment') { - // TODO - // Check attachment format - } else { - result.push(this.buildDiagnostic(`'${ type }' is not an attachment type.`, false)); - } - - return result; - } - - private static checkCardAttachment(input: any): Diagnostic[] { - const result: Diagnostic[] = []; - for (const key of Object.keys(input)) { - const property: string = key.trim().toLowerCase(); - const value: any = input[key]; - - switch (property) { - case 'buttons': - result.push(...this.checkButtons(value)); - break; - case 'autostart': - case 'shareable': - case 'autoloop': - const boolValue: boolean = this.getValidBooleanValue(value.toString()); - if (boolValue === undefined) { - result.push(this.buildDiagnostic(`'${ value.toString() }' is not a boolean value.`)); - } - break; - default: - break; - } - } - - return result; - } - - private static getStructureType(input: any): string { - let result = ''; - - if (input !== undefined) { - if (Evaluator.LGType in input) { - result = input[Evaluator.LGType].toString(); - } else if ('type' in input) { - // Adaptive card type - result = input['type'].toString(); - } - } - - return result.trim().toLowerCase(); - } - - - private static getValidBooleanValue(boolValue: string): boolean{ - if (boolValue.toLowerCase() === 'true') - { - return true; - } - else if (boolValue.toLowerCase() === 'false') - { - return false; - } - - return undefined; - } - - private static buildDiagnostic(message: string, isError: boolean = true): Diagnostic { - message = message === undefined ? '' : message; - const emptyRange: Range = new Range(new Position(0, 0), new Position(0, 0)); - return isError ? new Diagnostic(emptyRange, message, DiagnosticSeverity.Error) - : new Diagnostic(emptyRange, message, DiagnosticSeverity.Warning); - } - - private static normalizedToList(item: any): any[] { - if (item === undefined) { - return []; - } else if (Array.isArray(item)) { - return item; - } else { - return [item]; - } - } -} \ No newline at end of file diff --git a/libraries/botbuilder-lg/src/activityFactory.ts b/libraries/botbuilder-lg/src/activityFactory.ts deleted file mode 100644 index 4b90c5bc02..0000000000 --- a/libraries/botbuilder-lg/src/activityFactory.ts +++ /dev/null @@ -1,350 +0,0 @@ -/** - * @module botbuilder-lg - */ -/** - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { Activity, SuggestedActions, Attachment, ActivityTypes, ActionTypes, CardAction } from 'botframework-schema'; -import { MessageFactory, CardFactory } from 'botbuilder-core'; -import { Diagnostic, DiagnosticSeverity } from './diagnostic'; -import { ActivityChecker } from './activityChecker'; -import { Evaluator } from './evaluator'; - -/** - * The ActivityFactory - * to generate text and then uses simple markdown semantics like chatdown to create Activity. - */ -export class ActivityFactory { - private static adaptiveCardType: string = CardFactory.contentTypes.adaptiveCard; - - /** - * Generate the activity. - * @param lgResult string result from languageGenerator. - */ - public static createActivity(lgResult: any): Partial { - const diagnostics: Diagnostic[] = ActivityChecker.check(lgResult); - const errors: Diagnostic[] = diagnostics.filter((u: Diagnostic): boolean => u.severity === DiagnosticSeverity.Error); - if (errors !== undefined && errors.length > 0) { - throw new Error(`${ errors.join('\n') }`); - } - - if (typeof lgResult === 'string') { - const structuredLGResult: any = this.parseStructuredLGResult(lgResult.trim()); - return structuredLGResult === undefined ? - this.buildActivityFromText(lgResult.trim()) - :this.buildActivityFromLGStructuredResult(lgResult); - } - - return this.buildActivityFromLGStructuredResult(lgResult); - } - - /** - * Given a lg result, create a text activity. This method will create a MessageActivity from text. - * @param text lg text output. - */ - private static buildActivityFromText(text: string): Partial { - return MessageFactory.text(text, text); - } - - /** - * Given a structured lg result, create an activity. This method will create an MessageActivity from object - * @param lgValue lg output. - */ - private static buildActivityFromLGStructuredResult(lgValue: any): Partial { - let activity: Partial = {}; - - const type: string = this.getStructureType(lgValue); - if (ActivityChecker.genericCardTypeMapping.has(type) || type === 'attachment') { - activity = MessageFactory.attachment(this.getAttachment(lgValue)); - } else if (type === 'activity') { - activity = this.buildActivity(lgValue); - } - - return activity; - } - - private static buildActivity(messageValue: any): Partial { - let activity: Partial = { type: ActivityTypes.Message }; - for (const key of Object.keys(messageValue)) { - const property: string = key.trim(); - if (property === Evaluator.LGType) { - continue; - } - - const value: any = messageValue[key]; - - switch (property.toLowerCase()) { - case 'attachments': - activity.attachments = this.getAttachments(value); - break; - case 'suggestedactions': - activity.suggestedActions = this.getSuggestions(value); - break; - default: - var properties = ActivityChecker.activityProperties.map((u: string): string => u.toLowerCase()); - if (properties.includes(property.toLowerCase())) - { - var realPropertyName = ActivityChecker.activityProperties[properties.indexOf(property.toLowerCase())]; - activity[realPropertyName] = value; - } else { - activity[property.toLowerCase()] = value; - } - break; - } - } - - return activity; - } - - private static getSuggestions(suggestionsValue: any): SuggestedActions { - const actions: any[] = this.normalizedToList(suggestionsValue); - - const suggestedActions: SuggestedActions = { - actions : this.getCardActions(actions), - to: [] - }; - - return suggestedActions; - } - - private static getButtons(buttonsValue: any): CardAction[] { - const actions: any[] = this.normalizedToList(buttonsValue); - return this.getCardActions(actions); - } - - private static getCardActions(actions: any[]): CardAction[] { - return actions.map((u: any): CardAction => this.getCardAction(u)); - } - - private static getCardAction(action: any): CardAction - { - let cardAction: CardAction; - if (typeof action === 'string') { - cardAction = { type: ActionTypes.ImBack, value: action, title: action, channelData: undefined }; - } else { - const type: string = this.getStructureType(action); - cardAction = { - type: ActionTypes.ImBack, - title: '', - value: '' - }; - - if (type === 'cardaction') { - for (const key of Object.keys(action)) { - const property: string = key.trim(); - if (property === Evaluator.LGType) { - continue; - } - - const value: any = action[key]; - - switch (property.toLowerCase()) { - case 'displaytext': - cardAction.displayText = value; - break; - case 'channeldata': - cardAction.channelData = value; - break; - default: - cardAction[property.toLowerCase()] = value; - break; - } - } - } - } - - return cardAction; - } - - - private static getStructureType(input: any): string { - let result = ''; - - if (input && typeof input === 'object') { - if (Evaluator.LGType in input) { - result = input[Evaluator.LGType].toString(); - } else if ('type' in input) { - // Adaptive card type - result = input['type'].toString(); - } - } - - return result.trim().toLowerCase(); - } - - private static getAttachments(input: any): Attachment[] { - const attachments: Attachment[] = []; - const attachmentsJsonList: any[] = this.normalizedToList(input); - - for (const attachmentsJson of attachmentsJsonList) { - if (typeof attachmentsJson === 'object') { - attachments.push(this.getAttachment(attachmentsJson)); - } - } - - return attachments; - } - - private static getAttachment(input: any): Attachment { - let attachment: Attachment = { - contentType: '' - }; - const type: string = this.getStructureType(input); - if (ActivityChecker.genericCardTypeMapping.has(type)) { - attachment = this.getCardAttachment(ActivityChecker.genericCardTypeMapping.get(type), input); - } else if (type === 'adaptivecard') { - attachment = CardFactory.adaptiveCard(input); - } else if (type === 'attachment') { - attachment = this.getNormalAttachment(input); - } else { - attachment = {contentType: type, content: input}; - } - - return attachment; - } - - private static getNormalAttachment(input: any): Attachment { - const attachment: Attachment = {contentType:''}; - - for (const key of Object.keys(input)) { - const property: string = key.trim(); - const value: any = input[key]; - - switch (property.toLowerCase()) { - case 'contenttype': - const type: string = value.toString().toLowerCase(); - if (ActivityChecker.genericCardTypeMapping.has(type)) { - attachment.contentType = ActivityChecker.genericCardTypeMapping.get(type); - } else if (type === 'adaptivecard') { - attachment.contentType = this.adaptiveCardType; - } else { - attachment.contentType = type; - } - break; - case 'contenturl': - attachment.contentUrl = value; - break; - case 'thumbnailurl': - attachment.thumbnailUrl = value; - break; - default: - attachment[property.toLowerCase()] = value; - break; - } - } - - return attachment; - } - - private static getCardAttachment(type: string, input: any): Attachment { - const card: any = {}; - - for (const key of Object.keys(input)) { - const property: string = key.trim().toLowerCase(); - const value: any = input[key]; - - switch (property) { - case 'tap': - card[property] = this.getCardAction(value); - break; - case 'image': - case 'images': - if (type === CardFactory.contentTypes.heroCard || type === CardFactory.contentTypes.thumbnailCard) { - if (!('images' in card)) { - card['images'] = []; - } - - const imageList: string[] = this.normalizedToList(value).map((u): string => u.toString()); - imageList.forEach( (u): any => card['images'].push({url : u})); - } else { - card['image'] = {url: value.toString()}; - } - break; - case 'media': - if (!('media' in card)) { - card['media'] = []; - } - - const mediaList: string[] = this.normalizedToList(value).map((u): string => u.toString()); - mediaList.forEach( (u): any => card['media'].push({url : u})); - break; - case 'buttons': - if (!('buttons' in card)) { - card['buttons'] = []; - } - - const buttons: any[] = this.getButtons(value); - buttons.forEach( (u): any => card[property].push(u)); - break; - case 'autostart': - case 'shareable': - case 'autoloop': - const boolValue: boolean = this.getValidBooleanValue(value.toString()); - if (boolValue !== undefined) { - card[property] = boolValue; - } - break; - case 'connectionname': - card['connectionName'] = value; - break; - default: - card[property.toLowerCase()] = value; - break; - } - } - - const attachment: Attachment = { - contentType: type, - content: card - }; - - return attachment; - } - - private static getValidBooleanValue(boolValue: string): boolean{ - if (boolValue.toLowerCase() === 'true') - { - return true; - } - else if (boolValue.toLowerCase() === 'false') - { - return false; - } - - return undefined; - } - - private static normalizedToList(item: any): any[] { - if (item === undefined) { - return []; - } else if (Array.isArray(item)) { - return item; - } else { - return [item]; - } - } - - private static parseStructuredLGResult(lgStringResult: string): any - { - let lgStructuredResult: any = undefined; - if (lgStringResult === undefined || lgStringResult === '') { - return undefined; - } - - lgStringResult = lgStringResult.trim(); - - if (lgStringResult === '' || !lgStringResult.startsWith('{') || !lgStringResult.endsWith('}')) { - return undefined; - } - - try { - lgStructuredResult = JSON.parse(lgStringResult); - } catch (error) { - return undefined; - } - - return lgStructuredResult; - } -} \ No newline at end of file diff --git a/libraries/botbuilder-lg/src/analyzer.ts b/libraries/botbuilder-lg/src/analyzer.ts index 4d709682bf..5be3a97f59 100644 --- a/libraries/botbuilder-lg/src/analyzer.ts +++ b/libraries/botbuilder-lg/src/analyzer.ts @@ -12,10 +12,10 @@ import { EvaluationTarget } from './evaluationTarget'; import { Evaluator } from './evaluator'; import * as lp from './generated/LGFileParser'; import { LGFileParserVisitor } from './generated/LGFileParserVisitor'; -import { LGTemplate } from './lgTemplate'; -import { LGExtensions } from './lgExtensions'; +import { Template } from './template'; +import { TemplateExtensions } from './templateExtensions'; import { AnalyzerResult } from './analyzerResult'; -import {LGErrors} from './lgErrors'; +import {TemplateErrors} from './templateErrors'; /** * Analyzer engine. To to get the static analyzer results. @@ -24,16 +24,16 @@ export class Analyzer extends AbstractParseTreeVisitor implement /** * Templates. */ - public readonly templates: LGTemplate[]; + public readonly templates: Template[]; - private readonly templateMap: {[name: string]: LGTemplate}; + private readonly templateMap: {[name: string]: Template}; private readonly evalutationTargetStack: EvaluationTarget[] = []; private readonly _expressionParser: ExpressionParserInterface; - public constructor(templates: LGTemplate[], expressionParser: ExpressionParser) { + public constructor(templates: Template[], expressionParser: ExpressionParser) { super(); this.templates = templates; - this.templateMap = keyBy(templates, (t: LGTemplate): string => t.name); + this.templateMap = keyBy(templates, (t: Template): string => t.name); // create an evaluator to leverage its customized function look up for checking const evaluator: Evaluator = new Evaluator(this.templates, expressionParser); @@ -47,11 +47,11 @@ export class Analyzer extends AbstractParseTreeVisitor implement */ public analyzeTemplate(templateName: string): AnalyzerResult { if (!(templateName in this.templateMap)) { - throw new Error(LGErrors.templateNotExist(templateName)); + throw new Error(TemplateErrors.templateNotExist(templateName)); } if (this.evalutationTargetStack.find((u: EvaluationTarget): boolean => u.templateName === templateName) !== undefined) { - throw new Error(`${ LGErrors.loopDetected } ${ this.evalutationTargetStack.reverse() + throw new Error(`${ TemplateErrors.loopDetected } ${ this.evalutationTargetStack.reverse() .map((u: EvaluationTarget): string => u.templateName) .join(' => ') }`); } @@ -114,8 +114,8 @@ export class Analyzer extends AbstractParseTreeVisitor implement const values = ctx.keyValueStructureValue(); for (const value of values) { - if (LGExtensions.isPureExpression(value).hasExpr) { - result.union(this.analyzeExpression(LGExtensions.isPureExpression(value).expression)); + if (TemplateExtensions.isPureExpression(value).hasExpr) { + result.union(this.analyzeExpression(TemplateExtensions.isPureExpression(value).expression)); } else { const exprs = value.EXPRESSION_IN_STRUCTURE_BODY(); for (const expr of exprs) { @@ -198,7 +198,7 @@ export class Analyzer extends AbstractParseTreeVisitor implement private analyzeExpression(exp: string): AnalyzerResult { const result: AnalyzerResult = new AnalyzerResult(); - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); const parsed: Expression = this._expressionParser.parse(exp); const references: readonly string[] = Extensions.references(parsed); diff --git a/libraries/botbuilder-lg/src/errorListener.ts b/libraries/botbuilder-lg/src/errorListener.ts index 315b864c95..8e64765ab7 100644 --- a/libraries/botbuilder-lg/src/errorListener.ts +++ b/libraries/botbuilder-lg/src/errorListener.ts @@ -7,10 +7,10 @@ */ import { ANTLRErrorListener, RecognitionException, Recognizer } from 'antlr4ts'; import { Diagnostic, DiagnosticSeverity } from './diagnostic'; -import { LGException } from './lgException'; +import { TemplateException } from './templateException'; import { Position } from './position'; import { Range } from './range'; -import { LGErrors } from './lgErrors'; +import { TemplateErrors } from './templateErrors'; /** * LG parser error listener. @@ -33,8 +33,8 @@ export class ErrorListener implements ANTLRErrorListener { const startPosition: Position = new Position(line, charPositionInLine); const stopPosition: Position = new Position(line, charPositionInLine + offendingSymbol.stopIndex - offendingSymbol.startIndex + 1); const range: Range = new Range(startPosition, stopPosition); - const diagnostic: Diagnostic = new Diagnostic(range, LGErrors.syntaxError, DiagnosticSeverity.Error, this.source); + const diagnostic: Diagnostic = new Diagnostic(range, TemplateErrors.syntaxError, DiagnosticSeverity.Error, this.source); - throw new LGException(diagnostic.toString(), [diagnostic]); + throw new TemplateException(diagnostic.toString(), [diagnostic]); } } diff --git a/libraries/botbuilder-lg/src/evaluator.ts b/libraries/botbuilder-lg/src/evaluator.ts index b217b4a67d..2ae85d2ec4 100644 --- a/libraries/botbuilder-lg/src/evaluator.ts +++ b/libraries/botbuilder-lg/src/evaluator.ts @@ -13,11 +13,11 @@ import { CustomizedMemory } from './customizedMemory'; import { EvaluationTarget } from './evaluationTarget'; import * as lp from './generated/LGFileParser'; import { LGFileParserVisitor } from './generated/LGFileParserVisitor'; -import { LGTemplate } from './lgTemplate'; +import { Template } from './template'; import * as path from 'path'; import * as fs from 'fs'; -import { LGExtensions } from './lgExtensions'; -import { LGErrors } from './lgErrors'; +import { TemplateExtensions } from './templateExtensions'; +import { TemplateErrors } from './templateErrors'; /** * Evaluation runtime engine */ @@ -26,7 +26,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa /** * Templates. */ - public readonly templates: LGTemplate[]; + public readonly templates: Template[]; /** * Expression parser. @@ -36,7 +36,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa /** * TemplateMap. */ - public readonly templateMap: { [name: string]: LGTemplate }; + public readonly templateMap: { [name: string]: Template }; private readonly evaluationTargetStack: EvaluationTarget[] = []; private readonly strictMode: boolean; @@ -49,12 +49,12 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa public static readonly fromFileFunctionName = 'fromFile'; public static readonly templateFunctionName = 'template'; public static readonly isTemplateFunctionName = 'isTemplate'; - private static readonly ReExecuteSuffix = '!'; + public static readonly ReExecuteSuffix = '!'; - public constructor(templates: LGTemplate[], expressionParser: ExpressionParser, strictMode: boolean = false) { + public constructor(templates: Template[], expressionParser: ExpressionParser, strictMode: boolean = false) { super(); this.templates = templates; - this.templateMap = keyBy(templates, (t: LGTemplate): string => t.name); + this.templateMap = keyBy(templates, (t: Template): string => t.name); this.strictMode = strictMode; // generate a new customzied expression parser by injecting the templates as functions @@ -77,11 +77,11 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa ({reExecute, pureTemplateName: templateName} = this.parseTemplateName(inputTemplateName)); if (!(templateName in this.templateMap)) { - throw new Error(LGErrors.templateNotExist(templateName)); + throw new Error(TemplateErrors.templateNotExist(templateName)); } if (this.evaluationTargetStack.some((u: EvaluationTarget): boolean => u.templateName === templateName)) { - throw new Error(`${ LGErrors.loopDetected } ${ this.evaluationTargetStack.reverse() + throw new Error(`${ TemplateErrors.loopDetected } ${ this.evaluationTargetStack.reverse() .map((u: EvaluationTarget): string => u.templateName) .join(' => ') }`); } @@ -148,14 +148,14 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa const result = []; for(const item of values) { - if (LGExtensions.isPureExpression(item).hasExpr) { - result.push(this.evalExpression(LGExtensions.isPureExpression(item).expression, ctx)); + if (TemplateExtensions.isPureExpression(item).hasExpr) { + result.push(this.evalExpression(TemplateExtensions.isPureExpression(item).expression, ctx)); } else { let itemStringResult = ''; for(const node of item.children) { switch ((node as TerminalNode).symbol.type) { case (lp.LGFileParser.ESCAPE_CHARACTER_IN_STRUCTURE_BODY): - itemStringResult += LGExtensions.evalEscape(node.text); + itemStringResult += TemplateExtensions.evalEscape(node.text); break; case (lp.LGFileParser.EXPRESSION_IN_STRUCTURE_BODY): @@ -208,7 +208,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } public visitNormalTemplateString(ctx: lp.NormalTemplateStringContext): string { - const prefixErrorMsg = LGExtensions.getPrefixErrorMessage(ctx); + const prefixErrorMsg = TemplateExtensions.getPrefixErrorMessage(ctx); const result: any[] = []; for (const node of ctx.children) { const innerNode: TerminalNode = node as TerminalNode; @@ -218,7 +218,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa case lp.LGFileParser.DASH: break; case lp.LGFileParser.ESCAPE_CHARACTER: - result.push(LGExtensions.evalEscape(innerNode.text)); + result.push(TemplateExtensions.evalEscape(innerNode.text)); break; case lp.LGFileParser.EXPRESSION: result.push(this.evalExpression(innerNode.text, ctx, prefixErrorMsg)); @@ -247,7 +247,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa var templateName = this.parseTemplateName(inputTemplateName).pureTemplateName; if (!(templateName in this.templateMap)) { - throw new Error(LGErrors.templateNotExist(templateName)); + throw new Error(TemplateErrors.templateNotExist(templateName)); } const parameters: string[] = this.templateMap[templateName].parameters; @@ -262,7 +262,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa parameters.map((e: string, i: number): void => newScope[e] = args[i]); const memory = currentScope as CustomizedMemory; if (!memory) { - throw new Error(LGErrors.invalidMemory); + throw new Error(TemplateErrors.invalidMemory); } return new CustomizedMemory(memory.globalMemory, SimpleObjectMemory.wrap(newScope)); @@ -333,7 +333,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } private evalExpressionInCondition(exp: string, context?: ParserRuleContext, errorPrefix: string = ''): boolean { - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); let result: any; let error: string; ({value: result, error: error} = this.evalByAdaptiveExpression(exp, this.currentTarget().scope)); @@ -349,12 +349,12 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } else if (!result) { - childErrorMsg += LGErrors.nullExpression(exp); + childErrorMsg += TemplateErrors.nullExpression(exp); } if (context) { - errorMsg += LGErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); + errorMsg += TemplateErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); } if (this.evaluationTargetStack.length > 0) @@ -373,7 +373,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa private evalExpression(exp: string, context?: ParserRuleContext, errorPrefix: string = ''): any { - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); let result: any; let error: string; ({value: result, error: error} = this.evalByAdaptiveExpression(exp, this.currentTarget().scope)); @@ -389,12 +389,12 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } else if (result === undefined) { - childErrorMsg += LGErrors.nullExpression(exp); + childErrorMsg += TemplateErrors.nullExpression(exp); } if (context) { - errorMsg += LGErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); + errorMsg += TemplateErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); } if (this.evaluationTargetStack.length > 0) @@ -464,12 +464,12 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } private readonly fromFile = (): any => (args: readonly any[]): any => { - const filePath: string = LGExtensions.normalizePath(args[0].toString()); + const filePath: string = TemplateExtensions.normalizePath(args[0].toString()); const resourcePath: string = this.getResourcePath(filePath); const stringContent = fs.readFileSync(resourcePath, 'utf-8'); const result = this.wrappedEvalTextContainsExpression(stringContent, Evaluator.expressionRecognizeReverseRegex); - return LGExtensions.evalEscape(result); + return TemplateExtensions.evalEscape(result); } private getResourcePath(filePath: string): string { @@ -482,8 +482,8 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa if (inBrowser) { throw new Error('relative path is not support in browser.'); } - const template: LGTemplate = this.templateMap[this.currentTarget().templateName]; - const sourcePath: string = LGExtensions.normalizePath(template.source); + const template: Template = this.templateMap[this.currentTarget().templateName]; + const sourcePath: string = TemplateExtensions.normalizePath(template.source); let baseFolder: string = __dirname; if (path.isAbsolute(sourcePath)) { baseFolder = path.dirname(sourcePath); @@ -518,7 +518,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa // Validate return type if (children0.returnType !== ReturnType.Object && children0.returnType !== ReturnType.String) { - throw new Error(LGErrors.errorTemplateNameformat(children0.toString())); + throw new Error(TemplateErrors.errorTemplateNameformat(children0.toString())); } // Validate more if the name is string constant @@ -531,7 +531,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa private checkTemplateReference(templateName: string, children: Expression[]): void{ if (!(templateName in this.templateMap)) { - throw new Error(LGErrors.templateNotExist(templateName)); + throw new Error(TemplateErrors.templateNotExist(templateName)); } var expectedArgsCount = this.templateMap[templateName].parameters.length; @@ -539,7 +539,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa if (actualArgsCount !== 0 && expectedArgsCount !== actualArgsCount) { - throw new Error(LGErrors.argumentMismatch(templateName, expectedArgsCount, actualArgsCount)); + throw new Error(TemplateErrors.argumentMismatch(templateName, expectedArgsCount, actualArgsCount)); } } @@ -550,18 +550,7 @@ export class Evaluator extends AbstractParseTreeVisitor implements LGFilePa } private readonly validTemplateReference = (expression: Expression): void => { - const templateName: string = expression.type; - - if (!(templateName in this.templateMap)) { - throw new Error(`no such template '${ templateName }' to call in ${ expression }`); - } - - const expectedArgsCount: number = this.templateMap[templateName].parameters.length; - const actualArgsCount: number = expression.children.length; - - if (expectedArgsCount !== actualArgsCount) { - throw new Error(LGErrors.argumentMismatch(templateName, expectedArgsCount, actualArgsCount)); - } + return this.checkTemplateReference(expression.type, expression.children); } private parseTemplateName(templateName: string): { reExecute: boolean; pureTemplateName: string } { diff --git a/libraries/botbuilder-lg/src/expander.ts b/libraries/botbuilder-lg/src/expander.ts index 7aaefbe2fd..8e72a84225 100644 --- a/libraries/botbuilder-lg/src/expander.ts +++ b/libraries/botbuilder-lg/src/expander.ts @@ -7,16 +7,18 @@ */ import { AbstractParseTreeVisitor, TerminalNode } from 'antlr4ts/tree'; import { ParserRuleContext } from 'antlr4ts/ParserRuleContext'; -import { ExpressionFunctions, EvaluatorLookup, Expression, ExpressionParser, ExpressionEvaluator, ReturnType, SimpleObjectMemory } from 'adaptive-expressions'; +import { ExpressionFunctions, EvaluatorLookup, Expression, ExpressionParser, ExpressionEvaluator, ReturnType, SimpleObjectMemory, ExpressionType, Constant } from 'adaptive-expressions'; import { keyBy } from 'lodash'; import { EvaluationTarget } from './evaluationTarget'; import { Evaluator } from './evaluator'; +import * as path from 'path'; +import * as fs from 'fs'; import * as lp from './generated/LGFileParser'; import { LGFileParserVisitor } from './generated/LGFileParserVisitor'; -import { LGTemplate } from './lgTemplate'; -import { LGExtensions } from './lgExtensions'; +import { Template } from './template'; +import { TemplateExtensions } from './templateExtensions'; import { CustomizedMemory } from './customizedMemory'; -import { LGErrors } from './lgErrors'; +import { TemplateErrors } from './templateErrors'; /** * LG template expander. @@ -25,21 +27,21 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi /** * Templates. */ - public readonly templates: LGTemplate[]; + public readonly templates: Template[]; /** * TemplateMap. */ - public readonly templateMap: {[name: string]: LGTemplate}; + public readonly templateMap: {[name: string]: Template}; private readonly evaluationTargetStack: EvaluationTarget[] = []; private readonly expanderExpressionParser: ExpressionParser; private readonly evaluatorExpressionParser: ExpressionParser; private readonly strictMode: boolean; - public constructor(templates: LGTemplate[], expressionParser: ExpressionParser, strictMode: boolean = false) { + public constructor(templates: Template[], expressionParser: ExpressionParser, strictMode: boolean = false) { super(); this.templates = templates; - this.templateMap = keyBy(templates, (t: LGTemplate): string => t.name); + this.templateMap = keyBy(templates, (t: Template): string => t.name); this.strictMode = strictMode; // generate a new customzied expression parser by injecting the template as functions @@ -55,11 +57,11 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi */ public expandTemplate(templateName: string, scope: any): string[] { if (!(templateName in this.templateMap)) { - throw new Error(LGErrors.templateNotExist(templateName)); + throw new Error(TemplateErrors.templateNotExist(templateName)); } if (this.evaluationTargetStack.find((u: EvaluationTarget): boolean => u.templateName === templateName)) { - throw new Error(`${ LGErrors.loopDetected } ${ this.evaluationTargetStack.reverse() + throw new Error(`${ TemplateErrors.loopDetected } ${ this.evaluationTargetStack.reverse() .map((u: EvaluationTarget): string => u.templateName) .join(' => ') }`); } @@ -126,14 +128,14 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi if (value.length > 1) { const valueList = []; for (const item of value) { - const id = LGExtensions.newGuid(); + const id = TemplateExtensions.newGuid(); valueList.push(id); templateRefValues.set(id, item); } expandedResult.forEach((x): any[] => x[property] = valueList); } else { - const id = LGExtensions.newGuid(); + const id = TemplateExtensions.newGuid(); expandedResult.forEach((x): string => x[property] = id); templateRefValues.set(id, value[0]); } @@ -184,14 +186,14 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi let result: any[] = []; for (const item of values) { - if (LGExtensions.isPureExpression(item).hasExpr) { - result.push(this.evalExpression(LGExtensions.isPureExpression(item).expression, ctx)); + if (TemplateExtensions.isPureExpression(item).hasExpr) { + result.push(this.evalExpression(TemplateExtensions.isPureExpression(item).expression, ctx)); } else { let itemStringResult = ['']; for (const node of item.children) { switch ((node as TerminalNode).symbol.type) { case (lp.LGFileParser.ESCAPE_CHARACTER_IN_STRUCTURE_BODY): - itemStringResult = this.stringArrayConcat(itemStringResult, [LGExtensions.evalEscape(node.text)]); + itemStringResult = this.stringArrayConcat(itemStringResult, [TemplateExtensions.evalEscape(node.text)]); break; case (lp.LGFileParser.EXPRESSION_IN_STRUCTURE_BODY): const errorPrefix = `Property '${ ctx.STRUCTURE_IDENTIFIER().text }':`; @@ -248,7 +250,7 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } public visitNormalTemplateString(ctx: lp.NormalTemplateStringContext): string[] { - var prefixErrorMsg = LGExtensions.getPrefixErrorMessage(ctx); + var prefixErrorMsg = TemplateExtensions.getPrefixErrorMessage(ctx); let result: string[] = ['']; for (const node of ctx.children) { const innerNode: TerminalNode = node as TerminalNode; @@ -258,7 +260,7 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi case lp.LGFileParser.DASH: break; case lp.LGFileParser.ESCAPE_CHARACTER: - result = this.stringArrayConcat(result, [LGExtensions.evalEscape(innerNode.text)]); + result = this.stringArrayConcat(result, [TemplateExtensions.evalEscape(innerNode.text)]); break; case lp.LGFileParser.EXPRESSION: { result = this.stringArrayConcat(result, this.evalExpression(innerNode.text, ctx, prefixErrorMsg)); @@ -310,7 +312,7 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } private evalExpressionInCondition(exp: string, context?: ParserRuleContext, errorPrefix: string = ''): boolean { - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); let result: any; let error: string; ({value: result, error: error} = this.evalByAdaptiveExpression(exp, this.currentTarget().scope)); @@ -326,12 +328,12 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } else if (!result) { - childErrorMsg += LGErrors.nullExpression(exp); + childErrorMsg += TemplateErrors.nullExpression(exp); } if (context) { - errorMsg += LGErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); + errorMsg += TemplateErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); } if (this.evaluationTargetStack.length > 0) @@ -349,7 +351,7 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } private evalExpression(exp: string, context: ParserRuleContext, errorPrefix: string = ''): string[] { - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); let result: any; let error: string; ({value: result, error: error} = this.evalByAdaptiveExpression(exp, this.currentTarget().scope)); @@ -365,12 +367,12 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } else if (result === undefined) { - childErrorMsg += LGErrors.nullExpression(exp); + childErrorMsg += TemplateErrors.nullExpression(exp); } if (context !== undefined) { - errorMsg += LGErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); + errorMsg += TemplateErrors.errorExpression(context.text, this.currentTarget().templateName, errorPrefix); } if (this.evaluationTargetStack.length > 0) @@ -412,21 +414,46 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi } private readonly customizedEvaluatorLookup = (baseLookup: EvaluatorLookup, isExpander: boolean): any => (name: string): ExpressionEvaluator => { - const prebuiltPrefix = 'prebuilt.'; + const standardFunction = baseLookup(name); - if (name.startsWith(prebuiltPrefix)) { - return baseLookup(name.substring(prebuiltPrefix.length)); + if (standardFunction !== undefined) { + return standardFunction; + } + + if (name.startsWith('lg.')) { + name = name.substring(3); } - if (this.templateMap[name]) { + var templateName = this.parseTemplateName(name).pureTemplateName; + if (templateName in this.templateMap) { if (isExpander) { - return new ExpressionEvaluator(name, ExpressionFunctions.apply(this.templateExpander(name)), ReturnType.String, this.validTemplateReference); + return new ExpressionEvaluator(templateName, ExpressionFunctions.apply(this.templateExpander(name)), ReturnType.Object, this.validTemplateReference); } else { - return new ExpressionEvaluator(name, ExpressionFunctions.apply(this.templateEvaluator(name)), ReturnType.String, this.validTemplateReference); + return new ExpressionEvaluator(templateName, ExpressionFunctions.apply(this.templateEvaluator(name)), ReturnType.Object, this.validTemplateReference); } } - return baseLookup(name); + if (name === Evaluator.templateFunctionName) { + return new ExpressionEvaluator(Evaluator.templateFunctionName, ExpressionFunctions.apply(this.templateFunction()), ReturnType.Object, this.validateTemplateFunction); + } + + if (name === Evaluator.fromFileFunctionName) { + return new ExpressionEvaluator(Evaluator.fromFileFunctionName, ExpressionFunctions.apply(this.fromFile()), ReturnType.Object, ExpressionFunctions.validateUnaryString); + } + + if (name === Evaluator.activityAttachmentFunctionName) { + return new ExpressionEvaluator( + Evaluator.activityAttachmentFunctionName, + ExpressionFunctions.apply(this.activityAttachment()), + ReturnType.Object, + (expr): void => ExpressionFunctions.validateOrder(expr, undefined, ReturnType.Object, ReturnType.String)); + } + + if (name === Evaluator.isTemplateFunctionName) { + return new ExpressionEvaluator(Evaluator.isTemplateFunctionName, ExpressionFunctions.apply(this.isTemplate()), ReturnType.Boolean, ExpressionFunctions.validateUnaryString); + } + + return undefined; } private readonly templateEvaluator = (templateName: string): any => (args: readonly any[]): string => { @@ -444,21 +471,6 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi return this.expandTemplate(templateName, newScope); } - private readonly validTemplateReference = (expression: Expression): void => { - const templateName: string = expression.type; - - if (!this.templateMap[templateName]) { - throw new Error(LGErrors.templateNotExist(templateName)); - } - - const expectedArgsCount: number = this.templateMap[templateName].parameters.length; - const actualArgsCount: number = expression.children.length; - - if (expectedArgsCount !== actualArgsCount) { - throw new Error(LGErrors.argumentMismatch(templateName, expectedArgsCount, actualArgsCount)); - } - } - private reconstructExpression(expanderExpression: Expression, evaluatorExpression: Expression, foundPrebuiltFunction: boolean): Expression { if (this.templateMap[expanderExpression.type]) { if (foundPrebuiltFunction) { @@ -474,4 +486,110 @@ export class Expander extends AbstractParseTreeVisitor implements LGFi return expanderExpression; } + + private readonly isTemplate = (): any => (args: readonly any[]): boolean => { + const templateName = args[0].toString(); + return templateName in this.templateMap; + } + + private readonly fromFile = (): any => (args: readonly any[]): any => { + const filePath: string = TemplateExtensions.normalizePath(args[0].toString()); + const resourcePath: string = this.getResourcePath(filePath); + const stringContent = fs.readFileSync(resourcePath, 'utf-8'); + + // TODO + return stringContent; + //const result = this.wrappedEvalTextContainsExpression(stringContent, Evaluator.expressionRecognizeReverseRegex); + //return TemplateExtensions.evalEscape(result); + } + + private getResourcePath(filePath: string): string { + let resourcePath: string; + if (path.isAbsolute(filePath)) { + resourcePath = filePath; + } else { + // relative path is not support in broswer environment + const inBrowser: boolean = typeof window !== 'undefined'; + if (inBrowser) { + throw new Error('relative path is not support in browser.'); + } + const template: Template = this.templateMap[this.currentTarget().templateName]; + const sourcePath: string = TemplateExtensions.normalizePath(template.source); + let baseFolder: string = __dirname; + if (path.isAbsolute(sourcePath)) { + baseFolder = path.dirname(sourcePath); + } + + resourcePath = path.join(baseFolder, filePath); + } + + return resourcePath; + } + + private readonly activityAttachment = (): any => (args: readonly any[]): any => { + return { + [Evaluator.LGType]: 'attachment', + contenttype: args[1].toString(), + content: args[0] + }; + } + + private readonly templateFunction = (): any => (args: readonly any[]): any => { + const templateName: string = args[0]; + const newScope: any = this.constructScope(templateName, args.slice(1)); + + const value: string[] = this.expandTemplate(templateName, newScope); + const randomNumber: number = Math.floor(Math.random() * value.length); + + return value[randomNumber]; + } + + private readonly validateTemplateFunction = (expression: Expression): void => { + + ExpressionFunctions.validateAtLeastOne(expression); + + const children0: Expression = expression.children[0]; + + // Validate return type + if (children0.returnType !== ReturnType.Object && children0.returnType !== ReturnType.String) { + throw new Error(TemplateErrors.errorTemplateNameformat(children0.toString())); + } + + // Validate more if the name is string constant + if (children0.type === ExpressionType.Constant) { + const templateName: string = (children0 as Constant).value; + this.checkTemplateReference(templateName, expression.children.slice(1)); + } + } + + private checkTemplateReference(templateName: string, children: Expression[]): void{ + if (!(templateName in this.templateMap)) + { + throw new Error(TemplateErrors.templateNotExist(templateName)); + } + + var expectedArgsCount = this.templateMap[templateName].parameters.length; + var actualArgsCount = children.length; + + if (actualArgsCount !== 0 && expectedArgsCount !== actualArgsCount) + { + throw new Error(TemplateErrors.argumentMismatch(templateName, expectedArgsCount, actualArgsCount)); + } + } + + private readonly validTemplateReference = (expression: Expression): void => { + return this.checkTemplateReference(expression.type, expression.children); + } + + private parseTemplateName(templateName: string): { reExecute: boolean; pureTemplateName: string } { + if (!templateName) { + throw new Error('template name is empty.'); + } + + if (templateName.endsWith(Evaluator.ReExecuteSuffix)) { + return {reExecute:true, pureTemplateName: templateName.substr(0, templateName.length - Evaluator.ReExecuteSuffix.length)}; + } else { + return {reExecute:false, pureTemplateName: templateName}; + } + } } diff --git a/libraries/botbuilder-lg/src/extractor.ts b/libraries/botbuilder-lg/src/extractor.ts index e9fc68d2ca..c759d1190e 100644 --- a/libraries/botbuilder-lg/src/extractor.ts +++ b/libraries/botbuilder-lg/src/extractor.ts @@ -10,23 +10,23 @@ import { AbstractParseTreeVisitor, TerminalNode } from 'antlr4ts/tree'; import { keyBy } from 'lodash'; import * as lp from './generated/LGFileParser'; import { LGFileParserVisitor } from './generated/LGFileParserVisitor'; -import { LGTemplate } from './lgTemplate'; +import { Template } from './template'; /** * Lg template extracter. */ export class Extractor extends AbstractParseTreeVisitor> implements LGFileParserVisitor> { - public readonly templates: LGTemplate[]; - public readonly templateMap: {[name: string]: LGTemplate}; - public constructor(templates: LGTemplate[]) { + public readonly templates: Template[]; + public readonly templateMap: {[name: string]: Template}; + public constructor(templates: Template[]) { super(); this.templates = templates; - this.templateMap = keyBy(templates, (t: LGTemplate): string => t.name); + this.templateMap = keyBy(templates, (t: Template): string => t.name); } public extract(): Map[] { const result: Map[] = []; - this.templates.forEach((template: LGTemplate): any => { + this.templates.forEach((template: Template): any => { result.push(this.visit(template.parseTree)); }); diff --git a/libraries/botbuilder-lg/src/index.ts b/libraries/botbuilder-lg/src/index.ts index 621649aebc..cb4b167944 100644 --- a/libraries/botbuilder-lg/src/index.ts +++ b/libraries/botbuilder-lg/src/index.ts @@ -5,26 +5,25 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -export * from './lgFile'; +export * from './templates'; export * from './evaluator'; -export * from './lgParser'; +export * from './templateParser'; export * from './generated'; export * from './staticChecker'; export * from './analyzer'; -export * from './lgTemplate'; +export * from './template'; export * from './diagnostic'; -export * from './lgException'; +export * from './templateException'; export * from './extractor'; -export * from './lgImport'; +export * from './templateImport'; export * from './range'; export * from './position'; export * from './evaluationTarget'; -export * from './activityFactory'; -export * from './activityChecker'; -export * from './lgExtensions'; +export * from './templateExtensions'; export * from './analyzerResult'; -export * from './lgErrors'; +export * from './templateErrors'; export * from './evaluator'; export * from './errorListener'; export * from './customizedMemory'; export * from './expander'; +export * from './multiLanguageLG'; diff --git a/libraries/botbuilder-lg/src/multiLanguageLG.ts b/libraries/botbuilder-lg/src/multiLanguageLG.ts new file mode 100644 index 0000000000..51f3336069 --- /dev/null +++ b/libraries/botbuilder-lg/src/multiLanguageLG.ts @@ -0,0 +1,144 @@ +import { Templates } from './templates'; + +/** + * @module botbuilder-lg + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class MultiLanguageLG { + public languageFallbackPolicy: {}; + public lgPerLocale: Map; + + private readonly locales = [ + '','aa','aa-dj','aa-er','aa-et','af','af-na','af-za','agq','agq-cm','ak','ak-gh','am','am-et','ar','ar-001', + 'ar-ae','ar-bh','ar-dj','ar-dz','ar-eg','ar-er','ar-il','ar-iq','ar-jo','ar-km','ar-kw','ar-lb','ar-ly','ar-ma', + 'ar-mr','ar-om','ar-ps','ar-qa','ar-sa','ar-sd','ar-so','ar-ss','ar-sy','ar-td','ar-tn','ar-ye','arn','arn-cl', + 'as','as-in','asa','asa-tz','ast','ast-es','az','az-cyrl','az-cyrl-az','az-latn','az-latn-az','ba','ba-ru','bas', + 'bas-cm','be','be-by','bem','bem-zm','bez','bez-tz','bg','bg-bg','bin','bin-ng','bm','bm-latn','bm-latn-ml','bn', + 'bn-bd','bn-in','bo','bo-cn','bo-in','br','br-fr','brx','brx-in','bs','bs-cyrl','bs-cyrl-ba','bs-latn', + 'bs-latn-ba','byn','byn-er','ca','ca-ad','ca-es','ca-es-valencia','ca-fr','ca-it','ce','ce-ru','cgg','cgg-ug', + 'chr','chr-cher','chr-cher-us','co','co-fr','cs','cs-cz','cu','cu-ru','cy','cy-gb','da','da-dk','da-gl','dav', + 'dav-ke','de','de-at','de-be','de-ch','de-de','de-it','de-li','de-lu','dje','dje-ne','dsb','dsb-de','dua', + 'dua-cm','dv','dv-mv','dyo','dyo-sn','dz','dz-bt','ebu','ebu-ke','ee','ee-gh','ee-tg','el','el-cy','el-gr','en', + 'en-001','en-029','en-150','en-ag','en-ai','en-as','en-at','en-au','en-bb','en-be','en-bi','en-bm','en-bs', + 'en-bw','en-bz','en-ca','en-cc','en-ch','en-ck','en-cm','en-cx','en-cy','en-de','en-dk','en-dm','en-er','en-fi', + 'en-fj','en-fk','en-fm','en-gb','en-gd','en-gg','en-gh','en-gi','en-gm','en-gu','en-gy','en-hk','en-id','en-ie', + 'en-il','en-im','en-in','en-io','en-je','en-jm','en-ke','en-ki','en-kn','en-ky','en-lc','en-lr','en-ls','en-mg', + 'en-mh','en-mo','en-mp','en-ms','en-mt','en-mu','en-mw','en-my','en-na','en-nf','en-ng','en-nl','en-nr','en-nu', + 'en-nz','en-pg','en-ph','en-pk','en-pn','en-pr','en-pw','en-rw','en-sb','en-sc','en-sd','en-se','en-sg','en-sh', + 'en-si','en-sl','en-ss','en-sx','en-sz','en-tc','en-tk','en-to','en-tt','en-tv','en-tz','en-ug','en-um','en-us', + 'en-vc','en-vg','en-vi','en-vu','en-ws','en-za','en-zm','en-zw','eo','eo-001','es','es-419','es-ar','es-bo', + 'es-br','es-bz','es-cl','es-co','es-cr','es-cu','es-do','es-ec','es-es','es-gq','es-gt','es-hn','es-mx','es-ni', + 'es-pa','es-pe','es-ph','es-pr','es-py','es-sv','es-us','es-uy','es-ve','et','et-ee','eu','eu-es','ewo','ewo-cm', + 'fa','fa-ir','ff','ff-latn','ff-latn-bf','ff-latn-cm','ff-latn-gh','ff-latn-gm','ff-latn-gn','ff-latn-gw', + 'ff-latn-lr','ff-latn-mr','ff-latn-ne','ff-latn-ng','ff-latn-sl','ff-latn-sn','fi','fi-fi','fil','fil-ph','fo', + 'fo-dk','fo-fo','fr','fr-029','fr-be','fr-bf','fr-bi','fr-bj','fr-bl','fr-ca','fr-cd','fr-cf','fr-cg','fr-ch', + 'fr-ci','fr-cm','fr-dj','fr-dz','fr-fr','fr-ga','fr-gf','fr-gn','fr-gp','fr-gq','fr-ht','fr-km','fr-lu','fr-ma', + 'fr-mc','fr-mf','fr-mg','fr-ml','fr-mq','fr-mr','fr-mu','fr-nc','fr-ne','fr-pf','fr-pm','fr-re','fr-rw','fr-sc', + 'fr-sn','fr-sy','fr-td','fr-tg','fr-tn','fr-vu','fr-wf','fr-yt','fur','fur-it','fy','fy-nl','ga','ga-ie','gd', + 'gd-gb','gl','gl-es','gn','gn-py','gsw','gsw-ch','gsw-fr','gsw-li','gu','gu-in','guz','guz-ke','gv','gv-im','ha', + 'ha-latn','ha-latn-gh','ha-latn-ne','ha-latn-ng','haw','haw-us','he','he-il','hi','hi-in','hr','hr-ba','hr-hr', + 'hsb','hsb-de','hu','hu-hu','hy','hy-am','ia','ia-001','ibb','ibb-ng','id','id-id','ig','ig-ng','ii','ii-cn', + 'is','is-is','it','it-ch','it-it','it-sm','it-va','iu','iu-cans','iu-cans-ca','iu-latn','iu-latn-ca','ja', + 'ja-jp','jgo','jgo-cm','jmc','jmc-tz','jv','jv-java','jv-java-id','jv-latn','jv-latn-id','ka','ka-ge','kab', + 'kab-dz','kam','kam-ke','kde','kde-tz','kea','kea-cv','khq','khq-ml','ki','ki-ke','kk','kk-kz','kkj','kkj-cm', + 'kl','kl-gl','kln','kln-ke','km','km-kh','kn','kn-in','ko','ko-kp','ko-kr','kok','kok-in','kr','kr-latn', + 'kr-latn-ng','ks','ks-arab','ks-arab-in','ks-deva','ks-deva-in','ksb','ksb-tz','ksf','ksf-cm','ksh','ksh-de', + 'ku','ku-arab','ku-arab-iq','ku-arab-ir','kw','kw-gb','ky','ky-kg','la','la-001','lag','lag-tz','lb','lb-lu', + 'lg','lg-ug','lkt','lkt-us','ln','ln-ao','ln-cd','ln-cf','ln-cg','lo','lo-la','lrc','lrc-iq','lrc-ir','lt', + 'lt-lt','lu','lu-cd','luo','luo-ke','luy','luy-ke','lv','lv-lv','mas','mas-ke','mas-tz','mer','mer-ke','mfe', + 'mfe-mu','mg','mg-mg','mgh','mgh-mz','mgo','mgo-cm','mi','mi-nz','mk','mk-mk','ml','ml-in','mn','mn-cyrl', + 'mn-mn','mn-mong','mn-mong-cn','mn-mong-mn','mni','mni-in','moh','moh-ca','mr','mr-in','ms','ms-bn','ms-my', + 'ms-sg','mt','mt-mt','mua','mua-cm','my','my-mm','mzn','mzn-ir','naq','naq-na','nb','nb-no','nb-sj','nd','nd-zw', + 'nds','nds-de','nds-nl','ne','ne-in','ne-np','nl','nl-aw','nl-be','nl-bq','nl-cw','nl-nl','nl-sr','nl-sx','nmg', + 'nmg-cm','nn','nn-no','nnh','nnh-cm','no','nqo','nqo-gn','nr','nr-za','nso','nso-za','nus','nus-ss','nyn', + 'nyn-ug','oc','oc-fr','om','om-et','om-ke','or','or-in','os','os-ge','os-ru','pa','pa-arab','pa-arab-pk', + 'pa-guru','pa-in','pap','pap-029','pl','pl-pl','prg','prg-001','prs','prs-af','ps','ps-af','pt','pt-ao','pt-br', + 'pt-ch','pt-cv','pt-gq','pt-gw','pt-lu','pt-mo','pt-mz','pt-pt','pt-st','pt-tl','quc','quc-latn','quc-latn-gt', + 'quz','quz-bo','quz-ec','quz-pe','rm','rm-ch','rn','rn-bi','ro','ro-md','ro-ro','rof','rof-tz','ru','ru-by', + 'ru-kg','ru-kz','ru-md','ru-ru','ru-ua','rw','rw-rw','rwk','rwk-tz','sa','sa-in','sah','sah-ru','saq','saq-ke', + 'sbp','sbp-tz','sd','sd-arab','sd-arab-pk','sd-deva','sd-deva-in','se','se-fi','se-no','se-se','seh','seh-mz', + 'ses','ses-ml','sg','sg-cf','shi','shi-latn','shi-latn-ma','shi-tfng','shi-tfng-ma','si','si-lk','sk','sk-sk', + 'sl','sl-si','sma','sma-no','sma-se','smj','smj-no','smj-se','smn','smn-fi','sms','sms-fi','sn','sn-latn', + 'sn-latn-zw','so','so-dj','so-et','so-ke','so-so','sq','sq-al','sq-mk','sq-xk','sr','sr-cyrl','sr-cyrl-ba', + 'sr-cyrl-me','sr-cyrl-rs','sr-cyrl-xk','sr-latn','sr-latn-ba','sr-latn-me','sr-latn-rs','sr-latn-xk','ss', + 'ss-sz','ss-za','ssy','ssy-er','st','st-ls','st-za','sv','sv-ax','sv-fi','sv-se','sw','sw-cd','sw-ke','sw-tz', + 'sw-ug','syr','syr-sy','ta','ta-in','ta-lk','ta-my','ta-sg','te','te-in','teo','teo-ke','teo-ug','tg','tg-cyrl', + 'tg-cyrl-tj','th','th-th','ti','ti-er','ti-et','tig','tig-er','tk','tk-tm','tn','tn-bw','tn-za','to','to-to', + 'tr','tr-cy','tr-tr','ts','ts-za','tt','tt-ru','twq','twq-ne','tzm','tzm-arab','tzm-arab-ma','tzm-latn', + 'tzm-latn-dz','tzm-latn-ma','tzm-tfng','tzm-tfng-ma','ug','ug-cn','uk','uk-ua','ur','ur-in','ur-pk','uz', + 'uz-arab','uz-arab-af','uz-cyrl','uz-cyrl-uz','uz-latn','uz-latn-uz','vai','vai-latn','vai-latn-lr','vai-vaii', + 'vai-vaii-lr','ve','ve-za','vi','vi-vn','vo','vo-001','vun','vun-tz','wae','wae-ch','wal','wal-et','wo','wo-sn', + 'xh','xh-za','xog','xog-ug','yav','yav-cm','yi','yi-001','yo','yo-bj','yo-ng','zgh','zgh-tfng','zgh-tfng-ma', + 'zh','zh-cn','zh-hans','zh-hans-hk','zh-hans-mo','zh-hant','zh-hk','zh-mo','zh-sg','zh-tw','zu','zu-za']; + + public constructor(localeLGFiles: Map) { + this.lgPerLocale = new Map(); + this.languageFallbackPolicy = this.getDefaultPolicy(); + + if (!localeLGFiles) { + throw new Error(`input is empty`); + } + + for (const filesPerLocale of localeLGFiles.entries()) { + this.lgPerLocale.set(filesPerLocale[0], Templates.parseFile(filesPerLocale[1])); + } + } + + public generate(template: string, data?: object, locale?: string): any { + if (!template) { + throw new Error('template is empty'); + } + + if (!locale) { + locale = ''; + } + + if (this.lgPerLocale.has(locale)) { + return this.lgPerLocale.get(locale).evaluateText(template, data); + } else { + let locales: string[] = ['']; + if (!(locale in this.languageFallbackPolicy)) { + if (!('' in this.languageFallbackPolicy)) { + throw Error(`No supported language found for ${ locale }`); + } + } else { + locales = this.languageFallbackPolicy[locale]; + } + + for (const fallBackLocale of locales) { + if (this.lgPerLocale.has(fallBackLocale)) { + return this.lgPerLocale.get(fallBackLocale).evaluateText(template, data); + } + } + } + + throw new Error(`No LG responses found for locale: ${ locale }`); + + } + + private getDefaultPolicy(): any { + var result = {}; + for (const locale of this.locales) { + let lang = locale.toLowerCase(); + const fallback: string[] = []; + while (lang) { + fallback.push(lang); + const i = lang.lastIndexOf('-'); + if (i > 0) { + lang = lang.substr(0, i); + } else { + break; + } + } + + fallback.push(''); + result[locale] = fallback; + } + + return result; + } +} diff --git a/libraries/botbuilder-lg/src/staticChecker.ts b/libraries/botbuilder-lg/src/staticChecker.ts index 230eeb1535..c0fe8a95f1 100644 --- a/libraries/botbuilder-lg/src/staticChecker.ts +++ b/libraries/botbuilder-lg/src/staticChecker.ts @@ -12,24 +12,24 @@ import { Diagnostic, DiagnosticSeverity } from './diagnostic'; import { Evaluator } from './evaluator'; import * as lp from './generated/LGFileParser'; import { LGFileParserVisitor } from './generated/LGFileParserVisitor'; -import { LGFile } from './lgFile'; -import { LGErrors } from './lgErrors'; +import { Templates } from './templates'; +import { TemplateErrors } from './templateErrors'; import { Position } from './position'; import { Range } from './range'; -import { LGExtensions } from './lgExtensions'; +import { TemplateExtensions } from './templateExtensions'; /// /// LG managed code checker. /// export class StaticChecker extends AbstractParseTreeVisitor implements LGFileParserVisitor { private readonly baseExpressionParser: ExpressionParser; - private readonly lgFile: LGFile; + private readonly templates: Templates; private visitedTemplateNames: string[]; private _expressionParser: ExpressionParserInterface; - public constructor(lgFile: LGFile, expressionParser?: ExpressionParser) { + public constructor(templates: Templates, expressionParser?: ExpressionParser) { super(); - this.lgFile = lgFile; + this.templates = templates; this.baseExpressionParser = expressionParser || new ExpressionParser(); } @@ -37,7 +37,7 @@ export class StaticChecker extends AbstractParseTreeVisitor implem private get expressionParser(): ExpressionParserInterface { if (this._expressionParser === undefined) { // create an evaluator to leverage it's customized function look up for checking - var evaluator = new Evaluator(this.lgFile.allTemplates, this.baseExpressionParser); + var evaluator = new Evaluator(this.templates.allTemplates, this.baseExpressionParser); this._expressionParser = evaluator.expressionParser; } @@ -52,14 +52,14 @@ export class StaticChecker extends AbstractParseTreeVisitor implem this.visitedTemplateNames = []; var result = []; - if (this.lgFile.allTemplates.length === 0) + if (this.templates.allTemplates.length === 0) { - result.push(this.buildLGDiagnostic(LGErrors.noTemplate, DiagnosticSeverity.Warning, undefined, false)); + result.push(this.buildLGDiagnostic(TemplateErrors.noTemplate, DiagnosticSeverity.Warning, undefined, false)); return result; } - this.lgFile.templates.forEach((t): Diagnostic[] => result = result.concat(this.visit(t.parseTree))); + this.templates.toArray().forEach((t): Diagnostic[] => result = result.concat(this.visit(t.parseTree))); return result; } @@ -69,25 +69,25 @@ export class StaticChecker extends AbstractParseTreeVisitor implem var templateNameLine = context.templateNameLine(); var errorTemplateName = templateNameLine.errorTemplateName(); if (errorTemplateName) { - result.push(this.buildLGDiagnostic(LGErrors.invalidTemplateName, undefined, errorTemplateName, false)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidTemplateName, undefined, errorTemplateName, false)); } else { var templateName = context.templateNameLine().templateName().text; if (this.visitedTemplateNames.includes(templateName)) { - result.push(this.buildLGDiagnostic(LGErrors.duplicatedTemplateInSameTemplate(templateName),undefined, templateNameLine)); + result.push(this.buildLGDiagnostic(TemplateErrors.duplicatedTemplateInSameTemplate(templateName),undefined, templateNameLine)); } else { this.visitedTemplateNames.push(templateName); - for (const reference of this.lgFile.references) { - var sameTemplates = reference.templates.filter((u): boolean => u.name === templateName); + for (const reference of this.templates.references) { + var sameTemplates = reference.toArray().filter((u): boolean => u.name === templateName); for(const sameTemplate of sameTemplates) { - result.push(this.buildLGDiagnostic( LGErrors.duplicatedTemplateInDiffTemplate(sameTemplate.name, sameTemplate.source), undefined, templateNameLine)); + result.push(this.buildLGDiagnostic( TemplateErrors.duplicatedTemplateInDiffTemplate(sameTemplate.name, sameTemplate.source), undefined, templateNameLine)); } } if (result.length > 0) { return result; } else if (!context.templateBody()) { - result.push(this.buildLGDiagnostic(LGErrors.noTemplateBody(templateName), DiagnosticSeverity.Warning, context.templateNameLine())); + result.push(this.buildLGDiagnostic(TemplateErrors.noTemplateBody(templateName), DiagnosticSeverity.Warning, context.templateNameLine())); } else { result = result.concat(this.visit(context.templateBody())); } @@ -103,7 +103,7 @@ export class StaticChecker extends AbstractParseTreeVisitor implem const errorTemplateStr: lp.ErrorTemplateStringContext = templateStr.errorTemplateString(); if (errorTemplateStr) { - result.push(this.buildLGDiagnostic(LGErrors.invalidTemplateBody, undefined, errorTemplateStr)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidTemplateBody, undefined, errorTemplateStr)); } else { result = result.concat(this.visit(templateStr)); } @@ -116,20 +116,20 @@ export class StaticChecker extends AbstractParseTreeVisitor implem let result: Diagnostic[] = []; if (context.structuredBodyNameLine().errorStructuredName() !== undefined) { - result.push(this.buildLGDiagnostic(LGErrors.invalidStrucName, undefined, context.structuredBodyNameLine())); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidStrucName, undefined, context.structuredBodyNameLine())); } if (context.structuredBodyEndLine() === undefined) { - result.push(this.buildLGDiagnostic(LGErrors.missingStrucEnd, undefined, context)); + result.push(this.buildLGDiagnostic(TemplateErrors.missingStrucEnd, undefined, context)); } const bodys = context.structuredBodyContentLine(); if (!bodys || bodys.length === 0) { - result.push(this.buildLGDiagnostic(LGErrors.emptyStrucContent, undefined, context)); + result.push(this.buildLGDiagnostic(TemplateErrors.emptyStrucContent, undefined, context)); } else { for (const body of bodys) { if (body.errorStructureLine() !== undefined) { - result.push(this.buildLGDiagnostic(LGErrors.invalidStrucBody, undefined, body.errorStructureLine())); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidStrucBody, undefined, body.errorStructureLine())); } else if (body.objectStructureLine() !== undefined) { result = result.concat(this.checkExpression(body.objectStructureLine().text, body.objectStructureLine())); } else { @@ -163,41 +163,41 @@ export class StaticChecker extends AbstractParseTreeVisitor implem conditionNode.ELSE(); if (node.text.split(' ').length - 1 > 1) { - result.push(this.buildLGDiagnostic(LGErrors.invalidWhitespaceInCondition, undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidWhitespaceInCondition, undefined, conditionNode)); } if (idx === 0 && !ifExpr) { - result.push(this.buildLGDiagnostic(LGErrors.notStartWithIfInCondition, DiagnosticSeverity.Warning, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.notStartWithIfInCondition, DiagnosticSeverity.Warning, conditionNode)); } if (idx > 0 && ifExpr) { - result.push(this.buildLGDiagnostic(LGErrors.multipleIfInCondition, undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.multipleIfInCondition, undefined, conditionNode)); } if (idx === ifRules.length - 1 && !elseExpr) { - result.push(this.buildLGDiagnostic(LGErrors.notEndWithElseInCondition, DiagnosticSeverity.Warning, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.notEndWithElseInCondition, DiagnosticSeverity.Warning, conditionNode)); } if (idx > 0 && idx < ifRules.length - 1 && !elseIfExpr) { - result.push(this.buildLGDiagnostic(LGErrors.invalidMiddleInCondition, undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidMiddleInCondition, undefined, conditionNode)); } if (!elseExpr) { if (ifRule.ifCondition().EXPRESSION().length !== 1) { - result.push(this.buildLGDiagnostic(LGErrors.invalidExpressionInCondition,undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidExpressionInCondition,undefined, conditionNode)); } else { const errorPrefix = `Condition '` + conditionNode.EXPRESSION(0).text + `': `; result = result.concat(this.checkExpression(ifRule.ifCondition().EXPRESSION(0).text, conditionNode, errorPrefix)); } } else { if (ifRule.ifCondition().EXPRESSION().length !== 0) { - result.push(this.buildLGDiagnostic(LGErrors.extraExpressionInCondition, undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.extraExpressionInCondition, undefined, conditionNode)); } } if (ifRule.normalTemplateBody() !== undefined) { result = result.concat(this.visit(ifRule.normalTemplateBody())); } else { - result.push(this.buildLGDiagnostic(LGErrors.missingTemplateBodyInCondition, undefined, conditionNode)); + result.push(this.buildLGDiagnostic(TemplateErrors.missingTemplateBodyInCondition, undefined, conditionNode)); } idx = idx + 1; @@ -221,33 +221,33 @@ export class StaticChecker extends AbstractParseTreeVisitor implem caseExpr ? switchCaseStat.CASE() : switchCaseStat.DEFAULT(); if (node.text.split(' ').length - 1 > 1) { - result.push(this.buildLGDiagnostic(LGErrors.invalidWhitespaceInSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidWhitespaceInSwitchCase, undefined, switchCaseStat)); } if (idx === 0 && !switchExpr) { - result.push(this.buildLGDiagnostic(LGErrors.notStartWithSwitchInSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.notStartWithSwitchInSwitchCase, undefined, switchCaseStat)); } if (idx > 0 && switchExpr) { - result.push(this.buildLGDiagnostic(LGErrors.multipleSwithStatementInSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.multipleSwithStatementInSwitchCase, undefined, switchCaseStat)); } if (idx > 0 && idx < length - 1 && !caseExpr) { - result.push(this.buildLGDiagnostic(LGErrors.invalidStatementInMiddlerOfSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidStatementInMiddlerOfSwitchCase, undefined, switchCaseStat)); } if (idx === length - 1 && (caseExpr || defaultExpr)) { if (caseExpr) { - result.push(this.buildLGDiagnostic(LGErrors.notEndWithDefaultInSwitchCase, DiagnosticSeverity.Warning, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.notEndWithDefaultInSwitchCase, DiagnosticSeverity.Warning, switchCaseStat)); } else { if (length === 2) { - result.push(this.buildLGDiagnostic(LGErrors.missingCaseInSwitchCase, DiagnosticSeverity.Warning, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.missingCaseInSwitchCase, DiagnosticSeverity.Warning, switchCaseStat)); } } } if (switchExpr || caseExpr) { if (switchCaseStat.EXPRESSION().length !== 1) { - result.push(this.buildLGDiagnostic(LGErrors.invalidExpressionInSwiathCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.invalidExpressionInSwiathCase, undefined, switchCaseStat)); } else { let errorPrefix = switchExpr ? 'Switch' : 'Case'; errorPrefix += ` '${ switchCaseStat.EXPRESSION(0).text }': `; @@ -255,14 +255,14 @@ export class StaticChecker extends AbstractParseTreeVisitor implem } } else { if (switchCaseStat.EXPRESSION().length !== 0 || switchCaseStat.TEXT().length !== 0) { - result.push(this.buildLGDiagnostic(LGErrors.extraExpressionInSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.extraExpressionInSwitchCase, undefined, switchCaseStat)); } } if (caseExpr || defaultExpr) { if (iterNode.normalTemplateBody()) { result = result.concat(this.visit(iterNode.normalTemplateBody())); } else { - result.push(this.buildLGDiagnostic(LGErrors.missingTemplateBodyInSwitchCase, undefined, switchCaseStat)); + result.push(this.buildLGDiagnostic(TemplateErrors.missingTemplateBodyInSwitchCase, undefined, switchCaseStat)); } } idx = idx + 1; @@ -272,7 +272,7 @@ export class StaticChecker extends AbstractParseTreeVisitor implem } public visitNormalTemplateString(context: lp.NormalTemplateStringContext): Diagnostic[] { - const prefixErrorMsg = LGExtensions.getPrefixErrorMessage(context); + const prefixErrorMsg = TemplateExtensions.getPrefixErrorMessage(context); let result: Diagnostic[] = []; for (const expression of context.EXPRESSION()) { @@ -283,7 +283,7 @@ export class StaticChecker extends AbstractParseTreeVisitor implem const multiLineSuffix = context.MULTILINE_SUFFIX(); if (multiLinePrefix !== undefined && multiLineSuffix === undefined) { - result.push(this.buildLGDiagnostic(LGErrors.noEndingInMultiline, undefined, context)); + result.push(this.buildLGDiagnostic(TemplateErrors.noEndingInMultiline, undefined, context)); } return result; } @@ -295,14 +295,14 @@ export class StaticChecker extends AbstractParseTreeVisitor implem private checkExpression(exp: string, context: ParserRuleContext, prefix: string = ''): Diagnostic[] { const result: Diagnostic[] = []; if(!exp.endsWith('}')) { - result.push(this.buildLGDiagnostic(LGErrors.noCloseBracket, undefined, context)); + result.push(this.buildLGDiagnostic(TemplateErrors.noCloseBracket, undefined, context)); } else { - exp = LGExtensions.trimExpression(exp); + exp = TemplateExtensions.trimExpression(exp); try { this.expressionParser.parse(exp); } catch (e) { - const errorMsg = prefix + LGErrors.expressionParseError(exp) + e.message; + const errorMsg = prefix + TemplateErrors.expressionParseError(exp) + e.message; result.push(this.buildLGDiagnostic(errorMsg, undefined, context)); return result; @@ -319,6 +319,6 @@ export class StaticChecker extends AbstractParseTreeVisitor implem const range = new Range(startPosition, stopPosition); message = (this.visitedTemplateNames.length > 0 && includeTemplateNameInfo)? `[${ this.visitedTemplateNames[this.visitedTemplateNames.length - 1] }]`+ message : message; - return new Diagnostic(range, message, severity, this.lgFile.id); + return new Diagnostic(range, message, severity, this.templates.id); } } diff --git a/libraries/botbuilder-lg/src/lgTemplate.ts b/libraries/botbuilder-lg/src/template.ts similarity index 99% rename from libraries/botbuilder-lg/src/lgTemplate.ts rename to libraries/botbuilder-lg/src/template.ts index 72f78304e1..40485ea02f 100644 --- a/libraries/botbuilder-lg/src/lgTemplate.ts +++ b/libraries/botbuilder-lg/src/template.ts @@ -12,7 +12,7 @@ import { ParametersContext, TemplateDefinitionContext, TemplateNameContext, File * Here is a data model that can easily understanded and used as the context or all kinds of visitors * wether it's evalator, static checker, anayler.. etc */ -export class LGTemplate { +export class Template { /** * Name of the template, what's followed by '#' in a LG file */ diff --git a/libraries/botbuilder-lg/src/lgErrors.ts b/libraries/botbuilder-lg/src/templateErrors.ts similarity index 99% rename from libraries/botbuilder-lg/src/lgErrors.ts rename to libraries/botbuilder-lg/src/templateErrors.ts index 249dabb1f0..e43f2f587e 100644 --- a/libraries/botbuilder-lg/src/lgErrors.ts +++ b/libraries/botbuilder-lg/src/templateErrors.ts @@ -9,7 +9,7 @@ /** * Centralized LG errors. */ -export class LGErrors { +export class TemplateErrors { public static readonly noTemplate: string = `LG file must have at least one template definition. `; public static readonly invalidTemplateName: string = `Invalid template name. Template name should start with letter/number/_ and can only contains letter/number/_/./-. `; diff --git a/libraries/botbuilder-lg/src/lgException.ts b/libraries/botbuilder-lg/src/templateException.ts similarity index 81% rename from libraries/botbuilder-lg/src/lgException.ts rename to libraries/botbuilder-lg/src/templateException.ts index e4841a67c8..b2debd6e2e 100644 --- a/libraries/botbuilder-lg/src/lgException.ts +++ b/libraries/botbuilder-lg/src/templateException.ts @@ -10,13 +10,13 @@ import { Diagnostic } from './diagnostic'; /** * LG Exception that contains diagnostics. */ -export class LGException extends Error { +export class TemplateException extends Error { private diagnostics: Diagnostic[]; public constructor(m: string, diagnostics: Diagnostic[]) { super(m); this.diagnostics = diagnostics; - Object.setPrototypeOf(this, LGException .prototype); + Object.setPrototypeOf(this, TemplateException .prototype); } /** diff --git a/libraries/botbuilder-lg/src/lgExtensions.ts b/libraries/botbuilder-lg/src/templateExtensions.ts similarity index 96% rename from libraries/botbuilder-lg/src/lgExtensions.ts rename to libraries/botbuilder-lg/src/templateExtensions.ts index b3b4672aca..3aef88d552 100644 --- a/libraries/botbuilder-lg/src/lgExtensions.ts +++ b/libraries/botbuilder-lg/src/templateExtensions.ts @@ -12,7 +12,7 @@ import { TerminalNode } from 'antlr4ts/tree'; /** * Extension methods for LG. */ -export class LGExtensions { +export class TemplateExtensions { /** * trim expression. ${abc} => abc, ${a == {}} => a == {}. diff --git a/libraries/botbuilder-lg/src/lgImport.ts b/libraries/botbuilder-lg/src/templateImport.ts similarity index 97% rename from libraries/botbuilder-lg/src/lgImport.ts rename to libraries/botbuilder-lg/src/templateImport.ts index 60b1e332cb..fb7073db02 100644 --- a/libraries/botbuilder-lg/src/lgImport.ts +++ b/libraries/botbuilder-lg/src/templateImport.ts @@ -11,7 +11,7 @@ import { ImportDefinitionContext } from './generated/LGFileParser'; /** * Here is a data model that can help users understand and use the LG import definition in LG files easily. */ -export class LGImport { +export class TemplateImport { /** * Description of the import, what's included by '[]' in a lg file. diff --git a/libraries/botbuilder-lg/src/lgParser.ts b/libraries/botbuilder-lg/src/templateParser.ts similarity index 67% rename from libraries/botbuilder-lg/src/lgParser.ts rename to libraries/botbuilder-lg/src/templateParser.ts index 44887ffb73..b05f2f9926 100644 --- a/libraries/botbuilder-lg/src/lgParser.ts +++ b/libraries/botbuilder-lg/src/templateParser.ts @@ -10,12 +10,12 @@ import { CommonTokenStream } from 'antlr4ts/CommonTokenStream'; import { ErrorListener } from './errorListener'; import { LGFileLexer } from './generated/LGFileLexer'; import { FileContext, ImportDefinitionContext, LGFileParser, ParagraphContext, TemplateDefinitionContext, OptionsDefinitionContext } from './generated/LGFileParser'; -import { LGImport } from './lgImport'; -import { LGTemplate } from './lgTemplate'; -import { LGFile } from './lgFile'; +import { TemplateImport } from './templateImport'; +import { Template } from './template'; +import { Templates } from './templates'; import { StaticChecker } from './staticChecker'; -import { LGExtensions } from './lgExtensions'; -import { LGException } from './lgException'; +import { TemplateExtensions } from './templateExtensions'; +import { TemplateException } from './templateException'; import * as path from 'path'; import * as fs from 'fs'; import { Diagnostic, DiagnosticSeverity } from './diagnostic'; @@ -29,7 +29,7 @@ export declare type ImportResolverDelegate = (source: string, resourceId: string /** * LG Parser */ -export class LGParser { +export class TemplateParser { /// /// option regex. @@ -43,70 +43,69 @@ export class LGParser { * @param expressionParser Expression parser for evaluating expressions. * @returns new lg file. */ - public static parseFile(filePath: string, importResolver?: ImportResolverDelegate, expressionParser?: ExpressionParser): LGFile { - const fullPath = LGExtensions.normalizePath(filePath); + public static parseFile(filePath: string, importResolver?: ImportResolverDelegate, expressionParser?: ExpressionParser): Templates { + const fullPath = TemplateExtensions.normalizePath(filePath); const content = fs.readFileSync(fullPath, 'utf-8'); - return LGParser.parseText(content, fullPath, importResolver, expressionParser); + return TemplateParser.parseText(content, fullPath, importResolver, expressionParser); } /** - * Parser to turn lg content into a LGFile. + * Parser to turn lg content into a Templates. * @param content text content contains lg templates. * @param id id is the identifier of content. If importResolver is undefined, id must be a full path string. * @param importResolver resolver to resolve LG import id to template text. * @param expressionParser Expression parser for evaluating expressions. * @returns entity. */ - public static parseText(content: string, id: string = '', importResolver?: ImportResolverDelegate, expressionParser?: ExpressionParser): LGFile { - importResolver = importResolver || LGParser.defaultFileResolver; - let lgFile = new LGFile(); - lgFile.content = content; - lgFile.id = id; - lgFile.importResolver = importResolver; + public static parseText(content: string, id: string = '', importResolver?: ImportResolverDelegate, expressionParser?: ExpressionParser): Templates { + importResolver = importResolver || TemplateParser.defaultFileResolver; + let templates = new Templates(); + templates.content = content; + templates.id = id; + templates.importResolver = importResolver; if (expressionParser) { - lgFile.expressionParser = expressionParser; + templates.expressionParser = expressionParser; } let diagnostics: Diagnostic[] = []; try { - const parsedResult = LGParser.antlrParse(content, id); - lgFile.templates = parsedResult.templates; - lgFile.imports = parsedResult.imports; - lgFile.options = parsedResult.options; + const parsedResult = TemplateParser.antlrParse(content, id); + parsedResult.templates.forEach(t => templates.push(t)); + templates.imports = parsedResult.imports; + templates.options = parsedResult.options; diagnostics = diagnostics.concat(parsedResult.invalidTemplateErrors); - lgFile.references = this.getReferences(lgFile, importResolver); - const semanticErrors = new StaticChecker(lgFile, lgFile.expressionParser).check(); + templates.references = this.getReferences(templates, importResolver); + const semanticErrors = new StaticChecker(templates, templates.expressionParser).check(); diagnostics = diagnostics.concat(semanticErrors); } catch (err) { - if (err instanceof LGException) { + if (err instanceof TemplateException) { diagnostics = diagnostics.concat(err.getDiagnostic()); } else { diagnostics.push(this.buildDiagnostic(err.Message, undefined, id)); } } - lgFile.diagnostics = diagnostics; + templates.diagnostics = diagnostics; - return lgFile; + return templates; } - /// - /// Parser to turn lg content into a LGFile based on the original LGFile. - /// - /// Text content contains lg templates. - /// original LGFile. - /// new LGFile entity. - public static parseTextWithRef(content: string, lgFile: LGFile): LGFile { - if (!lgFile) { - throw Error(`LGFile`); + /** + * Parser to turn lg content into a Templates based on the original Templates. + * @param content Text content contains lg templates. + * @param originalTemplates original templates + */ + public static parseTextWithRef(content: string, originalTemplates: Templates): Templates { + if (!originalTemplates) { + throw Error(`templates is empty`); } const id = 'inline content'; - let newLgFile = new LGFile(); - newLgFile.content = content; - newLgFile.id = id; - newLgFile.importResolver = lgFile.importResolver; + let newTemplates = new Templates(); + newTemplates.content = content; + newTemplates.id = id; + newTemplates.importResolver = originalTemplates.importResolver; let diagnostics: Diagnostic[] = []; try { const antlrResult = this.antlrParse(content, id); @@ -114,36 +113,36 @@ export class LGParser { const imports = antlrResult.imports; const invalidTemplateErrors = antlrResult.invalidTemplateErrors; const options = antlrResult.options; - newLgFile.templates = templates; - newLgFile.imports = imports; - newLgFile.options = options; + templates.forEach(t => newTemplates.push(t)); + newTemplates.imports = imports; + newTemplates.options = options; diagnostics = diagnostics.concat(invalidTemplateErrors); - newLgFile.references = this.getReferences(newLgFile, newLgFile.importResolver) - .concat(lgFile.references) - .concat([lgFile]); + newTemplates.references = this.getReferences(newTemplates, newTemplates.importResolver) + .concat(originalTemplates.references) + .concat([originalTemplates]); - var semanticErrors = new StaticChecker(newLgFile).check(); + var semanticErrors = new StaticChecker(newTemplates).check(); diagnostics = diagnostics.concat(semanticErrors); } catch (err) { - if (err instanceof LGException) { + if (err instanceof TemplateException) { diagnostics = diagnostics.concat(err.getDiagnostic()); } else { diagnostics.push(this.buildDiagnostic(err.Message, undefined, id)); } } - newLgFile.diagnostics = diagnostics; + newTemplates.diagnostics = diagnostics; - return newLgFile; + return newTemplates; } public static defaultFileResolver(sourceId: string, resourceId: string): { content: string; id: string } { - let importPath = LGExtensions.normalizePath(resourceId); + let importPath = TemplateExtensions.normalizePath(resourceId); if (!path.isAbsolute(importPath)) { // get full path for importPath relative to path which is doing the import. - importPath = LGExtensions.normalizePath(path.join(path.dirname(sourceId), importPath)); + importPath = TemplateExtensions.normalizePath(path.join(path.dirname(sourceId), importPath)); } if (!fs.existsSync(importPath) || !fs.statSync(importPath).isFile()) { throw Error(`Could not find file: ${ importPath }`); @@ -153,25 +152,25 @@ export class LGParser { return { content, id: importPath }; } - private static antlrParse(text: string, id: string = ''): { templates: LGTemplate[]; imports: LGImport[]; invalidTemplateErrors: Diagnostic[]; options: string[]} { + private static antlrParse(text: string, id: string = ''): { templates: Template[]; imports: TemplateImport[]; invalidTemplateErrors: Diagnostic[]; options: string[]} { const fileContext: FileContext = this.getFileContentContext(text, id); - const templates: LGTemplate[] = this.extractLGTemplates(fileContext, text, id); - const imports: LGImport[] = this.extractLGImports(fileContext, id); + const templates: Template[] = this.extractLGTemplates(fileContext, text, id); + const imports: TemplateImport[] = this.extractLGImports(fileContext, id); const invalidTemplateErrors: Diagnostic[] = this.getInvalidTemplateErrors(fileContext, id); const options: string[] = this.extractLGOptions(fileContext); return { templates, imports, invalidTemplateErrors, options}; } - private static getReferences(file: LGFile, importResolver: ImportResolverDelegate): LGFile[] { - var resourcesFound = new Set(); + private static getReferences(file: Templates, importResolver: ImportResolverDelegate): Templates[] { + var resourcesFound = new Set(); this.resolveImportResources(file, resourcesFound, importResolver); resourcesFound.delete(file); return Array.from(resourcesFound); } - private static resolveImportResources(start: LGFile, resourcesFound: Set, importResolver: ImportResolverDelegate): void { - var resourceIds = start.imports.map((lg: LGImport): string => lg.id); + private static resolveImportResources(start: Templates, resourcesFound: Set, importResolver: ImportResolverDelegate): void { + var resourceIds = start.imports.map((lg: TemplateImport): string => lg.id); resourcesFound.add(start); for (const id of resourceIds) { @@ -181,15 +180,15 @@ export class LGParser { const path = result.id; const notExist = Array.from(resourcesFound).filter((u): boolean => u.id === path).length === 0; if (notExist) { - var childResource = LGParser.parseText(content, path, importResolver, start.expressionParser); + var childResource = TemplateParser.parseText(content, path, importResolver, start.expressionParser); this.resolveImportResources(childResource, resourcesFound, importResolver); } } catch (err) { - if (err instanceof LGException) { + if (err instanceof TemplateException) { throw err; } else { - throw new LGException(err.message, [this.buildDiagnostic(err.message, undefined, start.id)]); + throw new TemplateException(err.message, [this.buildDiagnostic(err.message, undefined, start.id)]); } } } @@ -263,7 +262,7 @@ export class LGParser { return parser.file(); } - private static extractLGTemplates(file: FileContext, lgfileContent: string, source: string = ''): LGTemplate[] { + private static extractLGTemplates(file: FileContext, lgfileContent: string, source: string = ''): Template[] { if (!file) { return []; } @@ -272,10 +271,10 @@ export class LGParser { .map((x: ParagraphContext): TemplateDefinitionContext => x.templateDefinition()) .filter((x: TemplateDefinitionContext): boolean => x !== undefined); - return templates.map((x: TemplateDefinitionContext): LGTemplate => new LGTemplate(x, lgfileContent, source)); + return templates.map((x: TemplateDefinitionContext): Template => new Template(x, lgfileContent, source)); } - private static extractLGImports(file: FileContext, source: string = ''): LGImport[] { + private static extractLGImports(file: FileContext, source: string = ''): TemplateImport[] { if (!file) { return []; } @@ -284,6 +283,6 @@ export class LGParser { .map((x: ParagraphContext): ImportDefinitionContext => x.importDefinition()) .filter((x: ImportDefinitionContext): boolean => x !== undefined); - return imports.map((x: ImportDefinitionContext): LGImport => new LGImport(x, source)); + return imports.map((x: ImportDefinitionContext): TemplateImport => new TemplateImport(x, source)); } } diff --git a/libraries/botbuilder-lg/src/lgFile.ts b/libraries/botbuilder-lg/src/templates.ts similarity index 74% rename from libraries/botbuilder-lg/src/lgFile.ts rename to libraries/botbuilder-lg/src/templates.ts index cc1769a088..66b0d876a4 100644 --- a/libraries/botbuilder-lg/src/lgFile.ts +++ b/libraries/botbuilder-lg/src/templates.ts @@ -6,31 +6,24 @@ * Licensed under the MIT License. */ -import { LGTemplate } from './lgTemplate'; -import { LGImport } from './lgImport'; +import { Template } from './template'; +import { TemplateImport } from './templateImport'; import { Diagnostic, DiagnosticSeverity } from './diagnostic'; import { ExpressionParser } from 'adaptive-expressions'; -import { ImportResolverDelegate } from './lgParser'; +import { ImportResolverDelegate } from './templateParser'; import { Evaluator } from './evaluator'; import { Expander } from './expander'; import { Analyzer } from './analyzer'; -import { LGParser } from './lgParser'; +import { TemplateParser } from './templateParser'; import { AnalyzerResult } from './analyzerResult'; -import { LGErrors } from './lgErrors'; -import { LGExtensions } from './lgExtensions'; +import { TemplateErrors } from './templateErrors'; +import { TemplateExtensions } from './templateExtensions'; /// /// LG entrance, including properties that LG file has, and evaluate functions. /// -export class LGFile { - - /// - /// Gets or sets templates that this LG file contains directly. - /// - /// - /// templates that this LG file contains directly. - /// - public templates: LGTemplate[]; +export class Templates implements Iterable