No Description
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.

index.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. 'use strict';
  2. import chalk from 'chalk';
  3. import Table from 'cli-table3';
  4. import cardinal from 'cardinal';
  5. import emoji from 'node-emoji';
  6. import ansiEscapes from 'ansi-escapes';
  7. import supportsHyperlinks from 'supports-hyperlinks';
  8. var TABLE_CELL_SPLIT = '^*||*^';
  9. var TABLE_ROW_WRAP = '*|*|*|*';
  10. var TABLE_ROW_WRAP_REGEXP = new RegExp(escapeRegExp(TABLE_ROW_WRAP), 'g');
  11. var COLON_REPLACER = '*#COLON|*';
  12. var COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), 'g');
  13. var TAB_ALLOWED_CHARACTERS = ['\t'];
  14. // HARD_RETURN holds a character sequence used to indicate text has a
  15. // hard (no-reflowing) line break. Previously \r and \r\n were turned
  16. // into \n in marked's lexer- preprocessing step. So \r is safe to use
  17. // to indicate a hard (non-reflowed) return.
  18. var HARD_RETURN = '\r',
  19. HARD_RETURN_RE = new RegExp(HARD_RETURN),
  20. HARD_RETURN_GFM_RE = new RegExp(HARD_RETURN + '|<br />');
  21. var defaultOptions = {
  22. code: chalk.yellow,
  23. blockquote: chalk.gray.italic,
  24. html: chalk.gray,
  25. heading: chalk.green.bold,
  26. firstHeading: chalk.magenta.underline.bold,
  27. hr: chalk.reset,
  28. listitem: chalk.reset,
  29. list: list,
  30. table: chalk.reset,
  31. paragraph: chalk.reset,
  32. strong: chalk.bold,
  33. em: chalk.italic,
  34. codespan: chalk.yellow,
  35. del: chalk.dim.gray.strikethrough,
  36. link: chalk.blue,
  37. href: chalk.blue.underline,
  38. text: identity,
  39. unescape: true,
  40. emoji: true,
  41. width: 80,
  42. showSectionPrefix: true,
  43. reflowText: false,
  44. tab: 4,
  45. tableOptions: {}
  46. };
  47. function Renderer(options, highlightOptions) {
  48. this.o = Object.assign({}, defaultOptions, options);
  49. this.tab = sanitizeTab(this.o.tab, defaultOptions.tab);
  50. this.tableSettings = this.o.tableOptions;
  51. this.emoji = this.o.emoji ? insertEmojis : identity;
  52. this.unescape = this.o.unescape ? unescapeEntities : identity;
  53. this.highlightOptions = highlightOptions || {};
  54. this.transform = compose(undoColon, this.unescape, this.emoji);
  55. }
  56. // Compute length of str not including ANSI escape codes.
  57. // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
  58. function textLength(str) {
  59. return str.replace(/\u001b\[(?:\d{1,3})(?:;\d{1,3})*m/g, '').length;
  60. }
  61. Renderer.prototype.textLength = textLength;
  62. function fixHardReturn(text, reflow) {
  63. return reflow ? text.replace(HARD_RETURN, /\n/g) : text;
  64. }
  65. Renderer.prototype.text = function (text) {
  66. return this.o.text(text);
  67. };
  68. Renderer.prototype.code = function (code, lang, escaped) {
  69. return section(
  70. indentify(this.tab, highlight(code, lang, this.o, this.highlightOptions))
  71. );
  72. };
  73. Renderer.prototype.blockquote = function (quote) {
  74. return section(this.o.blockquote(indentify(this.tab, quote.trim())));
  75. };
  76. Renderer.prototype.html = function (html) {
  77. return this.o.html(html);
  78. };
  79. Renderer.prototype.heading = function (text, level, raw) {
  80. text = this.transform(text);
  81. var prefix = this.o.showSectionPrefix
  82. ? new Array(level + 1).join('#') + ' '
  83. : '';
  84. text = prefix + text;
  85. if (this.o.reflowText) {
  86. text = reflowText(text, this.o.width, this.options.gfm);
  87. }
  88. return section(
  89. level === 1 ? this.o.firstHeading(text) : this.o.heading(text)
  90. );
  91. };
  92. Renderer.prototype.hr = function () {
  93. return section(this.o.hr(hr('-', this.o.reflowText && this.o.width)));
  94. };
  95. Renderer.prototype.list = function (body, ordered) {
  96. body = this.o.list(body, ordered, this.tab);
  97. return section(fixNestedLists(indentLines(this.tab, body), this.tab));
  98. };
  99. Renderer.prototype.listitem = function (text) {
  100. var transform = compose(this.o.listitem, this.transform);
  101. var isNested = text.indexOf('\n') !== -1;
  102. if (isNested) text = text.trim();
  103. // Use BULLET_POINT as a marker for ordered or unordered list item
  104. return '\n' + BULLET_POINT + transform(text);
  105. };
  106. Renderer.prototype.checkbox = function (checked) {
  107. return '[' + (checked ? 'X' : ' ') + '] ';
  108. };
  109. Renderer.prototype.paragraph = function (text) {
  110. var transform = compose(this.o.paragraph, this.transform);
  111. text = transform(text);
  112. if (this.o.reflowText) {
  113. text = reflowText(text, this.o.width, this.options.gfm);
  114. }
  115. return section(text);
  116. };
  117. Renderer.prototype.table = function (header, body) {
  118. var table = new Table(
  119. Object.assign(
  120. {},
  121. {
  122. head: generateTableRow(header)[0]
  123. },
  124. this.tableSettings
  125. )
  126. );
  127. generateTableRow(body, this.transform).forEach(function (row) {
  128. table.push(row);
  129. });
  130. return section(this.o.table(table.toString()));
  131. };
  132. Renderer.prototype.tablerow = function (content) {
  133. return TABLE_ROW_WRAP + content + TABLE_ROW_WRAP + '\n';
  134. };
  135. Renderer.prototype.tablecell = function (content, flags) {
  136. return content + TABLE_CELL_SPLIT;
  137. };
  138. // span level renderer
  139. Renderer.prototype.strong = function (text) {
  140. return this.o.strong(text);
  141. };
  142. Renderer.prototype.em = function (text) {
  143. text = fixHardReturn(text, this.o.reflowText);
  144. return this.o.em(text);
  145. };
  146. Renderer.prototype.codespan = function (text) {
  147. text = fixHardReturn(text, this.o.reflowText);
  148. return this.o.codespan(text.replace(/:/g, COLON_REPLACER));
  149. };
  150. Renderer.prototype.br = function () {
  151. return this.o.reflowText ? HARD_RETURN : '\n';
  152. };
  153. Renderer.prototype.del = function (text) {
  154. return this.o.del(text);
  155. };
  156. Renderer.prototype.link = function (href, title, text) {
  157. if (this.options.sanitize) {
  158. try {
  159. var prot = decodeURIComponent(unescape(href))
  160. .replace(/[^\w:]/g, '')
  161. .toLowerCase();
  162. } catch (e) {
  163. return '';
  164. }
  165. if (prot.indexOf('javascript:') === 0) {
  166. return '';
  167. }
  168. }
  169. var hasText = text && text !== href;
  170. var out = '';
  171. if (supportsHyperlinks.stdout) {
  172. let link = '';
  173. if (text) {
  174. link = this.o.href(this.emoji(text));
  175. } else {
  176. link = this.o.href(href);
  177. }
  178. out = ansiEscapes.link(link, href);
  179. } else {
  180. if (hasText) out += this.emoji(text) + ' (';
  181. out += this.o.href(href);
  182. if (hasText) out += ')';
  183. }
  184. return this.o.link(out);
  185. };
  186. Renderer.prototype.image = function (href, title, text) {
  187. if (typeof this.o.image === 'function') {
  188. return this.o.image(href, title, text);
  189. }
  190. var out = '![' + text;
  191. if (title) out += ' – ' + title;
  192. return out + '](' + href + ')\n';
  193. };
  194. export default Renderer;
  195. // Munge \n's and spaces in "text" so that the number of
  196. // characters between \n's is less than or equal to "width".
  197. function reflowText(text, width, gfm) {
  198. // Hard break was inserted by Renderer.prototype.br or is
  199. // <br /> when gfm is true
  200. var splitRe = gfm ? HARD_RETURN_GFM_RE : HARD_RETURN_RE,
  201. sections = text.split(splitRe),
  202. reflowed = [];
  203. sections.forEach(function (section) {
  204. // Split the section by escape codes so that we can
  205. // deal with them separately.
  206. var fragments = section.split(/(\u001b\[(?:\d{1,3})(?:;\d{1,3})*m)/g);
  207. var column = 0;
  208. var currentLine = '';
  209. var lastWasEscapeChar = false;
  210. while (fragments.length) {
  211. var fragment = fragments[0];
  212. if (fragment === '') {
  213. fragments.splice(0, 1);
  214. lastWasEscapeChar = false;
  215. continue;
  216. }
  217. // This is an escape code - leave it whole and
  218. // move to the next fragment.
  219. if (!textLength(fragment)) {
  220. currentLine += fragment;
  221. fragments.splice(0, 1);
  222. lastWasEscapeChar = true;
  223. continue;
  224. }
  225. var words = fragment.split(/[ \t\n]+/);
  226. for (var i = 0; i < words.length; i++) {
  227. var word = words[i];
  228. var addSpace = column != 0;
  229. if (lastWasEscapeChar) addSpace = false;
  230. // If adding the new word overflows the required width
  231. if (column + word.length + addSpace > width) {
  232. if (word.length <= width) {
  233. // If the new word is smaller than the required width
  234. // just add it at the beginning of a new line
  235. reflowed.push(currentLine);
  236. currentLine = word;
  237. column = word.length;
  238. } else {
  239. // If the new word is longer than the required width
  240. // split this word into smaller parts.
  241. var w = word.substr(0, width - column - addSpace);
  242. if (addSpace) currentLine += ' ';
  243. currentLine += w;
  244. reflowed.push(currentLine);
  245. currentLine = '';
  246. column = 0;
  247. word = word.substr(w.length);
  248. while (word.length) {
  249. var w = word.substr(0, width);
  250. if (!w.length) break;
  251. if (w.length < width) {
  252. currentLine = w;
  253. column = w.length;
  254. break;
  255. } else {
  256. reflowed.push(w);
  257. word = word.substr(width);
  258. }
  259. }
  260. }
  261. } else {
  262. if (addSpace) {
  263. currentLine += ' ';
  264. column++;
  265. }
  266. currentLine += word;
  267. column += word.length;
  268. }
  269. lastWasEscapeChar = false;
  270. }
  271. fragments.splice(0, 1);
  272. }
  273. if (textLength(currentLine)) reflowed.push(currentLine);
  274. });
  275. return reflowed.join('\n');
  276. }
  277. function indentLines(indent, text) {
  278. return text.replace(/(^|\n)(.+)/g, '$1' + indent + '$2');
  279. }
  280. function indentify(indent, text) {
  281. if (!text) return text;
  282. return indent + text.split('\n').join('\n' + indent);
  283. }
  284. var BULLET_POINT_REGEX = '\\*';
  285. var NUMBERED_POINT_REGEX = '\\d+\\.';
  286. var POINT_REGEX =
  287. '(?:' + [BULLET_POINT_REGEX, NUMBERED_POINT_REGEX].join('|') + ')';
  288. // Prevents nested lists from joining their parent list's last line
  289. function fixNestedLists(body, indent) {
  290. var regex = new RegExp(
  291. '' +
  292. '(\\S(?: | )?)' + // Last char of current point, plus one or two spaces
  293. // to allow trailing spaces
  294. '((?:' +
  295. indent +
  296. ')+)' + // Indentation of sub point
  297. '(' +
  298. POINT_REGEX +
  299. '(?:.*)+)$',
  300. 'gm'
  301. ); // Body of subpoint
  302. return body.replace(regex, '$1\n' + indent + '$2$3');
  303. }
  304. var isPointedLine = function (line, indent) {
  305. return line.match('^(?:' + indent + ')*' + POINT_REGEX);
  306. };
  307. function toSpaces(str) {
  308. return ' '.repeat(str.length);
  309. }
  310. var BULLET_POINT = '* ';
  311. function bulletPointLine(indent, line) {
  312. return isPointedLine(line, indent) ? line : toSpaces(BULLET_POINT) + line;
  313. }
  314. function bulletPointLines(lines, indent) {
  315. var transform = bulletPointLine.bind(null, indent);
  316. return lines.split('\n').filter(identity).map(transform).join('\n');
  317. }
  318. var numberedPoint = function (n) {
  319. return n + '. ';
  320. };
  321. function numberedLine(indent, line, num) {
  322. return isPointedLine(line, indent)
  323. ? {
  324. num: num + 1,
  325. line: line.replace(BULLET_POINT, numberedPoint(num + 1))
  326. }
  327. : {
  328. num: num,
  329. line: toSpaces(numberedPoint(num)) + line
  330. };
  331. }
  332. function numberedLines(lines, indent) {
  333. var transform = numberedLine.bind(null, indent);
  334. let num = 0;
  335. return lines
  336. .split('\n')
  337. .filter(identity)
  338. .map((line) => {
  339. const numbered = transform(line, num);
  340. num = numbered.num;
  341. return numbered.line;
  342. })
  343. .join('\n');
  344. }
  345. function list(body, ordered, indent) {
  346. body = body.trim();
  347. body = ordered ? numberedLines(body, indent) : bulletPointLines(body, indent);
  348. return body;
  349. }
  350. function section(text) {
  351. return text + '\n\n';
  352. }
  353. function highlight(code, lang, opts, hightlightOpts) {
  354. if (chalk.level === 0) return code;
  355. var style = opts.code;
  356. code = fixHardReturn(code, opts.reflowText);
  357. if (lang !== 'javascript' && lang !== 'js') {
  358. return style(code);
  359. }
  360. try {
  361. return cardinal.highlight(code, hightlightOpts);
  362. } catch (e) {
  363. return style(code);
  364. }
  365. }
  366. function insertEmojis(text) {
  367. return text.replace(/:([A-Za-z0-9_\-\+]+?):/g, function (emojiString) {
  368. var emojiSign = emoji.get(emojiString);
  369. if (!emojiSign) return emojiString;
  370. return emojiSign + ' ';
  371. });
  372. }
  373. function hr(inputHrStr, length) {
  374. length = length || process.stdout.columns;
  375. return new Array(length).join(inputHrStr);
  376. }
  377. function undoColon(str) {
  378. return str.replace(COLON_REPLACER_REGEXP, ':');
  379. }
  380. function generateTableRow(text, escape) {
  381. if (!text) return [];
  382. escape = escape || identity;
  383. var lines = escape(text).split('\n');
  384. var data = [];
  385. lines.forEach(function (line) {
  386. if (!line) return;
  387. var parsed = line
  388. .replace(TABLE_ROW_WRAP_REGEXP, '')
  389. .split(TABLE_CELL_SPLIT);
  390. data.push(parsed.splice(0, parsed.length - 1));
  391. });
  392. return data;
  393. }
  394. function escapeRegExp(str) {
  395. return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  396. }
  397. function unescapeEntities(html) {
  398. return html
  399. .replace(/&amp;/g, '&')
  400. .replace(/&lt;/g, '<')
  401. .replace(/&gt;/g, '>')
  402. .replace(/&quot;/g, '"')
  403. .replace(/&#39;/g, "'");
  404. }
  405. function identity(str) {
  406. return str;
  407. }
  408. function compose() {
  409. var funcs = arguments;
  410. return function () {
  411. var args = arguments;
  412. for (var i = funcs.length; i-- > 0; ) {
  413. args = [funcs[i].apply(this, args)];
  414. }
  415. return args[0];
  416. };
  417. }
  418. function isAllowedTabString(string) {
  419. return TAB_ALLOWED_CHARACTERS.some(function (char) {
  420. return string.match('^(' + char + ')+$');
  421. });
  422. }
  423. function sanitizeTab(tab, fallbackTab) {
  424. if (typeof tab === 'number') {
  425. return new Array(tab + 1).join(' ');
  426. } else if (typeof tab === 'string' && isAllowedTabString(tab)) {
  427. return tab;
  428. } else {
  429. return new Array(fallbackTab + 1).join(' ');
  430. }
  431. }