2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Adding Github Flavored Markdown support

closes #422, issue #295

- Added GFM mode to codemirror
- Took the github.js extension for Showdown and added all useful behaviour
- Now supports strikethrough, line breaking and
  multiple underscores, and auto linking urls & emails without breaking
  definition urls
- Also added definition url handling in preparation for #295
- Added unit tests for the extentions individually and integrated with
  showdown
This commit is contained in:
Hannah Wolfe 2013-08-19 22:52:50 +01:00
parent c11e30a17c
commit f318d164d4
15 changed files with 919 additions and 44 deletions

View file

@ -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: {

View file

@ -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);
}
};
};

View file

@ -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");

View file

@ -0,0 +1,74 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: GFM mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="../../addon/mode/overlay.js"></script>
<script src="../xml/xml.js"></script>
<script src="../markdown/markdown.js"></script>
<script src="gfm.js"></script>
<!-- Code block highlighting modes -->
<script src="../javascript/javascript.js"></script>
<script src="../css/css.js"></script>
<script src="../htmlmixed/htmlmixed.js"></script>
<script src="../clike/clike.js"></script>
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<link rel="stylesheet" href="../../doc/docs.css">
</head>
<body>
<h1>CodeMirror: GFM mode</h1>
<form><textarea id="code" name="code">
GitHub Flavored Markdown
========================
Everything from markdown plus GFM features:
## URL autolinking
Underscores_are_allowed_between_words.
## Fenced code blocks (and syntax highlighting)
```javascript
for (var i = 0; i &lt; items.length; i++) {
console.log(items[i], i); // log them
}
```
## Task Lists
- [ ] Incomplete task list item
- [x] **Completed** task list item
## A bit of GitHub spice
* SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* \#Num: #1
* User/#Num: mojombo#1
* User/Project#Num: mojombo/god#1
See http://github.github.com/github-flavored-markdown/.
</textarea></form>
<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
mode: 'gfm',
lineNumbers: true,
theme: "default"
});
</script>
<p>Optionally depends on other modes for properly highlighted code blocks.</p>
<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#gfm_*">normal</a>, <a href="../../test/index.html#verbose,gfm_*">verbose</a>.</p>
</body>
</html>

View file

@ -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/]");
})();

View file

@ -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 = '<img class="js-upload-target" src="' + src + '"/>';
} else if (id && defUrls.hasOwnProperty(id)) {
result = '<img class="js-upload-target" src="' + defUrls[id][2] + '"/>';
}
return '<section class="js-drop-zone image-uploader">' + result +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +

View file

@ -344,9 +344,9 @@
initMarkdown: function () {
var self = this;
this.converter = new Showdown.converter({extensions: ['ghostdown']});
this.converter = new Showdown.converter({extensions: ['ghostdown', 'github']});
this.editor = CodeMirror.fromTextArea(document.getElementById('entry-markdown'), {
mode: 'markdown',
mode: 'gfm',
tabMode: 'indent',
tabindex: "2",
lineWrapping: true,

View file

@ -5,7 +5,8 @@ var Post,
when = require('when'),
errors = require('../errorHandling'),
Showdown = require('showdown'),
converter = new Showdown.converter(),
github = require('../../shared/vendor/showdown/extensions/github'),
converter = new Showdown.converter({extensions: [github]}),
User = require('./user').User,
GhostBookshelf = require('./base');

View file

@ -52,9 +52,12 @@
<script src="/shared/vendor/jquery/jquery.fileupload.js"></script>
<script src="/public/vendor/codemirror/codemirror.js"></script>
<script src="/public/vendor/codemirror/addon/mode/overlay.js"></script>
<script src="/public/vendor/codemirror/mode/markdown/markdown.js"></script>
<script src="/public/vendor/codemirror/mode/gfm/gfm.js"></script>
<script src="/public/vendor/showdown/showdown.js"></script>
<script src="/public/vendor/showdown/extensions/ghostdown.js"></script>
<script src="/shared/vendor/showdown/extensions/github.js"></script>
<script src="/public/vendor/shortcuts.js"></script>
<script src="/public/vendor/countable.js"></script>
<script src="/public/vendor/to-title-case.js"></script>

View file

@ -0,0 +1,102 @@
//
// Github Extension (WIP)
// ~~strike-through~~ -> <del>strike-through</del>
//
(function () {
var github = function (converter) {
return [
{
// strike-through
// NOTE: showdown already replaced "~" with "~T", so we need to adjust accordingly.
type : 'lang',
regex : '(~T){2}([^~]+)(~T){2}',
replace : function (match, prefix, content, suffix) {
return '<del>' + content + '</del>';
}
},
{
// GFM newline and underscore modifications
type : 'lang',
filter : function (text) {
var extractions = {},
hashID = 0;
function hashId() {
return hashID++;
}
// Extract pre blocks
text = text.replace(/<pre>[\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 <br /> 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 "<a href='" + href + "'>" + wholeMatch + "</a>";
});
text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function (wholeMatch) {
return "<a href='mailto:" + wholeMatch + "'>" + wholeMatch + "</a>";
});
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;
}());

View file

@ -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](http://dsurl.stuff)",
"![](http://dsurl.stuff)",
"![][]",
"![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(/^<section.*?section>\n*$/);
});
});
it("should correctly include an image", function () {
[
"![image and another,/ image](http://dsurl.stuff)",
"![](http://dsurl.stuff)",
"![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(/<img class="js-upload-target"/);
});
});
});

View file

@ -0,0 +1,294 @@
/**
* Client showdown integration tests
*
* Ensures that the final output from showdown + client extensions is as expected
*/
/*globals describe, it */
var Showdown = require('showdown'),
github = require('../../shared/vendor/showdown/extensions/github'),
ghostdown = require('../../client/assets/vendor/showdown/extensions/ghostdown'),
converter = new Showdown.converter({extensions: [ghostdown, github]}),
should = require('should');
describe("Showdown client side converter", function () {
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 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);
});
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 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's a fine day", output: /^<p>Hello world <br \/>\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: /^<p>ruby <br \/>\npython <br \/>\nerlang<\/p>$/},
{input: "Hello world\nIt's a fine day\nout", output: /^<p>Hello world <br \/>\nIt\'s 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's a fine day\noutside\nthe window",
output: /^<p>Hello world <br \/>\nIt\'s 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", 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>$/
}
],
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: /^<p><a href=\'mailto:info@tryghost.org\'>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: /^<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>$/
}
],
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.*?<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>$/
}
],
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: "![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);
}
});
});
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 NOT auto-link image URL", function () {
var testPhrases = [
{
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);
});
});
});

View file

@ -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](http://dsurl 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(/^<section.*?section>\n*$/);
});
});
});

View file

@ -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: /<del>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: /^<a href=\'http:\/\/google.co.uk\'>http:\/\/google.co.uk<\/a>$/},
{
input: "https://atest.com/fizz/buzz?baz=fizzbuzz",
output: /^<a href=\'https:\/\/atest.com\/fizz\/buzz\?baz=fizzbuzz\'>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: /^<a href=\'mailto:info@tryghost.org\'>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);
});
});