FIX: remove word boundary regex (\b) for search result highlights. (#9338)

This commit is contained in:
Vinoth Kannan
2020-04-15 11:11:00 +05:30
committed by GitHub
parent c670a34013
commit 4a2c4232c5
14 changed files with 186 additions and 171 deletions

View File

@ -1,5 +1,6 @@
import Component from "@ember/component"; import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators"; import { on } from "discourse-common/utils/decorators";
import highlightHTML from "discourse/lib/highlight-html";
export default Component.extend({ export default Component.extend({
classNames: ["site-text"], classNames: ["site-text"],
@ -10,11 +11,13 @@ export default Component.extend({
const term = this._searchTerm(); const term = this._searchTerm();
if (term) { if (term) {
$( highlightHTML(
this.element.querySelector(".site-text-id, .site-text-value") this.element.querySelector(".site-text-id, .site-text-value"),
).highlight(term, { term,
{
className: "text-highlight" className: "text-highlight"
}); }
);
} }
$(this.element.querySelector(".site-text-value")).ellipsis(); $(this.element.querySelector(".site-text-value")).ellipsis();
}, },

View File

@ -31,7 +31,7 @@
<div class="fps-topic"> <div class="fps-topic">
<div class="topic"> <div class="topic">
<a href={{result.url}} class="search-link"> <a href={{result.url}} class="search-link">
{{topic-status topic=result.topic disableActions=true}}<span class="topic-title">{{#highlight-text highlight=term}}{{html-safe result.topic.fancyTitle}}{{/highlight-text}}</span> {{topic-status topic=result.topic disableActions=true}}<span class="topic-title">{{#highlight-search highlight=term}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}}</span>
</a> </a>
<div class="search-category"> <div class="search-category">
@ -54,9 +54,9 @@
</span> </span>
{{#if result.blurb}} {{#if result.blurb}}
{{#highlight-text highlight=term}} {{#highlight-search highlight=term}}
{{html-safe result.blurb}} {{html-safe result.blurb}}
{{/highlight-text}} {{/highlight-search}}
{{/if}} {{/if}}
</div> </div>
</div> </div>

View File

@ -0,0 +1,13 @@
import Component from "@ember/component";
import highlightSearch from "discourse/lib/highlight-search";
export default Component.extend({
tagName: "span",
_highlightOnInsert: function() {
const term = this.highlight;
highlightSearch(this.element, term);
}
.observes("highlight")
.on("didInsertElement")
});

View File

@ -1,13 +1,11 @@
import Component from "@ember/component"; import highlightSearch from "discourse/components/highlight-search";
import highlightText from "discourse/lib/highlight-text"; import deprecated from "discourse-common/lib/deprecated";
export default Component.extend({ export default highlightSearch.extend({
tagName: "span", init() {
this._super(...arguments);
_highlightOnInsert: function() { deprecated(
const term = this.highlight; "`highlight-text` component is deprecated, use the `highlight-search` instead."
highlightText($(this.element), term); );
} }
.observes("highlight")
.on("didInsertElement")
}); });

View File

@ -0,0 +1,84 @@
import { makeArray } from "discourse-common/lib/helpers";
function highlight(node, pattern, nodeName, className) {
if (
![Node.ELEMENT_NODE, Node.TEXT_NODE].includes(node.nodeType) ||
["SCRIPT", "STYLE"].includes(node.tagName) ||
(node.tagName === nodeName && node.className === className)
) {
return 0;
}
if (node.nodeType === Node.ELEMENT_NODE && node.childNodes) {
for (let i = 0; i < node.childNodes.length; i++) {
i += highlight(node.childNodes[i], pattern, nodeName, className);
}
return 0;
}
if (node.nodeType === Node.TEXT_NODE) {
const match = node.data.match(pattern);
if (!match) {
return 0;
}
const element = document.createElement(nodeName);
element.className = className;
element.innerText = match[0];
const matchNode = node.splitText(match.index);
matchNode.splitText(match[0].length);
matchNode.parentNode.replaceChild(element, matchNode);
return 1;
}
return 0;
}
export default function(node, words, opts = {}) {
let settings = {
nodeName: "span",
className: "highlighted",
matchCase: false
};
settings = Object.assign({}, settings, opts);
words = makeArray(words)
.filter(Boolean)
.map(word => word.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"));
if (!words.length) return node;
const pattern = `(${words.join(" ")})`;
let flag;
if (!settings.matchCase) {
flag = "i";
}
highlight(
node,
new RegExp(pattern, flag),
settings.nodeName.toUpperCase(),
settings.className
);
return node;
}
export function unhighlightHTML(opts = {}) {
let settings = {
nodeName: "span",
className: "highlighted"
};
settings = Object.assign({}, settings, opts);
document
.querySelectorAll(`${settings.nodeName}.${settings.className}`)
.forEach(element => {
const parentNode = element.parentNode;
parentNode.replaceChild(element.firstChild, element);
parentNode.normalize();
});
}

View File

@ -1,8 +1,9 @@
import { PHRASE_MATCH_REGEXP_PATTERN } from "discourse/lib/concerns/search-constants"; import { PHRASE_MATCH_REGEXP_PATTERN } from "discourse/lib/concerns/search-constants";
import highlightHTML from "discourse/lib/highlight-html";
export const CLASS_NAME = "search-highlight"; export const CLASS_NAME = "search-highlight";
export default function($elem, term, opts = {}) { export default function(elem, term, opts = {}) {
if (!_.isEmpty(term)) { if (!_.isEmpty(term)) {
// special case ignore "l" which is used for magic sorting // special case ignore "l" which is used for magic sorting
let words = _.reject( let words = _.reject(
@ -11,8 +12,8 @@ export default function($elem, term, opts = {}) {
); );
words = words.map(w => w.replace(/^"(.*)"$/, "$1")); words = words.map(w => w.replace(/^"(.*)"$/, "$1"));
const highlightOpts = { wordsOnly: true }; const highlightOpts = {};
if (!opts.defaultClassName) highlightOpts.className = CLASS_NAME; if (!opts.defaultClassName) highlightOpts.className = CLASS_NAME;
$elem.highlight(words, highlightOpts); highlightHTML(elem, words, highlightOpts);
} }
} }

View File

@ -88,7 +88,7 @@
<a href={{result.url}} {{action "logClick" result.topic_id}} class="search-link"> <a href={{result.url}} {{action "logClick" result.topic_id}} class="search-link">
{{topic-status topic=result.topic disableActions=true showPrivateMessageIcon=true}} {{topic-status topic=result.topic disableActions=true showPrivateMessageIcon=true}}
<span class="topic-title">{{#highlight-text highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-text}}</span> <span class="topic-title">{{#highlight-search highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}}</span>
</a> </a>
<div class="search-category"> <div class="search-category">
@ -112,9 +112,9 @@
</span> </span>
{{#if result.blurb}} {{#if result.blurb}}
{{#highlight-text highlight=highlightQuery}} {{#highlight-search highlight=highlightQuery}}
{{html-safe result.blurb}} {{html-safe result.blurb}}
{{/highlight-text}} {{/highlight-search}}
{{/if}} {{/if}}
</div> </div>

View File

@ -2,7 +2,11 @@ import { iconHTML } from "discourse-common/lib/icon-library";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { isValidLink } from "discourse/lib/click-track"; import { isValidLink } from "discourse/lib/click-track";
import { number } from "discourse/lib/formatter"; import { number } from "discourse/lib/formatter";
import highlightText from "discourse/lib/highlight-text"; import highlightSearch from "discourse/lib/highlight-search";
import {
default as highlightHTML,
unhighlightHTML
} from "discourse/lib/highlight-html";
let _decorators = []; let _decorators = [];
@ -48,17 +52,18 @@ export default class PostCooked {
} }
_applySearchHighlight($html) { _applySearchHighlight($html) {
const html = $html[0];
const highlight = this.attrs.highlightTerm; const highlight = this.attrs.highlightTerm;
if (highlight && highlight.length > 2) { if (highlight && highlight.length > 2) {
if (this._highlighted) { if (this._highlighted) {
$html.unhighlight(); unhighlightHTML(html);
} }
highlightText($html, highlight, { defaultClassName: true }); highlightSearch(html, highlight, { defaultClassName: true });
this._highlighted = true; this._highlighted = true;
} else if (this._highlighted) { } else if (this._highlighted) {
$html.unhighlight(); unhighlightHTML(html);
this._highlighted = false; this._highlighted = false;
} }
} }
@ -175,10 +180,8 @@ export default class PostCooked {
div.html(result.cooked); div.html(result.cooked);
_decorators.forEach(cb => cb(div, this.decoratorHelper)); _decorators.forEach(cb => cb(div, this.decoratorHelper));
div.highlight(originalText, { highlightHTML(div[0], originalText, {
caseSensitive: true, matchCase: true
element: "span",
className: "highlighted"
}); });
$blockQuote.showHtml(div, "fast", finished); $blockQuote.showHtml(div, "fast", finished);
}) })

View File

@ -3,7 +3,7 @@ import { dateNode } from "discourse/helpers/node";
import RawHtml from "discourse/widgets/raw-html"; import RawHtml from "discourse/widgets/raw-html";
import { createWidget } from "discourse/widgets/widget"; import { createWidget } from "discourse/widgets/widget";
import { h } from "virtual-dom"; import { h } from "virtual-dom";
import highlightText from "discourse/lib/highlight-text"; import highlightSearch from "discourse/lib/highlight-search";
import { escapeExpression, formatUsername } from "discourse/lib/utilities"; import { escapeExpression, formatUsername } from "discourse/lib/utilities";
import { iconNode } from "discourse-common/lib/icon-library"; import { iconNode } from "discourse-common/lib/icon-library";
import renderTag from "discourse/lib/render-tag"; import renderTag from "discourse/lib/render-tag";
@ -15,7 +15,7 @@ class Highlighted extends RawHtml {
} }
decorate($html) { decorate($html) {
highlightText($html, this.term); highlightSearch($html[0], this.term);
} }
} }

View File

@ -28,5 +28,4 @@
//= require jquery.autoellipsis-1.0.10 //= require jquery.autoellipsis-1.0.10
//= require virtual-dom //= require virtual-dom
//= require virtual-dom-amd //= require virtual-dom-amd
//= require highlight.js
//= require intersection-observer //= require intersection-observer

View File

@ -94,13 +94,13 @@ QUnit.test("Search with context", async assert => {
const highlighted = []; const highlighted = [];
find("#post_7 span.highlight-strong").map((_, span) => { find("#post_7 span.highlighted").map((_, span) => {
highlighted.push(span.innerText); highlighted.push(span.innerText);
}); });
assert.deepEqual( assert.deepEqual(
highlighted, highlighted,
["a", "a", "proper", "a"], ["a proper"],
"it should highlight the post with the search terms correctly" "it should highlight the post with the search terms correctly"
); );

View File

@ -0,0 +1,48 @@
import highlightSearch, { CLASS_NAME } from "discourse/lib/highlight-search";
import { fixture } from "helpers/qunit-helpers";
QUnit.module("lib:highlight-search");
QUnit.test("highlighting text", assert => {
fixture().html(
`
<p>This is some text to highlight</p>
`
);
highlightSearch(fixture()[0], "some text");
const terms = [];
fixture(`.${CLASS_NAME}`).each((_, elem) => {
terms.push(elem.textContent);
});
assert.equal(
terms.join(" "),
"some text",
"it should highlight the terms correctly"
);
});
QUnit.test("highlighting unicode text", assert => {
fixture().html(
`
<p>This is some தமிழ் & русский text to highlight</p>
`
);
highlightSearch(fixture()[0], "தமிழ் & русский");
const terms = [];
fixture(`.${CLASS_NAME}`).each((_, elem) => {
terms.push(elem.textContent);
});
assert.equal(
terms.join(" "),
"தமிழ் & русский",
"it should highlight the terms correctly"
);
});

View File

@ -1,26 +0,0 @@
import highlightText, { CLASS_NAME } from "discourse/lib/highlight-text";
import { fixture } from "helpers/qunit-helpers";
QUnit.module("lib:highlight-text");
QUnit.test("highlighting text", assert => {
fixture().html(
`
<p>This is some text to highlight</p>
`
);
highlightText(fixture(), "some text");
const terms = [];
fixture(`.${CLASS_NAME}`).each((_, elem) => {
terms.push(elem.textContent);
});
assert.equal(
terms.join(" "),
"some text",
"it should highlight the terms correctly"
);
});

View File

@ -1,108 +0,0 @@
// forked cause we may want to amend the logic a bit
/*
* jQuery Highlight plugin
*
* Based on highlight v3 by Johann Burkard
* http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html
*
* Code a little bit refactored and cleaned (in my humble opinion).
* Most important changes:
* - has an option to highlight only entire words (wordsOnly - false by default),
* - has an option to be case sensitive (caseSensitive - false by default)
* - highlight element tag and class names can be specified in options
*
* Usage:
* // wrap every occurrance of text 'lorem' in content
* // with <span class='highlight'> (default options)
* $('#content').highlight('lorem');
*
* // search for and highlight more terms at once
* // so you can save some time on traversing DOM
* $('#content').highlight(['lorem', 'ipsum']);
* $('#content').highlight('lorem ipsum');
*
* // search only for entire word 'lorem'
* $('#content').highlight('lorem', { wordsOnly: true });
*
* // don't ignore case during search of term 'lorem'
* $('#content').highlight('lorem', { caseSensitive: true });
*
* // wrap every occurrance of term 'ipsum' in content
* // with <em class='important'>
* $('#content').highlight('ipsum', { element: 'em', className: 'important' });
*
* // remove default highlight
* $('#content').unhighlight();
*
* // remove custom highlight
* $('#content').unhighlight({ element: 'em', className: 'important' });
*
*
* Copyright (c) 2009 Bartek Szopka
*
* Licensed under MIT license.
*
*/
jQuery.extend({
highlight: function (node, re, nodeName, className) {
if (node.nodeType === 3) {
var match = node.data.match(re);
if (match) {
var highlight = document.createElement(nodeName || 'span');
highlight.className = className || 'highlight';
var wordNode = node.splitText(match.index);
wordNode.splitText(match[0].length);
var wordClone = wordNode.cloneNode(true);
highlight.appendChild(wordClone);
wordNode.parentNode.replaceChild(highlight, wordNode);
return 1; //skip added node in parent
}
} else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children
!/(script|style)/i.test(node.tagName) && // ignore script and style nodes
!(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted
for (var i = 0; i < node.childNodes.length; i++) {
i += jQuery.highlight(node.childNodes[i], re, nodeName, className);
}
}
return 0;
}
});
jQuery.fn.unhighlight = function (options) {
var settings = { className: 'highlight-strong', element: 'span' };
jQuery.extend(settings, options);
return this.find(settings.element + "." + settings.className).each(function () {
var parent = this.parentNode;
parent.replaceChild(this.firstChild, this);
parent.normalize();
}).end();
};
jQuery.fn.highlight = function (words, options) {
var settings = { className: 'highlight-strong', element: 'span', caseSensitive: false, wordsOnly: false };
jQuery.extend(settings, options);
if (words.constructor === String) {
words = [words];
}
words = jQuery.grep(words, function(word){
return word !== '';
});
words = jQuery.map(words, function(word) {
return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
});
if (words.length === 0) { return this; }
var flag = settings.caseSensitive ? "" : "i";
var pattern = "(" + words.join("|") + ")";
if (settings.wordsOnly) {
pattern = "\\b" + pattern + "\\b";
}
var re = new RegExp(pattern, flag);
return this.each(function () {
jQuery.highlight(this, re, settings.element, settings.className);
});
};