From 1eec8c3fa606f955e623c50e5a241a6526d821da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 6 May 2024 17:08:34 +0200 Subject: [PATCH] FEATURE: add HTML replacements This adds support for Watched Words to allow replacement with HTML content rather than always replacing with text. Can be useful when automatically replacing with the '' tag for example. Discussion - https://meta.discourse.org/t/replace-text-with-more-than-just-links/305672 --- .../admin/addon/components/admin-watched-word.hbs | 3 +++ .../admin/addon/components/admin-watched-word.js | 4 +--- .../admin/addon/components/watched-word-form.hbs | 14 ++++++++++++++ .../admin/addon/components/watched-word-form.js | 6 +++--- .../javascripts/admin/addon/models/watched-word.js | 1 + .../src/features/watched-words.js | 7 +++++-- app/controllers/admin/watched_words_controller.rb | 2 +- app/models/watched_word.rb | 13 ++++++++----- app/serializers/watched_word_serializer.rb | 13 ++++++++++++- app/services/word_watcher.rb | 11 +++++------ config/locales/client.en.yml | 5 ++++- config/locales/server.en.yml | 1 + .../20240506125839_add_html_to_watched_words.rb | 7 +++++++ 13 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 db/migrate/20240506125839_add_html_to_watched_words.rb 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