##// END OF EJS Templates
Merge Security Pull Request: google-caja...
MinRK -
r15674:f33c5e99 merge
parent child Browse files
Show More
@@ -0,0 +1,126 b''
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2014 The IPython Development Team
3 //
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
7
8 //============================================================================
9 // Utilities
10 //============================================================================
11 IPython.namespace('IPython.security');
12
13 IPython.security = (function (IPython) {
14 "use strict";
15
16 var utils = IPython.utils;
17
18 var noop = function (x) { return x; };
19
20 var caja;
21 if (window && window.html) {
22 caja = window.html;
23 caja.html4 = window.html4;
24 caja.sanitizeStylesheet = window.sanitizeStylesheet;
25 }
26
27 var sanitizeAttribs = function (tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger) {
28 // add trusting data-attributes to the default sanitizeAttribs from caja
29 // this function is mostly copied from the caja source
30 var ATTRIBS = caja.html4.ATTRIBS;
31 for (var i = 0; i < attribs.length; i += 2) {
32 var attribName = attribs[i];
33 if (attribName.substr(0,5) == 'data-') {
34 var attribKey = '*::' + attribName;
35 if (!ATTRIBS.hasOwnProperty(attribKey)) {
36 ATTRIBS[attribKey] = 0;
37 }
38 }
39 }
40 return caja.sanitizeAttribs(tagName, attribs, opt_naiveUriRewriter, opt_nmTokenPolicy, opt_logger);
41 };
42
43 var sanitize_css = function (css, tagPolicy) {
44 // sanitize CSS
45 // like sanitize_html, but for CSS
46 // called by sanitize_stylesheets
47 return caja.sanitizeStylesheet(
48 window.location.pathname,
49 css,
50 {
51 containerClass: null,
52 idSuffix: '',
53 tagPolicy: tagPolicy,
54 virtualizeAttrName: noop
55 },
56 noop
57 );
58 };
59
60 var sanitize_stylesheets = function (html, tagPolicy) {
61 // sanitize just the css in style tags in a block of html
62 // called by sanitize_html, if allow_css is true
63 var h = $("<div/>").append(html);
64 var style_tags = h.find("style");
65 if (!style_tags.length) {
66 // no style tags to sanitize
67 return html;
68 }
69 style_tags.each(function(i, style) {
70 style.innerHTML = sanitize_css(style.innerHTML, tagPolicy);
71 });
72 return h.html();
73 };
74
75 var sanitize_html = function (html, allow_css) {
76 // sanitize HTML
77 // if allow_css is true (default: false), CSS is sanitized as well.
78 // otherwise, CSS elements and attributes are simply removed.
79 var html4 = caja.html4;
80
81 if (allow_css) {
82 // allow sanitization of style tags,
83 // not just scrubbing
84 html4.ELEMENTS.style &= ~html4.eflags.UNSAFE;
85 html4.ATTRIBS.style = html4.atype.STYLE;
86 } else {
87 // scrub all CSS
88 html4.ELEMENTS.style |= html4.eflags.UNSAFE;
89 html4.ATTRIBS.style = html4.atype.SCRIPT;
90 }
91
92 var record_messages = function (msg, opts) {
93 console.log("HTML Sanitizer", msg, opts);
94 };
95
96 var policy = function (tagName, attribs) {
97 if (!(html4.ELEMENTS[tagName] & html4.eflags.UNSAFE)) {
98 return {
99 'attribs': sanitizeAttribs(tagName, attribs,
100 noop, noop, record_messages)
101 };
102 } else {
103 record_messages(tagName + " removed", {
104 change: "removed",
105 tagName: tagName
106 });
107 }
108 };
109
110 var sanitized = caja.sanitizeWithPolicy(html, policy);
111
112 if (allow_css) {
113 // sanitize style tags as stylesheets
114 sanitized = sanitize_stylesheets(result.sanitized, policy);
115 }
116
117 return sanitized;
118 };
119
120 return {
121 caja: caja,
122 sanitize_html: sanitize_html
123 };
124
125 }(IPython));
126
@@ -0,0 +1,57 b''
1 safe_tests = [
2 "<p>Hi there</p>",
3 '<h1 class="foo">Hi There!</h1>',
4 '<a data-cite="foo">citation</a>',
5 '<div><span>Hi There</span></div>',
6 ];
7
8 unsafe_tests = [
9 "<script>alert(999);</script>",
10 '<a onmouseover="alert(999)">999</a>',
11 '<a onmouseover=alert(999)>999</a>',
12 '<IMG """><SCRIPT>alert("XSS")</SCRIPT>">',
13 '<IMG SRC=# onmouseover="alert(999)">',
14 '<<SCRIPT>alert(999);//<</SCRIPT>',
15 '<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >',
16 '<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">',
17 '<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(999);">',
18 '<IFRAME SRC="javascript:alert(999);"></IFRAME>',
19 '<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>',
20 '<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>',
21 // CSS is scrubbed
22 '<style src="http://untrusted/style.css"></style>',
23 '<style>div#notebook { background-color: alert-red; }</style>',
24 '<div style="background-color: alert-red;"></div>',
25 ];
26
27 var truncate = function (s, n) {
28 // truncate a string with an ellipsis
29 if (s.length > n) {
30 return s.substr(0, n-3) + '...';
31 } else {
32 return s;
33 }
34 };
35
36 casper.notebook_test(function () {
37 this.each(safe_tests, function (self, item) {
38 var sanitized = self.evaluate(function (item) {
39 return IPython.security.sanitize_html(item);
40 }, item);
41
42 // string equality may be too strict, but it works for now
43 this.test.assertEquals(sanitized, item, "Safe: '" + truncate(item, 32) + "'");
44 });
45
46 this.each(unsafe_tests, function (self, item) {
47 var sanitized = self.evaluate(function (item) {
48 return IPython.security.sanitize_html(item);
49 }, item);
50
51 this.test.assertNotEquals(sanitized, item,
52 "Sanitized: '" + truncate(item, 32) +
53 "' => '" + truncate(sanitized, 32) + "'"
54 );
55 this.test.assertEquals(sanitized.indexOf("alert"), -1, "alert removed");
56 });
57 }); No newline at end of file
@@ -1,483 +1,483 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 os
21 import os
22 import glob
22 import glob
23 import shutil
23 import shutil
24
24
25 from tornado import web
25 from tornado import web
26
26
27 from .nbmanager import NotebookManager
27 from .nbmanager import NotebookManager
28 from IPython.nbformat import current
28 from IPython.nbformat import current
29 from IPython.utils.traitlets import Unicode, Bool, TraitError
29 from IPython.utils.traitlets import Unicode, Bool, TraitError
30 from IPython.utils.py3compat import getcwd
30 from IPython.utils.py3compat import getcwd
31 from IPython.utils import tz
31 from IPython.utils import tz
32 from IPython.html.utils import is_hidden, to_os_path
32 from IPython.html.utils import is_hidden, to_os_path
33
33
34 def sort_key(item):
34 def sort_key(item):
35 """Case-insensitive sorting."""
35 """Case-insensitive sorting."""
36 return item['name'].lower()
36 return item['name'].lower()
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Classes
39 # Classes
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 class FileNotebookManager(NotebookManager):
42 class FileNotebookManager(NotebookManager):
43
43
44 save_script = Bool(False, config=True,
44 save_script = Bool(False, config=True,
45 help="""Automatically create a Python script when saving the notebook.
45 help="""Automatically create a Python script when saving the notebook.
46
46
47 For easier use of import, %run and %load across notebooks, a
47 For easier use of import, %run and %load across notebooks, a
48 <notebook-name>.py script will be created next to any
48 <notebook-name>.py script will be created next to any
49 <notebook-name>.ipynb on each save. This can also be set with the
49 <notebook-name>.ipynb on each save. This can also be set with the
50 short `--script` flag.
50 short `--script` flag.
51 """
51 """
52 )
52 )
53 notebook_dir = Unicode(getcwd(), config=True)
53 notebook_dir = Unicode(getcwd(), config=True)
54
54
55 def _notebook_dir_changed(self, name, old, new):
55 def _notebook_dir_changed(self, name, old, new):
56 """Do a bit of validation of the notebook dir."""
56 """Do a bit of validation of the notebook dir."""
57 if not os.path.isabs(new):
57 if not os.path.isabs(new):
58 # If we receive a non-absolute path, make it absolute.
58 # If we receive a non-absolute path, make it absolute.
59 self.notebook_dir = os.path.abspath(new)
59 self.notebook_dir = os.path.abspath(new)
60 return
60 return
61 if not os.path.exists(new) or not os.path.isdir(new):
61 if not os.path.exists(new) or not os.path.isdir(new):
62 raise TraitError("notebook dir %r is not a directory" % new)
62 raise TraitError("notebook dir %r is not a directory" % new)
63
63
64 checkpoint_dir = Unicode(config=True,
64 checkpoint_dir = Unicode(config=True,
65 help="""The location in which to keep notebook checkpoints
65 help="""The location in which to keep notebook checkpoints
66
66
67 By default, it is notebook-dir/.ipynb_checkpoints
67 By default, it is notebook-dir/.ipynb_checkpoints
68 """
68 """
69 )
69 )
70 def _checkpoint_dir_default(self):
70 def _checkpoint_dir_default(self):
71 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
71 return os.path.join(self.notebook_dir, '.ipynb_checkpoints')
72
72
73 def _checkpoint_dir_changed(self, name, old, new):
73 def _checkpoint_dir_changed(self, name, old, new):
74 """do a bit of validation of the checkpoint dir"""
74 """do a bit of validation of the checkpoint dir"""
75 if not os.path.isabs(new):
75 if not os.path.isabs(new):
76 # If we receive a non-absolute path, make it absolute.
76 # If we receive a non-absolute path, make it absolute.
77 abs_new = os.path.abspath(new)
77 abs_new = os.path.abspath(new)
78 self.checkpoint_dir = abs_new
78 self.checkpoint_dir = abs_new
79 return
79 return
80 if os.path.exists(new) and not os.path.isdir(new):
80 if os.path.exists(new) and not os.path.isdir(new):
81 raise TraitError("checkpoint dir %r is not a directory" % new)
81 raise TraitError("checkpoint dir %r is not a directory" % new)
82 if not os.path.exists(new):
82 if not os.path.exists(new):
83 self.log.info("Creating checkpoint dir %s", new)
83 self.log.info("Creating checkpoint dir %s", new)
84 try:
84 try:
85 os.mkdir(new)
85 os.mkdir(new)
86 except:
86 except:
87 raise TraitError("Couldn't create checkpoint dir %r" % new)
87 raise TraitError("Couldn't create checkpoint dir %r" % new)
88
88
89 def get_notebook_names(self, path=''):
89 def get_notebook_names(self, path=''):
90 """List all notebook names in the notebook dir and path."""
90 """List all notebook names in the notebook dir and path."""
91 path = path.strip('/')
91 path = path.strip('/')
92 if not os.path.isdir(self._get_os_path(path=path)):
92 if not os.path.isdir(self._get_os_path(path=path)):
93 raise web.HTTPError(404, 'Directory not found: ' + path)
93 raise web.HTTPError(404, 'Directory not found: ' + path)
94 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
94 names = glob.glob(self._get_os_path('*'+self.filename_ext, path))
95 names = [os.path.basename(name)
95 names = [os.path.basename(name)
96 for name in names]
96 for name in names]
97 return names
97 return names
98
98
99 def path_exists(self, path):
99 def path_exists(self, path):
100 """Does the API-style path (directory) actually exist?
100 """Does the API-style path (directory) actually exist?
101
101
102 Parameters
102 Parameters
103 ----------
103 ----------
104 path : string
104 path : string
105 The path to check. This is an API path (`/` separated,
105 The path to check. This is an API path (`/` separated,
106 relative to base notebook-dir).
106 relative to base notebook-dir).
107
107
108 Returns
108 Returns
109 -------
109 -------
110 exists : bool
110 exists : bool
111 Whether the path is indeed a directory.
111 Whether the path is indeed a directory.
112 """
112 """
113 path = path.strip('/')
113 path = path.strip('/')
114 os_path = self._get_os_path(path=path)
114 os_path = self._get_os_path(path=path)
115 return os.path.isdir(os_path)
115 return os.path.isdir(os_path)
116
116
117 def is_hidden(self, path):
117 def is_hidden(self, path):
118 """Does the API style path correspond to a hidden directory or file?
118 """Does the API style path correspond to a hidden directory or file?
119
119
120 Parameters
120 Parameters
121 ----------
121 ----------
122 path : string
122 path : string
123 The path to check. This is an API path (`/` separated,
123 The path to check. This is an API path (`/` separated,
124 relative to base notebook-dir).
124 relative to base notebook-dir).
125
125
126 Returns
126 Returns
127 -------
127 -------
128 exists : bool
128 exists : bool
129 Whether the path is hidden.
129 Whether the path is hidden.
130
130
131 """
131 """
132 path = path.strip('/')
132 path = path.strip('/')
133 os_path = self._get_os_path(path=path)
133 os_path = self._get_os_path(path=path)
134 return is_hidden(os_path, self.notebook_dir)
134 return is_hidden(os_path, self.notebook_dir)
135
135
136 def _get_os_path(self, name=None, path=''):
136 def _get_os_path(self, name=None, path=''):
137 """Given a notebook name and a URL path, return its file system
137 """Given a notebook name and a URL path, return its file system
138 path.
138 path.
139
139
140 Parameters
140 Parameters
141 ----------
141 ----------
142 name : string
142 name : string
143 The name of a notebook file with the .ipynb extension
143 The name of a notebook file with the .ipynb extension
144 path : string
144 path : string
145 The relative URL path (with '/' as separator) to the named
145 The relative URL path (with '/' as separator) to the named
146 notebook.
146 notebook.
147
147
148 Returns
148 Returns
149 -------
149 -------
150 path : string
150 path : string
151 A file system path that combines notebook_dir (location where
151 A file system path that combines notebook_dir (location where
152 server started), the relative path, and the filename with the
152 server started), the relative path, and the filename with the
153 current operating system's url.
153 current operating system's url.
154 """
154 """
155 if name is not None:
155 if name is not None:
156 path = path + '/' + name
156 path = path + '/' + name
157 return to_os_path(path, self.notebook_dir)
157 return to_os_path(path, self.notebook_dir)
158
158
159 def notebook_exists(self, name, path=''):
159 def notebook_exists(self, name, path=''):
160 """Returns a True if the notebook exists. Else, returns False.
160 """Returns a True if the notebook exists. Else, returns False.
161
161
162 Parameters
162 Parameters
163 ----------
163 ----------
164 name : string
164 name : string
165 The name of the notebook you are checking.
165 The name of the notebook you are checking.
166 path : string
166 path : string
167 The relative path to the notebook (with '/' as separator)
167 The relative path to the notebook (with '/' as separator)
168
168
169 Returns
169 Returns
170 -------
170 -------
171 bool
171 bool
172 """
172 """
173 path = path.strip('/')
173 path = path.strip('/')
174 nbpath = self._get_os_path(name, path=path)
174 nbpath = self._get_os_path(name, path=path)
175 return os.path.isfile(nbpath)
175 return os.path.isfile(nbpath)
176
176
177 # TODO: Remove this after we create the contents web service and directories are
177 # TODO: Remove this after we create the contents web service and directories are
178 # no longer listed by the notebook web service.
178 # no longer listed by the notebook web service.
179 def list_dirs(self, path):
179 def list_dirs(self, path):
180 """List the directories for a given API style path."""
180 """List the directories for a given API style path."""
181 path = path.strip('/')
181 path = path.strip('/')
182 os_path = self._get_os_path('', path)
182 os_path = self._get_os_path('', path)
183 if not os.path.isdir(os_path):
183 if not os.path.isdir(os_path):
184 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
184 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
185 elif is_hidden(os_path, self.notebook_dir):
185 elif is_hidden(os_path, self.notebook_dir):
186 self.log.info("Refusing to serve hidden directory, via 404 Error")
186 self.log.info("Refusing to serve hidden directory, via 404 Error")
187 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
187 raise web.HTTPError(404, u'directory does not exist: %r' % os_path)
188 dir_names = os.listdir(os_path)
188 dir_names = os.listdir(os_path)
189 dirs = []
189 dirs = []
190 for name in dir_names:
190 for name in dir_names:
191 os_path = self._get_os_path(name, path)
191 os_path = self._get_os_path(name, path)
192 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
192 if os.path.isdir(os_path) and not is_hidden(os_path, self.notebook_dir)\
193 and self.should_list(name):
193 and self.should_list(name):
194 try:
194 try:
195 model = self.get_dir_model(name, path)
195 model = self.get_dir_model(name, path)
196 except IOError:
196 except IOError:
197 pass
197 pass
198 dirs.append(model)
198 dirs.append(model)
199 dirs = sorted(dirs, key=sort_key)
199 dirs = sorted(dirs, key=sort_key)
200 return dirs
200 return dirs
201
201
202 # TODO: Remove this after we create the contents web service and directories are
202 # TODO: Remove this after we create the contents web service and directories are
203 # no longer listed by the notebook web service.
203 # no longer listed by the notebook web service.
204 def get_dir_model(self, name, path=''):
204 def get_dir_model(self, name, path=''):
205 """Get the directory model given a directory name and its API style path"""
205 """Get the directory model given a directory name and its API style path"""
206 path = path.strip('/')
206 path = path.strip('/')
207 os_path = self._get_os_path(name, path)
207 os_path = self._get_os_path(name, path)
208 if not os.path.isdir(os_path):
208 if not os.path.isdir(os_path):
209 raise IOError('directory does not exist: %r' % os_path)
209 raise IOError('directory does not exist: %r' % os_path)
210 info = os.stat(os_path)
210 info = os.stat(os_path)
211 last_modified = tz.utcfromtimestamp(info.st_mtime)
211 last_modified = tz.utcfromtimestamp(info.st_mtime)
212 created = tz.utcfromtimestamp(info.st_ctime)
212 created = tz.utcfromtimestamp(info.st_ctime)
213 # Create the notebook model.
213 # Create the notebook model.
214 model ={}
214 model ={}
215 model['name'] = name
215 model['name'] = name
216 model['path'] = path
216 model['path'] = path
217 model['last_modified'] = last_modified
217 model['last_modified'] = last_modified
218 model['created'] = created
218 model['created'] = created
219 model['type'] = 'directory'
219 model['type'] = 'directory'
220 return model
220 return model
221
221
222 def list_notebooks(self, path):
222 def list_notebooks(self, path):
223 """Returns a list of dictionaries that are the standard model
223 """Returns a list of dictionaries that are the standard model
224 for all notebooks in the relative 'path'.
224 for all notebooks in the relative 'path'.
225
225
226 Parameters
226 Parameters
227 ----------
227 ----------
228 path : str
228 path : str
229 the URL path that describes the relative path for the
229 the URL path that describes the relative path for the
230 listed notebooks
230 listed notebooks
231
231
232 Returns
232 Returns
233 -------
233 -------
234 notebooks : list of dicts
234 notebooks : list of dicts
235 a list of the notebook models without 'content'
235 a list of the notebook models without 'content'
236 """
236 """
237 path = path.strip('/')
237 path = path.strip('/')
238 notebook_names = self.get_notebook_names(path)
238 notebook_names = self.get_notebook_names(path)
239 notebooks = [self.get_notebook(name, path, content=False)
239 notebooks = [self.get_notebook(name, path, content=False)
240 for name in notebook_names if self.should_list(name)]
240 for name in notebook_names if self.should_list(name)]
241 notebooks = sorted(notebooks, key=sort_key)
241 notebooks = sorted(notebooks, key=sort_key)
242 return notebooks
242 return notebooks
243
243
244 def get_notebook(self, name, path='', content=True):
244 def get_notebook(self, name, path='', content=True):
245 """ Takes a path and name for a notebook and returns its model
245 """ Takes a path and name for a notebook and returns its model
246
246
247 Parameters
247 Parameters
248 ----------
248 ----------
249 name : str
249 name : str
250 the name of the notebook
250 the name of the notebook
251 path : str
251 path : str
252 the URL path that describes the relative path for
252 the URL path that describes the relative path for
253 the notebook
253 the notebook
254
254
255 Returns
255 Returns
256 -------
256 -------
257 model : dict
257 model : dict
258 the notebook model. If contents=True, returns the 'contents'
258 the notebook model. If contents=True, returns the 'contents'
259 dict in the model as well.
259 dict in the model as well.
260 """
260 """
261 path = path.strip('/')
261 path = path.strip('/')
262 if not self.notebook_exists(name=name, path=path):
262 if not self.notebook_exists(name=name, path=path):
263 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
263 raise web.HTTPError(404, u'Notebook does not exist: %s' % name)
264 os_path = self._get_os_path(name, path)
264 os_path = self._get_os_path(name, path)
265 info = os.stat(os_path)
265 info = os.stat(os_path)
266 last_modified = tz.utcfromtimestamp(info.st_mtime)
266 last_modified = tz.utcfromtimestamp(info.st_mtime)
267 created = tz.utcfromtimestamp(info.st_ctime)
267 created = tz.utcfromtimestamp(info.st_ctime)
268 # Create the notebook model.
268 # Create the notebook model.
269 model ={}
269 model ={}
270 model['name'] = name
270 model['name'] = name
271 model['path'] = path
271 model['path'] = path
272 model['last_modified'] = last_modified
272 model['last_modified'] = last_modified
273 model['created'] = created
273 model['created'] = created
274 model['type'] = 'notebook'
274 model['type'] = 'notebook'
275 if content:
275 if content:
276 with io.open(os_path, 'r', encoding='utf-8') as f:
276 with io.open(os_path, 'r', encoding='utf-8') as f:
277 try:
277 try:
278 nb = current.read(f, u'json')
278 nb = current.read(f, u'json')
279 except Exception as e:
279 except Exception as e:
280 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
280 raise web.HTTPError(400, u"Unreadable Notebook: %s %s" % (os_path, e))
281 self.mark_trusted_cells(nb, path, name)
281 self.mark_trusted_cells(nb, name, path)
282 model['content'] = nb
282 model['content'] = nb
283 return model
283 return model
284
284
285 def save_notebook(self, model, name='', path=''):
285 def save_notebook(self, model, name='', path=''):
286 """Save the notebook model and return the model with no content."""
286 """Save the notebook model and return the model with no content."""
287 path = path.strip('/')
287 path = path.strip('/')
288
288
289 if 'content' not in model:
289 if 'content' not in model:
290 raise web.HTTPError(400, u'No notebook JSON data provided')
290 raise web.HTTPError(400, u'No notebook JSON data provided')
291
291
292 # One checkpoint should always exist
292 # One checkpoint should always exist
293 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
293 if self.notebook_exists(name, path) and not self.list_checkpoints(name, path):
294 self.create_checkpoint(name, path)
294 self.create_checkpoint(name, path)
295
295
296 new_path = model.get('path', path).strip('/')
296 new_path = model.get('path', path).strip('/')
297 new_name = model.get('name', name)
297 new_name = model.get('name', name)
298
298
299 if path != new_path or name != new_name:
299 if path != new_path or name != new_name:
300 self.rename_notebook(name, path, new_name, new_path)
300 self.rename_notebook(name, path, new_name, new_path)
301
301
302 # Save the notebook file
302 # Save the notebook file
303 os_path = self._get_os_path(new_name, new_path)
303 os_path = self._get_os_path(new_name, new_path)
304 nb = current.to_notebook_json(model['content'])
304 nb = current.to_notebook_json(model['content'])
305
305
306 self.check_and_sign(nb, new_path, new_name)
306 self.check_and_sign(nb, new_name, new_path)
307
307
308 if 'name' in nb['metadata']:
308 if 'name' in nb['metadata']:
309 nb['metadata']['name'] = u''
309 nb['metadata']['name'] = u''
310 try:
310 try:
311 self.log.debug("Autosaving notebook %s", os_path)
311 self.log.debug("Autosaving notebook %s", os_path)
312 with io.open(os_path, 'w', encoding='utf-8') as f:
312 with io.open(os_path, 'w', encoding='utf-8') as f:
313 current.write(nb, f, u'json')
313 current.write(nb, f, u'json')
314 except Exception as e:
314 except Exception as e:
315 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
315 raise web.HTTPError(400, u'Unexpected error while autosaving notebook: %s %s' % (os_path, e))
316
316
317 # Save .py script as well
317 # Save .py script as well
318 if self.save_script:
318 if self.save_script:
319 py_path = os.path.splitext(os_path)[0] + '.py'
319 py_path = os.path.splitext(os_path)[0] + '.py'
320 self.log.debug("Writing script %s", py_path)
320 self.log.debug("Writing script %s", py_path)
321 try:
321 try:
322 with io.open(py_path, 'w', encoding='utf-8') as f:
322 with io.open(py_path, 'w', encoding='utf-8') as f:
323 current.write(nb, f, u'py')
323 current.write(nb, f, u'py')
324 except Exception as e:
324 except Exception as e:
325 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
325 raise web.HTTPError(400, u'Unexpected error while saving notebook as script: %s %s' % (py_path, e))
326
326
327 model = self.get_notebook(new_name, new_path, content=False)
327 model = self.get_notebook(new_name, new_path, content=False)
328 return model
328 return model
329
329
330 def update_notebook(self, model, name, path=''):
330 def update_notebook(self, model, name, path=''):
331 """Update the notebook's path and/or name"""
331 """Update the notebook's path and/or name"""
332 path = path.strip('/')
332 path = path.strip('/')
333 new_name = model.get('name', name)
333 new_name = model.get('name', name)
334 new_path = model.get('path', path).strip('/')
334 new_path = model.get('path', path).strip('/')
335 if path != new_path or name != new_name:
335 if path != new_path or name != new_name:
336 self.rename_notebook(name, path, new_name, new_path)
336 self.rename_notebook(name, path, new_name, new_path)
337 model = self.get_notebook(new_name, new_path, content=False)
337 model = self.get_notebook(new_name, new_path, content=False)
338 return model
338 return model
339
339
340 def delete_notebook(self, name, path=''):
340 def delete_notebook(self, name, path=''):
341 """Delete notebook by name and path."""
341 """Delete notebook by name and path."""
342 path = path.strip('/')
342 path = path.strip('/')
343 os_path = self._get_os_path(name, path)
343 os_path = self._get_os_path(name, path)
344 if not os.path.isfile(os_path):
344 if not os.path.isfile(os_path):
345 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
345 raise web.HTTPError(404, u'Notebook does not exist: %s' % os_path)
346
346
347 # clear checkpoints
347 # clear checkpoints
348 for checkpoint in self.list_checkpoints(name, path):
348 for checkpoint in self.list_checkpoints(name, path):
349 checkpoint_id = checkpoint['id']
349 checkpoint_id = checkpoint['id']
350 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
350 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
351 if os.path.isfile(cp_path):
351 if os.path.isfile(cp_path):
352 self.log.debug("Unlinking checkpoint %s", cp_path)
352 self.log.debug("Unlinking checkpoint %s", cp_path)
353 os.unlink(cp_path)
353 os.unlink(cp_path)
354
354
355 self.log.debug("Unlinking notebook %s", os_path)
355 self.log.debug("Unlinking notebook %s", os_path)
356 os.unlink(os_path)
356 os.unlink(os_path)
357
357
358 def rename_notebook(self, old_name, old_path, new_name, new_path):
358 def rename_notebook(self, old_name, old_path, new_name, new_path):
359 """Rename a notebook."""
359 """Rename a notebook."""
360 old_path = old_path.strip('/')
360 old_path = old_path.strip('/')
361 new_path = new_path.strip('/')
361 new_path = new_path.strip('/')
362 if new_name == old_name and new_path == old_path:
362 if new_name == old_name and new_path == old_path:
363 return
363 return
364
364
365 new_os_path = self._get_os_path(new_name, new_path)
365 new_os_path = self._get_os_path(new_name, new_path)
366 old_os_path = self._get_os_path(old_name, old_path)
366 old_os_path = self._get_os_path(old_name, old_path)
367
367
368 # Should we proceed with the move?
368 # Should we proceed with the move?
369 if os.path.isfile(new_os_path):
369 if os.path.isfile(new_os_path):
370 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
370 raise web.HTTPError(409, u'Notebook with name already exists: %s' % new_os_path)
371 if self.save_script:
371 if self.save_script:
372 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
372 old_py_path = os.path.splitext(old_os_path)[0] + '.py'
373 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
373 new_py_path = os.path.splitext(new_os_path)[0] + '.py'
374 if os.path.isfile(new_py_path):
374 if os.path.isfile(new_py_path):
375 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
375 raise web.HTTPError(409, u'Python script with name already exists: %s' % new_py_path)
376
376
377 # Move the notebook file
377 # Move the notebook file
378 try:
378 try:
379 os.rename(old_os_path, new_os_path)
379 os.rename(old_os_path, new_os_path)
380 except Exception as e:
380 except Exception as e:
381 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
381 raise web.HTTPError(500, u'Unknown error renaming notebook: %s %s' % (old_os_path, e))
382
382
383 # Move the checkpoints
383 # Move the checkpoints
384 old_checkpoints = self.list_checkpoints(old_name, old_path)
384 old_checkpoints = self.list_checkpoints(old_name, old_path)
385 for cp in old_checkpoints:
385 for cp in old_checkpoints:
386 checkpoint_id = cp['id']
386 checkpoint_id = cp['id']
387 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
387 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_name, old_path)
388 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
388 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_name, new_path)
389 if os.path.isfile(old_cp_path):
389 if os.path.isfile(old_cp_path):
390 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
390 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
391 os.rename(old_cp_path, new_cp_path)
391 os.rename(old_cp_path, new_cp_path)
392
392
393 # Move the .py script
393 # Move the .py script
394 if self.save_script:
394 if self.save_script:
395 os.rename(old_py_path, new_py_path)
395 os.rename(old_py_path, new_py_path)
396
396
397 # Checkpoint-related utilities
397 # Checkpoint-related utilities
398
398
399 def get_checkpoint_path(self, checkpoint_id, name, path=''):
399 def get_checkpoint_path(self, checkpoint_id, name, path=''):
400 """find the path to a checkpoint"""
400 """find the path to a checkpoint"""
401 path = path.strip('/')
401 path = path.strip('/')
402 basename, _ = os.path.splitext(name)
402 basename, _ = os.path.splitext(name)
403 filename = u"{name}-{checkpoint_id}{ext}".format(
403 filename = u"{name}-{checkpoint_id}{ext}".format(
404 name=basename,
404 name=basename,
405 checkpoint_id=checkpoint_id,
405 checkpoint_id=checkpoint_id,
406 ext=self.filename_ext,
406 ext=self.filename_ext,
407 )
407 )
408 cp_path = os.path.join(path, self.checkpoint_dir, filename)
408 cp_path = os.path.join(path, self.checkpoint_dir, filename)
409 return cp_path
409 return cp_path
410
410
411 def get_checkpoint_model(self, checkpoint_id, name, path=''):
411 def get_checkpoint_model(self, checkpoint_id, name, path=''):
412 """construct the info dict for a given checkpoint"""
412 """construct the info dict for a given checkpoint"""
413 path = path.strip('/')
413 path = path.strip('/')
414 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
414 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
415 stats = os.stat(cp_path)
415 stats = os.stat(cp_path)
416 last_modified = tz.utcfromtimestamp(stats.st_mtime)
416 last_modified = tz.utcfromtimestamp(stats.st_mtime)
417 info = dict(
417 info = dict(
418 id = checkpoint_id,
418 id = checkpoint_id,
419 last_modified = last_modified,
419 last_modified = last_modified,
420 )
420 )
421 return info
421 return info
422
422
423 # public checkpoint API
423 # public checkpoint API
424
424
425 def create_checkpoint(self, name, path=''):
425 def create_checkpoint(self, name, path=''):
426 """Create a checkpoint from the current state of a notebook"""
426 """Create a checkpoint from the current state of a notebook"""
427 path = path.strip('/')
427 path = path.strip('/')
428 nb_path = self._get_os_path(name, path)
428 nb_path = self._get_os_path(name, path)
429 # only the one checkpoint ID:
429 # only the one checkpoint ID:
430 checkpoint_id = u"checkpoint"
430 checkpoint_id = u"checkpoint"
431 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
431 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
432 self.log.debug("creating checkpoint for notebook %s", name)
432 self.log.debug("creating checkpoint for notebook %s", name)
433 if not os.path.exists(self.checkpoint_dir):
433 if not os.path.exists(self.checkpoint_dir):
434 os.mkdir(self.checkpoint_dir)
434 os.mkdir(self.checkpoint_dir)
435 shutil.copy2(nb_path, cp_path)
435 shutil.copy2(nb_path, cp_path)
436
436
437 # return the checkpoint info
437 # return the checkpoint info
438 return self.get_checkpoint_model(checkpoint_id, name, path)
438 return self.get_checkpoint_model(checkpoint_id, name, path)
439
439
440 def list_checkpoints(self, name, path=''):
440 def list_checkpoints(self, name, path=''):
441 """list the checkpoints for a given notebook
441 """list the checkpoints for a given notebook
442
442
443 This notebook manager currently only supports one checkpoint per notebook.
443 This notebook manager currently only supports one checkpoint per notebook.
444 """
444 """
445 path = path.strip('/')
445 path = path.strip('/')
446 checkpoint_id = "checkpoint"
446 checkpoint_id = "checkpoint"
447 path = self.get_checkpoint_path(checkpoint_id, name, path)
447 path = self.get_checkpoint_path(checkpoint_id, name, path)
448 if not os.path.exists(path):
448 if not os.path.exists(path):
449 return []
449 return []
450 else:
450 else:
451 return [self.get_checkpoint_model(checkpoint_id, name, path)]
451 return [self.get_checkpoint_model(checkpoint_id, name, path)]
452
452
453
453
454 def restore_checkpoint(self, checkpoint_id, name, path=''):
454 def restore_checkpoint(self, checkpoint_id, name, path=''):
455 """restore a notebook to a checkpointed state"""
455 """restore a notebook to a checkpointed state"""
456 path = path.strip('/')
456 path = path.strip('/')
457 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
457 self.log.info("restoring Notebook %s from checkpoint %s", name, checkpoint_id)
458 nb_path = self._get_os_path(name, path)
458 nb_path = self._get_os_path(name, path)
459 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
459 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
460 if not os.path.isfile(cp_path):
460 if not os.path.isfile(cp_path):
461 self.log.debug("checkpoint file does not exist: %s", cp_path)
461 self.log.debug("checkpoint file does not exist: %s", cp_path)
462 raise web.HTTPError(404,
462 raise web.HTTPError(404,
463 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
463 u'Notebook checkpoint does not exist: %s-%s' % (name, checkpoint_id)
464 )
464 )
465 # ensure notebook is readable (never restore from an unreadable notebook)
465 # ensure notebook is readable (never restore from an unreadable notebook)
466 with io.open(cp_path, 'r', encoding='utf-8') as f:
466 with io.open(cp_path, 'r', encoding='utf-8') as f:
467 current.read(f, u'json')
467 current.read(f, u'json')
468 shutil.copy2(cp_path, nb_path)
468 shutil.copy2(cp_path, nb_path)
469 self.log.debug("copying %s -> %s", cp_path, nb_path)
469 self.log.debug("copying %s -> %s", cp_path, nb_path)
470
470
471 def delete_checkpoint(self, checkpoint_id, name, path=''):
471 def delete_checkpoint(self, checkpoint_id, name, path=''):
472 """delete a notebook's checkpoint"""
472 """delete a notebook's checkpoint"""
473 path = path.strip('/')
473 path = path.strip('/')
474 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
474 cp_path = self.get_checkpoint_path(checkpoint_id, name, path)
475 if not os.path.isfile(cp_path):
475 if not os.path.isfile(cp_path):
476 raise web.HTTPError(404,
476 raise web.HTTPError(404,
477 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
477 u'Notebook checkpoint does not exist: %s%s-%s' % (path, name, checkpoint_id)
478 )
478 )
479 self.log.debug("unlinking %s", cp_path)
479 self.log.debug("unlinking %s", cp_path)
480 os.unlink(cp_path)
480 os.unlink(cp_path)
481
481
482 def info_string(self):
482 def info_string(self):
483 return "Serving notebooks from local directory: %s" % self.notebook_dir
483 return "Serving notebooks from local directory: %s" % self.notebook_dir
@@ -1,290 +1,288 b''
1 """Tornado handlers for the notebooks web service.
1 """Tornado handlers for the notebooks web service.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2011 The IPython Development Team
9 # Copyright (C) 2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import json
19 import json
20
20
21 from tornado import web
21 from tornado import web
22
22
23 from IPython.html.utils import url_path_join, url_escape
23 from IPython.html.utils import url_path_join, url_escape
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25
25
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
26 from IPython.html.base.handlers import (IPythonHandler, json_errors,
27 notebook_path_regex, path_regex,
27 notebook_path_regex, path_regex,
28 notebook_name_regex)
28 notebook_name_regex)
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Notebook web service handlers
31 # Notebook web service handlers
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 class NotebookHandler(IPythonHandler):
35 class NotebookHandler(IPythonHandler):
36
36
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
37 SUPPORTED_METHODS = (u'GET', u'PUT', u'PATCH', u'POST', u'DELETE')
38
38
39 def notebook_location(self, name, path=''):
39 def notebook_location(self, name, path=''):
40 """Return the full URL location of a notebook based.
40 """Return the full URL location of a notebook based.
41
41
42 Parameters
42 Parameters
43 ----------
43 ----------
44 name : unicode
44 name : unicode
45 The base name of the notebook, such as "foo.ipynb".
45 The base name of the notebook, such as "foo.ipynb".
46 path : unicode
46 path : unicode
47 The URL path of the notebook.
47 The URL path of the notebook.
48 """
48 """
49 return url_escape(url_path_join(
49 return url_escape(url_path_join(
50 self.base_url, 'api', 'notebooks', path, name
50 self.base_url, 'api', 'notebooks', path, name
51 ))
51 ))
52
52
53 def _finish_model(self, model, location=True):
53 def _finish_model(self, model, location=True):
54 """Finish a JSON request with a model, setting relevant headers, etc."""
54 """Finish a JSON request with a model, setting relevant headers, etc."""
55 if location:
55 if location:
56 location = self.notebook_location(model['name'], model['path'])
56 location = self.notebook_location(model['name'], model['path'])
57 self.set_header('Location', location)
57 self.set_header('Location', location)
58 self.set_header('Last-Modified', model['last_modified'])
58 self.set_header('Last-Modified', model['last_modified'])
59 self.finish(json.dumps(model, default=date_default))
59 self.finish(json.dumps(model, default=date_default))
60
60
61 @web.authenticated
61 @web.authenticated
62 @json_errors
62 @json_errors
63 def get(self, path='', name=None):
63 def get(self, path='', name=None):
64 """Return a Notebook or list of notebooks.
64 """Return a Notebook or list of notebooks.
65
65
66 * GET with path and no notebook name lists notebooks in a directory
66 * GET with path and no notebook name lists notebooks in a directory
67 * GET with path and notebook name returns notebook JSON
67 * GET with path and notebook name returns notebook JSON
68 """
68 """
69 nbm = self.notebook_manager
69 nbm = self.notebook_manager
70 # Check to see if a notebook name was given
70 # Check to see if a notebook name was given
71 if name is None:
71 if name is None:
72 # TODO: Remove this after we create the contents web service and directories are
72 # TODO: Remove this after we create the contents web service and directories are
73 # no longer listed by the notebook web service. This should only handle notebooks
73 # no longer listed by the notebook web service. This should only handle notebooks
74 # and not directories.
74 # and not directories.
75 dirs = nbm.list_dirs(path)
75 dirs = nbm.list_dirs(path)
76 notebooks = []
76 notebooks = []
77 index = []
77 index = []
78 for nb in nbm.list_notebooks(path):
78 for nb in nbm.list_notebooks(path):
79 if nb['name'].lower() == 'index.ipynb':
79 if nb['name'].lower() == 'index.ipynb':
80 index.append(nb)
80 index.append(nb)
81 else:
81 else:
82 notebooks.append(nb)
82 notebooks.append(nb)
83 notebooks = index + dirs + notebooks
83 notebooks = index + dirs + notebooks
84 self.finish(json.dumps(notebooks, default=date_default))
84 self.finish(json.dumps(notebooks, default=date_default))
85 return
85 return
86 # get and return notebook representation
86 # get and return notebook representation
87 model = nbm.get_notebook(name, path)
87 model = nbm.get_notebook(name, path)
88 self._finish_model(model, location=False)
88 self._finish_model(model, location=False)
89
89
90 @web.authenticated
90 @web.authenticated
91 @json_errors
91 @json_errors
92 def patch(self, path='', name=None):
92 def patch(self, path='', name=None):
93 """PATCH renames a notebook without re-uploading content."""
93 """PATCH renames a notebook without re-uploading content."""
94 nbm = self.notebook_manager
94 nbm = self.notebook_manager
95 if name is None:
95 if name is None:
96 raise web.HTTPError(400, u'Notebook name missing')
96 raise web.HTTPError(400, u'Notebook name missing')
97 model = self.get_json_body()
97 model = self.get_json_body()
98 if model is None:
98 if model is None:
99 raise web.HTTPError(400, u'JSON body missing')
99 raise web.HTTPError(400, u'JSON body missing')
100 model = nbm.update_notebook(model, name, path)
100 model = nbm.update_notebook(model, name, path)
101 self._finish_model(model)
101 self._finish_model(model)
102
102
103 def _copy_notebook(self, copy_from, path, copy_to=None):
103 def _copy_notebook(self, copy_from, path, copy_to=None):
104 """Copy a notebook in path, optionally specifying the new name.
104 """Copy a notebook in path, optionally specifying the new name.
105
105
106 Only support copying within the same directory.
106 Only support copying within the same directory.
107 """
107 """
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
108 self.log.info(u"Copying notebook from %s/%s to %s/%s",
109 path, copy_from,
109 path, copy_from,
110 path, copy_to or '',
110 path, copy_to or '',
111 )
111 )
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
112 model = self.notebook_manager.copy_notebook(copy_from, copy_to, path)
113 self.set_status(201)
113 self.set_status(201)
114 self._finish_model(model)
114 self._finish_model(model)
115
115
116 def _upload_notebook(self, model, path, name=None):
116 def _upload_notebook(self, model, path, name=None):
117 """Upload a notebook
117 """Upload a notebook
118
118
119 If name specified, create it in path/name.
119 If name specified, create it in path/name.
120 """
120 """
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
121 self.log.info(u"Uploading notebook to %s/%s", path, name or '')
122 if name:
122 if name:
123 model['name'] = name
123 model['name'] = name
124
124
125 model = self.notebook_manager.create_notebook(model, path)
125 model = self.notebook_manager.create_notebook(model, path)
126 self.set_status(201)
126 self.set_status(201)
127 self._finish_model(model)
127 self._finish_model(model)
128
128
129 def _create_empty_notebook(self, path, name=None):
129 def _create_empty_notebook(self, path, name=None):
130 """Create an empty notebook in path
130 """Create an empty notebook in path
131
131
132 If name specified, create it in path/name.
132 If name specified, create it in path/name.
133 """
133 """
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
134 self.log.info(u"Creating new notebook in %s/%s", path, name or '')
135 model = {}
135 model = {}
136 if name:
136 if name:
137 model['name'] = name
137 model['name'] = name
138 model = self.notebook_manager.create_notebook(model, path=path)
138 model = self.notebook_manager.create_notebook(model, path=path)
139 self.set_status(201)
139 self.set_status(201)
140 self._finish_model(model)
140 self._finish_model(model)
141
141
142 def _save_notebook(self, model, path, name):
142 def _save_notebook(self, model, path, name):
143 """Save an existing notebook."""
143 """Save an existing notebook."""
144 self.log.info(u"Saving notebook at %s/%s", path, name)
144 self.log.info(u"Saving notebook at %s/%s", path, name)
145 model = self.notebook_manager.save_notebook(model, name, path)
145 model = self.notebook_manager.save_notebook(model, name, path)
146 if model['path'] != path.strip('/') or model['name'] != name:
146 if model['path'] != path.strip('/') or model['name'] != name:
147 # a rename happened, set Location header
147 # a rename happened, set Location header
148 location = True
148 location = True
149 else:
149 else:
150 location = False
150 location = False
151 self._finish_model(model, location)
151 self._finish_model(model, location)
152
152
153 @web.authenticated
153 @web.authenticated
154 @json_errors
154 @json_errors
155 def post(self, path='', name=None):
155 def post(self, path='', name=None):
156 """Create a new notebook in the specified path.
156 """Create a new notebook in the specified path.
157
157
158 POST creates new notebooks. The server always decides on the notebook name.
158 POST creates new notebooks. The server always decides on the notebook name.
159
159
160 POST /api/notebooks/path
160 POST /api/notebooks/path
161 New untitled notebook in path. If content specified, upload a
161 New untitled notebook in path. If content specified, upload a
162 notebook, otherwise start empty.
162 notebook, otherwise start empty.
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
163 POST /api/notebooks/path?copy=OtherNotebook.ipynb
164 New copy of OtherNotebook in path
164 New copy of OtherNotebook in path
165 """
165 """
166
166
167 if name is not None:
167 if name is not None:
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
168 raise web.HTTPError(400, "Only POST to directories. Use PUT for full names.")
169
169
170 model = self.get_json_body()
170 model = self.get_json_body()
171
171
172 if model is not None:
172 if model is not None:
173 copy_from = model.get('copy_from')
173 copy_from = model.get('copy_from')
174 if copy_from:
174 if copy_from:
175 if model.get('content'):
175 if model.get('content'):
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
176 raise web.HTTPError(400, "Can't upload and copy at the same time.")
177 self._copy_notebook(copy_from, path)
177 self._copy_notebook(copy_from, path)
178 else:
178 else:
179 self._upload_notebook(model, path)
179 self._upload_notebook(model, path)
180 else:
180 else:
181 self._create_empty_notebook(path)
181 self._create_empty_notebook(path)
182
182
183 @web.authenticated
183 @web.authenticated
184 @json_errors
184 @json_errors
185 def put(self, path='', name=None):
185 def put(self, path='', name=None):
186 """Saves the notebook in the location specified by name and path.
186 """Saves the notebook in the location specified by name and path.
187
187
188 PUT is very similar to POST, but the requester specifies the name,
188 PUT is very similar to POST, but the requester specifies the name,
189 whereas with POST, the server picks the name.
189 whereas with POST, the server picks the name.
190
190
191 PUT /api/notebooks/path/Name.ipynb
191 PUT /api/notebooks/path/Name.ipynb
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
192 Save notebook at ``path/Name.ipynb``. Notebook structure is specified
193 in `content` key of JSON request body. If content is not specified,
193 in `content` key of JSON request body. If content is not specified,
194 create a new empty notebook.
194 create a new empty notebook.
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
195 PUT /api/notebooks/path/Name.ipynb?copy=OtherNotebook.ipynb
196 Copy OtherNotebook to Name
196 Copy OtherNotebook to Name
197 """
197 """
198 if name is None:
198 if name is None:
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
199 raise web.HTTPError(400, "Only PUT to full names. Use POST for directories.")
200
200
201 model = self.get_json_body()
201 model = self.get_json_body()
202 if model:
202 if model:
203 copy_from = model.get('copy_from')
203 copy_from = model.get('copy_from')
204 if copy_from:
204 if copy_from:
205 if model.get('content'):
205 if model.get('content'):
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
206 raise web.HTTPError(400, "Can't upload and copy at the same time.")
207 self._copy_notebook(copy_from, path, name)
207 self._copy_notebook(copy_from, path, name)
208 elif self.notebook_manager.notebook_exists(name, path):
208 elif self.notebook_manager.notebook_exists(name, path):
209 self._save_notebook(model, path, name)
209 self._save_notebook(model, path, name)
210 else:
210 else:
211 self._upload_notebook(model, path, name)
211 self._upload_notebook(model, path, name)
212 else:
212 else:
213 self._create_empty_notebook(path, name)
213 self._create_empty_notebook(path, name)
214
214
215 @web.authenticated
215 @web.authenticated
216 @json_errors
216 @json_errors
217 def delete(self, path='', name=None):
217 def delete(self, path='', name=None):
218 """delete the notebook in the given notebook path"""
218 """delete the notebook in the given notebook path"""
219 nbm = self.notebook_manager
219 nbm = self.notebook_manager
220 nbm.delete_notebook(name, path)
220 nbm.delete_notebook(name, path)
221 self.set_status(204)
221 self.set_status(204)
222 self.finish()
222 self.finish()
223
223
224
224
225 class NotebookCheckpointsHandler(IPythonHandler):
225 class NotebookCheckpointsHandler(IPythonHandler):
226
226
227 SUPPORTED_METHODS = ('GET', 'POST')
227 SUPPORTED_METHODS = ('GET', 'POST')
228
228
229 @web.authenticated
229 @web.authenticated
230 @json_errors
230 @json_errors
231 def get(self, path='', name=None):
231 def get(self, path='', name=None):
232 """get lists checkpoints for a notebook"""
232 """get lists checkpoints for a notebook"""
233 nbm = self.notebook_manager
233 nbm = self.notebook_manager
234 checkpoints = nbm.list_checkpoints(name, path)
234 checkpoints = nbm.list_checkpoints(name, path)
235 data = json.dumps(checkpoints, default=date_default)
235 data = json.dumps(checkpoints, default=date_default)
236 self.finish(data)
236 self.finish(data)
237
237
238 @web.authenticated
238 @web.authenticated
239 @json_errors
239 @json_errors
240 def post(self, path='', name=None):
240 def post(self, path='', name=None):
241 """post creates a new checkpoint"""
241 """post creates a new checkpoint"""
242 nbm = self.notebook_manager
242 nbm = self.notebook_manager
243 checkpoint = nbm.create_checkpoint(name, path)
243 checkpoint = nbm.create_checkpoint(name, path)
244 data = json.dumps(checkpoint, default=date_default)
244 data = json.dumps(checkpoint, default=date_default)
245 location = url_path_join(self.base_url, 'api/notebooks',
245 location = url_path_join(self.base_url, 'api/notebooks',
246 path, name, 'checkpoints', checkpoint['id'])
246 path, name, 'checkpoints', checkpoint['id'])
247 self.set_header('Location', url_escape(location))
247 self.set_header('Location', url_escape(location))
248 self.set_status(201)
248 self.set_status(201)
249 self.finish(data)
249 self.finish(data)
250
250
251
251
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
252 class ModifyNotebookCheckpointsHandler(IPythonHandler):
253
253
254 SUPPORTED_METHODS = ('POST', 'DELETE')
254 SUPPORTED_METHODS = ('POST', 'DELETE')
255
255
256 @web.authenticated
256 @web.authenticated
257 @json_errors
257 @json_errors
258 def post(self, path, name, checkpoint_id):
258 def post(self, path, name, checkpoint_id):
259 """post restores a notebook from a checkpoint"""
259 """post restores a notebook from a checkpoint"""
260 nbm = self.notebook_manager
260 nbm = self.notebook_manager
261 nbm.restore_checkpoint(checkpoint_id, name, path)
261 nbm.restore_checkpoint(checkpoint_id, name, path)
262 self.set_status(204)
262 self.set_status(204)
263 self.finish()
263 self.finish()
264
264
265 @web.authenticated
265 @web.authenticated
266 @json_errors
266 @json_errors
267 def delete(self, path, name, checkpoint_id):
267 def delete(self, path, name, checkpoint_id):
268 """delete clears a checkpoint for a given notebook"""
268 """delete clears a checkpoint for a given notebook"""
269 nbm = self.notebook_manager
269 nbm = self.notebook_manager
270 nbm.delete_checkpoint(checkpoint_id, name, path)
270 nbm.delete_checkpoint(checkpoint_id, name, path)
271 self.set_status(204)
271 self.set_status(204)
272 self.finish()
272 self.finish()
273
273
274 #-----------------------------------------------------------------------------
274 #-----------------------------------------------------------------------------
275 # URL to handler mappings
275 # URL to handler mappings
276 #-----------------------------------------------------------------------------
276 #-----------------------------------------------------------------------------
277
277
278
278
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
279 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
280
280
281 default_handlers = [
281 default_handlers = [
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
282 (r"/api/notebooks%s/checkpoints" % notebook_path_regex, NotebookCheckpointsHandler),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
283 (r"/api/notebooks%s/checkpoints/%s" % (notebook_path_regex, _checkpoint_id_regex),
284 ModifyNotebookCheckpointsHandler),
284 ModifyNotebookCheckpointsHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
285 (r"/api/notebooks%s" % notebook_path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
286 (r"/api/notebooks%s" % path_regex, NotebookHandler),
287 ]
287 ]
288
288
289
290
@@ -1,251 +1,283 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 from fnmatch import fnmatch
20 from fnmatch import fnmatch
21 import itertools
21 import itertools
22 import os
22 import os
23
23
24 from IPython.config.configurable import LoggingConfigurable
24 from IPython.config.configurable import LoggingConfigurable
25 from IPython.nbformat import current, sign
25 from IPython.nbformat import current, sign
26 from IPython.utils.traitlets import Instance, Unicode, List
26 from IPython.utils.traitlets import Instance, Unicode, List
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Classes
29 # Classes
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 class NotebookManager(LoggingConfigurable):
32 class NotebookManager(LoggingConfigurable):
33
33
34 filename_ext = Unicode(u'.ipynb')
34 filename_ext = Unicode(u'.ipynb')
35
35
36 notary = Instance(sign.NotebookNotary)
36 notary = Instance(sign.NotebookNotary)
37 def _notary_default(self):
37 def _notary_default(self):
38 return sign.NotebookNotary(parent=self)
38 return sign.NotebookNotary(parent=self)
39
39
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
40 hide_globs = List(Unicode, [u'__pycache__'], config=True, help="""
41 Glob patterns to hide in file and directory listings.
41 Glob patterns to hide in file and directory listings.
42 """)
42 """)
43
43
44 # NotebookManager API part 1: methods that must be
44 # NotebookManager API part 1: methods that must be
45 # implemented in subclasses.
45 # implemented in subclasses.
46
46
47 def path_exists(self, path):
47 def path_exists(self, path):
48 """Does the API-style path (directory) actually exist?
48 """Does the API-style path (directory) actually exist?
49
49
50 Override this method in subclasses.
50 Override this method in subclasses.
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54 path : string
54 path : string
55 The
55 The path to check
56
56
57 Returns
57 Returns
58 -------
58 -------
59 exists : bool
59 exists : bool
60 Whether the path does indeed exist.
60 Whether the path does indeed exist.
61 """
61 """
62 raise NotImplementedError
62 raise NotImplementedError
63
63
64 def is_hidden(self, path):
64 def is_hidden(self, path):
65 """Does the API style path correspond to a hidden directory or file?
65 """Does the API style path correspond to a hidden directory or file?
66
66
67 Parameters
67 Parameters
68 ----------
68 ----------
69 path : string
69 path : string
70 The path to check. This is an API path (`/` separated,
70 The path to check. This is an API path (`/` separated,
71 relative to base notebook-dir).
71 relative to base notebook-dir).
72
72
73 Returns
73 Returns
74 -------
74 -------
75 exists : bool
75 exists : bool
76 Whether the path is hidden.
76 Whether the path is hidden.
77
77
78 """
78 """
79 raise NotImplementedError
79 raise NotImplementedError
80
80
81 def notebook_exists(self, name, path=''):
81 def notebook_exists(self, name, path=''):
82 """Returns a True if the notebook exists. Else, returns False.
82 """Returns a True if the notebook exists. Else, returns False.
83
83
84 Parameters
84 Parameters
85 ----------
85 ----------
86 name : string
86 name : string
87 The name of the notebook you are checking.
87 The name of the notebook you are checking.
88 path : string
88 path : string
89 The relative path to the notebook (with '/' as separator)
89 The relative path to the notebook (with '/' as separator)
90
90
91 Returns
91 Returns
92 -------
92 -------
93 bool
93 bool
94 """
94 """
95 raise NotImplementedError('must be implemented in a subclass')
95 raise NotImplementedError('must be implemented in a subclass')
96
96
97 # TODO: Remove this after we create the contents web service and directories are
97 # TODO: Remove this after we create the contents web service and directories are
98 # no longer listed by the notebook web service.
98 # no longer listed by the notebook web service.
99 def list_dirs(self, path):
99 def list_dirs(self, path):
100 """List the directory models for a given API style path."""
100 """List the directory models for a given API style path."""
101 raise NotImplementedError('must be implemented in a subclass')
101 raise NotImplementedError('must be implemented in a subclass')
102
102
103 # TODO: Remove this after we create the contents web service and directories are
103 # TODO: Remove this after we create the contents web service and directories are
104 # no longer listed by the notebook web service.
104 # no longer listed by the notebook web service.
105 def get_dir_model(self, name, path=''):
105 def get_dir_model(self, name, path=''):
106 """Get the directory model given a directory name and its API style path.
106 """Get the directory model given a directory name and its API style path.
107
107
108 The keys in the model should be:
108 The keys in the model should be:
109 * name
109 * name
110 * path
110 * path
111 * last_modified
111 * last_modified
112 * created
112 * created
113 * type='directory'
113 * type='directory'
114 """
114 """
115 raise NotImplementedError('must be implemented in a subclass')
115 raise NotImplementedError('must be implemented in a subclass')
116
116
117 def list_notebooks(self, path=''):
117 def list_notebooks(self, path=''):
118 """Return a list of notebook dicts without content.
118 """Return a list of notebook dicts without content.
119
119
120 This returns a list of dicts, each of the form::
120 This returns a list of dicts, each of the form::
121
121
122 dict(notebook_id=notebook,name=name)
122 dict(notebook_id=notebook,name=name)
123
123
124 This list of dicts should be sorted by name::
124 This list of dicts should be sorted by name::
125
125
126 data = sorted(data, key=lambda item: item['name'])
126 data = sorted(data, key=lambda item: item['name'])
127 """
127 """
128 raise NotImplementedError('must be implemented in a subclass')
128 raise NotImplementedError('must be implemented in a subclass')
129
129
130 def get_notebook(self, name, path='', content=True):
130 def get_notebook(self, name, path='', content=True):
131 """Get the notebook model with or without content."""
131 """Get the notebook model with or without content."""
132 raise NotImplementedError('must be implemented in a subclass')
132 raise NotImplementedError('must be implemented in a subclass')
133
133
134 def save_notebook(self, model, name, path=''):
134 def save_notebook(self, model, name, path=''):
135 """Save the notebook and return the model with no content."""
135 """Save the notebook and return the model with no content."""
136 raise NotImplementedError('must be implemented in a subclass')
136 raise NotImplementedError('must be implemented in a subclass')
137
137
138 def update_notebook(self, model, name, path=''):
138 def update_notebook(self, model, name, path=''):
139 """Update the notebook and return the model with no content."""
139 """Update the notebook and return the model with no content."""
140 raise NotImplementedError('must be implemented in a subclass')
140 raise NotImplementedError('must be implemented in a subclass')
141
141
142 def delete_notebook(self, name, path=''):
142 def delete_notebook(self, name, path=''):
143 """Delete notebook by name and path."""
143 """Delete notebook by name and path."""
144 raise NotImplementedError('must be implemented in a subclass')
144 raise NotImplementedError('must be implemented in a subclass')
145
145
146 def create_checkpoint(self, name, path=''):
146 def create_checkpoint(self, name, path=''):
147 """Create a checkpoint of the current state of a notebook
147 """Create a checkpoint of the current state of a notebook
148
148
149 Returns a checkpoint_id for the new checkpoint.
149 Returns a checkpoint_id for the new checkpoint.
150 """
150 """
151 raise NotImplementedError("must be implemented in a subclass")
151 raise NotImplementedError("must be implemented in a subclass")
152
152
153 def list_checkpoints(self, name, path=''):
153 def list_checkpoints(self, name, path=''):
154 """Return a list of checkpoints for a given notebook"""
154 """Return a list of checkpoints for a given notebook"""
155 return []
155 return []
156
156
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
157 def restore_checkpoint(self, checkpoint_id, name, path=''):
158 """Restore a notebook from one of its checkpoints"""
158 """Restore a notebook from one of its checkpoints"""
159 raise NotImplementedError("must be implemented in a subclass")
159 raise NotImplementedError("must be implemented in a subclass")
160
160
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
161 def delete_checkpoint(self, checkpoint_id, name, path=''):
162 """delete a checkpoint for a notebook"""
162 """delete a checkpoint for a notebook"""
163 raise NotImplementedError("must be implemented in a subclass")
163 raise NotImplementedError("must be implemented in a subclass")
164
164
165 def info_string(self):
165 def info_string(self):
166 return "Serving notebooks"
166 return "Serving notebooks"
167
167
168 # NotebookManager API part 2: methods that have useable default
168 # NotebookManager API part 2: methods that have useable default
169 # implementations, but can be overridden in subclasses.
169 # implementations, but can be overridden in subclasses.
170
170
171 def increment_filename(self, basename, path=''):
171 def increment_filename(self, basename, path=''):
172 """Increment a notebook filename without the .ipynb to make it unique.
172 """Increment a notebook filename without the .ipynb to make it unique.
173
173
174 Parameters
174 Parameters
175 ----------
175 ----------
176 basename : unicode
176 basename : unicode
177 The name of a notebook without the ``.ipynb`` file extension.
177 The name of a notebook without the ``.ipynb`` file extension.
178 path : unicode
178 path : unicode
179 The URL path of the notebooks directory
179 The URL path of the notebooks directory
180
180
181 Returns
181 Returns
182 -------
182 -------
183 name : unicode
183 name : unicode
184 A notebook name (with the .ipynb extension) that starts
184 A notebook name (with the .ipynb extension) that starts
185 with basename and does not refer to any existing notebook.
185 with basename and does not refer to any existing notebook.
186 """
186 """
187 path = path.strip('/')
187 path = path.strip('/')
188 for i in itertools.count():
188 for i in itertools.count():
189 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
189 name = u'{basename}{i}{ext}'.format(basename=basename, i=i,
190 ext=self.filename_ext)
190 ext=self.filename_ext)
191 if not self.notebook_exists(name, path):
191 if not self.notebook_exists(name, path):
192 break
192 break
193 return name
193 return name
194
194
195 def create_notebook(self, model=None, path=''):
195 def create_notebook(self, model=None, path=''):
196 """Create a new notebook and return its model with no content."""
196 """Create a new notebook and return its model with no content."""
197 path = path.strip('/')
197 path = path.strip('/')
198 if model is None:
198 if model is None:
199 model = {}
199 model = {}
200 if 'content' not in model:
200 if 'content' not in model:
201 metadata = current.new_metadata(name=u'')
201 metadata = current.new_metadata(name=u'')
202 model['content'] = current.new_notebook(metadata=metadata)
202 model['content'] = current.new_notebook(metadata=metadata)
203 if 'name' not in model:
203 if 'name' not in model:
204 model['name'] = self.increment_filename('Untitled', path)
204 model['name'] = self.increment_filename('Untitled', path)
205
205
206 model['path'] = path
206 model['path'] = path
207 model = self.save_notebook(model, model['name'], model['path'])
207 model = self.save_notebook(model, model['name'], model['path'])
208 return model
208 return model
209
209
210 def copy_notebook(self, from_name, to_name=None, path=''):
210 def copy_notebook(self, from_name, to_name=None, path=''):
211 """Copy an existing notebook and return its new model.
211 """Copy an existing notebook and return its new model.
212
212
213 If to_name not specified, increment `from_name-Copy#.ipynb`.
213 If to_name not specified, increment `from_name-Copy#.ipynb`.
214 """
214 """
215 path = path.strip('/')
215 path = path.strip('/')
216 model = self.get_notebook(from_name, path)
216 model = self.get_notebook(from_name, path)
217 if not to_name:
217 if not to_name:
218 base = os.path.splitext(from_name)[0] + '-Copy'
218 base = os.path.splitext(from_name)[0] + '-Copy'
219 to_name = self.increment_filename(base, path)
219 to_name = self.increment_filename(base, path)
220 model['name'] = to_name
220 model['name'] = to_name
221 model = self.save_notebook(model, to_name, path)
221 model = self.save_notebook(model, to_name, path)
222 return model
222 return model
223
223
224 def log_info(self):
224 def log_info(self):
225 self.log.info(self.info_string())
225 self.log.info(self.info_string())
226
226
227 # NotebookManager methods provided for use in subclasses.
227 def trust_notebook(self, name, path=''):
228
228 """Explicitly trust a notebook
229 def check_and_sign(self, nb, path, name):
229
230 Parameters
231 ----------
232 name : string
233 The filename of the notebook
234 path : string
235 The notebook's directory
236 """
237 model = self.get_notebook(name, path)
238 nb = model['content']
239 self.log.warn("Trusting notebook %s/%s", path, name)
240 self.notary.mark_cells(nb, True)
241 self.save_notebook(model, name, path)
242
243 def check_and_sign(self, nb, name, path=''):
230 """Check for trusted cells, and sign the notebook.
244 """Check for trusted cells, and sign the notebook.
231
245
232 Called as a part of saving notebooks.
246 Called as a part of saving notebooks.
247
248 Parameters
249 ----------
250 nb : dict
251 The notebook structure
252 name : string
253 The filename of the notebook
254 path : string
255 The notebook's directory
233 """
256 """
234 if self.notary.check_cells(nb):
257 if self.notary.check_cells(nb):
235 self.notary.sign(nb)
258 self.notary.sign(nb)
236 else:
259 else:
237 self.log.warn("Saving untrusted notebook %s/%s", path, name)
260 self.log.warn("Saving untrusted notebook %s/%s", path, name)
238
261
239 def mark_trusted_cells(self, nb, path, name):
262 def mark_trusted_cells(self, nb, name, path=''):
240 """Mark cells as trusted if the notebook signature matches.
263 """Mark cells as trusted if the notebook signature matches.
241
264
242 Called as a part of loading notebooks.
265 Called as a part of loading notebooks.
266
267 Parameters
268 ----------
269 nb : dict
270 The notebook structure
271 name : string
272 The filename of the notebook
273 path : string
274 The notebook's directory
243 """
275 """
244 trusted = self.notary.check_signature(nb)
276 trusted = self.notary.check_signature(nb)
245 if not trusted:
277 if not trusted:
246 self.log.warn("Notebook %s/%s is not trusted", path, name)
278 self.log.warn("Notebook %s/%s is not trusted", path, name)
247 self.notary.mark_cells(nb, trusted)
279 self.notary.mark_cells(nb, trusted)
248
280
249 def should_list(self, name):
281 def should_list(self, name):
250 """Should this file/directory name be displayed in a listing?"""
282 """Should this file/directory name be displayed in a listing?"""
251 return not any(fnmatch(name, glob) for glob in self.hide_globs)
283 return not any(fnmatch(name, glob) for glob in self.hide_globs)
@@ -1,245 +1,306 b''
1 # coding: utf-8
1 # coding: utf-8
2 """Tests for the notebook manager."""
2 """Tests for the notebook manager."""
3 from __future__ import print_function
3 from __future__ import print_function
4
4
5 import logging
5 import os
6 import os
6
7
7 from tornado.web import HTTPError
8 from tornado.web import HTTPError
8 from unittest import TestCase
9 from unittest import TestCase
9 from tempfile import NamedTemporaryFile
10 from tempfile import NamedTemporaryFile
10
11
12 from IPython.nbformat import current
13
11 from IPython.utils.tempdir import TemporaryDirectory
14 from IPython.utils.tempdir import TemporaryDirectory
12 from IPython.utils.traitlets import TraitError
15 from IPython.utils.traitlets import TraitError
13 from IPython.html.utils import url_path_join
16 from IPython.html.utils import url_path_join
14
17
15 from ..filenbmanager import FileNotebookManager
18 from ..filenbmanager import FileNotebookManager
16 from ..nbmanager import NotebookManager
19 from ..nbmanager import NotebookManager
17
20
18
21
19 class TestFileNotebookManager(TestCase):
22 class TestFileNotebookManager(TestCase):
20
23
21 def test_nb_dir(self):
24 def test_nb_dir(self):
22 with TemporaryDirectory() as td:
25 with TemporaryDirectory() as td:
23 fm = FileNotebookManager(notebook_dir=td)
26 fm = FileNotebookManager(notebook_dir=td)
24 self.assertEqual(fm.notebook_dir, td)
27 self.assertEqual(fm.notebook_dir, td)
25
28
26 def test_missing_nb_dir(self):
29 def test_missing_nb_dir(self):
27 with TemporaryDirectory() as td:
30 with TemporaryDirectory() as td:
28 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
31 nbdir = os.path.join(td, 'notebook', 'dir', 'is', 'missing')
29 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
32 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=nbdir)
30
33
31 def test_invalid_nb_dir(self):
34 def test_invalid_nb_dir(self):
32 with NamedTemporaryFile() as tf:
35 with NamedTemporaryFile() as tf:
33 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
36 self.assertRaises(TraitError, FileNotebookManager, notebook_dir=tf.name)
34
37
35 def test_get_os_path(self):
38 def test_get_os_path(self):
36 # full filesystem path should be returned with correct operating system
39 # full filesystem path should be returned with correct operating system
37 # separators.
40 # separators.
38 with TemporaryDirectory() as td:
41 with TemporaryDirectory() as td:
39 nbdir = td
42 nbdir = td
40 fm = FileNotebookManager(notebook_dir=nbdir)
43 fm = FileNotebookManager(notebook_dir=nbdir)
41 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
44 path = fm._get_os_path('test.ipynb', '/path/to/notebook/')
42 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
45 rel_path_list = '/path/to/notebook/test.ipynb'.split('/')
43 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
46 fs_path = os.path.join(fm.notebook_dir, *rel_path_list)
44 self.assertEqual(path, fs_path)
47 self.assertEqual(path, fs_path)
45
48
46 fm = FileNotebookManager(notebook_dir=nbdir)
49 fm = FileNotebookManager(notebook_dir=nbdir)
47 path = fm._get_os_path('test.ipynb')
50 path = fm._get_os_path('test.ipynb')
48 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
51 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
49 self.assertEqual(path, fs_path)
52 self.assertEqual(path, fs_path)
50
53
51 fm = FileNotebookManager(notebook_dir=nbdir)
54 fm = FileNotebookManager(notebook_dir=nbdir)
52 path = fm._get_os_path('test.ipynb', '////')
55 path = fm._get_os_path('test.ipynb', '////')
53 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
56 fs_path = os.path.join(fm.notebook_dir, 'test.ipynb')
54 self.assertEqual(path, fs_path)
57 self.assertEqual(path, fs_path)
55
58
56 class TestNotebookManager(TestCase):
59 class TestNotebookManager(TestCase):
57
60
61 def setUp(self):
62 self._temp_dir = TemporaryDirectory()
63 self.td = self._temp_dir.name
64 self.notebook_manager = FileNotebookManager(
65 notebook_dir=self.td,
66 log=logging.getLogger()
67 )
68
69 def tearDown(self):
70 self._temp_dir.cleanup()
71
58 def make_dir(self, abs_path, rel_path):
72 def make_dir(self, abs_path, rel_path):
59 """make subdirectory, rel_path is the relative path
73 """make subdirectory, rel_path is the relative path
60 to that directory from the location where the server started"""
74 to that directory from the location where the server started"""
61 os_path = os.path.join(abs_path, rel_path)
75 os_path = os.path.join(abs_path, rel_path)
62 try:
76 try:
63 os.makedirs(os_path)
77 os.makedirs(os_path)
64 except OSError:
78 except OSError:
65 print("Directory already exists: %r" % os_path)
79 print("Directory already exists: %r" % os_path)
66
80
81 def add_code_cell(self, nb):
82 output = current.new_output("display_data", output_javascript="alert('hi');")
83 cell = current.new_code_cell("print('hi')", outputs=[output])
84 if not nb.worksheets:
85 nb.worksheets.append(current.new_worksheet())
86 nb.worksheets[0].cells.append(cell)
87
88 def new_notebook(self):
89 nbm = self.notebook_manager
90 model = nbm.create_notebook()
91 name = model['name']
92 path = model['path']
93
94 full_model = nbm.get_notebook(name, path)
95 nb = full_model['content']
96 self.add_code_cell(nb)
97
98 nbm.save_notebook(full_model, name, path)
99 return nb, name, path
100
67 def test_create_notebook(self):
101 def test_create_notebook(self):
68 with TemporaryDirectory() as td:
102 nm = self.notebook_manager
69 # Test in root directory
103 # Test in root directory
70 nm = FileNotebookManager(notebook_dir=td)
104 model = nm.create_notebook()
71 model = nm.create_notebook()
105 assert isinstance(model, dict)
72 assert isinstance(model, dict)
106 self.assertIn('name', model)
73 self.assertIn('name', model)
107 self.assertIn('path', model)
74 self.assertIn('path', model)
108 self.assertEqual(model['name'], 'Untitled0.ipynb')
75 self.assertEqual(model['name'], 'Untitled0.ipynb')
109 self.assertEqual(model['path'], '')
76 self.assertEqual(model['path'], '')
110
77
111 # Test in sub-directory
78 # Test in sub-directory
112 sub_dir = '/foo/'
79 sub_dir = '/foo/'
113 self.make_dir(nm.notebook_dir, 'foo')
80 self.make_dir(nm.notebook_dir, 'foo')
114 model = nm.create_notebook(None, sub_dir)
81 model = nm.create_notebook(None, sub_dir)
115 assert isinstance(model, dict)
82 assert isinstance(model, dict)
116 self.assertIn('name', model)
83 self.assertIn('name', model)
117 self.assertIn('path', model)
84 self.assertIn('path', model)
118 self.assertEqual(model['name'], 'Untitled0.ipynb')
85 self.assertEqual(model['name'], 'Untitled0.ipynb')
119 self.assertEqual(model['path'], sub_dir.strip('/'))
86 self.assertEqual(model['path'], sub_dir.strip('/'))
87
120
88 def test_get_notebook(self):
121 def test_get_notebook(self):
89 with TemporaryDirectory() as td:
122 nm = self.notebook_manager
90 # Test in root directory
123 # Create a notebook
91 # Create a notebook
124 model = nm.create_notebook()
92 nm = FileNotebookManager(notebook_dir=td)
125 name = model['name']
93 model = nm.create_notebook()
126 path = model['path']
94 name = model['name']
127
95 path = model['path']
128 # Check that we 'get' on the notebook we just created
96
129 model2 = nm.get_notebook(name, path)
97 # Check that we 'get' on the notebook we just created
130 assert isinstance(model2, dict)
98 model2 = nm.get_notebook(name, path)
131 self.assertIn('name', model2)
99 assert isinstance(model2, dict)
132 self.assertIn('path', model2)
100 self.assertIn('name', model2)
133 self.assertEqual(model['name'], name)
101 self.assertIn('path', model2)
134 self.assertEqual(model['path'], path)
102 self.assertEqual(model['name'], name)
135
103 self.assertEqual(model['path'], path)
136 # Test in sub-directory
104
137 sub_dir = '/foo/'
105 # Test in sub-directory
138 self.make_dir(nm.notebook_dir, 'foo')
106 sub_dir = '/foo/'
139 model = nm.create_notebook(None, sub_dir)
107 self.make_dir(nm.notebook_dir, 'foo')
140 model2 = nm.get_notebook(name, sub_dir)
108 model = nm.create_notebook(None, sub_dir)
141 assert isinstance(model2, dict)
109 model2 = nm.get_notebook(name, sub_dir)
142 self.assertIn('name', model2)
110 assert isinstance(model2, dict)
143 self.assertIn('path', model2)
111 self.assertIn('name', model2)
144 self.assertIn('content', model2)
112 self.assertIn('path', model2)
145 self.assertEqual(model2['name'], 'Untitled0.ipynb')
113 self.assertIn('content', model2)
146 self.assertEqual(model2['path'], sub_dir.strip('/'))
114 self.assertEqual(model2['name'], 'Untitled0.ipynb')
115 self.assertEqual(model2['path'], sub_dir.strip('/'))
116
147
117 def test_update_notebook(self):
148 def test_update_notebook(self):
118 with TemporaryDirectory() as td:
149 nm = self.notebook_manager
119 # Test in root directory
150 # Create a notebook
120 # Create a notebook
151 model = nm.create_notebook()
121 nm = FileNotebookManager(notebook_dir=td)
152 name = model['name']
122 model = nm.create_notebook()
153 path = model['path']
123 name = model['name']
154
124 path = model['path']
155 # Change the name in the model for rename
125
156 model['name'] = 'test.ipynb'
126 # Change the name in the model for rename
157 model = nm.update_notebook(model, name, path)
127 model['name'] = 'test.ipynb'
158 assert isinstance(model, dict)
128 model = nm.update_notebook(model, name, path)
159 self.assertIn('name', model)
129 assert isinstance(model, dict)
160 self.assertIn('path', model)
130 self.assertIn('name', model)
161 self.assertEqual(model['name'], 'test.ipynb')
131 self.assertIn('path', model)
162
132 self.assertEqual(model['name'], 'test.ipynb')
163 # Make sure the old name is gone
133
164 self.assertRaises(HTTPError, nm.get_notebook, name, path)
134 # Make sure the old name is gone
165
135 self.assertRaises(HTTPError, nm.get_notebook, name, path)
166 # Test in sub-directory
136
167 # Create a directory and notebook in that directory
137 # Test in sub-directory
168 sub_dir = '/foo/'
138 # Create a directory and notebook in that directory
169 self.make_dir(nm.notebook_dir, 'foo')
139 sub_dir = '/foo/'
170 model = nm.create_notebook(None, sub_dir)
140 self.make_dir(nm.notebook_dir, 'foo')
171 name = model['name']
141 model = nm.create_notebook(None, sub_dir)
172 path = model['path']
142 name = model['name']
173
143 path = model['path']
174 # Change the name in the model for rename
144
175 model['name'] = 'test_in_sub.ipynb'
145 # Change the name in the model for rename
176 model = nm.update_notebook(model, name, path)
146 model['name'] = 'test_in_sub.ipynb'
177 assert isinstance(model, dict)
147 model = nm.update_notebook(model, name, path)
178 self.assertIn('name', model)
148 assert isinstance(model, dict)
179 self.assertIn('path', model)
149 self.assertIn('name', model)
180 self.assertEqual(model['name'], 'test_in_sub.ipynb')
150 self.assertIn('path', model)
181 self.assertEqual(model['path'], sub_dir.strip('/'))
151 self.assertEqual(model['name'], 'test_in_sub.ipynb')
182
152 self.assertEqual(model['path'], sub_dir.strip('/'))
183 # Make sure the old name is gone
153
184 self.assertRaises(HTTPError, nm.get_notebook, name, path)
154 # Make sure the old name is gone
155 self.assertRaises(HTTPError, nm.get_notebook, name, path)
156
185
157 def test_save_notebook(self):
186 def test_save_notebook(self):
158 with TemporaryDirectory() as td:
187 nm = self.notebook_manager
159 # Test in the root directory
188 # Create a notebook
160 # Create a notebook
189 model = nm.create_notebook()
161 nm = FileNotebookManager(notebook_dir=td)
190 name = model['name']
162 model = nm.create_notebook()
191 path = model['path']
163 name = model['name']
192
164 path = model['path']
193 # Get the model with 'content'
165
194 full_model = nm.get_notebook(name, path)
166 # Get the model with 'content'
195
167 full_model = nm.get_notebook(name, path)
196 # Save the notebook
168
197 model = nm.save_notebook(full_model, name, path)
169 # Save the notebook
198 assert isinstance(model, dict)
170 model = nm.save_notebook(full_model, name, path)
199 self.assertIn('name', model)
171 assert isinstance(model, dict)
200 self.assertIn('path', model)
172 self.assertIn('name', model)
201 self.assertEqual(model['name'], name)
173 self.assertIn('path', model)
202 self.assertEqual(model['path'], path)
174 self.assertEqual(model['name'], name)
203
175 self.assertEqual(model['path'], path)
204 # Test in sub-directory
176
205 # Create a directory and notebook in that directory
177 # Test in sub-directory
206 sub_dir = '/foo/'
178 # Create a directory and notebook in that directory
207 self.make_dir(nm.notebook_dir, 'foo')
179 sub_dir = '/foo/'
208 model = nm.create_notebook(None, sub_dir)
180 self.make_dir(nm.notebook_dir, 'foo')
209 name = model['name']
181 model = nm.create_notebook(None, sub_dir)
210 path = model['path']
182 name = model['name']
211 model = nm.get_notebook(name, path)
183 path = model['path']
212
184 model = nm.get_notebook(name, path)
213 # Change the name in the model for rename
185
214 model = nm.save_notebook(model, name, path)
186 # Change the name in the model for rename
215 assert isinstance(model, dict)
187 model = nm.save_notebook(model, name, path)
216 self.assertIn('name', model)
188 assert isinstance(model, dict)
217 self.assertIn('path', model)
189 self.assertIn('name', model)
218 self.assertEqual(model['name'], 'Untitled0.ipynb')
190 self.assertIn('path', model)
219 self.assertEqual(model['path'], sub_dir.strip('/'))
191 self.assertEqual(model['name'], 'Untitled0.ipynb')
192 self.assertEqual(model['path'], sub_dir.strip('/'))
193
220
194 def test_save_notebook_with_script(self):
221 def test_save_notebook_with_script(self):
195 with TemporaryDirectory() as td:
222 nm = self.notebook_manager
196 # Create a notebook
223 # Create a notebook
197 nm = FileNotebookManager(notebook_dir=td)
224 model = nm.create_notebook()
198 nm.save_script = True
225 nm.save_script = True
199 model = nm.create_notebook()
226 model = nm.create_notebook()
200 name = model['name']
227 name = model['name']
201 path = model['path']
228 path = model['path']
202
229
203 # Get the model with 'content'
230 # Get the model with 'content'
204 full_model = nm.get_notebook(name, path)
231 full_model = nm.get_notebook(name, path)
205
232
206 # Save the notebook
233 # Save the notebook
207 model = nm.save_notebook(full_model, name, path)
234 model = nm.save_notebook(full_model, name, path)
208
235
209 # Check that the script was created
236 # Check that the script was created
210 py_path = os.path.join(td, os.path.splitext(name)[0]+'.py')
237 py_path = os.path.join(nm.notebook_dir, os.path.splitext(name)[0]+'.py')
211 assert os.path.exists(py_path), py_path
238 assert os.path.exists(py_path), py_path
212
239
213 def test_delete_notebook(self):
240 def test_delete_notebook(self):
214 with TemporaryDirectory() as td:
241 nm = self.notebook_manager
215 # Test in the root directory
242 # Create a notebook
216 # Create a notebook
243 nb, name, path = self.new_notebook()
217 nm = FileNotebookManager(notebook_dir=td)
244
218 model = nm.create_notebook()
245 # Delete the notebook
219 name = model['name']
246 nm.delete_notebook(name, path)
220 path = model['path']
247
221
248 # Check that a 'get' on the deleted notebook raises and error
222 # Delete the notebook
249 self.assertRaises(HTTPError, nm.get_notebook, name, path)
223 nm.delete_notebook(name, path)
224
225 # Check that a 'get' on the deleted notebook raises and error
226 self.assertRaises(HTTPError, nm.get_notebook, name, path)
227
250
228 def test_copy_notebook(self):
251 def test_copy_notebook(self):
229 with TemporaryDirectory() as td:
252 nm = self.notebook_manager
230 # Test in the root directory
253 path = u'Γ₯ b'
231 # Create a notebook
254 name = u'nb √.ipynb'
232 nm = FileNotebookManager(notebook_dir=td)
255 os.mkdir(os.path.join(nm.notebook_dir, path))
233 path = u'Γ₯ b'
256 orig = nm.create_notebook({'name' : name}, path=path)
234 name = u'nb √.ipynb'
257
235 os.mkdir(os.path.join(td, path))
258 # copy with unspecified name
236 orig = nm.create_notebook({'name' : name}, path=path)
259 copy = nm.copy_notebook(name, path=path)
237
260 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
238 # copy with unspecified name
261
239 copy = nm.copy_notebook(name, path=path)
262 # copy with specified name
240 self.assertEqual(copy['name'], orig['name'].replace('.ipynb', '-Copy0.ipynb'))
263 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
241
264 self.assertEqual(copy2['name'], u'copy 2.ipynb')
242 # copy with specified name
243 copy2 = nm.copy_notebook(name, u'copy 2.ipynb', path=path)
244 self.assertEqual(copy2['name'], u'copy 2.ipynb')
245
265
266 def test_trust_notebook(self):
267 nbm = self.notebook_manager
268 nb, name, path = self.new_notebook()
269
270 untrusted = nbm.get_notebook(name, path)['content']
271 assert not nbm.notary.check_cells(untrusted)
272
273 # print(untrusted)
274 nbm.trust_notebook(name, path)
275 trusted = nbm.get_notebook(name, path)['content']
276 # print(trusted)
277 assert nbm.notary.check_cells(trusted)
278
279 def test_mark_trusted_cells(self):
280 nbm = self.notebook_manager
281 nb, name, path = self.new_notebook()
282
283 nbm.mark_trusted_cells(nb, name, path)
284 for cell in nb.worksheets[0].cells:
285 if cell.cell_type == 'code':
286 assert not cell.trusted
287
288 nbm.trust_notebook(name, path)
289 nb = nbm.get_notebook(name, path)['content']
290 for cell in nb.worksheets[0].cells:
291 if cell.cell_type == 'code':
292 assert cell.trusted
293
294 def test_check_and_sign(self):
295 nbm = self.notebook_manager
296 nb, name, path = self.new_notebook()
297
298 nbm.mark_trusted_cells(nb, name, path)
299 nbm.check_and_sign(nb, name, path)
300 assert not nbm.notary.check_signature(nb)
301
302 nbm.trust_notebook(name, path)
303 nb = nbm.get_notebook(name, path)['content']
304 nbm.mark_trusted_cells(nb, name, path)
305 nbm.check_and_sign(nb, name, path)
306 assert nbm.notary.check_signature(nb)
@@ -1,514 +1,513 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2012 The IPython Development Team
2 // Copyright (C) 2008-2012 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 // Utilities
9 // Utilities
10 //============================================================================
10 //============================================================================
11 IPython.namespace('IPython.utils');
11 IPython.namespace('IPython.utils');
12
12
13 IPython.utils = (function (IPython) {
13 IPython.utils = (function (IPython) {
14 "use strict";
14 "use strict";
15
15
16 IPython.load_extensions = function () {
16 IPython.load_extensions = function () {
17 // load one or more IPython notebook extensions with requirejs
17 // load one or more IPython notebook extensions with requirejs
18
18
19 var extensions = [];
19 var extensions = [];
20 var extension_names = arguments;
20 var extension_names = arguments;
21 for (var i = 0; i < extension_names.length; i++) {
21 for (var i = 0; i < extension_names.length; i++) {
22 extensions.push("nbextensions/" + arguments[i]);
22 extensions.push("nbextensions/" + arguments[i]);
23 }
23 }
24
24
25 require(extensions,
25 require(extensions,
26 function () {
26 function () {
27 for (var i = 0; i < arguments.length; i++) {
27 for (var i = 0; i < arguments.length; i++) {
28 var ext = arguments[i];
28 var ext = arguments[i];
29 var ext_name = extension_names[i];
29 var ext_name = extension_names[i];
30 // success callback
30 // success callback
31 console.log("Loaded extension: " + ext_name);
31 console.log("Loaded extension: " + ext_name);
32 if (ext && ext.load_ipython_extension !== undefined) {
32 if (ext && ext.load_ipython_extension !== undefined) {
33 ext.load_ipython_extension();
33 ext.load_ipython_extension();
34 }
34 }
35 }
35 }
36 },
36 },
37 function (err) {
37 function (err) {
38 // failure callback
38 // failure callback
39 console.log("Failed to load extension(s):", err.requireModules, err);
39 console.log("Failed to load extension(s):", err.requireModules, err);
40 }
40 }
41 );
41 );
42 };
42 };
43
43
44 //============================================================================
44 //============================================================================
45 // Cross-browser RegEx Split
45 // Cross-browser RegEx Split
46 //============================================================================
46 //============================================================================
47
47
48 // This code has been MODIFIED from the code licensed below to not replace the
48 // This code has been MODIFIED from the code licensed below to not replace the
49 // default browser split. The license is reproduced here.
49 // default browser split. The license is reproduced here.
50
50
51 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
51 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
52 /*!
52 /*!
53 * Cross-Browser Split 1.1.1
53 * Cross-Browser Split 1.1.1
54 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
54 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
55 * Available under the MIT License
55 * Available under the MIT License
56 * ECMAScript compliant, uniform cross-browser split method
56 * ECMAScript compliant, uniform cross-browser split method
57 */
57 */
58
58
59 /**
59 /**
60 * Splits a string into an array of strings using a regex or string
60 * Splits a string into an array of strings using a regex or string
61 * separator. Matches of the separator are not included in the result array.
61 * separator. Matches of the separator are not included in the result array.
62 * However, if `separator` is a regex that contains capturing groups,
62 * However, if `separator` is a regex that contains capturing groups,
63 * backreferences are spliced into the result each time `separator` is
63 * backreferences are spliced into the result each time `separator` is
64 * matched. Fixes browser bugs compared to the native
64 * matched. Fixes browser bugs compared to the native
65 * `String.prototype.split` and can be used reliably cross-browser.
65 * `String.prototype.split` and can be used reliably cross-browser.
66 * @param {String} str String to split.
66 * @param {String} str String to split.
67 * @param {RegExp|String} separator Regex or string to use for separating
67 * @param {RegExp|String} separator Regex or string to use for separating
68 * the string.
68 * the string.
69 * @param {Number} [limit] Maximum number of items to include in the result
69 * @param {Number} [limit] Maximum number of items to include in the result
70 * array.
70 * array.
71 * @returns {Array} Array of substrings.
71 * @returns {Array} Array of substrings.
72 * @example
72 * @example
73 *
73 *
74 * // Basic use
74 * // Basic use
75 * regex_split('a b c d', ' ');
75 * regex_split('a b c d', ' ');
76 * // -> ['a', 'b', 'c', 'd']
76 * // -> ['a', 'b', 'c', 'd']
77 *
77 *
78 * // With limit
78 * // With limit
79 * regex_split('a b c d', ' ', 2);
79 * regex_split('a b c d', ' ', 2);
80 * // -> ['a', 'b']
80 * // -> ['a', 'b']
81 *
81 *
82 * // Backreferences in result array
82 * // Backreferences in result array
83 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
83 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
84 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
84 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
85 */
85 */
86 var regex_split = function (str, separator, limit) {
86 var regex_split = function (str, separator, limit) {
87 // If `separator` is not a regex, use `split`
87 // If `separator` is not a regex, use `split`
88 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
88 if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
89 return split.call(str, separator, limit);
89 return split.call(str, separator, limit);
90 }
90 }
91 var output = [],
91 var output = [],
92 flags = (separator.ignoreCase ? "i" : "") +
92 flags = (separator.ignoreCase ? "i" : "") +
93 (separator.multiline ? "m" : "") +
93 (separator.multiline ? "m" : "") +
94 (separator.extended ? "x" : "") + // Proposed for ES6
94 (separator.extended ? "x" : "") + // Proposed for ES6
95 (separator.sticky ? "y" : ""), // Firefox 3+
95 (separator.sticky ? "y" : ""), // Firefox 3+
96 lastLastIndex = 0,
96 lastLastIndex = 0,
97 // Make `global` and avoid `lastIndex` issues by working with a copy
97 // Make `global` and avoid `lastIndex` issues by working with a copy
98 separator = new RegExp(separator.source, flags + "g"),
98 separator = new RegExp(separator.source, flags + "g"),
99 separator2, match, lastIndex, lastLength;
99 separator2, match, lastIndex, lastLength;
100 str += ""; // Type-convert
100 str += ""; // Type-convert
101
101
102 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
102 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
103 if (!compliantExecNpcg) {
103 if (!compliantExecNpcg) {
104 // Doesn't need flags gy, but they don't hurt
104 // Doesn't need flags gy, but they don't hurt
105 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
105 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
106 }
106 }
107 /* Values for `limit`, per the spec:
107 /* Values for `limit`, per the spec:
108 * If undefined: 4294967295 // Math.pow(2, 32) - 1
108 * If undefined: 4294967295 // Math.pow(2, 32) - 1
109 * If 0, Infinity, or NaN: 0
109 * If 0, Infinity, or NaN: 0
110 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
110 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
111 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
111 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
112 * If other: Type-convert, then use the above rules
112 * If other: Type-convert, then use the above rules
113 */
113 */
114 limit = typeof(limit) === "undefined" ?
114 limit = typeof(limit) === "undefined" ?
115 -1 >>> 0 : // Math.pow(2, 32) - 1
115 -1 >>> 0 : // Math.pow(2, 32) - 1
116 limit >>> 0; // ToUint32(limit)
116 limit >>> 0; // ToUint32(limit)
117 while (match = separator.exec(str)) {
117 while (match = separator.exec(str)) {
118 // `separator.lastIndex` is not reliable cross-browser
118 // `separator.lastIndex` is not reliable cross-browser
119 lastIndex = match.index + match[0].length;
119 lastIndex = match.index + match[0].length;
120 if (lastIndex > lastLastIndex) {
120 if (lastIndex > lastLastIndex) {
121 output.push(str.slice(lastLastIndex, match.index));
121 output.push(str.slice(lastLastIndex, match.index));
122 // Fix browsers whose `exec` methods don't consistently return `undefined` for
122 // Fix browsers whose `exec` methods don't consistently return `undefined` for
123 // nonparticipating capturing groups
123 // nonparticipating capturing groups
124 if (!compliantExecNpcg && match.length > 1) {
124 if (!compliantExecNpcg && match.length > 1) {
125 match[0].replace(separator2, function () {
125 match[0].replace(separator2, function () {
126 for (var i = 1; i < arguments.length - 2; i++) {
126 for (var i = 1; i < arguments.length - 2; i++) {
127 if (typeof(arguments[i]) === "undefined") {
127 if (typeof(arguments[i]) === "undefined") {
128 match[i] = undefined;
128 match[i] = undefined;
129 }
129 }
130 }
130 }
131 });
131 });
132 }
132 }
133 if (match.length > 1 && match.index < str.length) {
133 if (match.length > 1 && match.index < str.length) {
134 Array.prototype.push.apply(output, match.slice(1));
134 Array.prototype.push.apply(output, match.slice(1));
135 }
135 }
136 lastLength = match[0].length;
136 lastLength = match[0].length;
137 lastLastIndex = lastIndex;
137 lastLastIndex = lastIndex;
138 if (output.length >= limit) {
138 if (output.length >= limit) {
139 break;
139 break;
140 }
140 }
141 }
141 }
142 if (separator.lastIndex === match.index) {
142 if (separator.lastIndex === match.index) {
143 separator.lastIndex++; // Avoid an infinite loop
143 separator.lastIndex++; // Avoid an infinite loop
144 }
144 }
145 }
145 }
146 if (lastLastIndex === str.length) {
146 if (lastLastIndex === str.length) {
147 if (lastLength || !separator.test("")) {
147 if (lastLength || !separator.test("")) {
148 output.push("");
148 output.push("");
149 }
149 }
150 } else {
150 } else {
151 output.push(str.slice(lastLastIndex));
151 output.push(str.slice(lastLastIndex));
152 }
152 }
153 return output.length > limit ? output.slice(0, limit) : output;
153 return output.length > limit ? output.slice(0, limit) : output;
154 };
154 };
155
155
156 //============================================================================
156 //============================================================================
157 // End contributed Cross-browser RegEx Split
157 // End contributed Cross-browser RegEx Split
158 //============================================================================
158 //============================================================================
159
159
160
160
161 var uuid = function () {
161 var uuid = function () {
162 // http://www.ietf.org/rfc/rfc4122.txt
162 // http://www.ietf.org/rfc/rfc4122.txt
163 var s = [];
163 var s = [];
164 var hexDigits = "0123456789ABCDEF";
164 var hexDigits = "0123456789ABCDEF";
165 for (var i = 0; i < 32; i++) {
165 for (var i = 0; i < 32; i++) {
166 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
166 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
167 }
167 }
168 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
168 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
169 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
169 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
170
170
171 var uuid = s.join("");
171 var uuid = s.join("");
172 return uuid;
172 return uuid;
173 };
173 };
174
174
175
175
176 //Fix raw text to parse correctly in crazy XML
176 //Fix raw text to parse correctly in crazy XML
177 function xmlencode(string) {
177 function xmlencode(string) {
178 return string.replace(/\&/g,'&'+'amp;')
178 return string.replace(/\&/g,'&'+'amp;')
179 .replace(/</g,'&'+'lt;')
179 .replace(/</g,'&'+'lt;')
180 .replace(/>/g,'&'+'gt;')
180 .replace(/>/g,'&'+'gt;')
181 .replace(/\'/g,'&'+'apos;')
181 .replace(/\'/g,'&'+'apos;')
182 .replace(/\"/g,'&'+'quot;')
182 .replace(/\"/g,'&'+'quot;')
183 .replace(/`/g,'&'+'#96;');
183 .replace(/`/g,'&'+'#96;');
184 }
184 }
185
185
186
186
187 //Map from terminal commands to CSS classes
187 //Map from terminal commands to CSS classes
188 var ansi_colormap = {
188 var ansi_colormap = {
189 "01":"ansibold",
189 "01":"ansibold",
190
190
191 "30":"ansiblack",
191 "30":"ansiblack",
192 "31":"ansired",
192 "31":"ansired",
193 "32":"ansigreen",
193 "32":"ansigreen",
194 "33":"ansiyellow",
194 "33":"ansiyellow",
195 "34":"ansiblue",
195 "34":"ansiblue",
196 "35":"ansipurple",
196 "35":"ansipurple",
197 "36":"ansicyan",
197 "36":"ansicyan",
198 "37":"ansigray",
198 "37":"ansigray",
199
199
200 "40":"ansibgblack",
200 "40":"ansibgblack",
201 "41":"ansibgred",
201 "41":"ansibgred",
202 "42":"ansibggreen",
202 "42":"ansibggreen",
203 "43":"ansibgyellow",
203 "43":"ansibgyellow",
204 "44":"ansibgblue",
204 "44":"ansibgblue",
205 "45":"ansibgpurple",
205 "45":"ansibgpurple",
206 "46":"ansibgcyan",
206 "46":"ansibgcyan",
207 "47":"ansibggray"
207 "47":"ansibggray"
208 };
208 };
209
209
210 function _process_numbers(attrs, numbers) {
210 function _process_numbers(attrs, numbers) {
211 // process ansi escapes
211 // process ansi escapes
212 var n = numbers.shift();
212 var n = numbers.shift();
213 if (ansi_colormap[n]) {
213 if (ansi_colormap[n]) {
214 if ( ! attrs["class"] ) {
214 if ( ! attrs["class"] ) {
215 attrs["class"] = ansi_colormap[n];
215 attrs["class"] = ansi_colormap[n];
216 } else {
216 } else {
217 attrs["class"] += " " + ansi_colormap[n];
217 attrs["class"] += " " + ansi_colormap[n];
218 }
218 }
219 } else if (n == "38" || n == "48") {
219 } else if (n == "38" || n == "48") {
220 // VT100 256 color or 24 bit RGB
220 // VT100 256 color or 24 bit RGB
221 if (numbers.length < 2) {
221 if (numbers.length < 2) {
222 console.log("Not enough fields for VT100 color", numbers);
222 console.log("Not enough fields for VT100 color", numbers);
223 return;
223 return;
224 }
224 }
225
225
226 var index_or_rgb = numbers.shift();
226 var index_or_rgb = numbers.shift();
227 var r,g,b;
227 var r,g,b;
228 if (index_or_rgb == "5") {
228 if (index_or_rgb == "5") {
229 // 256 color
229 // 256 color
230 var idx = parseInt(numbers.shift());
230 var idx = parseInt(numbers.shift());
231 if (idx < 16) {
231 if (idx < 16) {
232 // indexed ANSI
232 // indexed ANSI
233 // ignore bright / non-bright distinction
233 // ignore bright / non-bright distinction
234 idx = idx % 8;
234 idx = idx % 8;
235 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
235 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
236 if ( ! attrs["class"] ) {
236 if ( ! attrs["class"] ) {
237 attrs["class"] = ansiclass;
237 attrs["class"] = ansiclass;
238 } else {
238 } else {
239 attrs["class"] += " " + ansiclass;
239 attrs["class"] += " " + ansiclass;
240 }
240 }
241 return;
241 return;
242 } else if (idx < 232) {
242 } else if (idx < 232) {
243 // 216 color 6x6x6 RGB
243 // 216 color 6x6x6 RGB
244 idx = idx - 16;
244 idx = idx - 16;
245 b = idx % 6;
245 b = idx % 6;
246 g = Math.floor(idx / 6) % 6;
246 g = Math.floor(idx / 6) % 6;
247 r = Math.floor(idx / 36) % 6;
247 r = Math.floor(idx / 36) % 6;
248 // convert to rgb
248 // convert to rgb
249 r = (r * 51);
249 r = (r * 51);
250 g = (g * 51);
250 g = (g * 51);
251 b = (b * 51);
251 b = (b * 51);
252 } else {
252 } else {
253 // grayscale
253 // grayscale
254 idx = idx - 231;
254 idx = idx - 231;
255 // it's 1-24 and should *not* include black or white,
255 // it's 1-24 and should *not* include black or white,
256 // so a 26 point scale
256 // so a 26 point scale
257 r = g = b = Math.floor(idx * 256 / 26);
257 r = g = b = Math.floor(idx * 256 / 26);
258 }
258 }
259 } else if (index_or_rgb == "2") {
259 } else if (index_or_rgb == "2") {
260 // Simple 24 bit RGB
260 // Simple 24 bit RGB
261 if (numbers.length > 3) {
261 if (numbers.length > 3) {
262 console.log("Not enough fields for RGB", numbers);
262 console.log("Not enough fields for RGB", numbers);
263 return;
263 return;
264 }
264 }
265 r = numbers.shift();
265 r = numbers.shift();
266 g = numbers.shift();
266 g = numbers.shift();
267 b = numbers.shift();
267 b = numbers.shift();
268 } else {
268 } else {
269 console.log("unrecognized control", numbers);
269 console.log("unrecognized control", numbers);
270 return;
270 return;
271 }
271 }
272 if (r !== undefined) {
272 if (r !== undefined) {
273 // apply the rgb color
273 // apply the rgb color
274 var line;
274 var line;
275 if (n == "38") {
275 if (n == "38") {
276 line = "color: ";
276 line = "color: ";
277 } else {
277 } else {
278 line = "background-color: ";
278 line = "background-color: ";
279 }
279 }
280 line = line + "rgb(" + r + "," + g + "," + b + ");"
280 line = line + "rgb(" + r + "," + g + "," + b + ");"
281 if ( !attrs["style"] ) {
281 if ( !attrs["style"] ) {
282 attrs["style"] = line;
282 attrs["style"] = line;
283 } else {
283 } else {
284 attrs["style"] += " " + line;
284 attrs["style"] += " " + line;
285 }
285 }
286 }
286 }
287 }
287 }
288 }
288 }
289
289
290 function ansispan(str) {
290 function ansispan(str) {
291 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
291 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
292 // regular ansi escapes (using the table above)
292 // regular ansi escapes (using the table above)
293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
293 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
294 if (!pattern) {
294 if (!pattern) {
295 // [(01|22|39|)m close spans
295 // [(01|22|39|)m close spans
296 return "</span>";
296 return "</span>";
297 }
297 }
298 // consume sequence of color escapes
298 // consume sequence of color escapes
299 var numbers = pattern.match(/\d+/g);
299 var numbers = pattern.match(/\d+/g);
300 var attrs = {};
300 var attrs = {};
301 while (numbers.length > 0) {
301 while (numbers.length > 0) {
302 _process_numbers(attrs, numbers);
302 _process_numbers(attrs, numbers);
303 }
303 }
304
304
305 var span = "<span ";
305 var span = "<span ";
306 for (var attr in attrs) {
306 for (var attr in attrs) {
307 var value = attrs[attr];
307 var value = attrs[attr];
308 span = span + " " + attr + '="' + attrs[attr] + '"';
308 span = span + " " + attr + '="' + attrs[attr] + '"';
309 }
309 }
310 return span + ">";
310 return span + ">";
311 });
311 });
312 };
312 };
313
313
314 // Transform ANSI color escape codes into HTML <span> tags with css
314 // Transform ANSI color escape codes into HTML <span> tags with css
315 // classes listed in the above ansi_colormap object. The actual color used
315 // classes listed in the above ansi_colormap object. The actual color used
316 // are set in the css file.
316 // are set in the css file.
317 function fixConsole(txt) {
317 function fixConsole(txt) {
318 txt = xmlencode(txt);
318 txt = xmlencode(txt);
319 var re = /\033\[([\dA-Fa-f;]*?)m/;
319 var re = /\033\[([\dA-Fa-f;]*?)m/;
320 var opened = false;
320 var opened = false;
321 var cmds = [];
321 var cmds = [];
322 var opener = "";
322 var opener = "";
323 var closer = "";
323 var closer = "";
324
324
325 // Strip all ANSI codes that are not color related. Matches
325 // Strip all ANSI codes that are not color related. Matches
326 // all ANSI codes that do not end with "m".
326 // all ANSI codes that do not end with "m".
327 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
327 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
328 txt = txt.replace(ignored_re, "");
328 txt = txt.replace(ignored_re, "");
329
329
330 // color ansi codes
330 // color ansi codes
331 txt = ansispan(txt);
331 txt = ansispan(txt);
332 return txt;
332 return txt;
333 }
333 }
334
334
335 // Remove chunks that should be overridden by the effect of
335 // Remove chunks that should be overridden by the effect of
336 // carriage return characters
336 // carriage return characters
337 function fixCarriageReturn(txt) {
337 function fixCarriageReturn(txt) {
338 var tmp = txt;
338 var tmp = txt;
339 do {
339 do {
340 txt = tmp;
340 txt = tmp;
341 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
341 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
342 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
342 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
343 } while (tmp.length < txt.length);
343 } while (tmp.length < txt.length);
344 return txt;
344 return txt;
345 }
345 }
346
346
347 // Locate any URLs and convert them to a anchor tag
347 // Locate any URLs and convert them to a anchor tag
348 function autoLinkUrls(txt) {
348 function autoLinkUrls(txt) {
349 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
349 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
350 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
350 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
351 }
351 }
352
352
353 var points_to_pixels = function (points) {
353 var points_to_pixels = function (points) {
354 // A reasonably good way of converting between points and pixels.
354 // A reasonably good way of converting between points and pixels.
355 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
355 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
356 $(body).append(test);
356 $(body).append(test);
357 var pixel_per_point = test.width()/10000;
357 var pixel_per_point = test.width()/10000;
358 test.remove();
358 test.remove();
359 return Math.floor(points*pixel_per_point);
359 return Math.floor(points*pixel_per_point);
360 };
360 };
361
361
362 var always_new = function (constructor) {
362 var always_new = function (constructor) {
363 // wrapper around contructor to avoid requiring `var a = new constructor()`
363 // wrapper around contructor to avoid requiring `var a = new constructor()`
364 // useful for passing constructors as callbacks,
364 // useful for passing constructors as callbacks,
365 // not for programmer laziness.
365 // not for programmer laziness.
366 // from http://programmers.stackexchange.com/questions/118798
366 // from http://programmers.stackexchange.com/questions/118798
367 return function () {
367 return function () {
368 var obj = Object.create(constructor.prototype);
368 var obj = Object.create(constructor.prototype);
369 constructor.apply(obj, arguments);
369 constructor.apply(obj, arguments);
370 return obj;
370 return obj;
371 };
371 };
372 };
372 };
373
373
374 var url_path_join = function () {
374 var url_path_join = function () {
375 // join a sequence of url components with '/'
375 // join a sequence of url components with '/'
376 var url = '';
376 var url = '';
377 for (var i = 0; i < arguments.length; i++) {
377 for (var i = 0; i < arguments.length; i++) {
378 if (arguments[i] === '') {
378 if (arguments[i] === '') {
379 continue;
379 continue;
380 }
380 }
381 if (url.length > 0 && url[url.length-1] != '/') {
381 if (url.length > 0 && url[url.length-1] != '/') {
382 url = url + '/' + arguments[i];
382 url = url + '/' + arguments[i];
383 } else {
383 } else {
384 url = url + arguments[i];
384 url = url + arguments[i];
385 }
385 }
386 }
386 }
387 url = url.replace(/\/\/+/, '/');
387 url = url.replace(/\/\/+/, '/');
388 return url;
388 return url;
389 };
389 };
390
390
391 var parse_url = function (url) {
391 var parse_url = function (url) {
392 // an `a` element with an href allows attr-access to the parsed segments of a URL
392 // an `a` element with an href allows attr-access to the parsed segments of a URL
393 // a = parse_url("http://localhost:8888/path/name#hash")
393 // a = parse_url("http://localhost:8888/path/name#hash")
394 // a.protocol = "http:"
394 // a.protocol = "http:"
395 // a.host = "localhost:8888"
395 // a.host = "localhost:8888"
396 // a.hostname = "localhost"
396 // a.hostname = "localhost"
397 // a.port = 8888
397 // a.port = 8888
398 // a.pathname = "/path/name"
398 // a.pathname = "/path/name"
399 // a.hash = "#hash"
399 // a.hash = "#hash"
400 var a = document.createElement("a");
400 var a = document.createElement("a");
401 a.href = url;
401 a.href = url;
402 return a;
402 return a;
403 };
403 };
404
404
405 var encode_uri_components = function (uri) {
405 var encode_uri_components = function (uri) {
406 // encode just the components of a multi-segment uri,
406 // encode just the components of a multi-segment uri,
407 // leaving '/' separators
407 // leaving '/' separators
408 return uri.split('/').map(encodeURIComponent).join('/');
408 return uri.split('/').map(encodeURIComponent).join('/');
409 };
409 };
410
410
411 var url_join_encode = function () {
411 var url_join_encode = function () {
412 // join a sequence of url components with '/',
412 // join a sequence of url components with '/',
413 // encoding each component with encodeURIComponent
413 // encoding each component with encodeURIComponent
414 return encode_uri_components(url_path_join.apply(null, arguments));
414 return encode_uri_components(url_path_join.apply(null, arguments));
415 };
415 };
416
416
417
417
418 var splitext = function (filename) {
418 var splitext = function (filename) {
419 // mimic Python os.path.splitext
419 // mimic Python os.path.splitext
420 // Returns ['base', '.ext']
420 // Returns ['base', '.ext']
421 var idx = filename.lastIndexOf('.');
421 var idx = filename.lastIndexOf('.');
422 if (idx > 0) {
422 if (idx > 0) {
423 return [filename.slice(0, idx), filename.slice(idx)];
423 return [filename.slice(0, idx), filename.slice(idx)];
424 } else {
424 } else {
425 return [filename, ''];
425 return [filename, ''];
426 }
426 }
427 };
427 };
428
428
429
429
430 var escape_html = function (text) {
430 var escape_html = function (text) {
431 // escape text to HTML
431 // escape text to HTML
432 return $("<div/>").text(text).html();
432 return $("<div/>").text(text).html();
433 }
433 }
434
434
435
435
436 var get_body_data = function(key) {
436 var get_body_data = function(key) {
437 // get a url-encoded item from body.data and decode it
437 // get a url-encoded item from body.data and decode it
438 // we should never have any encoded URLs anywhere else in code
438 // we should never have any encoded URLs anywhere else in code
439 // until we are building an actual request
439 // until we are building an actual request
440 return decodeURIComponent($('body').data(key));
440 return decodeURIComponent($('body').data(key));
441 };
441 };
442
442
443
443
444 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
444 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
445 var browser = (function() {
445 var browser = (function() {
446 if (typeof navigator === 'undefined') {
446 if (typeof navigator === 'undefined') {
447 // navigator undefined in node
447 // navigator undefined in node
448 return 'None';
448 return 'None';
449 }
449 }
450 var N= navigator.appName, ua= navigator.userAgent, tem;
450 var N= navigator.appName, ua= navigator.userAgent, tem;
451 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
451 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
452 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
452 if (M && (tem= ua.match(/version\/([\.\d]+)/i))!= null) M[2]= tem[1];
453 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
453 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
454 return M;
454 return M;
455 })();
455 })();
456
456
457 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
457 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
458 var platform = (function () {
458 var platform = (function () {
459 if (typeof navigator === 'undefined') {
459 if (typeof navigator === 'undefined') {
460 // navigator undefined in node
460 // navigator undefined in node
461 return 'None';
461 return 'None';
462 }
462 }
463 var OSName="None";
463 var OSName="None";
464 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
464 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
465 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
465 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
466 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
466 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
467 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
467 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
468 return OSName
468 return OSName
469 })();
469 })();
470
470
471 var is_or_has = function (a, b) {
471 var is_or_has = function (a, b) {
472 // Is b a child of a or a itself?
472 // Is b a child of a or a itself?
473 return a.has(b).length !==0 || a.is(b);
473 return a.has(b).length !==0 || a.is(b);
474 }
474 }
475
475
476 var is_focused = function (e) {
476 var is_focused = function (e) {
477 // Is element e, or one of its children focused?
477 // Is element e, or one of its children focused?
478 e = $(e);
478 e = $(e);
479 var target = $(document.activeElement);
479 var target = $(document.activeElement);
480 if (target.length > 0) {
480 if (target.length > 0) {
481 if (is_or_has(e, target)) {
481 if (is_or_has(e, target)) {
482 return true;
482 return true;
483 } else {
483 } else {
484 return false;
484 return false;
485 }
485 }
486 } else {
486 } else {
487 return false;
487 return false;
488 }
488 }
489 }
489 }
490
490
491
492 return {
491 return {
493 regex_split : regex_split,
492 regex_split : regex_split,
494 uuid : uuid,
493 uuid : uuid,
495 fixConsole : fixConsole,
494 fixConsole : fixConsole,
496 fixCarriageReturn : fixCarriageReturn,
495 fixCarriageReturn : fixCarriageReturn,
497 autoLinkUrls : autoLinkUrls,
496 autoLinkUrls : autoLinkUrls,
498 points_to_pixels : points_to_pixels,
497 points_to_pixels : points_to_pixels,
499 get_body_data : get_body_data,
498 get_body_data : get_body_data,
500 parse_url : parse_url,
499 parse_url : parse_url,
501 url_path_join : url_path_join,
500 url_path_join : url_path_join,
502 url_join_encode : url_join_encode,
501 url_join_encode : url_join_encode,
503 encode_uri_components : encode_uri_components,
502 encode_uri_components : encode_uri_components,
504 splitext : splitext,
503 splitext : splitext,
505 escape_html : escape_html,
504 escape_html : escape_html,
506 always_new : always_new,
505 always_new : always_new,
507 browser : browser,
506 browser : browser,
508 platform: platform,
507 platform: platform,
509 is_or_has : is_or_has,
508 is_or_has : is_or_has,
510 is_focused : is_focused
509 is_focused : is_focused
511 };
510 };
512
511
513 }(IPython));
512 }(IPython));
514
513
@@ -1,1 +1,1 b''
1 Subproject commit 2f8958788c7e0416e5c44f532e9630a658df11fd
1 Subproject commit 7a9ba818b3e13123621cb5ff336c002d49470f55
@@ -1,318 +1,332 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 // MenuBar
9 // MenuBar
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule MenuBar
15 * @submodule MenuBar
16 */
16 */
17
17
18
18
19 var IPython = (function (IPython) {
19 var IPython = (function (IPython) {
20 "use strict";
20 "use strict";
21
21
22 var utils = IPython.utils;
22 var utils = IPython.utils;
23
23
24 /**
24 /**
25 * A MenuBar Class to generate the menubar of IPython notebook
25 * A MenuBar Class to generate the menubar of IPython notebook
26 * @Class MenuBar
26 * @Class MenuBar
27 *
27 *
28 * @constructor
28 * @constructor
29 *
29 *
30 *
30 *
31 * @param selector {string} selector for the menubar element in DOM
31 * @param selector {string} selector for the menubar element in DOM
32 * @param {object} [options]
32 * @param {object} [options]
33 * @param [options.base_url] {String} String to use for the
33 * @param [options.base_url] {String} String to use for the
34 * base project url. Default is to inspect
34 * base project url. Default is to inspect
35 * $('body').data('baseUrl');
35 * $('body').data('baseUrl');
36 * does not support change for now is set through this option
36 * does not support change for now is set through this option
37 */
37 */
38 var MenuBar = function (selector, options) {
38 var MenuBar = function (selector, options) {
39 options = options || {};
39 options = options || {};
40 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
40 this.base_url = options.base_url || IPython.utils.get_body_data("baseUrl");
41 this.selector = selector;
41 this.selector = selector;
42 if (this.selector !== undefined) {
42 if (this.selector !== undefined) {
43 this.element = $(selector);
43 this.element = $(selector);
44 this.style();
44 this.style();
45 this.bind_events();
45 this.bind_events();
46 }
46 }
47 };
47 };
48
48
49 MenuBar.prototype.style = function () {
49 MenuBar.prototype.style = function () {
50 this.element.addClass('border-box-sizing');
50 this.element.addClass('border-box-sizing');
51 this.element.find("li").click(function (event, ui) {
51 this.element.find("li").click(function (event, ui) {
52 // The selected cell loses focus when the menu is entered, so we
52 // The selected cell loses focus when the menu is entered, so we
53 // re-select it upon selection.
53 // re-select it upon selection.
54 var i = IPython.notebook.get_selected_index();
54 var i = IPython.notebook.get_selected_index();
55 IPython.notebook.select(i);
55 IPython.notebook.select(i);
56 }
56 }
57 );
57 );
58 };
58 };
59
59
60 MenuBar.prototype._nbconvert = function (format, download) {
60 MenuBar.prototype._nbconvert = function (format, download) {
61 download = download || false;
61 download = download || false;
62 var notebook_path = IPython.notebook.notebook_path;
62 var notebook_path = IPython.notebook.notebook_path;
63 var notebook_name = IPython.notebook.notebook_name;
63 var notebook_name = IPython.notebook.notebook_name;
64 if (IPython.notebook.dirty) {
64 if (IPython.notebook.dirty) {
65 IPython.notebook.save_notebook({async : false});
65 IPython.notebook.save_notebook({async : false});
66 }
66 }
67 var url = utils.url_join_encode(
67 var url = utils.url_join_encode(
68 this.base_url,
68 this.base_url,
69 'nbconvert',
69 'nbconvert',
70 format,
70 format,
71 notebook_path,
71 notebook_path,
72 notebook_name
72 notebook_name
73 ) + "?download=" + download.toString();
73 ) + "?download=" + download.toString();
74
74
75 window.open(url);
75 window.open(url);
76 };
76 };
77
77
78 MenuBar.prototype.bind_events = function () {
78 MenuBar.prototype.bind_events = function () {
79 // File
79 // File
80 var that = this;
80 var that = this;
81 this.element.find('#new_notebook').click(function () {
81 this.element.find('#new_notebook').click(function () {
82 IPython.notebook.new_notebook();
82 IPython.notebook.new_notebook();
83 });
83 });
84 this.element.find('#open_notebook').click(function () {
84 this.element.find('#open_notebook').click(function () {
85 window.open(utils.url_join_encode(
85 window.open(utils.url_join_encode(
86 IPython.notebook.base_url,
86 IPython.notebook.base_url,
87 'tree',
87 'tree',
88 IPython.notebook.notebook_path
88 IPython.notebook.notebook_path
89 ));
89 ));
90 });
90 });
91 this.element.find('#copy_notebook').click(function () {
91 this.element.find('#copy_notebook').click(function () {
92 IPython.notebook.copy_notebook();
92 IPython.notebook.copy_notebook();
93 return false;
93 return false;
94 });
94 });
95 this.element.find('#download_ipynb').click(function () {
95 this.element.find('#download_ipynb').click(function () {
96 var base_url = IPython.notebook.base_url;
96 var base_url = IPython.notebook.base_url;
97 var notebook_path = IPython.notebook.notebook_path;
97 var notebook_path = IPython.notebook.notebook_path;
98 var notebook_name = IPython.notebook.notebook_name;
98 var notebook_name = IPython.notebook.notebook_name;
99 if (IPython.notebook.dirty) {
99 if (IPython.notebook.dirty) {
100 IPython.notebook.save_notebook({async : false});
100 IPython.notebook.save_notebook({async : false});
101 }
101 }
102
102
103 var url = utils.url_join_encode(
103 var url = utils.url_join_encode(
104 base_url,
104 base_url,
105 'files',
105 'files',
106 notebook_path,
106 notebook_path,
107 notebook_name
107 notebook_name
108 );
108 );
109 window.location.assign(url);
109 window.location.assign(url);
110 });
110 });
111
111
112 this.element.find('#print_preview').click(function () {
112 this.element.find('#print_preview').click(function () {
113 that._nbconvert('html', false);
113 that._nbconvert('html', false);
114 });
114 });
115
115
116 this.element.find('#download_py').click(function () {
116 this.element.find('#download_py').click(function () {
117 that._nbconvert('python', true);
117 that._nbconvert('python', true);
118 });
118 });
119
119
120 this.element.find('#download_html').click(function () {
120 this.element.find('#download_html').click(function () {
121 that._nbconvert('html', true);
121 that._nbconvert('html', true);
122 });
122 });
123
123
124 this.element.find('#download_rst').click(function () {
124 this.element.find('#download_rst').click(function () {
125 that._nbconvert('rst', true);
125 that._nbconvert('rst', true);
126 });
126 });
127
127
128 this.element.find('#rename_notebook').click(function () {
128 this.element.find('#rename_notebook').click(function () {
129 IPython.save_widget.rename_notebook();
129 IPython.save_widget.rename_notebook();
130 });
130 });
131 this.element.find('#save_checkpoint').click(function () {
131 this.element.find('#save_checkpoint').click(function () {
132 IPython.notebook.save_checkpoint();
132 IPython.notebook.save_checkpoint();
133 });
133 });
134 this.element.find('#restore_checkpoint').click(function () {
134 this.element.find('#restore_checkpoint').click(function () {
135 });
135 });
136 this.element.find('#trust_notebook').click(function () {
137 IPython.notebook.trust_notebook();
138 });
139 $([IPython.events]).on('trust_changed.Notebook', function (event, trusted) {
140 if (trusted) {
141 that.element.find('#trust_notebook')
142 .addClass("disabled")
143 .find("a").text("Trusted Notebook");
144 } else {
145 that.element.find('#trust_notebook')
146 .removeClass("disabled")
147 .find("a").text("Trust Notebook");
148 }
149 });
136 this.element.find('#kill_and_exit').click(function () {
150 this.element.find('#kill_and_exit').click(function () {
137 IPython.notebook.session.delete();
151 IPython.notebook.session.delete();
138 setTimeout(function(){
152 setTimeout(function(){
139 // allow closing of new tabs in Chromium, impossible in FF
153 // allow closing of new tabs in Chromium, impossible in FF
140 window.open('', '_self', '');
154 window.open('', '_self', '');
141 window.close();
155 window.close();
142 }, 500);
156 }, 500);
143 });
157 });
144 // Edit
158 // Edit
145 this.element.find('#cut_cell').click(function () {
159 this.element.find('#cut_cell').click(function () {
146 IPython.notebook.cut_cell();
160 IPython.notebook.cut_cell();
147 });
161 });
148 this.element.find('#copy_cell').click(function () {
162 this.element.find('#copy_cell').click(function () {
149 IPython.notebook.copy_cell();
163 IPython.notebook.copy_cell();
150 });
164 });
151 this.element.find('#delete_cell').click(function () {
165 this.element.find('#delete_cell').click(function () {
152 IPython.notebook.delete_cell();
166 IPython.notebook.delete_cell();
153 });
167 });
154 this.element.find('#undelete_cell').click(function () {
168 this.element.find('#undelete_cell').click(function () {
155 IPython.notebook.undelete_cell();
169 IPython.notebook.undelete_cell();
156 });
170 });
157 this.element.find('#split_cell').click(function () {
171 this.element.find('#split_cell').click(function () {
158 IPython.notebook.split_cell();
172 IPython.notebook.split_cell();
159 });
173 });
160 this.element.find('#merge_cell_above').click(function () {
174 this.element.find('#merge_cell_above').click(function () {
161 IPython.notebook.merge_cell_above();
175 IPython.notebook.merge_cell_above();
162 });
176 });
163 this.element.find('#merge_cell_below').click(function () {
177 this.element.find('#merge_cell_below').click(function () {
164 IPython.notebook.merge_cell_below();
178 IPython.notebook.merge_cell_below();
165 });
179 });
166 this.element.find('#move_cell_up').click(function () {
180 this.element.find('#move_cell_up').click(function () {
167 IPython.notebook.move_cell_up();
181 IPython.notebook.move_cell_up();
168 });
182 });
169 this.element.find('#move_cell_down').click(function () {
183 this.element.find('#move_cell_down').click(function () {
170 IPython.notebook.move_cell_down();
184 IPython.notebook.move_cell_down();
171 });
185 });
172 this.element.find('#edit_nb_metadata').click(function () {
186 this.element.find('#edit_nb_metadata').click(function () {
173 IPython.notebook.edit_metadata();
187 IPython.notebook.edit_metadata();
174 });
188 });
175
189
176 // View
190 // View
177 this.element.find('#toggle_header').click(function () {
191 this.element.find('#toggle_header').click(function () {
178 $('div#header').toggle();
192 $('div#header').toggle();
179 IPython.layout_manager.do_resize();
193 IPython.layout_manager.do_resize();
180 });
194 });
181 this.element.find('#toggle_toolbar').click(function () {
195 this.element.find('#toggle_toolbar').click(function () {
182 $('div#maintoolbar').toggle();
196 $('div#maintoolbar').toggle();
183 IPython.layout_manager.do_resize();
197 IPython.layout_manager.do_resize();
184 });
198 });
185 // Insert
199 // Insert
186 this.element.find('#insert_cell_above').click(function () {
200 this.element.find('#insert_cell_above').click(function () {
187 IPython.notebook.insert_cell_above('code');
201 IPython.notebook.insert_cell_above('code');
188 IPython.notebook.select_prev();
202 IPython.notebook.select_prev();
189 });
203 });
190 this.element.find('#insert_cell_below').click(function () {
204 this.element.find('#insert_cell_below').click(function () {
191 IPython.notebook.insert_cell_below('code');
205 IPython.notebook.insert_cell_below('code');
192 IPython.notebook.select_next();
206 IPython.notebook.select_next();
193 });
207 });
194 // Cell
208 // Cell
195 this.element.find('#run_cell').click(function () {
209 this.element.find('#run_cell').click(function () {
196 IPython.notebook.execute_cell();
210 IPython.notebook.execute_cell();
197 });
211 });
198 this.element.find('#run_cell_select_below').click(function () {
212 this.element.find('#run_cell_select_below').click(function () {
199 IPython.notebook.execute_cell_and_select_below();
213 IPython.notebook.execute_cell_and_select_below();
200 });
214 });
201 this.element.find('#run_cell_insert_below').click(function () {
215 this.element.find('#run_cell_insert_below').click(function () {
202 IPython.notebook.execute_cell_and_insert_below();
216 IPython.notebook.execute_cell_and_insert_below();
203 });
217 });
204 this.element.find('#run_all_cells').click(function () {
218 this.element.find('#run_all_cells').click(function () {
205 IPython.notebook.execute_all_cells();
219 IPython.notebook.execute_all_cells();
206 });
220 });
207 this.element.find('#run_all_cells_above').click(function () {
221 this.element.find('#run_all_cells_above').click(function () {
208 IPython.notebook.execute_cells_above();
222 IPython.notebook.execute_cells_above();
209 });
223 });
210 this.element.find('#run_all_cells_below').click(function () {
224 this.element.find('#run_all_cells_below').click(function () {
211 IPython.notebook.execute_cells_below();
225 IPython.notebook.execute_cells_below();
212 });
226 });
213 this.element.find('#to_code').click(function () {
227 this.element.find('#to_code').click(function () {
214 IPython.notebook.to_code();
228 IPython.notebook.to_code();
215 });
229 });
216 this.element.find('#to_markdown').click(function () {
230 this.element.find('#to_markdown').click(function () {
217 IPython.notebook.to_markdown();
231 IPython.notebook.to_markdown();
218 });
232 });
219 this.element.find('#to_raw').click(function () {
233 this.element.find('#to_raw').click(function () {
220 IPython.notebook.to_raw();
234 IPython.notebook.to_raw();
221 });
235 });
222 this.element.find('#to_heading1').click(function () {
236 this.element.find('#to_heading1').click(function () {
223 IPython.notebook.to_heading(undefined, 1);
237 IPython.notebook.to_heading(undefined, 1);
224 });
238 });
225 this.element.find('#to_heading2').click(function () {
239 this.element.find('#to_heading2').click(function () {
226 IPython.notebook.to_heading(undefined, 2);
240 IPython.notebook.to_heading(undefined, 2);
227 });
241 });
228 this.element.find('#to_heading3').click(function () {
242 this.element.find('#to_heading3').click(function () {
229 IPython.notebook.to_heading(undefined, 3);
243 IPython.notebook.to_heading(undefined, 3);
230 });
244 });
231 this.element.find('#to_heading4').click(function () {
245 this.element.find('#to_heading4').click(function () {
232 IPython.notebook.to_heading(undefined, 4);
246 IPython.notebook.to_heading(undefined, 4);
233 });
247 });
234 this.element.find('#to_heading5').click(function () {
248 this.element.find('#to_heading5').click(function () {
235 IPython.notebook.to_heading(undefined, 5);
249 IPython.notebook.to_heading(undefined, 5);
236 });
250 });
237 this.element.find('#to_heading6').click(function () {
251 this.element.find('#to_heading6').click(function () {
238 IPython.notebook.to_heading(undefined, 6);
252 IPython.notebook.to_heading(undefined, 6);
239 });
253 });
240
254
241 this.element.find('#toggle_current_output').click(function () {
255 this.element.find('#toggle_current_output').click(function () {
242 IPython.notebook.toggle_output();
256 IPython.notebook.toggle_output();
243 });
257 });
244 this.element.find('#toggle_current_output_scroll').click(function () {
258 this.element.find('#toggle_current_output_scroll').click(function () {
245 IPython.notebook.toggle_output_scroll();
259 IPython.notebook.toggle_output_scroll();
246 });
260 });
247 this.element.find('#clear_current_output').click(function () {
261 this.element.find('#clear_current_output').click(function () {
248 IPython.notebook.clear_output();
262 IPython.notebook.clear_output();
249 });
263 });
250
264
251 this.element.find('#toggle_all_output').click(function () {
265 this.element.find('#toggle_all_output').click(function () {
252 IPython.notebook.toggle_all_output();
266 IPython.notebook.toggle_all_output();
253 });
267 });
254 this.element.find('#toggle_all_output_scroll').click(function () {
268 this.element.find('#toggle_all_output_scroll').click(function () {
255 IPython.notebook.toggle_all_output_scroll();
269 IPython.notebook.toggle_all_output_scroll();
256 });
270 });
257 this.element.find('#clear_all_output').click(function () {
271 this.element.find('#clear_all_output').click(function () {
258 IPython.notebook.clear_all_output();
272 IPython.notebook.clear_all_output();
259 });
273 });
260
274
261 // Kernel
275 // Kernel
262 this.element.find('#int_kernel').click(function () {
276 this.element.find('#int_kernel').click(function () {
263 IPython.notebook.session.interrupt_kernel();
277 IPython.notebook.session.interrupt_kernel();
264 });
278 });
265 this.element.find('#restart_kernel').click(function () {
279 this.element.find('#restart_kernel').click(function () {
266 IPython.notebook.restart_kernel();
280 IPython.notebook.restart_kernel();
267 });
281 });
268 // Help
282 // Help
269 this.element.find('#keyboard_shortcuts').click(function () {
283 this.element.find('#keyboard_shortcuts').click(function () {
270 IPython.quick_help.show_keyboard_shortcuts();
284 IPython.quick_help.show_keyboard_shortcuts();
271 });
285 });
272
286
273 this.update_restore_checkpoint(null);
287 this.update_restore_checkpoint(null);
274
288
275 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
289 $([IPython.events]).on('checkpoints_listed.Notebook', function (event, data) {
276 that.update_restore_checkpoint(IPython.notebook.checkpoints);
290 that.update_restore_checkpoint(IPython.notebook.checkpoints);
277 });
291 });
278
292
279 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
293 $([IPython.events]).on('checkpoint_created.Notebook', function (event, data) {
280 that.update_restore_checkpoint(IPython.notebook.checkpoints);
294 that.update_restore_checkpoint(IPython.notebook.checkpoints);
281 });
295 });
282 };
296 };
283
297
284 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
298 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
285 var ul = this.element.find("#restore_checkpoint").find("ul");
299 var ul = this.element.find("#restore_checkpoint").find("ul");
286 ul.empty();
300 ul.empty();
287 if (!checkpoints || checkpoints.length === 0) {
301 if (!checkpoints || checkpoints.length === 0) {
288 ul.append(
302 ul.append(
289 $("<li/>")
303 $("<li/>")
290 .addClass("disabled")
304 .addClass("disabled")
291 .append(
305 .append(
292 $("<a/>")
306 $("<a/>")
293 .text("No checkpoints")
307 .text("No checkpoints")
294 )
308 )
295 );
309 );
296 return;
310 return;
297 }
311 }
298
312
299 checkpoints.map(function (checkpoint) {
313 checkpoints.map(function (checkpoint) {
300 var d = new Date(checkpoint.last_modified);
314 var d = new Date(checkpoint.last_modified);
301 ul.append(
315 ul.append(
302 $("<li/>").append(
316 $("<li/>").append(
303 $("<a/>")
317 $("<a/>")
304 .attr("href", "#")
318 .attr("href", "#")
305 .text(d.format("mmm dd HH:MM:ss"))
319 .text(d.format("mmm dd HH:MM:ss"))
306 .click(function () {
320 .click(function () {
307 IPython.notebook.restore_checkpoint_dialog(checkpoint);
321 IPython.notebook.restore_checkpoint_dialog(checkpoint);
308 })
322 })
309 )
323 )
310 );
324 );
311 });
325 });
312 };
326 };
313
327
314 IPython.MenuBar = MenuBar;
328 IPython.MenuBar = MenuBar;
315
329
316 return IPython;
330 return IPython;
317
331
318 }(IPython));
332 }(IPython));
@@ -1,2332 +1,2401 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2011 The IPython Development Team
2 // Copyright (C) 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 // Notebook
9 // Notebook
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13 "use strict";
13 "use strict";
14
14
15 var utils = IPython.utils;
15 var utils = IPython.utils;
16
16
17 /**
17 /**
18 * A notebook contains and manages cells.
18 * A notebook contains and manages cells.
19 *
19 *
20 * @class Notebook
20 * @class Notebook
21 * @constructor
21 * @constructor
22 * @param {String} selector A jQuery selector for the notebook's DOM element
22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 * @param {Object} [options] A config object
23 * @param {Object} [options] A config object
24 */
24 */
25 var Notebook = function (selector, options) {
25 var Notebook = function (selector, options) {
26 this.options = options = options || {};
26 this.options = options = options || {};
27 this.base_url = options.base_url;
27 this.base_url = options.base_url;
28 this.notebook_path = options.notebook_path;
28 this.notebook_path = options.notebook_path;
29 this.notebook_name = options.notebook_name;
29 this.notebook_name = options.notebook_name;
30 this.element = $(selector);
30 this.element = $(selector);
31 this.element.scroll();
31 this.element.scroll();
32 this.element.data("notebook", this);
32 this.element.data("notebook", this);
33 this.next_prompt_number = 1;
33 this.next_prompt_number = 1;
34 this.session = null;
34 this.session = null;
35 this.kernel = null;
35 this.kernel = null;
36 this.clipboard = null;
36 this.clipboard = null;
37 this.undelete_backup = null;
37 this.undelete_backup = null;
38 this.undelete_index = null;
38 this.undelete_index = null;
39 this.undelete_below = false;
39 this.undelete_below = false;
40 this.paste_enabled = false;
40 this.paste_enabled = false;
41 // It is important to start out in command mode to match the intial mode
41 // It is important to start out in command mode to match the intial mode
42 // of the KeyboardManager.
42 // of the KeyboardManager.
43 this.mode = 'command';
43 this.mode = 'command';
44 this.set_dirty(false);
44 this.set_dirty(false);
45 this.metadata = {};
45 this.metadata = {};
46 this._checkpoint_after_save = false;
46 this._checkpoint_after_save = false;
47 this.last_checkpoint = null;
47 this.last_checkpoint = null;
48 this.checkpoints = [];
48 this.checkpoints = [];
49 this.autosave_interval = 0;
49 this.autosave_interval = 0;
50 this.autosave_timer = null;
50 this.autosave_timer = null;
51 // autosave *at most* every two minutes
51 // autosave *at most* every two minutes
52 this.minimum_autosave_interval = 120000;
52 this.minimum_autosave_interval = 120000;
53 // single worksheet for now
53 // single worksheet for now
54 this.worksheet_metadata = {};
54 this.worksheet_metadata = {};
55 this.notebook_name_blacklist_re = /[\/\\:]/;
55 this.notebook_name_blacklist_re = /[\/\\:]/;
56 this.nbformat = 3; // Increment this when changing the nbformat
56 this.nbformat = 3; // Increment this when changing the nbformat
57 this.nbformat_minor = 0; // Increment this when changing the nbformat
57 this.nbformat_minor = 0; // Increment this when changing the nbformat
58 this.style();
58 this.style();
59 this.create_elements();
59 this.create_elements();
60 this.bind_events();
60 this.bind_events();
61 };
61 };
62
62
63 /**
63 /**
64 * Tweak the notebook's CSS style.
64 * Tweak the notebook's CSS style.
65 *
65 *
66 * @method style
66 * @method style
67 */
67 */
68 Notebook.prototype.style = function () {
68 Notebook.prototype.style = function () {
69 $('div#notebook').addClass('border-box-sizing');
69 $('div#notebook').addClass('border-box-sizing');
70 };
70 };
71
71
72 /**
72 /**
73 * Create an HTML and CSS representation of the notebook.
73 * Create an HTML and CSS representation of the notebook.
74 *
74 *
75 * @method create_elements
75 * @method create_elements
76 */
76 */
77 Notebook.prototype.create_elements = function () {
77 Notebook.prototype.create_elements = function () {
78 var that = this;
78 var that = this;
79 this.element.attr('tabindex','-1');
79 this.element.attr('tabindex','-1');
80 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
80 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
81 // We add this end_space div to the end of the notebook div to:
81 // We add this end_space div to the end of the notebook div to:
82 // i) provide a margin between the last cell and the end of the notebook
82 // i) provide a margin between the last cell and the end of the notebook
83 // ii) to prevent the div from scrolling up when the last cell is being
83 // ii) to prevent the div from scrolling up when the last cell is being
84 // edited, but is too low on the page, which browsers will do automatically.
84 // edited, but is too low on the page, which browsers will do automatically.
85 var end_space = $('<div/>').addClass('end_space');
85 var end_space = $('<div/>').addClass('end_space');
86 end_space.dblclick(function (e) {
86 end_space.dblclick(function (e) {
87 var ncells = that.ncells();
87 var ncells = that.ncells();
88 that.insert_cell_below('code',ncells-1);
88 that.insert_cell_below('code',ncells-1);
89 });
89 });
90 this.element.append(this.container);
90 this.element.append(this.container);
91 this.container.append(end_space);
91 this.container.append(end_space);
92 };
92 };
93
93
94 /**
94 /**
95 * Bind JavaScript events: key presses and custom IPython events.
95 * Bind JavaScript events: key presses and custom IPython events.
96 *
96 *
97 * @method bind_events
97 * @method bind_events
98 */
98 */
99 Notebook.prototype.bind_events = function () {
99 Notebook.prototype.bind_events = function () {
100 var that = this;
100 var that = this;
101
101
102 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
102 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
103 var index = that.find_cell_index(data.cell);
103 var index = that.find_cell_index(data.cell);
104 var new_cell = that.insert_cell_below('code',index);
104 var new_cell = that.insert_cell_below('code',index);
105 new_cell.set_text(data.text);
105 new_cell.set_text(data.text);
106 that.dirty = true;
106 that.dirty = true;
107 });
107 });
108
108
109 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
109 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
110 that.dirty = data.value;
110 that.dirty = data.value;
111 });
111 });
112
112
113 $([IPython.events]).on('trust_changed.Notebook', function (event, data) {
114 that.trusted = data.value;
115 });
116
113 $([IPython.events]).on('select.Cell', function (event, data) {
117 $([IPython.events]).on('select.Cell', function (event, data) {
114 var index = that.find_cell_index(data.cell);
118 var index = that.find_cell_index(data.cell);
115 that.select(index);
119 that.select(index);
116 });
120 });
117
121
118 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
122 $([IPython.events]).on('edit_mode.Cell', function (event, data) {
119 that.handle_edit_mode(that.find_cell_index(data.cell));
123 that.handle_edit_mode(that.find_cell_index(data.cell));
120 });
124 });
121
125
122 $([IPython.events]).on('command_mode.Cell', function (event, data) {
126 $([IPython.events]).on('command_mode.Cell', function (event, data) {
123 that.command_mode();
127 that.command_mode();
124 });
128 });
125
129
126 $([IPython.events]).on('status_autorestarting.Kernel', function () {
130 $([IPython.events]).on('status_autorestarting.Kernel', function () {
127 IPython.dialog.modal({
131 IPython.dialog.modal({
128 title: "Kernel Restarting",
132 title: "Kernel Restarting",
129 body: "The kernel appears to have died. It will restart automatically.",
133 body: "The kernel appears to have died. It will restart automatically.",
130 buttons: {
134 buttons: {
131 OK : {
135 OK : {
132 class : "btn-primary"
136 class : "btn-primary"
133 }
137 }
134 }
138 }
135 });
139 });
136 });
140 });
137
141
138 var collapse_time = function (time) {
142 var collapse_time = function (time) {
139 var app_height = $('#ipython-main-app').height(); // content height
143 var app_height = $('#ipython-main-app').height(); // content height
140 var splitter_height = $('div#pager_splitter').outerHeight(true);
144 var splitter_height = $('div#pager_splitter').outerHeight(true);
141 var new_height = app_height - splitter_height;
145 var new_height = app_height - splitter_height;
142 that.element.animate({height : new_height + 'px'}, time);
146 that.element.animate({height : new_height + 'px'}, time);
143 };
147 };
144
148
145 this.element.bind('collapse_pager', function (event, extrap) {
149 this.element.bind('collapse_pager', function (event, extrap) {
146 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
150 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
147 collapse_time(time);
151 collapse_time(time);
148 });
152 });
149
153
150 var expand_time = function (time) {
154 var expand_time = function (time) {
151 var app_height = $('#ipython-main-app').height(); // content height
155 var app_height = $('#ipython-main-app').height(); // content height
152 var splitter_height = $('div#pager_splitter').outerHeight(true);
156 var splitter_height = $('div#pager_splitter').outerHeight(true);
153 var pager_height = $('div#pager').outerHeight(true);
157 var pager_height = $('div#pager').outerHeight(true);
154 var new_height = app_height - pager_height - splitter_height;
158 var new_height = app_height - pager_height - splitter_height;
155 that.element.animate({height : new_height + 'px'}, time);
159 that.element.animate({height : new_height + 'px'}, time);
156 };
160 };
157
161
158 this.element.bind('expand_pager', function (event, extrap) {
162 this.element.bind('expand_pager', function (event, extrap) {
159 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
163 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
160 expand_time(time);
164 expand_time(time);
161 });
165 });
162
166
163 // Firefox 22 broke $(window).on("beforeunload")
167 // Firefox 22 broke $(window).on("beforeunload")
164 // I'm not sure why or how.
168 // I'm not sure why or how.
165 window.onbeforeunload = function (e) {
169 window.onbeforeunload = function (e) {
166 // TODO: Make killing the kernel configurable.
170 // TODO: Make killing the kernel configurable.
167 var kill_kernel = false;
171 var kill_kernel = false;
168 if (kill_kernel) {
172 if (kill_kernel) {
169 that.session.kill_kernel();
173 that.session.kill_kernel();
170 }
174 }
171 // if we are autosaving, trigger an autosave on nav-away.
175 // if we are autosaving, trigger an autosave on nav-away.
172 // still warn, because if we don't the autosave may fail.
176 // still warn, because if we don't the autosave may fail.
173 if (that.dirty) {
177 if (that.dirty) {
174 if ( that.autosave_interval ) {
178 if ( that.autosave_interval ) {
175 // schedule autosave in a timeout
179 // schedule autosave in a timeout
176 // this gives you a chance to forcefully discard changes
180 // this gives you a chance to forcefully discard changes
177 // by reloading the page if you *really* want to.
181 // by reloading the page if you *really* want to.
178 // the timer doesn't start until you *dismiss* the dialog.
182 // the timer doesn't start until you *dismiss* the dialog.
179 setTimeout(function () {
183 setTimeout(function () {
180 if (that.dirty) {
184 if (that.dirty) {
181 that.save_notebook();
185 that.save_notebook();
182 }
186 }
183 }, 1000);
187 }, 1000);
184 return "Autosave in progress, latest changes may be lost.";
188 return "Autosave in progress, latest changes may be lost.";
185 } else {
189 } else {
186 return "Unsaved changes will be lost.";
190 return "Unsaved changes will be lost.";
187 }
191 }
188 }
192 }
189 // Null is the *only* return value that will make the browser not
193 // Null is the *only* return value that will make the browser not
190 // pop up the "don't leave" dialog.
194 // pop up the "don't leave" dialog.
191 return null;
195 return null;
192 };
196 };
193 };
197 };
194
198
195 /**
199 /**
196 * Set the dirty flag, and trigger the set_dirty.Notebook event
200 * Set the dirty flag, and trigger the set_dirty.Notebook event
197 *
201 *
198 * @method set_dirty
202 * @method set_dirty
199 */
203 */
200 Notebook.prototype.set_dirty = function (value) {
204 Notebook.prototype.set_dirty = function (value) {
201 if (value === undefined) {
205 if (value === undefined) {
202 value = true;
206 value = true;
203 }
207 }
204 if (this.dirty == value) {
208 if (this.dirty == value) {
205 return;
209 return;
206 }
210 }
207 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
211 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
208 };
212 };
209
213
210 /**
214 /**
211 * Scroll the top of the page to a given cell.
215 * Scroll the top of the page to a given cell.
212 *
216 *
213 * @method scroll_to_cell
217 * @method scroll_to_cell
214 * @param {Number} cell_number An index of the cell to view
218 * @param {Number} cell_number An index of the cell to view
215 * @param {Number} time Animation time in milliseconds
219 * @param {Number} time Animation time in milliseconds
216 * @return {Number} Pixel offset from the top of the container
220 * @return {Number} Pixel offset from the top of the container
217 */
221 */
218 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
222 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
219 var cells = this.get_cells();
223 var cells = this.get_cells();
220 time = time || 0;
224 time = time || 0;
221 cell_number = Math.min(cells.length-1,cell_number);
225 cell_number = Math.min(cells.length-1,cell_number);
222 cell_number = Math.max(0 ,cell_number);
226 cell_number = Math.max(0 ,cell_number);
223 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
227 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
224 this.element.animate({scrollTop:scroll_value}, time);
228 this.element.animate({scrollTop:scroll_value}, time);
225 return scroll_value;
229 return scroll_value;
226 };
230 };
227
231
228 /**
232 /**
229 * Scroll to the bottom of the page.
233 * Scroll to the bottom of the page.
230 *
234 *
231 * @method scroll_to_bottom
235 * @method scroll_to_bottom
232 */
236 */
233 Notebook.prototype.scroll_to_bottom = function () {
237 Notebook.prototype.scroll_to_bottom = function () {
234 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
238 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
235 };
239 };
236
240
237 /**
241 /**
238 * Scroll to the top of the page.
242 * Scroll to the top of the page.
239 *
243 *
240 * @method scroll_to_top
244 * @method scroll_to_top
241 */
245 */
242 Notebook.prototype.scroll_to_top = function () {
246 Notebook.prototype.scroll_to_top = function () {
243 this.element.animate({scrollTop:0}, 0);
247 this.element.animate({scrollTop:0}, 0);
244 };
248 };
245
249
246 // Edit Notebook metadata
250 // Edit Notebook metadata
247
251
248 Notebook.prototype.edit_metadata = function () {
252 Notebook.prototype.edit_metadata = function () {
249 var that = this;
253 var that = this;
250 IPython.dialog.edit_metadata(this.metadata, function (md) {
254 IPython.dialog.edit_metadata(this.metadata, function (md) {
251 that.metadata = md;
255 that.metadata = md;
252 }, 'Notebook');
256 }, 'Notebook');
253 };
257 };
254
258
255 // Cell indexing, retrieval, etc.
259 // Cell indexing, retrieval, etc.
256
260
257 /**
261 /**
258 * Get all cell elements in the notebook.
262 * Get all cell elements in the notebook.
259 *
263 *
260 * @method get_cell_elements
264 * @method get_cell_elements
261 * @return {jQuery} A selector of all cell elements
265 * @return {jQuery} A selector of all cell elements
262 */
266 */
263 Notebook.prototype.get_cell_elements = function () {
267 Notebook.prototype.get_cell_elements = function () {
264 return this.container.children("div.cell");
268 return this.container.children("div.cell");
265 };
269 };
266
270
267 /**
271 /**
268 * Get a particular cell element.
272 * Get a particular cell element.
269 *
273 *
270 * @method get_cell_element
274 * @method get_cell_element
271 * @param {Number} index An index of a cell to select
275 * @param {Number} index An index of a cell to select
272 * @return {jQuery} A selector of the given cell.
276 * @return {jQuery} A selector of the given cell.
273 */
277 */
274 Notebook.prototype.get_cell_element = function (index) {
278 Notebook.prototype.get_cell_element = function (index) {
275 var result = null;
279 var result = null;
276 var e = this.get_cell_elements().eq(index);
280 var e = this.get_cell_elements().eq(index);
277 if (e.length !== 0) {
281 if (e.length !== 0) {
278 result = e;
282 result = e;
279 }
283 }
280 return result;
284 return result;
281 };
285 };
282
286
283 /**
287 /**
284 * Try to get a particular cell by msg_id.
288 * Try to get a particular cell by msg_id.
285 *
289 *
286 * @method get_msg_cell
290 * @method get_msg_cell
287 * @param {String} msg_id A message UUID
291 * @param {String} msg_id A message UUID
288 * @return {Cell} Cell or null if no cell was found.
292 * @return {Cell} Cell or null if no cell was found.
289 */
293 */
290 Notebook.prototype.get_msg_cell = function (msg_id) {
294 Notebook.prototype.get_msg_cell = function (msg_id) {
291 return IPython.CodeCell.msg_cells[msg_id] || null;
295 return IPython.CodeCell.msg_cells[msg_id] || null;
292 };
296 };
293
297
294 /**
298 /**
295 * Count the cells in this notebook.
299 * Count the cells in this notebook.
296 *
300 *
297 * @method ncells
301 * @method ncells
298 * @return {Number} The number of cells in this notebook
302 * @return {Number} The number of cells in this notebook
299 */
303 */
300 Notebook.prototype.ncells = function () {
304 Notebook.prototype.ncells = function () {
301 return this.get_cell_elements().length;
305 return this.get_cell_elements().length;
302 };
306 };
303
307
304 /**
308 /**
305 * Get all Cell objects in this notebook.
309 * Get all Cell objects in this notebook.
306 *
310 *
307 * @method get_cells
311 * @method get_cells
308 * @return {Array} This notebook's Cell objects
312 * @return {Array} This notebook's Cell objects
309 */
313 */
310 // TODO: we are often calling cells as cells()[i], which we should optimize
314 // TODO: we are often calling cells as cells()[i], which we should optimize
311 // to cells(i) or a new method.
315 // to cells(i) or a new method.
312 Notebook.prototype.get_cells = function () {
316 Notebook.prototype.get_cells = function () {
313 return this.get_cell_elements().toArray().map(function (e) {
317 return this.get_cell_elements().toArray().map(function (e) {
314 return $(e).data("cell");
318 return $(e).data("cell");
315 });
319 });
316 };
320 };
317
321
318 /**
322 /**
319 * Get a Cell object from this notebook.
323 * Get a Cell object from this notebook.
320 *
324 *
321 * @method get_cell
325 * @method get_cell
322 * @param {Number} index An index of a cell to retrieve
326 * @param {Number} index An index of a cell to retrieve
323 * @return {Cell} A particular cell
327 * @return {Cell} A particular cell
324 */
328 */
325 Notebook.prototype.get_cell = function (index) {
329 Notebook.prototype.get_cell = function (index) {
326 var result = null;
330 var result = null;
327 var ce = this.get_cell_element(index);
331 var ce = this.get_cell_element(index);
328 if (ce !== null) {
332 if (ce !== null) {
329 result = ce.data('cell');
333 result = ce.data('cell');
330 }
334 }
331 return result;
335 return result;
332 };
336 };
333
337
334 /**
338 /**
335 * Get the cell below a given cell.
339 * Get the cell below a given cell.
336 *
340 *
337 * @method get_next_cell
341 * @method get_next_cell
338 * @param {Cell} cell The provided cell
342 * @param {Cell} cell The provided cell
339 * @return {Cell} The next cell
343 * @return {Cell} The next cell
340 */
344 */
341 Notebook.prototype.get_next_cell = function (cell) {
345 Notebook.prototype.get_next_cell = function (cell) {
342 var result = null;
346 var result = null;
343 var index = this.find_cell_index(cell);
347 var index = this.find_cell_index(cell);
344 if (this.is_valid_cell_index(index+1)) {
348 if (this.is_valid_cell_index(index+1)) {
345 result = this.get_cell(index+1);
349 result = this.get_cell(index+1);
346 }
350 }
347 return result;
351 return result;
348 };
352 };
349
353
350 /**
354 /**
351 * Get the cell above a given cell.
355 * Get the cell above a given cell.
352 *
356 *
353 * @method get_prev_cell
357 * @method get_prev_cell
354 * @param {Cell} cell The provided cell
358 * @param {Cell} cell The provided cell
355 * @return {Cell} The previous cell
359 * @return {Cell} The previous cell
356 */
360 */
357 Notebook.prototype.get_prev_cell = function (cell) {
361 Notebook.prototype.get_prev_cell = function (cell) {
358 // TODO: off-by-one
362 // TODO: off-by-one
359 // nb.get_prev_cell(nb.get_cell(1)) is null
363 // nb.get_prev_cell(nb.get_cell(1)) is null
360 var result = null;
364 var result = null;
361 var index = this.find_cell_index(cell);
365 var index = this.find_cell_index(cell);
362 if (index !== null && index > 1) {
366 if (index !== null && index > 1) {
363 result = this.get_cell(index-1);
367 result = this.get_cell(index-1);
364 }
368 }
365 return result;
369 return result;
366 };
370 };
367
371
368 /**
372 /**
369 * Get the numeric index of a given cell.
373 * Get the numeric index of a given cell.
370 *
374 *
371 * @method find_cell_index
375 * @method find_cell_index
372 * @param {Cell} cell The provided cell
376 * @param {Cell} cell The provided cell
373 * @return {Number} The cell's numeric index
377 * @return {Number} The cell's numeric index
374 */
378 */
375 Notebook.prototype.find_cell_index = function (cell) {
379 Notebook.prototype.find_cell_index = function (cell) {
376 var result = null;
380 var result = null;
377 this.get_cell_elements().filter(function (index) {
381 this.get_cell_elements().filter(function (index) {
378 if ($(this).data("cell") === cell) {
382 if ($(this).data("cell") === cell) {
379 result = index;
383 result = index;
380 }
384 }
381 });
385 });
382 return result;
386 return result;
383 };
387 };
384
388
385 /**
389 /**
386 * Get a given index , or the selected index if none is provided.
390 * Get a given index , or the selected index if none is provided.
387 *
391 *
388 * @method index_or_selected
392 * @method index_or_selected
389 * @param {Number} index A cell's index
393 * @param {Number} index A cell's index
390 * @return {Number} The given index, or selected index if none is provided.
394 * @return {Number} The given index, or selected index if none is provided.
391 */
395 */
392 Notebook.prototype.index_or_selected = function (index) {
396 Notebook.prototype.index_or_selected = function (index) {
393 var i;
397 var i;
394 if (index === undefined || index === null) {
398 if (index === undefined || index === null) {
395 i = this.get_selected_index();
399 i = this.get_selected_index();
396 if (i === null) {
400 if (i === null) {
397 i = 0;
401 i = 0;
398 }
402 }
399 } else {
403 } else {
400 i = index;
404 i = index;
401 }
405 }
402 return i;
406 return i;
403 };
407 };
404
408
405 /**
409 /**
406 * Get the currently selected cell.
410 * Get the currently selected cell.
407 * @method get_selected_cell
411 * @method get_selected_cell
408 * @return {Cell} The selected cell
412 * @return {Cell} The selected cell
409 */
413 */
410 Notebook.prototype.get_selected_cell = function () {
414 Notebook.prototype.get_selected_cell = function () {
411 var index = this.get_selected_index();
415 var index = this.get_selected_index();
412 return this.get_cell(index);
416 return this.get_cell(index);
413 };
417 };
414
418
415 /**
419 /**
416 * Check whether a cell index is valid.
420 * Check whether a cell index is valid.
417 *
421 *
418 * @method is_valid_cell_index
422 * @method is_valid_cell_index
419 * @param {Number} index A cell index
423 * @param {Number} index A cell index
420 * @return True if the index is valid, false otherwise
424 * @return True if the index is valid, false otherwise
421 */
425 */
422 Notebook.prototype.is_valid_cell_index = function (index) {
426 Notebook.prototype.is_valid_cell_index = function (index) {
423 if (index !== null && index >= 0 && index < this.ncells()) {
427 if (index !== null && index >= 0 && index < this.ncells()) {
424 return true;
428 return true;
425 } else {
429 } else {
426 return false;
430 return false;
427 }
431 }
428 };
432 };
429
433
430 /**
434 /**
431 * Get the index of the currently selected cell.
435 * Get the index of the currently selected cell.
432
436
433 * @method get_selected_index
437 * @method get_selected_index
434 * @return {Number} The selected cell's numeric index
438 * @return {Number} The selected cell's numeric index
435 */
439 */
436 Notebook.prototype.get_selected_index = function () {
440 Notebook.prototype.get_selected_index = function () {
437 var result = null;
441 var result = null;
438 this.get_cell_elements().filter(function (index) {
442 this.get_cell_elements().filter(function (index) {
439 if ($(this).data("cell").selected === true) {
443 if ($(this).data("cell").selected === true) {
440 result = index;
444 result = index;
441 }
445 }
442 });
446 });
443 return result;
447 return result;
444 };
448 };
445
449
446
450
447 // Cell selection.
451 // Cell selection.
448
452
449 /**
453 /**
450 * Programmatically select a cell.
454 * Programmatically select a cell.
451 *
455 *
452 * @method select
456 * @method select
453 * @param {Number} index A cell's index
457 * @param {Number} index A cell's index
454 * @return {Notebook} This notebook
458 * @return {Notebook} This notebook
455 */
459 */
456 Notebook.prototype.select = function (index) {
460 Notebook.prototype.select = function (index) {
457 if (this.is_valid_cell_index(index)) {
461 if (this.is_valid_cell_index(index)) {
458 var sindex = this.get_selected_index();
462 var sindex = this.get_selected_index();
459 if (sindex !== null && index !== sindex) {
463 if (sindex !== null && index !== sindex) {
460 this.get_cell(sindex).unselect();
464 this.get_cell(sindex).unselect();
461 }
465 }
462 var cell = this.get_cell(index);
466 var cell = this.get_cell(index);
463 cell.select();
467 cell.select();
464 if (cell.cell_type === 'heading') {
468 if (cell.cell_type === 'heading') {
465 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
469 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
466 {'cell_type':cell.cell_type,level:cell.level}
470 {'cell_type':cell.cell_type,level:cell.level}
467 );
471 );
468 } else {
472 } else {
469 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
473 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
470 {'cell_type':cell.cell_type}
474 {'cell_type':cell.cell_type}
471 );
475 );
472 }
476 }
473 }
477 }
474 return this;
478 return this;
475 };
479 };
476
480
477 /**
481 /**
478 * Programmatically select the next cell.
482 * Programmatically select the next cell.
479 *
483 *
480 * @method select_next
484 * @method select_next
481 * @return {Notebook} This notebook
485 * @return {Notebook} This notebook
482 */
486 */
483 Notebook.prototype.select_next = function () {
487 Notebook.prototype.select_next = function () {
484 var index = this.get_selected_index();
488 var index = this.get_selected_index();
485 this.select(index+1);
489 this.select(index+1);
486 return this;
490 return this;
487 };
491 };
488
492
489 /**
493 /**
490 * Programmatically select the previous cell.
494 * Programmatically select the previous cell.
491 *
495 *
492 * @method select_prev
496 * @method select_prev
493 * @return {Notebook} This notebook
497 * @return {Notebook} This notebook
494 */
498 */
495 Notebook.prototype.select_prev = function () {
499 Notebook.prototype.select_prev = function () {
496 var index = this.get_selected_index();
500 var index = this.get_selected_index();
497 this.select(index-1);
501 this.select(index-1);
498 return this;
502 return this;
499 };
503 };
500
504
501
505
502 // Edit/Command mode
506 // Edit/Command mode
503
507
504 /**
508 /**
505 * Gets the index of the cell that is in edit mode.
509 * Gets the index of the cell that is in edit mode.
506 *
510 *
507 * @method get_edit_index
511 * @method get_edit_index
508 *
512 *
509 * @return index {int}
513 * @return index {int}
510 **/
514 **/
511 Notebook.prototype.get_edit_index = function () {
515 Notebook.prototype.get_edit_index = function () {
512 var result = null;
516 var result = null;
513 this.get_cell_elements().filter(function (index) {
517 this.get_cell_elements().filter(function (index) {
514 if ($(this).data("cell").mode === 'edit') {
518 if ($(this).data("cell").mode === 'edit') {
515 result = index;
519 result = index;
516 }
520 }
517 });
521 });
518 return result;
522 return result;
519 };
523 };
520
524
521 /**
525 /**
522 * Make the notebook enter command mode.
526 * Make the notebook enter command mode.
523 *
527 *
524 * @method command_mode
528 * @method command_mode
525 **/
529 **/
526 Notebook.prototype.command_mode = function () {
530 Notebook.prototype.command_mode = function () {
527 // Make sure there isn't an edit mode cell lingering around.
531 // Make sure there isn't an edit mode cell lingering around.
528 var cell = this.get_cell(this.get_edit_index());
532 var cell = this.get_cell(this.get_edit_index());
529 if (cell) {
533 if (cell) {
530 cell.command_mode();
534 cell.command_mode();
531 }
535 }
532
536
533 // Notify the keyboard manager if this is a change of mode for the
537 // Notify the keyboard manager if this is a change of mode for the
534 // notebook as a whole.
538 // notebook as a whole.
535 if (this.mode !== 'command') {
539 if (this.mode !== 'command') {
536 this.mode = 'command';
540 this.mode = 'command';
537 $([IPython.events]).trigger('command_mode.Notebook');
541 $([IPython.events]).trigger('command_mode.Notebook');
538 IPython.keyboard_manager.command_mode();
542 IPython.keyboard_manager.command_mode();
539 }
543 }
540 };
544 };
541
545
542 /**
546 /**
543 * Handle when a cell fires it's edit_mode event.
547 * Handle when a cell fires it's edit_mode event.
544 *
548 *
545 * @method handle_edit_mode
549 * @method handle_edit_mode
546 * @param [index] {int} Cell index to select. If no index is provided,
550 * @param [index] {int} Cell index to select. If no index is provided,
547 * the current selected cell is used.
551 * the current selected cell is used.
548 **/
552 **/
549 Notebook.prototype.handle_edit_mode = function (index) {
553 Notebook.prototype.handle_edit_mode = function (index) {
550 // Make sure the cell exists.
554 // Make sure the cell exists.
551 var cell = this.get_cell(index);
555 var cell = this.get_cell(index);
552 if (cell === null) { return; }
556 if (cell === null) { return; }
553
557
554 // Set the cell to edit mode and notify the keyboard manager if this
558 // Set the cell to edit mode and notify the keyboard manager if this
555 // is a change of mode for the notebook as a whole.
559 // is a change of mode for the notebook as a whole.
556 if (this.mode !== 'edit') {
560 if (this.mode !== 'edit') {
557 cell.edit_mode();
561 cell.edit_mode();
558 this.mode = 'edit';
562 this.mode = 'edit';
559 $([IPython.events]).trigger('edit_mode.Notebook');
563 $([IPython.events]).trigger('edit_mode.Notebook');
560 IPython.keyboard_manager.edit_mode();
564 IPython.keyboard_manager.edit_mode();
561 }
565 }
562 };
566 };
563
567
564 /**
568 /**
565 * Make a cell enter edit mode.
569 * Make a cell enter edit mode.
566 *
570 *
567 * @method edit_mode
571 * @method edit_mode
568 * @param [index] {int} Cell index to select. If no index is provided,
572 * @param [index] {int} Cell index to select. If no index is provided,
569 * the current selected cell is used.
573 * the current selected cell is used.
570 **/
574 **/
571 Notebook.prototype.edit_mode = function (index) {
575 Notebook.prototype.edit_mode = function (index) {
572 if (index===undefined) {
576 if (index===undefined) {
573 index = this.get_selected_index();
577 index = this.get_selected_index();
574 }
578 }
575 // Make sure the cell exists.
579 // Make sure the cell exists.
576 var cell = this.get_cell(index);
580 var cell = this.get_cell(index);
577 if (cell === null) { return; }
581 if (cell === null) { return; }
578 if (cell.mode != 'edit') {
582 if (cell.mode != 'edit') {
579 cell.unrender();
583 cell.unrender();
580 cell.focus_editor();
584 cell.focus_editor();
581 }
585 }
582 };
586 };
583
587
584 /**
588 /**
585 * Focus the currently selected cell.
589 * Focus the currently selected cell.
586 *
590 *
587 * @method focus_cell
591 * @method focus_cell
588 **/
592 **/
589 Notebook.prototype.focus_cell = function () {
593 Notebook.prototype.focus_cell = function () {
590 var cell = this.get_selected_cell();
594 var cell = this.get_selected_cell();
591 if (cell === null) {return;} // No cell is selected
595 if (cell === null) {return;} // No cell is selected
592 cell.focus_cell();
596 cell.focus_cell();
593 };
597 };
594
598
595 // Cell movement
599 // Cell movement
596
600
597 /**
601 /**
598 * Move given (or selected) cell up and select it.
602 * Move given (or selected) cell up and select it.
599 *
603 *
600 * @method move_cell_up
604 * @method move_cell_up
601 * @param [index] {integer} cell index
605 * @param [index] {integer} cell index
602 * @return {Notebook} This notebook
606 * @return {Notebook} This notebook
603 **/
607 **/
604 Notebook.prototype.move_cell_up = function (index) {
608 Notebook.prototype.move_cell_up = function (index) {
605 var i = this.index_or_selected(index);
609 var i = this.index_or_selected(index);
606 if (this.is_valid_cell_index(i) && i > 0) {
610 if (this.is_valid_cell_index(i) && i > 0) {
607 var pivot = this.get_cell_element(i-1);
611 var pivot = this.get_cell_element(i-1);
608 var tomove = this.get_cell_element(i);
612 var tomove = this.get_cell_element(i);
609 if (pivot !== null && tomove !== null) {
613 if (pivot !== null && tomove !== null) {
610 tomove.detach();
614 tomove.detach();
611 pivot.before(tomove);
615 pivot.before(tomove);
612 this.select(i-1);
616 this.select(i-1);
613 var cell = this.get_selected_cell();
617 var cell = this.get_selected_cell();
614 cell.focus_cell();
618 cell.focus_cell();
615 }
619 }
616 this.set_dirty(true);
620 this.set_dirty(true);
617 }
621 }
618 return this;
622 return this;
619 };
623 };
620
624
621
625
622 /**
626 /**
623 * Move given (or selected) cell down and select it
627 * Move given (or selected) cell down and select it
624 *
628 *
625 * @method move_cell_down
629 * @method move_cell_down
626 * @param [index] {integer} cell index
630 * @param [index] {integer} cell index
627 * @return {Notebook} This notebook
631 * @return {Notebook} This notebook
628 **/
632 **/
629 Notebook.prototype.move_cell_down = function (index) {
633 Notebook.prototype.move_cell_down = function (index) {
630 var i = this.index_or_selected(index);
634 var i = this.index_or_selected(index);
631 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
635 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
632 var pivot = this.get_cell_element(i+1);
636 var pivot = this.get_cell_element(i+1);
633 var tomove = this.get_cell_element(i);
637 var tomove = this.get_cell_element(i);
634 if (pivot !== null && tomove !== null) {
638 if (pivot !== null && tomove !== null) {
635 tomove.detach();
639 tomove.detach();
636 pivot.after(tomove);
640 pivot.after(tomove);
637 this.select(i+1);
641 this.select(i+1);
638 var cell = this.get_selected_cell();
642 var cell = this.get_selected_cell();
639 cell.focus_cell();
643 cell.focus_cell();
640 }
644 }
641 }
645 }
642 this.set_dirty();
646 this.set_dirty();
643 return this;
647 return this;
644 };
648 };
645
649
646
650
647 // Insertion, deletion.
651 // Insertion, deletion.
648
652
649 /**
653 /**
650 * Delete a cell from the notebook.
654 * Delete a cell from the notebook.
651 *
655 *
652 * @method delete_cell
656 * @method delete_cell
653 * @param [index] A cell's numeric index
657 * @param [index] A cell's numeric index
654 * @return {Notebook} This notebook
658 * @return {Notebook} This notebook
655 */
659 */
656 Notebook.prototype.delete_cell = function (index) {
660 Notebook.prototype.delete_cell = function (index) {
657 var i = this.index_or_selected(index);
661 var i = this.index_or_selected(index);
658 var cell = this.get_selected_cell();
662 var cell = this.get_selected_cell();
659 this.undelete_backup = cell.toJSON();
663 this.undelete_backup = cell.toJSON();
660 $('#undelete_cell').removeClass('disabled');
664 $('#undelete_cell').removeClass('disabled');
661 if (this.is_valid_cell_index(i)) {
665 if (this.is_valid_cell_index(i)) {
662 var old_ncells = this.ncells();
666 var old_ncells = this.ncells();
663 var ce = this.get_cell_element(i);
667 var ce = this.get_cell_element(i);
664 ce.remove();
668 ce.remove();
665 if (i === 0) {
669 if (i === 0) {
666 // Always make sure we have at least one cell.
670 // Always make sure we have at least one cell.
667 if (old_ncells === 1) {
671 if (old_ncells === 1) {
668 this.insert_cell_below('code');
672 this.insert_cell_below('code');
669 }
673 }
670 this.select(0);
674 this.select(0);
671 this.undelete_index = 0;
675 this.undelete_index = 0;
672 this.undelete_below = false;
676 this.undelete_below = false;
673 } else if (i === old_ncells-1 && i !== 0) {
677 } else if (i === old_ncells-1 && i !== 0) {
674 this.select(i-1);
678 this.select(i-1);
675 this.undelete_index = i - 1;
679 this.undelete_index = i - 1;
676 this.undelete_below = true;
680 this.undelete_below = true;
677 } else {
681 } else {
678 this.select(i);
682 this.select(i);
679 this.undelete_index = i;
683 this.undelete_index = i;
680 this.undelete_below = false;
684 this.undelete_below = false;
681 }
685 }
682 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
686 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
683 this.set_dirty(true);
687 this.set_dirty(true);
684 }
688 }
685 return this;
689 return this;
686 };
690 };
687
691
688 /**
692 /**
689 * Restore the most recently deleted cell.
693 * Restore the most recently deleted cell.
690 *
694 *
691 * @method undelete
695 * @method undelete
692 */
696 */
693 Notebook.prototype.undelete_cell = function() {
697 Notebook.prototype.undelete_cell = function() {
694 if (this.undelete_backup !== null && this.undelete_index !== null) {
698 if (this.undelete_backup !== null && this.undelete_index !== null) {
695 var current_index = this.get_selected_index();
699 var current_index = this.get_selected_index();
696 if (this.undelete_index < current_index) {
700 if (this.undelete_index < current_index) {
697 current_index = current_index + 1;
701 current_index = current_index + 1;
698 }
702 }
699 if (this.undelete_index >= this.ncells()) {
703 if (this.undelete_index >= this.ncells()) {
700 this.select(this.ncells() - 1);
704 this.select(this.ncells() - 1);
701 }
705 }
702 else {
706 else {
703 this.select(this.undelete_index);
707 this.select(this.undelete_index);
704 }
708 }
705 var cell_data = this.undelete_backup;
709 var cell_data = this.undelete_backup;
706 var new_cell = null;
710 var new_cell = null;
707 if (this.undelete_below) {
711 if (this.undelete_below) {
708 new_cell = this.insert_cell_below(cell_data.cell_type);
712 new_cell = this.insert_cell_below(cell_data.cell_type);
709 } else {
713 } else {
710 new_cell = this.insert_cell_above(cell_data.cell_type);
714 new_cell = this.insert_cell_above(cell_data.cell_type);
711 }
715 }
712 new_cell.fromJSON(cell_data);
716 new_cell.fromJSON(cell_data);
713 if (this.undelete_below) {
717 if (this.undelete_below) {
714 this.select(current_index+1);
718 this.select(current_index+1);
715 } else {
719 } else {
716 this.select(current_index);
720 this.select(current_index);
717 }
721 }
718 this.undelete_backup = null;
722 this.undelete_backup = null;
719 this.undelete_index = null;
723 this.undelete_index = null;
720 }
724 }
721 $('#undelete_cell').addClass('disabled');
725 $('#undelete_cell').addClass('disabled');
722 };
726 };
723
727
724 /**
728 /**
725 * Insert a cell so that after insertion the cell is at given index.
729 * Insert a cell so that after insertion the cell is at given index.
726 *
730 *
727 * Similar to insert_above, but index parameter is mandatory
731 * Similar to insert_above, but index parameter is mandatory
728 *
732 *
729 * Index will be brought back into the accissible range [0,n]
733 * Index will be brought back into the accissible range [0,n]
730 *
734 *
731 * @method insert_cell_at_index
735 * @method insert_cell_at_index
732 * @param type {string} in ['code','markdown','heading']
736 * @param type {string} in ['code','markdown','heading']
733 * @param [index] {int} a valid index where to inser cell
737 * @param [index] {int} a valid index where to inser cell
734 *
738 *
735 * @return cell {cell|null} created cell or null
739 * @return cell {cell|null} created cell or null
736 **/
740 **/
737 Notebook.prototype.insert_cell_at_index = function(type, index){
741 Notebook.prototype.insert_cell_at_index = function(type, index){
738
742
739 var ncells = this.ncells();
743 var ncells = this.ncells();
740 index = Math.min(index,ncells);
744 index = Math.min(index,ncells);
741 index = Math.max(index,0);
745 index = Math.max(index,0);
742 var cell = null;
746 var cell = null;
743
747
744 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
748 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
745 if (type === 'code') {
749 if (type === 'code') {
746 cell = new IPython.CodeCell(this.kernel);
750 cell = new IPython.CodeCell(this.kernel);
747 cell.set_input_prompt();
751 cell.set_input_prompt();
748 } else if (type === 'markdown') {
752 } else if (type === 'markdown') {
749 cell = new IPython.MarkdownCell();
753 cell = new IPython.MarkdownCell();
750 } else if (type === 'raw') {
754 } else if (type === 'raw') {
751 cell = new IPython.RawCell();
755 cell = new IPython.RawCell();
752 } else if (type === 'heading') {
756 } else if (type === 'heading') {
753 cell = new IPython.HeadingCell();
757 cell = new IPython.HeadingCell();
754 }
758 }
755
759
756 if(this._insert_element_at_index(cell.element,index)) {
760 if(this._insert_element_at_index(cell.element,index)) {
757 cell.render();
761 cell.render();
758 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
762 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
759 cell.refresh();
763 cell.refresh();
760 // We used to select the cell after we refresh it, but there
764 // We used to select the cell after we refresh it, but there
761 // are now cases were this method is called where select is
765 // are now cases were this method is called where select is
762 // not appropriate. The selection logic should be handled by the
766 // not appropriate. The selection logic should be handled by the
763 // caller of the the top level insert_cell methods.
767 // caller of the the top level insert_cell methods.
764 this.set_dirty(true);
768 this.set_dirty(true);
765 }
769 }
766 }
770 }
767 return cell;
771 return cell;
768
772
769 };
773 };
770
774
771 /**
775 /**
772 * Insert an element at given cell index.
776 * Insert an element at given cell index.
773 *
777 *
774 * @method _insert_element_at_index
778 * @method _insert_element_at_index
775 * @param element {dom element} a cell element
779 * @param element {dom element} a cell element
776 * @param [index] {int} a valid index where to inser cell
780 * @param [index] {int} a valid index where to inser cell
777 * @private
781 * @private
778 *
782 *
779 * return true if everything whent fine.
783 * return true if everything whent fine.
780 **/
784 **/
781 Notebook.prototype._insert_element_at_index = function(element, index){
785 Notebook.prototype._insert_element_at_index = function(element, index){
782 if (element === undefined){
786 if (element === undefined){
783 return false;
787 return false;
784 }
788 }
785
789
786 var ncells = this.ncells();
790 var ncells = this.ncells();
787
791
788 if (ncells === 0) {
792 if (ncells === 0) {
789 // special case append if empty
793 // special case append if empty
790 this.element.find('div.end_space').before(element);
794 this.element.find('div.end_space').before(element);
791 } else if ( ncells === index ) {
795 } else if ( ncells === index ) {
792 // special case append it the end, but not empty
796 // special case append it the end, but not empty
793 this.get_cell_element(index-1).after(element);
797 this.get_cell_element(index-1).after(element);
794 } else if (this.is_valid_cell_index(index)) {
798 } else if (this.is_valid_cell_index(index)) {
795 // otherwise always somewhere to append to
799 // otherwise always somewhere to append to
796 this.get_cell_element(index).before(element);
800 this.get_cell_element(index).before(element);
797 } else {
801 } else {
798 return false;
802 return false;
799 }
803 }
800
804
801 if (this.undelete_index !== null && index <= this.undelete_index) {
805 if (this.undelete_index !== null && index <= this.undelete_index) {
802 this.undelete_index = this.undelete_index + 1;
806 this.undelete_index = this.undelete_index + 1;
803 this.set_dirty(true);
807 this.set_dirty(true);
804 }
808 }
805 return true;
809 return true;
806 };
810 };
807
811
808 /**
812 /**
809 * Insert a cell of given type above given index, or at top
813 * Insert a cell of given type above given index, or at top
810 * of notebook if index smaller than 0.
814 * of notebook if index smaller than 0.
811 *
815 *
812 * default index value is the one of currently selected cell
816 * default index value is the one of currently selected cell
813 *
817 *
814 * @method insert_cell_above
818 * @method insert_cell_above
815 * @param type {string} cell type
819 * @param type {string} cell type
816 * @param [index] {integer}
820 * @param [index] {integer}
817 *
821 *
818 * @return handle to created cell or null
822 * @return handle to created cell or null
819 **/
823 **/
820 Notebook.prototype.insert_cell_above = function (type, index) {
824 Notebook.prototype.insert_cell_above = function (type, index) {
821 index = this.index_or_selected(index);
825 index = this.index_or_selected(index);
822 return this.insert_cell_at_index(type, index);
826 return this.insert_cell_at_index(type, index);
823 };
827 };
824
828
825 /**
829 /**
826 * Insert a cell of given type below given index, or at bottom
830 * Insert a cell of given type below given index, or at bottom
827 * of notebook if index greater thatn number of cell
831 * of notebook if index greater thatn number of cell
828 *
832 *
829 * default index value is the one of currently selected cell
833 * default index value is the one of currently selected cell
830 *
834 *
831 * @method insert_cell_below
835 * @method insert_cell_below
832 * @param type {string} cell type
836 * @param type {string} cell type
833 * @param [index] {integer}
837 * @param [index] {integer}
834 *
838 *
835 * @return handle to created cell or null
839 * @return handle to created cell or null
836 *
840 *
837 **/
841 **/
838 Notebook.prototype.insert_cell_below = function (type, index) {
842 Notebook.prototype.insert_cell_below = function (type, index) {
839 index = this.index_or_selected(index);
843 index = this.index_or_selected(index);
840 return this.insert_cell_at_index(type, index+1);
844 return this.insert_cell_at_index(type, index+1);
841 };
845 };
842
846
843
847
844 /**
848 /**
845 * Insert cell at end of notebook
849 * Insert cell at end of notebook
846 *
850 *
847 * @method insert_cell_at_bottom
851 * @method insert_cell_at_bottom
848 * @param {String} type cell type
852 * @param {String} type cell type
849 *
853 *
850 * @return the added cell; or null
854 * @return the added cell; or null
851 **/
855 **/
852 Notebook.prototype.insert_cell_at_bottom = function (type){
856 Notebook.prototype.insert_cell_at_bottom = function (type){
853 var len = this.ncells();
857 var len = this.ncells();
854 return this.insert_cell_below(type,len-1);
858 return this.insert_cell_below(type,len-1);
855 };
859 };
856
860
857 /**
861 /**
858 * Turn a cell into a code cell.
862 * Turn a cell into a code cell.
859 *
863 *
860 * @method to_code
864 * @method to_code
861 * @param {Number} [index] A cell's index
865 * @param {Number} [index] A cell's index
862 */
866 */
863 Notebook.prototype.to_code = function (index) {
867 Notebook.prototype.to_code = function (index) {
864 var i = this.index_or_selected(index);
868 var i = this.index_or_selected(index);
865 if (this.is_valid_cell_index(i)) {
869 if (this.is_valid_cell_index(i)) {
866 var source_element = this.get_cell_element(i);
870 var source_element = this.get_cell_element(i);
867 var source_cell = source_element.data("cell");
871 var source_cell = source_element.data("cell");
868 if (!(source_cell instanceof IPython.CodeCell)) {
872 if (!(source_cell instanceof IPython.CodeCell)) {
869 var target_cell = this.insert_cell_below('code',i);
873 var target_cell = this.insert_cell_below('code',i);
870 var text = source_cell.get_text();
874 var text = source_cell.get_text();
871 if (text === source_cell.placeholder) {
875 if (text === source_cell.placeholder) {
872 text = '';
876 text = '';
873 }
877 }
874 target_cell.set_text(text);
878 target_cell.set_text(text);
875 // make this value the starting point, so that we can only undo
879 // make this value the starting point, so that we can only undo
876 // to this state, instead of a blank cell
880 // to this state, instead of a blank cell
877 target_cell.code_mirror.clearHistory();
881 target_cell.code_mirror.clearHistory();
878 source_element.remove();
882 source_element.remove();
879 this.select(i);
883 this.select(i);
880 this.set_dirty(true);
884 this.set_dirty(true);
881 }
885 }
882 }
886 }
883 };
887 };
884
888
885 /**
889 /**
886 * Turn a cell into a Markdown cell.
890 * Turn a cell into a Markdown cell.
887 *
891 *
888 * @method to_markdown
892 * @method to_markdown
889 * @param {Number} [index] A cell's index
893 * @param {Number} [index] A cell's index
890 */
894 */
891 Notebook.prototype.to_markdown = function (index) {
895 Notebook.prototype.to_markdown = function (index) {
892 var i = this.index_or_selected(index);
896 var i = this.index_or_selected(index);
893 if (this.is_valid_cell_index(i)) {
897 if (this.is_valid_cell_index(i)) {
894 var source_element = this.get_cell_element(i);
898 var source_element = this.get_cell_element(i);
895 var source_cell = source_element.data("cell");
899 var source_cell = source_element.data("cell");
896 if (!(source_cell instanceof IPython.MarkdownCell)) {
900 if (!(source_cell instanceof IPython.MarkdownCell)) {
897 var target_cell = this.insert_cell_below('markdown',i);
901 var target_cell = this.insert_cell_below('markdown',i);
898 var text = source_cell.get_text();
902 var text = source_cell.get_text();
899 if (text === source_cell.placeholder) {
903 if (text === source_cell.placeholder) {
900 text = '';
904 text = '';
901 }
905 }
902 // We must show the editor before setting its contents
906 // We must show the editor before setting its contents
903 target_cell.unrender();
907 target_cell.unrender();
904 target_cell.set_text(text);
908 target_cell.set_text(text);
905 // make this value the starting point, so that we can only undo
909 // make this value the starting point, so that we can only undo
906 // to this state, instead of a blank cell
910 // to this state, instead of a blank cell
907 target_cell.code_mirror.clearHistory();
911 target_cell.code_mirror.clearHistory();
908 source_element.remove();
912 source_element.remove();
909 this.select(i);
913 this.select(i);
910 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
914 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
911 target_cell.render();
915 target_cell.render();
912 }
916 }
913 this.set_dirty(true);
917 this.set_dirty(true);
914 }
918 }
915 }
919 }
916 };
920 };
917
921
918 /**
922 /**
919 * Turn a cell into a raw text cell.
923 * Turn a cell into a raw text cell.
920 *
924 *
921 * @method to_raw
925 * @method to_raw
922 * @param {Number} [index] A cell's index
926 * @param {Number} [index] A cell's index
923 */
927 */
924 Notebook.prototype.to_raw = function (index) {
928 Notebook.prototype.to_raw = function (index) {
925 var i = this.index_or_selected(index);
929 var i = this.index_or_selected(index);
926 if (this.is_valid_cell_index(i)) {
930 if (this.is_valid_cell_index(i)) {
927 var source_element = this.get_cell_element(i);
931 var source_element = this.get_cell_element(i);
928 var source_cell = source_element.data("cell");
932 var source_cell = source_element.data("cell");
929 var target_cell = null;
933 var target_cell = null;
930 if (!(source_cell instanceof IPython.RawCell)) {
934 if (!(source_cell instanceof IPython.RawCell)) {
931 target_cell = this.insert_cell_below('raw',i);
935 target_cell = this.insert_cell_below('raw',i);
932 var text = source_cell.get_text();
936 var text = source_cell.get_text();
933 if (text === source_cell.placeholder) {
937 if (text === source_cell.placeholder) {
934 text = '';
938 text = '';
935 }
939 }
936 // We must show the editor before setting its contents
940 // We must show the editor before setting its contents
937 target_cell.unrender();
941 target_cell.unrender();
938 target_cell.set_text(text);
942 target_cell.set_text(text);
939 // make this value the starting point, so that we can only undo
943 // make this value the starting point, so that we can only undo
940 // to this state, instead of a blank cell
944 // to this state, instead of a blank cell
941 target_cell.code_mirror.clearHistory();
945 target_cell.code_mirror.clearHistory();
942 source_element.remove();
946 source_element.remove();
943 this.select(i);
947 this.select(i);
944 this.set_dirty(true);
948 this.set_dirty(true);
945 }
949 }
946 }
950 }
947 };
951 };
948
952
949 /**
953 /**
950 * Turn a cell into a heading cell.
954 * Turn a cell into a heading cell.
951 *
955 *
952 * @method to_heading
956 * @method to_heading
953 * @param {Number} [index] A cell's index
957 * @param {Number} [index] A cell's index
954 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
958 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
955 */
959 */
956 Notebook.prototype.to_heading = function (index, level) {
960 Notebook.prototype.to_heading = function (index, level) {
957 level = level || 1;
961 level = level || 1;
958 var i = this.index_or_selected(index);
962 var i = this.index_or_selected(index);
959 if (this.is_valid_cell_index(i)) {
963 if (this.is_valid_cell_index(i)) {
960 var source_element = this.get_cell_element(i);
964 var source_element = this.get_cell_element(i);
961 var source_cell = source_element.data("cell");
965 var source_cell = source_element.data("cell");
962 var target_cell = null;
966 var target_cell = null;
963 if (source_cell instanceof IPython.HeadingCell) {
967 if (source_cell instanceof IPython.HeadingCell) {
964 source_cell.set_level(level);
968 source_cell.set_level(level);
965 } else {
969 } else {
966 target_cell = this.insert_cell_below('heading',i);
970 target_cell = this.insert_cell_below('heading',i);
967 var text = source_cell.get_text();
971 var text = source_cell.get_text();
968 if (text === source_cell.placeholder) {
972 if (text === source_cell.placeholder) {
969 text = '';
973 text = '';
970 }
974 }
971 // We must show the editor before setting its contents
975 // We must show the editor before setting its contents
972 target_cell.set_level(level);
976 target_cell.set_level(level);
973 target_cell.unrender();
977 target_cell.unrender();
974 target_cell.set_text(text);
978 target_cell.set_text(text);
975 // make this value the starting point, so that we can only undo
979 // make this value the starting point, so that we can only undo
976 // to this state, instead of a blank cell
980 // to this state, instead of a blank cell
977 target_cell.code_mirror.clearHistory();
981 target_cell.code_mirror.clearHistory();
978 source_element.remove();
982 source_element.remove();
979 this.select(i);
983 this.select(i);
980 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
984 if ((source_cell instanceof IPython.TextCell) && source_cell.rendered) {
981 target_cell.render();
985 target_cell.render();
982 }
986 }
983 }
987 }
984 this.set_dirty(true);
988 this.set_dirty(true);
985 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
989 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
986 {'cell_type':'heading',level:level}
990 {'cell_type':'heading',level:level}
987 );
991 );
988 }
992 }
989 };
993 };
990
994
991
995
992 // Cut/Copy/Paste
996 // Cut/Copy/Paste
993
997
994 /**
998 /**
995 * Enable UI elements for pasting cells.
999 * Enable UI elements for pasting cells.
996 *
1000 *
997 * @method enable_paste
1001 * @method enable_paste
998 */
1002 */
999 Notebook.prototype.enable_paste = function () {
1003 Notebook.prototype.enable_paste = function () {
1000 var that = this;
1004 var that = this;
1001 if (!this.paste_enabled) {
1005 if (!this.paste_enabled) {
1002 $('#paste_cell_replace').removeClass('disabled')
1006 $('#paste_cell_replace').removeClass('disabled')
1003 .on('click', function () {that.paste_cell_replace();});
1007 .on('click', function () {that.paste_cell_replace();});
1004 $('#paste_cell_above').removeClass('disabled')
1008 $('#paste_cell_above').removeClass('disabled')
1005 .on('click', function () {that.paste_cell_above();});
1009 .on('click', function () {that.paste_cell_above();});
1006 $('#paste_cell_below').removeClass('disabled')
1010 $('#paste_cell_below').removeClass('disabled')
1007 .on('click', function () {that.paste_cell_below();});
1011 .on('click', function () {that.paste_cell_below();});
1008 this.paste_enabled = true;
1012 this.paste_enabled = true;
1009 }
1013 }
1010 };
1014 };
1011
1015
1012 /**
1016 /**
1013 * Disable UI elements for pasting cells.
1017 * Disable UI elements for pasting cells.
1014 *
1018 *
1015 * @method disable_paste
1019 * @method disable_paste
1016 */
1020 */
1017 Notebook.prototype.disable_paste = function () {
1021 Notebook.prototype.disable_paste = function () {
1018 if (this.paste_enabled) {
1022 if (this.paste_enabled) {
1019 $('#paste_cell_replace').addClass('disabled').off('click');
1023 $('#paste_cell_replace').addClass('disabled').off('click');
1020 $('#paste_cell_above').addClass('disabled').off('click');
1024 $('#paste_cell_above').addClass('disabled').off('click');
1021 $('#paste_cell_below').addClass('disabled').off('click');
1025 $('#paste_cell_below').addClass('disabled').off('click');
1022 this.paste_enabled = false;
1026 this.paste_enabled = false;
1023 }
1027 }
1024 };
1028 };
1025
1029
1026 /**
1030 /**
1027 * Cut a cell.
1031 * Cut a cell.
1028 *
1032 *
1029 * @method cut_cell
1033 * @method cut_cell
1030 */
1034 */
1031 Notebook.prototype.cut_cell = function () {
1035 Notebook.prototype.cut_cell = function () {
1032 this.copy_cell();
1036 this.copy_cell();
1033 this.delete_cell();
1037 this.delete_cell();
1034 };
1038 };
1035
1039
1036 /**
1040 /**
1037 * Copy a cell.
1041 * Copy a cell.
1038 *
1042 *
1039 * @method copy_cell
1043 * @method copy_cell
1040 */
1044 */
1041 Notebook.prototype.copy_cell = function () {
1045 Notebook.prototype.copy_cell = function () {
1042 var cell = this.get_selected_cell();
1046 var cell = this.get_selected_cell();
1043 this.clipboard = cell.toJSON();
1047 this.clipboard = cell.toJSON();
1044 this.enable_paste();
1048 this.enable_paste();
1045 };
1049 };
1046
1050
1047 /**
1051 /**
1048 * Replace the selected cell with a cell in the clipboard.
1052 * Replace the selected cell with a cell in the clipboard.
1049 *
1053 *
1050 * @method paste_cell_replace
1054 * @method paste_cell_replace
1051 */
1055 */
1052 Notebook.prototype.paste_cell_replace = function () {
1056 Notebook.prototype.paste_cell_replace = function () {
1053 if (this.clipboard !== null && this.paste_enabled) {
1057 if (this.clipboard !== null && this.paste_enabled) {
1054 var cell_data = this.clipboard;
1058 var cell_data = this.clipboard;
1055 var new_cell = this.insert_cell_above(cell_data.cell_type);
1059 var new_cell = this.insert_cell_above(cell_data.cell_type);
1056 new_cell.fromJSON(cell_data);
1060 new_cell.fromJSON(cell_data);
1057 var old_cell = this.get_next_cell(new_cell);
1061 var old_cell = this.get_next_cell(new_cell);
1058 this.delete_cell(this.find_cell_index(old_cell));
1062 this.delete_cell(this.find_cell_index(old_cell));
1059 this.select(this.find_cell_index(new_cell));
1063 this.select(this.find_cell_index(new_cell));
1060 }
1064 }
1061 };
1065 };
1062
1066
1063 /**
1067 /**
1064 * Paste a cell from the clipboard above the selected cell.
1068 * Paste a cell from the clipboard above the selected cell.
1065 *
1069 *
1066 * @method paste_cell_above
1070 * @method paste_cell_above
1067 */
1071 */
1068 Notebook.prototype.paste_cell_above = function () {
1072 Notebook.prototype.paste_cell_above = function () {
1069 if (this.clipboard !== null && this.paste_enabled) {
1073 if (this.clipboard !== null && this.paste_enabled) {
1070 var cell_data = this.clipboard;
1074 var cell_data = this.clipboard;
1071 var new_cell = this.insert_cell_above(cell_data.cell_type);
1075 var new_cell = this.insert_cell_above(cell_data.cell_type);
1072 new_cell.fromJSON(cell_data);
1076 new_cell.fromJSON(cell_data);
1073 new_cell.focus_cell();
1077 new_cell.focus_cell();
1074 }
1078 }
1075 };
1079 };
1076
1080
1077 /**
1081 /**
1078 * Paste a cell from the clipboard below the selected cell.
1082 * Paste a cell from the clipboard below the selected cell.
1079 *
1083 *
1080 * @method paste_cell_below
1084 * @method paste_cell_below
1081 */
1085 */
1082 Notebook.prototype.paste_cell_below = function () {
1086 Notebook.prototype.paste_cell_below = function () {
1083 if (this.clipboard !== null && this.paste_enabled) {
1087 if (this.clipboard !== null && this.paste_enabled) {
1084 var cell_data = this.clipboard;
1088 var cell_data = this.clipboard;
1085 var new_cell = this.insert_cell_below(cell_data.cell_type);
1089 var new_cell = this.insert_cell_below(cell_data.cell_type);
1086 new_cell.fromJSON(cell_data);
1090 new_cell.fromJSON(cell_data);
1087 new_cell.focus_cell();
1091 new_cell.focus_cell();
1088 }
1092 }
1089 };
1093 };
1090
1094
1091 // Split/merge
1095 // Split/merge
1092
1096
1093 /**
1097 /**
1094 * Split the selected cell into two, at the cursor.
1098 * Split the selected cell into two, at the cursor.
1095 *
1099 *
1096 * @method split_cell
1100 * @method split_cell
1097 */
1101 */
1098 Notebook.prototype.split_cell = function () {
1102 Notebook.prototype.split_cell = function () {
1099 var mdc = IPython.MarkdownCell;
1103 var mdc = IPython.MarkdownCell;
1100 var rc = IPython.RawCell;
1104 var rc = IPython.RawCell;
1101 var cell = this.get_selected_cell();
1105 var cell = this.get_selected_cell();
1102 if (cell.is_splittable()) {
1106 if (cell.is_splittable()) {
1103 var texta = cell.get_pre_cursor();
1107 var texta = cell.get_pre_cursor();
1104 var textb = cell.get_post_cursor();
1108 var textb = cell.get_post_cursor();
1105 if (cell instanceof IPython.CodeCell) {
1109 if (cell instanceof IPython.CodeCell) {
1106 // In this case the operations keep the notebook in its existing mode
1110 // In this case the operations keep the notebook in its existing mode
1107 // so we don't need to do any post-op mode changes.
1111 // so we don't need to do any post-op mode changes.
1108 cell.set_text(textb);
1112 cell.set_text(textb);
1109 var new_cell = this.insert_cell_above('code');
1113 var new_cell = this.insert_cell_above('code');
1110 new_cell.set_text(texta);
1114 new_cell.set_text(texta);
1111 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1115 } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) {
1112 // We know cell is !rendered so we can use set_text.
1116 // We know cell is !rendered so we can use set_text.
1113 cell.set_text(textb);
1117 cell.set_text(textb);
1114 var new_cell = this.insert_cell_above(cell.cell_type);
1118 var new_cell = this.insert_cell_above(cell.cell_type);
1115 // Unrender the new cell so we can call set_text.
1119 // Unrender the new cell so we can call set_text.
1116 new_cell.unrender();
1120 new_cell.unrender();
1117 new_cell.set_text(texta);
1121 new_cell.set_text(texta);
1118 }
1122 }
1119 }
1123 }
1120 };
1124 };
1121
1125
1122 /**
1126 /**
1123 * Combine the selected cell into the cell above it.
1127 * Combine the selected cell into the cell above it.
1124 *
1128 *
1125 * @method merge_cell_above
1129 * @method merge_cell_above
1126 */
1130 */
1127 Notebook.prototype.merge_cell_above = function () {
1131 Notebook.prototype.merge_cell_above = function () {
1128 var mdc = IPython.MarkdownCell;
1132 var mdc = IPython.MarkdownCell;
1129 var rc = IPython.RawCell;
1133 var rc = IPython.RawCell;
1130 var index = this.get_selected_index();
1134 var index = this.get_selected_index();
1131 var cell = this.get_cell(index);
1135 var cell = this.get_cell(index);
1132 var render = cell.rendered;
1136 var render = cell.rendered;
1133 if (!cell.is_mergeable()) {
1137 if (!cell.is_mergeable()) {
1134 return;
1138 return;
1135 }
1139 }
1136 if (index > 0) {
1140 if (index > 0) {
1137 var upper_cell = this.get_cell(index-1);
1141 var upper_cell = this.get_cell(index-1);
1138 if (!upper_cell.is_mergeable()) {
1142 if (!upper_cell.is_mergeable()) {
1139 return;
1143 return;
1140 }
1144 }
1141 var upper_text = upper_cell.get_text();
1145 var upper_text = upper_cell.get_text();
1142 var text = cell.get_text();
1146 var text = cell.get_text();
1143 if (cell instanceof IPython.CodeCell) {
1147 if (cell instanceof IPython.CodeCell) {
1144 cell.set_text(upper_text+'\n'+text);
1148 cell.set_text(upper_text+'\n'+text);
1145 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1149 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1146 cell.unrender(); // Must unrender before we set_text.
1150 cell.unrender(); // Must unrender before we set_text.
1147 cell.set_text(upper_text+'\n\n'+text);
1151 cell.set_text(upper_text+'\n\n'+text);
1148 if (render) {
1152 if (render) {
1149 // The rendered state of the final cell should match
1153 // The rendered state of the final cell should match
1150 // that of the original selected cell;
1154 // that of the original selected cell;
1151 cell.render();
1155 cell.render();
1152 }
1156 }
1153 }
1157 }
1154 this.delete_cell(index-1);
1158 this.delete_cell(index-1);
1155 this.select(this.find_cell_index(cell));
1159 this.select(this.find_cell_index(cell));
1156 }
1160 }
1157 };
1161 };
1158
1162
1159 /**
1163 /**
1160 * Combine the selected cell into the cell below it.
1164 * Combine the selected cell into the cell below it.
1161 *
1165 *
1162 * @method merge_cell_below
1166 * @method merge_cell_below
1163 */
1167 */
1164 Notebook.prototype.merge_cell_below = function () {
1168 Notebook.prototype.merge_cell_below = function () {
1165 var mdc = IPython.MarkdownCell;
1169 var mdc = IPython.MarkdownCell;
1166 var rc = IPython.RawCell;
1170 var rc = IPython.RawCell;
1167 var index = this.get_selected_index();
1171 var index = this.get_selected_index();
1168 var cell = this.get_cell(index);
1172 var cell = this.get_cell(index);
1169 var render = cell.rendered;
1173 var render = cell.rendered;
1170 if (!cell.is_mergeable()) {
1174 if (!cell.is_mergeable()) {
1171 return;
1175 return;
1172 }
1176 }
1173 if (index < this.ncells()-1) {
1177 if (index < this.ncells()-1) {
1174 var lower_cell = this.get_cell(index+1);
1178 var lower_cell = this.get_cell(index+1);
1175 if (!lower_cell.is_mergeable()) {
1179 if (!lower_cell.is_mergeable()) {
1176 return;
1180 return;
1177 }
1181 }
1178 var lower_text = lower_cell.get_text();
1182 var lower_text = lower_cell.get_text();
1179 var text = cell.get_text();
1183 var text = cell.get_text();
1180 if (cell instanceof IPython.CodeCell) {
1184 if (cell instanceof IPython.CodeCell) {
1181 cell.set_text(text+'\n'+lower_text);
1185 cell.set_text(text+'\n'+lower_text);
1182 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1186 } else if ((cell instanceof mdc) || (cell instanceof rc)) {
1183 cell.unrender(); // Must unrender before we set_text.
1187 cell.unrender(); // Must unrender before we set_text.
1184 cell.set_text(text+'\n\n'+lower_text);
1188 cell.set_text(text+'\n\n'+lower_text);
1185 if (render) {
1189 if (render) {
1186 // The rendered state of the final cell should match
1190 // The rendered state of the final cell should match
1187 // that of the original selected cell;
1191 // that of the original selected cell;
1188 cell.render();
1192 cell.render();
1189 }
1193 }
1190 }
1194 }
1191 this.delete_cell(index+1);
1195 this.delete_cell(index+1);
1192 this.select(this.find_cell_index(cell));
1196 this.select(this.find_cell_index(cell));
1193 }
1197 }
1194 };
1198 };
1195
1199
1196
1200
1197 // Cell collapsing and output clearing
1201 // Cell collapsing and output clearing
1198
1202
1199 /**
1203 /**
1200 * Hide a cell's output.
1204 * Hide a cell's output.
1201 *
1205 *
1202 * @method collapse_output
1206 * @method collapse_output
1203 * @param {Number} index A cell's numeric index
1207 * @param {Number} index A cell's numeric index
1204 */
1208 */
1205 Notebook.prototype.collapse_output = function (index) {
1209 Notebook.prototype.collapse_output = function (index) {
1206 var i = this.index_or_selected(index);
1210 var i = this.index_or_selected(index);
1207 var cell = this.get_cell(i);
1211 var cell = this.get_cell(i);
1208 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1212 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1209 cell.collapse_output();
1213 cell.collapse_output();
1210 this.set_dirty(true);
1214 this.set_dirty(true);
1211 }
1215 }
1212 };
1216 };
1213
1217
1214 /**
1218 /**
1215 * Hide each code cell's output area.
1219 * Hide each code cell's output area.
1216 *
1220 *
1217 * @method collapse_all_output
1221 * @method collapse_all_output
1218 */
1222 */
1219 Notebook.prototype.collapse_all_output = function () {
1223 Notebook.prototype.collapse_all_output = function () {
1220 $.map(this.get_cells(), function (cell, i) {
1224 $.map(this.get_cells(), function (cell, i) {
1221 if (cell instanceof IPython.CodeCell) {
1225 if (cell instanceof IPython.CodeCell) {
1222 cell.collapse_output();
1226 cell.collapse_output();
1223 }
1227 }
1224 });
1228 });
1225 // this should not be set if the `collapse` key is removed from nbformat
1229 // this should not be set if the `collapse` key is removed from nbformat
1226 this.set_dirty(true);
1230 this.set_dirty(true);
1227 };
1231 };
1228
1232
1229 /**
1233 /**
1230 * Show a cell's output.
1234 * Show a cell's output.
1231 *
1235 *
1232 * @method expand_output
1236 * @method expand_output
1233 * @param {Number} index A cell's numeric index
1237 * @param {Number} index A cell's numeric index
1234 */
1238 */
1235 Notebook.prototype.expand_output = function (index) {
1239 Notebook.prototype.expand_output = function (index) {
1236 var i = this.index_or_selected(index);
1240 var i = this.index_or_selected(index);
1237 var cell = this.get_cell(i);
1241 var cell = this.get_cell(i);
1238 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1242 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1239 cell.expand_output();
1243 cell.expand_output();
1240 this.set_dirty(true);
1244 this.set_dirty(true);
1241 }
1245 }
1242 };
1246 };
1243
1247
1244 /**
1248 /**
1245 * Expand each code cell's output area, and remove scrollbars.
1249 * Expand each code cell's output area, and remove scrollbars.
1246 *
1250 *
1247 * @method expand_all_output
1251 * @method expand_all_output
1248 */
1252 */
1249 Notebook.prototype.expand_all_output = function () {
1253 Notebook.prototype.expand_all_output = function () {
1250 $.map(this.get_cells(), function (cell, i) {
1254 $.map(this.get_cells(), function (cell, i) {
1251 if (cell instanceof IPython.CodeCell) {
1255 if (cell instanceof IPython.CodeCell) {
1252 cell.expand_output();
1256 cell.expand_output();
1253 }
1257 }
1254 });
1258 });
1255 // this should not be set if the `collapse` key is removed from nbformat
1259 // this should not be set if the `collapse` key is removed from nbformat
1256 this.set_dirty(true);
1260 this.set_dirty(true);
1257 };
1261 };
1258
1262
1259 /**
1263 /**
1260 * Clear the selected CodeCell's output area.
1264 * Clear the selected CodeCell's output area.
1261 *
1265 *
1262 * @method clear_output
1266 * @method clear_output
1263 * @param {Number} index A cell's numeric index
1267 * @param {Number} index A cell's numeric index
1264 */
1268 */
1265 Notebook.prototype.clear_output = function (index) {
1269 Notebook.prototype.clear_output = function (index) {
1266 var i = this.index_or_selected(index);
1270 var i = this.index_or_selected(index);
1267 var cell = this.get_cell(i);
1271 var cell = this.get_cell(i);
1268 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1272 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1269 cell.clear_output();
1273 cell.clear_output();
1270 this.set_dirty(true);
1274 this.set_dirty(true);
1271 }
1275 }
1272 };
1276 };
1273
1277
1274 /**
1278 /**
1275 * Clear each code cell's output area.
1279 * Clear each code cell's output area.
1276 *
1280 *
1277 * @method clear_all_output
1281 * @method clear_all_output
1278 */
1282 */
1279 Notebook.prototype.clear_all_output = function () {
1283 Notebook.prototype.clear_all_output = function () {
1280 $.map(this.get_cells(), function (cell, i) {
1284 $.map(this.get_cells(), function (cell, i) {
1281 if (cell instanceof IPython.CodeCell) {
1285 if (cell instanceof IPython.CodeCell) {
1282 cell.clear_output();
1286 cell.clear_output();
1283 }
1287 }
1284 });
1288 });
1285 this.set_dirty(true);
1289 this.set_dirty(true);
1286 };
1290 };
1287
1291
1288 /**
1292 /**
1289 * Scroll the selected CodeCell's output area.
1293 * Scroll the selected CodeCell's output area.
1290 *
1294 *
1291 * @method scroll_output
1295 * @method scroll_output
1292 * @param {Number} index A cell's numeric index
1296 * @param {Number} index A cell's numeric index
1293 */
1297 */
1294 Notebook.prototype.scroll_output = function (index) {
1298 Notebook.prototype.scroll_output = function (index) {
1295 var i = this.index_or_selected(index);
1299 var i = this.index_or_selected(index);
1296 var cell = this.get_cell(i);
1300 var cell = this.get_cell(i);
1297 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1301 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1298 cell.scroll_output();
1302 cell.scroll_output();
1299 this.set_dirty(true);
1303 this.set_dirty(true);
1300 }
1304 }
1301 };
1305 };
1302
1306
1303 /**
1307 /**
1304 * Expand each code cell's output area, and add a scrollbar for long output.
1308 * Expand each code cell's output area, and add a scrollbar for long output.
1305 *
1309 *
1306 * @method scroll_all_output
1310 * @method scroll_all_output
1307 */
1311 */
1308 Notebook.prototype.scroll_all_output = function () {
1312 Notebook.prototype.scroll_all_output = function () {
1309 $.map(this.get_cells(), function (cell, i) {
1313 $.map(this.get_cells(), function (cell, i) {
1310 if (cell instanceof IPython.CodeCell) {
1314 if (cell instanceof IPython.CodeCell) {
1311 cell.scroll_output();
1315 cell.scroll_output();
1312 }
1316 }
1313 });
1317 });
1314 // this should not be set if the `collapse` key is removed from nbformat
1318 // this should not be set if the `collapse` key is removed from nbformat
1315 this.set_dirty(true);
1319 this.set_dirty(true);
1316 };
1320 };
1317
1321
1318 /** Toggle whether a cell's output is collapsed or expanded.
1322 /** Toggle whether a cell's output is collapsed or expanded.
1319 *
1323 *
1320 * @method toggle_output
1324 * @method toggle_output
1321 * @param {Number} index A cell's numeric index
1325 * @param {Number} index A cell's numeric index
1322 */
1326 */
1323 Notebook.prototype.toggle_output = function (index) {
1327 Notebook.prototype.toggle_output = function (index) {
1324 var i = this.index_or_selected(index);
1328 var i = this.index_or_selected(index);
1325 var cell = this.get_cell(i);
1329 var cell = this.get_cell(i);
1326 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1330 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1327 cell.toggle_output();
1331 cell.toggle_output();
1328 this.set_dirty(true);
1332 this.set_dirty(true);
1329 }
1333 }
1330 };
1334 };
1331
1335
1332 /**
1336 /**
1333 * Hide/show the output of all cells.
1337 * Hide/show the output of all cells.
1334 *
1338 *
1335 * @method toggle_all_output
1339 * @method toggle_all_output
1336 */
1340 */
1337 Notebook.prototype.toggle_all_output = function () {
1341 Notebook.prototype.toggle_all_output = function () {
1338 $.map(this.get_cells(), function (cell, i) {
1342 $.map(this.get_cells(), function (cell, i) {
1339 if (cell instanceof IPython.CodeCell) {
1343 if (cell instanceof IPython.CodeCell) {
1340 cell.toggle_output();
1344 cell.toggle_output();
1341 }
1345 }
1342 });
1346 });
1343 // this should not be set if the `collapse` key is removed from nbformat
1347 // this should not be set if the `collapse` key is removed from nbformat
1344 this.set_dirty(true);
1348 this.set_dirty(true);
1345 };
1349 };
1346
1350
1347 /**
1351 /**
1348 * Toggle a scrollbar for long cell outputs.
1352 * Toggle a scrollbar for long cell outputs.
1349 *
1353 *
1350 * @method toggle_output_scroll
1354 * @method toggle_output_scroll
1351 * @param {Number} index A cell's numeric index
1355 * @param {Number} index A cell's numeric index
1352 */
1356 */
1353 Notebook.prototype.toggle_output_scroll = function (index) {
1357 Notebook.prototype.toggle_output_scroll = function (index) {
1354 var i = this.index_or_selected(index);
1358 var i = this.index_or_selected(index);
1355 var cell = this.get_cell(i);
1359 var cell = this.get_cell(i);
1356 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1360 if (cell !== null && (cell instanceof IPython.CodeCell)) {
1357 cell.toggle_output_scroll();
1361 cell.toggle_output_scroll();
1358 this.set_dirty(true);
1362 this.set_dirty(true);
1359 }
1363 }
1360 };
1364 };
1361
1365
1362 /**
1366 /**
1363 * Toggle the scrolling of long output on all cells.
1367 * Toggle the scrolling of long output on all cells.
1364 *
1368 *
1365 * @method toggle_all_output_scrolling
1369 * @method toggle_all_output_scrolling
1366 */
1370 */
1367 Notebook.prototype.toggle_all_output_scroll = function () {
1371 Notebook.prototype.toggle_all_output_scroll = function () {
1368 $.map(this.get_cells(), function (cell, i) {
1372 $.map(this.get_cells(), function (cell, i) {
1369 if (cell instanceof IPython.CodeCell) {
1373 if (cell instanceof IPython.CodeCell) {
1370 cell.toggle_output_scroll();
1374 cell.toggle_output_scroll();
1371 }
1375 }
1372 });
1376 });
1373 // this should not be set if the `collapse` key is removed from nbformat
1377 // this should not be set if the `collapse` key is removed from nbformat
1374 this.set_dirty(true);
1378 this.set_dirty(true);
1375 };
1379 };
1376
1380
1377 // Other cell functions: line numbers, ...
1381 // Other cell functions: line numbers, ...
1378
1382
1379 /**
1383 /**
1380 * Toggle line numbers in the selected cell's input area.
1384 * Toggle line numbers in the selected cell's input area.
1381 *
1385 *
1382 * @method cell_toggle_line_numbers
1386 * @method cell_toggle_line_numbers
1383 */
1387 */
1384 Notebook.prototype.cell_toggle_line_numbers = function() {
1388 Notebook.prototype.cell_toggle_line_numbers = function() {
1385 this.get_selected_cell().toggle_line_numbers();
1389 this.get_selected_cell().toggle_line_numbers();
1386 };
1390 };
1387
1391
1388 // Session related things
1392 // Session related things
1389
1393
1390 /**
1394 /**
1391 * Start a new session and set it on each code cell.
1395 * Start a new session and set it on each code cell.
1392 *
1396 *
1393 * @method start_session
1397 * @method start_session
1394 */
1398 */
1395 Notebook.prototype.start_session = function () {
1399 Notebook.prototype.start_session = function () {
1396 this.session = new IPython.Session(this, this.options);
1400 this.session = new IPython.Session(this, this.options);
1397 this.session.start($.proxy(this._session_started, this));
1401 this.session.start($.proxy(this._session_started, this));
1398 };
1402 };
1399
1403
1400
1404
1401 /**
1405 /**
1402 * Once a session is started, link the code cells to the kernel and pass the
1406 * Once a session is started, link the code cells to the kernel and pass the
1403 * comm manager to the widget manager
1407 * comm manager to the widget manager
1404 *
1408 *
1405 */
1409 */
1406 Notebook.prototype._session_started = function(){
1410 Notebook.prototype._session_started = function(){
1407 this.kernel = this.session.kernel;
1411 this.kernel = this.session.kernel;
1408 var ncells = this.ncells();
1412 var ncells = this.ncells();
1409 for (var i=0; i<ncells; i++) {
1413 for (var i=0; i<ncells; i++) {
1410 var cell = this.get_cell(i);
1414 var cell = this.get_cell(i);
1411 if (cell instanceof IPython.CodeCell) {
1415 if (cell instanceof IPython.CodeCell) {
1412 cell.set_kernel(this.session.kernel);
1416 cell.set_kernel(this.session.kernel);
1413 }
1417 }
1414 }
1418 }
1415 };
1419 };
1416
1420
1417 /**
1421 /**
1418 * Prompt the user to restart the IPython kernel.
1422 * Prompt the user to restart the IPython kernel.
1419 *
1423 *
1420 * @method restart_kernel
1424 * @method restart_kernel
1421 */
1425 */
1422 Notebook.prototype.restart_kernel = function () {
1426 Notebook.prototype.restart_kernel = function () {
1423 var that = this;
1427 var that = this;
1424 IPython.dialog.modal({
1428 IPython.dialog.modal({
1425 title : "Restart kernel or continue running?",
1429 title : "Restart kernel or continue running?",
1426 body : $("<p/>").text(
1430 body : $("<p/>").text(
1427 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1431 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1428 ),
1432 ),
1429 buttons : {
1433 buttons : {
1430 "Continue running" : {},
1434 "Continue running" : {},
1431 "Restart" : {
1435 "Restart" : {
1432 "class" : "btn-danger",
1436 "class" : "btn-danger",
1433 "click" : function() {
1437 "click" : function() {
1434 that.session.restart_kernel();
1438 that.session.restart_kernel();
1435 }
1439 }
1436 }
1440 }
1437 }
1441 }
1438 });
1442 });
1439 };
1443 };
1440
1444
1441 /**
1445 /**
1442 * Execute or render cell outputs and go into command mode.
1446 * Execute or render cell outputs and go into command mode.
1443 *
1447 *
1444 * @method execute_cell
1448 * @method execute_cell
1445 */
1449 */
1446 Notebook.prototype.execute_cell = function () {
1450 Notebook.prototype.execute_cell = function () {
1447 // mode = shift, ctrl, alt
1451 // mode = shift, ctrl, alt
1448 var cell = this.get_selected_cell();
1452 var cell = this.get_selected_cell();
1449 var cell_index = this.find_cell_index(cell);
1453 var cell_index = this.find_cell_index(cell);
1450
1454
1451 cell.execute();
1455 cell.execute();
1452 cell.focus_cell();
1456 cell.focus_cell();
1453 this.command_mode();
1457 this.command_mode();
1454 this.set_dirty(true);
1458 this.set_dirty(true);
1455 };
1459 };
1456
1460
1457 /**
1461 /**
1458 * Execute or render cell outputs and insert a new cell below.
1462 * Execute or render cell outputs and insert a new cell below.
1459 *
1463 *
1460 * @method execute_cell_and_insert_below
1464 * @method execute_cell_and_insert_below
1461 */
1465 */
1462 Notebook.prototype.execute_cell_and_insert_below = function () {
1466 Notebook.prototype.execute_cell_and_insert_below = function () {
1463 var cell = this.get_selected_cell();
1467 var cell = this.get_selected_cell();
1464 var cell_index = this.find_cell_index(cell);
1468 var cell_index = this.find_cell_index(cell);
1465
1469
1466 cell.execute();
1470 cell.execute();
1467
1471
1468 // If we are at the end always insert a new cell and return
1472 // If we are at the end always insert a new cell and return
1469 if (cell_index === (this.ncells()-1)) {
1473 if (cell_index === (this.ncells()-1)) {
1470 this.insert_cell_below('code');
1474 this.insert_cell_below('code');
1471 this.edit_mode(cell_index+1);
1475 this.edit_mode(cell_index+1);
1472 this.scroll_to_bottom();
1476 this.scroll_to_bottom();
1473 this.set_dirty(true);
1477 this.set_dirty(true);
1474 return;
1478 return;
1475 }
1479 }
1476
1480
1477 this.insert_cell_below('code');
1481 this.insert_cell_below('code');
1478 this.edit_mode(cell_index+1);
1482 this.edit_mode(cell_index+1);
1479 this.set_dirty(true);
1483 this.set_dirty(true);
1480 };
1484 };
1481
1485
1482 /**
1486 /**
1483 * Execute or render cell outputs and select the next cell.
1487 * Execute or render cell outputs and select the next cell.
1484 *
1488 *
1485 * @method execute_cell_and_select_below
1489 * @method execute_cell_and_select_below
1486 */
1490 */
1487 Notebook.prototype.execute_cell_and_select_below = function () {
1491 Notebook.prototype.execute_cell_and_select_below = function () {
1488
1492
1489 var cell = this.get_selected_cell();
1493 var cell = this.get_selected_cell();
1490 var cell_index = this.find_cell_index(cell);
1494 var cell_index = this.find_cell_index(cell);
1491
1495
1492 cell.execute();
1496 cell.execute();
1493
1497
1494 // If we are at the end always insert a new cell and return
1498 // If we are at the end always insert a new cell and return
1495 if (cell_index === (this.ncells()-1)) {
1499 if (cell_index === (this.ncells()-1)) {
1496 this.insert_cell_below('code');
1500 this.insert_cell_below('code');
1497 this.edit_mode(cell_index+1);
1501 this.edit_mode(cell_index+1);
1498 this.scroll_to_bottom();
1502 this.scroll_to_bottom();
1499 this.set_dirty(true);
1503 this.set_dirty(true);
1500 return;
1504 return;
1501 }
1505 }
1502
1506
1503 this.select(cell_index+1);
1507 this.select(cell_index+1);
1504 this.get_cell(cell_index+1).focus_cell();
1508 this.get_cell(cell_index+1).focus_cell();
1505 this.command_mode();
1509 this.command_mode();
1506 this.set_dirty(true);
1510 this.set_dirty(true);
1507 };
1511 };
1508
1512
1509 /**
1513 /**
1510 * Execute all cells below the selected cell.
1514 * Execute all cells below the selected cell.
1511 *
1515 *
1512 * @method execute_cells_below
1516 * @method execute_cells_below
1513 */
1517 */
1514 Notebook.prototype.execute_cells_below = function () {
1518 Notebook.prototype.execute_cells_below = function () {
1515 this.execute_cell_range(this.get_selected_index(), this.ncells());
1519 this.execute_cell_range(this.get_selected_index(), this.ncells());
1516 this.scroll_to_bottom();
1520 this.scroll_to_bottom();
1517 };
1521 };
1518
1522
1519 /**
1523 /**
1520 * Execute all cells above the selected cell.
1524 * Execute all cells above the selected cell.
1521 *
1525 *
1522 * @method execute_cells_above
1526 * @method execute_cells_above
1523 */
1527 */
1524 Notebook.prototype.execute_cells_above = function () {
1528 Notebook.prototype.execute_cells_above = function () {
1525 this.execute_cell_range(0, this.get_selected_index());
1529 this.execute_cell_range(0, this.get_selected_index());
1526 };
1530 };
1527
1531
1528 /**
1532 /**
1529 * Execute all cells.
1533 * Execute all cells.
1530 *
1534 *
1531 * @method execute_all_cells
1535 * @method execute_all_cells
1532 */
1536 */
1533 Notebook.prototype.execute_all_cells = function () {
1537 Notebook.prototype.execute_all_cells = function () {
1534 this.execute_cell_range(0, this.ncells());
1538 this.execute_cell_range(0, this.ncells());
1535 this.scroll_to_bottom();
1539 this.scroll_to_bottom();
1536 };
1540 };
1537
1541
1538 /**
1542 /**
1539 * Execute a contiguous range of cells.
1543 * Execute a contiguous range of cells.
1540 *
1544 *
1541 * @method execute_cell_range
1545 * @method execute_cell_range
1542 * @param {Number} start Index of the first cell to execute (inclusive)
1546 * @param {Number} start Index of the first cell to execute (inclusive)
1543 * @param {Number} end Index of the last cell to execute (exclusive)
1547 * @param {Number} end Index of the last cell to execute (exclusive)
1544 */
1548 */
1545 Notebook.prototype.execute_cell_range = function (start, end) {
1549 Notebook.prototype.execute_cell_range = function (start, end) {
1546 for (var i=start; i<end; i++) {
1550 for (var i=start; i<end; i++) {
1547 this.select(i);
1551 this.select(i);
1548 this.execute_cell();
1552 this.execute_cell();
1549 }
1553 }
1550 };
1554 };
1551
1555
1552 // Persistance and loading
1556 // Persistance and loading
1553
1557
1554 /**
1558 /**
1555 * Getter method for this notebook's name.
1559 * Getter method for this notebook's name.
1556 *
1560 *
1557 * @method get_notebook_name
1561 * @method get_notebook_name
1558 * @return {String} This notebook's name (excluding file extension)
1562 * @return {String} This notebook's name (excluding file extension)
1559 */
1563 */
1560 Notebook.prototype.get_notebook_name = function () {
1564 Notebook.prototype.get_notebook_name = function () {
1561 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1565 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1562 return nbname;
1566 return nbname;
1563 };
1567 };
1564
1568
1565 /**
1569 /**
1566 * Setter method for this notebook's name.
1570 * Setter method for this notebook's name.
1567 *
1571 *
1568 * @method set_notebook_name
1572 * @method set_notebook_name
1569 * @param {String} name A new name for this notebook
1573 * @param {String} name A new name for this notebook
1570 */
1574 */
1571 Notebook.prototype.set_notebook_name = function (name) {
1575 Notebook.prototype.set_notebook_name = function (name) {
1572 this.notebook_name = name;
1576 this.notebook_name = name;
1573 };
1577 };
1574
1578
1575 /**
1579 /**
1576 * Check that a notebook's name is valid.
1580 * Check that a notebook's name is valid.
1577 *
1581 *
1578 * @method test_notebook_name
1582 * @method test_notebook_name
1579 * @param {String} nbname A name for this notebook
1583 * @param {String} nbname A name for this notebook
1580 * @return {Boolean} True if the name is valid, false if invalid
1584 * @return {Boolean} True if the name is valid, false if invalid
1581 */
1585 */
1582 Notebook.prototype.test_notebook_name = function (nbname) {
1586 Notebook.prototype.test_notebook_name = function (nbname) {
1583 nbname = nbname || '';
1587 nbname = nbname || '';
1584 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1588 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1585 return true;
1589 return true;
1586 } else {
1590 } else {
1587 return false;
1591 return false;
1588 }
1592 }
1589 };
1593 };
1590
1594
1591 /**
1595 /**
1592 * Load a notebook from JSON (.ipynb).
1596 * Load a notebook from JSON (.ipynb).
1593 *
1597 *
1594 * This currently handles one worksheet: others are deleted.
1598 * This currently handles one worksheet: others are deleted.
1595 *
1599 *
1596 * @method fromJSON
1600 * @method fromJSON
1597 * @param {Object} data JSON representation of a notebook
1601 * @param {Object} data JSON representation of a notebook
1598 */
1602 */
1599 Notebook.prototype.fromJSON = function (data) {
1603 Notebook.prototype.fromJSON = function (data) {
1600 var content = data.content;
1604 var content = data.content;
1601 var ncells = this.ncells();
1605 var ncells = this.ncells();
1602 var i;
1606 var i;
1603 for (i=0; i<ncells; i++) {
1607 for (i=0; i<ncells; i++) {
1604 // Always delete cell 0 as they get renumbered as they are deleted.
1608 // Always delete cell 0 as they get renumbered as they are deleted.
1605 this.delete_cell(0);
1609 this.delete_cell(0);
1606 }
1610 }
1607 // Save the metadata and name.
1611 // Save the metadata and name.
1608 this.metadata = content.metadata;
1612 this.metadata = content.metadata;
1609 this.notebook_name = data.name;
1613 this.notebook_name = data.name;
1614 var trusted = true;
1610 // Only handle 1 worksheet for now.
1615 // Only handle 1 worksheet for now.
1611 var worksheet = content.worksheets[0];
1616 var worksheet = content.worksheets[0];
1612 if (worksheet !== undefined) {
1617 if (worksheet !== undefined) {
1613 if (worksheet.metadata) {
1618 if (worksheet.metadata) {
1614 this.worksheet_metadata = worksheet.metadata;
1619 this.worksheet_metadata = worksheet.metadata;
1615 }
1620 }
1616 var new_cells = worksheet.cells;
1621 var new_cells = worksheet.cells;
1617 ncells = new_cells.length;
1622 ncells = new_cells.length;
1618 var cell_data = null;
1623 var cell_data = null;
1619 var new_cell = null;
1624 var new_cell = null;
1620 for (i=0; i<ncells; i++) {
1625 for (i=0; i<ncells; i++) {
1621 cell_data = new_cells[i];
1626 cell_data = new_cells[i];
1622 // VERSIONHACK: plaintext -> raw
1627 // VERSIONHACK: plaintext -> raw
1623 // handle never-released plaintext name for raw cells
1628 // handle never-released plaintext name for raw cells
1624 if (cell_data.cell_type === 'plaintext'){
1629 if (cell_data.cell_type === 'plaintext'){
1625 cell_data.cell_type = 'raw';
1630 cell_data.cell_type = 'raw';
1626 }
1631 }
1627
1632
1628 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1633 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1629 new_cell.fromJSON(cell_data);
1634 new_cell.fromJSON(cell_data);
1635 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1636 trusted = false;
1637 }
1630 }
1638 }
1631 }
1639 }
1640 if (trusted != this.trusted) {
1641 this.trusted = trusted;
1642 $([IPython.events]).trigger("trust_changed.Notebook", trusted);
1643 }
1632 if (content.worksheets.length > 1) {
1644 if (content.worksheets.length > 1) {
1633 IPython.dialog.modal({
1645 IPython.dialog.modal({
1634 title : "Multiple worksheets",
1646 title : "Multiple worksheets",
1635 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1647 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1636 "but this version of IPython can only handle the first. " +
1648 "but this version of IPython can only handle the first. " +
1637 "If you save this notebook, worksheets after the first will be lost.",
1649 "If you save this notebook, worksheets after the first will be lost.",
1638 buttons : {
1650 buttons : {
1639 OK : {
1651 OK : {
1640 class : "btn-danger"
1652 class : "btn-danger"
1641 }
1653 }
1642 }
1654 }
1643 });
1655 });
1644 }
1656 }
1645 };
1657 };
1646
1658
1647 /**
1659 /**
1648 * Dump this notebook into a JSON-friendly object.
1660 * Dump this notebook into a JSON-friendly object.
1649 *
1661 *
1650 * @method toJSON
1662 * @method toJSON
1651 * @return {Object} A JSON-friendly representation of this notebook.
1663 * @return {Object} A JSON-friendly representation of this notebook.
1652 */
1664 */
1653 Notebook.prototype.toJSON = function () {
1665 Notebook.prototype.toJSON = function () {
1654 var cells = this.get_cells();
1666 var cells = this.get_cells();
1655 var ncells = cells.length;
1667 var ncells = cells.length;
1656 var cell_array = new Array(ncells);
1668 var cell_array = new Array(ncells);
1669 var trusted = true;
1657 for (var i=0; i<ncells; i++) {
1670 for (var i=0; i<ncells; i++) {
1658 cell_array[i] = cells[i].toJSON();
1671 var cell = cells[i];
1672 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1673 trusted = false;
1674 }
1675 cell_array[i] = cell.toJSON();
1659 }
1676 }
1660 var data = {
1677 var data = {
1661 // Only handle 1 worksheet for now.
1678 // Only handle 1 worksheet for now.
1662 worksheets : [{
1679 worksheets : [{
1663 cells: cell_array,
1680 cells: cell_array,
1664 metadata: this.worksheet_metadata
1681 metadata: this.worksheet_metadata
1665 }],
1682 }],
1666 metadata : this.metadata
1683 metadata : this.metadata
1667 };
1684 };
1685 if (trusted != this.trusted) {
1686 this.trusted = trusted;
1687 $([IPython.events]).trigger("trust_changed.Notebook", trusted);
1688 }
1668 return data;
1689 return data;
1669 };
1690 };
1670
1691
1671 /**
1692 /**
1672 * Start an autosave timer, for periodically saving the notebook.
1693 * Start an autosave timer, for periodically saving the notebook.
1673 *
1694 *
1674 * @method set_autosave_interval
1695 * @method set_autosave_interval
1675 * @param {Integer} interval the autosave interval in milliseconds
1696 * @param {Integer} interval the autosave interval in milliseconds
1676 */
1697 */
1677 Notebook.prototype.set_autosave_interval = function (interval) {
1698 Notebook.prototype.set_autosave_interval = function (interval) {
1678 var that = this;
1699 var that = this;
1679 // clear previous interval, so we don't get simultaneous timers
1700 // clear previous interval, so we don't get simultaneous timers
1680 if (this.autosave_timer) {
1701 if (this.autosave_timer) {
1681 clearInterval(this.autosave_timer);
1702 clearInterval(this.autosave_timer);
1682 }
1703 }
1683
1704
1684 this.autosave_interval = this.minimum_autosave_interval = interval;
1705 this.autosave_interval = this.minimum_autosave_interval = interval;
1685 if (interval) {
1706 if (interval) {
1686 this.autosave_timer = setInterval(function() {
1707 this.autosave_timer = setInterval(function() {
1687 if (that.dirty) {
1708 if (that.dirty) {
1688 that.save_notebook();
1709 that.save_notebook();
1689 }
1710 }
1690 }, interval);
1711 }, interval);
1691 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1712 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1692 } else {
1713 } else {
1693 this.autosave_timer = null;
1714 this.autosave_timer = null;
1694 $([IPython.events]).trigger("autosave_disabled.Notebook");
1715 $([IPython.events]).trigger("autosave_disabled.Notebook");
1695 }
1716 }
1696 };
1717 };
1697
1718
1698 /**
1719 /**
1699 * Save this notebook on the server.
1720 * Save this notebook on the server.
1700 *
1721 *
1701 * @method save_notebook
1722 * @method save_notebook
1702 */
1723 */
1703 Notebook.prototype.save_notebook = function (extra_settings) {
1724 Notebook.prototype.save_notebook = function (extra_settings) {
1704 // Create a JSON model to be sent to the server.
1725 // Create a JSON model to be sent to the server.
1705 var model = {};
1726 var model = {};
1706 model.name = this.notebook_name;
1727 model.name = this.notebook_name;
1707 model.path = this.notebook_path;
1728 model.path = this.notebook_path;
1708 model.content = this.toJSON();
1729 model.content = this.toJSON();
1709 model.content.nbformat = this.nbformat;
1730 model.content.nbformat = this.nbformat;
1710 model.content.nbformat_minor = this.nbformat_minor;
1731 model.content.nbformat_minor = this.nbformat_minor;
1711 // time the ajax call for autosave tuning purposes.
1732 // time the ajax call for autosave tuning purposes.
1712 var start = new Date().getTime();
1733 var start = new Date().getTime();
1713 // We do the call with settings so we can set cache to false.
1734 // We do the call with settings so we can set cache to false.
1714 var settings = {
1735 var settings = {
1715 processData : false,
1736 processData : false,
1716 cache : false,
1737 cache : false,
1717 type : "PUT",
1738 type : "PUT",
1718 data : JSON.stringify(model),
1739 data : JSON.stringify(model),
1719 headers : {'Content-Type': 'application/json'},
1740 headers : {'Content-Type': 'application/json'},
1720 success : $.proxy(this.save_notebook_success, this, start),
1741 success : $.proxy(this.save_notebook_success, this, start),
1721 error : $.proxy(this.save_notebook_error, this)
1742 error : $.proxy(this.save_notebook_error, this)
1722 };
1743 };
1723 if (extra_settings) {
1744 if (extra_settings) {
1724 for (var key in extra_settings) {
1745 for (var key in extra_settings) {
1725 settings[key] = extra_settings[key];
1746 settings[key] = extra_settings[key];
1726 }
1747 }
1727 }
1748 }
1728 $([IPython.events]).trigger('notebook_saving.Notebook');
1749 $([IPython.events]).trigger('notebook_saving.Notebook');
1729 var url = utils.url_join_encode(
1750 var url = utils.url_join_encode(
1730 this.base_url,
1751 this.base_url,
1731 'api/notebooks',
1752 'api/notebooks',
1732 this.notebook_path,
1753 this.notebook_path,
1733 this.notebook_name
1754 this.notebook_name
1734 );
1755 );
1735 $.ajax(url, settings);
1756 $.ajax(url, settings);
1736 };
1757 };
1737
1758
1738 /**
1759 /**
1739 * Success callback for saving a notebook.
1760 * Success callback for saving a notebook.
1740 *
1761 *
1741 * @method save_notebook_success
1762 * @method save_notebook_success
1742 * @param {Integer} start the time when the save request started
1763 * @param {Integer} start the time when the save request started
1743 * @param {Object} data JSON representation of a notebook
1764 * @param {Object} data JSON representation of a notebook
1744 * @param {String} status Description of response status
1765 * @param {String} status Description of response status
1745 * @param {jqXHR} xhr jQuery Ajax object
1766 * @param {jqXHR} xhr jQuery Ajax object
1746 */
1767 */
1747 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1768 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1748 this.set_dirty(false);
1769 this.set_dirty(false);
1749 $([IPython.events]).trigger('notebook_saved.Notebook');
1770 $([IPython.events]).trigger('notebook_saved.Notebook');
1750 this._update_autosave_interval(start);
1771 this._update_autosave_interval(start);
1751 if (this._checkpoint_after_save) {
1772 if (this._checkpoint_after_save) {
1752 this.create_checkpoint();
1773 this.create_checkpoint();
1753 this._checkpoint_after_save = false;
1774 this._checkpoint_after_save = false;
1754 }
1775 }
1755 };
1776 };
1756
1777
1757 /**
1778 /**
1758 * update the autosave interval based on how long the last save took
1779 * update the autosave interval based on how long the last save took
1759 *
1780 *
1760 * @method _update_autosave_interval
1781 * @method _update_autosave_interval
1761 * @param {Integer} timestamp when the save request started
1782 * @param {Integer} timestamp when the save request started
1762 */
1783 */
1763 Notebook.prototype._update_autosave_interval = function (start) {
1784 Notebook.prototype._update_autosave_interval = function (start) {
1764 var duration = (new Date().getTime() - start);
1785 var duration = (new Date().getTime() - start);
1765 if (this.autosave_interval) {
1786 if (this.autosave_interval) {
1766 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1787 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1767 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1788 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1768 // round to 10 seconds, otherwise we will be setting a new interval too often
1789 // round to 10 seconds, otherwise we will be setting a new interval too often
1769 interval = 10000 * Math.round(interval / 10000);
1790 interval = 10000 * Math.round(interval / 10000);
1770 // set new interval, if it's changed
1791 // set new interval, if it's changed
1771 if (interval != this.autosave_interval) {
1792 if (interval != this.autosave_interval) {
1772 this.set_autosave_interval(interval);
1793 this.set_autosave_interval(interval);
1773 }
1794 }
1774 }
1795 }
1775 };
1796 };
1776
1797
1777 /**
1798 /**
1778 * Failure callback for saving a notebook.
1799 * Failure callback for saving a notebook.
1779 *
1800 *
1780 * @method save_notebook_error
1801 * @method save_notebook_error
1781 * @param {jqXHR} xhr jQuery Ajax object
1802 * @param {jqXHR} xhr jQuery Ajax object
1782 * @param {String} status Description of response status
1803 * @param {String} status Description of response status
1783 * @param {String} error HTTP error message
1804 * @param {String} error HTTP error message
1784 */
1805 */
1785 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1806 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1786 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1807 $([IPython.events]).trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1787 };
1808 };
1788
1809
1810 /**
1811 * Explicitly trust the output of this notebook.
1812 *
1813 * @method trust_notebook
1814 */
1815 Notebook.prototype.trust_notebook = function (extra_settings) {
1816 var body = $("<div>").append($("<p>")
1817 .text("A trusted IPython notebook may execute hidden malicious code ")
1818 .append($("<strong>")
1819 .append(
1820 $("<em>").text("when you open it")
1821 )
1822 ).append(".").append(
1823 " Selecting trust will immediately reload this notebook in a trusted state."
1824 ).append(
1825 " For more information, see the "
1826 ).append($("<a>").attr("href", "http://ipython.org/security.html")
1827 .text("IPython security documentation")
1828 ).append(".")
1829 );
1830
1831 var nb = this;
1832 IPython.dialog.modal({
1833 title: "Trust this notebook?",
1834 body: body,
1835
1836 buttons: {
1837 Cancel : {},
1838 Trust : {
1839 class : "btn-danger",
1840 click : function () {
1841 var cells = nb.get_cells();
1842 for (var i = 0; i < cells.length; i++) {
1843 var cell = cells[i];
1844 if (cell.cell_type == 'code') {
1845 cell.output_area.trusted = true;
1846 }
1847 }
1848 $([IPython.events]).on('notebook_saved.Notebook', function () {
1849 window.location.reload();
1850 });
1851 nb.save_notebook();
1852 }
1853 }
1854 }
1855 });
1856 };
1857
1789 Notebook.prototype.new_notebook = function(){
1858 Notebook.prototype.new_notebook = function(){
1790 var path = this.notebook_path;
1859 var path = this.notebook_path;
1791 var base_url = this.base_url;
1860 var base_url = this.base_url;
1792 var settings = {
1861 var settings = {
1793 processData : false,
1862 processData : false,
1794 cache : false,
1863 cache : false,
1795 type : "POST",
1864 type : "POST",
1796 dataType : "json",
1865 dataType : "json",
1797 async : false,
1866 async : false,
1798 success : function (data, status, xhr){
1867 success : function (data, status, xhr){
1799 var notebook_name = data.name;
1868 var notebook_name = data.name;
1800 window.open(
1869 window.open(
1801 utils.url_join_encode(
1870 utils.url_join_encode(
1802 base_url,
1871 base_url,
1803 'notebooks',
1872 'notebooks',
1804 path,
1873 path,
1805 notebook_name
1874 notebook_name
1806 ),
1875 ),
1807 '_blank'
1876 '_blank'
1808 );
1877 );
1809 }
1878 }
1810 };
1879 };
1811 var url = utils.url_join_encode(
1880 var url = utils.url_join_encode(
1812 base_url,
1881 base_url,
1813 'api/notebooks',
1882 'api/notebooks',
1814 path
1883 path
1815 );
1884 );
1816 $.ajax(url,settings);
1885 $.ajax(url,settings);
1817 };
1886 };
1818
1887
1819
1888
1820 Notebook.prototype.copy_notebook = function(){
1889 Notebook.prototype.copy_notebook = function(){
1821 var path = this.notebook_path;
1890 var path = this.notebook_path;
1822 var base_url = this.base_url;
1891 var base_url = this.base_url;
1823 var settings = {
1892 var settings = {
1824 processData : false,
1893 processData : false,
1825 cache : false,
1894 cache : false,
1826 type : "POST",
1895 type : "POST",
1827 dataType : "json",
1896 dataType : "json",
1828 data : JSON.stringify({copy_from : this.notebook_name}),
1897 data : JSON.stringify({copy_from : this.notebook_name}),
1829 async : false,
1898 async : false,
1830 success : function (data, status, xhr) {
1899 success : function (data, status, xhr) {
1831 window.open(utils.url_join_encode(
1900 window.open(utils.url_join_encode(
1832 base_url,
1901 base_url,
1833 'notebooks',
1902 'notebooks',
1834 data.path,
1903 data.path,
1835 data.name
1904 data.name
1836 ), '_blank');
1905 ), '_blank');
1837 }
1906 }
1838 };
1907 };
1839 var url = utils.url_join_encode(
1908 var url = utils.url_join_encode(
1840 base_url,
1909 base_url,
1841 'api/notebooks',
1910 'api/notebooks',
1842 path
1911 path
1843 );
1912 );
1844 $.ajax(url,settings);
1913 $.ajax(url,settings);
1845 };
1914 };
1846
1915
1847 Notebook.prototype.rename = function (nbname) {
1916 Notebook.prototype.rename = function (nbname) {
1848 var that = this;
1917 var that = this;
1849 if (!nbname.match(/\.ipynb$/)) {
1918 if (!nbname.match(/\.ipynb$/)) {
1850 nbname = nbname + ".ipynb";
1919 nbname = nbname + ".ipynb";
1851 }
1920 }
1852 var data = {name: nbname};
1921 var data = {name: nbname};
1853 var settings = {
1922 var settings = {
1854 processData : false,
1923 processData : false,
1855 cache : false,
1924 cache : false,
1856 type : "PATCH",
1925 type : "PATCH",
1857 data : JSON.stringify(data),
1926 data : JSON.stringify(data),
1858 dataType: "json",
1927 dataType: "json",
1859 headers : {'Content-Type': 'application/json'},
1928 headers : {'Content-Type': 'application/json'},
1860 success : $.proxy(that.rename_success, this),
1929 success : $.proxy(that.rename_success, this),
1861 error : $.proxy(that.rename_error, this)
1930 error : $.proxy(that.rename_error, this)
1862 };
1931 };
1863 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1932 $([IPython.events]).trigger('rename_notebook.Notebook', data);
1864 var url = utils.url_join_encode(
1933 var url = utils.url_join_encode(
1865 this.base_url,
1934 this.base_url,
1866 'api/notebooks',
1935 'api/notebooks',
1867 this.notebook_path,
1936 this.notebook_path,
1868 this.notebook_name
1937 this.notebook_name
1869 );
1938 );
1870 $.ajax(url, settings);
1939 $.ajax(url, settings);
1871 };
1940 };
1872
1941
1873 Notebook.prototype.delete = function () {
1942 Notebook.prototype.delete = function () {
1874 var that = this;
1943 var that = this;
1875 var settings = {
1944 var settings = {
1876 processData : false,
1945 processData : false,
1877 cache : false,
1946 cache : false,
1878 type : "DELETE",
1947 type : "DELETE",
1879 dataType: "json",
1948 dataType: "json",
1880 };
1949 };
1881 var url = utils.url_join_encode(
1950 var url = utils.url_join_encode(
1882 this.base_url,
1951 this.base_url,
1883 'api/notebooks',
1952 'api/notebooks',
1884 this.notebook_path,
1953 this.notebook_path,
1885 this.notebook_name
1954 this.notebook_name
1886 );
1955 );
1887 $.ajax(url, settings);
1956 $.ajax(url, settings);
1888 };
1957 };
1889
1958
1890
1959
1891 Notebook.prototype.rename_success = function (json, status, xhr) {
1960 Notebook.prototype.rename_success = function (json, status, xhr) {
1892 var name = this.notebook_name = json.name;
1961 var name = this.notebook_name = json.name;
1893 var path = json.path;
1962 var path = json.path;
1894 this.session.rename_notebook(name, path);
1963 this.session.rename_notebook(name, path);
1895 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1964 $([IPython.events]).trigger('notebook_renamed.Notebook', json);
1896 };
1965 };
1897
1966
1898 Notebook.prototype.rename_error = function (xhr, status, error) {
1967 Notebook.prototype.rename_error = function (xhr, status, error) {
1899 var that = this;
1968 var that = this;
1900 var dialog = $('<div/>').append(
1969 var dialog = $('<div/>').append(
1901 $("<p/>").addClass("rename-message")
1970 $("<p/>").addClass("rename-message")
1902 .text('This notebook name already exists.')
1971 .text('This notebook name already exists.')
1903 );
1972 );
1904 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1973 $([IPython.events]).trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
1905 IPython.dialog.modal({
1974 IPython.dialog.modal({
1906 title: "Notebook Rename Error!",
1975 title: "Notebook Rename Error!",
1907 body: dialog,
1976 body: dialog,
1908 buttons : {
1977 buttons : {
1909 "Cancel": {},
1978 "Cancel": {},
1910 "OK": {
1979 "OK": {
1911 class: "btn-primary",
1980 class: "btn-primary",
1912 click: function () {
1981 click: function () {
1913 IPython.save_widget.rename_notebook();
1982 IPython.save_widget.rename_notebook();
1914 }}
1983 }}
1915 },
1984 },
1916 open : function (event, ui) {
1985 open : function (event, ui) {
1917 var that = $(this);
1986 var that = $(this);
1918 // Upon ENTER, click the OK button.
1987 // Upon ENTER, click the OK button.
1919 that.find('input[type="text"]').keydown(function (event, ui) {
1988 that.find('input[type="text"]').keydown(function (event, ui) {
1920 if (event.which === IPython.keyboard.keycodes.enter) {
1989 if (event.which === IPython.keyboard.keycodes.enter) {
1921 that.find('.btn-primary').first().click();
1990 that.find('.btn-primary').first().click();
1922 }
1991 }
1923 });
1992 });
1924 that.find('input[type="text"]').focus();
1993 that.find('input[type="text"]').focus();
1925 }
1994 }
1926 });
1995 });
1927 };
1996 };
1928
1997
1929 /**
1998 /**
1930 * Request a notebook's data from the server.
1999 * Request a notebook's data from the server.
1931 *
2000 *
1932 * @method load_notebook
2001 * @method load_notebook
1933 * @param {String} notebook_name and path A notebook to load
2002 * @param {String} notebook_name and path A notebook to load
1934 */
2003 */
1935 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2004 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
1936 var that = this;
2005 var that = this;
1937 this.notebook_name = notebook_name;
2006 this.notebook_name = notebook_name;
1938 this.notebook_path = notebook_path;
2007 this.notebook_path = notebook_path;
1939 // We do the call with settings so we can set cache to false.
2008 // We do the call with settings so we can set cache to false.
1940 var settings = {
2009 var settings = {
1941 processData : false,
2010 processData : false,
1942 cache : false,
2011 cache : false,
1943 type : "GET",
2012 type : "GET",
1944 dataType : "json",
2013 dataType : "json",
1945 success : $.proxy(this.load_notebook_success,this),
2014 success : $.proxy(this.load_notebook_success,this),
1946 error : $.proxy(this.load_notebook_error,this),
2015 error : $.proxy(this.load_notebook_error,this),
1947 };
2016 };
1948 $([IPython.events]).trigger('notebook_loading.Notebook');
2017 $([IPython.events]).trigger('notebook_loading.Notebook');
1949 var url = utils.url_join_encode(
2018 var url = utils.url_join_encode(
1950 this.base_url,
2019 this.base_url,
1951 'api/notebooks',
2020 'api/notebooks',
1952 this.notebook_path,
2021 this.notebook_path,
1953 this.notebook_name
2022 this.notebook_name
1954 );
2023 );
1955 $.ajax(url, settings);
2024 $.ajax(url, settings);
1956 };
2025 };
1957
2026
1958 /**
2027 /**
1959 * Success callback for loading a notebook from the server.
2028 * Success callback for loading a notebook from the server.
1960 *
2029 *
1961 * Load notebook data from the JSON response.
2030 * Load notebook data from the JSON response.
1962 *
2031 *
1963 * @method load_notebook_success
2032 * @method load_notebook_success
1964 * @param {Object} data JSON representation of a notebook
2033 * @param {Object} data JSON representation of a notebook
1965 * @param {String} status Description of response status
2034 * @param {String} status Description of response status
1966 * @param {jqXHR} xhr jQuery Ajax object
2035 * @param {jqXHR} xhr jQuery Ajax object
1967 */
2036 */
1968 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2037 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1969 this.fromJSON(data);
2038 this.fromJSON(data);
1970 if (this.ncells() === 0) {
2039 if (this.ncells() === 0) {
1971 this.insert_cell_below('code');
2040 this.insert_cell_below('code');
1972 this.edit_mode(0);
2041 this.edit_mode(0);
1973 } else {
2042 } else {
1974 this.select(0);
2043 this.select(0);
1975 this.command_mode();
2044 this.command_mode();
1976 }
2045 }
1977 this.set_dirty(false);
2046 this.set_dirty(false);
1978 this.scroll_to_top();
2047 this.scroll_to_top();
1979 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2048 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1980 var msg = "This notebook has been converted from an older " +
2049 var msg = "This notebook has been converted from an older " +
1981 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2050 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1982 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2051 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1983 "newer notebook format will be used and older versions of IPython " +
2052 "newer notebook format will be used and older versions of IPython " +
1984 "may not be able to read it. To keep the older version, close the " +
2053 "may not be able to read it. To keep the older version, close the " +
1985 "notebook without saving it.";
2054 "notebook without saving it.";
1986 IPython.dialog.modal({
2055 IPython.dialog.modal({
1987 title : "Notebook converted",
2056 title : "Notebook converted",
1988 body : msg,
2057 body : msg,
1989 buttons : {
2058 buttons : {
1990 OK : {
2059 OK : {
1991 class : "btn-primary"
2060 class : "btn-primary"
1992 }
2061 }
1993 }
2062 }
1994 });
2063 });
1995 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2064 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1996 var that = this;
2065 var that = this;
1997 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2066 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1998 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2067 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1999 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2068 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2000 this_vs + ". You can still work with this notebook, but some features " +
2069 this_vs + ". You can still work with this notebook, but some features " +
2001 "introduced in later notebook versions may not be available.";
2070 "introduced in later notebook versions may not be available.";
2002
2071
2003 IPython.dialog.modal({
2072 IPython.dialog.modal({
2004 title : "Newer Notebook",
2073 title : "Newer Notebook",
2005 body : msg,
2074 body : msg,
2006 buttons : {
2075 buttons : {
2007 OK : {
2076 OK : {
2008 class : "btn-danger"
2077 class : "btn-danger"
2009 }
2078 }
2010 }
2079 }
2011 });
2080 });
2012
2081
2013 }
2082 }
2014
2083
2015 // Create the session after the notebook is completely loaded to prevent
2084 // Create the session after the notebook is completely loaded to prevent
2016 // code execution upon loading, which is a security risk.
2085 // code execution upon loading, which is a security risk.
2017 if (this.session === null) {
2086 if (this.session === null) {
2018 this.start_session();
2087 this.start_session();
2019 }
2088 }
2020 // load our checkpoint list
2089 // load our checkpoint list
2021 this.list_checkpoints();
2090 this.list_checkpoints();
2022
2091
2023 // load toolbar state
2092 // load toolbar state
2024 if (this.metadata.celltoolbar) {
2093 if (this.metadata.celltoolbar) {
2025 IPython.CellToolbar.global_show();
2094 IPython.CellToolbar.global_show();
2026 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
2095 IPython.CellToolbar.activate_preset(this.metadata.celltoolbar);
2027 }
2096 }
2028
2097
2029 $([IPython.events]).trigger('notebook_loaded.Notebook');
2098 $([IPython.events]).trigger('notebook_loaded.Notebook');
2030 };
2099 };
2031
2100
2032 /**
2101 /**
2033 * Failure callback for loading a notebook from the server.
2102 * Failure callback for loading a notebook from the server.
2034 *
2103 *
2035 * @method load_notebook_error
2104 * @method load_notebook_error
2036 * @param {jqXHR} xhr jQuery Ajax object
2105 * @param {jqXHR} xhr jQuery Ajax object
2037 * @param {String} status Description of response status
2106 * @param {String} status Description of response status
2038 * @param {String} error HTTP error message
2107 * @param {String} error HTTP error message
2039 */
2108 */
2040 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2109 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2041 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2110 $([IPython.events]).trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2042 var msg;
2111 var msg;
2043 if (xhr.status === 400) {
2112 if (xhr.status === 400) {
2044 msg = error;
2113 msg = error;
2045 } else if (xhr.status === 500) {
2114 } else if (xhr.status === 500) {
2046 msg = "An unknown error occurred while loading this notebook. " +
2115 msg = "An unknown error occurred while loading this notebook. " +
2047 "This version can load notebook formats " +
2116 "This version can load notebook formats " +
2048 "v" + this.nbformat + " or earlier.";
2117 "v" + this.nbformat + " or earlier.";
2049 }
2118 }
2050 IPython.dialog.modal({
2119 IPython.dialog.modal({
2051 title: "Error loading notebook",
2120 title: "Error loading notebook",
2052 body : msg,
2121 body : msg,
2053 buttons : {
2122 buttons : {
2054 "OK": {}
2123 "OK": {}
2055 }
2124 }
2056 });
2125 });
2057 };
2126 };
2058
2127
2059 /********************* checkpoint-related *********************/
2128 /********************* checkpoint-related *********************/
2060
2129
2061 /**
2130 /**
2062 * Save the notebook then immediately create a checkpoint.
2131 * Save the notebook then immediately create a checkpoint.
2063 *
2132 *
2064 * @method save_checkpoint
2133 * @method save_checkpoint
2065 */
2134 */
2066 Notebook.prototype.save_checkpoint = function () {
2135 Notebook.prototype.save_checkpoint = function () {
2067 this._checkpoint_after_save = true;
2136 this._checkpoint_after_save = true;
2068 this.save_notebook();
2137 this.save_notebook();
2069 };
2138 };
2070
2139
2071 /**
2140 /**
2072 * Add a checkpoint for this notebook.
2141 * Add a checkpoint for this notebook.
2073 * for use as a callback from checkpoint creation.
2142 * for use as a callback from checkpoint creation.
2074 *
2143 *
2075 * @method add_checkpoint
2144 * @method add_checkpoint
2076 */
2145 */
2077 Notebook.prototype.add_checkpoint = function (checkpoint) {
2146 Notebook.prototype.add_checkpoint = function (checkpoint) {
2078 var found = false;
2147 var found = false;
2079 for (var i = 0; i < this.checkpoints.length; i++) {
2148 for (var i = 0; i < this.checkpoints.length; i++) {
2080 var existing = this.checkpoints[i];
2149 var existing = this.checkpoints[i];
2081 if (existing.id == checkpoint.id) {
2150 if (existing.id == checkpoint.id) {
2082 found = true;
2151 found = true;
2083 this.checkpoints[i] = checkpoint;
2152 this.checkpoints[i] = checkpoint;
2084 break;
2153 break;
2085 }
2154 }
2086 }
2155 }
2087 if (!found) {
2156 if (!found) {
2088 this.checkpoints.push(checkpoint);
2157 this.checkpoints.push(checkpoint);
2089 }
2158 }
2090 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2159 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2091 };
2160 };
2092
2161
2093 /**
2162 /**
2094 * List checkpoints for this notebook.
2163 * List checkpoints for this notebook.
2095 *
2164 *
2096 * @method list_checkpoints
2165 * @method list_checkpoints
2097 */
2166 */
2098 Notebook.prototype.list_checkpoints = function () {
2167 Notebook.prototype.list_checkpoints = function () {
2099 var url = utils.url_join_encode(
2168 var url = utils.url_join_encode(
2100 this.base_url,
2169 this.base_url,
2101 'api/notebooks',
2170 'api/notebooks',
2102 this.notebook_path,
2171 this.notebook_path,
2103 this.notebook_name,
2172 this.notebook_name,
2104 'checkpoints'
2173 'checkpoints'
2105 );
2174 );
2106 $.get(url).done(
2175 $.get(url).done(
2107 $.proxy(this.list_checkpoints_success, this)
2176 $.proxy(this.list_checkpoints_success, this)
2108 ).fail(
2177 ).fail(
2109 $.proxy(this.list_checkpoints_error, this)
2178 $.proxy(this.list_checkpoints_error, this)
2110 );
2179 );
2111 };
2180 };
2112
2181
2113 /**
2182 /**
2114 * Success callback for listing checkpoints.
2183 * Success callback for listing checkpoints.
2115 *
2184 *
2116 * @method list_checkpoint_success
2185 * @method list_checkpoint_success
2117 * @param {Object} data JSON representation of a checkpoint
2186 * @param {Object} data JSON representation of a checkpoint
2118 * @param {String} status Description of response status
2187 * @param {String} status Description of response status
2119 * @param {jqXHR} xhr jQuery Ajax object
2188 * @param {jqXHR} xhr jQuery Ajax object
2120 */
2189 */
2121 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2190 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2122 data = $.parseJSON(data);
2191 data = $.parseJSON(data);
2123 this.checkpoints = data;
2192 this.checkpoints = data;
2124 if (data.length) {
2193 if (data.length) {
2125 this.last_checkpoint = data[data.length - 1];
2194 this.last_checkpoint = data[data.length - 1];
2126 } else {
2195 } else {
2127 this.last_checkpoint = null;
2196 this.last_checkpoint = null;
2128 }
2197 }
2129 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2198 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
2130 };
2199 };
2131
2200
2132 /**
2201 /**
2133 * Failure callback for listing a checkpoint.
2202 * Failure callback for listing a checkpoint.
2134 *
2203 *
2135 * @method list_checkpoint_error
2204 * @method list_checkpoint_error
2136 * @param {jqXHR} xhr jQuery Ajax object
2205 * @param {jqXHR} xhr jQuery Ajax object
2137 * @param {String} status Description of response status
2206 * @param {String} status Description of response status
2138 * @param {String} error_msg HTTP error message
2207 * @param {String} error_msg HTTP error message
2139 */
2208 */
2140 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2209 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2141 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2210 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
2142 };
2211 };
2143
2212
2144 /**
2213 /**
2145 * Create a checkpoint of this notebook on the server from the most recent save.
2214 * Create a checkpoint of this notebook on the server from the most recent save.
2146 *
2215 *
2147 * @method create_checkpoint
2216 * @method create_checkpoint
2148 */
2217 */
2149 Notebook.prototype.create_checkpoint = function () {
2218 Notebook.prototype.create_checkpoint = function () {
2150 var url = utils.url_join_encode(
2219 var url = utils.url_join_encode(
2151 this.base_url,
2220 this.base_url,
2152 'api/notebooks',
2221 'api/notebooks',
2153 this.notebook_path,
2222 this.notebook_path,
2154 this.notebook_name,
2223 this.notebook_name,
2155 'checkpoints'
2224 'checkpoints'
2156 );
2225 );
2157 $.post(url).done(
2226 $.post(url).done(
2158 $.proxy(this.create_checkpoint_success, this)
2227 $.proxy(this.create_checkpoint_success, this)
2159 ).fail(
2228 ).fail(
2160 $.proxy(this.create_checkpoint_error, this)
2229 $.proxy(this.create_checkpoint_error, this)
2161 );
2230 );
2162 };
2231 };
2163
2232
2164 /**
2233 /**
2165 * Success callback for creating a checkpoint.
2234 * Success callback for creating a checkpoint.
2166 *
2235 *
2167 * @method create_checkpoint_success
2236 * @method create_checkpoint_success
2168 * @param {Object} data JSON representation of a checkpoint
2237 * @param {Object} data JSON representation of a checkpoint
2169 * @param {String} status Description of response status
2238 * @param {String} status Description of response status
2170 * @param {jqXHR} xhr jQuery Ajax object
2239 * @param {jqXHR} xhr jQuery Ajax object
2171 */
2240 */
2172 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2241 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2173 data = $.parseJSON(data);
2242 data = $.parseJSON(data);
2174 this.add_checkpoint(data);
2243 this.add_checkpoint(data);
2175 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2244 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
2176 };
2245 };
2177
2246
2178 /**
2247 /**
2179 * Failure callback for creating a checkpoint.
2248 * Failure callback for creating a checkpoint.
2180 *
2249 *
2181 * @method create_checkpoint_error
2250 * @method create_checkpoint_error
2182 * @param {jqXHR} xhr jQuery Ajax object
2251 * @param {jqXHR} xhr jQuery Ajax object
2183 * @param {String} status Description of response status
2252 * @param {String} status Description of response status
2184 * @param {String} error_msg HTTP error message
2253 * @param {String} error_msg HTTP error message
2185 */
2254 */
2186 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2255 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2187 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2256 $([IPython.events]).trigger('checkpoint_failed.Notebook');
2188 };
2257 };
2189
2258
2190 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2259 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2191 var that = this;
2260 var that = this;
2192 checkpoint = checkpoint || this.last_checkpoint;
2261 checkpoint = checkpoint || this.last_checkpoint;
2193 if ( ! checkpoint ) {
2262 if ( ! checkpoint ) {
2194 console.log("restore dialog, but no checkpoint to restore to!");
2263 console.log("restore dialog, but no checkpoint to restore to!");
2195 return;
2264 return;
2196 }
2265 }
2197 var body = $('<div/>').append(
2266 var body = $('<div/>').append(
2198 $('<p/>').addClass("p-space").text(
2267 $('<p/>').addClass("p-space").text(
2199 "Are you sure you want to revert the notebook to " +
2268 "Are you sure you want to revert the notebook to " +
2200 "the latest checkpoint?"
2269 "the latest checkpoint?"
2201 ).append(
2270 ).append(
2202 $("<strong/>").text(
2271 $("<strong/>").text(
2203 " This cannot be undone."
2272 " This cannot be undone."
2204 )
2273 )
2205 )
2274 )
2206 ).append(
2275 ).append(
2207 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2276 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2208 ).append(
2277 ).append(
2209 $('<p/>').addClass("p-space").text(
2278 $('<p/>').addClass("p-space").text(
2210 Date(checkpoint.last_modified)
2279 Date(checkpoint.last_modified)
2211 ).css("text-align", "center")
2280 ).css("text-align", "center")
2212 );
2281 );
2213
2282
2214 IPython.dialog.modal({
2283 IPython.dialog.modal({
2215 title : "Revert notebook to checkpoint",
2284 title : "Revert notebook to checkpoint",
2216 body : body,
2285 body : body,
2217 buttons : {
2286 buttons : {
2218 Revert : {
2287 Revert : {
2219 class : "btn-danger",
2288 class : "btn-danger",
2220 click : function () {
2289 click : function () {
2221 that.restore_checkpoint(checkpoint.id);
2290 that.restore_checkpoint(checkpoint.id);
2222 }
2291 }
2223 },
2292 },
2224 Cancel : {}
2293 Cancel : {}
2225 }
2294 }
2226 });
2295 });
2227 };
2296 };
2228
2297
2229 /**
2298 /**
2230 * Restore the notebook to a checkpoint state.
2299 * Restore the notebook to a checkpoint state.
2231 *
2300 *
2232 * @method restore_checkpoint
2301 * @method restore_checkpoint
2233 * @param {String} checkpoint ID
2302 * @param {String} checkpoint ID
2234 */
2303 */
2235 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2304 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2236 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2305 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2237 var url = utils.url_join_encode(
2306 var url = utils.url_join_encode(
2238 this.base_url,
2307 this.base_url,
2239 'api/notebooks',
2308 'api/notebooks',
2240 this.notebook_path,
2309 this.notebook_path,
2241 this.notebook_name,
2310 this.notebook_name,
2242 'checkpoints',
2311 'checkpoints',
2243 checkpoint
2312 checkpoint
2244 );
2313 );
2245 $.post(url).done(
2314 $.post(url).done(
2246 $.proxy(this.restore_checkpoint_success, this)
2315 $.proxy(this.restore_checkpoint_success, this)
2247 ).fail(
2316 ).fail(
2248 $.proxy(this.restore_checkpoint_error, this)
2317 $.proxy(this.restore_checkpoint_error, this)
2249 );
2318 );
2250 };
2319 };
2251
2320
2252 /**
2321 /**
2253 * Success callback for restoring a notebook to a checkpoint.
2322 * Success callback for restoring a notebook to a checkpoint.
2254 *
2323 *
2255 * @method restore_checkpoint_success
2324 * @method restore_checkpoint_success
2256 * @param {Object} data (ignored, should be empty)
2325 * @param {Object} data (ignored, should be empty)
2257 * @param {String} status Description of response status
2326 * @param {String} status Description of response status
2258 * @param {jqXHR} xhr jQuery Ajax object
2327 * @param {jqXHR} xhr jQuery Ajax object
2259 */
2328 */
2260 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2329 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2261 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2330 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2262 this.load_notebook(this.notebook_name, this.notebook_path);
2331 this.load_notebook(this.notebook_name, this.notebook_path);
2263 };
2332 };
2264
2333
2265 /**
2334 /**
2266 * Failure callback for restoring a notebook to a checkpoint.
2335 * Failure callback for restoring a notebook to a checkpoint.
2267 *
2336 *
2268 * @method restore_checkpoint_error
2337 * @method restore_checkpoint_error
2269 * @param {jqXHR} xhr jQuery Ajax object
2338 * @param {jqXHR} xhr jQuery Ajax object
2270 * @param {String} status Description of response status
2339 * @param {String} status Description of response status
2271 * @param {String} error_msg HTTP error message
2340 * @param {String} error_msg HTTP error message
2272 */
2341 */
2273 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2342 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2274 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2343 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2275 };
2344 };
2276
2345
2277 /**
2346 /**
2278 * Delete a notebook checkpoint.
2347 * Delete a notebook checkpoint.
2279 *
2348 *
2280 * @method delete_checkpoint
2349 * @method delete_checkpoint
2281 * @param {String} checkpoint ID
2350 * @param {String} checkpoint ID
2282 */
2351 */
2283 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2352 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2284 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2353 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
2285 var url = utils.url_join_encode(
2354 var url = utils.url_join_encode(
2286 this.base_url,
2355 this.base_url,
2287 'api/notebooks',
2356 'api/notebooks',
2288 this.notebook_path,
2357 this.notebook_path,
2289 this.notebook_name,
2358 this.notebook_name,
2290 'checkpoints',
2359 'checkpoints',
2291 checkpoint
2360 checkpoint
2292 );
2361 );
2293 $.ajax(url, {
2362 $.ajax(url, {
2294 type: 'DELETE',
2363 type: 'DELETE',
2295 success: $.proxy(this.delete_checkpoint_success, this),
2364 success: $.proxy(this.delete_checkpoint_success, this),
2296 error: $.proxy(this.delete_notebook_error,this)
2365 error: $.proxy(this.delete_notebook_error,this)
2297 });
2366 });
2298 };
2367 };
2299
2368
2300 /**
2369 /**
2301 * Success callback for deleting a notebook checkpoint
2370 * Success callback for deleting a notebook checkpoint
2302 *
2371 *
2303 * @method delete_checkpoint_success
2372 * @method delete_checkpoint_success
2304 * @param {Object} data (ignored, should be empty)
2373 * @param {Object} data (ignored, should be empty)
2305 * @param {String} status Description of response status
2374 * @param {String} status Description of response status
2306 * @param {jqXHR} xhr jQuery Ajax object
2375 * @param {jqXHR} xhr jQuery Ajax object
2307 */
2376 */
2308 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2377 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2309 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2378 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2310 this.load_notebook(this.notebook_name, this.notebook_path);
2379 this.load_notebook(this.notebook_name, this.notebook_path);
2311 };
2380 };
2312
2381
2313 /**
2382 /**
2314 * Failure callback for deleting a notebook checkpoint.
2383 * Failure callback for deleting a notebook checkpoint.
2315 *
2384 *
2316 * @method delete_checkpoint_error
2385 * @method delete_checkpoint_error
2317 * @param {jqXHR} xhr jQuery Ajax object
2386 * @param {jqXHR} xhr jQuery Ajax object
2318 * @param {String} status Description of response status
2387 * @param {String} status Description of response status
2319 * @param {String} error_msg HTTP error message
2388 * @param {String} error_msg HTTP error message
2320 */
2389 */
2321 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2390 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2322 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2391 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2323 };
2392 };
2324
2393
2325
2394
2326 IPython.Notebook = Notebook;
2395 IPython.Notebook = Notebook;
2327
2396
2328
2397
2329 return IPython;
2398 return IPython;
2330
2399
2331 }(IPython));
2400 }(IPython));
2332
2401
@@ -1,865 +1,868 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.trusted = true;
35 this.clear_queued = null;
35 this.clear_queued = null;
36 if (prompt_area === undefined) {
36 if (prompt_area === undefined) {
37 this.prompt_area = true;
37 this.prompt_area = true;
38 } else {
38 } else {
39 this.prompt_area = prompt_area;
39 this.prompt_area = prompt_area;
40 }
40 }
41 this.create_elements();
41 this.create_elements();
42 this.style();
42 this.style();
43 this.bind_events();
43 this.bind_events();
44 };
44 };
45
45
46
46
47 /**
47 /**
48 * Class prototypes
48 * Class prototypes
49 **/
49 **/
50
50
51 OutputArea.prototype.create_elements = function () {
51 OutputArea.prototype.create_elements = function () {
52 this.element = $("<div/>");
52 this.element = $("<div/>");
53 this.collapse_button = $("<div/>");
53 this.collapse_button = $("<div/>");
54 this.prompt_overlay = $("<div/>");
54 this.prompt_overlay = $("<div/>");
55 this.wrapper.append(this.prompt_overlay);
55 this.wrapper.append(this.prompt_overlay);
56 this.wrapper.append(this.element);
56 this.wrapper.append(this.element);
57 this.wrapper.append(this.collapse_button);
57 this.wrapper.append(this.collapse_button);
58 };
58 };
59
59
60
60
61 OutputArea.prototype.style = function () {
61 OutputArea.prototype.style = function () {
62 this.collapse_button.hide();
62 this.collapse_button.hide();
63 this.prompt_overlay.hide();
63 this.prompt_overlay.hide();
64
64
65 this.wrapper.addClass('output_wrapper');
65 this.wrapper.addClass('output_wrapper');
66 this.element.addClass('output');
66 this.element.addClass('output');
67
67
68 this.collapse_button.addClass("btn output_collapsed");
68 this.collapse_button.addClass("btn output_collapsed");
69 this.collapse_button.attr('title', 'click to expand output');
69 this.collapse_button.attr('title', 'click to expand output');
70 this.collapse_button.text('. . .');
70 this.collapse_button.text('. . .');
71
71
72 this.prompt_overlay.addClass('out_prompt_overlay prompt');
72 this.prompt_overlay.addClass('out_prompt_overlay prompt');
73 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
73 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
74
74
75 this.collapse();
75 this.collapse();
76 };
76 };
77
77
78 /**
78 /**
79 * Should the OutputArea scroll?
79 * Should the OutputArea scroll?
80 * Returns whether the height (in lines) exceeds a threshold.
80 * Returns whether the height (in lines) exceeds a threshold.
81 *
81 *
82 * @private
82 * @private
83 * @method _should_scroll
83 * @method _should_scroll
84 * @param [lines=100]{Integer}
84 * @param [lines=100]{Integer}
85 * @return {Bool}
85 * @return {Bool}
86 *
86 *
87 */
87 */
88 OutputArea.prototype._should_scroll = function (lines) {
88 OutputArea.prototype._should_scroll = function (lines) {
89 if (lines <=0 ){ return }
89 if (lines <=0 ){ return }
90 if (!lines) {
90 if (!lines) {
91 lines = 100;
91 lines = 100;
92 }
92 }
93 // line-height from http://stackoverflow.com/questions/1185151
93 // line-height from http://stackoverflow.com/questions/1185151
94 var fontSize = this.element.css('font-size');
94 var fontSize = this.element.css('font-size');
95 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
95 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
96
96
97 return (this.element.height() > lines * lineHeight);
97 return (this.element.height() > lines * lineHeight);
98 };
98 };
99
99
100
100
101 OutputArea.prototype.bind_events = function () {
101 OutputArea.prototype.bind_events = function () {
102 var that = this;
102 var that = this;
103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
103 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
104 this.prompt_overlay.click(function () { that.toggle_scroll(); });
105
105
106 this.element.resize(function () {
106 this.element.resize(function () {
107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
107 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
108 if ( IPython.utils.browser[0] === "Firefox" ) {
108 if ( IPython.utils.browser[0] === "Firefox" ) {
109 return;
109 return;
110 }
110 }
111 // maybe scroll output,
111 // maybe scroll output,
112 // if it's grown large enough and hasn't already been scrolled.
112 // if it's grown large enough and hasn't already been scrolled.
113 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
113 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
114 that.scroll_area();
114 that.scroll_area();
115 }
115 }
116 });
116 });
117 this.collapse_button.click(function () {
117 this.collapse_button.click(function () {
118 that.expand();
118 that.expand();
119 });
119 });
120 };
120 };
121
121
122
122
123 OutputArea.prototype.collapse = function () {
123 OutputArea.prototype.collapse = function () {
124 if (!this.collapsed) {
124 if (!this.collapsed) {
125 this.element.hide();
125 this.element.hide();
126 this.prompt_overlay.hide();
126 this.prompt_overlay.hide();
127 if (this.element.html()){
127 if (this.element.html()){
128 this.collapse_button.show();
128 this.collapse_button.show();
129 }
129 }
130 this.collapsed = true;
130 this.collapsed = true;
131 }
131 }
132 };
132 };
133
133
134
134
135 OutputArea.prototype.expand = function () {
135 OutputArea.prototype.expand = function () {
136 if (this.collapsed) {
136 if (this.collapsed) {
137 this.collapse_button.hide();
137 this.collapse_button.hide();
138 this.element.show();
138 this.element.show();
139 this.prompt_overlay.show();
139 this.prompt_overlay.show();
140 this.collapsed = false;
140 this.collapsed = false;
141 }
141 }
142 };
142 };
143
143
144
144
145 OutputArea.prototype.toggle_output = function () {
145 OutputArea.prototype.toggle_output = function () {
146 if (this.collapsed) {
146 if (this.collapsed) {
147 this.expand();
147 this.expand();
148 } else {
148 } else {
149 this.collapse();
149 this.collapse();
150 }
150 }
151 };
151 };
152
152
153
153
154 OutputArea.prototype.scroll_area = function () {
154 OutputArea.prototype.scroll_area = function () {
155 this.element.addClass('output_scroll');
155 this.element.addClass('output_scroll');
156 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
156 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
157 this.scrolled = true;
157 this.scrolled = true;
158 };
158 };
159
159
160
160
161 OutputArea.prototype.unscroll_area = function () {
161 OutputArea.prototype.unscroll_area = function () {
162 this.element.removeClass('output_scroll');
162 this.element.removeClass('output_scroll');
163 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
163 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
164 this.scrolled = false;
164 this.scrolled = false;
165 };
165 };
166
166
167 /**
167 /**
168 *
168 *
169 * Scroll OutputArea if height supperior than a threshold (in lines).
169 * Scroll OutputArea if height supperior than a threshold (in lines).
170 *
170 *
171 * Threshold is a maximum number of lines. If unspecified, defaults to
171 * Threshold is a maximum number of lines. If unspecified, defaults to
172 * OutputArea.minimum_scroll_threshold.
172 * OutputArea.minimum_scroll_threshold.
173 *
173 *
174 * Negative threshold will prevent the OutputArea from ever scrolling.
174 * Negative threshold will prevent the OutputArea from ever scrolling.
175 *
175 *
176 * @method scroll_if_long
176 * @method scroll_if_long
177 *
177 *
178 * @param [lines=20]{Number} Default to 20 if not set,
178 * @param [lines=20]{Number} Default to 20 if not set,
179 * behavior undefined for value of `0`.
179 * behavior undefined for value of `0`.
180 *
180 *
181 **/
181 **/
182 OutputArea.prototype.scroll_if_long = function (lines) {
182 OutputArea.prototype.scroll_if_long = function (lines) {
183 var n = lines | OutputArea.minimum_scroll_threshold;
183 var n = lines | OutputArea.minimum_scroll_threshold;
184 if(n <= 0){
184 if(n <= 0){
185 return
185 return
186 }
186 }
187
187
188 if (this._should_scroll(n)) {
188 if (this._should_scroll(n)) {
189 // only allow scrolling long-enough output
189 // only allow scrolling long-enough output
190 this.scroll_area();
190 this.scroll_area();
191 }
191 }
192 };
192 };
193
193
194
194
195 OutputArea.prototype.toggle_scroll = function () {
195 OutputArea.prototype.toggle_scroll = function () {
196 if (this.scrolled) {
196 if (this.scrolled) {
197 this.unscroll_area();
197 this.unscroll_area();
198 } else {
198 } else {
199 // only allow scrolling long-enough output
199 // only allow scrolling long-enough output
200 this.scroll_if_long();
200 this.scroll_if_long();
201 }
201 }
202 };
202 };
203
203
204
204
205 // typeset with MathJax if MathJax is available
205 // typeset with MathJax if MathJax is available
206 OutputArea.prototype.typeset = function () {
206 OutputArea.prototype.typeset = function () {
207 if (window.MathJax){
207 if (window.MathJax){
208 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
208 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
209 }
209 }
210 };
210 };
211
211
212
212
213 OutputArea.prototype.handle_output = function (msg) {
213 OutputArea.prototype.handle_output = function (msg) {
214 var json = {};
214 var json = {};
215 var msg_type = json.output_type = msg.header.msg_type;
215 var msg_type = json.output_type = msg.header.msg_type;
216 var content = msg.content;
216 var content = msg.content;
217 if (msg_type === "stream") {
217 if (msg_type === "stream") {
218 json.text = content.data;
218 json.text = content.data;
219 json.stream = content.name;
219 json.stream = content.name;
220 } else if (msg_type === "display_data") {
220 } else if (msg_type === "display_data") {
221 json = content.data;
221 json = content.data;
222 json.output_type = msg_type;
222 json.output_type = msg_type;
223 json.metadata = content.metadata;
223 json.metadata = content.metadata;
224 } else if (msg_type === "pyout") {
224 } else if (msg_type === "pyout") {
225 json = content.data;
225 json = content.data;
226 json.output_type = msg_type;
226 json.output_type = msg_type;
227 json.metadata = content.metadata;
227 json.metadata = content.metadata;
228 json.prompt_number = content.execution_count;
228 json.prompt_number = content.execution_count;
229 } else if (msg_type === "pyerr") {
229 } else if (msg_type === "pyerr") {
230 json.ename = content.ename;
230 json.ename = content.ename;
231 json.evalue = content.evalue;
231 json.evalue = content.evalue;
232 json.traceback = content.traceback;
232 json.traceback = content.traceback;
233 }
233 }
234 this.append_output(json);
234 this.append_output(json);
235 };
235 };
236
236
237
237
238 OutputArea.prototype.rename_keys = function (data, key_map) {
238 OutputArea.prototype.rename_keys = function (data, key_map) {
239 var remapped = {};
239 var remapped = {};
240 for (var key in data) {
240 for (var key in data) {
241 var new_key = key_map[key] || key;
241 var new_key = key_map[key] || key;
242 remapped[new_key] = data[key];
242 remapped[new_key] = data[key];
243 }
243 }
244 return remapped;
244 return remapped;
245 };
245 };
246
246
247
247
248 OutputArea.output_types = [
248 OutputArea.output_types = [
249 'application/javascript',
249 'application/javascript',
250 'text/html',
250 'text/html',
251 'text/latex',
251 'text/latex',
252 'image/svg+xml',
252 'image/svg+xml',
253 'image/png',
253 'image/png',
254 'image/jpeg',
254 'image/jpeg',
255 'application/pdf',
255 'application/pdf',
256 'text/plain'
256 'text/plain'
257 ];
257 ];
258
258
259 OutputArea.prototype.validate_output = function (json) {
259 OutputArea.prototype.validate_output = function (json) {
260 // scrub invalid outputs
260 // scrub invalid outputs
261 // TODO: right now everything is a string, but JSON really shouldn't be.
261 // TODO: right now everything is a string, but JSON really shouldn't be.
262 // nbformat 4 will fix that.
262 // nbformat 4 will fix that.
263 $.map(OutputArea.output_types, function(key){
263 $.map(OutputArea.output_types, function(key){
264 if (json[key] !== undefined && typeof json[key] !== 'string') {
264 if (json[key] !== undefined && typeof json[key] !== 'string') {
265 console.log("Invalid type for " + key, json[key]);
265 console.log("Invalid type for " + key, json[key]);
266 delete json[key];
266 delete json[key];
267 }
267 }
268 });
268 });
269 return json;
269 return json;
270 };
270 };
271
271
272 OutputArea.prototype.append_output = function (json) {
272 OutputArea.prototype.append_output = function (json) {
273 this.expand();
273 this.expand();
274 // Clear the output if clear is queued.
274 // Clear the output if clear is queued.
275 var needs_height_reset = false;
275 var needs_height_reset = false;
276 if (this.clear_queued) {
276 if (this.clear_queued) {
277 this.clear_output(false);
277 this.clear_output(false);
278 needs_height_reset = true;
278 needs_height_reset = true;
279 }
279 }
280
280
281 // validate output data types
281 // validate output data types
282 json = this.validate_output(json);
282 json = this.validate_output(json);
283
283
284 if (json.output_type === 'pyout') {
284 if (json.output_type === 'pyout') {
285 this.append_pyout(json);
285 this.append_pyout(json);
286 } else if (json.output_type === 'pyerr') {
286 } else if (json.output_type === 'pyerr') {
287 this.append_pyerr(json);
287 this.append_pyerr(json);
288 } else if (json.output_type === 'display_data') {
288 } else if (json.output_type === 'display_data') {
289 this.append_display_data(json);
289 this.append_display_data(json);
290 } else if (json.output_type === 'stream') {
290 } else if (json.output_type === 'stream') {
291 this.append_stream(json);
291 this.append_stream(json);
292 }
292 }
293
293
294 this.outputs.push(json);
294 this.outputs.push(json);
295
295
296 // Only reset the height to automatic if the height is currently
296 // Only reset the height to automatic if the height is currently
297 // fixed (done by wait=True flag on clear_output).
297 // fixed (done by wait=True flag on clear_output).
298 if (needs_height_reset) {
298 if (needs_height_reset) {
299 this.element.height('');
299 this.element.height('');
300 }
300 }
301
301
302 var that = this;
302 var that = this;
303 setTimeout(function(){that.element.trigger('resize');}, 100);
303 setTimeout(function(){that.element.trigger('resize');}, 100);
304 };
304 };
305
305
306
306
307 OutputArea.prototype.create_output_area = function () {
307 OutputArea.prototype.create_output_area = function () {
308 var oa = $("<div/>").addClass("output_area");
308 var oa = $("<div/>").addClass("output_area");
309 if (this.prompt_area) {
309 if (this.prompt_area) {
310 oa.append($('<div/>').addClass('prompt'));
310 oa.append($('<div/>').addClass('prompt'));
311 }
311 }
312 return oa;
312 return oa;
313 };
313 };
314
314
315
315
316 function _get_metadata_key(metadata, key, mime) {
316 function _get_metadata_key(metadata, key, mime) {
317 var mime_md = metadata[mime];
317 var mime_md = metadata[mime];
318 // mime-specific higher priority
318 // mime-specific higher priority
319 if (mime_md && mime_md[key] !== undefined) {
319 if (mime_md && mime_md[key] !== undefined) {
320 return mime_md[key];
320 return mime_md[key];
321 }
321 }
322 // fallback on global
322 // fallback on global
323 return metadata[key];
323 return metadata[key];
324 }
324 }
325
325
326 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
326 OutputArea.prototype.create_output_subarea = function(md, classes, mime) {
327 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
327 var subarea = $('<div/>').addClass('output_subarea').addClass(classes);
328 if (_get_metadata_key(md, 'isolated', mime)) {
328 if (_get_metadata_key(md, 'isolated', mime)) {
329 // Create an iframe to isolate the subarea from the rest of the
329 // Create an iframe to isolate the subarea from the rest of the
330 // document
330 // document
331 var iframe = $('<iframe/>').addClass('box-flex1');
331 var iframe = $('<iframe/>').addClass('box-flex1');
332 iframe.css({'height':1, 'width':'100%', 'display':'block'});
332 iframe.css({'height':1, 'width':'100%', 'display':'block'});
333 iframe.attr('frameborder', 0);
333 iframe.attr('frameborder', 0);
334 iframe.attr('scrolling', 'auto');
334 iframe.attr('scrolling', 'auto');
335
335
336 // Once the iframe is loaded, the subarea is dynamically inserted
336 // Once the iframe is loaded, the subarea is dynamically inserted
337 iframe.on('load', function() {
337 iframe.on('load', function() {
338 // Workaround needed by Firefox, to properly render svg inside
338 // Workaround needed by Firefox, to properly render svg inside
339 // iframes, see http://stackoverflow.com/questions/10177190/
339 // iframes, see http://stackoverflow.com/questions/10177190/
340 // svg-dynamically-added-to-iframe-does-not-render-correctly
340 // svg-dynamically-added-to-iframe-does-not-render-correctly
341 this.contentDocument.open();
341 this.contentDocument.open();
342
342
343 // Insert the subarea into the iframe
343 // Insert the subarea into the iframe
344 // We must directly write the html. When using Jquery's append
344 // We must directly write the html. When using Jquery's append
345 // method, javascript is evaluated in the parent document and
345 // method, javascript is evaluated in the parent document and
346 // not in the iframe document. At this point, subarea doesn't
346 // not in the iframe document. At this point, subarea doesn't
347 // contain any user content.
347 // contain any user content.
348 this.contentDocument.write(subarea.html());
348 this.contentDocument.write(subarea.html());
349
349
350 this.contentDocument.close();
350 this.contentDocument.close();
351
351
352 var body = this.contentDocument.body;
352 var body = this.contentDocument.body;
353 // Adjust the iframe height automatically
353 // Adjust the iframe height automatically
354 iframe.height(body.scrollHeight + 'px');
354 iframe.height(body.scrollHeight + 'px');
355 });
355 });
356
356
357 // Elements should be appended to the inner subarea and not to the
357 // Elements should be appended to the inner subarea and not to the
358 // iframe
358 // iframe
359 iframe.append = function(that) {
359 iframe.append = function(that) {
360 subarea.append(that);
360 subarea.append(that);
361 };
361 };
362
362
363 return iframe;
363 return iframe;
364 } else {
364 } else {
365 return subarea;
365 return subarea;
366 }
366 }
367 }
367 }
368
368
369
369
370 OutputArea.prototype._append_javascript_error = function (err, element) {
370 OutputArea.prototype._append_javascript_error = function (err, element) {
371 // display a message when a javascript error occurs in display output
371 // display a message when a javascript error occurs in display output
372 var msg = "Javascript error adding output!"
372 var msg = "Javascript error adding output!"
373 if ( element === undefined ) return;
373 if ( element === undefined ) return;
374 element
374 element
375 .append($('<div/>').text(msg).addClass('js-error'))
375 .append($('<div/>').text(msg).addClass('js-error'))
376 .append($('<div/>').text(err.toString()).addClass('js-error'))
376 .append($('<div/>').text(err.toString()).addClass('js-error'))
377 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
377 .append($('<div/>').text('See your browser Javascript console for more details.').addClass('js-error'));
378 };
378 };
379
379
380 OutputArea.prototype._safe_append = function (toinsert) {
380 OutputArea.prototype._safe_append = function (toinsert) {
381 // safely append an item to the document
381 // safely append an item to the document
382 // this is an object created by user code,
382 // this is an object created by user code,
383 // and may have errors, which should not be raised
383 // and may have errors, which should not be raised
384 // under any circumstances.
384 // under any circumstances.
385 try {
385 try {
386 this.element.append(toinsert);
386 this.element.append(toinsert);
387 } catch(err) {
387 } catch(err) {
388 console.log(err);
388 console.log(err);
389 // Create an actual output_area and output_subarea, which creates
389 // Create an actual output_area and output_subarea, which creates
390 // the prompt area and the proper indentation.
390 // the prompt area and the proper indentation.
391 var toinsert = this.create_output_area();
391 var toinsert = this.create_output_area();
392 var subarea = $('<div/>').addClass('output_subarea');
392 var subarea = $('<div/>').addClass('output_subarea');
393 toinsert.append(subarea);
393 toinsert.append(subarea);
394 this._append_javascript_error(err, subarea);
394 this._append_javascript_error(err, subarea);
395 this.element.append(toinsert);
395 this.element.append(toinsert);
396 }
396 }
397 };
397 };
398
398
399
399
400 OutputArea.prototype.append_pyout = function (json) {
400 OutputArea.prototype.append_pyout = function (json) {
401 var n = json.prompt_number || ' ';
401 var n = json.prompt_number || ' ';
402 var toinsert = this.create_output_area();
402 var toinsert = this.create_output_area();
403 if (this.prompt_area) {
403 if (this.prompt_area) {
404 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
404 toinsert.find('div.prompt').addClass('output_prompt').text('Out[' + n + ']:');
405 }
405 }
406 this.append_mime_type(json, toinsert);
406 this.append_mime_type(json, toinsert);
407 this._safe_append(toinsert);
407 this._safe_append(toinsert);
408 // If we just output latex, typeset it.
408 // If we just output latex, typeset it.
409 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
409 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
410 this.typeset();
410 this.typeset();
411 }
411 }
412 };
412 };
413
413
414
414
415 OutputArea.prototype.append_pyerr = function (json) {
415 OutputArea.prototype.append_pyerr = function (json) {
416 var tb = json.traceback;
416 var tb = json.traceback;
417 if (tb !== undefined && tb.length > 0) {
417 if (tb !== undefined && tb.length > 0) {
418 var s = '';
418 var s = '';
419 var len = tb.length;
419 var len = tb.length;
420 for (var i=0; i<len; i++) {
420 for (var i=0; i<len; i++) {
421 s = s + tb[i] + '\n';
421 s = s + tb[i] + '\n';
422 }
422 }
423 s = s + '\n';
423 s = s + '\n';
424 var toinsert = this.create_output_area();
424 var toinsert = this.create_output_area();
425 this.append_text(s, {}, toinsert);
425 this.append_text(s, {}, toinsert);
426 this._safe_append(toinsert);
426 this._safe_append(toinsert);
427 }
427 }
428 };
428 };
429
429
430
430
431 OutputArea.prototype.append_stream = function (json) {
431 OutputArea.prototype.append_stream = function (json) {
432 // temporary fix: if stream undefined (json file written prior to this patch),
432 // temporary fix: if stream undefined (json file written prior to this patch),
433 // default to most likely stdout:
433 // default to most likely stdout:
434 if (json.stream == undefined){
434 if (json.stream == undefined){
435 json.stream = 'stdout';
435 json.stream = 'stdout';
436 }
436 }
437 var text = json.text;
437 var text = json.text;
438 var subclass = "output_"+json.stream;
438 var subclass = "output_"+json.stream;
439 if (this.outputs.length > 0){
439 if (this.outputs.length > 0){
440 // have at least one output to consider
440 // have at least one output to consider
441 var last = this.outputs[this.outputs.length-1];
441 var last = this.outputs[this.outputs.length-1];
442 if (last.output_type == 'stream' && json.stream == last.stream){
442 if (last.output_type == 'stream' && json.stream == last.stream){
443 // latest output was in the same stream,
443 // latest output was in the same stream,
444 // so append directly into its pre tag
444 // so append directly into its pre tag
445 // escape ANSI & HTML specials:
445 // escape ANSI & HTML specials:
446 var pre = this.element.find('div.'+subclass).last().find('pre');
446 var pre = this.element.find('div.'+subclass).last().find('pre');
447 var html = utils.fixCarriageReturn(
447 var html = utils.fixCarriageReturn(
448 pre.html() + utils.fixConsole(text));
448 pre.html() + utils.fixConsole(text));
449 // The only user content injected with this HTML call is
449 // The only user content injected with this HTML call is
450 // escaped by the fixConsole() method.
450 // escaped by the fixConsole() method.
451 pre.html(html);
451 pre.html(html);
452 return;
452 return;
453 }
453 }
454 }
454 }
455
455
456 if (!text.replace("\r", "")) {
456 if (!text.replace("\r", "")) {
457 // text is nothing (empty string, \r, etc.)
457 // text is nothing (empty string, \r, etc.)
458 // so don't append any elements, which might add undesirable space
458 // so don't append any elements, which might add undesirable space
459 return;
459 return;
460 }
460 }
461
461
462 // If we got here, attach a new div
462 // If we got here, attach a new div
463 var toinsert = this.create_output_area();
463 var toinsert = this.create_output_area();
464 this.append_text(text, {}, toinsert, "output_stream "+subclass);
464 this.append_text(text, {}, toinsert, "output_stream "+subclass);
465 this._safe_append(toinsert);
465 this._safe_append(toinsert);
466 };
466 };
467
467
468
468
469 OutputArea.prototype.append_display_data = function (json) {
469 OutputArea.prototype.append_display_data = function (json) {
470 var toinsert = this.create_output_area();
470 var toinsert = this.create_output_area();
471 if (this.append_mime_type(json, toinsert)) {
471 if (this.append_mime_type(json, toinsert)) {
472 this._safe_append(toinsert);
472 this._safe_append(toinsert);
473 // If we just output latex, typeset it.
473 // If we just output latex, typeset it.
474 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
474 if ((json['text/latex'] !== undefined) || (json['text/html'] !== undefined)) {
475 this.typeset();
475 this.typeset();
476 }
476 }
477 }
477 }
478 };
478 };
479
479
480
480
481 OutputArea.safe_outputs = {
481 OutputArea.safe_outputs = {
482 'text/plain' : true,
482 'text/plain' : true,
483 'text/latex' : true,
483 'image/png' : true,
484 'image/png' : true,
484 'image/jpeg' : true
485 'image/jpeg' : true
485 };
486 };
486
487
487 OutputArea.prototype.append_mime_type = function (json, element) {
488 OutputArea.prototype.append_mime_type = function (json, element) {
488 for (var type_i in OutputArea.display_order) {
489 for (var type_i in OutputArea.display_order) {
489 var type = OutputArea.display_order[type_i];
490 var type = OutputArea.display_order[type_i];
490 var append = OutputArea.append_map[type];
491 var append = OutputArea.append_map[type];
491 if ((json[type] !== undefined) && append) {
492 if ((json[type] !== undefined) && append) {
493 var value = json[type];
492 if (!this.trusted && !OutputArea.safe_outputs[type]) {
494 if (!this.trusted && !OutputArea.safe_outputs[type]) {
493 // not trusted show warning and do not display
495 // not trusted, sanitize HTML
494 var content = {
496 if (type==='text/html' || type==='text/svg') {
495 text : "Untrusted " + type + " output ignored.",
497 value = IPython.security.sanitize_html(value);
496 stream : "stderr"
498 } else {
499 // don't display if we don't know how to sanitize it
500 console.log("Ignoring untrusted " + type + " output.");
501 continue;
497 }
502 }
498 this.append_stream(content);
499 continue;
500 }
503 }
501 var md = json.metadata || {};
504 var md = json.metadata || {};
502 var toinsert = append.apply(this, [json[type], md, element]);
505 var toinsert = append.apply(this, [value, md, element]);
503 $([IPython.events]).trigger('output_appended.OutputArea', [type, json[type], md, toinsert]);
506 $([IPython.events]).trigger('output_appended.OutputArea', [type, value, md, toinsert]);
504 return true;
507 return true;
505 }
508 }
506 }
509 }
507 return false;
510 return false;
508 };
511 };
509
512
510
513
511 OutputArea.prototype.append_html = function (html, md, element) {
514 OutputArea.prototype.append_html = function (html, md, element) {
512 var type = 'text/html';
515 var type = 'text/html';
513 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
516 var toinsert = this.create_output_subarea(md, "output_html rendered_html", type);
514 IPython.keyboard_manager.register_events(toinsert);
517 IPython.keyboard_manager.register_events(toinsert);
515 toinsert.append(html);
518 toinsert.append(html);
516 element.append(toinsert);
519 element.append(toinsert);
517 return toinsert;
520 return toinsert;
518 };
521 };
519
522
520
523
521 OutputArea.prototype.append_javascript = function (js, md, element) {
524 OutputArea.prototype.append_javascript = function (js, md, element) {
522 // We just eval the JS code, element appears in the local scope.
525 // We just eval the JS code, element appears in the local scope.
523 var type = 'application/javascript';
526 var type = 'application/javascript';
524 var toinsert = this.create_output_subarea(md, "output_javascript", type);
527 var toinsert = this.create_output_subarea(md, "output_javascript", type);
525 IPython.keyboard_manager.register_events(toinsert);
528 IPython.keyboard_manager.register_events(toinsert);
526 element.append(toinsert);
529 element.append(toinsert);
527 // FIXME TODO : remove `container element for 3.0`
530 // FIXME TODO : remove `container element for 3.0`
528 //backward compat, js should be eval'ed in a context where `container` is defined.
531 //backward compat, js should be eval'ed in a context where `container` is defined.
529 var container = element;
532 var container = element;
530 container.show = function(){console.log('Warning "container.show()" is deprecated.')};
533 container.show = function(){console.log('Warning "container.show()" is deprecated.')};
531 // end backward compat
534 // end backward compat
532 try {
535 try {
533 eval(js);
536 eval(js);
534 } catch(err) {
537 } catch(err) {
535 console.log(err);
538 console.log(err);
536 this._append_javascript_error(err, toinsert);
539 this._append_javascript_error(err, toinsert);
537 }
540 }
538 return toinsert;
541 return toinsert;
539 };
542 };
540
543
541
544
542 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
545 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
543 var type = 'text/plain';
546 var type = 'text/plain';
544 var toinsert = this.create_output_subarea(md, "output_text", type);
547 var toinsert = this.create_output_subarea(md, "output_text", type);
545 // escape ANSI & HTML specials in plaintext:
548 // escape ANSI & HTML specials in plaintext:
546 data = utils.fixConsole(data);
549 data = utils.fixConsole(data);
547 data = utils.fixCarriageReturn(data);
550 data = utils.fixCarriageReturn(data);
548 data = utils.autoLinkUrls(data);
551 data = utils.autoLinkUrls(data);
549 if (extra_class){
552 if (extra_class){
550 toinsert.addClass(extra_class);
553 toinsert.addClass(extra_class);
551 }
554 }
552 // The only user content injected with this HTML call is
555 // The only user content injected with this HTML call is
553 // escaped by the fixConsole() method.
556 // escaped by the fixConsole() method.
554 toinsert.append($("<pre/>").html(data));
557 toinsert.append($("<pre/>").html(data));
555 element.append(toinsert);
558 element.append(toinsert);
556 return toinsert;
559 return toinsert;
557 };
560 };
558
561
559
562
560 OutputArea.prototype.append_svg = function (svg, md, element) {
563 OutputArea.prototype.append_svg = function (svg, md, element) {
561 var type = 'image/svg+xml';
564 var type = 'image/svg+xml';
562 var toinsert = this.create_output_subarea(md, "output_svg", type);
565 var toinsert = this.create_output_subarea(md, "output_svg", type);
563 toinsert.append(svg);
566 toinsert.append(svg);
564 element.append(toinsert);
567 element.append(toinsert);
565 return toinsert;
568 return toinsert;
566 };
569 };
567
570
568
571
569 OutputArea.prototype._dblclick_to_reset_size = function (img) {
572 OutputArea.prototype._dblclick_to_reset_size = function (img) {
570 // wrap image after it's loaded on the page,
573 // wrap image after it's loaded on the page,
571 // otherwise the measured initial size will be incorrect
574 // otherwise the measured initial size will be incorrect
572 img.on("load", function (){
575 img.on("load", function (){
573 var h0 = img.height();
576 var h0 = img.height();
574 var w0 = img.width();
577 var w0 = img.width();
575 if (!(h0 && w0)) {
578 if (!(h0 && w0)) {
576 // zero size, don't make it resizable
579 // zero size, don't make it resizable
577 return;
580 return;
578 }
581 }
579 img.resizable({
582 img.resizable({
580 aspectRatio: true,
583 aspectRatio: true,
581 autoHide: true
584 autoHide: true
582 });
585 });
583 img.dblclick(function () {
586 img.dblclick(function () {
584 // resize wrapper & image together for some reason:
587 // resize wrapper & image together for some reason:
585 img.parent().height(h0);
588 img.parent().height(h0);
586 img.height(h0);
589 img.height(h0);
587 img.parent().width(w0);
590 img.parent().width(w0);
588 img.width(w0);
591 img.width(w0);
589 });
592 });
590 });
593 });
591 };
594 };
592
595
593 var set_width_height = function (img, md, mime) {
596 var set_width_height = function (img, md, mime) {
594 // set width and height of an img element from metadata
597 // set width and height of an img element from metadata
595 var height = _get_metadata_key(md, 'height', mime);
598 var height = _get_metadata_key(md, 'height', mime);
596 if (height !== undefined) img.attr('height', height);
599 if (height !== undefined) img.attr('height', height);
597 var width = _get_metadata_key(md, 'width', mime);
600 var width = _get_metadata_key(md, 'width', mime);
598 if (width !== undefined) img.attr('width', width);
601 if (width !== undefined) img.attr('width', width);
599 };
602 };
600
603
601 OutputArea.prototype.append_png = function (png, md, element) {
604 OutputArea.prototype.append_png = function (png, md, element) {
602 var type = 'image/png';
605 var type = 'image/png';
603 var toinsert = this.create_output_subarea(md, "output_png", type);
606 var toinsert = this.create_output_subarea(md, "output_png", type);
604 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
607 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
605 set_width_height(img, md, 'image/png');
608 set_width_height(img, md, 'image/png');
606 this._dblclick_to_reset_size(img);
609 this._dblclick_to_reset_size(img);
607 toinsert.append(img);
610 toinsert.append(img);
608 element.append(toinsert);
611 element.append(toinsert);
609 return toinsert;
612 return toinsert;
610 };
613 };
611
614
612
615
613 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
616 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
614 var type = 'image/jpeg';
617 var type = 'image/jpeg';
615 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
618 var toinsert = this.create_output_subarea(md, "output_jpeg", type);
616 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
619 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
617 set_width_height(img, md, 'image/jpeg');
620 set_width_height(img, md, 'image/jpeg');
618 this._dblclick_to_reset_size(img);
621 this._dblclick_to_reset_size(img);
619 toinsert.append(img);
622 toinsert.append(img);
620 element.append(toinsert);
623 element.append(toinsert);
621 return toinsert;
624 return toinsert;
622 };
625 };
623
626
624
627
625 OutputArea.prototype.append_pdf = function (pdf, md, element) {
628 OutputArea.prototype.append_pdf = function (pdf, md, element) {
626 var type = 'application/pdf';
629 var type = 'application/pdf';
627 var toinsert = this.create_output_subarea(md, "output_pdf", type);
630 var toinsert = this.create_output_subarea(md, "output_pdf", type);
628 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
631 var a = $('<a/>').attr('href', 'data:application/pdf;base64,'+pdf);
629 a.attr('target', '_blank');
632 a.attr('target', '_blank');
630 a.text('View PDF')
633 a.text('View PDF')
631 toinsert.append(a);
634 toinsert.append(a);
632 element.append(toinsert);
635 element.append(toinsert);
633 return toinsert;
636 return toinsert;
634 }
637 }
635
638
636 OutputArea.prototype.append_latex = function (latex, md, element) {
639 OutputArea.prototype.append_latex = function (latex, md, element) {
637 // This method cannot do the typesetting because the latex first has to
640 // This method cannot do the typesetting because the latex first has to
638 // be on the page.
641 // be on the page.
639 var type = 'text/latex';
642 var type = 'text/latex';
640 var toinsert = this.create_output_subarea(md, "output_latex", type);
643 var toinsert = this.create_output_subarea(md, "output_latex", type);
641 toinsert.append(latex);
644 toinsert.append(latex);
642 element.append(toinsert);
645 element.append(toinsert);
643 return toinsert;
646 return toinsert;
644 };
647 };
645
648
646
649
647 OutputArea.prototype.append_raw_input = function (msg) {
650 OutputArea.prototype.append_raw_input = function (msg) {
648 var that = this;
651 var that = this;
649 this.expand();
652 this.expand();
650 var content = msg.content;
653 var content = msg.content;
651 var area = this.create_output_area();
654 var area = this.create_output_area();
652
655
653 // disable any other raw_inputs, if they are left around
656 // disable any other raw_inputs, if they are left around
654 $("div.output_subarea.raw_input").remove();
657 $("div.output_subarea.raw_input").remove();
655
658
656 area.append(
659 area.append(
657 $("<div/>")
660 $("<div/>")
658 .addClass("box-flex1 output_subarea raw_input")
661 .addClass("box-flex1 output_subarea raw_input")
659 .append(
662 .append(
660 $("<span/>")
663 $("<span/>")
661 .addClass("input_prompt")
664 .addClass("input_prompt")
662 .text(content.prompt)
665 .text(content.prompt)
663 )
666 )
664 .append(
667 .append(
665 $("<input/>")
668 $("<input/>")
666 .addClass("raw_input")
669 .addClass("raw_input")
667 .attr('type', 'text')
670 .attr('type', 'text')
668 .attr("size", 47)
671 .attr("size", 47)
669 .keydown(function (event, ui) {
672 .keydown(function (event, ui) {
670 // make sure we submit on enter,
673 // make sure we submit on enter,
671 // and don't re-execute the *cell* on shift-enter
674 // and don't re-execute the *cell* on shift-enter
672 if (event.which === IPython.keyboard.keycodes.enter) {
675 if (event.which === IPython.keyboard.keycodes.enter) {
673 that._submit_raw_input();
676 that._submit_raw_input();
674 return false;
677 return false;
675 }
678 }
676 })
679 })
677 )
680 )
678 );
681 );
679
682
680 this.element.append(area);
683 this.element.append(area);
681 var raw_input = area.find('input.raw_input');
684 var raw_input = area.find('input.raw_input');
682 // Register events that enable/disable the keyboard manager while raw
685 // Register events that enable/disable the keyboard manager while raw
683 // input is focused.
686 // input is focused.
684 IPython.keyboard_manager.register_events(raw_input);
687 IPython.keyboard_manager.register_events(raw_input);
685 // Note, the following line used to read raw_input.focus().focus().
688 // Note, the following line used to read raw_input.focus().focus().
686 // This seemed to be needed otherwise only the cell would be focused.
689 // This seemed to be needed otherwise only the cell would be focused.
687 // But with the modal UI, this seems to work fine with one call to focus().
690 // But with the modal UI, this seems to work fine with one call to focus().
688 raw_input.focus();
691 raw_input.focus();
689 }
692 }
690
693
691 OutputArea.prototype._submit_raw_input = function (evt) {
694 OutputArea.prototype._submit_raw_input = function (evt) {
692 var container = this.element.find("div.raw_input");
695 var container = this.element.find("div.raw_input");
693 var theprompt = container.find("span.input_prompt");
696 var theprompt = container.find("span.input_prompt");
694 var theinput = container.find("input.raw_input");
697 var theinput = container.find("input.raw_input");
695 var value = theinput.val();
698 var value = theinput.val();
696 var content = {
699 var content = {
697 output_type : 'stream',
700 output_type : 'stream',
698 name : 'stdout',
701 name : 'stdout',
699 text : theprompt.text() + value + '\n'
702 text : theprompt.text() + value + '\n'
700 }
703 }
701 // remove form container
704 // remove form container
702 container.parent().remove();
705 container.parent().remove();
703 // replace with plaintext version in stdout
706 // replace with plaintext version in stdout
704 this.append_output(content, false);
707 this.append_output(content, false);
705 $([IPython.events]).trigger('send_input_reply.Kernel', value);
708 $([IPython.events]).trigger('send_input_reply.Kernel', value);
706 }
709 }
707
710
708
711
709 OutputArea.prototype.handle_clear_output = function (msg) {
712 OutputArea.prototype.handle_clear_output = function (msg) {
710 // msg spec v4 had stdout, stderr, display keys
713 // msg spec v4 had stdout, stderr, display keys
711 // v4.1 replaced these with just wait
714 // v4.1 replaced these with just wait
712 // The default behavior is the same (stdout=stderr=display=True, wait=False),
715 // The default behavior is the same (stdout=stderr=display=True, wait=False),
713 // so v4 messages will still be properly handled,
716 // so v4 messages will still be properly handled,
714 // except for the rarely used clearing less than all output.
717 // except for the rarely used clearing less than all output.
715 this.clear_output(msg.content.wait || false);
718 this.clear_output(msg.content.wait || false);
716 };
719 };
717
720
718
721
719 OutputArea.prototype.clear_output = function(wait) {
722 OutputArea.prototype.clear_output = function(wait) {
720 if (wait) {
723 if (wait) {
721
724
722 // If a clear is queued, clear before adding another to the queue.
725 // If a clear is queued, clear before adding another to the queue.
723 if (this.clear_queued) {
726 if (this.clear_queued) {
724 this.clear_output(false);
727 this.clear_output(false);
725 };
728 };
726
729
727 this.clear_queued = true;
730 this.clear_queued = true;
728 } else {
731 } else {
729
732
730 // Fix the output div's height if the clear_output is waiting for
733 // Fix the output div's height if the clear_output is waiting for
731 // new output (it is being used in an animation).
734 // new output (it is being used in an animation).
732 if (this.clear_queued) {
735 if (this.clear_queued) {
733 var height = this.element.height();
736 var height = this.element.height();
734 this.element.height(height);
737 this.element.height(height);
735 this.clear_queued = false;
738 this.clear_queued = false;
736 }
739 }
737
740
738 // clear all, no need for logic
741 // clear all, no need for logic
739 this.element.html("");
742 this.element.html("");
740 this.outputs = [];
743 this.outputs = [];
741 this.trusted = true;
744 this.trusted = true;
742 this.unscroll_area();
745 this.unscroll_area();
743 return;
746 return;
744 };
747 };
745 };
748 };
746
749
747
750
748 // JSON serialization
751 // JSON serialization
749
752
750 OutputArea.prototype.fromJSON = function (outputs) {
753 OutputArea.prototype.fromJSON = function (outputs) {
751 var len = outputs.length;
754 var len = outputs.length;
752 var data;
755 var data;
753
756
754 for (var i=0; i<len; i++) {
757 for (var i=0; i<len; i++) {
755 data = outputs[i];
758 data = outputs[i];
756 var msg_type = data.output_type;
759 var msg_type = data.output_type;
757 if (msg_type === "display_data" || msg_type === "pyout") {
760 if (msg_type === "display_data" || msg_type === "pyout") {
758 // convert short keys to mime keys
761 // convert short keys to mime keys
759 // TODO: remove mapping of short keys when we update to nbformat 4
762 // TODO: remove mapping of short keys when we update to nbformat 4
760 data = this.rename_keys(data, OutputArea.mime_map_r);
763 data = this.rename_keys(data, OutputArea.mime_map_r);
761 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
764 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map_r);
762 }
765 }
763
766
764 this.append_output(data);
767 this.append_output(data);
765 }
768 }
766 };
769 };
767
770
768
771
769 OutputArea.prototype.toJSON = function () {
772 OutputArea.prototype.toJSON = function () {
770 var outputs = [];
773 var outputs = [];
771 var len = this.outputs.length;
774 var len = this.outputs.length;
772 var data;
775 var data;
773 for (var i=0; i<len; i++) {
776 for (var i=0; i<len; i++) {
774 data = this.outputs[i];
777 data = this.outputs[i];
775 var msg_type = data.output_type;
778 var msg_type = data.output_type;
776 if (msg_type === "display_data" || msg_type === "pyout") {
779 if (msg_type === "display_data" || msg_type === "pyout") {
777 // convert mime keys to short keys
780 // convert mime keys to short keys
778 data = this.rename_keys(data, OutputArea.mime_map);
781 data = this.rename_keys(data, OutputArea.mime_map);
779 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
782 data.metadata = this.rename_keys(data.metadata, OutputArea.mime_map);
780 }
783 }
781 outputs[i] = data;
784 outputs[i] = data;
782 }
785 }
783 return outputs;
786 return outputs;
784 };
787 };
785
788
786 /**
789 /**
787 * Class properties
790 * Class properties
788 **/
791 **/
789
792
790 /**
793 /**
791 * Threshold to trigger autoscroll when the OutputArea is resized,
794 * Threshold to trigger autoscroll when the OutputArea is resized,
792 * typically when new outputs are added.
795 * typically when new outputs are added.
793 *
796 *
794 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
797 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
795 * unless it is < 0, in which case autoscroll will never be triggered
798 * unless it is < 0, in which case autoscroll will never be triggered
796 *
799 *
797 * @property auto_scroll_threshold
800 * @property auto_scroll_threshold
798 * @type Number
801 * @type Number
799 * @default 100
802 * @default 100
800 *
803 *
801 **/
804 **/
802 OutputArea.auto_scroll_threshold = 100;
805 OutputArea.auto_scroll_threshold = 100;
803
806
804 /**
807 /**
805 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
808 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
806 * shorter than this are never scrolled.
809 * shorter than this are never scrolled.
807 *
810 *
808 * @property minimum_scroll_threshold
811 * @property minimum_scroll_threshold
809 * @type Number
812 * @type Number
810 * @default 20
813 * @default 20
811 *
814 *
812 **/
815 **/
813 OutputArea.minimum_scroll_threshold = 20;
816 OutputArea.minimum_scroll_threshold = 20;
814
817
815
818
816
819
817 OutputArea.mime_map = {
820 OutputArea.mime_map = {
818 "text/plain" : "text",
821 "text/plain" : "text",
819 "text/html" : "html",
822 "text/html" : "html",
820 "image/svg+xml" : "svg",
823 "image/svg+xml" : "svg",
821 "image/png" : "png",
824 "image/png" : "png",
822 "image/jpeg" : "jpeg",
825 "image/jpeg" : "jpeg",
823 "text/latex" : "latex",
826 "text/latex" : "latex",
824 "application/json" : "json",
827 "application/json" : "json",
825 "application/javascript" : "javascript",
828 "application/javascript" : "javascript",
826 };
829 };
827
830
828 OutputArea.mime_map_r = {
831 OutputArea.mime_map_r = {
829 "text" : "text/plain",
832 "text" : "text/plain",
830 "html" : "text/html",
833 "html" : "text/html",
831 "svg" : "image/svg+xml",
834 "svg" : "image/svg+xml",
832 "png" : "image/png",
835 "png" : "image/png",
833 "jpeg" : "image/jpeg",
836 "jpeg" : "image/jpeg",
834 "latex" : "text/latex",
837 "latex" : "text/latex",
835 "json" : "application/json",
838 "json" : "application/json",
836 "javascript" : "application/javascript",
839 "javascript" : "application/javascript",
837 };
840 };
838
841
839 OutputArea.display_order = [
842 OutputArea.display_order = [
840 'application/javascript',
843 'application/javascript',
841 'text/html',
844 'text/html',
842 'text/latex',
845 'text/latex',
843 'image/svg+xml',
846 'image/svg+xml',
844 'image/png',
847 'image/png',
845 'image/jpeg',
848 'image/jpeg',
846 'application/pdf',
849 'application/pdf',
847 'text/plain'
850 'text/plain'
848 ];
851 ];
849
852
850 OutputArea.append_map = {
853 OutputArea.append_map = {
851 "text/plain" : OutputArea.prototype.append_text,
854 "text/plain" : OutputArea.prototype.append_text,
852 "text/html" : OutputArea.prototype.append_html,
855 "text/html" : OutputArea.prototype.append_html,
853 "image/svg+xml" : OutputArea.prototype.append_svg,
856 "image/svg+xml" : OutputArea.prototype.append_svg,
854 "image/png" : OutputArea.prototype.append_png,
857 "image/png" : OutputArea.prototype.append_png,
855 "image/jpeg" : OutputArea.prototype.append_jpeg,
858 "image/jpeg" : OutputArea.prototype.append_jpeg,
856 "text/latex" : OutputArea.prototype.append_latex,
859 "text/latex" : OutputArea.prototype.append_latex,
857 "application/javascript" : OutputArea.prototype.append_javascript,
860 "application/javascript" : OutputArea.prototype.append_javascript,
858 "application/pdf" : OutputArea.prototype.append_pdf
861 "application/pdf" : OutputArea.prototype.append_pdf
859 };
862 };
860
863
861 IPython.OutputArea = OutputArea;
864 IPython.OutputArea = OutputArea;
862
865
863 return IPython;
866 return IPython;
864
867
865 }(IPython));
868 }(IPython));
@@ -1,561 +1,550 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2008-2012 The IPython Development Team
2 // Copyright (C) 2008-2012 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 // TextCell
9 // TextCell
10 //============================================================================
10 //============================================================================
11
11
12
12
13
13
14 /**
14 /**
15 A module that allow to create different type of Text Cell
15 A module that allow to create different type of Text Cell
16 @module IPython
16 @module IPython
17 @namespace IPython
17 @namespace IPython
18 */
18 */
19 var IPython = (function (IPython) {
19 var IPython = (function (IPython) {
20 "use strict";
20 "use strict";
21
21
22 // TextCell base class
22 // TextCell base class
23 var keycodes = IPython.keyboard.keycodes;
23 var keycodes = IPython.keyboard.keycodes;
24 var security = IPython.security;
24
25
25 /**
26 /**
26 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
27 * cell start as not redered.
28 * cell start as not redered.
28 *
29 *
29 * @class TextCell
30 * @class TextCell
30 * @constructor TextCell
31 * @constructor TextCell
31 * @extend IPython.Cell
32 * @extend IPython.Cell
32 * @param {object|undefined} [options]
33 * @param {object|undefined} [options]
33 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
34 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
35 */
36 */
36 var TextCell = function (options) {
37 var TextCell = function (options) {
37 // in all TextCell/Cell subclasses
38 // in all TextCell/Cell subclasses
38 // do not assign most of members here, just pass it down
39 // do not assign most of members here, just pass it down
39 // in the options dict potentially overwriting what you wish.
40 // in the options dict potentially overwriting what you wish.
40 // they will be assigned in the base class.
41 // they will be assigned in the base class.
41
42
42 // we cannot put this as a class key as it has handle to "this".
43 // we cannot put this as a class key as it has handle to "this".
43 var cm_overwrite_options = {
44 var cm_overwrite_options = {
44 onKeyEvent: $.proxy(this.handle_keyevent,this)
45 onKeyEvent: $.proxy(this.handle_keyevent,this)
45 };
46 };
46
47
47 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
48
49
49 this.cell_type = this.cell_type || 'text';
50 this.cell_type = this.cell_type || 'text';
50
51
51 IPython.Cell.apply(this, [options]);
52 IPython.Cell.apply(this, [options]);
52
53
53 this.rendered = false;
54 this.rendered = false;
54 };
55 };
55
56
56 TextCell.prototype = new IPython.Cell();
57 TextCell.prototype = new IPython.Cell();
57
58
58 TextCell.options_default = {
59 TextCell.options_default = {
59 cm_config : {
60 cm_config : {
60 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
61 mode: 'htmlmixed',
62 mode: 'htmlmixed',
62 lineWrapping : true,
63 lineWrapping : true,
63 }
64 }
64 };
65 };
65
66
66
67
67 /**
68 /**
68 * Create the DOM element of the TextCell
69 * Create the DOM element of the TextCell
69 * @method create_element
70 * @method create_element
70 * @private
71 * @private
71 */
72 */
72 TextCell.prototype.create_element = function () {
73 TextCell.prototype.create_element = function () {
73 IPython.Cell.prototype.create_element.apply(this, arguments);
74 IPython.Cell.prototype.create_element.apply(this, arguments);
74
75
75 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
76 cell.attr('tabindex','2');
77 cell.attr('tabindex','2');
77
78
78 var prompt = $('<div/>').addClass('prompt input_prompt');
79 var prompt = $('<div/>').addClass('prompt input_prompt');
79 cell.append(prompt);
80 cell.append(prompt);
80 var inner_cell = $('<div/>').addClass('inner_cell');
81 var inner_cell = $('<div/>').addClass('inner_cell');
81 this.celltoolbar = new IPython.CellToolbar(this);
82 this.celltoolbar = new IPython.CellToolbar(this);
82 inner_cell.append(this.celltoolbar.element);
83 inner_cell.append(this.celltoolbar.element);
83 var input_area = $('<div/>').addClass('input_area');
84 var input_area = $('<div/>').addClass('input_area');
84 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
85 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
85 // The tabindex=-1 makes this div focusable.
86 // The tabindex=-1 makes this div focusable.
86 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
87 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
87 addClass('rendered_html').attr('tabindex','-1');
88 addClass('rendered_html').attr('tabindex','-1');
88 inner_cell.append(input_area).append(render_area);
89 inner_cell.append(input_area).append(render_area);
89 cell.append(inner_cell);
90 cell.append(inner_cell);
90 this.element = cell;
91 this.element = cell;
91 };
92 };
92
93
93
94
94 /**
95 /**
95 * Bind the DOM evet to cell actions
96 * Bind the DOM evet to cell actions
96 * Need to be called after TextCell.create_element
97 * Need to be called after TextCell.create_element
97 * @private
98 * @private
98 * @method bind_event
99 * @method bind_event
99 */
100 */
100 TextCell.prototype.bind_events = function () {
101 TextCell.prototype.bind_events = function () {
101 IPython.Cell.prototype.bind_events.apply(this);
102 IPython.Cell.prototype.bind_events.apply(this);
102 var that = this;
103 var that = this;
103
104
104 this.element.dblclick(function () {
105 this.element.dblclick(function () {
105 if (that.selected === false) {
106 if (that.selected === false) {
106 $([IPython.events]).trigger('select.Cell', {'cell':that});
107 $([IPython.events]).trigger('select.Cell', {'cell':that});
107 }
108 }
108 var cont = that.unrender();
109 var cont = that.unrender();
109 if (cont) {
110 if (cont) {
110 that.focus_editor();
111 that.focus_editor();
111 }
112 }
112 });
113 });
113 };
114 };
114
115
115 TextCell.prototype.handle_keyevent = function (editor, event) {
116 TextCell.prototype.handle_keyevent = function (editor, event) {
116
117
117 // console.log('CM', this.mode, event.which, event.type)
118 // console.log('CM', this.mode, event.which, event.type)
118
119
119 if (this.mode === 'command') {
120 if (this.mode === 'command') {
120 return true;
121 return true;
121 } else if (this.mode === 'edit') {
122 } else if (this.mode === 'edit') {
122 return this.handle_codemirror_keyevent(editor, event);
123 return this.handle_codemirror_keyevent(editor, event);
123 }
124 }
124 };
125 };
125
126
126 /**
127 /**
127 * This method gets called in CodeMirror's onKeyDown/onKeyPress
128 * This method gets called in CodeMirror's onKeyDown/onKeyPress
128 * handlers and is used to provide custom key handling.
129 * handlers and is used to provide custom key handling.
129 *
130 *
130 * Subclass should override this method to have custom handeling
131 * Subclass should override this method to have custom handeling
131 *
132 *
132 * @method handle_codemirror_keyevent
133 * @method handle_codemirror_keyevent
133 * @param {CodeMirror} editor - The codemirror instance bound to the cell
134 * @param {CodeMirror} editor - The codemirror instance bound to the cell
134 * @param {event} event -
135 * @param {event} event -
135 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
136 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
136 */
137 */
137 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
138 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
138 var that = this;
139 var that = this;
139
140
140 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
141 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey || event.altKey)) {
141 // Always ignore shift-enter in CodeMirror as we handle it.
142 // Always ignore shift-enter in CodeMirror as we handle it.
142 return true;
143 return true;
143 } else if (event.which === keycodes.up && event.type === 'keydown') {
144 } else if (event.which === keycodes.up && event.type === 'keydown') {
144 // If we are not at the top, let CM handle the up arrow and
145 // If we are not at the top, let CM handle the up arrow and
145 // prevent the global keydown handler from handling it.
146 // prevent the global keydown handler from handling it.
146 if (!that.at_top()) {
147 if (!that.at_top()) {
147 event.stop();
148 event.stop();
148 return false;
149 return false;
149 } else {
150 } else {
150 return true;
151 return true;
151 };
152 };
152 } else if (event.which === keycodes.down && event.type === 'keydown') {
153 } else if (event.which === keycodes.down && event.type === 'keydown') {
153 // If we are not at the bottom, let CM handle the down arrow and
154 // If we are not at the bottom, let CM handle the down arrow and
154 // prevent the global keydown handler from handling it.
155 // prevent the global keydown handler from handling it.
155 if (!that.at_bottom()) {
156 if (!that.at_bottom()) {
156 event.stop();
157 event.stop();
157 return false;
158 return false;
158 } else {
159 } else {
159 return true;
160 return true;
160 };
161 };
161 } else if (event.which === keycodes.esc && event.type === 'keydown') {
162 } else if (event.which === keycodes.esc && event.type === 'keydown') {
162 if (that.code_mirror.options.keyMap === "vim-insert") {
163 if (that.code_mirror.options.keyMap === "vim-insert") {
163 // vim keyMap is active and in insert mode. In this case we leave vim
164 // vim keyMap is active and in insert mode. In this case we leave vim
164 // insert mode, but remain in notebook edit mode.
165 // insert mode, but remain in notebook edit mode.
165 // Let' CM handle this event and prevent global handling.
166 // Let' CM handle this event and prevent global handling.
166 event.stop();
167 event.stop();
167 return false;
168 return false;
168 } else {
169 } else {
169 // vim keyMap is not active. Leave notebook edit mode.
170 // vim keyMap is not active. Leave notebook edit mode.
170 // Don't let CM handle the event, defer to global handling.
171 // Don't let CM handle the event, defer to global handling.
171 return true;
172 return true;
172 }
173 }
173 }
174 }
174 return false;
175 return false;
175 };
176 };
176
177
177 // Cell level actions
178 // Cell level actions
178
179
179 TextCell.prototype.select = function () {
180 TextCell.prototype.select = function () {
180 var cont = IPython.Cell.prototype.select.apply(this);
181 var cont = IPython.Cell.prototype.select.apply(this);
181 if (cont) {
182 if (cont) {
182 if (this.mode === 'edit') {
183 if (this.mode === 'edit') {
183 this.code_mirror.refresh();
184 this.code_mirror.refresh();
184 }
185 }
185 }
186 }
186 return cont;
187 return cont;
187 };
188 };
188
189
189 TextCell.prototype.unrender = function () {
190 TextCell.prototype.unrender = function () {
190 if (this.read_only) return;
191 if (this.read_only) return;
191 var cont = IPython.Cell.prototype.unrender.apply(this);
192 var cont = IPython.Cell.prototype.unrender.apply(this);
192 if (cont) {
193 if (cont) {
193 var text_cell = this.element;
194 var text_cell = this.element;
194 var output = text_cell.find("div.text_cell_render");
195 var output = text_cell.find("div.text_cell_render");
195 output.hide();
196 output.hide();
196 text_cell.find('div.input_area').show();
197 text_cell.find('div.input_area').show();
197 if (this.get_text() === this.placeholder) {
198 if (this.get_text() === this.placeholder) {
198 this.set_text('');
199 this.set_text('');
199 }
200 }
200 this.refresh();
201 this.refresh();
201 }
202 }
202 return cont;
203 return cont;
203 };
204 };
204
205
205 TextCell.prototype.execute = function () {
206 TextCell.prototype.execute = function () {
206 this.render();
207 this.render();
207 };
208 };
208
209
209 /**
210 /**
210 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
211 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
211 * @method get_text
212 * @method get_text
212 * @retrun {string} CodeMirror current text value
213 * @retrun {string} CodeMirror current text value
213 */
214 */
214 TextCell.prototype.get_text = function() {
215 TextCell.prototype.get_text = function() {
215 return this.code_mirror.getValue();
216 return this.code_mirror.getValue();
216 };
217 };
217
218
218 /**
219 /**
219 * @param {string} text - Codemiror text value
220 * @param {string} text - Codemiror text value
220 * @see TextCell#get_text
221 * @see TextCell#get_text
221 * @method set_text
222 * @method set_text
222 * */
223 * */
223 TextCell.prototype.set_text = function(text) {
224 TextCell.prototype.set_text = function(text) {
224 this.code_mirror.setValue(text);
225 this.code_mirror.setValue(text);
225 this.code_mirror.refresh();
226 this.code_mirror.refresh();
226 };
227 };
227
228
228 /**
229 /**
229 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
230 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
230 * @method get_rendered
231 * @method get_rendered
231 * @return {html} html of rendered element
232 * @return {html} html of rendered element
232 * */
233 * */
233 TextCell.prototype.get_rendered = function() {
234 TextCell.prototype.get_rendered = function() {
234 return this.element.find('div.text_cell_render').html();
235 return this.element.find('div.text_cell_render').html();
235 };
236 };
236
237
237 /**
238 /**
238 * @method set_rendered
239 * @method set_rendered
239 */
240 */
240 TextCell.prototype.set_rendered = function(text) {
241 TextCell.prototype.set_rendered = function(text) {
241 this.element.find('div.text_cell_render').html(text);
242 this.element.find('div.text_cell_render').html(text);
242 };
243 };
243
244
244 /**
245 /**
245 * @method at_top
246 * @method at_top
246 * @return {Boolean}
247 * @return {Boolean}
247 */
248 */
248 TextCell.prototype.at_top = function () {
249 TextCell.prototype.at_top = function () {
249 if (this.rendered) {
250 if (this.rendered) {
250 return true;
251 return true;
251 } else {
252 } else {
252 var cursor = this.code_mirror.getCursor();
253 var cursor = this.code_mirror.getCursor();
253 if (cursor.line === 0 && cursor.ch === 0) {
254 if (cursor.line === 0 && cursor.ch === 0) {
254 return true;
255 return true;
255 } else {
256 } else {
256 return false;
257 return false;
257 }
258 }
258 }
259 }
259 };
260 };
260
261
261 /**
262 /**
262 * @method at_bottom
263 * @method at_bottom
263 * @return {Boolean}
264 * @return {Boolean}
264 * */
265 * */
265 TextCell.prototype.at_bottom = function () {
266 TextCell.prototype.at_bottom = function () {
266 if (this.rendered) {
267 if (this.rendered) {
267 return true;
268 return true;
268 } else {
269 } else {
269 var cursor = this.code_mirror.getCursor();
270 var cursor = this.code_mirror.getCursor();
270 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
271 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
271 return true;
272 return true;
272 } else {
273 } else {
273 return false;
274 return false;
274 }
275 }
275 }
276 }
276 };
277 };
277
278
278 /**
279 /**
279 * Create Text cell from JSON
280 * Create Text cell from JSON
280 * @param {json} data - JSON serialized text-cell
281 * @param {json} data - JSON serialized text-cell
281 * @method fromJSON
282 * @method fromJSON
282 */
283 */
283 TextCell.prototype.fromJSON = function (data) {
284 TextCell.prototype.fromJSON = function (data) {
284 IPython.Cell.prototype.fromJSON.apply(this, arguments);
285 IPython.Cell.prototype.fromJSON.apply(this, arguments);
285 if (data.cell_type === this.cell_type) {
286 if (data.cell_type === this.cell_type) {
286 if (data.source !== undefined) {
287 if (data.source !== undefined) {
287 this.set_text(data.source);
288 this.set_text(data.source);
288 // make this value the starting point, so that we can only undo
289 // make this value the starting point, so that we can only undo
289 // to this state, instead of a blank cell
290 // to this state, instead of a blank cell
290 this.code_mirror.clearHistory();
291 this.code_mirror.clearHistory();
291 // TODO: This HTML needs to be treated as potentially dangerous
292 // TODO: This HTML needs to be treated as potentially dangerous
292 // user input and should be handled before set_rendered.
293 // user input and should be handled before set_rendered.
293 this.set_rendered(data.rendered || '');
294 this.set_rendered(data.rendered || '');
294 this.rendered = false;
295 this.rendered = false;
295 this.render();
296 this.render();
296 }
297 }
297 }
298 }
298 };
299 };
299
300
300 /** Generate JSON from cell
301 /** Generate JSON from cell
301 * @return {object} cell data serialised to json
302 * @return {object} cell data serialised to json
302 */
303 */
303 TextCell.prototype.toJSON = function () {
304 TextCell.prototype.toJSON = function () {
304 var data = IPython.Cell.prototype.toJSON.apply(this);
305 var data = IPython.Cell.prototype.toJSON.apply(this);
305 data.source = this.get_text();
306 data.source = this.get_text();
306 if (data.source == this.placeholder) {
307 if (data.source == this.placeholder) {
307 data.source = "";
308 data.source = "";
308 }
309 }
309 return data;
310 return data;
310 };
311 };
311
312
312
313
313 /**
314 /**
314 * @class MarkdownCell
315 * @class MarkdownCell
315 * @constructor MarkdownCell
316 * @constructor MarkdownCell
316 * @extends IPython.HTMLCell
317 * @extends IPython.HTMLCell
317 */
318 */
318 var MarkdownCell = function (options) {
319 var MarkdownCell = function (options) {
319 options = this.mergeopt(MarkdownCell, options);
320 options = this.mergeopt(MarkdownCell, options);
320
321
321 this.cell_type = 'markdown';
322 this.cell_type = 'markdown';
322 TextCell.apply(this, [options]);
323 TextCell.apply(this, [options]);
323 };
324 };
324
325
325 MarkdownCell.options_default = {
326 MarkdownCell.options_default = {
326 cm_config: {
327 cm_config: {
327 mode: 'gfm'
328 mode: 'gfm'
328 },
329 },
329 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
330 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
330 };
331 };
331
332
332 MarkdownCell.prototype = new TextCell();
333 MarkdownCell.prototype = new TextCell();
333
334
334 /**
335 /**
335 * @method render
336 * @method render
336 */
337 */
337 MarkdownCell.prototype.render = function () {
338 MarkdownCell.prototype.render = function () {
338 var cont = IPython.TextCell.prototype.render.apply(this);
339 var cont = IPython.TextCell.prototype.render.apply(this);
339 if (cont) {
340 if (cont) {
340 var text = this.get_text();
341 var text = this.get_text();
341 var math = null;
342 var math = null;
342 if (text === "") { text = this.placeholder; }
343 if (text === "") { text = this.placeholder; }
343 var text_and_math = IPython.mathjaxutils.remove_math(text);
344 var text_and_math = IPython.mathjaxutils.remove_math(text);
344 text = text_and_math[0];
345 text = text_and_math[0];
345 math = text_and_math[1];
346 math = text_and_math[1];
346 var html = marked.parser(marked.lexer(text));
347 var html = marked.parser(marked.lexer(text));
347 html = $(IPython.mathjaxutils.replace_math(html, math));
348 html = IPython.mathjaxutils.replace_math(html, math);
348 // Links in markdown cells should open in new tabs.
349 html = security.sanitize_html(html);
350 html = $(html);
351 // links in markdown cells should open in new tabs
349 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
352 html.find("a[href]").not('[href^="#"]').attr("target", "_blank");
350 try {
353 this.set_rendered(html);
351 // TODO: This HTML needs to be treated as potentially dangerous
352 // user input and should be handled before set_rendered.
353 this.set_rendered(html);
354 } catch (e) {
355 console.log("Error running Javascript in Markdown:");
356 console.log(e);
357 this.set_rendered(
358 $("<div/>")
359 .append($("<div/>").text('Error rendering Markdown!').addClass("js-error"))
360 .append($("<div/>").text(e.toString()).addClass("js-error"))
361 .html()
362 );
363 }
364 this.element.find('div.input_area').hide();
354 this.element.find('div.input_area').hide();
365 this.element.find("div.text_cell_render").show();
355 this.element.find("div.text_cell_render").show();
366 this.typeset();
356 this.typeset();
367 }
357 }
368 return cont;
358 return cont;
369 };
359 };
370
360
371
361
372 // RawCell
362 // RawCell
373
363
374 /**
364 /**
375 * @class RawCell
365 * @class RawCell
376 * @constructor RawCell
366 * @constructor RawCell
377 * @extends IPython.TextCell
367 * @extends IPython.TextCell
378 */
368 */
379 var RawCell = function (options) {
369 var RawCell = function (options) {
380
370
381 options = this.mergeopt(RawCell,options);
371 options = this.mergeopt(RawCell,options);
382 TextCell.apply(this, [options]);
372 TextCell.apply(this, [options]);
383 this.cell_type = 'raw';
373 this.cell_type = 'raw';
384 // RawCell should always hide its rendered div
374 // RawCell should always hide its rendered div
385 this.element.find('div.text_cell_render').hide();
375 this.element.find('div.text_cell_render').hide();
386 };
376 };
387
377
388 RawCell.options_default = {
378 RawCell.options_default = {
389 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
379 placeholder : "Write raw LaTeX or other formats here, for use with nbconvert.\n" +
390 "It will not be rendered in the notebook.\n" +
380 "It will not be rendered in the notebook.\n" +
391 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
381 "When passing through nbconvert, a Raw Cell's content is added to the output unmodified."
392 };
382 };
393
383
394 RawCell.prototype = new TextCell();
384 RawCell.prototype = new TextCell();
395
385
396 /** @method bind_events **/
386 /** @method bind_events **/
397 RawCell.prototype.bind_events = function () {
387 RawCell.prototype.bind_events = function () {
398 TextCell.prototype.bind_events.apply(this);
388 TextCell.prototype.bind_events.apply(this);
399 var that = this;
389 var that = this;
400 this.element.focusout(function() {
390 this.element.focusout(function() {
401 that.auto_highlight();
391 that.auto_highlight();
402 });
392 });
403 };
393 };
404
394
405 /**
395 /**
406 * Trigger autodetection of highlight scheme for current cell
396 * Trigger autodetection of highlight scheme for current cell
407 * @method auto_highlight
397 * @method auto_highlight
408 */
398 */
409 RawCell.prototype.auto_highlight = function () {
399 RawCell.prototype.auto_highlight = function () {
410 this._auto_highlight(IPython.config.raw_cell_highlight);
400 this._auto_highlight(IPython.config.raw_cell_highlight);
411 };
401 };
412
402
413 /** @method render **/
403 /** @method render **/
414 RawCell.prototype.render = function () {
404 RawCell.prototype.render = function () {
415 // Make sure that this cell type can never be rendered
405 // Make sure that this cell type can never be rendered
416 if (this.rendered) {
406 if (this.rendered) {
417 this.unrender();
407 this.unrender();
418 }
408 }
419 var text = this.get_text();
409 var text = this.get_text();
420 if (text === "") { text = this.placeholder; }
410 if (text === "") { text = this.placeholder; }
421 this.set_text(text);
411 this.set_text(text);
422 };
412 };
423
413
424
414
425 /**
415 /**
426 * @class HeadingCell
416 * @class HeadingCell
427 * @extends IPython.TextCell
417 * @extends IPython.TextCell
428 */
418 */
429
419
430 /**
420 /**
431 * @constructor HeadingCell
421 * @constructor HeadingCell
432 * @extends IPython.TextCell
422 * @extends IPython.TextCell
433 */
423 */
434 var HeadingCell = function (options) {
424 var HeadingCell = function (options) {
435 options = this.mergeopt(HeadingCell, options);
425 options = this.mergeopt(HeadingCell, options);
436
426
437 this.level = 1;
427 this.level = 1;
438 this.cell_type = 'heading';
428 this.cell_type = 'heading';
439 TextCell.apply(this, [options]);
429 TextCell.apply(this, [options]);
440
430
441 /**
431 /**
442 * heading level of the cell, use getter and setter to access
432 * heading level of the cell, use getter and setter to access
443 * @property level
433 * @property level
444 */
434 */
445 };
435 };
446
436
447 HeadingCell.options_default = {
437 HeadingCell.options_default = {
448 placeholder: "Type Heading Here"
438 placeholder: "Type Heading Here"
449 };
439 };
450
440
451 HeadingCell.prototype = new TextCell();
441 HeadingCell.prototype = new TextCell();
452
442
453 /** @method fromJSON */
443 /** @method fromJSON */
454 HeadingCell.prototype.fromJSON = function (data) {
444 HeadingCell.prototype.fromJSON = function (data) {
455 if (data.level !== undefined){
445 if (data.level !== undefined){
456 this.level = data.level;
446 this.level = data.level;
457 }
447 }
458 TextCell.prototype.fromJSON.apply(this, arguments);
448 TextCell.prototype.fromJSON.apply(this, arguments);
459 };
449 };
460
450
461
451
462 /** @method toJSON */
452 /** @method toJSON */
463 HeadingCell.prototype.toJSON = function () {
453 HeadingCell.prototype.toJSON = function () {
464 var data = TextCell.prototype.toJSON.apply(this);
454 var data = TextCell.prototype.toJSON.apply(this);
465 data.level = this.get_level();
455 data.level = this.get_level();
466 return data;
456 return data;
467 };
457 };
468
458
469 /**
459 /**
470 * can the cell be split into two cells
460 * can the cell be split into two cells
471 * @method is_splittable
461 * @method is_splittable
472 **/
462 **/
473 HeadingCell.prototype.is_splittable = function () {
463 HeadingCell.prototype.is_splittable = function () {
474 return false;
464 return false;
475 };
465 };
476
466
477
467
478 /**
468 /**
479 * can the cell be merged with other cells
469 * can the cell be merged with other cells
480 * @method is_mergeable
470 * @method is_mergeable
481 **/
471 **/
482 HeadingCell.prototype.is_mergeable = function () {
472 HeadingCell.prototype.is_mergeable = function () {
483 return false;
473 return false;
484 };
474 };
485
475
486 /**
476 /**
487 * Change heading level of cell, and re-render
477 * Change heading level of cell, and re-render
488 * @method set_level
478 * @method set_level
489 */
479 */
490 HeadingCell.prototype.set_level = function (level) {
480 HeadingCell.prototype.set_level = function (level) {
491 this.level = level;
481 this.level = level;
492 if (this.rendered) {
482 if (this.rendered) {
493 this.rendered = false;
483 this.rendered = false;
494 this.render();
484 this.render();
495 }
485 }
496 };
486 };
497
487
498 /** The depth of header cell, based on html (h1 to h6)
488 /** The depth of header cell, based on html (h1 to h6)
499 * @method get_level
489 * @method get_level
500 * @return {integer} level - for 1 to 6
490 * @return {integer} level - for 1 to 6
501 */
491 */
502 HeadingCell.prototype.get_level = function () {
492 HeadingCell.prototype.get_level = function () {
503 return this.level;
493 return this.level;
504 };
494 };
505
495
506
496
507 HeadingCell.prototype.set_rendered = function (html) {
497 HeadingCell.prototype.set_rendered = function (html) {
508 this.element.find("div.text_cell_render").html(html);
498 this.element.find("div.text_cell_render").html(html);
509 };
499 };
510
500
511
501
512 HeadingCell.prototype.get_rendered = function () {
502 HeadingCell.prototype.get_rendered = function () {
513 var r = this.element.find("div.text_cell_render");
503 var r = this.element.find("div.text_cell_render");
514 return r.children().first().html();
504 return r.children().first().html();
515 };
505 };
516
506
517
507
518 HeadingCell.prototype.render = function () {
508 HeadingCell.prototype.render = function () {
519 var cont = IPython.TextCell.prototype.render.apply(this);
509 var cont = IPython.TextCell.prototype.render.apply(this);
520 if (cont) {
510 if (cont) {
521 var text = this.get_text();
511 var text = this.get_text();
522 var math = null;
512 var math = null;
523 // Markdown headings must be a single line
513 // Markdown headings must be a single line
524 text = text.replace(/\n/g, ' ');
514 text = text.replace(/\n/g, ' ');
525 if (text === "") { text = this.placeholder; }
515 if (text === "") { text = this.placeholder; }
526 text = Array(this.level + 1).join("#") + " " + text;
516 text = Array(this.level + 1).join("#") + " " + text;
527 var text_and_math = IPython.mathjaxutils.remove_math(text);
517 var text_and_math = IPython.mathjaxutils.remove_math(text);
528 text = text_and_math[0];
518 text = text_and_math[0];
529 math = text_and_math[1];
519 math = text_and_math[1];
530 var html = marked.parser(marked.lexer(text));
520 var html = marked.parser(marked.lexer(text));
531 var h = $(IPython.mathjaxutils.replace_math(html, math));
521 html = IPython.mathjaxutils.replace_math(html, math);
522 html = security.sanitize_html(html);
523 var h = $(html);
532 // add id and linkback anchor
524 // add id and linkback anchor
533 var hash = h.text().replace(/ /g, '-');
525 var hash = h.text().replace(/ /g, '-');
534 h.attr('id', hash);
526 h.attr('id', hash);
535 h.append(
527 h.append(
536 $('<a/>')
528 $('<a/>')
537 .addClass('anchor-link')
529 .addClass('anchor-link')
538 .attr('href', '#' + hash)
530 .attr('href', '#' + hash)
539 .text('ΒΆ')
531 .text('ΒΆ')
540 );
532 );
541 // TODO: This HTML needs to be treated as potentially dangerous
542 // user input and should be handled before set_rendered.
543 this.set_rendered(h);
533 this.set_rendered(h);
544 this.typeset();
534 this.element.find('div.text_cell_input').hide();
545 this.element.find('div.input_area').hide();
546 this.element.find("div.text_cell_render").show();
535 this.element.find("div.text_cell_render").show();
547
536 this.typeset();
548 }
537 }
549 return cont;
538 return cont;
550 };
539 };
551
540
552 IPython.TextCell = TextCell;
541 IPython.TextCell = TextCell;
553 IPython.MarkdownCell = MarkdownCell;
542 IPython.MarkdownCell = MarkdownCell;
554 IPython.RawCell = RawCell;
543 IPython.RawCell = RawCell;
555 IPython.HeadingCell = HeadingCell;
544 IPython.HeadingCell = HeadingCell;
556
545
557
546
558 return IPython;
547 return IPython;
559
548
560 }(IPython));
549 }(IPython));
561
550
@@ -1,10 +1,10 b''
1 @import "variables.less";
1 @import "variables.less";
2 @import "ansicolors.less";
2 @import "ansicolors.less";
3 @import "cell.less";
3 @import "cell.less";
4 @import "codecell.less";
4 @import "codecell.less";
5 @import "codemirror.less";
5 @import "codemirror.less";
6 @import "highlight.less";
6 @import "highlight.less";
7 @import "outputarea.less";
7 @import "outputarea.less";
8 @import "renderedhtml.less";
8 @import "renderedhtml.less";
9 @import "textcell.less";
9 @import "textcell.less";
10 @import "widgets.less";
10 @import "../../widgets/less/widgets.less";
@@ -1,353 +1,358 b''
1 {% extends "page.html" %}
1 {% extends "page.html" %}
2
2
3 {% block stylesheet %}
3 {% block stylesheet %}
4
4
5 {% if mathjax_url %}
5 {% if mathjax_url %}
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
6 <script type="text/javascript" src="{{mathjax_url}}?config=TeX-AMS_HTML-full&delayStartupUntil=configured" charset="utf-8"></script>
7 {% endif %}
7 {% endif %}
8 <script type="text/javascript">
8 <script type="text/javascript">
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
9 // MathJax disabled, set as null to distingish from *missing* MathJax,
10 // where it will be undefined, and should prompt a dialog later.
10 // where it will be undefined, and should prompt a dialog later.
11 window.mathjax_url = "{{mathjax_url}}";
11 window.mathjax_url = "{{mathjax_url}}";
12 </script>
12 </script>
13
13
14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
14 <link rel="stylesheet" href="{{ static_url("components/codemirror/lib/codemirror.css") }}">
15
15
16 {{super()}}
16 {{super()}}
17
17
18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
18 <link rel="stylesheet" href="{{ static_url("notebook/css/override.css") }}" type="text/css" />
19
19
20 {% endblock %}
20 {% endblock %}
21
21
22 {% block params %}
22 {% block params %}
23
23
24 data-project="{{project}}"
24 data-project="{{project}}"
25 data-base-url="{{base_url}}"
25 data-base-url="{{base_url}}"
26 data-notebook-name="{{notebook_name}}"
26 data-notebook-name="{{notebook_name}}"
27 data-notebook-path="{{notebook_path}}"
27 data-notebook-path="{{notebook_path}}"
28 class="notebook_app"
28 class="notebook_app"
29
29
30 {% endblock %}
30 {% endblock %}
31
31
32
32
33 {% block header %}
33 {% block header %}
34
34
35 <span id="save_widget" class="nav pull-left">
35 <span id="save_widget" class="nav pull-left">
36 <span id="notebook_name"></span>
36 <span id="notebook_name"></span>
37 <span id="checkpoint_status"></span>
37 <span id="checkpoint_status"></span>
38 <span id="autosave_status"></span>
38 <span id="autosave_status"></span>
39 </span>
39 </span>
40
40
41 {% endblock %}
41 {% endblock %}
42
42
43
43
44 {% block site %}
44 {% block site %}
45
45
46 <div id="menubar-container" class="container">
46 <div id="menubar-container" class="container">
47 <div id="menubar">
47 <div id="menubar">
48 <div class="navbar">
48 <div class="navbar">
49 <div class="navbar-inner">
49 <div class="navbar-inner">
50 <div class="container">
50 <div class="container">
51 <ul id="menus" class="nav">
51 <ul id="menus" class="nav">
52 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
52 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">File</a>
53 <ul id="file_menu" class="dropdown-menu">
53 <ul id="file_menu" class="dropdown-menu">
54 <li id="new_notebook"
54 <li id="new_notebook"
55 title="Make a new notebook (Opens a new window)">
55 title="Make a new notebook (Opens a new window)">
56 <a href="#">New</a></li>
56 <a href="#">New</a></li>
57 <li id="open_notebook"
57 <li id="open_notebook"
58 title="Opens a new window with the Dashboard view">
58 title="Opens a new window with the Dashboard view">
59 <a href="#">Open...</a></li>
59 <a href="#">Open...</a></li>
60 <!-- <hr/> -->
60 <!-- <hr/> -->
61 <li class="divider"></li>
61 <li class="divider"></li>
62 <li id="copy_notebook"
62 <li id="copy_notebook"
63 title="Open a copy of this notebook's contents and start a new kernel">
63 title="Open a copy of this notebook's contents and start a new kernel">
64 <a href="#">Make a Copy...</a></li>
64 <a href="#">Make a Copy...</a></li>
65 <li id="rename_notebook"><a href="#">Rename...</a></li>
65 <li id="rename_notebook"><a href="#">Rename...</a></li>
66 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
66 <li id="save_checkpoint"><a href="#">Save and Checkpoint</a></li>
67 <!-- <hr/> -->
67 <!-- <hr/> -->
68 <li class="divider"></li>
68 <li class="divider"></li>
69 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
69 <li id="restore_checkpoint" class="dropdown-submenu"><a href="#">Revert to Checkpoint</a>
70 <ul class="dropdown-menu">
70 <ul class="dropdown-menu">
71 <li><a href="#"></a></li>
71 <li><a href="#"></a></li>
72 <li><a href="#"></a></li>
72 <li><a href="#"></a></li>
73 <li><a href="#"></a></li>
73 <li><a href="#"></a></li>
74 <li><a href="#"></a></li>
74 <li><a href="#"></a></li>
75 <li><a href="#"></a></li>
75 <li><a href="#"></a></li>
76 </ul>
76 </ul>
77 </li>
77 </li>
78 <li class="divider"></li>
78 <li class="divider"></li>
79 <li id="print_preview"><a href="#">Print Preview</a></li>
79 <li id="print_preview"><a href="#">Print Preview</a></li>
80 <li class="dropdown-submenu"><a href="#">Download as</a>
80 <li class="dropdown-submenu"><a href="#">Download as</a>
81 <ul class="dropdown-menu">
81 <ul class="dropdown-menu">
82 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
82 <li id="download_ipynb"><a href="#">IPython Notebook (.ipynb)</a></li>
83 <li id="download_py"><a href="#">Python (.py)</a></li>
83 <li id="download_py"><a href="#">Python (.py)</a></li>
84 <li id="download_html"><a href="#">HTML (.html)</a></li>
84 <li id="download_html"><a href="#">HTML (.html)</a></li>
85 <li id="download_rst"><a href="#">reST (.rst)</a></li>
85 <li id="download_rst"><a href="#">reST (.rst)</a></li>
86 </ul>
86 </ul>
87 </li>
87 </li>
88 <li class="divider"></li>
88 <li class="divider"></li>
89
89 <li id="trust_notebook"
90 title="Trust the output of this notebook">
91 <a href="#" >Trust Notebook</a></li>
92 <li class="divider"></li>
90 <li id="kill_and_exit"
93 <li id="kill_and_exit"
91 title="Shutdown this notebook's kernel, and close this window">
94 title="Shutdown this notebook's kernel, and close this window">
92 <a href="#" >Close and halt</a></li>
95 <a href="#" >Close and halt</a></li>
93 </ul>
96 </ul>
94 </li>
97 </li>
95 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
98 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit</a>
96 <ul id="edit_menu" class="dropdown-menu">
99 <ul id="edit_menu" class="dropdown-menu">
97 <li id="cut_cell"><a href="#">Cut Cell</a></li>
100 <li id="cut_cell"><a href="#">Cut Cell</a></li>
98 <li id="copy_cell"><a href="#">Copy Cell</a></li>
101 <li id="copy_cell"><a href="#">Copy Cell</a></li>
99 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
102 <li id="paste_cell_above" class="disabled"><a href="#">Paste Cell Above</a></li>
100 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
103 <li id="paste_cell_below" class="disabled"><a href="#">Paste Cell Below</a></li>
101 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
104 <li id="paste_cell_replace" class="disabled"><a href="#">Paste Cell &amp; Replace</a></li>
102 <li id="delete_cell"><a href="#">Delete Cell</a></li>
105 <li id="delete_cell"><a href="#">Delete Cell</a></li>
103 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
106 <li id="undelete_cell" class="disabled"><a href="#">Undo Delete Cell</a></li>
104 <li class="divider"></li>
107 <li class="divider"></li>
105 <li id="split_cell"><a href="#">Split Cell</a></li>
108 <li id="split_cell"><a href="#">Split Cell</a></li>
106 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
109 <li id="merge_cell_above"><a href="#">Merge Cell Above</a></li>
107 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
110 <li id="merge_cell_below"><a href="#">Merge Cell Below</a></li>
108 <li class="divider"></li>
111 <li class="divider"></li>
109 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
112 <li id="move_cell_up"><a href="#">Move Cell Up</a></li>
110 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
113 <li id="move_cell_down"><a href="#">Move Cell Down</a></li>
111 <li class="divider"></li>
114 <li class="divider"></li>
112 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
115 <li id="edit_nb_metadata"><a href="#">Edit Notebook Metadata</a></li>
113 </ul>
116 </ul>
114 </li>
117 </li>
115 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
118 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">View</a>
116 <ul id="view_menu" class="dropdown-menu">
119 <ul id="view_menu" class="dropdown-menu">
117 <li id="toggle_header"
120 <li id="toggle_header"
118 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
121 title="Show/Hide the IPython Notebook logo and notebook title (above menu bar)">
119 <a href="#">Toggle Header</a></li>
122 <a href="#">Toggle Header</a></li>
120 <li id="toggle_toolbar"
123 <li id="toggle_toolbar"
121 title="Show/Hide the action icons (below menu bar)">
124 title="Show/Hide the action icons (below menu bar)">
122 <a href="#">Toggle Toolbar</a></li>
125 <a href="#">Toggle Toolbar</a></li>
123 </ul>
126 </ul>
124 </li>
127 </li>
125 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
128 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Insert</a>
126 <ul id="insert_menu" class="dropdown-menu">
129 <ul id="insert_menu" class="dropdown-menu">
127 <li id="insert_cell_above"
130 <li id="insert_cell_above"
128 title="Insert an empty Code cell above the currently active cell">
131 title="Insert an empty Code cell above the currently active cell">
129 <a href="#">Insert Cell Above</a></li>
132 <a href="#">Insert Cell Above</a></li>
130 <li id="insert_cell_below"
133 <li id="insert_cell_below"
131 title="Insert an empty Code cell below the currently active cell">
134 title="Insert an empty Code cell below the currently active cell">
132 <a href="#">Insert Cell Below</a></li>
135 <a href="#">Insert Cell Below</a></li>
133 </ul>
136 </ul>
134 </li>
137 </li>
135 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
138 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Cell</a>
136 <ul id="cell_menu" class="dropdown-menu">
139 <ul id="cell_menu" class="dropdown-menu">
137 <li id="run_cell" title="Run this cell, and move cursor to the next one">
140 <li id="run_cell" title="Run this cell, and move cursor to the next one">
138 <a href="#">Run</a></li>
141 <a href="#">Run</a></li>
139 <li id="run_cell_select_below" title="Run this cell, select below">
142 <li id="run_cell_select_below" title="Run this cell, select below">
140 <a href="#">Run and Select Below</a></li>
143 <a href="#">Run and Select Below</a></li>
141 <li id="run_cell_insert_below" title="Run this cell, insert below">
144 <li id="run_cell_insert_below" title="Run this cell, insert below">
142 <a href="#">Run and Insert Below</a></li>
145 <a href="#">Run and Insert Below</a></li>
143 <li id="run_all_cells" title="Run all cells in the notebook">
146 <li id="run_all_cells" title="Run all cells in the notebook">
144 <a href="#">Run All</a></li>
147 <a href="#">Run All</a></li>
145 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
148 <li id="run_all_cells_above" title="Run all cells above (but not including) this cell">
146 <a href="#">Run All Above</a></li>
149 <a href="#">Run All Above</a></li>
147 <li id="run_all_cells_below" title="Run this cell and all cells below it">
150 <li id="run_all_cells_below" title="Run this cell and all cells below it">
148 <a href="#">Run All Below</a></li>
151 <a href="#">Run All Below</a></li>
149 <li class="divider"></li>
152 <li class="divider"></li>
150 <li id="change_cell_type" class="dropdown-submenu"
153 <li id="change_cell_type" class="dropdown-submenu"
151 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
154 title="All cells in the notebook have a cell type. By default, new cells are created as 'Code' cells">
152 <a href="#">Cell Type</a>
155 <a href="#">Cell Type</a>
153 <ul class="dropdown-menu">
156 <ul class="dropdown-menu">
154 <li id="to_code"
157 <li id="to_code"
155 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
158 title="Contents will be sent to the kernel for execution, and output will display in the footer of cell">
156 <a href="#">Code</a></li>
159 <a href="#">Code</a></li>
157 <li id="to_markdown"
160 <li id="to_markdown"
158 title="Contents will be rendered as HTML and serve as explanatory text">
161 title="Contents will be rendered as HTML and serve as explanatory text">
159 <a href="#">Markdown</a></li>
162 <a href="#">Markdown</a></li>
160 <li id="to_raw"
163 <li id="to_raw"
161 title="Contents will pass through nbconvert unmodified">
164 title="Contents will pass through nbconvert unmodified">
162 <a href="#">Raw NBConvert</a></li>
165 <a href="#">Raw NBConvert</a></li>
163 <li id="to_heading1"><a href="#">Heading 1</a></li>
166 <li id="to_heading1"><a href="#">Heading 1</a></li>
164 <li id="to_heading2"><a href="#">Heading 2</a></li>
167 <li id="to_heading2"><a href="#">Heading 2</a></li>
165 <li id="to_heading3"><a href="#">Heading 3</a></li>
168 <li id="to_heading3"><a href="#">Heading 3</a></li>
166 <li id="to_heading4"><a href="#">Heading 4</a></li>
169 <li id="to_heading4"><a href="#">Heading 4</a></li>
167 <li id="to_heading5"><a href="#">Heading 5</a></li>
170 <li id="to_heading5"><a href="#">Heading 5</a></li>
168 <li id="to_heading6"><a href="#">Heading 6</a></li>
171 <li id="to_heading6"><a href="#">Heading 6</a></li>
169 </ul>
172 </ul>
170 </li>
173 </li>
171 <li class="divider"></li>
174 <li class="divider"></li>
172 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
175 <li id="current_outputs" class="dropdown-submenu"><a href="#">Current Output</a>
173 <ul class="dropdown-menu">
176 <ul class="dropdown-menu">
174 <li id="toggle_current_output"
177 <li id="toggle_current_output"
175 title="Hide/Show the output of the current cell">
178 title="Hide/Show the output of the current cell">
176 <a href="#">Toggle</a>
179 <a href="#">Toggle</a>
177 </li>
180 </li>
178 <li id="toggle_current_output_scroll"
181 <li id="toggle_current_output_scroll"
179 title="Scroll the output of the current cell">
182 title="Scroll the output of the current cell">
180 <a href="#">Toggle Scrolling</a>
183 <a href="#">Toggle Scrolling</a>
181 </li>
184 </li>
182 <li id="clear_current_output"
185 <li id="clear_current_output"
183 title="Clear the output of the current cell">
186 title="Clear the output of the current cell">
184 <a href="#">Clear</a>
187 <a href="#">Clear</a>
185 </li>
188 </li>
186 </ul>
189 </ul>
187 </li>
190 </li>
188 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
191 <li id="all_outputs" class="dropdown-submenu"><a href="#">All Output</a>
189 <ul class="dropdown-menu">
192 <ul class="dropdown-menu">
190 <li id="toggle_all_output"
193 <li id="toggle_all_output"
191 title="Hide/Show the output of all cells">
194 title="Hide/Show the output of all cells">
192 <a href="#">Toggle</a>
195 <a href="#">Toggle</a>
193 </li>
196 </li>
194 <li id="toggle_all_output_scroll"
197 <li id="toggle_all_output_scroll"
195 title="Scroll the output of all cells">
198 title="Scroll the output of all cells">
196 <a href="#">Toggle Scrolling</a>
199 <a href="#">Toggle Scrolling</a>
197 </li>
200 </li>
198 <li id="clear_all_output"
201 <li id="clear_all_output"
199 title="Clear the output of all cells">
202 title="Clear the output of all cells">
200 <a href="#">Clear</a>
203 <a href="#">Clear</a>
201 </li>
204 </li>
202 </ul>
205 </ul>
203 </li>
206 </li>
204 </ul>
207 </ul>
205 </li>
208 </li>
206 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
209 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Kernel</a>
207 <ul id="kernel_menu" class="dropdown-menu">
210 <ul id="kernel_menu" class="dropdown-menu">
208 <li id="int_kernel"
211 <li id="int_kernel"
209 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
212 title="Send KeyboardInterrupt (CTRL-C) to the Kernel">
210 <a href="#">Interrupt</a></li>
213 <a href="#">Interrupt</a></li>
211 <li id="restart_kernel"
214 <li id="restart_kernel"
212 title="Restart the Kernel">
215 title="Restart the Kernel">
213 <a href="#">Restart</a></li>
216 <a href="#">Restart</a></li>
214 </ul>
217 </ul>
215 </li>
218 </li>
216 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
219 <li class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
217 <ul id="help_menu" class="dropdown-menu">
220 <ul id="help_menu" class="dropdown-menu">
218 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
221 <li id="keyboard_shortcuts" title="Opens a tooltip with all keyboard shortcuts"><a href="#">Keyboard Shortcuts</a></li>
219 <li class="divider"></li>
222 <li class="divider"></li>
220 {% set
223 {% set
221 sections = (
224 sections = (
222 (
225 (
223 ("http://ipython.org/documentation.html","IPython Help",True),
226 ("http://ipython.org/documentation.html","IPython Help",True),
224 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
227 ("http://nbviewer.ipython.org/github/ipython/ipython/tree/master/examples/notebooks/", "Notebook Examples", True),
225 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
228 ("http://ipython.org/ipython-doc/stable/interactive/notebook.html","Notebook Help",True),
226 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
229 ("http://ipython.org/ipython-doc/dev/interactive/cm_keyboard.html","Editor Shortcuts",True),
227 ),(
230 ),(
228 ("http://docs.python.org","Python",True),
231 ("http://docs.python.org","Python",True),
229 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
232 ("http://docs.scipy.org/doc/numpy/reference/","NumPy",True),
230 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
233 ("http://docs.scipy.org/doc/scipy/reference/","SciPy",True),
231 ("http://matplotlib.org/contents.html","Matplotlib",True),
234 ("http://matplotlib.org/contents.html","Matplotlib",True),
232 ("http://docs.sympy.org/dev/index.html","SymPy",True),
235 ("http://docs.sympy.org/dev/index.html","SymPy",True),
233 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
236 ("http://pandas.pydata.org/pandas-docs/stable/","pandas", True)
234 )
237 )
235 )
238 )
236 %}
239 %}
237
240
238 {% for helplinks in sections %}
241 {% for helplinks in sections %}
239 {% for link in helplinks %}
242 {% for link in helplinks %}
240 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
243 <li><a href="{{link[0]}}" {{'target="_blank" title="Opens in a new window"' if link[2]}}>
241 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
244 {{'<i class="icon-external-link menu-icon pull-right"></i>' if link[2]}}
242 {{link[1]}}
245 {{link[1]}}
243 </a></li>
246 </a></li>
244 {% endfor %}
247 {% endfor %}
245 {% if not loop.last %}
248 {% if not loop.last %}
246 <li class="divider"></li>
249 <li class="divider"></li>
247 {% endif %}
250 {% endif %}
248 {% endfor %}
251 {% endfor %}
249 </li>
252 </li>
250 </ul>
253 </ul>
251 </li>
254 </li>
252 </ul>
255 </ul>
253 <div id="kernel_indicator" class="indicator_area pull-right">
256 <div id="kernel_indicator" class="indicator_area pull-right">
254 <i id="kernel_indicator_icon"></i>
257 <i id="kernel_indicator_icon"></i>
255 </div>
258 </div>
256 <div id="modal_indicator" class="indicator_area pull-right">
259 <div id="modal_indicator" class="indicator_area pull-right">
257 <i id="modal_indicator_icon"></i>
260 <i id="modal_indicator_icon"></i>
258 </div>
261 </div>
259 <div id="notification_area"></div>
262 <div id="notification_area"></div>
260 </div>
263 </div>
261 </div>
264 </div>
262 </div>
265 </div>
263 </div>
266 </div>
264 <div id="maintoolbar" class="navbar">
267 <div id="maintoolbar" class="navbar">
265 <div class="toolbar-inner navbar-inner navbar-nobg">
268 <div class="toolbar-inner navbar-inner navbar-nobg">
266 <div id="maintoolbar-container" class="container"></div>
269 <div id="maintoolbar-container" class="container"></div>
267 </div>
270 </div>
268 </div>
271 </div>
269 </div>
272 </div>
270
273
271 <div id="ipython-main-app">
274 <div id="ipython-main-app">
272
275
273 <div id="notebook_panel">
276 <div id="notebook_panel">
274 <div id="notebook"></div>
277 <div id="notebook"></div>
275 <div id="pager_splitter"></div>
278 <div id="pager_splitter"></div>
276 <div id="pager">
279 <div id="pager">
277 <div id='pager_button_area'>
280 <div id='pager_button_area'>
278 </div>
281 </div>
279 <div id="pager-container" class="container"></div>
282 <div id="pager-container" class="container"></div>
280 </div>
283 </div>
281 </div>
284 </div>
282
285
283 </div>
286 </div>
284 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
287 <div id='tooltip' class='ipython_tooltip' style='display:none'></div>
285
288
286
289
287 {% endblock %}
290 {% endblock %}
288
291
289
292
290 {% block script %}
293 {% block script %}
291
294
292 {{super()}}
295 {{super()}}
293
296
297 <script src="{{ static_url("components/google-caja/html-css-sanitizer-minified.js") }}" charset="utf-8"></script>
294 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
298 <script src="{{ static_url("components/codemirror/lib/codemirror.js") }}" charset="utf-8"></script>
295 <script type="text/javascript">
299 <script type="text/javascript">
296 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
300 CodeMirror.modeURL = "{{ static_url("components/codemirror/mode/%N/%N.js", include_version=False) }}";
297 </script>
301 </script>
298 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
302 <script src="{{ static_url("components/codemirror/addon/mode/loadmode.js") }}" charset="utf-8"></script>
299 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
303 <script src="{{ static_url("components/codemirror/addon/mode/multiplex.js") }}" charset="utf-8"></script>
300 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
304 <script src="{{ static_url("components/codemirror/addon/mode/overlay.js") }}" charset="utf-8"></script>
301 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
305 <script src="{{ static_url("components/codemirror/addon/edit/matchbrackets.js") }}" charset="utf-8"></script>
302 <script src="{{ static_url("components/codemirror/addon/edit/closebrackets.js") }}" charset="utf-8"></script>
306 <script src="{{ static_url("components/codemirror/addon/edit/closebrackets.js") }}" charset="utf-8"></script>
303 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
307 <script src="{{ static_url("components/codemirror/addon/comment/comment.js") }}" charset="utf-8"></script>
304 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
308 <script src="{{ static_url("components/codemirror/mode/htmlmixed/htmlmixed.js") }}" charset="utf-8"></script>
305 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
309 <script src="{{ static_url("components/codemirror/mode/xml/xml.js") }}" charset="utf-8"></script>
306 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
310 <script src="{{ static_url("components/codemirror/mode/javascript/javascript.js") }}" charset="utf-8"></script>
307 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
311 <script src="{{ static_url("components/codemirror/mode/css/css.js") }}" charset="utf-8"></script>
308 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
312 <script src="{{ static_url("components/codemirror/mode/rst/rst.js") }}" charset="utf-8"></script>
309 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
313 <script src="{{ static_url("components/codemirror/mode/markdown/markdown.js") }}" charset="utf-8"></script>
310 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
314 <script src="{{ static_url("components/codemirror/mode/gfm/gfm.js") }}" charset="utf-8"></script>
311 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
315 <script src="{{ static_url("components/codemirror/mode/python/python.js") }}" charset="utf-8"></script>
312 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
316 <script src="{{ static_url("notebook/js/codemirror-ipython.js") }}" charset="utf-8"></script>
313
317
314 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
318 <script src="{{ static_url("components/highlight.js/build/highlight.pack.js") }}" charset="utf-8"></script>
315
319
316 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
320 <script src="{{ static_url("dateformat/date.format.js") }}" charset="utf-8"></script>
317
321
318 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("base/js/events.js") }}" type="text/javascript" charset="utf-8"></script>
319 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("base/js/utils.js") }}" type="text/javascript" charset="utf-8"></script>
320 <script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
324 <script src="{{ static_url("base/js/keyboard.js") }}" type="text/javascript" charset="utf-8"></script>
325 <script src="{{ static_url("base/js/security.js") }}" type="text/javascript" charset="utf-8"></script>
321 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
326 <script src="{{ static_url("base/js/dialog.js") }}" type="text/javascript" charset="utf-8"></script>
322 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
327 <script src="{{ static_url("services/kernels/js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
323 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
328 <script src="{{ static_url("services/kernels/js/comm.js") }}" type="text/javascript" charset="utf-8"></script>
324 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
329 <script src="{{ static_url("services/sessions/js/session.js") }}" type="text/javascript" charset="utf-8"></script>
325 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
330 <script src="{{ static_url("notebook/js/layoutmanager.js") }}" type="text/javascript" charset="utf-8"></script>
326 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
331 <script src="{{ static_url("notebook/js/mathjaxutils.js") }}" type="text/javascript" charset="utf-8"></script>
327 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
332 <script src="{{ static_url("notebook/js/outputarea.js") }}" type="text/javascript" charset="utf-8"></script>
328 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
333 <script src="{{ static_url("notebook/js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
329 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
334 <script src="{{ static_url("notebook/js/celltoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
330 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
335 <script src="{{ static_url("notebook/js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
331 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
336 <script src="{{ static_url("notebook/js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
332 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
337 <script src="{{ static_url("notebook/js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
333 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
338 <script src="{{ static_url("notebook/js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
334 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
339 <script src="{{ static_url("notebook/js/quickhelp.js") }}" type="text/javascript" charset="utf-8"></script>
335 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
340 <script src="{{ static_url("notebook/js/pager.js") }}" type="text/javascript" charset="utf-8"></script>
336 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
341 <script src="{{ static_url("notebook/js/menubar.js") }}" type="text/javascript" charset="utf-8"></script>
337 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
342 <script src="{{ static_url("notebook/js/toolbar.js") }}" type="text/javascript" charset="utf-8"></script>
338 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
343 <script src="{{ static_url("notebook/js/maintoolbar.js") }}" type="text/javascript" charset="utf-8"></script>
339 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
344 <script src="{{ static_url("notebook/js/notebook.js") }}" type="text/javascript" charset="utf-8"></script>
340 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
345 <script src="{{ static_url("notebook/js/keyboardmanager.js") }}" type="text/javascript" charset="utf-8"></script>
341 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
346 <script src="{{ static_url("notebook/js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
342 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
347 <script src="{{ static_url("notebook/js/notificationarea.js") }}" type="text/javascript" charset="utf-8"></script>
343 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
348 <script src="{{ static_url("notebook/js/tooltip.js") }}" type="text/javascript" charset="utf-8"></script>
344 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
349 <script src="{{ static_url("notebook/js/config.js") }}" type="text/javascript" charset="utf-8"></script>
345 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
350 <script src="{{ static_url("notebook/js/main.js") }}" type="text/javascript" charset="utf-8"></script>
346
351
347 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
352 <script src="{{ static_url("notebook/js/contexthint.js") }}" charset="utf-8"></script>
348
353
349 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
354 <script src="{{ static_url("notebook/js/celltoolbarpresets/default.js") }}" type="text/javascript" charset="utf-8"></script>
350 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
355 <script src="{{ static_url("notebook/js/celltoolbarpresets/rawcell.js") }}" type="text/javascript" charset="utf-8"></script>
351 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
356 <script src="{{ static_url("notebook/js/celltoolbarpresets/slideshow.js") }}" type="text/javascript" charset="utf-8"></script>
352
357
353 {% endblock %}
358 {% endblock %}
@@ -1,696 +1,697 b''
1 # encoding: utf-8
1 # encoding: utf-8
2 """
2 """
3 This module defines the things that are used in setup.py for building IPython
3 This module defines the things that are used in setup.py for building IPython
4
4
5 This includes:
5 This includes:
6
6
7 * The basic arguments to setup
7 * The basic arguments to setup
8 * Functions for finding things like packages, package data, etc.
8 * Functions for finding things like packages, package data, etc.
9 * A function for checking dependencies.
9 * A function for checking dependencies.
10 """
10 """
11 from __future__ import print_function
11 from __future__ import print_function
12
12
13 #-------------------------------------------------------------------------------
13 #-------------------------------------------------------------------------------
14 # Copyright (C) 2008 The IPython Development Team
14 # Copyright (C) 2008 The IPython Development Team
15 #
15 #
16 # Distributed under the terms of the BSD License. The full license is in
16 # Distributed under the terms of the BSD License. The full license is in
17 # the file COPYING, distributed as part of this software.
17 # the file COPYING, distributed as part of this software.
18 #-------------------------------------------------------------------------------
18 #-------------------------------------------------------------------------------
19
19
20 #-------------------------------------------------------------------------------
20 #-------------------------------------------------------------------------------
21 # Imports
21 # Imports
22 #-------------------------------------------------------------------------------
22 #-------------------------------------------------------------------------------
23 import errno
23 import errno
24 import os
24 import os
25 import sys
25 import sys
26
26
27 from distutils.command.build_py import build_py
27 from distutils.command.build_py import build_py
28 from distutils.command.build_scripts import build_scripts
28 from distutils.command.build_scripts import build_scripts
29 from distutils.command.install import install
29 from distutils.command.install import install
30 from distutils.command.install_scripts import install_scripts
30 from distutils.command.install_scripts import install_scripts
31 from distutils.cmd import Command
31 from distutils.cmd import Command
32 from glob import glob
32 from glob import glob
33 from subprocess import call
33 from subprocess import call
34
34
35 from setupext import install_data_ext
35 from setupext import install_data_ext
36
36
37 #-------------------------------------------------------------------------------
37 #-------------------------------------------------------------------------------
38 # Useful globals and utility functions
38 # Useful globals and utility functions
39 #-------------------------------------------------------------------------------
39 #-------------------------------------------------------------------------------
40
40
41 # A few handy globals
41 # A few handy globals
42 isfile = os.path.isfile
42 isfile = os.path.isfile
43 pjoin = os.path.join
43 pjoin = os.path.join
44 repo_root = os.path.dirname(os.path.abspath(__file__))
44 repo_root = os.path.dirname(os.path.abspath(__file__))
45
45
46 def oscmd(s):
46 def oscmd(s):
47 print(">", s)
47 print(">", s)
48 os.system(s)
48 os.system(s)
49
49
50 # Py3 compatibility hacks, without assuming IPython itself is installed with
50 # Py3 compatibility hacks, without assuming IPython itself is installed with
51 # the full py3compat machinery.
51 # the full py3compat machinery.
52
52
53 try:
53 try:
54 execfile
54 execfile
55 except NameError:
55 except NameError:
56 def execfile(fname, globs, locs=None):
56 def execfile(fname, globs, locs=None):
57 locs = locs or globs
57 locs = locs or globs
58 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
58 exec(compile(open(fname).read(), fname, "exec"), globs, locs)
59
59
60 # A little utility we'll need below, since glob() does NOT allow you to do
60 # A little utility we'll need below, since glob() does NOT allow you to do
61 # exclusion on multiple endings!
61 # exclusion on multiple endings!
62 def file_doesnt_endwith(test,endings):
62 def file_doesnt_endwith(test,endings):
63 """Return true if test is a file and its name does NOT end with any
63 """Return true if test is a file and its name does NOT end with any
64 of the strings listed in endings."""
64 of the strings listed in endings."""
65 if not isfile(test):
65 if not isfile(test):
66 return False
66 return False
67 for e in endings:
67 for e in endings:
68 if test.endswith(e):
68 if test.endswith(e):
69 return False
69 return False
70 return True
70 return True
71
71
72 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
73 # Basic project information
73 # Basic project information
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75
75
76 # release.py contains version, authors, license, url, keywords, etc.
76 # release.py contains version, authors, license, url, keywords, etc.
77 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
77 execfile(pjoin(repo_root, 'IPython','core','release.py'), globals())
78
78
79 # Create a dict with the basic information
79 # Create a dict with the basic information
80 # This dict is eventually passed to setup after additional keys are added.
80 # This dict is eventually passed to setup after additional keys are added.
81 setup_args = dict(
81 setup_args = dict(
82 name = name,
82 name = name,
83 version = version,
83 version = version,
84 description = description,
84 description = description,
85 long_description = long_description,
85 long_description = long_description,
86 author = author,
86 author = author,
87 author_email = author_email,
87 author_email = author_email,
88 url = url,
88 url = url,
89 download_url = download_url,
89 download_url = download_url,
90 license = license,
90 license = license,
91 platforms = platforms,
91 platforms = platforms,
92 keywords = keywords,
92 keywords = keywords,
93 classifiers = classifiers,
93 classifiers = classifiers,
94 cmdclass = {'install_data': install_data_ext},
94 cmdclass = {'install_data': install_data_ext},
95 )
95 )
96
96
97
97
98 #---------------------------------------------------------------------------
98 #---------------------------------------------------------------------------
99 # Find packages
99 # Find packages
100 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
101
101
102 def find_packages():
102 def find_packages():
103 """
103 """
104 Find all of IPython's packages.
104 Find all of IPython's packages.
105 """
105 """
106 excludes = ['deathrow', 'quarantine']
106 excludes = ['deathrow', 'quarantine']
107 packages = []
107 packages = []
108 for dir,subdirs,files in os.walk('IPython'):
108 for dir,subdirs,files in os.walk('IPython'):
109 package = dir.replace(os.path.sep, '.')
109 package = dir.replace(os.path.sep, '.')
110 if any(package.startswith('IPython.'+exc) for exc in excludes):
110 if any(package.startswith('IPython.'+exc) for exc in excludes):
111 # package is to be excluded (e.g. deathrow)
111 # package is to be excluded (e.g. deathrow)
112 continue
112 continue
113 if '__init__.py' not in files:
113 if '__init__.py' not in files:
114 # not a package
114 # not a package
115 continue
115 continue
116 packages.append(package)
116 packages.append(package)
117 return packages
117 return packages
118
118
119 #---------------------------------------------------------------------------
119 #---------------------------------------------------------------------------
120 # Find package data
120 # Find package data
121 #---------------------------------------------------------------------------
121 #---------------------------------------------------------------------------
122
122
123 def find_package_data():
123 def find_package_data():
124 """
124 """
125 Find IPython's package_data.
125 Find IPython's package_data.
126 """
126 """
127 # This is not enough for these things to appear in an sdist.
127 # This is not enough for these things to appear in an sdist.
128 # We need to muck with the MANIFEST to get this to work
128 # We need to muck with the MANIFEST to get this to work
129
129
130 # exclude components from the walk,
130 # exclude components from the walk,
131 # we will build the components separately
131 # we will build the components separately
132 excludes = ['components']
132 excludes = ['components']
133
133
134 # add 'static/' prefix to exclusions, and tuplify for use in startswith
134 # add 'static/' prefix to exclusions, and tuplify for use in startswith
135 excludes = tuple([pjoin('static', ex) for ex in excludes])
135 excludes = tuple([pjoin('static', ex) for ex in excludes])
136
136
137 # walk notebook resources:
137 # walk notebook resources:
138 cwd = os.getcwd()
138 cwd = os.getcwd()
139 os.chdir(os.path.join('IPython', 'html'))
139 os.chdir(os.path.join('IPython', 'html'))
140 static_data = []
140 static_data = []
141 for parent, dirs, files in os.walk('static'):
141 for parent, dirs, files in os.walk('static'):
142 if parent.startswith(excludes):
142 if parent.startswith(excludes):
143 continue
143 continue
144 for f in files:
144 for f in files:
145 static_data.append(pjoin(parent, f))
145 static_data.append(pjoin(parent, f))
146 components = pjoin("static", "components")
146 components = pjoin("static", "components")
147 # select the components we actually need to install
147 # select the components we actually need to install
148 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
148 # (there are lots of resources we bundle for sdist-reasons that we don't actually use)
149 static_data.extend([
149 static_data.extend([
150 pjoin(components, "backbone", "backbone-min.js"),
150 pjoin(components, "backbone", "backbone-min.js"),
151 pjoin(components, "bootstrap", "bootstrap", "js", "bootstrap.min.js"),
151 pjoin(components, "bootstrap", "bootstrap", "js", "bootstrap.min.js"),
152 pjoin(components, "font-awesome", "font", "*.*"),
152 pjoin(components, "font-awesome", "font", "*.*"),
153 pjoin(components, "google-caja", "html-css-sanitizer-minified.js"),
153 pjoin(components, "highlight.js", "build", "highlight.pack.js"),
154 pjoin(components, "highlight.js", "build", "highlight.pack.js"),
154 pjoin(components, "jquery", "jquery.min.js"),
155 pjoin(components, "jquery", "jquery.min.js"),
155 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
156 pjoin(components, "jquery-ui", "ui", "minified", "jquery-ui.min.js"),
156 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
157 pjoin(components, "jquery-ui", "themes", "smoothness", "jquery-ui.min.css"),
157 pjoin(components, "marked", "lib", "marked.js"),
158 pjoin(components, "marked", "lib", "marked.js"),
158 pjoin(components, "requirejs", "require.js"),
159 pjoin(components, "requirejs", "require.js"),
159 pjoin(components, "underscore", "underscore-min.js"),
160 pjoin(components, "underscore", "underscore-min.js"),
160 ])
161 ])
161
162
162 # Ship all of Codemirror's CSS and JS
163 # Ship all of Codemirror's CSS and JS
163 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
164 for parent, dirs, files in os.walk(pjoin(components, 'codemirror')):
164 for f in files:
165 for f in files:
165 if f.endswith(('.js', '.css')):
166 if f.endswith(('.js', '.css')):
166 static_data.append(pjoin(parent, f))
167 static_data.append(pjoin(parent, f))
167
168
168 os.chdir(os.path.join('tests',))
169 os.chdir(os.path.join('tests',))
169 js_tests = glob('*.js') + glob('*/*.js')
170 js_tests = glob('*.js') + glob('*/*.js')
170
171
171 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
172 os.chdir(os.path.join(cwd, 'IPython', 'nbconvert'))
172 nbconvert_templates = [os.path.join(dirpath, '*.*')
173 nbconvert_templates = [os.path.join(dirpath, '*.*')
173 for dirpath, _, _ in os.walk('templates')]
174 for dirpath, _, _ in os.walk('templates')]
174
175
175 os.chdir(cwd)
176 os.chdir(cwd)
176
177
177 package_data = {
178 package_data = {
178 'IPython.config.profile' : ['README*', '*/*.py'],
179 'IPython.config.profile' : ['README*', '*/*.py'],
179 'IPython.core.tests' : ['*.png', '*.jpg'],
180 'IPython.core.tests' : ['*.png', '*.jpg'],
180 'IPython.lib.tests' : ['*.wav'],
181 'IPython.lib.tests' : ['*.wav'],
181 'IPython.testing.plugin' : ['*.txt'],
182 'IPython.testing.plugin' : ['*.txt'],
182 'IPython.html' : ['templates/*'] + static_data,
183 'IPython.html' : ['templates/*'] + static_data,
183 'IPython.html.tests' : js_tests,
184 'IPython.html.tests' : js_tests,
184 'IPython.qt.console' : ['resources/icon/*.svg'],
185 'IPython.qt.console' : ['resources/icon/*.svg'],
185 'IPython.nbconvert' : nbconvert_templates +
186 'IPython.nbconvert' : nbconvert_templates +
186 ['tests/files/*.*', 'exporters/tests/files/*.*'],
187 ['tests/files/*.*', 'exporters/tests/files/*.*'],
187 'IPython.nbconvert.filters' : ['marked.js'],
188 'IPython.nbconvert.filters' : ['marked.js'],
188 'IPython.nbformat' : ['tests/*.ipynb']
189 'IPython.nbformat' : ['tests/*.ipynb']
189 }
190 }
190
191
191 return package_data
192 return package_data
192
193
193
194
194 def check_package_data(package_data):
195 def check_package_data(package_data):
195 """verify that package_data globs make sense"""
196 """verify that package_data globs make sense"""
196 print("checking package data")
197 print("checking package data")
197 for pkg, data in package_data.items():
198 for pkg, data in package_data.items():
198 pkg_root = pjoin(*pkg.split('.'))
199 pkg_root = pjoin(*pkg.split('.'))
199 for d in data:
200 for d in data:
200 path = pjoin(pkg_root, d)
201 path = pjoin(pkg_root, d)
201 if '*' in path:
202 if '*' in path:
202 assert len(glob(path)) > 0, "No files match pattern %s" % path
203 assert len(glob(path)) > 0, "No files match pattern %s" % path
203 else:
204 else:
204 assert os.path.exists(path), "Missing package data: %s" % path
205 assert os.path.exists(path), "Missing package data: %s" % path
205
206
206
207
207 def check_package_data_first(command):
208 def check_package_data_first(command):
208 """decorator for checking package_data before running a given command
209 """decorator for checking package_data before running a given command
209
210
210 Probably only needs to wrap build_py
211 Probably only needs to wrap build_py
211 """
212 """
212 class DecoratedCommand(command):
213 class DecoratedCommand(command):
213 def run(self):
214 def run(self):
214 check_package_data(self.package_data)
215 check_package_data(self.package_data)
215 command.run(self)
216 command.run(self)
216 return DecoratedCommand
217 return DecoratedCommand
217
218
218
219
219 #---------------------------------------------------------------------------
220 #---------------------------------------------------------------------------
220 # Find data files
221 # Find data files
221 #---------------------------------------------------------------------------
222 #---------------------------------------------------------------------------
222
223
223 def make_dir_struct(tag,base,out_base):
224 def make_dir_struct(tag,base,out_base):
224 """Make the directory structure of all files below a starting dir.
225 """Make the directory structure of all files below a starting dir.
225
226
226 This is just a convenience routine to help build a nested directory
227 This is just a convenience routine to help build a nested directory
227 hierarchy because distutils is too stupid to do this by itself.
228 hierarchy because distutils is too stupid to do this by itself.
228
229
229 XXX - this needs a proper docstring!
230 XXX - this needs a proper docstring!
230 """
231 """
231
232
232 # we'll use these a lot below
233 # we'll use these a lot below
233 lbase = len(base)
234 lbase = len(base)
234 pathsep = os.path.sep
235 pathsep = os.path.sep
235 lpathsep = len(pathsep)
236 lpathsep = len(pathsep)
236
237
237 out = []
238 out = []
238 for (dirpath,dirnames,filenames) in os.walk(base):
239 for (dirpath,dirnames,filenames) in os.walk(base):
239 # we need to strip out the dirpath from the base to map it to the
240 # we need to strip out the dirpath from the base to map it to the
240 # output (installation) path. This requires possibly stripping the
241 # output (installation) path. This requires possibly stripping the
241 # path separator, because otherwise pjoin will not work correctly
242 # path separator, because otherwise pjoin will not work correctly
242 # (pjoin('foo/','/bar') returns '/bar').
243 # (pjoin('foo/','/bar') returns '/bar').
243
244
244 dp_eff = dirpath[lbase:]
245 dp_eff = dirpath[lbase:]
245 if dp_eff.startswith(pathsep):
246 if dp_eff.startswith(pathsep):
246 dp_eff = dp_eff[lpathsep:]
247 dp_eff = dp_eff[lpathsep:]
247 # The output path must be anchored at the out_base marker
248 # The output path must be anchored at the out_base marker
248 out_path = pjoin(out_base,dp_eff)
249 out_path = pjoin(out_base,dp_eff)
249 # Now we can generate the final filenames. Since os.walk only produces
250 # Now we can generate the final filenames. Since os.walk only produces
250 # filenames, we must join back with the dirpath to get full valid file
251 # filenames, we must join back with the dirpath to get full valid file
251 # paths:
252 # paths:
252 pfiles = [pjoin(dirpath,f) for f in filenames]
253 pfiles = [pjoin(dirpath,f) for f in filenames]
253 # Finally, generate the entry we need, which is a pari of (output
254 # Finally, generate the entry we need, which is a pari of (output
254 # path, files) for use as a data_files parameter in install_data.
255 # path, files) for use as a data_files parameter in install_data.
255 out.append((out_path, pfiles))
256 out.append((out_path, pfiles))
256
257
257 return out
258 return out
258
259
259
260
260 def find_data_files():
261 def find_data_files():
261 """
262 """
262 Find IPython's data_files.
263 Find IPython's data_files.
263
264
264 Just man pages at this point.
265 Just man pages at this point.
265 """
266 """
266
267
267 manpagebase = pjoin('share', 'man', 'man1')
268 manpagebase = pjoin('share', 'man', 'man1')
268
269
269 # Simple file lists can be made by hand
270 # Simple file lists can be made by hand
270 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
271 manpages = [f for f in glob(pjoin('docs','man','*.1.gz')) if isfile(f)]
271 if not manpages:
272 if not manpages:
272 # When running from a source tree, the manpages aren't gzipped
273 # When running from a source tree, the manpages aren't gzipped
273 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
274 manpages = [f for f in glob(pjoin('docs','man','*.1')) if isfile(f)]
274
275
275 # And assemble the entire output list
276 # And assemble the entire output list
276 data_files = [ (manpagebase, manpages) ]
277 data_files = [ (manpagebase, manpages) ]
277
278
278 return data_files
279 return data_files
279
280
280
281
281 def make_man_update_target(manpage):
282 def make_man_update_target(manpage):
282 """Return a target_update-compliant tuple for the given manpage.
283 """Return a target_update-compliant tuple for the given manpage.
283
284
284 Parameters
285 Parameters
285 ----------
286 ----------
286 manpage : string
287 manpage : string
287 Name of the manpage, must include the section number (trailing number).
288 Name of the manpage, must include the section number (trailing number).
288
289
289 Example
290 Example
290 -------
291 -------
291
292
292 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
293 >>> make_man_update_target('ipython.1') #doctest: +NORMALIZE_WHITESPACE
293 ('docs/man/ipython.1.gz',
294 ('docs/man/ipython.1.gz',
294 ['docs/man/ipython.1'],
295 ['docs/man/ipython.1'],
295 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
296 'cd docs/man && gzip -9c ipython.1 > ipython.1.gz')
296 """
297 """
297 man_dir = pjoin('docs', 'man')
298 man_dir = pjoin('docs', 'man')
298 manpage_gz = manpage + '.gz'
299 manpage_gz = manpage + '.gz'
299 manpath = pjoin(man_dir, manpage)
300 manpath = pjoin(man_dir, manpage)
300 manpath_gz = pjoin(man_dir, manpage_gz)
301 manpath_gz = pjoin(man_dir, manpage_gz)
301 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
302 gz_cmd = ( "cd %(man_dir)s && gzip -9c %(manpage)s > %(manpage_gz)s" %
302 locals() )
303 locals() )
303 return (manpath_gz, [manpath], gz_cmd)
304 return (manpath_gz, [manpath], gz_cmd)
304
305
305 # The two functions below are copied from IPython.utils.path, so we don't need
306 # The two functions below are copied from IPython.utils.path, so we don't need
306 # to import IPython during setup, which fails on Python 3.
307 # to import IPython during setup, which fails on Python 3.
307
308
308 def target_outdated(target,deps):
309 def target_outdated(target,deps):
309 """Determine whether a target is out of date.
310 """Determine whether a target is out of date.
310
311
311 target_outdated(target,deps) -> 1/0
312 target_outdated(target,deps) -> 1/0
312
313
313 deps: list of filenames which MUST exist.
314 deps: list of filenames which MUST exist.
314 target: single filename which may or may not exist.
315 target: single filename which may or may not exist.
315
316
316 If target doesn't exist or is older than any file listed in deps, return
317 If target doesn't exist or is older than any file listed in deps, return
317 true, otherwise return false.
318 true, otherwise return false.
318 """
319 """
319 try:
320 try:
320 target_time = os.path.getmtime(target)
321 target_time = os.path.getmtime(target)
321 except os.error:
322 except os.error:
322 return 1
323 return 1
323 for dep in deps:
324 for dep in deps:
324 dep_time = os.path.getmtime(dep)
325 dep_time = os.path.getmtime(dep)
325 if dep_time > target_time:
326 if dep_time > target_time:
326 #print "For target",target,"Dep failed:",dep # dbg
327 #print "For target",target,"Dep failed:",dep # dbg
327 #print "times (dep,tar):",dep_time,target_time # dbg
328 #print "times (dep,tar):",dep_time,target_time # dbg
328 return 1
329 return 1
329 return 0
330 return 0
330
331
331
332
332 def target_update(target,deps,cmd):
333 def target_update(target,deps,cmd):
333 """Update a target with a given command given a list of dependencies.
334 """Update a target with a given command given a list of dependencies.
334
335
335 target_update(target,deps,cmd) -> runs cmd if target is outdated.
336 target_update(target,deps,cmd) -> runs cmd if target is outdated.
336
337
337 This is just a wrapper around target_outdated() which calls the given
338 This is just a wrapper around target_outdated() which calls the given
338 command if target is outdated."""
339 command if target is outdated."""
339
340
340 if target_outdated(target,deps):
341 if target_outdated(target,deps):
341 os.system(cmd)
342 os.system(cmd)
342
343
343 #---------------------------------------------------------------------------
344 #---------------------------------------------------------------------------
344 # Find scripts
345 # Find scripts
345 #---------------------------------------------------------------------------
346 #---------------------------------------------------------------------------
346
347
347 def find_entry_points():
348 def find_entry_points():
348 """Find IPython's scripts.
349 """Find IPython's scripts.
349
350
350 if entry_points is True:
351 if entry_points is True:
351 return setuptools entry_point-style definitions
352 return setuptools entry_point-style definitions
352 else:
353 else:
353 return file paths of plain scripts [default]
354 return file paths of plain scripts [default]
354
355
355 suffix is appended to script names if entry_points is True, so that the
356 suffix is appended to script names if entry_points is True, so that the
356 Python 3 scripts get named "ipython3" etc.
357 Python 3 scripts get named "ipython3" etc.
357 """
358 """
358 ep = [
359 ep = [
359 'ipython%s = IPython:start_ipython',
360 'ipython%s = IPython:start_ipython',
360 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
361 'ipcontroller%s = IPython.parallel.apps.ipcontrollerapp:launch_new_instance',
361 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
362 'ipengine%s = IPython.parallel.apps.ipengineapp:launch_new_instance',
362 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
363 'ipcluster%s = IPython.parallel.apps.ipclusterapp:launch_new_instance',
363 'iptest%s = IPython.testing.iptestcontroller:main',
364 'iptest%s = IPython.testing.iptestcontroller:main',
364 ]
365 ]
365 suffix = str(sys.version_info[0])
366 suffix = str(sys.version_info[0])
366 return [e % '' for e in ep] + [e % suffix for e in ep]
367 return [e % '' for e in ep] + [e % suffix for e in ep]
367
368
368 script_src = """#!{executable}
369 script_src = """#!{executable}
369 # This script was automatically generated by setup.py
370 # This script was automatically generated by setup.py
370 if __name__ == '__main__':
371 if __name__ == '__main__':
371 from {mod} import {func}
372 from {mod} import {func}
372 {func}()
373 {func}()
373 """
374 """
374
375
375 class build_scripts_entrypt(build_scripts):
376 class build_scripts_entrypt(build_scripts):
376 def run(self):
377 def run(self):
377 self.mkpath(self.build_dir)
378 self.mkpath(self.build_dir)
378 outfiles = []
379 outfiles = []
379 for script in find_entry_points():
380 for script in find_entry_points():
380 name, entrypt = script.split('=')
381 name, entrypt = script.split('=')
381 name = name.strip()
382 name = name.strip()
382 entrypt = entrypt.strip()
383 entrypt = entrypt.strip()
383 outfile = os.path.join(self.build_dir, name)
384 outfile = os.path.join(self.build_dir, name)
384 outfiles.append(outfile)
385 outfiles.append(outfile)
385 print('Writing script to', outfile)
386 print('Writing script to', outfile)
386
387
387 mod, func = entrypt.split(':')
388 mod, func = entrypt.split(':')
388 with open(outfile, 'w') as f:
389 with open(outfile, 'w') as f:
389 f.write(script_src.format(executable=sys.executable,
390 f.write(script_src.format(executable=sys.executable,
390 mod=mod, func=func))
391 mod=mod, func=func))
391
392
392 return outfiles, outfiles
393 return outfiles, outfiles
393
394
394 class install_lib_symlink(Command):
395 class install_lib_symlink(Command):
395 user_options = [
396 user_options = [
396 ('install-dir=', 'd', "directory to install to"),
397 ('install-dir=', 'd', "directory to install to"),
397 ]
398 ]
398
399
399 def initialize_options(self):
400 def initialize_options(self):
400 self.install_dir = None
401 self.install_dir = None
401
402
402 def finalize_options(self):
403 def finalize_options(self):
403 self.set_undefined_options('symlink',
404 self.set_undefined_options('symlink',
404 ('install_lib', 'install_dir'),
405 ('install_lib', 'install_dir'),
405 )
406 )
406
407
407 def run(self):
408 def run(self):
408 if sys.platform == 'win32':
409 if sys.platform == 'win32':
409 raise Exception("This doesn't work on Windows.")
410 raise Exception("This doesn't work on Windows.")
410 pkg = os.path.join(os.getcwd(), 'IPython')
411 pkg = os.path.join(os.getcwd(), 'IPython')
411 dest = os.path.join(self.install_dir, 'IPython')
412 dest = os.path.join(self.install_dir, 'IPython')
412 if os.path.islink(dest):
413 if os.path.islink(dest):
413 print('removing existing symlink at %s' % dest)
414 print('removing existing symlink at %s' % dest)
414 os.unlink(dest)
415 os.unlink(dest)
415 print('symlinking %s -> %s' % (pkg, dest))
416 print('symlinking %s -> %s' % (pkg, dest))
416 os.symlink(pkg, dest)
417 os.symlink(pkg, dest)
417
418
418 class unsymlink(install):
419 class unsymlink(install):
419 def run(self):
420 def run(self):
420 dest = os.path.join(self.install_lib, 'IPython')
421 dest = os.path.join(self.install_lib, 'IPython')
421 if os.path.islink(dest):
422 if os.path.islink(dest):
422 print('removing symlink at %s' % dest)
423 print('removing symlink at %s' % dest)
423 os.unlink(dest)
424 os.unlink(dest)
424 else:
425 else:
425 print('No symlink exists at %s' % dest)
426 print('No symlink exists at %s' % dest)
426
427
427 class install_symlinked(install):
428 class install_symlinked(install):
428 def run(self):
429 def run(self):
429 if sys.platform == 'win32':
430 if sys.platform == 'win32':
430 raise Exception("This doesn't work on Windows.")
431 raise Exception("This doesn't work on Windows.")
431
432
432 # Run all sub-commands (at least those that need to be run)
433 # Run all sub-commands (at least those that need to be run)
433 for cmd_name in self.get_sub_commands():
434 for cmd_name in self.get_sub_commands():
434 self.run_command(cmd_name)
435 self.run_command(cmd_name)
435
436
436 # 'sub_commands': a list of commands this command might have to run to
437 # 'sub_commands': a list of commands this command might have to run to
437 # get its work done. See cmd.py for more info.
438 # get its work done. See cmd.py for more info.
438 sub_commands = [('install_lib_symlink', lambda self:True),
439 sub_commands = [('install_lib_symlink', lambda self:True),
439 ('install_scripts_sym', lambda self:True),
440 ('install_scripts_sym', lambda self:True),
440 ]
441 ]
441
442
442 class install_scripts_for_symlink(install_scripts):
443 class install_scripts_for_symlink(install_scripts):
443 """Redefined to get options from 'symlink' instead of 'install'.
444 """Redefined to get options from 'symlink' instead of 'install'.
444
445
445 I love distutils almost as much as I love setuptools.
446 I love distutils almost as much as I love setuptools.
446 """
447 """
447 def finalize_options(self):
448 def finalize_options(self):
448 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
449 self.set_undefined_options('build', ('build_scripts', 'build_dir'))
449 self.set_undefined_options('symlink',
450 self.set_undefined_options('symlink',
450 ('install_scripts', 'install_dir'),
451 ('install_scripts', 'install_dir'),
451 ('force', 'force'),
452 ('force', 'force'),
452 ('skip_build', 'skip_build'),
453 ('skip_build', 'skip_build'),
453 )
454 )
454
455
455 #---------------------------------------------------------------------------
456 #---------------------------------------------------------------------------
456 # Verify all dependencies
457 # Verify all dependencies
457 #---------------------------------------------------------------------------
458 #---------------------------------------------------------------------------
458
459
459 def check_for_dependencies():
460 def check_for_dependencies():
460 """Check for IPython's dependencies.
461 """Check for IPython's dependencies.
461
462
462 This function should NOT be called if running under setuptools!
463 This function should NOT be called if running under setuptools!
463 """
464 """
464 from setupext.setupext import (
465 from setupext.setupext import (
465 print_line, print_raw, print_status,
466 print_line, print_raw, print_status,
466 check_for_sphinx, check_for_pygments,
467 check_for_sphinx, check_for_pygments,
467 check_for_nose, check_for_pexpect,
468 check_for_nose, check_for_pexpect,
468 check_for_pyzmq, check_for_readline,
469 check_for_pyzmq, check_for_readline,
469 check_for_jinja2, check_for_tornado
470 check_for_jinja2, check_for_tornado
470 )
471 )
471 print_line()
472 print_line()
472 print_raw("BUILDING IPYTHON")
473 print_raw("BUILDING IPYTHON")
473 print_status('python', sys.version)
474 print_status('python', sys.version)
474 print_status('platform', sys.platform)
475 print_status('platform', sys.platform)
475 if sys.platform == 'win32':
476 if sys.platform == 'win32':
476 print_status('Windows version', sys.getwindowsversion())
477 print_status('Windows version', sys.getwindowsversion())
477
478
478 print_raw("")
479 print_raw("")
479 print_raw("OPTIONAL DEPENDENCIES")
480 print_raw("OPTIONAL DEPENDENCIES")
480
481
481 check_for_sphinx()
482 check_for_sphinx()
482 check_for_pygments()
483 check_for_pygments()
483 check_for_nose()
484 check_for_nose()
484 if os.name == 'posix':
485 if os.name == 'posix':
485 check_for_pexpect()
486 check_for_pexpect()
486 check_for_pyzmq()
487 check_for_pyzmq()
487 check_for_tornado()
488 check_for_tornado()
488 check_for_readline()
489 check_for_readline()
489 check_for_jinja2()
490 check_for_jinja2()
490
491
491 #---------------------------------------------------------------------------
492 #---------------------------------------------------------------------------
492 # VCS related
493 # VCS related
493 #---------------------------------------------------------------------------
494 #---------------------------------------------------------------------------
494
495
495 # utils.submodule has checks for submodule status
496 # utils.submodule has checks for submodule status
496 execfile(pjoin('IPython','utils','submodule.py'), globals())
497 execfile(pjoin('IPython','utils','submodule.py'), globals())
497
498
498 class UpdateSubmodules(Command):
499 class UpdateSubmodules(Command):
499 """Update git submodules
500 """Update git submodules
500
501
501 IPython's external javascript dependencies live in a separate repo.
502 IPython's external javascript dependencies live in a separate repo.
502 """
503 """
503 description = "Update git submodules"
504 description = "Update git submodules"
504 user_options = []
505 user_options = []
505
506
506 def initialize_options(self):
507 def initialize_options(self):
507 pass
508 pass
508
509
509 def finalize_options(self):
510 def finalize_options(self):
510 pass
511 pass
511
512
512 def run(self):
513 def run(self):
513 failure = False
514 failure = False
514 try:
515 try:
515 self.spawn('git submodule init'.split())
516 self.spawn('git submodule init'.split())
516 self.spawn('git submodule update --recursive'.split())
517 self.spawn('git submodule update --recursive'.split())
517 except Exception as e:
518 except Exception as e:
518 failure = e
519 failure = e
519 print(e)
520 print(e)
520
521
521 if not check_submodule_status(repo_root) == 'clean':
522 if not check_submodule_status(repo_root) == 'clean':
522 print("submodules could not be checked out")
523 print("submodules could not be checked out")
523 sys.exit(1)
524 sys.exit(1)
524
525
525
526
526 def git_prebuild(pkg_dir, build_cmd=build_py):
527 def git_prebuild(pkg_dir, build_cmd=build_py):
527 """Return extended build or sdist command class for recording commit
528 """Return extended build or sdist command class for recording commit
528
529
529 records git commit in IPython.utils._sysinfo.commit
530 records git commit in IPython.utils._sysinfo.commit
530
531
531 for use in IPython.utils.sysinfo.sys_info() calls after installation.
532 for use in IPython.utils.sysinfo.sys_info() calls after installation.
532
533
533 Also ensures that submodules exist prior to running
534 Also ensures that submodules exist prior to running
534 """
535 """
535
536
536 class MyBuildPy(build_cmd):
537 class MyBuildPy(build_cmd):
537 ''' Subclass to write commit data into installation tree '''
538 ''' Subclass to write commit data into installation tree '''
538 def run(self):
539 def run(self):
539 build_cmd.run(self)
540 build_cmd.run(self)
540 # this one will only fire for build commands
541 # this one will only fire for build commands
541 if hasattr(self, 'build_lib'):
542 if hasattr(self, 'build_lib'):
542 self._record_commit(self.build_lib)
543 self._record_commit(self.build_lib)
543
544
544 def make_release_tree(self, base_dir, files):
545 def make_release_tree(self, base_dir, files):
545 # this one will fire for sdist
546 # this one will fire for sdist
546 build_cmd.make_release_tree(self, base_dir, files)
547 build_cmd.make_release_tree(self, base_dir, files)
547 self._record_commit(base_dir)
548 self._record_commit(base_dir)
548
549
549 def _record_commit(self, base_dir):
550 def _record_commit(self, base_dir):
550 import subprocess
551 import subprocess
551 proc = subprocess.Popen('git rev-parse --short HEAD',
552 proc = subprocess.Popen('git rev-parse --short HEAD',
552 stdout=subprocess.PIPE,
553 stdout=subprocess.PIPE,
553 stderr=subprocess.PIPE,
554 stderr=subprocess.PIPE,
554 shell=True)
555 shell=True)
555 repo_commit, _ = proc.communicate()
556 repo_commit, _ = proc.communicate()
556 repo_commit = repo_commit.strip().decode("ascii")
557 repo_commit = repo_commit.strip().decode("ascii")
557
558
558 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
559 out_pth = pjoin(base_dir, pkg_dir, 'utils', '_sysinfo.py')
559 if os.path.isfile(out_pth) and not repo_commit:
560 if os.path.isfile(out_pth) and not repo_commit:
560 # nothing to write, don't clobber
561 # nothing to write, don't clobber
561 return
562 return
562
563
563 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
564 print("writing git commit '%s' to %s" % (repo_commit, out_pth))
564
565
565 # remove to avoid overwriting original via hard link
566 # remove to avoid overwriting original via hard link
566 try:
567 try:
567 os.remove(out_pth)
568 os.remove(out_pth)
568 except (IOError, OSError):
569 except (IOError, OSError):
569 pass
570 pass
570 with open(out_pth, 'w') as out_file:
571 with open(out_pth, 'w') as out_file:
571 out_file.writelines([
572 out_file.writelines([
572 '# GENERATED BY setup.py\n',
573 '# GENERATED BY setup.py\n',
573 'commit = "%s"\n' % repo_commit,
574 'commit = "%s"\n' % repo_commit,
574 ])
575 ])
575 return require_submodules(MyBuildPy)
576 return require_submodules(MyBuildPy)
576
577
577
578
578 def require_submodules(command):
579 def require_submodules(command):
579 """decorator for instructing a command to check for submodules before running"""
580 """decorator for instructing a command to check for submodules before running"""
580 class DecoratedCommand(command):
581 class DecoratedCommand(command):
581 def run(self):
582 def run(self):
582 if not check_submodule_status(repo_root) == 'clean':
583 if not check_submodule_status(repo_root) == 'clean':
583 print("submodules missing! Run `setup.py submodule` and try again")
584 print("submodules missing! Run `setup.py submodule` and try again")
584 sys.exit(1)
585 sys.exit(1)
585 command.run(self)
586 command.run(self)
586 return DecoratedCommand
587 return DecoratedCommand
587
588
588 #---------------------------------------------------------------------------
589 #---------------------------------------------------------------------------
589 # bdist related
590 # bdist related
590 #---------------------------------------------------------------------------
591 #---------------------------------------------------------------------------
591
592
592 def get_bdist_wheel():
593 def get_bdist_wheel():
593 """Construct bdist_wheel command for building wheels
594 """Construct bdist_wheel command for building wheels
594
595
595 Constructs py2-none-any tag, instead of py2.7-none-any
596 Constructs py2-none-any tag, instead of py2.7-none-any
596 """
597 """
597 class RequiresWheel(Command):
598 class RequiresWheel(Command):
598 description = "Dummy command for missing bdist_wheel"
599 description = "Dummy command for missing bdist_wheel"
599 user_options = []
600 user_options = []
600
601
601 def initialize_options(self):
602 def initialize_options(self):
602 pass
603 pass
603
604
604 def finalize_options(self):
605 def finalize_options(self):
605 pass
606 pass
606
607
607 def run(self):
608 def run(self):
608 print("bdist_wheel requires the wheel package")
609 print("bdist_wheel requires the wheel package")
609 sys.exit(1)
610 sys.exit(1)
610
611
611 if 'setuptools' not in sys.modules:
612 if 'setuptools' not in sys.modules:
612 return RequiresWheel
613 return RequiresWheel
613 else:
614 else:
614 try:
615 try:
615 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
616 from wheel.bdist_wheel import bdist_wheel, read_pkg_info, write_pkg_info
616 except ImportError:
617 except ImportError:
617 return RequiresWheel
618 return RequiresWheel
618
619
619 class bdist_wheel_tag(bdist_wheel):
620 class bdist_wheel_tag(bdist_wheel):
620
621
621 def get_tag(self):
622 def get_tag(self):
622 return ('py%i' % sys.version_info[0], 'none', 'any')
623 return ('py%i' % sys.version_info[0], 'none', 'any')
623
624
624 def add_requirements(self, metadata_path):
625 def add_requirements(self, metadata_path):
625 """transform platform-dependent requirements"""
626 """transform platform-dependent requirements"""
626 pkg_info = read_pkg_info(metadata_path)
627 pkg_info = read_pkg_info(metadata_path)
627 # pkg_info is an email.Message object (?!)
628 # pkg_info is an email.Message object (?!)
628 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
629 # we have to remove the unconditional 'readline' and/or 'pyreadline' entries
629 # and transform them to conditionals
630 # and transform them to conditionals
630 requires = pkg_info.get_all('Requires-Dist')
631 requires = pkg_info.get_all('Requires-Dist')
631 del pkg_info['Requires-Dist']
632 del pkg_info['Requires-Dist']
632 def _remove_startswith(lis, prefix):
633 def _remove_startswith(lis, prefix):
633 """like list.remove, but with startswith instead of =="""
634 """like list.remove, but with startswith instead of =="""
634 found = False
635 found = False
635 for idx, item in enumerate(lis):
636 for idx, item in enumerate(lis):
636 if item.startswith(prefix):
637 if item.startswith(prefix):
637 found = True
638 found = True
638 break
639 break
639 if found:
640 if found:
640 lis.pop(idx)
641 lis.pop(idx)
641
642
642 for pkg in ("gnureadline", "pyreadline", "mock"):
643 for pkg in ("gnureadline", "pyreadline", "mock"):
643 _remove_startswith(requires, pkg)
644 _remove_startswith(requires, pkg)
644 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
645 requires.append("gnureadline; sys.platform == 'darwin' and platform.python_implementation == 'CPython'")
645 requires.append("pyreadline (>=2.0); sys.platform == 'win32' and platform.python_implementation == 'CPython'")
646 requires.append("pyreadline (>=2.0); sys.platform == 'win32' and platform.python_implementation == 'CPython'")
646 requires.append("mock; extra == 'test' and python_version < '3.3'")
647 requires.append("mock; extra == 'test' and python_version < '3.3'")
647 for r in requires:
648 for r in requires:
648 pkg_info['Requires-Dist'] = r
649 pkg_info['Requires-Dist'] = r
649 write_pkg_info(metadata_path, pkg_info)
650 write_pkg_info(metadata_path, pkg_info)
650
651
651 return bdist_wheel_tag
652 return bdist_wheel_tag
652
653
653 #---------------------------------------------------------------------------
654 #---------------------------------------------------------------------------
654 # Notebook related
655 # Notebook related
655 #---------------------------------------------------------------------------
656 #---------------------------------------------------------------------------
656
657
657 class CompileCSS(Command):
658 class CompileCSS(Command):
658 """Recompile Notebook CSS
659 """Recompile Notebook CSS
659
660
660 Regenerate the compiled CSS from LESS sources.
661 Regenerate the compiled CSS from LESS sources.
661
662
662 Requires various dev dependencies, such as fabric and lessc.
663 Requires various dev dependencies, such as fabric and lessc.
663 """
664 """
664 description = "Recompile Notebook CSS"
665 description = "Recompile Notebook CSS"
665 user_options = []
666 user_options = []
666
667
667 def initialize_options(self):
668 def initialize_options(self):
668 pass
669 pass
669
670
670 def finalize_options(self):
671 def finalize_options(self):
671 pass
672 pass
672
673
673 def run(self):
674 def run(self):
674 call("fab css", shell=True, cwd=pjoin(repo_root, "IPython", "html"))
675 call("fab css", shell=True, cwd=pjoin(repo_root, "IPython", "html"))
675
676
676 class JavascriptVersion(Command):
677 class JavascriptVersion(Command):
677 """write the javascript version to notebook javascript"""
678 """write the javascript version to notebook javascript"""
678 description = "Write IPython version to javascript"
679 description = "Write IPython version to javascript"
679 user_options = []
680 user_options = []
680
681
681 def initialize_options(self):
682 def initialize_options(self):
682 pass
683 pass
683
684
684 def finalize_options(self):
685 def finalize_options(self):
685 pass
686 pass
686
687
687 def run(self):
688 def run(self):
688 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
689 nsfile = pjoin(repo_root, "IPython", "html", "static", "base", "js", "namespace.js")
689 with open(nsfile) as f:
690 with open(nsfile) as f:
690 lines = f.readlines()
691 lines = f.readlines()
691 with open(nsfile, 'w') as f:
692 with open(nsfile, 'w') as f:
692 for line in lines:
693 for line in lines:
693 if line.startswith("IPython.version"):
694 if line.startswith("IPython.version"):
694 line = 'IPython.version = "{0}";\n'.format(version)
695 line = 'IPython.version = "{0}";\n'.format(version)
695 f.write(line)
696 f.write(line)
696
697
General Comments 0
You need to be logged in to leave comments. Login now