1+ <script setup lang="ts">
2+ import { markRaw } from ' vue' ;
3+ import * as monaco from ' monaco-editor' ;
4+ import { toggleWrapSmart } from ' ./utils/monacoMarkdownToggle' ;
5+
6+ import {
7+ IconLinkOutline , IconCodeOutline , IconRectangleListOutline ,
8+ IconOrderedListOutline , IconLetterBoldOutline , IconLetterUnderlineOutline ,
9+ IconLetterItalicOutline , IconTextSlashOutline
10+ } from ' @iconify-prerendered/vue-flowbite' ;
11+ import { IconH116Solid , IconH216Solid , IconH316Solid } from ' @iconify-prerendered/vue-heroicons' ;
12+
13+ const props = defineProps <{
14+ editor: monaco .editor .IStandaloneCodeEditor | null ;
15+ meta: any ;
16+ }>();
17+
18+ const isBtnVisible = (btnKey : string ) => {
19+ const settings = props .meta ?.topPanelSettings ;
20+ if (! settings || Object .keys (settings ).length === 0 ) return true ;
21+ return settings [btnKey ] !== undefined ? settings [btnKey ] : true ;
22+ };
23+
24+ const btnClass = ' flex items-center justify-center h-8 px-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 transition-colors duration-200' ;
25+
26+ const fenceForCodeBlock = (text : string ): string => {
27+ let maxBackticks = 0 ;
28+ let current = 0 ;
29+ for (let i = 0 ; i < text .length ; i ++ ) {
30+ if (text [i ] === ' `' ) { current ++ ; if (current > maxBackticks ) maxBackticks = current ; }
31+ else { current = 0 ; }
32+ }
33+ return ' `' .repeat (Math .max (3 , maxBackticks + 1 ));
34+ };
35+
36+ const applyFormat = (type : string ) => {
37+ const editor = props .editor ;
38+ if (! editor ) return ;
39+
40+ const model = editor .getModel ();
41+ if (! model ) return ;
42+
43+ editor .focus ();
44+ const rawSelection = editor .getSelection ();
45+ if (! rawSelection ) return ;
46+
47+ const selection = rawSelection .startLineNumber !== rawSelection .endLineNumber && rawSelection .endColumn === 1
48+ ? new monaco .Selection (rawSelection .startLineNumber , rawSelection .startColumn , rawSelection .endLineNumber - 1 , model .getLineMaxColumn (rawSelection .endLineNumber - 1 ))
49+ : rawSelection ;
50+
51+ const selectedText = model .getValueInRange (selection );
52+
53+ const applyEdits = (id : string , edits : monaco .editor .IIdentifiedSingleEditOperation []) => {
54+ editor .executeEdits (id , edits );
55+ };
56+
57+ switch (type ) {
58+ case ' bold' : toggleWrapSmart (editor , ' **' ); break ;
59+ case ' italic' : toggleWrapSmart (editor , ' *' ); break ;
60+ case ' strike' : toggleWrapSmart (editor , ' ~~' ); break ;
61+ case ' underline' : toggleWrapSmart (editor , ' <u>' , ' </u>' ); break ;
62+ case ' codeBlock' : {
63+ const trimmed = selectedText .trim ();
64+ const match = trimmed .match (/ ^ (`{3,} )[^ \n ] * \n ([\s\S ] * )\n \1 $ / );
65+ if (match ) {
66+ applyEdits (' unwrap-code' , [{ range: selection , text: match [2 ], forceMoveMarkers: true }]);
67+ } else {
68+ const fence = fenceForCodeBlock (selectedText );
69+ applyEdits (' wrap-code' , [{ range: selection , text: ` \n ${fence }\n ${selectedText }\n ${fence }\n ` , forceMoveMarkers: true }]);
70+ }
71+ break ;
72+ }
73+ case ' link' : {
74+ const match = selectedText .trim ().match (/ ^ \[ (. *? )\]\( . *? \) $ / );
75+ if (match ) {
76+ applyEdits (' unlink' , [{ range: selection , text: match [1 ], forceMoveMarkers: true }]);
77+ } else {
78+ applyEdits (' insert-link' , [{ range: selection , text: ` [${selectedText }](url) ` , forceMoveMarkers: true }]);
79+ }
80+ break ;
81+ }
82+ case ' h1' : case ' h2' : case ' h3' : case ' ul' : case ' ol' : {
83+ const prefixMap: any = { h1: ' # ' , h2: ' ## ' , h3: ' ### ' , ul: ' * ' };
84+ const edits: any [] = [];
85+ for (let i = selection .startLineNumber ; i <= selection .endLineNumber ; i ++ ) {
86+ const line = model .getLineContent (i );
87+ const targetPrefix = type === ' ol' ? ` ${i - selection .startLineNumber + 1 }. ` : prefixMap [type ];
88+ const match = line .match (/ ^ (#{1,6} \s + | [*+-] \s + | \d + [. )] \s + )/ );
89+ if (match ) {
90+ edits .push ({ range: new monaco .Range (i , 1 , i , match [0 ].length + 1 ), text: match [0 ].trim () === targetPrefix .trim () ? ' ' : targetPrefix });
91+ } else {
92+ edits .push ({ range: new monaco .Range (i , 1 , i , 1 ), text: targetPrefix });
93+ }
94+ }
95+ applyEdits (' format-block' , edits );
96+ break ;
97+ }
98+ }
99+ };
100+
101+ const buttons = [
102+ { id: ' bold' , title: ' Bold' , icon: markRaw (IconLetterBoldOutline ), group: 1 },
103+ { id: ' italic' , title: ' Italic' , icon: markRaw (IconLetterItalicOutline ), group: 1 },
104+ { id: ' underline' , title: ' Underline' , icon: markRaw (IconLetterUnderlineOutline ), group: 1 },
105+ { id: ' strike' , title: ' Strike' , icon: markRaw (IconTextSlashOutline ), group: 1 , separator: true },
106+ { id: ' h1' , title: ' H1' , icon: markRaw (IconH116Solid ), group: 2 },
107+ { id: ' h2' , title: ' H2' , icon: markRaw (IconH216Solid ), group: 2 },
108+ { id: ' h3' , title: ' H3' , icon: markRaw (IconH316Solid ), group: 2 , separator: true },
109+ { id: ' ul' , title: ' UL' , icon: markRaw (IconRectangleListOutline ), group: 3 },
110+ { id: ' ol' , title: ' OL' , icon: markRaw (IconOrderedListOutline ), group: 3 },
111+ { id: ' link' , title: ' Link' , icon: markRaw (IconLinkOutline ), group: 3 },
112+ { id: ' codeBlock' , title: ' Code' , icon: markRaw (IconCodeOutline ), group: 3 },
113+ ];
114+ </script >
115+
116+ <template >
117+ <div class =" flex flex-wrap items-center gap-3 p-1.5 border border-gray-300 dark:border-gray-600 rounded-t-lg bg-gray-50 dark:bg-gray-800 w-full box-border" >
118+ <template v-for =" btn in buttons " :key =" btn .id " >
119+ <button
120+ v-if =" isBtnVisible(btn.id)"
121+ type =" button"
122+ @click =" applyFormat(btn.id)"
123+ :class =" btnClass"
124+ :title =" btn.title"
125+ >
126+ <component :is =" btn.icon" class =" w-5 h-5" />
127+ </button >
128+ <div v-if =" btn.separator && isBtnVisible(btn.id)" class =" w-px h-4 bg-gray-300 dark:bg-gray-600 mx-1" ></div >
129+ </template >
130+ </div >
131+ </template >
0 commit comments