import type { LanguageDescription , LanguageSupport } from '@codemirror/language' ; import { ApplySchemaAttributes , EditorView , extension , GetAttributes , InputRule , isElementDomNode , isTextSelection , keyBinding , KeyBindingProps , NodeExtension , NodeExtensionSpec , nodeInputRule , NodeSpecOverride , NodeViewMethod , PrioritizedKeyBindings , ProsemirrorNode , } from '@remirror/core' ; import { TextSelection } from '@remirror/pm/state' ; import { CodeMirror6NodeView } from './codemirror-node-view' ; import { CodeMirrorExtensionOptions } from './codemirror-types' ; import { arrowHandler } from './codemirror-utils' ; @ extension < CodeMirrorExtensionOptions > ( { defaultOptions : { extensions : null , languages : null , toggleName : 'paragraph' , } , } ) export class CodeMirrorExtension extends NodeExtension < CodeMirrorExtensionOptions > { get name ( ) { return 'codeMirror' as const ; } private languageMap : Record < string , LanguageDescription > | null = null ; createNodeSpec ( extra , override ) { return { group : 'block' , content : 'text*' , marks : '' , defining : true , ... override , code : true , attrs : { ... extra . defaults ( ) , language : { default : '' } , } , parseDOM : [ { tag : 'pre' , getAttrs : ( node ) => ( isElementDomNode ( node ) ? extra . parse ( node ) : false ) , } , ... ( override . parseDOM ?? [ ] ) , ] , toDOM ( ) { return [ 'pre' , [ 'code' , 0 ] ] ; } , isolating : true , } ; } createNodeViews ( ) { return ( node , view , getPos ) => { return new CodeMirror6NodeView ( { node , view , getPos : getPos as ( ) => number , extensions : this . options . extensions , loadLanguage : this . loadLanguage . bind ( this ) , toggleName : this . options . toggleName , } ) ; } ; } createKeymap ( ) { return { ArrowLeft : arrowHandler ( 'left' ) , ArrowRight : arrowHandler ( 'right' ) , ArrowUp : arrowHandler ( 'up' ) , ArrowDown : arrowHandler ( 'down' ) , } ; } /**
     * Create an input rule that listens converts the code fence into a code block
     * when typing triple back tick followed by a space.
     */ createInputRules ( ) { const regexp = /^```(\S+) $/ ; const getAttributes = ( match ) => { const language = match [ 1 ] ?? '' ; return { language } ; } ; return [ nodeInputRule ( { regexp , type : this . type , beforeDispatch : ( { tr , start } ) => { const $pos = tr . doc . resolve ( start ) ; tr . setSelection ( new TextSelection ( $pos ) ) ; } , getAttributes : getAttributes , } ) , ] ; } @ keyBinding ( { shortcut : 'Enter' } ) enterKey ( { dispatch , tr } ) { if ( ! ( isTextSelection ( tr . selection ) && tr . selection . empty ) ) { return false ; } const { nodeBefore , parent } = tr . selection . $anchor ; if ( ! nodeBefore || ! nodeBefore . isText || ! parent . type . isTextblock ) { return false ; } const regex = /^```(\S*)?$/ ; const { text , nodeSize } = nodeBefore ; const { textContent } = parent ; if ( ! text ) { return false ; } const matchesNodeBefore = text . match ( regex ) ; const matchesParent = textContent . match ( regex ) ; if ( ! matchesNodeBefore || ! matchesParent ) { return false ; } const language = matchesNodeBefore [ 1 ] ?? '' ; const pos = tr . selection . $from . before ( ) ; const end = pos + nodeSize + 1 ; tr . replaceWith ( pos , end , this . type . create ( { language } ) ) ; tr . setSelection ( TextSelection . create ( tr . doc , pos + 1 ) ) ; if ( dispatch ) { dispatch ( tr ) ; } return true ; } private getLanguageMap ( ) { if ( ! this . languageMap ) { this . languageMap = { } ; for ( const language of this . options . languages ?? [ ] ) { for ( const alias of language . alias ) { this . languageMap [ alias ] = language ; } } } return this . languageMap ; } private loadLanguage ( languageName ) { if ( typeof languageName !== 'string' ) { return undefined ; } const languageMap = this . getLanguageMap ( ) ; const language = languageMap [ languageName . toLowerCase ( ) ] ; if ( ! language ) { return undefined ; } return language . support || language . load ( ) ; } }