]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/LexicalCodeBlockNode.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / rich-text / LexicalCodeBlockNode.ts
1 import {
2     DecoratorNode,
3     DOMConversion,
4     DOMConversionMap,
5     DOMConversionOutput, DOMExportOutput,
6     LexicalEditor, LexicalNode,
7     SerializedLexicalNode,
8     Spread
9 } from "lexical";
10 import type {EditorConfig} from "lexical/LexicalEditor";
11 import {EditorDecoratorAdapter} from "../../ui/framework/decorator";
12 import {CodeEditor} from "../../../components";
13 import {el} from "../../utils/dom";
14
15 export type SerializedCodeBlockNode = Spread<{
16     language: string;
17     id: string;
18     code: string;
19 }, SerializedLexicalNode>
20
21 const getLanguageFromClassList = (classes: string) => {
22     const langClasses = classes.split(' ').filter(cssClass => cssClass.startsWith('language-'));
23     return (langClasses[0] || '').replace('language-', '');
24 };
25
26 export class CodeBlockNode extends DecoratorNode<EditorDecoratorAdapter> {
27     __id: string = '';
28     __language: string = '';
29     __code: string = '';
30
31     static getType(): string {
32         return 'code-block';
33     }
34
35     static clone(node: CodeBlockNode): CodeBlockNode {
36         const newNode = new CodeBlockNode(node.__language, node.__code, node.__key);
37         newNode.__id = node.__id;
38         return newNode;
39     }
40
41     constructor(language: string = '', code: string = '', key?: string) {
42         super(key);
43         this.__language = language;
44         this.__code = code;
45     }
46
47     setLanguage(language: string): void {
48         const self = this.getWritable();
49         self.__language = language;
50     }
51
52     getLanguage(): string {
53         const self = this.getLatest();
54         return self.__language;
55     }
56
57     setCode(code: string): void {
58         const self = this.getWritable();
59         self.__code = code;
60     }
61
62     getCode(): string {
63         const self = this.getLatest();
64         return self.__code;
65     }
66
67     setId(id: string) {
68         const self = this.getWritable();
69         self.__id = id;
70     }
71
72     getId(): string {
73         const self = this.getLatest();
74         return self.__id;
75     }
76
77     decorate(editor: LexicalEditor, config: EditorConfig): EditorDecoratorAdapter {
78         return {
79             type: 'code',
80             getNode: () => this,
81         };
82     }
83
84     isInline(): boolean {
85         return false;
86     }
87
88     isIsolated() {
89         return true;
90     }
91
92     createDOM(_config: EditorConfig, _editor: LexicalEditor) {
93         const codeBlock = el('pre', {
94             id: this.__id || null,
95         }, [
96             el('code', {
97                 class: this.__language ? `language-${this.__language}` : null,
98             }, [this.__code]),
99         ]);
100
101         return el('div', {class: 'editor-code-block-wrap'}, [codeBlock]);
102     }
103
104     updateDOM(prevNode: CodeBlockNode, dom: HTMLElement) {
105         const code = dom.querySelector('code');
106         if (!code) return false;
107
108         if (prevNode.__language !== this.__language) {
109             code.className = this.__language ? `language-${this.__language}` : '';
110         }
111
112         if (prevNode.__id !== this.__id) {
113             dom.setAttribute('id', this.__id);
114         }
115
116         if (prevNode.__code !== this.__code) {
117             code.textContent = this.__code;
118         }
119
120         return false;
121     }
122
123     exportDOM(editor: LexicalEditor): DOMExportOutput {
124         const dom = this.createDOM(editor._config, editor);
125         return {
126             element: dom.querySelector('pre') as HTMLElement,
127         };
128     }
129
130     static importDOM(): DOMConversionMap|null {
131         return {
132             pre(node: HTMLElement): DOMConversion|null {
133                 return {
134                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
135
136                         const codeEl = element.querySelector('code');
137                         const language = getLanguageFromClassList(element.className)
138                                         || (codeEl && getLanguageFromClassList(codeEl.className))
139                                         || '';
140
141                         const code = codeEl ? (codeEl.textContent || '').trim() : (element.textContent || '').trim();
142                         const node = $createCodeBlockNode(language, code);
143
144                         if (element.id) {
145                             node.setId(element.id);
146                         }
147
148                         return {
149                             node,
150                             after(childNodes): LexicalNode[] {
151                                 // Remove any child nodes that may get parsed since we're manually
152                                 // controlling the code contents.
153                                 return [];
154                             },
155                         };
156                     },
157                     priority: 3,
158                 };
159             },
160         };
161     }
162
163     exportJSON(): SerializedCodeBlockNode {
164         return {
165             type: 'code-block',
166             version: 1,
167             id: this.__id,
168             language: this.__language,
169             code: this.__code,
170         };
171     }
172
173     static importJSON(serializedNode: SerializedCodeBlockNode): CodeBlockNode {
174         const node = $createCodeBlockNode(serializedNode.language, serializedNode.code);
175         node.setId(serializedNode.id || '');
176         return node;
177     }
178 }
179
180 export function $createCodeBlockNode(language: string = '', code: string = ''): CodeBlockNode {
181     return new CodeBlockNode(language, code);
182 }
183
184 export function $isCodeBlockNode(node: LexicalNode | null | undefined) {
185     return node instanceof CodeBlockNode;
186 }
187
188 export function $openCodeEditorForNode(editor: LexicalEditor, node: CodeBlockNode): void {
189     const code = node.getCode();
190     const language = node.getLanguage();
191
192     // @ts-ignore
193     const codeEditor = window.$components.first('code-editor') as CodeEditor;
194     // TODO - Handle direction
195     codeEditor.open(code, language, 'ltr', (newCode: string, newLang: string) => {
196         editor.update(() => {
197             node.setCode(newCode);
198             node.setLanguage(newLang);
199         });
200         // TODO - Re-focus
201     }, () => {
202         // TODO - Re-focus
203     });
204 }