diff --git a/Changelog.md b/Changelog.md
index 5c4eba623de242c88094a0ed695ec36338aef267..488b0ca9d5861878db0ee4f90e37e0e01484ab25 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -141,6 +141,7 @@ diaspora.yml file**. The existing settings from 0.4.x and before will not work a
 * Don't include the content of non-public posts into notification mails [#5494](https://github.com/diaspora/diaspora/pull/5494)
 * Allow to set unhosted button and currency for paypal donation [#5452](https://github.com/diaspora/diaspora/pull/5452)
 * Add followed tags in the mobile menu [#5468](https://github.com/diaspora/diaspora/pull/5468)
+* Replace Pagedown with markdown-it [#5526](https://github.com/diaspora/diaspora/pull/5526)
 
 # 0.4.1.2
 
diff --git a/Gemfile b/Gemfile
index 9805de1c471fc9a3425f84c80c52d45741ba4439..665ff45a200f7fe4bacab326a242c6b0e6c75219 100644
--- a/Gemfile
+++ b/Gemfile
@@ -82,13 +82,17 @@ gem 'entypo-rails', '2.2.2'
 
 # JavaScript
 
-gem 'backbone-on-rails',     '1.1.2'
-gem 'handlebars_assets',     '0.18.0'
-gem 'jquery-rails',          '3.1.2'
-gem 'rails-assets-jquery',   '1.11.1' # Should be kept in sync with jquery-rails
-gem 'js_image_paths',        '0.0.1'
-gem 'js-routes',             '0.9.9'
-gem 'rails-assets-punycode', '1.3.2'
+gem 'backbone-on-rails',                                '1.1.2'
+gem 'handlebars_assets',                                '0.18.0'
+gem 'jquery-rails',                                     '3.1.2'
+gem 'rails-assets-jquery',                              '1.11.1' # Should be kept in sync with jquery-rails
+gem 'js_image_paths',                                   '0.0.1'
+gem 'js-routes',                                        '0.9.9'
+gem 'rails-assets-punycode',                            '1.3.2'
+gem 'rails-assets-markdown-it',                         '3.0.1'
+gem 'rails-assets-markdown-it-hashtag',                 '0.2.1'
+gem 'rails-assets-markdown-it-diaspora-mention',        '0.1.0'
+gem 'rails-assets-markdown-it--markdown-it-for-inline', '0.1.0'
 
 # jQuery plugins
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 0ac8dbe9695e8b84640fe6f11b551c2ac5407038..b6e6e7943f9dafb24bfa383310fac8b667f71f2c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -438,6 +438,10 @@ GEM
       rails-assets-jquery (>= 1.6)
     rails-assets-jquery.slimscroll (1.3.3)
       rails-assets-jquery (>= 1.7)
+    rails-assets-markdown-it--markdown-it-for-inline (0.1.0)
+    rails-assets-markdown-it (3.0.1)
+    rails-assets-markdown-it-diaspora-mention (0.1.0)
+    rails-assets-markdown-it-hashtag (0.2.1)
     rails-assets-perfect-scrollbar (0.5.7)
       rails-assets-jquery (>= 1.10)
     rails-assets-punycode (1.3.2)
@@ -681,6 +685,10 @@ DEPENDENCIES
   rails-assets-jquery-idletimer (= 1.0.1)
   rails-assets-jquery-placeholder (= 2.0.8)
   rails-assets-jquery-textchange (= 0.2.3)
+  rails-assets-markdown-it (= 3.0.1)
+  rails-assets-markdown-it--markdown-it-for-inline (= 0.1.0)
+  rails-assets-markdown-it-diaspora-mention (= 0.1.0)
+  rails-assets-markdown-it-hashtag (= 0.2.1)
   rails-assets-perfect-scrollbar (= 0.5.7)
   rails-assets-punycode (= 1.3.2)
   rails-i18n (= 4.0.3)
diff --git a/app/assets/javascripts/app/helpers/handlebars-helpers.js b/app/assets/javascripts/app/helpers/handlebars-helpers.js
index 17f42e3c2a639b752fc7571a32340cf0aa076023..0b59ba93b47a939fbb75edefa4aedb0feadf2889 100644
--- a/app/assets/javascripts/app/helpers/handlebars-helpers.js
+++ b/app/assets/javascripts/app/helpers/handlebars-helpers.js
@@ -99,7 +99,7 @@ Handlebars.registerHelper('fmtTags', function(tags) {
 });
 
 Handlebars.registerHelper('fmtText', function(text) {
-  return new Handlebars.SafeString(app.helpers.textFormatter(text, null));
+  return new Handlebars.SafeString(app.helpers.textFormatter(text));
 });
 
 Handlebars.registerHelper('isCurrentPage', function(path_helper, id, options){
diff --git a/app/assets/javascripts/app/helpers/text_formatter.js b/app/assets/javascripts/app/helpers/text_formatter.js
index 71f07abc70e87afb7767eadee3efa6ea9fbe9de3..a4642b0e7ae5a331573fadbc62fbf5647a2dcbb6 100644
--- a/app/assets/javascripts/app/helpers/text_formatter.js
+++ b/app/assets/javascripts/app/helpers/text_formatter.js
@@ -1,146 +1,76 @@
 // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
 
-// cache url regex globally, for direct acces when testing
-$(function() {
-  Diaspora.url_regex = /(^|\s)\b((?:(?:https?|ftp):(?:\/{1,3})|www\.)(?:[^"<>\)\s]|\(([^\s()<>]+|(\([^\s()<>]+\)))\))+)(?=\s|$)/gi;
-});
-
 (function(){
-  //make it so I take text and mentions rather than the modelapp.helpers.textFormatter(
-  var textFormatter = function textFormatter(text, model) {
-    var mentions = model ? model.get("mentioned_people") : [];
-
-    return textFormatter.mentionify(
-      textFormatter.hashtagify(
-        textFormatter.markdownify(text)
-        ), mentions
-      )
-  };
-
-  textFormatter.markdownify = function markdownify(text){
-    var converter = Markdown.getSanitizingConverter();
-
-    // punycode non-ascii chars in urls
-    converter.hooks.chain("preConversion", function(text) {
-
-      // add < > around plain urls, effectively making them "autolinks"
-      text = text.replace(Diaspora.url_regex, function() {
-        var url = arguments[2];
-        if( url.match(/^[^\w]/) ) return url; // evil witchcraft, noop
-        return arguments[1]+"<"+url+">";
-      });
-
-      // process links
-      // regex copied from: https://code.google.com/p/pagedown/source/browse/Markdown.Converter.js#1198 (and slightly expanded)
-      var linkRegex = /(\[.*\]:\s)?(<|\()((?:(https?|ftp):\/\/[^\/'">\s]|www)[^'">\s]+?)([>\)]{1,2})/gi;
-      text = text.replace(linkRegex, function() {
-        var unicodeUrl = arguments[3];
-        var urlSuffix = arguments[5];
-
-        unicodeUrl = ( unicodeUrl.match(/^www/) ) ? ('http://' + unicodeUrl) : unicodeUrl;
-
-        // handle parentheses, especially in case the link ends with ')'
-        if( urlSuffix.indexOf(')') != -1 && urlSuffix.indexOf('>') != -1 ) {
-          unicodeUrl += ')';
-          urlSuffix = '>';
-        }
-
-        // url*DE*code as much as possible
-        try {
-          while( unicodeUrl.indexOf("%") !== -1 && unicodeUrl != decodeURI(unicodeUrl) ) {
-            unicodeUrl = decodeURI(unicodeUrl);
-          }
-        }
-        catch(e){}
-
-        // markdown doesn't like '(' or ')' anywhere, except where it wants
-        var workingUrl = unicodeUrl.replace(/\(/, "%28").replace(/\)/, "%29");
+  app.helpers.textFormatter = function(text, mentions) {
+    mentions = mentions ? mentions : [];
 
-        var addr = parse_url(unicodeUrl);
-        if( !addr.host ) addr.host = ""; // must not be 'undefined'
-
-        var asciiUrl = // rebuild the url
-          (!addr.scheme ? '' : addr.scheme +
-          ( (addr.scheme.toLowerCase()=="mailto") ? ':' : '://')) +
-          (!addr.user ? '' : addr.user +
-          (!addr.pass ? '' : ':'+addr.pass) + '@') +
-         punycode.toASCII(addr.host) +
-          (!addr.port ? '' : ':' + addr.port) +
-          (!addr.path ? '' : encodeURI(addr.path) ) +
-          (!addr.query ? '' : '?' + encodeURI(addr.query) ) +
-          (!addr.fragment ? '' : '#' + encodeURI(addr.fragment) );
-        if( !arguments[1] || arguments[1] == "") { // inline link
-          if(arguments[2] == "<") return "["+workingUrl+"]("+asciiUrl+")"; // without link text
-          else return arguments[2]+asciiUrl+urlSuffix; // with link text
-        } else { // reference style link
-          return arguments[1]+asciiUrl;
-        }
-      });
-
-      return text;
+    var punycodeURL = function(url){
+      try {
+        while(url.indexOf("%") !== -1 && url != decodeURI(url)) url = decodeURI(url);
+      }
+      catch(e){}
+
+      var addr = parse_url(url);
+      if( !addr.host ) addr.host = ""; // must not be 'undefined'
+
+      url = // rebuild the url
+        (!addr.scheme ? '' : addr.scheme +
+        ( (addr.scheme.toLowerCase()=="mailto") ? ':' : '://')) +
+        (!addr.user ? '' : addr.user +
+        (!addr.pass ? '' : ':'+addr.pass) + '@') +
+        punycode.toASCII(addr.host) +
+        (!addr.port ? '' : ':' + addr.port) +
+        (!addr.path ? '' : encodeURI(addr.path) ) +
+        (!addr.query ? '' : '?' + encodeURI(addr.query) ) +
+        (!addr.fragment ? '' : '#' + encodeURI(addr.fragment) );
+      return url;
+    };
+
+    var md = window.markdownit({
+      breaks:      true,
+      html:        false,
+      linkify:     true,
+      typographer: true
     });
 
-    // make nice little utf-8 symbols
-    converter.hooks.chain("preConversion", function(text) {
-      var input_strings = [
-        "<->", "->", "<-",
-        "(c)", "(r)", "(tm)",
-        "<3"
-      ];
-      var output_symbols = [
-        "↔", "→", "←",
-        "©", "®", "™",
-        "♥"
-      ];
-      // quote function from: http://stackoverflow.com/a/494122
-      var quote = function(str) {
-        return str.replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
-      };
-
-      _.each(input_strings, function(str, idx) {
-        var r = new RegExp(quote(str), "gi");
-        text = text.replace(r, output_symbols[idx]);
-      });
-      return text;
+    var inlinePlugin = window.markdownitForInline;
+    md.use(inlinePlugin, 'utf8_symbols', 'text', function (tokens, idx) {
+      tokens[idx].content = tokens[idx].content.replace(/<->/g, "↔")
+                                               .replace(/<-/g,  "←")
+                                               .replace(/->/g,  "→")
+                                               .replace(/<3/g,  "♥");
     });
 
-    converter.hooks.chain("postConversion", function (text) {
-      return text.replace(/(\"(?:(?:http|https):\/\/)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(?:\/\S*)?\")(\>)/g, '$1 target="_blank">')
+    md.use(inlinePlugin, 'link_new_window_and_punycode', 'link_open', function (tokens, idx) {
+      tokens[idx].href = punycodeURL(tokens[idx].href);
+      tokens[idx].target = "_blank";
     });
 
-    return converter.makeHtml(text)
-  };
-
-  textFormatter.hashtagify = function hashtagify(text){
-    var utf8WordCharcters =/(<a[^>]*>.*?<\/a>)|(\s|^|>)#([\u0080-\uFFFF|\w|-]+|&lt;3)/g;
+    md.use(inlinePlugin, 'image_punycode', 'image', function (tokens, idx) {
+      tokens[idx].src = punycodeURL(tokens[idx].src);
+    });
 
-    return text.replace(utf8WordCharcters, function(result, linkTag, preceeder, tagText) {
-      if(linkTag)
-        return linkTag;
-      else
-        return preceeder + "<a href='/tags/" + tagText.toLowerCase() +
-                           "' class='tag'>#" + tagText + "</a>";
+    var hashtagPlugin = window.markdownitHashtag;
+    md.use(hashtagPlugin, {
+      // compare tag_text_regexp in app/models/acts_as_taggable_on-tag.rb
+      hashtagRegExp: '[' + PosixBracketExpressions.alnum + '_\\-]+|<3',
+      // compare tag_strings in lib/diaspora/taggabe.rb
+      preceding: '^|\\s'
     });
-  };
 
-  textFormatter.mentionify = function mentionify(text, mentions) {
-    var mentionRegex = /@\{([^;]+); ([^\}]+)\}/g
-    return text.replace(mentionRegex, function(mentionText, fullName, diasporaId) {
-      var person = _.find(mentions, function(person){
-        return (diasporaId == person.diaspora_id || person.handle) //jquery.mentionsInput gives us person.handle
-      })
-      if(person) {
-        var url = person.url || "/people/" + person.guid //jquery.mentionsInput gives us person.url
-          , personText = "<a href='" + url + "' class='mention'>" + fullName + "</a>"
-      } else {
-        personText = fullName;
-      }
+    var mentionPlugin = window.markdownitDiasporaMention;
+    md.use(mentionPlugin, mentions);
 
-      return personText
-    })
-  }
+    // TODO this is a temporary fix
+    // remove it as soon as markdown-it fixes its autolinking feature
+    var linkifyPlugin = window.markdownitDiasporaLinkify;
+    md.use(linkifyPlugin);
 
-  app.helpers.textFormatter = textFormatter;
+    // Bootstrap table markup
+    md.renderer.rules.table_open = function () { return '<table class="table table-striped">\n'; };
+
+    return md.render(text);
+  };
 })();
 // @license-end
 
diff --git a/app/assets/javascripts/app/pages/single-post-viewer.js b/app/assets/javascripts/app/pages/single-post-viewer.js
index 1e730890e4123101b7b23979b3be44f954b6aea4..b5af389b3d5c263f3a7497246b25b4f76b1e0eae 100644
--- a/app/assets/javascripts/app/pages/single-post-viewer.js
+++ b/app/assets/javascripts/app/pages/single-post-viewer.js
@@ -33,7 +33,7 @@ app.pages.SinglePostViewer = app.views.Base.extend({
   postRenderTemplate : function() {
     if(this.model.get("title")){
       // formats title to html...
-      var html_title = app.helpers.textFormatter(this.model.get("title"), this.model);
+      var html_title = app.helpers.textFormatter(this.model.get("title"), this.model.get("mentioned_people"));
       //... and converts html to plain text
       document.title = $('<div>').html(html_title).text();
     }
diff --git a/app/assets/javascripts/app/views/comment_view.js b/app/assets/javascripts/app/views/comment_view.js
index daa5b31a64bae09201d0370013b1e2682dfd88de..f99bd7f664415f3fc5e5aae7e55dcf68e08be3ad 100644
--- a/app/assets/javascripts/app/views/comment_view.js
+++ b/app/assets/javascripts/app/views/comment_view.js
@@ -20,7 +20,7 @@ app.views.Comment = app.views.Content.extend({
   presenter : function() {
     return _.extend(this.defaultPresenter(), {
       canRemove: this.canRemove(),
-      text : app.helpers.textFormatter(this.model.get("text"), this.model)
+      text : app.helpers.textFormatter(this.model.get("text"))
     })
   },
 
diff --git a/app/assets/javascripts/app/views/content_view.js b/app/assets/javascripts/app/views/content_view.js
index 01226fdabb0924bacb8c00e8db49112b9ab41920..f5e7061e5df983e66c814a081fa0f1fe69649c0f 100644
--- a/app/assets/javascripts/app/views/content_view.js
+++ b/app/assets/javascripts/app/views/content_view.js
@@ -7,7 +7,7 @@ app.views.Content = app.views.Base.extend({
 
   presenter : function(){
     return _.extend(this.defaultPresenter(), {
-      text : app.helpers.textFormatter(this.model.get("text"), this.model),
+      text : app.helpers.textFormatter(this.model.get("text"), this.model.get("mentioned_people")),
       largePhoto : this.largePhoto(),
       smallPhotos : this.smallPhotos(),
       location: this.location()
diff --git a/app/assets/javascripts/app/views/post_view.js b/app/assets/javascripts/app/views/post_view.js
index 48473b9959ca1934be32d9a6c089c24b8111df9a..195b468e1d1a39c26d95dfac5ad7949b07bf1ff1 100644
--- a/app/assets/javascripts/app/views/post_view.js
+++ b/app/assets/javascripts/app/views/post_view.js
@@ -5,7 +5,7 @@ app.views.Post = app.views.Base.extend({
     return _.extend(this.defaultPresenter(), {
       authorIsCurrentUser : app.currentUser.isAuthorOf(this.model), 
       showPost : this.showPost(),
-      text : app.helpers.textFormatter(this.model.get("text"), this.model)
+      text : app.helpers.textFormatter(this.model.get("text"), this.model.get("mentioned_people"))
     })
   },
 
diff --git a/app/assets/javascripts/app/views/single-post-viewer/single_post_content_view.js b/app/assets/javascripts/app/views/single-post-viewer/single_post_content_view.js
index 87d81d5a523711f0b13953fbb71a0d7900d4b746..dba3b08e9b41653e9983aac4339d0ca1d0a74405 100644
--- a/app/assets/javascripts/app/views/single-post-viewer/single_post_content_view.js
+++ b/app/assets/javascripts/app/views/single-post-viewer/single_post_content_view.js
@@ -29,7 +29,7 @@ app.views.SinglePostContent = app.views.Base.extend({
     return _.extend(this.defaultPresenter(), {
       authorIsCurrentUser :app.currentUser.isAuthorOf(this.model),
       showPost : this.showPost(),
-      text : app.helpers.textFormatter(this.model.get("text"), this.model)
+      text : app.helpers.textFormatter(this.model.get("text"), this.model.get("mentioned_people"))
     })
   },
 
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index d683db631db8429fea4318af113b96b26bb6b93f..2e0e1714768e4b3db92c0a431c95b823b6f7a79c 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -23,7 +23,12 @@
 //= require keycodes
 //= require fileuploader-custom
 //= require handlebars.runtime
-//= require markdown
+//= require posix-bracket-expressions
+//= require markdown-it
+//= require markdown-it-hashtag
+//= require markdown-it-diaspora-linkify
+//= require markdown-it-diaspora-mention
+//= require markdown-it-for-inline
 //= require punycode
 //= require parse_url
 //= require clear-form
diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml
index b046af3245932c74927f5f69e92ece08c268e634..6ab014aaa7bee7c5bb14839b59824a750fde72e1 100644
--- a/config/locales/diaspora/en.yml
+++ b/config/locales/diaspora/en.yml
@@ -1102,8 +1102,8 @@ en:
       discard_post: "Discard post"
       new_user_prefill:
         newhere: "NewHere"
-        hello: "Hey everyone, I'm #%{new_user_tag}. "
-        i_like: "I'm interested in %{tags}. "
+        hello: "Hey everyone, I’m #%{new_user_tag}. "
+        i_like: "I’m interested in %{tags}. "
         invited_by: "Thanks for the invite, "
       poll:
         remove_poll_answer: "Remove option"
diff --git a/features/desktop/signs_up.feature b/features/desktop/signs_up.feature
index d5529f641fa3148d4277c73ab62a5ec9a94bf4da..ce8bc6c797247fc8ec4cfd7dcc2697f2503a32ab 100644
--- a/features/desktop/signs_up.feature
+++ b/features/desktop/signs_up.feature
@@ -44,7 +44,7 @@ Feature: new user registration
     And I confirm the alert
     Then I should be on the stream page
     When I submit the publisher
-    Then "Hey everyone, I'm #NewHere." should be post 1
+    Then "Hey everyone, I’m #NewHere." should be post 1
 
   Scenario: new user with some tags posts first status message
     When I fill in the following:
@@ -54,7 +54,7 @@ Feature: new user registration
     And I follow "awesome_button"
     Then I should be on the stream page
     When I submit the publisher
-    Then "Hey everyone, I'm #NewHere. I'm interested in #rockstar." should be post 1
+    Then "Hey everyone, I’m #NewHere. I’m interested in #rockstar." should be post 1
 
   Scenario: closing a popover clears getting started
     When I follow "awesome_button"
diff --git a/lib/assets/javascripts/markdown-it-diaspora-linkify.js b/lib/assets/javascripts/markdown-it-diaspora-linkify.js
new file mode 100644
index 0000000000000000000000000000000000000000..319497b0858027039707f0400ec7d0bce7233ab2
--- /dev/null
+++ b/lib/assets/javascripts/markdown-it-diaspora-linkify.js
@@ -0,0 +1,180 @@
+// TODO this is a temporary fix
+// remove it as soon as markdown-it fixes its autolinking feature
+
+/*! markdown-it-diaspora-linkify 0.1.0 https://github.com/diaspora/markdown-it-diaspora-linkify @license MIT */!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.markdownitDiasporaLinkify=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+'use strict';
+
+var ip = require('ip-regex').v4().source;
+var tlds = require('./tlds.json').join('|');
+
+/**
+ * Regular expression for matching URLs
+ *
+ * @param {Object} opts
+ * @api public
+ */
+
+module.exports = function (opts) {
+	opts = opts || {};
+
+	var auth = '(?:\\S+(?::\\S*)?@)?';
+	var domain = '(?:\\.(?:xn--[a-z0-9\\-]{1,59}|(?:[a-z\\u00a1-\\uffff0-9]+-?){0,62}[a-z\\u00a1-\\uffff0-9]{1,63}))*';
+	var host = '(?:xn--[a-z0-9\\-]{1,59}|(?:(?:[a-z\\u00a1-\\uffff0-9]+-?){0,62}[a-z\\u00a1-\\uffff0-9]{1,63}))';
+	var path = '(?:\/[^\\s]*)?';
+	var port = '(?::\\d{2,5})?';
+	var protocol = '(?:(?:(?:\\w)+:)?\/\/)?';
+	var tld = '(?:\\.(?:xn--[a-z0-9\\-]{1,59}|' + tlds + '+))';
+
+	var regex = [
+		protocol + auth + '(?:' + ip + '|',
+		'(?:localhost)|' + host + domain + tld + ')' + port + path
+	].join('');
+
+	return opts.exact ? new RegExp('(?:^' + regex + '$)', 'i') :
+						new RegExp('(?:^|\\s)(["\'])?' + regex + '\\1', 'ig');
+};
+
+},{"./tlds.json":3,"ip-regex":2}],2:[function(require,module,exports){
+'use strict';
+
+var v4 = '(?:25[0-5]|2[0-4][0-9]|1?[0-9][0-9]{1,2}|[0-9]){1,}(?:\\.(?:25[0-5]|2[0-4][0-9]|1?[0-9]{1,2}|0)){3}';
+var v6 = '(?:(?:[0-9a-fA-F:]){1,4}(?:(?::(?:[0-9a-fA-F]){1,4}|:)){2,7})+';
+
+var ip = module.exports = function (opts) {
+	opts = opts || {};
+	return opts.exact ? new RegExp('(?:^' + v4 + '$)|(?:^' + v6 + '$)') :
+	                    new RegExp('(?:' + v4 + ')|(?:' + v6 + ')', 'g');
+};
+
+ip.v4 = function (opts) {
+	opts = opts || {};
+	return opts.exact ? new RegExp('^' + v4 + '$') : new RegExp(v4, 'g');
+};
+
+ip.v6 = function (opts) {
+	opts = opts || {};
+	return opts.exact ? new RegExp('^' + v6 + '$') : new RegExp(v6, 'g');
+};
+
+},{}],3:[function(require,module,exports){
+module.exports=["vermögensberatung","vermögensberater","cancerresearch","international","versicherung","construction","contractors","engineering","motorcycles","சிங்கப்பூர்","accountants","investments","enterprises","williamhill","photography","blackfriday","productions","properties","healthcare","immobilien","university","republican","consulting","technology","industries","creditcard","cuisinella","foundation","restaurant","bnpparibas","associates","management","vlaanderen","furniture","bloomberg","equipment","melbourne","financial","education","directory","solutions","allfinanz","institute","christmas","community","vacations","marketing","training","capetown","pharmacy","partners","delivery","democrat","diamonds","software","discount","السعودية","saarland","catering","airforce","mortgage","attorney","services","engineer","supplies","cleaning","property","clothing","lighting","exchange","feedback","boutique","flsmidth","brussels","plumbing","budapest","computer","builders","business","yokohama","bargains","holdings","ventures","graphics","pictures","whoswho","dentist","recipes","digital","neustar","schmidt","realtor","shiksha","domains","network","support","android","youtube","college","cologne","surgery","capital","company","caravan","இந்தியா","abogado","academy","limited","careers","spiegel","lacaixa","exposed","cooking","finance","country","fishing","fitness","flights","florist","reviews","kitchen","channel","forsale","cricket","frogans","cruises","systems","الجزائر","gallery","science","auction","organic","okinawa","hosting","holiday","website","wedding","hamburg","rentals","singles","guitars","travel","google","hiphop","global","онлайн","москва","insure","futbol","joburg","juegos","kaufen","امارات","expert","lawyer","events","london","estate","luxury","maison","الاردن","market","energy","emerck","monash","moscow","المغرب","museum","nagoya","durban","direct","dental","degree","webcam","مليسيا","voyage","dating","otsuka","gratis","credit","photos","physio","condos","coffee","clinic","quebec","claims","reisen","vision","church","repair","report","chrome","center","villas","viajes","ryukyu","career","camera","இலங்கை","schule","فلسطين","yachts","social","yandex","berlin","bayern","supply","suzuki","sydney","alsace","taipei","tattoo","agency","active","tienda","voting","globo","mango","ایران","pizza","place","سورية","poker","praxi","press","jetzt","codes","media","vodka","homes","click","miami","citic","rehab","reise","works","horse","email","భారత్","بھارت","house","cheap","koeln","world","संगठन","rocks","rodeo","glass","nexus","cards","lease","gives","ninja","build","deals","black","shoes","بازار","watch","loans","solar","wales","vegas","space","guide","autos","lotto","audio","archi","green","gifts","paris","dance","tatar","parts","gripe","actor","cymru","photo","tirol","today","tokyo","tools","gmail","trade","party","aero","ভারত","شبكة","kiwi","pics","club","pink","army","kred","casa","pohl","land","post","cash","ਭਾਰਤ","vote","prod","lgbt","prof","life","भारत","ભારત","qpon","limo","link","buzz","ලංකා","arpa","تونس","luxe","reit","cern","fail","farm","desi","blue","rest","guru","diet","rich","meet","haus","meme","menu","rsvp","ruhr","fish","help","sarl","mini","mobi","moda","work","here","scot","beer","sexy","дети","asia","camp","best","cool","sohu","name","navy","wiki","host","coop","wien","yoga","dvag","surf","сайт","immo","city","عمان","info","bike","wang","fund","zone","voto","组织机构","tips","موقع","band","care","gbiz","jobs","town","toys","gent","gift","ltda","top","tel","uno","uol","tax","soy","scb","sca","vet","rip","rio","ren","red","pub","pro","ovh","org","ooo","onl","ong","nyc","nrw","nra","wed","nhk","ngo","new","net","mov","wme","moe","mil","krd","wtc","wtf","kim","int","我爱你","ink","қаз","ing","ibm","срб","орг","tui","hiv","мкд","中文网","gov","gop","gmx","gmo","gle","укр","мон","gal","frl","foo","fly","eus","esq","edu","eat","dnp","day","dad","crs","ไทย","com","рус","ceo","みんな","cat","cal","cab","مصر","قطر","bzh","boo","新加坡","bmw","xxx","xyz","biz","bio","bid","bar","axa","zip","how","pk","pl","er","hr","pm","pn","ht","hu","es","pr","id","ie","il","im","ca","ae","in","et","ps","pt","eu","pw","py","qa","as","al","re","bf","bg","bh","bi","zw","io","iq","ir","is","it","je","at","jm","jo","fi","af","jp","cr","ro","au","ke","rs","kg","ru","kh","rw","ki","sa","bj","am","sb","sc","fj","km","kn","fk","kp","kr","sd","se","an","ag","sg","sh","kw","ky","si","kz","sj","sk","sl","sm","sn","so","la","cu","aw","fm","lb","lc","fo","cv","sr","st","su","li","cw","cx","fr","cy","cc","sv","sx","sy","lk","cz","sz","cd","bm","lr","ls","tc","td","lt","ga","tf","tg","th","lu","ax","bn","tj","tk","tl","tm","tn","to","lv","ly","ac","gb","de","gd","tp","tr","ge","cf","mc","tt","md","tv","tw","tz","ua","ug","uk","me","gf","gg","us","uy","uz","va","gh","vc","ve","gi","cg","mg","mh","vg","vi","ch","ao","gl","mk","vn","ml","mm","mn","mo","bo","vu","az","ba","br","gm","ci","aq","bs","wf","mp","mq","mr","ms","mt","mu","gn","mv","ws","mw","mx","佛山","集团","在线","한국","my","八卦","mz","公益","公司","移动","na","ck","cl","dj","nc","ne","gp","삼성","gq","商标","商城","gr","dk","dm","中信","中国","中國","nf","ng","bt","do","ni","网络","gs","香港","台湾","台灣","手机","nl","no","np","nr","gt","gu","nu","ar","nz","ad","om","ai","გე","机构","gw","gy","dz","bb","рф","ec","bd","世界","pa","网址","游戏","cm","ee","企业","eg","hk","广东","pe","pf","pg","ph","政务","hm","hn","cn","co","ye","bv","bw","by","yt","za","bz","zm","be","ma"]
+},{}],4:[function(require,module,exports){
+// Replace link-like texts with link nodes.
+//
+'use strict';
+
+var urlRegex = require('url-regex');
+var LINK_SCAN_RE = /www|@|\:\/\//;
+
+function isLinkOpen(str) {
+  return /^<a[>\s]/i.test(str);
+}
+function isLinkClose(str) {
+  return /^<\/a\s*>/i.test(str);
+}
+
+module.exports = function linkify_plugin(md) {
+  var arrayReplaceAt = md.utils.arrayReplaceAt;
+
+  function linkify(state) {
+    var i, j, l, tokens, token, text, nodes, ln, pos, level, htmlLinkLevel,
+        blockTokens = state.tokens, links, href;
+
+    if (!state.md.options.linkify) { return; }
+
+    for (j = 0, l = blockTokens.length; j < l; j++) {
+      if (blockTokens[j].type !== 'inline') { continue; }
+      tokens = blockTokens[j].children;
+
+      htmlLinkLevel = 0;
+
+      // We scan from the end, to keep position when new tags added.
+      // Use reversed logic in links start/end match
+      for (i = tokens.length - 1; i >= 0; i--) {
+        token = tokens[i];
+
+        // Skip content of markdown links
+        if (token.type === 'link_close') {
+          i--;
+          while (tokens[i].level !== token.level && tokens[i].type !== 'link_open') {
+            i--;
+          }
+          continue;
+        }
+
+        // Skip content of html tag links
+        if (token.type === 'html_inline') {
+          if (isLinkOpen(token.content) && htmlLinkLevel > 0) {
+            htmlLinkLevel--;
+          }
+          if (isLinkClose(token.content)) {
+            htmlLinkLevel++;
+          }
+        }
+        if (htmlLinkLevel > 0) { continue; }
+
+        if (token.type === 'text' && LINK_SCAN_RE.test(token.content)) {
+
+          text = token.content;
+          links = text.match(urlRegex());
+
+          if (links === null || !links.length) { continue; }
+
+          // Now split string to nodes
+          nodes = [];
+          level = token.level;
+
+          for (ln = 0; ln < links.length; ln++) {
+
+            if (!state.md.inline.validateLink(links[ln])) { continue; }
+
+            pos = text.indexOf(links[ln]);
+
+            href = links[ln];
+
+            if (pos) {
+              level = level;
+              nodes.push({
+                type: 'text',
+                content: text.slice(0, pos),
+                level: level
+              });
+            }
+            nodes.push({
+              type: 'link_open',
+              href: href,
+              target: '',
+              title: '',
+              level: level++
+            });
+            nodes.push({
+              type: 'text',
+              content: links[ln],
+              level: level
+            });
+            nodes.push({
+              type: 'link_close',
+              level: --level
+            });
+            text = text.slice(pos + links[ln].length);
+          }
+          if (text.length) {
+            nodes.push({
+              type: 'text',
+              content: text,
+              level: level
+            });
+          }
+
+          // replace current node
+          blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
+        }
+      }
+    }
+  }
+
+  md.core.ruler.at('linkify', linkify);
+};
+
+},{"url-regex":1}]},{},[4])(4)
+});
diff --git a/lib/assets/javascripts/posix-bracket-expressions.js b/lib/assets/javascripts/posix-bracket-expressions.js
new file mode 100644
index 0000000000000000000000000000000000000000..900c574671e1689b4bc35a6172f788cbc152f069
--- /dev/null
+++ b/lib/assets/javascripts/posix-bracket-expressions.js
@@ -0,0 +1,571 @@
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+
+var PosixBracketExpressions = {
+  alnum : '\\u0030-\\u0039'
+        + '\\u0041-\\u005a'
+        + '\\u0061-\\u007a'
+        + '\\u00aa'
+        + '\\u00b5'
+        + '\\u00ba'
+        + '\\u00c0-\\u00d6'
+        + '\\u00d8-\\u00f6'
+        + '\\u00f8-\\u02c1'
+        + '\\u02c6-\\u02d1'
+        + '\\u02e0-\\u02e4'
+        + '\\u02ec'
+        + '\\u02ee'
+        + '\\u0345'
+        + '\\u0370-\\u0374'
+        + '\\u0376-\\u0377'
+        + '\\u037a-\\u037d'
+        + '\\u0386'
+        + '\\u0388-\\u038a'
+        + '\\u038c'
+        + '\\u038e-\\u03a1'
+        + '\\u03a3-\\u03f5'
+        + '\\u03f7-\\u0481'
+        + '\\u048a-\\u0527'
+        + '\\u0531-\\u0556'
+        + '\\u0559'
+        + '\\u0561-\\u0587'
+        + '\\u05b0-\\u05bd'
+        + '\\u05bf'
+        + '\\u05c1-\\u05c2'
+        + '\\u05c4-\\u05c5'
+        + '\\u05c7'
+        + '\\u05d0-\\u05ea'
+        + '\\u05f0-\\u05f2'
+        + '\\u0610-\\u061a'
+        + '\\u0620-\\u0657'
+        + '\\u0659-\\u0669'
+        + '\\u066e-\\u06d3'
+        + '\\u06d5-\\u06dc'
+        + '\\u06e1-\\u06e8'
+        + '\\u06ed-\\u06fc'
+        + '\\u06ff'
+        + '\\u0710-\\u073f'
+        + '\\u074d-\\u07b1'
+        + '\\u07c0-\\u07ea'
+        + '\\u07f4-\\u07f5'
+        + '\\u07fa'
+        + '\\u0800-\\u0817'
+        + '\\u081a-\\u082c'
+        + '\\u0840-\\u0858'
+        + '\\u08a0'
+        + '\\u08a2-\\u08ac'
+        + '\\u08e4-\\u08e9'
+        + '\\u08f0-\\u08fe'
+        + '\\u0900-\\u093b'
+        + '\\u093d-\\u094c'
+        + '\\u094e-\\u0950'
+        + '\\u0955-\\u0963'
+        + '\\u0966-\\u096f'
+        + '\\u0971-\\u0977'
+        + '\\u0979-\\u097f'
+        + '\\u0981-\\u0983'
+        + '\\u0985-\\u098c'
+        + '\\u098f-\\u0990'
+        + '\\u0993-\\u09a8'
+        + '\\u09aa-\\u09b0'
+        + '\\u09b2'
+        + '\\u09b6-\\u09b9'
+        + '\\u09bd-\\u09c4'
+        + '\\u09c7-\\u09c8'
+        + '\\u09cb-\\u09cc'
+        + '\\u09ce'
+        + '\\u09d7'
+        + '\\u09dc-\\u09dd'
+        + '\\u09df-\\u09e3'
+        + '\\u09e6-\\u09f1'
+        + '\\u0a01-\\u0a03'
+        + '\\u0a05-\\u0a0a'
+        + '\\u0a0f-\\u0a10'
+        + '\\u0a13-\\u0a28'
+        + '\\u0a2a-\\u0a30'
+        + '\\u0a32-\\u0a33'
+        + '\\u0a35-\\u0a36'
+        + '\\u0a38-\\u0a39'
+        + '\\u0a3e-\\u0a42'
+        + '\\u0a47-\\u0a48'
+        + '\\u0a4b-\\u0a4c'
+        + '\\u0a51'
+        + '\\u0a59-\\u0a5c'
+        + '\\u0a5e'
+        + '\\u0a66-\\u0a75'
+        + '\\u0a81-\\u0a83'
+        + '\\u0a85-\\u0a8d'
+        + '\\u0a8f-\\u0a91'
+        + '\\u0a93-\\u0aa8'
+        + '\\u0aaa-\\u0ab0'
+        + '\\u0ab2-\\u0ab3'
+        + '\\u0ab5-\\u0ab9'
+        + '\\u0abd-\\u0ac5'
+        + '\\u0ac7-\\u0ac9'
+        + '\\u0acb-\\u0acc'
+        + '\\u0ad0'
+        + '\\u0ae0-\\u0ae3'
+        + '\\u0ae6-\\u0aef'
+        + '\\u0b01-\\u0b03'
+        + '\\u0b05-\\u0b0c'
+        + '\\u0b0f-\\u0b10'
+        + '\\u0b13-\\u0b28'
+        + '\\u0b2a-\\u0b30'
+        + '\\u0b32-\\u0b33'
+        + '\\u0b35-\\u0b39'
+        + '\\u0b3d-\\u0b44'
+        + '\\u0b47-\\u0b48'
+        + '\\u0b4b-\\u0b4c'
+        + '\\u0b56-\\u0b57'
+        + '\\u0b5c-\\u0b5d'
+        + '\\u0b5f-\\u0b63'
+        + '\\u0b66-\\u0b6f'
+        + '\\u0b71'
+        + '\\u0b82-\\u0b83'
+        + '\\u0b85-\\u0b8a'
+        + '\\u0b8e-\\u0b90'
+        + '\\u0b92-\\u0b95'
+        + '\\u0b99-\\u0b9a'
+        + '\\u0b9c'
+        + '\\u0b9e-\\u0b9f'
+        + '\\u0ba3-\\u0ba4'
+        + '\\u0ba8-\\u0baa'
+        + '\\u0bae-\\u0bb9'
+        + '\\u0bbe-\\u0bc2'
+        + '\\u0bc6-\\u0bc8'
+        + '\\u0bca-\\u0bcc'
+        + '\\u0bd0'
+        + '\\u0bd7'
+        + '\\u0be6-\\u0bef'
+        + '\\u0c01-\\u0c03'
+        + '\\u0c05-\\u0c0c'
+        + '\\u0c0e-\\u0c10'
+        + '\\u0c12-\\u0c28'
+        + '\\u0c2a-\\u0c33'
+        + '\\u0c35-\\u0c39'
+        + '\\u0c3d-\\u0c44'
+        + '\\u0c46-\\u0c48'
+        + '\\u0c4a-\\u0c4c'
+        + '\\u0c55-\\u0c56'
+        + '\\u0c58-\\u0c59'
+        + '\\u0c60-\\u0c63'
+        + '\\u0c66-\\u0c6f'
+        + '\\u0c82-\\u0c83'
+        + '\\u0c85-\\u0c8c'
+        + '\\u0c8e-\\u0c90'
+        + '\\u0c92-\\u0ca8'
+        + '\\u0caa-\\u0cb3'
+        + '\\u0cb5-\\u0cb9'
+        + '\\u0cbd-\\u0cc4'
+        + '\\u0cc6-\\u0cc8'
+        + '\\u0cca-\\u0ccc'
+        + '\\u0cd5-\\u0cd6'
+        + '\\u0cde'
+        + '\\u0ce0-\\u0ce3'
+        + '\\u0ce6-\\u0cef'
+        + '\\u0cf1-\\u0cf2'
+        + '\\u0d02-\\u0d03'
+        + '\\u0d05-\\u0d0c'
+        + '\\u0d0e-\\u0d10'
+        + '\\u0d12-\\u0d3a'
+        + '\\u0d3d-\\u0d44'
+        + '\\u0d46-\\u0d48'
+        + '\\u0d4a-\\u0d4c'
+        + '\\u0d4e'
+        + '\\u0d57'
+        + '\\u0d60-\\u0d63'
+        + '\\u0d66-\\u0d6f'
+        + '\\u0d7a-\\u0d7f'
+        + '\\u0d82-\\u0d83'
+        + '\\u0d85-\\u0d96'
+        + '\\u0d9a-\\u0db1'
+        + '\\u0db3-\\u0dbb'
+        + '\\u0dbd'
+        + '\\u0dc0-\\u0dc6'
+        + '\\u0dcf-\\u0dd4'
+        + '\\u0dd6'
+        + '\\u0dd8-\\u0ddf'
+        + '\\u0df2-\\u0df3'
+        + '\\u0e01-\\u0e3a'
+        + '\\u0e40-\\u0e46'
+        + '\\u0e4d'
+        + '\\u0e50-\\u0e59'
+        + '\\u0e81-\\u0e82'
+        + '\\u0e84'
+        + '\\u0e87-\\u0e88'
+        + '\\u0e8a'
+        + '\\u0e8d'
+        + '\\u0e94-\\u0e97'
+        + '\\u0e99-\\u0e9f'
+        + '\\u0ea1-\\u0ea3'
+        + '\\u0ea5'
+        + '\\u0ea7'
+        + '\\u0eaa-\\u0eab'
+        + '\\u0ead-\\u0eb9'
+        + '\\u0ebb-\\u0ebd'
+        + '\\u0ec0-\\u0ec4'
+        + '\\u0ec6'
+        + '\\u0ecd'
+        + '\\u0ed0-\\u0ed9'
+        + '\\u0edc-\\u0edf'
+        + '\\u0f00'
+        + '\\u0f20-\\u0f29'
+        + '\\u0f40-\\u0f47'
+        + '\\u0f49-\\u0f6c'
+        + '\\u0f71-\\u0f81'
+        + '\\u0f88-\\u0f97'
+        + '\\u0f99-\\u0fbc'
+        + '\\u1000-\\u1036'
+        + '\\u1038'
+        + '\\u103b-\\u1049'
+        + '\\u1050-\\u1062'
+        + '\\u1065-\\u1068'
+        + '\\u106e-\\u1086'
+        + '\\u108e'
+        + '\\u1090-\\u1099'
+        + '\\u109c-\\u109d'
+        + '\\u10a0-\\u10c5'
+        + '\\u10c7'
+        + '\\u10cd'
+        + '\\u10d0-\\u10fa'
+        + '\\u10fc-\\u1248'
+        + '\\u124a-\\u124d'
+        + '\\u1250-\\u1256'
+        + '\\u1258'
+        + '\\u125a-\\u125d'
+        + '\\u1260-\\u1288'
+        + '\\u128a-\\u128d'
+        + '\\u1290-\\u12b0'
+        + '\\u12b2-\\u12b5'
+        + '\\u12b8-\\u12be'
+        + '\\u12c0'
+        + '\\u12c2-\\u12c5'
+        + '\\u12c8-\\u12d6'
+        + '\\u12d8-\\u1310'
+        + '\\u1312-\\u1315'
+        + '\\u1318-\\u135a'
+        + '\\u135f'
+        + '\\u1380-\\u138f'
+        + '\\u13a0-\\u13f4'
+        + '\\u1401-\\u166c'
+        + '\\u166f-\\u167f'
+        + '\\u1681-\\u169a'
+        + '\\u16a0-\\u16ea'
+        + '\\u16ee-\\u16f0'
+        + '\\u1700-\\u170c'
+        + '\\u170e-\\u1713'
+        + '\\u1720-\\u1733'
+        + '\\u1740-\\u1753'
+        + '\\u1760-\\u176c'
+        + '\\u176e-\\u1770'
+        + '\\u1772-\\u1773'
+        + '\\u1780-\\u17b3'
+        + '\\u17b6-\\u17c8'
+        + '\\u17d7'
+        + '\\u17dc'
+        + '\\u17e0-\\u17e9'
+        + '\\u1810-\\u1819'
+        + '\\u1820-\\u1877'
+        + '\\u1880-\\u18aa'
+        + '\\u18b0-\\u18f5'
+        + '\\u1900-\\u191c'
+        + '\\u1920-\\u192b'
+        + '\\u1930-\\u1938'
+        + '\\u1946-\\u196d'
+        + '\\u1970-\\u1974'
+        + '\\u1980-\\u19ab'
+        + '\\u19b0-\\u19c9'
+        + '\\u19d0-\\u19d9'
+        + '\\u1a00-\\u1a1b'
+        + '\\u1a20-\\u1a5e'
+        + '\\u1a61-\\u1a74'
+        + '\\u1a80-\\u1a89'
+        + '\\u1a90-\\u1a99'
+        + '\\u1aa7'
+        + '\\u1b00-\\u1b33'
+        + '\\u1b35-\\u1b43'
+        + '\\u1b45-\\u1b4b'
+        + '\\u1b50-\\u1b59'
+        + '\\u1b80-\\u1ba9'
+        + '\\u1bac-\\u1be5'
+        + '\\u1be7-\\u1bf1'
+        + '\\u1c00-\\u1c35'
+        + '\\u1c40-\\u1c49'
+        + '\\u1c4d-\\u1c7d'
+        + '\\u1ce9-\\u1cec'
+        + '\\u1cee-\\u1cf3'
+        + '\\u1cf5-\\u1cf6'
+        + '\\u1d00-\\u1dbf'
+        + '\\u1e00-\\u1f15'
+        + '\\u1f18-\\u1f1d'
+        + '\\u1f20-\\u1f45'
+        + '\\u1f48-\\u1f4d'
+        + '\\u1f50-\\u1f57'
+        + '\\u1f59'
+        + '\\u1f5b'
+        + '\\u1f5d'
+        + '\\u1f5f-\\u1f7d'
+        + '\\u1f80-\\u1fb4'
+        + '\\u1fb6-\\u1fbc'
+        + '\\u1fbe'
+        + '\\u1fc2-\\u1fc4'
+        + '\\u1fc6-\\u1fcc'
+        + '\\u1fd0-\\u1fd3'
+        + '\\u1fd6-\\u1fdb'
+        + '\\u1fe0-\\u1fec'
+        + '\\u1ff2-\\u1ff4'
+        + '\\u1ff6-\\u1ffc'
+        + '\\u2071'
+        + '\\u207f'
+        + '\\u2090-\\u209c'
+        + '\\u2102'
+        + '\\u2107'
+        + '\\u210a-\\u2113'
+        + '\\u2115'
+        + '\\u2119-\\u211d'
+        + '\\u2124'
+        + '\\u2126'
+        + '\\u2128'
+        + '\\u212a-\\u212d'
+        + '\\u212f-\\u2139'
+        + '\\u213c-\\u213f'
+        + '\\u2145-\\u2149'
+        + '\\u214e'
+        + '\\u2160-\\u2188'
+        + '\\u24b6-\\u24e9'
+        + '\\u2c00-\\u2c2e'
+        + '\\u2c30-\\u2c5e'
+        + '\\u2c60-\\u2ce4'
+        + '\\u2ceb-\\u2cee'
+        + '\\u2cf2-\\u2cf3'
+        + '\\u2d00-\\u2d25'
+        + '\\u2d27'
+        + '\\u2d2d'
+        + '\\u2d30-\\u2d67'
+        + '\\u2d6f'
+        + '\\u2d80-\\u2d96'
+        + '\\u2da0-\\u2da6'
+        + '\\u2da8-\\u2dae'
+        + '\\u2db0-\\u2db6'
+        + '\\u2db8-\\u2dbe'
+        + '\\u2dc0-\\u2dc6'
+        + '\\u2dc8-\\u2dce'
+        + '\\u2dd0-\\u2dd6'
+        + '\\u2dd8-\\u2dde'
+        + '\\u2de0-\\u2dff'
+        + '\\u2e2f'
+        + '\\u3005-\\u3007'
+        + '\\u3021-\\u3029'
+        + '\\u3031-\\u3035'
+        + '\\u3038-\\u303c'
+        + '\\u3041-\\u3096'
+        + '\\u309d-\\u309f'
+        + '\\u30a1-\\u30fa'
+        + '\\u30fc-\\u30ff'
+        + '\\u3105-\\u312d'
+        + '\\u3131-\\u318e'
+        + '\\u31a0-\\u31ba'
+        + '\\u31f0-\\u31ff'
+        + '\\u3400-\\u4db5'
+        + '\\u4e00-\\u9fcc'
+        + '\\ua000-\\ua48c'
+        + '\\ua4d0-\\ua4fd'
+        + '\\ua500-\\ua60c'
+        + '\\ua610-\\ua62b'
+        + '\\ua640-\\ua66e'
+        + '\\ua674-\\ua67b'
+        + '\\ua67f-\\ua697'
+        + '\\ua69f-\\ua6ef'
+        + '\\ua717-\\ua71f'
+        + '\\ua722-\\ua788'
+        + '\\ua78b-\\ua78e'
+        + '\\ua790-\\ua793'
+        + '\\ua7a0-\\ua7aa'
+        + '\\ua7f8-\\ua801'
+        + '\\ua803-\\ua805'
+        + '\\ua807-\\ua80a'
+        + '\\ua80c-\\ua827'
+        + '\\ua840-\\ua873'
+        + '\\ua880-\\ua8c3'
+        + '\\ua8d0-\\ua8d9'
+        + '\\ua8f2-\\ua8f7'
+        + '\\ua8fb'
+        + '\\ua900-\\ua92a'
+        + '\\ua930-\\ua952'
+        + '\\ua960-\\ua97c'
+        + '\\ua980-\\ua9b2'
+        + '\\ua9b4-\\ua9bf'
+        + '\\ua9cf-\\ua9d9'
+        + '\\uaa00-\\uaa36'
+        + '\\uaa40-\\uaa4d'
+        + '\\uaa50-\\uaa59'
+        + '\\uaa60-\\uaa76'
+        + '\\uaa7a'
+        + '\\uaa80-\\uaabe'
+        + '\\uaac0'
+        + '\\uaac2'
+        + '\\uaadb-\\uaadd'
+        + '\\uaae0-\\uaaef'
+        + '\\uaaf2-\\uaaf5'
+        + '\\uab01-\\uab06'
+        + '\\uab09-\\uab0e'
+        + '\\uab11-\\uab16'
+        + '\\uab20-\\uab26'
+        + '\\uab28-\\uab2e'
+        + '\\uabc0-\\uabea'
+        + '\\uabf0-\\uabf9'
+        + '\\uac00-\\ud7a3'
+        + '\\ud7b0-\\ud7c6'
+        + '\\ud7cb-\\ud7fb'
+        + '\\uf900-\\ufa6d'
+        + '\\ufa70-\\ufad9'
+        + '\\ufb00-\\ufb06'
+        + '\\ufb13-\\ufb17'
+        + '\\ufb1d-\\ufb28'
+        + '\\ufb2a-\\ufb36'
+        + '\\ufb38-\\ufb3c'
+        + '\\ufb3e'
+        + '\\ufb40-\\ufb41'
+        + '\\ufb43-\\ufb44'
+        + '\\ufb46-\\ufbb1'
+        + '\\ufbd3-\\ufd3d'
+        + '\\ufd50-\\ufd8f'
+        + '\\ufd92-\\ufdc7'
+        + '\\ufdf0-\\ufdfb'
+        + '\\ufe70-\\ufe74'
+        + '\\ufe76-\\ufefc'
+        + '\\uff10-\\uff19'
+        + '\\uff21-\\uff3a'
+        + '\\uff41-\\uff5a'
+        + '\\uff66-\\uffbe'
+        + '\\uffc2-\\uffc7'
+        + '\\uffca-\\uffcf'
+        + '\\uffd2-\\uffd7'
+        + '\\uffda-\\uffdc'
+        + '\\u10000-\\u1000b'
+        + '\\u1000d-\\u10026'
+        + '\\u10028-\\u1003a'
+        + '\\u1003c-\\u1003d'
+        + '\\u1003f-\\u1004d'
+        + '\\u10050-\\u1005d'
+        + '\\u10080-\\u100fa'
+        + '\\u10140-\\u10174'
+        + '\\u10280-\\u1029c'
+        + '\\u102a0-\\u102d0'
+        + '\\u10300-\\u1031e'
+        + '\\u10330-\\u1034a'
+        + '\\u10380-\\u1039d'
+        + '\\u103a0-\\u103c3'
+        + '\\u103c8-\\u103cf'
+        + '\\u103d1-\\u103d5'
+        + '\\u10400-\\u1049d'
+        + '\\u104a0-\\u104a9'
+        + '\\u10800-\\u10805'
+        + '\\u10808'
+        + '\\u1080a-\\u10835'
+        + '\\u10837-\\u10838'
+        + '\\u1083c'
+        + '\\u1083f-\\u10855'
+        + '\\u10900-\\u10915'
+        + '\\u10920-\\u10939'
+        + '\\u10980-\\u109b7'
+        + '\\u109be-\\u109bf'
+        + '\\u10a00-\\u10a03'
+        + '\\u10a05-\\u10a06'
+        + '\\u10a0c-\\u10a13'
+        + '\\u10a15-\\u10a17'
+        + '\\u10a19-\\u10a33'
+        + '\\u10a60-\\u10a7c'
+        + '\\u10b00-\\u10b35'
+        + '\\u10b40-\\u10b55'
+        + '\\u10b60-\\u10b72'
+        + '\\u10c00-\\u10c48'
+        + '\\u11000-\\u11045'
+        + '\\u11066-\\u1106f'
+        + '\\u11082-\\u110b8'
+        + '\\u110d0-\\u110e8'
+        + '\\u110f0-\\u110f9'
+        + '\\u11100-\\u11132'
+        + '\\u11136-\\u1113f'
+        + '\\u11180-\\u111bf'
+        + '\\u111c1-\\u111c4'
+        + '\\u111d0-\\u111d9'
+        + '\\u11680-\\u116b5'
+        + '\\u116c0-\\u116c9'
+        + '\\u12000-\\u1236e'
+        + '\\u12400-\\u12462'
+        + '\\u13000-\\u1342e'
+        + '\\u16800-\\u16a38'
+        + '\\u16f00-\\u16f44'
+        + '\\u16f50-\\u16f7e'
+        + '\\u16f93-\\u16f9f'
+        + '\\u1b000-\\u1b001'
+        + '\\u1d400-\\u1d454'
+        + '\\u1d456-\\u1d49c'
+        + '\\u1d49e-\\u1d49f'
+        + '\\u1d4a2'
+        + '\\u1d4a5-\\u1d4a6'
+        + '\\u1d4a9-\\u1d4ac'
+        + '\\u1d4ae-\\u1d4b9'
+        + '\\u1d4bb'
+        + '\\u1d4bd-\\u1d4c3'
+        + '\\u1d4c5-\\u1d505'
+        + '\\u1d507-\\u1d50a'
+        + '\\u1d50d-\\u1d514'
+        + '\\u1d516-\\u1d51c'
+        + '\\u1d51e-\\u1d539'
+        + '\\u1d53b-\\u1d53e'
+        + '\\u1d540-\\u1d544'
+        + '\\u1d546'
+        + '\\u1d54a-\\u1d550'
+        + '\\u1d552-\\u1d6a5'
+        + '\\u1d6a8-\\u1d6c0'
+        + '\\u1d6c2-\\u1d6da'
+        + '\\u1d6dc-\\u1d6fa'
+        + '\\u1d6fc-\\u1d714'
+        + '\\u1d716-\\u1d734'
+        + '\\u1d736-\\u1d74e'
+        + '\\u1d750-\\u1d76e'
+        + '\\u1d770-\\u1d788'
+        + '\\u1d78a-\\u1d7a8'
+        + '\\u1d7aa-\\u1d7c2'
+        + '\\u1d7c4-\\u1d7cb'
+        + '\\u1d7ce-\\u1d7ff'
+        + '\\u1ee00-\\u1ee03'
+        + '\\u1ee05-\\u1ee1f'
+        + '\\u1ee21-\\u1ee22'
+        + '\\u1ee24'
+        + '\\u1ee27'
+        + '\\u1ee29-\\u1ee32'
+        + '\\u1ee34-\\u1ee37'
+        + '\\u1ee39'
+        + '\\u1ee3b'
+        + '\\u1ee42'
+        + '\\u1ee47'
+        + '\\u1ee49'
+        + '\\u1ee4b'
+        + '\\u1ee4d-\\u1ee4f'
+        + '\\u1ee51-\\u1ee52'
+        + '\\u1ee54'
+        + '\\u1ee57'
+        + '\\u1ee59'
+        + '\\u1ee5b'
+        + '\\u1ee5d'
+        + '\\u1ee5f'
+        + '\\u1ee61-\\u1ee62'
+        + '\\u1ee64'
+        + '\\u1ee67-\\u1ee6a'
+        + '\\u1ee6c-\\u1ee72'
+        + '\\u1ee74-\\u1ee77'
+        + '\\u1ee79-\\u1ee7c'
+        + '\\u1ee7e'
+        + '\\u1ee80-\\u1ee89'
+        + '\\u1ee8b-\\u1ee9b'
+        + '\\u1eea1-\\u1eea3'
+        + '\\u1eea5-\\u1eea9'
+        + '\\u1eeab-\\u1eebb'
+        + '\\u20000-\\u2a6d6'
+        + '\\u2a700-\\u2b734'
+        + '\\u2b740-\\u2b81d'
+        + '\\u2f800-\\u2fa1d'
+};
+// @license-end
diff --git a/spec/javascripts/app/helpers/text_formatter_spec.js b/spec/javascripts/app/helpers/text_formatter_spec.js
index 14d8a149c8d55485ae61f626b94b692f3f13aff8..9de8e584f749c416b67d833b32898e2e3c14612c 100644
--- a/spec/javascripts/app/helpers/text_formatter_spec.js
+++ b/spec/javascripts/app/helpers/text_formatter_spec.js
@@ -5,26 +5,80 @@ describe("app.helpers.textFormatter", function(){
     this.formatter = app.helpers.textFormatter;
   })
 
-  describe("main", function(){
-    it("calls mentionify, hashtagify, and markdownify", function(){
-      spyOn(app.helpers.textFormatter, "mentionify")
-      spyOn(app.helpers.textFormatter, "hashtagify")
-      spyOn(app.helpers.textFormatter, "markdownify")
-
-      app.helpers.textFormatter(this.statusMessage.get("text"), this.statusMessage)
-      expect(app.helpers.textFormatter.mentionify).toHaveBeenCalled()
-      expect(app.helpers.textFormatter.hashtagify).toHaveBeenCalled()
-      expect(app.helpers.textFormatter.markdownify).toHaveBeenCalled()
+  // Some basic specs. For more detailed specs see
+  // https://github.com/svbergerem/markdown-it-hashtag/tree/master/test
+  context("hashtags", function() {
+    beforeEach(function() {
+      this.tags = [
+        "tag",
+        "diaspora",
+        "PARTIES",
+        "<3"
+      ];
+    });
+
+    it("renders tags as links", function() {
+      var formattedText = this.formatter('#'+this.tags.join(" #"));
+      _.each(this.tags, function(tag) {
+        var link ='<a class="tag" href="/tags/'+tag.toLowerCase()+'">#'+tag+'</a>';
+        expect(formattedText).toContain(link);
+      });
+    });
+  });
+
+  // Some basic specs. For more detailed specs see
+  // https://github.com/diaspora/markdown-it-diaspora-mention/tree/master/test
+  context("mentions", function() {
+    beforeEach(function(){
+      this.alice = factory.author({
+        name : "Alice Smith",
+        diaspora_id : "alice@example.com",
+        id : "555"
+      })
+
+      this.bob = factory.author({
+        name : "Bob Grimm",
+        diaspora_id : "bob@example.com",
+        id : "666"
+      })
+
+      this.statusMessage.set({text: "hey there @{Alice Smith; alice@example.com} and @{Bob Grimm; bob@example.com}"})
+      this.statusMessage.set({mentioned_people : [this.alice, this.bob]})
     })
 
-    // A couple of complex (intergration) test cases here would be rad.
-  })
+    it("matches mentions", function(){
+      var formattedText = this.formatter(this.statusMessage.get("text"), this.statusMessage.get("mentioned_people"))
+      var wrapper = $("<div>").html(formattedText);
+
+      _.each([this.alice, this.bob], function(person) {
+        expect(wrapper.find("a[href='/people/" + person.guid + "']").text()).toContain(person.name)
+      })
+    });
+
+    it("returns mentions for on posts that haven't been saved yet (framer posts)", function(){
+      var freshBob = factory.author({
+        name : "Bob Grimm",
+        handle : "bob@example.com",
+        url : 'googlebot.com',
+        id : "666"
+      })
 
-  describe(".markdownify", function(){
-    // NOTE: for some strange reason, links separated by just a whitespace character
-    // will not be autolinked; thus we join our URLS here with (" and ").
-    // This test will fail if our join is just (" ") -- an edge case that should be addressed.
+      this.statusMessage.set({'mentioned_people' : [freshBob] })
 
+      var formattedText = this.formatter(this.statusMessage.get("text"), this.statusMessage.get("mentioned_people"))
+      var wrapper = $("<div>").html(formattedText);
+      expect(wrapper.find("a[href='googlebot.com']").text()).toContain(freshBob.name)
+    })
+
+    it('returns the name of the mention if the mention does not exist in the array', function(){
+      var text = "hey there @{Chris Smith; chris@example.com}"
+      var formattedText = this.formatter(text, [])
+      expect(formattedText.match(/\<a/)).toBeNull();
+      expect(formattedText).toContain('Chris Smith');
+    });
+  });
+
+  context("markdown", function(){
     it("autolinks", function(){
       var links = [
         "http://google.com",
@@ -37,11 +91,7 @@ describe("app.helpers.textFormatter", function(){
         "www.google.com"
       ];
 
-      // The join that would make this particular test fail:
-      //
-      // var formattedText = this.formatter.markdownify(links.join(" "))
-
-      var formattedText = this.formatter.markdownify(links.join(" and "));
+      var formattedText = this.formatter(links.join(" "));
       var wrapper = $("<div>").html(formattedText);
 
       _.each(links, function(link) {
@@ -67,13 +117,13 @@ describe("app.helpers.textFormatter", function(){
 
       it("correctly converts the input strings to their corresponding output symbol", function() {
         _.each(this.input_strings, function(str, idx) {
-          var text = this.formatter.markdownify(str);
+          var text = this.formatter(str);
           expect(text).toContain(this.output_symbols[idx]);
         }, this);
       });
 
       it("converts all symbols at once", function() {
-        var text = this.formatter.markdownify(this.input_strings.join(" "));
+        var text = this.formatter(this.input_strings.join(" "));
         _.each(this.output_symbols, function(sym) {
           expect(text).toContain(sym);
         });
@@ -97,21 +147,21 @@ describe("app.helpers.textFormatter", function(){
           "http://xn--4gbrim.xn----ymcbaaajlc6dj7bxne2c.xn--wgbh1c/",
           "http:///scholar.google.com/citations?view_op=top_venues",
           "http://lyricstranslate.com/en/someone-you-%E0%B4%A8%E0%B4%BF%E0%B4%A8%E0%B5%8D%E0%B4%A8%E0%B5%86-%E0%B4%AA%E0%B5%8B%E0%B4%B2%E0%B5%8A%E0%B4%B0%E0%B4%BE%E0%B4%B3%E0%B5%8D%E2%80%8D.html",
-          "http://de.wikipedia.org/wiki/Liste_der_Abk%C3%BCrzungen_%28Netzjargon%29",
+          "http://de.wikipedia.org/wiki/Liste_der_Abk%C3%BCrzungen_(Netzjargon)",
           "http://wiki.com/?query=Kr%E4fte",
         ];
       });
 
       it("correctly encodes to punycode", function() {
         _.each(this.evilUrls, function(url, num) {
-          var text = this.formatter.markdownify( "<" + url + ">" );
+          var text = this.formatter( "<" + url + ">" );
           expect(text).toContain(this.asciiUrls[num]);
         }, this);
       });
 
       it("doesn't break link texts", function() {
         var linkText = "check out this awesome link!";
-        var text = this.formatter.markdownify( "["+linkText+"]("+this.evilUrls[0]+")" );
+        var text = this.formatter( "["+linkText+"]("+this.evilUrls[0]+")" );
 
         expect(text).toContain(this.asciiUrls[0]);
         expect(text).toContain(linkText);
@@ -119,30 +169,29 @@ describe("app.helpers.textFormatter", function(){
 
       it("doesn't break reference style links", function() {
         var postContent = "blabla blab [my special link][1] bla blabla\n\n[1]: "+this.evilUrls[0]+" and an optional title)";
-        var text = this.formatter.markdownify(postContent);
+        var text = this.formatter(postContent);
 
-        expect(text).not.toContain(this.evilUrls[0]);
+        expect(text).not.toContain('"'+this.evilUrls[0]+'"');
         expect(text).toContain(this.asciiUrls[0]);
       });
 
       it("can be used as img src", function() {
         var postContent = "![logo]("+ this.evilUrls[1] +")";
         var niceImg = 'src="'+ this.asciiUrls[1] +'"'; // the "" are from src=""
-        var text = this.formatter.markdownify(postContent);
+        var text = this.formatter(postContent);
 
         expect(text).toContain(niceImg);
       });
 
       it("doesn't break linked images", function() {
         var postContent = "I am linking an image here [![some-alt-text]("+this.evilUrls[1]+")]("+this.evilUrls[3]+")";
-        var text = this.formatter.markdownify(postContent);
+        var text = this.formatter(postContent);
         var linked_image = 'src="'+this.asciiUrls[1]+'"';
         var image_link = 'href="'+this.asciiUrls[3]+'"';
 
         expect(text).toContain(linked_image);
         expect(text).toContain(image_link);
       });
-
     });
 
     context("misc breakage and/or other issues with weird urls", function(){
@@ -151,10 +200,10 @@ describe("app.helpers.textFormatter", function(){
         var text_part = 'Revert "rails admin is conflicting with client side validations: see https://github.com/sferik/rails_admin/issues/985"';
         var link_part = 'https://github.com/diaspora/diaspora/commit/61f40fc6bfe6bb859c995023b5a17d22c9b5e6e5';
         var content = '['+text_part+']('+link_part+')';
-        var parsed = this.formatter.markdownify(content);
+        var parsed = this.formatter(content);
 
         var link = 'href="' + link_part + '"';
-        var text = '>'+ text_part +'<';
+        var text = '>Revert “rails admin is conflicting with client side validations: see https://github.com/sferik/rails_admin/issues/985”<';
 
         expect(parsed).toContain(link);
         expect(parsed).toContain(text);
@@ -167,149 +216,16 @@ describe("app.helpers.textFormatter", function(){
         });
 
         it("doesn't get double-encoded", function(){
-          var parsed = this.formatter.markdownify(this.input);
+          var parsed = this.formatter(this.input);
           expect(parsed).toContain(this.correctHref);
         });
 
         it("gets correctly decoded, even when multiply encoded", function() {
           var uglyUrl = encodeURI(encodeURI(encodeURI(this.input)));
-          var parsed = this.formatter.markdownify(uglyUrl);
+          var parsed = this.formatter(uglyUrl);
           expect(parsed).toContain(this.correctHref);
         });
       });
-
-      it("tests a bunch of benchmark urls", function(){
-        var self = this;
-        $.ajax({
-          async: false,
-          cache: false,
-          url: '/spec/fixtures/good_urls.txt',
-          success: function(data) { self.url_list = data.split("\n"); }
-        });
-
-        _.each(this.url_list, function(url) {
-          // 'comments'
-          if( url.match(/^#/) ) return;
-
-          // regex.test is stupid, use match and boolean-ify it
-          var result = !!url.match(Diaspora.url_regex);
-          expect(result).toBeTruthy();
-          if( !result && console && console.log ) {
-            console.log(url);
-          }
-        });
-      });
-
-      // TODO: try to match the 'bad_urls.txt' and have as few matches as possible
     });
-
-  })
-
-  describe(".hashtagify", function(){
-    context("changes hashtags to links", function(){
-      it("creates links to hashtags", function(){
-        var formattedText = this.formatter.hashtagify("I love #parties and #rockstars and #unicorns")
-        var wrapper = $("<div>").html(formattedText);
-
-        _.each(["parties", "rockstars", "unicorns"], function(tagName){
-          expect(wrapper.find("a[href='/tags/" + tagName + "']").text()).toContain(tagName)
-        })
-      })
-
-      it("requires hashtags to be preceeded with a space", function(){
-        var formattedText = this.formatter.hashtagify("I love the#parties")
-        expect(formattedText).not.toContain('/tags/parties')
-      })
-
-      // NOTE THIS DIVERGES FROM GRUBER'S ORIGINAL DIALECT OF MARKDOWN.
-      // We had to edit Markdown.Converter.js line 747
-      //
-      //    text = text.replace(/^(\#{1,6})[ \t]+(.+?)[ \t]*\#*\n+/gm,
-      //    [ \t]* changed to [ \t]+
-      //
-      it("doesn't create a header tag if the first word is a hashtag", function(){
-        var formattedText = this.formatter.hashtagify("#parties, I love")
-        var wrapper = $("<div>").html(formattedText);
-
-        expect(wrapper.find("h1").length).toBe(0)
-        expect(wrapper.find("a[href='/tags/parties']").text()).toContain("#parties")
-      })
-
-      it("and the resultant link has the tags name downcased", function(){
-        var formattedText = this.formatter.hashtagify("#PARTIES, I love")
-
-        expect(formattedText).toContain("/tags/parties")
-      })
-
-      it("doesn't create tag if the text is a link", function(){
-        var tags = ['diaspora', 'twitter', 'hrabrahabr'];
-
-        var text = $('<a/>', { href: 'http://me.co' }).html('#me')[0].outerHTML;
-        _.each(tags, function(tagName){
-          text += ' #'+tagName+',';
-        });
-        text += 'I love';
-
-        var formattedText = this.formatter.hashtagify(text);
-        var wrapper = $('<div>').html(formattedText);
-
-        expect(wrapper.find("a[href='http://me.co']").text()).toContain('#me');
-        _.each(tags, function(tagName){
-          expect(wrapper.find("a[href='/tags/"+tagName+"']").text()).toContain('#'+tagName);
-        });
-
-      })
-    })
-  })
-
-  describe(".mentionify", function(){
-    context("changes mention markup to links", function(){
-      beforeEach(function(){
-        this.alice = factory.author({
-          name : "Alice Smith",
-          diaspora_id : "alice@example.com",
-          id : "555"
-        })
-
-        this.bob = factory.author({
-          name : "Bob Grimm",
-          diaspora_id : "bob@example.com",
-          id : "666"
-        })
-
-        this.statusMessage.set({text: "hey there @{Alice Smith; alice@example.com} and @{Bob Grimm; bob@example.com}"})
-        this.statusMessage.set({mentioned_people : [this.alice, this.bob]})
-      })
-
-      it("matches mentions", function(){
-        var formattedText = this.formatter.mentionify(this.statusMessage.get("text"), this.statusMessage.get("mentioned_people"))
-        var wrapper = $("<div>").html(formattedText);
-
-        _.each([this.alice, this.bob], function(person) {
-          expect(wrapper.find("a[href='/people/" + person.guid + "']").text()).toContain(person.name)
-        })
-      });
-
-      it("returns mentions for on posts that haven't been saved yet (framer posts)", function(){
-        var freshBob = factory.author({
-          name : "Bob Grimm",
-          handle : "bob@example.com",
-          url : 'googlebot.com',
-          id : "666"
-        })
-
-        this.statusMessage.set({'mentioned_people' : [freshBob] })
-
-        var formattedText = this.formatter.mentionify(this.statusMessage.get("text"), this.statusMessage.get("mentioned_people"))
-        var wrapper = $("<div>").html(formattedText);
-        expect(wrapper.find("a[href='googlebot.com']").text()).toContain(freshBob.name)
-      })
-
-      it('returns the name of the mention if the mention does not exist in the array', function(){
-        var text = "hey there @{Chris Smith; chris@example.com}"
-        var formattedText = this.formatter.mentionify(text, [])
-        expect(formattedText.match(/\<a/)).toBeNull();
-      });
-    })
-  })
+  });
 })
diff --git a/vendor/assets/javascripts/markdown.js b/vendor/assets/javascripts/markdown.js
deleted file mode 100644
index 88dd96256e996b2665545daf64c85b95722ca406..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/markdown.js
+++ /dev/null
@@ -1 +0,0 @@
-//= require_tree ./markdown
\ No newline at end of file
diff --git a/vendor/assets/javascripts/markdown/Markdown.Converter.js b/vendor/assets/javascripts/markdown/Markdown.Converter.js
deleted file mode 100644
index 34a624b3a590c04cca3dd539b9d3db9623bae70b..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/markdown/Markdown.Converter.js
+++ /dev/null
@@ -1,1332 +0,0 @@
-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 <a>
-            // and <img> 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]*
-                <?(\S+?)>?          // 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+?)>?(?=\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, "&quot;"));
-                    }
-
-                    // 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 <p>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.:
-            //   <div>
-            //     <div>
-            //     tags for inner block must be indented.
-            //     </div>
-            //   </div>
-            //
-            // 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 `<div>` and stop at the first `</div>`.
-
-            // 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
-                    </\2>               // 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<tag>` to `</tag>\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
-                    .*</\2>             // 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 <hr />. 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 = "<hr />\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
-            // <p> 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 `<http://example.com/>`
-            // Must come after _DoAnchors(), because you can use < and >
-            // delimiters in inline links like [this](<url>).
-            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, " <br>\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 <a> 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
-                    <?(                     // href = $4
-                        (?:
-                            \([^)]*\)       // allow one level of (correctly nested) parens (think MSDN)
-                            |
-                            [^()]
-                        )*?
-                    )>?
-                    [ \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 = "<a href=\"" + url + "\"";
-
-            if (title != "") {
-                title = attributeEncode(title);
-                title = escapeCharacters(title, "*_");
-                result += " title=\"" + title + "\"";
-            }
-
-            result += ">" + link_text + "</a>";
-
-            return result;
-        }
-
-        function _DoImages(text) {
-            //
-            // Turn Markdown image shortcuts into <img> 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
-                    <?(\S+?)>?      // src url = $4
-                    [ \t]*
-                    (               // $5
-                        (['"])      // quote char = $6
-                        (.*?)       // title = $7
-                        \6          // matching quote
-                        [ \t]*
-                    )?              // title is optional
-                    \)
-                )
-            /g, writeImageTag);
-            */
-            text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()<?(\S+?)>?[ \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, "&gt;").replace(/</g, "&lt;").replace(/"/g, "&quot;");
-        }
-
-        function writeImageTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
-            var whole_match = m1;
-            var alt_text = m2;
-            var link_id = m3.toLowerCase();
-            var url = m4;
-            var title = m7;
-
-            if (!title) title = "";
-
-            if (url == "") {
-                if (link_id == "") {
-                    // lower-case and turn embedded newlines into spaces
-                    link_id = alt_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 {
-                    return whole_match;
-                }
-            }
-
-            alt_text = escapeCharacters(attributeEncode(alt_text), "*_[]()");
-            url = escapeCharacters(url, "*_");
-            var result = "<img src=\"" + url + "\" alt=\"" + alt_text + "\"";
-
-            // attacklab: Markdown.pl adds empty title attributes to images.
-            // Replicate this bug.
-
-            //if (title != "") {
-            title = attributeEncode(title);
-            title = escapeCharacters(title, "*_");
-            result += " title=\"" + title + "\"";
-            //}
-
-            result += " />";
-
-            return result;
-        }
-
-        function _DoHeaders(text) {
-
-            // Setext-style headers:
-            //  Header 1
-            //  ========
-            //
-            //  Header 2
-            //  --------
-            //
-            text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm,
-                function (wholeMatch, m1) { return "<h1>" + _RunSpanGamut(m1) + "</h1>\n\n"; }
-            );
-
-            text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm,
-                function (matchFound, m1) { return "<h2>" + _RunSpanGamut(m1) + "</h2>\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 "<h" + h_level + ">" + _RunSpanGamut(m2) + "</h" + h_level + ">\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 `</$list_type>`
-                    // 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 + "</" + list_type + ">\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 + "</" + list_type + ">\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 <li>, 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 "<li>" + item + "</li>\n";
-                }
-            );
-
-            // attacklab: strip sentinel
-            list_str = list_str.replace(/~0/g, "");
-
-            g_list_level--;
-            return list_str;
-        }
-
-        function _DoCodeBlocks(text) {
-            //
-            //  Process Markdown `<pre><code>` 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 = "<pre><code>" + codeblock + "\n</code></pre>";
-
-                    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 <code></code> 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:
-            //
-            //      <p>Just type <code>foo `bar` baz</code> at the prompt.</p>
-            //
-            //   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 <code>`bar`</code> ...
-            //
-
-            /*
-            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 + "<code>" + c + "</code>";
-                }
-            );
-
-            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, "&amp;");
-
-            // Do the angle bracket song and dance:
-            text = text.replace(/</g, "&lt;");
-            text = text.replace(/>/g, "&gt;");
-
-            // 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) {
-
-            // <strong> must go first:
-            text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g,
-            "$1<strong>$3</strong>$4");
-
-            text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g,
-            "$1<em>$3</em>$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 <pre> content, so we need to fix that:
-                    bq = bq.replace(
-                            /(\s*<pre>[^\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("<blockquote>\n" + bq + "\n</blockquote>");
-                }
-            );
-            return text;
-        }
-
-        function _FormParagraphs(text, doNotUnhash) {
-            //
-            //  Params:
-            //    $text - string to process with html <p> 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 <p> 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, "<p>");
-                    str += "</p>"
-                    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, "&amp;");
-
-            // Encode naked <'s
-            text = text.replace(/<(?![a-z\/?\$!])/gi, "&lt;");
-
-            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 <a href=""></a>
-            // *except* for the <http://www.foo.com> 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 <http://example.com>
-
-            var replacer = function (wholematch, m1) { return "<a href=\"" + m1 + "\">" + pluginHooks.plainLinkText(m1) + "</a>"; }
-            text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer);
-
-            // Email addresses: <address@domain.foo>
-            /*
-            text = text.replace(/
-                <
-                (?:mailto:)?
-                (
-                    [-.\w]+
-                    \@
-                    [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+
-                )
-                >
-            /gi, _DoAutoLinks_callback());
-            */
-
-            /* disabling email autolinking, since we don't do that on the server, either
-            text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,
-                function(wholeMatch,m1) {
-                    return _EncodeEmailAddress( _UnescapeSpecialChars(m1) );
-                }
-            );
-            */
-            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 ":"
-                }
-                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/vendor/assets/javascripts/markdown/Markdown.Sanitizer.js b/vendor/assets/javascripts/markdown/Markdown.Sanitizer.js
deleted file mode 100644
index cc5826fa8fafa166d4bc7d39f7485ef4cca2afba..0000000000000000000000000000000000000000
--- a/vendor/assets/javascripts/markdown/Markdown.Sanitizer.js
+++ /dev/null
@@ -1,108 +0,0 @@
-(function () {
-    var output, Converter;
-    if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
-        output = exports;
-        Converter = require("./Markdown.Converter").Converter;
-    } else {
-        output = window.Markdown;
-        Converter = output.Converter;
-    }
-        
-    output.getSanitizingConverter = function () {
-        var converter = new Converter();
-        converter.hooks.chain("postConversion", sanitizeHtml);
-        converter.hooks.chain("postConversion", balanceTags);
-        return converter;
-    }
-
-    function sanitizeHtml(html) {
-        return html.replace(/<[^>]*>?/gi, sanitizeTag);
-    }
-
-    // (tags that can be opened/closed) | (tags that stand alone)
-    var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
-    // <a href="url..." optional title>|</a>
-    var a_white = /^(<a\shref="((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\stitle="[^"<>]+")?\s?>|<\/a>)$/i;
-
-    // <img src="url..." optional width  optional height  optional alt  optional title
-    var img_white = /^(<img\ssrc="(https?:\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+"(\swidth="\d{1,3}")?(\sheight="\d{1,3}")?(\salt="[^"<>]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
-
-    function sanitizeTag(tag) {
-        if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
-            return tag;
-        else
-            return "";
-    }
-
-    /// <summary>
-    /// attempt to balance HTML tags in the html string
-    /// by removing any unmatched opening or closing tags
-    /// IMPORTANT: we *assume* HTML has *already* been 
-    /// sanitized and is safe/sane before balancing!
-    /// 
-    /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
-    /// </summary>
-    function balanceTags(html) {
-
-        if (html == "")
-            return "";
-
-        var re = /<\/?\w+[^>]*(\s|$|>)/g;
-        // convert everything to lower case; this makes
-        // our case insensitive comparisons easier
-        var tags = html.toLowerCase().match(re);
-
-        // no HTML tags present? nothing to do; exit now
-        var tagcount = (tags || []).length;
-        if (tagcount == 0)
-            return html;
-
-        var tagname, tag;
-        var ignoredtags = "<p><img><br><li><hr>";
-        var match;
-        var tagpaired = [];
-        var tagremove = [];
-        var needsRemoval = false;
-
-        // loop through matched tags in forward order
-        for (var ctag = 0; ctag < tagcount; ctag++) {
-            tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
-            // skip any already paired tags
-            // and skip tags in our ignore list; assume they're self-closed
-            if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
-                continue;
-
-            tag = tags[ctag];
-            match = -1;
-
-            if (!/^<\//.test(tag)) {
-                // this is an opening tag
-                // search forwards (next tags), look for closing tags
-                for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
-                    if (!tagpaired[ntag] && tags[ntag] == "</" + tagname + ">") {
-                        match = ntag;
-                        break;
-                    }
-                }
-            }
-
-            if (match == -1)
-                needsRemoval = tagremove[ctag] = true; // mark for removal
-            else
-                tagpaired[match] = true; // mark paired
-        }
-
-        if (!needsRemoval)
-            return html;
-
-        // delete all orphaned tags from the string
-
-        var ctag = 0;
-        html = html.replace(re, function (match) {
-            var res = tagremove[ctag] ? "" : match;
-            ctag++;
-            return res;
-        });
-        return html;
-    }
-})();