You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

519 lines
13 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

'use strict';
var chalk = require('chalk');
var Table = require('cli-table');
var cardinal = require('cardinal');
var emoji = require('node-emoji');
const ansiEscapes = require('ansi-escapes');
const supportsHyperlinks = require('supports-hyperlinks');
var TABLE_CELL_SPLIT = '^*||*^';
var TABLE_ROW_WRAP = '*|*|*|*';
var TABLE_ROW_WRAP_REGEXP = new RegExp(escapeRegExp(TABLE_ROW_WRAP), 'g');
var COLON_REPLACER = '*#COLON|*';
var COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), 'g');
var TAB_ALLOWED_CHARACTERS = ['\t'];
// HARD_RETURN holds a character sequence used to indicate text has a
// hard (no-reflowing) line break. Previously \r and \r\n were turned
// into \n in marked's lexer- preprocessing step. So \r is safe to use
// to indicate a hard (non-reflowed) return.
var HARD_RETURN = '\r',
HARD_RETURN_RE = new RegExp(HARD_RETURN),
HARD_RETURN_GFM_RE = new RegExp(HARD_RETURN + '|<br />');
var defaultOptions = {
code: chalk.yellow,
blockquote: chalk.gray.italic,
html: chalk.gray,
heading: chalk.green.bold,
firstHeading: chalk.magenta.underline.bold,
hr: chalk.reset,
listitem: chalk.reset,
list: list,
table: chalk.reset,
paragraph: chalk.reset,
strong: chalk.bold,
em: chalk.italic,
codespan: chalk.yellow,
del: chalk.dim.gray.strikethrough,
link: chalk.blue,
href: chalk.blue.underline,
text: identity,
unescape: true,
emoji: true,
width: 80,
showSectionPrefix: true,
reflowText: false,
tab: 4,
tableOptions: {}
};
function Renderer(options, highlightOptions) {
this.o = Object.assign({}, defaultOptions, options);
this.tab = sanitizeTab(this.o.tab, defaultOptions.tab);
this.tableSettings = this.o.tableOptions;
this.emoji = this.o.emoji ? insertEmojis : identity;
this.unescape = this.o.unescape ? unescapeEntities : identity;
this.highlightOptions = highlightOptions || {};
this.transform = compose(undoColon, this.unescape, this.emoji);
}
// Compute length of str not including ANSI escape codes.
// See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
function textLength(str) {
return str.replace(/\u001b\[(?:\d{1,3})(?:;\d{1,3})*m/g, '').length;
}
Renderer.prototype.textLength = textLength;
function fixHardReturn(text, reflow) {
return reflow ? text.replace(HARD_RETURN, /\n/g) : text;
}
Renderer.prototype.text = function(text) {
return this.o.text(text);
};
Renderer.prototype.code = function(code, lang, escaped) {
return section(
indentify(this.tab, highlight(code, lang, this.o, this.highlightOptions))
);
};
Renderer.prototype.blockquote = function(quote) {
return section(this.o.blockquote(indentify(this.tab, quote.trim())));
};
Renderer.prototype.html = function(html) {
return this.o.html(html);
};
Renderer.prototype.heading = function(text, level, raw) {
text = this.transform(text);
var prefix = this.o.showSectionPrefix
? new Array(level + 1).join('#') + ' '
: '';
text = prefix + text;
if (this.o.reflowText) {
text = reflowText(text, this.o.width, this.options.gfm);
}
return section(
level === 1 ? this.o.firstHeading(text) : this.o.heading(text)
);
};
Renderer.prototype.hr = function() {
return section(this.o.hr(hr('-', this.o.reflowText && this.o.width)));
};
Renderer.prototype.list = function(body, ordered) {
body = this.o.list(body, ordered, this.tab);
return section(fixNestedLists(indentLines(this.tab, body), this.tab));
};
Renderer.prototype.listitem = function(text) {
var transform = compose(this.o.listitem, this.transform);
var isNested = text.indexOf('\n') !== -1;
if (isNested) text = text.trim();
// Use BULLET_POINT as a marker for ordered or unordered list item
return '\n' + BULLET_POINT + transform(text);
};
Renderer.prototype.checkbox = function(checked) {
return '[' + (checked ? 'X' : ' ') + '] ';
};
Renderer.prototype.paragraph = function(text) {
var transform = compose(this.o.paragraph, this.transform);
text = transform(text);
if (this.o.reflowText) {
text = reflowText(text, this.o.width, this.options.gfm);
}
return section(text);
};
Renderer.prototype.table = function(header, body) {
var table = new Table(
Object.assign(
{},
{
head: generateTableRow(header)[0]
},
this.tableSettings
)
);
generateTableRow(body, this.transform).forEach(function(row) {
table.push(row);
});
return section(this.o.table(table.toString()));
};
Renderer.prototype.tablerow = function(content) {
return TABLE_ROW_WRAP + content + TABLE_ROW_WRAP + '\n';
};
Renderer.prototype.tablecell = function(content, flags) {
return content + TABLE_CELL_SPLIT;
};
// span level renderer
Renderer.prototype.strong = function(text) {
return this.o.strong(text);
};
Renderer.prototype.em = function(text) {
text = fixHardReturn(text, this.o.reflowText);
return this.o.em(text);
};
Renderer.prototype.codespan = function(text) {
text = fixHardReturn(text, this.o.reflowText);
return this.o.codespan(text.replace(/:/g, COLON_REPLACER));
};
Renderer.prototype.br = function() {
return this.o.reflowText ? HARD_RETURN : '\n';
};
Renderer.prototype.del = function(text) {
return this.o.del(text);
};
Renderer.prototype.link = function(href, title, text) {
if (this.options.sanitize) {
try {
var prot = decodeURIComponent(unescape(href))
.replace(/[^\w:]/g, '')
.toLowerCase();
} catch (e) {
return '';
}
if (prot.indexOf('javascript:') === 0) {
return '';
}
}
var hasText = text && text !== href;
var out = '';
if (supportsHyperlinks.stdout) {
let link = '';
if (text) {
link = this.o.href(this.emoji(text));
} else {
link = this.o.href(href);
}
out = ansiEscapes.link(link, href);
} else {
if (hasText) out += this.emoji(text) + ' (';
out += this.o.href(href);
if (hasText) out += ')';
}
return this.o.link(out);
};
Renderer.prototype.image = function(href, title, text) {
if (typeof this.o.image === 'function') {
return this.o.image(href, title, text);
}
var out = '![' + text;
if (title) out += ' ' + title;
return out + '](' + href + ')\n';
};
module.exports = Renderer;
// Munge \n's and spaces in "text" so that the number of
// characters between \n's is less than or equal to "width".
function reflowText(text, width, gfm) {
// Hard break was inserted by Renderer.prototype.br or is
// <br /> when gfm is true
var splitRe = gfm ? HARD_RETURN_GFM_RE : HARD_RETURN_RE,
sections = text.split(splitRe),
reflowed = [];
sections.forEach(function(section) {
// Split the section by escape codes so that we can
// deal with them separately.
var fragments = section.split(/(\u001b\[(?:\d{1,3})(?:;\d{1,3})*m)/g);
var column = 0;
var currentLine = '';
var lastWasEscapeChar = false;
while (fragments.length) {
var fragment = fragments[0];
if (fragment === '') {
fragments.splice(0, 1);
lastWasEscapeChar = false;
continue;
}
// This is an escape code - leave it whole and
// move to the next fragment.
if (!textLength(fragment)) {
currentLine += fragment;
fragments.splice(0, 1);
lastWasEscapeChar = true;
continue;
}
var words = fragment.split(/[ \t\n]+/);
for (var i = 0; i < words.length; i++) {
var word = words[i];
var addSpace = column != 0;
if (lastWasEscapeChar) addSpace = false;
// If adding the new word overflows the required width
if (column + word.length + addSpace > width) {
if (word.length <= width) {
// If the new word is smaller than the required width
// just add it at the beginning of a new line
reflowed.push(currentLine);
currentLine = word;
column = word.length;
} else {
// If the new word is longer than the required width
// split this word into smaller parts.
var w = word.substr(0, width - column - addSpace);
if (addSpace) currentLine += ' ';
currentLine += w;
reflowed.push(currentLine);
currentLine = '';
column = 0;
word = word.substr(w.length);
while (word.length) {
var w = word.substr(0, width);
if (!w.length) break;
if (w.length < width) {
currentLine = w;
column = w.length;
break;
} else {
reflowed.push(w);
word = word.substr(width);
}
}
}
} else {
if (addSpace) {
currentLine += ' ';
column++;
}
currentLine += word;
column += word.length;
}
lastWasEscapeChar = false;
}
fragments.splice(0, 1);
}
if (textLength(currentLine)) reflowed.push(currentLine);
});
return reflowed.join('\n');
}
function indentLines(indent, text) {
return text.replace(/(^|\n)(.+)/g, '$1' + indent + '$2');
}
function indentify(indent, text) {
if (!text) return text;
return indent + text.split('\n').join('\n' + indent);
}
var BULLET_POINT_REGEX = '\\*';
var NUMBERED_POINT_REGEX = '\\d+\\.';
var POINT_REGEX =
'(?:' + [BULLET_POINT_REGEX, NUMBERED_POINT_REGEX].join('|') + ')';
// Prevents nested lists from joining their parent list's last line
function fixNestedLists(body, indent) {
var regex = new RegExp(
'' +
'(\\S(?: | )?)' + // Last char of current point, plus one or two spaces
// to allow trailing spaces
'((?:' +
indent +
')+)' + // Indentation of sub point
'(' +
POINT_REGEX +
'(?:.*)+)$',
'gm'
); // Body of subpoint
return body.replace(regex, '$1\n' + indent + '$2$3');
}
var isPointedLine = function(line, indent) {
return line.match('^(?:' + indent + ')*' + POINT_REGEX);
};
function toSpaces(str) {
return ' '.repeat(str.length);
}
var BULLET_POINT = '* ';
function bulletPointLine(indent, line) {
return isPointedLine(line, indent) ? line : toSpaces(BULLET_POINT) + line;
}
function bulletPointLines(lines, indent) {
var transform = bulletPointLine.bind(null, indent);
return lines
.split('\n')
.filter(identity)
.map(transform)
.join('\n');
}
var numberedPoint = function(n) {
return n + '. ';
};
function numberedLine(indent, line, num) {
return isPointedLine(line, indent)
? {
num: num + 1,
line: line.replace(BULLET_POINT, numberedPoint(num + 1))
}
: {
num: num,
line: toSpaces(numberedPoint(num)) + line
};
}
function numberedLines(lines, indent) {
var transform = numberedLine.bind(null, indent);
let num = 0;
return lines
.split('\n')
.filter(identity)
.map(line => {
const numbered = transform(line, num);
num = numbered.num;
return numbered.line;
})
.join('\n');
}
function list(body, ordered, indent) {
body = body.trim();
body = ordered ? numberedLines(body, indent) : bulletPointLines(body, indent);
return body;
}
function section(text) {
return text + '\n\n';
}
function highlight(code, lang, opts, hightlightOpts) {
if (chalk.level === 0) return code;
var style = opts.code;
code = fixHardReturn(code, opts.reflowText);
if (lang !== 'javascript' && lang !== 'js') {
return style(code);
}
try {
return cardinal.highlight(code, hightlightOpts);
} catch (e) {
return style(code);
}
}
function insertEmojis(text) {
return text.replace(/:([A-Za-z0-9_\-\+]+?):/g, function(emojiString) {
var emojiSign = emoji.get(emojiString);
if (!emojiSign) return emojiString;
return emojiSign + ' ';
});
}
function hr(inputHrStr, length) {
length = length || process.stdout.columns;
return new Array(length).join(inputHrStr);
}
function undoColon(str) {
return str.replace(COLON_REPLACER_REGEXP, ':');
}
function generateTableRow(text, escape) {
if (!text) return [];
escape = escape || identity;
var lines = escape(text).split('\n');
var data = [];
lines.forEach(function(line) {
if (!line) return;
var parsed = line
.replace(TABLE_ROW_WRAP_REGEXP, '')
.split(TABLE_CELL_SPLIT);
data.push(parsed.splice(0, parsed.length - 1));
});
return data;
}
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
}
function unescapeEntities(html) {
return html
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
}
function identity(str) {
return str;
}
function compose() {
var funcs = arguments;
return function() {
var args = arguments;
for (var i = funcs.length; i-- > 0; ) {
args = [funcs[i].apply(this, args)];
}
return args[0];
};
}
function isAllowedTabString(string) {
return TAB_ALLOWED_CHARACTERS.some(function(char) {
return string.match('^(' + char + ')+$');
});
}
function sanitizeTab(tab, fallbackTab) {
if (typeof tab === 'number') {
return new Array(tab + 1).join(' ');
} else if (typeof tab === 'string' && isAllowedTabString(tab)) {
return tab;
} else {
return new Array(fallbackTab + 1).join(' ');
}
}