angular-cn/aio/tools/transforms/remark-package/services/renderMarkdown.js

214 lines
6.8 KiB
JavaScript
Raw Normal View History

const remark = require('remark');
const html = require('remark-html');
const code = require('./handlers/code');
const mapHeadings = require('./plugins/mapHeadings');
/**
* @dgService renderMarkdown
* @description
* Render the markdown in the given string as HTML.
* @param headingMap A map of headings to convert.
* E.g. `{h3: 'h4'} will map heading 3 level into heading 4.
*/
module.exports = function renderMarkdown() {
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 } });
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];
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);
}
}
}
}
};
/**
* 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;
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;
}
}
}
}
if (count) {
throw new Error(str.slice(start, index));
}
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}>`;
}