Chocobozzz/PeerTube

View on GitHub
server/core/helpers/markdown.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import MarkdownItClass from 'markdown-it'
// FIXME: use direct import: import markdownItEmoji from 'markdown-it-emoji/lib/light.mjs' if it improves perf'
// when https://github.com/privatenumber/tsx/issues/334 is fixed
import { light as markdownItEmoji } from 'markdown-it-emoji'
import sanitizeHtml from 'sanitize-html'
import { getDefaultSanitizeOptions, getTextOnlySanitizeOptions, TEXT_WITH_HTML_RULES } from '@peertube/peertube-core-utils'

const defaultSanitizeOptions = getDefaultSanitizeOptions()
const textOnlySanitizeOptions = getTextOnlySanitizeOptions()

const markdownItForSafeHtml = new MarkdownItClass('default', { linkify: true, breaks: true, html: true })
  .enable(TEXT_WITH_HTML_RULES)
  .use(markdownItEmoji)

const markdownItForPlainText = new MarkdownItClass('default', { linkify: false, breaks: true, html: false })
  .use(markdownItEmoji)
  .use(plainTextPlugin)

const toSafeHtml = (text: string) => {
  if (!text) return ''

  // Restore line feed
  const textWithLineFeed = text.replace(/<br.?\/?>/g, '\r\n')

  // Convert possible markdown (emojis, emphasis and lists) to html
  const html = markdownItForSafeHtml.render(textWithLineFeed)

  // Convert to safe Html
  return sanitizeHtml(html, defaultSanitizeOptions)
}

const mdToOneLinePlainText = (text: string) => {
  if (!text) return ''

  markdownItForPlainText.render(text)

  // Convert to safe Html
  return sanitizeHtml(markdownItForPlainText.plainText, textOnlySanitizeOptions)
}

// ---------------------------------------------------------------------------

export {
  toSafeHtml,
  mdToOneLinePlainText
}

// ---------------------------------------------------------------------------

// Thanks: https://github.com/wavesheep/markdown-it-plain-text
function plainTextPlugin (markdownIt: any) {
  function plainTextRule (state: any) {
    const text = scan(state.tokens)

    markdownIt.plainText = text
  }

  function scan (tokens: any[]) {
    let lastSeparator = ''
    let text = ''

    function buildSeparator (token: any) {
      if (token.type === 'list_item_close') {
        lastSeparator = ', '
      }

      if (token.tag === 'br' || token.type === 'paragraph_close') {
        lastSeparator = ' '
      }
    }

    for (const token of tokens) {
      buildSeparator(token)

      if (token.type !== 'inline') continue

      for (const child of token.children) {
        buildSeparator(child)

        if (!child.content) continue

        text += lastSeparator + child.content
        lastSeparator = ''
      }
    }

    return text
  }

  markdownIt.core.ruler.push('plainText', plainTextRule)
}