REFACTOR: Migrate markdown functionality in ES6

This commit is contained in:
Robin Ward
2016-06-14 14:31:51 -04:00
parent bc25d9a7a0
commit a546395397
146 changed files with 3259 additions and 5675 deletions

View File

@ -11,9 +11,6 @@ acceptance("Category hashtag", {
];
};
server.get('/category_hashtags/check', () => { //eslint-disable-line
return response({ valid: [{ slug: "bug", url: '/c/bugs' }] });
});
}
});

View File

@ -101,6 +101,10 @@ export default function() {
return response({ post_reply_histories: [{ id: 1234, cooked: 'wat' }] });
});
this.get('/category_hashtags/check', () => {
return response({ valid: [{ slug: "bug", url: '/c/bugs' }] });
});
this.put('/categories/:category_id', request => {
const category = parsePostData(request.requestBody);
return response({category});

View File

@ -22,7 +22,7 @@ export default function() {
return (this._tracker);
}
if (type === "site-settings:main") {
this._settings = this._settings || Discourse.SiteSettings.current();
this._settings = this._settings || Discourse.SiteSettings;
return (this._settings);
}
},

View File

@ -33,16 +33,11 @@ function AcceptanceModal(option, _relatedTarget) {
window.bootbox.$body = $('#ember-testing');
$.fn.modal = AcceptanceModal;
var oldAvatar = Discourse.Utilities.avatarImg;
function acceptance(name, options) {
module("Acceptance: " + name, {
setup() {
resetMobile();
// Don't render avatars in acceptance tests, it's faster and no 404s
Discourse.Utilities.avatarImg = () => "";
// For now don't do scrolling stuff in Test Mode
HeaderComponent.reopen({examineDockHeader: Ember.K});
@ -79,7 +74,6 @@ function acceptance(name, options) {
Discourse.User.resetCurrent();
Discourse.Site.resetCurrent(Discourse.Site.create(jQuery.extend(true, {}, fixtures['site.json'].site)));
Discourse.Utilities.avatarImg = oldAvatar;
Discourse.reset();
}
});

View File

@ -60,7 +60,7 @@ Discourse.SiteSettingsOriginal = {
"newuser_max_attachments":0,
"display_name_on_posts":true,
"short_progress_text_threshold":10000,
"default_code_lang":"lang-auto",
"default_code_lang":"auto",
"autohighlight_all_code":false,
"email_in":false,
"authorized_extensions":".jpg|.jpeg|.png|.gif|.svg|.txt|.ico|.yml",

View File

@ -1,156 +0,0 @@
import Quote from 'discourse/lib/quote';
import Post from 'discourse/models/post';
module("Discourse.BBCode");
var format = function(input, expected, text) {
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false, sanitize: true});
equal(cooked, "<p>" + expected + "</p>", text);
};
var formatQ = function(input, expected, text) {
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false, sanitize: true});
equal(cooked, expected, text);
};
test('basic bbcode', function() {
format("[b]strong[/b]", "<span class=\"bbcode-b\">strong</span>", "bolds text");
format("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text");
format("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text");
format("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text");
format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\">", "links images");
format("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title");
format("[b]evil [i]trout[/i][/b]",
"<span class=\"bbcode-b\">evil <span class=\"bbcode-i\">trout</span></span>",
"allows embedding of tags");
format("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports upper case bbcode");
format("[b]strong [b]stronger[/b][/b]", "<span class=\"bbcode-b\">strong <span class=\"bbcode-b\">stronger</span></span>", "accepts nested bbcode tags");
});
test('urls', function() {
format("[url]not a url[/url]", "not a url", "supports [url] that isn't a url");
format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without parameter");
format("[url=http://example.com]example[/url]", "<a href=\"http://example.com\">example</a>", "supports [url] with given href");
format("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]",
"<a href=\"http://www.example.com\"><img src=\"http://example.com/logo.png\"></a>",
"supports [url] with an embedded [img]");
});
test('invalid bbcode', function() {
var cooked = Discourse.Markdown.cook("[code]I am not closed\n\nThis text exists.", {lookupAvatar: false});
equal(cooked, "<p>[code]I am not closed</p>\n\n<p>This text exists.</p>", "does not raise an error with an open bbcode tag.");
});
test('code', function() {
format("[code]\nx++\n[/code]", "<pre><code class=\"lang-auto\">x++</code></pre>", "makes code into pre");
format("[code]\nx++\ny++\nz++\n[/code]", "<pre><code class=\"lang-auto\">x++\ny++\nz++</code></pre>", "makes code into pre");
format("[code]abc\n#def\n[/code]", '<pre><code class=\"lang-auto\">abc\n#def</code></pre>', 'it handles headings in a [code] block');
format("[code]\n s[/code]",
"<pre><code class=\"lang-auto\"> s</code></pre>",
"it doesn't trim leading whitespace");
});
test('lists', function() {
format("[ul][li]option one[/li][/ul]", "<ul><li>option one</li></ul>", "creates an ul");
format("[ol][li]option one[/li][/ol]", "<ol><li>option one</li></ol>", "creates an ol");
format("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "<ul><li>option one</li><li>option two</li></ul>", "suppresses empty lines in lists");
});
test('tags with arguments', function() {
format("[url=http://bettercallsaul.com]better call![/url]", "<a href=\"http://bettercallsaul.com\">better call!</a>", "supports [url] with a title");
format("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title");
format("[u][i]abc[/i][/u]", "<span class=\"bbcode-u\"><span class=\"bbcode-i\">abc</span></span>", "can nest tags");
format("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line");
});
test("quotes", function() {
var post = Post.create({
cooked: "<p><b>lorem</b> ipsum</p>",
username: "eviltrout",
post_number: 1,
topic_id: 2
});
var formatQuote = function(val, expected, text) {
equal(Quote.build(post, val), expected, text);
};
formatQuote(undefined, "", "empty string for undefined content");
formatQuote(null, "", "empty string for null content");
formatQuote("", "", "empty string for empty string content");
formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
formatQuote(" lorem \t ",
"[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
"trims white spaces before & after the quoted contents");
formatQuote("lorem ipsum",
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n",
"marks quotes as full when the quote is the full message");
formatQuote("**lorem** ipsum",
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n",
"keeps BBCode formatting");
formatQuote("this is <not> a bug",
"[quote=\"eviltrout, post:1, topic:2\"]\nthis is &lt;not&gt; a bug\n[/quote]\n\n",
"it escapes the contents of the quote");
format("[quote]test[/quote]",
"<aside class=\"quote\"><blockquote><p>test</p></blockquote></aside>",
"it supports quotes without params");
format("[quote]\n*test*\n[/quote]",
"<aside class=\"quote\"><blockquote><p><em>test</em></p></blockquote></aside>",
"it doesn't insert a new line for italics");
format("[quote=,script='a'><script>alert('test');//':a][/quote]",
"<aside class=\"quote\"><blockquote></blockquote></aside>",
"It will not create a script tag within an attribute");
});
test("quote formatting", function() {
formatQ("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]",
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
"<div class=\"quote-controls\"></div>EvilTrout:</div><blockquote><p>[sam]</p></blockquote></aside>",
"it allows quotes with [] inside");
formatQ("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout:" +
"</div><blockquote><p>abc</p></blockquote></aside>",
"renders quotes properly");
formatQ("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout:" +
"</div><blockquote><p>abc</p></blockquote></aside>\n\n<p>hello</p>",
"handles new lines properly");
formatQ("[quote=\"Alice, post:1, topic:1\"]\n[quote=\"Bob, post:2, topic:1\"]\n[/quote]\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:" +
"</div><blockquote><aside class=\"quote\" data-post=\"2\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Bob:" +
"</div><blockquote></blockquote></aside></blockquote></aside>",
"quotes can be nested");
formatQ("[quote=\"Alice, post:1, topic:1\"]\n[quote=\"Bob, post:2, topic:1\"]\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:" +
"</div><blockquote><p>[quote=\"Bob, post:2, topic:1\"]</p></blockquote></aside>",
"handles mismatched nested quote tags");
formatQ("[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo =&#x27;foo&#x27;;\nvar bar = &#x27;bar&#x27;;</code></pre></p></blockquote></aside>",
"quotes can have code blocks without leading newline");
formatQ("[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo =&#x27;foo&#x27;;\nvar bar = &#x27;bar&#x27;;</code></pre></p></blockquote></aside>",
"quotes can have code blocks with leading newline");
});
test("quotes with trailing formatting", function() {
var cooked = Discourse.Markdown.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]\nhello\n[/quote]\n*Test*", {lookupAvatar: false});
equal(cooked,
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
"<div class=\"quote-controls\"></div>EvilTrout:</div><blockquote><p>hello</p></blockquote></aside>\n\n<p><em>Test</em></p>",
"it allows trailing formatting");
});

View File

@ -1,15 +1,13 @@
import { emojiSearch, IMAGE_VERSION as v } from 'pretty-text/emoji';
import { emojiUnescape } from 'discourse/lib/text';
module('emoji');
module('lib:emoji');
var testUnescape = function(input, expected, description) {
var unescaped = Discourse.Emoji.unescape(input);
equal(unescaped, expected, description);
function testUnescape(input, expected, description) {
equal(emojiUnescape(input), expected, description);
};
test("Emoji.unescape", function(){
const v = Discourse.Emoji.ImageVersion;
test("emojiUnescape", () => {
testUnescape("Not emoji :O) :frog) :smile)", "Not emoji :O) :frog) :smile)", "title without emoji");
testUnescape("Not emoji :frog :smile", "Not emoji :frog :smile", "end colon is not optional");
testUnescape("emoticons :)", "emoticons <img src='/images/emoji/emoji_one/slight_smile.png?v=2' title='slight_smile' alt='slight_smile' class='emoji'>", "emoticons are still supported");
@ -23,12 +21,12 @@ test("Emoji.unescape", function(){
});
test("Emoji.search", function(){
test("Emoji search", () => {
// able to find an alias
equal(Discourse.Emoji.search("+1").length, 1);
equal(emojiSearch("+1").length, 1);
// able to find middle of line search
equal(Discourse.Emoji.search("check", {maxResults: 3}).length, 3);
equal(emojiSearch("check", {maxResults: 3}).length, 3);
});

View File

@ -1,23 +0,0 @@
module("Discourse.Onebox", {
setup: function() {
this.anchor = $("<a href='http://bla.com'></a>")[0];
}
});
asyncTestDiscourse("Stops rapid calls with cache true", function() {
sandbox.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
Discourse.Onebox.load(this.anchor, true);
Discourse.Onebox.load(this.anchor, true);
start();
ok(Discourse.ajax.calledOnce);
});
asyncTestDiscourse("Stops rapid calls with cache true", function() {
sandbox.stub(Discourse, "ajax").returns(Ember.RSVP.resolve());
Discourse.Onebox.load(this.anchor, false);
Discourse.Onebox.load(this.anchor, false);
start();
ok(Discourse.ajax.calledOnce);
});

View File

@ -1,21 +1,41 @@
module("Discourse.Markdown", {
setup: function() {
Discourse.SiteSettings.traditional_markdown_linebreaks = false;
Discourse.SiteSettings.default_code_lang = "auto";
}
import Quote from 'discourse/lib/quote';
import Post from 'discourse/models/post';
import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text';
import { IMAGE_VERSION as v} from 'pretty-text/emoji';
module("lib:pretty-text");
const defaultOpts = buildOptions({
siteSettings: {
enable_emoji: true,
emoji_set: 'emoji_one',
highlighted_languages: 'json|ruby|javascript',
default_code_lang: 'auto',
censored_words: 'shucks|whiz|whizzer'
},
getURL: url => url
});
var cooked = function(input, expected, text) {
var result = Discourse.Markdown.cook(input, {sanitize: true});
expected = expected.replace(/\/>/g, ">");
// result = result.replace("/>", ">");
equal(result, expected, text);
function cooked(input, expected, text) {
equal(new PrettyText(defaultOpts).cook(input), expected.replace(/\/>/g, ">"), text);
};
var cookedOptions = function(input, opts, expected, text) {
equal(Discourse.Markdown.cook(input, opts), expected, text);
function cookedOptions(input, opts, expected, text) {
equal(new PrettyText(_.merge({}, defaultOpts, opts)).cook(input), expected, text);
};
function cookedPara(input, expected, text) {
cooked(input, `<p>${expected}</p>`, text);
};
test("buildOptions", () => {
ok(buildOptions({ siteSettings: { allow_html_tables: true } }).features.table, 'tables enabled');
ok(!buildOptions({ siteSettings: { allow_html_tables: false } }).features.table, 'tables disabled');
ok(buildOptions({ siteSettings: { enable_emoji: true } }).features.emoji, 'emoji enabled');
ok(!buildOptions({ siteSettings: { enable_emoji: false } }).features.emoji, 'emoji disabled');
});
test("basic cooking", function() {
cooked("hello", "<p>hello</p>", "surrounds text with paragraphs");
cooked("**evil**", "<p><strong>evil</strong></p>", "it bolds text.");
@ -35,18 +55,11 @@ test("Nested bold and italics", function() {
});
test("Traditional Line Breaks", function() {
var input = "1\n2\n3";
const input = "1\n2\n3";
cooked(input, "<p>1<br/>2<br/>3</p>", "automatically handles trivial newlines");
var traditionalOutput = "<p>1\n2\n3</p>";
cookedOptions(input,
{traditional_markdown_linebreaks: true},
traditionalOutput,
"It supports traditional markdown via an option");
Discourse.SiteSettings.traditional_markdown_linebreaks = true;
cooked(input, traditionalOutput, "It supports traditional markdown via a Site Setting");
const result = new PrettyText({ traditionalMarkdownLinebreaks: true }).cook(input);
equal(result, "<p>1\n2\n3</p>");
});
test("Unbalanced underscores", function() {
@ -72,7 +85,6 @@ test("Paragraphs for HTML", function() {
cooked("<p>hello world</p>", "<p>hello world</p>", "it doesn't surround <p> with paragraphs");
cooked("<i>hello world</i>", "<p><i>hello world</i></p>", "it surrounds inline <i> html tags with paragraphs");
cooked("<b>hello world</b>", "<p><b>hello world</b></p>", "it surrounds inline <b> html tags with paragraphs");
});
test("Links", function() {
@ -145,7 +157,7 @@ test("Links", function() {
cooked("[http://google.com ... wat](http://discourse.org)",
"<p><a href=\"http://discourse.org\">http://google.com ... wat</a></p>",
"it supports linkins within links");
"it supports links within links");
cooked("[Link](http://www.example.com) (with an outer \"description\")",
"<p><a href=\"http://www.example.com\">Link</a> (with an outer \"description\")</p>",
@ -202,7 +214,7 @@ test("Quotes", function() {
test("Mentions", function() {
var alwaysTrue = { mentionLookup: (function() { return "user"; }) };
const alwaysTrue = { mentionLookup: (function() { return "user"; }) };
cookedOptions("Hello @sam", alwaysTrue,
"<p>Hello <a class=\"mention\" href=\"/users/sam\">@sam</a></p>",
@ -290,7 +302,7 @@ test("Mentions", function() {
});
test("Category hashtags", () => {
var alwaysTrue = { categoryHashtagLookup: (function() { return ["http://test.discourse.org/category-hashtag", "category-hashtag"]; }) };
const alwaysTrue = { categoryHashtagLookup: (function() { return ["http://test.discourse.org/category-hashtag", "category-hashtag"]; }) };
cookedOptions("Check out #category-hashtag", alwaysTrue,
"<p>Check out <a class=\"hashtag\" href=\"http://test.discourse.org/category-hashtag\">#<span>category-hashtag</span></a></p>",
@ -360,8 +372,8 @@ test("New Lines", function() {
test("Oneboxing", function() {
var matches = function(input, regexp) {
return Discourse.Markdown.cook(input).match(regexp);
function matches(input, regexp) {
return new PrettyText(defaultOpts).cook(input).match(regexp);
};
ok(!matches("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org", /onebox/),
@ -472,49 +484,6 @@ test("Code Blocks", function() {
"it handles headings with code blocks after them.");
});
test("sanitize", function() {
var sanitize = Discourse.Markdown.sanitize;
equal(sanitize("<i class=\"fa-bug fa-spin\">bug</i>"), "<i>bug</i>");
equal(sanitize("<div><script>alert('hi');</script></div>"), "<div></div>");
equal(sanitize("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p>hello</p></div>");
equal(sanitize("<3 <3"), "&lt;3 &lt;3");
equal(sanitize("<_<"), "&lt;_&lt;");
cooked("hello<script>alert(42)</script>", "<p>hello</p>", "it sanitizes while cooking");
cooked("<a href='http://disneyland.disney.go.com/'>disney</a> <a href='http://reddit.com'>reddit</a>",
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
"we can embed proper links");
cooked("<center>hello</center>", "<p>hello</p>", "it does not allow centering");
cooked("<table><tr><td>hello</td></tr></table>\nafter", "<p>after</p>", "it does not allow tables");
cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize");
cooked("<iframe src=\"http://discourse.org\" width=\"100\" height=\"42\"></iframe>", "", "it does not allow most iframe");
cooked("<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"it allows iframe to google maps");
cooked("<iframe width=\"425\" height=\"350\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" src=\"http://www.openstreetmap.org/export/embed.html?bbox=22.49454975128174%2C51.220338322410775%2C22.523088455200195%2C51.23345342732931&amp;layer=mapnik\"></iframe>",
"<iframe width=\"425\" height=\"350\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" src=\"http://www.openstreetmap.org/export/embed.html?bbox=22.49454975128174%2C51.220338322410775%2C22.523088455200195%2C51.23345342732931&amp;layer=mapnik\"></iframe>",
"it allows iframe to OpenStreetMap");
equal(sanitize("<textarea>hullo</textarea>"), "hullo");
equal(sanitize("<button>press me!</button>"), "press me!");
equal(sanitize("<canvas>draw me!</canvas>"), "draw me!");
equal(sanitize("<progress>hello"), "hello");
equal(sanitize("<mark>highlight</mark>"), "highlight");
cooked("[the answer](javascript:alert(42))", "<p><a>the answer</a></p>", "it prevents XSS");
cooked("<i class=\"fa fa-bug fa-spin\" style=\"font-size:600%\"></i>\n<!-- -->", "<p><i></i><br/></p>", "it doesn't circumvent XSS with comments");
cooked("<span class=\"-bbcode-s fa fa-spin\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"fa fa-spin -bbcode-s\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"bbcode-s\">a</span>", "<p><span class=\"bbcode-s\">a</span></p>", "it sanitizes spans");
});
test("URLs in BBCode tags", function() {
cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]",
@ -531,23 +500,6 @@ test("URLs in BBCode tags", function() {
});
test("urlAllowed", function() {
var urlAllowed = Discourse.Markdown.urlAllowed;
var allowed = function(url, msg) {
equal(urlAllowed(url), url, msg);
};
allowed("/foo/bar.html", "allows relative urls");
allowed("http://eviltrout.com/evil/trout", "allows full urls");
allowed("https://eviltrout.com/evil/trout", "allows https urls");
allowed("//eviltrout.com/evil/trout", "allows protocol relative urls");
equal(urlAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"),
"http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf",
"escape single quotes");
});
test("images", function() {
cooked("[![folksy logo](http://folksy.com/images/folksy-colour.png)](http://folksy.com/)",
"<p><a href=\"http://folksy.com/\"><img src=\"http://folksy.com/images/folksy-colour.png\" alt=\"folksy logo\"/></a></p>",
@ -559,7 +511,6 @@ test("images", function() {
});
test("censoring", function() {
Discourse.SiteSettings.censored_words = "shucks|whiz|whizzer";
cooked("aw shucks, golly gee whiz.",
"<p>aw &#9632;&#9632;&#9632;&#9632;&#9632;&#9632;, golly gee &#9632;&#9632;&#9632;&#9632;.</p>",
"it censors words in the Site Settings");
@ -583,3 +534,165 @@ test("code blocks/spans hoisting", function() {
"<p><code>$&amp;</code></p>",
"it works even when hoisting special replacement patterns");
});
test('basic bbcode', function() {
cookedPara("[b]strong[/b]", "<span class=\"bbcode-b\">strong</span>", "bolds text");
cookedPara("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text");
cookedPara("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text");
cookedPara("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text");
cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\">", "links images");
cookedPara("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title");
cookedPara("[b]evil [i]trout[/i][/b]",
"<span class=\"bbcode-b\">evil <span class=\"bbcode-i\">trout</span></span>",
"allows embedding of tags");
cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports upper case bbcode");
cookedPara("[b]strong [b]stronger[/b][/b]", "<span class=\"bbcode-b\">strong <span class=\"bbcode-b\">stronger</span></span>", "accepts nested bbcode tags");
});
test('urls', function() {
cookedPara("[url]not a url[/url]", "not a url", "supports [url] that isn't a url");
cookedPara("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without parameter");
cookedPara("[url=http://example.com]example[/url]", "<a href=\"http://example.com\">example</a>", "supports [url] with given href");
cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]",
"<a href=\"http://www.example.com\"><img src=\"http://example.com/logo.png\"></a>",
"supports [url] with an embedded [img]");
});
test('invalid bbcode', function() {
const result = new PrettyText({ lookupAvatar: false }).cook("[code]I am not closed\n\nThis text exists.");
equal(result, "<p>[code]I am not closed</p>\n\n<p>This text exists.</p>", "does not raise an error with an open bbcode tag.");
});
test('code', function() {
cookedPara("[code]\nx++\n[/code]", "<pre><code class=\"lang-auto\">x++</code></pre>", "makes code into pre");
cookedPara("[code]\nx++\ny++\nz++\n[/code]", "<pre><code class=\"lang-auto\">x++\ny++\nz++</code></pre>", "makes code into pre");
cookedPara("[code]abc\n#def\n[/code]", '<pre><code class=\"lang-auto\">abc\n#def</code></pre>', 'it handles headings in a [code] block');
cookedPara("[code]\n s[/code]",
"<pre><code class=\"lang-auto\"> s</code></pre>",
"it doesn't trim leading whitespace");
});
test('lists', function() {
cookedPara("[ul][li]option one[/li][/ul]", "<ul><li>option one</li></ul>", "creates an ul");
cookedPara("[ol][li]option one[/li][/ol]", "<ol><li>option one</li></ol>", "creates an ol");
cookedPara("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "<ul><li>option one</li><li>option two</li></ul>", "suppresses empty lines in lists");
});
test('tags with arguments', function() {
cookedPara("[url=http://bettercallsaul.com]better call![/url]", "<a href=\"http://bettercallsaul.com\">better call!</a>", "supports [url] with a title");
cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">evil trout</a>", "supports [email] with a title");
cookedPara("[u][i]abc[/i][/u]", "<span class=\"bbcode-u\"><span class=\"bbcode-i\">abc</span></span>", "can nest tags");
cookedPara("[b]first[/b] [b]second[/b]", "<span class=\"bbcode-b\">first</span> <span class=\"bbcode-b\">second</span>", "can bold two things on the same line");
});
test("quotes", function() {
const post = Post.create({
cooked: "<p><b>lorem</b> ipsum</p>",
username: "eviltrout",
post_number: 1,
topic_id: 2
});
function formatQuote(val, expected, text) {
equal(Quote.build(post, val), expected, text);
};
formatQuote(undefined, "", "empty string for undefined content");
formatQuote(null, "", "empty string for null content");
formatQuote("", "", "empty string for empty string content");
formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
formatQuote(" lorem \t ",
"[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
"trims white spaces before & after the quoted contents");
formatQuote("lorem ipsum",
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n",
"marks quotes as full when the quote is the full message");
formatQuote("**lorem** ipsum",
"[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n",
"keeps BBCode formatting");
formatQuote("this is <not> a bug",
"[quote=\"eviltrout, post:1, topic:2\"]\nthis is &lt;not&gt; a bug\n[/quote]\n\n",
"it escapes the contents of the quote");
cookedPara("[quote]test[/quote]",
"<aside class=\"quote\"><blockquote><p>test</p></blockquote></aside>",
"it supports quotes without params");
cookedPara("[quote]\n*test*\n[/quote]",
"<aside class=\"quote\"><blockquote><p><em>test</em></p></blockquote></aside>",
"it doesn't insert a new line for italics");
cookedPara("[quote=,script='a'><script>alert('test');//':a][/quote]",
"<aside class=\"quote\"><blockquote></blockquote></aside>",
"It will not create a script tag within an attribute");
});
test("quote formatting", function() {
cooked("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]",
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
"<div class=\"quote-controls\"></div>EvilTrout:</div><blockquote><p>[sam]</p></blockquote></aside>",
"it allows quotes with [] inside");
cooked("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout:" +
"</div><blockquote><p>abc</p></blockquote></aside>",
"renders quotes properly");
cooked("[quote=\"eviltrout, post:1, topic:1\"]abc[/quote]\nhello",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout:" +
"</div><blockquote><p>abc</p></blockquote></aside>\n\n<p>hello</p>",
"handles new lines properly");
cooked("[quote=\"Alice, post:1, topic:1\"]\n[quote=\"Bob, post:2, topic:1\"]\n[/quote]\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:" +
"</div><blockquote><aside class=\"quote\" data-post=\"2\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Bob:" +
"</div><blockquote></blockquote></aside></blockquote></aside>",
"quotes can be nested");
cooked("[quote=\"Alice, post:1, topic:1\"]\n[quote=\"Bob, post:2, topic:1\"]\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:" +
"</div><blockquote><p>[quote=\"Bob, post:2, topic:1\"]</p></blockquote></aside>",
"handles mismatched nested quote tags");
cooked("[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo =&#x27;foo&#x27;;\nvar bar = &#x27;bar&#x27;;</code></pre></p></blockquote></aside>",
"quotes can have code blocks without leading newline");
cooked("[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]",
"<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo =&#x27;foo&#x27;;\nvar bar = &#x27;bar&#x27;;</code></pre></p></blockquote></aside>",
"quotes can have code blocks with leading newline");
});
test("quotes with trailing formatting", function() {
const result = new PrettyText(defaultOpts).cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]\nhello\n[/quote]\n*Test*");
equal(result,
"<aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">" +
"<div class=\"quote-controls\"></div>EvilTrout:</div><blockquote><p>hello</p></blockquote></aside>\n\n<p><em>Test</em></p>",
"it allows trailing formatting");
});
test("enable/disable features", () => {
const table = `<table><tr><th>hello</th></tr><tr><td>world</td></tr></table>`;
const hasTable = new PrettyText({ features: {table: true}, sanitize: true}).cook(table);
equal(hasTable, `<table class="md-table"><tr><th>hello</th></tr><tr><td>world</td></tr></table>`);
const noTable = new PrettyText({ features: { table: false }, sanitize: true}).cook(table);
equal(noTable, `<p></p>`, 'tables are stripped when disabled');
});
test("emoji", () => {
cooked(":smile:", `<p><img src="/images/emoji/emoji_one/smile.png?v=${v}" title=":smile:" class="emoji" alt=":smile:"></p>`);
cooked(":(", `<p><img src="/images/emoji/emoji_one/frowning.png?v=${v}" title=":frowning:" class="emoji" alt=":frowning:"></p>`);
cooked("8-)", `<p><img src="/images/emoji/emoji_one/sunglasses.png?v=${v}" title=":sunglasses:" class="emoji" alt=":sunglasses:"></p>`);
});
test("emoji - emojiSet", () => {
cookedOptions(":smile:",
{ emojiSet: 'twitter' },
`<p><img src="/images/emoji/twitter/smile.png?v=${v}" title=":smile:" class="emoji" alt=":smile:"></p>`);
});

View File

@ -0,0 +1,62 @@
import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text';
import { hrefAllowed } from 'pretty-text/sanitizer';
module("lib:sanitizer");
test("sanitize", function() {
const pt = new PrettyText(buildOptions({ siteSettings: {} }));
const cooked = (input, expected, text) => equal(pt.cook(input), expected.replace(/\/>/g, ">"), text);
equal(pt.sanitize("<i class=\"fa-bug fa-spin\">bug</i>"), "<i>bug</i>");
equal(pt.sanitize("<div><script>alert('hi');</script></div>"), "<div></div>");
equal(pt.sanitize("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p>hello</p></div>");
equal(pt.sanitize("<3 <3"), "&lt;3 &lt;3");
equal(pt.sanitize("<_<"), "&lt;_&lt;");
cooked("hello<script>alert(42)</script>", "<p>hello</p>", "it sanitizes while cooking");
cooked("<a href='http://disneyland.disney.go.com/'>disney</a> <a href='http://reddit.com'>reddit</a>",
"<p><a href=\"http://disneyland.disney.go.com/\">disney</a> <a href=\"http://reddit.com\">reddit</a></p>",
"we can embed proper links");
cooked("<center>hello</center>", "<p>hello</p>", "it does not allow centering");
cooked("<table><tr><td>hello</td></tr></table>\nafter", "<p>after</p>", "it does not allow tables");
cooked("<blockquote>a\n</blockquote>\n", "<blockquote>a\n\n<br/>\n\n</blockquote>", "it does not double sanitize");
cooked("<iframe src=\"http://discourse.org\" width=\"100\" height=\"42\"></iframe>", "", "it does not allow most iframes");
cooked("<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"<iframe src=\"https://www.google.com/maps/embed?pb=!1m10!1m8!1m3!1d2624.9983685732213!2d2.29432085!3d48.85824149999999!3m2!1i1024!2i768!4f13.1!5e0!3m2!1sen!2s!4v1385737436368\" width=\"100\" height=\"42\"></iframe>",
"it allows iframe to google maps");
cooked("<iframe width=\"425\" height=\"350\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" src=\"http://www.openstreetmap.org/export/embed.html?bbox=22.49454975128174%2C51.220338322410775%2C22.523088455200195%2C51.23345342732931&amp;layer=mapnik\"></iframe>",
"<iframe width=\"425\" height=\"350\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" src=\"http://www.openstreetmap.org/export/embed.html?bbox=22.49454975128174%2C51.220338322410775%2C22.523088455200195%2C51.23345342732931&amp;layer=mapnik\"></iframe>",
"it allows iframe to OpenStreetMap");
equal(pt.sanitize("<textarea>hullo</textarea>"), "hullo");
equal(pt.sanitize("<button>press me!</button>"), "press me!");
equal(pt.sanitize("<canvas>draw me!</canvas>"), "draw me!");
equal(pt.sanitize("<progress>hello"), "hello");
equal(pt.sanitize("<mark>highlight</mark>"), "highlight");
cooked("[the answer](javascript:alert(42))", "<p><a>the answer</a></p>", "it prevents XSS");
cooked("<i class=\"fa fa-bug fa-spin\" style=\"font-size:600%\"></i>\n<!-- -->", "<p><i></i><br/></p>", "it doesn't circumvent XSS with comments");
cooked("<span class=\"-bbcode-s fa fa-spin\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"fa fa-spin -bbcode-s\">a</span>", "<p><span>a</span></p>", "it sanitizes spans");
cooked("<span class=\"bbcode-s\">a</span>", "<p><span class=\"bbcode-s\">a</span></p>", "it sanitizes spans");
});
test("urlAllowed", function() {
const allowed = (url, msg) => equal(hrefAllowed(url), url, msg);
allowed("/foo/bar.html", "allows relative urls");
allowed("http://eviltrout.com/evil/trout", "allows full urls");
allowed("https://eviltrout.com/evil/trout", "allows https urls");
allowed("//eviltrout.com/evil/trout", "allows protocol relative urls");
equal(hrefAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"),
"http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf",
"escape single quotes");
});

View File

@ -1,16 +1,27 @@
/* global Int8Array:true */
import { blank } from 'helpers/qunit-helpers';
import {
emailValid,
isAnImage,
avatarUrl,
allowsAttachments,
getRawSize,
avatarImg,
defaultHomepage,
validateUploadedFiles,
getUploadMarkdown,
caretRowCol,
setCaretPosition
} from 'discourse/lib/utilities';
module("Discourse.Utilities");
var utils = Discourse.Utilities;
module("lib:utilities");
test("emailValid", function() {
ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails");
ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
ok(emailValid('Bob@example.com'), "allows upper case in the first part of emails");
ok(emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
});
var validUpload = utils.validateUploadedFiles;
var validUpload = validateUploadedFiles;
test("validateUploadedFiles", function() {
not(validUpload(null), "no files are invalid");
@ -80,8 +91,8 @@ test("allows valid uploads to go through", function() {
not(bootbox.alert.calledOnce);
});
var getUploadMarkdown = function(filename) {
return utils.getUploadMarkdown({
var testUploadMarkdown = function(filename) {
return getUploadMarkdown({
original_filename: filename,
filesize: 42,
width: 100,
@ -91,26 +102,26 @@ var getUploadMarkdown = function(filename) {
};
test("getUploadMarkdown", function() {
ok(getUploadMarkdown("lolcat.gif") === '<img src="/uploads/123/abcdef.ext" width="100" height="200">');
ok(getUploadMarkdown("important.txt") === '<a class="attachment" href="/uploads/123/abcdef.ext">important.txt</a> (42 Bytes)\n');
ok(testUploadMarkdown("lolcat.gif") === '<img src="/uploads/123/abcdef.ext" width="100" height="200">');
ok(testUploadMarkdown("important.txt") === '<a class="attachment" href="/uploads/123/abcdef.ext">important.txt</a> (42 Bytes)\n');
});
test("isAnImage", function() {
_.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff", "ico"], function(extension) {
var image = "image." + extension;
ok(utils.isAnImage(image), image + " is recognized as an image");
ok(utils.isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image");
ok(isAnImage(image), image + " is recognized as an image");
ok(isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image");
});
not(utils.isAnImage("file.txt"));
not(utils.isAnImage("http://foo.bar/path/to/file.txt"));
not(utils.isAnImage(""));
not(isAnImage("file.txt"));
not(isAnImage("http://foo.bar/path/to/file.txt"));
not(isAnImage(""));
});
test("avatarUrl", function() {
var rawSize = utils.getRawSize;
blank(utils.avatarUrl('', 'tiny'), "no template returns blank");
equal(utils.avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url");
equal(utils.avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size");
var rawSize = getRawSize;
blank(avatarUrl('', 'tiny'), "no template returns blank");
equal(avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url");
equal(avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size");
});
var setDevicePixelRatio = function(value) {
@ -126,19 +137,19 @@ test("avatarImg", function() {
setDevicePixelRatio(2);
var avatarTemplate = "/path/to/avatar/{size}.png";
equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}),
equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}),
"<img alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar'>",
"it returns the avatar html");
equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}),
equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}),
"<img alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar' title='evilest trout'>",
"it adds a title if supplied");
equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}),
equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}),
"<img alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar evil fish'>",
"it adds extra classes if supplied");
blank(utils.avatarImg({avatarTemplate: "", size: 'tiny'}),
blank(avatarImg({avatarTemplate: "", size: 'tiny'}),
"it doesn't render avatars for invalid avatar template");
setDevicePixelRatio(oldRatio);
@ -146,18 +157,18 @@ test("avatarImg", function() {
test("allowsAttachments", function() {
Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif";
not(utils.allowsAttachments(), "no attachments allowed by default");
not(allowsAttachments(), "no attachments allowed by default");
Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*";
ok(utils.allowsAttachments(), "attachments are allowed when all extensions are allowed");
ok(allowsAttachments(), "attachments are allowed when all extensions are allowed");
Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf";
ok(utils.allowsAttachments(), "attachments are allowed when at least one extension is not an image extension");
ok(allowsAttachments(), "attachments are allowed when at least one extension is not an image extension");
});
test("defaultHomepage", function() {
Discourse.SiteSettings.top_menu = "latest|top|hot";
equal(utils.defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting");
equal(defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting");
});
test("caretRowCol", () => {
@ -167,9 +178,9 @@ test("caretRowCol", () => {
document.body.appendChild(textarea);
const assertResult = (setCaretPos, expectedRowNum, expectedColNum) => {
Discourse.Utilities.setCaretPosition(textarea, setCaretPos);
setCaretPosition(textarea, setCaretPos);
const result = Discourse.Utilities.caretRowCol(textarea);
const result = caretRowCol(textarea);
equal(result.rowNum, expectedRowNum, "returns the right row of the caret");
equal(result.colNum, expectedColNum, "returns the right col of the caret");
};

View File

@ -1,8 +1,9 @@
module("MDTest", {
setup: function() {
Discourse.SiteSettings.traditional_markdown_linebreaks = false;
}
});
import { sanitize } from 'pretty-text/sanitizer';
import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text';
import { hashString } from 'discourse/lib/hash';
// Run the MDTest spec
module("MDTest");
// This is cheating, but the trivial differences between sanitization
// do not affect formatting.
@ -15,44 +16,42 @@ function normalize(str) {
// We use a custom sanitizer for MD test that hoists out comments. In Discourse
// they are stripped, but to be compliant with the spec they should not be.
function hoistingSanitizer(result) {
var hoisted,
m = result.match(/<!--[\s\S]*?-->/g);
function sanitizer(result, whiteLister) {
let hoisted;
const m = result.match(/<!--[\s\S]*?-->/g);
if (m && m.length) {
hoisted = [];
for (var i=0; i<m.length; i++) {
var c = m[i],
id = md5("discourse:hoisted-comment:" + i);
for (let i=0; i<m.length; i++) {
const c = m[i];
const id = hashString("discourse:hoisted-comment:" + i).toString();
result = result.replace(c, id);
hoisted.push([c, id]);
}
}
result = Discourse.Markdown.sanitize(result);
result = sanitize(result, whiteLister);
if (hoisted) {
hoisted.forEach(function(tuple) {
result = result.replace(tuple[1], tuple[0]);
});
hoisted.forEach(tuple => result = result.replace(tuple[1], tuple[0]));
}
return result;
}
var md = function(input, expected, text) {
var result = Discourse.Markdown.cook(input, {
sanitizerFunction: hoistingSanitizer,
traditional_markdown_linebreaks: true
}),
resultNorm = normalize(result),
expectedNorm = normalize(expected),
same = (result === expected) || (resultNorm === expectedNorm);
function md(input, expected, text) {
const opts = buildOptions({ siteSettings: {} });
opts.traditionalMarkdownLinebreaks = true;
opts.sanitizer = sanitizer;
const cooker = new PrettyText(opts);
const result = cooker.cook(input);
const resultNorm = normalize(result);
const expectedNorm = normalize(expected);
const same = (result === expected) || (resultNorm === expectedNorm);
if (same) {
ok(same, text);
} else {
console.log(resultNorm);
console.log(expectedNorm);
equal(resultNorm, expectedNorm, text);
}
};

View File

@ -1,4 +1,6 @@
import { blank, present } from 'helpers/qunit-helpers';
import { IMAGE_VERSION as v} from 'pretty-text/emoji';
module("model:topic");
import Topic from 'discourse/models/topic';
@ -75,7 +77,6 @@ test("recover", function() {
test('fancyTitle', function() {
var topic = Topic.create({ fancy_title: ":smile: with all :) the emojis :pear::peach:" });
const v = Discourse.Emoji.ImageVersion;
equal(topic.get('fancyTitle'),
`<img src='/images/emoji/emoji_one/smile.png?v=${v}' title='smile' alt='smile' class='emoji'> with all <img src='/images/emoji/emoji_one/slight_smile.png?v=${v}' title='slight_smile' alt='slight_smile' class='emoji'> the emojis <img src='/images/emoji/emoji_one/pear.png?v=${v}' title='pear' alt='pear' class='emoji'><img src='/images/emoji/emoji_one/peach.png?v=${v}' title='peach' alt='peach' class='emoji'>`,

View File

@ -22,9 +22,9 @@
//= require htmlparser.js
// Stuff we need to load first
//= require pretty-text-bundle
//= require main_include
//= require admin
//= require_tree ../../app/assets/javascripts/defer
//= require sinon-1.7.1
//= require sinon-qunit-1.0.0
@ -43,12 +43,6 @@
window.inTestEnv = true;
window.assetPath = function(url) {
if (url.indexOf('defer') === 0) {
return "/assets/" + url;
}
};
// Stop the message bus so we don't get ajax calls
window.MessageBus.stop();
@ -137,3 +131,5 @@ Object.keys(requirejs.entries).forEach(function(entry) {
require(entry, null, null, true);
}
});
require('mdtest/mdtest', null, null, true);