From 461f324e09b7d1e91f212753197c3636de58bf35 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 30 Sep 2013 19:08:24 -0400 Subject: [PATCH] Add markdown support for comments and repo descriptions --- static/css/quay.css | 51 +- static/js/controllers.js | 69 +- static/lib/pagedown/LICENSE.txt | 32 + static/lib/pagedown/Markdown.Converter.js | 1344 +++++++++++ .../lib/pagedown/Markdown.Editor.Icons.fw.png | Bin 0 -> 51360 bytes static/lib/pagedown/Markdown.Editor.Icons.png | Bin 0 -> 52640 bytes static/lib/pagedown/Markdown.Editor.js | 2114 +++++++++++++++++ static/lib/pagedown/Markdown.Editor.less | 80 + static/lib/pagedown/Markdown.Sanitizer.js | 111 + static/partials/repo-list.html | 2 +- static/partials/view-repo.html | 24 +- templates/index.html | 3 + 12 files changed, 3816 insertions(+), 14 deletions(-) create mode 100755 static/lib/pagedown/LICENSE.txt create mode 100755 static/lib/pagedown/Markdown.Converter.js create mode 100755 static/lib/pagedown/Markdown.Editor.Icons.fw.png create mode 100755 static/lib/pagedown/Markdown.Editor.Icons.png create mode 100755 static/lib/pagedown/Markdown.Editor.js create mode 100755 static/lib/pagedown/Markdown.Editor.less create mode 100755 static/lib/pagedown/Markdown.Sanitizer.js diff --git a/static/css/quay.css b/static/css/quay.css index 9fc96054c..c8b6f929d 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -32,10 +32,17 @@ margin-right: 8px; } +.editable { + position: relative; +} + .editable i { + position: absolute; + top: 0px; + right: -40px; + opacity: 0.2; font-size: 85%; - margin-left: 10px; display: inline-block; transition: opacity 500ms ease-in-out; @@ -244,6 +251,10 @@ p.editable:hover i { min-width: 300px; } +.repo .description p { + margin: 0px; +} + .repo thead td { padding: 4px; color: #999; @@ -257,6 +268,17 @@ p.editable:hover i { margin: 10px; } +.repo .images .image-id { + font-size: 85%; + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} + +.repo .images p { + margin: 0px; +} + .navbar-nav > li > .user-dropdown { padding-top: 9px; padding-bottom: 9px; @@ -348,6 +370,33 @@ p.editable:hover i { border: inherit; } +/* Overrides for the markdown editor. */ + +.wmd-panel .btn-toolbar { + margin-bottom: 10px; +} + +.wmd-panel textarea { + outline: none; +} + +.wmd-panel.wmd-preview:before { + display: inline-block; + content: "Preview"; + background: #eee; + padding: 4px; + + position: absolute; + top: 0px; + right: 0px; +} + +.wmd-panel.wmd-preview { + position: relative; + border: 1px solid #eee; + margin-top: 10px; + padding: 10px; +} /* Overrides for typeahead to work with bootstrap 3. */ diff --git a/static/js/controllers.js b/static/js/controllers.js index acb7c61ea..43e0c57e3 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1,3 +1,40 @@ +function getFirstTextLine(commentString) { + if (!commentString) { return; } + + var lines = commentString.split('\n'); + var MARKDOWN_CHARS = { + '#': true, + '-': true, + '>': true, + '`': true + }; + + for (var i = 0; i < lines.length; ++i) { + // Skip code lines. + if (lines[i].indexOf(' ') == 0) { + continue; + } + + // Skip empty lines. + if ($.trim(lines[i]).length == 0) { + continue; + } + + // Skip control lines. + if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) { + continue; + } + + return getMarkedDown(lines[i]); + } + + return ''; +} + +function getMarkedDown(string) { + return Markdown.getSanitizingConverter().makeHtml(string || ''); +} + function HeaderCtrl($scope, UserService) { $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; @@ -25,7 +62,7 @@ function HeaderCtrl($scope, UserService) { template += '' template += '' + datum.repo.namespace +'/' + datum.repo.name + '' if (datum.repo.description) { - template += '' + datum.repo.description + '' + template += '' + getFirstTextLine(datum.repo.description) + '' } template += '' @@ -45,6 +82,15 @@ function RepoListCtrl($scope, Restangular) { repositoryFetch.getList().then(function(resp) { $scope.repositories = resp.repositories; }); + + $scope.getCommentFirstLine = function(commentString) { + return getMarkedDown(getFirstTextLine(commentString)); + }; + + $scope.getMarkedDown = function(string) { + if (!string) { return ''; } + return getMarkedDown(string); + }; } function LandingCtrl($scope) { @@ -72,19 +118,36 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) { $scope.editDescription = function() { if (!$scope.repo.can_write) { return; } - $('#descriptionEdit')[0].value = $scope.repo.description || ''; + + if (!$scope.markdownDescriptionEditor) { + var converter = Markdown.getSanitizingConverter(); + var editor = new Markdown.Editor(converter, '-description'); + editor.run(); + $scope.markdownDescriptionEditor = editor; + } + + $('#wmd-input-description')[0].value = $scope.repo.description; $('#editModal').modal({}); }; $scope.saveDescription = function() { $('#editModal').modal('hide'); - $scope.repo.description = $('#descriptionEdit')[0].value; + $scope.repo.description = $('#wmd-input-description')[0].value; $scope.repo.put(); }; $scope.parseDate = function(dateString) { return Date.parse(dateString); }; + + $scope.getCommentFirstLine = function(commentString) { + return getMarkedDown(getFirstTextLine(commentString)); + }; + + $scope.getMarkedDown = function(string) { + if (!string) { return ''; } + return getMarkedDown(string); + }; $scope.listImages = function() { if ($scope.imageHistory) { return; } diff --git a/static/lib/pagedown/LICENSE.txt b/static/lib/pagedown/LICENSE.txt new file mode 100755 index 000000000..ca48b4f78 --- /dev/null +++ b/static/lib/pagedown/LICENSE.txt @@ -0,0 +1,32 @@ +A javascript port of Markdown, as used on Stack Overflow +and the rest of Stack Exchange network. + +Largely based on showdown.js by John Fraser (Attacklab). + +Original Markdown Copyright (c) 2004-2005 John Gruber + + + +Original Showdown code copyright (c) 2007 John Fraser + +Modifications and bugfixes (c) 2009 Dana Robinson +Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/static/lib/pagedown/Markdown.Converter.js b/static/lib/pagedown/Markdown.Converter.js new file mode 100755 index 000000000..d1a6431be --- /dev/null +++ b/static/lib/pagedown/Markdown.Converter.js @@ -0,0 +1,1344 @@ +var Markdown; + +if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module + Markdown = exports; +else + Markdown = {}; + +// The following text is included for historical reasons, but should +// be taken with a pinch of salt; it's not all true anymore. + +// +// Wherever possible, Showdown is a straight, line-by-line port +// of the Perl version of Markdown. +// +// This is not a normal parser design; it's basically just a +// series of string substitutions. It's hard to read and +// maintain this way, but keeping Showdown close to the original +// design makes it easier to port new features. +// +// More importantly, Showdown behaves like markdown.pl in most +// edge cases. So web applications can do client-side preview +// in Javascript, and then build identical HTML on the server. +// +// This port needs the new RegExp functionality of ECMA 262, +// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers +// should do fine. Even with the new regular expression features, +// We do a lot of work to emulate Perl's regex functionality. +// The tricky changes in this file mostly have the "attacklab:" +// label. Major or self-explanatory changes don't. +// +// Smart diff tools like Araxis Merge will be able to match up +// this file with markdown.pl in a useful way. A little tweaking +// helps: in a copy of markdown.pl, replace "#" with "//" and +// replace "$text" with "text". Be sure to ignore whitespace +// and line endings. +// + + +// +// Usage: +// +// var text = "Markdown *rocks*."; +// +// var converter = new Markdown.Converter(); +// var html = converter.makeHtml(text); +// +// alert(html); +// +// Note: move the sample code to the bottom of this +// file before uncommenting it. +// + +(function () { + + function identity(x) { return x; } + function returnFalse(x) { return false; } + + function HookCollection() { } + + HookCollection.prototype = { + + chain: function (hookname, func) { + var original = this[hookname]; + if (!original) + throw new Error("unknown hook " + hookname); + + if (original === identity) + this[hookname] = func; + else + this[hookname] = function (x) { return func(original(x)); } + }, + set: function (hookname, func) { + if (!this[hookname]) + throw new Error("unknown hook " + hookname); + this[hookname] = func; + }, + addNoop: function (hookname) { + this[hookname] = identity; + }, + addFalse: function (hookname) { + this[hookname] = returnFalse; + } + }; + + Markdown.HookCollection = HookCollection; + + // g_urls and g_titles allow arbitrary user-entered strings as keys. This + // caused an exception (and hence stopped the rendering) when the user entered + // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this + // (since no builtin property starts with "s_"). See + // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug + // (granted, switching from Array() to Object() alone would have left only __proto__ + // to be a problem) + function SaveHash() { } + SaveHash.prototype = { + set: function (key, value) { + this["s_" + key] = value; + }, + get: function (key) { + return this["s_" + key]; + } + }; + + Markdown.Converter = function () { + var pluginHooks = this.hooks = new HookCollection(); + 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 + 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 + pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml + + // + // Private state of the converter instance: + // + + // Global hashes, used by various utility routines + var g_urls; + var g_titles; + var g_html_blocks; + + // Used to track when we're inside an ordered or unordered list + // (see _ProcessListItems() for details): + var g_list_level; + + this.makeHtml = function (text) { + + // + // Main function. The order in which other subs are called here is + // essential. Link and image substitutions need to happen before + // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the + // and tags get encoded. + // + + // This will only happen if makeHtml on the same converter instance is called from a plugin hook. + // Don't do that. + if (g_urls) + throw new Error("Recursive call to converter.makeHtml"); + + // Create the private state objects. + g_urls = new SaveHash(); + g_titles = new SaveHash(); + g_html_blocks = []; + g_list_level = 0; + + text = pluginHooks.preConversion(text); + + // attacklab: Replace ~ with ~T + // This lets us use tilde as an escape char to avoid md5 hashes + // The choice of character is arbitray; anything that isn't + // magic in Markdown will work. + text = text.replace(/~/g, "~T"); + + // attacklab: Replace $ with ~D + // RegExp interprets $ as a special character + // when it's in a replacement string + text = text.replace(/\$/g, "~D"); + + // Standardize line endings + text = text.replace(/\r\n/g, "\n"); // DOS to Unix + text = text.replace(/\r/g, "\n"); // Mac to Unix + + // Make sure text begins and ends with a couple of newlines: + text = "\n\n" + text + "\n\n"; + + // Convert all tabs to spaces. + text = _Detab(text); + + // Strip any lines consisting only of spaces and tabs. + // This makes subsequent regexen easier to write, because we can + // match consecutive blank lines with /\n+/ instead of something + // contorted like /[ \t]*\n+/ . + text = text.replace(/^[ \t]+$/mg, ""); + + // Turn block-level HTML blocks into hash entries + text = _HashHTMLBlocks(text); + + // Strip link definitions, store in hashes. + text = _StripLinkDefinitions(text); + + text = _RunBlockGamut(text); + + text = _UnescapeSpecialChars(text); + + // attacklab: Restore dollar signs + text = text.replace(/~D/g, "$$"); + + // attacklab: Restore tildes + text = text.replace(/~T/g, "~"); + + text = pluginHooks.postConversion(text); + + g_html_blocks = g_titles = g_urls = null; + + return text; + }; + + function _StripLinkDefinitions(text) { + // + // Strips link definitions from text, stores the URLs and titles in + // hash references. + // + + // Link defs are in the form: ^[id]: url "optional title" + + /* + text = text.replace(/ + ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 + [ \t]* + \n? // maybe *one* newline + [ \t]* + ? // url = $2 + (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below + [ \t]* + \n? // maybe one newline + [ \t]* + ( // (potential) title = $3 + (\n*) // any lines skipped = $4 attacklab: lookbehind removed + [ \t]+ + ["(] + (.+?) // title = $5 + [")] + [ \t]* + )? // title is optional + (?:\n+|$) + /gm, function(){...}); + */ + + text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, + function (wholeMatch, m1, m2, m3, m4, m5) { + m1 = m1.toLowerCase(); + g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive + if (m4) { + // Oops, found blank lines, so it's not a title. + // Put back the parenthetical statement we stole. + return m3; + } else if (m5) { + g_titles.set(m1, m5.replace(/"/g, """)); + } + + // Completely remove the definition from the text + return ""; + } + ); + + return text; + } + + function _HashHTMLBlocks(text) { + + // Hashify HTML blocks: + // We only want to do this for block-level HTML tags, such as headers, + // lists, and tables. That's because we still want to wrap

s around + // "paragraphs" that are wrapped in non-block-level tags, such as anchors, + // phrase emphasis, and spans. The list of tags we're looking for is + // hard-coded: + var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" + var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" + + // First, look for nested blocks, e.g.: + //

+ //
+ // tags for inner block must be indented. + //
+ //
+ // + // The outermost tags must start at the left margin for this to match, and + // the inner nested divs must be indented. + // We need to do this before the next, more liberal match, because the next + // match will start at the first `
` and stop at the first `
`. + + // attacklab: This regex can be expensive when it fails. + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_a) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*?\n // any number of lines, minimally matching + // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + 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); + + // + // Now match more liberally, simply from `\n` to `\n` + // + + /* + text = text.replace(/ + ( // save in $1 + ^ // start of line (with /m) + <($block_tags_b) // start tag = $2 + \b // word break + // attacklab: hack around khtml/pcre bug... + [^\r]*? // any number of lines, minimally matching + .* // the matching end tag + [ \t]* // trailing spaces/tabs + (?=\n+) // followed by a newline + ) // attacklab: there are sentinel newlines at end of document + /gm,function(){...}}; + */ + 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); + + // Special case just for
. It was easier to make a special case than + // to make the other regex more complicated. + + /* + text = text.replace(/ + \n // Starting after a blank line + [ ]{0,3} + ( // save in $1 + (<(hr) // start tag = $2 + \b // word break + ([^<>])*? + \/?>) // the matching end tag + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); + + // Special case for standalone HTML comments: + + /* + text = text.replace(/ + \n\n // Starting after a blank line + [ ]{0,3} // attacklab: g_tab_width - 1 + ( // save in $1 + -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256 + > + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement); + + // PHP and ASP-style processor instructions ( and <%...%>) + + /* + text = text.replace(/ + (?: + \n\n // Starting after a blank line + ) + ( // save in $1 + [ ]{0,3} // attacklab: g_tab_width - 1 + (?: + <([?%]) // $2 + [^\r]*? + \2> + ) + [ \t]* + (?=\n{2,}) // followed by a blank line + ) + /g,hashElement); + */ + text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); + + return text; + } + + function hashElement(wholeMatch, m1) { + var blockText = m1; + + // Undo double lines + blockText = blockText.replace(/^\n+/, ""); + + // strip trailing blank lines + blockText = blockText.replace(/\n+$/g, ""); + + // Replace the element text with a marker ("~KxK" where x is its key) + blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; + + return blockText; + } + + function _RunBlockGamut(text, doNotUnhash) { + // + // These are all the transformations that form block-level + // tags like paragraphs, headers, and list items. + // + text = _DoHeaders(text); + + // Do Horizontal Rules: + var replacement = "
\n"; + text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); + text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); + + text = _DoLists(text); + text = _DoCodeBlocks(text); + text = _DoBlockQuotes(text); + + // We already ran _HashHTMLBlocks() before, in Markdown(), but that + // was to escape raw HTML in the original Markdown source. This time, + // we're escaping the markup we've just created, so that we don't wrap + //

tags around block-level tags. + text = _HashHTMLBlocks(text); + text = _FormParagraphs(text, doNotUnhash); + + return text; + } + + function _RunSpanGamut(text) { + // + // These are all the transformations that occur *within* block-level + // tags like paragraphs, headers, and list items. + // + + text = _DoCodeSpans(text); + text = _EscapeSpecialCharsWithinTagAttributes(text); + text = _EncodeBackslashEscapes(text); + + // Process anchor and image tags. Images must come first, + // because ![foo][f] looks like an anchor. + text = _DoImages(text); + text = _DoAnchors(text); + + // Make links out of things like `` + // Must come after _DoAnchors(), because you can use < and > + // delimiters in inline links like [this](). + text = _DoAutoLinks(text); + + text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now + + text = _EncodeAmpsAndAngles(text); + text = _DoItalicsAndBold(text); + + // Do hard breaks: + text = text.replace(/ +\n/g, "
\n"); + + return text; + } + + function _EscapeSpecialCharsWithinTagAttributes(text) { + // + // Within tags -- meaning between < and > -- encode [\ ` * _] so they + // don't conflict with their use in Markdown for code, italics and strong. + // + + // Build a regex to find HTML tags and comments. See Friedl's + // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. + + // SE: changed the comment part of the regex + + var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; + + text = text.replace(regex, function (wholeMatch) { + var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); + tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987 + return tag; + }); + + return text; + } + + function _DoAnchors(text) { + // + // Turn Markdown link shortcuts into XHTML
tags. + // + // + // First, handle reference-style links: [link text] [id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[] // or anything else + )* + ) + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad remaining backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); + + // + // Next, inline-style links: [link text](url "optional title") + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ( + (?: + \[[^\]]*\] // allow brackets nested one level + | + [^\[\]] // or anything else + )* + ) + \] + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // Title = $7 + \6 // matching quote + [ \t]* // ignore any spaces/tabs between closing quote and ) + )? // title is optional + \) + ) + /g, writeAnchorTag); + */ + + text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); + + // + // Last, handle reference-style shortcuts: [link text] + // These must come last in case you've also got [link test][1] + // or [link test](/foo) + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + \[ + ([^\[\]]+) // link text = $2; can't contain '[' or ']' + \] + ) + ()()()()() // pad rest of backreferences + /g, writeAnchorTag); + */ + text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); + + return text; + } + + function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { + if (m7 == undefined) m7 = ""; + var whole_match = m1; + var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs + var link_id = m3.toLowerCase(); + var url = m4; + var title = m7; + + if (url == "") { + if (link_id == "") { + // lower-case and turn embedded newlines into spaces + link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); + } + url = "#" + link_id; + + if (g_urls.get(link_id) != undefined) { + url = g_urls.get(link_id); + if (g_titles.get(link_id) != undefined) { + title = g_titles.get(link_id); + } + } + else { + if (whole_match.search(/\(\s*\)$/m) > -1) { + // Special case for explicit empty url + url = ""; + } else { + return whole_match; + } + } + } + url = encodeProblemUrlChars(url); + url = escapeCharacters(url, "*_"); + var result = ""; + + return result; + } + + function _DoImages(text) { + // + // Turn Markdown image shortcuts into tags. + // + + // + // First, handle reference-style labeled images: ![alt text][id] + // + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + + [ ]? // one optional space + (?:\n[ ]*)? // one optional newline followed by spaces + + \[ + (.*?) // id = $3 + \] + ) + ()()()() // pad rest of backreferences + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); + + // + // Next, handle inline images: ![alt text](url "optional title") + // Don't forget: encode * and _ + + /* + text = text.replace(/ + ( // wrap whole match in $1 + !\[ + (.*?) // alt text = $2 + \] + \s? // One optional whitespace character + \( // literal paren + [ \t]* + () // no id, so leave $3 empty + ? // src url = $4 + [ \t]* + ( // $5 + (['"]) // quote char = $6 + (.*?) // title = $7 + \6 // matching quote + [ \t]* + )? // title is optional + \) + ) + /g, writeImageTag); + */ + text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); + + return text; + } + + function attributeEncode(text) { + // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) + // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) + return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } + ); + + text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, + function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } + ); + + // atx-style headers: + // # Header 1 + // ## Header 2 + // ## Header 2 with closing hashes ## + // ... + // ###### Header 6 + // + + /* + text = text.replace(/ + ^(\#{1,6}) // $1 = string of #'s + [ \t]* + (.+?) // $2 = Header text + [ \t]* + \#* // optional closing #'s (not counted) + \n+ + /gm, function() {...}); + */ + + text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, + function (wholeMatch, m1, m2) { + var h_level = m1.length; + return "" + _RunSpanGamut(m2) + "\n\n"; + } + ); + + return text; + } + + function _DoLists(text) { + // + // Form HTML ordered (numbered) and unordered (bulleted) lists. + // + + // attacklab: add sentinel to hack around khtml/safari bug: + // http://bugs.webkit.org/show_bug.cgi?id=11231 + text += "~0"; + + // Re-usable pattern to match any entirel ul or ol list: + + /* + var whole_list = / + ( // $1 = whole list + ( // $2 + [ ]{0,3} // attacklab: g_tab_width - 1 + ([*+-]|\d+[.]) // $3 = first list item marker + [ \t]+ + ) + [^\r]+? + ( // $4 + ~0 // sentinel for workaround; should be $ + | + \n{2,} + (?=\S) + (?! // Negative lookahead for another list item marker + [ \t]* + (?:[*+-]|\d+[.])[ \t]+ + ) + ) + ) + /g + */ + var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; + + if (g_list_level) { + text = text.replace(whole_list, function (wholeMatch, m1, m2) { + var list = m1; + var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; + + var result = _ProcessListItems(list, list_type); + + // Trim any trailing whitespace, to put the closing `` + // up on the preceding line, to get it past the current stupid + // HTML block parser. This is a hack to work around the terrible + // hack that is the HTML block parser. + result = result.replace(/\s+$/, ""); + result = "<" + list_type + ">" + result + "\n"; + return result; + }); + } else { + whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; + text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { + var runup = m1; + var list = m2; + + var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; + var result = _ProcessListItems(list, list_type); + result = runup + "<" + list_type + ">\n" + result + "\n"; + return result; + }); + } + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; + + function _ProcessListItems(list_str, list_type) { + // + // Process the contents of a single ordered or unordered list, splitting it + // into individual list items. + // + // list_type is either "ul" or "ol". + + // The $g_list_level global keeps track of when we're inside a list. + // Each time we enter a list, we increment it; when we leave a list, + // we decrement. If it's zero, we're not in a list anymore. + // + // We do this because when we're not inside a list, we want to treat + // something like this: + // + // I recommend upgrading to version + // 8. Oops, now this line is treated + // as a sub-list. + // + // As a single paragraph, despite the fact that the second line starts + // with a digit-period-space sequence. + // + // Whereas when we're inside a list (or sub-list), that line will be + // treated as the start of a sub-list. What a kludge, huh? This is + // an aspect of Markdown's syntax that's hard to parse perfectly + // without resorting to mind-reading. Perhaps the solution is to + // change the syntax rules such that sub-lists must start with a + // starting cardinal number; e.g. "1." or "a.". + + g_list_level++; + + // trim trailing blank lines: + list_str = list_str.replace(/\n{2,}$/, "\n"); + + // attacklab: add sentinel to emulate \z + list_str += "~0"; + + // In the original attacklab showdown, list_type was not given to this function, and anything + // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: + // + // Markdown rendered by WMD rendered by MarkdownSharp + // ------------------------------------------------------------------ + // 1. first 1. first 1. first + // 2. second 2. second 2. second + // - third 3. third * third + // + // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, + // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: + + /* + list_str = list_str.replace(/ + (^[ \t]*) // leading whitespace = $1 + ({MARKER}) [ \t]+ // list marker = $2 + ([^\r]+? // list item text = $3 + (\n+) + ) + (?= + (~0 | \2 ({MARKER}) [ \t]+) + ) + /gm, function(){...}); + */ + + var marker = _listItemMarkers[list_type]; + var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); + var last_item_had_a_double_newline = false; + list_str = list_str.replace(re, + function (wholeMatch, m1, m2, m3) { + var item = m3; + var leading_space = m1; + var ends_with_double_newline = /\n\n$/.test(item); + var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; + + if (contains_double_newline || last_item_had_a_double_newline) { + item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true); + } + else { + // Recursion for sub-lists: + item = _DoLists(_Outdent(item)); + item = item.replace(/\n$/, ""); // chomp(item) + item = _RunSpanGamut(item); + } + last_item_had_a_double_newline = ends_with_double_newline; + return "
  • " + item + "
  • \n"; + } + ); + + // attacklab: strip sentinel + list_str = list_str.replace(/~0/g, ""); + + g_list_level--; + return list_str; + } + + function _DoCodeBlocks(text) { + // + // Process Markdown `
    ` blocks.
    +            //  
    +
    +            /*
    +            text = text.replace(/
    +                (?:\n\n|^)
    +                (                               // $1 = the code block -- one or more lines, starting with a space/tab
    +                    (?:
    +                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    +                        .*\n+
    +                    )+
    +                )
    +                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    +            /g ,function(){...});
    +            */
    +
    +            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    +            text += "~0";
    +
    +            text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    +                function (wholeMatch, m1, m2) {
    +                    var codeblock = m1;
    +                    var nextChar = m2;
    +
    +                    codeblock = _EncodeCode(_Outdent(codeblock));
    +                    codeblock = _Detab(codeblock);
    +                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    +                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    +
    +                    codeblock = '
    ' + codeblock + '\n
    '; + + return "\n\n" + codeblock + "\n\n" + nextChar; + } + ); + + // attacklab: strip sentinel + text = text.replace(/~0/, ""); + + return text; + } + + function hashBlock(text) { + text = text.replace(/(^\n+|\n+$)/g, ""); + return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; + } + + function _DoCodeSpans(text) { + // + // * Backtick quotes are used for spans. + // + // * You can use multiple backticks as the delimiters if you want to + // include literal backticks in the code span. So, this input: + // + // Just type ``foo `bar` baz`` at the prompt. + // + // Will translate to: + // + //

    Just type foo `bar` baz at the prompt.

    + // + // There's no arbitrary limit to the number of backticks you + // can use as delimters. If you need three consecutive backticks + // in your code, use four for delimiters, etc. + // + // * You can use spaces to get literal backticks at the edges: + // + // ... type `` `bar` `` ... + // + // Turns to: + // + // ... type `bar` ... + // + + /* + text = text.replace(/ + (^|[^\\]) // Character before opening ` can't be a backslash + (`+) // $2 = Opening run of ` + ( // $3 = The code block + [^\r]*? + [^`] // attacklab: work around lack of lookbehind + ) + \2 // Matching closer + (?!`) + /gm, function(){...}); + */ + + text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, + function (wholeMatch, m1, m2, m3, m4) { + var c = m3; + c = c.replace(/^([ \t]*)/g, ""); // leading whitespace + c = c.replace(/[ \t]*$/g, ""); // trailing whitespace + c = _EncodeCode(c); + c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. + return m1 + "" + c + ""; + } + ); + + return text; + } + + function _EncodeCode(text) { + // + // Encode/escape certain characters inside Markdown code runs. + // The point is that in code, these characters are literals, + // and lose their special Markdown meanings. + // + // Encode all ampersands; HTML entities are not + // entities within a Markdown code span. + text = text.replace(/&/g, "&"); + + // Do the angle bracket song and dance: + text = text.replace(//g, ">"); + + // Now, escape characters that are magic in Markdown: + text = escapeCharacters(text, "\*_{}[]\\", false); + + // jj the line above breaks this: + //--- + + //* Item + + // 1. Subitem + + // special char: * + //--- + + return text; + } + + function _DoItalicsAndBold(text) { + + // must go first: + text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, + "$1$3$4"); + + text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, + "$1$3$4"); + + return text; + } + + function _DoBlockQuotes(text) { + + /* + text = text.replace(/ + ( // Wrap whole match in $1 + ( + ^[ \t]*>[ \t]? // '>' at the start of a line + .+\n // rest of the first line + (.+\n)* // subsequent consecutive lines + \n* // blanks + )+ + ) + /gm, function(){...}); + */ + + text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, + function (wholeMatch, m1) { + var bq = m1; + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting + + // attacklab: clean up hack + bq = bq.replace(/~0/g, ""); + + bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines + bq = _RunBlockGamut(bq); // recurse + + bq = bq.replace(/(^|\n)/g, "$1 "); + // These leading spaces screw with
     content, so we need to fix that:
    +                    bq = bq.replace(
    +                            /(\s*
    [^\r]+?<\/pre>)/gm,
    +                        function (wholeMatch, m1) {
    +                            var pre = m1;
    +                            // attacklab: hack around Konqueror 3.5.4 bug:
    +                            pre = pre.replace(/^  /mg, "~0");
    +                            pre = pre.replace(/~0/g, "");
    +                            return pre;
    +                        });
    +
    +                    return hashBlock("
    \n" + bq + "\n
    "); + } + ); + return text; + } + + function _FormParagraphs(text, doNotUnhash) { + // + // Params: + // $text - string to process with html

    tags + // + + // Strip leading and trailing lines: + text = text.replace(/^\n+/g, ""); + text = text.replace(/\n+$/g, ""); + + var grafs = text.split(/\n{2,}/g); + var grafsOut = []; + + var markerRe = /~K(\d+)K/; + + // + // Wrap

    tags. + // + var end = grafs.length; + for (var i = 0; i < end; i++) { + var str = grafs[i]; + + // if this is an HTML marker, copy it + if (markerRe.test(str)) { + grafsOut.push(str); + } + else if (/\S/.test(str)) { + str = _RunSpanGamut(str); + str = str.replace(/^([ \t]*)/g, "

    "); + str += "

    " + grafsOut.push(str); + } + + } + // + // Unhashify HTML blocks + // + if (!doNotUnhash) { + end = grafsOut.length; + for (var i = 0; i < end; i++) { + var foundAny = true; + while (foundAny) { // we may need several runs, since the data may be nested + foundAny = false; + grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { + foundAny = true; + return g_html_blocks[id]; + }); + } + } + } + return grafsOut.join("\n\n"); + } + + function _EncodeAmpsAndAngles(text) { + // Smart processing for ampersands and angle brackets that need to be encoded. + + // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: + // http://bumppo.net/projects/amputator/ + text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); + + // Encode naked <'s + text = text.replace(/<(?![a-z\/?\$!])/gi, "<"); + + return text; + } + + function _EncodeBackslashEscapes(text) { + // + // Parameter: String. + // Returns: The string, with after processing the following backslash + // escape sequences. + // + + // attacklab: The polite way to do this is with the new + // escapeCharacters() function: + // + // text = escapeCharacters(text,"\\",true); + // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); + // + // ...but we're sidestepping its use of the (slow) RegExp constructor + // as an optimization for Firefox. This function gets called a LOT. + + text = text.replace(/\\(\\)/g, escapeCharacters_callback); + text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); + return text; + } + + function _DoAutoLinks(text) { + + // note that at this point, all other URL in the text are already hyperlinked as
    + // *except* for the case + + // automatically add < and > around unadorned raw hyperlinks + // must be preceded by space/BOF and followed by non-word/EOF character + text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4"); + + // autolink anything like + + var replacer = function (wholematch, m1) { return "" + pluginHooks.plainLinkText(m1) + ""; } + text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); + + // Email addresses: + /* + text = text.replace(/ + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ + ) + > + /gi, _DoAutoLinks_callback()); + */ + + var email_replacer = function(wholematch, m1) { + var mailto = 'mailto:' + var link + var email + if (m1.substring(0, mailto.length) != mailto){ + link = mailto + m1; + email = m1; + } else { + link = m1; + email = m1.substring(mailto.length, m1.length); + } + return "" + pluginHooks.plainLinkText(email) + ""; + } + text = text.replace(/<((?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+))>/gi, email_replacer); + + return text; + } + + function _UnescapeSpecialChars(text) { + // + // Swap back in all the special characters we've hidden. + // + text = text.replace(/~E(\d+)E/g, + function (wholeMatch, m1) { + var charCodeToReplace = parseInt(m1); + return String.fromCharCode(charCodeToReplace); + } + ); + return text; + } + + function _Outdent(text) { + // + // Remove one level of line-leading tabs or spaces + // + + // attacklab: hack around Konqueror 3.5.4 bug: + // "----------bug".replace(/^-/g,"") == "bug" + + text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width + + // attacklab: clean up hack + text = text.replace(/~0/g, "") + + return text; + } + + function _Detab(text) { + if (!/\t/.test(text)) + return text; + + var spaces = [" ", " ", " ", " "], + skew = 0, + v; + + return text.replace(/[\n\t]/g, function (match, offset) { + if (match === "\n") { + skew = offset + 1; + return match; + } + v = (offset - skew) % 4; + skew = offset + 1; + return spaces[v]; + }); + } + + // + // attacklab: Utility functions + // + + var _problemUrlChars = /(?:["'*()[\]:]|~D)/g; + + // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems + function encodeProblemUrlChars(url) { + if (!url) + return ""; + + var len = url.length; + + return url.replace(_problemUrlChars, function (match, offset) { + if (match == "~D") // escape for dollar + return "%24"; + if (match == ":") { + if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1))) + return ":"; + if (url.substring(0, 'mailto:'.length) === 'mailto:') + return ":"; + if (url.substring(0, 'magnet:'.length) === 'magnet:') + return ":"; + } + return "%" + match.charCodeAt(0).toString(16); + }); + } + + + function escapeCharacters(text, charsToEscape, afterBackslash) { + // First we have to escape the escape characters so that + // we can build a character class out of them + var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; + + if (afterBackslash) { + regexString = "\\\\" + regexString; + } + + var regex = new RegExp(regexString, "g"); + text = text.replace(regex, escapeCharacters_callback); + + return text; + } + + + function escapeCharacters_callback(wholeMatch, m1) { + var charCodeToEscape = m1.charCodeAt(0); + return "~E" + charCodeToEscape + "E"; + } + + }; // end of the Markdown.Converter constructor + +})(); diff --git a/static/lib/pagedown/Markdown.Editor.Icons.fw.png b/static/lib/pagedown/Markdown.Editor.Icons.fw.png new file mode 100755 index 0000000000000000000000000000000000000000..80b998a44c0be9cc7664165e43961b97f442bb8b GIT binary patch literal 51360 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 literal 0 HcmV?d00001 diff --git a/static/lib/pagedown/Markdown.Editor.Icons.png b/static/lib/pagedown/Markdown.Editor.Icons.png new file mode 100755 index 0000000000000000000000000000000000000000..3c9ff7171a0c202a634318e7fc578869278c01f4 GIT binary patch literal 52640 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≀I^VWo?!B|8c z_yuJGHJ32C5u=>FK@bZ;C^=qkOmku=`=dlA+afjr`L42-$f@CugbC9{Cm}kudp&$o zi(r1N{?%>jON%x4O7H*~M8MtGM z(lL13B408KE(X{gnXBeKLC_fWSnGKpjKj~JqS{h~61Fy1j=wWSknJgC;`46AK zA;0o+#K~0DM*Ie3Pp$mn%v$s8>AKnDC5u`Ly>ZLTdhDKjJ_#r}2Ezv{@B)SmSp4s9 zJR{pxghKyG_XZaCfPwNn<`wYRHo6SVrbd>2KzO0U^1PnaE7$QVVe8Ml~<_ z_{?&{s5N+@MP&Bcz?6xj3AD}Z7!D%<0E~fW!KmtQ17~Rlqz*DCUU*W)q|#Jr89g1tylGWJZJmMg=-xRB#o;;2x!&{8dAw%v4cDyz@}OCQ&4Z z1S?6j--}2XO>Oz#DK_Dl?+$2LsqQ^cB%l#zE#JpX*rJuhzB~it5CvKwJ6%8;WUGms z&q=AT{7EJ{yFn29LkfvATz`U=e~2(Ts2ZmYG&~A`$&wR+q1H>Ijg&>KKa?W#i^;m+ zMHC(R96#CT*w2Hs2Kp_F;nFRnus(XqWHE{%r#L>tH3AhODxm?l^f>SW@yNRxD-dU1 z`Bs3csRzDVXtq#zGZjB-EC%;+Qt?Dhi{jsCE(X6xzsNh2~wo zF_%tbT%3^4{4b5 z*AX<)#e@?>h9Ue9gKPUt<{;_8ogF$*bDWEuM8QC>YhC~Fu#7|_8qEmFs2+#A$#6CU;=$U06G+`mHoBgs{jCC zMiaXr*d~}CE@qFJI6mD)*%%Iij4^bQ1luFx19bF2=(@6K!} zzx|s79E}=`LpI`M%HaVq1O}xzO*EnfvJ@81=z?h6)E7V!7<2(nv&pr_&a(;6nTnYc_nva^3!wU>{Fo&1AZn1(DGik2 z(+j0T2KIgI5g(AKDC7jvSrt7@5X4BoNfZc;C|U~XG(BHH;i@M#euP7i`Oo&52&cKq4B)-Z3bxTNT4?z7g z)^7&y@lYC}qGh_Zz{BM@gyfB8?Uy%fjhGUS(3F>=VFW>s$T|GNe?%3I<7EAd6e+3o zHcd-V@evR*j8a$4ay6>yD!n7us5W~+q|9R)O2p{jHvzUIIOgA=&aND?W_mKwMP7UB<5EZ^TX!i7LgOO-<0$ZVgB|0$kbiXu2$XnidQ!g3Vst|PpbijygC ztVTXG3j=(P7;yL}KD`h!KZzMzNDtzG7P^KJHFB?43ENOEG)1nq2kUFEG3Ibn2m@7! zTZudp^FRm!)KDn{O0gQ#gx)t}`J=_Q38Jt^Xhw;AkbzyrQdNVy_2NX;Dh3NAEJXyT z7OO*A%H$ZBYN;??M4Eo2&vc!4j8PM7)lwpUwFZYsX=#9D>_RP0Wp7nU#vn#}7<>Fv zhz%CHI-qKdCb3zDtzNvgR8iAx49w$+n*U|LZDkIA_p=(QleGEW5{?TWFVRj)j2V^= z{)D(o3bE} z8&RzoHgy@B$vWo5Lkgd2NFPKp=A#-knUGAvctl2~Uc9arP8@I>!vS0Lfy>8aDSK2B z;6ldZismvw)Yqj;6lP1B=s}$L4o%xb_L69>OE$N)<->I#e1X{@!(=dP0#UhC#KBw< zfX6paI1|@AYU(6;*VDs5t1OTuk50(U781Bgxi&+|^yrq0@*bXb2uPCk15zU!Jr5-o z;!v>trIAt-ztC@InEQUgi?kLU}$fRlb_l4$fv+n8E&jB2vz z(Scro&m0T($_%5YCx>v7sFPFAOon&)S4v5Xqk$>vjIbniU7!lO{W6v6f#m0-W$TjS zotg9xh<;80BhMv#J%Z0Tl$8@^Jt#3r9E%fGK)7ZV0OJf|8e`s`mhfdjWo{B@VJ{qa~Rg)G6N2j?rK!bdsa42E~h41ly8x1{$~So17&7n~3bG7ys|xh~6*n zQXsx(&R#^l2nj}6+ld!wBQ?O*%$?Voc92u7MvWB0qX6WMRxrQ}ER4;;3=2YprE@jN z(!Sg`(rXrI^s9_=JD}mfr}i|HB11vlpNA-dH{o9l2@=8V9z~Q>Mo!9(Mh5hK_Wgqm z#YY@D_wtg$+V~e>M!+F>(ua`ENWg$MF<%TBA>q0!#zWu}927dMXfw=pqDk)+@3 zVb!2Vp-){`qekuVhGB|Xhz}gx2d=K!%7HPV{>}ttMNLBpa^UwD#!eiSs#$+24xg+x z$wthlG#=p2OhtZIRLIngc>qrKYP8I$M{m8gX`6vl;Fan_T`5%)L7zgf!*P$z8 zOuN@$1hQzwzGp#Nh|`D|2e-up*gmr*9 z1~CyPPenRm{cB0g`F89dYN&@15p3M{NOVq{V)6o`E>lnXkQ~oGV)Dq`&BPjuInO6Kt&!GnE~1 zxUMP{sUyooa4MU+zG-a?8MYe5vbYNP6r{;5yEfaG&Xr`0l@K5$QR9negbq?XYK=^H ztQzZ%9r;pAn0IEcqFz%rn)dQ|)D}HX{ab|pVALx1?9_ z)xmHpP9dVT=fhmgp+)z;h2M5%5MJMlg;(LS&66>`n>UK2G%H?UoKyjS16(`euVO>E z;}5?6V}MD^av0Tr3s>UytfcX=b|>eW@t&Sq;dYwyb5bL9e<6Kv$l*eN`Vxw8CY#OD zB--61dn0yXEs`}%tL5AQ*%sL8@}s*PWwI~la$CpZ!;0yC_Lh4r?IXGl3b?FAzCFrP zuV3bifVR)N%>zpk`=)aL>DAwBa8prDeXpgfV&PwR$jmDMU!i@RmBC|2n-9ZP(Xg%R zui{gkMPKXabB#HCz@rA>5B#gn9BwDWDR2wNIIb_c8dYf3T)(T?E_CrgSZvyUP}Dig zt2>uB*enTa_c7K13~?<|A`Ec_Qeu?hk3`8bamU0smTPvoEnFDIpCW|NIM&eD_?6zi zZT^KK>#8d_{n7?&^Qr*jhS0<~3by=gQ5fkBKY+n~8&0}BTRP8UBfy*bf{kzS+3G^} z;^mN!r9!bXkI$nQMW$4Oj$cSA;TfA>eGjK)JJRsN3r1=mRMe5blGo8g1^YL$Lr96i z;>KDH!Q7sT5T-1eV-q|$+BUV@_$q0qWqb|noPc3=<>;>_puHwCC?s6%7PeI+5169y z%792yMO$sraO~`OS!4?4N?ahz^_Ikdj%y(Nxu9lF!JQgb7D*-!OS3GON)CXTzGd{S zOH-Phv+(3lbv{5kS@WFHJpFm!Z~h|I93rRMm>U@h za(5;WJjg5vcc8ZWbs0lbo|xtI8dRcAse}(2&_n1tfsv$Jth{KE{~cG^nSa0Wap<;l zab)_o6s;7@W<{t9!5rtDEG#7@_6N+Z0G;v=NQhc~XyY%zPsUtr5p16+gIvK*+BUb8NKki$Y5D$bWVu<2y+RiGD!Q%^9;`^#xFaxDu{jJ{?26m_a2U zHY4$AwOR5jYQ^gyrNAooa7uNf*5f7nZ=9*q@=@Fq0Y^sGQ26+u>N3TivcJ5Ytm!*Q zKSz_Q86+u?-A$49h!d*T(~hc4(mR}r=Sqh1-J2(I;i$c2*>Yz&!oA;y4k);z7Bu~5 zd|zExSLpvwP?bI3HF?YH7d|(%$EICrAYD>=-na1eot-9T5jZV>Z_hx+jrq zJMCl0Et^Y5%i=~dQ^8_c2F$U_E(@x77XXm*c!DJ}mVm~SwjZ-JH*fWS};QV`*{taW!v5{UIdkqvc;YVpz zk)s{H-GmFhIqS|`&nwhDZeI?UE1ZZoBC{G^nldgHJi=LmZaueB)+JGj?IaxpTU4zV z$61vG=7~V7M}@y6ZG3R~WMm|C1l7Tp=1&Jv_RDut^7bjAc0fp*bfH&G|Fu=q$55KG z+bZo0{Z4bD3u{THH0q;dhzN{gD6cJ~Ev!{=Dw>(i2_h59uMnz_z8wbLm8PQHKTJtj zIV51Jv`O1yP^1a&T{R@oGmPb7D%OL)Al+_c52}f%1B#bL!G;17pT;umc8$}h7poZK zH^}zZnwYgv()~83MUuale1P{QjroPcX_I^FLU3{+iqP^VhFJ;2rr?hNnVI?I29`OU zlWfhL&EH+`w$G^Uo#Xt^*;(aHPbDDVO)GRU{2H#1YEj&D}}{}*`5SYIw+)NCir(5W<- zL87?5a){u7MT8uon9O%wib^K6bPAc;>G+nw7RyHn&sR0bX{_u_Qq7!NsMY-DRH=^c zD0RLL@*dq0OWH|_=_H-uOje0NvaTh_0{-$>?##1S9DzspP?4l}csPm5LJ2mR>5e{v zQbu~wQRXxq+>5Ft-MYus-~ubAK%Sq)bQsLiq_Ld(p^grW3o(3$d}^`e)O4xa5q>t_$cZ=R=2o1ldLjYmOp5E9voF48fq=v=m+~B! za@r+9X^4(Gr9sc2FZ8|kjm|?p=MtA77b7W~48VzTd5RQI_ishRLh}$AU+_|Pv59op zTyQSX^6iA8Z%N=b=*iwWqDVEjm?Im*loRvHV%A@T`-i|_$_~lDDpRK256i)CXb5#7 z%fhJS;vugU(;qf1hq4h$H;N=6nAL2ZP|Jc0Sun91wL>l)Q2M(Rd!Iymn~WK{TMc1X za-0m7&4811!2J-5t_uZGp;AqJKFFQ7^$~I=^FP?{+U?FN;yM=LuCWI$vIuG@W~W%9 zv*-UX^-!m1Y@+hS%!;xG88;em`B!lJ%NzWVp?6IX2y6`jJy&5G(7_!zmKmm&Y3)6R zhSoI*>16rIH4osn_Lrn24_E{{BE?@EVIq$mg?O*%-EE+U-}6-7j(_YzbDq)YDvq=X)d5_;&N1Og!t zl91%$Th9M{-#z!7_ndq8v!7&=nOS?*%%0g(ezSJm)9L^(mx|HOQ41f%?{4KrNke$m z0vX>8I7%(Wd}wN&wYwZMk$AwQSQhDg1sUM#5L0H}Bj2@l>jy(;5yx4Iwswx2Y4P_@ zd@Qt{DyXbQp+S?)IwYNxMaAOV@8_lIxi5~pCo=9x@eX12?dg)IwD0m|e~{S8?2-{b zvGvp-m8`urKSfvi-#1_fGtu9_SI`%#<$l*KF1cQx{16l6t!-lJVZEMY#q@pj?#(ay z+dl*(#0r)dA8@K)JgUjQh) zI(>NAG!pB{E9F#^m7@B>JlMJIw%_ktQz!v(0_%+I{dNArnd_3WCThG_CBPeZd3`t8 zv_Z5y(lu;tZERF0`*BwzsPb@x;|pov(4H8w@#qkr>*Y-NOb5;)c&MeFtG@8O|9J|w z`eMa8Av4CmWGCVqc{Y^u88+>qaG0}d8-0QN*hYEW0@$PN61H;ZzW^o|b0W~n!=CGz zAvFFdub!&IJA$d}&iPFNyD$3EsNYAm{L{KIcw3eHt8Sy z;?08c?_=iN-l}6WGr!R(_|#)@&K#97f?uwmd1!BFb8M=_icWgKOT7AawN`R5Z<6q2 z2uA1qN9SMDVepBGdzhN&t`g&88s^QSH$?FZx-(uwN zTyk5V{WLuVg*}L?gzFWGwl(flGkk56fB~7Cd}VMu*9s(9!%REupazgJ*JgcDhG)C4 z3kPw}KC#%ON>6sU@XawNx4sMZ=E=r{9-Sb zF)dyV?c=S)bFyc7a`eDwgr3iAs){Q&dF5_Er|i-X*y7s~dD-?1H+~J@U7fqDcWRkJ zfJ5@$F|Yp(dW%8RGnk{LBJVEB#q1pnm3%;Q%Ntx`6dT;&JB?ENJAAr~(u>0kH^!$8 zFS-=HrFf_5BA=GE2ByS_pB=wbbd_x$QIAMv@^a_z;D;pf**G((H(N2kmvP7s_thRR z*uRtYB$iu1H+})9VF~TBR%5xy_UPEk-l2 zfcu0AO?5?eIZv^MlI!Ybt8*;sMzR$^19zlA+gxWoAMzPig=xAwD^4dmYT5Y7o$H-< z-mp}SV+xjH^fTwbK5%|4Q&a~eZ{w4UtDRSxN}jBTtwbR4bVJ^JuI&jjOr#uDzm6T1TPM*acz!=OiX{Ao z%;&Q4?jd>29#VnSrQ42-cW3xRho>}3ODpk!@=|j>-AL9*h~38qa2Jvu)oIk|`v4vK z!NLB#t_op$B9rfAaPfSE_3CbO>QPm|41wFk3qn*xWt3(Fq=uddyG?BF!7Wsukt91n72<**L=o1D-`;T)(6d=_3O>9i4!F49|>emmSIRo260*^TVz=@8?>v!X*KS$xZr2hsm$`6lf{2>(O8EjvF zp%ik2p06I(Z~cX@ohvJd_*wM_i1f{>3F(^tLnGSqmyA3Q{2Q*RpfzhV`zIA3*Qoyn zFKNb3fHh(&mNtkTW5a)dNY1zlla*utver}DQtcnKWmN9=F9ov7DJL1#&d4x->i37* z6#1J=ff(^T@RZ5?ld$c3zd_(rPErd&7v=K@%skDOfl*t9i#vdP*ea6dnpRQe+0*Uj zlRMRi=a0pG?jI|k3mMay3aw+Al~btx+VZ}sba2wr4Q7U}Fqqc1noCe^=v)8EqH4LW zM}7Xd;b~Ye7`*0HP_dKA!L(yWep%1@A-kt>+r_YoH+!`sv$4AUSq;JBP1MdsjeG`PisI?u z9-$wDWq;XuQuFtM^zk^C?Xn>#oL65csz}(*Va+p!{DS$k53hPN`Wullof-yz_b%4&UqE@gKk^RI=46-u;nLkb_3CRGVdf?Hl*)y}Twq z^<~y)vZ@2Bct6OZ5)*JuN{|w7ta%P?OMQtm}{{tjsX|EpBXX~VdLERkzaQHLq>Cj^oP-$ z?0U8(e;kon%LrYi=V6P||~a?tSyI({~3O8IneMnb1svkzc$ z%*)?>Y>xW+9Tq~1r-HtA6ly2l5Br+8NZg)ml)4VHKhV)7;Y-Qf1e7FxO&Gs47!wqF zy0wO2TfeBVym#@$bI^??3?Mqx{(t5fWdKOKYySTyo>AH9CBKYMvAv3a!$_~Rw~`^hmVZu-b(NsJ75;na_v+Pw zAYOIrh=P(U^(friptRCA)|mrrGvjw)UUe_q&xIwp0TRu8qt3a~UyKfe(o~Sw6?4CG zUiU>?Bku$P#EuA58+H`z#P7i_?voF_79KTvI6s6%*nZd3`?t-%$q66$ZH35-;1c(p zI8#F{VvoE%^9Y!{gf7S71Aa^T1v<+R*-5pBWw{7t&&qL1YeD;1vn1{@dyT|k*^4## z6iYa+Fn%ef;iXT~uvOr41qgB4;AR3noXT4>k|mvI0>LJS-ZK}tLaK_><{sMRpf`aX z5u(|7m8#X!iXYrD`%ow{W-8zV5!pW{#i7=qpnrVKnd_GA#-6t+ zvh@dv+}`&5AsebqO?i6n?#)1OhxxdeW?C*V>wQUm%$n;|Q!6egKlS}DSWsxaO?jqe z(HlTfswU5-li_!m`@-#9UaTH-MmC~6?}s;W;`zK}aRXu2#BN&u>!f7s z>bGX9kP!W2>!0kwlvaVM!#W6+W{CB1#oiVrYUlf*oV@%#+v@aH{opx5)jOkZN;S1; z^r5U*AC<;qr?u7h`}lh4eQFsUby+>9D{)INYaWpv{o%PS!VY zSYECm=`bpP5y{rAn)FwanG}^di;Xkxt^gaU+bi?Of0CZwiKTqCLy}gaM+o%0P9r6S z<>(xXh+o!W?{>UlIuX+|Ux=W$tPy&yH)@EJOIeC8k7MA1N+U6{qW-^Qycu5t8$|05 zRNF#ar@tB#TC~uOy@I#1q1T^z4L>$WGH12tFxkB+vVpS>)znyep`pgu_Or&*1NcBX zKu_A-z=}~QJ;vXP*QA~sSsO=zZO+a1x0GTj{U3KCM>;6+C&C)Nh}*jb*gjn!kk?R2`nxk1tNmc)f5`LYa-=y-&_W z3`5#}MTE8waNf@d(4OYJ|AW>V%|Tj2YGyYiWz?AuTi++g`U*6CD#B2#_Gr(`vt2=y>XPDjb)h-T?rtaUwN)-mS@k(4Qa4N1H{ZBO^=r-{HGsdD=!m z=2L2NTTq3>GF(SWX=l18d&ril>ZTudY7b+dMNf4nXT`NaH+yLe&+^{4=x??;UT&sz zs&mg;WT!V$8|_z9HcNtD#;~Ed#)U~*$E=zwU%Z=}w&LfH*R2n{wGbT}ah+!cf1KmY z(fLT;tg6=j;afko%Ud#h%y955 zxwI&2ad|^4qmFl;>miPe;ve;qi%UFT3bU%WAD{nM&hy@8%1es0PO29=ttmObkF7< z^q}!8=E?G~&@Brb~aUEj=T zFgk1F|J3@pii-M|0?(`aT#PLn6iq%q5K=c`3ZEG6?MBqC{^=sEp}eT0Bb1TERL^no zoFvl{aYyJ^<|S8Dzf*iEv||urRup@VOtzlV0~(y~>11N%F0J(5UnCv+l`!*3{P9Y%wUtsL`K2 zA8l*Jy(Yhg>2%$=`!(4qX)WQaLBYMVGo#hft;H$P>YP^MUu#*O-M`Fa7$(g0E~>Wq zu3zT)+Wg-)JDxpo{(P6Ew6NsOSVo1UfamSrt{w0dW*;=S@KRn4+hP#TO?gU*}0O3VppQHt_XZ%evu|4jf@axcFS^dzSmkJ+Gpc+qBo??ZE-8##aC{* zI!*VEj2M~QAHw>{Ei9Qp!}AOJb0c5?j-v_*aaqY~*+978cCmY<+@?PAV4#&MDD-ts z`5#ykLla^x>;&3@Ztk>OxmnSh~fYr9HU&Y`!-VwZvU$LN{Tt+p&E2Gx2Mvhx=7upnsv1G zfoMxvX>s?-l8CffOy(&r$pd>W{{b)+T*7`U=h`)=*5_XU&}0sOAb6x_HS1k_)IBxb ztHtYMH>Ms{*jRsiM62jOyP^O#mAH%vAHM7rVQe zsJ8;B15?X)%d0Axyjk;ZI*jP8hz>DOj;4`V=^0r(cOgGw z;E9Fva=o+N(0~Ws(6`iYkNOg1dqOGPc2YhmIt|@}yX&iF!eJcjIrZ3JAE=>gzl1ftW7M6 zbYR|d?bh$gxT`*g^YD4Rmw9TJG8g~eE?Q^2C~_{@?%K1S>Uy*v)3b{)Giq*c-d$&F zO!7}~DW39tpU4=UD%Z6lcKM;+GW7*9?7i{WXbt@&S1BgXHoBbzzmlV8Yf@a|uV*i) zC#u9b7uQuq%kprVP)%G^t+1lWHwk?`D*i#8I#zq=j`j_O$9Dz1J}N9z2hGxZ4&I1v z!xVOZe(JgfaO8UH_}Pl)_J!|1&jSnEH7dJq+!6oqRpHmA*RCIKym646O%O^eS$DQQg!;#W3IRWqp!h!U_F!WN z-KFjiUvB04Kcg!Wee-0auum@1aqS&#%A*IgH=MI&Vjhmw?C1->NlM$3W6N8Jc&9>DQ6Ll{eau}UuaOS#e z!zR-ou)WxP<5h$uRft~uxMb;Mq}Xlk>K`TC^dIgO(QEffxoZfWWe}L4xvUhE*$GX4 zjRiL7(k;`j4ahvz*XHr)D_|RVJN-GHNwx$-b%`~zP;c+doc!d?*N)a`zAvEfn(s|V+2uAnhD_Fvw_SkCxgsx_ynNZr$3_Lrs+b_n=v%JK2d zl$iGe23k6=$Py=3tg6}JTb z!f=}-E6*c_vCQRW4SSSgcf@Ot`@hnzL`yBXMl9By&nWUrQAf8*Jr(-^J3AVdQY#R9 zL-WYYdNEtjCFkeWGkLvLep(S7U3$zX*8;Qq#8NU2Q)+4WA0w}Q92h^1HEbWyinTwp~Ii!vT3Ma43qE>kt z&u%;u>DJAPdGKzp?8>88Sm6w~0+jEqK!MCXb6N=oDhN|i@!8QDp=_^|4g*C#p7bmA z1ySqFnW^;qHJYyoS~nEMZxwD(Z+tV*Nl5Qh(AN|ldi(Ua(o`oS*|8wGR8j}j=*5o@ zOz63n5SQ{{xhFWGQ$54z?VZOo|K>M4uDizk zWX0T1M+Wq0Sd+$cQ3Mm?EN*hCEV0zxbdp0x!>%m`)}~N#_vIM{fM~bG{qpwfT84D* zSFIl>4r%Ilh3JV?do+GqDtl3a32XeeT$XDZaQ92WN^8IQ%TPzV{>iE04PaMmQn`kW z^yTw>STOTPE8GuD-sals4XYlVRiRl-G1$wI=xM*EJ<_-FHmmWezR% z>(4t)R@P0yCD2~#RoX$1u`t@`43pvA|IlZDig&zT|P$1NFWcvBCBMj8q~x3^f) zt{=yl#ot4VF$pxW{g6pwUjv@K$Cyxb(9!20eei&8MF@TK!iAnv7l&epCjAY&j@6{~ z^Q=D}XLHOB`%T7L40eRRu2CG7H1Ruk+l6nWFZDyrfy_R-7gDT||i1?U3Z;E5pI=nYS?fOevAd!;I7;4h+?? zK?jM5!M8(Y#+NSLPv3r%cMf7L;?zF%9btc2>?CI^*PnL#Eej2jI`UTM;k~!on5=+n za8#!f-?==G zwm!c3aYv?BQ#zrO&(+Y2d9bauOy69{TB#yF>5rdZ4^zszRPsm zPZnmeeiealpZqx5hRW2P@n3*-Te#vqFi z_}Z%*QZqQxP}Yy;;`t43CzhRjj@);%`td|qfk7t~fpaQpya|7bn>Ev!m@X@EF#2#; zQf=JVB-8pueBh8Migg-BFY>YHW8^|nDcjbL;YaPI(yu(Vm#z6j%PHsDS_k%%gz5B3gnJTSc25_b;lC-aSET~I5uAHGaE z6Etcy=4|gWa%y$6{nQZWsnJYZ!@&!{(<-mL7ELrcO)*pC5aq23}HW@zBr*ZvS^$Ev(afaM5 zwqb8>dK#>o%G?!c8pbJoq(@IQ{@~TEC#V$TJ{B2wYQLc;{`qf5cENj@O6=dx**Ejp z7iv|_X`gEH^HdX4!pNyfQQ*4L|CK$_e?&etCHhPH0+rPHGacl93S?O1C2F+EZjsiH z;THeR!7b;tIe8LNvKaI#5k>JL&}H|@dM#MA0#*qha;a(0Y7+$h^*#GHy~c!v^O!!oA;4lJ2>#C5C{bSh{1p&9{cOq<&lBuSq7BNHRPwkywAsQRl8Sw6R;xWre zn`KtOkU~u@p(enPV;u0!;*r{b^aKBIF&6IT6y!-F$v!}TPn(zW0oE8T`66cspZ_!3 zemTocKh?q(UIoq^*0$uqIC%{mh*=lrndVq>>b01*pI+{AeKwx5v$L}o7Yz(9#6b=C zfg0I2TE~}jj&x7J=G=qwt?7w&IG|dx`#89{S{%uByg|-$U#Fmyfnb9N$3IX{c5XvW z2Oi;MNu~?@l~}}gZZ}HB-UD9VNxawP8$2$nBtf)l%;YiP-EfBQCKB)SYNyKaXd9J{ zZ3~xY|HOh<%OU}w30Si)M;x@ny2f$?PINoZK# zzRHC2(5Bp`bEXeod82Y>fKpIx+tq(8`SClP(qx-~sJ8_a%aLGV zm(Nqw9V&pkpWe1xAd{EBo9^YCPlj1>*1H1QJ?|V@w9Y}rH~7mA%IN{-lBjeiJ4yo| zj_*nsN5D|sK&@Hf^uhJ%1`4xA&KN0?qU)lj3nT-2C>)?8azfy8P8&zuu`H$k33G%; z8zCn>jOIAhv9(40S=#V&1sgcPmbt1!>xRtRHNde-A`@+CT zNc5mez1gvJiyM3WDoF|H*!z6>DYEu%SnjbbPAEP#66Cqh=Pl?x0G{rn)Z&OiGoBY)XnSJ=H zDfyqfP8*@gCHF{&Yf;8@eoSJ`6(h&kH7^SI%j|CTfY?$4W${p&l9Q8@k(W=O zot+)Xx3RT#@$-|uc<~}H?7>OH!urXHij$L*ySsb;>Z$Lq+p4_-0|UorX0pIwu!@Sx>c`^4%cf(L%}rqw6BBLX z{{DWD;~)gTh6dtMi;FiP8CGa@7Luf-du zvq@3C7Sx2t<*&OdLh3(@_pkc>=?`gKqEbJ9w(8(5$onHrQkWL6nNz8@2eC}!yq0 zL@r^5cihp;em5?6e^9tWw|tMQ&u()|4c_8*UmXS`KdJQaaBFeDa`pUZBse|2(?N=z zfj07=UyFbf!r}HB5|#%FUsl4EAea3M%BWn9ah(+7O?(VlHk?8xtxXge{mok@9FJK; zz$Ds%#3N<&4|$ifSfZ}ZFJdh?6>M|}p^+OSR~*?N8WRFj3zOkH2z;)sDW+`otnKwS zyMnyk+*!mo0tcoxI)c=V-m}e zlA~(hBVMbRzX)trR~z61;8@IZN%#cVAcBK}mS$`0A#bP<_z!ni3&^P?Aop@d{gf(M z$NI*piV;y|SmT+Gng?GDGbQ#KBt&--o9tXb&cBdtCkDQv);2xjM@8J8E$G!t&PNuY z_vm4Q5OAIpza_WpH&43N&R+uv9hFPl)SM_Dc4zBDA3*^agOfFmVfhNa6HGz2(b+;L763+_WaBf(sa5dKUD3BpyHPpd|L9`2>NIXAXPcJ2#KMkiqxcL$wI+ zDd_6&CratX*v)3{f8Hia3b^!n&@jfgQ?>D}sDk*x9bnHa_@~oacfk{wv)}8mr)K1> zX_mZ6zl`{w#Um9PjmzF>zS7?MQa6#25)7}IO=!c3HNI9?V{KBE*{(?RMZo?^SlzaP z`P%OjGCk6)03b{;S1TV+K8P9-Ch(j?1}D4=&pBhcSvLNxREIXJXy>{i=1@-$$>U= zu4y0quS1bM^J(7QCwTs~YpRqXoxV8|sN@W+GzpY0YW?MU>7oucq9^2>Ua5^idc@xn zJYqEdK0o#uBla$Mz77({0X{OND>{O(Z22x;PYJ3FH*saFAkKA8AqL?HZ&hqx;g1p5(dFY zj$QY*TwF}pZaQ{Iqfi#SiM6z_zK$aZGvGWl9smy-?87!jnsMF7ty&=cQX=;h z%6EUmk@xe}=yFBV^Y_))P$`VhGqGI8J$Xw9eY#F$tMrnST#QX`$job3gTA(c!ul5vl3B@F0#-nbK`#jBBWh z1~fhq5#xqJ*>8f7UQBpG(I8vMC@Bs$P6qcZ*I-mDb8Ft3W&S?o4v|#_-{7RAT4Uwx zcZf|h6j@s{lKtQ=3eANrp?EIXQYzO$wiI~v-(@d&P+rA}t3@s4y$s(x7EO1*OD>)B zp}Ybwm5$`OHCqi^hN~P(8;10hg=@=`Jqdr;1os1QSTRWnmGsP>gR8BoWWgw^pGFda zhIzwlE$pvH-r>R294BgP0c#c^<<4aTWfhg<&Y@iYtUMwcb2b1Ag$~{Pq(L|@FEwnL z#SSnBN@@^##g)3zeyCqZHqdVD!DeWpU+fY7?|$G zMga0=P@~GYsV8&e9}VcP`s@BDqjEr6zOw}b6Y$Lq;rIy<#eY@O&z%te*X0Sdn;P;$jA zNI`St@iTka0D5&SyhT=)kI_5rg^j$KVyyQTLMW_fEcbbCW_zH^+!{^L=%zKXj1XM8 zeOdQAx^9R7i9^^@5LEy-q=tyDI%ASKBkb%5hilKk`PT zYuu_8pPSpI>efZ5Wm?;xhHo@9YB0fZbL6qe{_|Pb3hF>^%%F`MlN#EsS9s9v@Tm^^ zKo^ja%{5Ou1**xE`0r-|Rj9YEo`yD8R?pJeP{%D@yKaPfUJzG#?kQ`#&#eg7Jz!b; zWvK5)XsRsx{#eE(Z~c{v8>K6OX1yb1Te}wbTEk|>g|$WtZjGZB{#smc+;&RoCQahH z2V&r5_aTbwqTj_&HA-~(5SX^==%rAB$#)1pys(zj{%=d_I_;jZoo{}bwG{&)k9A4n zUqk~Kgc$X07>Uoy9!)R_7R-s*O?|0`*j1t`_x)y6y|1c+bu~hEN?*n%t*Ong zIFcxjR#h^sst7u>zcf4pqPzEi+o(}%VC{^BEk2DUI8IukCKFxvnNiVRJ8!hAc==Kr z$(-Z|B~u@Q23CXv$AysJS3VCU0%0hKweH&4eLQ#2_nf?4XfqA>EOsSKd^yq=+r~9# z!tD%CwI2kdN8phIrnmEgL=YoEg!c`Z$x5`pXq$_mP&asaeX+9{IRR7s{8m$4IqbVGZ0l-#X701f@8&oylJ@M z%Nl>na95fWSsSgYgeZ8lYreul4Gzf5SE+^GP`O=t0@;$)FAZVVDsE(#WW(P{%jbFl z6?RvxBa29ekHpy}*bW*u8II;k`^D)to&Qu2q}J6GwwAhdQAMZZ@ZjLnNT)eD??tst zo6q!umoH`N(^L^Oyvn=YH@lujaM|xEs(k0`#K*zi7r72DAk>zNYZZkhenakp6y0~P z(4;(njRpJa)>8H#yNu((@>P?lhJ2A-GvKG)^2Zed2OOu{VIW!+%w;E#S$iaX!%SYp zeH)VT5j>*G6LdlwQVip_Z=iVi(nZ`0Flns$Sx`{_UxCwYV0YJds-K-ij+Mm_lesIAn{H)3Dv!zCIvMh>8K zsiGTmP9+T$u3pbL>r~SXYpY;+UxYU&EQT!ive30Soi<|X zvuSZAJVC<>nsi*L7{7|HU5YI+pG`G>Q-%)f6|XtrB4-YCIl?tXgUd@Xk4ZueQ=46b zOAGYoWQ5DWk(DXem+iNA=83#$N09T^au*r8g(Q-w&%WgmY#ns0d6?cDPm^cr_qM|1 z1O%s?X(3-mq-?5WccG6#<<_@hswW^8?_-pFg(H<+6GsS#J#JcICug6*=qjtA@^rI zv4>k?3LBdyViP+fm?>pPZ8gwwggcXiufb8sTeSe+;x2@{jo$~9{y{j9avn`J&PbF8 z_nQO;C=To5cJ_-=l@O<&?fgg6n`#YWFc06oa!gRz;qNE{&z1L+ob%Q14Txv)s-@l* z8(Ww@nE?B5N1w_l3R9J7Ynyo|`XkqXk;TVCa`f>#FZjB<5g6Mv!VY@D) z_$xjGi)RN};fD>{N@Xac*)K-QDwHycf9^?0diL&@U%+hYne2AY55NdQ>~7WX@VbXd zQD{L04lPqQa>bW2U(xT@7GT1gp`+Ng<0!Mt)nYW-IP;M7LhuOJwrhg~l?x)1Ugnru zHo}6iF+-se_YMkFzt35OPxMFlsoH;OrwS=)f7mj&kDQ*S9){x5Dp#TLo@DBmMd+s{ zsxs@u@PhFwSV*nmj6Kk(a@PfpPgzfFn~gtOB<;O4Sj;hz%ky*4qU;QLaJiCdWz+Zv zH|Ktw4g!8lU!-jA=jCnXJhivlJ8TfYVP#M zRHDx6ygH-|RbAVRVr7O{-KqFZ0xva>c0He;D;&{pds?z=J)9G6CzceV{ z{g*}jCjn12fOkIZQonNU(S(N54e{>wGu)4~E_^pPV(72WmEEP8aN36C_=ENj!= z)afZI#JDV^FZ8s>Vs%>rCr(DHx0`XJC#fk{WnU!BZUEVi2o%5r@Ye&=RWkagnK$Ez zs$_n%7!r7Wif#5esF992BXd98yt#7cr#|G&pC4sk;lqi_d;Yuz4z4=aEbmoTMpjgI zJq*4~;@B^w;Dwu>%?5IlA}+O44h~RNsE%pm+AR{!7D-&p$IRy;R5w5oB9_M#y8lGS z|H4b8oqDWQt!v{|?&+H1g}T3TxH96ui1{yh|9xs__H;$q! zl6y8-7W{kb{kQX1zfvU-@c!&kj{kNB-){#|$g^l@JTHa2MbIoREPZJjzqi!o3;8oo zQi7XlcOzLSmN0Y?U$?@RkM`2YO)d3tzkLNk6}lFe!qg&8rY3(sAn#JVKm_b29N~*- zC!Ju{20?_cM{ISK7-12~ku`Q?q9MtqVg^<<2zxM+OO{l^gUBa% zxZI%;MZ776Awf~*DFA6o?z@%=CwsABJx2rp4a_q%Yn(@@ReKrCaplG;OnOxlRL79$ zU&n&8T!r3URBKm?QllDhQk6zc_fyKqM))8!!$!!e9jH|N27IPoj5PG59}!BmK}-b) zbdCBy#Ph?G%xt&S^z3mN9U;e-NzlM%zS$3lv1|6;-R=yO#{tV^|5{$#;U_uXyhZkI zL}5$I1z|>l_qr`SL@d3!%zNfWXq0sSR@OiAGFY1(ib&S|0WYE)_)4NJG4li-n=Q{~ zxx~R@w<983076sY8iR@;XKk85lwF6ROpTeu_|=MIr!pM98kLYW;}qnPo!~ z7B*?IA7<&Bdz#q;$-OBM*!HSX&Oj#iF*yg0?;;1Htn~rb8o&a8I4S68XRYp+_t@`5 z5>Q3A&uLBnZ7M_>7~NVFu(lt(ZVxT44HLOTNpYcv5?%%%zeMy>&V1tJxVvqzMyCUH0 zdbHL0F=KPcJ@9t=0Swh}aCakmlY)HG4=SKo1nyxAij259g?DZetDK}pxCR3)7n7%Y zE;3M%Rv2`6#A1`ZfF$gk*a#N%+~(C_e^bcFk5eDX(f&1oq$)yqh!xQ^W+6F}c zg}e08&|Kx+`4*cU>$a7U@P%m__AcaM_a}wiU$FV(JU6OUB4lu&${=hN@V9)CVnDLn zFM7(hcOawGYLixuY4SK{=X%+WLszet_8-&ON9LYDPv2>2V?tiJ7_L$wOnDmqI=*KB zDc|1l&zssuqAvC2b`+N*=qkXg8)v7rl8Rso^eiu%YU8h1Jk1OjV#2O4rB+r&)(%cL z%UR#_({r!P*$eLk>b%|Hp)Z=9v*ruB>4G7sNURKi9kM&O?1W@PhcK|CjerYzorPYr zqH-++L}2Z1wn}Cc{7P5l`%CSf^0pL7uL&nkYX_3i%n^Rn2iOKjVzSTT@5vwx7Ab_%kckheCySXRazN=10yft~``=KZRp*864brDZmuv*ndAzsjOa_W*BKw)A8*hy>&}c^+6+F}#h*sfYkOqW)Q8 zKeDU?Y9ZO8d-BI7fqrFOXw>>-`BBKoJLCoKHo(H0L{L1Cz5dHu&h;mFWYA7eed0{u z5CF0E15vz(ySGPq9gnw~P2LmtlS~63YW7tjTPjS0LlO0{+3)#Di0Som%Jh1RTK&Yd zjwdB{_NE{CIP?egi*ktZhA+KhZ}*ErvE%67@*EmNp|BCoDnw8f86+QHfu2{X1|YAh_h7(5u8Y10E*h?ysDJ?9Qa)&r#AA6wb@DR;q$#pBR* z=vaJaq!Yxpu%`M;@MSx~me+es>*rL)QK%3z+!bF%QHtQrD;%7d&bOoX`s~iwpkV$ebLoW&tVwN~#$Y6G zB(xc>EQ&XQZR%oA?t}xTTz7bDDWkZ05dic>TsqDo0wx3V?J*>$P3H}qwgx6-MG8E^ zInJ>ZnSsRYPQIV(M|fY;`zUvW^6q#-v8K#FAipCeM2z@LtX!Qr6OuDVN$`%mGhJ^q zw}WO0*W{s;JD-3txMNA=@ibiqx%25=@@zk(FN8dQj5hnXD*ul*@$W68RI5byBi8f? z#(t9PmNCu9AUe$Fei zfVBu^CDjKi>F8Mm`HMK}CDnd&cT$L<#sVjF)2)T9MUJDlSt1&emY)phZ>9SS3vSx! zQ`~GKmXR{RY`GoauNp|h5?Z*4XU@mgsD=1qf!mIA2v_6rJ|gO7!)_Sw^Yx}>yo_Sd zE4h?YYfPgOTzR45fjde>Q4*Are^C7}Y{4R(hZIkIVA)h@g)1M}s{867&=7lRXZ)L& zV*I<-tH{(HaD67%?@AcI5zH)9bfeRZ0#H6(p2hpeA?KX4B6tG#7Y!S--`~qc%8N)? zB1<_%1C=I1T$HiL3WLDHh)v3T_`8N`(XPaWcZOs|Q2ye42QW2Evlq<&lD2X)10KAa zu=2ayKmJKS#IrKiq>R^mC;LS5H?k$*fvWEogDwfOr}FW4i-8~|zxu$S45P%jde636 zZqB33H8&IEW{|OcHZ)hVQgo`#IxH>|!QK2~Z(a-gu)-4VR~WIhVco(N79aAlHNOHg z9CDrB>0Do^2Bc^K+k#D8P@dQgLccAodIeQXFJlhv_Ybvzq!9Hgat=@j=Id_H+l3Z@+Z%m!;%X>U*9+% zw3DP^F6AM8z1wT_nZn)kSz=$GFU9_^_Rc*Vspmh;SmFI&2n7!cMZ**ol4Ek&5eA%ryzhzMG=WaA3wkAAu~du zJsUgia=pLk-aSL`5B1%HgW5?+NtwmP#$m0EjVo(wYlFhV*15U4HMX?4ya0*?VG;*_ zY6Cht-1p|R?DO<&h#0N((F8MR#l^3|?gsO9MTLcZ+$n;{)7|}XaD06Ho{eYURVgbe z`Os)iKpk>`#bR;udowaJ`uh9r8fF@FbaeXX^t9J2AwndC9`z)RK~uv)pyBF=p{*`w^p~qd`n&+NBj$)zuk*#V_svtGTUB&~1*K zlBb7+U;vh4@A1^(QQ*8X#XM+j_o#E3feO1bw0q%F|GiQ9fX%Vk*w`7*DFj(B#id8k zy$uX(|H5~fe=pJ3RiN)$tfb3PQsSF|Hvpfw;_9_6&o^JzNJ25Z_a~ZMLkFwp-`HrR z3Um^C2O>^c)|$N(y-=QfRIhyn$m0xgW(45RDWI;Or;K@-SPxP<$xNC;IKX# zRNY!AvUu}XqW<<893RzjZ4)E-tRGua7yNo0FrJot>RoS}L5H z5mZ)I?znZlSZHcsa3&--FE6pHtE;4>B=+>_)8N|K$u=}Fu(n_)mzI`3Yj2MO<9bD0 zaGAM;>O8y)@ObuS78X_DTJO0)2@FgUiA0)f;%kZl+O8+uLsvy%p`qOq6GqaDGvd2yg@F?&JQb++RM>%wG0)_Xi3N&31MD z1a5OPkd7(nYqHl*^S@87>~VqxJ}zeG(^ z-m`#2Vq#)FplloD<@L&wF#2VyFPXRqD$uGQNVyP zseTz&OD=q_o33QRj^*z2qHh?0c5H5(yBD{(JcI_c2OJlyvdW+WN1v(xcz}F_XHXnQ zd)z0O__mM4$tp?AL0QUn?Pdk=Zk+TNHH3V_sn3VbEvd$(F(^4nFI(!dp_3Add0BLI zTdKHm?ERD|oukO3a@?2|tLlF(XPuC(pg3%}dOz)E(X3H$Jw3ZM2S2lc*765M*jH&@ z%ucNxWu>~eoey@$S-RSCUNrCs=VCepcUO*EE{Xr4@0S_ndLhsr(lFUB;A;wJCxLJ1 z=33+!(5Ut+kUg`$94w`4flBqE`O>~&Fh|8|WfW?;_-4fqzbli*qmS*_VnsNEMl5FX zEnFYHJx|};;$CFBKK%_lha;j87j=)d2&-=Z1+vCoxgHaZWyRbJVzHwlU@*#E1K5Xq z8(H)3-_oA!lB_zb<`u9NuLN3vyihFf*f-QT9|{)x%!6EkS}plxv(&LI*H%!tc8>tH!AWa)9|lQQ(vE0rv#c1=mu)tvt8~isj^n>$ZCy9; zpy?Dk=|V@2Fk3b$Px6wvXUJjZ$%yfT2?eumGU0+~`80iDx-#C=!?}UN=lwBU|3~dK z_v%dbB=HoBaj2DD=@yh!#)=cM>xDp*0c-kZCUDI2BSHK1y36b-aLLH3Qdg>|} zpUJRmaRxum+!w&}&7h|GD?JGQ3E2B+C3mcRoDw<|tfcar-9Hx%OO%#QeV@g?up0jQ zZ(z{CGRf4B1~Ri`QoV8h1iFe3 z!Iuepl!xJ%1*4sW$2>xcS>PM$jF-RnX z!O&&s>r!aGNIg?iQzQyKmj+bm1Rn{cdoy(cgVjDI`BxrmLNJa-qS8r}K$W??-aeGW zbaPeJxrydqpDQC%=O+pb{!9<#h-7+Gk$So)tPAl3Yi~Ni0*%r`>!9>?^iX^C^e_g7Ae94O#D5a~DcpgA zClQbQpKvrr-xQ;V{v7^==ySLkat_27U_MfxtLab2|36@O+&nMpVOqe)%Yw%t2>}E$ zA&?#nGSmCt%d>z z0{8z&`cFyWNg%<6alyDC5r8Cy-38-VRyl}AQ6Bh zhTR3@f=H50FoGX7mN!M0Z3xlT`(?41R#lFcfq(I5r8Cy-38-< zL;#W)b{C8b5&=l!uewWm;V}cE2FNO`9_Hw2=(}yn^uUutM zjXjm!-#T@QTF%RkmP9SHKQbywmGRqt;6=PRKtbK*M^&e&$7O0hAEwLd6b{3XkuW11K{#jQjRSZO*Q@7( z@bx$sCmmy7o2GE(rI*BX5uQ6j4R5DL4M$yzlGpGU?)^plJMGe>UeSkjEO)WBb}6}! zMrxE?PiB@e+&0AybG9JQdN>(qw$}|B++J>PxADCZ_X3e!S#u_wfLzjh*pgC|ES@aJ zMgYgG1Q(#nU4Tob@*?={Pe+-c`Uky?RpGHf3#+rlDeiR?73pgUix3+`P{l^naz{ zwUoK3`Q5=RK+H_s3^AkLA9(5-pVvwio=A98r_J|r{DE=$&JrJwCi)|Pg?mJntYGI_ z8MnLgjm@Ge1r6SyNxq#^jSQi!MRJWs#7WOmXXEVmhsm`|uD{++ujLiJvFA=rzuxc) ztM9)!m8#BCn3SIsXFle2o+`v_&PDuclC(=KC+l5RLdwIgm@};1VN)HrG#5-_id_4& zZS*ptkxqYI#znl#%|taJow|~YxkI#RdUz|7VE6W6mhcfT5MdkKBk@ZAm20X-f%^9& ze6P|3(sWm7q_$m3l?f?Czw~I1Bf;*LOmne-?@UlGK5^+0@1{vcBrWyEhJ0L!hX2rg zMrS+PvAJyhjefGjD-TP`kG7X=rk`Ct-!9|svf=|`M4S5SWr=q|R!&)73FwUcm302) zR*-xJJ|^_ZD=vRmbQ@*v_}(aj_N z606LY&MY^ROXHs`#1m7a?naArM@p{n;~z`;v?y2&1f4nmlDCGsuAgi<9y2{LYn$IL z$ZR;6=bGho!ysAfLG_(L`+Elx7wx#}gxDt~qNtVbG77&zUu8>=8l5$HvQvYP1EfsE z!k4?BE9|eXU)&q@qTD@2otxk$@ghd%rdW;U?-q^"; + var imageDialogText = "

    http://example.com/images/diagram.jpg \"optional title\"

    "; + + // The default text that appears in the dialog input box when entering + // links. + var imageDefaultText = "http://"; + var linkDefaultText = "http://"; + + var defaultHelpHoverTitle = "Markdown Editing Help"; + + // ------------------------------------------------------------------- + // END OF YOUR CHANGES + // ------------------------------------------------------------------- + + // help, if given, should have a property "handler", the click handler for the help button, + // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). + // If help isn't given, not help button is created. + // + // The constructed editor object has the methods: + // - getConverter() returns the markdown converter object that was passed to the constructor + // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. + // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. + Markdown.Editor = function (markdownConverter, idPostfix, help) { + + idPostfix = idPostfix || ""; + + var hooks = this.hooks = new Markdown.HookCollection(); + hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed + hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text + hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates + * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen + * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. + */ + + this.getConverter = function () { return markdownConverter; } + + var that = this, + panels; + + this.run = function () { + if (panels) + return; // already initialized + + panels = new PanelCollection(idPostfix); + var commandManager = new CommandManager(hooks); + var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); + var undoManager, uiManager; + + if (!/\?noundo/.test(doc.location.href)) { + undoManager = new UndoManager(function () { + previewManager.refresh(); + if (uiManager) // not available on the first call + uiManager.setUndoRedoButtonStates(); + }, panels); + this.textOperation = function (f) { + undoManager.setCommandMode(); + f(); + that.refreshPreview(); + } + } + + uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help); + uiManager.setUndoRedoButtonStates(); + + var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; + + forceRefresh(); + }; + + } + + // before: contains all the text in the input box BEFORE the selection. + // after: contains all the text in the input box AFTER the selection. + function Chunks() { } + + // startRegex: a regular expression to find the start tag + // endRegex: a regular expresssion to find the end tag + Chunks.prototype.findTags = function (startRegex, endRegex) { + + var chunkObj = this; + var regex; + + if (startRegex) { + + regex = util.extendRegExp(startRegex, "", "$"); + + this.before = this.before.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + + regex = util.extendRegExp(startRegex, "^", ""); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.startTag = chunkObj.startTag + match; + return ""; + }); + } + + if (endRegex) { + + regex = util.extendRegExp(endRegex, "", "$"); + + this.selection = this.selection.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + + regex = util.extendRegExp(endRegex, "^", ""); + + this.after = this.after.replace(regex, + function (match) { + chunkObj.endTag = match + chunkObj.endTag; + return ""; + }); + } + }; + + // If remove is false, the whitespace is transferred + // to the before/after regions. + // + // If remove is true, the whitespace disappears. + Chunks.prototype.trimWhitespace = function (remove) { + var beforeReplacer, afterReplacer, that = this; + if (remove) { + beforeReplacer = afterReplacer = ""; + } else { + beforeReplacer = function (s) { that.before += s; return ""; } + afterReplacer = function (s) { that.after = s + that.after; return ""; } + } + + this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); + }; + + + Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { + + if (nLinesBefore === undefined) { + nLinesBefore = 1; + } + + if (nLinesAfter === undefined) { + nLinesAfter = 1; + } + + nLinesBefore++; + nLinesAfter++; + + var regexText; + var replacementText; + + // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 + if (navigator.userAgent.match(/Chrome/)) { + "X".match(/()./); + } + + this.selection = this.selection.replace(/(^\n*)/, ""); + + this.startTag = this.startTag + re.$1; + + this.selection = this.selection.replace(/(\n*$)/, ""); + this.endTag = this.endTag + re.$1; + this.startTag = this.startTag.replace(/(^\n*)/, ""); + this.before = this.before + re.$1; + this.endTag = this.endTag.replace(/(\n*$)/, ""); + this.after = this.after + re.$1; + + if (this.before) { + + regexText = replacementText = ""; + + while (nLinesBefore--) { + regexText += "\\n?"; + replacementText += "\n"; + } + + if (findExtraNewlines) { + regexText = "\\n*"; + } + this.before = this.before.replace(new re(regexText + "$", ""), replacementText); + } + + if (this.after) { + + regexText = replacementText = ""; + + while (nLinesAfter--) { + regexText += "\\n?"; + replacementText += "\n"; + } + if (findExtraNewlines) { + regexText = "\\n*"; + } + + this.after = this.after.replace(new re(regexText, ""), replacementText); + } + }; + + // end of Chunks + + // A collection of the important regions on the page. + // Cached so we don't have to keep traversing the DOM. + // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around + // this issue: + // Internet explorer has problems with CSS sprite buttons that use HTML + // lists. When you click on the background image "button", IE will + // select the non-existent link text and discard the selection in the + // textarea. The solution to this is to cache the textarea selection + // on the button's mousedown event and set a flag. In the part of the + // code where we need to grab the selection, we check for the flag + // and, if it's set, use the cached area instead of querying the + // textarea. + // + // This ONLY affects Internet Explorer (tested on versions 6, 7 + // and 8) and ONLY on button clicks. Keyboard shortcuts work + // normally since the focus never leaves the textarea. + function PanelCollection(postfix) { + this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); + this.preview = doc.getElementById("wmd-preview" + postfix); + this.input = doc.getElementById("wmd-input" + postfix); + }; + + // Returns true if the DOM element is visible, false if it's hidden. + // Checks if display is anything other than none. + util.isVisible = function (elem) { + + if (window.getComputedStyle) { + // Most browsers + return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; + } + else if (elem.currentStyle) { + // IE + return elem.currentStyle["display"] !== "none"; + } + }; + + + // Adds a listener callback to a DOM element which is fired on a specified + // event. + util.addEvent = function (elem, event, listener) { + if (elem.attachEvent) { + // IE only. The "on" is mandatory. + elem.attachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.addEventListener(event, listener, false); + } + }; + + + // Removes a listener callback from a DOM element which is fired on a specified + // event. + util.removeEvent = function (elem, event, listener) { + if (elem.detachEvent) { + // IE only. The "on" is mandatory. + elem.detachEvent("on" + event, listener); + } + else { + // Other browsers. + elem.removeEventListener(event, listener, false); + } + }; + + // Converts \r\n and \r to \n. + util.fixEolChars = function (text) { + text = text.replace(/\r\n/g, "\n"); + text = text.replace(/\r/g, "\n"); + return text; + }; + + // Extends a regular expression. Returns a new RegExp + // using pre + regex + post as the expression. + // Used in a few functions where we have a base + // expression and we want to pre- or append some + // conditions to it (e.g. adding "$" to the end). + // The flags are unchanged. + // + // regex is a RegExp, pre and post are strings. + util.extendRegExp = function (regex, pre, post) { + + if (pre === null || pre === undefined) { + pre = ""; + } + if (post === null || post === undefined) { + post = ""; + } + + var pattern = regex.toString(); + var flags; + + // Replace the flags with empty space and store them. + pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { + flags = flagsPart; + return ""; + }); + + // Remove the slash delimiters on the regular expression. + pattern = pattern.replace(/(^\/|\/$)/g, ""); + pattern = pre + pattern + post; + + return new re(pattern, flags); + } + + // UNFINISHED + // The assignment in the while loop makes jslint cranky. + // I'll change it to a better loop later. + position.getTop = function (elem, isInner) { + var result = elem.offsetTop; + if (!isInner) { + while (elem = elem.offsetParent) { + result += elem.offsetTop; + } + } + return result; + }; + + position.getHeight = function (elem) { + return elem.offsetHeight || elem.scrollHeight; + }; + + position.getWidth = function (elem) { + return elem.offsetWidth || elem.scrollWidth; + }; + + position.getPageSize = function () { + + var scrollWidth, scrollHeight; + var innerWidth, innerHeight; + + // It's not very clear which blocks work with which browsers. + if (self.innerHeight && self.scrollMaxY) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = self.innerHeight + self.scrollMaxY; + } + else if (doc.body.scrollHeight > doc.body.offsetHeight) { + scrollWidth = doc.body.scrollWidth; + scrollHeight = doc.body.scrollHeight; + } + else { + scrollWidth = doc.body.offsetWidth; + scrollHeight = doc.body.offsetHeight; + } + + if (self.innerHeight) { + // Non-IE browser + innerWidth = self.innerWidth; + innerHeight = self.innerHeight; + } + else if (doc.documentElement && doc.documentElement.clientHeight) { + // Some versions of IE (IE 6 w/ a DOCTYPE declaration) + innerWidth = doc.documentElement.clientWidth; + innerHeight = doc.documentElement.clientHeight; + } + else if (doc.body) { + // Other versions of IE + innerWidth = doc.body.clientWidth; + innerHeight = doc.body.clientHeight; + } + + var maxWidth = Math.max(scrollWidth, innerWidth); + var maxHeight = Math.max(scrollHeight, innerHeight); + return [maxWidth, maxHeight, innerWidth, innerHeight]; + }; + + // Handles pushing and popping TextareaStates for undo/redo commands. + // I should rename the stack variables to list. + function UndoManager(callback, panels) { + + var undoObj = this; + var undoStack = []; // A stack of undo states + var stackPtr = 0; // The index of the current state + var mode = "none"; + var lastState; // The last state + var timer; // The setTimeout handle for cancelling the timer + var inputStateObj; + + // Set the mode for later logic steps. + var setMode = function (newMode, noSave) { + if (mode != newMode) { + mode = newMode; + if (!noSave) { + saveState(); + } + } + + if (!uaSniffed.isIE || mode != "moving") { + timer = setTimeout(refreshState, 1); + } + else { + inputStateObj = null; + } + }; + + var refreshState = function (isInitialState) { + inputStateObj = new TextareaState(panels, isInitialState); + timer = undefined; + }; + + this.setCommandMode = function () { + mode = "command"; + saveState(); + timer = setTimeout(refreshState, 0); + }; + + this.canUndo = function () { + return stackPtr > 1; + }; + + this.canRedo = function () { + if (undoStack[stackPtr + 1]) { + return true; + } + return false; + }; + + // Removes the last state and restores it. + this.undo = function () { + + if (undoObj.canUndo()) { + if (lastState) { + // What about setting state -1 to null or checking for undefined? + lastState.restore(); + lastState = null; + } + else { + undoStack[stackPtr] = new TextareaState(panels); + undoStack[--stackPtr].restore(); + + if (callback) { + callback(); + } + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Redo an action. + this.redo = function () { + + if (undoObj.canRedo()) { + + undoStack[++stackPtr].restore(); + + if (callback) { + callback(); + } + } + + mode = "none"; + panels.input.focus(); + refreshState(); + }; + + // Push the input area state to the stack. + var saveState = function () { + var currState = inputStateObj || new TextareaState(panels); + + if (!currState) { + return false; + } + if (mode == "moving") { + if (!lastState) { + lastState = currState; + } + return; + } + if (lastState) { + if (undoStack[stackPtr - 1].text != lastState.text) { + undoStack[stackPtr++] = lastState; + } + lastState = null; + } + undoStack[stackPtr++] = currState; + undoStack[stackPtr + 1] = null; + if (callback) { + callback(); + } + }; + + var handleCtrlYZ = function (event) { + + var handled = false; + + if (event.ctrlKey || event.metaKey) { + + // IE and Opera do not support charCode. + var keyCode = event.charCode || event.keyCode; + var keyCodeChar = String.fromCharCode(keyCode); + + switch (keyCodeChar) { + + case "y": + undoObj.redo(); + handled = true; + break; + + case "z": + if (!event.shiftKey) { + undoObj.undo(); + } + else { + undoObj.redo(); + } + handled = true; + break; + } + } + + if (handled) { + if (event.preventDefault) { + event.preventDefault(); + } + if (window.event) { + window.event.returnValue = false; + } + return; + } + }; + + // Set the mode depending on what is going on in the input area. + var handleModeChange = function (event) { + + if (!event.ctrlKey && !event.metaKey) { + + var keyCode = event.keyCode; + + if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { + // 33 - 40: page up/dn and arrow keys + // 63232 - 63235: page up/dn and arrow keys on safari + setMode("moving"); + } + else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { + // 8: backspace + // 46: delete + // 127: delete + setMode("deleting"); + } + else if (keyCode == 13) { + // 13: Enter + setMode("newlines"); + } + else if (keyCode == 27) { + // 27: escape + setMode("escape"); + } + else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { + // 16-20 are shift, etc. + // 91: left window key + // I think this might be a little messed up since there are + // a lot of nonprinting keys above 20. + setMode("typing"); + } + } + }; + + var setEventHandlers = function () { + util.addEvent(panels.input, "keypress", function (event) { + // keyCode 89: y + // keyCode 90: z + if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { + event.preventDefault(); + } + }); + + var handlePaste = function () { + if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { + if (timer == undefined) { + mode = "paste"; + saveState(); + refreshState(); + } + } + }; + + util.addEvent(panels.input, "keydown", handleCtrlYZ); + util.addEvent(panels.input, "keydown", handleModeChange); + util.addEvent(panels.input, "mousedown", function () { + setMode("moving"); + }); + + panels.input.onpaste = handlePaste; + panels.input.ondrop = handlePaste; + }; + + var init = function () { + setEventHandlers(); + refreshState(true); + saveState(); + }; + + init(); + } + + // end of UndoManager + + // The input textarea state/contents. + // This is used to implement undo/redo by the undo manager. + function TextareaState(panels, isInitialState) { + + // Aliases + var stateObj = this; + var inputArea = panels.input; + this.init = function () { + if (!util.isVisible(inputArea)) { + return; + } + if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box + return; + } + + this.setInputAreaSelectionStartEnd(); + this.scrollTop = inputArea.scrollTop; + if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { + this.text = inputArea.value; + } + + } + + // Sets the selected text in the input box after we've performed an + // operation. + this.setInputAreaSelection = function () { + + if (!util.isVisible(inputArea)) { + return; + } + + if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { + + inputArea.focus(); + inputArea.selectionStart = stateObj.start; + inputArea.selectionEnd = stateObj.end; + inputArea.scrollTop = stateObj.scrollTop; + } + else if (doc.selection) { + + if (doc.activeElement && doc.activeElement !== inputArea) { + return; + } + + inputArea.focus(); + var range = inputArea.createTextRange(); + range.moveStart("character", -inputArea.value.length); + range.moveEnd("character", -inputArea.value.length); + range.moveEnd("character", stateObj.end); + range.moveStart("character", stateObj.start); + range.select(); + } + }; + + this.setInputAreaSelectionStartEnd = function () { + + if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { + + stateObj.start = inputArea.selectionStart; + stateObj.end = inputArea.selectionEnd; + } + else if (doc.selection) { + + stateObj.text = util.fixEolChars(inputArea.value); + + // IE loses the selection in the textarea when buttons are + // clicked. On IE we cache the selection. Here, if something is cached, + // we take it. + var range = panels.ieCachedRange || doc.selection.createRange(); + + var fixedRange = util.fixEolChars(range.text); + var marker = "\x07"; + var markedRange = marker + fixedRange + marker; + range.text = markedRange; + var inputText = util.fixEolChars(inputArea.value); + + range.moveStart("character", -markedRange.length); + range.text = fixedRange; + + stateObj.start = inputText.indexOf(marker); + stateObj.end = inputText.lastIndexOf(marker) - marker.length; + + var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; + + if (len) { + range.moveStart("character", -fixedRange.length); + while (len--) { + fixedRange += "\n"; + stateObj.end += 1; + } + range.text = fixedRange; + } + + if (panels.ieCachedRange) + stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange + + panels.ieCachedRange = null; + + this.setInputAreaSelection(); + } + }; + + // Restore this state into the input area. + this.restore = function () { + + if (stateObj.text != undefined && stateObj.text != inputArea.value) { + inputArea.value = stateObj.text; + } + this.setInputAreaSelection(); + inputArea.scrollTop = stateObj.scrollTop; + }; + + // Gets a collection of HTML chunks from the inptut textarea. + this.getChunks = function () { + + var chunk = new Chunks(); + chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); + chunk.startTag = ""; + chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); + chunk.endTag = ""; + chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); + chunk.scrollTop = stateObj.scrollTop; + + return chunk; + }; + + // Sets the TextareaState properties given a chunk of markdown. + this.setChunks = function (chunk) { + + chunk.before = chunk.before + chunk.startTag; + chunk.after = chunk.endTag + chunk.after; + + this.start = chunk.before.length; + this.end = chunk.before.length + chunk.selection.length; + this.text = chunk.before + chunk.selection + chunk.after; + this.scrollTop = chunk.scrollTop; + }; + this.init(); + }; + + function PreviewManager(converter, panels, previewRefreshCallback) { + + var managerObj = this; + var timeout; + var elapsedTime; + var oldInputText; + var maxDelay = 3000; + var startType = "delayed"; // The other legal value is "manual" + + // Adds event listeners to elements + var setupEvents = function (inputElem, listener) { + + util.addEvent(inputElem, "input", listener); + inputElem.onpaste = listener; + inputElem.ondrop = listener; + + util.addEvent(inputElem, "keypress", listener); + util.addEvent(inputElem, "keydown", listener); + }; + + var getDocScrollTop = function () { + + var result = 0; + + if (window.innerHeight) { + result = window.pageYOffset; + } + else + if (doc.documentElement && doc.documentElement.scrollTop) { + result = doc.documentElement.scrollTop; + } + else + if (doc.body) { + result = doc.body.scrollTop; + } + + return result; + }; + + var makePreviewHtml = function () { + + // If there is no registered preview panel + // there is nothing to do. + if (!panels.preview) + return; + + + var text = panels.input.value; + if (text && text == oldInputText) { + return; // Input text hasn't changed. + } + else { + oldInputText = text; + } + + var prevTime = new Date().getTime(); + + text = converter.makeHtml(text); + + // Calculate the processing time of the HTML creation. + // It's used as the delay time in the event listener. + var currTime = new Date().getTime(); + elapsedTime = currTime - prevTime; + + pushPreviewHtml(text); + }; + + // setTimeout is already used. Used as an event listener. + var applyTimeout = function () { + + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if (startType !== "manual") { + + var delay = 0; + + if (startType === "delayed") { + delay = elapsedTime; + } + + if (delay > maxDelay) { + delay = maxDelay; + } + timeout = setTimeout(makePreviewHtml, delay); + } + }; + + var getScaleFactor = function (panel) { + if (panel.scrollHeight <= panel.clientHeight) { + return 1; + } + return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); + }; + + var setPanelScrollTops = function () { + if (panels.preview) { + panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); + } + }; + + this.refresh = function (requiresRefresh) { + + if (requiresRefresh) { + oldInputText = ""; + makePreviewHtml(); + } + else { + applyTimeout(); + } + }; + + this.processingTime = function () { + return elapsedTime; + }; + + var isFirstTimeFilled = true; + + // IE doesn't let you use innerHTML if the element is contained somewhere in a table + // (which is the case for inline editing) -- in that case, detach the element, set the + // value, and reattach. Yes, that *is* ridiculous. + var ieSafePreviewSet = function (text) { + var preview = panels.preview; + var parent = preview.parentNode; + var sibling = preview.nextSibling; + parent.removeChild(preview); + preview.innerHTML = text; + if (!sibling) + parent.appendChild(preview); + else + parent.insertBefore(preview, sibling); + } + + var nonSuckyBrowserPreviewSet = function (text) { + panels.preview.innerHTML = text; + } + + var previewSetter; + + var previewSet = function (text) { + if (previewSetter) + return previewSetter(text); + + try { + nonSuckyBrowserPreviewSet(text); + previewSetter = nonSuckyBrowserPreviewSet; + } catch (e) { + previewSetter = ieSafePreviewSet; + previewSetter(text); + } + }; + + var pushPreviewHtml = function (text) { + + var emptyTop = position.getTop(panels.input) - getDocScrollTop(); + + if (panels.preview) { + previewSet(text); + previewRefreshCallback(); + } + + setPanelScrollTops(); + + if (isFirstTimeFilled) { + isFirstTimeFilled = false; + return; + } + + var fullTop = position.getTop(panels.input) - getDocScrollTop(); + + if (uaSniffed.isIE) { + setTimeout(function () { + window.scrollBy(0, fullTop - emptyTop); + }, 0); + } + else { + window.scrollBy(0, fullTop - emptyTop); + } + }; + + var init = function () { + + setupEvents(panels.input, applyTimeout); + makePreviewHtml(); + + if (panels.preview) { + panels.preview.scrollTop = 0; + } + }; + + init(); + }; + + + // This simulates a modal dialog box and asks for the URL when you + // click the hyperlink or image buttons. + // + // text: The html for the input box. + // defaultInputText: The default value that appears in the input box. + // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. + // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel + // was chosen). + ui.prompt = function (title, text, defaultInputText, callback) { + + // These variables need to be declared at this level since they are used + // in multiple functions. + var dialog; // The dialog box. + var input; // The text box where you enter the hyperlink. + + + if (defaultInputText === undefined) { + defaultInputText = ""; + } + + // Used as a keydown event handler. Esc dismisses the prompt. + // Key code 27 is ESC. + var checkEscape = function (key) { + var code = (key.charCode || key.keyCode); + if (code === 27) { + close(true); + } + }; + + // Dismisses the hyperlink input box. + // isCancel is true if we don't care about the input text. + // isCancel is false if we are going to keep the text. + var close = function (isCancel) { + util.removeEvent(doc.body, "keydown", checkEscape); + var text = input.value; + + if (isCancel) { + text = null; + } + else { + // Fixes common pasting errors. + text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); + if (!/^(?:https?|ftp):\/\//.test(text)) + text = 'http://' + text; + } + + $(dialog).modal('hide'); + + callback(text); + return false; + }; + + + + // Create the text input box form/window. + var createDialog = function () { + // + + // The main dialog box. + dialog = doc.createElement("div"); + dialog.className = "modal hide fade"; + dialog.style.display = "none"; + + // The header. + var header = doc.createElement("div"); + header.className = "modal-header"; + header.innerHTML = '×

    '+title+'

    '; + dialog.appendChild(header); + + // The body. + var body = doc.createElement("div"); + body.className = "modal-body"; + dialog.appendChild(body); + + // The footer. + var footer = doc.createElement("div"); + footer.className = "modal-footer"; + dialog.appendChild(footer); + + // The dialog text. + var question = doc.createElement("p"); + question.innerHTML = text; + question.style.padding = "5px"; + body.appendChild(question); + + // The web form container for the text box and buttons. + var form = doc.createElement("form"), + style = form.style; + form.onsubmit = function () { return close(false); }; + style.padding = "0"; + style.margin = "0"; + body.appendChild(form); + + // The input text box + input = doc.createElement("input"); + input.type = "text"; + input.value = defaultInputText; + style = input.style; + style.display = "block"; + style.width = "80%"; + style.marginLeft = style.marginRight = "auto"; + form.appendChild(input); + + // The ok button + var okButton = doc.createElement("button"); + okButton.className = "btn btn-primary"; + okButton.type = "button"; + okButton.onclick = function () { return close(false); }; + okButton.innerHTML = "OK"; + + // The cancel button + var cancelButton = doc.createElement("button"); + cancelButton.className = "btn btn-primary"; + cancelButton.type = "button"; + cancelButton.onclick = function () { return close(true); }; + cancelButton.innerHTML = "Cancel"; + + footer.appendChild(okButton); + footer.appendChild(cancelButton); + + util.addEvent(doc.body, "keydown", checkEscape); + + doc.body.appendChild(dialog); + + }; + + // Why is this in a zero-length timeout? + // Is it working around a browser bug? + setTimeout(function () { + + createDialog(); + + var defTextLen = defaultInputText.length; + if (input.selectionStart !== undefined) { + input.selectionStart = 0; + input.selectionEnd = defTextLen; + } + else if (input.createTextRange) { + var range = input.createTextRange(); + range.collapse(false); + range.moveStart("character", -defTextLen); + range.moveEnd("character", defTextLen); + range.select(); + } + + $(dialog).on('shown', function () { + input.focus(); + }) + + $(dialog).on('hidden', function () { + dialog.parentNode.removeChild(dialog); + }) + + $(dialog).modal() + + }, 0); + }; + + function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) { + + var inputBox = panels.input, + buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. + + makeSpritedButtonRow(); + + var keyEvent = "keydown"; + if (uaSniffed.isOpera) { + keyEvent = "keypress"; + } + + util.addEvent(inputBox, keyEvent, function (key) { + + // Check to see if we have a button key and, if so execute the callback. + if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { + + var keyCode = key.charCode || key.keyCode; + var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); + + switch (keyCodeStr) { + case "b": + doClick(buttons.bold); + break; + case "i": + doClick(buttons.italic); + break; + case "l": + doClick(buttons.link); + break; + case "q": + doClick(buttons.quote); + break; + case "k": + doClick(buttons.code); + break; + case "g": + doClick(buttons.image); + break; + case "o": + doClick(buttons.olist); + break; + case "u": + doClick(buttons.ulist); + break; + case "h": + doClick(buttons.heading); + break; + case "r": + doClick(buttons.hr); + break; + case "y": + doClick(buttons.redo); + break; + case "z": + if (key.shiftKey) { + doClick(buttons.redo); + } + else { + doClick(buttons.undo); + } + break; + default: + return; + } + + + if (key.preventDefault) { + key.preventDefault(); + } + + if (window.event) { + window.event.returnValue = false; + } + } + }); + + // Auto-indent on shift-enter + util.addEvent(inputBox, "keyup", function (key) { + if (key.shiftKey && !key.ctrlKey && !key.metaKey) { + var keyCode = key.charCode || key.keyCode; + // Character 13 is Enter + if (keyCode === 13) { + var fakeButton = {}; + fakeButton.textOp = bindCommand("doAutoindent"); + doClick(fakeButton); + } + } + }); + + // special handler because IE clears the context of the textbox on ESC + if (uaSniffed.isIE) { + util.addEvent(inputBox, "keydown", function (key) { + var code = key.keyCode; + if (code === 27) { + return false; + } + }); + } + + + // Perform the button's action. + function doClick(button) { + + inputBox.focus(); + + if (button.textOp) { + + if (undoManager) { + undoManager.setCommandMode(); + } + + var state = new TextareaState(panels); + + if (!state) { + return; + } + + var chunks = state.getChunks(); + + // Some commands launch a "modal" prompt dialog. Javascript + // can't really make a modal dialog box and the WMD code + // will continue to execute while the dialog is displayed. + // This prevents the dialog pattern I'm used to and means + // I can't do something like this: + // + // var link = CreateLinkDialog(); + // makeMarkdownLink(link); + // + // Instead of this straightforward method of handling a + // dialog I have to pass any code which would execute + // after the dialog is dismissed (e.g. link creation) + // in a function parameter. + // + // Yes this is awkward and I think it sucks, but there's + // no real workaround. Only the image and link code + // create dialogs and require the function pointers. + var fixupInputArea = function () { + + inputBox.focus(); + + if (chunks) { + state.setChunks(chunks); + } + + state.restore(); + previewManager.refresh(); + }; + + var noCleanup = button.textOp(chunks, fixupInputArea); + + if (!noCleanup) { + fixupInputArea(); + } + + } + + if (button.execute) { + button.execute(undoManager); + } + }; + + function setupButton(button, isEnabled) { + + if (isEnabled) { + button.disabled = false; + + if (!button.isHelp) { + button.onclick = function () { + if (this.onmouseout) { + this.onmouseout(); + } + doClick(this); + return false; + } + } + } + else { + button.disabled = true; + } + } + + function bindCommand(method) { + if (typeof method === "string") + method = commandManager[method]; + return function () { method.apply(commandManager, arguments); } + } + + function makeSpritedButtonRow() { + + var buttonBar = panels.buttonBar; + var buttonRow = document.createElement("div"); + buttonRow.id = "wmd-button-row" + postfix; + buttonRow.className = 'btn-toolbar'; + buttonRow = buttonBar.appendChild(buttonRow); + + var makeButton = function (id, title, icon, textOp, group) { + var button = document.createElement("button"); + button.className = "btn"; + var buttonImage = document.createElement("i"); + buttonImage.className = icon; + button.id = id + postfix; + button.appendChild(buttonImage); + button.title = title; + $(button).tooltip({placement: 'bottom'}) + if (textOp) + button.textOp = textOp; + setupButton(button, true); + if (group) { + group.appendChild(button); + } else { + buttonRow.appendChild(button); + } + return button; + }; + var makeGroup = function (num) { + var group = document.createElement("div"); + group.className = "btn-group wmd-button-group" + num; + group.id = "wmd-button-group" + num + postfix; + buttonRow.appendChild(group); + return group + } + + group1 = makeGroup(1); + buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "icon-bold", bindCommand("doBold"), group1); + buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "icon-italic", bindCommand("doItalic"), group1); + + group2 = makeGroup(2); + /* + buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "icon-link", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, false); + }), group2); + */ + buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "icon-quote-left", bindCommand("doBlockquote"), group2); + buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "icon-code", bindCommand("doCode"), group2); + /* + buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "icon-picture", bindCommand(function (chunk, postProcessing) { + return this.doLinkOrImage(chunk, postProcessing, true); + }), group2); + */ + + group3 = makeGroup(3); + buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "icon-list", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, true); + }), group3); + buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "icon-list-ul", bindCommand(function (chunk, postProcessing) { + this.doList(chunk, postProcessing, false); + }), group3); + buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "icon-tasks", bindCommand("doHeading"), group3); + buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "icon-minus", bindCommand("doHorizontalRule"), group3); + + group4 = makeGroup(4); + buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "icon-undo", null, group4); + buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; + + var redoTitle = /win/.test(nav.platform.toLowerCase()) ? + "Redo - Ctrl+Y" : + "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms + + buttons.redo = makeButton("wmd-redo-button", redoTitle, "icon-share-alt", null, group4); + buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; + + if (helpOptions) { + group5 = makeGroup(5); + group5.className = group5.className + " pull-right"; + var helpButton = document.createElement("button"); + var helpButtonImage = document.createElement("i"); + helpButtonImage.className = "icon-question-sign"; + helpButton.appendChild(helpButtonImage); + helpButton.className = "btn"; + helpButton.id = "wmd-help-button" + postfix; + helpButton.isHelp = true; + helpButton.title = helpOptions.title || defaultHelpHoverTitle; + $(helpButton).tooltip({placement: 'bottom'}) + helpButton.onclick = helpOptions.handler; + + setupButton(helpButton, true); + group5.appendChild(helpButton); + buttons.help = helpButton; + } + + setUndoRedoButtonStates(); + } + + function setUndoRedoButtonStates() { + if (undoManager) { + setupButton(buttons.undo, undoManager.canUndo()); + setupButton(buttons.redo, undoManager.canRedo()); + } + }; + + this.setUndoRedoButtonStates = setUndoRedoButtonStates; + + } + + function CommandManager(pluginHooks) { + this.hooks = pluginHooks; + } + + var commandProto = CommandManager.prototype; + + // The markdown symbols - 4 spaces = code, > = blockquote, etc. + commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; + + // Remove markdown symbols from the chunk selection. + commandProto.unwrap = function (chunk) { + var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); + chunk.selection = chunk.selection.replace(txt, "$1 $2"); + }; + + commandProto.wrap = function (chunk, len) { + this.unwrap(chunk); + var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), + that = this; + + chunk.selection = chunk.selection.replace(regex, function (line, marked) { + if (new re("^" + that.prefixes, "").test(line)) { + return line; + } + return marked + "\n"; + }); + + chunk.selection = chunk.selection.replace(/\s+$/, ""); + }; + + commandProto.doBold = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 2, "strong text"); + }; + + commandProto.doItalic = function (chunk, postProcessing) { + return this.doBorI(chunk, postProcessing, 1, "emphasized text"); + }; + + // chunk: The selected region that will be enclosed with */** + // nStars: 1 for italics, 2 for bold + // insertText: If you just click the button without highlighting text, this gets inserted + commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { + + // Get rid of whitespace and fixup newlines. + chunk.trimWhitespace(); + chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); + + // Look for stars before and after. Is the chunk already marked up? + // note that these regex matches cannot fail + var starsBefore = /(\**$)/.exec(chunk.before)[0]; + var starsAfter = /(^\**)/.exec(chunk.after)[0]; + + var prevStars = Math.min(starsBefore.length, starsAfter.length); + + // Remove stars if we have to since the button acts as a toggle. + if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { + chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); + chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); + } + else if (!chunk.selection && starsAfter) { + // It's not really clear why this code is necessary. It just moves + // some arbitrary stuff around. + chunk.after = chunk.after.replace(/^([*_]*)/, ""); + chunk.before = chunk.before.replace(/(\s?)$/, ""); + var whitespace = re.$1; + chunk.before = chunk.before + starsAfter + whitespace; + } + else { + + // In most cases, if you don't have any selected text and click the button + // you'll get a selected, marked up region with the default text inserted. + if (!chunk.selection && !starsAfter) { + chunk.selection = insertText; + } + + // Add the true markup. + var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? + chunk.before = chunk.before + markup; + chunk.after = markup + chunk.after; + } + + return; + }; + + commandProto.stripLinkDefs = function (text, defsToAdd) { + + text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, + function (totalMatch, id, link, newlines, title) { + defsToAdd[id] = totalMatch.replace(/\s*$/, ""); + if (newlines) { + // Strip the title and return that separately. + defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); + return newlines + title; + } + return ""; + }); + + return text; + }; + + commandProto.addLinkDef = function (chunk, linkDef) { + + var refNumber = 0; // The current reference number + var defsToAdd = {}; // + // Start with a clean slate by removing all previous link definitions. + chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); + chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); + chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); + + var defs = ""; + var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; + + var addDefNumber = function (def) { + refNumber++; + def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); + defs += "\n" + def; + }; + + // note that + // a) the recursive call to getLink cannot go infinite, because by definition + // of regex, inner is always a proper substring of wholeMatch, and + // b) more than one level of nesting is neither supported by the regex + // nor making a lot of sense (the only use case for nesting is a linked image) + var getLink = function (wholeMatch, before, inner, afterInner, id, end) { + inner = inner.replace(regex, getLink); + if (defsToAdd[id]) { + addDefNumber(defsToAdd[id]); + return before + inner + afterInner + refNumber + end; + } + return wholeMatch; + }; + + chunk.before = chunk.before.replace(regex, getLink); + + if (linkDef) { + addDefNumber(linkDef); + } + else { + chunk.selection = chunk.selection.replace(regex, getLink); + } + + var refOut = refNumber; + + chunk.after = chunk.after.replace(regex, getLink); + + if (chunk.after) { + chunk.after = chunk.after.replace(/\n*$/, ""); + } + if (!chunk.after) { + chunk.selection = chunk.selection.replace(/\n*$/, ""); + } + + chunk.after += "\n\n" + defs; + + return refOut; + }; + + // takes the line as entered into the add link/as image dialog and makes + // sure the URL and the optinal title are "nice". + function properlyEncoded(linkdef) { + return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { + link = link.replace(/\?.*$/, function (querypart) { + return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical + }); + link = decodeURIComponent(link); // unencode first, to prevent double encoding + link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'); + link = link.replace(/\?.*$/, function (querypart) { + return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded + }); + if (title) { + title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); + title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); + } + return title ? link + ' "' + title + '"' : link; + }); + } + + commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { + + chunk.trimWhitespace(); + chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); + var background; + + if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { + + chunk.startTag = chunk.startTag.replace(/!?\[/, ""); + chunk.endTag = ""; + this.addLinkDef(chunk, null); + + } + else { + + // We're moving start and end tag back into the selection, since (as we're in the else block) we're not + // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the + // link text. linkEnteredCallback takes care of escaping any brackets. + chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; + chunk.startTag = chunk.endTag = ""; + + if (/\n\n/.test(chunk.selection)) { + this.addLinkDef(chunk, null); + return; + } + var that = this; + // The function to be executed when you enter a link and press OK or Cancel. + // Marks up the link and adds the ref. + var linkEnteredCallback = function (link) { + + if (link !== null) { + // ( $1 + // [^\\] anything that's not a backslash + // (?:\\\\)* an even number (this includes zero) of backslashes + // ) + // (?= followed by + // [[\]] an opening or closing bracket + // ) + // + // In other words, a non-escaped bracket. These have to be escaped now to make sure they + // don't count as the end of the link or similar. + // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), + // the bracket in one match may be the "not a backslash" character in the next match, so it + // should not be consumed by the first match. + // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the + // start of the string, so this also works if the selection begins with a bracket. We cannot solve + // this by anchoring with ^, because in the case that the selection starts with two brackets, this + // would mean a zero-width match at the start. Since zero-width matches advance the string position, + // the first bracket could then not act as the "not a backslash" for the second. + chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); + + var linkDef = " [999]: " + properlyEncoded(link); + + var num = that.addLinkDef(chunk, linkDef); + chunk.startTag = isImage ? "![" : "["; + chunk.endTag = "][" + num + "]"; + + if (!chunk.selection) { + if (isImage) { + chunk.selection = "enter image description here"; + } + else { + chunk.selection = "enter link description here"; + } + } + } + postProcessing(); + }; + + + if (isImage) { + if (!this.hooks.insertImageDialog(linkEnteredCallback)) + ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback); + } + else { + ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); + } + return true; + } + }; + + // When making a list, hitting shift-enter will put your cursor on the next line + // at the current indent level. + commandProto.doAutoindent = function (chunk, postProcessing) { + + var commandMgr = this, + fakeSelection = false; + + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); + chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); + + // There's no selection, end the cursor wasn't at the end of the line: + // The user wants to split the current list item / code line / blockquote line + // (for the latter it doesn't really matter) in two. Temporarily select the + // (rest of the) line to achieve this. + if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { + chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { + chunk.selection = wholeMatch; + return ""; + }); + fakeSelection = true; + } + + if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doList) { + commandMgr.doList(chunk); + } + } + if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { + if (commandMgr.doBlockquote) { + commandMgr.doBlockquote(chunk); + } + } + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + if (commandMgr.doCode) { + commandMgr.doCode(chunk); + } + } + + if (fakeSelection) { + chunk.after = chunk.selection + chunk.after; + chunk.selection = ""; + } + }; + + commandProto.doBlockquote = function (chunk, postProcessing) { + + chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, + function (totalMatch, newlinesBefore, text, newlinesAfter) { + chunk.before += newlinesBefore; + chunk.after = newlinesAfter + chunk.after; + return text; + }); + + chunk.before = chunk.before.replace(/(>[ \t]*)$/, + function (totalMatch, blankLine) { + chunk.selection = blankLine + chunk.selection; + return ""; + }); + + chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); + chunk.selection = chunk.selection || "Blockquote"; + + // The original code uses a regular expression to find out how much of the + // text *directly before* the selection already was a blockquote: + + /* + if (chunk.before) { + chunk.before = chunk.before.replace(/\n?$/, "\n"); + } + chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, + function (totalMatch) { + chunk.startTag = totalMatch; + return ""; + }); + */ + + // This comes down to: + // Go backwards as many lines a possible, such that each line + // a) starts with ">", or + // b) is almost empty, except for whitespace, or + // c) is preceeded by an unbroken chain of non-empty lines + // leading up to a line that starts with ">" and at least one more character + // and in addition + // d) at least one line fulfills a) + // + // Since this is essentially a backwards-moving regex, it's susceptible to + // catstrophic backtracking and can cause the browser to hang; + // see e.g. http://meta.stackoverflow.com/questions/9807. + // + // Hence we replaced this by a simple state machine that just goes through the + // lines and checks for a), b), and c). + + var match = "", + leftOver = "", + line; + if (chunk.before) { + var lines = chunk.before.replace(/\n$/, "").split("\n"); + var inChain = false; + for (var i = 0; i < lines.length; i++) { + var good = false; + line = lines[i]; + inChain = inChain && line.length > 0; // c) any non-empty line continues the chain + if (/^>/.test(line)) { // a) + good = true; + if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain + inChain = true; + } else if (/^[ \t]*$/.test(line)) { // b) + good = true; + } else { + 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 + } + if (good) { + match += line + "\n"; + } else { + leftOver += match + line; + match = "\n"; + } + } + if (!/(^|\n)>/.test(match)) { // d) + leftOver += match; + match = ""; + } + } + + chunk.startTag = match; + chunk.before = leftOver; + + // end of change + + if (chunk.after) { + chunk.after = chunk.after.replace(/^\n?/, "\n"); + } + + chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, + function (totalMatch) { + chunk.endTag = totalMatch; + return ""; + } + ); + + var replaceBlanksInTags = function (useBracket) { + + var replacement = useBracket ? "> " : ""; + + if (chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + if (chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, + function (totalMatch, markdown) { + return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; + }); + } + }; + + if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { + this.wrap(chunk, SETTINGS.lineLength - 2); + chunk.selection = chunk.selection.replace(/^/gm, "> "); + replaceBlanksInTags(true); + chunk.skipLines(); + } else { + chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); + this.unwrap(chunk); + replaceBlanksInTags(false); + + if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { + chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); + } + + if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { + chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); + } + } + + chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); + + if (!/\n/.test(chunk.selection)) { + chunk.selection = chunk.selection.replace(/^(> *)/, + function (wholeMatch, blanks) { + chunk.startTag += blanks; + return ""; + }); + } + }; + + commandProto.doCode = function (chunk, postProcessing) { + + var hasTextBefore = /\S[ ]*$/.test(chunk.before); + var hasTextAfter = /^[ ]*\S/.test(chunk.after); + + // Use 'four space' markdown if the selection is on its own + // line or is multiline. + if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { + + chunk.before = chunk.before.replace(/[ ]{4}$/, + function (totalMatch) { + chunk.selection = totalMatch + chunk.selection; + return ""; + }); + + var nLinesBack = 1; + var nLinesForward = 1; + + if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { + nLinesBack = 0; + } + if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { + nLinesForward = 0; + } + + chunk.skipLines(nLinesBack, nLinesForward); + + if (!chunk.selection) { + chunk.startTag = " "; + chunk.selection = "enter code here"; + } + else { + if (/^[ ]{0,3}\S/m.test(chunk.selection)) { + if (/\n/.test(chunk.selection)) + chunk.selection = chunk.selection.replace(/^/gm, " "); + else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior + chunk.before += " "; + } + else { + chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, ""); + } + } + } + else { + // Use backticks (`) to delimit the code block. + + chunk.trimWhitespace(); + chunk.findTags(/`/, /`/); + + if (!chunk.startTag && !chunk.endTag) { + chunk.startTag = chunk.endTag = "`"; + if (!chunk.selection) { + chunk.selection = "enter code here"; + } + } + else if (chunk.endTag && !chunk.startTag) { + chunk.before += chunk.endTag; + chunk.endTag = ""; + } + else { + chunk.startTag = chunk.endTag = ""; + } + } + }; + + commandProto.doList = function (chunk, postProcessing, isNumberedList) { + + // These are identical except at the very beginning and end. + // Should probably use the regex extension function to make this clearer. + var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; + var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; + + // The default bullet is a dash but others are possible. + // This has nothing to do with the particular HTML bullet, + // it's just a markdown bullet. + var bullet = "-"; + + // The number in a numbered list. + var num = 1; + + // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. + var getItemPrefix = function () { + var prefix; + if (isNumberedList) { + prefix = " " + num + ". "; + num++; + } + else { + prefix = " " + bullet + " "; + } + return prefix; + }; + + // Fixes the prefixes of the other list items. + var getPrefixedItem = function (itemText) { + + // The numbering flag is unset when called by autoindent. + if (isNumberedList === undefined) { + isNumberedList = /^\s*\d/.test(itemText); + } + + // Renumber/bullet the list element. + itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, + function (_) { + return getItemPrefix(); + }); + + return itemText; + }; + + chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); + + if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { + chunk.before += chunk.startTag; + chunk.startTag = ""; + } + + if (chunk.startTag) { + + var hasDigits = /\d+[.]/.test(chunk.startTag); + chunk.startTag = ""; + chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); + this.unwrap(chunk); + chunk.skipLines(); + + if (hasDigits) { + // Have to renumber the bullet points if this is a numbered list. + chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); + } + if (isNumberedList == hasDigits) { + return; + } + } + + var nLinesUp = 1; + + chunk.before = chunk.before.replace(previousItemsRegex, + function (itemText) { + if (/^\s*([*+-])/.test(itemText)) { + bullet = re.$1; + } + nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + if (!chunk.selection) { + chunk.selection = "List item"; + } + + var prefix = getItemPrefix(); + + var nLinesDown = 1; + + chunk.after = chunk.after.replace(nextItemsRegex, + function (itemText) { + nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; + return getPrefixedItem(itemText); + }); + + chunk.trimWhitespace(true); + chunk.skipLines(nLinesUp, nLinesDown, true); + chunk.startTag = prefix; + var spaces = prefix.replace(/./g, " "); + this.wrap(chunk, SETTINGS.lineLength - spaces.length); + chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); + + }; + + commandProto.doHeading = function (chunk, postProcessing) { + + // Remove leading/trailing whitespace and reduce internal spaces to single spaces. + chunk.selection = chunk.selection.replace(/\s+/g, " "); + chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); + + // If we clicked the button with no selected text, we just + // make a level 2 hash header around some default text. + if (!chunk.selection) { + chunk.startTag = "## "; + chunk.selection = "Heading"; + chunk.endTag = " ##"; + return; + } + + var headerLevel = 0; // The existing header level of the selected text. + + // Remove any existing hash heading markdown and save the header level. + chunk.findTags(/#+[ ]*/, /[ ]*#+/); + if (/#+/.test(chunk.startTag)) { + headerLevel = re.lastMatch.length; + } + chunk.startTag = chunk.endTag = ""; + + // Try to get the current header level by looking for - and = in the line + // below the selection. + chunk.findTags(null, /\s?(-+|=+)/); + if (/=+/.test(chunk.endTag)) { + headerLevel = 1; + } + if (/-+/.test(chunk.endTag)) { + headerLevel = 2; + } + + // Skip to the next line so we can create the header markdown. + chunk.startTag = chunk.endTag = ""; + chunk.skipLines(1, 1); + + // We make a level 2 header if there is no current header. + // If there is a header level, we substract one from the header level. + // If it's already a level 1 header, it's removed. + var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; + + if (headerLevelToCreate > 0) { + + // The button only creates level 1 and 2 underline headers. + // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? + var headerChar = headerLevelToCreate >= 2 ? "-" : "="; + var len = chunk.selection.length; + if (len > SETTINGS.lineLength) { + len = SETTINGS.lineLength; + } + chunk.endTag = "\n"; + while (len--) { + chunk.endTag += headerChar; + } + } + }; + + commandProto.doHorizontalRule = function (chunk, postProcessing) { + chunk.startTag = "----------\n"; + chunk.selection = ""; + chunk.skipLines(2, 1, true); + } + + +})(); \ No newline at end of file diff --git a/static/lib/pagedown/Markdown.Editor.less b/static/lib/pagedown/Markdown.Editor.less new file mode 100755 index 000000000..e1c0cc4df --- /dev/null +++ b/static/lib/pagedown/Markdown.Editor.less @@ -0,0 +1,80 @@ + +.wmd-panel { + width: 100%; +} + +.wmd-input { + height: 300px; + width: 100%; + box-sizing: border-box; + -webkit-box-sizing:border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; +} + +.wmd-preview { + .well; + width: 100%; + box-sizing: border-box; + -webkit-box-sizing:border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; +} + +.wmd-panel .btn-toolbar { + margin-bottom: 0; + padding: 0; + width: 100%; +} + +.icon-link, +.icon-blockquote, +.icon-code, +.icon-bullet-list, +.icon-list, +.icon-header, +.icon-hr-line, +.icon-undo { + background-image: url(Markdown.Editor.Icons.png); +} +.icon-link { background-position: 0 0; } +.icon-blockquote { background-position: -24px 0; } +.icon-code { background-position: -48px 0; } +.icon-bullet-list { background-position: -72px 0; } +.icon-list { background-position: -96px 0; } +.icon-header { background-position: -120px 0; } +.icon-hr-line { background-position: -144px 0; } +.icon-undo { background-position: -168px 0; } + + + + + +.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/static/lib/pagedown/Markdown.Sanitizer.js b/static/lib/pagedown/Markdown.Sanitizer.js new file mode 100755 index 000000000..be0445ab5 --- /dev/null +++ b/static/lib/pagedown/Markdown.Sanitizer.js @@ -0,0 +1,111 @@ +(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|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; + // | + var a_white = /^(]+")?\s?>|<\/a>)$/i; + + // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; + + //
    |
    for twitter bootstrap + var pre_white = /^(|<\/pre>)$/i; + + function sanitizeTag(tag) { + if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white)) + return tag; + else + return ""; + } + + /// + /// 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 + /// + 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 = "



  • "; + 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] == "") { + 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/static/partials/repo-list.html b/static/partials/repo-list.html index 3a97b77e6..4a81873d7 100644 --- a/static/partials/repo-list.html +++ b/static/partials/repo-list.html @@ -3,6 +3,6 @@
    {{repository.namespace}}/{{repository.name}} -
    {{repository.description}}
    +
    diff --git a/static/partials/view-repo.html b/static/partials/view-repo.html index 2d2532d92..a2fc7cd47 100644 --- a/static/partials/view-repo.html +++ b/static/partials/view-repo.html @@ -41,8 +41,10 @@ -

    {{repo.description}}

    - +

    + + +