From 9783f16e766d2b76cf420c270fb4a986fdf2c267 Mon Sep 17 00:00:00 2001 From: Hannah Wolfe Date: Wed, 3 Dec 2014 19:52:29 +0000 Subject: [PATCH] Adds markdown highlight support closes #4574 - adds highlight showdown extension with tests --- Gruntfile.js | 6 +- core/client/assets/sass/patterns/global.scss | 2 +- core/client/helpers/gh-format-markdown.js | 2 +- core/server/models/post.js | 5 +- .../lib/showdown/extensions/ghosthighlight.js | 71 ++++++++++++++ .../unit/showdown_client_integrated_spec.js | 94 ++++++++++++++++++- core/test/unit/showdown_highlight_spec.js | 81 ++++++++++++++++ 7 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 core/shared/lib/showdown/extensions/ghosthighlight.js create mode 100644 core/test/unit/showdown_highlight_spec.js diff --git a/Gruntfile.js b/Gruntfile.js index 298d813a38..1c81bb8e3c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -590,7 +590,8 @@ var _ = require('lodash'), 'core/shared/lib/showdown/extensions/ghostimagepreview.js', 'core/shared/lib/showdown/extensions/ghostgfm.js', - 'core/shared/lib/showdown/extensions/ghostfootnotes.js' + 'core/shared/lib/showdown/extensions/ghostfootnotes.js', + 'core/shared/lib/showdown/extensions/ghosthighlight.js' ] }, @@ -626,7 +627,8 @@ var _ = require('lodash'), 'core/shared/lib/showdown/extensions/ghostimagepreview.js', 'core/shared/lib/showdown/extensions/ghostgfm.js', - 'core/shared/lib/showdown/extensions/ghostfootnotes.js' + 'core/shared/lib/showdown/extensions/ghostfootnotes.js', + 'core/shared/lib/showdown/extensions/ghosthighlight.js' ] } }, diff --git a/core/client/assets/sass/patterns/global.scss b/core/client/assets/sass/patterns/global.scss index 7ca1789ab2..1e2e35ca44 100644 --- a/core/client/assets/sass/patterns/global.scss +++ b/core/client/assets/sass/patterns/global.scss @@ -101,7 +101,7 @@ ul ol, ol ul { } mark { - background-color: #ffc336; + background-color: #fdffb6; } a { diff --git a/core/client/helpers/gh-format-markdown.js b/core/client/helpers/gh-format-markdown.js index 69be7fc13d..03dabc2f8d 100644 --- a/core/client/helpers/gh-format-markdown.js +++ b/core/client/helpers/gh-format-markdown.js @@ -4,7 +4,7 @@ import cajaSanitizers from 'ghost/utils/caja-sanitizers'; var showdown, formatMarkdown; -showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes']}); +showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']}); formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) { var escapedhtml = ''; diff --git a/core/server/models/post.js b/core/server/models/post.js index 8a1483b142..d57ee03bf9 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -5,8 +5,9 @@ var _ = require('lodash'), errors = require('../errors'), Showdown = require('showdown-ghost'), ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'), - ghostfootnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'), - converter = new Showdown.converter({extensions: [ghostgfm, ghostfootnotes]}), + footnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'), + highlight = require('../../shared/lib/showdown/extensions/ghosthighlight'), + converter = new Showdown.converter({extensions: [ghostgfm, footnotes, highlight]}), ghostBookshelf = require('./base'), xmlrpc = require('../xmlrpc'), sitemap = require('../data/sitemap'), diff --git a/core/shared/lib/showdown/extensions/ghosthighlight.js b/core/shared/lib/showdown/extensions/ghosthighlight.js new file mode 100644 index 0000000000..b867b0183b --- /dev/null +++ b/core/shared/lib/showdown/extensions/ghosthighlight.js @@ -0,0 +1,71 @@ +/* jshint node:true, browser:true, -W044 */ + +// Adds highlight syntax as per RedCarpet: +// +// https://github.com/vmg/redcarpet +// +// This is ==highlighted==. It looks like this: highlighted + +(function () { + var highlight = function () { + return [ + { + type: 'html', + filter: function (text) { + var highlightRegex = /(=){2}([\s\S]+?)(=){2}/gim, + preExtractions = {}, + codeExtractions = {}, + hashID = 0; + + function hashId() { + return hashID += 1; + } + + // Extract pre blocks + text = text.replace(/
[\s\S]*?<\/pre>/gim, function (x) {
+                        var hash = hashId();
+                        preExtractions[hash] = x;
+                        return '{gfm-js-extract-pre-' + hash + '}';
+                    }, 'm');
+
+                    // Extract code blocks
+                    text = text.replace(/[\s\S]*?<\/code>/gim, function (x) {
+                        var hash = hashId();
+                        codeExtractions[hash] = x;
+                        return '{gfm-js-extract-code-' + hash + '}';
+                    }, 'm');
+
+                    text = text.replace(highlightRegex, function (match, n, content) {
+                        // Check the content isn't just an `=`
+                        if (!/^=+$/.test(content)) {
+                            return '' + content + '';
+                        }
+
+                        return match;
+                    });
+
+                    // replace pre extractions
+                    text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) {
+                        return preExtractions[y];
+                    });
+
+                     // replace code extractions
+                    text = text.replace(/\{gfm-js-extract-code-([0-9]+)\}/gm, function (x, y) {
+                        return codeExtractions[y];
+                    });
+
+                    return text;
+                }
+            }
+        ];
+    };
+
+    // Client-side export
+    if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) {
+        window.Showdown.extensions.highlight = highlight;
+    }
+    // Server-side export
+    if (typeof module !== 'undefined') {
+        module.exports = highlight;
+    }
+}());
diff --git a/core/test/unit/showdown_client_integrated_spec.js b/core/test/unit/showdown_client_integrated_spec.js
index 04f180896c..023fe22589 100644
--- a/core/test/unit/showdown_client_integrated_spec.js
+++ b/core/test/unit/showdown_client_integrated_spec.js
@@ -12,9 +12,10 @@ var should      = require('should'),
     Showdown    = require('showdown-ghost'),
     ghostgfm            = require('../../shared/lib/showdown/extensions/ghostgfm'),
     ghostimagepreview   = require('../../shared/lib/showdown/extensions/ghostimagepreview'),
-    ghostfootnotes      = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
+    footnotes      = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
+    highlight      = require('../../shared/lib/showdown/extensions/ghosthighlight'),
 
-    converter   = new Showdown.converter({extensions: [ghostimagepreview, ghostgfm, ghostfootnotes]});
+    converter   = new Showdown.converter({extensions: [ghostimagepreview, ghostgfm, footnotes, highlight]});
 
 // To stop jshint complaining
 should.equal(true, true);
@@ -532,6 +533,95 @@ describe('Showdown client side converter', function () {
         });
     });
 
+    it('should replace showdown highlight with html', function () {
+        var testPhrases = [
+            {
+                input: '==foo_bar==',
+                output: /^

foo_bar<\/mark><\/p>$/ + }, + { + input: 'My stuff that has a ==highlight== in the middle.', + output: /^

My stuff that has a highlight<\/mark> in the middle.<\/p>$/ + }, + { + input: 'My stuff that has a ==multiple word highlight== in the middle.', + output: /^

My stuff that has a multiple word highlight<\/mark> in the middle.<\/p>$/ + }, + { + input: 'My stuff that has a ==multiple word **bold** highlight== in the middle.', + output: /^

My stuff that has a multiple word bold<\/strong> highlight<\/mark> in the middle.<\/p>$/ + }, + { + input: 'My stuff that has a ==multiple word and\n line broken highlight== in the middle.', + output: /^

My stuff that has a multiple word and
\n line broken highlight<\/mark> in the middle.<\/p>$/ + }, + { + input: 'Test ==Highlighting with a [link](https://ghost.org) in the middle== of it.', + output: /^

Test Highlighting with a link<\/a> in the middle<\/mark> of it.<\/p>$/ + }, + { + input: '==[link](http://ghost.org)==', + output: /^

link<\/a><\/mark><\/p>$/ + }, + { + input: '[==link==](http://ghost.org)', + output: /^

link<\/mark><\/a><\/p>$/ + }, + { + input: '====test==test==test====test', + output: /^

==test<\/mark>testtest<\/mark>==test<\/p>$/ + } + ]; + + testPhrases.forEach(function (testPhrase) { + var processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it('should not effect pre tags', function () { + var testPhrase = { + input: '```javascript\n' + + 'var foo = "bar";\n' + + 'if (foo === "bar") {\n' + + ' return true;\n' + + '} else if (foo === "baz") {\n' + + ' return "magic happened";\n' + + '}\n' + + '```', + output: /^<\/mark>$/ + }, + processedMarkup = converter.makeHtml(testPhrase.input); + + // this does not get mark tags + processedMarkup.should.not.match(testPhrase.output); + }); + + it('should ignore multiple equals', function () { + var testPhrase = {input: '=====', output: /^

=====<\/p>$/}, + processedMarkup = converter.makeHtml(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it('should still handle headers correctly', function () { + var testPhrases = [ + { + input: 'Header\n==', + output: /^

Header <\/h1>$/ + }, + { + input: 'First Header\n==\nSecond Header\n==', + output: /^

First Header <\/h1>\n\n

Second Header <\/h1>$/ + } + ]; + + testPhrases.forEach(function (testPhrase) { + var processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + // Waiting for showdown typography to be updated // it('should correctly convert quotes to curly quotes', function () { // var testPhrases = [ diff --git a/core/test/unit/showdown_highlight_spec.js b/core/test/unit/showdown_highlight_spec.js new file mode 100644 index 0000000000..5547961d9d --- /dev/null +++ b/core/test/unit/showdown_highlight_spec.js @@ -0,0 +1,81 @@ +/** + * Tests the highlight extension for showdown + * + * Note, that the showdown highlight extension is a post-HTML filter, so most of the tests are in + * showdown_client_integration_spec, to make it easier to test the behaviour on top of the already converted HTML + * + */ + +/*globals describe, it */ +/*jshint expr:true*/ +var should = require('should'), + + // Stuff we are testing + ghosthighlight = require('../../shared/lib/showdown/extensions/ghosthighlight'); + +// To stop jshint complaining +should.equal(true, true); + +function _ExecuteExtension(ext, text) { + if (ext.regex) { + var re = new RegExp(ext.regex, 'g'); + return text.replace(re, ext.replace); + } else if (ext.filter) { + return ext.filter(text); + } +} + +function _ConvertPhrase(testPhrase) { + return ghosthighlight().reduce(function (text, ext) { + return _ExecuteExtension(ext, text); + }, testPhrase); +} + +describe('Ghost highlight showdown extension', function () { + /*jslint regexp: true */ + + it('should export an array of methods for processing', function () { + ghosthighlight.should.be.a.function; + ghosthighlight().should.be.an.Array; + + ghosthighlight().forEach(function (processor) { + processor.should.be.an.Object; + processor.should.have.property('type'); + processor.type.should.be.a.String; + }); + }); + + it('should replace showdown highlight with html', function () { + var testPhrases = [ + { + input: '==foo_bar==', + output: /^foo_bar<\/mark>$/ + }, + { + input: 'My stuff that has a ==highlight== in the middle.', + output: /^My stuff that has a highlight<\/mark> in the middle.$/ + }, + { + input: 'My stuff that has a ==multiple word highlight== in the middle.', + output: /^My stuff that has a multiple word highlight<\/mark> in the middle.$/ + }, + { + input: 'My stuff that has a ==multiple word and\n line broken highlight== in the middle.', + output: /^My stuff that has a multiple word and\n line broken highlight<\/mark> in the middle.$/ + } + + ]; + + testPhrases.forEach(function (testPhrase) { + var processedMarkup = _ConvertPhrase(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it('should ignore multiple equals', function () { + var testPhrase = {input: '=====', output: /^=====$/}, + processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); +});