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 '<abbr>' tag for example. Discussion - https://meta.discourse.org/t/replace-text-with-more-than-just-links/305672
This commit is contained in:
parent
9b70cbf4bb
commit
1eec8c3fa6
|
@ -18,3 +18,6 @@
|
|||
"admin.watched_words.case_sensitive"
|
||||
}}</span>
|
||||
{{/if}}
|
||||
{{#if this.isHtml}}
|
||||
<span class="html">{{i18n "admin.watched_words.html"}}</span>
|
||||
{{/if}}
|
|
@ -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) {
|
||||
|
|
|
@ -75,6 +75,20 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="watched-word-input">
|
||||
<label for="watched-html">{{i18n
|
||||
"admin.watched_words.form.html_label"
|
||||
}}</label>
|
||||
<label class="html-checkbox checkbox-label">
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{this.isHtml}}
|
||||
disabled={{this.formSubmitted}}
|
||||
/>
|
||||
{{i18n "admin.watched_words.form.html_description"}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DButton
|
||||
@action={{this.submitForm}}
|
||||
@disabled={{this.submitDisabled}}
|
||||
|
|
|
@ -19,15 +19,13 @@ export default class WatchedWordForm extends Component {
|
|||
actionKey = null;
|
||||
showMessage = false;
|
||||
isCaseSensitive = false;
|
||||
isHtml = false;
|
||||
selectedTags = [];
|
||||
words = [];
|
||||
|
||||
@empty("words") submitDisabled;
|
||||
|
||||
@equal("actionKey", "replace") canReplace;
|
||||
|
||||
@equal("actionKey", "tag") canTag;
|
||||
|
||||
@equal("actionKey", "link") canLink;
|
||||
|
||||
@discourseComputed("siteSettings.watched_words_regular_expressions")
|
||||
|
@ -102,6 +100,7 @@ export default class WatchedWordForm extends Component {
|
|||
: null,
|
||||
action: this.actionKey,
|
||||
isCaseSensitive: this.isCaseSensitive,
|
||||
isHtml: this.isHtml,
|
||||
});
|
||||
|
||||
watchedWord
|
||||
|
@ -114,6 +113,7 @@ export default class WatchedWordForm extends Component {
|
|||
showMessage: true,
|
||||
message: I18n.t("admin.watched_words.form.success"),
|
||||
isCaseSensitive: false,
|
||||
isHtml: false,
|
||||
});
|
||||
if (result.words) {
|
||||
result.words.forEach((word) => {
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -4766,7 +4766,7 @@ en:
|
|||
clear_filter: "Clear filter"
|
||||
no_results:
|
||||
title: "No results"
|
||||
description: "We couldn’t find anything matching ‘%{filter}’.<br><br>Did you want to <a class=\"sidebar-additional-filter-settings\" href=\"%{settings_filter_url}\">search site settings</a> or the <a class=\"sidebar-additional-filter-users\" href=\"%{user_list_filter_url}\">admin user list?</a>"
|
||||
description: 'We couldn’t find anything matching ‘%{filter}’.<br><br>Did you want to <a class="sidebar-additional-filter-settings" href="%{settings_filter_url}">search site settings</a> or the <a class="sidebar-additional-filter-users" href="%{user_list_filter_url}">admin user list?</a>'
|
||||
|
||||
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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue