OverviewFilesCommitsIssuesDocsReleasesPeers

holepunchto-deps / @holepunchto / keet-editor-parser / src / document / to-display.js

211 lines · javascript ref:
const {
  DISPLAY_TYPES,
  NODE_TYPE,
  MARK_TYPE,
  LINK_PROTOCOL,
  MENTION_PREFIX,
  BREAKLINE_CHAR
} = require('../constants.js')

const { normalizeUrl } = require('../utils.js')

const {
  MENTION,
  HTTP_LINK,
  PEAR_LINK,
  BOLD,
  ITALIC,
  CODE,
  EMOJI,
  CODE_BLOCK,
  STRIKE_THROUGH
} = DISPLAY_TYPES

module.exports = { documentToDisplay }

function documentToDisplay(doc) {
  return parseNode(doc)
}

function parseNode(node, start = 0) {
  let text = ''
  let display = []

  if (isLeaf(node)) {
    text = getNodeText(node)
    display = getNodeDisplay(node, start)
  } else {
    const blockStart = start
    let textChildren = ''
    let displayChildren = []

    for (const [index, childNode] of node.content.entries()) {
      const child = parseNode(childNode, start)

      if (needsBreakline(node, index)) {
        child.text += BREAKLINE_CHAR
      }

      textChildren += child.text
      displayChildren = displayChildren.concat(child.display)
      start += child.text.length
    }

    const textBlock = getNodeText(node, textChildren)
    if (textBlock) {
      text = textBlock
    } else {
      text = textChildren
    }

    const displayBlock = getNodeDisplay(node, blockStart, text.length)
    if (Object.keys(displayBlock).length > 0) {
      display = displayBlock
    } else {
      display = displayChildren
    }
  }

  return { text, display }
}

function getNodeText(node, textChildren = '') {
  let text = ''

  switch (node.type) {
    case NODE_TYPE.TEXT:
      text = node.text
      break
    case NODE_TYPE.EMOJI:
      text = node.attrs['data-emoji']
      break
    case NODE_TYPE.MENTION:
      text = `${MENTION_PREFIX}${node.attrs.label}`
      break
    case NODE_TYPE.CODE_BLOCK:
      text = ` ${textChildren} `
      break
    case NODE_TYPE.HARD_BREAK:
      text = BREAKLINE_CHAR
      break
    default:
      break
  }

  return text
}

function getNodeDisplay(node, start, lengthChildren = 0) {
  const display = []

  switch (node.type) {
    case NODE_TYPE.TEXT: {
      const marks = node.marks || []

      for (const mark of marks) {
        const markDisplay = getMarkDisplay(mark)
        if (markDisplay) {
          const { type, ...rest } = markDisplay
          display.push({
            type,
            start,
            length: node.text.length,
            ...rest
          })
        }
      }

      break
    }
    case NODE_TYPE.EMOJI: {
      display.push({
        type: EMOJI,
        start,
        length: node.attrs['data-emoji'].length,
        content: node.attrs['data-shortcode']
      })
      break
    }
    case NODE_TYPE.MENTION: {
      display.push({
        type: MENTION,
        start,
        length: MENTION_PREFIX.length + node.attrs.label.length,
        memberId: node.attrs.id
      })
      break
    }
    case NODE_TYPE.CODE_BLOCK: {
      display.push({
        type: CODE_BLOCK,
        start: start + 1,
        length: Math.max(0, lengthChildren - 2)
      })
      break
    }
    default:
      break
  }

  return display
}

function getMarkDisplay(mark) {
  let display

  switch (mark.type) {
    case MARK_TYPE.BOLD:
      display = { type: BOLD }
      break
    case MARK_TYPE.ITALIC:
      display = { type: ITALIC }
      break
    case MARK_TYPE.STRIKE_THROUGH:
      display = { type: STRIKE_THROUGH }
      break
    case MARK_TYPE.CODE:
      display = { type: CODE }
      break
    case MARK_TYPE.LINK:
      if (isHttpLink(mark)) {
        display = {
          type: HTTP_LINK,
          content: getLink(mark)
        }
      } else if (isPearLink(mark)) {
        display = {
          type: PEAR_LINK,
          content: getLink(mark)
        }
      }
      break
    default:
      break
  }

  return display
}

function needsBreakline(node, index) {
  return (
    node.content[index].type === NODE_TYPE.PARAGRAPH &&
    node.content[index + 1]?.type === NODE_TYPE.PARAGRAPH
  )
}

function isLeaf(node) {
  return !node.content
}

function getLink(mark) {
  return normalizeUrl(mark.attrs.href)
}

function isHttpLink(mark) {
  return getLink(mark).startsWith(LINK_PROTOCOL.HTTP)
}

function isPearLink(mark) {
  return getLink(mark).startsWith(LINK_PROTOCOL.PEAR)
}