diff --git a/IPython/frontend/html/notebook/static/js/initmathjax.js b/IPython/frontend/html/notebook/static/js/mathjaxutils.js similarity index 54% rename from IPython/frontend/html/notebook/static/js/initmathjax.js rename to IPython/frontend/html/notebook/static/js/mathjaxutils.js index 5987eaa..8eeeb7f 100644 --- a/IPython/frontend/html/notebook/static/js/initmathjax.js +++ b/IPython/frontend/html/notebook/static/js/mathjaxutils.js @@ -1,23 +1,28 @@ //---------------------------------------------------------------------------- -// Copyright (C) 2008-2011 The IPython Development Team +// Copyright (C) 2008-2012 The IPython Development Team // // Distributed under the terms of the BSD License. The full license is in // the file COPYING, distributed as part of this software. //---------------------------------------------------------------------------- //============================================================================ -// MathJax initialization +// MathJax utility functions //============================================================================ -var IPython = (function (IPython) { +IPython.namespace('IPython.mathjaxutils'); - var init_mathjax = function () { +IPython.mathjaxutils = (function (IPython) { + + var init = function () { if (window.MathJax) { // MathJax loaded MathJax.Hub.Config({ + TeX: { equationNumbers: { autoNumber: "AMS", useLabelIds: true }, + extensions: ["autoload-all.js"] }, tex2jax: { inlineMath: [ ['$','$'], ["\\(","\\)"] ], - displayMath: [ ['$$','$$'], ["\\[","\\]"] ] + displayMath: [ ['$$','$$'], ["\\[","\\]"] ], + processEnvironments: true }, displayAlign: 'left', // Change this to 'center' to center equations. "HTML-CSS": { @@ -74,10 +79,152 @@ var IPython = (function (IPython) { }; }; + // Some magic for deferring mathematical expressions to MathJaX + // Some of the code here is adapted with permission from Stack Exchange Inc. + + var inline = "$"; // the inline math delimiter + var blocks, start, end, last, braces; // used in searching for math + var math; // stores math until pagedown (Markdown parser) is done + var HUB = MathJax.Hub; + + // MATHSPLIT contains the pattern for math delimiters and special symbols + // needed for searching for math in the text input. + var MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i; + + // The math is in blocks i through j, so + // collect it into one block and clear the others. + // Replace &, <, and > by named entities. + // For IE, put
at the ends of comments since IE removes \n. + // Clear the current math positions and store the index of the + // math, then push the math string onto the storage array. + // The preProcess function is called on all blocks if it has been passed in + function processMath(i, j, preProcess) { + var block = blocks.slice(i, j + 1).join("").replace(/&/g, "&") // use HTML entity for & + .replace(//g, ">") // use HTML entity for > + ; + if (HUB.Browser.isMSIE) { + block = block.replace(/(%[^\n]*)\n/g, "$1
\n") + } + while (j > i) { + blocks[j] = ""; + j--; + } + blocks[i] = "@@" + math.length + "@@"; // replace the current block text with a unique tag to find later + if (preProcess) + block = preProcess(block); + math.push(block); + start = end = last = null; + } + + // Break up the text into its component parts and search + // through them for math delimiters, braces, linebreaks, etc. + // Math delimiters must match and braces must balance. + // Don't allow math to pass through a double linebreak + // (which will be a paragraph). + // + function removeMath(text) { + start = end = last = null; // for tracking math delimiters + math = []; // stores math strings for later + + // Except for extreme edge cases, this should catch precisely those pieces of the markdown + // source that will later be turned into code spans. While MathJax will not TeXify code spans, + // we still have to consider them at this point; the following issue has happened several times: + // + // `$foo` and `$bar` are varibales. --> $foo ` and `$bar are variables. + + var hasCodeSpans = /`/.test(text), + deTilde; + if (hasCodeSpans) { + text = text.replace(/~/g, "~T").replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, function (wholematch) { + return wholematch.replace(/\$/g, "~D"); + }); + deTilde = function (text) { return text.replace(/~([TD])/g, function (wholematch, character) { return { T: "~", D: "$" }[character]; }) }; + } else { + deTilde = function (text) { return text; }; + } + + blocks = IPython.utils.regex_split(text.replace(/\r\n?/g, "\n"),MATHSPLIT); + + for (var i = 1, m = blocks.length; i < m; i += 2) { + var block = blocks[i]; + if (block.charAt(0) === "@") { + // + // Things that look like our math markers will get + // stored and then retrieved along with the math. + // + blocks[i] = "@@" + math.length + "@@"; + math.push(block); + } + else if (start) { + // + // If we are in math, look for the end delimiter, + // but don't go past double line breaks, and + // and balance braces within the math. + // + if (block === end) { + if (braces) { + last = i + } + else { + processMath(start, i, deTilde) + } + } + else if (block.match(/\n.*\n/)) { + if (last) { + i = last; + processMath(start, i, deTilde) + } + start = end = last = null; + braces = 0; + } + else if (block === "{") { + braces++ + } + else if (block === "}" && braces) { + braces-- + } + } + else { + // + // Look for math start delimiters and when + // found, set up the end delimiter. + // + if (block === inline || block === "$$") { + start = i; + end = block; + braces = 0; + } + else if (block.substr(1, 5) === "begin") { + start = i; + end = "\\end" + block.substr(6); + braces = 0; + } + } + } + if (last) { + processMath(start, last, deTilde) + } + return deTilde(blocks.join("")); + } - // Set module variables - IPython.init_mathjax = init_mathjax; + // + // Put back the math strings that were saved, + // and clear the math array (no need to keep it around). + // + function replaceMath(text) { + text = text.replace(/@@(\d+)@@/g, function (match, n) { + return math[n] + }); + math = null; + return text; + } - return IPython; + return { + init : init, + processMath : processMath, + removeMath : removeMath, + replaceMath : replaceMath, + }; }(IPython)); \ No newline at end of file diff --git a/IPython/frontend/html/notebook/static/js/notebookmain.js b/IPython/frontend/html/notebook/static/js/notebookmain.js index a7d7d43..13059fc 100644 --- a/IPython/frontend/html/notebook/static/js/notebookmain.js +++ b/IPython/frontend/html/notebook/static/js/notebookmain.js @@ -31,7 +31,7 @@ $(document).ready(function () { } // end monkey patching CodeMirror - IPython.init_mathjax(); + IPython.mathjaxutils.init(); IPython.read_only = $('body').data('readOnly') === 'True'; $('div#main_app').addClass('border-box-sizing ui-widget'); diff --git a/IPython/frontend/html/notebook/static/js/printnotebookmain.js b/IPython/frontend/html/notebook/static/js/printnotebookmain.js index ba773cc..59a62b0 100644 --- a/IPython/frontend/html/notebook/static/js/printnotebookmain.js +++ b/IPython/frontend/html/notebook/static/js/printnotebookmain.js @@ -12,7 +12,7 @@ $(document).ready(function () { - IPython.init_mathjax(); + IPython.mathjaxutils.init(); IPython.read_only = $('body').data('readOnly') === 'True'; $('div#main_app').addClass('border-box-sizing ui-widget'); diff --git a/IPython/frontend/html/notebook/static/js/textcell.js b/IPython/frontend/html/notebook/static/js/textcell.js index 14704d1..54bc5a3 100644 --- a/IPython/frontend/html/notebook/static/js/textcell.js +++ b/IPython/frontend/html/notebook/static/js/textcell.js @@ -1,5 +1,5 @@ //---------------------------------------------------------------------------- -// Copyright (C) 2008-2011 The IPython Development Team +// Copyright (C) 2008-2012 The IPython Development Team // // Distributed under the terms of the BSD License. The full license is in // the file COPYING, distributed as part of this software. @@ -221,7 +221,11 @@ var IPython = (function (IPython) { if (this.rendered === false) { var text = this.get_text(); if (text === "") { text = this.placeholder; } + + text = IPython.mathjaxutils.removeMath(text) var html = IPython.markdown_converter.makeHtml(text); + html = IPython.mathjaxutils.replaceMath(html) + try { this.set_rendered(html); } catch (e) { diff --git a/IPython/frontend/html/notebook/static/js/utils.js b/IPython/frontend/html/notebook/static/js/utils.js index 9dfa478..d38d6da 100644 --- a/IPython/frontend/html/notebook/static/js/utils.js +++ b/IPython/frontend/html/notebook/static/js/utils.js @@ -1,5 +1,5 @@ //---------------------------------------------------------------------------- -// Copyright (C) 2008-2011 The IPython Development Team +// Copyright (C) 2008-2012 The IPython Development Team // // Distributed under the terms of the BSD License. The full license is in // the file COPYING, distributed as part of this software. @@ -13,6 +13,123 @@ IPython.namespace('IPython.utils'); IPython.utils = (function (IPython) { + //============================================================================ + // Cross-browser RegEx Split + //============================================================================ + + // This code has been MODIFIED from the code licensed below to not replace the + // default browser split. The license is reproduced here. + + // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info: + /*! + * Cross-Browser Split 1.1.1 + * Copyright 2007-2012 Steven Levithan + * Available under the MIT License + * ECMAScript compliant, uniform cross-browser split method + */ + + /** + * Splits a string into an array of strings using a regex or string + * separator. Matches of the separator are not included in the result array. + * However, if `separator` is a regex that contains capturing groups, + * backreferences are spliced into the result each time `separator` is + * matched. Fixes browser bugs compared to the native + * `String.prototype.split` and can be used reliably cross-browser. + * @param {String} str String to split. + * @param {RegExp|String} separator Regex or string to use for separating + * the string. + * @param {Number} [limit] Maximum number of items to include in the result + * array. + * @returns {Array} Array of substrings. + * @example + * + * // Basic use + * regex_split('a b c d', ' '); + * // -> ['a', 'b', 'c', 'd'] + * + * // With limit + * regex_split('a b c d', ' ', 2); + * // -> ['a', 'b'] + * + * // Backreferences in result array + * regex_split('..word1 word2..', /([a-z]+)(\d+)/i); + * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] + */ + var regex_split = function (str, separator, limit) { + // If `separator` is not a regex, use `split` + if (Object.prototype.toString.call(separator) !== "[object RegExp]") { + return split.call(str, separator, limit); + } + var output = [], + flags = (separator.ignoreCase ? "i" : "") + + (separator.multiline ? "m" : "") + + (separator.extended ? "x" : "") + // Proposed for ES6 + (separator.sticky ? "y" : ""), // Firefox 3+ + lastLastIndex = 0, + // Make `global` and avoid `lastIndex` issues by working with a copy + separator = new RegExp(separator.source, flags + "g"), + separator2, match, lastIndex, lastLength; + str += ""; // Type-convert + + compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined" + if (!compliantExecNpcg) { + // Doesn't need flags gy, but they don't hurt + separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags); + } + /* Values for `limit`, per the spec: + * If undefined: 4294967295 // Math.pow(2, 32) - 1 + * If 0, Infinity, or NaN: 0 + * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296; + * If negative number: 4294967296 - Math.floor(Math.abs(limit)) + * If other: Type-convert, then use the above rules + */ + limit = typeof(limit) === "undefined" ? + -1 >>> 0 : // Math.pow(2, 32) - 1 + limit >>> 0; // ToUint32(limit) + while (match = separator.exec(str)) { + // `separator.lastIndex` is not reliable cross-browser + lastIndex = match.index + match[0].length; + if (lastIndex > lastLastIndex) { + output.push(str.slice(lastLastIndex, match.index)); + // Fix browsers whose `exec` methods don't consistently return `undefined` for + // nonparticipating capturing groups + if (!compliantExecNpcg && match.length > 1) { + match[0].replace(separator2, function () { + for (var i = 1; i < arguments.length - 2; i++) { + if (typeof(arguments[i]) === "undefined") { + match[i] = undefined; + } + } + }); + } + if (match.length > 1 && match.index < str.length) { + Array.prototype.push.apply(output, match.slice(1)); + } + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= limit) { + break; + } + } + if (separator.lastIndex === match.index) { + separator.lastIndex++; // Avoid an infinite loop + } + } + if (lastLastIndex === str.length) { + if (lastLength || !separator.test("")) { + output.push(""); + } + } else { + output.push(str.slice(lastLastIndex)); + } + return output.length > limit ? output.slice(0, limit) : output; + }; + + //============================================================================ + // End contributed Cross-browser RegEx Split + //============================================================================ + + var uuid = function () { // http://www.ietf.org/rfc/rfc4122.txt var s = []; @@ -138,6 +255,7 @@ IPython.utils = (function (IPython) { return { + regex_split : regex_split, uuid : uuid, fixConsole : fixConsole, keycodes : keycodes, diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 08579a6..ef991d4 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -199,7 +199,7 @@ data-notebook-id={{notebook_id}} - + diff --git a/IPython/frontend/html/notebook/templates/printnotebook.html b/IPython/frontend/html/notebook/templates/printnotebook.html index ca0a34a..93aeae4 100644 --- a/IPython/frontend/html/notebook/templates/printnotebook.html +++ b/IPython/frontend/html/notebook/templates/printnotebook.html @@ -69,7 +69,7 @@ data-notebook-id={{notebook_id}} - +