mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
Adds markdown highlight support
closes #4574 - adds highlight showdown extension with tests
This commit is contained in:
parent
83ef557c88
commit
9783f16e76
7 changed files with 253 additions and 8 deletions
|
@ -590,7 +590,8 @@ var _ = require('lodash'),
|
||||||
|
|
||||||
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
'core/shared/lib/showdown/extensions/ghostimagepreview.js',
|
||||||
'core/shared/lib/showdown/extensions/ghostgfm.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/ghostimagepreview.js',
|
||||||
'core/shared/lib/showdown/extensions/ghostgfm.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'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -101,7 +101,7 @@ ul ol, ol ul {
|
||||||
}
|
}
|
||||||
|
|
||||||
mark {
|
mark {
|
||||||
background-color: #ffc336;
|
background-color: #fdffb6;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import cajaSanitizers from 'ghost/utils/caja-sanitizers';
|
||||||
var showdown,
|
var showdown,
|
||||||
formatMarkdown;
|
formatMarkdown;
|
||||||
|
|
||||||
showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes']});
|
showdown = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
|
||||||
|
|
||||||
formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) {
|
formatMarkdown = Ember.Handlebars.makeBoundHelper(function (markdown) {
|
||||||
var escapedhtml = '';
|
var escapedhtml = '';
|
||||||
|
|
|
@ -5,8 +5,9 @@ var _ = require('lodash'),
|
||||||
errors = require('../errors'),
|
errors = require('../errors'),
|
||||||
Showdown = require('showdown-ghost'),
|
Showdown = require('showdown-ghost'),
|
||||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
||||||
ghostfootnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
|
footnotes = require('../../shared/lib/showdown/extensions/ghostfootnotes'),
|
||||||
converter = new Showdown.converter({extensions: [ghostgfm, ghostfootnotes]}),
|
highlight = require('../../shared/lib/showdown/extensions/ghosthighlight'),
|
||||||
|
converter = new Showdown.converter({extensions: [ghostgfm, footnotes, highlight]}),
|
||||||
ghostBookshelf = require('./base'),
|
ghostBookshelf = require('./base'),
|
||||||
xmlrpc = require('../xmlrpc'),
|
xmlrpc = require('../xmlrpc'),
|
||||||
sitemap = require('../data/sitemap'),
|
sitemap = require('../data/sitemap'),
|
||||||
|
|
71
core/shared/lib/showdown/extensions/ghosthighlight.js
Normal file
71
core/shared/lib/showdown/extensions/ghosthighlight.js
Normal file
|
@ -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: <mark>highlighted</mark>
|
||||||
|
|
||||||
|
(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(/<pre>[\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(/<code>[\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 '<mark>' + content + '</mark>';
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}());
|
|
@ -12,9 +12,10 @@ var should = require('should'),
|
||||||
Showdown = require('showdown-ghost'),
|
Showdown = require('showdown-ghost'),
|
||||||
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
ghostgfm = require('../../shared/lib/showdown/extensions/ghostgfm'),
|
||||||
ghostimagepreview = require('../../shared/lib/showdown/extensions/ghostimagepreview'),
|
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
|
// To stop jshint complaining
|
||||||
should.equal(true, true);
|
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: /^<p><mark>foo_bar<\/mark><\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'My stuff that has a ==highlight== in the middle.',
|
||||||
|
output: /^<p>My stuff that has a <mark>highlight<\/mark> in the middle.<\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'My stuff that has a ==multiple word highlight== in the middle.',
|
||||||
|
output: /^<p>My stuff that has a <mark>multiple word highlight<\/mark> in the middle.<\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'My stuff that has a ==multiple word **bold** highlight== in the middle.',
|
||||||
|
output: /^<p>My stuff that has a <mark>multiple word <strong>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: /^<p>My stuff that has a <mark>multiple word and <br \/>\n line broken highlight<\/mark> in the middle.<\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'Test ==Highlighting with a [link](https://ghost.org) in the middle== of it.',
|
||||||
|
output: /^<p>Test <mark>Highlighting with a <a href="https:\/\/ghost.org">link<\/a> in the middle<\/mark> of it.<\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '==[link](http://ghost.org)==',
|
||||||
|
output: /^<p><mark><a href="http:\/\/ghost.org">link<\/a><\/mark><\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '[==link==](http://ghost.org)',
|
||||||
|
output: /^<p><a href="http:\/\/ghost.org"><mark>link<\/mark><\/a><\/p>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: '====test==test==test====test',
|
||||||
|
output: /^<p><mark>==test<\/mark>test<mark>test<\/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><\/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>=====<\/p>$/},
|
||||||
|
processedMarkup = converter.makeHtml(testPhrase.input);
|
||||||
|
|
||||||
|
processedMarkup.should.match(testPhrase.output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still handle headers correctly', function () {
|
||||||
|
var testPhrases = [
|
||||||
|
{
|
||||||
|
input: 'Header\n==',
|
||||||
|
output: /^<h1 id="header">Header <\/h1>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'First Header\n==\nSecond Header\n==',
|
||||||
|
output: /^<h1 id="firstheader">First Header <\/h1>\n\n<h1 id="secondheader">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
|
// Waiting for showdown typography to be updated
|
||||||
// it('should correctly convert quotes to curly quotes', function () {
|
// it('should correctly convert quotes to curly quotes', function () {
|
||||||
// var testPhrases = [
|
// var testPhrases = [
|
||||||
|
|
81
core/test/unit/showdown_highlight_spec.js
Normal file
81
core/test/unit/showdown_highlight_spec.js
Normal file
|
@ -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: /^<mark>foo_bar<\/mark>$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'My stuff that has a ==highlight== in the middle.',
|
||||||
|
output: /^My stuff that has a <mark>highlight<\/mark> in the middle.$/
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'My stuff that has a ==multiple word highlight== in the middle.',
|
||||||
|
output: /^My stuff that has a <mark>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 <mark>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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Add table
Reference in a new issue