Skip to content

Commit

Permalink
FIX: Make replace watched words work with wildcard (#13084)
Browse files Browse the repository at this point in the history
Watched words are always regular expressions, despite watched_words_
_regular_expressions being enabled or not. Internally, wildcard
characters are replaced with a regular expression that matches any non
whitespace character.
  • Loading branch information
nbianca committed May 18, 2021
1 parent a21700a commit c1dfd76
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,60 +6,38 @@ function isLinkClose(str) {
return /^<\/a\s*>/i.test(str);
}

function findAllMatches(text, matchers, useRegExp) {
function findAllMatches(text, matchers) {
const matches = [];

if (useRegExp) {
const maxMatches = 100;
let count = 0;

matchers.forEach((matcher) => {
let match;
while (
(match = matcher.pattern.exec(text)) !== null &&
count++ < maxMatches
) {
matches.push({
index: match.index,
text: match[0],
replacement: matcher.replacement,
});
}
});
} else {
const lowerText = text.toLowerCase();
matchers.forEach((matcher) => {
const lowerPattern = matcher.pattern.toLowerCase();
let index = -1;
while ((index = lowerText.indexOf(lowerPattern, index + 1)) !== -1) {
matches.push({
index,
text: text.substr(index, lowerPattern.length),
replacement: matcher.replacement,
});
}
});
}
const maxMatches = 100;
let count = 0;

matchers.forEach((matcher) => {
let match;
while (
(match = matcher.pattern.exec(text)) !== null &&
count++ < maxMatches
) {
matches.push({
index: match.index,
text: match[0],
replacement: matcher.replacement,
});
}
});

return matches.sort((a, b) => a.index - b.index);
}

export function setup(helper) {
helper.registerOptions((opts, siteSettings) => {
opts.watchedWordsRegularExpressions =
siteSettings.watched_words_regular_expressions;
});

helper.registerPlugin((md) => {
const replacements = md.options.discourse.watchedWordsReplacements;
if (!replacements) {
return;
}

const matchers = Object.keys(replacements).map((word) => ({
pattern: md.options.discourse.watchedWordsRegularExpressions
? new RegExp(word, "gi")
: word,
pattern: new RegExp(word, "gi"),
replacement: replacements[word],
}));

Expand Down Expand Up @@ -110,12 +88,7 @@ export function setup(helper) {
if (currentToken.type === "text") {
const text = currentToken.content;
const matches = (cache[text] =
cache[text] ||
findAllMatches(
text,
matchers,
md.options.discourse.watchedWordsRegularExpressions
));
cache[text] || findAllMatches(text, matchers));

// Now split string to nodes
const nodes = [];
Expand Down
2 changes: 1 addition & 1 deletion app/serializers/site_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def include_shared_drafts_category_id?
end

def watched_words_replace
WordWatcher.get_cached_words(:replace)
WordWatcher.word_matcher_regexps(:replace)
end

private
Expand Down
6 changes: 6 additions & 0 deletions app/services/word_watcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ def self.word_matcher_regexp(action, raise_errors: false)
nil # Admin will be alerted via admin_dashboard_data.rb
end

def self.word_matcher_regexps(action)
if words = get_cached_words(action)
words.map { |w, r| [word_to_regexp(w), r] }.to_h
end
end

def self.word_to_regexp(word)
if SiteSetting.watched_words_regular_expressions?
# Strip ruby regexp format if present, we're going to make the whole thing
Expand Down
2 changes: 1 addition & 1 deletion lib/pretty_text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def self.markdown(text, opts = {})
__optInput.emojiUnicodeReplacer = __emojiUnicodeReplacer;
__optInput.lookupUploadUrls = __lookupUploadUrls;
__optInput.censoredRegexp = #{WordWatcher.word_matcher_regexp(:censor)&.source.to_json};
__optInput.watchedWordsReplacements = #{WordWatcher.get_cached_words(:replace).to_json};
__optInput.watchedWordsReplacements = #{WordWatcher.word_matcher_regexps(:replace).to_json};
JS

if opts[:topicId]
Expand Down
10 changes: 9 additions & 1 deletion spec/components/pretty_text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1401,11 +1401,19 @@ def expect_cooked_match(raw, expected_cooked)
after(:all) { Discourse.redis.flushdb }

it "replaces words with other words" do
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "dolor sit", replacement: "something else")
Fabricate(:watched_word, action: WatchedWord.actions[:replace], word: "dolor sit*", replacement: "something else")

expect(PrettyText.cook("Lorem ipsum dolor sit amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p>
HTML

expect(PrettyText.cook("Lorem ipsum dolor sits amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p>
HTML

expect(PrettyText.cook("Lorem ipsum dolor sittt amet")).to match_html(<<~HTML)
<p>Lorem ipsum something else amet</p>
HTML
end

it "replaces words with links" do
Expand Down

0 comments on commit c1dfd76

Please sign in to comment.