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 = '', siteUrl = 'http://127.0.0.1:2369/', postTitle = '', - postStart = 'You\'re live!', + postStart = 'You’re live!', postEnd = 'you think :)

]]>
', postLink = 'http://127.0.0.1:2369/welcome-to-ghost/', postCreator = '';