Mirror of PeerTube
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.

markdown.service.ts 3.2KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { Injectable } from '@angular/core'
  2. import { MarkdownIt } from 'markdown-it'
  3. @Injectable()
  4. export class MarkdownService {
  5. static TEXT_RULES = [
  6. 'linkify',
  7. 'autolink',
  8. 'emphasis',
  9. 'link',
  10. 'newline',
  11. 'list'
  12. ]
  13. static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
  14. static COMPLETE_RULES = MarkdownService.ENHANCED_RULES.concat([ 'block', 'inline', 'heading', 'html_inline', 'html_block', 'paragraph' ])
  15. private textMarkdownIt: MarkdownIt
  16. private enhancedMarkdownIt: MarkdownIt
  17. private completeMarkdownIt: MarkdownIt
  18. async textMarkdownToHTML (markdown: string) {
  19. if (!markdown) return ''
  20. if (!this.textMarkdownIt) {
  21. this.textMarkdownIt = await this.createMarkdownIt(MarkdownService.TEXT_RULES)
  22. }
  23. const html = this.textMarkdownIt.render(markdown)
  24. return this.avoidTruncatedTags(html)
  25. }
  26. async enhancedMarkdownToHTML (markdown: string) {
  27. if (!markdown) return ''
  28. if (!this.enhancedMarkdownIt) {
  29. this.enhancedMarkdownIt = await this.createMarkdownIt(MarkdownService.ENHANCED_RULES)
  30. }
  31. const html = this.enhancedMarkdownIt.render(markdown)
  32. return this.avoidTruncatedTags(html)
  33. }
  34. async completeMarkdownToHTML (markdown: string) {
  35. if (!markdown) return ''
  36. if (!this.completeMarkdownIt) {
  37. this.completeMarkdownIt = await this.createMarkdownIt(MarkdownService.COMPLETE_RULES, true)
  38. }
  39. const html = this.completeMarkdownIt.render(markdown)
  40. return this.avoidTruncatedTags(html)
  41. }
  42. private async createMarkdownIt (rules: string[], html = false) {
  43. // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
  44. const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
  45. const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html })
  46. for (const rule of rules) {
  47. markdownIt.enable(rule)
  48. }
  49. this.setTargetToLinks(markdownIt)
  50. return markdownIt
  51. }
  52. private setTargetToLinks (markdownIt: MarkdownIt) {
  53. // Snippet from markdown-it documentation: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
  54. const defaultRender = markdownIt.renderer.rules.link_open || function (tokens, idx, options, env, self) {
  55. return self.renderToken(tokens, idx, options)
  56. }
  57. markdownIt.renderer.rules.link_open = function (tokens, index, options, env, self) {
  58. const token = tokens[index]
  59. const targetIndex = token.attrIndex('target')
  60. if (targetIndex < 0) token.attrPush([ 'target', '_blank' ])
  61. else token.attrs[targetIndex][1] = '_blank'
  62. const relIndex = token.attrIndex('rel')
  63. if (relIndex < 0) token.attrPush([ 'rel', 'noopener noreferrer' ])
  64. else token.attrs[relIndex][1] = 'noopener noreferrer'
  65. // pass token to default renderer.
  66. return defaultRender(tokens, index, options, env, self)
  67. }
  68. }
  69. private avoidTruncatedTags (html: string) {
  70. return html.replace(/\*\*?([^*]+)$/, '$1')
  71. .replace(/<a[^>]+>([^<]+)<\/a>\s*...((<\/p>)|(<\/li>)|(<\/strong>))?$/mi, '$1...')
  72. .replace(/\[[^\]]+\]?\(?([^\)]+)$/, '$1')
  73. }
  74. }