diff --git a/app/assets/javascripts/admin/components/site-text-summary.js b/app/assets/javascripts/admin/components/site-text-summary.js
index 11c6bc45ebb..23ee2d8aa6b 100644
--- a/app/assets/javascripts/admin/components/site-text-summary.js
+++ b/app/assets/javascripts/admin/components/site-text-summary.js
@@ -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();
},
diff --git a/app/assets/javascripts/admin/templates/search-logs-term.hbs b/app/assets/javascripts/admin/templates/search-logs-term.hbs
index 5014fd7b909..54348aea2e1 100644
--- a/app/assets/javascripts/admin/templates/search-logs-term.hbs
+++ b/app/assets/javascripts/admin/templates/search-logs-term.hbs
@@ -31,7 +31,7 @@
diff --git a/app/assets/javascripts/discourse/components/highlight-search.js b/app/assets/javascripts/discourse/components/highlight-search.js
new file mode 100644
index 00000000000..a7077a79c7a
--- /dev/null
+++ b/app/assets/javascripts/discourse/components/highlight-search.js
@@ -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")
+});
diff --git a/app/assets/javascripts/discourse/components/highlight-text.js b/app/assets/javascripts/discourse/components/highlight-text.js
index a98ffdb653b..d97572d2154 100644
--- a/app/assets/javascripts/discourse/components/highlight-text.js
+++ b/app/assets/javascripts/discourse/components/highlight-text.js
@@ -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")
});
diff --git a/app/assets/javascripts/discourse/lib/highlight-html.js b/app/assets/javascripts/discourse/lib/highlight-html.js
new file mode 100644
index 00000000000..a6f093b632d
--- /dev/null
+++ b/app/assets/javascripts/discourse/lib/highlight-html.js
@@ -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();
+ });
+}
diff --git a/app/assets/javascripts/discourse/lib/highlight-text.js b/app/assets/javascripts/discourse/lib/highlight-search.js
similarity index 72%
rename from app/assets/javascripts/discourse/lib/highlight-text.js
rename to app/assets/javascripts/discourse/lib/highlight-search.js
index 6fa7a09faf1..0d9318fb7af 100644
--- a/app/assets/javascripts/discourse/lib/highlight-text.js
+++ b/app/assets/javascripts/discourse/lib/highlight-search.js
@@ -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);
}
}
diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs
index d6cdd922273..12a2bd5b19c 100644
--- a/app/assets/javascripts/discourse/templates/full-page-search.hbs
+++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs
@@ -88,7 +88,7 @@
{{topic-status topic=result.topic disableActions=true showPrivateMessageIcon=true}}
- {{#highlight-text highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-text}}
+ {{#highlight-search highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}}
@@ -112,9 +112,9 @@
{{#if result.blurb}}
- {{#highlight-text highlight=highlightQuery}}
+ {{#highlight-search highlight=highlightQuery}}
{{html-safe result.blurb}}
- {{/highlight-text}}
+ {{/highlight-search}}
{{/if}}
diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js b/app/assets/javascripts/discourse/widgets/post-cooked.js
index 3d55314f08a..3b0ca2423d8 100644
--- a/app/assets/javascripts/discourse/widgets/post-cooked.js
+++ b/app/assets/javascripts/discourse/widgets/post-cooked.js
@@ -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);
})
diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js b/app/assets/javascripts/discourse/widgets/search-menu-results.js
index 636c069f941..6d46819263d 100644
--- a/app/assets/javascripts/discourse/widgets/search-menu-results.js
+++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js
@@ -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);
}
}
diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js
index adc682eba5e..64119c49403 100644
--- a/app/assets/javascripts/vendor.js
+++ b/app/assets/javascripts/vendor.js
@@ -28,5 +28,4 @@
//= require jquery.autoellipsis-1.0.10
//= require virtual-dom
//= require virtual-dom-amd
-//= require highlight.js
//= require intersection-observer
diff --git a/test/javascripts/acceptance/search-test.js b/test/javascripts/acceptance/search-test.js
index 1b930c7905f..62d4323867f 100644
--- a/test/javascripts/acceptance/search-test.js
+++ b/test/javascripts/acceptance/search-test.js
@@ -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"
);
diff --git a/test/javascripts/lib/highlight-search-test.js.es6 b/test/javascripts/lib/highlight-search-test.js.es6
new file mode 100644
index 00000000000..fcbc9f4fa42
--- /dev/null
+++ b/test/javascripts/lib/highlight-search-test.js.es6
@@ -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(
+ `
+
This is some text to highlight
+ `
+ );
+
+ 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(
+ `
+
This is some தமிழ் & русский text to highlight
+ `
+ );
+
+ highlightSearch(fixture()[0], "தமிழ் & русский");
+
+ const terms = [];
+
+ fixture(`.${CLASS_NAME}`).each((_, elem) => {
+ terms.push(elem.textContent);
+ });
+
+ assert.equal(
+ terms.join(" "),
+ "தமிழ் & русский",
+ "it should highlight the terms correctly"
+ );
+});
diff --git a/test/javascripts/lib/highlight-text-test.js b/test/javascripts/lib/highlight-text-test.js
deleted file mode 100644
index d222b8a7539..00000000000
--- a/test/javascripts/lib/highlight-text-test.js
+++ /dev/null
@@ -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(
- `
-
This is some text to highlight
- `
- );
-
- 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"
- );
-});
diff --git a/vendor/assets/javascripts/highlight.js b/vendor/assets/javascripts/highlight.js
deleted file mode 100644
index c13dd7ff9f8..00000000000
--- a/vendor/assets/javascripts/highlight.js
+++ /dev/null
@@ -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
(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
- * $('#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);
- });
-};