diff --git a/bin/index.js b/bin/index.js index 4f697a3d..09f2f48c 100755 --- a/bin/index.js +++ b/bin/index.js @@ -29,7 +29,12 @@ require('yargs') type: 'string' }); yargs.option('--generateMarkdown', { - describe: 'path to the output file', + describe: 'roundtrip back to Markdown', + type: 'boolean', + default: false + }); + yargs.option('--withAP', { + describe: 'further transform for AP', type: 'boolean', default: false }); @@ -40,7 +45,7 @@ require('yargs') try { argv = Commands.validateParseArgs(argv); - return Commands.parse(argv.sample, argv.out, argv.generateMarkdown) + return Commands.parse(argv.sample, argv.out, argv.generateMarkdown, argv.withAP) .then((result) => { if(result) {Logger.info('\n'+result);} }) diff --git a/src/Commands.js b/src/Commands.js index 4c296407..02ad3ed0 100644 --- a/src/Commands.js +++ b/src/Commands.js @@ -19,6 +19,7 @@ const Logger = require('./Logger'); const CommonmarkParser = require('./CommonmarkParser'); const CommonmarkToString = require('./CommonmarkToString'); const CommonmarkToAP = require('./CommonmarkToAP'); +const CommonmarkFromAP = require('./CommonmarkFromAP'); /** * Utility class that implements the commands exposed by the CLI. @@ -85,15 +86,21 @@ class Commands { * @param {string} samplePath to the sample file * @param {string} outPath to an output file * @param {boolean} generateMarkdown whether to transform back to markdown + * @param {boolean} withAP whether to further transform for AP * @returns {object} Promise to the result of parsing */ - static parse(samplePath, outPath, generateMarkdown) { + static parse(samplePath, outPath, generateMarkdown, withAP) { const parser = new CommonmarkParser(); const markdownText = fs.readFileSync(samplePath, 'utf8'); let concertoObject = parser.parse(markdownText); - concertoObject = CommonmarkToAP(concertoObject); + if (withAP) { + concertoObject = CommonmarkToAP(concertoObject); + } let result; if (generateMarkdown) { + if (withAP) { + concertoObject = CommonmarkFromAP(concertoObject); + } result = CommonmarkToString(concertoObject); } else { const json = parser.getSerializer().toJSON(concertoObject); diff --git a/src/Commonmark.test.js b/src/Commonmark.test.js index 0d4cdcd4..50ed48b8 100644 --- a/src/Commonmark.test.js +++ b/src/Commonmark.test.js @@ -122,7 +122,7 @@ function extractSpecTests(testfile) { return examples; } -describe('markdown', () => { +describe.only('markdown', () => { getMarkdownFiles().forEach( ([file, markdownText]) => { it(`converts ${file} to concerto`, () => { const concertoObject = parser.parse(markdownText); diff --git a/src/CommonmarkAP.test.js b/src/CommonmarkAP.test.js index 9ddb4259..b62e25bf 100644 --- a/src/CommonmarkAP.test.js +++ b/src/CommonmarkAP.test.js @@ -20,6 +20,7 @@ const fs = require('fs'); const diff = require('jest-diff'); const CommonmarkParser = require('./CommonmarkParser'); const CommonmarkToAP = require('./CommonmarkToAP'); +const CommonmarkFromAP = require('./CommonmarkFromAP'); const CommonmarkToString = require('./CommonmarkToString'); let parser = null; @@ -30,7 +31,7 @@ expect.extend({ const json1 = parser.getSerializer().toJSON(concertoObject1); const newMarkdown = CommonmarkToString(concertoObject1); let concertoObject2 = parser.parse(newMarkdown); - concertoObject2 = CommonmarkToAP(concertoObject2); + concertoObject2 = CommonmarkFromAP(CommonmarkToAP(concertoObject2)); const json2 = parser.getSerializer().toJSON(concertoObject2); const pass = JSON.stringify(json1) === JSON.stringify(json2); diff --git a/src/CommonmarkFromAP.js b/src/CommonmarkFromAP.js new file mode 100644 index 00000000..18e3d66c --- /dev/null +++ b/src/CommonmarkFromAP.js @@ -0,0 +1,52 @@ +/* + * 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 ModelManager = require('composer-concerto').ModelManager; +const Factory = require('composer-concerto').Factory; +const Serializer = require('composer-concerto').Serializer; + +const FromAPVisitor = require('./FromAPVisitor'); +const CommonmarkToString = require('./CommonmarkToString'); +const { commonmarkModel } = require('./Models'); + +/** + * Converts a commonmark document to a markdown string + * @param {*} concertoObject concerto commonmark object + * @returns {*} concertoObject concerto commonmark for AP object + */ +function commonMarkFromAP(concertoObject) { + // Setup for validation + const modelManager = new ModelManager(); + modelManager.addModelFile( commonmarkModel, 'commonmark.cto'); + const factory = new Factory(modelManager); + const serializer = new Serializer(factory, modelManager); + + // Add AP nodes + const parameters = { + commonmarkToString: CommonmarkToString, + modelManager : modelManager , + factory : factory, + serializer : serializer + }; + const visitor = new FromAPVisitor(); + concertoObject.accept( visitor, parameters ); + + // Validate + const json = serializer.toJSON(concertoObject); + return serializer.fromJSON(json); +} + +module.exports = commonMarkFromAP; \ No newline at end of file diff --git a/src/CommonmarkToAP.js b/src/CommonmarkToAP.js index 87430a4d..e19c90ab 100644 --- a/src/CommonmarkToAP.js +++ b/src/CommonmarkToAP.js @@ -18,6 +18,7 @@ const ModelManager = require('composer-concerto').ModelManager; const Factory = require('composer-concerto').Factory; const Serializer = require('composer-concerto').Serializer; +const CommonmarkParser = require('./CommonmarkParser'); const ToAPVisitor = require('./ToAPVisitor'); const { commonmarkModel } = require('./Models'); @@ -27,6 +28,9 @@ const { commonmarkModel } = require('./Models'); * @returns {*} concertoObject concerto commonmark for AP object */ function commonMarkToAP(concertoObject) { + // Setup for Nested Parsing + const commonmarkParser = new CommonmarkParser(); + // Setup for validation const modelManager = new ModelManager(); modelManager.addModelFile( commonmarkModel, 'commonmark.cto'); @@ -35,6 +39,7 @@ function commonMarkToAP(concertoObject) { // Add AP nodes const parameters = { + parser: commonmarkParser, modelManager : modelManager , factory : factory, serializer : serializer diff --git a/src/FromAPVisitor.js b/src/FromAPVisitor.js new file mode 100644 index 00000000..b27d55db --- /dev/null +++ b/src/FromAPVisitor.js @@ -0,0 +1,172 @@ +/* + * 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 { NS_PREFIX } = require('./Models'); + +/** + * Converts a commonmark model instance to a markdown string. + * + * Note that there are several ways of representing the same markdown AST as text, + * so this transformation is not guaranteed to equivalent if you roundtrip + * markdown content. The resulting AST *should* be equivalent however. + */ +class FromAPVisitor { + + /** + * Visits a sub-tree and return the markdown + * @param {*} visitor the visitor to use + * @param {*} thing the node to visit + * @param {*} [parameters] optional parameters + */ + static visitChildren(visitor, thing, parameters) { + if(thing.nodes) { + thing.nodes.forEach(node => { + node.accept(visitor, parameters); + }); + } + } + + /** + * Visit a node + * @param {*} thing the object being visited + * @param {*} parameters the parameters + */ + visit(thing, parameters) { + switch(thing.getType()) { + case 'Clause': { + let jsonSource = {}; + let jsonTarget = {}; + + // Revert to CodeBlock + jsonTarget.$class = NS_PREFIX + 'CodeBlock'; + + // Get the content + const clauseJson = parameters.serializer.toJSON(thing); + jsonSource.$class = NS_PREFIX + 'Document'; + jsonSource.xmlns = 'http://commonmark.org/xml/1.0'; + jsonSource.nodes = clauseJson.nodes; + + const content = parameters.commonmarkToString(parameters.serializer.fromJSON(jsonSource)); + const attributeString = `src="${clauseJson.src}" clauseid="${clauseJson.clauseid}"`; + + jsonTarget.text = `\n${content}\n\n`; + + // Create the proper tag + let tag = {}; + tag.$class = NS_PREFIX + 'TagInfo'; + tag.tagName = 'clause'; + tag.attributeString = attributeString; + tag.content = content; + tag.closed = false; + tag.attributes = []; + + let attribute1 = {}; + attribute1.$class = NS_PREFIX + 'Attribute'; + attribute1.name = 'src'; + attribute1.value = clauseJson.src; + tag.attributes.push(attribute1); + + let attribute2 = {}; + attribute2.$class = NS_PREFIX + 'Attribute'; + attribute2.name = 'clauseid'; + attribute2.value = clauseJson.clauseid; + tag.attributes.push(attribute2); + + jsonTarget.tag = tag; + + let validatedTarget = parameters.serializer.fromJSON(jsonTarget); + + delete thing.clauseid; + delete thing.src; + + thing.$classDeclaration = validatedTarget.$classDeclaration; + thing.tag = validatedTarget.tag; + thing.nodes = validatedTarget.nodes; + thing.text = validatedTarget.text; + } + break; + case 'Variable': { + // Revert to HtmlInline + thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'HtmlInline'); + + // Create the text for that document + const content = ''; + const attributeString = `id="${thing.id}" value="${thing.value}"`; + thing.text = ``; + + // Create the proper tag + let tag = {}; + tag.$class = NS_PREFIX + 'TagInfo'; + tag.tagName = 'variable'; + tag.attributeString = attributeString; + tag.content = content; + tag.closed = true; + tag.attributes = []; + + let attribute1 = {}; + attribute1.$class = NS_PREFIX + 'Attribute'; + attribute1.name = 'id'; + attribute1.value = thing.id; + tag.attributes.push(attribute1); + + let attribute2 = {}; + attribute2.$class = NS_PREFIX + 'Attribute'; + attribute2.name = 'value'; + attribute2.value = thing.value; + tag.attributes.push(attribute2); + + thing.tag = parameters.serializer.fromJSON(tag); + + delete thing.id; + delete thing.value; + } + break; + case 'ComputedVariable': { + // Revert to HtmlInline + thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'HtmlInline'); + + // Create the text for that document + const content = ''; + const attributeString = `value="${thing.value}"`; + thing.text = ``; + + // Create the proper tag + let tag = {}; + tag.$class = NS_PREFIX + 'TagInfo'; + tag.tagName = 'computed'; + tag.attributeString = attributeString; + tag.content = content; + tag.closed = true; + tag.attributes = []; + + let attribute1 = {}; + attribute1.$class = NS_PREFIX + 'Attribute'; + attribute1.name = 'value'; + attribute1.value = thing.value; + tag.attributes.push(attribute1); + + thing.tag = parameters.serializer.fromJSON(tag); + + delete thing.value; + } + break; + default: + FromAPVisitor.visitChildren(this, thing, parameters); + } + } +} + +module.exports = FromAPVisitor; \ No newline at end of file diff --git a/src/ToAPVisitor.js b/src/ToAPVisitor.js index 6648b8a0..8bea2185 100644 --- a/src/ToAPVisitor.js +++ b/src/ToAPVisitor.js @@ -49,17 +49,22 @@ class ToAPVisitor { case 'CodeBlock': if (thing.tag && thing.tag.tagName === 'clause' && thing.tag.attributes.length === 2) { const tag = thing.tag; + //console.log('CONTENT! : ' + tag.content); if (tag.attributes[0].name === 'src' && tag.attributes[1].name === 'clauseid') { thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'Clause'); - thing.src = tag.attributes[0].name; - thing.clauseid = tag.attributes[1].name; + thing.src = tag.attributes[0].value; + thing.clauseid = tag.attributes[1].value; + thing.nodes = parameters.parser.parse(tag.content).nodes; // Parse text as markdown (in the nodes for the root) + thing.text = null; // Remove text } else if (tag.attributes[1].name === 'src' && tag.attributes[0].name === 'clauseid') { thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'Clause'); - thing.clauseid = tag.attributes[0].name; - thing.src = tag.attributes[1].name; + thing.clauseid = tag.attributes[0].value; + thing.src = tag.attributes[1].value; + thing.nodes = parameters.parser.parse(tag.content).nodes; // Parse text as markdown (in the nodes for the root) + thing.text = null; // Remove text } else { //console.log('Found Clause but without \'clauseid\' and \'src\' attributes '); } @@ -72,14 +77,14 @@ class ToAPVisitor { if (tag.attributes[0].name === 'id' && tag.attributes[1].name === 'value') { thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'Variable'); - thing.id = tag.attributes[0].name; - thing.value = tag.attributes[1].name; + thing.id = tag.attributes[0].value; + thing.value = tag.attributes[1].value; } else if (tag.attributes[1].name === 'id' && tag.attributes[0].name === 'value') { thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'Clause'); - thing.value = tag.attributes[0].name; - thing.id = tag.attributes[1].name; + thing.value = tag.attributes[0].value; + thing.id = tag.attributes[1].value; } else { //console.log('Found Variable but without \'id\' and \'value\' attributes '); } @@ -88,7 +93,7 @@ class ToAPVisitor { const tag = thing.tag; if (tag.attributes[0].name === 'value') { thing.$classDeclaration = parameters.modelManager.getType(NS_PREFIX + 'ComputedVariable'); - thing.value = tag.attributes[0].name; + thing.value = tag.attributes[0].value; } else { //console.log('Found ComputedVariable but without \'value\' attributes '); diff --git a/src/ToStringVisitor.js b/src/ToStringVisitor.js index b9f4c6b7..8e041e72 100644 --- a/src/ToStringVisitor.js +++ b/src/ToStringVisitor.js @@ -123,16 +123,13 @@ class ToStringVisitor { switch(thing.getType()) { case 'CodeBlock': - case 'Clause': ToStringVisitor.newParagraph(parameters); - parameters.result += `\`\`\` ${thing.info ? thing.info : ''}\n${thing.text}\`\`\`\n\n`; + parameters.result += `\`\`\`${thing.info ? thing.info : ''}\n${thing.text}\`\`\`\n\n`; break; case 'Code': parameters.result += `\`${thing.text}\``; break; case 'HtmlInline': - case 'Variable': - case 'ComputedVariable': parameters.result += thing.text; break; case 'Emph':