UNPKG

9.3 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/**
16 * Converts a CommonMark DOM to a markdown string.
17 *
18 * Note that there are multiple ways of representing the same CommonMark DOM as text,
19 * so this transformation is not guaranteed to equivalent if you roundtrip
20 * markdown content. For example an H1 can be specified using either '#' or '='
21 * notation.
22 *
23 * The resulting AST *should* be equivalent however.
24 */
25
26class ToMarkdownStringVisitor {
27 /**
28 * Construct the visitor.
29 * @param {object} [options] configuration options
30 * @param {boolean} [options.noIndex] do not index ordered list (i.e., use 1. everywhere)
31 */
32 constructor(options) {
33 this.options = options;
34 }
35 /**
36 * Visits a sub-tree and return the markdown
37 * @param {*} visitor - the visitor to use
38 * @param {*} thing - the node to visit
39 * @param {*} parameters - the current parameters
40 * @param {*} paramsFun - function to construct the parameters for children
41 * @returns {string} the markdown for the sub tree
42 */
43
44
45 static visitChildren(visitor, thing, parameters, paramsFun) {
46 const paramsFunActual = paramsFun ? paramsFun : ToMarkdownStringVisitor.mkParameters;
47 const parametersIn = paramsFunActual(parameters);
48
49 if (thing.nodes) {
50 thing.nodes.forEach(node => {
51 node.accept(visitor, parametersIn);
52 });
53 }
54
55 return parametersIn.result;
56 }
57 /**
58 * Set parameters for general blocks
59 * @param {*} parametersOut - the current parameters
60 * @return {*} the new parameters with block quote level incremented
61 */
62
63
64 static mkParameters(parametersOut) {
65 let parameters = {};
66 parameters.result = '';
67 parameters.first = false;
68 parameters.stack = parametersOut.stack.slice();
69 return parameters;
70 }
71 /**
72 * Set parameters for block quote
73 * @param {*} parametersOut - the current parameters
74 * @return {*} the new parameters with block quote level incremented
75 */
76
77
78 static mkParametersInBlockQuote(parametersOut) {
79 let parameters = {};
80 parameters.result = '';
81 parameters.first = false;
82 parameters.stack = parametersOut.stack.slice();
83 parameters.stack.push('block');
84 return parameters;
85 }
86 /**
87 * Set parameters for inner list
88 * @param {*} parametersOut - the current parameters
89 * @return {*} the new parameters with first set to true
90 */
91
92
93 static mkParametersInList(parametersOut) {
94 let parameters = {};
95 parameters.result = '';
96 parameters.first = true;
97 parameters.stack = parametersOut.stack.slice();
98 parameters.stack.push('list');
99 return parameters;
100 }
101 /**
102 * Create prefix
103 * @param {*} parameters - the parameters
104 * @param {*} newlines - number of newlines
105 * @return {string} the prefix
106 */
107
108
109 static mkPrefix(parameters, newlines) {
110 const stack = parameters.stack;
111 const newlinesFix = parameters.first ? 0 : newlines;
112 let prefix = '';
113
114 for (let i = 0; i < stack.length; i++) {
115 if (stack[i] === 'list') {
116 prefix += ' ';
117 } else if (stack[i] === 'block') {
118 prefix += '> ';
119 }
120 }
121
122 return ('\n' + prefix).repeat(newlinesFix);
123 }
124 /**
125 * Create Setext heading
126 * @param {number} level - the heading level
127 * @return {string} the markup for the heading
128 */
129
130
131 static mkSetextHeading(level) {
132 if (level === 1) {
133 return '====';
134 } else {
135 return '----';
136 }
137 }
138 /**
139 * Create ATX heading
140 * @param {number} level - the heading level
141 * @return {string} the markup for the heading
142 */
143
144
145 static mkATXHeading(level) {
146 return Array(level).fill('#').join('');
147 }
148 /**
149 * Adding escapes for code blocks
150 * @param {string} input - unescaped
151 * @return {string} escaped
152 */
153
154
155 static escapeCodeBlock(input) {
156 return input.replace(/`/g, '\\`');
157 }
158 /**
159 * Adding escapes for text nodes
160 * @param {string} input - unescaped
161 * @return {string} escaped
162 */
163
164
165 static escapeText(input) {
166 return input.replace(/[*`#&]/g, '\\$&') // Replaces special characters
167 .replace(/^(\d+)\./g, '$1\\.') // Replaces ordered lists
168 .replace(/^-/g, '\\-'); // Replaces unordered lists
169 }
170 /**
171 * Visit a node
172 * @param {*} thing the object being visited
173 * @param {*} parameters the parameters
174 */
175
176
177 visit(thing, parameters) {
178 const nodeText = thing.text ? thing.text : '';
179
180 switch (thing.getType()) {
181 case 'CodeBlock':
182 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
183 parameters.result += "```".concat(thing.info ? ' ' + thing.info : '', "\n").concat(ToMarkdownStringVisitor.escapeCodeBlock(thing.text), "```");
184 break;
185
186 case 'Code':
187 parameters.result += "`".concat(nodeText, "`");
188 break;
189
190 case 'HtmlInline':
191 parameters.result += nodeText;
192 break;
193
194 case 'Emph':
195 parameters.result += "*".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "*");
196 break;
197
198 case 'Strong':
199 parameters.result += "**".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "**");
200 break;
201
202 case 'BlockQuote':
203 parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters, ToMarkdownStringVisitor.mkParametersInBlockQuote);
204 break;
205
206 case 'Heading':
207 {
208 const level = parseInt(thing.level);
209
210 if (level < 3) {
211 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
212 parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters);
213 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1);
214 parameters.result += ToMarkdownStringVisitor.mkSetextHeading(level);
215 } else {
216 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
217 parameters.result += ToMarkdownStringVisitor.mkATXHeading(level);
218 parameters.result += ' ';
219 parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters);
220 }
221 }
222 break;
223
224 case 'ThematicBreak':
225 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
226 parameters.result += '---';
227 break;
228
229 case 'Linebreak':
230 parameters.result += '\\';
231 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1);
232 break;
233
234 case 'Softbreak':
235 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 1);
236 break;
237
238 case 'Link':
239 parameters.result += "[".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "](").concat(thing.destination, " \"").concat(thing.title ? thing.title : '', "\")");
240 break;
241
242 case 'Image':
243 parameters.result += "![".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters), "](").concat(thing.destination, " \"").concat(thing.title ? thing.title : '', "\")");
244 break;
245
246 case 'Paragraph':
247 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
248 parameters.result += "".concat(ToMarkdownStringVisitor.visitChildren(this, thing, parameters));
249 break;
250
251 case 'HtmlBlock':
252 parameters.result += ToMarkdownStringVisitor.mkPrefix(parameters, 2);
253 parameters.result += nodeText;
254 break;
255
256 case 'Text':
257 parameters.result += ToMarkdownStringVisitor.escapeText(nodeText);
258 break;
259
260 case 'List':
261 {
262 const first = thing.start ? parseInt(thing.start) : 1;
263 let index = first;
264 thing.nodes.forEach(item => {
265 if (thing.tight === 'false' && index !== first) {
266 parameters.result += '\n';
267 }
268
269 if (thing.type === 'ordered') {
270 parameters.result += "".concat(ToMarkdownStringVisitor.mkPrefix(parameters, 1)).concat(this.options.noIndex ? 1 : index, ". ").concat(ToMarkdownStringVisitor.visitChildren(this, item, parameters, ToMarkdownStringVisitor.mkParametersInList));
271 } else {
272 parameters.result += "".concat(ToMarkdownStringVisitor.mkPrefix(parameters, 1), "- ").concat(ToMarkdownStringVisitor.visitChildren(this, item, parameters, ToMarkdownStringVisitor.mkParametersInList));
273 }
274
275 index++;
276 });
277 }
278 break;
279
280 case 'Item':
281 throw new Error('Item node should not occur outside of List nodes');
282
283 case 'Document':
284 parameters.result += ToMarkdownStringVisitor.visitChildren(this, thing, parameters);
285 break;
286
287 default:
288 throw new Error("Unhandled type ".concat(thing.getType()));
289 }
290
291 parameters.first = false;
292 }
293
294}
295
296module.exports = ToMarkdownStringVisitor;
\No newline at end of file