diff --git a/core/server/apps/default-cards/cards/markdown.js b/core/server/apps/default-cards/cards/markdown.js index 95a30c0be1..ac204b4c04 100644 --- a/core/server/apps/default-cards/cards/markdown.js +++ b/core/server/apps/default-cards/cards/markdown.js @@ -1,7 +1,6 @@ var SimpleDom = require('simple-dom'), tokenizer = require('simple-html-tokenizer').tokenize, - Showdown = require('showdown-ghost'), - converter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}), + markdownConverter = require('../../../utils/markdown-converter'), parser; module.exports = { @@ -9,6 +8,10 @@ module.exports = { type: 'dom', render(opts) { parser = new SimpleDom.HTMLParser(tokenizer, opts.env.dom, SimpleDom.voidMap); - return parser.parse('
' + converter.makeHtml(opts.payload.markdown || '') + '
'); + return parser.parse('' + + '
' + + markdownConverter.render(opts.payload.markdown || '') + + '
' + ); } }; diff --git a/core/server/apps/default-cards/tests/markdown_spec.js b/core/server/apps/default-cards/tests/markdown_spec.js index f2f1a29f6e..0bea0f9b88 100644 --- a/core/server/apps/default-cards/tests/markdown_spec.js +++ b/core/server/apps/default-cards/tests/markdown_spec.js @@ -15,6 +15,6 @@ describe('Markdown card', function () { }; var serializer = new SimpleDom.HTMLSerializer([]); - serializer.serialize(card.render(opts)).should.match('

HEADING

\n\n
'); + serializer.serialize(card.render(opts)).should.match('

HEADING

\n\n
'); }); }); diff --git a/core/server/models/post.js b/core/server/models/post.js index 3138499056..3c30ee963d 100644 --- a/core/server/models/post.js +++ b/core/server/models/post.js @@ -5,8 +5,7 @@ var _ = require('lodash'), Promise = require('bluebird'), sequence = require('../utils/sequence'), errors = require('../errors'), - Showdown = require('showdown-ghost'), - legacyConverter = new Showdown.converter({extensions: ['ghostgfm', 'footnotes', 'highlight']}), + legacyConverter = require('../utils/markdown-converter'), htmlToText = require('html-to-text'), ghostBookshelf = require('./base'), events = require('../events'), @@ -234,11 +233,11 @@ Post = ghostBookshelf.Model.extend({ if (mobiledoc) { this.set('html', utils.mobiledocConverter.render(JSON.parse(mobiledoc))); } else { - // legacy showdown mode - this.set('html', legacyConverter.makeHtml(_.toString(this.get('markdown')))); + // legacy markdown mode + 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'), { wordwrap: 80, ignoreImage: true, diff --git a/core/server/utils/index.js b/core/server/utils/index.js index dd7f0cb075..15e4acb68b 100644 --- a/core/server/utils/index.js +++ b/core/server/utils/index.js @@ -112,7 +112,8 @@ utils = { tokens: require('./tokens'), sequence: require('./sequence'), ghostVersion: require('./ghost-version'), - mobiledocConverter: require('./mobiledoc-converter') + mobiledocConverter: require('./mobiledoc-converter'), + markdownConverter: require('./markdown-converter') }; module.exports = utils; diff --git a/core/server/utils/markdown-converter.js b/core/server/utils/markdown-converter.js new file mode 100644 index 0000000000..8c8d97135b --- /dev/null +++ b/core/server/utils/markdown-converter.js @@ -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); + } +}; diff --git a/core/test/unit/rss_spec.js b/core/test/unit/rss_spec.js index d2f18ef012..0e55de12e9 100644 --- a/core/test/unit/rss_spec.js +++ b/core/test/unit/rss_spec.js @@ -167,7 +167,7 @@ describe('RSS', function () { // item tags xmlData.should.match(/<!\[CDATA\[Short and Sweet\]\]>/); 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(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); xmlData.should.match(/<category><!\[CDATA\[public\]\]/); @@ -197,7 +197,7 @@ describe('RSS', function () { // special/optional tags xmlData.should.match(/<title><!\[CDATA\[Short and Sweet\]\]>/); 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(/<media:content url="http:\/\/placekitten.com\/500\/200" medium="image"\/>/); diff --git a/core/test/unit/showdown_client_integrated_spec.js b/core/test/unit/showdown_client_integrated_spec.js deleted file mode 100644 index 9ab2bc33ca..0000000000 --- a/core/test/unit/showdown_client_integrated_spec.js +++ /dev/null @@ -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><>>> <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 \/>\nIt’s a fine day <br \/>\nout<\/p>$/} - // ]; - - // testPhrases.forEach(function (testPhrase) { - // processedMarkup = converter.makeHtml(testPhrase.input); - // processedMarkup.should.match(testPhrase.output); - // }); - // }); -}); diff --git a/core/test/unit/utils/mobiledoc-converter_spec.js b/core/test/unit/utils/mobiledoc-converter_spec.js index ce2712dc1f..667162022b 100644 --- a/core/test/unit/utils/mobiledoc-converter_spec.js +++ b/core/test/unit/utils/mobiledoc-converter_spec.js @@ -29,6 +29,6 @@ describe('Convert 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>'); }); }); diff --git a/core/test/utils/fixtures/data-generator.js b/core/test/utils/fixtures/data-generator.js index f7a1784eea..e5ad197ae8 100644 --- a/core/test/utils/fixtures/data-generator.js +++ b/core/test/utils/fixtures/data-generator.js @@ -28,7 +28,7 @@ DataGenerator.Content = { title: "Short and Sweet", slug: "short-and-sweet", 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", meta_description: "test stuff", published_at: new Date("2015-01-03"), diff --git a/package.json b/package.json index 94ab798d19..39262f0d3b 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,11 @@ "knex": "0.13.0", "knex-migrator": "2.0.16", "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", "moment": "2.18.1", "moment-timezone": "0.5.13", @@ -79,7 +84,6 @@ "rss": "1.2.2", "sanitize-html": "1.14.1", "semver": "5.3.0", - "showdown-ghost": "0.3.6", "simple-dom": "0.3.2", "simple-html-tokenizer": "0.4.1", "superagent": "3.5.2", @@ -130,7 +134,6 @@ "ignore": [ "glob", "nodemailer", - "showdown-ghost", "grunt", "grunt-bg-shell", "grunt-cli", diff --git a/yarn.lock b/yarn.lock index 856c5e8d9a..df720aa356 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2959,6 +2959,12 @@ liftoff@~2.2.0: rechoir "^0.6.2" 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: version "2.2.2" 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" 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: version "1.0.1" 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" 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: version "0.3.0" 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-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: version "1.0.1" 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" 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: version "0.10.31" 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" 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: version "2.7.5" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.7.5.tgz#4612c0c7baaee2ba7c487de4904ae122079f2ca8"