@@ -534,6 +534,168 @@ function ExportJSON() {
534534 ) ;
535535}
536536
537+ function ExportJupyterNB ( ) {
538+ const { id : repoId } = useParams ( ) ;
539+ const store = useContext ( RepoContext ) ;
540+ if ( ! store ) throw new Error ( "Missing BearContext.Provider in the tree" ) ;
541+ const repoName = useStore ( store , ( state ) => state . repoName ) ;
542+ const pods = useStore ( store , ( state ) => state . pods ) ;
543+ const filename = `${
544+ repoName || "Untitled"
545+ } -${ new Date ( ) . toISOString ( ) } .ipynb`;
546+ const [ loading , setLoading ] = useState ( false ) ;
547+
548+ const onClick = ( ) => {
549+ setLoading ( true ) ;
550+
551+ // Hard-code Jupyter cell format. Reference, https://nbformat.readthedocs.io/en/latest/format_description.html
552+ let jupyterCellList : {
553+ cell_type : string ;
554+ execution_count : number ;
555+ metadata : object ;
556+ source : string [ ] ;
557+ } [ ] = [ ] ;
558+
559+ // Queue to sort the pods geographically
560+ let q = new Array ( ) ;
561+ // adjacency list for podId -> parentId mapping
562+ let adj = { } ;
563+ q . push ( [ pods [ "ROOT" ] , "0.0" ] ) ;
564+ while ( q . length > 0 ) {
565+ let [ curPod , curScore ] = q . shift ( ) ;
566+
567+ // sort the pods geographically(top-down, left-right)
568+ let sortedChildren = curPod . children
569+ . map ( ( x ) => x . id )
570+ . sort ( ( id1 , id2 ) => {
571+ let pod1 = pods [ id1 ] ;
572+ let pod2 = pods [ id2 ] ;
573+ if ( pod1 && pod2 ) {
574+ if ( pod1 . y === pod2 . y ) {
575+ return pod1 . x - pod2 . x ;
576+ } else {
577+ return pod1 . y - pod2 . y ;
578+ }
579+ } else {
580+ return 0 ;
581+ }
582+ } ) ;
583+
584+ for ( let i = 0 ; i < sortedChildren . length ; i ++ ) {
585+ let pod = pods [ sortedChildren [ i ] ] ;
586+ let geoScore = curScore + `${ i + 1 } ` ;
587+ adj [ pod . id ] = {
588+ name : pod . name ,
589+ parentId : pod . parent ,
590+ geoScore : geoScore ,
591+ } ;
592+
593+ if ( pod . type == "SCOPE" ) {
594+ q . push ( [ pod , geoScore . substring ( 0 , 2 ) + "0" + geoScore . substring ( 2 ) ] ) ;
595+ } else if ( pod . type == "CODE" ) {
596+ jupyterCellList . push ( {
597+ cell_type : "code" ,
598+ // hard-code execution_count
599+ execution_count : 1 ,
600+ // TODO: expand other Codepod related-metadata fields, or run a real-time search in database when importing.
601+ metadata : { id : pod . id , geoScore : Number ( geoScore ) } ,
602+ source : [ pod . content || "" ] ,
603+ } ) ;
604+ } else if ( pod . type == "RICH" ) {
605+ jupyterCellList . push ( {
606+ cell_type : "markdown" ,
607+ // hard-code execution_count
608+ execution_count : 1 ,
609+ // TODO: expand other Codepod related-metadata fields, or run a real-time search in database when importing.
610+ metadata : { id : pod . id , geoScore : Number ( geoScore ) } ,
611+ source : [ pod . richContent || "" ] ,
612+ } ) ;
613+ }
614+ }
615+ }
616+
617+ // sort the generated cells by their geoScore
618+ jupyterCellList . sort ( ( cell1 , cell2 ) => {
619+ if (
620+ Number ( cell1 . metadata [ "geoScore" ] ) < Number ( cell2 . metadata [ "geoScore" ] )
621+ ) {
622+ return - 1 ;
623+ } else {
624+ return 1 ;
625+ }
626+ } ) ;
627+
628+ // Append the scope structure as comment for each cell and format source
629+ for ( const cell of jupyterCellList ) {
630+ let scopes : string [ ] = [ ] ;
631+ let parentId = adj [ cell . metadata [ "id" ] ] . parentId ;
632+
633+ // iterative {parentId,name} retrieval
634+ while ( parentId && parentId != "ROOT" ) {
635+ scopes . push ( adj [ parentId ] . name ) ;
636+ parentId = adj [ parentId ] . parentId ;
637+ }
638+
639+ // Add scope structure as a block comment at the head of each cell
640+ let scopeStructureAsComment =
641+ scopes . length > 0
642+ ? [
643+ "'''\n" ,
644+ `CodePod Scope structure: ${ scopes . reverse ( ) . join ( "/" ) } \n` ,
645+ "'''\n" ,
646+ ]
647+ : [ "" ] ;
648+
649+ const sourceArray = cell . source [ 0 ]
650+ . split ( / \r ? \n / )
651+ . map ( ( line ) => line + "\n" ) ;
652+
653+ cell . source = [ ...scopeStructureAsComment , ...sourceArray ] ;
654+ }
655+
656+ const fileContent = JSON . stringify ( {
657+ // hard-code Jupyter Notebook top-level metadata
658+ metadata : {
659+ name : repoName ,
660+ kernelspec : {
661+ name : "python3" ,
662+ display_name : "Python 3" ,
663+ } ,
664+ language_info : { name : "python" } ,
665+ Codepod_version : "v0.0.1" ,
666+ } ,
667+ nbformat : 4 ,
668+ nbformat_minor : 0 ,
669+ cells : jupyterCellList ,
670+ } ) ;
671+
672+ // Generate the download link on the fly
673+ let element = document . createElement ( "a" ) ;
674+ element . setAttribute (
675+ "href" ,
676+ "data:text/plain;charset=utf-8," + encodeURIComponent ( fileContent )
677+ ) ;
678+ element . setAttribute ( "download" , filename ) ;
679+
680+ element . style . display = "none" ;
681+ document . body . appendChild ( element ) ;
682+ element . click ( ) ;
683+ document . body . removeChild ( element ) ;
684+ } ;
685+
686+ return (
687+ < Button
688+ variant = "outlined"
689+ size = "small"
690+ color = "secondary"
691+ onClick = { onClick }
692+ disabled = { false }
693+ >
694+ Jupyter Notebook
695+ </ Button >
696+ ) ;
697+ }
698+
537699function ExportSVG ( ) {
538700 // The name should contain the name of the repo, the ID of the repo, and the current date
539701 const { id : repoId } = useParams ( ) ;
@@ -590,6 +752,7 @@ function ExportButtons() {
590752 < Stack spacing = { 1 } >
591753 < ExportFile />
592754 < ExportJSON />
755+ < ExportJupyterNB />
593756 < ExportSVG />
594757 </ Stack >
595758 ) ;
0 commit comments