From 23d67d21009964c636cd69c06fb99da8a666af08 Mon Sep 17 00:00:00 2001
From: Jakub Macina ℵ£¢ ddd @hello @hello @hello @hello @hello-hello hi @sam! hi hi @a,@b @hello @hello-hello ℵ£¢ X X aa *a a
+
+ a
+
+ X
+ tags on line
alone
Run all of pretty text spec on new engine
---
app/assets/javascripts/markdown-it-bundle.js | 1 +
.../engines/markdown-it/html_img.js.es6 | 74 ++
spec/components/pretty_text_spec.rb | 806 ++++++++++--------
3 files changed, 515 insertions(+), 366 deletions(-)
create mode 100644 app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6
diff --git a/app/assets/javascripts/markdown-it-bundle.js b/app/assets/javascripts/markdown-it-bundle.js
index 51fe107f81..3460c3a602 100644
--- a/app/assets/javascripts/markdown-it-bundle.js
+++ b/app/assets/javascripts/markdown-it-bundle.js
@@ -12,3 +12,4 @@
//= require ./pretty-text/engines/markdown-it/table
//= require ./pretty-text/engines/markdown-it/paragraph
//= require ./pretty-text/engines/markdown-it/newline
+//= require ./pretty-text/engines/markdown-it/html_img
diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6 b/app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6
new file mode 100644
index 0000000000..8d5b02efa7
--- /dev/null
+++ b/app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6
@@ -0,0 +1,74 @@
+// special handling for IMG tags on a line by themeselves
+// we always have to handle it as so it is an inline
+// see: https://talk.commonmark.org/t/newline-and-img-tags/2511
+
+const REGEX = /^
-HTML
- expect(PrettyText.cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]ddd\n[/quote]", topic_id: 1)).to match_html expected
- end
- end
+ describe "Quoting" do
describe "with avatar" do
let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" }
@@ -38,19 +34,91 @@ HTML
User.stubs(:default_template).returns(default_avatar)
end
- it "produces a quote even with new lines in it" do
- expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html ""
+ it "do off topic quoting with emoji unescape" do
+
+ topic = Fabricate(:topic, title: "this is a test topic :slight_smile:")
+ expected = <<~HTML
+
+ HTML
+
+ expect(cook("[quote=\"EvilTrout, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected))
end
- it "should produce a quote" do
- expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html ""
+ it "produces a quote even with new lines in it" do
+ md = <<~MD
+ [quote="#{user.username}, post:123, topic:456, full:true"]
+
+ ddd
+
+ [/quote]
+ MD
+ html = <<~HTML
+
+ HTML
+
+ expect(PrettyText.cook(md)).to eq(html.strip)
end
+
it "trims spaces on quote params" do
- expect(PrettyText.cook("[quote=\"#{user.username}, post:555, topic: 666\"]ddd[/quote]")).to match_html ""
+ md = <<~MD
+ [quote="#{user.username}, post:555, topic: 666"]
+ ddd
+ [/quote]
+ MD
+
+ html = <<~HTML
+
+ HTML
+
+ expect(PrettyText.cook(md)).to eq(html.strip)
end
end
+ it "can handle quote edge cases" do
+ expect(PrettyText.cook("a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside')
+ expect(PrettyText.cook("- a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside')
+ expect(PrettyText.cook("[quote]\ntest")).not_to include('aside')
+ expect(PrettyText.cook("[quote]abc\ntest\n[/quote]")).not_to include('aside')
+ expect(PrettyText.cook("[quote]\ntest\n[/quote]z")).not_to include('aside')
+
+ nested = <<~QUOTE
+ [quote]
+ a
+ [quote]
+ b
+ [/quote]
+ c
+ [/quote]
+ QUOTE
+
+ cooked = PrettyText.cook(nested)
+ expect(cooked.scan('aside').length).to eq(4)
+ expect(cooked.scan('quote]').length).to eq(0)
+ end
+
describe "with letter avatar" do
let(:user) { Fabricate(:user) }
@@ -61,22 +129,44 @@ HTML
end
it "should have correct avatar url" do
- expect(PrettyText.cook("[quote=\"#{user.username}, post:123, topic:456, full:true\"]ddd[/quote]")).to include("/forum/letter_avatar_proxy")
+ md = <<~MD
+ [quote="#{user.username}, post:123, topic:456, full:true"]
+ ddd
+ [/quote]
+ MD
+ expect(PrettyText.cook(md)).to include("/forum/letter_avatar_proxy")
end
end
end
+ end
+
+ describe "Mentions" do
it "should handle 3 mentions in a row" do
expect(PrettyText.cook('@hello @hello @hello')).to match_html "
\n@sam
+ HTML
+
+ expect(cooked).to eq(html.strip)
end
+ it "doesn't replace emoji in code blocks with our emoji sets if emoji is enabled" do
+ expect(PrettyText.cook("```\n💣`\n```\n")).not_to match(/\:bomb\:/)
+ end
+
+ it 'can include code class correctly' do
+ expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html(" #
+ x
+
")
+ expect(PrettyText.cook("```\ncpp\n```")).to match_html("cpp\n
")
+ expect(PrettyText.cook("```text\ncpp\n```")).to match_html("cpp\n
")
+ end
+
+ it 'indents code correctly' do
+ code = "X\n```\n\n #\n x\n```"
+ cooked = PrettyText.cook(code)
+ expect(cooked).to match_html("cpp\n
")
+ end
+
+ it 'does censor code fences' do
+ SiteSetting.censored_words = 'apple|banana'
+ expect(PrettyText.cook("# banana")).not_to include('banana')
+ end
end
+
describe "rel nofollow" do
before do
SiteSetting.add_rel_nofollow_to_user_content = true
@@ -330,7 +459,6 @@ HTML
)).to eq("boom")
end
end
-
end
describe "strip links" do
@@ -402,20 +530,55 @@ HTML
end
end
- it 'can escape *' do
- expect(PrettyText.cook("***a***a")).to match_html("\n #\n x\n
+
+
+
+
+
+
+ a
+
+
+ - li
+
+
+ ```
+ test
+ ```
+
+ ```
+ test
+ ```
+ MD
+
+ html = <<~HTML
+
+
+
+
+
+ test
+
+ HTML
+
+ expect(PrettyText.cook(raw)).to eq(html.strip)
end
- it 'can include code class correctly' do
- expect(PrettyText.cook("```cpp\ncpp\n```")).to match_html("test
+
")
- end
-
- it 'indents code correctly' do
- code = "X\n```\n\n #\n x\n```"
- cooked = PrettyText.cook(code)
- expect(cooked).to match_html("cpp
")
- end
it 'can substitute s3 cdn correctly' do
SiteSetting.enable_s3_uploads = true
@@ -425,19 +588,22 @@ HTML
SiteSetting.s3_cdn_url = "https://awesome.cdn"
# add extra img tag to ensure it does not blow up
- raw = <
- #\n x
-
-
+ raw = <<~HTML
+
+
+
+
+ HTML
-HTML
+ html = <<~HTML
+
+ 
+ 
+ 



#unknown::tag #known
- HTML - - expect(cooked).to match_html(html) - end - - # TODO does it make sense to generate hashtags for tags that are missing in action? - end - describe "custom emoji" do it "replaces the custom emoji" do CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload)) @@ -504,317 +650,245 @@ HTML end end - context "markdown it" do - - before do - SiteSetting.enable_experimental_markdown_it = true - end - - it "replaces skin toned emoji" do - expect(PrettyText.cook("hello 👱🏿♀️")).to eq("hello 
hello 
hello 
hello 
hello 
hello 
hello 
hello 
#unknown::tag #known
- HTML + html = <<~HTML +#unknown::tag #known
+ HTML - expect(cooked).to eq(html.strip) + expect(cooked).to eq(html.strip) - cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") + cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") - html = <<~HTML - - HTML + html = <<~HTML + + HTML - expect(cooked).to eq(html.strip) + expect(cooked).to eq(html.strip) - cooked = PrettyText.cook("`a` #known::tag here") + cooked = PrettyText.cook("`a` #known::tag here") - expect(cooked).to eq(html.strip) + expect(cooked).to eq(html.strip) - cooked = PrettyText.cook("test #known::tag") - html = <<~HTML - - HTML + cooked = PrettyText.cook("test #known::tag") + html = <<~HTML + + HTML - expect(cooked).to eq(html.strip) + expect(cooked).to eq(html.strip) - # ensure it does not fight with the autolinker - expect(PrettyText.cook(' http://somewhere.com/#known')).not_to include('hashtag') - expect(PrettyText.cook(' http://somewhere.com/?#known')).not_to include('hashtag') - expect(PrettyText.cook(' http://somewhere.com/?abc#known')).not_to include('hashtag') - - end - - it "can handle mixed lists" do - # known bug in old md engine - cooked = PrettyText.cook("* a\n\n1. b") - expect(cooked).to match_html("1 2
" - - SiteSetting.traditional_markdown_linebreaks = false - expect(PrettyText.cook("1\n2")).to match_html "1
\n2
hi @sam! hi
' - expect(PrettyText.cook("hi\n@sam")).to eq("hi
\n@sam
@a,@b
') - end - - it "can handle emoji by name" do - - expected = <
-HTML
- expect(PrettyText.cook(":smile::sunny:")).to eq(expected.strip)
- end
-
- it "handles emoji boundaries correctly" do
- cooked = PrettyText.cook("a,:man:t2:,b")
- expected = 'a,
,b

cpp\n")
- expect(PrettyText.cook("```\ncpp\n```")).to match_html("cpp\n")
- expect(PrettyText.cook("```text\ncpp\n```")).to match_html("cpp\n")
- end
-
- it 'indents code correctly' do
- code = "X\n```\n\n #\n x\n```"
- cooked = PrettyText.cook(code)
- expect(cooked).to match_html("X
\n\n #\n x\n")
- end
-
- it 'can censor words correctly' do
- SiteSetting.censored_words = 'apple|banana'
- expect(PrettyText.cook('yay banana yay')).not_to include('banana')
- expect(PrettyText.cook('yay `banana` yay')).not_to include('banana')
- expect(PrettyText.cook("yay \n\n```\nbanana\n````\n yay")).not_to include('banana')
- expect(PrettyText.cook("# banana")).not_to include('banana')
- expect(PrettyText.cook("# banana")).to include("\u25a0\u25a0")
- end
-
- it 'supports typographer' do
- SiteSetting.enable_markdown_typographer = true
- expect(PrettyText.cook('(tm)')).to eq('™
') - - SiteSetting.enable_markdown_typographer = false - expect(PrettyText.cook('(tm)')).to eq('(tm)
') - end - - it 'handles onebox correctly' do - expect(PrettyText.cook("http://a.com\nhttp://b.com").split("onebox").length).to eq(3) - expect(PrettyText.cook("http://a.com\n\nhttp://b.com").split("onebox").length).to eq(3) - expect(PrettyText.cook("a\nhttp://a.com")).to include('onebox') - expect(PrettyText.cook("> http://a.com")).not_to include('onebox') - expect(PrettyText.cook("a\nhttp://a.com a")).not_to include('onebox') - expect(PrettyText.cook("a\nhttp://a.com\na")).to include('onebox') - expect(PrettyText.cook("http://a.com")).to include('onebox') - expect(PrettyText.cook("http://a.com ")).to include('onebox') - expect(PrettyText.cook("http://a.com a")).not_to include('onebox') - expect(PrettyText.cook("- http://a.com")).not_to include('onebox') - expect(PrettyText.cook("abc
') - expect(PrettyText.cook("a[i]b[/i]c")).to eq('abc
') - end - - it "can handle quote edge cases" do - expect(PrettyText.cook("a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') - expect(PrettyText.cook("- a\n[quote]\ntest\n[/quote]\n\n\na")).to include('aside') - expect(PrettyText.cook("[quote]\ntest")).not_to include('aside') - expect(PrettyText.cook("[quote]abc\ntest\n[/quote]")).not_to include('aside') - expect(PrettyText.cook("[quote]\ntest\n[/quote]z")).not_to include('aside') - - nested = <<~QUOTE - [quote] - a - [quote] - b - [/quote] - c - [/quote] - QUOTE - - cooked = PrettyText.cook(nested) - expect(cooked.scan('aside').length).to eq(4) - expect(cooked.scan('quote]').length).to eq(0) - end - - it "can onebox local topics" do - op = Fabricate(:post) - reply = Fabricate(:post, topic_id: op.topic_id) - - - url = Discourse.base_url + reply.url - quote = create_post(topic_id: op.topic.id, raw: "This is a sample reply with a quote\n\n#{url}") - quote.reload - - expect(quote.cooked).not_to include('[quote') - end - - it "supports tables" do - - markdown = <<~MD - | Tables | Are | Cool | - | ------------- |:-------------:| -----:| - | col 3 is | right-aligned | $1600 | - MD - - expected = <<~HTML -| Tables | -Are | -Cool | -
|---|---|---|
| col 3 is | -right-aligned | -$1600 | -

Testing codified **stuff** and `more` stuff
codified\n\n\n **stuff** and `more` stuff"
- expect(cooked).to eq(html)
- end
-
- it "support special handling for space in urls" do
- cooked = PrettyText.cook "http://testing.com?a%20b"
- html = ''
- expect(cooked).to eq(html)
- end
-
- it "supports onebox for decoded urls" do
- cooked = PrettyText.cook "http://testing.com?a%50b"
- html = ''
- expect(cooked).to eq(html)
- end
+ # ensure it does not fight with the autolinker
+ expect(PrettyText.cook(' http://somewhere.com/#known')).not_to include('hashtag')
+ expect(PrettyText.cook(' http://somewhere.com/?#known')).not_to include('hashtag')
+ expect(PrettyText.cook(' http://somewhere.com/?abc#known')).not_to include('hashtag')
end
+ it "can handle mixed lists" do
+ # known bug in old md engine
+ cooked = PrettyText.cook("* a\n\n1. b")
+ expect(cooked).to match_html("1 2
" + + SiteSetting.traditional_markdown_linebreaks = false + expect(PrettyText.cook("1\n2")).to match_html "1
\n2

+HTML
+ expect(PrettyText.cook(":smile::sunny:")).to eq(expected.strip)
+ end
+
+ it "handles emoji boundaries correctly" do
+ cooked = PrettyText.cook("a,:man:t2:,b")
+ expected = 'a,
,b

™
') + + SiteSetting.enable_markdown_typographer = false + expect(PrettyText.cook('(tm)')).to eq('(tm)
') + end + + it 'handles onebox correctly' do + expect(PrettyText.cook("http://a.com\nhttp://b.com").split("onebox").length).to eq(3) + expect(PrettyText.cook("http://a.com\n\nhttp://b.com").split("onebox").length).to eq(3) + expect(PrettyText.cook("a\nhttp://a.com")).to include('onebox') + expect(PrettyText.cook("> http://a.com")).not_to include('onebox') + expect(PrettyText.cook("a\nhttp://a.com a")).not_to include('onebox') + expect(PrettyText.cook("a\nhttp://a.com\na")).to include('onebox') + expect(PrettyText.cook("http://a.com")).to include('onebox') + expect(PrettyText.cook("http://a.com ")).to include('onebox') + expect(PrettyText.cook("http://a.com a")).not_to include('onebox') + expect(PrettyText.cook("- http://a.com")).not_to include('onebox') + expect(PrettyText.cook("abc
') + expect(PrettyText.cook("a[i]b[/i]c")).to eq('abc
') + end + + + it "can onebox local topics" do + op = Fabricate(:post) + reply = Fabricate(:post, topic_id: op.topic_id) + + + url = Discourse.base_url + reply.url + quote = create_post(topic_id: op.topic.id, raw: "This is a sample reply with a quote\n\n#{url}") + quote.reload + + expect(quote.cooked).not_to include('[quote') + end + + it "supports tables" do + + markdown = <<~MD + | Tables | Are | Cool | + | ------------- |:-------------:| -----:| + | col 3 is | right-aligned | $1600 | + MD + + expected = <<~HTML +| Tables | +Are | +Cool | +
|---|---|---|
| col 3 is | +right-aligned | +$1600 | +

Testing codified **stuff** and `more` stuff
codified\n\n\n **stuff** and `more` stuff"
+ expect(cooked).to eq(html)
+ end
+
+ it "support special handling for space in urls" do
+ cooked = PrettyText.cook "http://testing.com?a%20b"
+ html = ''
+ expect(cooked).to eq(html)
+ end
+
+ it "supports onebox for decoded urls" do
+ cooked = PrettyText.cook "http://testing.com?a%50b"
+ html = ''
+ expect(cooked).to eq(html)
+ end
+
+
+ it "should sanitize the html" do
+ expect(PrettyText.cook("")).to eq ""
+ end
+
+
end
From d29a0eeedf23dfd2dd67dcb78aedbc3834078c78 Mon Sep 17 00:00:00 2001
From: Sam 
+ 
+ 
+ 
+ 

HTML
- puts cooked
expect(cooked).to eq(html.strip)
end
From 98e03b04b50624110572558c49acaa520f28d7ed Mon Sep 17 00:00:00 2001
From: Sam ${escapedContent}\n`;
}
-registerOption((siteSettings, opts) => {
- opts.features.code = true;
- opts.defaultCodeLang = siteSettings.default_code_lang;
- opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);
-});
-
export function setup(helper) {
+ if (!helper.markdownIt) { return; }
- if (helper.markdownIt) { return; }
+ helper.registerOptions((opts, siteSettings) => {
+ opts.defaultCodeLang = siteSettings.default_code_lang;
+ opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);
+ });
helper.whiteList({
custom(tag, name, value) {
@@ -34,50 +45,7 @@ export function setup(helper) {
}
});
- helper.replaceBlock({
- start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
- stop: /^```$/gm,
- withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match
- emitter(blockContents, matches) {
- const opts = helper.getOptions();
-
- let codeLang = opts.defaultCodeLang;
- const acceptableCodeClasses = opts.acceptableCodeClasses;
- if (acceptableCodeClasses && matches[1] && acceptableCodeClasses.indexOf(matches[1]) !== -1) {
- codeLang = matches[1];
- }
-
- if (TEXT_CODE_CLASSES.indexOf(matches[1]) !== -1) {
- return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]];
- } else {
- return ['p', ['pre', ['code', {'class': 'lang-' + codeLang}, codeFlattenBlocks(blockContents) ]]];
- }
- }
- });
-
- helper.replaceBlock({
- start: /(]*\>)([\s\S]*)/igm,
- stop: /<\/pre>/igm,
- rawContents: true,
- skipIfTradtionalLinebreaks: true,
-
- emitter(blockContents) {
- return ['p', ['pre', codeFlattenBlocks(blockContents)]];
- }
- });
-
- // Ensure that content in a code block is fully escaped. This way it's not white listed
- // and we can use HTML and Javascript examples.
- helper.onParseNode(function(event) {
- const node = event.node,
- path = event.path;
-
- if (node[0] === 'code') {
- const regexp = (path && path[path.length-1] && path[path.length-1][0] && path[path.length-1][0] === "pre") ?
- / +$/g : /^ +| +$/g;
-
- const contents = node[node.length-1];
- node[node.length-1] = escape(contents.replace(regexp,''));
- }
+ helper.registerPlugin(md=>{
+ md.renderer.rules.fence = (tokens,idx,options,env,slf)=>render(tokens,idx,options,env,slf,md);
});
}
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6
index 60864a2263..0e4eed203b 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6
@@ -1,117 +1,246 @@
-import { registerOption } from 'pretty-text/pretty-text';
import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji';
import { translations } from 'pretty-text/emoji/data';
-let _unicodeReplacements;
-let _unicodeRegexp;
-export function setUnicodeReplacements(replacements) {
- _unicodeReplacements = replacements;
- if (replacements) {
- // We sort and reverse to match longer emoji sequences first
- _unicodeRegexp = new RegExp(Object.keys(replacements).sort().reverse().join("|"), "g");
- }
-};
+const MAX_NAME_LENGTH = 60;
-function escapeRegExp(s) {
- return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
+let translationTree = null;
+
+// This allows us to efficiently search for aliases
+// We build a data structure that allows us to quickly
+// search through our N next chars to see if any match
+// one of our alias emojis.
+//
+function buildTranslationTree() {
+ let tree = [];
+ let lastNode;
+
+ Object.keys(translations).forEach(function(key){
+ let i;
+ let node = tree;
+
+ for(i=0;i 0) {
+ let prev = content.charCodeAt(pos-1);
+ if (!state.md.utils.isSpace(prev) && !state.md.utils.isPunctChar(String.fromCharCode(prev))) {
+ return;
}
}
- return true;
+
+ pos++;
+ if (content.charCodeAt(pos) === 58) {
+ return;
+ }
+
+ let length = 0;
+ while(length < MAX_NAME_LENGTH) {
+ length++;
+
+ if (content.charCodeAt(pos+length) === 58) {
+ // check for t2-t6
+ if (content.substr(pos+length+1, 3).match(/t[2-6]:/)) {
+ length += 3;
+ }
+ break;
+ }
+
+ if (pos+length > content.length) {
+ return;
+ }
+ }
+
+ if (length === MAX_NAME_LENGTH) {
+ return;
+ }
+
+ return content.substr(pos, length);
}
-registerOption((siteSettings, opts, state) => {
- opts.features.emoji = !!siteSettings.enable_emoji;
- opts.emojiSet = siteSettings.emoji_set || "";
- opts.customEmoji = state.customEmoji;
-});
+// straight forward :smile: to emoji image
+function getEmojiTokenByName(name, state) {
+
+ let info;
+ if (info = imageFor(name, state.md.options.discourse)) {
+ let token = new state.Token('emoji', 'img', 0);
+ token.attrs = [['src', info.url],
+ ['title', info.title],
+ ['class', info.classes],
+ ['alt', info.title]];
+
+ return token;
+ }
+}
+
+function getEmojiTokenByTranslation(content, pos, state) {
+
+ translationTree = translationTree || buildTranslationTree();
+
+ let currentTree = translationTree;
+
+ let i;
+ let search = true;
+ let found = false;
+ let start = pos;
+
+ while(search) {
+
+ search = false;
+ let code = content.charCodeAt(pos);
+
+ for (i=0;i 0) {
+ let leading = content.charAt(start-1);
+ if (!state.md.utils.isSpace(leading.charCodeAt(0)) && !state.md.utils.isPunctChar(leading)) {
+ return;
+ }
+ }
+
+ // check trailing for punct or space
+ if (pos < content.length) {
+ let trailing = content.charCodeAt(pos);
+ if (!state.md.utils.isSpace(trailing)){
+ return;
+ }
+ }
+
+ let token = getEmojiTokenByName(found, state);
+ if (token) {
+ return { pos, token };
+ }
+}
+
+function applyEmoji(content, state, emojiUnicodeReplacer) {
+ let i;
+ let result = null;
+ let contentToken = null;
+
+ let start = 0;
+
+ if (emojiUnicodeReplacer) {
+ content = emojiUnicodeReplacer(content);
+ }
+
+ let endToken = content.length;
+
+ for (i=0; i0) {
+ contentToken = new state.Token('text', '', 0);
+ contentToken.content = content.slice(start,i);
+ result.push(contentToken);
+ }
+
+ result.push(token);
+ endToken = start = i + offset;
+ }
+ }
+
+ if (endToken < content.length) {
+ contentToken = new state.Token('text', '', 0);
+ contentToken.content = content.slice(endToken);
+ result.push(contentToken);
+ }
+
+ return result;
+}
export function setup(helper) {
- if (helper.markdownIt) { return; }
+ if (!helper.markdownIt) { return; }
- helper.whiteList('img.emoji');
-
- function imageFor(code) {
- code = code.toLowerCase();
- const opts = helper.getOptions();
- const url = buildEmojiUrl(code, opts);
- if (url) {
- const title = `:${code}:`;
- const classes = isCustomEmoji(code, opts) ? "emoji emoji-custom" : "emoji";
- return ['img', { href: url, title, 'class': classes, alt: title }];
- }
- }
-
- const translationsWithColon = {};
- Object.keys(translations).forEach(t => {
- if (t[0] === ':') {
- translationsWithColon[t] = translations[t];
- } else {
- const replacement = translations[t];
- helper.inlineReplace(t, (token, match, prev) => {
- return checkPrev(prev) ? imageFor(replacement) : token;
- });
- }
- });
- const translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(t => `(${escapeRegExp(t)})`).join("|"));
-
- helper.registerInline(':', (text, match, prev) => {
- const endPos = text.indexOf(':', 1);
- const firstSpace = text.search(/\s/);
- if (!checkPrev(prev)) { return; }
-
- // If there is no trailing colon, check our translations that begin with colons
- if (endPos === -1 || (firstSpace !== -1 && endPos > firstSpace)) {
- translationColonRegexp.lastIndex = 0;
- const m = translationColonRegexp.exec(text);
- if (m && m[0] && text.indexOf(m[0]) === 0) {
- // Check outer edge
- const lastChar = text.charAt(m[0].length);
- if (lastChar && !/\s/.test(lastChar)) return;
- const contents = imageFor(translationsWithColon[m[0]]);
- if (contents) {
- return [m[0].length, contents];
- }
- }
- return;
- }
-
- let between;
- const emojiNameMatch = text.match(/(?:.*?)(:(?!:).?[\w-]*(?::t\d)?:)/);
- if (emojiNameMatch) {
- between = emojiNameMatch[0].slice(1, -1);
- } else {
- between = text.slice(1, -1);
- }
-
- const contents = imageFor(between);
- if (contents) {
- return [text.indexOf(between, 1) + between.length + 1, contents];
- }
+ helper.registerOptions((opts, siteSettings, state)=>{
+ opts.features.emoji = !!siteSettings.enable_emoji;
+ opts.emojiSet = siteSettings.emoji_set || "";
+ opts.customEmoji = state.customEmoji;
});
- helper.addPreProcessor(text => {
- if (_unicodeReplacements) {
- _unicodeRegexp.lastIndex = 0;
-
- let m;
- while ((m = _unicodeRegexp.exec(text)) !== null) {
- let replacement = ":" + _unicodeReplacements[m[0]] + ":";
- const before = text.charAt(m.index-1);
- if (!/\B/.test(before)) {
- replacement = "\u200b" + replacement;
- }
- text = text.replace(m[0], replacement);
- }
- }
- return text;
+ helper.registerPlugin((md)=>{
+ md.core.ruler.push('emoji', state => md.options.discourse.helpers.textReplace(
+ state, (c,s)=>applyEmoji(c,s,md.options.discourse.emojiUnicodeReplacer))
+ );
});
}
diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6
similarity index 100%
rename from app/assets/javascripts/pretty-text/engines/markdown-it/helpers.js.es6
rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/helpers.js.es6
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6
deleted file mode 100644
index 1d8f21a205..0000000000
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6
+++ /dev/null
@@ -1,52 +0,0 @@
-const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'details',
- 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
- 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output',
- 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video', 'summary'];
-
-function splitAtLast(tag, block, next, first) {
- const endTag = `${tag}>`;
- let endTagIndex = first ? block.indexOf(endTag) : block.lastIndexOf(endTag);
-
- if (endTagIndex !== -1) {
- endTagIndex += endTag.length;
-
- const trailing = block.substr(endTagIndex).replace(/^\s+/, '');
- if (trailing.length) {
- next.unshift(trailing);
- }
-
- return [ block.substr(0, endTagIndex) ];
- }
-};
-
-export function setup(helper) {
-
- if (helper.markdownIt) { return; }
-
- // If a row begins with HTML tags, don't parse it.
- helper.registerBlock('html', function(block, next) {
- let split, pos;
-
- // Fix manual blockquote paragraphing even though it's not strictly correct
- // PERF NOTE: /\S+= 0) {
- if(block.substring(0, pos).search(/\s/) === -1) {
- split = splitAtLast('blockquote', block, next, true);
- if (split) { return this.processInline(split[0]); }
- }
- }
-
- const m = /^\s*<\/?([^>]+)\>/.exec(block);
- if (m && m[1]) {
- const tag = m[1].split(/\s/);
- if (tag && tag[0] && BLOCK_TAGS.indexOf(tag[0]) !== -1) {
- split = splitAtLast(tag[0], block, next);
- if (split) {
- if (split.length === 1 && split[0] === block) { return; }
- return split;
- }
- return [ block.toString() ];
- }
- }
- });
-}
diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html_img.js.es6
similarity index 100%
rename from app/assets/javascripts/pretty-text/engines/markdown-it/html_img.js.es6
rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/html_img.js.es6
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6
index 84be9e5f32..602af3c15a 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6
@@ -1,51 +1,88 @@
-/**
- Supports our custom @mention syntax for calling out a user in a post.
- It will add a special class to them, and create a link if the user is found in a
- local map.
-**/
+const regex = /^(\w[\w.-]{0,59})\b/i;
+
+function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) {
+
+ let pos = state.pos;
+
+ // 64 = @
+ if (silent || state.src.charCodeAt(pos) !== 64) {
+ return false;
+ }
+
+ if (pos > 0) {
+ let prev = state.src.charCodeAt(pos-1);
+ if (!isWhiteSpace(prev) && !isPunctChar(String.fromCharCode(prev))) {
+ return false;
+ }
+ }
+
+ // skip if in a link
+ if (state.tokens) {
+ let last = state.tokens[state.tokens.length-1];
+ if (last) {
+ if (last.type === 'link_open') {
+ return false;
+ }
+ if (last.type === 'html_inline' && last.content.substr(0,2) === " {
- const node = event.node,
- path = event.path;
-
- if (node[1] && node[1]["class"] === 'mention') {
- const parent = path[path.length - 1];
-
- // If the parent is an 'a', remove it
- if (parent && parent[0] === 'a') {
- const name = node[2];
- node.length = 0;
- node[0] = "__RAW";
- node[1] = name;
- }
- }
- });
-
- helper.inlineRegexp({
- start: '@',
- // NOTE: since we can't use SiteSettings here (they loads later in process)
- // we are being less strict to account for more cases than allowed
- matcher: /^@(\w[\w.-]{0,59})\b/i,
- wordBoundary: true,
-
- emitter(matches) {
- const mention = matches[0].trim();
- const name = matches[1];
- const opts = helper.getOptions();
- const mentionLookup = opts.mentionLookup;
-
- const type = mentionLookup && mentionLookup(name);
- if (type === "user") {
- return ['a', {'class': 'mention', href: opts.getURL("/u/") + name.toLowerCase()}, mention];
- } else if (type === "group") {
- return ['a', {'class': 'mention-group', href: opts.getURL("/groups/") + name}, mention];
- } else {
- return ['span', {'class': 'mention'}, mention];
- }
- }
+ helper.registerPlugin(md => {
+ md.inline.ruler.push('mentions', (state,silent)=> applyMentions(
+ state,
+ silent,
+ md.utils.isWhiteSpace,
+ md.utils.isPunctChar,
+ md.options.discourse.mentionLookup,
+ md.options.discourse.getURL
+ ));
});
}
+
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6
index a453445a2c..f1eb2ba759 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6
@@ -1,30 +1,53 @@
-// Support for the newline behavior in markdown that most expect. Look through all text nodes
-// in the tree, replace any new lines with `br`s.
+// see: https://github.com/markdown-it/markdown-it/issues/375
+//
+// we use a custom paragraph rule cause we have to signal when a
+// link starts with a space, so we can bypass a onebox
+// this is a freedom patch, so careful, may break on updates
+
+
+function newline(state, silent) {
+ var token, pmax, max, pos = state.pos;
+
+ if (state.src.charCodeAt(pos) !== 0x0A/* \n */) { return false; }
+
+ pmax = state.pending.length - 1;
+ max = state.posMax;
+
+ // ' \n' -> hardbreak
+ // Lookup in pending chars is bad practice! Don't copy to other rules!
+ // Pending string is stored in concat mode, indexed lookups will cause
+ // convertion to flat mode.
+ if (!silent) {
+ if (pmax >= 0 && state.pending.charCodeAt(pmax) === 0x20) {
+ if (pmax >= 1 && state.pending.charCodeAt(pmax - 1) === 0x20) {
+ state.pending = state.pending.replace(/ +$/, '');
+ token = state.push('hardbreak', 'br', 0);
+ } else {
+ state.pending = state.pending.slice(0, -1);
+ token = state.push('softbreak', 'br', 0);
+ }
+
+ } else {
+ token = state.push('softbreak', 'br', 0);
+ }
+ }
+
+ pos++;
+
+ // skip heading spaces for next line
+ while (pos < max && state.md.utils.isSpace(state.src.charCodeAt(pos))) {
+ if (token) {
+ token.leading_space = true;
+ }
+ pos++;
+ }
+
+ state.pos = pos;
+ return true;
+};
export function setup(helper) {
-
- if (helper.markdownIt) { return; }
-
- helper.postProcessText((text, event) => {
- const { options, insideCounts } = event;
- if (options.traditionalMarkdownLinebreaks || (insideCounts.pre > 0)) { return; }
-
- if (text === "\n") {
- // If the tag is just a new line, replace it with a `
`
- return [['br']];
- } else {
- // If the text node contains new lines, perhaps with text between them, insert the
- // `
` tags.
- const split = text.split(/\n+/);
- if (split.length) {
- const replacement = [];
- for (var i=0; i 0) { replacement.push(split[i]); }
- if (i !== split.length-1) { replacement.push(['br']); }
- }
-
- return replacement;
- }
- }
+ helper.registerPlugin(md => {
+ md.inline.ruler.at('newline', newline);
});
}
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
index 875321911f..5f04b72b1d 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6
@@ -1,71 +1,89 @@
import { lookupCache } from 'pretty-text/oneboxer';
-// Given a node in the document and its parent, determine whether it is on its own line or not.
-function isOnOneLine(link, parent) {
- if (!parent) { return false; }
-
- const siblings = parent.slice(1);
- if ((!siblings) || (siblings.length < 1)) { return false; }
-
- const idx = siblings.indexOf(link);
- if (idx === -1) { return false; }
-
- if (idx > 0) {
- const prev = siblings[idx-1];
- if (prev[0] !== 'br') { return false; }
+function applyOnebox(state, silent) {
+ if (silent || !state.tokens || state.tokens.length < 3) {
+ return;
}
- if (idx < siblings.length) {
- const next = siblings[idx+1];
- if (next && (!((next[0] === 'br') || (typeof next === 'string' && next.trim() === "")))) { return false; }
- }
+ let i;
+ for(i=1;i {
- const node = event.node,
- path = event.path;
+ if (j === 0 && token.leading_space) {
+ continue;
+ } else if (j > 0) {
+ let prevSibling = token.children[j-1];
- // We only care about links
- if (node[0] !== 'a') { return; }
+ if (prevSibling.tag !== 'br' || prevSibling.leading_space) {
+ continue;
+ }
+ }
- const parent = path[path.length - 1];
+ // look ahead for soft or hard break
+ let text = token.children[j+1];
+ let close = token.children[j+2];
+ let lookahead = token.children[j+3];
- // We don't onebox bbcode
- if (node[1]['data-bbcode']) {
- delete node[1]['data-bbcode'];
- return;
- }
+ if (lookahead && lookahead.tag !== 'br') {
+ continue;
+ }
- // We don't onebox mentions
- if (node[1]['class'] === 'mention') { return; }
+ // check attrs only include a href
+ let attrs = child["attrs"];
- // Don't onebox links within a list
- for (var i=0; i {
+ md.core.ruler.after('linkify', 'onebox', applyOnebox);
});
}
diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6
similarity index 100%
rename from app/assets/javascripts/pretty-text/engines/markdown-it/paragraph.js.es6
rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/paragraph.js.es6
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6
deleted file mode 100644
index e8a4a9a131..0000000000
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6
+++ /dev/null
@@ -1,83 +0,0 @@
-import { register } from 'pretty-text/engines/discourse-markdown/bbcode';
-import { registerOption } from 'pretty-text/pretty-text';
-import { performEmojiUnescape } from 'pretty-text/emoji';
-
-registerOption((siteSettings, opts) => {
- opts.enableEmoji = siteSettings.enable_emoji;
- opts.emojiSet = siteSettings.emoji_set;
-});
-
-
-export function setup(helper) {
-
- if (helper.markdownIt) { return; }
-
- register(helper, 'quote', {noWrap: true, singlePara: true}, (contents, bbParams, options) => {
-
- const params = {'class': 'quote'};
- let username = null;
- const opts = helper.getOptions();
-
- if (bbParams) {
- const paramsSplit = bbParams.split(/\,\s*/);
- username = paramsSplit[0];
-
- paramsSplit.forEach(function(p,i) {
- if (i > 0) {
- var assignment = p.split(':');
- if (assignment[0] && assignment[1]) {
- const escaped = helper.escape(assignment[0]);
- // don't escape attributes, makes no sense
- if (escaped === assignment[0]) {
- params['data-' + assignment[0]] = helper.escape(assignment[1].trim());
- }
- }
- }
- });
- }
-
- let avatarImg;
- const postNumber = parseInt(params['data-post'], 10);
- const topicId = parseInt(params['data-topic'], 10);
-
- if (options.lookupAvatarByPostNumber) {
- // client-side, we can retrieve the avatar from the post
- avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId);
- } else if (options.lookupAvatar) {
- // server-side, we need to lookup the avatar from the username
- avatarImg = options.lookupAvatar(username);
- }
-
- // If there's no username just return a simple quote
- if (!username) {
- return ['p', ['aside', params, ['blockquote'].concat(contents)]];
- }
-
- const header = ['div', {'class': 'title'},
- ['div', {'class': 'quote-controls'}],
- avatarImg ? ['__RAW', avatarImg] : "",
- username ? `${username}:` : "" ];
-
- if (options.topicId && postNumber && options.getTopicInfo && topicId !== options.topicId) {
- const topicInfo = options.getTopicInfo(topicId);
- if (topicInfo) {
- var href = topicInfo.href;
- if (postNumber > 0) { href += "/" + postNumber; }
- // get rid of username said stuff
- header.pop();
-
- let title = topicInfo.title;
-
- if (opts.enableEmoji) {
- title = performEmojiUnescape(topicInfo.title, {
- getURL: opts.getURL, emojiSet: opts.emojiSet
- });
- }
-
- header.push(['a', {'href': href}, title]);
- }
- }
-
- return ['aside', params, header, ['blockquote'].concat(contents)];
- });
-}
diff --git a/app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
similarity index 100%
rename from app/assets/javascripts/pretty-text/engines/markdown-it/quotes.js.es6
rename to app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6
diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6
index 1b148e6843..4bb5ef92d6 100644
--- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6
+++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6
@@ -1,35 +1,31 @@
-import { registerOption } from 'pretty-text/pretty-text';
-
-function tableFlattenBlocks(blocks) {
- let result = "";
-
- blocks.forEach(b => {
- result += b;
- if (b.trailing) { result += b.trailing; }
- });
-
- // bypass newline insertion
- return result.replace(/[\n\r]/g, " ");
-};
-
-registerOption((siteSettings, opts) => {
- opts.features.table = !!siteSettings.allow_html_tables;
-});
-
export function setup(helper) {
- if (helper.markdownIt) { return; }
+ if (!helper.markdownIt) { return; }
- helper.whiteList(['table', 'table.md-table', 'tbody', 'thead', 'tr', 'th', 'td']);
+ // this is built in now
+ // TODO: sanitizer needs fixing, does not properly support this yet
- helper.replaceBlock({
- start: /(