| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  | const remark = require('remark'); | 
					
						
							|  |  |  | const html = require('remark-html'); | 
					
						
							| 
									
										
										
										
											2017-04-29 15:24:43 +01:00
										 |  |  | const code = require('./handlers/code'); | 
					
						
							| 
									
										
										
										
											2018-05-18 15:07:11 +01:00
										 |  |  | const mapHeadings = require('./plugins/mapHeadings'); | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @dgService renderMarkdown | 
					
						
							|  |  |  |  * @description | 
					
						
							|  |  |  |  * Render the markdown in the given string as HTML. | 
					
						
							| 
									
										
										
										
											2018-05-18 15:07:11 +01:00
										 |  |  |  * @param headingMap A map of headings to convert. | 
					
						
							|  |  |  |  *                   E.g. `{h3: 'h4'} will map heading 3 level into heading 4.
 | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  |  */ | 
					
						
							|  |  |  | module.exports = function renderMarkdown() { | 
					
						
							| 
									
										
										
										
											2018-05-18 15:07:11 +01:00
										 |  |  |   return function renderMarkdownImpl(content, headingMap) { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const renderer = remark() | 
					
						
							|  |  |  |       .use(inlineTagDefs) | 
					
						
							|  |  |  |       .use(noIndentedCodeBlocks) | 
					
						
							|  |  |  |       .use(plainHTMLBlocks) | 
					
						
							|  |  |  |       // USEFUL DEBUGGING CODE
 | 
					
						
							|  |  |  |       // .use(() => tree => {
 | 
					
						
							|  |  |  |       //   console.log(require('util').inspect(tree, { colors: true, depth: 4 }));
 | 
					
						
							|  |  |  |       // })
 | 
					
						
							|  |  |  |       .use(mapHeadings(headingMap)) | 
					
						
							|  |  |  |       .use(html, { handlers: { code } }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  |     return renderer.processSync(content).toString(); | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Teach remark not to render indented codeblocks | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   function noIndentedCodeBlocks() { | 
					
						
							|  |  |  |     const blockMethods = this.Parser.prototype.blockMethods; | 
					
						
							|  |  |  |     blockMethods.splice(blockMethods.indexOf('indentedCode'), 1); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Teach remark about inline tags, so that it neither wraps block level | 
					
						
							|  |  |  |    * tags in paragraphs nor processes the text within the tag. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   function inlineTagDefs() { | 
					
						
							|  |  |  |     const Parser = this.Parser; | 
					
						
							|  |  |  |     const inlineTokenizers = Parser.prototype.inlineTokenizers; | 
					
						
							|  |  |  |     const inlineMethods = Parser.prototype.inlineMethods; | 
					
						
							|  |  |  |     const blockTokenizers = Parser.prototype.blockTokenizers; | 
					
						
							|  |  |  |     const blockMethods = Parser.prototype.blockMethods; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     blockTokenizers.inlineTag = tokenizeInlineTag; | 
					
						
							|  |  |  |     blockMethods.splice(blockMethods.indexOf('paragraph'), 0, 'inlineTag'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     inlineTokenizers.inlineTag = tokenizeInlineTag; | 
					
						
							|  |  |  |     inlineMethods.splice(blockMethods.indexOf('text'), 0, 'inlineTag'); | 
					
						
							|  |  |  |     tokenizeInlineTag.notInLink = true; | 
					
						
							|  |  |  |     tokenizeInlineTag.locator = inlineTagLocator; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function tokenizeInlineTag(eat, value, silent) { | 
					
						
							|  |  |  |       const match = /^\{@[^\s\}]+[^\}]*\}/.exec(value); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (match) { | 
					
						
							|  |  |  |         if (silent) { | 
					
						
							|  |  |  |           return true; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return eat(match[0])({ | 
					
						
							|  |  |  |           'type': 'inlineTag', | 
					
						
							|  |  |  |           'value': match[0] | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function inlineTagLocator(value, fromIndex) { | 
					
						
							|  |  |  |       return value.indexOf('{@', fromIndex); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Teach remark that some HTML blocks never include markdown | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   function plainHTMLBlocks() { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const plainBlocks = ['code-example', 'code-tabs']; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Create matchers for each block
 | 
					
						
							|  |  |  |     const anyBlockMatcher = new RegExp('^' + createOpenMatcher(`(${plainBlocks.join('|')})`)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const Parser = this.Parser; | 
					
						
							|  |  |  |     const blockTokenizers = Parser.prototype.blockTokenizers; | 
					
						
							|  |  |  |     const blockMethods = Parser.prototype.blockMethods; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     blockTokenizers.plainHTMLBlocks = tokenizePlainHTMLBlocks; | 
					
						
							|  |  |  |     blockMethods.splice(blockMethods.indexOf('html'), 0, 'plainHTMLBlocks'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     function tokenizePlainHTMLBlocks(eat, value, silent) { | 
					
						
							|  |  |  |       const openMatch = anyBlockMatcher.exec(value); | 
					
						
							|  |  |  |       if (openMatch) { | 
					
						
							|  |  |  |         const blockName = openMatch[1]; | 
					
						
							| 
									
										
										
										
											2017-05-04 22:02:26 +01:00
										 |  |  |         try { | 
					
						
							|  |  |  |           const fullMatch = matchRecursiveRegExp(value, createOpenMatcher(blockName), createCloseMatcher(blockName))[0]; | 
					
						
							|  |  |  |           if (silent || !fullMatch) { | 
					
						
							|  |  |  |             // either we are not eating (silent) or the match failed
 | 
					
						
							|  |  |  |             return !!fullMatch; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           return eat(fullMatch[0])({ | 
					
						
							|  |  |  |             type: 'html', | 
					
						
							|  |  |  |             value: fullMatch[0] | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } catch(e) { | 
					
						
							|  |  |  |           this.file.fail('Unmatched plain HTML block tag ' + e.message); | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * matchRecursiveRegExp | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * (c) 2007 Steven Levithan <stevenlevithan.com> | 
					
						
							|  |  |  |  * MIT License | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Accepts a string to search, a left and right format delimiter | 
					
						
							|  |  |  |  * as regex patterns, and optional regex flags. Returns an array | 
					
						
							|  |  |  |  * of matches, allowing nested instances of left/right delimiters. | 
					
						
							|  |  |  |  * Use the "g" flag to return all matches, otherwise only the | 
					
						
							|  |  |  |  * first is returned. Be careful to ensure that the left and | 
					
						
							|  |  |  |  * right format delimiters produce mutually exclusive matches. | 
					
						
							|  |  |  |  * Backreferences are not supported within the right delimiter | 
					
						
							|  |  |  |  * due to how it is internally combined with the left delimiter. | 
					
						
							|  |  |  |  * When matching strings whose format delimiters are unbalanced | 
					
						
							|  |  |  |  * to the left or right, the output is intentionally as a | 
					
						
							|  |  |  |  * conventional regex library with recursion support would | 
					
						
							|  |  |  |  * produce, e.g. "<<x>" and "<x>>" both produce ["x"] when using | 
					
						
							|  |  |  |  * "<" and ">" as the delimiters (both strings contain a single, | 
					
						
							|  |  |  |  * balanced instance of "<x>"). | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * examples: | 
					
						
							|  |  |  |  * matchRecursiveRegExp("test", "\\(", "\\)") | 
					
						
							|  |  |  |  * returns: [] | 
					
						
							|  |  |  |  * matchRecursiveRegExp("<t<<e>><s>>t<>", "<", ">", "g") | 
					
						
							|  |  |  |  * returns: ["t<<e>><s>", ""] | 
					
						
							|  |  |  |  * matchRecursiveRegExp("<div id=\"x\">test</div>", "<div\\b[^>]*>", "</div>", "gi") | 
					
						
							|  |  |  |  * returns: ["test"] | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function matchRecursiveRegExp(str, left, right, flags) { | 
					
						
							|  |  |  |   'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const matchPos = rgxFindMatchPos(str, left, right, flags); | 
					
						
							|  |  |  |   const results = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   for (var i = 0; i < matchPos.length; ++i) { | 
					
						
							|  |  |  |     results.push([ | 
					
						
							|  |  |  |       str.slice(matchPos[i].wholeMatch.start, matchPos[i].wholeMatch.end), | 
					
						
							|  |  |  |       str.slice(matchPos[i].match.start, matchPos[i].match.end), | 
					
						
							|  |  |  |       str.slice(matchPos[i].left.start, matchPos[i].left.end), | 
					
						
							|  |  |  |       str.slice(matchPos[i].right.start, matchPos[i].right.end) | 
					
						
							|  |  |  |     ]); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return results; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function rgxFindMatchPos(str, left, right, flags) { | 
					
						
							|  |  |  |   'use strict'; | 
					
						
							|  |  |  |   flags = flags || ''; | 
					
						
							|  |  |  |   const global = flags.indexOf('g') > -1; | 
					
						
							|  |  |  |   const bothMatcher = new RegExp(left + '|' + right, 'g' + flags.replace(/g/g, '')); | 
					
						
							|  |  |  |   const leftMatcher = new RegExp(left, flags.replace(/g/g, '')); | 
					
						
							|  |  |  |   const pos = []; | 
					
						
							|  |  |  |   let index, match, start, end; | 
					
						
							|  |  |  |   let count = 0; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-05-04 22:02:26 +01:00
										 |  |  |   while ((match = bothMatcher.exec(str))) { | 
					
						
							|  |  |  |     if (leftMatcher.test(match[0])) { | 
					
						
							|  |  |  |       if (!(count++)) { | 
					
						
							|  |  |  |         index = bothMatcher.lastIndex; | 
					
						
							|  |  |  |         start = index - match[0].length; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } else if (count) { | 
					
						
							|  |  |  |       if (!--count) { | 
					
						
							|  |  |  |         end = match.index + match[0].length; | 
					
						
							|  |  |  |         var obj = { | 
					
						
							|  |  |  |           left: {start: start, end: index}, | 
					
						
							|  |  |  |           match: {start: index, end: match.index}, | 
					
						
							|  |  |  |           right: {start: match.index, end: end}, | 
					
						
							|  |  |  |           wholeMatch: {start: start, end: end} | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |         pos.push(obj); | 
					
						
							|  |  |  |         if (!global) { | 
					
						
							|  |  |  |           return pos; | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2017-05-04 22:02:26 +01:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (count) { | 
					
						
							|  |  |  |     throw new Error(str.slice(start, index)); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-04-11 18:24:08 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return pos; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createOpenMatcher(elementNameMatcher) { | 
					
						
							|  |  |  |   const attributeName = '[a-zA-Z_:][a-zA-Z0-9:._-]*'; | 
					
						
							|  |  |  |   const unquoted = '[^"\'=<>`\\u0000-\\u0020]+'; | 
					
						
							|  |  |  |   const singleQuoted = '\'[^\']*\''; | 
					
						
							|  |  |  |   const doubleQuoted = '"[^"]*"'; | 
					
						
							|  |  |  |   const attributeValue = '(?:' + unquoted + '|' + singleQuoted + '|' + doubleQuoted + ')'; | 
					
						
							|  |  |  |   const attribute = '(?:\\s+' + attributeName + '(?:\\s*=\\s*' + attributeValue + ')?)'; | 
					
						
							|  |  |  |   return `<${elementNameMatcher}${attribute}*\\s*>`; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createCloseMatcher(elementNameMatcher) { | 
					
						
							|  |  |  |   return `</${elementNameMatcher}>`; | 
					
						
							|  |  |  | } |