@@ -603,6 +603,168 @@ function ExportJSON() {
603603 ) ;
604604}
605605
606+ function ExportJupyterNB ( ) {
607+ const { id : repoId } = useParams ( ) ;
608+ const store = useContext ( RepoContext ) ;
609+ if ( ! store ) throw new Error ( "Missing BearContext.Provider in the tree" ) ;
610+ const repoName = useStore ( store , ( state ) => state . repoName ) ;
611+ const pods = useStore ( store , ( state ) => state . pods ) ;
612+ const filename = `${
613+ repoName || "Untitled"
614+ } -${ new Date ( ) . toISOString ( ) } .ipynb`;
615+ const [ loading , setLoading ] = useState ( false ) ;
616+
617+ const onClick = ( ) => {
618+ setLoading ( true ) ;
619+
620+ // Hard-code Jupyter cell format. Reference, https://nbformat.readthedocs.io/en/latest/format_description.html
621+ let jupyterCellList : {
622+ cell_type : string ;
623+ execution_count : number ;
624+ metadata : object ;
625+ source : string [ ] ;
626+ } [ ] = [ ] ;
627+
628+ // Queue to sort the pods geographically
629+ let q = new Array ( ) ;
630+ // adjacency list for podId -> parentId mapping
631+ let adj = { } ;
632+ q . push ( [ pods [ "ROOT" ] , "0.0" ] ) ;
633+ while ( q . length > 0 ) {
634+ let [ curPod , curScore ] = q . shift ( ) ;
635+
636+ // sort the pods geographically(top-down, left-right)
637+ let sortedChildren = curPod . children
638+ . map ( ( x ) => x . id )
639+ . sort ( ( id1 , id2 ) => {
640+ let pod1 = pods [ id1 ] ;
641+ let pod2 = pods [ id2 ] ;
642+ if ( pod1 && pod2 ) {
643+ if ( pod1 . y === pod2 . y ) {
644+ return pod1 . x - pod2 . x ;
645+ } else {
646+ return pod1 . y - pod2 . y ;
647+ }
648+ } else {
649+ return 0 ;
650+ }
651+ } ) ;
652+
653+ for ( let i = 0 ; i < sortedChildren . length ; i ++ ) {
654+ let pod = pods [ sortedChildren [ i ] ] ;
655+ let geoScore = curScore + `${ i + 1 } ` ;
656+ adj [ pod . id ] = {
657+ name : pod . name ,
658+ parentId : pod . parent ,
659+ geoScore : geoScore ,
660+ } ;
661+
662+ if ( pod . type == "SCOPE" ) {
663+ q . push ( [ pod , geoScore . substring ( 0 , 2 ) + "0" + geoScore . substring ( 2 ) ] ) ;
664+ } else if ( pod . type == "CODE" ) {
665+ jupyterCellList . push ( {
666+ cell_type : "code" ,
667+ // hard-code execution_count
668+ execution_count : 1 ,
669+ // TODO: expand other Codepod related-metadata fields, or run a real-time search in database when importing.
670+ metadata : { id : pod . id , geoScore : Number ( geoScore ) } ,
671+ source : [ pod . content || "" ] ,
672+ } ) ;
673+ } else if ( pod . type == "RICH" ) {
674+ jupyterCellList . push ( {
675+ cell_type : "markdown" ,
676+ // hard-code execution_count
677+ execution_count : 1 ,
678+ // TODO: expand other Codepod related-metadata fields, or run a real-time search in database when importing.
679+ metadata : { id : pod . id , geoScore : Number ( geoScore ) } ,
680+ source : [ pod . richContent || "" ] ,
681+ } ) ;
682+ }
683+ }
684+ }
685+
686+ // sort the generated cells by their geoScore
687+ jupyterCellList . sort ( ( cell1 , cell2 ) => {
688+ if (
689+ Number ( cell1 . metadata [ "geoScore" ] ) < Number ( cell2 . metadata [ "geoScore" ] )
690+ ) {
691+ return - 1 ;
692+ } else {
693+ return 1 ;
694+ }
695+ } ) ;
696+
697+ // Append the scope structure as comment for each cell and format source
698+ for ( const cell of jupyterCellList ) {
699+ let scopes : string [ ] = [ ] ;
700+ let parentId = adj [ cell . metadata [ "id" ] ] . parentId ;
701+
702+ // iterative {parentId,name} retrieval
703+ while ( parentId && parentId != "ROOT" ) {
704+ scopes . push ( adj [ parentId ] . name ) ;
705+ parentId = adj [ parentId ] . parentId ;
706+ }
707+
708+ // Add scope structure as a block comment at the head of each cell
709+ let scopeStructureAsComment =
710+ scopes . length > 0
711+ ? [
712+ "'''\n" ,
713+ `CodePod Scope structure: ${ scopes . reverse ( ) . join ( "/" ) } \n` ,
714+ "'''\n" ,
715+ ]
716+ : [ "" ] ;
717+
718+ const sourceArray = cell . source [ 0 ]
719+ . split ( / \r ? \n / )
720+ . map ( ( line ) => line + "\n" ) ;
721+
722+ cell . source = [ ...scopeStructureAsComment , ...sourceArray ] ;
723+ }
724+
725+ const fileContent = JSON . stringify ( {
726+ // hard-code Jupyter Notebook top-level metadata
727+ metadata : {
728+ name : repoName ,
729+ kernelspec : {
730+ name : "python3" ,
731+ display_name : "Python 3" ,
732+ } ,
733+ language_info : { name : "python" } ,
734+ Codepod_version : "v0.0.1" ,
735+ } ,
736+ nbformat : 4 ,
737+ nbformat_minor : 0 ,
738+ cells : jupyterCellList ,
739+ } ) ;
740+
741+ // Generate the download link on the fly
742+ let element = document . createElement ( "a" ) ;
743+ element . setAttribute (
744+ "href" ,
745+ "data:text/plain;charset=utf-8," + encodeURIComponent ( fileContent )
746+ ) ;
747+ element . setAttribute ( "download" , filename ) ;
748+
749+ element . style . display = "none" ;
750+ document . body . appendChild ( element ) ;
751+ element . click ( ) ;
752+ document . body . removeChild ( element ) ;
753+ } ;
754+
755+ return (
756+ < Button
757+ variant = "outlined"
758+ size = "small"
759+ color = "secondary"
760+ onClick = { onClick }
761+ disabled = { false }
762+ >
763+ Jupyter Notebook
764+ </ Button >
765+ ) ;
766+ }
767+
606768function ExportSVG ( ) {
607769 // The name should contain the name of the repo, the ID of the repo, and the current date
608770 const { id : repoId } = useParams ( ) ;
@@ -659,6 +821,7 @@ function ExportButtons() {
659821 < Stack spacing = { 1 } >
660822 < ExportFile />
661823 < ExportJSON />
824+ < ExportJupyterNB />
662825 < ExportSVG />
663826 </ Stack >
664827 ) ;
0 commit comments