diff --git a/Gruntfile.js b/Gruntfile.js index d7b84ec577..7c1a41bce3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -183,8 +183,16 @@ var path = require('path'), src: ['core/test/unit/**/api*_spec.js'] }, - frontend: { - src: ['core/test/unit/**/frontend*_spec.js'] + client: { + src: ['core/test/unit/**/client*_spec.js'] + }, + + server: { + src: ['core/test/unit/**/server*_spec.js'] + }, + + shared: { + src: ['core/test/unit/**/shared*_spec.js'] }, perm: { diff --git a/core/client/assets/vendor/codemirror/addon/mode/overlay.js b/core/client/assets/vendor/codemirror/addon/mode/overlay.js new file mode 100644 index 0000000000..b7928a7bbf --- /dev/null +++ b/core/client/assets/vendor/codemirror/addon/mode/overlay.js @@ -0,0 +1,59 @@ +// Utility function that allows modes to be combined. The mode given +// as the base argument takes care of most of the normal mode +// functionality, but a second (typically simple) mode is used, which +// can override the style of text. Both modes get to parse all of the +// text, but when both assign a non-null style to a piece of code, the +// overlay wins, unless the combine argument was true, in which case +// the styles are combined. + +// overlayParser is the old, deprecated name +CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, combine) { + return { + startState: function() { + return { + base: CodeMirror.startState(base), + overlay: CodeMirror.startState(overlay), + basePos: 0, baseCur: null, + overlayPos: 0, overlayCur: null + }; + }, + copyState: function(state) { + return { + base: CodeMirror.copyState(base, state.base), + overlay: CodeMirror.copyState(overlay, state.overlay), + basePos: state.basePos, baseCur: null, + overlayPos: state.overlayPos, overlayCur: null + }; + }, + + token: function(stream, state) { + if (stream.start == state.basePos) { + state.baseCur = base.token(stream, state.base); + state.basePos = stream.pos; + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start; + state.overlayCur = overlay.token(stream, state.overlay); + state.overlayPos = stream.pos; + } + stream.pos = Math.min(state.basePos, state.overlayPos); + if (stream.eol()) state.basePos = state.overlayPos = 0; + + if (state.overlayCur == null) return state.baseCur; + if (state.baseCur != null && combine) return state.baseCur + " " + state.overlayCur; + else return state.overlayCur; + }, + + indent: base.indent && function(state, textAfter) { + return base.indent(state.base, textAfter); + }, + electricChars: base.electricChars, + + innerMode: function(state) { return {state: state.base, mode: base}; }, + + blankLine: function(state) { + if (base.blankLine) base.blankLine(state.base); + if (overlay.blankLine) overlay.blankLine(state.overlay); + } + }; +}; diff --git a/core/client/assets/vendor/codemirror/mode/gfm/gfm.js b/core/client/assets/vendor/codemirror/mode/gfm/gfm.js new file mode 100644 index 0000000000..1179b53dc6 --- /dev/null +++ b/core/client/assets/vendor/codemirror/mode/gfm/gfm.js @@ -0,0 +1,96 @@ +CodeMirror.defineMode("gfm", function(config) { + var codeDepth = 0; + function blankLine(state) { + state.code = false; + return null; + } + var gfmOverlay = { + startState: function() { + return { + code: false, + codeBlock: false, + ateSpace: false + }; + }, + copyState: function(s) { + return { + code: s.code, + codeBlock: s.codeBlock, + ateSpace: s.ateSpace + }; + }, + token: function(stream, state) { + // Hack to prevent formatting override inside code blocks (block and inline) + if (state.codeBlock) { + if (stream.match(/^```/)) { + state.codeBlock = false; + return null; + } + stream.skipToEnd(); + return null; + } + if (stream.sol()) { + state.code = false; + } + if (stream.sol() && stream.match(/^```/)) { + stream.skipToEnd(); + state.codeBlock = true; + return null; + } + // If this block is changed, it may need to be updated in Markdown mode + if (stream.peek() === '`') { + stream.next(); + var before = stream.pos; + stream.eatWhile('`'); + var difference = 1 + stream.pos - before; + if (!state.code) { + codeDepth = difference; + state.code = true; + } else { + if (difference === codeDepth) { // Must be exact + state.code = false; + } + } + return null; + } else if (state.code) { + stream.next(); + return null; + } + // Check if space. If so, links can be formatted later on + if (stream.eatSpace()) { + state.ateSpace = true; + return null; + } + if (stream.sol() || state.ateSpace) { + state.ateSpace = false; + if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) { + // User/Project@SHA + // User@SHA + // SHA + return "link"; + } else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) { + // User/Project#Num + // User#Num + // #Num + return "link"; + } + } + if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i)) { + // URLs + // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls + // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine + return "link"; + } + stream.next(); + return null; + }, + blankLine: blankLine + }; + CodeMirror.defineMIME("gfmBase", { + name: "markdown", + underscoresBreakWords: false, + taskLists: true, + fencedCodeBlocks: true + }); + return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay); +}, "markdown"); diff --git a/core/client/assets/vendor/codemirror/mode/gfm/index.html b/core/client/assets/vendor/codemirror/mode/gfm/index.html new file mode 100644 index 0000000000..826a96d2de --- /dev/null +++ b/core/client/assets/vendor/codemirror/mode/gfm/index.html @@ -0,0 +1,74 @@ + + +
+ +Optionally depends on other modes for properly highlighted code blocks.
+ +Parsing/Highlighting Tests: normal, verbose.
+ + + diff --git a/core/client/assets/vendor/codemirror/mode/gfm/test.js b/core/client/assets/vendor/codemirror/mode/gfm/test.js new file mode 100644 index 0000000000..3ccaec5010 --- /dev/null +++ b/core/client/assets/vendor/codemirror/mode/gfm/test.js @@ -0,0 +1,112 @@ +(function() { + var mode = CodeMirror.getMode({tabSize: 4}, "gfm"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + MT("emInWordAsterisk", + "foo[em *bar*]hello"); + + MT("emInWordUnderscore", + "foo_bar_hello"); + + MT("emStrongUnderscore", + "[strong __][em&strong _foo__][em _] bar"); + + MT("fencedCodeBlocks", + "[comment ```]", + "[comment foo]", + "", + "[comment ```]", + "bar"); + + MT("fencedCodeBlockModeSwitching", + "[comment ```javascript]", + "[variable foo]", + "", + "[comment ```]", + "bar"); + + MT("taskListAsterisk", + "[variable-2 * []] foo]", // Invalid; must have space or x between [] + "[variable-2 * [ ]]bar]", // Invalid; must have space after ] + "[variable-2 * [x]]hello]", // Invalid; must have space after ] + "[variable-2 * ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links + " [variable-3 * ][property [x]]][variable-3 foo]"); // Valid; can be nested + + MT("taskListPlus", + "[variable-2 + []] foo]", // Invalid; must have space or x between [] + "[variable-2 + [ ]]bar]", // Invalid; must have space after ] + "[variable-2 + [x]]hello]", // Invalid; must have space after ] + "[variable-2 + ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links + " [variable-3 + ][property [x]]][variable-3 foo]"); // Valid; can be nested + + MT("taskListDash", + "[variable-2 - []] foo]", // Invalid; must have space or x between [] + "[variable-2 - [ ]]bar]", // Invalid; must have space after ] + "[variable-2 - [x]]hello]", // Invalid; must have space after ] + "[variable-2 - ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links + " [variable-3 - ][property [x]]][variable-3 foo]"); // Valid; can be nested + + MT("taskListNumber", + "[variable-2 1. []] foo]", // Invalid; must have space or x between [] + "[variable-2 2. [ ]]bar]", // Invalid; must have space after ] + "[variable-2 3. [x]]hello]", // Invalid; must have space after ] + "[variable-2 4. ][meta [ ]]][variable-2 [world]]]", // Valid; tests reference style links + " [variable-3 1. ][property [x]]][variable-3 foo]"); // Valid; can be nested + + MT("SHA", + "foo [link be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] bar"); + + MT("shortSHA", + "foo [link be6a8cc] bar"); + + MT("tooShortSHA", + "foo be6a8c bar"); + + MT("longSHA", + "foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar"); + + MT("badSHA", + "foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar"); + + MT("userSHA", + "foo [link bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] hello"); + + MT("userProjectSHA", + "foo [link bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2] world"); + + MT("num", + "foo [link #1] bar"); + + MT("badNum", + "foo #1bar hello"); + + MT("userNum", + "foo [link bar#1] hello"); + + MT("userProjectNum", + "foo [link bar/hello#1] world"); + + MT("vanillaLink", + "foo [link http://www.example.com/] bar"); + + MT("vanillaLinkPunctuation", + "foo [link http://www.example.com/]. bar"); + + MT("vanillaLinkExtension", + "foo [link http://www.example.com/index.html] bar"); + + MT("notALink", + "[comment ```css]", + "[tag foo] {[property color][operator :][keyword black];}", + "[comment ```][link http://www.example.com/]"); + + MT("notALink", + "[comment ``foo `bar` http://www.example.com/``] hello"); + + MT("notALink", + "[comment `foo]", + "[link http://www.example.com/]", + "[comment `foo]", + "", + "[link http://www.example.com/]"); +})(); diff --git a/core/client/assets/vendor/showdown/extensions/ghostdown.js b/core/client/assets/vendor/showdown/extensions/ghostdown.js index edc996b9a2..242d337b90 100644 --- a/core/client/assets/vendor/showdown/extensions/ghostdown.js +++ b/core/client/assets/vendor/showdown/extensions/ghostdown.js @@ -5,12 +5,22 @@ { type: 'lang', filter: function (text) { - return text.replace(/\n?!\[([^\n\]]*)\](?:\(([^\n\)]*)\))?/gi, function (match, alt, src) { + var defRegex = /^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gim, + match, + defUrls = {}; + + while ((match = defRegex.exec(text)) !== null) { + defUrls[match[1]] = match; + } + + return text.replace(/^!(?:\[([^\n\]]*)\])(?:\[([^\n\]]*)\]|\(([^\n\]]*)\))?$/gim, function (match, alt, id, src) { var result = ""; /* regex from isURL in node-validator. Yum! */ if (src && src.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i)) { result = '[\s\S]*?<\/pre>/gim, function (x) { + var hash = hashId(); + extractions[hash] = x; + return "{gfm-js-extract-pre-" + hash + "}"; + }, 'm'); + + // prevent foo_bar_baz from ending up with an italic word in the middle + text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) { + return x.replace(/_/gm, '\\_'); + }); + + // in very clear cases, let newlines become
tags + text = text.replace(/^[\w\<][^\n]*\n+/gm, function (x) { + return x.match(/\n{2}/) ? x : x.trim() + " \n"; + }); + + text = text.replace(/\{gfm-js-extract-pre-([0-9]+)\}/gm, function (x, y) { + return "\n\n" + extractions[y]; + }); + + + return text; + } + }, + { + // Auto-link URLs and emails + type : 'lang', + filter : function (text) { + var extractions = {}, + hashID = 0; + + function hashId() { + return hashID++; + } + + // filter out def urls + text = text.replace(/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gim, + function (x) { + var hash = hashId(); + extractions[hash] = x; + return "{gfm-js-extract-ref-url-" + hash + "}"; + }); + + // taken from https://gist.github.com/jorilallo/1283095#L158 + text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function (wholeMatch, matchIndex) { + var left = text.slice(0, matchIndex), right = text.slice(matchIndex), + href; + if (left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) { + return wholeMatch; + } + href = wholeMatch.replace(/^http:\/\/github.com\//, "https://github.com/"); + return "" + wholeMatch + ""; + }); + + text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function (wholeMatch) { + return "" + wholeMatch + ""; + }); + + text = text.replace(/\{gfm-js-extract-ref-url-([0-9]+)\}/gm, function (x, y) { + return "\n\n" + extractions[y]; + }); + + return text; + } + } + ]; + }; + + // Client-side export + if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.github = github; } + // Server-side export + if (typeof module !== 'undefined') module.exports = github; +}()); \ No newline at end of file diff --git a/core/test/unit/client_ghostdown_spec.js b/core/test/unit/client_ghostdown_spec.js new file mode 100644 index 0000000000..b81debd75c --- /dev/null +++ b/core/test/unit/client_ghostdown_spec.js @@ -0,0 +1,69 @@ +/** + * Test the ghostdown extension + * + * Only ever runs on the client (i.e in the editor) + * Server processes showdown without it so there can never be an image upload form in a post. + */ + +/*globals describe, it */ +var gdPath = "../../client/assets/vendor/showdown/extensions/ghostdown.js", + should = require('should'), + ghostdown = require(gdPath); + +describe("Ghostdown showdown extensions", function () { + + it("should export an array of methods for processing", function () { + + ghostdown.should.be.a("function"); + ghostdown().should.be.an.instanceof(Array); + + ghostdown().forEach(function (processor) { + processor.should.be.a("object"); + processor.should.have.property("type"); + processor.should.have.property("filter"); + processor.type.should.be.a("string"); + processor.filter.should.be.a("function"); + }); + }); + + it("should accurately detect images in markdown", function () { + [ + "![]", + "![]()", + "![image and another,/ image]", + "![image and another,/ image]()", + "", + "", + "![][]", + "![image and another,/ image][stuff]", + "![][stuff]", + "![image and another,/ image][]" + ] + .forEach(function (imageMarkup) { + var processedMarkup = + ghostdown().reduce(function (prev, processor) { + return processor.filter(prev); + }, imageMarkup); + + // The image is the entire markup, so the image box should be too + processedMarkup.should.match(/^\n*$/); + }); + }); + + it("should correctly include an image", function () { + [ + "", + "", + "![image and another,/ image][test]\n\n[test]: http://dsurl.stuff", + "![][test]\n\n[test]: http://dsurl.stuff" + ] + .forEach(function (imageMarkup) { + var processedMarkup = + ghostdown().reduce(function (prev, processor) { + return processor.filter(prev); + }, imageMarkup); + + processedMarkup.should.match(/ 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 not touch single underscores inside words", function () { + var testPhrase = {input: "foo_bar", output: /^foo_bar<\/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: /^
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: "
\nfoo_bar_baz\n", output: /^\nfoo_bar_baz\n<\/pre>$/}, + {input: "foo_bar_baz", output: /^foo_bar_baz<\/pre>$/} + ], + 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: "\nthis is `a\\_test` and this\\_too and finally_this_is\n", output: /^\nthis is `a\\_test` and this\\_too and finally_this_is\n<\/pre>$/}, + {input: "hmm\nthis is `a\\_test` and this\\_too and finally_this_is\n", output: /^hmm<\/p>\n\n
\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: /^foo_bar_baz<\/p>$/}, + {input: "foo_bar_baz_bat", output: /^
foo_bar_baz_bat<\/p>$/}, + {input: "foo_bar_baz_bat_boo", output: /^
foo_bar_baz_bat_boo<\/p>$/}, + {input: "FOO_BAR", output: /^
FOO_BAR<\/p>$/}, + {input: "FOO_BAR_BAZ", output: /^
FOO_BAR_BAZ<\/p>$/}, + {input: "FOO_bar_BAZ_bat", output: /^
FOO_bar_BAZ_bat<\/p>$/}, + {input: "FOO_bar_BAZ_bat_BOO", output: /^
FOO_bar_BAZ_bat_BOO<\/p>$/}, + {input: "foo_BAR_baz_BAT_boo", output: /^
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: /^
fizz
\nbuzz<\/p>$/}, + {input: "Hello world\nIt's a fine day", output: /^Hello world
\nIt\'s a fine day<\/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: /^ruby
\npython
\nerlang<\/p>$/}, + {input: "Hello world\nIt's a fine day\nout", output: /^Hello world
\nIt\'s a fine day
\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: /^ruby
\npython
\nerlang
\ngo<\/p>$/}, + { + input: "Hello world\nIt's a fine day\noutside\nthe window", + output: /^Hello world
\nIt\'s a fine day
\noutside
\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: /^fizz<\/h1>\n\n
buzz<\/h1>\n\n
baz<\/h3>$/}, + {input: "* foo\n* bar", output: /^
\n
- foo<\/li>\n
- bar<\/li>\n<\/ul>$/} + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it("should auto-link URL", function () { + var testPhrases = [ + {input: "http://google.co.uk", output: /^
http:\/\/google.co.uk<\/a><\/p>$/}, + { + input: "https://atest.com/fizz/buzz?baz=fizzbuzz", + output: /^
https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz<\/a><\/p>$/ + } + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it("should auto-link Email", function () { + var testPhrase = {input: "info@tryghost.org", output: /^
info@tryghost.org<\/a><\/p>$/}, + processedMarkup = converter.makeHtml(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + // Fails + it("should convert reference format URL", function () { + var testPhrases = [ + { + input: "[Google][1]\n\n[1]: http://google.co.uk", + output: /^
Google<\/a><\/p>$/, + }, + { + input: "[Google][1]\n\n[1]: http://google.co.uk \"some text\"", + output: /^
Google<\/a><\/p>$/ + } + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it("should convert reference format image", function () { + var testPhrases = [ + { + input: "![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg", + output: /^
.*?<\/section>$/, + }, + { + input: "![Google][1]\n\n[1]: http://dsurl.stuff/something.jpg \"some text\"", + output: /^ .*?<\/section>$/ + } + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it("should NOT auto-link reference URL", function () { + var testPhrase = {input: "[1]: http://google.co.uk", output: /^$/}, + processedMarkup = converter.makeHtml(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + + it("should NOT auto-link image reference URL", function () { + var testPhrase = {input: "[1]: http://dsurl.stuff/something.jpg", output: /^$/}, + processedMarkup = converter.makeHtml(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it("should show placeholder for image markdown", function () { + var testPhrases = [ + {input: "", output: /^ \n*$/}, + {input: "![image and another,/ image]", output: /^ \n*$/}, + {input: "![]()", output: /^ \n*$/}, + {input: "![]", output: /^ \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: "", + output: /^ $/}, + {input: "![]()", output: / $/}, + {input: "![]", output: /
$/} + ], + 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 have placeholder with image if image reference is present", function () { + var testPhrases = [ + { + input: "![alt][id]\n\n[id]: http://dsurl.stuff/something.jpg", + output: /^
$/}, + {input: "![][]", output: / $/}, + {input: "![][id]", output: /
$/ + } + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = converter.makeHtml(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + +}); \ No newline at end of file diff --git a/core/test/unit/frontend_ghostdown_spec.js b/core/test/unit/frontend_ghostdown_spec.js deleted file mode 100644 index dc6f98d958..0000000000 --- a/core/test/unit/frontend_ghostdown_spec.js +++ /dev/null @@ -1,38 +0,0 @@ -/*globals describe, it */ -var gdPath = "../../client/assets/vendor/showdown/extensions/ghostdown.js", - should = require('should'), - ghostdown = require(gdPath); - -describe("Ghostdown showdown extensions", function () { - - it("should export an array of methods for processing", function () { - - ghostdown.should.be.a("function"); - ghostdown().should.be.an.instanceof(Array); - - ghostdown().forEach(function (processor) { - processor.should.be.a("object"); - processor.should.have.property("type"); - processor.should.have.property("filter"); - processor.type.should.be.a("string"); - processor.filter.should.be.a("function"); - }); - }); - - it("should accurately detect images in markdown", function () { - - [ "", - "![image and another,/ image]", - "![]()", - "![]" ] - .forEach(function (imageMarkup) { - var processedMarkup = - ghostdown().reduce(function(prev,processor) { - return processor.filter(prev); - },imageMarkup); - - // The image is the entire markup, so the image box should be too - processedMarkup.should.match(/^
\n*$/); - }); - }); -}); \ No newline at end of file diff --git a/core/test/unit/frontend_helpers_index_spec.js b/core/test/unit/server_helpers_index_spec.js similarity index 100% rename from core/test/unit/frontend_helpers_index_spec.js rename to core/test/unit/server_helpers_index_spec.js diff --git a/core/test/unit/shared_gfm_spec.js b/core/test/unit/shared_gfm_spec.js new file mode 100644 index 0000000000..43d7b4b10b --- /dev/null +++ b/core/test/unit/shared_gfm_spec.js @@ -0,0 +1,85 @@ +/** + * Tests the github extension for showdown + * + */ + +/*globals describe, it */ +var ghPath = "../../shared/vendor/showdown/extensions/github.js", + should = require('should'), + github = require(ghPath); + +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 github().reduce(function (text, ext) { + return _ExecuteExtension(ext, text); + }, testPhrase); +} + + +describe("Github showdown extensions", function () { + + it("should export an array of methods for processing", function () { + github.should.be.a("function"); + github().should.be.an.instanceof(Array); + + github().forEach(function (processor) { + processor.should.be.a("object"); + processor.should.have.property("type"); + processor.type.should.be.a("string"); + }); + + }); + + it("should replace showdown strike through with html", function () { + var testPhrase = {input: "~T~Tfoo_bar~T~T", output: / foo_bar<\/del>/}, + processedMarkup = _ConvertPhrase(testPhrase.input); + + // The image is the entire markup, so the image box should be too + processedMarkup.should.match(testPhrase.output); + }); + + it("should auto-link URL", function () { + var testPhrases = [ + {input: "http://google.co.uk", output: /^http:\/\/google.co.uk<\/a>$/}, + { + input: "https://atest.com/fizz/buzz?baz=fizzbuzz", + output: /^https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz<\/a>$/ + } + ], + processedMarkup; + + testPhrases.forEach(function (testPhrase) { + processedMarkup = _ConvertPhrase(testPhrase.input); + processedMarkup.should.match(testPhrase.output); + }); + }); + + it("should auto-link Email", function () { + var testPhrase = {input: "info@tryghost.org", output: /^info@tryghost.org<\/a>$/}, + processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it("should NOT auto-link reference URL", function () { + var testPhrase = {input: "[1]: http://google.co.uk", output: /^\n\n\[1\]: http:\/\/google.co.uk$/}, + processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); + + it("should NOT auto-link image URL", function () { + var testPhrase = {input: "[1]: http://dsurl.stuff/something.jpg", output: /^\n\n\[1\]: http:\/\/dsurl.stuff\/something.jpg$/}, + processedMarkup = _ConvertPhrase(testPhrase.input); + + processedMarkup.should.match(testPhrase.output); + }); +}); \ No newline at end of file