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"
|
"admin.watched_words.case_sensitive"
|
||||||
}}</span>
|
}}</span>
|
||||||
{{/if}}
|
{{/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;
|
@service dialog;
|
||||||
|
|
||||||
@equal("actionKey", "replace") isReplace;
|
@equal("actionKey", "replace") isReplace;
|
||||||
|
|
||||||
@equal("actionKey", "tag") isTag;
|
@equal("actionKey", "tag") isTag;
|
||||||
|
|
||||||
@equal("actionKey", "link") isLink;
|
@equal("actionKey", "link") isLink;
|
||||||
|
|
||||||
@alias("word.case_sensitive") isCaseSensitive;
|
@alias("word.case_sensitive") isCaseSensitive;
|
||||||
|
@alias("word.html") isHtml;
|
||||||
|
|
||||||
@discourseComputed("word.replacement")
|
@discourseComputed("word.replacement")
|
||||||
tags(replacement) {
|
tags(replacement) {
|
||||||
|
|
|
@ -75,6 +75,20 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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
|
<DButton
|
||||||
@action={{this.submitForm}}
|
@action={{this.submitForm}}
|
||||||
@disabled={{this.submitDisabled}}
|
@disabled={{this.submitDisabled}}
|
||||||
|
|
|
@ -19,15 +19,13 @@ export default class WatchedWordForm extends Component {
|
||||||
actionKey = null;
|
actionKey = null;
|
||||||
showMessage = false;
|
showMessage = false;
|
||||||
isCaseSensitive = false;
|
isCaseSensitive = false;
|
||||||
|
isHtml = false;
|
||||||
selectedTags = [];
|
selectedTags = [];
|
||||||
words = [];
|
words = [];
|
||||||
|
|
||||||
@empty("words") submitDisabled;
|
@empty("words") submitDisabled;
|
||||||
|
|
||||||
@equal("actionKey", "replace") canReplace;
|
@equal("actionKey", "replace") canReplace;
|
||||||
|
|
||||||
@equal("actionKey", "tag") canTag;
|
@equal("actionKey", "tag") canTag;
|
||||||
|
|
||||||
@equal("actionKey", "link") canLink;
|
@equal("actionKey", "link") canLink;
|
||||||
|
|
||||||
@discourseComputed("siteSettings.watched_words_regular_expressions")
|
@discourseComputed("siteSettings.watched_words_regular_expressions")
|
||||||
|
@ -102,6 +100,7 @@ export default class WatchedWordForm extends Component {
|
||||||
: null,
|
: null,
|
||||||
action: this.actionKey,
|
action: this.actionKey,
|
||||||
isCaseSensitive: this.isCaseSensitive,
|
isCaseSensitive: this.isCaseSensitive,
|
||||||
|
isHtml: this.isHtml,
|
||||||
});
|
});
|
||||||
|
|
||||||
watchedWord
|
watchedWord
|
||||||
|
@ -114,6 +113,7 @@ export default class WatchedWordForm extends Component {
|
||||||
showMessage: true,
|
showMessage: true,
|
||||||
message: I18n.t("admin.watched_words.form.success"),
|
message: I18n.t("admin.watched_words.form.success"),
|
||||||
isCaseSensitive: false,
|
isCaseSensitive: false,
|
||||||
|
isHtml: false,
|
||||||
});
|
});
|
||||||
if (result.words) {
|
if (result.words) {
|
||||||
result.words.forEach((word) => {
|
result.words.forEach((word) => {
|
||||||
|
|
|
@ -38,6 +38,7 @@ export default class WatchedWord extends EmberObject {
|
||||||
replacement: this.replacement,
|
replacement: this.replacement,
|
||||||
action_key: this.action,
|
action_key: this.action,
|
||||||
case_sensitive: this.isCaseSensitive,
|
case_sensitive: this.isCaseSensitive,
|
||||||
|
html: this.isHtml,
|
||||||
},
|
},
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ function isLinkClose(str) {
|
||||||
function findAllMatches(text, matchers) {
|
function findAllMatches(text, matchers) {
|
||||||
const matches = [];
|
const matches = [];
|
||||||
|
|
||||||
for (const { word, pattern, replacement, link } of matchers) {
|
for (const { word, pattern, replacement, link, html } of matchers) {
|
||||||
if (matches.length >= MAX_MATCHES) {
|
if (matches.length >= MAX_MATCHES) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ function findAllMatches(text, matchers) {
|
||||||
text: match[1],
|
text: match[1],
|
||||||
replacement,
|
replacement,
|
||||||
link,
|
link,
|
||||||
|
html,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (matches.length >= MAX_MATCHES) {
|
if (matches.length >= MAX_MATCHES) {
|
||||||
|
@ -65,6 +66,7 @@ export function setup(helper) {
|
||||||
pattern: createWatchedWordRegExp(word),
|
pattern: createWatchedWordRegExp(word),
|
||||||
replacement: options.replacement,
|
replacement: options.replacement,
|
||||||
link: false,
|
link: false,
|
||||||
|
html: options.html,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -239,7 +241,8 @@ export function setup(helper) {
|
||||||
nodes.push(token);
|
nodes.push(token);
|
||||||
}
|
}
|
||||||
} else {
|
} 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.content = matches[ln].replacement;
|
||||||
token.level = level;
|
token.level = level;
|
||||||
nodes.push(token);
|
nodes.push(token);
|
||||||
|
|
|
@ -120,6 +120,6 @@ class Admin::WatchedWordsController < Admin::StaffController
|
||||||
|
|
||||||
def watched_words_params
|
def watched_words_params
|
||||||
@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
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,7 @@ class WatchedWord < ActiveRecord::Base
|
||||||
validates :action, presence: true
|
validates :action, presence: true
|
||||||
validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] }
|
validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] }
|
||||||
validate :replacement_is_tag_list, if: -> { action == WatchedWord.actions[:tag] }
|
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|
|
validates_each :word do |record, attr, val|
|
||||||
if WatchedWord.where(action: record.action).count >= MAX_WORDS_PER_ACTION
|
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_key = params[:action_key] if params[:action_key]
|
||||||
word.action = params[:action] if params[:action]
|
word.action = params[:action] if params[:action]
|
||||||
word.case_sensitive = params[:case_sensitive] if !params[:case_sensitive].nil?
|
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.watched_word_group_id = params[:watched_word_group_id]
|
||||||
word.save
|
word.save
|
||||||
word
|
word
|
||||||
|
@ -79,11 +81,7 @@ class WatchedWord < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def action_log_details
|
def action_log_details
|
||||||
if replacement.present?
|
replacement.present? ? "#{word} → #{replacement}" : word
|
||||||
"#{word} → #{replacement}"
|
|
||||||
else
|
|
||||||
word
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -107,6 +105,10 @@ class WatchedWord < ActiveRecord::Base
|
||||||
errors.add(:base, :invalid_tag_list)
|
errors.add(:base, :invalid_tag_list)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def replacement_is_html
|
||||||
|
errors.add(:base, :invalid_html) if action != WatchedWord.actions[:replace]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# == Schema Information
|
# == Schema Information
|
||||||
|
@ -121,6 +123,7 @@ end
|
||||||
# replacement :string
|
# replacement :string
|
||||||
# case_sensitive :boolean default(FALSE), not null
|
# case_sensitive :boolean default(FALSE), not null
|
||||||
# watched_word_group_id :bigint
|
# watched_word_group_id :bigint
|
||||||
|
# html :boolean default(FALSE), not null
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WatchedWordSerializer < ApplicationSerializer
|
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
|
def regexp
|
||||||
WordWatcher.word_to_regexp(word, engine: :js)
|
WordWatcher.word_to_regexp(word, engine: :js)
|
||||||
|
@ -14,4 +21,8 @@ class WatchedWordSerializer < ApplicationSerializer
|
||||||
def include_replacement?
|
def include_replacement?
|
||||||
WatchedWord.has_replacement?(action)
|
WatchedWord.has_replacement?(action)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def include_html?
|
||||||
|
object.action == WatchedWord.actions[:replace] && object.html
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,12 +31,11 @@ class WordWatcher
|
||||||
.where(action: WatchedWord.actions[action.to_sym])
|
.where(action: WatchedWord.actions[action.to_sym])
|
||||||
.limit(WatchedWord::MAX_WORDS_PER_ACTION)
|
.limit(WatchedWord::MAX_WORDS_PER_ACTION)
|
||||||
.order(:id)
|
.order(:id)
|
||||||
.pluck(:word, :replacement, :case_sensitive)
|
.pluck(:word, :replacement, :case_sensitive, :html)
|
||||||
.to_h do |w, r, c|
|
.to_h do |w, r, c, h|
|
||||||
[
|
opts = { word: w, replacement: r, case_sensitive: c }.compact
|
||||||
word_to_regexp(w, match_word: false),
|
opts[:html] = true if h
|
||||||
{ word: w, replacement: r, case_sensitive: c }.compact,
|
[word_to_regexp(w, match_word: false), opts]
|
||||||
]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4766,7 +4766,7 @@ en:
|
||||||
clear_filter: "Clear filter"
|
clear_filter: "Clear filter"
|
||||||
no_results:
|
no_results:
|
||||||
title: "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:
|
welcome_topic_banner:
|
||||||
title: "Create your Welcome Topic"
|
title: "Create your Welcome Topic"
|
||||||
|
@ -6102,6 +6102,7 @@ en:
|
||||||
one: "show %{count} word"
|
one: "show %{count} word"
|
||||||
other: "show %{count} words"
|
other: "show %{count} words"
|
||||||
case_sensitive: "(case-sensitive)"
|
case_sensitive: "(case-sensitive)"
|
||||||
|
html: "(html)"
|
||||||
download: Download
|
download: Download
|
||||||
clear_all: Clear All
|
clear_all: Clear All
|
||||||
clear_all_confirm: "Are you sure you want to clear all watched words for the %{action} action?"
|
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."
|
upload_successful: "Upload successful. Words have been added."
|
||||||
case_sensitivity_label: "Is case-sensitive"
|
case_sensitivity_label: "Is case-sensitive"
|
||||||
case_sensitivity_description: "Only words with matching character casing"
|
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"
|
words_or_phrases: "words or phrases"
|
||||||
test:
|
test:
|
||||||
button_label: "Test"
|
button_label: "Test"
|
||||||
|
|
|
@ -800,6 +800,7 @@ en:
|
||||||
base:
|
base:
|
||||||
invalid_url: "Replacement URL is invalid"
|
invalid_url: "Replacement URL is invalid"
|
||||||
invalid_tag_list: "Replacement tag list is invalid"
|
invalid_tag_list: "Replacement tag list is invalid"
|
||||||
|
invalid_html: "HTML can only be used for replacement"
|
||||||
sidebar_section_link:
|
sidebar_section_link:
|
||||||
attributes:
|
attributes:
|
||||||
linkable_type:
|
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