module("Discourse.Markdown", { setup: function() { Discourse.SiteSettings.traditional_markdown_linebreaks = false; Discourse.SiteSettings.default_code_lang = "auto"; } }); 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); }; var cookedOptions = function(input, opts, expected, text) { equal(Discourse.Markdown.cook(input, opts), expected, text); }; test("basic cooking", function() { cooked("hello", "

hello

", "surrounds text with paragraphs"); cooked("**evil**", "

evil

", "it bolds text."); cooked("__bold__", "

bold

", "it bolds text."); cooked("*trout*", "

trout

", "it italicizes text."); cooked("_trout_", "

trout

", "it italicizes text."); cooked("***hello***", "

hello

", "it can do bold and italics at once."); cooked("word_with_underscores", "

word_with_underscores

", "it doesn't do intraword italics"); cooked("common/_special_font_face.html.erb", "

common/_special_font_face.html.erb

", "it doesn't intraword with a slash"); cooked("hello \\*evil\\*", "

hello *evil*

", "it supports escaping of asterisks"); cooked("hello \\_evil\\_", "

hello _evil_

", "it supports escaping of italics"); cooked("brussels sprouts are *awful*.", "

brussels sprouts are awful.

", "it doesn't swallow periods."); }); test("Nested bold and italics", function() { cooked("*this is italic **with some bold** inside*", "

this is italic with some bold inside

", "it handles nested bold in italics"); }); test("Traditional Line Breaks", function() { var input = "1\n2\n3"; cooked(input, "

1
2
3

", "automatically handles trivial newlines"); var traditionalOutput = "

1\n2\n3

"; 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"); }); test("Unbalanced underscores", function() { cooked("[evil_trout][1] hello_\n\n[1]: http://eviltrout.com", "

evil_trout hello_

"); }); test("Line Breaks", function() { cooked("[] first choice\n[] second choice", "

[] first choice
[] second choice

", "it handles new lines correctly with [] options"); cooked("
evil
\ntrout", "
evil
\n\n

trout

", "it doesn't insert
after blockquotes"); cooked("leading
evil
\ntrout", "leading
evil
\n\n

trout

", "it doesn't insert
after blockquotes with leading text"); }); test("Paragraphs for HTML", function() { cooked("
hello world
", "
hello world
", "it doesn't surround
with paragraphs"); cooked("

hello world

", "

hello world

", "it doesn't surround

with paragraphs"); cooked("hello world", "

hello world

", "it surrounds inline html tags with paragraphs"); cooked("hello world", "

hello world

", "it surrounds inline html tags with paragraphs"); }); test("Links", function() { cooked("EvilTrout: http://eviltrout.com", '

EvilTrout: http://eviltrout.com

', "autolinks a URL"); cooked("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A", '

Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A

', "allows links to contain query params"); cooked("Derpy: http://derp.com?__test=1", '

Derpy: http://derp.com?__test=1

', "works with double underscores in urls"); cooked("Derpy: http://derp.com?_test_=1", '

Derpy: http://derp.com?_test_=1

', "works with underscores in urls"); cooked("Atwood: www.codinghorror.com", '

Atwood: www.codinghorror.com

', "autolinks something that begins with www"); cooked("Atwood: http://www.codinghorror.com", '

Atwood: http://www.codinghorror.com

', "autolinks a URL with http://www"); cooked("EvilTrout: http://eviltrout.com hello", '

EvilTrout: http://eviltrout.com hello

', "autolinks with trailing text"); cooked("here is [an example](http://twitter.com)", '

here is an example

', "supports markdown style links"); cooked("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)", '

Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)

', "autolinks a URL with parentheses (like Wikipedia)"); cooked("Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200", "

Here's a tweet:
https://twitter.com/evil_trout/status/345954894420787200

", "It doesn't strip the new line."); cooked("1. View @eviltrout's profile here: http://meta.discourse.org/users/eviltrout/activity
next line.", "
  1. View @eviltrout's profile here: http://meta.discourse.org/users/eviltrout/activity
    next line.
", "allows autolinking within a list without inserting a paragraph."); cooked("[3]: http://eviltrout.com", "", "It doesn't autolink markdown link references"); cooked("[]: http://eviltrout.com", "

[]: http://eviltrout.com

", "It doesn't accept empty link references"); cooked("[b]label[/b]: description", "

label: description

", "It doesn't accept BBCode as link references"); cooked("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369", "

http://discourse.org and " + "http://discourse.org/another_url and " + "http://www.imdb.com/name/nm2225369

", 'allows multiple links on one line'); cooked("* [Evil Trout][1]\n [1]: http://eviltrout.com", "", "allows markdown link references in a list"); cooked("User [MOD]: Hello!", "

User [MOD]: Hello!

", "It does not consider references that are obviously not URLs"); cooked("http://eviltrout.com", "

http://eviltrout.com

", "Links within HTML tags"); cooked("[http://google.com ... wat](http://discourse.org)", "

http://google.com ... wat

", "it supports linkins within links"); cooked("[Link](http://www.example.com) (with an outer \"description\")", "

Link (with an outer \"description\")

", "it doesn't consume closing parens as part of the url"); cooked("[ul][1]\n\n[1]: http://eviltrout.com", "

ul

", "it can use `ul` as a link name"); }); test("simple quotes", function() { cooked("> nice!", "

nice!

", "it supports simple quotes"); cooked(" > nice!", "

nice!

", "it allows quotes with preceding spaces"); cooked("> level 1\n> > level 2", "

level 1

level 2

", "it allows nesting of blockquotes"); cooked("> level 1\n> > level 2", "

level 1

level 2

", "it allows nesting of blockquotes with spaces"); cooked("- hello\n\n > world\n > eviltrout", "
  • hello
\n\n

world
eviltrout

", "it allows quotes within a list."); cooked("-

eviltrout

", "
  • eviltrout

", "it allows paragraphs within a list."); cooked(" > indent 1\n > indent 2", "

indent 1
indent 2

", "allow multiple spaces to indent"); }); test("Quotes", function() { cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n\nthird line[/quote]", { topicId: 2 }, "", "works with multiple lines"); cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", { topicId: 2, lookupAvatar: function(name) { return "" + name; }, sanitize: true }, "

1

\n\n\n\n

2

", "handles quotes properly"); cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2", { topicId: 2, lookupAvatar: function() { } }, "

1

\n\n\n\n

2

", "includes no avatar if none is found"); }); test("Mentions", function() { var alwaysTrue = { mentionLookup: (function() { return true; }) }; cookedOptions("Hello @sam", alwaysTrue, "

Hello @sam

", "translates mentions to links"); cooked("[@codinghorror](https://twitter.com/codinghorror)", "

@codinghorror

", "it doesn't do mentions within links"); cookedOptions("[@codinghorror](https://twitter.com/codinghorror)", alwaysTrue, "

@codinghorror

", "it doesn't do link mentions within links"); cooked("Hello @EvilTrout", "

Hello @EvilTrout

", "adds a mention class"); cooked("robin@email.host", "

robin@email.host

", "won't add mention class to an email address"); cooked("hanzo55@yahoo.com", "

hanzo55@yahoo.com

", "won't be affected by email addresses that have a number before the @ symbol"); cooked("@EvilTrout yo", "

@EvilTrout yo

", "it handles mentions at the beginning of a string"); cooked("yo\n@EvilTrout", "

yo
@EvilTrout

", "it handles mentions at the beginning of a new line"); cooked("`evil` @EvilTrout `trout`", "

evil @EvilTrout trout

", "deals correctly with multiple blocks"); cooked("```\na @test\n```", "

a @test

", "should not do mentions within a code block."); cooked("> foo bar baz @eviltrout", "

foo bar baz @eviltrout

", "handles mentions in simple quotes"); cooked("> foo bar baz @eviltrout ohmagerd\nlook at this", "

foo bar baz @eviltrout ohmagerd
look at this

", "does mentions properly with trailing text within a simple quote"); cooked("`code` is okay before @mention", "

code is okay before @mention

", "Does not mention in an inline code block"); cooked("@mention is okay before `code`", "

@mention is okay before code

", "Does not mention in an inline code block"); cooked("don't `@mention`", "

don't @mention

", "Does not mention in an inline code block"); cooked("Yes `@this` should be code @eviltrout", "

Yes @this should be code @eviltrout

", "Does not mention in an inline code block"); cooked("@eviltrout and `@eviltrout`", "

@eviltrout and @eviltrout

", "you can have a mention in an inline code block following a real mention."); cooked("1. this is a list\n\n2. this is an @eviltrout mention\n", "
  1. this is a list

  2. this is an @eviltrout mention

", "it mentions properly in a list."); cookedOptions("@eviltrout", alwaysTrue, "

@eviltrout

", "it doesn't onebox mentions"); cookedOptions("a @sam c", alwaysTrue, "

a @sam c

", "it allows mentions within HTML tags"); }); test("Heading", function() { cooked("**Bold**\n----------", "

Bold

", "It will bold the heading"); }); test("bold and italics", function() { cooked("a \"**hello**\"", "

a \"hello\"

", "bolds in quotes"); cooked("(**hello**)", "

(hello)

", "bolds in parens"); cooked("**hello**\nworld", "

hello
world

", "allows newline after bold"); cooked("**hello**\n**world**", "

hello
world

", "newline between two bolds"); cooked("**a*_b**", "

a*_b

", "allows for characters within bold"); cooked("** hello**", "

** hello**

", "does not bold on a space boundary"); cooked("**hello **", "

**hello **

", "does not bold on a space boundary"); cooked("你**hello**", "

你**hello**

", "does not bold chinese intra word"); cooked("**你hello**", "

你hello

", "allows bolded chinese"); }); test("Escaping", function() { cooked("*\\*laughs\\**", "

*laughs*

", "allows escaping strong"); cooked("*\\_laughs\\_*", "

_laughs_

", "allows escaping em"); }); test("New Lines", function() { // Note: This behavior was discussed and we determined it does not make sense to do this // unless you're using traditional line breaks cooked("_abc\ndef_", "

_abc
def_

", "it does not allow markup to span new lines"); cooked("_abc\n\ndef_", "

_abc

\n\n

def_

", "it does not allow markup to span new paragraphs"); }); test("Oneboxing", function() { var matches = function(input, regexp) { return Discourse.Markdown.cook(input).match(regexp); }; ok(!matches("- http://www.textfiles.com/bbs/MINDVOX/FORUMS/ethics\n\n- http://drupal.org", /onebox/), "doesn't onebox a link within a list"); ok(matches("http://test.com", /onebox/), "adds a onebox class to a link on its own line"); ok(matches("http://test.com\nhttp://test2.com", /onebox[\s\S]+onebox/m), "supports multiple links"); ok(!matches("http://test.com bob", /onebox/), "doesn't onebox links that have trailing text"); ok(!matches("[Tom Cruise](http://www.tomcruise.com/)", "onebox"), "Markdown links with labels are not oneboxed"); ok(matches("[http://www.tomcruise.com/](http://www.tomcruise.com/)", "onebox"), "Markdown links where the label is the same as the url are oneboxed"); cooked("http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street", "

http://en.wikipedia.org/wiki/Homicide:_Life_on_the_Street

", "works with links that have underscores in them"); }); test("links with full urls", function() { cooked("[http://eviltrout.com][1] is a url\n\n[1]: http://eviltrout.com", "

http://eviltrout.com is a url

", "it supports links that are full URLs"); }); test("Code Blocks", function() { cooked("
\nhello\n
\n", "

hello

", "pre blocks don't include extra lines"); cooked("```\na\nb\nc\n\nd\n```", "

a\nb\nc\n\nd

", "it treats new lines properly"); cooked("```\ntest\n```", "

test

", "it supports basic code blocks"); cooked("```json\n{hello: 'world'}\n```\ntrailing", "

{hello: 'world'}

\n\n

trailing

", "It does not truncate text after a code block."); cooked("```json\nline 1\n\nline 2\n\n\nline 3\n```", "

line 1\n\nline 2\n\n\nline 3

", "it maintains new lines inside a code block."); cooked("hello\nworld\n```json\nline 1\n\nline 2\n\n\nline 3\n```", "

hello
world

\n\n

line 1\n\nline 2\n\n\nline 3

", "it maintains new lines inside a code block with leading content."); cooked("```ruby\n
hello
\n```", "

<header>hello</header>

", "it escapes code in the code block"); cooked("```text\ntext\n```", "

text

", "handles text by adding nohighlight"); cooked("```ruby\n# cool\n```", "

# cool

", "it supports changing the language"); cooked(" ```\n hello\n ```", "
```\nhello\n```
", "only detect ``` at the beginning of lines"); cooked("```ruby\ndef self.parse(text)\n\n text\nend\n```", "

def self.parse(text)\n\n  text\nend

", "it allows leading spaces on lines in a code block."); cooked("```ruby\nhello `eviltrout`\n```", "

hello `eviltrout`

", "it allows code with backticks in it"); cooked("```eviltrout\nhello\n```", "

hello

", "it doesn't not whitelist all classes"); cooked("```\n[quote=\"sam, post:1, topic:9441, full:true\"]This is `` a bug.[/quote]\n```", "

[quote="sam, post:1, topic:9441, full:true"]This is `<not>` a bug.[/quote]

", "it allows code with backticks in it"); cooked(" hello\n
test
", "
hello
\n\n
test
", "it allows an indented code block to by followed by a `
`"); cooked("``` foo bar ```", "

foo bar

", "it tolerates misuse of code block tags as inline code"); cooked("```\nline1\n```\n```\nline2\n\nline3\n```", "

line1

\n\n

line2\n\nline3

", "it does not consume next block's trailing newlines"); cooked("
test
", "
<pre>test</pre>
", "it does not parse other block types in markdown code blocks"); cooked(" [quote]test[/quote]", "
[quote]test[/quote]
", "it does not parse other block types in markdown code blocks"); cooked("## a\nb\n```\nc\n```", "

a

\n\n

c

", "it handles headings with code blocks after them."); }); test("sanitize", function() { var sanitize = Discourse.Markdown.sanitize; equal(sanitize("bug"), "bug"); equal(sanitize("
"), "
"); equal(sanitize("

hello

"), "

hello

"); equal(sanitize("<3 <3"), "<3 <3"); equal(sanitize("<_<"), "<_<"); cooked("hello", "

hello

", "it sanitizes while cooking"); cooked("disney reddit", "

disney reddit

", "we can embed proper links"); cooked("
hello
", "

hello

", "it does not allow centering"); cooked("
hello
\nafter", "

after

", "it does not allow tables"); cooked("
a\n
\n", "
a\n\n
\n\n
", "it does not double sanitize"); cooked("", "", "it does not allow most iframe"); cooked("", "", "it allows iframe to google maps"); cooked("", "", "it allows iframe to OpenStreetMap"); equal(sanitize(""), "hullo"); equal(sanitize(""), "press me!"); equal(sanitize("draw me!"), "draw me!"); equal(sanitize("hello"), "hello"); equal(sanitize("highlight"), "highlight"); cooked("[the answer](javascript:alert(42))", "

the answer

", "it prevents XSS"); cooked("\n", "


", "it doesn't circumvent XSS with comments"); cooked("a", "

a

", "it sanitizes spans"); cooked("a", "

a

", "it sanitizes spans"); cooked("a", "

a

", "it sanitizes spans"); }); test("URLs in BBCode tags", function() { cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]", "

", "images are properly parsed"); cooked("[url]http://discourse.org[/url]", "

http://discourse.org

", "links are properly parsed"); cooked("[url=http://discourse.org]discourse[/url]", "

discourse

", "named links are properly parsed"); }); 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/)", "

\"folksy

", "It allows images with links around them"); cooked("\"Red", "

\"Red

", "It allows data images"); }); test("censoring", function() { Discourse.SiteSettings.censored_words = "shucks|whiz|whizzer"; cooked("aw shucks, golly gee whiz.", "

aw ■■■■■■, golly gee ■■■■.

", "it censors words in the Site Settings"); cooked("you are a whizzard! I love cheesewhiz. Whiz.", "

you are a whizzard! I love cheesewhiz. ■■■■.

", "it doesn't censor words unless they have boundaries."); cooked("you are a whizzer! I love cheesewhiz. Whiz.", "

you are a ■■■■■■■! I love cheesewhiz. ■■■■.

", "it censors words even if previous partial matches exist."); });