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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 171 deletions

View File

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

View File

@ -31,7 +31,7 @@
<div class="fps-topic">
<div class="topic">
<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>
<div class="search-category">
@ -54,9 +54,9 @@
</span>
{{#if result.blurb}}
{{#highlight-text highlight=term}}
{{#highlight-search highlight=term}}
{{html-safe result.blurb}}
{{/highlight-text}}
{{/highlight-search}}
{{/if}}
</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 highlightText from "discourse/lib/highlight-text";
import highlightSearch from "discourse/components/highlight-search";
import deprecated from "discourse-common/lib/deprecated";
export default Component.extend({
tagName: "span",
_highlightOnInsert: function() {
const term = this.highlight;
highlightText($(this.element), term);
export default highlightSearch.extend({
init() {
this._super(...arguments);
deprecated(
"`highlight-text` component is deprecated, use the `highlight-search` instead."
);
}
.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 highlightHTML from "discourse/lib/highlight-html";
export const CLASS_NAME = "search-highlight";
export default function($elem, term, opts = {}) {
export default function(elem, term, opts = {}) {
if (!_.isEmpty(term)) {
// special case ignore "l" which is used for magic sorting
let words = _.reject(
@ -11,8 +12,8 @@ export default function($elem, term, opts = {}) {
);
words = words.map(w => w.replace(/^"(.*)"$/, "$1"));
const highlightOpts = { wordsOnly: true };
const highlightOpts = {};
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">
{{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>
<div class="search-category">
@ -112,9 +112,9 @@
</span>
{{#if result.blurb}}
{{#highlight-text highlight=highlightQuery}}
{{#highlight-search highlight=highlightQuery}}
{{html-safe result.blurb}}
{{/highlight-text}}
{{/highlight-search}}
{{/if}}
</div>

View File

@ -2,7 +2,11 @@ import { iconHTML } from "discourse-common/lib/icon-library";
import { ajax } from "discourse/lib/ajax";
import { isValidLink } from "discourse/lib/click-track";
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 = [];
@ -48,17 +52,18 @@ export default class PostCooked {
}
_applySearchHighlight($html) {
const html = $html[0];
const highlight = this.attrs.highlightTerm;
if (highlight && highlight.length > 2) {
if (this._highlighted) {
$html.unhighlight();
unhighlightHTML(html);
}
highlightText($html, highlight, { defaultClassName: true });
highlightSearch(html, highlight, { defaultClassName: true });
this._highlighted = true;
} else if (this._highlighted) {
$html.unhighlight();
unhighlightHTML(html);
this._highlighted = false;
}
}
@ -175,10 +180,8 @@ export default class PostCooked {
div.html(result.cooked);
_decorators.forEach(cb => cb(div, this.decoratorHelper));
div.highlight(originalText, {
caseSensitive: true,
element: "span",
className: "highlighted"
highlightHTML(div[0], originalText, {
matchCase: true
});
$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 { createWidget } from "discourse/widgets/widget";
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 { iconNode } from "discourse-common/lib/icon-library";
import renderTag from "discourse/lib/render-tag";
@ -15,7 +15,7 @@ class Highlighted extends RawHtml {
}
decorate($html) {
highlightText($html, this.term);
highlightSearch($html[0], this.term);
}
}

View File

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

View File

@ -94,13 +94,13 @@ QUnit.test("Search with context", async assert => {
const highlighted = [];
find("#post_7 span.highlight-strong").map((_, span) => {
find("#post_7 span.highlighted").map((_, span) => {
highlighted.push(span.innerText);
});
assert.deepEqual(
highlighted,
["a", "a", "proper", "a"],
["a proper"],
"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);
});
};