1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | 'use strict';
|
15 |
|
16 | const fsPath = require('path');
|
17 |
|
18 | const TemplateException = require('./templateexception');
|
19 |
|
20 | const RelationshipDeclaration = require('@accordproject/concerto-core').RelationshipDeclaration;
|
21 |
|
22 | const Writer = require('@accordproject/concerto-core').Writer;
|
23 |
|
24 | const Logger = require('@accordproject/concerto-core').Logger;
|
25 |
|
26 | const nearley = require('nearley');
|
27 |
|
28 | const compile = require('nearley/lib/compile');
|
29 |
|
30 | const generate = require('nearley/lib/generate');
|
31 |
|
32 | const nearleyGrammar = require('nearley/lib/nearley-language-bootstrapped');
|
33 |
|
34 | const templateGrammar = require('./tdl.js');
|
35 |
|
36 | const GrammarVisitor = require('./grammarvisitor');
|
37 |
|
38 | const uuid = require('uuid');
|
39 |
|
40 | const nunjucks = require('nunjucks');
|
41 |
|
42 | const DateTimeFormatParser = require('./datetimeformatparser');
|
43 |
|
44 | const CommonMarkTransformer = require('@accordproject/markdown-common').CommonMarkTransformer;
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | if (process.browser) {
|
51 | require('./compiled_template');
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 | class ParserManager {
|
60 | |
61 |
|
62 |
|
63 |
|
64 | constructor(template) {
|
65 | this.template = template;
|
66 | this.grammar = null;
|
67 | this.grammarAst = null;
|
68 | this.templatizedGrammar = null;
|
69 | this.templateAst = null;
|
70 | }
|
71 | |
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 | getParser() {
|
78 | if (!this.grammarAst) {
|
79 | throw new Error('Must call setGrammar or buildGrammar before calling getParser');
|
80 | }
|
81 |
|
82 | return new nearley.Parser(nearley.Grammar.fromCompiled(this.grammarAst));
|
83 | }
|
84 | |
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 | getTemplateAst() {
|
91 | if (!this.grammarAst) {
|
92 | throw new Error('Must call setGrammar or buildGrammar before calling getTemplateAst');
|
93 | }
|
94 |
|
95 | return this.templateAst;
|
96 | }
|
97 | |
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | setGrammar(grammar) {
|
104 | this.grammarAst = ParserManager.compileGrammar(grammar);
|
105 | this.grammar = grammar;
|
106 | }
|
107 | |
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 | static adjustListBlock(x, separator) {
|
116 | if (x.data[0] && x.data[0].type === 'Chunk') {
|
117 | x.data[0].value = separator + x.data[0].value;
|
118 | return x;
|
119 | } else {
|
120 | throw new Error('List block in template should contain text');
|
121 | }
|
122 | }
|
123 | |
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 | buildGrammar(templatizedGrammar) {
|
131 |
|
132 | templatizedGrammar = this.roundtripMarkdown(templatizedGrammar);
|
133 |
|
134 | Logger.debug('buildGrammar', templatizedGrammar);
|
135 | const parser = new nearley.Parser(nearley.Grammar.fromCompiled(templateGrammar));
|
136 | parser.feed(templatizedGrammar);
|
137 |
|
138 | if (parser.results.length !== 1) {
|
139 | throw new Error('Ambiguous parse!');
|
140 | }
|
141 |
|
142 |
|
143 | const ast = parser.results[0];
|
144 | this.templateAst = ast;
|
145 | const parts = {
|
146 | textRules: [],
|
147 | modelRules: [],
|
148 | grammars: {}
|
149 | };
|
150 | parts.grammars.base = require('./grammars/base');
|
151 | this.buildGrammarRules(ast, this.template.getTemplateModel(), 'rule', parts);
|
152 |
|
153 | const parameters = {
|
154 | writer: new Writer(),
|
155 | rules: []
|
156 | };
|
157 | const gv = new GrammarVisitor();
|
158 | this.template.getModelManager().accept(gv, parameters);
|
159 | parts.modelRules.push(...parameters.rules);
|
160 |
|
161 | nunjucks.configure(fsPath.resolve(__dirname), {
|
162 | tags: {
|
163 | blockStart: '<%',
|
164 | blockEnd: '%>'
|
165 | },
|
166 | autoescape: false
|
167 |
|
168 | });
|
169 | const combined = nunjucks.render('template.ne', parts);
|
170 | Logger.debug('Generated template grammar' + combined);
|
171 |
|
172 | this.setGrammar(combined);
|
173 | this.templatizedGrammar = templatizedGrammar;
|
174 | }
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 | buildGrammarRules(ast, templateModel, prefix, parts) {
|
185 |
|
186 | const rules = {};
|
187 |
|
188 | let textRules = {};
|
189 |
|
190 | textRules.prefix = prefix;
|
191 | textRules.symbols = [];
|
192 | ast.data.forEach((element, index) => {
|
193 |
|
194 | if (element && (element.type !== 'Chunk' || element.value.length > 0)) {
|
195 | Logger.debug("element ".concat(prefix).concat(index, " ").concat(JSON.stringify(element)));
|
196 | rules[prefix + index] = element;
|
197 | textRules.symbols.push(prefix + index);
|
198 | }
|
199 | }, this);
|
200 |
|
201 | textRules.class = templateModel.getFullyQualifiedName();
|
202 | const identifier = templateModel.getIdentifierFieldName();
|
203 |
|
204 | if (identifier !== null) {
|
205 | textRules.identifier = "".concat(identifier, " : \"").concat(uuid.v4(), "\"");
|
206 | }
|
207 |
|
208 |
|
209 |
|
210 | textRules.properties = [];
|
211 | templateModel.getProperties().forEach((property, index) => {
|
212 | const sep = index < templateModel.getProperties().length - 1 ? ',' : '';
|
213 | const bindingIndex = this.findFirstBinding(property.getName(), ast.data);
|
214 |
|
215 | if (bindingIndex !== -1) {
|
216 |
|
217 | textRules.properties.push("".concat(property.getName(), " : ").concat(prefix).concat(bindingIndex).concat(sep));
|
218 | }
|
219 | });
|
220 | parts.textRules.push(textRules);
|
221 |
|
222 | for (let rule in rules) {
|
223 | const element = rules[rule];
|
224 |
|
225 | switch (element.type) {
|
226 | case 'Chunk':
|
227 | case 'ExprChunk':
|
228 | case 'LastChunk':
|
229 | parts.modelRules.push({
|
230 | prefix: rule,
|
231 | symbols: [this.cleanChunk(element.value)]
|
232 | });
|
233 | break;
|
234 |
|
235 | case 'IfBinding':
|
236 | {
|
237 | const property = ParserManager.getProperty(templateModel, element);
|
238 |
|
239 | if (property.getType() !== 'Boolean') {
|
240 | ParserManager._throwTemplateExceptionForElement("An if block can only be used with a boolean property. Property ".concat(element.fieldName.value, " has type ").concat(property.getType()), element);
|
241 | }
|
242 |
|
243 | parts.modelRules.push({
|
244 | prefix: rule,
|
245 | symbols: ["\"".concat(element.stringIf.value, "\":? {% (d) => {return d[0] !== null;}%} # ").concat(element.fieldName.value)]
|
246 | });
|
247 | }
|
248 | break;
|
249 |
|
250 | case 'IfElseBinding':
|
251 | {
|
252 | const property = ParserManager.getProperty(templateModel, element);
|
253 |
|
254 | if (property.getType() !== 'Boolean') {
|
255 | ParserManager._throwTemplateExceptionForElement("An if block can only be used with a boolean property. Property ".concat(element.fieldName.value, " has type ").concat(property.getType()), element);
|
256 | }
|
257 |
|
258 | parts.modelRules.push({
|
259 | prefix: rule,
|
260 | symbols: ["(\"".concat(element.stringIf.value, "\"|\"").concat(element.stringElse.value, "\") {% (d) => {return d[0][0] === \"").concat(element.stringIf.value, "\";}%} # ").concat(element.fieldName.value)]
|
261 | });
|
262 | }
|
263 | break;
|
264 |
|
265 | case 'FormattedBinding':
|
266 | case 'Binding':
|
267 | case 'ClauseBinding':
|
268 | case 'WithBinding':
|
269 | case 'UListBinding':
|
270 | case 'OListBinding':
|
271 | case 'JoinBinding':
|
272 | this.handleBinding(templateModel, parts, rule, element);
|
273 | break;
|
274 |
|
275 | case 'Expr':
|
276 | parts.modelRules.push({
|
277 | prefix: rule,
|
278 | symbols: ['Any']
|
279 | });
|
280 | break;
|
281 |
|
282 | default:
|
283 | ParserManager._throwTemplateExceptionForElement("Unrecognized type ".concat(element.type), element);
|
284 |
|
285 | }
|
286 | }
|
287 | }
|
288 | |
289 |
|
290 |
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 | static getProperty(templateModel, element) {
|
297 | const propertyName = element.fieldName.value;
|
298 | const property = templateModel.getProperty(propertyName);
|
299 |
|
300 | if (!property) {
|
301 | ParserManager._throwTemplateExceptionForElement("Template references a property '".concat(propertyName, "' that is not declared in the template model '").concat(templateModel.getFullyQualifiedName(), "'"), element);
|
302 | }
|
303 |
|
304 | return property;
|
305 | }
|
306 | |
307 |
|
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | static _throwTemplateExceptionForElement(message, element) {
|
315 | const fileName = 'text/grammar.tem.md';
|
316 | let column = element.fieldName.col;
|
317 | let line = element.fieldName.line;
|
318 | let token = element.value ? element.value : ' ';
|
319 | const endColumn = column + token.length;
|
320 | const fileLocation = {
|
321 | start: {
|
322 | line,
|
323 | column
|
324 | },
|
325 | end: {
|
326 | line,
|
327 | endColumn
|
328 |
|
329 | }
|
330 | };
|
331 | throw new TemplateException(message, fileLocation, fileName, null, 'cicero-core');
|
332 | }
|
333 | |
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | handleBinding(templateModel, parts, inputRule, element) {
|
343 | const propertyName = element.fieldName.value;
|
344 | const property = ParserManager.getProperty(templateModel, element);
|
345 | let action = null;
|
346 | let suffix = ':';
|
347 | let type = property.getType();
|
348 | let firstType = null;
|
349 |
|
350 | if (!action) {
|
351 | action = '{% id %}';
|
352 |
|
353 | if (property.getType() === 'DateTime' || element.type === 'FormattedBinding') {
|
354 | if (property.getType() !== 'DateTime') {
|
355 | ParserManager._throwTemplateExceptionForElement('Formatted types are currently only supported for DateTime properties.', element);
|
356 | }
|
357 |
|
358 |
|
359 | if (!parts.grammars.dateTime) {
|
360 | parts.grammars.dateTime = require('./grammars/datetime');
|
361 | parts.grammars.dateTimeEn = require('./grammars/datetime-en');
|
362 | }
|
363 |
|
364 |
|
365 | const format = element.format ? element.format.value : '"MM/DD/YYYY"';
|
366 | const formatRule = DateTimeFormatParser.buildDateTimeFormatRule(format);
|
367 | type = formatRule.name;
|
368 | const ruleExists = parts.modelRules.some(rule => rule.prefix === formatRule.name);
|
369 |
|
370 | if (!ruleExists) {
|
371 | parts.modelRules.push({
|
372 | prefix: formatRule.name,
|
373 | symbols: ["".concat(formatRule.tokens, " ").concat(formatRule.action, " # ").concat(propertyName, " as ").concat(format)]
|
374 | });
|
375 | }
|
376 | } else if (element.type === 'ClauseBinding' || element.type === 'WithBinding') {
|
377 | const nestedTemplate = element.template;
|
378 | const nestedTemplateModel = this.template.getIntrospector().getClassDeclaration(property.getFullyQualifiedTypeName());
|
379 | this.buildGrammarRules(nestedTemplate, nestedTemplateModel, propertyName, parts);
|
380 | type = element.fieldName.value;
|
381 | } else if (element.type === 'UListBinding' || element.type === 'OListBinding' || element.type === 'JoinBinding') {
|
382 | const nestedTemplateModel = this.template.getIntrospector().getClassDeclaration(property.getFullyQualifiedTypeName());
|
383 |
|
384 | let separator;
|
385 |
|
386 | if (element.type === 'JoinBinding') {
|
387 | separator = element.separator;
|
388 | } else {
|
389 | separator = element.type === 'UListBinding' ? '- ' : '1. ';
|
390 | }
|
391 |
|
392 |
|
393 | let firstNestedTemplate;
|
394 |
|
395 | if (element.type === 'JoinBinding') {
|
396 | firstNestedTemplate = element.template;
|
397 | } else {
|
398 | firstNestedTemplate = ParserManager.adjustListBlock(element.template, separator);
|
399 | }
|
400 |
|
401 | this.buildGrammarRules(firstNestedTemplate, nestedTemplateModel, propertyName + 'First', parts);
|
402 | firstType = element.fieldName.value + 'First';
|
403 |
|
404 | let nestedTemplate;
|
405 |
|
406 | if (element.type === 'JoinBinding') {
|
407 | nestedTemplate = ParserManager.adjustListBlock(element.template, separator);
|
408 | } else {
|
409 | nestedTemplate = ParserManager.adjustListBlock(element.template, '\n');
|
410 | }
|
411 |
|
412 | this.buildGrammarRules(nestedTemplate, nestedTemplateModel, propertyName, parts);
|
413 | type = element.fieldName.value;
|
414 | action = "\n{%\n ([ ".concat(propertyName + 'First', ", ").concat(propertyName, " ]) => {\n return [").concat(propertyName + 'First', "].concat(").concat(propertyName, ");\n}\n%}");
|
415 | } else {
|
416 |
|
417 | if (property instanceof RelationshipDeclaration) {
|
418 | type = 'String';
|
419 | }
|
420 | }
|
421 | }
|
422 |
|
423 | if (property.isArray()) {
|
424 | suffix += '*';
|
425 | }
|
426 |
|
427 | if (property.isOptional()) {
|
428 | suffix += '?';
|
429 | }
|
430 |
|
431 | if (suffix === ':') {
|
432 | suffix = '';
|
433 | }
|
434 |
|
435 |
|
436 | if (element.type === 'UListBinding' || element.type === 'OListBinding' || element.type === 'JoinBinding') {
|
437 | parts.modelRules.push({
|
438 | prefix: inputRule,
|
439 |
|
440 | symbols: ["".concat(firstType, " ").concat(type).concat(suffix, " ").concat(action, " # ").concat(propertyName)]
|
441 | });
|
442 | } else {
|
443 | parts.modelRules.push({
|
444 | prefix: inputRule,
|
445 |
|
446 | symbols: ["".concat(type).concat(suffix, " ").concat(action, " # ").concat(propertyName)]
|
447 | });
|
448 | }
|
449 | }
|
450 | |
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 |
|
457 |
|
458 |
|
459 |
|
460 | cleanChunk(input) {
|
461 |
|
462 | let text = input.replace(/\n/gm, '\\n');
|
463 |
|
464 | text = text.replace(/"/gm, '\\"');
|
465 | return "\"".concat(text, "\"");
|
466 | }
|
467 | |
468 |
|
469 |
|
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 | findFirstBinding(propertyName, elements) {
|
477 | for (let n = 0; n < elements.length; n++) {
|
478 | const element = elements[n];
|
479 |
|
480 | if (element !== null && ['Binding', 'FormattedBinding', 'IfBinding', 'IfElseBinding', 'UListBinding', 'OListBinding', 'JoinBinding', 'ClauseBinding', 'WithBinding'].includes(element.type)) {
|
481 | if (element.fieldName.value === propertyName) {
|
482 | return n;
|
483 | }
|
484 | }
|
485 | }
|
486 |
|
487 | return -1;
|
488 | }
|
489 | |
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 | getGrammar() {
|
496 | return this.grammar;
|
497 | }
|
498 | |
499 |
|
500 |
|
501 |
|
502 |
|
503 |
|
504 | getTemplatizedGrammar() {
|
505 | return this.templatizedGrammar;
|
506 | }
|
507 | |
508 |
|
509 |
|
510 |
|
511 |
|
512 |
|
513 |
|
514 | static compileGrammar(sourceCode) {
|
515 | try {
|
516 |
|
517 | const grammarParser = new nearley.Parser(nearleyGrammar);
|
518 | grammarParser.feed(sourceCode);
|
519 | const grammarAst = grammarParser.results[0];
|
520 |
|
521 |
|
522 | const grammarInfoObject = compile(grammarAst, {});
|
523 |
|
524 | const grammarJs = generate(grammarInfoObject, 'grammar');
|
525 |
|
526 | const module = {
|
527 | exports: {}
|
528 | };
|
529 | eval(grammarJs);
|
530 | return module.exports;
|
531 | } catch (err) {
|
532 | Logger.error(err);
|
533 | throw err;
|
534 | }
|
535 | }
|
536 | |
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 | roundtripMarkdown(text) {
|
544 |
|
545 | const commonMarkTransformer = new CommonMarkTransformer({
|
546 | noIndex: true
|
547 | });
|
548 | const concertoAst = commonMarkTransformer.fromMarkdown(text);
|
549 | return commonMarkTransformer.toMarkdown(concertoAst);
|
550 | }
|
551 |
|
552 | }
|
553 |
|
554 | module.exports = ParserManager; |
\ | No newline at end of file |