renderer.mjs 8.7 KB

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