@@ -23,6 +23,8 @@ import { Plugin, PluginKey } from '@tiptap/pm/state'
23
23
import { Decoration , DecorationSet , type EditorView } from '@tiptap/pm/view'
24
24
import { createLowlight } from 'lowlight'
25
25
import { isChangeEditable } from './editable'
26
+ import { TextSelection } from '@tiptap/pm/state'
27
+ import { Fragment } from '@tiptap/pm/model'
26
28
27
29
type Lowlight = ReturnType < typeof createLowlight >
28
30
@@ -40,11 +42,32 @@ export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowl
40
42
41
43
addCommands ( ) {
42
44
return {
43
- setCodeBlock :
44
- ( attributes ) =>
45
- ( { commands } ) => {
46
- return commands . setNode ( this . name , attributes )
47
- } ,
45
+ setCodeBlock : ( attributes ) => ( { editor, commands } ) => {
46
+ return commands . command ( ( { tr, dispatch } ) => {
47
+ const { from, to } = tr . selection
48
+ const codeBlockType = editor . schema . nodes [ this . name ]
49
+ const paragraphType = editor . schema . nodes . paragraph
50
+
51
+ if ( ! codeBlockType || ! paragraphType ) {
52
+ return false
53
+ }
54
+
55
+ const codeBlockNode = codeBlockType . create ( attributes )
56
+ const paragraphNode = paragraphType . create ( )
57
+
58
+ const fragmentToInsert = Fragment . fromArray ( [ codeBlockNode , paragraphNode ] )
59
+
60
+ tr . replaceWith ( from , to , fragmentToInsert )
61
+
62
+ const newSelectionPos = from + codeBlockNode . nodeSize + 1
63
+ tr . setSelection ( TextSelection . create ( tr . doc , newSelectionPos ) )
64
+
65
+ if ( dispatch ) {
66
+ dispatch ( tr . scrollIntoView ( ) )
67
+ }
68
+ return true
69
+ } )
70
+ } ,
48
71
toggleCodeBlock :
49
72
( attributes ) =>
50
73
( { chain, commands, state } ) => {
@@ -60,10 +83,10 @@ export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowl
60
83
}
61
84
if ( node . type . name !== 'paragraph' ) {
62
85
if ( pos + 1 <= from && pos + node . nodeSize - 1 >= to ) {
63
- // skip nodes outside of the selected range
86
+ // skip nodes outside of the selected range
64
87
return false
65
88
} else {
66
- // cannot merge non-paragraph nodes inside selection
89
+ // cannot merge non-paragraph nodes inside selection
67
90
hasParagraphsOnlySelected = false
68
91
return false
69
92
}
@@ -78,10 +101,17 @@ export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowl
78
101
if ( hasParagraphsOnlySelected && textArr . length > 1 ) {
79
102
return chain ( )
80
103
. command ( ( { state, tr } ) => {
81
- tr . replaceRangeWith ( from , to , this . type . create ( attributes , state . schema . text ( textArr . join ( '\n' ) ) ) )
104
+ const codeBlockNode = this . type . create ( attributes , state . schema . text ( textArr . join ( '\n' ) ) )
105
+ const paragraphNode = state . schema . nodes . paragraph . create ( )
106
+ const fragmentToInsert = Fragment . fromArray ( [ codeBlockNode , paragraphNode ] )
107
+
108
+ tr . replaceWith ( from , to , fragmentToInsert )
109
+
110
+ const newSelectionPos = from + codeBlockNode . nodeSize + 1
111
+ tr . setSelection ( TextSelection . create ( tr . doc , newSelectionPos ) )
112
+
82
113
return true
83
114
} )
84
- . setTextSelection ( { from : from + 2 , to : from + 2 } )
85
115
. run ( )
86
116
}
87
117
}
@@ -94,20 +124,65 @@ export const CodeBlockHighlighExtension = CodeBlockLowlight.extend<CodeBlockLowl
94
124
return [
95
125
textblockTypeInputRule ( {
96
126
find : backtickInputRegex ,
97
- type : this . type
127
+ type : this . type ,
128
+ getAttributes : ( match ) => {
129
+ return { language : match [ 3 ] || null }
130
+ }
98
131
} ) ,
99
132
textblockTypeInputRule ( {
100
133
find : tildeInputRegex ,
101
- type : this . type
134
+ type : this . type ,
135
+ getAttributes : ( match ) => {
136
+ return { language : match [ 3 ] || null }
137
+ }
102
138
} )
103
139
]
104
140
} ,
105
141
106
142
addProseMirrorPlugins ( ) {
107
- return [ ...( this . parent ?.( ) ?? [ ] ) , LanguageSelector ( this . options ) ]
143
+ return [ ...( this . parent ?.( ) ?? [ ] ) , LanguageSelector ( this . options ) , CodeBlockEnhancer ( ) ]
108
144
}
109
145
} )
110
146
147
+ function CodeBlockEnhancer ( ) : Plugin {
148
+ return new Plugin ( {
149
+ key : new PluginKey ( 'codeblock-enhancer' ) ,
150
+ appendTransaction ( transactions , oldState , newState ) {
151
+ const tr = newState . tr
152
+ let modified = false
153
+
154
+ transactions . forEach ( transaction => {
155
+ if ( ! transaction . docChanged ) return
156
+
157
+ transaction . steps . forEach ( ( step : any ) => {
158
+ if ( step . slice ) {
159
+ const content = step . slice . content
160
+ content . descendants ( ( node : any , pos : number ) => {
161
+ if ( node . type . name === 'codeBlock' ) {
162
+ const nodePos = step . from + pos
163
+ const nodeEnd = nodePos + node . nodeSize
164
+
165
+ const nextNode = newState . doc . nodeAt ( nodeEnd )
166
+
167
+ if ( ! nextNode || nextNode . type . name !== 'paragraph' ) {
168
+ const paragraphType = newState . schema . nodes . paragraph
169
+ if ( paragraphType ) {
170
+ const newParagraph = paragraphType . create ( )
171
+ tr . insert ( nodeEnd , newParagraph )
172
+ modified = true
173
+ }
174
+ }
175
+ }
176
+ } )
177
+ }
178
+ } )
179
+ } )
180
+
181
+ return modified ? tr : null
182
+ }
183
+ } )
184
+ }
185
+
111
186
export function LanguageSelector ( options : CodeBlockLowlightOptions ) : Plugin {
112
187
return new Plugin < DecorationSet > ( {
113
188
key : new PluginKey ( 'codeblock-language-selector' ) ,
@@ -212,4 +287,4 @@ function handleLangButtonClick (
212
287
}
213
288
}
214
289
)
215
- }
290
+ }
0 commit comments