##// END OF EJS Templates
cleanup whitespace
Matthias Bussonnier -
Show More
@@ -1,588 +1,588
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 /**
5 5 *
6 6 *
7 7 * @module cell
8 8 * @namespace cell
9 9 * @class Cell
10 10 */
11 11
12 12
13 13 define([
14 14 'base/js/namespace',
15 15 'jquery',
16 16 'base/js/utils',
17 17 'codemirror/lib/codemirror',
18 18 'codemirror/addon/edit/matchbrackets',
19 19 'codemirror/addon/edit/closebrackets',
20 20 'codemirror/addon/comment/comment'
21 21 ], function(IPython, $, utils, CodeMirror, cm_match, cm_closeb, cm_comment) {
22 22 // TODO: remove IPython dependency here
23 23 "use strict";
24 24
25 25 var Cell = function (options) {
26 26 /* Constructor
27 27 *
28 28 * The Base `Cell` class from which to inherit.
29 29 * @constructor
30 30 * @param:
31 31 * options: dictionary
32 32 * Dictionary of keyword arguments.
33 * events: $(Events) instance
33 * events: $(Events) instance
34 34 * config: dictionary
35 * keyboard_manager: KeyboardManager instance
35 * keyboard_manager: KeyboardManager instance
36 36 */
37 37 options = options || {};
38 38 this.keyboard_manager = options.keyboard_manager;
39 39 this.events = options.events;
40 40 var config = utils.mergeopt(Cell, options.config);
41 41 // superclass default overwrite our default
42 42
43 43 this.placeholder = config.placeholder || '';
44 44 this.read_only = config.cm_config.readOnly;
45 45 this.selected = false;
46 46 this.rendered = false;
47 47 this.mode = 'command';
48 48 this.metadata = {};
49 49 // load this from metadata later ?
50 50 this.user_highlight = 'auto';
51 51 this.cm_config = config.cm_config;
52 52 this.cell_id = utils.uuid();
53 53 this._options = config;
54 54
55 55 // For JS VM engines optimization, attributes should be all set (even
56 56 // to null) in the constructor, and if possible, if different subclass
57 57 // have new attributes with same name, they should be created in the
58 58 // same order. Easiest is to create and set to null in parent class.
59 59
60 60 this.element = null;
61 61 this.cell_type = this.cell_type || null;
62 62 this.code_mirror = null;
63 63
64 64 this.create_element();
65 65 if (this.element !== null) {
66 66 this.element.data("cell", this);
67 67 this.bind_events();
68 68 this.init_classes();
69 69 }
70 70 };
71 71
72 72 Cell.options_default = {
73 73 cm_config : {
74 74 indentUnit : 4,
75 75 readOnly: false,
76 76 theme: "default",
77 77 extraKeys: {
78 78 "Cmd-Right":"goLineRight",
79 79 "End":"goLineRight",
80 80 "Cmd-Left":"goLineLeft"
81 81 }
82 82 }
83 83 };
84 84
85 85 // FIXME: Workaround CM Bug #332 (Safari segfault on drag)
86 86 // by disabling drag/drop altogether on Safari
87 87 // https://github.com/codemirror/CodeMirror/issues/332
88 88 if (utils.browser[0] == "Safari") {
89 89 Cell.options_default.cm_config.dragDrop = false;
90 90 }
91 91
92 92 /**
93 93 * Empty. Subclasses must implement create_element.
94 94 * This should contain all the code to create the DOM element in notebook
95 95 * and will be called by Base Class constructor.
96 96 * @method create_element
97 97 */
98 98 Cell.prototype.create_element = function () {
99 99 };
100 100
101 101 Cell.prototype.init_classes = function () {
102 102 // Call after this.element exists to initialize the css classes
103 103 // related to selected, rendered and mode.
104 104 if (this.selected) {
105 105 this.element.addClass('selected');
106 106 } else {
107 107 this.element.addClass('unselected');
108 108 }
109 109 if (this.rendered) {
110 110 this.element.addClass('rendered');
111 111 } else {
112 112 this.element.addClass('unrendered');
113 113 }
114 114 if (this.mode === 'edit') {
115 115 this.element.addClass('edit_mode');
116 116 } else {
117 117 this.element.addClass('command_mode');
118 118 }
119 119 };
120 120
121 121 /**
122 122 * Subclasses can implement override bind_events.
123 123 * Be carefull to call the parent method when overwriting as it fires event.
124 124 * this will be triggerd after create_element in constructor.
125 125 * @method bind_events
126 126 */
127 127 Cell.prototype.bind_events = function () {
128 128 var that = this;
129 129 // We trigger events so that Cell doesn't have to depend on Notebook.
130 130 that.element.click(function (event) {
131 131 if (!that.selected) {
132 132 that.events.trigger('select.Cell', {'cell':that});
133 133 }
134 134 });
135 135 that.element.focusin(function (event) {
136 136 if (!that.selected) {
137 137 that.events.trigger('select.Cell', {'cell':that});
138 138 }
139 139 });
140 140 if (this.code_mirror) {
141 141 this.code_mirror.on("change", function(cm, change) {
142 142 that.events.trigger("set_dirty.Notebook", {value: true});
143 143 });
144 144 }
145 145 if (this.code_mirror) {
146 146 this.code_mirror.on('focus', function(cm, change) {
147 147 that.events.trigger('edit_mode.Cell', {cell: that});
148 148 });
149 149 }
150 150 if (this.code_mirror) {
151 151 this.code_mirror.on('blur', function(cm, change) {
152 152 that.events.trigger('command_mode.Cell', {cell: that});
153 153 });
154 154 }
155 155
156 156 this.element.dblclick(function () {
157 157 if (that.selected === false) {
158 158 this.events.trigger('select.Cell', {'cell':that});
159 159 }
160 160 var cont = that.unrender();
161 161 if (cont) {
162 162 that.focus_editor();
163 163 }
164 164 });
165 165 };
166 166
167 167 /**
168 168 * This method gets called in CodeMirror's onKeyDown/onKeyPress
169 169 * handlers and is used to provide custom key handling.
170 170 *
171 171 * To have custom handling, subclasses should override this method, but still call it
172 172 * in order to process the Edit mode keyboard shortcuts.
173 173 *
174 174 * @method handle_codemirror_keyevent
175 175 * @param {CodeMirror} editor - The codemirror instance bound to the cell
176 176 * @param {event} event - key press event which either should or should not be handled by CodeMirror
177 177 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
178 178 */
179 179 Cell.prototype.handle_codemirror_keyevent = function (editor, event) {
180 180 var shortcuts = this.keyboard_manager.edit_shortcuts;
181 181
182 182 var cur = editor.getCursor();
183 183 if((cur.line !== 0 || cur.ch !==0) && event.keyCode === 38){
184 184 event._ipkmIgnore = true;
185 185 }
186 var nLastLine = editor.lastLine()
186 var nLastLine = editor.lastLine()
187 187 if( ( event.keyCode === 40)
188 188 && (( cur.line !== nLastLine)
189 189 || ( cur.ch !== editor.getLineHandle(nLastLine).text.length))
190 190 ){
191 191 event._ipkmIgnore = true;
192 192 }
193 193 // if this is an edit_shortcuts shortcut, the global keyboard/shortcut
194 194 // manager will handle it
195 if (shortcuts.handles(event)) {
196 return true;
195 if (shortcuts.handles(event)) {
196 return true;
197 197 }
198 198
199 199 return false;
200 200 };
201 201
202 202
203 203 /**
204 204 * Triger typsetting of math by mathjax on current cell element
205 205 * @method typeset
206 206 */
207 207 Cell.prototype.typeset = function () {
208 208 if (window.MathJax) {
209 209 var cell_math = this.element.get(0);
210 210 MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]);
211 211 }
212 212 };
213 213
214 214 /**
215 215 * handle cell level logic when a cell is selected
216 216 * @method select
217 217 * @return is the action being taken
218 218 */
219 219 Cell.prototype.select = function () {
220 220 if (!this.selected) {
221 221 this.element.addClass('selected');
222 222 this.element.removeClass('unselected');
223 223 this.selected = true;
224 224 return true;
225 225 } else {
226 226 return false;
227 227 }
228 228 };
229 229
230 230 /**
231 231 * handle cell level logic when a cell is unselected
232 232 * @method unselect
233 233 * @return is the action being taken
234 234 */
235 235 Cell.prototype.unselect = function () {
236 236 if (this.selected) {
237 237 this.element.addClass('unselected');
238 238 this.element.removeClass('selected');
239 239 this.selected = false;
240 240 return true;
241 241 } else {
242 242 return false;
243 243 }
244 244 };
245 245
246 246 /**
247 247 * handle cell level logic when a cell is rendered
248 248 * @method render
249 249 * @return is the action being taken
250 250 */
251 251 Cell.prototype.render = function () {
252 252 if (!this.rendered) {
253 253 this.element.addClass('rendered');
254 254 this.element.removeClass('unrendered');
255 255 this.rendered = true;
256 256 return true;
257 257 } else {
258 258 return false;
259 259 }
260 260 };
261 261
262 262 /**
263 263 * handle cell level logic when a cell is unrendered
264 264 * @method unrender
265 265 * @return is the action being taken
266 266 */
267 267 Cell.prototype.unrender = function () {
268 268 if (this.rendered) {
269 269 this.element.addClass('unrendered');
270 270 this.element.removeClass('rendered');
271 271 this.rendered = false;
272 272 return true;
273 273 } else {
274 274 return false;
275 275 }
276 276 };
277 277
278 278 /**
279 279 * Delegates keyboard shortcut handling to either IPython keyboard
280 280 * manager when in command mode, or CodeMirror when in edit mode
281 281 *
282 282 * @method handle_keyevent
283 283 * @param {CodeMirror} editor - The codemirror instance bound to the cell
284 284 * @param {event} - key event to be handled
285 285 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
286 286 */
287 287 Cell.prototype.handle_keyevent = function (editor, event) {
288 288 if (this.mode === 'command') {
289 289 return true;
290 290 } else if (this.mode === 'edit') {
291 291 return this.handle_codemirror_keyevent(editor, event);
292 292 }
293 293 };
294 294
295 295 /**
296 296 * @method at_top
297 297 * @return {Boolean}
298 298 */
299 299 Cell.prototype.at_top = function () {
300 300 var cm = this.code_mirror;
301 301 var cursor = cm.getCursor();
302 302 if (cursor.line === 0 && cursor.ch === 0) {
303 303 return true;
304 304 }
305 305 return false;
306 306 };
307 307
308 308 /**
309 309 * @method at_bottom
310 310 * @return {Boolean}
311 311 * */
312 312 Cell.prototype.at_bottom = function () {
313 313 var cm = this.code_mirror;
314 314 var cursor = cm.getCursor();
315 315 if (cursor.line === (cm.lineCount()-1) && cursor.ch === cm.getLine(cursor.line).length) {
316 316 return true;
317 317 }
318 318 return false;
319 319 };
320 320
321 321 /**
322 322 * enter the command mode for the cell
323 323 * @method command_mode
324 324 * @return is the action being taken
325 325 */
326 326 Cell.prototype.command_mode = function () {
327 327 if (this.mode !== 'command') {
328 328 this.element.addClass('command_mode');
329 329 this.element.removeClass('edit_mode');
330 330 this.mode = 'command';
331 331 return true;
332 332 } else {
333 333 return false;
334 334 }
335 335 };
336 336
337 337 /**
338 338 * enter the edit mode for the cell
339 339 * @method command_mode
340 340 * @return is the action being taken
341 341 */
342 342 Cell.prototype.edit_mode = function () {
343 343 if (this.mode !== 'edit') {
344 344 this.element.addClass('edit_mode');
345 345 this.element.removeClass('command_mode');
346 346 this.mode = 'edit';
347 347 return true;
348 348 } else {
349 349 return false;
350 350 }
351 351 };
352 352
353 353 /**
354 354 * Focus the cell in the DOM sense
355 355 * @method focus_cell
356 356 */
357 357 Cell.prototype.focus_cell = function () {
358 358 this.element.focus();
359 359 };
360 360
361 361 /**
362 362 * Focus the editor area so a user can type
363 363 *
364 364 * NOTE: If codemirror is focused via a mouse click event, you don't want to
365 365 * call this because it will cause a page jump.
366 366 * @method focus_editor
367 367 */
368 368 Cell.prototype.focus_editor = function () {
369 369 this.refresh();
370 370 this.code_mirror.focus();
371 371 };
372 372
373 373 /**
374 374 * Refresh codemirror instance
375 375 * @method refresh
376 376 */
377 377 Cell.prototype.refresh = function () {
378 378 this.code_mirror.refresh();
379 379 };
380 380
381 381 /**
382 382 * should be overritten by subclass
383 383 * @method get_text
384 384 */
385 385 Cell.prototype.get_text = function () {
386 386 };
387 387
388 388 /**
389 389 * should be overritten by subclass
390 390 * @method set_text
391 391 * @param {string} text
392 392 */
393 393 Cell.prototype.set_text = function (text) {
394 394 };
395 395
396 396 /**
397 397 * should be overritten by subclass
398 398 * serialise cell to json.
399 399 * @method toJSON
400 400 **/
401 401 Cell.prototype.toJSON = function () {
402 402 var data = {};
403 403 // deepcopy the metadata so copied cells don't share the same object
404 404 data.metadata = JSON.parse(JSON.stringify(this.metadata));
405 405 data.cell_type = this.cell_type;
406 406 return data;
407 407 };
408 408
409 409
410 410 /**
411 411 * should be overritten by subclass
412 412 * @method fromJSON
413 413 **/
414 414 Cell.prototype.fromJSON = function (data) {
415 415 if (data.metadata !== undefined) {
416 416 this.metadata = data.metadata;
417 417 }
418 418 this.celltoolbar.rebuild();
419 419 };
420 420
421 421
422 422 /**
423 423 * can the cell be split into two cells (false if not deletable)
424 424 * @method is_splittable
425 425 **/
426 426 Cell.prototype.is_splittable = function () {
427 427 return this.is_deletable();
428 428 };
429 429
430 430
431 431 /**
432 432 * can the cell be merged with other cells (false if not deletable)
433 433 * @method is_mergeable
434 434 **/
435 435 Cell.prototype.is_mergeable = function () {
436 436 return this.is_deletable();
437 437 };
438 438
439 439 /**
440 440 * is the cell deletable? only false (undeletable) if
441 441 * metadata.deletable is explicitly false -- everything else
442 442 * counts as true
443 443 *
444 444 * @method is_deletable
445 445 **/
446 446 Cell.prototype.is_deletable = function () {
447 447 if (this.metadata.deletable === false) {
448 448 return false;
449 449 }
450 450 return true;
451 451 };
452 452
453 453 /**
454 454 * @return {String} - the text before the cursor
455 455 * @method get_pre_cursor
456 456 **/
457 457 Cell.prototype.get_pre_cursor = function () {
458 458 var cursor = this.code_mirror.getCursor();
459 459 var text = this.code_mirror.getRange({line:0, ch:0}, cursor);
460 460 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
461 461 return text;
462 462 };
463 463
464 464
465 465 /**
466 466 * @return {String} - the text after the cursor
467 467 * @method get_post_cursor
468 468 **/
469 469 Cell.prototype.get_post_cursor = function () {
470 470 var cursor = this.code_mirror.getCursor();
471 471 var last_line_num = this.code_mirror.lineCount()-1;
472 472 var last_line_len = this.code_mirror.getLine(last_line_num).length;
473 473 var end = {line:last_line_num, ch:last_line_len};
474 474 var text = this.code_mirror.getRange(cursor, end);
475 475 text = text.replace(/^\n+/, '').replace(/\n+$/, '');
476 476 return text;
477 477 };
478 478
479 479 /**
480 480 * Show/Hide CodeMirror LineNumber
481 481 * @method show_line_numbers
482 482 *
483 483 * @param value {Bool} show (true), or hide (false) the line number in CodeMirror
484 484 **/
485 485 Cell.prototype.show_line_numbers = function (value) {
486 486 this.code_mirror.setOption('lineNumbers', value);
487 487 this.code_mirror.refresh();
488 488 };
489 489
490 490 /**
491 491 * Toggle CodeMirror LineNumber
492 492 * @method toggle_line_numbers
493 493 **/
494 494 Cell.prototype.toggle_line_numbers = function () {
495 495 var val = this.code_mirror.getOption('lineNumbers');
496 496 this.show_line_numbers(!val);
497 497 };
498 498
499 499 /**
500 500 * Force codemirror highlight mode
501 501 * @method force_highlight
502 502 * @param {object} - CodeMirror mode
503 503 **/
504 504 Cell.prototype.force_highlight = function(mode) {
505 505 this.user_highlight = mode;
506 506 this.auto_highlight();
507 507 };
508 508
509 509 /**
510 510 * Try to autodetect cell highlight mode, or use selected mode
511 511 * @methods _auto_highlight
512 512 * @private
513 513 * @param {String|object|undefined} - CodeMirror mode | 'auto'
514 514 **/
515 515 Cell.prototype._auto_highlight = function (modes) {
516 516 //Here we handle manually selected modes
517 517 var that = this;
518 518 var mode;
519 519 if( this.user_highlight !== undefined && this.user_highlight != 'auto' )
520 520 {
521 521 mode = this.user_highlight;
522 522 CodeMirror.autoLoadMode(this.code_mirror, mode);
523 523 this.code_mirror.setOption('mode', mode);
524 524 return;
525 525 }
526 526 var current_mode = this.code_mirror.getOption('mode', mode);
527 527 var first_line = this.code_mirror.getLine(0);
528 528 // loop on every pairs
529 529 for(mode in modes) {
530 530 var regs = modes[mode].reg;
531 531 // only one key every time but regexp can't be keys...
532 532 for(var i=0; i<regs.length; i++) {
533 533 // here we handle non magic_modes
534 534 if(first_line.match(regs[i]) !== null) {
535 535 if(current_mode == mode){
536 536 return;
537 537 }
538 538 if (mode.search('magic_') !== 0) {
539 539 utils.requireCodeMirrorMode(mode, function () {
540 540 that.code_mirror.setOption('mode', mode);
541 541 });
542 542 return;
543 543 }
544 544 var open = modes[mode].open || "%%";
545 545 var close = modes[mode].close || "%%end";
546 var mmode = mode;
547 mode = mmode.substr(6);
548 if(current_mode == mmode){
546 var magic_mode = mode;
547 mode = magic_mode.substr(6);
548 if(current_mode == magic_mode){
549 549 return;
550 550 }
551 551 utils.requireCodeMirrorMode(mode, function () {
552 552 // create on the fly a mode that switch between
553 553 // plain/text and something else, otherwise `%%` is
554 554 // source of some highlight issues.
555 CodeMirror.defineMode(mmode, function(config) {
555 CodeMirror.defineMode(magic_mode, function(config) {
556 556 return CodeMirror.multiplexingMode(
557 557 CodeMirror.getMode(config, 'text/plain'),
558 // always set someting on close
558 // always set something on close
559 559 {open: open, close: close,
560 560 mode: CodeMirror.getMode(config, mode),
561 561 delimStyle: "delimit"
562 562 }
563 563 );
564 564 });
565 that.code_mirror.setOption('mode', mmode);
565 that.code_mirror.setOption('mode', magic_mode);
566 566 });
567 567 return;
568 568 }
569 569 }
570 570 }
571 571 // fallback on default
572 572 var default_mode;
573 573 try {
574 574 default_mode = this._options.cm_config.mode;
575 575 } catch(e) {
576 576 default_mode = 'text/plain';
577 577 }
578 578 if( current_mode === default_mode){
579 579 return;
580 580 }
581 581 this.code_mirror.setOption('mode', default_mode);
582 582 };
583 583
584 584 // Backwards compatibility.
585 585 IPython.Cell = Cell;
586 586
587 587 return {'Cell': Cell};
588 588 });
@@ -1,39 +1,38
1 1 // IPython mode is just a slightly altered Python Mode with `?` beeing a extra
2 2 // single operator. Here we define `ipython` mode in the require `python`
3 3 // callback to auto-load python mode, which is more likely not the best things
4 4 // to do, but at least the simple one for now.
5 5
6 6 (function(mod) {
7 7 if (typeof exports == "object" && typeof module == "object"){ // CommonJS
8 mod(require("codemirror/lib/codemirror"),
8 mod(require("codemirror/lib/codemirror"),
9 9 require("codemirror/mode/python/python")
10 10 );
11 11 } else if (typeof define == "function" && define.amd){ // AMD
12 define(["codemirror/lib/codemirror",
12 define(["codemirror/lib/codemirror",
13 13 "codemirror/mode/python/python"], mod);
14 14 } else {// Plain browser env
15 15 mod(CodeMirror);
16 16 }
17 17 })(function(CodeMirror) {
18 18 "use strict";
19
19
20 20 CodeMirror.defineMode("ipython", function(conf, parserConf) {
21 21 var pythonConf = {};
22 22 for (var prop in parserConf) {
23 23 if (parserConf.hasOwnProperty(prop)) {
24 24 pythonConf[prop] = parserConf[prop];
25 25 }
26 26 }
27 27 pythonConf.name = 'python';
28 28 pythonConf.singleOperators = new RegExp("^[\\+\\-\\*/%&|\\^~<>!\\?]");
29 29 if (pythonConf.version === 3) {
30 30 pythonConf.identifiers = new RegExp("^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*");
31 31 } else if (pythonConf.version === 2) {
32 32 pythonConf.identifiers = new RegExp("^[_A-Za-z][_A-Za-z0-9]*");
33 33 }
34 34 return CodeMirror.getMode(conf, pythonConf);
35 35 }, 'python');
36 36
37 37 CodeMirror.defineMIME("text/x-ipython", "ipython");
38 38 })
39
@@ -1,64 +1,62
1 // IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
2 // Mode with support for latex.
1 // IPython GFM (GitHub Flavored Markdown) mode is just a slightly altered GFM
2 // Mode with support for latex.
3 3 //
4 // Latex support was supported by Codemirror GFM as of
4 // Latex support was supported by Codemirror GFM as of
5 5 // https://github.com/codemirror/CodeMirror/pull/567
6 6 // But was later removed in
7 7 // https://github.com/codemirror/CodeMirror/commit/d9c9f1b1ffe984aee41307f3e927f80d1f23590c
8 8
9 9
10 10 (function(mod) {
11 11 if (typeof exports == "object" && typeof module == "object"){ // CommonJS
12 12 mod(require("codemirror/lib/codemirror")
13 13 ,require("codemirror/addon/mode/multiplex")
14 14 ,require("codemirror/mode/gfm/gfm")
15 15 ,require("codemirror/mode/stex/stex")
16 16 );
17 17 } else if (typeof define == "function" && define.amd){ // AMD
18 define(["codemirror/lib/codemirror"
18 define(["codemirror/lib/codemirror"
19 19 ,"codemirror/addon/mode/multiplex"
20 20 ,"codemirror/mode/python/python"
21 21 ,"codemirror/mode/stex/stex"
22 22 ], mod);
23 23 } else {// Plain browser env
24 24 mod(CodeMirror);
25 25 }
26 26 })( function(CodeMirror){
27 27 "use strict";
28 28
29 29 CodeMirror.defineMode("ipythongfm", function(config, parserConfig) {
30
30
31 31 var gfm_mode = CodeMirror.getMode(config, "gfm");
32 32 var tex_mode = CodeMirror.getMode(config, "stex");
33
33
34 34 return CodeMirror.multiplexingMode(
35 35 gfm_mode,
36 36 {
37 37 open: "$", close: "$",
38 38 mode: tex_mode,
39 39 delimStyle: "delimit"
40 40 },
41 41 {
42 42 // not sure this works as $$ is interpreted at (opening $, closing $, as defined just above)
43 43 open: "$$", close: "$$",
44 44 mode: tex_mode,
45 45 delimStyle: "delimit"
46 46 },
47 47 {
48 48 open: "\\(", close: "\\)",
49 49 mode: tex_mode,
50 50 delimStyle: "delimit"
51 51 },
52 52 {
53 53 open: "\\[", close: "\\]",
54 54 mode: tex_mode,
55 55 delimStyle: "delimit"
56 56 }
57 57 // .. more multiplexed styles can follow here
58 58 );
59 59 }, 'gfm');
60
61 CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
62
63 60
61 CodeMirror.defineMIME("text/x-ipythongfm", "ipythongfm");
64 62 })
General Comments 0
You need to be logged in to leave comments. Login now