diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js
index 87f5bd2cca..0b7f17b2b2 100644
--- a/src/HtmlUtils.js
+++ b/src/HtmlUtils.js
@@ -17,6 +17,7 @@ limitations under the License.
'use strict';
var React = require('react');
+var ReactDOMServer = require('react-dom/server')
var sanitizeHtml = require('sanitize-html');
var highlight = require('highlight.js');
@@ -57,24 +58,17 @@ class Highlighter {
this._key = 0;
}
- applyHighlights(safeSnippet, highlights) {
+ applyHighlights(safeSnippet, safeHighlights) {
var lastOffset = 0;
var offset;
var nodes = [];
- // XXX: when highlighting HTML, synapse performs the search on the plaintext body,
- // but we're attempting to apply the highlights here to the HTML body. This is
- // never going to end well - we really should be hooking into the sanitzer HTML
- // parser to only attempt to highlight text nodes to avoid corrupting tags.
- // If and when this happens, we'll probably have to split this method in two between
- // HTML and plain-text highlighting.
-
- var safeHighlight = this.html ? sanitizeHtml(highlights[0], sanitizeHtmlParams) : highlights[0];
+ var safeHighlight = safeHighlights[0];
while ((offset = safeSnippet.toLowerCase().indexOf(safeHighlight.toLowerCase(), lastOffset)) >= 0) {
// handle preamble
if (offset > lastOffset) {
var subSnippet = safeSnippet.substring(lastOffset, offset);
- nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
+ nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
}
// do highlight
@@ -86,15 +80,15 @@ class Highlighter {
// handle postamble
if (lastOffset != safeSnippet.length) {
var subSnippet = safeSnippet.substring(lastOffset, undefined);
- nodes = nodes.concat(this._applySubHighlights(subSnippet, highlights));
+ nodes = nodes.concat(this._applySubHighlights(subSnippet, safeHighlights));
}
return nodes;
}
- _applySubHighlights(safeSnippet, highlights) {
- if (highlights[1]) {
+ _applySubHighlights(safeSnippet, safeHighlights) {
+ if (safeHighlights[1]) {
// recurse into this range to check for the next set of highlight matches
- return this.applyHighlights(safeSnippet, highlights.slice(1));
+ return this.applyHighlights(safeSnippet, safeHighlights.slice(1));
}
else {
// no more highlights to be found, just return the unhighlighted string
@@ -132,7 +126,7 @@ module.exports = {
*
* content: 'content' of the MatrixEvent
*
- * highlights: optional list of words to highlight
+ * highlights: optional list of words to highlight, ordered by longest word first
*
* opts.onHighlightClick: optional callback function to be called when a
* highlighted word is clicked
@@ -144,26 +138,42 @@ module.exports = {
var safeBody;
if (isHtml) {
- safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ // 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. foobar won't get highlighted
+ // by an attempt to search for 'foobar'. Then again, the search query probably wouldn't work either
+ try {
+ if (highlights && highlights.length > 0) {
+ var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
+ var safeHighlights = highlights.map(function(highlight) {
+ return sanitizeHtml(highlight, sanitizeHtmlParams);
+ });
+ // XXX: hacky bodge to temporarily apply a textFilter to the sanitizeHtmlParams structure.
+ sanitizeHtmlParams.textFilter = function(safeText) {
+ return highlighter.applyHighlights(safeText, safeHighlights).map(function(span) {
+ // XXX: rather clunky conversion from the react nodes returned by applyHighlights
+ // (which need to be nodes for the non-html highlighting case), to convert them
+ // back into raw HTML given that's what sanitize-html works in terms of.
+ return ReactDOMServer.renderToString(span);
+ }).join('');
+ };
+ }
+ safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams);
+ }
+ finally {
+ delete sanitizeHtmlParams.textFilter;
+ }
+ return ;
} else {
safeBody = content.body;
- }
-
- var body;
- if (highlights && highlights.length > 0) {
- var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
- body = highlighter.applyHighlights(safeBody, highlights);
- }
- else {
- if (isHtml) {
- body = ;
+ if (highlights && highlights.length > 0) {
+ var highlighter = new Highlighter(isHtml, "mx_EventTile_searchHighlight", opts.onHighlightClick);
+ return highlighter.applyHighlights(safeBody, highlights);
}
else {
- body = safeBody;
+ return safeBody;
}
}
-
- return body;
},
highlightDom: function(element) {