diff --git a/Gruntfile.js b/Gruntfile.js
index 64ef36da8f..d1c60be155 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -382,6 +382,7 @@ var path = require('path'),
'core/client/assets/vendor/codemirror/mode/gfm/gfm.js',
'core/client/assets/vendor/showdown/showdown.js',
'core/client/assets/vendor/showdown/extensions/ghostdown.js',
+ 'core/shared/vendor/showdown/extensions/typography.js',
'core/shared/vendor/showdown/extensions/github.js',
'core/client/assets/vendor/shortcuts.js',
'core/client/assets/vendor/validator-client.js',
@@ -437,6 +438,7 @@ var path = require('path'),
'core/client/assets/vendor/codemirror/mode/gfm/gfm.js',
'core/client/assets/vendor/showdown/showdown.js',
'core/client/assets/vendor/showdown/extensions/ghostdown.js',
+ 'core/shared/vendor/showdown/extensions/typography.js',
'core/shared/vendor/showdown/extensions/github.js',
'core/client/assets/vendor/shortcuts.js',
'core/client/assets/vendor/validator-client.js',
diff --git a/core/client/views/editor.js b/core/client/views/editor.js
index 4ebd2dbda1..fcccbd9468 100644
--- a/core/client/views/editor.js
+++ b/core/client/views/editor.js
@@ -449,7 +449,7 @@
initMarkdown: function () {
var self = this;
- this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
+ this.converter = new Showdown.converter({extensions: ['typography', 'ghostdown', 'github']});
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'gfm',
tabMode: 'indent',
diff --git a/core/server/models/post.js b/core/server/models/post.js
index 66de0722c4..34b2ad48b3 100644
--- a/core/server/models/post.js
+++ b/core/server/models/post.js
@@ -4,7 +4,8 @@ var _ = require('lodash'),
errors = require('../errorHandling'),
Showdown = require('showdown'),
github = require('../../shared/vendor/showdown/extensions/github'),
- converter = new Showdown.converter({extensions: [github]}),
+ typography = require('../../shared/vendor/showdown/extensions/typography'),
+ converter = new Showdown.converter({extensions: [typography, github]}),
User = require('./user').User,
Tag = require('./tag').Tag,
Tags = require('./tag').Tags,
@@ -453,4 +454,4 @@ Posts = ghostBookshelf.Collection.extend({
module.exports = {
Post: Post,
Posts: Posts
-};
\ No newline at end of file
+};
diff --git a/core/shared/vendor/showdown/extensions/typography.js b/core/shared/vendor/showdown/extensions/typography.js
new file mode 100644
index 0000000000..998aba1e70
--- /dev/null
+++ b/core/shared/vendor/showdown/extensions/typography.js
@@ -0,0 +1,114 @@
+/*global module */
+//
+// Replaces straight quotes with curly ones, -- and --- with en dash and em
+// dash respectively, and ... with horizontal ellipses.
+//
+
+(function () {
+ var typography = function () {
+ return [
+ {
+ type: "lang",
+ filter: function (text) {
+ var fCodeblocks = {}, nCodeblocks = {}, iCodeblocks = {},
+ e = {
+ endash: '\u2009\u2013\u2009', // U+2009 = thin space
+ emdash: '\u2014',
+ lsquo: '\u2018',
+ rsquo: '\u2019',
+ ldquo: '\u201c',
+ rdquo: '\u201d',
+ hellip: '\u2026'
+ },
+
+ i;
+
+ // Extract fenced code blocks.
+ i = -1;
+ text = text.replace(/```((?:.|\n)+?)```/g,
+ function (match, code) {
+ i += 1;
+ fCodeblocks[i] = "```" + code + "```";
+ return "{typog-fcb-" + i + "}";
+ });
+
+ // Extract indented code blocks.
+ i = -1;
+ text = text.replace(/((\n+([ ]{4}|\t).+)+)/g,
+ function (match, code) {
+ i += 1;
+ nCodeblocks[i] = " " + code;
+ return "{typog-ncb-" + i + "}";
+ });
+
+ // Extract inline code blocks
+ i = -1;
+ text = text.replace(/`(.+)`/g, function (match, code) {
+ i += 1;
+ iCodeblocks[i] = "`" + code + "`";
+ return "{typog-icb-" + i + "}";
+ });
+
+ // Perform typographic symbol replacement.
+
+ // Double quotes. There might be a reason this doesn't use
+ // the same \b matching style as the single quotes, but I
+ // can't remember what it is :(
+ text = text.
+ // Opening quotes
+ replace(/"([\w'])/g, e.ldquo + "$1").
+ // All the rest
+ replace(/"/g, e.rdquo);
+
+ // Single quotes/apostrophes
+ text = text.
+ // Apostrophes first
+ replace(/\b'\b/g, e.rsquo).
+ // Opening quotes
+ replace(/'\b/g, e.lsquo).
+ // All the rest
+ replace(/'/g, e.rsquo);
+
+ // Dashes
+ text = text.
+ // Don't replace lines containing only hyphens
+ replace(/^-+$/gm, "{typog-hr}").
+ replace(/---/g, e.emdash).
+ replace(/ -- /g, e.endash).
+ replace(/{typog-hr}/g, "----");
+
+ // Ellipses.
+ text = text.replace(/\.{3}/g, e.hellip);
+
+
+ // Restore fenced code blocks.
+ text = text.replace(/{typog-fcb-([0-9]+)}/g, function (x, y) {
+ return fCodeblocks[y];
+ });
+
+ // Restore indented code blocks.
+ text = text.replace(/{typog-ncb-([0-9]+)}/g, function (x, y) {
+ return nCodeblocks[y];
+ });
+
+ // Restore inline code blocks.
+ text = text.replace(/{typog-icb-([0-9]+)}/g, function (x, y) {
+ return iCodeblocks[y];
+ });
+
+ return text;
+ }
+ }
+ ];
+ };
+
+ // Client-side export
+ if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
+ window.Showdown.extensions.typography = typography;
+ }
+ // Server-side export
+ if (typeof module !== 'undefined') {
+ module.exports = typography;
+ }
+}());
+
diff --git a/core/test/functional/frontend/feed_test.js b/core/test/functional/frontend/feed_test.js
index 791d14e83a..439765dc69 100644
--- a/core/test/functional/frontend/feed_test.js
+++ b/core/test/functional/frontend/feed_test.js
@@ -10,7 +10,7 @@ CasperTest.begin('Ensure that RSS is available', 11, function suite(test) {
siteDescription = '