Fix highlights not being applied to plaintext messages (#9126)

* Fix highlights not being applied to plaintext messages

* Add percy test for search result highlighting

* Fix percy target

* Update timeline.spec.ts
This commit is contained in:
Michael Telatynski 2022-08-05 08:08:56 +01:00 committed by GitHub
parent a866005bea
commit e864dbfacf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 41 additions and 29 deletions

View file

@ -57,12 +57,12 @@ const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
});
};
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
const sendEvent = (roomId: string, html = false): Chainable<ISendEventResponse> => {
return cy.sendEvent(
roomId,
null,
"m.room.message" as EventType,
MessageEvent.from("Message").serialize().content,
MessageEvent.from("Message", html ? "<b>Message</b>" : undefined).serialize().content,
);
};
@ -261,6 +261,18 @@ describe("Timeline", () => {
// Make sure "collapse" link button worked
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]");
});
it("should highlight search result words regardless of formatting", () => {
sendEvent(roomId);
sendEvent(roomId, true);
cy.visit("/#/room/" + roomId);
cy.get(".mx_RoomHeader_searchButton").click();
cy.get(".mx_SearchBar_input input").type("Message{enter}");
cy.get(".mx_EventTile:not(.mx_EventTile_contextual)").find(".mx_EventTile_searchHighlight").should("exist");
cy.get(".mx_RoomView_searchResultsPanel").percySnapshotElement("Highlighted search results");
});
});
describe("message sending", () => {

View file

@ -470,40 +470,38 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
}
let strippedBody: string;
let safeBody: string;
let isDisplayedWithHtml: boolean;
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
let safeBody: string; // safe, sanitised HTML, preferred over `strippedBody` which is fully plaintext
try {
if (highlights && highlights.length > 0) {
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
const safeHighlights = highlights
// sanitizeHtml can hang if an unclosed HTML tag is thrown at it
// A search for `<foo` will make the browser crash
// an alternative would be to escape HTML special characters
// but that would bring no additional benefit as the highlighter
// does not work with those special chars
.filter((highlight: string): boolean => !highlight.includes("<"))
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
// sanitizeHtml can hang if an unclosed HTML tag is thrown at it
// A search for `<foo` will make the browser crash an alternative would be to escape HTML special characters
// but that would bring no additional benefit as the highlighter does not work with those special chars
const safeHighlights = highlights
?.filter((highlight: string): boolean => !highlight.includes("<"))
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
let formattedBody = typeof content.formatted_body === 'string' ? content.formatted_body : null;
const plainBody = typeof content.body === 'string' ? content.body : "";
if (opts.stripReplyFallback && formattedBody) formattedBody = stripHTMLReply(formattedBody);
strippedBody = opts.stripReplyFallback ? stripPlainReply(plainBody) : plainBody;
bodyHasEmoji = mightContainEmoji(isFormattedBody ? formattedBody : plainBody);
// Only generate safeBody if the message was sent as org.matrix.custom.html
const highlighter = safeHighlights?.length
? new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink)
: null;
if (isFormattedBody) {
isDisplayedWithHtml = true;
if (highlighter) {
// XXX: We sanitize the HTML whilst also highlighting its text nodes, to avoid accidentally trying
// to highlight HTML tags themselves. However, this does mean that we don't highlight textnodes which
// are interrupted by HTML tags (not that we did before) - e.g. foo<span/>bar won't get highlighted
// by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
sanitizeParams.textFilter = function(safeText) {
return highlighter.applyHighlights(safeText, safeHighlights).join('');
};
}
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
const phtml = cheerio.load(safeBody, {
@ -533,12 +531,14 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
if (bodyHasEmoji) {
safeBody = formatEmojis(safeBody, true).join('');
}
} else if (highlighter) {
safeBody = highlighter.applyHighlights(plainBody, safeHighlights).join('');
}
} finally {
delete sanitizeParams.textFilter;
}
const contentBody = isDisplayedWithHtml ? safeBody : strippedBody;
const contentBody = safeBody ?? strippedBody;
if (opts.returnString) {
return contentBody;
}
@ -576,11 +576,11 @@ export function bodyToHtml(content: IContent, highlights: Optional<string[]>, op
});
let emojiBodyElements: JSX.Element[];
if (!isDisplayedWithHtml && bodyHasEmoji) {
if (!safeBody && bodyHasEmoji) {
emojiBodyElements = formatEmojis(strippedBody, false) as JSX.Element[];
}
return isDisplayedWithHtml ?
return safeBody ?
<span
key="body"
ref={opts.ref}