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