11import { toMarkdown as mdastUtilToMarkdown } from "https://esm.sh/[email protected] " ; 22import Quill from "https://esm.sh/[email protected] " ; 3+ import { fromMarkdown } from "https://esm.sh/[email protected] " ; 34
45/**
56 * @typedef {Object } QuillAttributes
@@ -61,7 +62,8 @@ function createAndReplaceTextarea(textarea) {
6162 } else {
6263 label . parentNode . insertBefore ( editorDiv , label . nextSibling ) ;
6364 }
64- textarea . style . display = "none" ;
65+ // Hide the original textarea, but keep it focusable for validation
66+ textarea . style = "transform: scale(0); position: absolute; opacity: 0;" ;
6567 return editorDiv ;
6668}
6769
@@ -105,11 +107,146 @@ function initializeQuillEditor(editorDiv, toolbarOptions, initialValue) {
105107 ] ,
106108 } ) ;
107109 if ( initialValue ) {
108- quill . setText ( initialValue ) ;
110+ const delta = markdownToDelta ( initialValue ) ;
111+ quill . setContents ( delta ) ;
109112 }
110113 return quill ;
111114}
112115
116+ /**
117+ * Converts Markdown string to a Quill Delta object.
118+ * @param {string } markdown - The markdown string to convert.
119+ * @returns {QuillDelta } - Quill Delta representation.
120+ */
121+ function markdownToDelta ( markdown ) {
122+ try {
123+ const mdastTree = fromMarkdown ( markdown ) ;
124+ return mdastToDelta ( mdastTree ) ;
125+ } catch ( error ) {
126+ console . error ( "Error parsing markdown:" , error ) ;
127+ return { ops : [ { insert : markdown } ] } ;
128+ }
129+ }
130+
131+ /**
132+ * Converts MDAST to Quill Delta.
133+ * @param {MdastNode } tree - The MDAST tree to convert.
134+ * @returns {QuillDelta } - Quill Delta representation.
135+ */
136+ function mdastToDelta ( tree ) {
137+ const delta = { ops : [ ] } ;
138+ if ( ! tree || ! tree . children ) return delta ;
139+
140+ for ( const node of tree . children ) {
141+ traverseMdastNode ( node , delta ) ;
142+ }
143+
144+ return delta ;
145+ }
146+
147+ /**
148+ * Recursively traverse MDAST nodes and convert to Delta operations.
149+ * @param {MdastNode } node - The MDAST node to process.
150+ * @param {QuillDelta } delta - The Delta object to append operations to.
151+ * @param {QuillAttributes } [attributes={}] - The current attributes to apply.
152+ */
153+ function traverseMdastNode ( node , delta , attributes = { } ) {
154+ if ( ! node ) return ;
155+
156+ switch ( node . type ) {
157+ case 'root' :
158+ for ( const child of node . children || [ ] ) {
159+ traverseMdastNode ( child , delta ) ;
160+ }
161+ break ;
162+
163+ case 'paragraph' :
164+ for ( const child of node . children || [ ] ) {
165+ traverseMdastNode ( child , delta , attributes ) ;
166+ }
167+ delta . ops . push ( { insert : '\n' } ) ;
168+ break ;
169+
170+ case 'heading' :
171+ for ( const child of node . children || [ ] ) {
172+ traverseMdastNode ( child , delta , { header : node . depth } ) ;
173+ }
174+ delta . ops . push ( { insert : '\n' , attributes : { header : node . depth } } ) ;
175+ break ;
176+
177+ case 'text' :
178+ delta . ops . push ( { insert : node . value || '' , attributes } ) ;
179+ break ;
180+
181+ case 'strong' :
182+ for ( const child of node . children || [ ] ) {
183+ traverseMdastNode ( child , delta , { ...attributes , bold : true } ) ;
184+ }
185+ break ;
186+
187+ case 'emphasis' :
188+ for ( const child of node . children || [ ] ) {
189+ traverseMdastNode ( child , delta , { ...attributes , italic : true } ) ;
190+ }
191+ break ;
192+
193+ case 'link' :
194+ for ( const child of node . children || [ ] ) {
195+ traverseMdastNode ( child , delta , { ...attributes , link : node . url } ) ;
196+ }
197+ break ;
198+
199+ case 'image' :
200+ delta . ops . push ( {
201+ insert : { image : node . url } ,
202+ attributes : { alt : node . alt || '' }
203+ } ) ;
204+ break ;
205+
206+ case 'list' :
207+ for ( const child of node . children || [ ] ) {
208+ traverseMdastNode ( child , delta , {
209+ ...attributes ,
210+ list : node . ordered ? 'ordered' : 'bullet'
211+ } ) ;
212+ }
213+ break ;
214+
215+ case 'listItem' :
216+ for ( const child of node . children || [ ] ) {
217+ traverseMdastNode ( child , delta , attributes ) ;
218+ }
219+ break ;
220+
221+ case 'blockquote' :
222+ for ( const child of node . children || [ ] ) {
223+ traverseMdastNode ( child , delta , { ...attributes , blockquote : true } ) ;
224+ }
225+ break ;
226+
227+ case 'code' :
228+ delta . ops . push ( {
229+ insert : node . value || '' ,
230+ attributes : { 'code-block' : node . lang || 'plain' }
231+ } ) ;
232+ delta . ops . push ( { insert : '\n' , attributes : { 'code-block' : node . lang || 'plain' } } ) ;
233+ break ;
234+
235+ case 'inlineCode' :
236+ delta . ops . push ( { insert : node . value || '' , attributes : { code : true } } ) ;
237+ break ;
238+
239+ default :
240+ if ( node . children ) {
241+ for ( const child of node . children ) {
242+ traverseMdastNode ( child , delta , attributes ) ;
243+ }
244+ } else if ( node . value ) {
245+ delta . ops . push ( { insert : node . value , attributes } ) ;
246+ }
247+ }
248+ }
249+
113250/**
114251 * Attaches a submit event listener to the form to update the hidden textarea.
115252 * @param {HTMLFormElement|null } form - The form containing the editor.
@@ -125,10 +262,19 @@ function updateTextareaOnSubmit(form, textarea, quill) {
125262 ) ;
126263 return ;
127264 }
128- form . addEventListener ( "submit" , ( ) => {
265+ form . addEventListener ( "submit" , ( event ) => {
129266 const delta = quill . getContents ( ) ;
130267 const markdownContent = deltaToMarkdown ( delta ) ;
131268 textarea . value = markdownContent ;
269+ if ( textarea . required && ! markdownContent ) {
270+ textarea . setCustomValidity ( `${ textarea . name } cannot be empty` ) ;
271+ quill . once ( "text-change" , ( delta ) => {
272+ textarea . value = deltaToMarkdown ( delta ) ;
273+ textarea . setCustomValidity ( "" ) ;
274+ } ) ;
275+ quill . focus ( ) ;
276+ event . preventDefault ( ) ;
277+ }
132278 } ) ;
133279}
134280
0 commit comments