diff --git a/common/static/js/vendor/Markdown.Editor.js b/common/static/js/vendor/Markdown.Editor.js index 1267058702..9959ade634 100644 --- a/common/static/js/vendor/Markdown.Editor.js +++ b/common/static/js/vendor/Markdown.Editor.js @@ -54,7 +54,7 @@ idPostfix = idPostfix || ""; var hooks = this.hooks = new Markdown.HookCollection(); - hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("onPreviewPush"); // called with no arguments after the preview has been refreshed hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen @@ -72,7 +72,7 @@ panels = new PanelCollection(idPostfix); var commandManager = new CommandManager(hooks); - var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); + var previewManager = new PreviewManager(markdownConverter, panels, function (text, previewSet) { hooks.onPreviewPush(text, previewSet); }); var undoManager, uiManager; if (!/\?noundo/.test(doc.location.href)) { @@ -769,7 +769,7 @@ this.init(); }; - function PreviewManager(converter, panels, previewRefreshCallback) { + function PreviewManager(converter, panels, previewPushCallback) { var managerObj = this; var timeout; @@ -928,8 +928,7 @@ var emptyTop = position.getTop(panels.input) - getDocScrollTop(); if (panels.preview) { - previewSet(text); - previewRefreshCallback(); + previewPushCallback(text, previewSet); } setPanelScrollTops(); @@ -2157,4 +2156,4 @@ } -})(); \ No newline at end of file +})(); diff --git a/common/static/js/vendor/split.js b/common/static/js/vendor/split.js new file mode 100644 index 0000000000..50a3bbde07 --- /dev/null +++ b/common/static/js/vendor/split.js @@ -0,0 +1,118 @@ +/*! + * 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 + * split('a b c d', ' '); + * // -> ['a', 'b', 'c', 'd'] + * + * // With limit + * split('a b c d', ' ', 2); + * // -> ['a', 'b'] + * + * // Backreferences in result array + * split('..word1 word2..', /([a-z]+)(\d+)/i); + * // -> ['..', 'word', '1', ' ', 'word', '2', '..'] + */ + +var _split; // instead of split for a less common name; avoid conflict + +// Avoid running twice; that would break the `nativeSplit` reference +_split = _split || function (undef) { + + var nativeSplit = String.prototype.split, + compliantExecNpcg = /()??/.exec("")[1] === undef, // NPCG: nonparticipating capturing group + self; + + self = function (str, separator, limit) { + // If `separator` is not a regex, use `nativeSplit` + if (Object.prototype.toString.call(separator) !== "[object RegExp]") { + return nativeSplit.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 + 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 = limit === undef ? + -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 (arguments[i] === undef) { + match[i] = undef; + } + } + }); + } + 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; + }; + + // For convenience + String.prototype.split = function (separator, limit) { + return self(this, separator, limit); + }; + + return self; + +}(); + diff --git a/lms/djangoapps/django_comment_client/utils.py b/lms/djangoapps/django_comment_client/utils.py index 603029a7f6..35c2a86a9a 100644 --- a/lms/djangoapps/django_comment_client/utils.py +++ b/lms/djangoapps/django_comment_client/utils.py @@ -35,9 +35,6 @@ def get_discussion_title(request, course, discussion_id): if not _DISCUSSIONINFO: initialize_discussion_info(request, course) title = _DISCUSSIONINFO['by_id'].get(discussion_id, {}).get('title', '(no title)') - if title == '(no title)': - print "title shouldn't be none" - import pdb; pdb.set_trace() return title def initialize_discussion_info(request, course): diff --git a/lms/static/coffee/src/customwmd.coffee b/lms/static/coffee/src/customwmd.coffee index 94434738c6..6b3a040a35 100644 --- a/lms/static/coffee/src/customwmd.coffee +++ b/lms/static/coffee/src/customwmd.coffee @@ -1,21 +1,179 @@ +# Mostly adapted from math.stackexchange.com: http://cdn.sstatic.net/js/mathjax-editing-new.js + $ -> + HUB = MathJax.Hub + + class MathJaxProcessor + + inlineMark = "$" + math = null + blocks = null + + MATHSPLIT = /// ( + \$\$? # normal inline or display delimiter + | \\(?:begin|end)\{[a-z]*\*?\} # \begin{} \end{} style + | \\[\\{}$] + | [{}] + | (?:\n\s*)+ # only treat as math when there's single new line + | @@\d+@@ # delimiter similar to the one used internally + ) ///i + + CODESPAN = /// + (^|[^\\]) # match beginning or any previous character other than escape delimiter ('/') + (`+) # code span starts + ([^\n]*?[^`\n]) # code content + \2 # code span ends + (?!`) + ///gm + + ###HUB.Queue -> + console.log "initializing" + renderReady = true + HUB.processUpdateTime = 50 + HUB.Config + "HTML-CSS": + EqnChunk: 10 + EqnChunkFactor: 1 + SVG: + EqnChunk: 10 + EqnChunkFactor: 1 + ### + + @processMath: (start, last, preProcess) => + block = blocks.slice(start, last + 1).join("").replace(/&/g, "&") + .replace(//g, ">") + if HUB.Browser.isMSIE + block = block.replace /(%[^\n]*)\n/g, "$1
\n" + blocks[i] = "" for i in [start+1..last] + blocks[start] = "@@#{math.length}@@" + block = preProcess(block) if preProcess + math.push block + + @removeMath: (text) => + + math = [] + start = end = last = null + braces = 0 + + hasCodeSpans = /`/.test text + if hasCodeSpans + text = text.replace(/~/g, "~T").replace CODESPAN, ($0) -> # replace dollar sign in code span temporarily + $0.replace /\$/g, "~D" + deTilde = (text) -> + text.replace /~([TD])/g, ($0, $1) -> + {T: "~", D: "$"}[$1] + else + deTilde = (text) -> text + + blocks = _split(text.replace(/\r\n?/g, "\n"), MATHSPLIT) + + for current in [1...blocks.length] by 2 + block = blocks[current] + if block.charAt(0) == "@" + blocks[current] = "@@#{math.length}@@" + math.push block + else if start + if block == end + if braces + last = current + else + @processMath(start, current, deTilde) + start = end = last = null + else if block.match /\n.*\n/ + if last + current = last + @processMath(start, current, deTilde) + start = end = last = null + braces = 0 + else if block == "{" + ++braces + else if block == "}" and braces + --braces + else + if block == inlineMark or block == "$$" + start = current + end = block + braces = 0 + else if block.substr(1, 5) == "begin" + start = current + end = "\\end" + block.substr(6) + braces = 0 + + if last + @processMath(start, last, deTilde) + start = end = last = null + + deTilde(blocks.join("")) + + @replaceMath: (text) => + text = text.replace /@@(\d+)@@/g, ($0, $1) => math[$1] + math = null + text + + @updateMathJax: => + HUB.Queue(["Typeset", HUB, "wmd-preview"]) + + + + ### + if not HUB.Cancel? #and 1 == 2 + HUB.cancelTypeset = false + CANCELMESSAGE = "MathJax Canceled" + + HOOKS = [ + { + name: "HTML-CSS Jax Config" + engine: -> window["MathJax"].OutputJax["HTML-CSS"] + }, + { + name: "SVG Jax Config" + engine: -> window["MathJax"].OutputJax["SVG"] + }, + { + name: "TeX Jax Config" + engine: -> window["MathJax"].InputJax.TeX + }, + ] + + for hook in HOOKS + do (hook) -> + HUB.Register.StartupHook hook.name, -> + engine = hook.engine() + engine.Augment + Translate: (script, state) -> + console.log "translating" + if HUB.cancelTypeset or state.cancelled + throw Error(CANCELMESSAGE) + engine.Translate.call(engine, script, state) + + prevProcessError = HUB.processError + HUB.processError = (error, state, type) -> + if error.message != CANCELMESSAGE + return prevProcessError.call(HUB, error, state, type) + else + console.log "handling message" + MathJax.Message.Clear(0, 0) + state.jaxIds = [] + state.jax = {} + state.scripts = [] + state.i = state.j = 0 + state.cancelled = true + return null + + HUB.Cancel = -> + this.cancelTypeset = true + ### + if Markdown? - mathRenderer = new MathJaxDelayRenderer() - removeMath = (text) -> text - - replaceMath = (text) -> text - - updateMathJax = -> - console.log "updating" - #mathRenderer.render - # element: $("#wmd-preview") - MathJax.Hub.Queue(["Typeset", MathJax.Hub, "wmd-preview"]) - - converter = Markdown.getSanitizingConverter() editor = new Markdown.Editor(converter) - converter.hooks.chain "preConversion", removeMath - converter.hooks.chain "postConversion", replaceMath - editor.hooks.chain "onPreviewRefresh", updateMathJax + converter.hooks.chain "preConversion", MathJaxProcessor.removeMath + converter.hooks.chain "postConversion", MathJaxProcessor.replaceMath + delayRenderer = new MathJaxDelayRenderer() + editor.hooks.chain "onPreviewPush", (text, previewSet) -> + delayRenderer.render + text: text + previewSetter: previewSet editor.run() diff --git a/lms/static/coffee/src/mathjax_delay_renderer.coffee b/lms/static/coffee/src/mathjax_delay_renderer.coffee new file mode 100644 index 0000000000..03bb4b3cc9 --- /dev/null +++ b/lms/static/coffee/src/mathjax_delay_renderer.coffee @@ -0,0 +1,73 @@ +getTime = -> + new Date().getTime() + +class @MathJaxDelayRenderer + + maxDelay: 3000 + mathjaxRunning: false + elapsedTime: 0 + mathjaxDelay: 0 + mathjaxTimeout: undefined + bufferId: "mathjax_delay_buffer" + + constructor: (params) -> + params = params || {} + @maxDelay = params["maxDelay"] || @maxDelay + @bufferId = params["buffer"] || @bufferId + if not $("##{@bufferId}").length + $("
").attr("id", @bufferId).css("display", "none").appendTo($("body")) + + # render: (params) -> + # params: + # elem: jquery element to be rendered + # text: text to be rendered & put into the element; + # if blank, then just render the current text in the element + # preprocessor: pre-process the text before rendering using MathJax + # if text is blank, it will pre-process the html in the element + # previewSetter: if provided, will pass text back to it instead of + # directly setting the element + + render: (params) -> + + elem = params["element"] + previewSetter = params["previewSetter"] + text = params["text"] + if not text? + text = $(elem).html() + preprocessor = params["preprocessor"] + buffer = $("##{@bufferId}") + + if params["delay"] == false + if preprocessor? + text = preprocessor(text) + $(elem).html(text) + MathJax.Hub.Queue ["Typeset", MathJax.Hub, $(elem).attr("id")] + else + if @mathjaxTimeout + window.clearTimeout(@mathjaxTimeout) + @mathjaxTimeout = undefined + delay = Math.min @elapsedTime + @mathjaxDelay, @maxDelay + + renderer = => + if @mathjaxRunning + return + prevTime = getTime() + if preprocessor? + text = preprocessor(text) + buffer.html(text) + curTime = getTime() + @elapsedTime = curTime - prevTime + if MathJax + prevTime = getTime() + @mathjaxRunning = true + MathJax.Hub.Queue ["Typeset", MathJax.Hub, buffer.attr("id")], => + @mathjaxRunning = false + curTime = getTime() + @mathjaxDelay = curTime - prevTime + if previewSetter + previewSetter($(buffer).html()) + else + $(elem).html($(buffer).html()) + else + @mathjaxDelay = 0 + @mathjaxTimeout = window.setTimeout(renderer, delay) diff --git a/lms/templates/discussion/index.html b/lms/templates/discussion/index.html index 36a5cd1dfb..db1f2e5153 100644 --- a/lms/templates/discussion/index.html +++ b/lms/templates/discussion/index.html @@ -27,6 +27,7 @@ It can't be run through static.url because MathJax uses crazy url introspection to do lazy loading of MathJax extension libraries --> +