##// END OF EJS Templates
sign notebooks
MinRK -
Show More
@@ -1,414 +1,419 b''
1 """A notebook manager that uses the local file system for storage.
1 """A notebook manager that uses the local file system for storage.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import io
20 import io
21 import itertools
21 import itertools
22 import os
22 import os
23 import glob
23 import glob
24 import shutil
24 import shutil
25
25
26 from tornado import web
26 from tornado import web
27
27
28 from .nbmanager import NotebookManager
28 from .nbmanager import NotebookManager
29 from IPython.nbformat import current
29 from IPython.nbformat import current, sign
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
30 from IPython.utils.traitlets import Unicode, Dict, Bool, TraitError
31 from IPython.utils import tz
31 from IPython.utils import tz
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Classes
34 # Classes
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 class FileNotebookManager(NotebookManager):
37 class FileNotebookManager(NotebookManager):
38
38
39 save_script = Bool(False, config=True,
39 save_script = Bool(False, config=True,
40 help="""Automatically create a Python script when saving the notebook.
40 help="""Automatically create a Python script when saving the notebook.
41
41
42 For easier use of import, %run and %load across notebooks, a
42 For easier use of import, %run and %load across notebooks, a
43 <notebook-name>.py script will be created next to any
43 <notebook-name>.py script will be created next to any
44 <notebook-name>.ipynb on each save. This can also be set with the
44 <notebook-name>.ipynb on each save. This can also be set with the
45 short `--script` flag.
45 short `--script` flag.
46 """
46 """
47 )
47 )
48
48
49 checkpoint_dir = Unicode(config=True,
49 checkpoint_dir = Unicode(config=True,
50 help="""The location in which to keep notebook checkpoints
50 help="""The location in which to keep notebook checkpoints
51
51
52 By default, it is notebook-dir/.ipynb_checkpoints
52 By default, it is notebook-dir/.ipynb_checkpoints
53 """
53 """
54 )
54 )
55 def _checkpoint_dir_default(self):
55 def _checkpoint_dir_default(self):
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
56 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
57
57
58 def _checkpoint_dir_changed(self, name, old, new):
58 def _checkpoint_dir_changed(self, name, old, new):
59 """do a bit of validation of the checkpoint dir"""
59 """do a bit of validation of the checkpoint dir"""
60 if not os.path.isabs(new):
60 if not os.path.isabs(new):
61 # If we receive a non-absolute path, make it absolute.
61 # If we receive a non-absolute path, make it absolute.
62 abs_new = os.path.abspath(new)
62 abs_new = os.path.abspath(new)
63 self.checkpoint_dir = abs_new
63 self.checkpoint_dir = abs_new
64 return
64 return
65 if os.path.exists(new) and not os.path.isdir(new):
65 if os.path.exists(new) and not os.path.isdir(new):
66 raise TraitError("checkpoint dir %r is not a directory" % new)
66 raise TraitError("checkpoint dir %r is not a directory" % new)
67 if not os.path.exists(new):
67 if not os.path.exists(new):
68 self.log.info("Creating checkpoint dir %s", new)
68 self.log.info("Creating checkpoint dir %s", new)
69 try:
69 try:
70 os.mkdir(new)
70 os.mkdir(new)
71 except:
71 except:
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
72 raise TraitError("Couldn't create checkpoint dir %r" % new)
73
73
74 def get_notebook_names(self, path=''):
74 def get_notebook_names(self, path=''):
75 """List all notebook names in the notebook dir and path."""
75 """List all notebook names in the notebook dir and path."""
76 path = path.strip('/')
76 path = path.strip('/')
77 if not os.path.isdir(self.get_os_path(path=path)):
77 if not os.path.isdir(self.get_os_path(path=path)):
78 raise web.HTTPError(404, 'Directory not found: ' + path)
78 raise web.HTTPError(404, 'Directory not found: ' + path)
79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
79 names = glob.glob(self.get_os_path('*'+self.filename_ext, path))
80 names = [os.path.basename(name)
80 names = [os.path.basename(name)
81 for name in names]
81 for name in names]
82 return names
82 return names
83
83
84 def increment_filename(self, basename, path='', ext='.ipynb'):
84 def increment_filename(self, basename, path='', ext='.ipynb'):
85 """Return a non-used filename of the form basename<int>."""
85 """Return a non-used filename of the form basename<int>."""
86 path = path.strip('/')
86 path = path.strip('/')
87 for i in itertools.count():
87 for i in itertools.count():
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
88 name = u'{basename}{i}{ext}'.format(basename=basename, i=i, ext=ext)
89 os_path = self.get_os_path(name, path)
89 os_path = self.get_os_path(name, path)
90 if not os.path.isfile(os_path):
90 if not os.path.isfile(os_path):
91 break
91 break
92 return name
92 return name
93
93
94 def path_exists(self, path):
94 def path_exists(self, path):
95 """Does the API-style path (directory) actually exist?
95 """Does the API-style path (directory) actually exist?
96
96
97 Parameters
97 Parameters
98 ----------
98 ----------
99 path : string
99 path : string
100 The path to check. This is an API path (`/` separated,
100 The path to check. This is an API path (`/` separated,
101 relative to base notebook-dir).
101 relative to base notebook-dir).
102
102
103 Returns
103 Returns
104 -------
104 -------
105 exists : bool
105 exists : bool
106 Whether the path is indeed a directory.
106 Whether the path is indeed a directory.
107 """
107 """
108 path = path.strip('/')
108 path = path.strip('/')
109 os_path = self.get_os_path(path=path)
109 os_path = self.get_os_path(path=path)
110 return os.path.isdir(os_path)
110 return os.path.isdir(os_path)
111
111
112 def get_os_path(self, name=None, path=''):
112 def get_os_path(self, name=None, path=''):
113 """Given a notebook name and a URL path, return its file system
113 """Given a notebook name and a URL path, return its file system
114 path.
114 path.
115
115
116 Parameters
116 Parameters
117 ----------
117 ----------
118 name : string
118 name : string
119 The name of a notebook file with the .ipynb extension
119 The name of a notebook file with the .ipynb extension
120 path : string
120 path : string
121 The relative URL path (with '/' as separator) to the named
121 The relative URL path (with '/' as separator) to the named
122 notebook.
122 notebook.
123
123
124 Returns
124 Returns
125 -------
125 -------
126 path : string
126 path : string
127 A file system path that combines notebook_dir (location where
127 A file system path that combines notebook_dir (location where
128 server started), the relative path, and the filename with the
128 server started), the relative path, and the filename with the
129 current operating system's url.
129 current operating system's url.
130 """
130 """
131 parts = path.strip('/').split('/')
131 parts = path.strip('/').split('/')
132 parts = [p for p in parts if p != ''] # remove duplicate splits
132 parts = [p for p in parts if p != ''] # remove duplicate splits
133 if name is not None:
133 if name is not None:
134 parts.append(name)
134 parts.append(name)
135 path = os.path.join(self.notebook_dir, *parts)
135 path = os.path.join(self.notebook_dir, *parts)
136 return path
136 return path
137
137
138 def notebook_exists(self, name, path=''):
138 def notebook_exists(self, name, path=''):
139 """Returns a True if the notebook exists. Else, returns False.
139 """Returns a True if the notebook exists. Else, returns False.
140
140
141 Parameters
141 Parameters
142 ----------
142 ----------
143 name : string
143 name : string
144 The name of the notebook you are checking.
144 The name of the notebook you are checking.
145 path : string
145 path : string
146 The relative path to the notebook (with '/' as separator)
146 The relative path to the notebook (with '/' as separator)
147
147
148 Returns
148 Returns
149 -------
149 -------
150 bool
150 bool
151 """
151 """
152 path = path.strip('/')
152 path = path.strip('/')
153 nbpath = self.get_os_path(name, path=path)
153 nbpath = self.get_os_path(name, path=path)
154 return os.path.isfile(nbpath)
154 return os.path.isfile(nbpath)
155
155
156 def list_notebooks(self, path):
156 def list_notebooks(self, path):
157 """Returns a list of dictionaries that are the standard model
157 """Returns a list of dictionaries that are the standard model
158 for all notebooks in the relative 'path'.
158 for all notebooks in the relative 'path'.
159
159
160 Parameters
160 Parameters
161 ----------
161 ----------
162 path : str
162 path : str
163 the URL path that describes the relative path for the
163 the URL path that describes the relative path for the
164 listed notebooks
164 listed notebooks
165
165
166 Returns
166 Returns
167 -------
167 -------
168 notebooks : list of dicts
168 notebooks : list of dicts
169 a list of the notebook models without 'content'
169 a list of the notebook models without 'content'
170 """
170 """
171 path = path.strip('/')
171 path = path.strip('/')
172 notebook_names = self.get_notebook_names(path)
172 notebook_names = self.get_notebook_names(path)
173 notebooks = []
173 notebooks = []
174 for name in notebook_names:
174 for name in notebook_names:
175 model = self.get_notebook_model(name, path, content=False)
175 model = self.get_notebook_model(name, path, content=False)
176 notebooks.append(model)
176 notebooks.append(model)
177 notebooks = sorted(notebooks, key=lambda item: item['name'])
177 notebooks = sorted(notebooks, key=lambda item: item['name'])
178 return notebooks
178 return notebooks
179
179
180 def get_notebook_model(self, name, path='', content=True):
180 def get_notebook_model(self, name, path='', content=True):
181 """ Takes a path and name for a notebook and returns its model
181 """ Takes a path and name for a notebook and returns its model
182
182
183 Parameters
183 Parameters
184 ----------
184 ----------
185 name : str
185 name : str
186 the name of the notebook
186 the name of the notebook
187 path : str
187 path : str
188 the URL path that describes the relative path for
188 the URL path that describes the relative path for
189 the notebook
189 the notebook
190
190
191 Returns
191 Returns
192 -------
192 -------
193 model : dict
193 model : dict
194 the notebook model. If contents=True, returns the 'contents'
194 the notebook model. If contents=True, returns the 'contents'
195 dict in the model as well.
195 dict in the model as well.
196 """
196 """
197 path = path.strip('/')
197 path = path.strip('/')
198 if not self.notebook_exists(name=name, path=path):
198 if not self.notebook_exists(name=name, path=path):
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
199 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
200 os_path = self.get_os_path(name, path)
200 os_path = self.get_os_path(name, path)
201 info = os.stat(os_path)
201 info = os.stat(os_path)
202 last_modified = tz.utcfromtimestamp(info.st_mtime)
202 last_modified = tz.utcfromtimestamp(info.st_mtime)
203 created = tz.utcfromtimestamp(info.st_ctime)
203 created = tz.utcfromtimestamp(info.st_ctime)
204 # Create the notebook model.
204 # Create the notebook model.
205 model ={}
205 model ={}
206 model['name'] = name
206 model['name'] = name
207 model['path'] = path
207 model['path'] = path
208 model['last_modified'] = last_modified
208 model['last_modified'] = last_modified
209 model['created'] = created
209 model['created'] = created
210 if content is True:
210 if content:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
211 with io.open(os_path, 'r', encoding='utf-8') as f:
212 try:
212 try:
213 nb = current.read(f, u'json')
213 nb = current.read(f, u'json')
214 except Exception as e:
214 except Exception as e:
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
215 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
216 model['content'] = nb
216 model['content'] = nb
217 sign.mark_trusted_cells(nb, self.secret)
217 return model
218 return model
218
219
219 def save_notebook_model(self, model, name='', path=''):
220 def save_notebook_model(self, model, name='', path=''):
220 """Save the notebook model and return the model with no content."""
221 """Save the notebook model and return the model with no content."""
221 path = path.strip('/')
222 path = path.strip('/')
222
223
223 if 'content' not in model:
224 if 'content' not in model:
224 raise web.HTTPError(400, u'No notebook JSON data provided')
225 raise web.HTTPError(400, u'No notebook JSON data provided')
225
226
226 # One checkpoint should always exist
227 # One checkpoint should always exist
227 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
228 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
228 self.create_checkpoint(name, path)
229 self.create_checkpoint(name, path)
229
230
230 new_path = model.get('path', path).strip('/')
231 new_path = model.get('path', path).strip('/')
231 new_name = model.get('name', name)
232 new_name = model.get('name', name)
232
233
233 if path != new_path or name != new_name:
234 if path != new_path or name != new_name:
234 self.rename_notebook(name, path, new_name, new_path)
235 self.rename_notebook(name, path, new_name, new_path)
235
236
236 # Save the notebook file
237 # Save the notebook file
237 os_path = self.get_os_path(new_name, new_path)
238 os_path = self.get_os_path(new_name, new_path)
238 nb = current.to_notebook_json(model['content'])
239 nb = current.to_notebook_json(model['content'])
240
241 if sign.check_trusted_cells(nb):
242 sign.trust_notebook(nb, self.secret, self.signature_scheme)
243
239 if 'name' in nb['metadata']:
244 if 'name' in nb['metadata']:
240 nb['metadata']['name'] = u''
245 nb['metadata']['name'] = u''
241 try:
246 try:
242 self.log.debug("Autosaving notebook %s", os_path)
247 self.log.debug("Autosaving notebook %s", os_path)
243 with io.open(os_path, 'w', encoding='utf-8') as f:
248 with io.open(os_path, 'w', encoding='utf-8') as f:
244 current.write(nb, f, u'json')
249 current.write(nb, f, u'json')
245 except Exception as e:
250 except Exception as e:
246 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
251 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
247
252
248 # Save .py script as well
253 # Save .py script as well
249 if self.save_script:
254 if self.save_script:
250 py_path = os.path.splitext(os_path)[0] + '.py'
255 py_path = os.path.splitext(os_path)[0] + '.py'
251 self.log.debug("Writing script %s", py_path)
256 self.log.debug("Writing script %s", py_path)
252 try:
257 try:
253 with io.open(py_path, 'w', encoding='utf-8') as f:
258 with io.open(py_path, 'w', encoding='utf-8') as f:
254 current.write(nb, f, u'py')
259 current.write(nb, f, u'py')
255 except Exception as e:
260 except Exception as e:
256 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
261 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
257
262
258 model = self.get_notebook_model(new_name, new_path, content=False)
263 model = self.get_notebook_model(new_name, new_path, content=False)
259 return model
264 return model
260
265
261 def update_notebook_model(self, model, name, path=''):
266 def update_notebook_model(self, model, name, path=''):
262 """Update the notebook's path and/or name"""
267 """Update the notebook's path and/or name"""
263 path = path.strip('/')
268 path = path.strip('/')
264 new_name = model.get('name', name)
269 new_name = model.get('name', name)
265 new_path = model.get('path', path).strip('/')
270 new_path = model.get('path', path).strip('/')
266 if path != new_path or name != new_name:
271 if path != new_path or name != new_name:
267 self.rename_notebook(name, path, new_name, new_path)
272 self.rename_notebook(name, path, new_name, new_path)
268 model = self.get_notebook_model(new_name, new_path, content=False)
273 model = self.get_notebook_model(new_name, new_path, content=False)
269 return model
274 return model
270
275
271 def delete_notebook_model(self, name, path=''):
276 def delete_notebook_model(self, name, path=''):
272 """Delete notebook by name and path."""
277 """Delete notebook by name and path."""
273 path = path.strip('/')
278 path = path.strip('/')
274 os_path = self.get_os_path(name, path)
279 os_path = self.get_os_path(name, path)
275 if not os.path.isfile(os_path):
280 if not os.path.isfile(os_path):
276 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
281 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
277
282
278 # clear checkpoints
283 # clear checkpoints
279 for checkpoint in self.list_checkpoints(name, path):
284 for checkpoint in self.list_checkpoints(name, path):
280 checkpoint_id = checkpoint['id']
285 checkpoint_id = checkpoint['id']
281 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
286 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
282 if os.path.isfile(cp_path):
287 if os.path.isfile(cp_path):
283 self.log.debug("Unlinking checkpoint %s", cp_path)
288 self.log.debug("Unlinking checkpoint %s", cp_path)
284 os.unlink(cp_path)
289 os.unlink(cp_path)
285
290
286 self.log.debug("Unlinking notebook %s", os_path)
291 self.log.debug("Unlinking notebook %s", os_path)
287 os.unlink(os_path)
292 os.unlink(os_path)
288
293
289 def rename_notebook(self, old_name, old_path, new_name, new_path):
294 def rename_notebook(self, old_name, old_path, new_name, new_path):
290 """Rename a notebook."""
295 """Rename a notebook."""
291 old_path = old_path.strip('/')
296 old_path = old_path.strip('/')
292 new_path = new_path.strip('/')
297 new_path = new_path.strip('/')
293 if new_name == old_name and new_path == old_path:
298 if new_name == old_name and new_path == old_path:
294 return
299 return
295
300
296 new_os_path = self.get_os_path(new_name, new_path)
301 new_os_path = self.get_os_path(new_name, new_path)
297 old_os_path = self.get_os_path(old_name, old_path)
302 old_os_path = self.get_os_path(old_name, old_path)
298
303
299 # Should we proceed with the move?
304 # Should we proceed with the move?
300 if os.path.isfile(new_os_path):
305 if os.path.isfile(new_os_path):
301 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
306 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
302 if self.save_script:
307 if self.save_script:
303 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
308 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
304 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
309 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
305 if os.path.isfile(new_py_path):
310 if os.path.isfile(new_py_path):
306 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
311 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
307
312
308 # Move the notebook file
313 # Move the notebook file
309 try:
314 try:
310 os.rename(old_os_path, new_os_path)
315 os.rename(old_os_path, new_os_path)
311 except Exception as e:
316 except Exception as e:
312 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
317 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
313
318
314 # Move the checkpoints
319 # Move the checkpoints
315 old_checkpoints = self.list_checkpoints(old_name, old_path)
320 old_checkpoints = self.list_checkpoints(old_name, old_path)
316 for cp in old_checkpoints:
321 for cp in old_checkpoints:
317 checkpoint_id = cp['id']
322 checkpoint_id = cp['id']
318 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
323 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
319 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
324 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
320 if os.path.isfile(old_cp_path):
325 if os.path.isfile(old_cp_path):
321 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
326 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
322 os.rename(old_cp_path, new_cp_path)
327 os.rename(old_cp_path, new_cp_path)
323
328
324 # Move the .py script
329 # Move the .py script
325 if self.save_script:
330 if self.save_script:
326 os.rename(old_py_path, new_py_path)
331 os.rename(old_py_path, new_py_path)
327
332
328 # Checkpoint-related utilities
333 # Checkpoint-related utilities
329
334
330 def get_checkpoint_path(self, checkpoint_id, name, path=''):
335 def get_checkpoint_path(self, checkpoint_id, name, path=''):
331 """find the path to a checkpoint"""
336 """find the path to a checkpoint"""
332 path = path.strip('/')
337 path = path.strip('/')
333 basename, _ = os.path.splitext(name)
338 basename, _ = os.path.splitext(name)
334 filename = u"{name}-{checkpoint_id}{ext}".format(
339 filename = u"{name}-{checkpoint_id}{ext}".format(
335 name=basename,
340 name=basename,
336 checkpoint_id=checkpoint_id,
341 checkpoint_id=checkpoint_id,
337 ext=self.filename_ext,
342 ext=self.filename_ext,
338 )
343 )
339 cp_path = os.path.join(path, self.checkpoint_dir, filename)
344 cp_path = os.path.join(path, self.checkpoint_dir, filename)
340 return cp_path
345 return cp_path
341
346
342 def get_checkpoint_model(self, checkpoint_id, name, path=''):
347 def get_checkpoint_model(self, checkpoint_id, name, path=''):
343 """construct the info dict for a given checkpoint"""
348 """construct the info dict for a given checkpoint"""
344 path = path.strip('/')
349 path = path.strip('/')
345 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
350 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
346 stats = os.stat(cp_path)
351 stats = os.stat(cp_path)
347 last_modified = tz.utcfromtimestamp(stats.st_mtime)
352 last_modified = tz.utcfromtimestamp(stats.st_mtime)
348 info = dict(
353 info = dict(
349 id = checkpoint_id,
354 id = checkpoint_id,
350 last_modified = last_modified,
355 last_modified = last_modified,
351 )
356 )
352 return info
357 return info
353
358
354 # public checkpoint API
359 # public checkpoint API
355
360
356 def create_checkpoint(self, name, path=''):
361 def create_checkpoint(self, name, path=''):
357 """Create a checkpoint from the current state of a notebook"""
362 """Create a checkpoint from the current state of a notebook"""
358 path = path.strip('/')
363 path = path.strip('/')
359 nb_path = self.get_os_path(name, path)
364 nb_path = self.get_os_path(name, path)
360 # only the one checkpoint ID:
365 # only the one checkpoint ID:
361 checkpoint_id = u"checkpoint"
366 checkpoint_id = u"checkpoint"
362 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
367 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
363 self.log.debug("creating checkpoint for notebook %s", name)
368 self.log.debug("creating checkpoint for notebook %s", name)
364 if not os.path.exists(self.checkpoint_dir):
369 if not os.path.exists(self.checkpoint_dir):
365 os.mkdir(self.checkpoint_dir)
370 os.mkdir(self.checkpoint_dir)
366 shutil.copy2(nb_path, cp_path)
371 shutil.copy2(nb_path, cp_path)
367
372
368 # return the checkpoint info
373 # return the checkpoint info
369 return self.get_checkpoint_model(checkpoint_id, name, path)
374 return self.get_checkpoint_model(checkpoint_id, name, path)
370
375
371 def list_checkpoints(self, name, path=''):
376 def list_checkpoints(self, name, path=''):
372 """list the checkpoints for a given notebook
377 """list the checkpoints for a given notebook
373
378
374 This notebook manager currently only supports one checkpoint per notebook.
379 This notebook manager currently only supports one checkpoint per notebook.
375 """
380 """
376 path = path.strip('/')
381 path = path.strip('/')
377 checkpoint_id = "checkpoint"
382 checkpoint_id = "checkpoint"
378 path = self.get_checkpoint_path(checkpoint_id, name, path)
383 path = self.get_checkpoint_path(checkpoint_id, name, path)
379 if not os.path.exists(path):
384 if not os.path.exists(path):
380 return []
385 return []
381 else:
386 else:
382 return [self.get_checkpoint_model(checkpoint_id, name, path)]
387 return [self.get_checkpoint_model(checkpoint_id, name, path)]
383
388
384
389
385 def restore_checkpoint(self, checkpoint_id, name, path=''):
390 def restore_checkpoint(self, checkpoint_id, name, path=''):
386 """restore a notebook to a checkpointed state"""
391 """restore a notebook to a checkpointed state"""
387 path = path.strip('/')
392 path = path.strip('/')
388 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
393 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
389 nb_path = self.get_os_path(name, path)
394 nb_path = self.get_os_path(name, path)
390 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
395 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
391 if not os.path.isfile(cp_path):
396 if not os.path.isfile(cp_path):
392 self.log.debug("checkpoint file does not exist: %s", cp_path)
397 self.log.debug("checkpoint file does not exist: %s", cp_path)
393 raise web.HTTPError(404,
398 raise web.HTTPError(404,
394 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
399 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
395 )
400 )
396 # ensure notebook is readable (never restore from an unreadable notebook)
401 # ensure notebook is readable (never restore from an unreadable notebook)
397 with io.open(cp_path, 'r', encoding='utf-8') as f:
402 with io.open(cp_path, 'r', encoding='utf-8') as f:
398 nb = current.read(f, u'json')
403 nb = current.read(f, u'json')
399 shutil.copy2(cp_path, nb_path)
404 shutil.copy2(cp_path, nb_path)
400 self.log.debug("copying %s -> %s", cp_path, nb_path)
405 self.log.debug("copying %s -> %s", cp_path, nb_path)
401
406
402 def delete_checkpoint(self, checkpoint_id, name, path=''):
407 def delete_checkpoint(self, checkpoint_id, name, path=''):
403 """delete a notebook's checkpoint"""
408 """delete a notebook's checkpoint"""
404 path = path.strip('/')
409 path = path.strip('/')
405 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
410 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
406 if not os.path.isfile(cp_path):
411 if not os.path.isfile(cp_path):
407 raise web.HTTPError(404,
412 raise web.HTTPError(404,
408 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
413 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
409 )
414 )
410 self.log.debug("unlinking %s", cp_path)
415 self.log.debug("unlinking %s", cp_path)
411 os.unlink(cp_path)
416 os.unlink(cp_path)
412
417
413 def info_string(self):
418 def info_string(self):
414 return "Serving notebooks from local directory: %s" % self.notebook_dir
419 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,174 +1,207 b''
1 """A base class notebook manager.
1 """A base class notebook manager.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 * Zach Sailer
6 * Zach Sailer
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10 # Copyright (C) 2011 The IPython Development Team
10 # Copyright (C) 2011 The IPython Development Team
11 #
11 #
12 # Distributed under the terms of the BSD License. The full license is in
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15
15
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 # Imports
17 # Imports
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19
19
20 import base64
21 import hashlib
22 import io
20 import os
23 import os
21
24
22 from IPython.config.configurable import LoggingConfigurable
25 from IPython.config.configurable import LoggingConfigurable
26 from IPython.core.application import BaseIPythonApplication
23 from IPython.nbformat import current
27 from IPython.nbformat import current
24 from IPython.utils import py3compat
28 from IPython.utils import py3compat
25 from IPython.utils.traitlets import Unicode, TraitError
29 from IPython.utils.traitlets import Unicode, TraitError, Enum, Bytes
26
30
27 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
28 # Classes
32 # Classes
29 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
30
34
31 class NotebookManager(LoggingConfigurable):
35 class NotebookManager(LoggingConfigurable):
32
36
33 # Todo:
37 # Todo:
34 # The notebook_dir attribute is used to mean a couple of different things:
38 # The notebook_dir attribute is used to mean a couple of different things:
35 # 1. Where the notebooks are stored if FileNotebookManager is used.
39 # 1. Where the notebooks are stored if FileNotebookManager is used.
36 # 2. The cwd of the kernel for a project.
40 # 2. The cwd of the kernel for a project.
37 # Right now we use this attribute in a number of different places and
41 # Right now we use this attribute in a number of different places and
38 # we are going to have to disentangle all of this.
42 # we are going to have to disentangle all of this.
39 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
43 notebook_dir = Unicode(py3compat.getcwd(), config=True, help="""
40 The directory to use for notebooks.
44 The directory to use for notebooks.
41 """)
45 """)
42
46
43 filename_ext = Unicode(u'.ipynb')
47 filename_ext = Unicode(u'.ipynb')
44
48
49 signature_scheme = Enum(hashlib.algorithms, default_value='sha256', config=True,
50 help="""The signature scheme used to sign notebooks."""
51 )
52
53 secret = Bytes(config=True,
54 help="""The secret key with which notebooks are signed."""
55 )
56 def _secret_default(self):
57 # note : this assumes an Application is running
58 profile_dir = BaseIPythonApplication.instance().profile_dir
59 secret_file = os.path.join(profile_dir.security_dir, 'notebook_secret')
60 if os.path.exists(secret_file):
61 with io.open(secret_file, 'rb') as f:
62 return f.read()
63 else:
64 secret = base64.encodestring(os.urandom(1024))
65 self.log.info("Writing output secret to %s", secret_file)
66 with io.open(secret_file, 'wb') as f:
67 f.write(secret)
68 try:
69 os.chmod(secret_file, 0o600)
70 except OSError:
71 self.log.warn(
72 "Could not set permissions on %s",
73 secret_file
74 )
75 return secret
76
77
45 def path_exists(self, path):
78 def path_exists(self, path):
46 """Does the API-style path (directory) actually exist?
79 """Does the API-style path (directory) actually exist?
47
80
48 Override this method in subclasses.
81 Override this method in subclasses.
49
82
50 Parameters
83 Parameters
51 ----------
84 ----------
52 path : string
85 path : string
53 The
86 The
54
87
55 Returns
88 Returns
56 -------
89 -------
57 exists : bool
90 exists : bool
58 Whether the path does indeed exist.
91 Whether the path does indeed exist.
59 """
92 """
60 raise NotImplementedError
93 raise NotImplementedError
61
94
62 def _notebook_dir_changed(self, name, old, new):
95 def _notebook_dir_changed(self, name, old, new):
63 """Do a bit of validation of the notebook dir."""
96 """Do a bit of validation of the notebook dir."""
64 if not os.path.isabs(new):
97 if not os.path.isabs(new):
65 # If we receive a non-absolute path, make it absolute.
98 # If we receive a non-absolute path, make it absolute.
66 self.notebook_dir = os.path.abspath(new)
99 self.notebook_dir = os.path.abspath(new)
67 return
100 return
68 if os.path.exists(new) and not os.path.isdir(new):
101 if os.path.exists(new) and not os.path.isdir(new):
69 raise TraitError("notebook dir %r is not a directory" % new)
102 raise TraitError("notebook dir %r is not a directory" % new)
70 if not os.path.exists(new):
103 if not os.path.exists(new):
71 self.log.info("Creating notebook dir %s", new)
104 self.log.info("Creating notebook dir %s", new)
72 try:
105 try:
73 os.mkdir(new)
106 os.mkdir(new)
74 except:
107 except:
75 raise TraitError("Couldn't create notebook dir %r" % new)
108 raise TraitError("Couldn't create notebook dir %r" % new)
76
109
77 # Main notebook API
110 # Main notebook API
78
111
79 def increment_filename(self, basename, path=''):
112 def increment_filename(self, basename, path=''):
80 """Increment a notebook filename without the .ipynb to make it unique.
113 """Increment a notebook filename without the .ipynb to make it unique.
81
114
82 Parameters
115 Parameters
83 ----------
116 ----------
84 basename : unicode
117 basename : unicode
85 The name of a notebook without the ``.ipynb`` file extension.
118 The name of a notebook without the ``.ipynb`` file extension.
86 path : unicode
119 path : unicode
87 The URL path of the notebooks directory
120 The URL path of the notebooks directory
88 """
121 """
89 return basename
122 return basename
90
123
91 def list_notebooks(self, path=''):
124 def list_notebooks(self, path=''):
92 """Return a list of notebook dicts without content.
125 """Return a list of notebook dicts without content.
93
126
94 This returns a list of dicts, each of the form::
127 This returns a list of dicts, each of the form::
95
128
96 dict(notebook_id=notebook,name=name)
129 dict(notebook_id=notebook,name=name)
97
130
98 This list of dicts should be sorted by name::
131 This list of dicts should be sorted by name::
99
132
100 data = sorted(data, key=lambda item: item['name'])
133 data = sorted(data, key=lambda item: item['name'])
101 """
134 """
102 raise NotImplementedError('must be implemented in a subclass')
135 raise NotImplementedError('must be implemented in a subclass')
103
136
104 def get_notebook_model(self, name, path='', content=True):
137 def get_notebook_model(self, name, path='', content=True):
105 """Get the notebook model with or without content."""
138 """Get the notebook model with or without content."""
106 raise NotImplementedError('must be implemented in a subclass')
139 raise NotImplementedError('must be implemented in a subclass')
107
140
108 def save_notebook_model(self, model, name, path=''):
141 def save_notebook_model(self, model, name, path=''):
109 """Save the notebook model and return the model with no content."""
142 """Save the notebook model and return the model with no content."""
110 raise NotImplementedError('must be implemented in a subclass')
143 raise NotImplementedError('must be implemented in a subclass')
111
144
112 def update_notebook_model(self, model, name, path=''):
145 def update_notebook_model(self, model, name, path=''):
113 """Update the notebook model and return the model with no content."""
146 """Update the notebook model and return the model with no content."""
114 raise NotImplementedError('must be implemented in a subclass')
147 raise NotImplementedError('must be implemented in a subclass')
115
148
116 def delete_notebook_model(self, name, path=''):
149 def delete_notebook_model(self, name, path=''):
117 """Delete notebook by name and path."""
150 """Delete notebook by name and path."""
118 raise NotImplementedError('must be implemented in a subclass')
151 raise NotImplementedError('must be implemented in a subclass')
119
152
120 def create_notebook_model(self, model=None, path=''):
153 def create_notebook_model(self, model=None, path=''):
121 """Create a new notebook and return its model with no content."""
154 """Create a new notebook and return its model with no content."""
122 path = path.strip('/')
155 path = path.strip('/')
123 if model is None:
156 if model is None:
124 model = {}
157 model = {}
125 if 'content' not in model:
158 if 'content' not in model:
126 metadata = current.new_metadata(name=u'')
159 metadata = current.new_metadata(name=u'')
127 model['content'] = current.new_notebook(metadata=metadata)
160 model['content'] = current.new_notebook(metadata=metadata)
128 if 'name' not in model:
161 if 'name' not in model:
129 model['name'] = self.increment_filename('Untitled', path)
162 model['name'] = self.increment_filename('Untitled', path)
130
163
131 model['path'] = path
164 model['path'] = path
132 model = self.save_notebook_model(model, model['name'], model['path'])
165 model = self.save_notebook_model(model, model['name'], model['path'])
133 return model
166 return model
134
167
135 def copy_notebook(self, from_name, to_name=None, path=''):
168 def copy_notebook(self, from_name, to_name=None, path=''):
136 """Copy an existing notebook and return its new model.
169 """Copy an existing notebook and return its new model.
137
170
138 If to_name not specified, increment `from_name-Copy#.ipynb`.
171 If to_name not specified, increment `from_name-Copy#.ipynb`.
139 """
172 """
140 path = path.strip('/')
173 path = path.strip('/')
141 model = self.get_notebook_model(from_name, path)
174 model = self.get_notebook_model(from_name, path)
142 if not to_name:
175 if not to_name:
143 base = os.path.splitext(from_name)[0] + '-Copy'
176 base = os.path.splitext(from_name)[0] + '-Copy'
144 to_name = self.increment_filename(base, path)
177 to_name = self.increment_filename(base, path)
145 model['name'] = to_name
178 model['name'] = to_name
146 model = self.save_notebook_model(model, to_name, path)
179 model = self.save_notebook_model(model, to_name, path)
147 return model
180 return model
148
181
149 # Checkpoint-related
182 # Checkpoint-related
150
183
151 def create_checkpoint(self, name, path=''):
184 def create_checkpoint(self, name, path=''):
152 """Create a checkpoint of the current state of a notebook
185 """Create a checkpoint of the current state of a notebook
153
186
154 Returns a checkpoint_id for the new checkpoint.
187 Returns a checkpoint_id for the new checkpoint.
155 """
188 """
156 raise NotImplementedError("must be implemented in a subclass")
189 raise NotImplementedError("must be implemented in a subclass")
157
190
158 def list_checkpoints(self, name, path=''):
191 def list_checkpoints(self, name, path=''):
159 """Return a list of checkpoints for a given notebook"""
192 """Return a list of checkpoints for a given notebook"""
160 return []
193 return []
161
194
162 def restore_checkpoint(self, checkpoint_id, name, path=''):
195 def restore_checkpoint(self, checkpoint_id, name, path=''):
163 """Restore a notebook from one of its checkpoints"""
196 """Restore a notebook from one of its checkpoints"""
164 raise NotImplementedError("must be implemented in a subclass")
197 raise NotImplementedError("must be implemented in a subclass")
165
198
166 def delete_checkpoint(self, checkpoint_id, name, path=''):
199 def delete_checkpoint(self, checkpoint_id, name, path=''):
167 """delete a checkpoint for a notebook"""
200 """delete a checkpoint for a notebook"""
168 raise NotImplementedError("must be implemented in a subclass")
201 raise NotImplementedError("must be implemented in a subclass")
169
202
170 def log_info(self):
203 def log_info(self):
171 self.log.info(self.info_string())
204 self.log.info(self.info_string())
172
205
173 def info_string(self):
206 def info_string(self):
174 return "Serving notebooks"
207 return "Serving notebooks"
@@ -1,563 +1,565 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2011 The IPython Development Team
2 // Copyright (C) 2008-2011 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // CodeCell
9 // CodeCell
10 //============================================================================
10 //============================================================================
11 /**
11 /**
12 * An extendable module that provide base functionnality to create cell for notebook.
12 * An extendable module that provide base functionnality to create cell for notebook.
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule CodeCell
15 * @submodule CodeCell
16 */
16 */
17
17
18
18
19 /* local util for codemirror */
19 /* local util for codemirror */
20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
21
21
22 /**
22 /**
23 *
23 *
24 * function to delete until previous non blanking space character
24 * function to delete until previous non blanking space character
25 * or first multiple of 4 tabstop.
25 * or first multiple of 4 tabstop.
26 * @private
26 * @private
27 */
27 */
28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
30 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 var tabsize = cm.getOption('tabSize');
32 var tabsize = cm.getOption('tabSize');
33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
34 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
35 var select = cm.getRange(from,cur);
35 var select = cm.getRange(from,cur);
36 if( select.match(/^\ +$/) !== null){
36 if( select.match(/^\ +$/) !== null){
37 cm.replaceRange("",from,cur);
37 cm.replaceRange("",from,cur);
38 } else {
38 } else {
39 cm.deleteH(-1,"char");
39 cm.deleteH(-1,"char");
40 }
40 }
41 };
41 };
42
42
43
43
44 var IPython = (function (IPython) {
44 var IPython = (function (IPython) {
45 "use strict";
45 "use strict";
46
46
47 var utils = IPython.utils;
47 var utils = IPython.utils;
48 var key = IPython.utils.keycodes;
48 var key = IPython.utils.keycodes;
49
49
50 /**
50 /**
51 * A Cell conceived to write code.
51 * A Cell conceived to write code.
52 *
52 *
53 * The kernel doesn't have to be set at creation time, in that case
53 * The kernel doesn't have to be set at creation time, in that case
54 * it will be null and set_kernel has to be called later.
54 * it will be null and set_kernel has to be called later.
55 * @class CodeCell
55 * @class CodeCell
56 * @extends IPython.Cell
56 * @extends IPython.Cell
57 *
57 *
58 * @constructor
58 * @constructor
59 * @param {Object|null} kernel
59 * @param {Object|null} kernel
60 * @param {object|undefined} [options]
60 * @param {object|undefined} [options]
61 * @param [options.cm_config] {object} config to pass to CodeMirror
61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 */
62 */
63 var CodeCell = function (kernel, options) {
63 var CodeCell = function (kernel, options) {
64 this.kernel = kernel || null;
64 this.kernel = kernel || null;
65 this.collapsed = false;
65 this.collapsed = false;
66
66
67 // create all attributed in constructor function
67 // create all attributed in constructor function
68 // even if null for V8 VM optimisation
68 // even if null for V8 VM optimisation
69 this.input_prompt_number = null;
69 this.input_prompt_number = null;
70 this.celltoolbar = null;
70 this.celltoolbar = null;
71 this.output_area = null;
71 this.output_area = null;
72 this.last_msg_id = null;
72 this.last_msg_id = null;
73 this.completer = null;
73 this.completer = null;
74
74
75
75
76 var cm_overwrite_options = {
76 var cm_overwrite_options = {
77 onKeyEvent: $.proxy(this.handle_keyevent,this)
77 onKeyEvent: $.proxy(this.handle_keyevent,this)
78 };
78 };
79
79
80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
80 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
81
81
82 IPython.Cell.apply(this,[options]);
82 IPython.Cell.apply(this,[options]);
83
83
84 // Attributes we want to override in this subclass.
84 // Attributes we want to override in this subclass.
85 this.cell_type = "code";
85 this.cell_type = "code";
86
86
87 var that = this;
87 var that = this;
88 this.element.focusout(
88 this.element.focusout(
89 function() { that.auto_highlight(); }
89 function() { that.auto_highlight(); }
90 );
90 );
91 };
91 };
92
92
93 CodeCell.options_default = {
93 CodeCell.options_default = {
94 cm_config : {
94 cm_config : {
95 extraKeys: {
95 extraKeys: {
96 "Tab" : "indentMore",
96 "Tab" : "indentMore",
97 "Shift-Tab" : "indentLess",
97 "Shift-Tab" : "indentLess",
98 "Backspace" : "delSpaceToPrevTabStop",
98 "Backspace" : "delSpaceToPrevTabStop",
99 "Cmd-/" : "toggleComment",
99 "Cmd-/" : "toggleComment",
100 "Ctrl-/" : "toggleComment"
100 "Ctrl-/" : "toggleComment"
101 },
101 },
102 mode: 'ipython',
102 mode: 'ipython',
103 theme: 'ipython',
103 theme: 'ipython',
104 matchBrackets: true
104 matchBrackets: true
105 }
105 }
106 };
106 };
107
107
108 CodeCell.msg_cells = {};
108 CodeCell.msg_cells = {};
109
109
110 CodeCell.prototype = new IPython.Cell();
110 CodeCell.prototype = new IPython.Cell();
111
111
112 /**
112 /**
113 * @method auto_highlight
113 * @method auto_highlight
114 */
114 */
115 CodeCell.prototype.auto_highlight = function () {
115 CodeCell.prototype.auto_highlight = function () {
116 this._auto_highlight(IPython.config.cell_magic_highlight);
116 this._auto_highlight(IPython.config.cell_magic_highlight);
117 };
117 };
118
118
119 /** @method create_element */
119 /** @method create_element */
120 CodeCell.prototype.create_element = function () {
120 CodeCell.prototype.create_element = function () {
121 IPython.Cell.prototype.create_element.apply(this, arguments);
121 IPython.Cell.prototype.create_element.apply(this, arguments);
122
122
123 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
123 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
124 cell.attr('tabindex','2');
124 cell.attr('tabindex','2');
125
125
126 var input = $('<div></div>').addClass('input');
126 var input = $('<div></div>').addClass('input');
127 var prompt = $('<div/>').addClass('prompt input_prompt');
127 var prompt = $('<div/>').addClass('prompt input_prompt');
128 var inner_cell = $('<div/>').addClass('inner_cell');
128 var inner_cell = $('<div/>').addClass('inner_cell');
129 this.celltoolbar = new IPython.CellToolbar(this);
129 this.celltoolbar = new IPython.CellToolbar(this);
130 inner_cell.append(this.celltoolbar.element);
130 inner_cell.append(this.celltoolbar.element);
131 var input_area = $('<div/>').addClass('input_area');
131 var input_area = $('<div/>').addClass('input_area');
132 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
132 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
133 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
133 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
134 inner_cell.append(input_area);
134 inner_cell.append(input_area);
135 input.append(prompt).append(inner_cell);
135 input.append(prompt).append(inner_cell);
136
136
137 var widget_area = $('<div/>')
137 var widget_area = $('<div/>')
138 .addClass('widget-area')
138 .addClass('widget-area')
139 .hide();
139 .hide();
140 this.widget_area = widget_area;
140 this.widget_area = widget_area;
141 var widget_prompt = $('<div/>')
141 var widget_prompt = $('<div/>')
142 .addClass('prompt')
142 .addClass('prompt')
143 .appendTo(widget_area);
143 .appendTo(widget_area);
144 var widget_subarea = $('<div/>')
144 var widget_subarea = $('<div/>')
145 .addClass('widget-subarea')
145 .addClass('widget-subarea')
146 .appendTo(widget_area);
146 .appendTo(widget_area);
147 this.widget_subarea = widget_subarea;
147 this.widget_subarea = widget_subarea;
148 var widget_clear_buton = $('<button />')
148 var widget_clear_buton = $('<button />')
149 .addClass('close')
149 .addClass('close')
150 .html('&times;')
150 .html('&times;')
151 .click(function() {
151 .click(function() {
152 widget_area.slideUp('', function(){ widget_subarea.html(''); });
152 widget_area.slideUp('', function(){ widget_subarea.html(''); });
153 })
153 })
154 .appendTo(widget_prompt);
154 .appendTo(widget_prompt);
155
155
156 var output = $('<div></div>');
156 var output = $('<div></div>');
157 cell.append(input).append(widget_area).append(output);
157 cell.append(input).append(widget_area).append(output);
158 this.element = cell;
158 this.element = cell;
159 this.output_area = new IPython.OutputArea(output, true);
159 this.output_area = new IPython.OutputArea(output, true);
160 this.completer = new IPython.Completer(this);
160 this.completer = new IPython.Completer(this);
161 };
161 };
162
162
163 /** @method bind_events */
163 /** @method bind_events */
164 CodeCell.prototype.bind_events = function () {
164 CodeCell.prototype.bind_events = function () {
165 IPython.Cell.prototype.bind_events.apply(this);
165 IPython.Cell.prototype.bind_events.apply(this);
166 var that = this;
166 var that = this;
167
167
168 this.element.focusout(
168 this.element.focusout(
169 function() { that.auto_highlight(); }
169 function() { that.auto_highlight(); }
170 );
170 );
171 };
171 };
172
172
173 CodeCell.prototype.handle_keyevent = function (editor, event) {
173 CodeCell.prototype.handle_keyevent = function (editor, event) {
174
174
175 // console.log('CM', this.mode, event.which, event.type)
175 // console.log('CM', this.mode, event.which, event.type)
176
176
177 if (this.mode === 'command') {
177 if (this.mode === 'command') {
178 return true;
178 return true;
179 } else if (this.mode === 'edit') {
179 } else if (this.mode === 'edit') {
180 return this.handle_codemirror_keyevent(editor, event);
180 return this.handle_codemirror_keyevent(editor, event);
181 }
181 }
182 };
182 };
183
183
184 /**
184 /**
185 * This method gets called in CodeMirror's onKeyDown/onKeyPress
185 * This method gets called in CodeMirror's onKeyDown/onKeyPress
186 * handlers and is used to provide custom key handling. Its return
186 * handlers and is used to provide custom key handling. Its return
187 * value is used to determine if CodeMirror should ignore the event:
187 * value is used to determine if CodeMirror should ignore the event:
188 * true = ignore, false = don't ignore.
188 * true = ignore, false = don't ignore.
189 * @method handle_codemirror_keyevent
189 * @method handle_codemirror_keyevent
190 */
190 */
191 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
191 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
192
192
193 var that = this;
193 var that = this;
194 // whatever key is pressed, first, cancel the tooltip request before
194 // whatever key is pressed, first, cancel the tooltip request before
195 // they are sent, and remove tooltip if any, except for tab again
195 // they are sent, and remove tooltip if any, except for tab again
196 var tooltip_closed = null;
196 var tooltip_closed = null;
197 if (event.type === 'keydown' && event.which != key.TAB ) {
197 if (event.type === 'keydown' && event.which != key.TAB ) {
198 tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip();
198 tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip();
199 }
199 }
200
200
201 var cur = editor.getCursor();
201 var cur = editor.getCursor();
202 if (event.keyCode === key.ENTER){
202 if (event.keyCode === key.ENTER){
203 this.auto_highlight();
203 this.auto_highlight();
204 }
204 }
205
205
206 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey || event.altKey)) {
206 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey || event.altKey)) {
207 // Always ignore shift-enter in CodeMirror as we handle it.
207 // Always ignore shift-enter in CodeMirror as we handle it.
208 return true;
208 return true;
209 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
209 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
210 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
210 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
211 // browser and keyboard layout !
211 // browser and keyboard layout !
212 // Pressing '(' , request tooltip, don't forget to reappend it
212 // Pressing '(' , request tooltip, don't forget to reappend it
213 // The second argument says to hide the tooltip if the docstring
213 // The second argument says to hide the tooltip if the docstring
214 // is actually empty
214 // is actually empty
215 IPython.tooltip.pending(that, true);
215 IPython.tooltip.pending(that, true);
216 } else if (event.which === key.UPARROW && event.type === 'keydown') {
216 } else if (event.which === key.UPARROW && event.type === 'keydown') {
217 // If we are not at the top, let CM handle the up arrow and
217 // If we are not at the top, let CM handle the up arrow and
218 // prevent the global keydown handler from handling it.
218 // prevent the global keydown handler from handling it.
219 if (!that.at_top()) {
219 if (!that.at_top()) {
220 event.stop();
220 event.stop();
221 return false;
221 return false;
222 } else {
222 } else {
223 return true;
223 return true;
224 }
224 }
225 } else if (event.which === key.ESC && event.type === 'keydown') {
225 } else if (event.which === key.ESC && event.type === 'keydown') {
226 // First see if the tooltip is active and if so cancel it.
226 // First see if the tooltip is active and if so cancel it.
227 if (tooltip_closed) {
227 if (tooltip_closed) {
228 // The call to remove_and_cancel_tooltip above in L177 doesn't pass
228 // The call to remove_and_cancel_tooltip above in L177 doesn't pass
229 // force=true. Because of this it won't actually close the tooltip
229 // force=true. Because of this it won't actually close the tooltip
230 // if it is in sticky mode. Thus, we have to check again if it is open
230 // if it is in sticky mode. Thus, we have to check again if it is open
231 // and close it with force=true.
231 // and close it with force=true.
232 if (!IPython.tooltip._hidden) {
232 if (!IPython.tooltip._hidden) {
233 IPython.tooltip.remove_and_cancel_tooltip(true);
233 IPython.tooltip.remove_and_cancel_tooltip(true);
234 }
234 }
235 // If we closed the tooltip, don't let CM or the global handlers
235 // If we closed the tooltip, don't let CM or the global handlers
236 // handle this event.
236 // handle this event.
237 event.stop();
237 event.stop();
238 return true;
238 return true;
239 }
239 }
240 if (that.code_mirror.options.keyMap === "vim-insert") {
240 if (that.code_mirror.options.keyMap === "vim-insert") {
241 // vim keyMap is active and in insert mode. In this case we leave vim
241 // vim keyMap is active and in insert mode. In this case we leave vim
242 // insert mode, but remain in notebook edit mode.
242 // insert mode, but remain in notebook edit mode.
243 // Let' CM handle this event and prevent global handling.
243 // Let' CM handle this event and prevent global handling.
244 event.stop();
244 event.stop();
245 return false;
245 return false;
246 } else {
246 } else {
247 // vim keyMap is not active. Leave notebook edit mode.
247 // vim keyMap is not active. Leave notebook edit mode.
248 // Don't let CM handle the event, defer to global handling.
248 // Don't let CM handle the event, defer to global handling.
249 return true;
249 return true;
250 }
250 }
251 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
251 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
252 // If we are not at the bottom, let CM handle the down arrow and
252 // If we are not at the bottom, let CM handle the down arrow and
253 // prevent the global keydown handler from handling it.
253 // prevent the global keydown handler from handling it.
254 if (!that.at_bottom()) {
254 if (!that.at_bottom()) {
255 event.stop();
255 event.stop();
256 return false;
256 return false;
257 } else {
257 } else {
258 return true;
258 return true;
259 }
259 }
260 } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) {
260 } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) {
261 if (editor.somethingSelected()){
261 if (editor.somethingSelected()){
262 var anchor = editor.getCursor("anchor");
262 var anchor = editor.getCursor("anchor");
263 var head = editor.getCursor("head");
263 var head = editor.getCursor("head");
264 if( anchor.line != head.line){
264 if( anchor.line != head.line){
265 return false;
265 return false;
266 }
266 }
267 }
267 }
268 IPython.tooltip.request(that);
268 IPython.tooltip.request(that);
269 event.stop();
269 event.stop();
270 return true;
270 return true;
271 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
271 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
272 // Tab completion.
272 // Tab completion.
273 IPython.tooltip.remove_and_cancel_tooltip();
273 IPython.tooltip.remove_and_cancel_tooltip();
274 if (editor.somethingSelected()) {
274 if (editor.somethingSelected()) {
275 return false;
275 return false;
276 }
276 }
277 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
277 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
278 if (pre_cursor.trim() === "") {
278 if (pre_cursor.trim() === "") {
279 // Don't autocomplete if the part of the line before the cursor
279 // Don't autocomplete if the part of the line before the cursor
280 // is empty. In this case, let CodeMirror handle indentation.
280 // is empty. In this case, let CodeMirror handle indentation.
281 return false;
281 return false;
282 } else {
282 } else {
283 event.stop();
283 event.stop();
284 this.completer.startCompletion();
284 this.completer.startCompletion();
285 return true;
285 return true;
286 }
286 }
287 } else {
287 } else {
288 // keypress/keyup also trigger on TAB press, and we don't want to
288 // keypress/keyup also trigger on TAB press, and we don't want to
289 // use those to disable tab completion.
289 // use those to disable tab completion.
290 return false;
290 return false;
291 }
291 }
292 return false;
292 return false;
293 };
293 };
294
294
295 // Kernel related calls.
295 // Kernel related calls.
296
296
297 CodeCell.prototype.set_kernel = function (kernel) {
297 CodeCell.prototype.set_kernel = function (kernel) {
298 this.kernel = kernel;
298 this.kernel = kernel;
299 };
299 };
300
300
301 /**
301 /**
302 * Execute current code cell to the kernel
302 * Execute current code cell to the kernel
303 * @method execute
303 * @method execute
304 */
304 */
305 CodeCell.prototype.execute = function () {
305 CodeCell.prototype.execute = function () {
306 this.output_area.clear_output();
306 this.output_area.clear_output();
307
307
308 // Clear widget area
308 // Clear widget area
309 this.widget_subarea.html('');
309 this.widget_subarea.html('');
310 this.widget_subarea.height('');
310 this.widget_subarea.height('');
311 this.widget_area.height('');
311 this.widget_area.height('');
312 this.widget_area.hide();
312 this.widget_area.hide();
313
313
314 this.set_input_prompt('*');
314 this.set_input_prompt('*');
315 this.element.addClass("running");
315 this.element.addClass("running");
316 if (this.last_msg_id) {
316 if (this.last_msg_id) {
317 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
317 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
318 }
318 }
319 var callbacks = this.get_callbacks();
319 var callbacks = this.get_callbacks();
320
320
321 var old_msg_id = this.last_msg_id;
321 var old_msg_id = this.last_msg_id;
322 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
322 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
323 if (old_msg_id) {
323 if (old_msg_id) {
324 delete CodeCell.msg_cells[old_msg_id];
324 delete CodeCell.msg_cells[old_msg_id];
325 }
325 }
326 CodeCell.msg_cells[this.last_msg_id] = this;
326 CodeCell.msg_cells[this.last_msg_id] = this;
327 };
327 };
328
328
329 /**
329 /**
330 * Construct the default callbacks for
330 * Construct the default callbacks for
331 * @method get_callbacks
331 * @method get_callbacks
332 */
332 */
333 CodeCell.prototype.get_callbacks = function () {
333 CodeCell.prototype.get_callbacks = function () {
334 return {
334 return {
335 shell : {
335 shell : {
336 reply : $.proxy(this._handle_execute_reply, this),
336 reply : $.proxy(this._handle_execute_reply, this),
337 payload : {
337 payload : {
338 set_next_input : $.proxy(this._handle_set_next_input, this),
338 set_next_input : $.proxy(this._handle_set_next_input, this),
339 page : $.proxy(this._open_with_pager, this)
339 page : $.proxy(this._open_with_pager, this)
340 }
340 }
341 },
341 },
342 iopub : {
342 iopub : {
343 output : $.proxy(this.output_area.handle_output, this.output_area),
343 output : $.proxy(this.output_area.handle_output, this.output_area),
344 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
344 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
345 },
345 },
346 input : $.proxy(this._handle_input_request, this)
346 input : $.proxy(this._handle_input_request, this)
347 };
347 };
348 };
348 };
349
349
350 CodeCell.prototype._open_with_pager = function (payload) {
350 CodeCell.prototype._open_with_pager = function (payload) {
351 $([IPython.events]).trigger('open_with_text.Pager', payload);
351 $([IPython.events]).trigger('open_with_text.Pager', payload);
352 };
352 };
353
353
354 /**
354 /**
355 * @method _handle_execute_reply
355 * @method _handle_execute_reply
356 * @private
356 * @private
357 */
357 */
358 CodeCell.prototype._handle_execute_reply = function (msg) {
358 CodeCell.prototype._handle_execute_reply = function (msg) {
359 this.set_input_prompt(msg.content.execution_count);
359 this.set_input_prompt(msg.content.execution_count);
360 this.element.removeClass("running");
360 this.element.removeClass("running");
361 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
361 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
362 };
362 };
363
363
364 /**
364 /**
365 * @method _handle_set_next_input
365 * @method _handle_set_next_input
366 * @private
366 * @private
367 */
367 */
368 CodeCell.prototype._handle_set_next_input = function (payload) {
368 CodeCell.prototype._handle_set_next_input = function (payload) {
369 var data = {'cell': this, 'text': payload.text};
369 var data = {'cell': this, 'text': payload.text};
370 $([IPython.events]).trigger('set_next_input.Notebook', data);
370 $([IPython.events]).trigger('set_next_input.Notebook', data);
371 };
371 };
372
372
373 /**
373 /**
374 * @method _handle_input_request
374 * @method _handle_input_request
375 * @private
375 * @private
376 */
376 */
377 CodeCell.prototype._handle_input_request = function (msg) {
377 CodeCell.prototype._handle_input_request = function (msg) {
378 this.output_area.append_raw_input(msg);
378 this.output_area.append_raw_input(msg);
379 };
379 };
380
380
381
381
382 // Basic cell manipulation.
382 // Basic cell manipulation.
383
383
384 CodeCell.prototype.select = function () {
384 CodeCell.prototype.select = function () {
385 var cont = IPython.Cell.prototype.select.apply(this);
385 var cont = IPython.Cell.prototype.select.apply(this);
386 if (cont) {
386 if (cont) {
387 this.code_mirror.refresh();
387 this.code_mirror.refresh();
388 this.auto_highlight();
388 this.auto_highlight();
389 }
389 }
390 return cont;
390 return cont;
391 };
391 };
392
392
393 CodeCell.prototype.render = function () {
393 CodeCell.prototype.render = function () {
394 var cont = IPython.Cell.prototype.render.apply(this);
394 var cont = IPython.Cell.prototype.render.apply(this);
395 // Always execute, even if we are already in the rendered state
395 // Always execute, even if we are already in the rendered state
396 return cont;
396 return cont;
397 };
397 };
398
398
399 CodeCell.prototype.unrender = function () {
399 CodeCell.prototype.unrender = function () {
400 // CodeCell is always rendered
400 // CodeCell is always rendered
401 return false;
401 return false;
402 };
402 };
403
403
404 CodeCell.prototype.edit_mode = function () {
404 CodeCell.prototype.edit_mode = function () {
405 var cont = IPython.Cell.prototype.edit_mode.apply(this);
405 var cont = IPython.Cell.prototype.edit_mode.apply(this);
406 if (cont) {
406 if (cont) {
407 this.focus_editor();
407 this.focus_editor();
408 }
408 }
409 return cont;
409 return cont;
410 }
410 }
411
411
412 CodeCell.prototype.select_all = function () {
412 CodeCell.prototype.select_all = function () {
413 var start = {line: 0, ch: 0};
413 var start = {line: 0, ch: 0};
414 var nlines = this.code_mirror.lineCount();
414 var nlines = this.code_mirror.lineCount();
415 var last_line = this.code_mirror.getLine(nlines-1);
415 var last_line = this.code_mirror.getLine(nlines-1);
416 var end = {line: nlines-1, ch: last_line.length};
416 var end = {line: nlines-1, ch: last_line.length};
417 this.code_mirror.setSelection(start, end);
417 this.code_mirror.setSelection(start, end);
418 };
418 };
419
419
420
420
421 CodeCell.prototype.collapse = function () {
421 CodeCell.prototype.collapse = function () {
422 this.collapsed = true;
422 this.collapsed = true;
423 this.output_area.collapse();
423 this.output_area.collapse();
424 };
424 };
425
425
426
426
427 CodeCell.prototype.expand = function () {
427 CodeCell.prototype.expand = function () {
428 this.collapsed = false;
428 this.collapsed = false;
429 this.output_area.expand();
429 this.output_area.expand();
430 };
430 };
431
431
432
432
433 CodeCell.prototype.toggle_output = function () {
433 CodeCell.prototype.toggle_output = function () {
434 this.collapsed = Boolean(1 - this.collapsed);
434 this.collapsed = Boolean(1 - this.collapsed);
435 this.output_area.toggle_output();
435 this.output_area.toggle_output();
436 };
436 };
437
437
438
438
439 CodeCell.prototype.toggle_output_scroll = function () {
439 CodeCell.prototype.toggle_output_scroll = function () {
440 this.output_area.toggle_scroll();
440 this.output_area.toggle_scroll();
441 };
441 };
442
442
443
443
444 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
444 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
445 var ns;
445 var ns;
446 if (prompt_value == undefined) {
446 if (prompt_value == undefined) {
447 ns = "&nbsp;";
447 ns = "&nbsp;";
448 } else {
448 } else {
449 ns = encodeURIComponent(prompt_value);
449 ns = encodeURIComponent(prompt_value);
450 }
450 }
451 return 'In&nbsp;[' + ns + ']:';
451 return 'In&nbsp;[' + ns + ']:';
452 };
452 };
453
453
454 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
454 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
455 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
455 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
456 for(var i=1; i < lines_number; i++) {
456 for(var i=1; i < lines_number; i++) {
457 html.push(['...:']);
457 html.push(['...:']);
458 }
458 }
459 return html.join('<br/>');
459 return html.join('<br/>');
460 };
460 };
461
461
462 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
462 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
463
463
464
464
465 CodeCell.prototype.set_input_prompt = function (number) {
465 CodeCell.prototype.set_input_prompt = function (number) {
466 var nline = 1;
466 var nline = 1;
467 if (this.code_mirror !== undefined) {
467 if (this.code_mirror !== undefined) {
468 nline = this.code_mirror.lineCount();
468 nline = this.code_mirror.lineCount();
469 }
469 }
470 this.input_prompt_number = number;
470 this.input_prompt_number = number;
471 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
471 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
472 this.element.find('div.input_prompt').html(prompt_html);
472 this.element.find('div.input_prompt').html(prompt_html);
473 };
473 };
474
474
475
475
476 CodeCell.prototype.clear_input = function () {
476 CodeCell.prototype.clear_input = function () {
477 this.code_mirror.setValue('');
477 this.code_mirror.setValue('');
478 };
478 };
479
479
480
480
481 CodeCell.prototype.get_text = function () {
481 CodeCell.prototype.get_text = function () {
482 return this.code_mirror.getValue();
482 return this.code_mirror.getValue();
483 };
483 };
484
484
485
485
486 CodeCell.prototype.set_text = function (code) {
486 CodeCell.prototype.set_text = function (code) {
487 return this.code_mirror.setValue(code);
487 return this.code_mirror.setValue(code);
488 };
488 };
489
489
490
490
491 CodeCell.prototype.at_top = function () {
491 CodeCell.prototype.at_top = function () {
492 var cursor = this.code_mirror.getCursor();
492 var cursor = this.code_mirror.getCursor();
493 if (cursor.line === 0 && cursor.ch === 0) {
493 if (cursor.line === 0 && cursor.ch === 0) {
494 return true;
494 return true;
495 } else {
495 } else {
496 return false;
496 return false;
497 }
497 }
498 };
498 };
499
499
500
500
501 CodeCell.prototype.at_bottom = function () {
501 CodeCell.prototype.at_bottom = function () {
502 var cursor = this.code_mirror.getCursor();
502 var cursor = this.code_mirror.getCursor();
503 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
503 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
504 return true;
504 return true;
505 } else {
505 } else {
506 return false;
506 return false;
507 }
507 }
508 };
508 };
509
509
510
510
511 CodeCell.prototype.clear_output = function (wait) {
511 CodeCell.prototype.clear_output = function (wait) {
512 this.output_area.clear_output(wait);
512 this.output_area.clear_output(wait);
513 };
513 };
514
514
515
515
516 // JSON serialization
516 // JSON serialization
517
517
518 CodeCell.prototype.fromJSON = function (data) {
518 CodeCell.prototype.fromJSON = function (data) {
519 IPython.Cell.prototype.fromJSON.apply(this, arguments);
519 IPython.Cell.prototype.fromJSON.apply(this, arguments);
520 if (data.cell_type === 'code') {
520 if (data.cell_type === 'code') {
521 if (data.input !== undefined) {
521 if (data.input !== undefined) {
522 this.set_text(data.input);
522 this.set_text(data.input);
523 // make this value the starting point, so that we can only undo
523 // make this value the starting point, so that we can only undo
524 // to this state, instead of a blank cell
524 // to this state, instead of a blank cell
525 this.code_mirror.clearHistory();
525 this.code_mirror.clearHistory();
526 this.auto_highlight();
526 this.auto_highlight();
527 }
527 }
528 if (data.prompt_number !== undefined) {
528 if (data.prompt_number !== undefined) {
529 this.set_input_prompt(data.prompt_number);
529 this.set_input_prompt(data.prompt_number);
530 } else {
530 } else {
531 this.set_input_prompt();
531 this.set_input_prompt();
532 }
532 }
533 this.output_area.trusted = data.trusted || false;
533 this.output_area.fromJSON(data.outputs);
534 this.output_area.fromJSON(data.outputs);
534 if (data.collapsed !== undefined) {
535 if (data.collapsed !== undefined) {
535 if (data.collapsed) {
536 if (data.collapsed) {
536 this.collapse();
537 this.collapse();
537 } else {
538 } else {
538 this.expand();
539 this.expand();
539 }
540 }
540 }
541 }
541 }
542 }
542 };
543 };
543
544
544
545
545 CodeCell.prototype.toJSON = function () {
546 CodeCell.prototype.toJSON = function () {
546 var data = IPython.Cell.prototype.toJSON.apply(this);
547 var data = IPython.Cell.prototype.toJSON.apply(this);
547 data.input = this.get_text();
548 data.input = this.get_text();
548 // is finite protect against undefined and '*' value
549 // is finite protect against undefined and '*' value
549 if (isFinite(this.input_prompt_number)) {
550 if (isFinite(this.input_prompt_number)) {
550 data.prompt_number = this.input_prompt_number;
551 data.prompt_number = this.input_prompt_number;
551 }
552 }
552 var outputs = this.output_area.toJSON();
553 var outputs = this.output_area.toJSON();
553 data.outputs = outputs;
554 data.outputs = outputs;
554 data.language = 'python';
555 data.language = 'python';
556 data.trusted = this.output_area.trusted;
555 data.collapsed = this.collapsed;
557 data.collapsed = this.collapsed;
556 return data;
558 return data;
557 };
559 };
558
560
559
561
560 IPython.CodeCell = CodeCell;
562 IPython.CodeCell = CodeCell;
561
563
562 return IPython;
564 return IPython;
563 }(IPython));
565 }(IPython));
@@ -1,819 +1,826 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008 The IPython Development Team
2 // Copyright (C) 2008 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // OutputArea
9 // OutputArea
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule OutputArea
15 * @submodule OutputArea
16 */
16 */
17 var IPython = (function (IPython) {
17 var IPython = (function (IPython) {
18 "use strict";
18 "use strict";
19
19
20 var utils = IPython.utils;
20 var utils = IPython.utils;
21
21
22 /**
22 /**
23 * @class OutputArea
23 * @class OutputArea
24 *
24 *
25 * @constructor
25 * @constructor
26 */
26 */
27
27
28 var OutputArea = function (selector, prompt_area) {
28 var OutputArea = function (selector, prompt_area) {
29 this.selector = selector;
29 this.selector = selector;
30 this.wrapper = $(selector);
30 this.wrapper = $(selector);
31 this.outputs = [];
31 this.outputs = [];
32 this.collapsed = false;
32 this.collapsed = false;
33 this.scrolled = false;
33 this.scrolled = false;
34 this.trusted = true;
34 this.clear_queued = null;
35 this.clear_queued = null;
35 if (prompt_area === undefined) {
36 if (prompt_area === undefined) {
36 this.prompt_area = true;
37 this.prompt_area = true;
37 } else {
38 } else {
38 this.prompt_area = prompt_area;
39 this.prompt_area = prompt_area;
39 }
40 }
40 this.create_elements();
41 this.create_elements();
41 this.style();
42 this.style();
42 this.bind_events();
43 this.bind_events();
43 };
44 };
44
45
45 OutputArea.prototype.create_elements = function () {
46 OutputArea.prototype.create_elements = function () {
46 this.element = $("<div/>");
47 this.element = $("<div/>");
47 this.collapse_button = $("<div/>");
48 this.collapse_button = $("<div/>");
48 this.prompt_overlay = $("<div/>");
49 this.prompt_overlay = $("<div/>");
49 this.wrapper.append(this.prompt_overlay);
50 this.wrapper.append(this.prompt_overlay);
50 this.wrapper.append(this.element);
51 this.wrapper.append(this.element);
51 this.wrapper.append(this.collapse_button);
52 this.wrapper.append(this.collapse_button);
52 };
53 };
53
54
54
55
55 OutputArea.prototype.style = function () {
56 OutputArea.prototype.style = function () {
56 this.collapse_button.hide();
57 this.collapse_button.hide();
57 this.prompt_overlay.hide();
58 this.prompt_overlay.hide();
58
59
59 this.wrapper.addClass('output_wrapper');
60 this.wrapper.addClass('output_wrapper');
60 this.element.addClass('output');
61 this.element.addClass('output');
61
62
62 this.collapse_button.addClass("btn output_collapsed");
63 this.collapse_button.addClass("btn output_collapsed");
63 this.collapse_button.attr('title', 'click to expand output');
64 this.collapse_button.attr('title', 'click to expand output');
64 this.collapse_button.text('. . .');
65 this.collapse_button.text('. . .');
65
66
66 this.prompt_overlay.addClass('out_prompt_overlay prompt');
67 this.prompt_overlay.addClass('out_prompt_overlay prompt');
67 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
68 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
68
69
69 this.collapse();
70 this.collapse();
70 };
71 };
71
72
72 /**
73 /**
73 * Should the OutputArea scroll?
74 * Should the OutputArea scroll?
74 * Returns whether the height (in lines) exceeds a threshold.
75 * Returns whether the height (in lines) exceeds a threshold.
75 *
76 *
76 * @private
77 * @private
77 * @method _should_scroll
78 * @method _should_scroll
78 * @param [lines=100]{Integer}
79 * @param [lines=100]{Integer}
79 * @return {Bool}
80 * @return {Bool}
80 *
81 *
81 */
82 */
82 OutputArea.prototype._should_scroll = function (lines) {
83 OutputArea.prototype._should_scroll = function (lines) {
83 if (lines <=0 ){ return }
84 if (lines <=0 ){ return }
84 if (!lines) {
85 if (!lines) {
85 lines = 100;
86 lines = 100;
86 }
87 }
87 // line-height from http://stackoverflow.com/questions/1185151
88 // line-height from http://stackoverflow.com/questions/1185151
88 var fontSize = this.element.css('font-size');
89 var fontSize = this.element.css('font-size');
89 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
90 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
90
91
91 return (this.element.height() > lines * lineHeight);
92 return (this.element.height() > lines * lineHeight);
92 };
93 };
93
94
94
95
95 OutputArea.prototype.bind_events = function () {
96 OutputArea.prototype.bind_events = function () {
96 var that = this;
97 var that = this;
97 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
98 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
98 this.prompt_overlay.click(function () { that.toggle_scroll(); });
99 this.prompt_overlay.click(function () { that.toggle_scroll(); });
99
100
100 this.element.resize(function () {
101 this.element.resize(function () {
101 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
102 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
102 if ( IPython.utils.browser[0] === "Firefox" ) {
103 if ( IPython.utils.browser[0] === "Firefox" ) {
103 return;
104 return;
104 }
105 }
105 // maybe scroll output,
106 // maybe scroll output,
106 // if it's grown large enough and hasn't already been scrolled.
107 // if it's grown large enough and hasn't already been scrolled.
107 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
108 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
108 that.scroll_area();
109 that.scroll_area();
109 }
110 }
110 });
111 });
111 this.collapse_button.click(function () {
112 this.collapse_button.click(function () {
112 that.expand();
113 that.expand();
113 });
114 });
114 };
115 };
115
116
116
117
117 OutputArea.prototype.collapse = function () {
118 OutputArea.prototype.collapse = function () {
118 if (!this.collapsed) {
119 if (!this.collapsed) {
119 this.element.hide();
120 this.element.hide();
120 this.prompt_overlay.hide();
121 this.prompt_overlay.hide();
121 if (this.element.html()){
122 if (this.element.html()){
122 this.collapse_button.show();
123 this.collapse_button.show();
123 }
124 }
124 this.collapsed = true;
125 this.collapsed = true;
125 }
126 }
126 };
127 };
127
128
128
129
129 OutputArea.prototype.expand = function () {
130 OutputArea.prototype.expand = function () {
130 if (this.collapsed) {
131 if (this.collapsed) {
131 this.collapse_button.hide();
132 this.collapse_button.hide();
132 this.element.show();
133 this.element.show();
133 this.prompt_overlay.show();
134 this.prompt_overlay.show();
134 this.collapsed = false;
135 this.collapsed = false;
135 }
136 }
136 };
137 };
137
138
138
139
139 OutputArea.prototype.toggle_output = function () {
140 OutputArea.prototype.toggle_output = function () {
140 if (this.collapsed) {
141 if (this.collapsed) {
141 this.expand();
142 this.expand();
142 } else {
143 } else {
143 this.collapse();
144 this.collapse();
144 }
145 }
145 };
146 };
146
147
147
148
148 OutputArea.prototype.scroll_area = function () {
149 OutputArea.prototype.scroll_area = function () {
149 this.element.addClass('output_scroll');
150 this.element.addClass('output_scroll');
150 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
151 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
151 this.scrolled = true;
152 this.scrolled = true;
152 };
153 };
153
154
154
155
155 OutputArea.prototype.unscroll_area = function () {
156 OutputArea.prototype.unscroll_area = function () {
156 this.element.removeClass('output_scroll');
157 this.element.removeClass('output_scroll');
157 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
158 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
158 this.scrolled = false;
159 this.scrolled = false;
159 };
160 };
160
161
161 /**
162 /**
162 * Threshold to trigger autoscroll when the OutputArea is resized,
163 * Threshold to trigger autoscroll when the OutputArea is resized,
163 * typically when new outputs are added.
164 * typically when new outputs are added.
164 *
165 *
165 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
166 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
166 * unless it is < 0, in which case autoscroll will never be triggered
167 * unless it is < 0, in which case autoscroll will never be triggered
167 *
168 *
168 * @property auto_scroll_threshold
169 * @property auto_scroll_threshold
169 * @type Number
170 * @type Number
170 * @default 100
171 * @default 100
171 *
172 *
172 **/
173 **/
173 OutputArea.auto_scroll_threshold = 100;
174 OutputArea.auto_scroll_threshold = 100;
174
175
175
176
176 /**
177 /**
177 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
178 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
178 * shorter than this are never scrolled.
179 * shorter than this are never scrolled.
179 *
180 *
180 * @property minimum_scroll_threshold
181 * @property minimum_scroll_threshold
181 * @type Number
182 * @type Number
182 * @default 20
183 * @default 20
183 *
184 *
184 **/
185 **/
185 OutputArea.minimum_scroll_threshold = 20;
186 OutputArea.minimum_scroll_threshold = 20;
186
187
187
188
188 /**
189 /**
189 *
190 *
190 * Scroll OutputArea if height supperior than a threshold (in lines).
191 * Scroll OutputArea if height supperior than a threshold (in lines).
191 *
192 *
192 * Threshold is a maximum number of lines. If unspecified, defaults to
193 * Threshold is a maximum number of lines. If unspecified, defaults to
193 * OutputArea.minimum_scroll_threshold.
194 * OutputArea.minimum_scroll_threshold.
194 *
195 *
195 * Negative threshold will prevent the OutputArea from ever scrolling.
196 * Negative threshold will prevent the OutputArea from ever scrolling.
196 *
197 *
197 * @method scroll_if_long
198 * @method scroll_if_long
198 *
199 *
199 * @param [lines=20]{Number} Default to 20 if not set,
200 * @param [lines=20]{Number} Default to 20 if not set,
200 * behavior undefined for value of `0`.
201 * behavior undefined for value of `0`.
201 *
202 *
202 **/
203 **/
203 OutputArea.prototype.scroll_if_long = function (lines) {
204 OutputArea.prototype.scroll_if_long = function (lines) {
204 var n = lines | OutputArea.minimum_scroll_threshold;
205 var n = lines | OutputArea.minimum_scroll_threshold;
205 if(n <= 0){
206 if(n <= 0){
206 return
207 return
207 }
208 }
208
209
209 if (this._should_scroll(n)) {
210 if (this._should_scroll(n)) {
210 // only allow scrolling long-enough output
211 // only allow scrolling long-enough output
211 this.scroll_area();
212 this.scroll_area();
212 }
213 }
213 };
214 };
214
215
215
216
216 OutputArea.prototype.toggle_scroll = function () {
217 OutputArea.prototype.toggle_scroll = function () {
217 if (this.scrolled) {
218 if (this.scrolled) {
218 this.unscroll_area();
219 this.unscroll_area();
219 } else {
220 } else {
220 // only allow scrolling long-enough output
221 // only allow scrolling long-enough output
221 this.scroll_if_long();
222 this.scroll_if_long();
222 }
223 }
223 };
224 };
224
225
225
226
226 // typeset with MathJax if MathJax is available
227 // typeset with MathJax if MathJax is available
227 OutputArea.prototype.typeset = function () {
228 OutputArea.prototype.typeset = function () {
228 if (window.MathJax){
229 if (window.MathJax){
229 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
230 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
230 }
231 }
231 };
232 };
232
233
233
234
234 OutputArea.prototype.handle_output = function (msg) {
235 OutputArea.prototype.handle_output = function (msg) {
235 var json = {};
236 var json = {};
236 var msg_type = json.output_type = msg.header.msg_type;
237 var msg_type = json.output_type = msg.header.msg_type;
237 var content = msg.content;
238 var content = msg.content;
238 if (msg_type === "stream") {
239 if (msg_type === "stream") {
239 json.text = content.data;
240 json.text = content.data;
240 json.stream = content.name;
241 json.stream = content.name;
241 } else if (msg_type === "display_data") {
242 } else if (msg_type === "display_data") {
242 json = content.data;
243 json = content.data;
243 json.output_type = msg_type;
244 json.output_type = msg_type;
244 json.metadata = content.metadata;
245 json.metadata = content.metadata;
245 } else if (msg_type === "pyout") {
246 } else if (msg_type === "pyout") {
246 json = content.data;
247 json = content.data;
247 json.output_type = msg_type;
248 json.output_type = msg_type;
248 json.metadata = content.metadata;
249 json.metadata = content.metadata;
249 json.prompt_number = content.execution_count;
250 json.prompt_number = content.execution_count;
250 } else if (msg_type === "pyerr") {
251 } else if (msg_type === "pyerr") {
251 json.ename = content.ename;
252 json.ename = content.ename;
252 json.evalue = content.evalue;
253 json.evalue = content.evalue;
253 json.traceback = content.traceback;
254 json.traceback = content.traceback;
254 }
255 }
255 this.append_output(json);
256 this.append_output(json);
256 };
257 };
257
258
258 OutputArea.mime_map = {
259 OutputArea.mime_map = {
259 "text/plain" : "text",
260 "text/plain" : "text",
260 "text/html" : "html",
261 "text/html" : "html",
261 "image/svg+xml" : "svg",
262 "image/svg+xml" : "svg",
262 "image/png" : "png",
263 "image/png" : "png",
263 "image/jpeg" : "jpeg",
264 "image/jpeg" : "jpeg",
264 "text/latex" : "latex",
265 "text/latex" : "latex",
265 "application/json" : "json",
266 "application/json" : "json",
266 "application/javascript" : "javascript",
267 "application/javascript" : "javascript",
267 };
268 };
268
269
269 OutputArea.mime_map_r = {
270 OutputArea.mime_map_r = {
270 "text" : "text/plain",
271 "text" : "text/plain",
271 "html" : "text/html",
272 "html" : "text/html",
272 "svg" : "image/svg+xml",
273 "svg" : "image/svg+xml",
273 "png" : "image/png",
274 "png" : "image/png",
274 "jpeg" : "image/jpeg",
275 "jpeg" : "image/jpeg",
275 "latex" : "text/latex",
276 "latex" : "text/latex",
276 "json" : "application/json",
277 "json" : "application/json",
277 "javascript" : "application/javascript",
278 "javascript" : "application/javascript",
278 };
279 };
279
280
280 OutputArea.prototype.rename_keys = function (data, key_map) {
281 OutputArea.prototype.rename_keys = function (data, key_map) {
281 var remapped = {};
282 var remapped = {};
282 for (var key in data) {
283 for (var key in data) {
283 var new_key = key_map[key] || key;
284 var new_key = key_map[key] || key;
284 remapped[new_key] = data[key];
285 remapped[new_key] = data[key];
285 }
286 }
286 return remapped;
287 return remapped;
287 };
288 };
288
289
289
290
290 OutputArea.output_types = [
291 OutputArea.output_types = [
291 'application/javascript',
292 'application/javascript',
292 'text/html',
293 'text/html',
293 'text/latex',
294 'text/latex',
294 'image/svg+xml',
295 'image/svg+xml',
295 'image/png',
296 'image/png',
296 'image/jpeg',
297 'image/jpeg',
297 'text/plain'
298 'text/plain'
298 ];
299 ];
299
300
300 OutputArea.prototype.validate_output = function (json) {
301 OutputArea.prototype.validate_output = function (json) {
301 // scrub invalid outputs
302 // scrub invalid outputs
302 // TODO: right now everything is a string, but JSON really shouldn't be.
303 // TODO: right now everything is a string, but JSON really shouldn't be.
303 // nbformat 4 will fix that.
304 // nbformat 4 will fix that.
304 $.map(OutputArea.output_types, function(key){
305 $.map(OutputArea.output_types, function(key){
305 if (json[key] !== undefined && typeof json[key] !== 'string') {
306 if (json[key] !== undefined && typeof json[key] !== 'string') {
306 console.log("Invalid type for " + key, json[key]);
307 console.log("Invalid type for " + key, json[key]);
307 delete json[key];
308 delete json[key];
308 }
309 }
309 });
310 });
310 return json;
311 return json;
311 };
312 };
312
313
313 OutputArea.prototype.append_output = function (json) {
314 OutputArea.prototype.append_output = function (json) {
314 this.expand();
315 this.expand();
315 // Clear the output if clear is queued.
316 // Clear the output if clear is queued.
316 var needs_height_reset = false;
317 var needs_height_reset = false;
317 if (this.clear_queued) {
318 if (this.clear_queued) {
318 this.clear_output(false);
319 this.clear_output(false);
319 needs_height_reset = true;
320 needs_height_reset = true;
320 }
321 }
321
322
322 // validate output data types
323 // validate output data types
323 json = this.validate_output(json);
324 json = this.validate_output(json);
324
325
325 if (json.output_type === 'pyout') {
326 if (json.output_type === 'pyout') {
326 this.append_pyout(json);
327 this.append_pyout(json);
327 } else if (json.output_type === 'pyerr') {
328 } else if (json.output_type === 'pyerr') {
328 this.append_pyerr(json);
329 this.append_pyerr(json);
329 } else if (json.output_type === 'display_data') {
330 } else if (json.output_type === 'display_data') {
330 this.append_display_data(json);
331 this.append_display_data(json);
331 } else if (json.output_type === 'stream') {
332 } else if (json.output_type === 'stream') {
332 this.append_stream(json);
333 this.append_stream(json);
333 }
334 }
335
334 this.outputs.push(json);
336 this.outputs.push(json);
335
337
336 // Only reset the height to automatic if the height is currently
338 // Only reset the height to automatic if the height is currently
337 // fixed (done by wait=True flag on clear_output).
339 // fixed (done by wait=True flag on clear_output).
338 if (needs_height_reset) {
340 if (needs_height_reset) {
339 this.element.height('');
341 this.element.height('');
340 }
342 }
341
343
342 var that = this;
344 var that = this;
343 setTimeout(function(){that.element.trigger('resize');}, 100);
345 setTimeout(function(){that.element.trigger('resize');}, 100);
344 };
346 };
345
347
346
348
347 OutputArea.prototype.create_output_area = function () {
349 OutputArea.prototype.create_output_area = function () {
348 var oa = $("<div/>").addClass("output_area");
350 var oa = $("<div/>").addClass("output_area");
349 if (this.prompt_area) {
351 if (this.prompt_area) {
350 oa.append($('<div/>').addClass('prompt'));
352 oa.append($('<div/>').addClass('prompt'));
351 }
353 }
352 return oa;
354 return oa;
353 };
355 };
354
356
355
357
356 function _get_metadata_key(metadata, key, mime) {
358 function _get_metadata_key(metadata, key, mime) {
357 var mime_md = metadata[mime];
359 var mime_md = metadata[mime];
358 // mime-specific higher priority
360 // mime-specific higher priority
359 if (mime_md && mime_md[key] !== undefined) {
361 if (mime_md && mime_md[key] !== undefined) {
360 return mime_md[key];
362 return mime_md[key];
361 }
363 }
362 // fallback on global
364 // fallback on global
363 return metadata[key];
365 return metadata[key];
364 }
366 }
365
367
366 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
368 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
367 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
369 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
368 if (_get_metadata_key(md, 'isolated', mime)) {
370 if (_get_metadata_key(md, 'isolated', mime)) {
369 // Create an iframe to isolate the subarea from the rest of the
371 // Create an iframe to isolate the subarea from the rest of the
370 // document
372 // document
371 var iframe = $('<iframe/>').addClass('box-flex1');
373 var iframe = $('<iframe/>').addClass('box-flex1');
372 iframe.css({'height':1, 'width':'100%', 'display':'block'});
374 iframe.css({'height':1, 'width':'100%', 'display':'block'});
373 iframe.attr('frameborder', 0);
375 iframe.attr('frameborder', 0);
374 iframe.attr('scrolling', 'auto');
376 iframe.attr('scrolling', 'auto');
375
377
376 // Once the iframe is loaded, the subarea is dynamically inserted
378 // Once the iframe is loaded, the subarea is dynamically inserted
377 iframe.on('load', function() {
379 iframe.on('load', function() {
378 // Workaround needed by Firefox, to properly render svg inside
380 // Workaround needed by Firefox, to properly render svg inside
379 // iframes, see http://stackoverflow.com/questions/10177190/
381 // iframes, see http://stackoverflow.com/questions/10177190/
380 // svg-dynamically-added-to-iframe-does-not-render-correctly
382 // svg-dynamically-added-to-iframe-does-not-render-correctly
381 this.contentDocument.open();
383 this.contentDocument.open();
382
384
383 // Insert the subarea into the iframe
385 // Insert the subarea into the iframe
384 // We must directly write the html. When using Jquery's append
386 // We must directly write the html. When using Jquery's append
385 // method, javascript is evaluated in the parent document and
387 // method, javascript is evaluated in the parent document and
386 // not in the iframe document.
388 // not in the iframe document.
387 this.contentDocument.write(subarea.html());
389 this.contentDocument.write(subarea.html());
388
390
389 this.contentDocument.close();
391 this.contentDocument.close();
390
392
391 var body = this.contentDocument.body;
393 var body = this.contentDocument.body;
392 // Adjust the iframe height automatically
394 // Adjust the iframe height automatically
393 iframe.height(body.scrollHeight + 'px');
395 iframe.height(body.scrollHeight + 'px');
394 });
396 });
395
397
396 // Elements should be appended to the inner subarea and not to the
398 // Elements should be appended to the inner subarea and not to the
397 // iframe
399 // iframe
398 iframe.append = function(that) {
400 iframe.append = function(that) {
399 subarea.append(that);
401 subarea.append(that);
400 };
402 };
401
403
402 return iframe;
404 return iframe;
403 } else {
405 } else {
404 return subarea;
406 return subarea;
405 }
407 }
406 }
408 }
407
409
408
410
409 OutputArea.prototype._append_javascript_error = function (err, element) {
411 OutputArea.prototype._append_javascript_error = function (err, element) {
410 // display a message when a javascript error occurs in display output
412 // display a message when a javascript error occurs in display output
411 var msg = "Javascript error adding output!"
413 var msg = "Javascript error adding output!"
412 if ( element === undefined ) return;
414 if ( element === undefined ) return;
413 element.append(
415 element.append(
414 $('<div/>').html(msg + "<br/>" +
416 $('<div/>').html(msg + "<br/>" +
415 err.toString() +
417 err.toString() +
416 '<br/>See your browser Javascript console for more details.'
418 '<br/>See your browser Javascript console for more details.'
417 ).addClass('js-error')
419 ).addClass('js-error')
418 );
420 );
419 };
421 };
420
422
421 OutputArea.prototype._safe_append = function (toinsert) {
423 OutputArea.prototype._safe_append = function (toinsert) {
422 // safely append an item to the document
424 // safely append an item to the document
423 // this is an object created by user code,
425 // this is an object created by user code,
424 // and may have errors, which should not be raised
426 // and may have errors, which should not be raised
425 // under any circumstances.
427 // under any circumstances.
426 try {
428 try {
427 this.element.append(toinsert);
429 this.element.append(toinsert);
428 } catch(err) {
430 } catch(err) {
429 console.log(err);
431 console.log(err);
430 // Create an actual output_area and output_subarea, which creates
432 // Create an actual output_area and output_subarea, which creates
431 // the prompt area and the proper indentation.
433 // the prompt area and the proper indentation.
432 var toinsert = this.create_output_area();
434 var toinsert = this.create_output_area();
433 var subarea = $('<div/>').addClass('output_subarea');
435 var subarea = $('<div/>').addClass('output_subarea');
434 toinsert.append(subarea);
436 toinsert.append(subarea);
435 this._append_javascript_error(err, subarea);
437 this._append_javascript_error(err, subarea);
436 this.element.append(toinsert);
438 this.element.append(toinsert);
437 }
439 }
438 };
440 };
439
441
440
442
441 OutputArea.prototype.append_pyout = function (json) {
443 OutputArea.prototype.append_pyout = function (json) {
442 var n = json.prompt_number || ' ';
444 var n = json.prompt_number || ' ';
443 var toinsert = this.create_output_area();
445 var toinsert = this.create_output_area();
444 if (this.prompt_area) {
446 if (this.prompt_area) {
445 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
447 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
446 }
448 }
447 this.append_mime_type(json, toinsert);
449 this.append_mime_type(json, toinsert);
448 this._safe_append(toinsert);
450 this._safe_append(toinsert);
449 // If we just output latex, typeset it.
451 // If we just output latex, typeset it.
450 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
452 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
451 this.typeset();
453 this.typeset();
452 }
454 }
453 };
455 };
454
456
455
457
456 OutputArea.prototype.append_pyerr = function (json) {
458 OutputArea.prototype.append_pyerr = function (json) {
457 var tb = json.traceback;
459 var tb = json.traceback;
458 if (tb !== undefined && tb.length > 0) {
460 if (tb !== undefined && tb.length > 0) {
459 var s = '';
461 var s = '';
460 var len = tb.length;
462 var len = tb.length;
461 for (var i=0; i<len; i++) {
463 for (var i=0; i<len; i++) {
462 s = s + tb[i] + '\n';
464 s = s + tb[i] + '\n';
463 }
465 }
464 s = s + '\n';
466 s = s + '\n';
465 var toinsert = this.create_output_area();
467 var toinsert = this.create_output_area();
466 this.append_text(s, {}, toinsert);
468 this.append_text(s, {}, toinsert);
467 this._safe_append(toinsert);
469 this._safe_append(toinsert);
468 }
470 }
469 };
471 };
470
472
471
473
472 OutputArea.prototype.append_stream = function (json) {
474 OutputArea.prototype.append_stream = function (json) {
473 // temporary fix: if stream undefined (json file written prior to this patch),
475 // temporary fix: if stream undefined (json file written prior to this patch),
474 // default to most likely stdout:
476 // default to most likely stdout:
475 if (json.stream == undefined){
477 if (json.stream == undefined){
476 json.stream = 'stdout';
478 json.stream = 'stdout';
477 }
479 }
478 var text = json.text;
480 var text = json.text;
479 var subclass = "output_"+json.stream;
481 var subclass = "output_"+json.stream;
480 if (this.outputs.length > 0){
482 if (this.outputs.length > 0){
481 // have at least one output to consider
483 // have at least one output to consider
482 var last = this.outputs[this.outputs.length-1];
484 var last = this.outputs[this.outputs.length-1];
483 if (last.output_type == 'stream' && json.stream == last.stream){
485 if (last.output_type == 'stream' && json.stream == last.stream){
484 // latest output was in the same stream,
486 // latest output was in the same stream,
485 // so append directly into its pre tag
487 // so append directly into its pre tag
486 // escape ANSI & HTML specials:
488 // escape ANSI & HTML specials:
487 var pre = this.element.find('div.'+subclass).last().find('pre');
489 var pre = this.element.find('div.'+subclass).last().find('pre');
488 var html = utils.fixCarriageReturn(
490 var html = utils.fixCarriageReturn(
489 pre.html() + utils.fixConsole(text));
491 pre.html() + utils.fixConsole(text));
490 pre.html(html);
492 pre.html(html);
491 return;
493 return;
492 }
494 }
493 }
495 }
494
496
495 if (!text.replace("\r", "")) {
497 if (!text.replace("\r", "")) {
496 // text is nothing (empty string, \r, etc.)
498 // text is nothing (empty string, \r, etc.)
497 // so don't append any elements, which might add undesirable space
499 // so don't append any elements, which might add undesirable space
498 return;
500 return;
499 }
501 }
500
502
501 // If we got here, attach a new div
503 // If we got here, attach a new div
502 var toinsert = this.create_output_area();
504 var toinsert = this.create_output_area();
503 this.append_text(text, {}, toinsert, "output_stream "+subclass);
505 this.append_text(text, {}, toinsert, "output_stream "+subclass);
504 this._safe_append(toinsert);
506 this._safe_append(toinsert);
505 };
507 };
506
508
507
509
508 OutputArea.prototype.append_display_data = function (json) {
510 OutputArea.prototype.append_display_data = function (json) {
509 var toinsert = this.create_output_area();
511 var toinsert = this.create_output_area();
510 if (this.append_mime_type(json, toinsert)) {
512 if (this.append_mime_type(json, toinsert)) {
511 this._safe_append(toinsert);
513 this._safe_append(toinsert);
512 // If we just output latex, typeset it.
514 // If we just output latex, typeset it.
513 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
515 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
514 this.typeset();
516 this.typeset();
515 }
517 }
516 }
518 }
517 };
519 };
518
520
519 OutputArea.display_order = [
521 OutputArea.display_order = [
520 'application/javascript',
522 'application/javascript',
521 'text/html',
523 'text/html',
522 'text/latex',
524 'text/latex',
523 'image/svg+xml',
525 'image/svg+xml',
524 'image/png',
526 'image/png',
525 'image/jpeg',
527 'image/jpeg',
526 'text/plain'
528 'text/plain'
527 ];
529 ];
528
530
531 OutputArea.safe_outputs = {
532 'text/plain' : true,
533 'image/png' : true,
534 'image/jpeg' : true
535 };
536
529 OutputArea.prototype.append_mime_type = function (json, element) {
537 OutputArea.prototype.append_mime_type = function (json, element) {
530
531 for (var type_i in OutputArea.display_order) {
538 for (var type_i in OutputArea.display_order) {
532 var type = OutputArea.display_order[type_i];
539 var type = OutputArea.display_order[type_i];
533 var append = OutputArea.append_map[type];
540 var append = OutputArea.append_map[type];
534 if ((json[type] !== undefined) && append) {
541 if ((json[type] !== undefined) && append) {
542 if (!this.trusted && !OutputArea.safe_outputs[type]) {
543 // not trusted show warning and do not display
544 var content = {
545 text : "Untrusted " + type + " output ignored.",
546 stream : "stderr"
547 }
548 this.append_stream(content);
549 continue;
550 }
535 var md = json.metadata || {};
551 var md = json.metadata || {};
536 append.apply(this, [json[type], md, element]);
552 append.apply(this, [json[type], md, element]);
537 return true;
553 return true;
538 }
554 }
539 }
555 }
540 return false;
556 return false;
541 };
557 };
542
558
543
559
544 OutputArea.prototype.append_html = function (html, md, element) {
560 OutputArea.prototype.append_html = function (html, md, element) {
545 var type = 'text/html';
561 var type = 'text/html';
546 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
562 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
547 IPython.keyboard_manager.register_events(toinsert);
563 IPython.keyboard_manager.register_events(toinsert);
548 toinsert.append(html);
564 toinsert.append(html);
549 element.append(toinsert);
565 element.append(toinsert);
550 };
566 };
551
567
552
568
553 OutputArea.prototype.append_javascript = function (js, md, container) {
569 OutputArea.prototype.append_javascript = function (js, md, container) {
554 // We just eval the JS code, element appears in the local scope.
570 // We just eval the JS code, element appears in the local scope.
555 var type = 'application/javascript';
571 var type = 'application/javascript';
556 var element = this.create_output_subarea(md, "output_javascript", type);
572 var element = this.create_output_subarea(md, "output_javascript", type);
557 IPython.keyboard_manager.register_events(element);
573 IPython.keyboard_manager.register_events(element);
558 container.append(element);
574 container.append(element);
559 try {
575 try {
560 eval(js);
576 eval(js);
561 } catch(err) {
577 } catch(err) {
562 console.log(err);
578 console.log(err);
563 this._append_javascript_error(err, element);
579 this._append_javascript_error(err, element);
564 }
580 }
565 };
581 };
566
582
567
583
568 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
584 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
569 var type = 'text/plain';
585 var type = 'text/plain';
570 var toinsert = this.create_output_subarea(md, "output_text", type);
586 var toinsert = this.create_output_subarea(md, "output_text", type);
571 // escape ANSI & HTML specials in plaintext:
587 // escape ANSI & HTML specials in plaintext:
572 data = utils.fixConsole(data);
588 data = utils.fixConsole(data);
573 data = utils.fixCarriageReturn(data);
589 data = utils.fixCarriageReturn(data);
574 data = utils.autoLinkUrls(data);
590 data = utils.autoLinkUrls(data);
575 if (extra_class){
591 if (extra_class){
576 toinsert.addClass(extra_class);
592 toinsert.addClass(extra_class);
577 }
593 }
578 toinsert.append($("<pre/>").html(data));
594 toinsert.append($("<pre/>").html(data));
579 element.append(toinsert);
595 element.append(toinsert);
580 };
596 };
581
597
582
598
583 OutputArea.prototype.append_svg = function (svg, md, element) {
599 OutputArea.prototype.append_svg = function (svg, md, element) {
584 var type = 'image/svg+xml';
600 var type = 'image/svg+xml';
585 var toinsert = this.create_output_subarea(md, "output_svg", type);
601 var toinsert = this.create_output_subarea(md, "output_svg", type);
586 toinsert.append(svg);
602 toinsert.append(svg);
587 element.append(toinsert);
603 element.append(toinsert);
588 };
604 };
589
605
590
606
591 OutputArea.prototype._dblclick_to_reset_size = function (img) {
607 OutputArea.prototype._dblclick_to_reset_size = function (img) {
592 // schedule wrapping image in resizable after a delay,
608 // schedule wrapping image in resizable after a delay,
593 // so we don't end up calling resize on a zero-size object
609 // so we don't end up calling resize on a zero-size object
594 var that = this;
610 var that = this;
595 setTimeout(function () {
611 setTimeout(function () {
596 var h0 = img.height();
612 var h0 = img.height();
597 var w0 = img.width();
613 var w0 = img.width();
598 if (!(h0 && w0)) {
614 if (!(h0 && w0)) {
599 // zero size, schedule another timeout
615 // zero size, schedule another timeout
600 that._dblclick_to_reset_size(img);
616 that._dblclick_to_reset_size(img);
601 return;
617 return;
602 }
618 }
603 img.resizable({
619 img.resizable({
604 aspectRatio: true,
620 aspectRatio: true,
605 autoHide: true
621 autoHide: true
606 });
622 });
607 img.dblclick(function () {
623 img.dblclick(function () {
608 // resize wrapper & image together for some reason:
624 // resize wrapper & image together for some reason:
609 img.parent().height(h0);
625 img.parent().height(h0);
610 img.height(h0);
626 img.height(h0);
611 img.parent().width(w0);
627 img.parent().width(w0);
612 img.width(w0);
628 img.width(w0);
613 });
629 });
614 }, 250);
630 }, 250);
615 };
631 };
616
632
617
633
618 OutputArea.prototype.append_png = function (png, md, element) {
634 OutputArea.prototype.append_png = function (png, md, element) {
619 var type = 'image/png';
635 var type = 'image/png';
620 var toinsert = this.create_output_subarea(md, "output_png", type);
636 var toinsert = this.create_output_subarea(md, "output_png", type);
621 var img = $("<img/>");
637 var img = $("<img/>");
622 img[0].setAttribute('src','data:image/png;base64,'+png);
638 img[0].setAttribute('src','data:image/png;base64,'+png);
623 if (md['height']) {
639 if (md['height']) {
624 img[0].setAttribute('height', md['height']);
640 img[0].setAttribute('height', md['height']);
625 }
641 }
626 if (md['width']) {
642 if (md['width']) {
627 img[0].setAttribute('width', md['width']);
643 img[0].setAttribute('width', md['width']);
628 }
644 }
629 this._dblclick_to_reset_size(img);
645 this._dblclick_to_reset_size(img);
630 toinsert.append(img);
646 toinsert.append(img);
631 element.append(toinsert);
647 element.append(toinsert);
632 };
648 };
633
649
634
650
635 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
651 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
636 var type = 'image/jpeg';
652 var type = 'image/jpeg';
637 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
653 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
638 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
654 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
639 if (md['height']) {
655 if (md['height']) {
640 img.attr('height', md['height']);
656 img.attr('height', md['height']);
641 }
657 }
642 if (md['width']) {
658 if (md['width']) {
643 img.attr('width', md['width']);
659 img.attr('width', md['width']);
644 }
660 }
645 this._dblclick_to_reset_size(img);
661 this._dblclick_to_reset_size(img);
646 toinsert.append(img);
662 toinsert.append(img);
647 element.append(toinsert);
663 element.append(toinsert);
648 };
664 };
649
665
650
666
651 OutputArea.prototype.append_latex = function (latex, md, element) {
667 OutputArea.prototype.append_latex = function (latex, md, element) {
652 // This method cannot do the typesetting because the latex first has to
668 // This method cannot do the typesetting because the latex first has to
653 // be on the page.
669 // be on the page.
654 var type = 'text/latex';
670 var type = 'text/latex';
655 var toinsert = this.create_output_subarea(md, "output_latex", type);
671 var toinsert = this.create_output_subarea(md, "output_latex", type);
656 toinsert.append(latex);
672 toinsert.append(latex);
657 element.append(toinsert);
673 element.append(toinsert);
658 };
674 };
659
675
660 OutputArea.append_map = {
676 OutputArea.append_map = {
661 "text/plain" : OutputArea.prototype.append_text,
677 "text/plain" : OutputArea.prototype.append_text,
662 "text/html" : OutputArea.prototype.append_html,
678 "text/html" : OutputArea.prototype.append_html,
663 "image/svg+xml" : OutputArea.prototype.append_svg,
679 "image/svg+xml" : OutputArea.prototype.append_svg,
664 "image/png" : OutputArea.prototype.append_png,
680 "image/png" : OutputArea.prototype.append_png,
665 "image/jpeg" : OutputArea.prototype.append_jpeg,
681 "image/jpeg" : OutputArea.prototype.append_jpeg,
666 "text/latex" : OutputArea.prototype.append_latex,
682 "text/latex" : OutputArea.prototype.append_latex,
667 "application/json" : OutputArea.prototype.append_json,
683 "application/json" : OutputArea.prototype.append_json,
668 "application/javascript" : OutputArea.prototype.append_javascript,
684 "application/javascript" : OutputArea.prototype.append_javascript,
669 };
685 };
670
686
671 OutputArea.prototype.append_raw_input = function (msg) {
687 OutputArea.prototype.append_raw_input = function (msg) {
672 var that = this;
688 var that = this;
673 this.expand();
689 this.expand();
674 var content = msg.content;
690 var content = msg.content;
675 var area = this.create_output_area();
691 var area = this.create_output_area();
676
692
677 // disable any other raw_inputs, if they are left around
693 // disable any other raw_inputs, if they are left around
678 $("div.output_subarea.raw_input").remove();
694 $("div.output_subarea.raw_input").remove();
679
695
680 area.append(
696 area.append(
681 $("<div/>")
697 $("<div/>")
682 .addClass("box-flex1 output_subarea raw_input")
698 .addClass("box-flex1 output_subarea raw_input")
683 .append(
699 .append(
684 $("<span/>")
700 $("<span/>")
685 .addClass("input_prompt")
701 .addClass("input_prompt")
686 .text(content.prompt)
702 .text(content.prompt)
687 )
703 )
688 .append(
704 .append(
689 $("<input/>")
705 $("<input/>")
690 .addClass("raw_input")
706 .addClass("raw_input")
691 .attr('type', 'text')
707 .attr('type', 'text')
692 .attr("size", 47)
708 .attr("size", 47)
693 .keydown(function (event, ui) {
709 .keydown(function (event, ui) {
694 // make sure we submit on enter,
710 // make sure we submit on enter,
695 // and don't re-execute the *cell* on shift-enter
711 // and don't re-execute the *cell* on shift-enter
696 if (event.which === utils.keycodes.ENTER) {
712 if (event.which === utils.keycodes.ENTER) {
697 that._submit_raw_input();
713 that._submit_raw_input();
698 return false;
714 return false;
699 }
715 }
700 })
716 })
701 )
717 )
702 );
718 );
703
719
704 this.element.append(area);
720 this.element.append(area);
705 var raw_input = area.find('input.raw_input');
721 var raw_input = area.find('input.raw_input');
706 // Register events that enable/disable the keyboard manager while raw
722 // Register events that enable/disable the keyboard manager while raw
707 // input is focused.
723 // input is focused.
708 IPython.keyboard_manager.register_events(raw_input);
724 IPython.keyboard_manager.register_events(raw_input);
709 // Note, the following line used to read raw_input.focus().focus().
725 // Note, the following line used to read raw_input.focus().focus().
710 // This seemed to be needed otherwise only the cell would be focused.
726 // This seemed to be needed otherwise only the cell would be focused.
711 // But with the modal UI, this seems to work fine with one call to focus().
727 // But with the modal UI, this seems to work fine with one call to focus().
712 raw_input.focus();
728 raw_input.focus();
713 }
729 }
714
730
715 OutputArea.prototype._submit_raw_input = function (evt) {
731 OutputArea.prototype._submit_raw_input = function (evt) {
716 var container = this.element.find("div.raw_input");
732 var container = this.element.find("div.raw_input");
717 var theprompt = container.find("span.input_prompt");
733 var theprompt = container.find("span.input_prompt");
718 var theinput = container.find("input.raw_input");
734 var theinput = container.find("input.raw_input");
719 var value = theinput.val();
735 var value = theinput.val();
720 var content = {
736 var content = {
721 output_type : 'stream',
737 output_type : 'stream',
722 name : 'stdout',
738 name : 'stdout',
723 text : theprompt.text() + value + '\n'
739 text : theprompt.text() + value + '\n'
724 }
740 }
725 // remove form container
741 // remove form container
726 container.parent().remove();
742 container.parent().remove();
727 // replace with plaintext version in stdout
743 // replace with plaintext version in stdout
728 this.append_output(content, false);
744 this.append_output(content, false);
729 $([IPython.events]).trigger('send_input_reply.Kernel', value);
745 $([IPython.events]).trigger('send_input_reply.Kernel', value);
730 }
746 }
731
747
732
748
733 OutputArea.prototype.handle_clear_output = function (msg) {
749 OutputArea.prototype.handle_clear_output = function (msg) {
734 this.clear_output(msg.content.wait);
750 this.clear_output(msg.content.wait);
735 };
751 };
736
752
737
753
738 OutputArea.prototype.clear_output = function(wait) {
754 OutputArea.prototype.clear_output = function(wait) {
739 if (wait) {
755 if (wait) {
740
756
741 // If a clear is queued, clear before adding another to the queue.
757 // If a clear is queued, clear before adding another to the queue.
742 if (this.clear_queued) {
758 if (this.clear_queued) {
743 this.clear_output(false);
759 this.clear_output(false);
744 };
760 };
745
761
746 this.clear_queued = true;
762 this.clear_queued = true;
747 } else {
763 } else {
748
764
749 // Fix the output div's height if the clear_output is waiting for
765 // Fix the output div's height if the clear_output is waiting for
750 // new output (it is being used in an animation).
766 // new output (it is being used in an animation).
751 if (this.clear_queued) {
767 if (this.clear_queued) {
752 var height = this.element.height();
768 var height = this.element.height();
753 this.element.height(height);
769 this.element.height(height);
754 this.clear_queued = false;
770 this.clear_queued = false;
755 }
771 }
756
772
757 // clear all, no need for logic
773 // clear all, no need for logic
758 this.element.html("");
774 this.element.html("");
759 this.outputs = [];
775 this.outputs = [];
776 this.trusted = true;
760 this.unscroll_area();
777 this.unscroll_area();
761 return;
778 return;
762 };
779 };
763 };
780 };
764
781
765
782
766 // JSON serialization
783 // JSON serialization
767
784
768 OutputArea.prototype.fromJSON = function (outputs) {
785 OutputArea.prototype.fromJSON = function (outputs) {
769 var len = outputs.length;
786 var len = outputs.length;
770 var data;
787 var data;
771
788
772 // We don't want to display javascript on load, so remove it from the
773 // display order for the duration of this function call, but be sure to
774 // put it back in there so incoming messages that contain javascript
775 // representations get displayed
776 var js_index = OutputArea.display_order.indexOf('application/javascript');
777 OutputArea.display_order.splice(js_index, 1);
778
779 for (var i=0; i<len; i++) {
789 for (var i=0; i<len; i++) {
780 data = outputs[i];
790 data = outputs[i];
781 var msg_type = data.output_type;
791 var msg_type = data.output_type;
782 if (msg_type === "display_data" || msg_type === "pyout") {
792 if (msg_type === "display_data" || msg_type === "pyout") {
783 // convert short keys to mime keys
793 // convert short keys to mime keys
784 // TODO: remove mapping of short keys when we update to nbformat 4
794 // TODO: remove mapping of short keys when we update to nbformat 4
785 data = this.rename_keys(data, OutputArea.mime_map_r);
795 data = this.rename_keys(data, OutputArea.mime_map_r);
786 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
796 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
787 }
797 }
788
798
789 this.append_output(data);
799 this.append_output(data);
790 }
800 }
791
792 // reinsert javascript into display order, see note above
793 OutputArea.display_order.splice(js_index, 0, 'application/javascript');
794 };
801 };
795
802
796
803
797 OutputArea.prototype.toJSON = function () {
804 OutputArea.prototype.toJSON = function () {
798 var outputs = [];
805 var outputs = [];
799 var len = this.outputs.length;
806 var len = this.outputs.length;
800 var data;
807 var data;
801 for (var i=0; i<len; i++) {
808 for (var i=0; i<len; i++) {
802 data = this.outputs[i];
809 data = this.outputs[i];
803 var msg_type = data.output_type;
810 var msg_type = data.output_type;
804 if (msg_type === "display_data" || msg_type === "pyout") {
811 if (msg_type === "display_data" || msg_type === "pyout") {
805 // convert mime keys to short keys
812 // convert mime keys to short keys
806 data = this.rename_keys(data, OutputArea.mime_map);
813 data = this.rename_keys(data, OutputArea.mime_map);
807 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
814 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
808 }
815 }
809 outputs[i] = data;
816 outputs[i] = data;
810 }
817 }
811 return outputs;
818 return outputs;
812 };
819 };
813
820
814
821
815 IPython.OutputArea = OutputArea;
822 IPython.OutputArea = OutputArea;
816
823
817 return IPython;
824 return IPython;
818
825
819 }(IPython));
826 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now