From bc9899451c5cbde3a636203d4b3d4d397da14c7e Mon Sep 17 00:00:00 2001 From: Dan Selman Date: Wed, 5 Jul 2023 12:14:55 +0100 Subject: [PATCH] feat(markdown) support multiple key/value attributes on a block (#549) * feat(markdown) support multiple key/value attributes on a block Signed-off-by: Dan Selman * fix(tests) : update snapshots etc to templatemark v0.5.0 Signed-off-by: Dan Selman * chore(concerto) bump version Signed-off-by: Dan Selman * chore(concerto) bump version Signed-off-by: Dan Selman --------- Signed-off-by: Dan Selman --- .../markdown-it-template/lib/template_re.js | 46 ++++++--- .../markdown-template/lib/templaterules.js | 6 +- .../markdown-template/src/templaterules.js | 6 +- .../test/TemplateMarkTransformer.js | 95 +++++++++++++++++++ 4 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 packages/markdown-template/test/TemplateMarkTransformer.js diff --git a/packages/markdown-it-template/lib/template_re.js b/packages/markdown-it-template/lib/template_re.js index 199957b5..a06d8a43 100644 --- a/packages/markdown-it-template/lib/template_re.js +++ b/packages/markdown-it-template/lib/template_re.js @@ -21,12 +21,12 @@ const names = require('./names.json'); const string = '"([^"]*)"'; const identifier = '([a-zA-Z_][a-zA-Z0-9_]+)'; const name = '(?:\\s+([A-Za-z0-9_-]+))'; -const attribute = '(?:\\s+' + identifier + '(?:\\s*=\\s*' + string + ')?)'; +const attributes = '(.*?)'; const format = '(:?\\s+as\\s*'+ string + '\\s*)?'; const variable = '{{\\s*' + identifier + format + '\\s*}}'; -const open_block = '{{#\\s*' + identifier + name + attribute + '*\\s*}}'; +const open_block = '{{#\\s*' + identifier + name + attributes + '\\s*}}'; const close_block = '{{/\\s*' + identifier + '\\s*}}'; const formula = '{{%([^%]*)%}}'; @@ -35,19 +35,39 @@ const OPEN_BLOCK_RE = new RegExp('^(?:' + open_block + ')'); const CLOSE_BLOCK_RE = new RegExp('^(?:' + close_block + ')'); const FORMULA_RE = new RegExp('^(?:' + formula + ')'); +/** + * Parses an argument string into an object + * @param {string} input the argument string to parse + * @returns {[string]} an array of strings, key/value + */ +function parseArguments(input) { + const regex = /(\w+)\s*=\s*"([^"]+)"/g; + let match; + const result = []; + while ((match = regex.exec(input))) { + const argName = match[1]; + const argValue = match[2]; + result.push([argName, argValue]); + } + return result; +} + /** * Extract attributes from opening blocks - * @param {string[]} match + * @param {string[]} match the block data * @return {*[]} attributes */ function getBlockAttributes(match) { - const result = []; + let result = []; // name is always present in the block result.push([ 'name', match[2] ]); - // those are block attributes - for(let i = 3; i < match.length; i = i+2) { - if (match[i]) { - result.push([ match[i], match[i+1] ]); + + // the fourth match is all the arguments + // e.g. style="long" locale="en" + if(match[3]) { + const args = parseArguments(match[3]); + if(args && args.length > 0) { + result = result.concat(args); } } return result; @@ -60,9 +80,9 @@ function getBlockAttributes(match) { * @return {*} open tag */ function matchOpenBlock(text,stack) { - var match = text.match(OPEN_BLOCK_RE); + const match = text.match(OPEN_BLOCK_RE); if (!match) { return null; } - var block_open = match[1]; + const block_open = match[1]; if (!names.blocks.includes(block_open)) { return null; } stack.unshift(block_open); return { tag: block_open, attrs: getBlockAttributes(match), matched: match }; @@ -75,14 +95,14 @@ function matchOpenBlock(text,stack) { * @return {*} close tag */ function matchCloseBlock(text,block_open,stack) { - var match = text.match(CLOSE_BLOCK_RE); + const match = text.match(CLOSE_BLOCK_RE); if (!match) { return null; } - var block_close = match[1]; + const block_close = match[1]; // Handle proper nesting if (stack[0] === block_close) { - stack.shift() + stack.shift(); } // Handle stack depleted if (stack.length > 0) { diff --git a/packages/markdown-template/lib/templaterules.js b/packages/markdown-template/lib/templaterules.js index 13e63df5..f08103a8 100644 --- a/packages/markdown-template/lib/templaterules.js +++ b/packages/markdown-template/lib/templaterules.js @@ -66,7 +66,7 @@ var formulaRule = { node.name = formulaName(code); node.code = { $class: "".concat(TemplateMarkModel.NAMESPACE, ".Code"), - type: 'ES_2020', + type: 'TYPESCRIPT', contents: code }; node.dependencies = []; @@ -84,7 +84,7 @@ var ifOpenRule = { if (condition) { node.condition = { $class: "".concat(TemplateMarkModel.NAMESPACE, ".Code"), - type: 'ES_2020', + type: 'TYPESCRIPT', contents: condition }; } @@ -206,7 +206,7 @@ var clauseOpenRule = { if (condition) { node.condition = { $class: "".concat(TemplateMarkModel.NAMESPACE, ".Code"), - type: 'ES_2020', + type: 'TYPESCRIPT', contents: condition }; } diff --git a/packages/markdown-template/src/templaterules.js b/packages/markdown-template/src/templaterules.js index 476209d3..b1886ac8 100644 --- a/packages/markdown-template/src/templaterules.js +++ b/packages/markdown-template/src/templaterules.js @@ -61,7 +61,7 @@ const formulaRule = { node.name = formulaName(code); node.code = { $class: `${TemplateMarkModel.NAMESPACE}.Code`, - type: 'ES_2020', + type: 'TYPESCRIPT', contents: code }; node.dependencies = []; @@ -79,7 +79,7 @@ const ifOpenRule = { if(condition) { node.condition = { $class: `${TemplateMarkModel.NAMESPACE}.Code`, - type: 'ES_2020', + type: 'TYPESCRIPT', contents: condition }; } @@ -195,7 +195,7 @@ const clauseOpenRule = { if(condition) { node.condition = { $class: `${TemplateMarkModel.NAMESPACE}.Code`, - type: 'ES_2020', + type: 'TYPESCRIPT', contents: condition }; } diff --git a/packages/markdown-template/test/TemplateMarkTransformer.js b/packages/markdown-template/test/TemplateMarkTransformer.js new file mode 100644 index 00000000..e8d8cc66 --- /dev/null +++ b/packages/markdown-template/test/TemplateMarkTransformer.js @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const chai = require('chai'); +chai.use(require('chai-string')); + +chai.should(); +chai.use(require('chai-things')); +chai.use(require('chai-as-promised')); + +const { ModelManager } = require('@accordproject/concerto-core'); +const { TemplateMarkModel } = require('@accordproject/markdown-common'); + +const TemplateMarkTransformer = require('../lib/TemplateMarkTransformer'); + +const MODEL = ` +namespace test@1.0.0 +@template +concept Thing { + o String[] items +}`; + +describe('#TemplateMarkTransformer', () => { + describe('#tokensToMarkdownTemplate', () => { + it('should handle join with type, style and locale', async () => { + const transformer = new TemplateMarkTransformer(); + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL); + const tokens = transformer.toTokens({content: '{{#join items type="conjunction" style="long" locale="en"}}{{/join}}'}); + const result = transformer.tokensToMarkdownTemplate(tokens, modelManager, 'clause'); + const joinNode = result.nodes[0].nodes[0].nodes[0]; + joinNode.$class.should.equal(`${TemplateMarkModel.NAMESPACE}.JoinDefinition`); + joinNode.locale.should.equal('en'); + joinNode.type.should.equal('conjunction'); + joinNode.style.should.equal('long'); + }); + + it('should handle join with type, style', async () => { + const transformer = new TemplateMarkTransformer(); + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL); + const tokens = transformer.toTokens({content: '{{#join items type="conjunction" style="long"}}{{/join}}'}); + const result = transformer.tokensToMarkdownTemplate(tokens, modelManager, 'clause'); + const joinNode = result.nodes[0].nodes[0].nodes[0]; + joinNode.$class.should.equal(`${TemplateMarkModel.NAMESPACE}.JoinDefinition`); + joinNode.type.should.equal('conjunction'); + joinNode.style.should.equal('long'); + }); + + it('should handle join with type', async () => { + const transformer = new TemplateMarkTransformer(); + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL); + const tokens = transformer.toTokens({content: '{{#join items type="conjunction"}}{{/join}}'}); + const result = transformer.tokensToMarkdownTemplate(tokens, modelManager, 'clause'); + const joinNode = result.nodes[0].nodes[0].nodes[0]; + joinNode.$class.should.equal(`${TemplateMarkModel.NAMESPACE}.JoinDefinition`); + joinNode.type.should.equal('conjunction'); + }); + + it('should handle join', async () => { + const transformer = new TemplateMarkTransformer(); + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL); + const tokens = transformer.toTokens({content: '{{#join items}}{{/join}}'}); + const result = transformer.tokensToMarkdownTemplate(tokens, modelManager, 'clause'); + const joinNode = result.nodes[0].nodes[0].nodes[0]; + joinNode.$class.should.equal(`${TemplateMarkModel.NAMESPACE}.JoinDefinition`); + }); + + it('should ignore unknown attributes on join', async () => { + const transformer = new TemplateMarkTransformer(); + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL); + const tokens = transformer.toTokens({content: '{{#join items foo="bar"}}{{/join}}'}); + const result = transformer.tokensToMarkdownTemplate(tokens, modelManager, 'clause'); + const joinNode = result.nodes[0].nodes[0].nodes[0]; + joinNode.$class.should.equal(`${TemplateMarkModel.NAMESPACE}.JoinDefinition`); + (joinNode.foo === undefined).should.be.true; + }); + }); +});