diff --git a/app/assets/javascripts/admin/addon/components/admin-watched-word.hbs b/app/assets/javascripts/admin/addon/components/admin-watched-word.hbs
index 4323174a114..8af02f51571 100644
--- a/app/assets/javascripts/admin/addon/components/admin-watched-word.hbs
+++ b/app/assets/javascripts/admin/addon/components/admin-watched-word.hbs
@@ -17,4 +17,7 @@
{{i18n
"admin.watched_words.case_sensitive"
}}
+{{/if}}
+{{#if this.isHtml}}
+ {{i18n "admin.watched_words.html"}}
{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/admin/addon/components/admin-watched-word.js b/app/assets/javascripts/admin/addon/components/admin-watched-word.js
index b057e2af874..cffbe13d990 100644
--- a/app/assets/javascripts/admin/addon/components/admin-watched-word.js
+++ b/app/assets/javascripts/admin/addon/components/admin-watched-word.js
@@ -11,12 +11,10 @@ export default class AdminWatchedWord extends Component {
@service dialog;
@equal("actionKey", "replace") isReplace;
-
@equal("actionKey", "tag") isTag;
-
@equal("actionKey", "link") isLink;
-
@alias("word.case_sensitive") isCaseSensitive;
+ @alias("word.html") isHtml;
@discourseComputed("word.replacement")
tags(replacement) {
diff --git a/app/assets/javascripts/admin/addon/components/watched-word-form.hbs b/app/assets/javascripts/admin/addon/components/watched-word-form.hbs
index 606862e3e65..1570bd74caf 100644
--- a/app/assets/javascripts/admin/addon/components/watched-word-form.hbs
+++ b/app/assets/javascripts/admin/addon/components/watched-word-form.hbs
@@ -75,6 +75,20 @@
+
+ {{i18n
+ "admin.watched_words.form.html_label"
+ }}
+
+
+ {{i18n "admin.watched_words.form.html_description"}}
+
+
+
{
diff --git a/app/assets/javascripts/admin/addon/models/watched-word.js b/app/assets/javascripts/admin/addon/models/watched-word.js
index d59f3e6a650..e6010084274 100644
--- a/app/assets/javascripts/admin/addon/models/watched-word.js
+++ b/app/assets/javascripts/admin/addon/models/watched-word.js
@@ -38,6 +38,7 @@ export default class WatchedWord extends EmberObject {
replacement: this.replacement,
action_key: this.action,
case_sensitive: this.isCaseSensitive,
+ html: this.isHtml,
},
dataType: "json",
}
diff --git a/app/assets/javascripts/discourse-markdown-it/src/features/watched-words.js b/app/assets/javascripts/discourse-markdown-it/src/features/watched-words.js
index 00f9d91fe1b..284174e562c 100644
--- a/app/assets/javascripts/discourse-markdown-it/src/features/watched-words.js
+++ b/app/assets/javascripts/discourse-markdown-it/src/features/watched-words.js
@@ -16,7 +16,7 @@ function isLinkClose(str) {
function findAllMatches(text, matchers) {
const matches = [];
- for (const { word, pattern, replacement, link } of matchers) {
+ for (const { word, pattern, replacement, link, html } of matchers) {
if (matches.length >= MAX_MATCHES) {
break;
}
@@ -28,6 +28,7 @@ function findAllMatches(text, matchers) {
text: match[1],
replacement,
link,
+ html,
});
if (matches.length >= MAX_MATCHES) {
@@ -65,6 +66,7 @@ export function setup(helper) {
pattern: createWatchedWordRegExp(word),
replacement: options.replacement,
link: false,
+ html: options.html,
});
}
);
@@ -239,7 +241,8 @@ export function setup(helper) {
nodes.push(token);
}
} else {
- token = new state.Token("text", "", 0);
+ let tokenType = matches[ln].html ? "html_inline" : "text";
+ token = new state.Token(tokenType, "", 0);
token.content = matches[ln].replacement;
token.level = level;
nodes.push(token);
diff --git a/app/controllers/admin/watched_words_controller.rb b/app/controllers/admin/watched_words_controller.rb
index d2d7f52290e..9cf532e63b8 100644
--- a/app/controllers/admin/watched_words_controller.rb
+++ b/app/controllers/admin/watched_words_controller.rb
@@ -120,6 +120,6 @@ class Admin::WatchedWordsController < Admin::StaffController
def watched_words_params
@watched_words_params ||=
- params.permit(:id, :replacement, :action_key, :case_sensitive, words: [])
+ params.permit(:id, :replacement, :action_key, :case_sensitive, :html, words: [])
end
end
diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb
index a1f6b882b04..aebc1a701d8 100644
--- a/app/models/watched_word.rb
+++ b/app/models/watched_word.rb
@@ -16,6 +16,7 @@ class WatchedWord < ActiveRecord::Base
validates :action, presence: true
validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] }
validate :replacement_is_tag_list, if: -> { action == WatchedWord.actions[:tag] }
+ validate :replacement_is_html, if: -> { replacement.present? && html? }
validates_each :word do |record, attr, val|
if WatchedWord.where(action: record.action).count >= MAX_WORDS_PER_ACTION
@@ -65,6 +66,7 @@ class WatchedWord < ActiveRecord::Base
word.action_key = params[:action_key] if params[:action_key]
word.action = params[:action] if params[:action]
word.case_sensitive = params[:case_sensitive] if !params[:case_sensitive].nil?
+ word.html = params[:html] if params[:html]
word.watched_word_group_id = params[:watched_word_group_id]
word.save
word
@@ -79,11 +81,7 @@ class WatchedWord < ActiveRecord::Base
end
def action_log_details
- if replacement.present?
- "#{word} → #{replacement}"
- else
- word
- end
+ replacement.present? ? "#{word} → #{replacement}" : word
end
private
@@ -107,6 +105,10 @@ class WatchedWord < ActiveRecord::Base
errors.add(:base, :invalid_tag_list)
end
end
+
+ def replacement_is_html
+ errors.add(:base, :invalid_html) if action != WatchedWord.actions[:replace]
+ end
end
# == Schema Information
@@ -121,6 +123,7 @@ end
# replacement :string
# case_sensitive :boolean default(FALSE), not null
# watched_word_group_id :bigint
+# html :boolean default(FALSE), not null
#
# Indexes
#
diff --git a/app/serializers/watched_word_serializer.rb b/app/serializers/watched_word_serializer.rb
index 8a1658e285f..aff7cb7a7f7 100644
--- a/app/serializers/watched_word_serializer.rb
+++ b/app/serializers/watched_word_serializer.rb
@@ -1,7 +1,14 @@
# frozen_string_literal: true
class WatchedWordSerializer < ApplicationSerializer
- attributes :id, :word, :regexp, :replacement, :action, :case_sensitive, :watched_word_group_id
+ attributes :id,
+ :word,
+ :regexp,
+ :replacement,
+ :action,
+ :case_sensitive,
+ :watched_word_group_id,
+ :html
def regexp
WordWatcher.word_to_regexp(word, engine: :js)
@@ -14,4 +21,8 @@ class WatchedWordSerializer < ApplicationSerializer
def include_replacement?
WatchedWord.has_replacement?(action)
end
+
+ def include_html?
+ object.action == WatchedWord.actions[:replace] && object.html
+ end
end
diff --git a/app/services/word_watcher.rb b/app/services/word_watcher.rb
index 4b5760de8a3..c71706b692f 100644
--- a/app/services/word_watcher.rb
+++ b/app/services/word_watcher.rb
@@ -31,12 +31,11 @@ class WordWatcher
.where(action: WatchedWord.actions[action.to_sym])
.limit(WatchedWord::MAX_WORDS_PER_ACTION)
.order(:id)
- .pluck(:word, :replacement, :case_sensitive)
- .to_h do |w, r, c|
- [
- word_to_regexp(w, match_word: false),
- { word: w, replacement: r, case_sensitive: c }.compact,
- ]
+ .pluck(:word, :replacement, :case_sensitive, :html)
+ .to_h do |w, r, c, h|
+ opts = { word: w, replacement: r, case_sensitive: c }.compact
+ opts[:html] = true if h
+ [word_to_regexp(w, match_word: false), opts]
end
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index f74ec3fefe7..282ebc65440 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -4766,7 +4766,7 @@ en:
clear_filter: "Clear filter"
no_results:
title: "No results"
- description: "We couldn’t find anything matching ‘%{filter}’. Did you want to or the "
+ description: 'We couldn’t find anything matching ‘%{filter}’. Did you want to or the '
welcome_topic_banner:
title: "Create your Welcome Topic"
@@ -6102,6 +6102,7 @@ en:
one: "show %{count} word"
other: "show %{count} words"
case_sensitive: "(case-sensitive)"
+ html: "(html)"
download: Download
clear_all: Clear All
clear_all_confirm: "Are you sure you want to clear all watched words for the %{action} action?"
@@ -6141,6 +6142,8 @@ en:
upload_successful: "Upload successful. Words have been added."
case_sensitivity_label: "Is case-sensitive"
case_sensitivity_description: "Only words with matching character casing"
+ html_label: "HTML"
+ html_description: "Outputs HTML in the replacement"
words_or_phrases: "words or phrases"
test:
button_label: "Test"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index e2759496398..e7ef4712482 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -800,6 +800,7 @@ en:
base:
invalid_url: "Replacement URL is invalid"
invalid_tag_list: "Replacement tag list is invalid"
+ invalid_html: "HTML can only be used for replacement"
sidebar_section_link:
attributes:
linkable_type:
diff --git a/db/migrate/20240506125839_add_html_to_watched_words.rb b/db/migrate/20240506125839_add_html_to_watched_words.rb
new file mode 100644
index 00000000000..9b4194a7673
--- /dev/null
+++ b/db/migrate/20240506125839_add_html_to_watched_words.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddHtmlToWatchedWords < ActiveRecord::Migration[7.0]
+ def change
+ add_column :watched_words, :html, :boolean, default: false, null: false
+ end
+end