UNPKG

13.8 kBJavaScriptView Raw
1/*
2 * Licensed under the Apache License, Version 2.0 (the "License");
3 * you may not use this file except in compliance with the License.
4 * You may obtain a copy of the License at
5 *
6 * http://www.apache.org/licenses/LICENSE-2.0
7 *
8 * Unless required by applicable law or agreed to in writing, software
9 * distributed under the License is distributed on an "AS IS" BASIS,
10 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 * See the License for the specific language governing permissions and
12 * limitations under the License.
13 */
14'use strict';
15
16const fsPath = require('path');
17
18const RelationshipDeclaration = require('composer-concerto').RelationshipDeclaration;
19
20const Writer = require('composer-concerto').Writer;
21
22const Logger = require('@accordproject/ergo-compiler').Logger;
23
24const nearley = require('nearley');
25
26const compile = require('nearley/lib/compile');
27
28const generate = require('nearley/lib/generate');
29
30const nearleyGrammar = require('nearley/lib/nearley-language-bootstrapped');
31
32const templateGrammar = require('./tdl.js');
33
34const GrammarVisitor = require('./grammarvisitor');
35
36const uuid = require('uuid');
37
38const nunjucks = require('nunjucks');
39
40const DateTimeFormatParser = require('./datetimeformatparser'); // This required because only compiled nunjucks templates are supported browser-side
41// https://mozilla.github.io/nunjucks/api.html#browser-usage
42// We can't always import it in Cicero because precompiling is not supported server-side!
43// https://github.com/mozilla/nunjucks/issues/1065
44
45
46if (process.browser) {
47 require('./compiled_template');
48}
49/**
50 * Generates and manages a Nearley parser for a template.
51 * @class
52 * @public
53 * @memberof module:cicero-core
54 */
55
56
57class ParserManager {
58 /**
59 * Create the ParserManager.
60 * @param {object} template - the template instance
61 */
62 constructor(template) {
63 this.template = template;
64 this.grammar = null;
65 this.grammarAst = null;
66 this.templatizedGrammar = null;
67 this.templateAst = null;
68 }
69 /**
70 * Gets a parser object for this template
71 * @return {object} the parser for this template
72 */
73
74
75 getParser() {
76 if (!this.grammarAst) {
77 throw new Error('Must call setGrammar or buildGrammar before calling getParser');
78 }
79
80 return new nearley.Parser(nearley.Grammar.fromCompiled(this.grammarAst));
81 }
82 /**
83 * Gets the AST for the template
84 * @return {object} the AST for the template
85 */
86
87
88 getTemplateAst() {
89 if (!this.grammarAst) {
90 throw new Error('Must call setGrammar or buildGrammar before calling getTemplateAst');
91 }
92
93 return this.templateAst;
94 }
95 /**
96 * Set the grammar for the template
97 * @param {String} grammar - the grammar for the template
98 */
99
100
101 setGrammar(grammar) {
102 this.grammarAst = ParserManager.compileGrammar(grammar);
103 this.grammar = grammar;
104 }
105 /**
106 * Build a grammar from a template
107 * @param {String} templatizedGrammar - the annotated template
108 */
109
110
111 buildGrammar(templatizedGrammar) {
112 Logger.debug('buildGrammar', templatizedGrammar);
113 const parser = new nearley.Parser(nearley.Grammar.fromCompiled(templateGrammar));
114 parser.feed(templatizedGrammar);
115
116 if (parser.results.length !== 1) {
117 throw new Error('Ambiguous parse!');
118 } // parse the template grammar to generate a dynamic grammar
119
120
121 const ast = parser.results[0];
122 this.templateAst = ast;
123 Logger.debug('Template AST', ast);
124 const parts = {
125 textRules: [],
126 modelRules: [],
127 grammars: {}
128 };
129 parts.grammars.base = require('./grammars/base');
130 this.buildGrammarRules(ast, this.template.getTemplateModel(), 'rule', parts); // generate the grammar for the model
131
132 const parameters = {
133 writer: new Writer(),
134 rules: []
135 };
136 const gv = new GrammarVisitor();
137 this.template.getModelManager().accept(gv, parameters);
138 parts.modelRules.push(...parameters.rules); // combine the results
139
140 nunjucks.configure(fsPath.resolve(__dirname), {
141 tags: {
142 blockStart: '<%',
143 blockEnd: '%>'
144 },
145 autoescape: false // Required to allow nearley syntax strings
146
147 });
148 const combined = nunjucks.render('template.ne', parts);
149 Logger.debug('Generated template grammar' + combined); // console.log(combined);
150
151 this.setGrammar(combined);
152 this.templatizedGrammar = templatizedGrammar;
153 }
154 /**
155 * Build grammar rules from a template
156 * @param {object} ast - the AST from which to build the grammar
157 * @param {ClassDeclaration} templateModel - the type of the parent class for this AST
158 * @param {String} prefix - A unique prefix for the grammar rules
159 * @param {Object} parts - Result object to acculumate rules and required sub-grammars
160 */
161
162
163 buildGrammarRules(ast, templateModel, prefix, parts) {
164 // these are the rules for variables
165 const rules = {}; // these are the rules for static text
166
167 let textRules = {}; // generate all the rules for the static text
168
169 textRules.prefix = prefix;
170 textRules.symbols = [];
171 ast.data.forEach((element, index) => {
172 // ignore empty chunks (issue #1) and missing optional last chunks
173 if (element && (element.type !== 'Chunk' || element.value.length > 0)) {
174 Logger.debug("element ".concat(prefix).concat(index, " ").concat(JSON.stringify(element)));
175 rules[prefix + index] = element;
176 textRules.symbols.push(prefix + index);
177 }
178 }, this); // the result of parsing is an instance of the template model
179
180 textRules.class = templateModel.getFullyQualifiedName();
181 const identifier = templateModel.getIdentifierFieldName();
182
183 if (identifier !== null) {
184 textRules.identifier = "".concat(identifier, " : \"").concat(uuid.v4(), "\"");
185 } // we then bind each variable in the template model
186 // to the first occurence of the variable in the template grammar
187
188
189 textRules.properties = [];
190 templateModel.getProperties().forEach((property, index) => {
191 const sep = index < templateModel.getProperties().length - 1 ? ',' : '';
192 const bindingIndex = this.findFirstBinding(property.getName(), ast.data);
193
194 if (bindingIndex !== -1) {
195 // ignore things like transactionId
196 // TODO (DCS) add !==null check for BooleanBinding
197 textRules.properties.push("".concat(property.getName(), " : ").concat(prefix).concat(bindingIndex).concat(sep));
198 }
199 });
200 parts.textRules.push(textRules); // Now create the child rules for each symbol in the root rule
201
202 for (let rule in rules) {
203 const element = rules[rule];
204
205 switch (element.type) {
206 case 'Chunk':
207 case 'LastChunk':
208 parts.modelRules.push({
209 prefix: rule,
210 symbols: [this.cleanChunk(element.value)]
211 });
212 break;
213
214 case 'BooleanBinding':
215 {
216 const property = ParserManager.getProperty(templateModel, element.fieldName.value);
217
218 if (property.getType() !== 'Boolean') {
219 throw new Error("A boolean binding can only be used with a boolean property. Property ".concat(element.fieldName.value, " has type ").concat(property.getType()));
220 }
221
222 parts.modelRules.push({
223 prefix: rule,
224 symbols: ["".concat(element.string.value, ":? {% (d) => {return d[0] !== null;}%} # ").concat(element.fieldName.value)]
225 });
226 }
227 break;
228
229 case 'FormattedBinding':
230 case 'Binding':
231 case 'ClauseBinding':
232 this.handleBinding(templateModel, parts, rule, element);
233 break;
234
235 default:
236 throw new Error("Unrecognized type ".concat(element.type));
237 }
238 }
239 }
240 /**
241 * Throws an error if a template variable doesn't exist on the model.
242 * @param {*} templateModel - the model for the template
243 * @param {String} propertyName - the name of the property
244 * @returns {*} the property
245 */
246
247
248 static getProperty(templateModel, propertyName) {
249 const property = templateModel.getProperty(propertyName);
250
251 if (!property) {
252 throw new Error("Template references a property '".concat(propertyName, "' that is not declared in the template model '").concat(templateModel.getFullyQualifiedName(), "'"));
253 }
254
255 return property;
256 }
257 /**
258 * Utility method to generate a grammar rule for a variable binding
259 * @param {ClassDeclaration} templateModel - the current template model
260 * @param {*} parts - the parts, where the rule will be added
261 * @param {*} inputRule - the rule we are processing in the AST
262 * @param {*} element - the current element in the AST
263 */
264
265
266 handleBinding(templateModel, parts, inputRule, element) {
267 const propertyName = element.fieldName.value;
268 const property = ParserManager.getProperty(templateModel, propertyName);
269 let action = null;
270 let suffix = ':';
271 let type = property.getType(); // allow the type and action to be defined using a decorator
272
273 const decorator = property.getDecorator('AccordType');
274
275 if (decorator) {
276 if (decorator.getArguments().length > 0) {
277 type = decorator.getArguments()[0];
278 }
279
280 if (decorator.getArguments().length > 1) {
281 action = decorator.getArguments()[1];
282 }
283 } // if the type/action have not been set explicity, then we infer them
284
285
286 if (!action) {
287 action = '{% id %}';
288
289 if (element.type === 'FormattedBinding') {
290 if (property.getType() !== 'DateTime') {
291 throw new Error('Formatted types are currently only supported for DateTime properties.');
292 } // we only include the datetime grammar if custom formats are used
293
294
295 if (!parts.grammars.dateTime) {
296 parts.grammars.dateTime = require('./grammars/datetime');
297 parts.grammars.dateTimeEn = require('./grammars/datetime-en');
298 } // push the formatting rule, iff it has not been already declared
299
300
301 const formatRule = DateTimeFormatParser.buildDateTimeFormatRule(element.format.value);
302 type = formatRule.name;
303 const ruleExists = parts.modelRules.some(rule => rule.prefix === formatRule.name);
304
305 if (!ruleExists) {
306 parts.modelRules.push({
307 prefix: formatRule.name,
308 symbols: ["".concat(formatRule.tokens, " ").concat(formatRule.action, " # ").concat(propertyName, " as ").concat(element.format.value)]
309 });
310 }
311 } else if (element.type === 'ClauseBinding') {
312 const clauseTemplate = element.template;
313 const clauseTemplateModel = this.template.getIntrospector().getClassDeclaration(property.getFullyQualifiedTypeName());
314 this.buildGrammarRules(clauseTemplate, clauseTemplateModel, propertyName, parts);
315 type = element.fieldName.value;
316 } else {
317 // relationships need to be transformed into strings
318 if (property instanceof RelationshipDeclaration) {
319 type = 'String';
320 }
321 }
322 }
323
324 if (property.isArray()) {
325 suffix += '+';
326 }
327
328 if (property.isOptional()) {
329 suffix += '?';
330 }
331
332 if (suffix === ':') {
333 suffix = '';
334 } // console.log(`${inputRule} => ${type}${suffix} ${action} # ${propertyName}`);
335
336
337 parts.modelRules.push({
338 prefix: inputRule,
339 symbols: ["".concat(type).concat(suffix, " ").concat(action, " # ").concat(propertyName)]
340 });
341 }
342 /**
343 * Cleans a chunk of text to make it safe to include
344 * as a grammar rule. We need to remove linefeeds and
345 * escape any '"' characters.
346 *
347 * @param {string} input - the input text from the template
348 * @return {string} cleaned text
349 */
350
351
352 cleanChunk(input) {
353 // we replace all \r and \n with \n
354 let text = input.replace(/\r?\n|\r/gm, '\\n'); // replace all " with \", even across newlines
355
356 text = text.replace(/"/gm, '\\"');
357 return "\"".concat(text, "\"");
358 }
359 /**
360 * Finds the first binding for the given property
361 *
362 * @param {string} propertyName the name of the property
363 * @param {object[]} elements the result of parsing the template_txt.
364 * @return {int} the index of the element or -1
365 */
366
367
368 findFirstBinding(propertyName, elements) {
369 for (let n = 0; n < elements.length; n++) {
370 const element = elements[n];
371
372 if (element !== null && ['Binding', 'FormattedBinding', 'BooleanBinding', 'ClauseBinding'].includes(element.type)) {
373 if (element.fieldName.value === propertyName) {
374 return n;
375 }
376 }
377 }
378
379 return -1;
380 }
381 /**
382 * Get the (compiled) grammar for the template
383 * @return {String} - the grammar for the template
384 */
385
386
387 getGrammar() {
388 return this.grammar;
389 }
390 /**
391 * Returns the templatized grammar
392 * @return {String} the contents of the templatized grammar
393 */
394
395
396 getTemplatizedGrammar() {
397 return this.templatizedGrammar;
398 }
399 /**
400 * Compiles a Nearley grammar to its AST
401 * @param {string} sourceCode - the source text for the grammar
402 * @return {object} the AST for the grammar
403 */
404
405
406 static compileGrammar(sourceCode) {
407 try {
408 // Parse the grammar source into an AST
409 const grammarParser = new nearley.Parser(nearleyGrammar);
410 grammarParser.feed(sourceCode);
411 const grammarAst = grammarParser.results[0]; // TODO check for errors
412 // Compile the AST into a set of rules
413
414 const grammarInfoObject = compile(grammarAst, {}); // Generate JavaScript code from the rules
415
416 const grammarJs = generate(grammarInfoObject, 'grammar'); // Pretend this is a CommonJS environment to catch exports from the grammar.
417
418 const module = {
419 exports: {}
420 };
421 eval(grammarJs);
422 return module.exports;
423 } catch (err) {
424 Logger.error(err);
425 throw err;
426 }
427 }
428
429}
430
431module.exports = ParserManager;
\No newline at end of file