diff --git a/reviewboard/reviews/markdown_utils.py b/reviewboard/reviews/markdown_utils.py
index 2a694f97af627ed6bfc9e75ae82c81ea7be4207d..48ce13a8990cb391dc8bcff07c110d0ef39597b0 100644
--- a/reviewboard/reviews/markdown_utils.py
+++ b/reviewboard/reviews/markdown_utils.py
@@ -3,10 +3,24 @@ import re
 from markdown import Markdown
 
 
-ESCAPED_CHARS_RE = \
-    re.compile(r'([%s])' % re.escape(''.join(Markdown.ESCAPED_CHARS)))
-UNESCAPED_CHARS_RE = \
-    re.compile(r'\\([%s])' % re.escape(''.join(Markdown.ESCAPED_CHARS)))
+# NOTE: Any changes made here or in markdown_escape below should be
+#       reflected in reviewboard/static/rb/js/utils/textUtils.js.
+
+MARKDOWN_SPECIAL_CHARS = re.escape(''.join(Markdown.ESCAPED_CHARS))
+MARKDOWN_SPECIAL_CHARS_RE = re.compile('([%s])' % MARKDOWN_SPECIAL_CHARS)
+
+# Markdown.ESCAPED_CHARS lists '.' as a character to escape, but it's not
+# that simple. We only want to escape this if it's after a number at the
+# beginning of the line (ignoring leading whitespace), indicating a
+# numbered list. We'll handle that specially.
+MARKDOWN_ESCAPED_CHARS = list(Markdown.ESCAPED_CHARS)
+MARKDOWN_ESCAPED_CHARS.remove('.')
+
+ESCAPE_CHARS_RE = \
+    re.compile(r'(^\s*(\d+\.)+|[%s])'
+               % re.escape(''.join(MARKDOWN_ESCAPED_CHARS)),
+               re.M)
+UNESCAPE_CHARS_RE = re.compile(r'\\([%s])' % MARKDOWN_SPECIAL_CHARS)
 
 
 def markdown_escape(text):
@@ -15,7 +29,9 @@ def markdown_escape(text):
     This will escape the provided text so that none of the characters will
     be rendered specially by Markdown.
     """
-    return ESCAPED_CHARS_RE.sub(r'\\\1', text)
+    return ESCAPE_CHARS_RE.sub(
+        lambda m: MARKDOWN_SPECIAL_CHARS_RE.sub(r'\\\1', m.group(0)),
+        text)
 
 
 def markdown_unescape(escaped_text):
@@ -24,7 +40,7 @@ def markdown_unescape(escaped_text):
     This will unescape the provided Markdown-formatted text so that any
     escaped characters will be unescaped.
     """
-    return UNESCAPED_CHARS_RE.sub(r'\1', escaped_text)
+    return UNESCAPE_CHARS_RE.sub(r'\1', escaped_text)
 
 
 def markdown_escape_field(model, field_name):
diff --git a/reviewboard/reviews/tests.py b/reviewboard/reviews/tests.py
index ea1d548902e3df358f06494ffa1d4274e16999a1..3a3fc2ecf0ef8ae738ea2530c889203bc7dd23a0 100644
--- a/reviewboard/reviews/tests.py
+++ b/reviewboard/reviews/tests.py
@@ -2614,13 +2614,25 @@ class UserInfoboxTests(TestCase):
 
 class MarkdownUtilsTests(TestCase):
     UNESCAPED_TEXT = '\\`*_{}[]()>#+-.!'
-    ESCAPED_TEXT = '\\\\\\`\\*\\_\\{\\}\\[\\]\\(\\)\\>\\#\\+\\-\\.\\!'
+    ESCAPED_TEXT = '\\\\\\`\\*\\_\\{\\}\\[\\]\\(\\)\\>\\#\\+\\-.\\!'
 
     def test_markdown_escape(self):
         """Testing markdown_escape"""
         self.assertEqual(markdown_escape(self.UNESCAPED_TEXT),
                          self.ESCAPED_TEXT)
 
+    def test_markdown_escape_periods(self):
+        """Testing markdown_escape with '.' placement"""
+        self.assertEqual(
+            markdown_escape('Line. 1.\n'
+                            '1. Line. 2.\n'
+                            '1.2. Line. 3.\n'
+                            '  1. Line. 4.'),
+            ('Line. 1.\n'
+             '1\\. Line. 2.\n'
+             '1\\.2\\. Line. 3.\n'
+             '  1\\. Line. 4.'))
+
     def test_markdown_unescape(self):
         """Testing markdown_unescape"""
         self.assertEqual(markdown_unescape(self.ESCAPED_TEXT),
diff --git a/reviewboard/static/rb/js/utils/tests/textUtilsTests.js b/reviewboard/static/rb/js/utils/tests/textUtilsTests.js
index a3f54499672ef51de74d8bd25190d30cd2f04769..8904e86567ba8fe70a3a14f4b5f968958ccd8f8b 100644
--- a/reviewboard/static/rb/js/utils/tests/textUtilsTests.js
+++ b/reviewboard/static/rb/js/utils/tests/textUtilsTests.js
@@ -1,6 +1,20 @@
 describe('utils/textUtils', function() {
-    it('escapeMarkDown', function() {
-        expect(RB.escapeMarkdown('hello \\`*_{}[]()>#+-.! world.')).toBe(
-            'hello \\\\\\`\\*\\_\\{\\}\\[\\]\\(\\)\\>\\#\\+\\-\\.\\! world\\.');
+    describe('escapeMarkdown', function() {
+        it('All standard characters', function() {
+            expect(RB.escapeMarkdown('hello \\`*_{}[]()>#+-.! world.')).toBe(
+                'hello \\\\\\`\\*\\_\\{\\}\\[\\]\\(\\)\\>\\#\\+\\-.\\! world.');
+        });
+
+        it("With '.' placement", function() {
+            expect(RB.escapeMarkdown('Line. 1.\n' +
+                                     '1. Line. 2.\n' +
+                                     '1.2. Line. 3.\n' +
+                                     '  1. Line. 4.'))
+                .toBe(
+                    'Line. 1.\n' +
+                    '1\\. Line. 2.\n' +
+                    '1\\.2\\. Line. 3.\n' +
+                    '  1\\. Line. 4.');
+        });
     });
 });
diff --git a/reviewboard/static/rb/js/utils/textUtils.js b/reviewboard/static/rb/js/utils/textUtils.js
index b7dd20fd22986867e78243a75e127326b5f06ea1..89cd7c05396a83a2924248e789440a8af082234c 100644
--- a/reviewboard/static/rb/js/utils/textUtils.js
+++ b/reviewboard/static/rb/js/utils/textUtils.js
@@ -1,7 +1,12 @@
 (function() {
 
 
-var ESCAPED_CHARS_RE = /([\\`\*_\{\}\[\]\(\)\>\#\+\-\.\!])/g;
+/*
+ * NOTE: Any changes made here or in escapeMarkdown below should be
+ *       reflected in reviewboard/reviews/markdown_utils.py.
+ */
+var MARKDOWN_SPECIAL_CHARS_RE = /([\\`\*_\{\}\[\]\(\)\>\#\+\-\.\!])/g,
+    ESCAPE_CHARS_RE = /(^\s*(\d+\.)+|[\\`\*_\{\}\[\]\(\)\>\#\+\-\!])/gm;
 
 
 // If `marked` is defined, initialize it with our preferred options
@@ -89,7 +94,9 @@ RB.formatText = function($el, text, bugTrackerURL, options) {
  * characters to be interpreted as Markdown.
  */
 RB.escapeMarkdown = function(text) {
-    return text.replace(ESCAPED_CHARS_RE, '\\$1');
+    return text.replace(ESCAPE_CHARS_RE, function(text, m1) {
+        return m1.replace(MARKDOWN_SPECIAL_CHARS_RE, '\\$1');
+    });
 }
 
 
