UNPKG

6.91 kBPlain TextView Raw
1/**
2 * Copyright (c) Microsoft Corporation. All rights reserved.
3 * Licensed under the MIT License.
4 */
5import * as fs from 'fs'
6import * as path from 'path'
7import { Template, TemplateVariable, RenderedActionArgument } from '@conversationlearner/models'
8import { CLDebug } from './CLDebug'
9
10export class TemplateProvider {
11 private static hasSubmitError = false
12
13 // TODO: Decouple template renderer from types from Action classes
14 // E.g. use generic key,value object instead of RenderedActionArgument
15 public static async RenderTemplate(templateName: string, templateArguments: RenderedActionArgument[]): Promise<any | null> {
16 let template = this.GetTemplate(templateName)
17 if (template === null) {
18 return null
19 }
20
21 let templateString = JSON.stringify(template)
22 let argumentNames = this.GetArgumentNames(templateString)
23
24 // Substitute argument values
25 for (let argumentName of argumentNames) {
26 let renderedActionArgument = templateArguments.find(a => a.parameter == argumentName)
27 if (renderedActionArgument) {
28 templateString = templateString.replace(new RegExp(`{{${argumentName}}}`, 'g'), renderedActionArgument.value || '')
29 }
30 }
31 templateString = this.RemoveEmptyArguments(templateString)
32 return JSON.parse(templateString)
33 }
34
35 public static GetTemplates(): Template[] {
36 let templates: Template[] = []
37 let files = this.GetTemplatesNames()
38 for (let file of files) {
39 let fileContent = this.GetTemplate(file)
40
41 // Clear submit check (will the set by extracting template variables)
42 this.hasSubmitError = false
43 let tvs = this.UniqueTemplateVariables(fileContent)
44
45 // Make sure template has submit item
46 let validationError = this.hasSubmitError
47 ? `Template "${file}" has an "Action.Submit" item but no data. Submit item must be of the form: "type": "Action.Submit", "data": string` : null
48
49 let templateBody = JSON.stringify(fileContent)
50 let template: Template = {
51 name: file,
52 variables: tvs,
53 body: templateBody,
54 validationError: validationError
55 }
56 templates.push(template)
57 }
58
59 return templates
60 }
61
62 private static UniqueTemplateVariables(template: any): TemplateVariable[] {
63 // Get all template variables
64 let templateVariables = this.GetTemplateVariables(template)
65
66 // Make entries unique, and use verion with existing type
67 let unique = []
68 for (let tv of templateVariables) {
69 let existing = unique.find(i => i.key == tv.key)
70 if (existing) {
71 if (existing.type != null && tv.type != null && existing.type != tv.type) {
72 CLDebug.Error(
73 `Template variable "${tv.key}" used for two diffent types - "${tv.type}" and "${existing.type}". Ignoring.`
74 )
75 } else {
76 existing.type = existing.type || tv.type
77 }
78 } else {
79 unique.push(tv)
80 }
81 }
82 return unique
83 }
84
85 public static RemoveEmptyArguments(formString: string) {
86 return formString.replace(/{{\s*[\w\.]+\s*}}/g, '')
87 }
88
89 public static GetArgumentNames(formString: string) {
90 // Get set of unique entities
91 let mustaches = formString.match(/{{\s*[\w\.]+\s*}}/g)
92 if (mustaches) {
93 let entities = [...new Set(mustaches.map(x => x.match(/[\w\.]+/)![0]))]
94 return entities
95 }
96 return []
97 }
98
99 public static TemplateDirectory(): string {
100 //TODO - make this configurable
101 let templateDirectory = path.join(process.cwd(), './cards')
102
103 // Try up a directory or two as could be in /lib or /dist folder depending on deployment
104 if (!fs.existsSync(templateDirectory)) {
105 templateDirectory = path.join(process.cwd(), '../cards')
106 }
107 if (!fs.existsSync(templateDirectory)) {
108 templateDirectory = path.join(process.cwd(), '../../cards')
109 }
110 return templateDirectory;
111 }
112
113 public static GetTemplate(templateName: string): any {
114
115 const filename = path.join(this.TemplateDirectory(), `${templateName}.json`);
116
117 try {
118 const templateString = fs.readFileSync(filename, 'utf-8')
119
120 try {
121 const template = JSON.parse(templateString)
122 return template
123 } catch {
124 CLDebug.Error(`Invalid JSON: Failed to Parse template named "${templateName}"`)
125 return null
126 }
127
128 } catch {
129 CLDebug.Error(`Can't find template named: "${filename}"`)
130 }
131 }
132
133 public static GetTemplatesNames(): string[] {
134 try {
135 let fileNames: string[] = fs.readdirSync(this.TemplateDirectory())
136 fileNames = fileNames.filter(fn => fn.endsWith('.json'))
137 let templates = fileNames.map(f => f.slice(0, f.lastIndexOf('.')))
138 return templates
139 } catch {
140 CLDebug.Log("No Card directory found")
141 return [];
142 }
143 }
144
145 private static GetVarNames(template: any): string[] {
146 let mustaches: string[] = []
147 for (let i in template) {
148 if (typeof template[i] != 'object') {
149 let searchStr = JSON.stringify(template[i])
150 let results = searchStr.match(/{{\s*[\w\.]+\s*}}/g)
151 if (results) {
152 mustaches = mustaches.concat(results.map(x => x.match(/[\w\.]+/)![0]))
153 }
154 }
155 }
156 return mustaches
157 }
158
159 private static GetTemplateVariables(template: any): TemplateVariable[] {
160 let tvs: TemplateVariable[] = []
161 if (template && template.type === "Action.Submit" && (typeof template.data !== 'string')) {
162 this.hasSubmitError = true
163 }
164
165 // Get variable names
166 let vars = this.GetVarNames(template)
167 if (vars.length > 0) {
168 for (let key of vars) {
169 let tv: TemplateVariable = { key: key, type: template['type'] }
170 tvs.push(tv)
171 }
172 }
173
174 // Iterate through keys
175 for (let i in template) {
176 if (!template.hasOwnProperty(i)) {
177 continue
178 }
179 if (template[i] instanceof Array) {
180 for (let item of template[i]) {
181 tvs = tvs.concat(this.GetTemplateVariables(item))
182 }
183 } else if (typeof template[i] == 'object') {
184 tvs = tvs.concat(this.GetTemplateVariables(template[i]))
185 }
186 }
187
188 return tvs
189 }
190}