initial commit
authorbalpha <devnull@localhost>
Wed, 3 Aug 2011 16:29:58 +0000 (18:29 +0200)
committerbalpha <devnull@localhost>
Wed, 3 Aug 2011 16:29:58 +0000 (18:29 +0200)
LICENSE.txt [new file with mode: 0644]
Markdown.Converter.js [new file with mode: 0644]
Markdown.Editor.js [new file with mode: 0644]
Markdown.Sanitizer.js [new file with mode: 0644]
README.txt [new file with mode: 0644]
demo/browser/demo.css [new file with mode: 0644]
demo/browser/demo.html [new file with mode: 0644]
demo/node/demo.js [new file with mode: 0644]
resources/wmd-buttons.psd [new file with mode: 0644]
wmd-buttons.png [new file with mode: 0644]

diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644 (file)
index 0000000..a0c9467
--- /dev/null
@@ -0,0 +1,32 @@
+A javascript port of Markdown, as used on Stack Overflow\r
+and the rest of Stack Exchange network.\r
+\r
+Largely based on showdown.js by John Fraser (Attacklab).\r
+\r
+Original Markdown Copyright (c) 2004-2005 John Gruber\r
+  <http://daringfireball.net/projects/markdown/>\r
+\r
+\r
+Original Showdown code copyright (c) 2007 John Fraser\r
+\r
+Modifications and bugfixes (c) 2009 Dana Robinson\r
+Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc.\r
+\r
+Permission is hereby granted, free of charge, to any person obtaining a copy\r
+of this software and associated documentation files (the "Software"), to deal\r
+in the Software without restriction, including without limitation the rights\r
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+copies of the Software, and to permit persons to whom the Software is\r
+furnished to do so, subject to the following conditions:\r
+\r
+The above copyright notice and this permission notice shall be included in\r
+all copies or substantial portions of the Software.\r
+\r
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
+THE SOFTWARE.\r
+\r
diff --git a/Markdown.Converter.js b/Markdown.Converter.js
new file mode 100644 (file)
index 0000000..4460c32
--- /dev/null
@@ -0,0 +1,1318 @@
+var Markdown;\r
+\r
+if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module\r
+    Markdown = exports;\r
+else\r
+    Markdown = {};\r
+    \r
+// The following text is included for historical reasons, but should\r
+// be taken with a pinch of salt; it's not all true anymore.\r
+\r
+//\r
+// Wherever possible, Showdown is a straight, line-by-line port\r
+// of the Perl version of Markdown.\r
+//\r
+// This is not a normal parser design; it's basically just a\r
+// series of string substitutions.  It's hard to read and\r
+// maintain this way,  but keeping Showdown close to the original\r
+// design makes it easier to port new features.\r
+//\r
+// More importantly, Showdown behaves like markdown.pl in most\r
+// edge cases.  So web applications can do client-side preview\r
+// in Javascript, and then build identical HTML on the server.\r
+//\r
+// This port needs the new RegExp functionality of ECMA 262,\r
+// 3rd Edition (i.e. Javascript 1.5).  Most modern web browsers\r
+// should do fine.  Even with the new regular expression features,\r
+// We do a lot of work to emulate Perl's regex functionality.\r
+// The tricky changes in this file mostly have the "attacklab:"\r
+// label.  Major or self-explanatory changes don't.\r
+//\r
+// Smart diff tools like Araxis Merge will be able to match up\r
+// this file with markdown.pl in a useful way.  A little tweaking\r
+// helps: in a copy of markdown.pl, replace "#" with "//" and\r
+// replace "$text" with "text".  Be sure to ignore whitespace\r
+// and line endings.\r
+//\r
+\r
+\r
+//\r
+// Usage:\r
+//\r
+//   var text = "Markdown *rocks*.";\r
+//\r
+//   var converter = new Markdown.Converter();\r
+//   var html = converter.makeHtml(text);\r
+//\r
+//   alert(html);\r
+//\r
+// Note: move the sample code to the bottom of this\r
+// file before uncommenting it.\r
+//\r
+\r
+(function () {\r
+\r
+    function identity(x) { return x; }\r
+    function returnFalse(x) { return false; }\r
+\r
+    function HookCollection() { }\r
+\r
+    HookCollection.prototype = {\r
+\r
+        chain: function (hookname, func) {\r
+            var original = this[hookname];\r
+            if (!original)\r
+                throw new Error("unknown hook " + hookname);\r
+\r
+            if (original === identity)\r
+                this[hookname] = func;\r
+            else\r
+                this[hookname] = function (x) { return func(original(x)); }\r
+        },\r
+        set: function (hookname, func) {\r
+            if (!this[hookname])\r
+                throw new Error("unknown hook " + hookname);\r
+            this[hookname] = func;\r
+        },\r
+        addNoop: function (hookname) {\r
+            this[hookname] = identity;\r
+        },\r
+        addFalse: function (hookname) {\r
+            this[hookname] = returnFalse;\r
+        }\r
+    };\r
+\r
+    Markdown.HookCollection = HookCollection;\r
+\r
+    // g_urls and g_titles allow arbitrary user-entered strings as keys. This\r
+    // caused an exception (and hence stopped the rendering) when the user entered\r
+    // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this\r
+    // (since no builtin property starts with "s_"). See\r
+    // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug\r
+    // (granted, switching from Array() to Object() alone would have left only __proto__\r
+    // to be a problem)\r
+    function SaveHash() { }\r
+    SaveHash.prototype = {\r
+        set: function (key, value) {\r
+            this["s_" + key] = value;\r
+        },\r
+        get: function (key) {\r
+            return this["s_" + key];\r
+        }\r
+    };\r
+\r
+    Markdown.Converter = function () {\r
+        var pluginHooks = this.hooks = new HookCollection();\r
+        pluginHooks.addNoop("plainLinkText");  // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link\r
+        pluginHooks.addNoop("preConversion");  // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked\r
+        pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml\r
+\r
+        //\r
+        // Private state of the converter instance:\r
+        //\r
+\r
+        // Global hashes, used by various utility routines\r
+        var g_urls;\r
+        var g_titles;\r
+        var g_html_blocks;\r
+\r
+        // Used to track when we're inside an ordered or unordered list\r
+        // (see _ProcessListItems() for details):\r
+        var g_list_level;\r
+\r
+        this.makeHtml = function (text) {\r
+\r
+            //\r
+            // Main function. The order in which other subs are called here is\r
+            // essential. Link and image substitutions need to happen before\r
+            // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the <a>\r
+            // and <img> tags get encoded.\r
+            //\r
+\r
+            // This will only happen if makeHtml on the same converter instance is called from a plugin hook.\r
+            // Don't do that.\r
+            if (g_urls)\r
+                throw new Error("Recursive call to converter.makeHtml");\r
+        \r
+            // Create the private state objects.\r
+            g_urls = new SaveHash();\r
+            g_titles = new SaveHash();\r
+            g_html_blocks = [];\r
+            g_list_level = 0;\r
+\r
+            text = pluginHooks.preConversion(text);\r
+\r
+            // attacklab: Replace ~ with ~T\r
+            // This lets us use tilde as an escape char to avoid md5 hashes\r
+            // The choice of character is arbitray; anything that isn't\r
+            // magic in Markdown will work.\r
+            text = text.replace(/~/g, "~T");\r
+\r
+            // attacklab: Replace $ with ~D\r
+            // RegExp interprets $ as a special character\r
+            // when it's in a replacement string\r
+            text = text.replace(/\$/g, "~D");\r
+\r
+            // Standardize line endings\r
+            text = text.replace(/\r\n/g, "\n"); // DOS to Unix\r
+            text = text.replace(/\r/g, "\n"); // Mac to Unix\r
+\r
+            // Make sure text begins and ends with a couple of newlines:\r
+            text = "\n\n" + text + "\n\n";\r
+\r
+            // Convert all tabs to spaces.\r
+            text = _Detab(text);\r
+\r
+            // Strip any lines consisting only of spaces and tabs.\r
+            // This makes subsequent regexen easier to write, because we can\r
+            // match consecutive blank lines with /\n+/ instead of something\r
+            // contorted like /[ \t]*\n+/ .\r
+            text = text.replace(/^[ \t]+$/mg, "");\r
+\r
+            // Turn block-level HTML blocks into hash entries\r
+            text = _HashHTMLBlocks(text);\r
+\r
+            // Strip link definitions, store in hashes.\r
+            text = _StripLinkDefinitions(text);\r
+\r
+            text = _RunBlockGamut(text);\r
+\r
+            text = _UnescapeSpecialChars(text);\r
+\r
+            // attacklab: Restore dollar signs\r
+            text = text.replace(/~D/g, "$$");\r
+\r
+            // attacklab: Restore tildes\r
+            text = text.replace(/~T/g, "~");\r
+\r
+            text = pluginHooks.postConversion(text);\r
+\r
+            g_html_blocks = g_titles = g_urls = null;\r
+\r
+            return text;\r
+        };\r
+\r
+        function _StripLinkDefinitions(text) {\r
+            //\r
+            // Strips link definitions from text, stores the URLs and titles in\r
+            // hash references.\r
+            //\r
+\r
+            // Link defs are in the form: ^[id]: url "optional title"\r
+\r
+            /*\r
+            text = text.replace(/\r
+                ^[ ]{0,3}\[(.+)\]:  // id = $1  attacklab: g_tab_width - 1\r
+                [ \t]*\r
+                \n?                 // maybe *one* newline\r
+                [ \t]*\r
+                <?(\S+?)>?          // url = $2\r
+                (?=\s|$)            // lookahead for whitespace instead of the lookbehind removed below\r
+                [ \t]*\r
+                \n?                 // maybe one newline\r
+                [ \t]*\r
+                (                   // (potential) title = $3\r
+                    (\n*)           // any lines skipped = $4 attacklab: lookbehind removed\r
+                    [ \t]+\r
+                    ["(]\r
+                    (.+?)           // title = $5\r
+                    [")]\r
+                    [ \t]*\r
+                )?                  // title is optional\r
+                (?:\n+|$)\r
+            /gm, function(){...});\r
+            */\r
+\r
+            text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm,\r
+                function (wholeMatch, m1, m2, m3, m4, m5) {\r
+                    m1 = m1.toLowerCase();\r
+                    g_urls.set(m1, _EncodeAmpsAndAngles(m2));  // Link IDs are case-insensitive\r
+                    if (m4) {\r
+                        // Oops, found blank lines, so it's not a title.\r
+                        // Put back the parenthetical statement we stole.\r
+                        return m3;\r
+                    } else if (m5) {\r
+                        g_titles.set(m1, m5.replace(/"/g, "&quot;"));\r
+                    }\r
+\r
+                    // Completely remove the definition from the text\r
+                    return "";\r
+                }\r
+            );\r
+\r
+            return text;\r
+        }\r
+\r
+        function _HashHTMLBlocks(text) {\r
+\r
+            // Hashify HTML blocks:\r
+            // We only want to do this for block-level HTML tags, such as headers,\r
+            // lists, and tables. That's because we still want to wrap <p>s around\r
+            // "paragraphs" that are wrapped in non-block-level tags, such as anchors,\r
+            // phrase emphasis, and spans. The list of tags we're looking for is\r
+            // hard-coded:\r
+            var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del"\r
+            var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math"\r
+\r
+            // First, look for nested blocks, e.g.:\r
+            //   <div>\r
+            //     <div>\r
+            //     tags for inner block must be indented.\r
+            //     </div>\r
+            //   </div>\r
+            //\r
+            // The outermost tags must start at the left margin for this to match, and\r
+            // the inner nested divs must be indented.\r
+            // We need to do this before the next, more liberal match, because the next\r
+            // match will start at the first `<div>` and stop at the first `</div>`.\r
+\r
+            // attacklab: This regex can be expensive when it fails.\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                       // save in $1\r
+                    ^                   // start of line  (with /m)\r
+                    <($block_tags_a)    // start tag = $2\r
+                    \b                  // word break\r
+                                        // attacklab: hack around khtml/pcre bug...\r
+                    [^\r]*?\n           // any number of lines, minimally matching\r
+                    </\2>               // the matching end tag\r
+                    [ \t]*              // trailing spaces/tabs\r
+                    (?=\n+)             // followed by a newline\r
+                )                       // attacklab: there are sentinel newlines at end of document\r
+            /gm,function(){...}};\r
+            */\r
+            text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement);\r
+\r
+            //\r
+            // Now match more liberally, simply from `\n<tag>` to `</tag>\n`\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                       // save in $1\r
+                    ^                   // start of line  (with /m)\r
+                    <($block_tags_b)    // start tag = $2\r
+                    \b                  // word break\r
+                                        // attacklab: hack around khtml/pcre bug...\r
+                    [^\r]*?             // any number of lines, minimally matching\r
+                    .*</\2>             // the matching end tag\r
+                    [ \t]*              // trailing spaces/tabs\r
+                    (?=\n+)             // followed by a newline\r
+                )                       // attacklab: there are sentinel newlines at end of document\r
+            /gm,function(){...}};\r
+            */\r
+            text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement);\r
+\r
+            // Special case just for <hr />. It was easier to make a special case than\r
+            // to make the other regex more complicated.  \r
+\r
+            /*\r
+            text = text.replace(/\r
+                \n                  // Starting after a blank line\r
+                [ ]{0,3}\r
+                (                   // save in $1\r
+                    (<(hr)          // start tag = $2\r
+                        \b          // word break\r
+                        ([^<>])*?\r
+                    \/?>)           // the matching end tag\r
+                    [ \t]*\r
+                    (?=\n{2,})      // followed by a blank line\r
+                )\r
+            /g,hashElement);\r
+            */\r
+            text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement);\r
+\r
+            // Special case for standalone HTML comments:\r
+\r
+            /*\r
+            text = text.replace(/\r
+                \n\n                                            // Starting after a blank line\r
+                [ ]{0,3}                                        // attacklab: g_tab_width - 1\r
+                (                                               // save in $1\r
+                    <!\r
+                    (--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)   // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256\r
+                    >\r
+                    [ \t]*\r
+                    (?=\n{2,})                                  // followed by a blank line\r
+                )\r
+            /g,hashElement);\r
+            */\r
+            text = text.replace(/\n\n[ ]{0,3}(<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement);\r
+\r
+            // PHP and ASP-style processor instructions (<?...?> and <%...%>)\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (?:\r
+                    \n\n            // Starting after a blank line\r
+                )\r
+                (                   // save in $1\r
+                    [ ]{0,3}        // attacklab: g_tab_width - 1\r
+                    (?:\r
+                        <([?%])     // $2\r
+                        [^\r]*?\r
+                        \2>\r
+                    )\r
+                    [ \t]*\r
+                    (?=\n{2,})      // followed by a blank line\r
+                )\r
+            /g,hashElement);\r
+            */\r
+            text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement);\r
+\r
+            return text;\r
+        }\r
+\r
+        function hashElement(wholeMatch, m1) {\r
+            var blockText = m1;\r
+\r
+            // Undo double lines\r
+            blockText = blockText.replace(/^\n+/, "");\r
+\r
+            // strip trailing blank lines\r
+            blockText = blockText.replace(/\n+$/g, "");\r
+\r
+            // Replace the element text with a marker ("~KxK" where x is its key)\r
+            blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n";\r
+\r
+            return blockText;\r
+        }\r
+\r
+        function _RunBlockGamut(text, doNotUnhash) {\r
+            //\r
+            // These are all the transformations that form block-level\r
+            // tags like paragraphs, headers, and list items.\r
+            //\r
+            text = _DoHeaders(text);\r
+\r
+            // Do Horizontal Rules:\r
+            var replacement = "<hr />\n";\r
+            text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement);\r
+            text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement);\r
+            text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement);\r
+\r
+            text = _DoLists(text);\r
+            text = _DoCodeBlocks(text);\r
+            text = _DoBlockQuotes(text);\r
+\r
+            // We already ran _HashHTMLBlocks() before, in Markdown(), but that\r
+            // was to escape raw HTML in the original Markdown source. This time,\r
+            // we're escaping the markup we've just created, so that we don't wrap\r
+            // <p> tags around block-level tags.\r
+            text = _HashHTMLBlocks(text);\r
+            text = _FormParagraphs(text, doNotUnhash);\r
+\r
+            return text;\r
+        }\r
+\r
+        function _RunSpanGamut(text) {\r
+            //\r
+            // These are all the transformations that occur *within* block-level\r
+            // tags like paragraphs, headers, and list items.\r
+            //\r
+\r
+            text = _DoCodeSpans(text);\r
+            text = _EscapeSpecialCharsWithinTagAttributes(text);\r
+            text = _EncodeBackslashEscapes(text);\r
+\r
+            // Process anchor and image tags. Images must come first,\r
+            // because ![foo][f] looks like an anchor.\r
+            text = _DoImages(text);\r
+            text = _DoAnchors(text);\r
+\r
+            // Make links out of things like `<http://example.com/>`\r
+            // Must come after _DoAnchors(), because you can use < and >\r
+            // delimiters in inline links like [this](<url>).\r
+            text = _DoAutoLinks(text);\r
+            text = _EncodeAmpsAndAngles(text);\r
+            text = _DoItalicsAndBold(text);\r
+\r
+            // Do hard breaks:\r
+            text = text.replace(/  +\n/g, " <br>\n");\r
+\r
+            return text;\r
+        }\r
+\r
+        function _EscapeSpecialCharsWithinTagAttributes(text) {\r
+            //\r
+            // Within tags -- meaning between < and > -- encode [\ ` * _] so they\r
+            // don't conflict with their use in Markdown for code, italics and strong.\r
+            //\r
+\r
+            // Build a regex to find HTML tags and comments.  See Friedl's \r
+            // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.\r
+\r
+            // SE: changed the comment part of the regex\r
+\r
+            var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|<!(--(?:|(?:[^>-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi;\r
+\r
+            text = text.replace(regex, function (wholeMatch) {\r
+                var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");\r
+                tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987\r
+                return tag;\r
+            });\r
+\r
+            return text;\r
+        }\r
+\r
+        function _DoAnchors(text) {\r
+            //\r
+            // Turn Markdown link shortcuts into XHTML <a> tags.\r
+            //\r
+            //\r
+            // First, handle reference-style links: [link text] [id]\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                           // wrap whole match in $1\r
+                    \[\r
+                    (\r
+                        (?:\r
+                            \[[^\]]*\]      // allow brackets nested one level\r
+                            |\r
+                            [^\[]           // or anything else\r
+                        )*\r
+                    )\r
+                    \]\r
+\r
+                    [ ]?                    // one optional space\r
+                    (?:\n[ ]*)?             // one optional newline followed by spaces\r
+\r
+                    \[\r
+                    (.*?)                   // id = $3\r
+                    \]\r
+                )\r
+                ()()()()                    // pad remaining backreferences\r
+            /g, writeAnchorTag);\r
+            */\r
+            text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);\r
+\r
+            //\r
+            // Next, inline-style links: [link text](url "optional title")\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                           // wrap whole match in $1\r
+                    \[\r
+                    (\r
+                        (?:\r
+                            \[[^\]]*\]      // allow brackets nested one level\r
+                            |\r
+                            [^\[\]]         // or anything else\r
+                        )*\r
+                    )\r
+                    \]\r
+                    \(                      // literal paren\r
+                    [ \t]*\r
+                    ()                      // no id, so leave $3 empty\r
+                    <?(                     // href = $4\r
+                        (?:\r
+                            \([^)]*\)       // allow one level of (correctly nested) parens (think MSDN)\r
+                            |\r
+                            [^()]\r
+                        )*?\r
+                    )>?                \r
+                    [ \t]*\r
+                    (                       // $5\r
+                        (['"])              // quote char = $6\r
+                        (.*?)               // Title = $7\r
+                        \6                  // matching quote\r
+                        [ \t]*              // ignore any spaces/tabs between closing quote and )\r
+                    )?                      // title is optional\r
+                    \)\r
+                )\r
+            /g, writeAnchorTag);\r
+            */\r
+\r
+            text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?((?:\([^)]*\)|[^()])*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag);\r
+\r
+            //\r
+            // Last, handle reference-style shortcuts: [link text]\r
+            // These must come last in case you've also got [link test][1]\r
+            // or [link test](/foo)\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                   // wrap whole match in $1\r
+                    \[\r
+                    ([^\[\]]+)      // link text = $2; can't contain '[' or ']'\r
+                    \]\r
+                )\r
+                ()()()()()          // pad rest of backreferences\r
+            /g, writeAnchorTag);\r
+            */\r
+            text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);\r
+\r
+            return text;\r
+        }\r
+\r
+        function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {\r
+            if (m7 == undefined) m7 = "";\r
+            var whole_match = m1;\r
+            var link_text = m2;\r
+            var link_id = m3.toLowerCase();\r
+            var url = m4;\r
+            var title = m7;\r
+\r
+            if (url == "") {\r
+                if (link_id == "") {\r
+                    // lower-case and turn embedded newlines into spaces\r
+                    link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");\r
+                }\r
+                url = "#" + link_id;\r
+\r
+                if (g_urls.get(link_id) != undefined) {\r
+                    url = g_urls.get(link_id);\r
+                    if (g_titles.get(link_id) != undefined) {\r
+                        title = g_titles.get(link_id);\r
+                    }\r
+                }\r
+                else {\r
+                    if (whole_match.search(/\(\s*\)$/m) > -1) {\r
+                        // Special case for explicit empty url\r
+                        url = "";\r
+                    } else {\r
+                        return whole_match;\r
+                    }\r
+                }\r
+            }\r
+            url = encodeProblemUrlChars(url);\r
+            url = escapeCharacters(url, "*_");\r
+            var result = "<a href=\"" + url + "\"";\r
+\r
+            if (title != "") {\r
+                title = title.replace(/"/g, "&quot;");\r
+                title = escapeCharacters(title, "*_");\r
+                result += " title=\"" + title + "\"";\r
+            }\r
+\r
+            result += ">" + link_text + "</a>";\r
+\r
+            return result;\r
+        }\r
+\r
+        function _DoImages(text) {\r
+            //\r
+            // Turn Markdown image shortcuts into <img> tags.\r
+            //\r
+\r
+            //\r
+            // First, handle reference-style labeled images: ![alt text][id]\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                   // wrap whole match in $1\r
+                    !\[\r
+                    (.*?)           // alt text = $2\r
+                    \]\r
+\r
+                    [ ]?            // one optional space\r
+                    (?:\n[ ]*)?     // one optional newline followed by spaces\r
+\r
+                    \[\r
+                    (.*?)           // id = $3\r
+                    \]\r
+                )\r
+                ()()()()            // pad rest of backreferences\r
+            /g, writeImageTag);\r
+            */\r
+            text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag);\r
+\r
+            //\r
+            // Next, handle inline images:  ![alt text](url "optional title")\r
+            // Don't forget: encode * and _\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                   // wrap whole match in $1\r
+                    !\[\r
+                    (.*?)           // alt text = $2\r
+                    \]\r
+                    \s?             // One optional whitespace character\r
+                    \(              // literal paren\r
+                    [ \t]*\r
+                    ()              // no id, so leave $3 empty\r
+                    <?(\S+?)>?      // src url = $4\r
+                    [ \t]*\r
+                    (               // $5\r
+                        (['"])      // quote char = $6\r
+                        (.*?)       // title = $7\r
+                        \6          // matching quote\r
+                        [ \t]*\r
+                    )?              // title is optional\r
+                    \)\r
+                )\r
+            /g, writeImageTag);\r
+            */\r
+            text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag);\r
+\r
+            return text;\r
+        }\r
+\r
+        function writeImageTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {\r
+            var whole_match = m1;\r
+            var alt_text = m2;\r
+            var link_id = m3.toLowerCase();\r
+            var url = m4;\r
+            var title = m7;\r
+\r
+            if (!title) title = "";\r
+\r
+            if (url == "") {\r
+                if (link_id == "") {\r
+                    // lower-case and turn embedded newlines into spaces\r
+                    link_id = alt_text.toLowerCase().replace(/ ?\n/g, " ");\r
+                }\r
+                url = "#" + link_id;\r
+\r
+                if (g_urls.get(link_id) != undefined) {\r
+                    url = g_urls.get(link_id);\r
+                    if (g_titles.get(link_id) != undefined) {\r
+                        title = g_titles.get(link_id);\r
+                    }\r
+                }\r
+                else {\r
+                    return whole_match;\r
+                }\r
+            }\r
+\r
+            alt_text = alt_text.replace(/"/g, "&quot;");\r
+            url = escapeCharacters(url, "*_");\r
+            var result = "<img src=\"" + url + "\" alt=\"" + alt_text + "\"";\r
+\r
+            // attacklab: Markdown.pl adds empty title attributes to images.\r
+            // Replicate this bug.\r
+\r
+            //if (title != "") {\r
+            title = title.replace(/"/g, "&quot;");\r
+            title = escapeCharacters(title, "*_");\r
+            result += " title=\"" + title + "\"";\r
+            //}\r
+\r
+            result += " />";\r
+\r
+            return result;\r
+        }\r
+\r
+        function _DoHeaders(text) {\r
+\r
+            // Setext-style headers:\r
+            //  Header 1\r
+            //  ========\r
+            //  \r
+            //  Header 2\r
+            //  --------\r
+            //\r
+            text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,\r
+                function (wholeMatch, m1) { return "<h1>" + _RunSpanGamut(m1) + "</h1>\n\n"; }\r
+            );\r
+\r
+            text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,\r
+                function (matchFound, m1) { return "<h2>" + _RunSpanGamut(m1) + "</h2>\n\n"; }\r
+            );\r
+\r
+            // atx-style headers:\r
+            //  # Header 1\r
+            //  ## Header 2\r
+            //  ## Header 2 with closing hashes ##\r
+            //  ...\r
+            //  ###### Header 6\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                ^(\#{1,6})      // $1 = string of #'s\r
+                [ \t]*\r
+                (.+?)           // $2 = Header text\r
+                [ \t]*\r
+                \#*             // optional closing #'s (not counted)\r
+                \n+\r
+            /gm, function() {...});\r
+            */\r
+\r
+            text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm,\r
+                function (wholeMatch, m1, m2) {\r
+                    var h_level = m1.length;\r
+                    return "<h" + h_level + ">" + _RunSpanGamut(m2) + "</h" + h_level + ">\n\n";\r
+                }\r
+            );\r
+\r
+            return text;\r
+        }\r
+\r
+        function _DoLists(text) {\r
+            //\r
+            // Form HTML ordered (numbered) and unordered (bulleted) lists.\r
+            //\r
+\r
+            // attacklab: add sentinel to hack around khtml/safari bug:\r
+            // http://bugs.webkit.org/show_bug.cgi?id=11231\r
+            text += "~0";\r
+\r
+            // Re-usable pattern to match any entirel ul or ol list:\r
+\r
+            /*\r
+            var whole_list = /\r
+                (                                   // $1 = whole list\r
+                    (                               // $2\r
+                        [ ]{0,3}                    // attacklab: g_tab_width - 1\r
+                        ([*+-]|\d+[.])              // $3 = first list item marker\r
+                        [ \t]+\r
+                    )\r
+                    [^\r]+?\r
+                    (                               // $4\r
+                        ~0                          // sentinel for workaround; should be $\r
+                        |\r
+                        \n{2,}\r
+                        (?=\S)\r
+                        (?!                         // Negative lookahead for another list item marker\r
+                            [ \t]*\r
+                            (?:[*+-]|\d+[.])[ \t]+\r
+                        )\r
+                    )\r
+                )\r
+            /g\r
+            */\r
+            var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm;\r
+\r
+            if (g_list_level) {\r
+                text = text.replace(whole_list, function (wholeMatch, m1, m2) {\r
+                    var list = m1;\r
+                    var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol";\r
+\r
+                    var result = _ProcessListItems(list, list_type);\r
+\r
+                    // Trim any trailing whitespace, to put the closing `</$list_type>`\r
+                    // up on the preceding line, to get it past the current stupid\r
+                    // HTML block parser. This is a hack to work around the terrible\r
+                    // hack that is the HTML block parser.\r
+                    result = result.replace(/\s+$/, "");\r
+                    result = "<" + list_type + ">" + result + "</" + list_type + ">\n";\r
+                    return result;\r
+                });\r
+            } else {\r
+                whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g;\r
+                text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) {\r
+                    var runup = m1;\r
+                    var list = m2;\r
+\r
+                    var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol";\r
+                    var result = _ProcessListItems(list, list_type);\r
+                    result = runup + "<" + list_type + ">\n" + result + "</" + list_type + ">\n";\r
+                    return result;\r
+                });\r
+            }\r
+\r
+            // attacklab: strip sentinel\r
+            text = text.replace(/~0/, "");\r
+\r
+            return text;\r
+        }\r
+\r
+        var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" };\r
+\r
+        function _ProcessListItems(list_str, list_type) {\r
+            //\r
+            //  Process the contents of a single ordered or unordered list, splitting it\r
+            //  into individual list items.\r
+            //\r
+            //  list_type is either "ul" or "ol".\r
+\r
+            // The $g_list_level global keeps track of when we're inside a list.\r
+            // Each time we enter a list, we increment it; when we leave a list,\r
+            // we decrement. If it's zero, we're not in a list anymore.\r
+            //\r
+            // We do this because when we're not inside a list, we want to treat\r
+            // something like this:\r
+            //\r
+            //    I recommend upgrading to version\r
+            //    8. Oops, now this line is treated\r
+            //    as a sub-list.\r
+            //\r
+            // As a single paragraph, despite the fact that the second line starts\r
+            // with a digit-period-space sequence.\r
+            //\r
+            // Whereas when we're inside a list (or sub-list), that line will be\r
+            // treated as the start of a sub-list. What a kludge, huh? This is\r
+            // an aspect of Markdown's syntax that's hard to parse perfectly\r
+            // without resorting to mind-reading. Perhaps the solution is to\r
+            // change the syntax rules such that sub-lists must start with a\r
+            // starting cardinal number; e.g. "1." or "a.".\r
+\r
+            g_list_level++;\r
+\r
+            // trim trailing blank lines:\r
+            list_str = list_str.replace(/\n{2,}$/, "\n");\r
+\r
+            // attacklab: add sentinel to emulate \z\r
+            list_str += "~0";\r
+\r
+            // In the original attacklab showdown, list_type was not given to this function, and anything\r
+            // that matched /[*+-]|\d+[.]/ would just create the next <li>, causing this mismatch:\r
+            //\r
+            //  Markdown          rendered by WMD        rendered by MarkdownSharp\r
+            //  ------------------------------------------------------------------\r
+            //  1. first          1. first               1. first\r
+            //  2. second         2. second              2. second\r
+            //  - third           3. third                   * third\r
+            //\r
+            // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx,\r
+            // with {MARKER} being one of \d+[.] or [*+-], depending on list_type:\r
+        \r
+            /*\r
+            list_str = list_str.replace(/\r
+                (^[ \t]*)                       // leading whitespace = $1\r
+                ({MARKER}) [ \t]+               // list marker = $2\r
+                ([^\r]+?                        // list item text   = $3\r
+                    (\n+)\r
+                )\r
+                (?=\r
+                    (~0 | \2 ({MARKER}) [ \t]+)\r
+                )\r
+            /gm, function(){...});\r
+            */\r
+\r
+            var marker = _listItemMarkers[list_type];\r
+            var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm");\r
+            var last_item_had_a_double_newline = false;\r
+            list_str = list_str.replace(re,\r
+                function (wholeMatch, m1, m2, m3) {\r
+                    var item = m3;\r
+                    var leading_space = m1;\r
+                    var ends_with_double_newline = /\n\n$/.test(item);\r
+                    var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1;\r
+\r
+                    if (contains_double_newline || last_item_had_a_double_newline) {\r
+                        item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true);\r
+                    }\r
+                    else {\r
+                        // Recursion for sub-lists:\r
+                        item = _DoLists(_Outdent(item));\r
+                        item = item.replace(/\n$/, ""); // chomp(item)\r
+                        item = _RunSpanGamut(item);\r
+                    }\r
+                    last_item_had_a_double_newline = ends_with_double_newline;\r
+                    return "<li>" + item + "</li>\n";\r
+                }\r
+            );\r
+\r
+            // attacklab: strip sentinel\r
+            list_str = list_str.replace(/~0/g, "");\r
+\r
+            g_list_level--;\r
+            return list_str;\r
+        }\r
+\r
+        function _DoCodeBlocks(text) {\r
+            //\r
+            //  Process Markdown `<pre><code>` blocks.\r
+            //  \r
+\r
+            /*\r
+            text = text.replace(/\r
+                (?:\n\n|^)\r
+                (                               // $1 = the code block -- one or more lines, starting with a space/tab\r
+                    (?:\r
+                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width\r
+                        .*\n+\r
+                    )+\r
+                )\r
+                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width\r
+            /g ,function(){...});\r
+            */\r
+\r
+            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug\r
+            text += "~0";\r
+\r
+            text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,\r
+                function (wholeMatch, m1, m2) {\r
+                    var codeblock = m1;\r
+                    var nextChar = m2;\r
+\r
+                    codeblock = _EncodeCode(_Outdent(codeblock));\r
+                    codeblock = _Detab(codeblock);\r
+                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines\r
+                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace\r
+\r
+                    codeblock = "<pre><code>" + codeblock + "\n</code></pre>";\r
+\r
+                    return "\n\n" + codeblock + "\n\n" + nextChar;\r
+                }\r
+            );\r
+\r
+            // attacklab: strip sentinel\r
+            text = text.replace(/~0/, "");\r
+\r
+            return text;\r
+        }\r
+\r
+        function hashBlock(text) {\r
+            text = text.replace(/(^\n+|\n+$)/g, "");\r
+            return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n";\r
+        }\r
+\r
+        function _DoCodeSpans(text) {\r
+            //\r
+            // * Backtick quotes are used for <code></code> spans.\r
+            // \r
+            // * You can use multiple backticks as the delimiters if you want to\r
+            //   include literal backticks in the code span. So, this input:\r
+            //     \r
+            //      Just type ``foo `bar` baz`` at the prompt.\r
+            //     \r
+            //   Will translate to:\r
+            //     \r
+            //      <p>Just type <code>foo `bar` baz</code> at the prompt.</p>\r
+            //     \r
+            //   There's no arbitrary limit to the number of backticks you\r
+            //   can use as delimters. If you need three consecutive backticks\r
+            //   in your code, use four for delimiters, etc.\r
+            //\r
+            // * You can use spaces to get literal backticks at the edges:\r
+            //     \r
+            //      ... type `` `bar` `` ...\r
+            //     \r
+            //   Turns to:\r
+            //     \r
+            //      ... type <code>`bar`</code> ...\r
+            //\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (^|[^\\])       // Character before opening ` can't be a backslash\r
+                (`+)            // $2 = Opening run of `\r
+                (               // $3 = The code block\r
+                    [^\r]*?\r
+                    [^`]        // attacklab: work around lack of lookbehind\r
+                )\r
+                \2              // Matching closer\r
+                (?!`)\r
+            /gm, function(){...});\r
+            */\r
+\r
+            text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,\r
+                function (wholeMatch, m1, m2, m3, m4) {\r
+                    var c = m3;\r
+                    c = c.replace(/^([ \t]*)/g, ""); // leading whitespace\r
+                    c = c.replace(/[ \t]*$/g, ""); // trailing whitespace\r
+                    c = _EncodeCode(c);\r
+                    return m1 + "<code>" + c + "</code>";\r
+                }\r
+            );\r
+\r
+            return text;\r
+        }\r
+\r
+        function _EncodeCode(text) {\r
+            //\r
+            // Encode/escape certain characters inside Markdown code runs.\r
+            // The point is that in code, these characters are literals,\r
+            // and lose their special Markdown meanings.\r
+            //\r
+            // Encode all ampersands; HTML entities are not\r
+            // entities within a Markdown code span.\r
+            text = text.replace(/&/g, "&amp;");\r
+\r
+            // Do the angle bracket song and dance:\r
+            text = text.replace(/</g, "&lt;");\r
+            text = text.replace(/>/g, "&gt;");\r
+\r
+            // Now, escape characters that are magic in Markdown:\r
+            text = escapeCharacters(text, "\*_{}[]\\", false);\r
+\r
+            // jj the line above breaks this:\r
+            //---\r
+\r
+            //* Item\r
+\r
+            //   1. Subitem\r
+\r
+            //            special char: *\r
+            //---\r
+\r
+            return text;\r
+        }\r
+\r
+        function _DoItalicsAndBold(text) {\r
+\r
+            // <strong> must go first:\r
+            text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,\r
+            "$1<strong>$3</strong>$4");\r
+\r
+            text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,\r
+            "$1<em>$3</em>$4");\r
+\r
+            return text;\r
+        }\r
+\r
+        function _DoBlockQuotes(text) {\r
+\r
+            /*\r
+            text = text.replace(/\r
+                (                           // Wrap whole match in $1\r
+                    (\r
+                        ^[ \t]*>[ \t]?      // '>' at the start of a line\r
+                        .+\n                // rest of the first line\r
+                        (.+\n)*             // subsequent consecutive lines\r
+                        \n*                 // blanks\r
+                    )+\r
+                )\r
+            /gm, function(){...});\r
+            */\r
+\r
+            text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,\r
+                function (wholeMatch, m1) {\r
+                    var bq = m1;\r
+\r
+                    // attacklab: hack around Konqueror 3.5.4 bug:\r
+                    // "----------bug".replace(/^-/g,"") == "bug"\r
+\r
+                    bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting\r
+\r
+                    // attacklab: clean up hack\r
+                    bq = bq.replace(/~0/g, "");\r
+\r
+                    bq = bq.replace(/^[ \t]+$/gm, "");     // trim whitespace-only lines\r
+                    bq = _RunBlockGamut(bq);             // recurse\r
+\r
+                    bq = bq.replace(/(^|\n)/g, "$1  ");\r
+                    // These leading spaces screw with <pre> content, so we need to fix that:\r
+                    bq = bq.replace(\r
+                            /(\s*<pre>[^\r]+?<\/pre>)/gm,\r
+                        function (wholeMatch, m1) {\r
+                            var pre = m1;\r
+                            // attacklab: hack around Konqueror 3.5.4 bug:\r
+                            pre = pre.replace(/^  /mg, "~0");\r
+                            pre = pre.replace(/~0/g, "");\r
+                            return pre;\r
+                        });\r
+\r
+                    return hashBlock("<blockquote>\n" + bq + "\n</blockquote>");\r
+                }\r
+            );\r
+            return text;\r
+        }\r
+\r
+        function _FormParagraphs(text, doNotUnhash) {\r
+            //\r
+            //  Params:\r
+            //    $text - string to process with html <p> tags\r
+            //\r
+\r
+            // Strip leading and trailing lines:\r
+            text = text.replace(/^\n+/g, "");\r
+            text = text.replace(/\n+$/g, "");\r
+\r
+            var grafs = text.split(/\n{2,}/g);\r
+            var grafsOut = [];\r
+\r
+            //\r
+            // Wrap <p> tags.\r
+            //\r
+            var end = grafs.length;\r
+            for (var i = 0; i < end; i++) {\r
+                var str = grafs[i];\r
+\r
+                // if this is an HTML marker, copy it\r
+                if (str.search(/~K(\d+)K/g) >= 0) {\r
+                    grafsOut.push(str);\r
+                }\r
+                else if (str.search(/\S/) >= 0) {\r
+                    str = _RunSpanGamut(str);\r
+                    str = str.replace(/^([ \t]*)/g, "<p>");\r
+                    str += "</p>"\r
+                    grafsOut.push(str);\r
+                }\r
+\r
+            }\r
+            //\r
+            // Unhashify HTML blocks\r
+            //\r
+            if (!doNotUnhash) {\r
+                end = grafsOut.length;\r
+                for (var i = 0; i < end; i++) {\r
+                    // if this is a marker for an html block...\r
+                    while (grafsOut[i].search(/~K(\d+)K/) >= 0) {\r
+                        var blockText = g_html_blocks[RegExp.$1];\r
+                        blockText = blockText.replace(/\$/g, "$$$$"); // Escape any dollar signs\r
+                        grafsOut[i] = grafsOut[i].replace(/~K\d+K/, blockText);\r
+                    }\r
+                }\r
+            }\r
+            return grafsOut.join("\n\n");\r
+        }\r
+\r
+        function _EncodeAmpsAndAngles(text) {\r
+            // Smart processing for ampersands and angle brackets that need to be encoded.\r
+\r
+            // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:\r
+            //   http://bumppo.net/projects/amputator/\r
+            text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&amp;");\r
+\r
+            // Encode naked <'s\r
+            text = text.replace(/<(?![a-z\/?\$!])/gi, "&lt;");\r
+\r
+            return text;\r
+        }\r
+\r
+        function _EncodeBackslashEscapes(text) {\r
+            //\r
+            //   Parameter:  String.\r
+            //   Returns:    The string, with after processing the following backslash\r
+            //               escape sequences.\r
+            //\r
+\r
+            // attacklab: The polite way to do this is with the new\r
+            // escapeCharacters() function:\r
+            //\r
+            //     text = escapeCharacters(text,"\\",true);\r
+            //     text = escapeCharacters(text,"`*_{}[]()>#+-.!",true);\r
+            //\r
+            // ...but we're sidestepping its use of the (slow) RegExp constructor\r
+            // as an optimization for Firefox.  This function gets called a LOT.\r
+\r
+            text = text.replace(/\\(\\)/g, escapeCharacters_callback);\r
+            text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback);\r
+            return text;\r
+        }\r
+\r
+        function _DoAutoLinks(text) {\r
+\r
+            // note that at this point, all other URL in the text are already hyperlinked as <a href=""></a>\r
+            // *except* for the <http://www.foo.com> case\r
+\r
+            // automatically add < and > around unadorned raw hyperlinks\r
+            // must be preceded by space/BOF and followed by non-word/EOF character    \r
+            text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4");\r
+\r
+            //  autolink anything like <http://example.com>\r
+            \r
+            var replacer = function (wholematch, m1) { return "<a href=\"" + m1 + "\">" + pluginHooks.plainLinkText(m1) + "</a>"; }\r
+            text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);\r
+\r
+            // Email addresses: <address@domain.foo>\r
+            /*\r
+            text = text.replace(/\r
+                <\r
+                (?:mailto:)?\r
+                (\r
+                    [-.\w]+\r
+                    \@\r
+                    [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+\r
+                )\r
+                >\r
+            /gi, _DoAutoLinks_callback());\r
+            */\r
+\r
+            /* disabling email autolinking, since we don't do that on the server, either\r
+            text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,\r
+                function(wholeMatch,m1) {\r
+                    return _EncodeEmailAddress( _UnescapeSpecialChars(m1) );\r
+                }\r
+            );\r
+            */\r
+            return text;\r
+        }\r
+\r
+        function _UnescapeSpecialChars(text) {\r
+            //\r
+            // Swap back in all the special characters we've hidden.\r
+            //\r
+            text = text.replace(/~E(\d+)E/g,\r
+                function (wholeMatch, m1) {\r
+                    var charCodeToReplace = parseInt(m1);\r
+                    return String.fromCharCode(charCodeToReplace);\r
+                }\r
+            );\r
+            return text;\r
+        }\r
+\r
+        function _Outdent(text) {\r
+            //\r
+            // Remove one level of line-leading tabs or spaces\r
+            //\r
+\r
+            // attacklab: hack around Konqueror 3.5.4 bug:\r
+            // "----------bug".replace(/^-/g,"") == "bug"\r
+\r
+            text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width\r
+\r
+            // attacklab: clean up hack\r
+            text = text.replace(/~0/g, "")\r
+\r
+            return text;\r
+        }\r
+\r
+        function _Detab(text) {\r
+            if (!/\t/.test(text))\r
+                return text;\r
+\r
+            var spaces = ["    ", "   ", "  ", " "],\r
+            skew = 0,\r
+            v;\r
+\r
+            return text.replace(/[\n\t]/g, function (match, offset) {\r
+                if (match === "\n") {\r
+                    skew = offset + 1;\r
+                    return match;\r
+                }\r
+                v = (offset - skew) % 4;\r
+                skew = offset + 1;\r
+                return spaces[v];\r
+            });\r
+        }\r
+\r
+        //\r
+        //  attacklab: Utility functions\r
+        //\r
+\r
+        var _problemUrlChars = /(?:["'*()[\]:]|~D)/g;\r
+\r
+        // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems \r
+        function encodeProblemUrlChars(url) {\r
+            if (!url)\r
+                return "";\r
+\r
+            var len = url.length;\r
+\r
+            return url.replace(_problemUrlChars, function (match, offset) {\r
+                if (match == "~D") // escape for dollar\r
+                    return "%24";\r
+                if (match == ":") {\r
+                    if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1)))\r
+                        return ":"\r
+                }\r
+                return "%" + match.charCodeAt(0).toString(16);\r
+            });\r
+        }\r
+\r
+\r
+        function escapeCharacters(text, charsToEscape, afterBackslash) {\r
+            // First we have to escape the escape characters so that\r
+            // we can build a character class out of them\r
+            var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])";\r
+\r
+            if (afterBackslash) {\r
+                regexString = "\\\\" + regexString;\r
+            }\r
+\r
+            var regex = new RegExp(regexString, "g");\r
+            text = text.replace(regex, escapeCharacters_callback);\r
+\r
+            return text;\r
+        }\r
+\r
+\r
+        function escapeCharacters_callback(wholeMatch, m1) {\r
+            var charCodeToEscape = m1.charCodeAt(0);\r
+            return "~E" + charCodeToEscape + "E";\r
+        }\r
+\r
+    }; // end of the Markdown.Converter constructor\r
+\r
+})();\r
diff --git a/Markdown.Editor.js b/Markdown.Editor.js
new file mode 100644 (file)
index 0000000..ba2e1b7
--- /dev/null
@@ -0,0 +1,2116 @@
+// needs Markdown.Converter.js at the moment\r
+\r
+(function () {\r
+\r
+    var util = {},\r
+        position = {},\r
+        ui = {},\r
+        doc = top.document,\r
+        re = top.RegExp,\r
+        nav = top.navigator,\r
+        SETTINGS = { lineLength: 72 },\r
+\r
+    // Used to work around some browser bugs where we can't use feature testing.\r
+        uaSniffed = {\r
+            isIE: /msie/.test(nav.userAgent.toLowerCase()),\r
+            isIE_5or6: /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase()),\r
+            isOpera: /opera/.test(nav.userAgent.toLowerCase())\r
+        };\r
+\r
+\r
+    // -------------------------------------------------------------------\r
+    //  YOUR CHANGES GO HERE\r
+    //\r
+    // I've tried to localize the things you are likely to change to \r
+    // this area.\r
+    // -------------------------------------------------------------------\r
+\r
+    // The text that appears on the upper part of the dialog box when\r
+    // entering links.\r
+    var linkDialogText = "<p><b>Insert Hyperlink</b></p><p>http://example.com/ \"optional title\"</p>";\r
+    var imageDialogText = "<p><b>Insert Image</b></p><p>http://example.com/images/diagram.jpg \"optional title\"<br><br>Need <a href='http://www.google.com/search?q=free+image+hosting' target='_blank'>free image hosting?</a></p>";\r
+\r
+    // The default text that appears in the dialog input box when entering\r
+    // links.\r
+    var imageDefaultText = "http://";\r
+    var linkDefaultText = "http://";\r
+\r
+    var defaultHelpHoverTitle = "Markdown Editing Help";\r
+\r
+    // -------------------------------------------------------------------\r
+    //  END OF YOUR CHANGES\r
+    // -------------------------------------------------------------------\r
+\r
+    // help, if given, should have a property "handler", the click handler for the help button,\r
+    // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help").\r
+    // If help isn't given, not help button is created.\r
+    //\r
+    // The constructed editor object has the methods:\r
+    // - getConverter() returns the markdown converter object that was passed to the constructor\r
+    // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op.\r
+    // - refreshPreview() forces the preview to be updated. This method is only available after run() was called.\r
+    Markdown.Editor = function (markdownConverter, idPostfix, help) {\r
+\r
+        idPostfix = idPostfix || "";\r
+\r
+        var hooks = this.hooks = new Markdown.HookCollection();\r
+        hooks.addNoop("onPreviewRefresh");       // called with no arguments after the preview has been refreshed\r
+        hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text\r
+        hooks.addFalse("insertImageDialog");     /* called with one parameter: a callback to be called with the URL of the image. If the application creates\r
+                                                  * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen\r
+                                                  * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.\r
+                                                  */\r
+\r
+        this.getConverter = function () { return markdownConverter; }\r
+\r
+        var that = this,\r
+            panels;\r
+\r
+        this.run = function () {\r
+            if (panels)\r
+                return; // already initialized\r
+\r
+            panels = new PanelCollection(idPostfix);\r
+            var commandManager = new CommandManager(hooks);\r
+            var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); });\r
+            var undoManager, uiManager;\r
+\r
+            if (!/\?noundo/.test(doc.location.href)) {\r
+                undoManager = new UndoManager(function () {\r
+                    previewManager.refresh();\r
+                    if (uiManager) // not available on the first call\r
+                        uiManager.setUndoRedoButtonStates();\r
+                }, panels);\r
+            }\r
+\r
+            uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help);\r
+            uiManager.setUndoRedoButtonStates();\r
+\r
+            var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };\r
+\r
+            forceRefresh();\r
+        };\r
+\r
+    }\r
+\r
+    // before: contains all the text in the input box BEFORE the selection.\r
+    // after: contains all the text in the input box AFTER the selection.\r
+    function Chunks() { }\r
+\r
+    // startRegex: a regular expression to find the start tag\r
+    // endRegex: a regular expresssion to find the end tag\r
+    Chunks.prototype.findTags = function (startRegex, endRegex) {\r
+\r
+        var chunkObj = this;\r
+        var regex;\r
+\r
+        if (startRegex) {\r
+\r
+            regex = util.extendRegExp(startRegex, "", "$");\r
+\r
+            this.before = this.before.replace(regex,\r
+                function (match) {\r
+                    chunkObj.startTag = chunkObj.startTag + match;\r
+                    return "";\r
+                });\r
+\r
+            regex = util.extendRegExp(startRegex, "^", "");\r
+\r
+            this.selection = this.selection.replace(regex,\r
+                function (match) {\r
+                    chunkObj.startTag = chunkObj.startTag + match;\r
+                    return "";\r
+                });\r
+        }\r
+\r
+        if (endRegex) {\r
+\r
+            regex = util.extendRegExp(endRegex, "", "$");\r
+\r
+            this.selection = this.selection.replace(regex,\r
+                function (match) {\r
+                    chunkObj.endTag = match + chunkObj.endTag;\r
+                    return "";\r
+                });\r
+\r
+            regex = util.extendRegExp(endRegex, "^", "");\r
+\r
+            this.after = this.after.replace(regex,\r
+                function (match) {\r
+                    chunkObj.endTag = match + chunkObj.endTag;\r
+                    return "";\r
+                });\r
+        }\r
+    };\r
+\r
+    // If remove is false, the whitespace is transferred\r
+    // to the before/after regions.\r
+    //\r
+    // If remove is true, the whitespace disappears.\r
+    Chunks.prototype.trimWhitespace = function (remove) {\r
+\r
+        this.selection = this.selection.replace(/^(\s*)/, "");\r
+\r
+        if (!remove) {\r
+            this.before += re.$1;\r
+        }\r
+\r
+        this.selection = this.selection.replace(/(\s*)$/, "");\r
+\r
+        if (!remove) {\r
+            this.after = re.$1 + this.after;\r
+        }\r
+    };\r
+\r
+\r
+    Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {\r
+\r
+        if (nLinesBefore === undefined) {\r
+            nLinesBefore = 1;\r
+        }\r
+\r
+        if (nLinesAfter === undefined) {\r
+            nLinesAfter = 1;\r
+        }\r
+\r
+        nLinesBefore++;\r
+        nLinesAfter++;\r
+\r
+        var regexText;\r
+        var replacementText;\r
+\r
+        // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985\r
+        if (navigator.userAgent.match(/Chrome/)) {\r
+            "X".match(/()./);\r
+        }\r
+\r
+        this.selection = this.selection.replace(/(^\n*)/, "");\r
+\r
+        this.startTag = this.startTag + re.$1;\r
+\r
+        this.selection = this.selection.replace(/(\n*$)/, "");\r
+        this.endTag = this.endTag + re.$1;\r
+        this.startTag = this.startTag.replace(/(^\n*)/, "");\r
+        this.before = this.before + re.$1;\r
+        this.endTag = this.endTag.replace(/(\n*$)/, "");\r
+        this.after = this.after + re.$1;\r
+\r
+        if (this.before) {\r
+\r
+            regexText = replacementText = "";\r
+\r
+            while (nLinesBefore--) {\r
+                regexText += "\\n?";\r
+                replacementText += "\n";\r
+            }\r
+\r
+            if (findExtraNewlines) {\r
+                regexText = "\\n*";\r
+            }\r
+            this.before = this.before.replace(new re(regexText + "$", ""), replacementText);\r
+        }\r
+\r
+        if (this.after) {\r
+\r
+            regexText = replacementText = "";\r
+\r
+            while (nLinesAfter--) {\r
+                regexText += "\\n?";\r
+                replacementText += "\n";\r
+            }\r
+            if (findExtraNewlines) {\r
+                regexText = "\\n*";\r
+            }\r
+\r
+            this.after = this.after.replace(new re(regexText, ""), replacementText);\r
+        }\r
+    };\r
+\r
+    // end of Chunks \r
+\r
+    // A collection of the important regions on the page.\r
+    // Cached so we don't have to keep traversing the DOM.\r
+    // Also holds ieRetardedClick and ieCachedRange, where necessary; working around\r
+    // this issue:\r
+    // Internet explorer has problems with CSS sprite buttons that use HTML\r
+    // lists.  When you click on the background image "button", IE will \r
+    // select the non-existent link text and discard the selection in the\r
+    // textarea.  The solution to this is to cache the textarea selection\r
+    // on the button's mousedown event and set a flag.  In the part of the\r
+    // code where we need to grab the selection, we check for the flag\r
+    // and, if it's set, use the cached area instead of querying the\r
+    // textarea.\r
+    //\r
+    // This ONLY affects Internet Explorer (tested on versions 6, 7\r
+    // and 8) and ONLY on button clicks.  Keyboard shortcuts work\r
+    // normally since the focus never leaves the textarea.\r
+    function PanelCollection(postfix) {\r
+        this.buttonBar = doc.getElementById("wmd-button-bar" + postfix);\r
+        this.preview = doc.getElementById("wmd-preview" + postfix);\r
+        this.input = doc.getElementById("wmd-input" + postfix);\r
+    };\r
+\r
+    // Returns true if the DOM element is visible, false if it's hidden.\r
+    // Checks if display is anything other than none.\r
+    util.isVisible = function (elem) {\r
+\r
+        if (window.getComputedStyle) {\r
+            // Most browsers\r
+            return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none";\r
+        }\r
+        else if (elem.currentStyle) {\r
+            // IE\r
+            return elem.currentStyle["display"] !== "none";\r
+        }\r
+    };\r
+\r
+\r
+    // Adds a listener callback to a DOM element which is fired on a specified\r
+    // event.\r
+    util.addEvent = function (elem, event, listener) {\r
+        if (elem.attachEvent) {\r
+            // IE only.  The "on" is mandatory.\r
+            elem.attachEvent("on" + event, listener);\r
+        }\r
+        else {\r
+            // Other browsers.\r
+            elem.addEventListener(event, listener, false);\r
+        }\r
+    };\r
+\r
+\r
+    // Removes a listener callback from a DOM element which is fired on a specified\r
+    // event.\r
+    util.removeEvent = function (elem, event, listener) {\r
+        if (elem.detachEvent) {\r
+            // IE only.  The "on" is mandatory.\r
+            elem.detachEvent("on" + event, listener);\r
+        }\r
+        else {\r
+            // Other browsers.\r
+            elem.removeEventListener(event, listener, false);\r
+        }\r
+    };\r
+\r
+    // Converts \r\n and \r to \n.\r
+    util.fixEolChars = function (text) {\r
+        text = text.replace(/\r\n/g, "\n");\r
+        text = text.replace(/\r/g, "\n");\r
+        return text;\r
+    };\r
+\r
+    // Extends a regular expression.  Returns a new RegExp\r
+    // using pre + regex + post as the expression.\r
+    // Used in a few functions where we have a base\r
+    // expression and we want to pre- or append some\r
+    // conditions to it (e.g. adding "$" to the end).\r
+    // The flags are unchanged.\r
+    //\r
+    // regex is a RegExp, pre and post are strings.\r
+    util.extendRegExp = function (regex, pre, post) {\r
+\r
+        if (pre === null || pre === undefined) {\r
+            pre = "";\r
+        }\r
+        if (post === null || post === undefined) {\r
+            post = "";\r
+        }\r
+\r
+        var pattern = regex.toString();\r
+        var flags;\r
+\r
+        // Replace the flags with empty space and store them.\r
+        pattern = pattern.replace(/\/([gim]*)$/, "");\r
+        flags = re.$1;\r
+\r
+        // Remove the slash delimiters on the regular expression.\r
+        pattern = pattern.replace(/(^\/|\/$)/g, "");\r
+        pattern = pre + pattern + post;\r
+\r
+        return new re(pattern, flags);\r
+    }\r
+\r
+    // UNFINISHED\r
+    // The assignment in the while loop makes jslint cranky.\r
+    // I'll change it to a better loop later.\r
+    position.getTop = function (elem, isInner) {\r
+        var result = elem.offsetTop;\r
+        if (!isInner) {\r
+            while (elem = elem.offsetParent) {\r
+                result += elem.offsetTop;\r
+            }\r
+        }\r
+        return result;\r
+    };\r
+\r
+    position.getHeight = function (elem) {\r
+        return elem.offsetHeight || elem.scrollHeight;\r
+    };\r
+\r
+    position.getWidth = function (elem) {\r
+        return elem.offsetWidth || elem.scrollWidth;\r
+    };\r
+\r
+    position.getPageSize = function () {\r
+\r
+        var scrollWidth, scrollHeight;\r
+        var innerWidth, innerHeight;\r
+\r
+        // It's not very clear which blocks work with which browsers.\r
+        if (self.innerHeight && self.scrollMaxY) {\r
+            scrollWidth = doc.body.scrollWidth;\r
+            scrollHeight = self.innerHeight + self.scrollMaxY;\r
+        }\r
+        else if (doc.body.scrollHeight > doc.body.offsetHeight) {\r
+            scrollWidth = doc.body.scrollWidth;\r
+            scrollHeight = doc.body.scrollHeight;\r
+        }\r
+        else {\r
+            scrollWidth = doc.body.offsetWidth;\r
+            scrollHeight = doc.body.offsetHeight;\r
+        }\r
+\r
+        if (self.innerHeight) {\r
+            // Non-IE browser\r
+            innerWidth = self.innerWidth;\r
+            innerHeight = self.innerHeight;\r
+        }\r
+        else if (doc.documentElement && doc.documentElement.clientHeight) {\r
+            // Some versions of IE (IE 6 w/ a DOCTYPE declaration)\r
+            innerWidth = doc.documentElement.clientWidth;\r
+            innerHeight = doc.documentElement.clientHeight;\r
+        }\r
+        else if (doc.body) {\r
+            // Other versions of IE\r
+            innerWidth = doc.body.clientWidth;\r
+            innerHeight = doc.body.clientHeight;\r
+        }\r
+\r
+        var maxWidth = Math.max(scrollWidth, innerWidth);\r
+        var maxHeight = Math.max(scrollHeight, innerHeight);\r
+        return [maxWidth, maxHeight, innerWidth, innerHeight];\r
+    };\r
+\r
+    // Handles pushing and popping TextareaStates for undo/redo commands.\r
+    // I should rename the stack variables to list.\r
+    function UndoManager(callback, panels) {\r
+\r
+        var undoObj = this;\r
+        var undoStack = []; // A stack of undo states\r
+        var stackPtr = 0; // The index of the current state\r
+        var mode = "none";\r
+        var lastState; // The last state\r
+        var timer; // The setTimeout handle for cancelling the timer\r
+        var inputStateObj;\r
+\r
+        // Set the mode for later logic steps.\r
+        var setMode = function (newMode, noSave) {\r
+            if (mode != newMode) {\r
+                mode = newMode;\r
+                if (!noSave) {\r
+                    saveState();\r
+                }\r
+            }\r
+\r
+            if (!uaSniffed.isIE || mode != "moving") {\r
+                timer = top.setTimeout(refreshState, 1);\r
+            }\r
+            else {\r
+                inputStateObj = null;\r
+            }\r
+        };\r
+\r
+        var refreshState = function (isInitialState) {\r
+            inputStateObj = new TextareaState(panels, isInitialState);\r
+            timer = undefined;\r
+        };\r
+\r
+        this.setCommandMode = function () {\r
+            mode = "command";\r
+            saveState();\r
+            timer = top.setTimeout(refreshState, 0);\r
+        };\r
+\r
+        this.canUndo = function () {\r
+            return stackPtr > 1;\r
+        };\r
+\r
+        this.canRedo = function () {\r
+            if (undoStack[stackPtr + 1]) {\r
+                return true;\r
+            }\r
+            return false;\r
+        };\r
+\r
+        // Removes the last state and restores it.\r
+        this.undo = function () {\r
+\r
+            if (undoObj.canUndo()) {\r
+                if (lastState) {\r
+                    // What about setting state -1 to null or checking for undefined?\r
+                    lastState.restore();\r
+                    lastState = null;\r
+                }\r
+                else {\r
+                    undoStack[stackPtr] = new TextareaState(panels);\r
+                    undoStack[--stackPtr].restore();\r
+\r
+                    if (callback) {\r
+                        callback();\r
+                    }\r
+                }\r
+            }\r
+\r
+            mode = "none";\r
+            panels.input.focus();\r
+            refreshState();\r
+        };\r
+\r
+        // Redo an action.\r
+        this.redo = function () {\r
+\r
+            if (undoObj.canRedo()) {\r
+\r
+                undoStack[++stackPtr].restore();\r
+\r
+                if (callback) {\r
+                    callback();\r
+                }\r
+            }\r
+\r
+            mode = "none";\r
+            panels.input.focus();\r
+            refreshState();\r
+        };\r
+\r
+        // Push the input area state to the stack.\r
+        var saveState = function () {\r
+            var currState = inputStateObj || new TextareaState(panels);\r
+\r
+            if (!currState) {\r
+                return false;\r
+            }\r
+            if (mode == "moving") {\r
+                if (!lastState) {\r
+                    lastState = currState;\r
+                }\r
+                return;\r
+            }\r
+            if (lastState) {\r
+                if (undoStack[stackPtr - 1].text != lastState.text) {\r
+                    undoStack[stackPtr++] = lastState;\r
+                }\r
+                lastState = null;\r
+            }\r
+            undoStack[stackPtr++] = currState;\r
+            undoStack[stackPtr + 1] = null;\r
+            if (callback) {\r
+                callback();\r
+            }\r
+        };\r
+\r
+        var handleCtrlYZ = function (event) {\r
+\r
+            var handled = false;\r
+\r
+            if (event.ctrlKey || event.metaKey) {\r
+\r
+                // IE and Opera do not support charCode.\r
+                var keyCode = event.charCode || event.keyCode;\r
+                var keyCodeChar = String.fromCharCode(keyCode);\r
+\r
+                switch (keyCodeChar) {\r
+\r
+                    case "y":\r
+                        undoObj.redo();\r
+                        handled = true;\r
+                        break;\r
+\r
+                    case "z":\r
+                        if (!event.shiftKey) {\r
+                            undoObj.undo();\r
+                        }\r
+                        else {\r
+                            undoObj.redo();\r
+                        }\r
+                        handled = true;\r
+                        break;\r
+                }\r
+            }\r
+\r
+            if (handled) {\r
+                if (event.preventDefault) {\r
+                    event.preventDefault();\r
+                }\r
+                if (top.event) {\r
+                    top.event.returnValue = false;\r
+                }\r
+                return;\r
+            }\r
+        };\r
+\r
+        // Set the mode depending on what is going on in the input area.\r
+        var handleModeChange = function (event) {\r
+\r
+            if (!event.ctrlKey && !event.metaKey) {\r
+\r
+                var keyCode = event.keyCode;\r
+\r
+                if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {\r
+                    // 33 - 40: page up/dn and arrow keys\r
+                    // 63232 - 63235: page up/dn and arrow keys on safari\r
+                    setMode("moving");\r
+                }\r
+                else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {\r
+                    // 8: backspace\r
+                    // 46: delete\r
+                    // 127: delete\r
+                    setMode("deleting");\r
+                }\r
+                else if (keyCode == 13) {\r
+                    // 13: Enter\r
+                    setMode("newlines");\r
+                }\r
+                else if (keyCode == 27) {\r
+                    // 27: escape\r
+                    setMode("escape");\r
+                }\r
+                else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {\r
+                    // 16-20 are shift, etc. \r
+                    // 91: left window key\r
+                    // I think this might be a little messed up since there are\r
+                    // a lot of nonprinting keys above 20.\r
+                    setMode("typing");\r
+                }\r
+            }\r
+        };\r
+\r
+        var setEventHandlers = function () {\r
+            util.addEvent(panels.input, "keypress", function (event) {\r
+                // keyCode 89: y\r
+                // keyCode 90: z\r
+                if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {\r
+                    event.preventDefault();\r
+                }\r
+            });\r
+\r
+            var handlePaste = function () {\r
+                if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) {\r
+                    if (timer == undefined) {\r
+                        mode = "paste";\r
+                        saveState();\r
+                        refreshState();\r
+                    }\r
+                }\r
+            };\r
+\r
+            util.addEvent(panels.input, "keydown", handleCtrlYZ);\r
+            util.addEvent(panels.input, "keydown", handleModeChange);\r
+            util.addEvent(panels.input, "mousedown", function () {\r
+                setMode("moving");\r
+            });\r
+\r
+            panels.input.onpaste = handlePaste;\r
+            panels.input.ondrop = handlePaste;\r
+        };\r
+\r
+        var init = function () {\r
+            setEventHandlers();\r
+            refreshState(true);\r
+            saveState();\r
+        };\r
+\r
+        init();\r
+    }\r
+\r
+    // end of UndoManager\r
+\r
+    // The input textarea state/contents.\r
+    // This is used to implement undo/redo by the undo manager.\r
+    function TextareaState(panels, isInitialState) {\r
+\r
+        // Aliases\r
+        var stateObj = this;\r
+        var inputArea = panels.input;\r
+        this.init = function () {\r
+            if (!util.isVisible(inputArea)) {\r
+                return;\r
+            }\r
+            if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box\r
+                return;\r
+            }\r
+\r
+            this.setInputAreaSelectionStartEnd();\r
+            this.scrollTop = inputArea.scrollTop;\r
+            if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {\r
+                this.text = inputArea.value;\r
+            }\r
+\r
+        }\r
+\r
+        // Sets the selected text in the input box after we've performed an\r
+        // operation.\r
+        this.setInputAreaSelection = function () {\r
+\r
+            if (!util.isVisible(inputArea)) {\r
+                return;\r
+            }\r
+\r
+            if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) {\r
+\r
+                inputArea.focus();\r
+                inputArea.selectionStart = stateObj.start;\r
+                inputArea.selectionEnd = stateObj.end;\r
+                inputArea.scrollTop = stateObj.scrollTop;\r
+            }\r
+            else if (doc.selection) {\r
+\r
+                if (doc.activeElement && doc.activeElement !== inputArea) {\r
+                    return;\r
+                }\r
+\r
+                inputArea.focus();\r
+                var range = inputArea.createTextRange();\r
+                range.moveStart("character", -inputArea.value.length);\r
+                range.moveEnd("character", -inputArea.value.length);\r
+                range.moveEnd("character", stateObj.end);\r
+                range.moveStart("character", stateObj.start);\r
+                range.select();\r
+            }\r
+        };\r
+\r
+        this.setInputAreaSelectionStartEnd = function () {\r
+\r
+            if (!panels.ieRetardedClick && (inputArea.selectionStart || inputArea.selectionStart === 0)) {\r
+\r
+                stateObj.start = inputArea.selectionStart;\r
+                stateObj.end = inputArea.selectionEnd;\r
+            }\r
+            else if (doc.selection) {\r
+\r
+                stateObj.text = util.fixEolChars(inputArea.value);\r
+\r
+                // IE loses the selection in the textarea when buttons are\r
+                // clicked.  On IE we cache the selection and set a flag\r
+                // which we check for here.\r
+                var range;\r
+                if (panels.ieRetardedClick && panels.ieCachedRange) {\r
+                    range = panels.ieCachedRange;\r
+                    panels.ieRetardedClick = false;\r
+                }\r
+                else {\r
+                    range = doc.selection.createRange();\r
+                }\r
+\r
+                var fixedRange = util.fixEolChars(range.text);\r
+                var marker = "\x07";\r
+                var markedRange = marker + fixedRange + marker;\r
+                range.text = markedRange;\r
+                var inputText = util.fixEolChars(inputArea.value);\r
+\r
+                range.moveStart("character", -markedRange.length);\r
+                range.text = fixedRange;\r
+\r
+                stateObj.start = inputText.indexOf(marker);\r
+                stateObj.end = inputText.lastIndexOf(marker) - marker.length;\r
+\r
+                var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;\r
+\r
+                if (len) {\r
+                    range.moveStart("character", -fixedRange.length);\r
+                    while (len--) {\r
+                        fixedRange += "\n";\r
+                        stateObj.end += 1;\r
+                    }\r
+                    range.text = fixedRange;\r
+                }\r
+\r
+                this.setInputAreaSelection();\r
+            }\r
+        };\r
+\r
+        // Restore this state into the input area.\r
+        this.restore = function () {\r
+\r
+            if (stateObj.text != undefined && stateObj.text != inputArea.value) {\r
+                inputArea.value = stateObj.text;\r
+            }\r
+            this.setInputAreaSelection();\r
+            inputArea.scrollTop = stateObj.scrollTop;\r
+        };\r
+\r
+        // Gets a collection of HTML chunks from the inptut textarea.\r
+        this.getChunks = function () {\r
+\r
+            var chunk = new Chunks();\r
+            chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));\r
+            chunk.startTag = "";\r
+            chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));\r
+            chunk.endTag = "";\r
+            chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));\r
+            chunk.scrollTop = stateObj.scrollTop;\r
+\r
+            return chunk;\r
+        };\r
+\r
+        // Sets the TextareaState properties given a chunk of markdown.\r
+        this.setChunks = function (chunk) {\r
+\r
+            chunk.before = chunk.before + chunk.startTag;\r
+            chunk.after = chunk.endTag + chunk.after;\r
+\r
+            this.start = chunk.before.length;\r
+            this.end = chunk.before.length + chunk.selection.length;\r
+            this.text = chunk.before + chunk.selection + chunk.after;\r
+            this.scrollTop = chunk.scrollTop;\r
+        };\r
+        this.init();\r
+    };\r
+\r
+    function PreviewManager(converter, panels, previewRefreshCallback) {\r
+\r
+        var managerObj = this;\r
+        var timeout;\r
+        var elapsedTime;\r
+        var oldInputText;\r
+        var maxDelay = 3000;\r
+        var startType = "delayed"; // The other legal value is "manual"\r
+\r
+        // Adds event listeners to elements\r
+        var setupEvents = function (inputElem, listener) {\r
+\r
+            util.addEvent(inputElem, "input", listener);\r
+            inputElem.onpaste = listener;\r
+            inputElem.ondrop = listener;\r
+\r
+            util.addEvent(inputElem, "keypress", listener);\r
+            util.addEvent(inputElem, "keydown", listener);\r
+        };\r
+\r
+        var getDocScrollTop = function () {\r
+\r
+            var result = 0;\r
+\r
+            if (top.innerHeight) {\r
+                result = top.pageYOffset;\r
+            }\r
+            else\r
+                if (doc.documentElement && doc.documentElement.scrollTop) {\r
+                    result = doc.documentElement.scrollTop;\r
+                }\r
+                else\r
+                    if (doc.body) {\r
+                        result = doc.body.scrollTop;\r
+                    }\r
+\r
+            return result;\r
+        };\r
+\r
+        var makePreviewHtml = function () {\r
+\r
+            // If there is no registered preview panel\r
+            // there is nothing to do.\r
+            if (!panels.preview)\r
+                return;\r
+\r
+\r
+            var text = panels.input.value;\r
+            if (text && text == oldInputText) {\r
+                return; // Input text hasn't changed.\r
+            }\r
+            else {\r
+                oldInputText = text;\r
+            }\r
+\r
+            var prevTime = new Date().getTime();\r
+\r
+            text = converter.makeHtml(text);\r
+\r
+            // Calculate the processing time of the HTML creation.\r
+            // It's used as the delay time in the event listener.\r
+            var currTime = new Date().getTime();\r
+            elapsedTime = currTime - prevTime;\r
+\r
+            pushPreviewHtml(text);\r
+        };\r
+\r
+        // setTimeout is already used.  Used as an event listener.\r
+        var applyTimeout = function () {\r
+\r
+            if (timeout) {\r
+                top.clearTimeout(timeout);\r
+                timeout = undefined;\r
+            }\r
+\r
+            if (startType !== "manual") {\r
+\r
+                var delay = 0;\r
+\r
+                if (startType === "delayed") {\r
+                    delay = elapsedTime;\r
+                }\r
+\r
+                if (delay > maxDelay) {\r
+                    delay = maxDelay;\r
+                }\r
+                timeout = top.setTimeout(makePreviewHtml, delay);\r
+            }\r
+        };\r
+\r
+        var getScaleFactor = function (panel) {\r
+            if (panel.scrollHeight <= panel.clientHeight) {\r
+                return 1;\r
+            }\r
+            return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);\r
+        };\r
+\r
+        var setPanelScrollTops = function () {\r
+            if (panels.preview) {\r
+                panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview);\r
+            }\r
+        };\r
+\r
+        this.refresh = function (requiresRefresh) {\r
+\r
+            if (requiresRefresh) {\r
+                oldInputText = "";\r
+                makePreviewHtml();\r
+            }\r
+            else {\r
+                applyTimeout();\r
+            }\r
+        };\r
+\r
+        this.processingTime = function () {\r
+            return elapsedTime;\r
+        };\r
+\r
+        var isFirstTimeFilled = true;\r
+\r
+        // IE doesn't let you use innerHTML if the element is contained somewhere in a table\r
+        // (which is the case for inline editing) -- in that case, detach the element, set the\r
+        // value, and reattach. Yes, that *is* ridiculous.\r
+        var ieSafePreviewSet = function (text) {\r
+            var preview = panels.preview;\r
+            var parent = preview.parentNode;\r
+            var sibling = preview.nextSibling;\r
+            parent.removeChild(preview);\r
+            preview.innerHTML = text;\r
+            if (!sibling)\r
+                parent.appendChild(preview);\r
+            else\r
+                parent.insertBefore(preview, sibling);\r
+        }\r
+\r
+        var nonSuckyBrowserPreviewSet = function (text) {\r
+            panels.preview.innerHTML = text;\r
+        }\r
+\r
+        var previewSetter;\r
+\r
+        var previewSet = function (text) {\r
+            if (previewSetter)\r
+                return previewSetter(text);\r
+\r
+            try {\r
+                nonSuckyBrowserPreviewSet(text);\r
+                previewSetter = nonSuckyBrowserPreviewSet;\r
+            } catch (e) {\r
+                previewSetter = ieSafePreviewSet;\r
+                previewSetter(text);\r
+            }\r
+        };\r
+\r
+        var pushPreviewHtml = function (text) {\r
+\r
+            var emptyTop = position.getTop(panels.input) - getDocScrollTop();\r
+\r
+            if (panels.preview) {\r
+                previewSet(text);\r
+                previewRefreshCallback();\r
+            }\r
+\r
+            setPanelScrollTops();\r
+\r
+            if (isFirstTimeFilled) {\r
+                isFirstTimeFilled = false;\r
+                return;\r
+            }\r
+\r
+            var fullTop = position.getTop(panels.input) - getDocScrollTop();\r
+\r
+            if (uaSniffed.isIE) {\r
+                top.setTimeout(function () {\r
+                    top.scrollBy(0, fullTop - emptyTop);\r
+                }, 0);\r
+            }\r
+            else {\r
+                top.scrollBy(0, fullTop - emptyTop);\r
+            }\r
+        };\r
+\r
+        var init = function () {\r
+\r
+            setupEvents(panels.input, applyTimeout);\r
+            makePreviewHtml();\r
+\r
+            if (panels.preview) {\r
+                panels.preview.scrollTop = 0;\r
+            }\r
+        };\r
+\r
+        init();\r
+    };\r
+\r
+    // Creates the background behind the hyperlink text entry box.\r
+    // And download dialog\r
+    // Most of this has been moved to CSS but the div creation and\r
+    // browser-specific hacks remain here.\r
+    ui.createBackground = function () {\r
+\r
+        var background = doc.createElement("div");\r
+        background.className = "wmd-prompt-background";\r
+        style = background.style;\r
+        style.position = "absolute";\r
+        style.top = "0";\r
+\r
+        style.zIndex = "1000";\r
+\r
+        if (uaSniffed.isIE) {\r
+            style.filter = "alpha(opacity=50)";\r
+        }\r
+        else {\r
+            style.opacity = "0.5";\r
+        }\r
+\r
+        var pageSize = position.getPageSize();\r
+        style.height = pageSize[1] + "px";\r
+\r
+        if (uaSniffed.isIE) {\r
+            style.left = doc.documentElement.scrollLeft;\r
+            style.width = doc.documentElement.clientWidth;\r
+        }\r
+        else {\r
+            style.left = "0";\r
+            style.width = "100%";\r
+        }\r
+\r
+        doc.body.appendChild(background);\r
+        return background;\r
+    };\r
+\r
+    // This simulates a modal dialog box and asks for the URL when you\r
+    // click the hyperlink or image buttons.\r
+    //\r
+    // text: The html for the input box.\r
+    // defaultInputText: The default value that appears in the input box.\r
+    // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel.\r
+    //      It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel\r
+    //      was chosen).\r
+    ui.prompt = function (text, defaultInputText, callback) {\r
+\r
+        // These variables need to be declared at this level since they are used\r
+        // in multiple functions.\r
+        var dialog;         // The dialog box.\r
+        var input;         // The text box where you enter the hyperlink.\r
+\r
+\r
+        if (defaultInputText === undefined) {\r
+            defaultInputText = "";\r
+        }\r
+\r
+        // Used as a keydown event handler. Esc dismisses the prompt.\r
+        // Key code 27 is ESC.\r
+        var checkEscape = function (key) {\r
+            var code = (key.charCode || key.keyCode);\r
+            if (code === 27) {\r
+                close(true);\r
+            }\r
+        };\r
+\r
+        // Dismisses the hyperlink input box.\r
+        // isCancel is true if we don't care about the input text.\r
+        // isCancel is false if we are going to keep the text.\r
+        var close = function (isCancel) {\r
+            util.removeEvent(doc.body, "keydown", checkEscape);\r
+            var text = input.value;\r
+\r
+            if (isCancel) {\r
+                text = null;\r
+            }\r
+            else {\r
+                // Fixes common pasting errors.\r
+                text = text.replace('http://http://', 'http://');\r
+                text = text.replace('http://https://', 'https://');\r
+                text = text.replace('http://ftp://', 'ftp://');\r
+\r
+                if (text.indexOf('http://') === -1 && text.indexOf('ftp://') === -1 && text.indexOf('https://') === -1) {\r
+                    text = 'http://' + text;\r
+                }\r
+            }\r
+\r
+            dialog.parentNode.removeChild(dialog);\r
+\r
+            callback(text);\r
+            return false;\r
+        };\r
+\r
+\r
+\r
+        // Create the text input box form/window.\r
+        var createDialog = function () {\r
+\r
+            // The main dialog box.\r
+            dialog = doc.createElement("div");\r
+            dialog.className = "wmd-prompt-dialog";\r
+            dialog.style.padding = "10px;";\r
+            dialog.style.position = "fixed";\r
+            dialog.style.width = "400px";\r
+            dialog.style.zIndex = "1001";\r
+\r
+            // The dialog text.\r
+            var question = doc.createElement("div");\r
+            question.innerHTML = text;\r
+            question.style.padding = "5px";\r
+            dialog.appendChild(question);\r
+\r
+            // The web form container for the text box and buttons.\r
+            var form = doc.createElement("form");\r
+            form.onsubmit = function () { return close(false); };\r
+            style = form.style;\r
+            style.padding = "0";\r
+            style.margin = "0";\r
+            style.cssFloat = "left";\r
+            style.width = "100%";\r
+            style.textAlign = "center";\r
+            style.position = "relative";\r
+            dialog.appendChild(form);\r
+\r
+            // The input text box\r
+            input = doc.createElement("input");\r
+            input.type = "text";\r
+            input.value = defaultInputText;\r
+            style = input.style;\r
+            style.display = "block";\r
+            style.width = "80%";\r
+            style.marginLeft = style.marginRight = "auto";\r
+            form.appendChild(input);\r
+\r
+            // The ok button\r
+            var okButton = doc.createElement("input");\r
+            okButton.type = "button";\r
+            okButton.onclick = function () { return close(false); };\r
+            okButton.value = "OK";\r
+            style = okButton.style;\r
+            style.margin = "10px";\r
+            style.display = "inline";\r
+            style.width = "7em";\r
+\r
+\r
+            // The cancel button\r
+            var cancelButton = doc.createElement("input");\r
+            cancelButton.type = "button";\r
+            cancelButton.onclick = function () { return close(true); };\r
+            cancelButton.value = "Cancel";\r
+            style = cancelButton.style;\r
+            style.margin = "10px";\r
+            style.display = "inline";\r
+            style.width = "7em";\r
+\r
+            form.appendChild(okButton);\r
+            form.appendChild(cancelButton);\r
+\r
+            util.addEvent(doc.body, "keydown", checkEscape);\r
+            dialog.style.top = "50%";\r
+            dialog.style.left = "50%";\r
+            dialog.style.display = "block";\r
+            if (uaSniffed.isIE_5or6) {\r
+                dialog.style.position = "absolute";\r
+                dialog.style.top = doc.documentElement.scrollTop + 200 + "px";\r
+                dialog.style.left = "50%";\r
+            }\r
+            doc.body.appendChild(dialog);\r
+\r
+            // This has to be done AFTER adding the dialog to the form if you\r
+            // want it to be centered.\r
+            dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";\r
+            dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";\r
+\r
+        };\r
+\r
+        // Why is this in a zero-length timeout?\r
+        // Is it working around a browser bug?\r
+        top.setTimeout(function () {\r
+\r
+            createDialog();\r
+\r
+            var defTextLen = defaultInputText.length;\r
+            if (input.selectionStart !== undefined) {\r
+                input.selectionStart = 0;\r
+                input.selectionEnd = defTextLen;\r
+            }\r
+            else if (input.createTextRange) {\r
+                var range = input.createTextRange();\r
+                range.collapse(false);\r
+                range.moveStart("character", -defTextLen);\r
+                range.moveEnd("character", defTextLen);\r
+                range.select();\r
+            }\r
+\r
+            input.focus();\r
+        }, 0);\r
+    };\r
+\r
+    function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) {\r
+\r
+        var inputBox = panels.input,\r
+            buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.\r
+\r
+        makeSpritedButtonRow();\r
+\r
+        var keyEvent = "keydown";\r
+        if (uaSniffed.isOpera) {\r
+            keyEvent = "keypress";\r
+        }\r
+\r
+        util.addEvent(inputBox, keyEvent, function (key) {\r
+\r
+            // Check to see if we have a button key and, if so execute the callback.\r
+            if ((key.ctrlKey || key.metaKey) && !key.altKey) {\r
+\r
+                var keyCode = key.charCode || key.keyCode;\r
+                var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();\r
+\r
+                switch (keyCodeStr) {\r
+                    case "b":\r
+                        doClick(buttons.bold);\r
+                        break;\r
+                    case "i":\r
+                        doClick(buttons.italic);\r
+                        break;\r
+                    case "l":\r
+                        doClick(buttons.link);\r
+                        break;\r
+                    case "q":\r
+                        doClick(buttons.quote);\r
+                        break;\r
+                    case "k":\r
+                        doClick(buttons.code);\r
+                        break;\r
+                    case "g":\r
+                        doClick(buttons.image);\r
+                        break;\r
+                    case "o":\r
+                        doClick(buttons.olist);\r
+                        break;\r
+                    case "u":\r
+                        doClick(buttons.ulist);\r
+                        break;\r
+                    case "h":\r
+                        doClick(buttons.heading);\r
+                        break;\r
+                    case "r":\r
+                        doClick(buttons.hr);\r
+                        break;\r
+                    case "y":\r
+                        doClick(buttons.redo);\r
+                        break;\r
+                    case "z":\r
+                        if (key.shiftKey) {\r
+                            doClick(buttons.redo);\r
+                        }\r
+                        else {\r
+                            doClick(buttons.undo);\r
+                        }\r
+                        break;\r
+                    default:\r
+                        return;\r
+                }\r
+\r
+\r
+                if (key.preventDefault) {\r
+                    key.preventDefault();\r
+                }\r
+\r
+                if (top.event) {\r
+                    top.event.returnValue = false;\r
+                }\r
+            }\r
+        });\r
+\r
+        // Auto-indent on shift-enter\r
+        util.addEvent(inputBox, "keyup", function (key) {\r
+            if (key.shiftKey && !key.ctrlKey && !key.metaKey) {\r
+                var keyCode = key.charCode || key.keyCode;\r
+                // Character 13 is Enter\r
+                if (keyCode === 13) {\r
+                    fakeButton = {};\r
+                    fakeButton.textOp = bindCommand("doAutoindent");\r
+                    doClick(fakeButton);\r
+                }\r
+            }\r
+        });\r
+\r
+        // special handler because IE clears the context of the textbox on ESC\r
+        if (uaSniffed.isIE) {\r
+            util.addEvent(inputBox, "keydown", function (key) {\r
+                var code = key.keyCode;\r
+                if (code === 27) {\r
+                    return false;\r
+                }\r
+            });\r
+        }\r
+\r
+\r
+        // Perform the button's action.\r
+        function doClick(button) {\r
+\r
+            inputBox.focus();\r
+\r
+            if (button.textOp) {\r
+\r
+                if (undoManager) {\r
+                    undoManager.setCommandMode();\r
+                }\r
+\r
+                var state = new TextareaState(panels);\r
+\r
+                if (!state) {\r
+                    return;\r
+                }\r
+\r
+                var chunks = state.getChunks();\r
+\r
+                // Some commands launch a "modal" prompt dialog.  Javascript\r
+                // can't really make a modal dialog box and the WMD code\r
+                // will continue to execute while the dialog is displayed.\r
+                // This prevents the dialog pattern I'm used to and means\r
+                // I can't do something like this:\r
+                //\r
+                // var link = CreateLinkDialog();\r
+                // makeMarkdownLink(link);\r
+                // \r
+                // Instead of this straightforward method of handling a\r
+                // dialog I have to pass any code which would execute\r
+                // after the dialog is dismissed (e.g. link creation)\r
+                // in a function parameter.\r
+                //\r
+                // Yes this is awkward and I think it sucks, but there's\r
+                // no real workaround.  Only the image and link code\r
+                // create dialogs and require the function pointers.\r
+                var fixupInputArea = function () {\r
+\r
+                    inputBox.focus();\r
+\r
+                    if (chunks) {\r
+                        state.setChunks(chunks);\r
+                    }\r
+\r
+                    state.restore();\r
+                    previewManager.refresh();\r
+                };\r
+\r
+                var noCleanup = button.textOp(chunks, fixupInputArea);\r
+\r
+                if (!noCleanup) {\r
+                    fixupInputArea();\r
+                }\r
+\r
+            }\r
+\r
+            if (button.execute) {\r
+                button.execute(undoManager);\r
+            }\r
+        };\r
+\r
+        function setupButton(button, isEnabled) {\r
+\r
+            var normalYShift = "0px";\r
+            var disabledYShift = "-20px";\r
+            var highlightYShift = "-40px";\r
+            var image = button.getElementsByTagName("span")[0];\r
+            if (isEnabled) {\r
+                image.style.backgroundPosition = button.XShift + " " + normalYShift;\r
+                button.onmouseover = function () {\r
+                    image.style.backgroundPosition = this.XShift + " " + highlightYShift;\r
+                };\r
+\r
+                button.onmouseout = function () {\r
+                    image.style.backgroundPosition = this.XShift + " " + normalYShift;\r
+                };\r
+\r
+                // IE tries to select the background image "button" text (it's\r
+                // implemented in a list item) so we have to cache the selection\r
+                // on mousedown.\r
+                if (uaSniffed.isIE) {\r
+                    button.onmousedown = function () {\r
+                        if (doc.activeElement && doc.activeElement !== panels.input) { // we're not even in the input box, so there's no selection\r
+                            return;\r
+                        }\r
+                        panels.ieRetardedClick = true;\r
+                        panels.ieCachedRange = document.selection.createRange();\r
+                    };\r
+                }\r
+\r
+                if (!button.isHelp) {\r
+                    button.onclick = function () {\r
+                        if (this.onmouseout) {\r
+                            this.onmouseout();\r
+                        }\r
+                        doClick(this);\r
+                        return false;\r
+                    }\r
+                }\r
+            }\r
+            else {\r
+                image.style.backgroundPosition = button.XShift + " " + disabledYShift;\r
+                button.onmouseover = button.onmouseout = button.onclick = function () { };\r
+            }\r
+        }\r
+\r
+        function bindCommand(method) {\r
+            if (typeof method === "string")\r
+                method = commandManager[method];\r
+            return function () { method.apply(commandManager, arguments); }\r
+        }\r
+\r
+        function makeSpritedButtonRow() {\r
+\r
+            var buttonBar = panels.buttonBar;\r
+\r
+            var normalYShift = "0px";\r
+            var disabledYShift = "-20px";\r
+            var highlightYShift = "-40px";\r
+\r
+            var buttonRow = document.createElement("ul");\r
+            buttonRow.id = "wmd-button-row" + postfix;\r
+            buttonRow.className = 'wmd-button-row';\r
+            buttonRow = buttonBar.appendChild(buttonRow);\r
+            var xPosition = 0;\r
+            var makeButton = function (id, title, XShift, textOp) {\r
+                var button = document.createElement("li");\r
+                button.className = "wmd-button";\r
+                button.style.left = xPosition + "px";\r
+                xPosition += 25;\r
+                var buttonImage = document.createElement("span");\r
+                button.id = id + postfix;\r
+                button.appendChild(buttonImage);\r
+                button.title = title;\r
+                button.XShift = XShift;\r
+                if (textOp)\r
+                    button.textOp = textOp;\r
+                setupButton(button, true);\r
+                buttonRow.appendChild(button);\r
+                return button;\r
+            };\r
+            var makeSpacer = function (num) {\r
+                var spacer = document.createElement("li");\r
+                spacer.className = "wmd-spacer wmd-spacer" + num;\r
+                spacer.id = "wmd-spacer" + num + postfix;\r
+                buttonRow.appendChild(spacer);\r
+                xPosition += 25;\r
+            }\r
+\r
+            buttons.bold = makeButton("wmd-bold-button", "Strong <strong> Ctrl+B", "0px", bindCommand("doBold"));\r
+            buttons.italic = makeButton("wmd-italic-button", "Emphasis <em> Ctrl+I", "-20px", bindCommand("doItalic"));\r
+            makeSpacer(1);\r
+            buttons.link = makeButton("wmd-link-button", "Hyperlink <a> Ctrl+L", "-40px", bindCommand(function (chunk, postProcessing) {\r
+                return this.doLinkOrImage(chunk, postProcessing, false);\r
+            }));\r
+            buttons.quote = makeButton("wmd-quote-button", "Blockquote <blockquote> Ctrl+Q", "-60px", bindCommand("doBlockquote"));\r
+            buttons.code = makeButton("wmd-code-button", "Code Sample <pre><code> Ctrl+K", "-80px", bindCommand("doCode"));\r
+            buttons.image = makeButton("wmd-image-button", "Image <img> Ctrl+G", "-100px", bindCommand(function (chunk, postProcessing) {\r
+                return this.doLinkOrImage(chunk, postProcessing, true);\r
+            }));\r
+            makeSpacer(2);\r
+            buttons.olist = makeButton("wmd-olist-button", "Numbered List <ol> Ctrl+O", "-120px", bindCommand(function (chunk, postProcessing) {\r
+                this.doList(chunk, postProcessing, true);\r
+            }));\r
+            buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List <ul> Ctrl+U", "-140px", bindCommand(function (chunk, postProcessing) {\r
+                this.doList(chunk, postProcessing, false);\r
+            }));\r
+            buttons.heading = makeButton("wmd-heading-button", "Heading <h1>/<h2> Ctrl+H", "-160px", bindCommand("doHeading"));\r
+            buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule <hr> Ctrl+R", "-180px", bindCommand("doHorizontalRule"));\r
+            makeSpacer(3);\r
+            buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "-200px", null);\r
+            buttons.undo.execute = function (manager) { if (manager) manager.undo(); };\r
+\r
+            var redoTitle = /win/.test(nav.platform.toLowerCase()) ?\r
+                "Redo - Ctrl+Y" :\r
+                "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms\r
+\r
+            buttons.redo = makeButton("wmd-redo-button", redoTitle, "-220px", null);\r
+            buttons.redo.execute = function (manager) { if (manager) manager.redo(); };\r
+\r
+            if (helpOptions) {\r
+                var helpButton = document.createElement("li");\r
+                var helpButtonImage = document.createElement("span");\r
+                helpButton.appendChild(helpButtonImage);\r
+                helpButton.className = "wmd-button wmd-help-button";\r
+                helpButton.id = "wmd-help-button" + postfix;\r
+                helpButton.XShift = "-240px";\r
+                helpButton.isHelp = true;\r
+                helpButton.style.right = "0px";\r
+                helpButton.title = helpOptions.title || defaultHelpHoverTitle;\r
+                helpButton.onclick = helpOptions.handler;\r
+\r
+                setupButton(helpButton, true);\r
+                buttonRow.appendChild(helpButton);\r
+                buttons.help = helpButton;\r
+            }\r
+\r
+            setUndoRedoButtonStates();\r
+        }\r
+\r
+        function setUndoRedoButtonStates() {\r
+            if (undoManager) {\r
+                setupButton(buttons.undo, undoManager.canUndo());\r
+                setupButton(buttons.redo, undoManager.canRedo());\r
+            }\r
+        };\r
+\r
+        this.setUndoRedoButtonStates = setUndoRedoButtonStates;\r
+\r
+    }\r
+\r
+    function CommandManager(pluginHooks) {\r
+        this.hooks = pluginHooks;\r
+    }\r
+\r
+    var commandProto = CommandManager.prototype;\r
+\r
+    // The markdown symbols - 4 spaces = code, > = blockquote, etc.\r
+    commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";\r
+\r
+    // Remove markdown symbols from the chunk selection.\r
+    commandProto.unwrap = function (chunk) {\r
+        var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g");\r
+        chunk.selection = chunk.selection.replace(txt, "$1 $2");\r
+    };\r
+\r
+    commandProto.wrap = function (chunk, len) {\r
+        this.unwrap(chunk);\r
+        var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");\r
+\r
+        chunk.selection = chunk.selection.replace(regex, function (line, marked) {\r
+            if (new re("^" + this.prefixes, "").test(line)) {\r
+                return line;\r
+            }\r
+            return marked + "\n";\r
+        });\r
+\r
+        chunk.selection = chunk.selection.replace(/\s+$/, "");\r
+    };\r
+\r
+    commandProto.doBold = function (chunk, postProcessing) {\r
+        return this.doBorI(chunk, postProcessing, 2, "strong text");\r
+    };\r
+\r
+    commandProto.doItalic = function (chunk, postProcessing) {\r
+        return this.doBorI(chunk, postProcessing, 1, "emphasized text");\r
+    };\r
+\r
+    // chunk: The selected region that will be enclosed with */**\r
+    // nStars: 1 for italics, 2 for bold\r
+    // insertText: If you just click the button without highlighting text, this gets inserted\r
+    commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {\r
+\r
+        // Get rid of whitespace and fixup newlines.\r
+        chunk.trimWhitespace();\r
+        chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");\r
+\r
+        // Look for stars before and after.  Is the chunk already marked up?\r
+        chunk.before.search(/(\**$)/);\r
+        var starsBefore = re.$1;\r
+\r
+        chunk.after.search(/(^\**)/);\r
+        var starsAfter = re.$1;\r
+\r
+        var prevStars = Math.min(starsBefore.length, starsAfter.length);\r
+\r
+        // Remove stars if we have to since the button acts as a toggle.\r
+        if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {\r
+            chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");\r
+            chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");\r
+        }\r
+        else if (!chunk.selection && starsAfter) {\r
+            // It's not really clear why this code is necessary.  It just moves\r
+            // some arbitrary stuff around.\r
+            chunk.after = chunk.after.replace(/^([*_]*)/, "");\r
+            chunk.before = chunk.before.replace(/(\s?)$/, "");\r
+            var whitespace = re.$1;\r
+            chunk.before = chunk.before + starsAfter + whitespace;\r
+        }\r
+        else {\r
+\r
+            // In most cases, if you don't have any selected text and click the button\r
+            // you'll get a selected, marked up region with the default text inserted.\r
+            if (!chunk.selection && !starsAfter) {\r
+                chunk.selection = insertText;\r
+            }\r
+\r
+            // Add the true markup.\r
+            var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?\r
+            chunk.before = chunk.before + markup;\r
+            chunk.after = markup + chunk.after;\r
+        }\r
+\r
+        return;\r
+    };\r
+\r
+    commandProto.stripLinkDefs = function (text, defsToAdd) {\r
+\r
+        text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*<?(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm,\r
+            function (totalMatch, id, link, newlines, title) {\r
+                defsToAdd[id] = totalMatch.replace(/\s*$/, "");\r
+                if (newlines) {\r
+                    // Strip the title and return that separately.\r
+                    defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");\r
+                    return newlines + title;\r
+                }\r
+                return "";\r
+            });\r
+\r
+        return text;\r
+    };\r
+\r
+    commandProto.addLinkDef = function (chunk, linkDef) {\r
+\r
+        var refNumber = 0; // The current reference number\r
+        var defsToAdd = {}; //\r
+        // Start with a clean slate by removing all previous link definitions.\r
+        chunk.before = this.stripLinkDefs(chunk.before, defsToAdd);\r
+        chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd);\r
+        chunk.after = this.stripLinkDefs(chunk.after, defsToAdd);\r
+\r
+        var defs = "";\r
+        var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;\r
+\r
+        var addDefNumber = function (def) {\r
+            refNumber++;\r
+            def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, "  [" + refNumber + "]:");\r
+            defs += "\n" + def;\r
+        };\r
+\r
+        // note that\r
+        // a) the recursive call to getLink cannot go infinite, because by definition\r
+        //    of regex, inner is always a proper substring of wholeMatch, and\r
+        // b) more than one level of nesting is neither supported by the regex\r
+        //    nor making a lot of sense (the only use case for nesting is a linked image)\r
+        var getLink = function (wholeMatch, before, inner, afterInner, id, end) {\r
+            inner = inner.replace(regex, getLink);\r
+            if (defsToAdd[id]) {\r
+                addDefNumber(defsToAdd[id]);\r
+                return before + inner + afterInner + refNumber + end;\r
+            }\r
+            return wholeMatch;\r
+        };\r
+\r
+        chunk.before = chunk.before.replace(regex, getLink);\r
+\r
+        if (linkDef) {\r
+            addDefNumber(linkDef);\r
+        }\r
+        else {\r
+            chunk.selection = chunk.selection.replace(regex, getLink);\r
+        }\r
+\r
+        var refOut = refNumber;\r
+\r
+        chunk.after = chunk.after.replace(regex, getLink);\r
+\r
+        if (chunk.after) {\r
+            chunk.after = chunk.after.replace(/\n*$/, "");\r
+        }\r
+        if (!chunk.after) {\r
+            chunk.selection = chunk.selection.replace(/\n*$/, "");\r
+        }\r
+\r
+        chunk.after += "\n\n" + defs;\r
+\r
+        return refOut;\r
+    };\r
+\r
+    // takes the line as entered into the add link/as image dialog and makes\r
+    // sure the URL and the optinal title are "nice".\r
+    function properlyEncoded(linkdef) {\r
+        return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {\r
+            link = link.replace(/\?.*$/, function (querypart) {\r
+                return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical\r
+            });\r
+            link = decodeURIComponent(link); // unencode first, to prevent double encoding\r
+            link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');\r
+            link = link.replace(/\?.*$/, function (querypart) {\r
+                return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded\r
+            });\r
+            if (title) {\r
+                title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");\r
+                title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");\r
+            }\r
+            return title ? link + ' "' + title + '"' : link;\r
+        });\r
+    }\r
+\r
+    commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) {\r
+\r
+        chunk.trimWhitespace();\r
+        chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);\r
+        var background;\r
+\r
+        if (chunk.endTag.length > 1) {\r
+\r
+            chunk.startTag = chunk.startTag.replace(/!?\[/, "");\r
+            chunk.endTag = "";\r
+            this.addLinkDef(chunk, null);\r
+\r
+        }\r
+        else {\r
+\r
+            if (/\n\n/.test(chunk.selection)) {\r
+                this.addLinkDef(chunk, null);\r
+                return;\r
+            }\r
+            var that = this;\r
+            // The function to be executed when you enter a link and press OK or Cancel.\r
+            // Marks up the link and adds the ref.\r
+            var linkEnteredCallback = function (link) {\r
+\r
+                background.parentNode.removeChild(background);\r
+\r
+                if (link !== null) {\r
+\r
+                    chunk.startTag = chunk.endTag = "";\r
+                    var linkDef = " [999]: " + properlyEncoded(link);\r
+\r
+                    var num = that.addLinkDef(chunk, linkDef);\r
+                    chunk.startTag = isImage ? "![" : "[";\r
+                    chunk.endTag = "][" + num + "]";\r
+\r
+                    if (!chunk.selection) {\r
+                        if (isImage) {\r
+                            chunk.selection = "enter image description here";\r
+                        }\r
+                        else {\r
+                            chunk.selection = "enter link description here";\r
+                        }\r
+                    }\r
+                }\r
+                postProcessing();\r
+            };\r
+\r
+            background = ui.createBackground();\r
+\r
+            if (isImage) {\r
+                if (!this.hooks.insertImageDialog(linkEnteredCallback))\r
+                    ui.prompt(imageDialogText, imageDefaultText, linkEnteredCallback);\r
+            }\r
+            else {\r
+                ui.prompt(linkDialogText, linkDefaultText, linkEnteredCallback);\r
+            }\r
+            return true;\r
+        }\r
+    };\r
+\r
+    // When making a list, hitting shift-enter will put your cursor on the next line\r
+    // at the current indent level.\r
+    commandProto.doAutoindent = function (chunk, postProcessing) {\r
+\r
+        var commandMgr = this;\r
+\r
+        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");\r
+        chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");\r
+        chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");\r
+\r
+        if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {\r
+            if (commandMgr.doList) {\r
+                commandMgr.doList(chunk);\r
+            }\r
+        }\r
+        if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {\r
+            if (commandMgr.doBlockquote) {\r
+                commandMgr.doBlockquote(chunk);\r
+            }\r
+        }\r
+        if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {\r
+            if (commandMgr.doCode) {\r
+                commandMgr.doCode(chunk);\r
+            }\r
+        }\r
+    };\r
+\r
+    commandProto.doBlockquote = function (chunk, postProcessing) {\r
+\r
+        chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,\r
+            function (totalMatch, newlinesBefore, text, newlinesAfter) {\r
+                chunk.before += newlinesBefore;\r
+                chunk.after = newlinesAfter + chunk.after;\r
+                return text;\r
+            });\r
+\r
+        chunk.before = chunk.before.replace(/(>[ \t]*)$/,\r
+            function (totalMatch, blankLine) {\r
+                chunk.selection = blankLine + chunk.selection;\r
+                return "";\r
+            });\r
+\r
+        chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");\r
+        chunk.selection = chunk.selection || "Blockquote";\r
+\r
+        // The original code uses a regular expression to find out how much of the\r
+        // text *directly before* the selection already was a blockquote:\r
+\r
+        /*\r
+        if (chunk.before) {\r
+        chunk.before = chunk.before.replace(/\n?$/, "\n");\r
+        }\r
+        chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/,\r
+        function (totalMatch) {\r
+        chunk.startTag = totalMatch;\r
+        return "";\r
+        });\r
+        */\r
+\r
+        // This comes down to:\r
+        // Go backwards as many lines a possible, such that each line\r
+        //  a) starts with ">", or\r
+        //  b) is almost empty, except for whitespace, or\r
+        //  c) is preceeded by an unbroken chain of non-empty lines\r
+        //     leading up to a line that starts with ">" and at least one more character\r
+        // and in addition\r
+        //  d) at least one line fulfills a)\r
+        //\r
+        // Since this is essentially a backwards-moving regex, it's susceptible to\r
+        // catstrophic backtracking and can cause the browser to hang;\r
+        // see e.g. http://meta.stackoverflow.com/questions/9807.\r
+        //\r
+        // Hence we replaced this by a simple state machine that just goes through the\r
+        // lines and checks for a), b), and c).\r
+\r
+        var match = "",\r
+            leftOver = "",\r
+            line;\r
+        if (chunk.before) {\r
+            var lines = chunk.before.replace(/\n$/, "").split("\n");\r
+            var inChain = false;\r
+            for (var i = 0; i < lines.length; i++ ) {\r
+                var good = false;\r
+                line = lines[i];\r
+                inChain = inChain && line.length > 0; // c) any non-empty line continues the chain\r
+                if (/^>/.test(line)) {                // a)\r
+                    good = true;\r
+                    if (!inChain && line.length > 1)  // c) any line that starts with ">" and has at least one more character starts the chain\r
+                        inChain = true;\r
+                } else if (/^[ \t]*$/.test(line)) {   // b)\r
+                    good = true;\r
+                } else {\r
+                    good = inChain;                   // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain\r
+                }\r
+                if (good) {\r
+                    match += line + "\n";\r
+                } else {\r
+                    leftOver += match + line;\r
+                    match = "\n";\r
+                }\r
+            }\r
+            if (!/(^|\n)>/.test(match)) {             // d)\r
+                leftOver += match;\r
+                match = "";\r
+            }\r
+        }\r
+\r
+        chunk.startTag = match;\r
+        chunk.before = leftOver;\r
+\r
+        // end of change\r
+\r
+        if (chunk.after) {\r
+            chunk.after = chunk.after.replace(/^\n?/, "\n");\r
+        }\r
+\r
+        chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/,\r
+            function (totalMatch) {\r
+                chunk.endTag = totalMatch;\r
+                return "";\r
+            }\r
+        );\r
+\r
+        var replaceBlanksInTags = function (useBracket) {\r
+\r
+            var replacement = useBracket ? "> " : "";\r
+\r
+            if (chunk.startTag) {\r
+                chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/,\r
+                    function (totalMatch, markdown) {\r
+                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";\r
+                    });\r
+            }\r
+            if (chunk.endTag) {\r
+                chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/,\r
+                    function (totalMatch, markdown) {\r
+                        return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";\r
+                    });\r
+            }\r
+        };\r
+\r
+        if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {\r
+            this.wrap(chunk, SETTINGS.lineLength - 2);\r
+            chunk.selection = chunk.selection.replace(/^/gm, "> ");\r
+            replaceBlanksInTags(true);\r
+            chunk.skipLines();\r
+        } else {\r
+            chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");\r
+            this.unwrap(chunk);\r
+            replaceBlanksInTags(false);\r
+\r
+            if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {\r
+                chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");\r
+            }\r
+\r
+            if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {\r
+                chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");\r
+            }\r
+        }\r
+\r
+        chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection);\r
+\r
+        if (!/\n/.test(chunk.selection)) {\r
+            chunk.selection = chunk.selection.replace(/^(> *)/,\r
+            function (wholeMatch, blanks) {\r
+                chunk.startTag += blanks;\r
+                return "";\r
+            });\r
+        }\r
+    };\r
+\r
+    commandProto.doCode = function (chunk, postProcessing) {\r
+\r
+        var hasTextBefore = /\S[ ]*$/.test(chunk.before);\r
+        var hasTextAfter = /^[ ]*\S/.test(chunk.after);\r
+\r
+        // Use 'four space' markdown if the selection is on its own\r
+        // line or is multiline.\r
+        if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {\r
+\r
+            chunk.before = chunk.before.replace(/[ ]{4}$/,\r
+                function (totalMatch) {\r
+                    chunk.selection = totalMatch + chunk.selection;\r
+                    return "";\r
+                });\r
+\r
+            var nLinesBack = 1;\r
+            var nLinesForward = 1;\r
+\r
+            if (/\n(\t|[ ]{4,}).*\n$/.test(chunk.before)) {\r
+                nLinesBack = 0;\r
+            }\r
+            if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {\r
+                nLinesForward = 0;\r
+            }\r
+\r
+            chunk.skipLines(nLinesBack, nLinesForward);\r
+\r
+            if (!chunk.selection) {\r
+                chunk.startTag = "    ";\r
+                chunk.selection = "enter code here";\r
+            }\r
+            else {\r
+                if (/^[ ]{0,3}\S/m.test(chunk.selection)) {\r
+                    chunk.selection = chunk.selection.replace(/^/gm, "    ");\r
+                }\r
+                else {\r
+                    chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");\r
+                }\r
+            }\r
+        }\r
+        else {\r
+            // Use backticks (`) to delimit the code block.\r
+\r
+            chunk.trimWhitespace();\r
+            chunk.findTags(/`/, /`/);\r
+\r
+            if (!chunk.startTag && !chunk.endTag) {\r
+                chunk.startTag = chunk.endTag = "`";\r
+                if (!chunk.selection) {\r
+                    chunk.selection = "enter code here";\r
+                }\r
+            }\r
+            else if (chunk.endTag && !chunk.startTag) {\r
+                chunk.before += chunk.endTag;\r
+                chunk.endTag = "";\r
+            }\r
+            else {\r
+                chunk.startTag = chunk.endTag = "";\r
+            }\r
+        }\r
+    };\r
+\r
+    commandProto.doList = function (chunk, postProcessing, isNumberedList) {\r
+\r
+        // These are identical except at the very beginning and end.\r
+        // Should probably use the regex extension function to make this clearer.\r
+        var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;\r
+        var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;\r
+\r
+        // The default bullet is a dash but others are possible.\r
+        // This has nothing to do with the particular HTML bullet,\r
+        // it's just a markdown bullet.\r
+        var bullet = "-";\r
+\r
+        // The number in a numbered list.\r
+        var num = 1;\r
+\r
+        // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.\r
+        var getItemPrefix = function () {\r
+            var prefix;\r
+            if (isNumberedList) {\r
+                prefix = " " + num + ". ";\r
+                num++;\r
+            }\r
+            else {\r
+                prefix = " " + bullet + " ";\r
+            }\r
+            return prefix;\r
+        };\r
+\r
+        // Fixes the prefixes of the other list items.\r
+        var getPrefixedItem = function (itemText) {\r
+\r
+            // The numbering flag is unset when called by autoindent.\r
+            if (isNumberedList === undefined) {\r
+                isNumberedList = /^\s*\d/.test(itemText);\r
+            }\r
+\r
+            // Renumber/bullet the list element.\r
+            itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm,\r
+                function (_) {\r
+                    return getItemPrefix();\r
+                });\r
+\r
+            return itemText;\r
+        };\r
+\r
+        chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);\r
+\r
+        if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {\r
+            chunk.before += chunk.startTag;\r
+            chunk.startTag = "";\r
+        }\r
+\r
+        if (chunk.startTag) {\r
+\r
+            var hasDigits = /\d+[.]/.test(chunk.startTag);\r
+            chunk.startTag = "";\r
+            chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");\r
+            this.unwrap(chunk);\r
+            chunk.skipLines();\r
+\r
+            if (hasDigits) {\r
+                // Have to renumber the bullet points if this is a numbered list.\r
+                chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);\r
+            }\r
+            if (isNumberedList == hasDigits) {\r
+                return;\r
+            }\r
+        }\r
+\r
+        var nLinesUp = 1;\r
+\r
+        chunk.before = chunk.before.replace(previousItemsRegex,\r
+            function (itemText) {\r
+                if (/^\s*([*+-])/.test(itemText)) {\r
+                    bullet = re.$1;\r
+                }\r
+                nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;\r
+                return getPrefixedItem(itemText);\r
+            });\r
+\r
+        if (!chunk.selection) {\r
+            chunk.selection = "List item";\r
+        }\r
+\r
+        var prefix = getItemPrefix();\r
+\r
+        var nLinesDown = 1;\r
+\r
+        chunk.after = chunk.after.replace(nextItemsRegex,\r
+            function (itemText) {\r
+                nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;\r
+                return getPrefixedItem(itemText);\r
+            });\r
+\r
+        chunk.trimWhitespace(true);\r
+        chunk.skipLines(nLinesUp, nLinesDown, true);\r
+        chunk.startTag = prefix;\r
+        var spaces = prefix.replace(/./g, " ");\r
+        this.wrap(chunk, SETTINGS.lineLength - spaces.length);\r
+        chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);\r
+\r
+    };\r
+\r
+    commandProto.doHeading = function (chunk, postProcessing) {\r
+\r
+        // Remove leading/trailing whitespace and reduce internal spaces to single spaces.\r
+        chunk.selection = chunk.selection.replace(/\s+/g, " ");\r
+        chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");\r
+\r
+        // If we clicked the button with no selected text, we just\r
+        // make a level 2 hash header around some default text.\r
+        if (!chunk.selection) {\r
+            chunk.startTag = "## ";\r
+            chunk.selection = "Heading";\r
+            chunk.endTag = " ##";\r
+            return;\r
+        }\r
+\r
+        var headerLevel = 0;     // The existing header level of the selected text.\r
+\r
+        // Remove any existing hash heading markdown and save the header level.\r
+        chunk.findTags(/#+[ ]*/, /[ ]*#+/);\r
+        if (/#+/.test(chunk.startTag)) {\r
+            headerLevel = re.lastMatch.length;\r
+        }\r
+        chunk.startTag = chunk.endTag = "";\r
+\r
+        // Try to get the current header level by looking for - and = in the line\r
+        // below the selection.\r
+        chunk.findTags(null, /\s?(-+|=+)/);\r
+        if (/=+/.test(chunk.endTag)) {\r
+            headerLevel = 1;\r
+        }\r
+        if (/-+/.test(chunk.endTag)) {\r
+            headerLevel = 2;\r
+        }\r
+\r
+        // Skip to the next line so we can create the header markdown.\r
+        chunk.startTag = chunk.endTag = "";\r
+        chunk.skipLines(1, 1);\r
+\r
+        // We make a level 2 header if there is no current header.\r
+        // If there is a header level, we substract one from the header level.\r
+        // If it's already a level 1 header, it's removed.\r
+        var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;\r
+\r
+        if (headerLevelToCreate > 0) {\r
+\r
+            // The button only creates level 1 and 2 underline headers.\r
+            // Why not have it iterate over hash header levels?  Wouldn't that be easier and cleaner?\r
+            var headerChar = headerLevelToCreate >= 2 ? "-" : "=";\r
+            var len = chunk.selection.length;\r
+            if (len > SETTINGS.lineLength) {\r
+                len = SETTINGS.lineLength;\r
+            }\r
+            chunk.endTag = "\n";\r
+            while (len--) {\r
+                chunk.endTag += headerChar;\r
+            }\r
+        }\r
+    };\r
+\r
+    commandProto.doHorizontalRule = function (chunk, postProcessing) {\r
+        chunk.startTag = "----------\n";\r
+        chunk.selection = "";\r
+        chunk.skipLines(2, 1, true);\r
+    }\r
+\r
+\r
+})();
\ No newline at end of file
diff --git a/Markdown.Sanitizer.js b/Markdown.Sanitizer.js
new file mode 100644 (file)
index 0000000..238cfe2
--- /dev/null
@@ -0,0 +1,108 @@
+(function () {
+    var output, Converter;
+    if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
+        output = exports;
+        Converter = require("./Markdown.Converter").Converter;
+    } else {
+        output = window.Markdown;
+        Converter = output.Converter;
+    }
+        
+    output.getSanitizingConverter = function () {
+        var converter = new Converter();
+        converter.hooks.chain("postConversion", sanitizeHtml);
+        converter.hooks.chain("postConversion", balanceTags);
+        return converter;
+    }
+
+    function sanitizeHtml(html) {
+        return html.replace(/<[^>]*>?/gi, sanitizeTag);
+    }
+
+    // (tags that can be opened/closed) | (tags that stand alone)
+    var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
+    // <a href="url..." optional title>|</a>
+    var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
+
+    // <img src="url..." optional width  optional height  optional alt  optional title
+    var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
+
+    function sanitizeTag(tag) {
+        if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
+            return tag;
+        else
+            return "";
+    }
+
+    /// <summary>
+    /// attempt to balance HTML tags in the html string
+    /// by removing any unmatched opening or closing tags
+    /// IMPORTANT: we *assume* HTML has *already* been 
+    /// sanitized and is safe/sane before balancing!
+    /// 
+    /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
+    /// </summary>
+    function balanceTags(html) {
+
+        if (html == "")
+            return "";
+
+        var re = /<\/?\w+[^>]*(\s|$|>)/g;
+        // convert everything to lower case; this makes
+        // our case insensitive comparisons easier
+        var tags = html.toLowerCase().match(re);
+
+        // no HTML tags present? nothing to do; exit now
+        var tagcount = (tags || []).length;
+        if (tagcount == 0)
+            return html;
+
+        var tagname, tag;
+        var ignoredtags = "<p><img><br><li><hr>";
+        var match;
+        var tagpaired = [];
+        var tagremove = [];
+        var needsRemoval = false;
+
+        // loop through matched tags in forward order
+        for (var ctag = 0; ctag < tagcount; ctag++) {
+            tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
+            // skip any already paired tags
+            // and skip tags in our ignore list; assume they're self-closed
+            if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
+                continue;
+
+            tag = tags[ctag];
+            match = -1;
+
+            if (!/^<\//.test(tag)) {
+                // this is an opening tag
+                // search forwards (next tags), look for closing tags
+                for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
+                    if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
+                        match = ntag;
+                        break;
+                    }
+                }
+            }
+
+            if (match == -1)
+                needsRemoval = tagremove[ctag] = true; // mark for removal
+            else
+                tagpaired[match] = true; // mark paired
+        }
+
+        if (!needsRemoval)
+            return html;
+
+        // delete all orphaned tags from the string
+
+        var ctag = 0;
+        html = html.replace(re, function (match) {
+            var res = tagremove[ctag] ? "" : match;
+            ctag++;
+            return res;
+        });
+        return html;
+    }
+})()
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/demo/browser/demo.css b/demo/browser/demo.css
new file mode 100644 (file)
index 0000000..acf4f46
--- /dev/null
@@ -0,0 +1,120 @@
+body 
+{ 
+       background-color: White;
+    font-family: sans-serif;
+}
+
+.wmd-panel
+{
+       margin-left: 25%;
+       margin-right: 25%;
+       width: 50%;
+       min-width: 500px;
+}
+
+.wmd-button-bar 
+{
+       width: 100%;
+       background-color: Silver; 
+}
+
+.wmd-input 
+{ 
+       height: 300px;
+       width: 100%;
+       background-color: Gainsboro;
+       border: 1px solid DarkGray;
+}
+
+.wmd-preview 
+{ 
+       background-color: #c0e0ff; 
+}
+
+.wmd-button-row 
+{
+       position: relative; 
+       margin-left: 5px;
+       margin-right: 5px;
+       margin-bottom: 5px;
+       margin-top: 10px;
+       padding: 0px;  
+       height: 20px;
+}
+
+.wmd-spacer
+{
+       width: 1px; 
+       height: 20px; 
+       margin-left: 14px;
+       
+       position: absolute;
+       background-color: Silver;
+       display: inline-block; 
+       list-style: none;
+}
+
+.wmd-button {
+    width: 20px;
+    height: 20px;
+    padding-left: 2px;
+    padding-right: 3px;
+    position: absolute;
+    display: inline-block;
+    list-style: none;
+    cursor: pointer;
+}
+
+.wmd-button > span {
+    background-image: url(../../wmd-buttons.png);
+    background-repeat: no-repeat;
+    background-position: 0px 0px;
+    width: 20px;
+    height: 20px;
+    display: inline-block;
+}
+
+.wmd-spacer1
+{
+    left: 50px;
+}
+.wmd-spacer2
+{
+    left: 175px;
+}
+.wmd-spacer3
+{
+    left: 300px;
+}
+
+
+
+
+.wmd-prompt-background
+{
+       background-color: Black;
+}
+
+.wmd-prompt-dialog
+{
+       border: 1px solid #999999;
+       background-color: #F5F5F5;
+}
+
+.wmd-prompt-dialog > div {
+       font-size: 0.8em;
+       font-family: arial, helvetica, sans-serif;
+}
+
+
+.wmd-prompt-dialog > form > input[type="text"] {
+       border: 1px solid #999999;
+       color: black;
+}
+
+.wmd-prompt-dialog > form > input[type="button"]{
+       border: 1px solid #888888;
+       font-family: trebuchet MS, helvetica, sans-serif;
+       font-size: 0.8em;
+       font-weight: bold;
+}
diff --git a/demo/browser/demo.html b/demo/browser/demo.html
new file mode 100644 (file)
index 0000000..6aaf30d
--- /dev/null
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+
+<html>
+
+    <head>
+        <title>PageDown Demo Page</title>
+        
+        <link rel="stylesheet" type="text/css" href="demo.css" />
+        
+        <script type="text/javascript" src="../../Markdown.Converter.js"></script>
+        <script type="text/javascript" src="../../Markdown.Sanitizer.js"></script>
+        <script type="text/javascript" src="../../Markdown.Editor.js"></script>
+    </head>
+    
+    <body>
+        <div class="wmd-panel">
+            <div id="wmd-button-bar"></div>
+            <textarea class="wmd-input" id="wmd-input">
+This is the *first* editor.
+------------------------------
+
+Just plain **Markdown**, except that the input is sanitized:
+
+<marquee>I'm the ghost from the past!</marquee>
+</textarea>
+        </div>
+        <div id="wmd-preview" class="wmd-panel wmd-preview"></div>
+        
+        <br /> <br />
+        
+        <div class="wmd-panel">
+            <div id="wmd-button-bar-second"></div>
+            <textarea class="wmd-input" id="wmd-input-second">
+This is the *second* editor.
+------------------------------
+
+It has a plugin hook registered that surrounds all words starting with the
+letter A with asterisks before doing the Markdown conversion. Another one gives bare links
+a nicer link text. User input isn't sanitized here:
+
+<marquee>I'm the ghost from the past!</marquee>
+
+http://google.com
+
+http://stackoverflow.com
+
+It also includes a help button.
+</textarea>
+        </div>
+        <div id="wmd-preview-second" class="wmd-panel wmd-preview"></div>
+
+
+        <script type="text/javascript">
+            (function () {
+                var converter1 = Markdown.getSanitizingConverter();
+                var editor1 = new Markdown.Editor(converter1);
+                editor1.run();
+                
+                var converter2 = new Markdown.Converter();
+
+                converter2.hooks.chain("preConversion", function (text) {
+                    return text.replace(/\b(a\w*)/gi, "*$1*");
+                });
+
+                converter2.hooks.chain("plainLinkText", function (url) {
+                    return "This is a link to " + url.replace(/^https?:\/\//, "");
+                });
+                
+                var help = function () { alert("Do you need help?"); }
+                
+                var editor2 = new Markdown.Editor(converter2, "-second", { handler: help });
+                
+                editor2.run();
+            })();
+        </script>
+    </body>
+</html>
diff --git a/demo/node/demo.js b/demo/node/demo.js
new file mode 100644 (file)
index 0000000..097222d
--- /dev/null
@@ -0,0 +1,44 @@
+// NOTE: This is just a demo -- in a production environment,
+// be sure to spend a few more thoughts on sanitizing user input.
+// (also, you probably wouldn't use a get request)
+
+var http = require("http"),
+    url = require("url"),
+    querystring = require("querystring"),
+    Converter = require("../../Markdown.Converter").Converter,
+    getSanitizingConverter = require("../../Markdown.Sanitizer").getSanitizingConverter,
+    conv = new Converter(),
+    saneConv = getSanitizingConverter();
+
+http.createServer(function (req, res) {
+
+    var route = url.parse(req.url);
+    if (route.pathname !== "/") {
+        res.writeHead(404);
+        res.end("Page not found");
+        return;
+    }
+    
+    var query = querystring.parse(route.query);
+
+    res.writeHead(200, { "Content-type": "text/html" });
+    res.write("<html><body>");
+    
+    var markdown = query.md || "## Hello!\n\n<marquee>I'm walking</marquee>\n\nVisit [Stack Overflow](http://stackoverflow.com)\n\n<b><i>This is never closed!";
+
+    res.write("<h1>Your output, sanitized:</h1>\n" + saneConv.makeHtml(markdown))
+    res.write("<h1>Your output, unsanitized:</h1>\n" + conv.makeHtml(markdown))
+        
+    res.write(
+        "<h1>Enter Markdown</h1>\n" +
+        "<form method='get' action='/'>" +
+            "<textarea cols=50 rows=10 name='md'>" +
+                markdown.replace(/</g, "&lt;") +
+            "</textarea><br>" +
+            "<input type='submit' value='Convert!'>" +
+        "</form>"
+    );
+    
+    res.end("</body></html>");
+
+}).listen(8000);
diff --git a/resources/wmd-buttons.psd b/resources/wmd-buttons.psd
new file mode 100644 (file)
index 0000000..e61ff37
Binary files /dev/null and b/resources/wmd-buttons.psd differ
diff --git a/wmd-buttons.png b/wmd-buttons.png
new file mode 100644 (file)
index 0000000..50b3709
Binary files /dev/null and b/wmd-buttons.png differ