@@ -534,6 +534,168 @@ function ExportJSON() {
534
534
) ;
535
535
}
536
536
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
+
537
699
function ExportSVG ( ) {
538
700
// The name should contain the name of the repo, the ID of the repo, and the current date
539
701
const { id : repoId } = useParams ( ) ;
@@ -590,6 +752,7 @@ function ExportButtons() {
590
752
< Stack spacing = { 1 } >
591
753
< ExportFile />
592
754
< ExportJSON />
755
+ < ExportJupyterNB />
593
756
< ExportSVG />
594
757
</ Stack >
595
758
) ;
0 commit comments