From 73489b652e2aea5dfb6c391168e13bc7dcdb395c Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Tue, 27 Aug 2013 12:24:17 -0400
Subject: [PATCH] FIX: Allow intra-word underscores.

---
 .../dialects/bold_italics_dialect.js          | 41 ++++++----
 .../{markdown.js => better_markdown.js}       | 78 ++++---------------
 lib/pretty_text.rb                            |  2 +-
 test/javascripts/components/markdown_test.js  |  6 ++
 4 files changed, 48 insertions(+), 79 deletions(-)
 rename app/assets/javascripts/external/{markdown.js => better_markdown.js} (95%)

diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index 704b6a16465..b4fadffb778 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -10,23 +10,34 @@ Discourse.Dialect.on("register", function(event) {
   var dialect = event.dialect,
       MD = event.MD;
 
-  /**
-    Handles simultaneous bold and italics
 
-    @method parseMentions
-    @param {String} text the text match
-    @param {Array} match the match found
-    @param {Array} prev the previous jsonML
-    @return {Array} an array containing how many chars we've replaced and the jsonML content for it.
-    @namespace Discourse.Dialect
-  **/
-  dialect.inline['***'] = function boldItalics(text, match, prev) {
-    var regExp = /^\*{3}([^\*]+)\*{3}/,
-        m = regExp.exec(text);
+  var inlineBuilder = function(symbol, tag, surround) {
+    return function(text, match, prev) {
+      if (prev && (prev.length > 0)) {
+        var last = prev[prev.length - 1];
+        if (typeof last === "string" && (!last.match(/\W$/))) { return; }
+      }
 
-    if (m) {
-      return [m[0].length, ['strong', ['em'].concat(this.processInline(m[1]))]];
-    }
+      var regExp = new RegExp("^\\" + symbol + "([^\\" + symbol + "]+)" + "\\" + symbol, "igm"),
+          m = regExp.exec(text);
+
+      if (m) {
+
+        var contents = [tag].concat(this.processInline(m[1]));
+        if (surround) {
+          contents = [surround, contents];
+        }
+
+        return [m[0].length, contents];
+      }
+    };
   };
 
+  dialect.inline['***'] = inlineBuilder('**', 'em', 'strong');
+  dialect.inline['**'] = inlineBuilder('**', 'strong');
+  dialect.inline['*'] = inlineBuilder('*', 'em');
+  dialect.inline['_'] = inlineBuilder('_', 'em');
+
+
+
 });
diff --git a/app/assets/javascripts/external/markdown.js b/app/assets/javascripts/external/better_markdown.js
similarity index 95%
rename from app/assets/javascripts/external/markdown.js
rename to app/assets/javascripts/external/better_markdown.js
index e11d71e6c27..29e708fb857 100644
--- a/app/assets/javascripts/external/markdown.js
+++ b/app/assets/javascripts/external/better_markdown.js
@@ -1,3 +1,18 @@
+/*
+  This is a fork of markdown-js with a few changes to support discourse:
+
+  * We have replaced the strong/em handlers because we prefer them only to work on word
+    boundaries.
+
+  * We removed the maraku support as we don't use it.
+
+  * We don't escape the contents of HTML as we prefer to use a whitelist.
+
+  * Note the name BetterMarkdown doesn't mean it's *better* than markdown-js, it refers
+    to it being better than our previous markdown parser!
+
+*/
+
 // Released under MIT license
 // Copyright (c) 2009-2010 Dominic Baggott
 // Copyright (c) 2009-2010 Ash Berlin
@@ -1004,69 +1019,6 @@ Markdown.dialects.Gruber.inline = {
 
 };
 
-// Meta Helper/generator method for em and strong handling
-function strong_em( tag, md ) {
-
-  var state_slot = tag + "_state",
-      other_slot = tag == "strong" ? "em_state" : "strong_state";
-
-  function CloseTag(len) {
-    this.len_after = len;
-    this.name = "close_" + md;
-  }
-
-  return function ( text, orig_match ) {
-
-    if ( this[state_slot][0] == md ) {
-      // Most recent em is of this type
-      //D:this.debug("closing", md);
-      this[state_slot].shift();
-
-      // "Consume" everything to go back to the recrusion in the else-block below
-      return[ text.length, new CloseTag(text.length-md.length) ];
-    }
-    else {
-      // Store a clone of the em/strong states
-      var other = this[other_slot].slice(),
-          state = this[state_slot].slice();
-
-      this[state_slot].unshift(md);
-
-      //D:this.debug_indent += "  ";
-
-      // Recurse
-      var res = this.processInline( text.substr( md.length ) );
-      //D:this.debug_indent = this.debug_indent.substr(2);
-
-      var last = res[res.length - 1];
-
-      //D:this.debug("processInline from", tag + ": ", uneval( res ) );
-
-      var check = this[state_slot].shift();
-      if ( last instanceof CloseTag ) {
-        res.pop();
-        // We matched! Huzzah.
-        var consumed = text.length - last.len_after;
-        return [ consumed, [ tag ].concat(res) ];
-      }
-      else {
-        // Restore the state of the other kind. We might have mistakenly closed it.
-        this[other_slot] = other;
-        this[state_slot] = state;
-
-        // We can't reuse the processed result as it could have wrong parsing contexts in it.
-        return [ md.length, md ];
-      }
-    }
-  }; // End returned function
-}
-
-Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
-Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
-Markdown.dialects.Gruber.inline["*"]  = strong_em("em", "*");
-Markdown.dialects.Gruber.inline["_"]  = strong_em("em", "_");
-
-
 // Build default order from insertion order.
 Markdown.buildBlockOrder = function(d) {
   var ord = [];
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 48fe7fdf23c..eecb5ca7eda 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -105,7 +105,7 @@ module PrettyText
     ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
 
     ctx_load(ctx,
-              "app/assets/javascripts/external/markdown.js",
+              "app/assets/javascripts/external/better_markdown.js",
               "app/assets/javascripts/discourse/dialects/dialect.js",
               "app/assets/javascripts/discourse/components/utilities.js",
               "app/assets/javascripts/discourse/components/markdown.js")
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index f4714baee30..2370f0b0a6d 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -17,7 +17,13 @@ var cookedOptions = function(input, opts, expected, text) {
 
 test("basic cooking", function() {
   cooked("hello", "<p>hello</p>", "surrounds text with paragraphs");
+  cooked("**evil**", "<p><strong>evil</strong></p>", "it bolds text.");
+  cooked("*trout*", "<p><em>trout</em></p>", "it italicizes text.");
+  cooked("_trout_", "<p><em>trout</em></p>", "it italicizes text.");
   cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
+  cooked("word_with_underscores", "<p>word_with_underscores</p>", "it doesn't do intraword italics");
+  cooked("hello \\*evil\\*", "<p>hello *evil*</p>", "it supports escaping of asterisks");
+  cooked("hello \\_evil\\_", "<p>hello _evil_</p>", "it supports escaping of italics");
 });
 
 test("Traditional Line Breaks", function() {