renderer.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. /**
  2. * class Renderer
  3. *
  4. * Generates HTML from parsed token stream. Each instance has independent
  5. * copy of rules. Those can be rewritten with ease. Also, you can add new
  6. * rules if you create plugin and adds new token types.
  7. **/
  8. 'use strict';
  9. var assign = require('./common/utils').assign;
  10. var unescapeAll = require('./common/utils').unescapeAll;
  11. var escapeHtml = require('./common/utils').escapeHtml;
  12. ////////////////////////////////////////////////////////////////////////////////
  13. var default_rules = {};
  14. default_rules.code_inline = function (tokens, idx, options, env, slf) {
  15. var token = tokens[idx];
  16. return '<code' + slf.renderAttrs(token) + '>' +
  17. escapeHtml(tokens[idx].content) +
  18. '</code>';
  19. };
  20. default_rules.code_block = function (tokens, idx, options, env, slf) {
  21. var token = tokens[idx];
  22. return '<pre' + slf.renderAttrs(token) + '><code>' +
  23. escapeHtml(tokens[idx].content) +
  24. '</code></pre>\n';
  25. };
  26. default_rules.fence = function (tokens, idx, options, env, slf) {
  27. var token = tokens[idx],
  28. info = token.info ? unescapeAll(token.info).trim() : '',
  29. langName = '',
  30. langAttrs = '',
  31. highlighted, i, arr, tmpAttrs, tmpToken;
  32. if (info) {
  33. arr = info.split(/(\s+)/g);
  34. langName = arr[0];
  35. langAttrs = arr.slice(2).join('');
  36. }
  37. if (options.highlight) {
  38. highlighted = options.highlight(token.content, langName, langAttrs) || escapeHtml(token.content);
  39. } else {
  40. highlighted = escapeHtml(token.content);
  41. }
  42. if (highlighted.indexOf('<pre') === 0) {
  43. return highlighted + '\n';
  44. }
  45. // If language exists, inject class gently, without modifying original token.
  46. // May be, one day we will add .deepClone() for token and simplify this part, but
  47. // now we prefer to keep things local.
  48. if (info) {
  49. i = token.attrIndex('class');
  50. tmpAttrs = token.attrs ? token.attrs.slice() : [];
  51. if (i < 0) {
  52. tmpAttrs.push([ 'class', options.langPrefix + langName ]);
  53. } else {
  54. tmpAttrs[i] = tmpAttrs[i].slice();
  55. tmpAttrs[i][1] += ' ' + options.langPrefix + langName;
  56. }
  57. // Fake token just to render attributes
  58. tmpToken = {
  59. attrs: tmpAttrs
  60. };
  61. return '<pre><code' + slf.renderAttrs(tmpToken) + '>'
  62. + highlighted
  63. + '</code></pre>\n';
  64. }
  65. return '<pre><code' + slf.renderAttrs(token) + '>'
  66. + highlighted
  67. + '</code></pre>\n';
  68. };
  69. default_rules.image = function (tokens, idx, options, env, slf) {
  70. var token = tokens[idx];
  71. // "alt" attr MUST be set, even if empty. Because it's mandatory and
  72. // should be placed on proper position for tests.
  73. //
  74. // Replace content with actual value
  75. token.attrs[token.attrIndex('alt')][1] =
  76. slf.renderInlineAsText(token.children, options, env);
  77. return slf.renderToken(tokens, idx, options);
  78. };
  79. default_rules.hardbreak = function (tokens, idx, options /*, env */) {
  80. return options.xhtmlOut ? '<br />\n' : '<br>\n';
  81. };
  82. default_rules.softbreak = function (tokens, idx, options /*, env */) {
  83. return options.breaks ? (options.xhtmlOut ? '<br />\n' : '<br>\n') : '\n';
  84. };
  85. default_rules.text = function (tokens, idx /*, options, env */) {
  86. return escapeHtml(tokens[idx].content);
  87. };
  88. default_rules.html_block = function (tokens, idx /*, options, env */) {
  89. return tokens[idx].content;
  90. };
  91. default_rules.html_inline = function (tokens, idx /*, options, env */) {
  92. return tokens[idx].content;
  93. };
  94. /**
  95. * new Renderer()
  96. *
  97. * Creates new [[Renderer]] instance and fill [[Renderer#rules]] with defaults.
  98. **/
  99. function Renderer() {
  100. /**
  101. * Renderer#rules -> Object
  102. *
  103. * Contains render rules for tokens. Can be updated and extended.
  104. *
  105. * ##### Example
  106. *
  107. * ```javascript
  108. * var md = require('markdown-it')();
  109. *
  110. * md.renderer.rules.strong_open = function () { return '<b>'; };
  111. * md.renderer.rules.strong_close = function () { return '</b>'; };
  112. *
  113. * var result = md.renderInline(...);
  114. * ```
  115. *
  116. * Each rule is called as independent static function with fixed signature:
  117. *
  118. * ```javascript
  119. * function my_token_render(tokens, idx, options, env, renderer) {
  120. * // ...
  121. * return renderedHTML;
  122. * }
  123. * ```
  124. *
  125. * See [source code](https://github.com/markdown-it/markdown-it/blob/master/lib/renderer.js)
  126. * for more details and examples.
  127. **/
  128. this.rules = assign({}, default_rules);
  129. }
  130. /**
  131. * Renderer.renderAttrs(token) -> String
  132. *
  133. * Render token attributes to string.
  134. **/
  135. Renderer.prototype.renderAttrs = function renderAttrs(token) {
  136. var i, l, result;
  137. if (!token.attrs) { return ''; }
  138. result = '';
  139. for (i = 0, l = token.attrs.length; i < l; i++) {
  140. result += ' ' + escapeHtml(token.attrs[i][0]) + '="' + escapeHtml(token.attrs[i][1]) + '"';
  141. }
  142. return result;
  143. };
  144. /**
  145. * Renderer.renderToken(tokens, idx, options) -> String
  146. * - tokens (Array): list of tokens
  147. * - idx (Numbed): token index to render
  148. * - options (Object): params of parser instance
  149. *
  150. * Default token renderer. Can be overriden by custom function
  151. * in [[Renderer#rules]].
  152. **/
  153. Renderer.prototype.renderToken = function renderToken(tokens, idx, options) {
  154. var nextToken,
  155. result = '',
  156. needLf = false,
  157. token = tokens[idx];
  158. // Tight list paragraphs
  159. if (token.hidden) {
  160. return '';
  161. }
  162. // Insert a newline between hidden paragraph and subsequent opening
  163. // block-level tag.
  164. //
  165. // For example, here we should insert a newline before blockquote:
  166. // - a
  167. // >
  168. //
  169. if (token.block && token.nesting !== -1 && idx && tokens[idx - 1].hidden) {
  170. result += '\n';
  171. }
  172. // Add token name, e.g. `<img`
  173. result += (token.nesting === -1 ? '</' : '<') + token.tag;
  174. // Encode attributes, e.g. `<img src="foo"`
  175. result += this.renderAttrs(token);
  176. // Add a slash for self-closing tags, e.g. `<img src="foo" /`
  177. if (token.nesting === 0 && options.xhtmlOut) {
  178. result += ' /';
  179. }
  180. // Check if we need to add a newline after this tag
  181. if (token.block) {
  182. needLf = true;
  183. if (token.nesting === 1) {
  184. if (idx + 1 < tokens.length) {
  185. nextToken = tokens[idx + 1];
  186. if (nextToken.type === 'inline' || nextToken.hidden) {
  187. // Block-level tag containing an inline tag.
  188. //
  189. needLf = false;
  190. } else if (nextToken.nesting === -1 && nextToken.tag === token.tag) {
  191. // Opening tag + closing tag of the same type. E.g. `<li></li>`.
  192. //
  193. needLf = false;
  194. }
  195. }
  196. }
  197. }
  198. result += needLf ? '>\n' : '>';
  199. return result;
  200. };
  201. /**
  202. * Renderer.renderInline(tokens, options, env) -> String
  203. * - tokens (Array): list on block tokens to render
  204. * - options (Object): params of parser instance
  205. * - env (Object): additional data from parsed input (references, for example)
  206. *
  207. * The same as [[Renderer.render]], but for single token of `inline` type.
  208. **/
  209. Renderer.prototype.renderInline = function (tokens, options, env) {
  210. var type,
  211. result = '',
  212. rules = this.rules;
  213. for (var i = 0, len = tokens.length; i < len; i++) {
  214. type = tokens[i].type;
  215. if (typeof rules[type] !== 'undefined') {
  216. result += rules[type](tokens, i, options, env, this);
  217. } else {
  218. result += this.renderToken(tokens, i, options);
  219. }
  220. }
  221. return result;
  222. };
  223. /** internal
  224. * Renderer.renderInlineAsText(tokens, options, env) -> String
  225. * - tokens (Array): list on block tokens to render
  226. * - options (Object): params of parser instance
  227. * - env (Object): additional data from parsed input (references, for example)
  228. *
  229. * Special kludge for image `alt` attributes to conform CommonMark spec.
  230. * Don't try to use it! Spec requires to show `alt` content with stripped markup,
  231. * instead of simple escaping.
  232. **/
  233. Renderer.prototype.renderInlineAsText = function (tokens, options, env) {
  234. var result = '';
  235. for (var i = 0, len = tokens.length; i < len; i++) {
  236. if (tokens[i].type === 'text') {
  237. result += tokens[i].content;
  238. } else if (tokens[i].type === 'image') {
  239. result += this.renderInlineAsText(tokens[i].children, options, env);
  240. } else if (tokens[i].type === 'softbreak') {
  241. result += '\n';
  242. }
  243. }
  244. return result;
  245. };
  246. /**
  247. * Renderer.render(tokens, options, env) -> String
  248. * - tokens (Array): list on block tokens to render
  249. * - options (Object): params of parser instance
  250. * - env (Object): additional data from parsed input (references, for example)
  251. *
  252. * Takes token stream and generates HTML. Probably, you will never need to call
  253. * this method directly.
  254. **/
  255. Renderer.prototype.render = function (tokens, options, env) {
  256. var i, len, type,
  257. result = '',
  258. rules = this.rules;
  259. for (i = 0, len = tokens.length; i < len; i++) {
  260. type = tokens[i].type;
  261. if (type === 'inline') {
  262. result += this.renderInline(tokens[i].children, options, env);
  263. } else if (typeof rules[type] !== 'undefined') {
  264. result += rules[tokens[i].type](tokens, i, options, env, this);
  265. } else {
  266. result += this.renderToken(tokens, i, options, env);
  267. }
  268. }
  269. return result;
  270. };
  271. module.exports = Renderer;