OverviewFilesCommitsIssuesDocsReleasesPeers

holepunchto-deps / @holepunchto / keet-editor / src / extensions / code-block.js

155 lines · javascript ref:
import { nodeInputRule, textblockTypeInputRule } from '@tiptap/core'
import { CodeBlock as CodeBlockTipTap } from '@tiptap/extension-code-block'

export const lineBeginingBacktickInputRegex = /^```[a-z]*[\s\n]$/
export const backtickInputRegex = /(?<=\s|\n)```[a-z]*[\s\n]$/
export const lineBeginingTildeInputRegex = /^~~~[a-z]*[\s\n]$/
export const tildeInputRegex = /(?<=\s|\n)~~~[a-z]*[\s\n]$/

export const CodeBlock = CodeBlockTipTap.extend({
  addInputRules () {
    return [
      nodeInputRule({
        find: backtickInputRegex,
        type: this.type,
        getAttributes: (match) => ({
          language: match[1]
        })
      }),
      textblockTypeInputRule({
        find: lineBeginingBacktickInputRegex,
        type: this.type,
        getAttributes: (match) => ({
          language: match[1]
        })
      }),
      nodeInputRule({
        find: tildeInputRegex,
        type: this.type,
        getAttributes: (match) => ({
          language: match[1]
        })
      }),
      textblockTypeInputRule({
        find: lineBeginingTildeInputRegex,
        type: this.type,
        getAttributes: (match) => ({
          language: match[1]
        })
      })
    ]
  },
  addKeyboardShortcuts () {
    return {
      'Mod-Alt-c': () => this.editor.commands.toggleCodeBlock(),

      Backspace: () => {
        const { empty, $anchor } = this.editor.state.selection
        const isAtStart = $anchor.pos === 1

        if (!empty || $anchor.parent.type.name !== this.name) {
          return false
        }

        if (isAtStart || !$anchor.parent.textContent.length) {
          return this.editor.commands.clearNodes()
        }

        return false
      },

      Enter: () => {
        const { state } = this.editor
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          // const cursorAtEnd = to === state.doc.content.size - 1
          if (inCodeBlock) {
            return this.editor.commands.exitCode()
          }
        }

        return false
      },

      ArrowDown: () => {
        const { state } = this.editor
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          const cursorAtEnd = to === state.doc.content.size - 1
          if (inCodeBlock && cursorAtEnd) {
            return this.editor.commands.exitCode()
          }
        }

        return false
      },

      'Shift-Enter': () => {
        const { state } = this.editor
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          if (inCodeBlock) {
            return this.editor.commands.newlineInCode()
          }
        }

        return false
      },

      'Mod-Enter': () => {
        const { state } = this.editor
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          if (inCodeBlock) {
            return this.editor.commands.newlineInCode()
          }
        }

        return false
      },

      'Alt-Enter': () => {
        const { state } = this.editor
        const { from, to } = state.selection

        if (from > 1 && from === to) {
          let inCodeBlock = false
          state.doc.nodesBetween(from - 1, to - 1, (node) => {
            if (node.type.name === 'codeBlock') inCodeBlock = true
          })

          if (inCodeBlock) {
            return this.editor.commands.newlineInCode()
          }
        }

        return false
      }
    }
  }
})