mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
Merge pull request #459 from ErisDS/showdown-gfm
Adding Github Flavored Markdown support
This commit is contained in:
commit
0a89cf12b2
19 changed files with 1608 additions and 469 deletions
12
Gruntfile.js
12
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: {
|
||||
|
|
59
core/client/assets/vendor/codemirror/addon/mode/overlay.js
vendored
Normal file
59
core/client/assets/vendor/codemirror/addon/mode/overlay.js
vendored
Normal 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);
|
||||
}
|
||||
};
|
||||
};
|
1052
core/client/assets/vendor/codemirror/codemirror.js
vendored
1052
core/client/assets/vendor/codemirror/codemirror.js
vendored
File diff suppressed because it is too large
Load diff
96
core/client/assets/vendor/codemirror/mode/gfm/gfm.js
vendored
Normal file
96
core/client/assets/vendor/codemirror/mode/gfm/gfm.js
vendored
Normal 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");
|
74
core/client/assets/vendor/codemirror/mode/gfm/index.html
vendored
Normal file
74
core/client/assets/vendor/codemirror/mode/gfm/index.html
vendored
Normal 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 < 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>
|
112
core/client/assets/vendor/codemirror/mode/gfm/test.js
vendored
Normal file
112
core/client/assets/vendor/codemirror/mode/gfm/test.js
vendored
Normal 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/]");
|
||||
})();
|
|
@ -8,7 +8,12 @@
|
|||
<script src="../../addon/edit/continuelist.js"></script>
|
||||
<script src="../xml/xml.js"></script>
|
||||
<script src="markdown.js"></script>
|
||||
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
|
||||
<style type="text/css">
|
||||
.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}
|
||||
.cm-s-default .cm-trailing-space-a:before,
|
||||
.cm-s-default .cm-trailing-space-b:before {position: absolute; content: "\00B7"; color: #777;}
|
||||
.cm-s-default .cm-trailing-space-new-line:before {position: absolute; content: "\21B5"; color: #777;}
|
||||
</style>
|
||||
<link rel="stylesheet" href="../../doc/docs.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
||||
|
||||
var htmlFound = CodeMirror.mimeModes.hasOwnProperty("text/html");
|
||||
var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? "text/html" : "text/plain");
|
||||
var htmlFound = CodeMirror.modes.hasOwnProperty("xml");
|
||||
var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? {name: "xml", htmlMode: true} : "text/plain");
|
||||
var aliases = {
|
||||
html: "htmlmixed",
|
||||
js: "javascript",
|
||||
|
@ -103,6 +103,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
state.f = inlineNormal;
|
||||
state.block = blockNormal;
|
||||
}
|
||||
// Reset state.trailingSpace
|
||||
state.trailingSpace = 0;
|
||||
state.trailingSpaceNewLine = false;
|
||||
// Mark this line as blank
|
||||
state.thisLineHasContent = false;
|
||||
return null;
|
||||
|
@ -217,6 +220,12 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
}
|
||||
}
|
||||
|
||||
if (state.trailingSpaceNewLine) {
|
||||
styles.push("trailing-space-new-line");
|
||||
} else if (state.trailingSpace) {
|
||||
styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b"));
|
||||
}
|
||||
|
||||
return styles.length ? styles.join(' ') : null;
|
||||
}
|
||||
|
||||
|
@ -308,11 +317,11 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
return type;
|
||||
}
|
||||
|
||||
if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, true)) {
|
||||
if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) {
|
||||
return switchInline(stream, state, inlineElement(linkinline, '>'));
|
||||
}
|
||||
|
||||
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, true)) {
|
||||
if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
|
||||
return switchInline(stream, state, inlineElement(linkemail, '>'));
|
||||
}
|
||||
|
||||
|
@ -369,6 +378,14 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
}
|
||||
}
|
||||
|
||||
if (ch === ' ') {
|
||||
if (stream.match(/ +$/, false)) {
|
||||
state.trailingSpace++;
|
||||
} else if (state.trailingSpace) {
|
||||
state.trailingSpaceNewLine = true;
|
||||
}
|
||||
}
|
||||
|
||||
return getType(state);
|
||||
}
|
||||
|
||||
|
@ -453,7 +470,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
taskList: false,
|
||||
list: false,
|
||||
listDepth: 0,
|
||||
quote: 0
|
||||
quote: 0,
|
||||
trailingSpace: 0,
|
||||
trailingSpaceNewLine: false
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -481,6 +500,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
list: s.list,
|
||||
listDepth: s.listDepth,
|
||||
quote: s.quote,
|
||||
trailingSpace: s.trailingSpace,
|
||||
trailingSpaceNewLine: s.trailingSpaceNewLine,
|
||||
md_inside: s.md_inside
|
||||
};
|
||||
},
|
||||
|
@ -504,6 +525,10 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
|
|||
// Reset state.code
|
||||
state.code = false;
|
||||
|
||||
// Reset state.trailingSpace
|
||||
state.trailingSpace = 0;
|
||||
state.trailingSpaceNewLine = false;
|
||||
|
||||
state.f = state.block;
|
||||
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length;
|
||||
var difference = Math.floor((indentation - state.indentation) / 4) * 4;
|
||||
|
|
|
@ -5,6 +5,20 @@
|
|||
MT("plainText",
|
||||
"foo");
|
||||
|
||||
// Don't style single trailing space
|
||||
MT("trailingSpace1",
|
||||
"foo ");
|
||||
|
||||
// Two or more trailing spaces should be styled with line break character
|
||||
MT("trailingSpace2",
|
||||
"foo[trailing-space-a ][trailing-space-new-line ]");
|
||||
|
||||
MT("trailingSpace3",
|
||||
"foo[trailing-space-a ][trailing-space-b ][trailing-space-new-line ]");
|
||||
|
||||
MT("trailingSpace4",
|
||||
"foo[trailing-space-a ][trailing-space-b ][trailing-space-a ][trailing-space-new-line ]");
|
||||
|
||||
// Code blocks using 4 spaces (regardless of CodeMirror.tabSize value)
|
||||
MT("codeBlocksUsing4Spaces",
|
||||
" [comment foo]");
|
||||
|
@ -533,9 +547,15 @@
|
|||
MT("linkWeb",
|
||||
"[link <http://example.com/>] foo");
|
||||
|
||||
MT("linkWebDouble",
|
||||
"[link <http://example.com/>] foo [link <http://example.com/>]");
|
||||
|
||||
MT("linkEmail",
|
||||
"[link <user@example.com>] foo");
|
||||
|
||||
MT("linkEmailDouble",
|
||||
"[link <user@example.com>] foo [link <user@example.com>]");
|
||||
|
||||
MT("emAsterisk",
|
||||
"[em *foo*] bar");
|
||||
|
||||
|
|
|
@ -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>' +
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
102
core/shared/vendor/showdown/extensions/github.js
vendored
Normal file
102
core/shared/vendor/showdown/extensions/github.js
vendored
Normal 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;
|
||||
}());
|
69
core/test/unit/client_ghostdown_spec.js
Normal file
69
core/test/unit/client_ghostdown_spec.js
Normal 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"/);
|
||||
});
|
||||
});
|
||||
});
|
294
core/test/unit/client_showdown_int_spec.js
Normal file
294
core/test/unit/client_showdown_int_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -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*$/);
|
||||
});
|
||||
});
|
||||
});
|
85
core/test/unit/shared_gfm_spec.js
Normal file
85
core/test/unit/shared_gfm_spec.js
Normal 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);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue