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', censored_pattern: '\\d{3}-\\d{4}|tech\\w*' }, getURL: url => url }); function cooked(input, expected, text) { equal(new PrettyText(defaultOpts).cook(input), expected.replace(/\/>/g, ">"), text); }; function cookedOptions(input, opts, expected, text) { equal(new PrettyText(_.merge({}, defaultOpts, opts)).cook(input), expected, text); }; function cookedPara(input, expected, text) { cooked(input, `

${expected}

`, 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", "

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() { const input = "1\n2\n3"; cooked(input, "

1
2
3

", "automatically handles trivial newlines"); const result = new PrettyText({ traditionalMarkdownLinebreaks: true }).cook(input); equal(result, "

1\n2\n3

"); }); 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 links within links"); cooked("[http://google.com](http://discourse.org)", "

http://google.com

", "it supports markdown links where the name and link match"); 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("A link inside parentheses (http://www.example.com)", "

A link inside parentheses (http://www.example.com)

", "it auto-links a url within parentheses"); 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() { const alwaysTrue = { mentionLookup: (function() { return "user"; }) }; 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."); cooked("Hello @foo/@bar", "

Hello @foo/@bar

", "handles mentions separated by a slash."); cookedOptions("@eviltrout", alwaysTrue, "

@eviltrout

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

a @sam c

", "it allows mentions within HTML tags"); }); test("Category hashtags", () => { const alwaysTrue = { categoryHashtagLookup: (function() { return ["http://test.discourse.org/category-hashtag", "category-hashtag"]; }) }; cookedOptions("Check out #category-hashtag", alwaysTrue, "

Check out #category-hashtag

", "it translates category hashtag into links"); cooked("Check out #category-hashtag", "

Check out #category-hashtag

", "it does not translate category hashtag into links if it is not a valid category hashtag"); cookedOptions("[#category-hashtag](http://www.test.com)", alwaysTrue, "

#category-hashtag

", "it does not translate category hashtag within links"); cooked("```\n# #category-hashtag\n```", "

# #category-hashtag

", "it does not translate category hashtags to links in code blocks"); cooked("># #category-hashtag\n", "

#category-hashtag

", "it handles category hashtags in simple quotes"); cooked("# #category-hashtag", "

#category-hashtag

", "it works within ATX-style headers"); cooked("don't `#category-hashtag`", "

don't #category-hashtag

", "it does not mention in an inline code block"); cooked("test #hashtag1/#hashtag2", "

test #hashtag1/#hashtag2

", "it does not convert category hashtag not bounded by spaces"); cooked("#category-hashtag", "

#category-hashtag

", "it works between 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() { 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/), "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\nline3\n```", "

line 1\n\nline 2\n\n\nline3

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

hello
world

\n\n

line 1\n\nline 2\n\n\nline3

", "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("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("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() { 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."); cooked("The link still works. [whiz](http://www.whiz.com)", "

The link still works. ■■■■

", "it won't break links by censoring them."); cooked("Call techapj the computer whiz at 555-555-1234 for free help.", "

Call ■■■■■■■ the computer ■■■■ at 555-■■■■■■■■ for free help.

", "uses both censored words and patterns from site settings"); }); test("code blocks/spans hoisting", function() { cooked("```\n\n some code\n```", "

    some code

", "it works when nesting standard markdown code blocks within a fenced code block"); cooked("`$&`", "

$&

", "it works even when hoisting special replacement patterns"); }); test('basic bbcode', function() { cookedPara("[b]strong[/b]", "strong", "bolds text"); cookedPara("[i]emphasis[/i]", "emphasis", "italics text"); cookedPara("[u]underlined[/u]", "underlined", "underlines text"); cookedPara("[s]strikethrough[/s]", "strikethrough", "strikes-through text"); cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "", "links images"); cookedPara("[email]eviltrout@mailinator.com[/email]", "eviltrout@mailinator.com", "supports [email] without a title"); cookedPara("[b]evil [i]trout[/i][/b]", "evil trout", "allows embedding of tags"); cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "eviltrout@mailinator.com", "supports upper case bbcode"); cookedPara("[b]strong [b]stronger[/b][/b]", "strong stronger", "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]abc.com[/url]", "abc.com", "no error when a url has no protocol and begins with a"); cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter"); cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href"); cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]", "", "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, "

[code]I am not closed

\n\n

This text exists.

", "does not raise an error with an open bbcode tag."); }); test('code', function() { cookedPara("[code]\nx++\n[/code]", "
x++
", "makes code into pre"); cookedPara("[code]\nx++\ny++\nz++\n[/code]", "
x++\ny++\nz++
", "makes code into pre"); cookedPara("[code]abc\n#def\n[/code]", '
abc\n#def
', 'it handles headings in a [code] block'); cookedPara("[code]\n s[/code]", "
   s
", "it doesn't trim leading whitespace"); }); test('lists', function() { cookedPara("[ul][li]option one[/li][/ul]", "
  • option one
", "creates an ul"); cookedPara("[ol][li]option one[/li][/ol]", "
  1. option one
", "creates an ol"); cookedPara("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "
  • option one
  • option two
", "suppresses empty lines in lists"); }); test('tags with arguments', function() { cookedPara("[url=http://bettercallsaul.com]better call![/url]", "better call!", "supports [url] with a title"); cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "evil trout", "supports [email] with a title"); cookedPara("[u][i]abc[/i][/u]", "abc", "can nest tags"); cookedPara("[b]first[/b] [b]second[/b]", "first second", "can bold two things on the same line"); }); test("quotes", function() { const post = Post.create({ cooked: "

lorem ipsum

", 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 a bug", "[quote=\"eviltrout, post:1, topic:2\"]\nthis is <not> a bug\n[/quote]\n\n", "it escapes the contents of the quote"); cookedPara("[quote]test[/quote]", "", "it supports quotes without params"); cookedPara("[quote]\n*test*\n[/quote]", "", "it doesn't insert a new line for italics"); cookedPara("[quote=,script='a'>