##// END OF EJS Templates
Markdown in heading cells (take 2)...
MinRK -
Show More
@@ -1,552 +1,557 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // TextCell
10 10 //============================================================================
11 11
12 12
13 13
14 14 /**
15 15 A module that allow to create different type of Text Cell
16 16 @module IPython
17 17 @namespace IPython
18 18 */
19 19 var IPython = (function (IPython) {
20 20
21 21 // TextCell base class
22 22 var key = IPython.utils.keycodes;
23 23
24 24 /**
25 25 * Construct a new TextCell, codemirror mode is by default 'htmlmixed', and cell type is 'text'
26 26 * cell start as not redered.
27 27 *
28 28 * @class TextCell
29 29 * @constructor TextCell
30 * @extend Ipython.Cell
30 * @extend IPython.Cell
31 31 * @param {object|undefined} [options]
32 32 * @param [options.cm_config] {object} config to pass to CodeMirror, will extend/overwrite default config
33 33 * @param [options.placeholder] {string} default string to use when souce in empty for rendering (only use in some TextCell subclass)
34 34 */
35 35 var TextCell = function (options) {
36 36 // in all TextCell/Cell subclasses
37 37 // do not assign most of members here, just pass it down
38 38 // in the options dict potentially overwriting what you wish.
39 39 // they will be assigned in the base class.
40 40
41 41 // we cannot put this as a class key as it has handle to "this".
42 42 var cm_overwrite_options = {
43 43 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
44 44 };
45 45
46 46 options = this.mergeopt(TextCell,options,{cm_config:cm_overwrite_options});
47 47
48 48 IPython.Cell.apply(this, [options]);
49 49
50 50
51 51 this.rendered = false;
52 52 this.cell_type = this.cell_type || 'text';
53 53 };
54 54
55 55 TextCell.prototype = new IPython.Cell();
56 56
57 57 TextCell.options_default = {
58 58 cm_config : {
59 59 extraKeys: {"Tab": "indentMore","Shift-Tab" : "indentLess"},
60 60 mode: 'htmlmixed',
61 61 lineWrapping : true,
62 62 }
63 63 };
64 64
65 65
66 66
67 67 /**
68 68 * Create the DOM element of the TextCell
69 69 * @method create_element
70 70 * @private
71 71 */
72 72 TextCell.prototype.create_element = function () {
73 73 IPython.Cell.prototype.create_element.apply(this, arguments);
74 74 var cell = $("<div>").addClass('cell text_cell border-box-sizing');
75 75 cell.attr('tabindex','2');
76 76
77 77 this.celltoolbar = new IPython.CellToolbar(this);
78 78 cell.append(this.celltoolbar.element);
79 79
80 80 var input_area = $('<div/>').addClass('text_cell_input border-box-sizing');
81 81 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
82 82
83 83 // The tabindex=-1 makes this div focusable.
84 84 var render_area = $('<div/>').addClass('text_cell_render border-box-sizing').
85 85 addClass('rendered_html').attr('tabindex','-1');
86 86 cell.append(input_area).append(render_area);
87 87 this.element = cell;
88 88 };
89 89
90 90
91 91 /**
92 92 * Bind the DOM evet to cell actions
93 93 * Need to be called after TextCell.create_element
94 94 * @private
95 95 * @method bind_event
96 96 */
97 97 TextCell.prototype.bind_events = function () {
98 98 IPython.Cell.prototype.bind_events.apply(this);
99 99 var that = this;
100 100 this.element.keydown(function (event) {
101 101 if (event.which === 13 && !event.shiftKey) {
102 102 if (that.rendered) {
103 103 that.edit();
104 104 return false;
105 105 };
106 106 };
107 107 });
108 108 this.element.dblclick(function () {
109 109 that.edit();
110 110 });
111 111 };
112 112
113 113 /**
114 114 * This method gets called in CodeMirror's onKeyDown/onKeyPress
115 115 * handlers and is used to provide custom key handling.
116 116 *
117 117 * Subclass should override this method to have custom handeling
118 118 *
119 119 * @method handle_codemirror_keyevent
120 120 * @param {CodeMirror} editor - The codemirror instance bound to the cell
121 121 * @param {event} event -
122 122 * @return {Boolean} `true` if CodeMirror should ignore the event, `false` Otherwise
123 123 */
124 124 TextCell.prototype.handle_codemirror_keyevent = function (editor, event) {
125 125
126 126 if (event.keyCode === 13 && (event.shiftKey || event.ctrlKey)) {
127 127 // Always ignore shift-enter in CodeMirror as we handle it.
128 128 return true;
129 129 }
130 130 return false;
131 131 };
132 132
133 133 /**
134 134 * Select the current cell and trigger 'focus'
135 135 * @method select
136 136 */
137 137 TextCell.prototype.select = function () {
138 138 IPython.Cell.prototype.select.apply(this);
139 139 var output = this.element.find("div.text_cell_render");
140 140 output.trigger('focus');
141 141 };
142 142
143 143 /**
144 144 * unselect the current cell and `render` it
145 145 * @method unselect
146 146 */
147 147 TextCell.prototype.unselect = function() {
148 148 // render on selection of another cell
149 149 this.render();
150 150 IPython.Cell.prototype.unselect.apply(this);
151 151 };
152 152
153 153 /**
154 154 *
155 155 * put the current cell in edition mode
156 156 * @method edit
157 157 */
158 158 TextCell.prototype.edit = function () {
159 159 if ( this.read_only ) return;
160 160 if (this.rendered === true) {
161 161 var text_cell = this.element;
162 162 var output = text_cell.find("div.text_cell_render");
163 163 output.hide();
164 164 text_cell.find('div.text_cell_input').show();
165 165 this.code_mirror.refresh();
166 166 this.code_mirror.focus();
167 167 // We used to need an additional refresh() after the focus, but
168 168 // it appears that this has been fixed in CM. This bug would show
169 169 // up on FF when a newly loaded markdown cell was edited.
170 170 this.rendered = false;
171 171 if (this.get_text() === this.placeholder) {
172 172 this.set_text('');
173 173 this.refresh();
174 174 }
175 175 }
176 176 };
177 177
178 178
179 179 /**
180 180 * Empty, Subclasses must define render.
181 181 * @method render
182 182 */
183 183 TextCell.prototype.render = function () {};
184 184
185 185
186 186 /**
187 187 * setter: {{#crossLink "TextCell/set_text"}}{{/crossLink}}
188 188 * @method get_text
189 189 * @retrun {string} CodeMirror current text value
190 190 */
191 191 TextCell.prototype.get_text = function() {
192 192 return this.code_mirror.getValue();
193 193 };
194 194
195 195 /**
196 196 * @param {string} text - Codemiror text value
197 197 * @see TextCell#get_text
198 198 * @method set_text
199 199 * */
200 200 TextCell.prototype.set_text = function(text) {
201 201 this.code_mirror.setValue(text);
202 202 this.code_mirror.refresh();
203 203 };
204 204
205 205 /**
206 206 * setter :{{#crossLink "TextCell/set_rendered"}}{{/crossLink}}
207 207 * @method get_rendered
208 208 * @return {html} html of rendered element
209 209 * */
210 210 TextCell.prototype.get_rendered = function() {
211 211 return this.element.find('div.text_cell_render').html();
212 212 };
213 213
214 214 /**
215 215 * @method set_rendered
216 216 */
217 217 TextCell.prototype.set_rendered = function(text) {
218 218 this.element.find('div.text_cell_render').html(text);
219 219 };
220 220
221 221 /**
222 222 * not deprecated, but implementation wrong
223 223 * @method at_top
224 224 * @deprecated
225 225 * @return {Boolean} true is cell rendered, false otherwise
226 226 * I doubt this is what it is supposed to do
227 227 * this implementation is completly false
228 228 */
229 229 TextCell.prototype.at_top = function () {
230 230 if (this.rendered) {
231 231 return true;
232 232 } else {
233 233 return false;
234 234 }
235 235 };
236 236
237 237
238 238 /**
239 239 * not deprecated, but implementation wrong
240 240 * @method at_bottom
241 241 * @deprecated
242 242 * @return {Boolean} true is cell rendered, false otherwise
243 243 * I doubt this is what it is supposed to do
244 244 * this implementation is completly false
245 245 * */
246 246 TextCell.prototype.at_bottom = function () {
247 247 if (this.rendered) {
248 248 return true;
249 249 } else {
250 250 return false;
251 251 }
252 252 };
253 253
254 254 /**
255 255 * Create Text cell from JSON
256 256 * @param {json} data - JSON serialized text-cell
257 257 * @method fromJSON
258 258 */
259 259 TextCell.prototype.fromJSON = function (data) {
260 260 IPython.Cell.prototype.fromJSON.apply(this, arguments);
261 261 if (data.cell_type === this.cell_type) {
262 262 if (data.source !== undefined) {
263 263 this.set_text(data.source);
264 264 // make this value the starting point, so that we can only undo
265 265 // to this state, instead of a blank cell
266 266 this.code_mirror.clearHistory();
267 267 this.set_rendered(data.rendered || '');
268 268 this.rendered = false;
269 269 this.render();
270 270 }
271 271 }
272 272 };
273 273
274 274 /** Generate JSON from cell
275 275 * @return {object} cell data serialised to json
276 276 */
277 277 TextCell.prototype.toJSON = function () {
278 278 var data = IPython.Cell.prototype.toJSON.apply(this);
279 279 data.cell_type = this.cell_type;
280 280 data.source = this.get_text();
281 281 return data;
282 282 };
283 283
284 284
285 285 /**
286 286 * @class MarkdownCell
287 287 * @constructor MarkdownCell
288 * @extends Ipython.HtmlCell
288 * @extends IPython.HTMLCell
289 289 */
290 290 var MarkdownCell = function (options) {
291 291 var options = options || {};
292 292
293 293 options = this.mergeopt(MarkdownCell,options);
294 294 TextCell.apply(this, [options]);
295 295
296 296 this.cell_type = 'markdown';
297 297 };
298 298
299 299 MarkdownCell.options_default = {
300 300 cm_config: {
301 301 mode: 'gfm'
302 302 },
303 303 placeholder: "Type *Markdown* and LaTeX: $\\alpha^2$"
304 304 }
305 305
306 306
307 307
308 308
309 309 MarkdownCell.prototype = new TextCell();
310 310
311 311 /**
312 312 * @method render
313 313 */
314 314 MarkdownCell.prototype.render = function () {
315 315 if (this.rendered === false) {
316 316 var text = this.get_text();
317 317 if (text === "") { text = this.placeholder; }
318 318 text = IPython.mathjaxutils.remove_math(text);
319 319 var html = marked.parser(marked.lexer(text));
320 320 html = $(IPython.mathjaxutils.replace_math(html));
321 321 // links in markdown cells should open in new tabs
322 322 html.find("a[href]").attr("target", "_blank");
323 323 try {
324 324 this.set_rendered(html);
325 325 } catch (e) {
326 326 console.log("Error running Javascript in Markdown:");
327 327 console.log(e);
328 328 this.set_rendered($("<div/>").addClass("js-error").html(
329 329 "Error rendering Markdown!<br/>" + e.toString())
330 330 );
331 331 }
332 332 this.element.find('div.text_cell_input').hide();
333 333 this.element.find("div.text_cell_render").show();
334 334 this.typeset()
335 335 this.rendered = true;
336 336 }
337 337 };
338 338
339 339
340 340 // RawCell
341 341
342 342 /**
343 343 * @class RawCell
344 344 * @constructor RawCell
345 * @extends Ipython.TextCell
345 * @extends IPython.TextCell
346 346 */
347 347 var RawCell = function (options) {
348 348
349 349 options = this.mergeopt(RawCell,options)
350 350 TextCell.apply(this, [options]);
351 351
352 352 this.cell_type = 'raw';
353 353
354 354 var that = this
355 355 this.element.focusout(
356 356 function() { that.auto_highlight(); }
357 357 );
358 358 };
359 359
360 360 RawCell.options_default = {
361 361 placeholder : "Type plain text and LaTeX: $\\alpha^2$"
362 362 };
363 363
364 364
365 365
366 366 RawCell.prototype = new TextCell();
367 367
368 368 /**
369 369 * Trigger autodetection of highlight scheme for current cell
370 370 * @method auto_highlight
371 371 */
372 372 RawCell.prototype.auto_highlight = function () {
373 373 this._auto_highlight(IPython.config.raw_cell_highlight);
374 374 };
375 375
376 376 /** @method render **/
377 377 RawCell.prototype.render = function () {
378 378 this.rendered = true;
379 379 this.edit();
380 380 };
381 381
382 382
383 383 /** @method handle_codemirror_keyevent **/
384 384 RawCell.prototype.handle_codemirror_keyevent = function (editor, event) {
385 385
386 386 var that = this;
387 387 if (event.which === key.UPARROW && event.type === 'keydown') {
388 388 // If we are not at the top, let CM handle the up arrow and
389 389 // prevent the global keydown handler from handling it.
390 390 if (!that.at_top()) {
391 391 event.stop();
392 392 return false;
393 393 } else {
394 394 return true;
395 395 };
396 396 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
397 397 // If we are not at the bottom, let CM handle the down arrow and
398 398 // prevent the global keydown handler from handling it.
399 399 if (!that.at_bottom()) {
400 400 event.stop();
401 401 return false;
402 402 } else {
403 403 return true;
404 404 };
405 405 };
406 406 return false;
407 407 };
408 408
409 409 /** @method select **/
410 410 RawCell.prototype.select = function () {
411 411 IPython.Cell.prototype.select.apply(this);
412 412 this.code_mirror.refresh();
413 413 this.code_mirror.focus();
414 414 };
415 415
416 416 /** @method at_top **/
417 417 RawCell.prototype.at_top = function () {
418 418 var cursor = this.code_mirror.getCursor();
419 419 if (cursor.line === 0 && cursor.ch === 0) {
420 420 return true;
421 421 } else {
422 422 return false;
423 423 }
424 424 };
425 425
426 426
427 427 /** @method at_bottom **/
428 428 RawCell.prototype.at_bottom = function () {
429 429 var cursor = this.code_mirror.getCursor();
430 430 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
431 431 return true;
432 432 } else {
433 433 return false;
434 434 }
435 435 };
436 436
437 437
438 438 /**
439 439 * @class HeadingCell
440 * @extends Ipython.TextCell
440 * @extends IPython.TextCell
441 441 */
442 442
443 443 /**
444 444 * @constructor HeadingCell
445 * @extends Ipython.TextCell
445 * @extends IPython.TextCell
446 446 */
447 447 var HeadingCell = function (options) {
448 448
449 449 options = this.mergeopt(HeadingCell,options)
450 450 TextCell.apply(this, [options]);
451 451
452 452 /**
453 453 * heading level of the cell, use getter and setter to access
454 454 * @property level
455 455 */
456 456 this.level = 1;
457 457 this.cell_type = 'heading';
458 458 };
459 459
460 460 HeadingCell.options_default = {
461 461 placeholder: "Type Heading Here"
462 462 };
463 463
464 464 HeadingCell.prototype = new TextCell();
465 465
466 466 /** @method fromJSON */
467 467 HeadingCell.prototype.fromJSON = function (data) {
468 468 if (data.level != undefined){
469 469 this.level = data.level;
470 470 }
471 471 TextCell.prototype.fromJSON.apply(this, arguments);
472 472 };
473 473
474 474
475 475 /** @method toJSON */
476 476 HeadingCell.prototype.toJSON = function () {
477 477 var data = TextCell.prototype.toJSON.apply(this);
478 478 data.level = this.get_level();
479 479 return data;
480 480 };
481 481
482 482
483 483 /**
484 484 * Change heading level of cell, and re-render
485 485 * @method set_level
486 486 */
487 487 HeadingCell.prototype.set_level = function (level) {
488 488 this.level = level;
489 489 if (this.rendered) {
490 490 this.rendered = false;
491 491 this.render();
492 492 };
493 493 };
494 494
495 495 /** The depth of header cell, based on html (h1 to h6)
496 496 * @method get_level
497 497 * @return {integer} level - for 1 to 6
498 498 */
499 499 HeadingCell.prototype.get_level = function () {
500 500 return this.level;
501 501 };
502 502
503 503
504 HeadingCell.prototype.set_rendered = function (text) {
505 var r = this.element.find("div.text_cell_render");
506 r.empty();
507 var link = text.replace(/ /g, '_');
508 r.append(
509 $('<h'+this.level+'/>')
510 .append(
511 $('<a/>')
512 .addClass('heading-anchor')
513 .attr('id', link)
514 .html(text)
515 ).append(
516 $('<a/>')
517 .addClass('anchor-link')
518 .attr('href', '#' + link)
519 .text('¶')
520 )
521 );
504 HeadingCell.prototype.set_rendered = function (html) {
505 this.element.find("div.text_cell_render").html(html);
522 506 };
523 507
524 508
525 509 HeadingCell.prototype.get_rendered = function () {
526 510 var r = this.element.find("div.text_cell_render");
527 511 return r.children().first().html();
528 512 };
529 513
530 514
531 515 HeadingCell.prototype.render = function () {
532 516 if (this.rendered === false) {
533 517 var text = this.get_text();
534 518 if (text === "") { text = this.placeholder; }
535 this.set_rendered(text);
519 text = Array(this.level + 1).join("#") + " " + text;
520 text = IPython.mathjaxutils.remove_math(text);
521 var html = marked.parser(marked.lexer(text));
522 var h = $(IPython.mathjaxutils.replace_math(html));
523 // move the markdown-rendered html down one level,
524 // into an anchor tag for linking
525 html = h.html();
526 var hash = h.text().replace(/ /g, '_');
527 h.empty();
528 var a = $('<a/>')
529 .addClass('heading-anchor')
530 .html(html)
531 .attr('id', hash);
532 // and append two anchors (one with the text, one clickable)
533 h.append(a).append(
534 $('<a/>')
535 .addClass('anchor-link')
536 .attr('href', '#' + hash)
537 .text('¶')
538 );
539
540 this.set_rendered(h);
536 541 this.typeset();
537 542 this.element.find('div.text_cell_input').hide();
538 543 this.element.find("div.text_cell_render").show();
539 544 this.rendered = true;
540 545 };
541 546 };
542 547
543 548 IPython.TextCell = TextCell;
544 549 IPython.MarkdownCell = MarkdownCell;
545 550 IPython.RawCell = RawCell;
546 551 IPython.HeadingCell = HeadingCell;
547 552
548 553
549 554 return IPython;
550 555
551 556 }(IPython));
552 557
General Comments 0
You need to be logged in to leave comments. Login now