FIX: medium URL with @ incorrectly handled as mention

Also:

- remove unused code
- rename bbcode_ruler to bbcode.ruler
- add md.core.textPostProcess.ruler to apply at end of chain (excluding links)
This commit is contained in:
Sam 2017-07-17 16:21:47 -04:00
parent 29d529020b
commit 8921058c67
17 changed files with 147 additions and 150 deletions

View File

@ -12,4 +12,5 @@
//= require ./pretty-text/engines/discourse-markdown/table //= require ./pretty-text/engines/discourse-markdown/table
//= require ./pretty-text/engines/discourse-markdown/paragraph //= require ./pretty-text/engines/discourse-markdown/paragraph
//= require ./pretty-text/engines/discourse-markdown/newline //= require ./pretty-text/engines/discourse-markdown/newline
//= require ./pretty-text/engines/discourse-markdown/html_img //= require ./pretty-text/engines/discourse-markdown/html-img
//= require ./pretty-text/engines/discourse-markdown/text-post-process

View File

@ -71,11 +71,16 @@ class Ruler {
// block bb code ruler for parsing of quotes / code / polls // block bb code ruler for parsing of quotes / code / polls
function setupBlockBBCode(md) { function setupBlockBBCode(md) {
md.block.bbcode_ruler = new Ruler(); md.block.bbcode = { ruler: new Ruler() };
} }
function setupInlineBBCode(md) { function setupInlineBBCode(md) {
md.inline.bbcode_ruler = new Ruler(); md.inline.bbcode = { ruler: new Ruler() };
}
function setupTextPostProcessRuler(md) {
const TextPostProcessRuler = requirejs('pretty-text/engines/discourse-markdown/text-post-process').TextPostProcessRuler;
md.core.textPostProcess = { ruler: new TextPostProcessRuler() };
} }
function renderHoisted(tokens, idx, options) { function renderHoisted(tokens, idx, options) {
@ -217,6 +222,7 @@ export function setup(opts, siteSettings, state) {
setupImageDimensions(opts.engine); setupImageDimensions(opts.engine);
setupBlockBBCode(opts.engine); setupBlockBBCode(opts.engine);
setupInlineBBCode(opts.engine); setupInlineBBCode(opts.engine);
setupTextPostProcessRuler(opts.engine);
pluginCallbacks.forEach(([feature, callback])=>{ pluginCallbacks.forEach(([feature, callback])=>{
if (opts.discourse.features[feature]) { if (opts.discourse.features[feature]) {

View File

@ -120,7 +120,7 @@ function applyBBCode(state, startLine, endLine, silent, md) {
return false; return false;
} }
let ruleInfo = md.block.bbcode_ruler.getRuleForTag(info.tag); let ruleInfo = md.block.bbcode.ruler.getRuleForTag(info.tag);
if (!ruleInfo) { return false; } if (!ruleInfo) { return false; }
rule = ruleInfo.rule; rule = ruleInfo.rule;
@ -249,11 +249,8 @@ function applyBBCode(state, startLine, endLine, silent, md) {
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => { helper.registerPlugin(md => {
const ruler = md.block.bbcode_ruler; const ruler = md.block.bbcode.ruler;
ruler.push('code', { ruler.push('code', {
tag: 'code', tag: 'code',

View File

@ -141,9 +141,6 @@ function processBBCode(state, silent) {
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']); helper.whiteList(['span.bbcode-b', 'span.bbcode-i', 'span.bbcode-u', 'span.bbcode-s']);
helper.registerOptions(opts => { helper.registerOptions(opts => {
@ -151,7 +148,7 @@ export function setup(helper) {
}); });
helper.registerPlugin(md => { helper.registerPlugin(md => {
const ruler = md.inline.bbcode_ruler; const ruler = md.inline.bbcode.ruler;
md.inline.ruler.push('bbcode-inline', (state,silent) => tokanizeBBCode(state,silent,ruler)); md.inline.ruler.push('bbcode-inline', (state,silent) => tokanizeBBCode(state,silent,ruler));
md.inline.ruler2.before('text_collapse', 'bbcode-inline', processBBCode); md.inline.ruler2.before('text_collapse', 'bbcode-inline', processBBCode);

View File

@ -1,6 +1,6 @@
function addHashtag(buffer, matches, state) { function addHashtag(buffer, match, state) {
const options = state.md.options.discourse; const options = state.md.options.discourse;
const [hashtag, slug] = matches; const slug = match.slice(1);
const categoryHashtagLookup = options.categoryHashtagLookup; const categoryHashtagLookup = options.categoryHashtagLookup;
const result = categoryHashtagLookup && categoryHashtagLookup(slug); const result = categoryHashtagLookup && categoryHashtagLookup(slug);
@ -34,7 +34,7 @@ function addHashtag(buffer, matches, state) {
buffer.push(token); buffer.push(token);
token = new state.Token('text', '', 0); token = new state.Token('text', '', 0);
token.content = hashtag; token.content = match;
buffer.push(token); buffer.push(token);
token = new state.Token('span_close', 'span', -1); token = new state.Token('span_close', 'span', -1);
@ -42,63 +42,14 @@ function addHashtag(buffer, matches, state) {
} }
} }
const REGEX = /#([\w-:]{1,101})/gi;
function allowedBoundary(content, index, utils) {
let code = content.charCodeAt(index);
return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code)));
}
function applyHashtag(content, state) {
let result = null,
match,
pos = 0;
while (match = REGEX.exec(content)) {
// check boundary
if (match.index > 0) {
if (!allowedBoundary(content, match.index-1, state.md.utils)) {
continue;
}
}
// check forward boundary as well
if (match.index + match[0].length < content.length) {
if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) {
continue;
}
}
if (match.index > pos) {
result = result || [];
let token = new state.Token('text', '', 0);
token.content = content.slice(pos, match.index);
result.push(token);
}
result = result || [];
addHashtag(result, match, state);
pos = match.index + match[0].length;
}
if (result && pos < content.length) {
let token = new state.Token('text', '', 0);
token.content = content.slice(pos);
result.push(token);
}
return result;
}
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md=>{ helper.registerPlugin(md=>{
md.core.ruler.push('category-hashtag', state => md.options.discourse.helpers.textReplace( const rule = {
state, applyHashtag, true /* skip all links */ matcher: /#[\w-:]{1,101}/,
)); onMatch: addHashtag
};
md.core.textPostProcess.ruler.push('category-hashtag', rule);
}); });
} }

View File

@ -23,9 +23,6 @@ function censorTree(state, censor) {
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => { helper.registerOptions((opts, siteSettings) => {
opts.censoredWords = siteSettings.censored_words; opts.censoredWords = siteSettings.censored_words;
opts.censoredPattern = siteSettings.censored_pattern; opts.censoredPattern = siteSettings.censored_pattern;

View File

@ -27,8 +27,6 @@ function render(tokens, idx, options, env, slf, md) {
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => { helper.registerOptions((opts, siteSettings) => {
opts.defaultCodeLang = siteSettings.default_code_lang; opts.defaultCodeLang = siteSettings.default_code_lang;
opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']); opts.acceptableCodeClasses = (siteSettings.highlighted_languages || "").split("|").concat(['auto', 'nohighlight']);

View File

@ -229,9 +229,6 @@ function applyEmoji(content, state, emojiUnicodeReplacer) {
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings, state)=>{ helper.registerOptions((opts, siteSettings, state)=>{
opts.features.emoji = !!siteSettings.enable_emoji; opts.features.emoji = !!siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set || ""; opts.emojiSet = siteSettings.emoji_set || "";

View File

@ -65,9 +65,6 @@ function rule(state, startLine, endLine) {
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md=>{ helper.registerPlugin(md=>{
md.block.ruler.before('html_block', 'html_img', rule, {alt: ['fence']}); md.block.ruler.before('html_block', 'html_img', rule, {alt: ['fence']});
}); });

View File

@ -1,43 +1,7 @@
const regex = /^(\w[\w.-]{0,59})\b/i; function addMention(buffer, match, state) {
let username = match.slice(1);
function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup, getURL) { let mentionLookup = state.md.options.discourse.mentionLookup;
let getURL = state.md.options.discourse.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) === "<a") {
return false;
}
}
}
let maxMention = state.src.substr(pos+1, 60);
let matches = maxMention.match(regex);
if (!matches) {
return false;
}
let username = matches[1];
let type = mentionLookup && mentionLookup(username); let type = mentionLookup && mentionLookup(username);
@ -54,35 +18,31 @@ function applyMentions(state, silent, isWhiteSpace, isPunctChar, mentionLookup,
tag = 'span'; tag = 'span';
} }
let token = state.push('mention_open', tag, 1); let token = new state.Token('mention_open', tag, 1);
token.attrs = [['class', className]]; token.attrs = [['class', className]];
if (href) { if (href) {
token.attrs.push(['href', href]); token.attrs.push(['href', href]);
} }
token = state.push('text', '', 0); buffer.push(token);
token = new state.Token('text', '', 0);
token.content = '@'+username; token.content = '@'+username;
state.push('mention_close', tag, -1); buffer.push(token);
state.pos = pos + username.length + 1; token = new state.Token('mention_close', tag, -1);
buffer.push(token);
return true;
} }
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => { helper.registerPlugin(md => {
md.inline.ruler.push('mentions', (state,silent)=> applyMentions(
state, const rule = {
silent, matcher: /@\w[\w.-]{0,59}/,
md.utils.isWhiteSpace, onMatch: addMention
md.utils.isPunctChar, };
md.options.discourse.mentionLookup,
md.options.discourse.getURL md.core.textPostProcess.ruler.push('mentions', rule);
));
}); });
} }

View File

@ -86,9 +86,6 @@ function applyOnebox(state, silent) {
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerPlugin(md => { helper.registerPlugin(md => {
md.core.ruler.after('linkify', 'onebox', applyOnebox); md.core.ruler.after('linkify', 'onebox', applyOnebox);
}); });

View File

@ -121,14 +121,12 @@ const rule = {
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
helper.registerOptions((opts, siteSettings) => { helper.registerOptions((opts, siteSettings) => {
opts.enableEmoji = siteSettings.enable_emoji; opts.enableEmoji = siteSettings.enable_emoji;
opts.emojiSet = siteSettings.emoji_set; opts.emojiSet = siteSettings.emoji_set;
}); });
helper.registerPlugin(md=>{ helper.registerPlugin(md=>{
md.block.bbcode_ruler.push('quotes', rule); md.block.bbcode.ruler.push('quotes', rule);
}); });
} }

View File

@ -1,7 +1,4 @@
export function setup(helper) { export function setup(helper) {
if (!helper.markdownIt) { return; }
// this is built in now // this is built in now
// TODO: sanitizer needs fixing, does not properly support this yet // TODO: sanitizer needs fixing, does not properly support this yet

View File

@ -0,0 +1,100 @@
export class TextPostProcessRuler {
constructor() {
this.rules = [];
}
getRules() {
return this.rules;
}
// TODO error handling
getMatcher() {
if (this.matcher) { return this.matcher; }
this.matcher = new RegExp(this.rules.map((r) =>
"(" + r.rule.matcher.toString().slice(1,-1) + ")"
).join('|'), 'g');
return this.matcher;
}
applyRule(buffer, match, state) {
let i;
for(i=0; i<this.rules.length; i++) {
if (match[i+1]) {
this.rules[i].rule.onMatch(buffer, match[i+1], state);
break;
}
}
}
// TODO validate inputs
push(name, rule) {
this.rules.push({name, rule});
this.matcher = null;
}
}
function allowedBoundary(content, index, utils) {
let code = content.charCodeAt(index);
return (utils.isWhiteSpace(code) || utils.isPunctChar(String.fromCharCode(code)));
}
function textPostProcess(content, state, ruler) {
let result = null,
match,
pos = 0;
const matcher = ruler.getMatcher();
while (match = matcher.exec(content)) {
// check boundary
if (match.index > 0) {
if (!allowedBoundary(content, match.index-1, state.md.utils)) {
continue;
}
}
// check forward boundary as well
if (match.index + match[0].length < content.length) {
if (!allowedBoundary(content, match.index + match[0].length, state.md.utils)) {
continue;
}
}
if (match.index > pos) {
result = result || [];
let token = new state.Token('text', '', 0);
token.content = content.slice(pos, match.index);
result.push(token);
}
result = result || [];
ruler.applyRule(result, match, state);
pos = match.index + match[0].length;
}
if (result && pos < content.length) {
let token = new state.Token('text', '', 0);
token.content = content.slice(pos);
result.push(token);
}
return result;
}
export function setup(helper) {
helper.registerPlugin(md => {
const ruler = md.core.textPostProcess.ruler;
const replacer = (content, state) => textPostProcess(content, state, ruler);
md.core.ruler.push('text-post-process', state =>
md.options.discourse.helpers.textReplace(state, replacer, true)
);
});
}

View File

@ -25,6 +25,6 @@ export function setup(helper) {
]); ]);
helper.registerPlugin(md => { helper.registerPlugin(md => {
md.block.bbcode_ruler.push('details', rule); md.block.bbcode.ruler.push('details', rule);
}); });
} }

View File

@ -295,7 +295,7 @@ function newApiInit(helper) {
}); });
helper.registerPlugin(md => { helper.registerPlugin(md => {
md.block.bbcode_ruler.push('poll', rule); md.block.bbcode.ruler.push('poll', rule);
}); });
} }

View File

@ -175,6 +175,10 @@ describe PrettyText do
) )
end end
it 'should not treat a medium link as a mention' do
expect(PrettyText.cook(". http://test/@sam")).not_to include('mention')
end
end end
describe "code fences" do describe "code fences" do