index.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. "use strict";
  2. var chalk = require('chalk');
  3. var Table = require('cli-table');
  4. var assign = require('lodash.assign');
  5. var cardinal = require('cardinal');
  6. var emoji = require('node-emoji');
  7. var TABLE_CELL_SPLIT = '^*||*^';
  8. var TABLE_ROW_WRAP = '*|*|*|*';
  9. var TABLE_ROW_WRAP_REGEXP = new RegExp(escapeRegExp(TABLE_ROW_WRAP), 'g');
  10. var COLON_REPLACER = '*#COLON|*';
  11. var COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), 'g');
  12. var TAB_ALLOWED_CHARACTERS = ['\t'];
  13. // HARD_RETURN holds a character sequence used to indicate text has a
  14. // hard (no-reflowing) line break. Previously \r and \r\n were turned
  15. // into \n in marked's lexer- preprocessing step. So \r is safe to use
  16. // to indicate a hard (non-reflowed) return.
  17. var HARD_RETURN = '\r',
  18. HARD_RETURN_RE = new RegExp(HARD_RETURN),
  19. HARD_RETURN_GFM_RE = new RegExp(HARD_RETURN + '|<br />');
  20. var defaultOptions = {
  21. code: chalk.yellow,
  22. blockquote: chalk.gray.italic,
  23. html: chalk.gray,
  24. heading: chalk.green.bold,
  25. firstHeading: chalk.magenta.underline.bold,
  26. hr: chalk.reset,
  27. listitem: chalk.reset,
  28. table: chalk.reset,
  29. paragraph: chalk.reset,
  30. strong: chalk.bold,
  31. em: chalk.italic,
  32. codespan: chalk.yellow,
  33. del: chalk.dim.gray.strikethrough,
  34. link: chalk.blue,
  35. href: chalk.blue.underline,
  36. text: identity,
  37. unescape: true,
  38. emoji: true,
  39. width: 80,
  40. showSectionPrefix: true,
  41. reflowText: false,
  42. tab: 3,
  43. tableOptions: {}
  44. };
  45. function Renderer(options, highlightOptions) {
  46. this.o = assign({}, defaultOptions, options);
  47. this.tab = sanitizeTab(this.o.tab, defaultOptions.tab);
  48. this.tableSettings = this.o.tableOptions;
  49. this.emoji = this.o.emoji ? insertEmojis : identity;
  50. this.unescape = this.o.unescape ? unescapeEntities : identity;
  51. this.highlightOptions = highlightOptions || {};
  52. this.transform = compose(undoColon, this.unescape, this.emoji);
  53. };
  54. // Compute length of str not including ANSI escape codes.
  55. // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
  56. function textLength(str) {
  57. return str.replace(/\u001b\[(?:\d{1,3})(?:;\d{1,3})*m/g, "").length;
  58. };
  59. Renderer.prototype.textLength = textLength;
  60. function fixHardReturn(text, reflow) {
  61. return reflow ? text.replace(HARD_RETURN, /\n/g) : text;
  62. }
  63. Renderer.prototype.text = function (text) {
  64. return this.o.text(text);
  65. };
  66. Renderer.prototype.code = function(code, lang, escaped) {
  67. return '\n' + indentify(highlight(code, lang, this.o, this.highlightOptions), this.tab) + '\n\n';
  68. };
  69. Renderer.prototype.blockquote = function(quote) {
  70. return '\n' + this.o.blockquote(indentify(quote.trim(), this.tab)) + '\n\n';
  71. };
  72. Renderer.prototype.html = function(html) {
  73. return this.o.html(html);
  74. };
  75. Renderer.prototype.heading = function(text, level, raw) {
  76. text = this.transform(text);
  77. var prefix = this.o.showSectionPrefix ?
  78. (new Array(level + 1)).join('#')+' ' : '';
  79. text = prefix + text;
  80. if (this.o.reflowText) {
  81. text = reflowText(text, this.o.width, this.options.gfm);
  82. }
  83. if (level === 1) {
  84. return this.o.firstHeading(text) + '\n';
  85. }
  86. return this.o.heading(text) + '\n';
  87. };
  88. Renderer.prototype.hr = function() {
  89. return this.o.hr(hr('-', this.o.reflowText && this.o.width)) + '\n';
  90. };
  91. Renderer.prototype.list = function(body, ordered) {
  92. body = indentLines(this.o.listitem(body), this.tab);
  93. if (!ordered) return body;
  94. return changeToOrdered(body);
  95. };
  96. Renderer.prototype.listitem = function(text) {
  97. var isNested = ~text.indexOf('\n');
  98. if (isNested) text = text.trim();
  99. return '\n * ' + this.transform(text);
  100. };
  101. Renderer.prototype.paragraph = function(text) {
  102. var transform = compose(this.o.paragraph, this.transform);
  103. text = transform(text);
  104. if (this.o.reflowText) {
  105. text = reflowText(text, this.o.width, this.options.gfm);
  106. }
  107. return text + '\n\n';
  108. };
  109. Renderer.prototype.table = function(header, body) {
  110. var table = new Table(assign({}, {
  111. head: generateTableRow(header)[0]
  112. }, this.tableSettings));
  113. generateTableRow(body, this.transform).forEach(function (row) {
  114. table.push(row);
  115. });
  116. return this.o.table(table.toString()) + '\n\n';
  117. };
  118. Renderer.prototype.tablerow = function(content) {
  119. return TABLE_ROW_WRAP + content + TABLE_ROW_WRAP + '\n';
  120. };
  121. Renderer.prototype.tablecell = function(content, flags) {
  122. return content + TABLE_CELL_SPLIT;
  123. };
  124. // span level renderer
  125. Renderer.prototype.strong = function(text) {
  126. return this.o.strong(text);
  127. };
  128. Renderer.prototype.em = function(text) {
  129. text = fixHardReturn(text, this.o.reflowText);
  130. return this.o.em(text);
  131. };
  132. Renderer.prototype.codespan = function(text) {
  133. text = fixHardReturn(text, this.o.reflowText);
  134. return this.o.codespan(text.replace(/:/g, COLON_REPLACER));
  135. };
  136. Renderer.prototype.br = function() {
  137. return this.o.reflowText ? HARD_RETURN : '\n';
  138. };
  139. Renderer.prototype.del = function(text) {
  140. return this.o.del(text);
  141. };
  142. Renderer.prototype.link = function(href, title, text) {
  143. if (this.options.sanitize) {
  144. try {
  145. var prot = decodeURIComponent(unescape(href))
  146. .replace(/[^\w:]/g, '')
  147. .toLowerCase();
  148. } catch (e) {
  149. return '';
  150. }
  151. if (prot.indexOf('javascript:') === 0) {
  152. return '';
  153. }
  154. }
  155. var hasText = text && text !== href;
  156. var out = '';
  157. if (hasText) out += this.emoji(text) + ' (';
  158. out += this.o.href(href);
  159. if (hasText) out += ')';
  160. return this.o.link(out);
  161. };
  162. Renderer.prototype.image = function(href, title, text) {
  163. var out = '!['+text;
  164. if (title) out += ' – ' + title;
  165. return out + '](' + href + ')\n';
  166. };
  167. module.exports = Renderer;
  168. // Munge \n's and spaces in "text" so that the number of
  169. // characters between \n's is less than or equal to "width".
  170. function reflowText (text, width, gfm) {
  171. // Hard break was inserted by Renderer.prototype.br or is
  172. // <br /> when gfm is true
  173. var splitRe = gfm ? HARD_RETURN_GFM_RE : HARD_RETURN_RE,
  174. sections = text.split(splitRe),
  175. reflowed = [];
  176. sections.forEach(function (section) {
  177. var words = section.split(/[ \t\n]+/),
  178. column = 0,
  179. nextText = '';
  180. words.forEach(function (word) {
  181. var addOne = column != 0;
  182. if ((column + textLength(word) + addOne) > width) {
  183. nextText += '\n';
  184. column = 0;
  185. } else if (addOne) {
  186. nextText += " ";
  187. column += 1;
  188. }
  189. nextText += word;
  190. column += textLength(word);
  191. });
  192. reflowed.push(nextText);
  193. });
  194. return reflowed.join('\n');
  195. }
  196. function indentLines (text, tab) {
  197. return text.replace(/\n/g, '\n' + tab) + '\n\n';
  198. }
  199. function changeToOrdered(text) {
  200. var i = 1;
  201. return text.split('\n').reduce(function (acc, line) {
  202. if (!line) return '\n' + acc;
  203. return acc + line.replace(/(\s*)\*/, '$1' + (i++) + '.') + '\n';
  204. });
  205. }
  206. function highlight(code, lang, opts, hightlightOpts) {
  207. if (!chalk.enabled) return code;
  208. var style = opts.code;
  209. code = fixHardReturn(code, opts.reflowText);
  210. if (lang !== 'javascript' && lang !== 'js') {
  211. return style(code);
  212. }
  213. try {
  214. return cardinal.highlight(code, hightlightOpts);
  215. } catch (e) {
  216. return style(code);
  217. }
  218. }
  219. function insertEmojis(text) {
  220. return text.replace(/:([A-Za-z0-9_\-\+]+?):/g, function (emojiString) {
  221. var emojiSign = emoji.get(emojiString);
  222. if (!emojiSign) return emojiString;
  223. return emojiSign + ' ';
  224. });
  225. }
  226. function hr(inputHrStr, length) {
  227. length = length || process.stdout.columns;
  228. return (new Array(length)).join(inputHrStr);
  229. }
  230. function undoColon (str) {
  231. return str.replace(COLON_REPLACER_REGEXP, ':');
  232. }
  233. function indentify(text, tab) {
  234. if (!text) return text;
  235. return tab + text.split('\n').join('\n' + tab);
  236. }
  237. function generateTableRow(text, escape) {
  238. if (!text) return [];
  239. escape = escape || identity;
  240. var lines = escape(text).split('\n');
  241. var data = [];
  242. lines.forEach(function (line) {
  243. if (!line) return;
  244. var parsed = line.replace(TABLE_ROW_WRAP_REGEXP, '').split(TABLE_CELL_SPLIT);
  245. data.push(parsed.splice(0, parsed.length - 1));
  246. });
  247. return data;
  248. }
  249. function escapeRegExp(str) {
  250. return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
  251. }
  252. function unescapeEntities(html) {
  253. return html
  254. .replace(/&amp;/g, '&')
  255. .replace(/&lt;/g, '<')
  256. .replace(/&gt;/g, '>')
  257. .replace(/&quot;/g, '"')
  258. .replace(/&#39;/g, "'");
  259. }
  260. function identity (str) {
  261. return str;
  262. }
  263. function compose () {
  264. var funcs = arguments;
  265. return function() {
  266. var args = arguments;
  267. for (var i = funcs.length; i-- > 0;) {
  268. args = [funcs[i].apply(this, args)];
  269. }
  270. return args[0];
  271. };
  272. }
  273. function isAllowedTabString (string) {
  274. return TAB_ALLOWED_CHARACTERS.some(function (char) {
  275. return string.match('^('+char+')+$');
  276. });
  277. }
  278. function sanitizeTab (tab, fallbackTab) {
  279. if (typeof tab === 'number') {
  280. return (new Array(tab + 1)).join(' ');
  281. } else if (typeof tab === 'string' && isAllowedTabString(tab)) {
  282. return tab;
  283. } else {
  284. return (new Array(fallbackTab + 1)).join(' ');
  285. }
  286. }