0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

replace custom showdown fork with markdown-it (#8451)

refs https://github.com/TryGhost/Ghost-Admin/pull/690, closes #1501, closes #2093, closes #4592, closes #4627, closes #4659, closes #5039, closes #5237, closes #5587, closes #5625, closes #5632, closes #5822, closes #5939, closes #6840, closes #7183, closes #7536

- replace custom showdown fork with markdown-it
- swaps showdown for markdown-it when rendering markdown
- match existing header ID behaviour
- allow headers without a space after the #s
- add duplicate header ID handling
- remove legacy markdown spec
- move markdown-it setup into markdown-converter util
- update mobiledoc specs to match markdown-it newline behaviour
- update data-generator HTML to match markdown-it newline behaviour
- fix Post "converts html to plaintext" test
- update rss spec to match markdown-it newline behaviour
- close almost all related showdown bugs
This commit is contained in:
Kevin Ansfield 2017-05-15 17:48:14 +01:00 committed by Katharina Irrgang
parent 3bae41ccff
commit 5d868d14ad
11 changed files with 94 additions and 648 deletions

View file

@ -1,7 +1,6 @@
var SimpleDom = require('simple-dom'), var SimpleDom = require('simple-dom'),
tokenizer = require('simple-html-tokenizer').tokenize, tokenizer = require('simple-html-tokenizer').tokenize,
Showdown = require('showdown-ghost'), markdownConverter = require('../../../utils/markdown-converter'),
converter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}),
parser; parser;
module.exports = { module.exports = {
@ -9,6 +8,10 @@ module.exports = {
type: 'dom', type: 'dom',
render(opts) { render(opts) {
parser = new SimpleDom.HTMLParser(tokenizer, opts.env.dom, SimpleDom.voidMap); parser = new SimpleDom.HTMLParser(tokenizer, opts.env.dom, SimpleDom.voidMap);
return parser.parse('<div class="kg-card-markdown">' + converter.makeHtml(opts.payload.markdown || '') + '</div>'); return parser.parse(''
+ '<div class="kg-card-markdown">'
+ markdownConverter.render(opts.payload.markdown || '')
+ '</div>'
);
} }
}; };

View file

@ -15,6 +15,6 @@ describe('Markdown card', function () {
}; };
var serializer = new SimpleDom.HTMLSerializer([]); var serializer = new SimpleDom.HTMLSerializer([]);
serializer.serialize(card.render(opts)).should.match('<div class="kg-card-markdown"><h1 id="heading">HEADING</h1>\n\n<ul>\n<li>list</li>\n<li>items</li>\n</ul></div>'); serializer.serialize(card.render(opts)).should.match('<div class="kg-card-markdown"><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n</div>');
}); });
}); });

View file

@ -5,8 +5,7 @@ var _ = require('lodash'),
Promise = require('bluebird'), Promise = require('bluebird'),
sequence = require('../utils/sequence'), sequence = require('../utils/sequence'),
errors = require('../errors'), errors = require('../errors'),
Showdown = require('showdown-ghost'), legacyConverter = require('../utils/markdown-converter'),
legacyConverter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}),
htmlToText = require('html-to-text'), htmlToText = require('html-to-text'),
ghostBookshelf = require('./base'), ghostBookshelf = require('./base'),
events = require('../events'), events = require('../events'),
@ -234,11 +233,11 @@ Post = ghostBookshelf.Model.extend({
if (mobiledoc) { if (mobiledoc) {
this.set('html', utils.mobiledocConverter.render(JSON.parse(mobiledoc))); this.set('html', utils.mobiledocConverter.render(JSON.parse(mobiledoc)));
} else { } else {
// legacy showdown mode // legacy markdown mode
this.set('html', legacyConverter.makeHtml(_.toString(this.get('markdown')))); this.set('html', legacyConverter.render(_.toString(this.get('markdown'))));
} }
if (this.hasChanged('html')) { if (this.hasChanged('html') || !this.get('plaintext')) {
this.set('plaintext', htmlToText.fromString(this.get('html'), { this.set('plaintext', htmlToText.fromString(this.get('html'), {
wordwrap: 80, wordwrap: 80,
ignoreImage: true, ignoreImage: true,

View file

@ -112,7 +112,8 @@ utils = {
tokens: require('./tokens'), tokens: require('./tokens'),
sequence: require('./sequence'), sequence: require('./sequence'),
ghostVersion: require('./ghost-version'), ghostVersion: require('./ghost-version'),
mobiledocConverter: require('./mobiledoc-converter') mobiledocConverter: require('./mobiledoc-converter'),
markdownConverter: require('./markdown-converter')
}; };
module.exports = utils; module.exports = utils;

View file

@ -0,0 +1,26 @@
var MarkdownIt = require('markdown-it'),
converter = new MarkdownIt({
html: true,
breaks: true,
linkify: true
})
.use(require('markdown-it-footnote'))
.use(require('markdown-it-lazy-headers'))
.use(require('markdown-it-mark'))
.use(require('markdown-it-named-headers'), {
// match legacy Showdown IDs otherwise default is github style dasherized
slugify: function (inputString, usedHeaders) {
var slug = inputString.replace(/[^\w]/g, '').toLowerCase();
if (usedHeaders[slug]) {
usedHeaders[slug] += 1;
slug += usedHeaders[slug];
}
return slug;
}
});
module.exports = {
render: function (markdown) {
return converter.render(markdown);
}
};

View file

@ -167,7 +167,7 @@ describe('RSS', function () {
// item tags // item tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/); xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/); xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n\n/); xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/); xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);
xmlData.should.match(/<category><!\[CDATA\[public\]\]/); xmlData.should.match(/<category><!\[CDATA\[public\]\]/);
@ -197,7 +197,7 @@ describe('RSS', function () {
// special/optional tags // special/optional tags
xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/); xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/);
xmlData.should.match(/<description><!\[CDATA\[test stuff/); xmlData.should.match(/<description><!\[CDATA\[test stuff/);
xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n\n/); xmlData.should.match(/<content:encoded><!\[CDATA\[<h2 id="testing">testing<\/h2>\n/);
xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/); xmlData.should.match(/<img src="http:\/\/placekitten.com\/500\/200"/);
xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); xmlData.should.match(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/);

View file

@ -1,628 +0,0 @@
/**
* Client showdown integration tests
*
* Ensures that the final output from showdown + client extensions is as expected
*/
var should = require('should'), // jshint ignore:line
// Stuff we are testing
Showdown = require('showdown-ghost'),
converter = new Showdown.converter({extensions: ['ghostimagepreview', 'ghostgfm', 'footnotes', 'highlight']});
describe('Showdown client side converter', function () {
/*jslint regexp: true */
it('should replace showdown strike through with html', function () {
var testPhrase = {input: '~~foo_bar~~', output: /^<p><del>foo_bar<\/del><\/p>$/},
processedMarkup = converter.makeHtml(testPhrase.input);
// The image is the entire markup, so the image box should be too
processedMarkup.should.match(testPhrase.output);
});
it('should honour escaped tildes', function () {
var testPhrase = {input: '\\~\\~foo_bar\\~\\~', output: /^<p>~~foo_bar~~<\/p>$/},
processedMarkup = converter.makeHtml(testPhrase.input);
// The image is the entire markup, so the image box should be too
processedMarkup.should.match(testPhrase.output);
});
it('should not touch single underscores inside words', function () {
var testPhrase = {input: 'foo_bar', output: /^<p>foo_bar<\/p>$/},
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
// Currently failing - fixing this causes other issues
// it('should not create italic words between lines', function () {
// var testPhrase = {input: 'foo_bar\nbar_foo', output: /^<p>foo_bar <br \/>\nbar_foo<\/p>$/},
// processedMarkup = converter.makeHtml(testPhrase.input);
// processedMarkup.should.match(testPhrase.output);
// });
it('should not touch underscores in code blocks', function () {
var testPhrase = {input: ' foo_bar_baz', output: /^<pre><code>foo_bar_baz\n<\/code><\/pre>$/},
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
it('should not touch underscores in pre blocks', function () {
var testPhrases = [
{input: '<pre>\nfoo_bar_baz\n</pre>', output: /^<pre>\nfoo_bar_baz\n<\/pre>$/},
{input: '<pre>foo_bar_baz</pre>', output: /^<pre>foo_bar_baz<\/pre>$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should not escape double underscores at the beginning of a line', function () {
var testPhrases = [
{input: '\n__test__\n', output: /^<p><strong>test<\/strong><\/p>$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should not treat pre blocks with pre-text differently', function () {
var testPhrases = [
{
input: '<pre>\nthis is `a\\_test` and this\\_too and finally_this_is\n</pre>',
output: /^<pre>\nthis is `a\\_test` and this\\_too and finally_this_is\n<\/pre>$/
},
{
input: 'hmm<pre>\nthis is `a\\_test` and this\\_too and finally_this_is\n</pre>',
output: /^<p>hmm<\/p>\n\n<pre>\nthis is `a\\_test` and this\\_too and finally_this_is\n<\/pre>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should escape two or more underscores inside words', function () {
var testPhrases = [
{input: 'foo_bar_baz', output: /^<p>foo_bar_baz<\/p>$/},
{input: 'foo_bar_baz_bat', output: /^<p>foo_bar_baz_bat<\/p>$/},
{input: 'foo_bar_baz_bat_boo', output: /^<p>foo_bar_baz_bat_boo<\/p>$/},
{input: 'FOO_BAR', output: /^<p>FOO_BAR<\/p>$/},
{input: 'FOO_BAR_BAZ', output: /^<p>FOO_BAR_BAZ<\/p>$/},
{input: 'FOO_bar_BAZ_bat', output: /^<p>FOO_bar_BAZ_bat<\/p>$/},
{input: 'FOO_bar_BAZ_bat_BOO', output: /^<p>FOO_bar_BAZ_bat_BOO<\/p>$/},
{input: 'foo_BAR_baz_BAT_boo', output: /^<p>foo_BAR_baz_BAT_boo<\/p>$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should turn newlines into br tags in simple cases', function () {
var testPhrases = [
{input: 'fizz\nbuzz', output: /^<p>fizz <br \/>\nbuzz<\/p>$/},
{input: 'Hello world\nIt is a fine day', output: /^<p>Hello world <br \/>\nIt is a fine day<\/p>$/},
{input: '\'first\nsecond', output: /^<p>\'first <br \/>\nsecond<\/p>$/},
{input: '\'first\nsecond', output: /^<p>\'first <br \/>\nsecond<\/p>$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should convert newlines in all groups', function () {
var testPhrases = [
{
input: 'ruby\npython\nerlang',
output: /^<p>ruby <br \/>\npython <br \/>\nerlang<\/p>$/
},
{
input: 'Hello world\nIt is a fine day\nout',
output: /^<p>Hello world <br \/>\nIt is a fine day <br \/>\nout<\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should convert newlines in even long groups', function () {
var testPhrases = [
{
input: 'ruby\npython\nerlang\ngo',
output: /^<p>ruby <br \/>\npython <br \/>\nerlang <br \/>\ngo<\/p>$/
},
{
input: 'Hello world\nIt is a fine day\noutside\nthe window',
output: /^<p>Hello world <br \/>\nIt is a fine day <br \/>\noutside <br \/>\nthe window<\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should not convert newlines in lists', function () {
var testPhrases = [
{
input: '#fizz\n# buzz\n### baz',
output: /^<h1 id="fizz">fizz<\/h1>\n\n<h1 id="buzz">buzz<\/h1>\n\n<h3 id="baz">baz<\/h3>$/
},
{
input: '* foo\n* bar',
output: /^<ul>\n<li>foo<\/li>\n<li>bar<\/li>\n<\/ul>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should auto-link URL in text with markdown syntax', function () {
var testPhrases = [
{
input: 'http://google.co.uk',
output: /^<p><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: 'https://atest.com/fizz/buzz?baz=fizzbuzz',
output: /^<p><a href="https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz">https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz<\/a><\/p>$/
},
{
input: 'Some [ text (http://www.google.co.uk) some other text',
output: /^<p>Some \[ text \(<a href="http:\/\/www.google.co.uk">http:\/\/www.google.co.uk<\/a>\) some other text<\/p>$/
},
{
input: '>http://google.co.uk',
output: /^<blockquote>\n <p><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>\n<\/blockquote>$/
},
{
input: '> http://google.co.uk',
output: /^<blockquote>\n <p><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>\n<\/blockquote>$/
},
{
input: '<>>> http://google.co.uk',
output: /^<p>&lt;>>> <a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: '<strong>http://google.co.uk',
output: /^<p><strong><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: '# http://google.co.uk',
output: /^<h1 id="httpgooglecouk"><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/h1>$/
},
{
input: '* http://google.co.uk',
output: /^<ul>\n<li><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/li>\n<\/ul>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should convert reference format URL', function () {
var testPhrases = [
{
input: '[Google][1]\n\n[1]: http://google.co.uk',
output: /^<p><a href="http:\/\/google.co.uk">Google<\/a><\/p>$/
},
{
input: '[Google][1]\n\n[1]: http://google.co.uk \"some text\"',
output: /^<p><a href="http:\/\/google.co.uk" title="some text">Google<\/a><\/p>$/
},
{
input: '[http://google.co.uk]: http://google.co.uk\n\n[Hello][http://google.co.uk]',
output: /^<p><a href="http:\/\/google.co.uk">Hello<\/a><\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
/* No ref-style for now
it('should convert reference format image', function () {
var testPhrases = [
{
input: '![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg',
output: /^<section.*?<img.*?src="http:\/\/dsurl.stuff\/something.jpg"\/>.*?<\/section>$/,
},
{
input: '![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg \"some text\"',
output: /^<section.*?<img.*?src="http:\/\/dsurl.stuff\/something.jpg"\/>.*?<\/section>$/
},
{
input: '[http://www.google.co.uk]: http://www.google.co.uk\n\n![Hello][http://www.google.co.uk]',
output: /^<section.*?<img.*?src="http:\/\/www.google.co.uk"\/>.*?<\/section>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
*/
it('should NOT auto-link URL in HTML', function () {
var testPhrases = [
{
input: '<img src="http://placekitten.com/50">',
output: /^<p><img src=\"http:\/\/placekitten.com\/50\"><\/p>$/
},
{
input: '<img src="http://placekitten.com/50" />',
output: /^<p><img src=\"http:\/\/placekitten.com\/50\" \/><\/p>$/
},
{
input: '<script type="text/javascript" src="http://google.co.uk"></script>',
output: /^<script type=\"text\/javascript\" src=\"http:\/\/google.co.uk\"><\/script>$/
},
{
input: '<a href="http://facebook.com">http://google.co.uk</a>',
output: /^<p><a href=\"http:\/\/facebook.com\">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: '<a href="http://facebook.com">test</a> http://google.co.uk',
output: /^<p><a href=\"http:\/\/facebook.com\">test<\/a> <a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should NOT escape underscore inside of code/pre blocks', function () {
var testPhrase = {
input: '```\n_____\n```',
output: /^<pre><code>_____ \n<\/code><\/pre>$/
},
processedMarkup;
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
it('should NOT auto-link URLS inside of code/pre blocks', function () {
var testPhrases = [
{
input: '```\nurl: http://google.co.uk\n```',
output: /^<pre><code>url: http:\/\/google.co.uk \n<\/code><\/pre>$/
},
{
input: '`url: http://google.co.uk`',
output: /^<p><code>url: http:\/\/google.co.uk<\/code><\/p>$/
},
{
input: 'Hello type some `url: http://google.co.uk` stuff',
output: /^<p>Hello type some <code>url: http:\/\/google.co.uk<\/code> stuff<\/p>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should not display anything for reference URL', function () {
var testPhrases = [
{
input: '[1]: http://www.google.co.uk',
output: /^$/
},
{
input: '[http://www.google.co.uk]: http://www.google.co.uk',
output: /^$/
},
{
input: '[1]: http://dsurl.stuff/something.jpg',
output: /^$/
},
{
input: '[1]:http://www.google.co.uk',
output: /^$/
},
{
input: ' [1]:http://www.google.co.uk',
output: /^$/
},
{
input: '',
output: /^$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should show placeholder for image markdown', function () {
var testPhrases = [
{input: '![image and another,/ image](http://dsurl stuff)', output: /^<section.*?section>\n*$/},
{input: '![image and another,/ image]', output: /^<section.*?section>\n*$/},
{input: '![]()', output: /^<section.*?section>\n*$/},
{input: '![]', output: /^<section.*?section>\n*$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should have placeholder with image ONLY if image URL is present and valid', function () {
var testPhrases = [
{
input: '![image stuff](http://dsurl.stuff/something.jpg)',
output: /^<section.*?<img class="js-upload-target.*?<\/section>$/
},
{input: '![]', output: /<img class="js-upload-target"/, not: true},
{input: '![]', output: /^<section.*?<\/section>$/},
{input: '![]()', output: /<img class="js-upload-target"/, not: true},
{input: '![]()', output: /^<section.*?<\/section>$/},
{input: '![]', output: /<img class="js-upload-target"/, not: true},
{input: '![]', output: /^<section.*?<\/section>$/}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
if (testPhrase.not) {
processedMarkup.should.not.match(testPhrase.output);
} else {
processedMarkup.should.match(testPhrase.output);
}
});
});
/* No ref-style for now
it('should have placeholder with image if image reference is present', function () {
var testPhrases = [
{
input: '![alt][id]\n\n[id]: http://dsurl.stuff/something.jpg',
output: /^<section.*?<img class="js-upload-target.*?<\/section>$/
},
{input: '![][]', output: /^<section.*?<\/section>$/},
{input: '![][]', output: /<img class="js-upload-target"/, not: true},
{input: '![][id]', output: /^<section.*?<\/section>$/},
{input: '![][id]', output: /<img class="js-upload-target"/, not: true}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
if (testPhrase.not) {
processedMarkup.should.not.match(testPhrase.output);
} else {
processedMarkup.should.match(testPhrase.output);
}
});
});
*/
it('should correctly output link and image markdown without autolinks', function () {
var testPhrases = [
{
input: '[1](http://google.co.uk)',
output: /^<p><a href="http:\/\/google.co.uk">1<\/a><\/p>$/
},
{
input: ' [1](http://google.co.uk)',
output: /^<p><a href="http:\/\/google.co.uk">1<\/a><\/p>$/
},
{
input: '[http://google.co.uk](http://google.co.uk)',
output: /^<p><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: '[http://google.co.uk][id]\n\n[id]: http://google.co.uk',
output: /^<p><a href="http:\/\/google.co.uk">http:\/\/google.co.uk<\/a><\/p>$/
},
{
input: '![http://google.co.uk/kitten.jpg](http://google.co.uk/kitten.jpg)',
output: /^<section.*?((?!<a href="http:\/\/google.co.uk\/kitten.jpg").)*<\/section>$/
},
{
input: '![image stuff](http://dsurl.stuff/something)',
output: /^<section.*?((?!<a href="http:\/\/dsurl.stuff\/something").)*<\/section>$/
}
],
processedMarkup;
testPhrases.forEach(function (testPhrase) {
processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should output block HTML untouched', function () {
var testPhrases = [
{
input: '<table class=\"test\">\n <tr>\n <td>Foo</td>\n </tr>\n <tr>\n <td>Bar</td>\n </tr>\n</table>',
output: /^<table class=\"test\"> \n <tr>\n <td>Foo<\/td>\n <\/tr>\n <tr>\n <td>Bar<\/td>\n <\/tr>\n<\/table>$/
},
{
input: '<hr />',
output: /^<hr \/>$/
},
{ // audio isn't counted as a block tag by showdown so gets wrapped in <p></p>
input: '<audio class=\"podcastplayer\" controls>\n <source src=\"foobar.mp3\" type=\"audio/mp3\" preload=\"none\"></source>\n <source src=\"foobar.off\" type=\"audio/ogg\" preload=\"none\"></source>\n</audio>',
output: /^<audio class=\"podcastplayer\" controls> \n <source src=\"foobar.mp3\" type=\"audio\/mp3\" preload=\"none\"><\/source>\n <source src=\"foobar.off\" type=\"audio\/ogg\" preload=\"none\"><\/source>\n<\/audio>$/
}
];
testPhrases.forEach(function (testPhrase) {
var processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
it('should treat ![^n] as footnote unless it occurs on a new line', function () {
var testPhrases = [
{
input: 'Foo![^1](bar)',
output: '<p>Foo!<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>(bar)</p>'
},
{
input: '![^1](bar)',
output: '<section class="js-drop-zone image-uploader"><img class="js-upload-target" src="bar"/><div class="description">Add image of <strong>^1</strong></div><input data-url="upload" class="js-fileupload main fileupload" type="file" name="uploadimage"></section>'
}
];
testPhrases.forEach(function (testPhrase) {
var processedMarkup = converter.makeHtml(testPhrase.input);
processedMarkup.should.match(testPhrase.output);
});
});
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
// it('should correctly convert quotes to curly quotes', function () {
// var testPhrases = [
// {
// input: 'Hello world\nIt's a fine day\nout',
// output: /^<p>Hello world <br \/>\nIts a fine day <br \/>\nout<\/p>$/}
// ];
// testPhrases.forEach(function (testPhrase) {
// processedMarkup = converter.makeHtml(testPhrase.input);
// processedMarkup.should.match(testPhrase.output);
// });
// });
});

View file

@ -29,6 +29,6 @@ describe('Convert mobiledoc to HTML ', function () {
] ]
}; };
it('Converts a mobiledoc to HTML', function () { it('Converts a mobiledoc to HTML', function () {
converter.render(mobiledoc).should.match('<p>test</p><div class="kg-card-markdown"><h1 id="heading">heading</h1>\n\n<ul>\n<li>list one</li>\n<li>list two</li>\n<li>list three</li>\n</ul></div><div class="kg-card-html"><p>HTML CARD</p></div>'); converter.render(mobiledoc).should.match('<p>test</p><div class="kg-card-markdown"><h1 id="heading">heading</h1>\n<ul>\n<li>list one</li>\n<li>list two</li>\n<li>list three</li>\n</ul>\n</div><div class="kg-card-html"><p>HTML CARD</p></div>');
}); });
}); });

View file

@ -28,7 +28,7 @@ DataGenerator.Content = {
title: "Short and Sweet", title: "Short and Sweet",
slug: "short-and-sweet", slug: "short-and-sweet",
markdown: "## testing\n\nmctesters\n\n- test\n- line\n- items", markdown: "## testing\n\nmctesters\n\n- test\n- line\n- items",
html: "<h2 id=\"testing\">testing</h2>\n\n<p>mctesters</p>\n\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>", html: "<h2 id=\"testing\">testing</h2>\n<p>mctesters</p>\n<ul>\n<li>test</li>\n<li>line</li>\n<li>items</li>\n</ul>\n",
feature_image: "http://placekitten.com/500/200", feature_image: "http://placekitten.com/500/200",
meta_description: "test stuff", meta_description: "test stuff",
published_at: new Date("2015-01-03"), published_at: new Date("2015-01-03"),

View file

@ -62,6 +62,11 @@
"knex": "0.13.0", "knex": "0.13.0",
"knex-migrator": "2.0.16", "knex-migrator": "2.0.16",
"lodash": "4.17.4", "lodash": "4.17.4",
"markdown-it": "8.3.1",
"markdown-it-footnote": "3.0.1",
"markdown-it-lazy-headers": "0.1.3",
"markdown-it-mark": "2.0.0",
"markdown-it-named-headers": "0.0.4",
"mobiledoc-dom-renderer": "0.6.5", "mobiledoc-dom-renderer": "0.6.5",
"moment": "2.18.1", "moment": "2.18.1",
"moment-timezone": "0.5.13", "moment-timezone": "0.5.13",
@ -79,7 +84,6 @@
"rss": "1.2.2", "rss": "1.2.2",
"sanitize-html": "1.14.1", "sanitize-html": "1.14.1",
"semver": "5.3.0", "semver": "5.3.0",
"showdown-ghost": "0.3.6",
"simple-dom": "0.3.2", "simple-dom": "0.3.2",
"simple-html-tokenizer": "0.4.1", "simple-html-tokenizer": "0.4.1",
"superagent": "3.5.2", "superagent": "3.5.2",
@ -130,7 +134,6 @@
"ignore": [ "ignore": [
"glob", "glob",
"nodemailer", "nodemailer",
"showdown-ghost",
"grunt", "grunt",
"grunt-bg-shell", "grunt-bg-shell",
"grunt-cli", "grunt-cli",

View file

@ -2959,6 +2959,12 @@ liftoff@~2.2.0:
rechoir "^0.6.2" rechoir "^0.6.2"
resolve "^1.1.7" resolve "^1.1.7"
linkify-it@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.0.3.tgz#d94a4648f9b1c179d64fa97291268bdb6ce9434f"
dependencies:
uc.micro "^1.0.1"
livereload-js@^2.2.0: livereload-js@^2.2.0:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2" resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.2.2.tgz#6c87257e648ab475bc24ea257457edcc1f8d0bc2"
@ -3255,6 +3261,34 @@ map-obj@^1.0.0, map-obj@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
markdown-it-footnote@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.1.tgz#7f3730747cacc86e2fe0bf8a17a710f34791517a"
markdown-it-lazy-headers@0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/markdown-it-lazy-headers/-/markdown-it-lazy-headers-0.1.3.tgz#e70dd4da79c87a9ce82ca4701b8b7c0e2d72297b"
markdown-it-mark@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-mark/-/markdown-it-mark-2.0.0.tgz#46a1aa947105aed8188978e0a016179e404f42c7"
markdown-it-named-headers@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/markdown-it-named-headers/-/markdown-it-named-headers-0.0.4.tgz#82efc28324240a6b1e77b9aae501771d5f351c1f"
dependencies:
string "^3.0.1"
markdown-it@8.3.1:
version "8.3.1"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.3.1.tgz#2f4b622948ccdc193d66f3ca2d43125ac4ac7323"
dependencies:
argparse "^1.0.7"
entities "~1.1.1"
linkify-it "^2.0.0"
mdurl "^1.0.1"
uc.micro "^1.0.3"
matchdep@1.0.1: matchdep@1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-1.0.1.tgz#a57a33804491fbae208aba8f68380437abc2dca5" resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-1.0.1.tgz#a57a33804491fbae208aba8f68380437abc2dca5"
@ -3277,6 +3311,10 @@ maxmin@^1.1.0:
gzip-size "^1.0.0" gzip-size "^1.0.0"
pretty-bytes "^1.0.0" pretty-bytes "^1.0.0"
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
media-typer@0.3.0: media-typer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -4807,10 +4845,6 @@ should@11.2.1:
should-type-adaptors "^1.0.1" should-type-adaptors "^1.0.1"
should-util "^1.0.0" should-util "^1.0.0"
showdown-ghost@0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/showdown-ghost/-/showdown-ghost-0.3.6.tgz#ec73685cc5b4790352b00ed9e2cb26efc337d2f1"
sigmund@^1.0.1, sigmund@~1.0.0: sigmund@^1.0.1, sigmund@~1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590"
@ -4981,6 +5015,10 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0" is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
string@^3.0.1:
version "3.3.3"
resolved "https://registry.yarnpkg.com/string/-/string-3.3.3.tgz#5ea211cd92d228e184294990a6cc97b366a77cb0"
string_decoder@~0.10.x: string_decoder@~0.10.x:
version "0.10.31" version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@ -5234,6 +5272,10 @@ typedarray@~0.0.5:
version "0.0.6" version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
uc.micro@^1.0.1, uc.micro@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
uglify-js@^2.6, uglify-js@~2.7.0: uglify-js@^2.6, uglify-js@~2.7.0:
version "2.7.5" version "2.7.5"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"