UNPKG

@accordproject/markdown-common

Version:
296 lines (246 loc) • 9.3 kB
/* * 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'; /** * Converts a CommonMark DOM to a markdown string. * * Note that there are multiple ways of representing the same CommonMark DOM as text, * so this transformation is not guaranteed to equivalent if you roundtrip * markdown content. For example an H1 can be specified using either '#' or '=' * notation. * * The resulting AST *should* be equivalent however. */ class ToMarkdownStringVisitor { /** * Construct the visitor. * @param {object} [options] configuration options * @param {boolean} [options.noIndex] do not index ordered list (i.e., use 1. everywhere) */ constructor(options) { this.options = options; } /** * Visits a sub-tree and return the markdown * @param {*} visitor - the visitor to use * @param {*} thing - the node to visit * @param {*} parameters - the current parameters * @param {*} paramsFun - function to construct the parameters for children * @returns {string} the markdown for the sub tree */ static visitChildren(visitor, thing, parameters, paramsFun) { const paramsFunActual = paramsFun ? paramsFun : ToMarkdownStringVisitor.mkParameters; const parametersIn = paramsFunActual(parameters); if (thing.nodes) { thing.nodes.forEach(node => { node.accept(visitor, parametersIn); }); } return parametersIn.result; } /** * Set parameters for general blocks * @param {*} parametersOut - the current parameters * @return {*} the new parameters with block quote level incremented */ static mkParameters(parametersOut) { let parameters = {}; parameters.result = ''; parameters.first = false; parameters.stack = parametersOut.stack.slice(); return parameters; } /** * Set parameters for block quote * @param {*} parametersOut - the current parameters * @return {*} the new parameters with block quote level incremented */ static mkParametersInBlockQuote(parametersOut) { let parameters = {}; parameters.result = ''; parameters.first = false; parameters.stack = parametersOut.stack.slice(); parameters.stack.push('block'); return parameters; } /** * Set parameters for inner list * @param {*} parametersOut - the current parameters * @return {*} the new parameters with first set to true */ static mkParametersInList(parametersOut) { let parameters = {}; parameters.result = ''; parameters.first = true; parameters.stack = parametersOut.stack.slice(); parameters.stack.push('list'); return parameters; } /** * Create prefix * @param {*} parameters - the parameters * @param {*} newlines - number of newlines * @return {string} the prefix */ static mkPrefix(parameters, newlines) { const stack = parameters.stack; const newlinesFix = parameters.first ? 0 : newlines; let prefix = ''; for (let i = 0; i < stack.length; i++) { if (stack[i] === 'list') { prefix += ' '; } else if (stack[i] === 'block') { prefix += '> '; } } return ('\n' + prefix).repeat(newlinesFix); } /** * Create Setext heading * @param {number} level - the heading level * @return {string} the markup for the heading */ static mkSetextHeading(level) { if (level === 1) { return '===='; } else { return '----'; } } /** * Create ATX heading * @param {number} level - the heading level * @return {string} the markup for the heading */ static mkATXHeading(level) { return Array(level).fill('#').join(''); } /** * Adding escapes for code blocks * @param {string} input - unescaped * @return {string} escaped */ static escapeCodeBlock(input) { return input.replace(/`/g, '\\`'); } /** * Adding escapes for text nodes * @param {string} input - unescaped * @return {string} escaped */ static escapeText(input) { return input.replace(/[*`#&]/g, '\\$&') // Replaces special characters .replace(/^(\d+)\./g, '$1\\.') // Replaces ordered lists .replace(/^-/g, '\\-'); // Replaces unordered lists } /** * Visit a node * @param {*} thing the object being visited * @param {*} parameters the parameters */ visit(thing, parameters) { const nodeText = thing.text ? thing.text : ''; switch (thing.getType()) { case 'CodeBlock': parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += "```".concat(thing.info ? ' ' + thing.info : '', "\n").concat(ToMarkdownStringVisitor.escapeCodeBlock(thing.text), "```"); break; case 'Code': parameters.result += "`".concat(nodeText, "`"); break; case 'HtmlInline': parameters.result += nodeText; break; case 'Emph': parameters.result += "*".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "*"); break; case 'Strong': parameters.result += "**".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "**"); break; case 'BlockQuote': parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters, ToMarkdownStringVisitor.mkParametersInBlockQuote); break; case 'Heading': { const level = parseInt(thing.level); if (level < 3) { parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters); parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1); parameters.result += ToMarkdownStringVisitor.mkSetextHeading(level); } else { parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += ToMarkdownStringVisitor.mkATXHeading(level); parameters.result += ' '; parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters); } } break; case 'ThematicBreak': parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += '---'; break; case 'Linebreak': parameters.result += '\\'; parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1); break; case 'Softbreak': parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1); break; case 'Link': parameters.result += "[".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "](").concat(thing.destination, " \"").concat(thing.title ? thing.title : '', "\")"); break; case 'Image': parameters.result += "![".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "](").concat(thing.destination, " \"").concat(thing.title ? thing.title : '', "\")"); break; case 'Paragraph': parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += "".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters)); break; case 'HtmlBlock': parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2); parameters.result += nodeText; break; case 'Text': parameters.result += ToMarkdownStringVisitor.escapeText(nodeText); break; case 'List': { const first = thing.start ? parseInt(thing.start) : 1; let index = first; thing.nodes.forEach(item => { if (thing.tight === 'false' && index !== first) { parameters.result += '\n'; } if (thing.type === 'ordered') { parameters.result += "".concat(ToMarkdownStringVisitor.mkPrefix(parameters, 1)).concat(this.options.noIndex ? 1 : index, ". ").concat(ToMarkdownStringVisitor.visitChildren(this, item, parameters, ToMarkdownStringVisitor.mkParametersInList)); } else { parameters.result += "".concat(ToMarkdownStringVisitor.mkPrefix(parameters, 1), "- ").concat(ToMarkdownStringVisitor.visitChildren(this, item, parameters, ToMarkdownStringVisitor.mkParametersInList)); } index++; }); } break; case 'Item': throw new Error('Item node should not occur outside of List nodes'); case 'Document': parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters); break; default: throw new Error("Unhandled type ".concat(thing.getType())); } parameters.first = false; } } module.exports = ToMarkdownStringVisitor;