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 @@ +
+ + +
+ { 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 search site settings or the admin user list?" + description: 'We couldn’t find anything matching ‘%{filter}’.

Did you want to search site settings or the admin user list?' 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