1
1
import React , { StrictMode } from 'react' ;
2
2
import ReactDOM from 'react-dom/client' ;
3
3
import Terminal from './components/Terminal' ;
4
- import Editor from './components/Editor' ;
4
+ import Editor , { EditorItem } from './components/Editor' ;
5
5
import Plot from './components/Plot' ;
6
6
import Files from './components/Files' ;
7
7
import { Readline } from 'xterm-readline' ;
@@ -11,6 +11,7 @@ import { CanvasMessage, PagerMessage, ViewMessage, BrowseMessage } from '../webR
11
11
import { Panel , PanelGroup , PanelResizeHandle , ImperativePanelHandle } from 'react-resizable-panels' ;
12
12
import './App.css' ;
13
13
import { NamedObject , WebRDataJsAtomic } from '../webR/robj' ;
14
+ import { decodeShareData , isShareItems , ShareItem } from './components/Share' ;
14
15
15
16
const webR = new WebR ( {
16
17
RArgs : [ ] ,
@@ -22,6 +23,7 @@ const webR = new WebR({
22
23
} ,
23
24
} ) ;
24
25
( globalThis as any ) . webR = webR ;
26
+ const encoder = new TextEncoder ( ) ;
25
27
26
28
export interface TerminalInterface {
27
29
println : Readline [ 'println' ] ;
@@ -31,8 +33,9 @@ export interface TerminalInterface {
31
33
32
34
export interface FilesInterface {
33
35
refreshFilesystem : ( ) => Promise < void > ;
34
- openFileInEditor : ( name : string , path : string , readOnly : boolean ) => Promise < void > ;
35
- openDataInEditor : ( title : string , data : NamedObject < WebRDataJsAtomic < string > > ) => void ;
36
+ openFilesInEditor : ( openFiles : { name : string , path : string , readOnly ?: boolean , forceRead ?: boolean } [ ] , replace ?: boolean ) => Promise < void > ;
37
+ openContentInEditor : ( openFiles : { name : string , content : Uint8Array } [ ] , replace ?: boolean ) => void ;
38
+ openDataInEditor : ( title : string , data : NamedObject < WebRDataJsAtomic < string > > ) => void ;
36
39
openHtmlInEditor : ( src : string , path : string ) => void ;
37
40
}
38
41
@@ -50,7 +53,8 @@ const terminalInterface: TerminalInterface = {
50
53
51
54
const filesInterface : FilesInterface = {
52
55
refreshFilesystem : ( ) => Promise . resolve ( ) ,
53
- openFileInEditor : ( ) => { throw new Error ( 'Unable to open file, editor not initialised.' ) ; } ,
56
+ openFilesInEditor : ( ) => { throw new Error ( 'Unable to open file(s), editor not initialised.' ) ; } ,
57
+ openContentInEditor : ( ) => { throw new Error ( 'Unable to show content, editor not initialised.' ) ; } ,
54
58
openDataInEditor : ( ) => { throw new Error ( 'Unable to view data, editor not initialised.' ) ; } ,
55
59
openHtmlInEditor : ( ) => { throw new Error ( 'Unable to view HTML, editor not initialised.' ) ; } ,
56
60
} ;
@@ -73,7 +77,7 @@ function handleCanvasMessage(msg: CanvasMessage) {
73
77
74
78
async function handlePagerMessage ( msg : PagerMessage ) {
75
79
const { path, title, deleteFile } = msg . data ;
76
- await filesInterface . openFileInEditor ( title , path , true ) ;
80
+ await filesInterface . openFilesInEditor ( [ { name : title , path, readOnly : true } ] ) ;
77
81
if ( deleteFile ) {
78
82
await webR . FS . unlink ( path ) ;
79
83
}
@@ -99,7 +103,7 @@ async function handleBrowseMessage(msg: BrowseMessage) {
99
103
*/
100
104
const jsRegex = / < s c r i p t .* s r c = [ " ' ` ] ( .+ \. j s ) [ " ' ` ] .* > .* < \/ s c r i p t > / g;
101
105
const jsMatches = Array . from ( content . matchAll ( jsRegex ) || [ ] ) ;
102
- const jsContent : { [ idx : number ] : string } = { } ;
106
+ const jsContent : { [ idx : number ] : string } = { } ;
103
107
await Promise . all ( jsMatches . map ( ( match , idx ) => {
104
108
return webR . FS . readFile ( `${ root } /${ match [ 1 ] } ` )
105
109
. then ( ( file ) => bufferToBase64 ( file ) )
@@ -117,7 +121,7 @@ async function handleBrowseMessage(msg: BrowseMessage) {
117
121
const cssBaseStyle = `<style>body{font-family: sans-serif;}</style>` ;
118
122
const cssRegex = / < l i n k .* h r e f = [ " ' ` ] ( .+ \. c s s ) [ " ' ` ] .* > / g;
119
123
const cssMatches = Array . from ( content . matchAll ( cssRegex ) || [ ] ) ;
120
- const cssContent : { [ idx : number ] : string } = { } ;
124
+ const cssContent : { [ idx : number ] : string } = { } ;
121
125
await Promise . all ( cssMatches . map ( ( match , idx ) => {
122
126
return webR . FS . readFile ( `${ root } /${ match [ 1 ] } ` )
123
127
. then ( ( file ) => bufferToBase64 ( file ) )
@@ -127,7 +131,7 @@ async function handleBrowseMessage(msg: BrowseMessage) {
127
131
} ) ) ;
128
132
cssMatches . forEach ( ( match , idx ) => {
129
133
let cssHtml = `<link rel="stylesheet" href="${ cssContent [ idx ] } "/>` ;
130
- if ( ! injectedBaseStyle ) {
134
+ if ( ! injectedBaseStyle ) {
131
135
cssHtml = cssBaseStyle + cssHtml ;
132
136
injectedBaseStyle = true ;
133
137
}
@@ -148,36 +152,89 @@ const onPanelResize = (size: number) => {
148
152
149
153
function App ( ) {
150
154
const rightPanelRef = React . useRef < ImperativePanelHandle | null > ( null ) ;
155
+
156
+ async function applyShareData ( items : ShareItem [ ] ) : Promise < void > {
157
+ // Write files to VFS
158
+ await webR . init ( ) ;
159
+ await Promise . all ( items . map ( async ( item ) => {
160
+ return webR . FS . writeFile ( item . path , item . data ? item . data : encoder . encode ( item . text ) ) ;
161
+ } ) ) ;
162
+
163
+ // Load saved files into editor
164
+ void filesInterface . refreshFilesystem ( ) ;
165
+ void filesInterface . openFilesInEditor ( items . map ( ( item ) => ( {
166
+ name : item . name ,
167
+ path : item . path ,
168
+ forceRead : true
169
+ } ) ) , true ) ;
170
+ }
171
+
172
+ function applyShareHash ( hash : string ) : void {
173
+ const shareHash = hash . match ( / ( c o d e ) = ( [ ^ & ] + ) (?: & ( \w + ) ) ? / ) ;
174
+ if ( shareHash && shareHash [ 1 ] === 'code' ) {
175
+ const items = decodeShareData ( shareHash [ 2 ] , shareHash [ 3 ] ) ;
176
+
177
+ // Load initial content into editor
178
+ void filesInterface . openContentInEditor ( items . map ( ( item ) => ( {
179
+ name : item . name ,
180
+ content : item . data ? item . data : encoder . encode ( item . text )
181
+ } ) ) , true ) ;
182
+
183
+ void applyShareData ( items ) ;
184
+ }
185
+ }
186
+
151
187
React . useEffect ( ( ) => {
152
188
window . addEventListener ( "resize" , ( ) => {
153
189
if ( ! rightPanelRef . current ) return ;
154
190
onPanelResize ( rightPanelRef . current . getSize ( ) ) ;
155
191
} ) ;
192
+
193
+ // Show share content whenever URL hash code changes
194
+ window . addEventListener ( "hashchange" , ( event : HashChangeEvent ) => {
195
+ const url = new URL ( event . newURL ) ;
196
+ applyShareHash ( url . hash ) ;
197
+ } ) ;
198
+
199
+ // Listen for messages containing shared files data. See `encodeShareData()` for details.
200
+ window . addEventListener ( "message" , ( event : MessageEvent < { items : EditorItem [ ] } > ) => {
201
+ const items = event . data . items ;
202
+ if ( ! isShareItems ( items ) ) {
203
+ throw new Error ( "Provided postMessage data does not contain a valid set of share files." ) ;
204
+ }
205
+ void applyShareData ( items ) ;
206
+ } ) ;
207
+ } , [ ] ) ;
208
+
209
+ // Show share content on initial load
210
+ React . useEffect ( ( ) => {
211
+ const url = new URL ( window . location . href ) ;
212
+ applyShareHash ( url . hash ) ;
156
213
} , [ ] ) ;
157
214
158
215
return (
159
216
< div className = 'repl' >
160
- < PanelGroup direction = "horizontal" >
161
- < Panel defaultSize = { 50 } minSize = { 10 } >
162
- < PanelGroup autoSaveId = "conditional" direction = "vertical" >
163
- < Editor
164
- webR = { webR }
165
- terminalInterface = { terminalInterface }
166
- filesInterface = { filesInterface }
167
- />
168
- < PanelResizeHandle />
169
- < Terminal webR = { webR } terminalInterface = { terminalInterface } />
170
- </ PanelGroup >
171
- </ Panel >
172
- < PanelResizeHandle />
173
- < Panel ref = { rightPanelRef } onResize = { onPanelResize } minSize = { 10 } >
174
- < PanelGroup direction = "vertical" >
175
- < Files webR = { webR } filesInterface = { filesInterface } />
176
- < PanelResizeHandle />
177
- < Plot webR = { webR } plotInterface = { plotInterface } />
178
- </ PanelGroup >
179
- </ Panel >
180
- </ PanelGroup >
217
+ < PanelGroup direction = "horizontal" >
218
+ < Panel defaultSize = { 50 } minSize = { 10 } >
219
+ < PanelGroup autoSaveId = "conditional" direction = "vertical" >
220
+ < Editor
221
+ webR = { webR }
222
+ terminalInterface = { terminalInterface }
223
+ filesInterface = { filesInterface }
224
+ />
225
+ < PanelResizeHandle />
226
+ < Terminal webR = { webR } terminalInterface = { terminalInterface } />
227
+ </ PanelGroup >
228
+ </ Panel >
229
+ < PanelResizeHandle />
230
+ < Panel ref = { rightPanelRef } onResize = { onPanelResize } minSize = { 10 } >
231
+ < PanelGroup direction = "vertical" >
232
+ < Files webR = { webR } filesInterface = { filesInterface } />
233
+ < PanelResizeHandle />
234
+ < Plot webR = { webR } plotInterface = { plotInterface } />
235
+ </ PanelGroup >
236
+ </ Panel >
237
+ </ PanelGroup >
181
238
</ div >
182
239
) ;
183
240
}
0 commit comments