@@ -8,7 +8,10 @@ import { languages } from "@codemirror/language-data";
88import  {  stexMath  }  from  "@codemirror/legacy-modes/mode/stex" ; 
99import  type  {  Extension  }  from  "@codemirror/state" ; 
1010import  {  type  EditorView ,  ViewPlugin  }  from  "@codemirror/view" ; 
11- import  dedent  from  "string-dedent" ; 
11+ import  { 
12+   type  MarkdownMetadata , 
13+   MarkdownParser , 
14+ }  from  "@marimo-team/smart-cells" ; 
1215import  type  {  CellId  }  from  "@/core/cells/ids" ; 
1316import  type  {  CompletionConfig  }  from  "@/core/config/config-schema" ; 
1417import  type  {  HotkeyProvider  }  from  "@/core/hotkeys/hotkeys" ; 
@@ -22,183 +25,43 @@ import { parsePython } from "../embedded/embedded-python";
2225import  {  parseLatex  }  from  "../embedded/latex" ; 
2326import  {  languageMetadataField  }  from  "../metadata" ; 
2427import  type  {  LanguageAdapter  }  from  "../types" ; 
25- import  { 
26-   QUOTE_PREFIX_KINDS , 
27-   type  QuotePrefixKind , 
28-   splitQuotePrefix , 
29- }  from  "../utils/quotes" ; 
30- 
31- export  interface  MarkdownLanguageAdapterMetadata  { 
32-   quotePrefix : QuotePrefixKind ; 
33- } 
3428
35- const  quoteKinds  =  [ 
36-   [ '"""' ,  '"""' ] , 
37-   [ "'''" ,  "'''" ] , 
38-   [ '"' ,  '"' ] , 
39-   [ "'" ,  "'" ] , 
40- ] ; 
41- 
42- // explode into all combinations 
43- const  pairs  =  QUOTE_PREFIX_KINDS . flatMap ( ( prefix )  => 
44-   quoteKinds . map ( ( [ start ,  end ] )  =>  [ prefix  +  start ,  end ] ) , 
45- ) ; 
46- 
47- const  regexes  =  pairs . map ( 
48-   ( [ start ,  end ] )  => 
49-     // mo.md( + any number of spaces + start + capture + any number of spaces + end) 
50-     [ 
51-       start , 
52-       new  RegExp ( `^mo\\.md\\(\\s*${ start }  (.*)${ end }  \\s*\\)$` ,  "s" ) , 
53-     ]  as  const , 
54- ) ; 
29+ export  interface  MarkdownLanguageAdapterMetadata  extends  MarkdownMetadata  { } 
5530
5631/** 
5732 * Language adapter for Markdown. 
5833 */ 
5934export  class  MarkdownLanguageAdapter 
6035  implements  LanguageAdapter < MarkdownLanguageAdapterMetadata > 
6136{ 
37+   private  parser  =  new  MarkdownParser ( ) ; 
38+ 
6239  readonly  type  =  "markdown" ; 
63-   readonly  defaultCode  =  'mo.md(r"""\n""")' ; 
64-   readonly  defaultMetadata : MarkdownLanguageAdapterMetadata  =  { 
65-     quotePrefix : "r" , 
66-   } ; 
40+   readonly  defaultCode  =  this . parser . defaultCode ; 
41+   readonly  defaultMetadata : MarkdownLanguageAdapterMetadata  = 
42+     this . parser . defaultMetadata ; 
6743
6844  static  fromMarkdown ( markdown : string )  { 
69-     return  `mo.md(r"""\n ${ markdown } \n""")` ; 
45+     return  MarkdownParser . fromMarkdown ( markdown ) ; 
7046  } 
7147
7248  transformIn ( 
7349    pythonCode : string , 
7450  ) : [ string ,  number ,  MarkdownLanguageAdapterMetadata ]  { 
75-     pythonCode  =  pythonCode . trim ( ) ; 
76- 
77-     const  metadata  =  {  ...this . defaultMetadata  } ; 
78- 
79-     // empty string 
80-     if  ( pythonCode  ===  "" )  { 
81-       return  [ "" ,  0 ,  metadata ] ; 
82-     } 
83- 
84-     for  ( const  [ start ,  regex ]  of  regexes )  { 
85-       const  match  =  pythonCode . match ( regex ) ; 
86-       if  ( match )  { 
87-         const  innerCode  =  match [ 1 ] ; 
88-         const  [ quotePrefix ,  quoteType ]  =  splitQuotePrefix ( start ) ; 
89-         metadata . quotePrefix  =  quotePrefix ; 
90-         const  unescapedCode  =  innerCode . replaceAll ( `\\${ quoteType }  ` ,  quoteType ) ; 
91- 
92-         const  offset  =  pythonCode . indexOf ( innerCode ) ; 
93-         // string-dedent expects the first and last line to be empty / contain only whitespace, so we pad with \n 
94-         return  [ dedent ( `\n${ unescapedCode }  \n` ) . trim ( ) ,  offset ,  metadata ] ; 
95-       } 
96-     } 
97- 
98-     // no match 
99-     return  [ pythonCode ,  0 ,  metadata ] ; 
51+     const  result  =  this . parser . transformIn ( pythonCode ) ; 
52+     return  [ result . code ,  result . offset ,  result . metadata ] ; 
10053  } 
10154
10255  transformOut ( 
10356    code : string , 
10457    metadata : MarkdownLanguageAdapterMetadata , 
10558  ) : [ string ,  number ]  { 
106-     // NB. Must be kept consistent with marimo/_convert/utils.py 
107-     // ::markdown_to_marimo 
108- 
109-     // Empty string 
110-     if  ( code  ===  "" )  { 
111-       // Need at least a space, otherwise the output will be 6 quotes 
112-       code  =  " " ; 
113-     } 
114- 
115-     const  {  quotePrefix }  =  metadata ; 
116- 
117-     // We always transform back with triple quotes, as to avoid needing to 
118-     // escape single quotes. 
119-     const  escapedCode  =  code . replaceAll ( '"""' ,  String . raw `\"""` ) ; 
120- 
121-     // If its one line and not bounded by quotes, write it as single line 
122-     const  isOneLine  =  ! code . includes ( "\n" ) ; 
123-     const  boundedByQuote  =  code . startsWith ( '"' )  ||  code . endsWith ( '"' ) ; 
124-     if  ( isOneLine  &&  ! boundedByQuote )  { 
125-       const  start  =  `mo.md(${ quotePrefix }  """` ; 
126-       const  end  =  `""")` ; 
127-       return  [ start  +  escapedCode  +  end ,  start . length ] ; 
128-     } 
129- 
130-     // Multiline code 
131-     const  start  =  `mo.md(\n    ${ quotePrefix }  """\n` ; 
132-     const  end  =  `\n"""\n)` ; 
133-     return  [ start  +  escapedCode  +  end ,  start . length  +  1 ] ; 
59+     const  result  =  this . parser . transformOut ( code ,  metadata ) ; 
60+     return  [ result . code ,  result . offset ] ; 
13461  } 
13562
13663  isSupported ( pythonCode : string ) : boolean  { 
137-     pythonCode  =  pythonCode . trim ( ) ; 
138- 
139-     // Empty strings are supported 
140-     if  ( pythonCode  ===  "" )  { 
141-       return  true ; 
142-     } 
143- 
144-     // Must start with mo.md( 
145-     if  ( ! pythonCode . startsWith ( "mo.md(" ) )  { 
146-       return  false ; 
147-     } 
148- 
149-     // Empty function calls are supported 
150-     if  ( pythonCode  ===  "mo.md()" )  { 
151-       return  true ; 
152-     } 
153- 
154-     // Parse the code using Lezer and check for the exact match of mo.md() signature 
155-     const  tree  =  pythonLanguage . parser . parse ( pythonCode ) ; 
156- 
157-     // This is the exact match of mo.md() signature 
158-     const  enterOrder : {  match : string  |  RegExp ;  stop ?: boolean  } [ ]  =  [ 
159-       {  match : "Script"  } , 
160-       {  match : "ExpressionStatement"  } , 
161-       {  match : "CallExpression"  } , 
162-       {  match : "MemberExpression"  } , 
163-       {  match : "VariableName"  } , 
164-       {  match : "."  } , 
165-       {  match : "PropertyName"  } , 
166-       {  match : "ArgList"  } , 
167-       {  match : "("  } , 
168-       {  match : / S t r i n g | F o r m a t S t r i n g / ,  stop : true  } , 
169-       {  match : ")"  } , 
170-     ] ; 
171- 
172-     let  isValid  =  true ; 
173- 
174-     // Parse the code using Lezer to check for multiple function calls and string content 
175-     tree . iterate ( { 
176-       enter : ( node )  =>  { 
177-         const  current  =  enterOrder . shift ( ) ; 
178-         if  ( current  ===  undefined )  { 
179-           // If our list is empty, but we are still going 
180-           // then this is not a valid call 
181-           isValid  =  false ; 
182-           return  false ; 
183-         } 
184- 
185-         const  match  =  current . match ; 
186- 
187-         if  ( typeof  match  ===  "string" )  { 
188-           isValid  =  isValid  &&  match  ===  node . name ; 
189-           return  isValid  &&  ! current . stop ; 
190-         } 
191- 
192-         if  ( ! match . test ( node . name ) )  { 
193-           isValid  =  false ; 
194-           return  isValid  &&  ! current . stop ; 
195-         } 
196- 
197-         return  isValid  &&  ! current . stop ; 
198-       } , 
199-     } ) ; 
200- 
201-     return  isValid ; 
64+     return  this . parser . isSupported ( pythonCode ) ; 
20265  } 
20366
20467  getExtension ( 
0 commit comments