mirror of
https://github.com/zulip/zulip.git
synced 2025-11-01 20:44:04 +00:00
search: Remove highlighting in search typeahead.
Context: https://chat.zulip.org/#narrow/channel/101-design/topic/search.20typeahead.20highlighting/near/2188093
This commit is contained in:
@@ -56,7 +56,7 @@ const MAX_LOOKBACK_FOR_TYPEAHEAD_COMPLETION = 60 + 6 + 20;
|
||||
//
|
||||
// So if you are not using trusted input, you MUST use a
|
||||
// highlighter that escapes (i.e. one that calls
|
||||
// typeahead_helper.highlight_with_escaping).
|
||||
// Handlebars.Utils.escapeExpression).
|
||||
|
||||
// ---------------- TYPE DECLARATIONS ----------------
|
||||
// There are many types of suggestions that can show
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Handlebars from "handlebars/runtime.js";
|
||||
import _ from "lodash";
|
||||
import assert from "minimalistic-assert";
|
||||
|
||||
@@ -261,7 +260,7 @@ export function create_user_pill_context(user: User): UserPillItem {
|
||||
|
||||
return {
|
||||
id: user.user_id,
|
||||
display_value: new Handlebars.SafeString(user.full_name),
|
||||
display_value: user.full_name,
|
||||
has_image: true,
|
||||
img_src: avatar_url,
|
||||
should_add_guest_user_indicator: people.should_add_guest_user_indicator(user.user_id),
|
||||
|
||||
@@ -20,7 +20,7 @@ import * as util from "./util.ts";
|
||||
|
||||
export type UserPillItem = {
|
||||
id: number;
|
||||
display_value: Handlebars.SafeString;
|
||||
display_value: string;
|
||||
has_image: boolean;
|
||||
img_src: string;
|
||||
should_add_guest_user_indicator: boolean;
|
||||
@@ -49,23 +49,12 @@ function channel_matches_query(channel_name: string, q: string): boolean {
|
||||
return common.phrase_match(q, channel_name);
|
||||
}
|
||||
|
||||
function make_person_highlighter(query: string): (person: User) => string {
|
||||
const highlight_query = typeahead_helper.make_query_highlighter(query);
|
||||
|
||||
return function (person: User): string {
|
||||
return highlight_query(person.full_name);
|
||||
};
|
||||
}
|
||||
|
||||
function highlight_person(person: User, highlighter: (person: User) => string): UserPillItem {
|
||||
const avatar_url = people.small_avatar_url_for_person(person);
|
||||
const highlighted_name = highlighter(person);
|
||||
|
||||
function user_pill_item(person: User): UserPillItem {
|
||||
return {
|
||||
id: person.user_id,
|
||||
display_value: new Handlebars.SafeString(highlighted_name),
|
||||
display_value: person.full_name,
|
||||
has_image: true,
|
||||
img_src: avatar_url,
|
||||
img_src: people.small_avatar_url_for_person(person),
|
||||
should_add_guest_user_indicator: people.should_add_guest_user_indicator(person.user_id),
|
||||
};
|
||||
}
|
||||
@@ -164,14 +153,11 @@ function get_channel_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggest
|
||||
|
||||
channels = typeahead_helper.sorter(query, channels, (x) => x);
|
||||
|
||||
const regex = typeahead_helper.build_highlight_regex(query);
|
||||
const highlight_query = typeahead_helper.highlight_with_escaping_and_regex;
|
||||
|
||||
return channels.map((channel_name) => {
|
||||
const prefix = "channel";
|
||||
const highlighted_channel = highlight_query(regex, channel_name);
|
||||
const verb = last.negated ? "exclude " : "";
|
||||
const description_html = verb + prefix + " " + highlighted_channel;
|
||||
const description_html =
|
||||
verb + prefix + " " + Handlebars.Utils.escapeExpression(channel_name);
|
||||
const channel = stream_data.get_sub_by_name(channel_name);
|
||||
assert(channel !== undefined);
|
||||
const term = {
|
||||
@@ -278,8 +264,6 @@ function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestio
|
||||
|
||||
const prefix = Filter.operator_to_prefix("dm", negated);
|
||||
|
||||
const person_highlighter = make_person_highlighter(last_part);
|
||||
|
||||
return persons.map((person) => {
|
||||
const term = {
|
||||
operator: "dm",
|
||||
@@ -292,10 +276,7 @@ function get_group_suggestions(last: NarrowTerm, terms: NarrowTerm[]): Suggestio
|
||||
terms = [{operator: "is", operand: "dm"}, term];
|
||||
}
|
||||
|
||||
const all_user_pill_contexts = [
|
||||
...user_pill_contexts,
|
||||
highlight_person(person, person_highlighter),
|
||||
];
|
||||
const all_user_pill_contexts = [...user_pill_contexts, user_pill_item(person)];
|
||||
|
||||
return {
|
||||
description_html: prefix,
|
||||
@@ -346,8 +327,6 @@ function get_person_suggestions(
|
||||
last = {operator: "dm", operand: "", negated: false};
|
||||
}
|
||||
|
||||
const query = last.operand;
|
||||
|
||||
// Be especially strict about the less common "from" operator.
|
||||
if (autocomplete_operator === "from" && last.operator !== "from") {
|
||||
return [];
|
||||
@@ -383,8 +362,6 @@ function get_person_suggestions(
|
||||
|
||||
const prefix = Filter.operator_to_prefix(autocomplete_operator, last.negated);
|
||||
|
||||
const person_highlighter = make_person_highlighter(query);
|
||||
|
||||
return persons.map((person) => {
|
||||
const terms: NarrowTerm[] = [
|
||||
{
|
||||
@@ -410,7 +387,7 @@ function get_person_suggestions(
|
||||
is_people: true,
|
||||
users: [
|
||||
{
|
||||
user_pill_context: highlight_person(person, person_highlighter),
|
||||
user_pill_context: user_pill_item(person),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1103,9 +1080,7 @@ export function get_search_result(
|
||||
suggestion_line = [
|
||||
{
|
||||
search_string: last.operand,
|
||||
description_html: `search for <strong>${Handlebars.Utils.escapeExpression(
|
||||
last.operand,
|
||||
)}</strong>`,
|
||||
description_html: `search for ${Handlebars.Utils.escapeExpression(last.operand)}`,
|
||||
is_people: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Handlebars from "handlebars/runtime.js";
|
||||
import _ from "lodash";
|
||||
import assert from "minimalistic-assert";
|
||||
|
||||
@@ -48,48 +47,6 @@ export type CombinedPillContainer = InputPillContainer<CombinedPill>;
|
||||
export type GroupSettingPill = UserGroupPill | UserPill;
|
||||
export type GroupSettingPillContainer = InputPillContainer<GroupSettingPill>;
|
||||
|
||||
export function build_highlight_regex(query: string): RegExp {
|
||||
const regex = new RegExp("(" + _.escapeRegExp(query) + ")", "ig");
|
||||
return regex;
|
||||
}
|
||||
|
||||
export function highlight_with_escaping_and_regex(regex: RegExp, item: string): string {
|
||||
// if regex is empty return entire item escaped
|
||||
if (regex.source === "()") {
|
||||
return Handlebars.Utils.escapeExpression(item);
|
||||
}
|
||||
|
||||
// We need to assemble this manually (as opposed to doing 'join') because we need to
|
||||
// (1) escape all the pieces and (2) the regex is case-insensitive, and we need
|
||||
// to know the case of the content we're replacing (you can't just use a bolded
|
||||
// version of 'query')
|
||||
|
||||
const pieces = item.split(regex).filter(Boolean);
|
||||
let result = "";
|
||||
|
||||
for (const [i, piece] of pieces.entries()) {
|
||||
if (regex.test(piece) && (i === 0 || pieces[i - 1]!.endsWith(" "))) {
|
||||
// only highlight if the matching part is a word prefix, ie
|
||||
// if it is the 1st piece or if there was a space before it
|
||||
result += "<strong>" + Handlebars.Utils.escapeExpression(piece) + "</strong>";
|
||||
} else {
|
||||
result += Handlebars.Utils.escapeExpression(piece);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function make_query_highlighter(query: string): (phrase: string) => string {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const regex = build_highlight_regex(query);
|
||||
|
||||
return function (phrase) {
|
||||
return highlight_with_escaping_and_regex(regex, phrase);
|
||||
};
|
||||
}
|
||||
|
||||
type StreamData = {
|
||||
invite_only: boolean;
|
||||
is_web_public: boolean;
|
||||
|
||||
@@ -96,7 +96,7 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
[
|
||||
"stream:Verona",
|
||||
{
|
||||
description_html: "Stream <strong>Ver</strong>ona",
|
||||
description_html: "Stream Verona",
|
||||
search_string: "stream:Verona",
|
||||
},
|
||||
],
|
||||
@@ -121,7 +121,7 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
let expected_value = `<div class="search_list_item">\n <span>Search for ver</span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[0]), expected_value);
|
||||
|
||||
expected_value = `<div class="search_list_item">\n <span>Stream <strong>Ver</strong>ona</span>\n</div>\n`;
|
||||
expected_value = `<div class="search_list_item">\n <span>Stream Verona</span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[1]), expected_value);
|
||||
|
||||
/* Test sorter */
|
||||
@@ -140,7 +140,7 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
users: [
|
||||
{
|
||||
user_pill_context: {
|
||||
display_value: "<strong>Zo</strong>e",
|
||||
display_value: "Zoe",
|
||||
has_image: true,
|
||||
id: 7,
|
||||
img_src:
|
||||
@@ -159,7 +159,7 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
users: [
|
||||
{
|
||||
user_pill_context: {
|
||||
display_value: "<strong>Zo</strong>e",
|
||||
display_value: "Zoe",
|
||||
has_image: true,
|
||||
id: 7,
|
||||
img_src:
|
||||
@@ -178,7 +178,7 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
users: [
|
||||
{
|
||||
user_pill_context: {
|
||||
display_value: "<strong>Zo</strong>e",
|
||||
display_value: "Zoe",
|
||||
has_image: true,
|
||||
id: 7,
|
||||
img_src:
|
||||
@@ -209,13 +209,13 @@ run_test("initialize", ({override, override_rewire, mock_template}) => {
|
||||
let expected_value = `<div class="search_list_item">\n <span>Search for zo</span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[0]), expected_value);
|
||||
|
||||
expected_value = `<div class="search_list_item">\n <span>sent by</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n <strong>Zo</strong>e\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
expected_value = `<div class="search_list_item">\n <span>sent by</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n Zoe\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[1]), expected_value);
|
||||
|
||||
expected_value = `<div class="search_list_item">\n <span>direct messages with</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n <strong>Zo</strong>e\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
expected_value = `<div class="search_list_item">\n <span>direct messages with</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n Zoe\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[2]), expected_value);
|
||||
|
||||
expected_value = `<div class="search_list_item">\n <span>group direct messages including</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n <strong>Zo</strong>e\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
expected_value = `<div class="search_list_item">\n <span>group direct messages including</span>\n <span class="pill-container">\n <div class='pill ' tabindex=0>\n <img class="pill-image" src="https://secure.gravatar.com/avatar/0f030c97ab51312c7bbffd3966198ced?d=identicon&version=1" />\n <div class="pill-image-border"></div>\n <span class="pill-label">\n <span class="pill-value">\n Zoe\n </span></span>\n <div class="exit">\n <a role="button" class="zulip-icon zulip-icon-close pill-close-button"></a>\n </div>\n</div>\n </span>\n</div>\n`;
|
||||
assert.equal(opts.highlighter_html(source[3]), expected_value);
|
||||
|
||||
/* Test sorter */
|
||||
|
||||
@@ -705,7 +705,7 @@ test("topic_suggestions", ({override, mock_template}) => {
|
||||
function describe(q) {
|
||||
return suggestions.lookup_table.get(q).description_html;
|
||||
}
|
||||
assert.equal(describe("te"), "Search for <strong>te</strong>");
|
||||
assert.equal(describe("te"), "Search for te");
|
||||
assert.equal(describe(`channel:${office_id} topic:team`), "Channel office > team");
|
||||
|
||||
suggestions = get_suggestions(`topic:staplers channel:${office_id}`);
|
||||
@@ -818,6 +818,18 @@ test("whitespace_glitch", ({override, mock_template}) => {
|
||||
assert.deepEqual(suggestions.strings, expected);
|
||||
});
|
||||
|
||||
test("xss_channel_name", () => {
|
||||
const stream_id = new_stream_id();
|
||||
stream_data.add_sub({stream_id, name: "<em> Italics </em>", subscribed: true});
|
||||
|
||||
const query = "channel:ita";
|
||||
const suggestions = get_suggestions(query);
|
||||
assert.deepEqual(
|
||||
suggestions.lookup_table.get(`channel:${stream_id}`).description_html,
|
||||
"Channel <em> Italics </em>",
|
||||
);
|
||||
});
|
||||
|
||||
test("channel_completion", ({override}) => {
|
||||
const office_stream_id = new_stream_id();
|
||||
stream_data.add_sub({stream_id: office_stream_id, name: "office", subscribed: true});
|
||||
@@ -944,7 +956,7 @@ test("people_suggestions", ({override, mock_template}) => {
|
||||
test_describe("sender:ted@zulip.com", "Sent by");
|
||||
test_describe("dm-including:ted@zulip.com", "Direct messages including");
|
||||
|
||||
let expectedString = "<strong>Te</strong>d Smith";
|
||||
let expectedString = "Ted Smith";
|
||||
|
||||
function test_full_name(q, full_name_html) {
|
||||
return suggestions.lookup_table.get(q).description_html.includes(full_name_html);
|
||||
|
||||
@@ -925,36 +925,6 @@ test("test compare directly for direct message", () => {
|
||||
assert.equal(th.compare_people_for_relevance(zman_item, all_obj_item), -1);
|
||||
});
|
||||
|
||||
test("highlight_with_escaping", () => {
|
||||
function highlight(query, item) {
|
||||
return th.make_query_highlighter(query)(item);
|
||||
}
|
||||
|
||||
let item = "Denmark";
|
||||
let query = "Den";
|
||||
let expected = "<strong>Den</strong>mark";
|
||||
let result = highlight(query, item);
|
||||
assert.equal(result, expected);
|
||||
|
||||
item = "w3IrD_naMe";
|
||||
query = "w3IrD_naMe";
|
||||
expected = "<strong>w3IrD_naMe</strong>";
|
||||
result = highlight(query, item);
|
||||
assert.equal(result, expected);
|
||||
|
||||
item = "development help";
|
||||
query = "development h";
|
||||
expected = "<strong>development h</strong>elp";
|
||||
result = highlight(query, item);
|
||||
assert.equal(result, expected);
|
||||
|
||||
item = "Prefix notprefix prefix";
|
||||
query = "pre";
|
||||
expected = "<strong>Pre</strong>fix notprefix <strong>pre</strong>fix";
|
||||
result = highlight(query, item);
|
||||
assert.equal(result, expected);
|
||||
});
|
||||
|
||||
test("render_person when emails hidden", ({mock_template, override}) => {
|
||||
// Test render_person with regular person, under hidden email visibility case
|
||||
override(realm, "custom_profile_field_types", {
|
||||
|
||||
Reference in New Issue
Block a user