diff --git a/app/assets/javascripts/admin/components/site-text-summary.js b/app/assets/javascripts/admin/components/site-text-summary.js index 23ee2d8aa6b..11c6bc45ebb 100644 --- a/app/assets/javascripts/admin/components/site-text-summary.js +++ b/app/assets/javascripts/admin/components/site-text-summary.js @@ -1,6 +1,5 @@ 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"], @@ -11,13 +10,11 @@ export default Component.extend({ const term = this._searchTerm(); if (term) { - highlightHTML( - this.element.querySelector(".site-text-id, .site-text-value"), - term, - { - className: "text-highlight" - } - ); + $( + this.element.querySelector(".site-text-id, .site-text-value") + ).highlight(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 ac55d129e02..27f61f2940e 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 @@
- {{topic-status topic=result.topic disableActions=true}}{{#highlight-search highlight=term}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}} + {{topic-status topic=result.topic disableActions=true}}{{#highlight-text highlight=term}}{{html-safe result.topic.fancyTitle}}{{/highlight-text}}
@@ -54,9 +54,9 @@ {{#if result.blurb}} - {{#highlight-search highlight=term}} + {{#highlight-text highlight=term}} {{html-safe result.blurb}} - {{/highlight-search}} + {{/highlight-text}} {{/if}}
diff --git a/app/assets/javascripts/discourse/components/highlight-search.js b/app/assets/javascripts/discourse/components/highlight-text.js similarity index 68% rename from app/assets/javascripts/discourse/components/highlight-search.js rename to app/assets/javascripts/discourse/components/highlight-text.js index 322336699ff..a98ffdb653b 100644 --- a/app/assets/javascripts/discourse/components/highlight-search.js +++ b/app/assets/javascripts/discourse/components/highlight-text.js @@ -1,12 +1,12 @@ import Component from "@ember/component"; -import highlightSearch from "discourse/lib/highlight-search"; +import highlightText from "discourse/lib/highlight-text"; export default Component.extend({ tagName: "span", _highlightOnInsert: function() { const term = this.highlight; - highlightSearch($(this.element), term); + highlightText($(this.element), term); } .observes("highlight") .on("didInsertElement") diff --git a/app/assets/javascripts/discourse/lib/highlight-html.js b/app/assets/javascripts/discourse/lib/highlight-html.js deleted file mode 100644 index 94a1cd863c9..00000000000 --- a/app/assets/javascripts/discourse/lib/highlight-html.js +++ /dev/null @@ -1,93 +0,0 @@ -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", - wholeWord: false, - matchCase: false - }; - - Object.assign(settings, opts); - words = typeof words === "string" ? [words] : words; - words = words - .filter(Boolean) - .map(word => word.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")); - - if (!words.length) return node; - - let pattern = `(${words.join("|")})`; - let flag; - - if (settings.wholeWord) { - const hasUnicode = words.some(word => { - return !word.match(new RegExp(`\b${word}\b`)); - }); - pattern = hasUnicode - ? `(?<=[\\s,.:;"']|^)${pattern}(?=[\\s,.:;"']|$)` - : `\b${pattern}\b`; - } - - 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" - }; - - Object.assign(settings, opts); - - document - .querySelectorAll(`${settings.nodeName}.${settings.className}`) - .forEach(e => { - const parentNode = e.parentNode; - parentNode.replaceChild(e.firstChild, e); - parentNode.normalize(); - }); -} diff --git a/app/assets/javascripts/discourse/lib/highlight-search.js b/app/assets/javascripts/discourse/lib/highlight-text.js similarity index 71% rename from app/assets/javascripts/discourse/lib/highlight-search.js rename to app/assets/javascripts/discourse/lib/highlight-text.js index 870e9c3693a..6fa7a09faf1 100644 --- a/app/assets/javascripts/discourse/lib/highlight-search.js +++ b/app/assets/javascripts/discourse/lib/highlight-text.js @@ -1,5 +1,4 @@ import { PHRASE_MATCH_REGEXP_PATTERN } from "discourse/lib/concerns/search-constants"; -import highlightHTML from "discourse/lib/highlight-html"; export const CLASS_NAME = "search-highlight"; @@ -12,10 +11,8 @@ export default function($elem, term, opts = {}) { ); words = words.map(w => w.replace(/^"(.*)"$/, "$1")); - const highlightOpts = { wholeWord: true }; + const highlightOpts = { wordsOnly: true }; if (!opts.defaultClassName) highlightOpts.className = CLASS_NAME; - for (let i = 0; i <= $elem.length; i++) { - highlightHTML($elem[0], words, highlightOpts); - } + $elem.highlight(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 2ed17fa1bd5..9394f354864 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-search highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-search}} + {{#highlight-text highlight=q}}{{html-safe result.topic.fancyTitle}}{{/highlight-text}}
@@ -112,9 +112,9 @@ {{#if result.blurb}} - {{#highlight-search highlight=highlightQuery}} + {{#highlight-text highlight=highlightQuery}} {{html-safe result.blurb}} - {{/highlight-search}} + {{/highlight-text}} {{/if}}
diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js b/app/assets/javascripts/discourse/widgets/post-cooked.js index c16820de447..3d55314f08a 100644 --- a/app/assets/javascripts/discourse/widgets/post-cooked.js +++ b/app/assets/javascripts/discourse/widgets/post-cooked.js @@ -2,11 +2,7 @@ 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 highlightSearch from "discourse/lib/highlight-search"; -import { - default as highlightHTML, - unhighlightHTML -} from "discourse/lib/highlight-html"; +import highlightText from "discourse/lib/highlight-text"; let _decorators = []; @@ -56,13 +52,13 @@ export default class PostCooked { if (highlight && highlight.length > 2) { if (this._highlighted) { - unhighlightHTML($html[0]); + $html.unhighlight(); } - highlightSearch($html, highlight, { defaultClassName: true }); + highlightText($html, highlight, { defaultClassName: true }); this._highlighted = true; } else if (this._highlighted) { - unhighlightHTML($html[0]); + $html.unhighlight(); this._highlighted = false; } } @@ -179,8 +175,10 @@ export default class PostCooked { div.html(result.cooked); _decorators.forEach(cb => cb(div, this.decoratorHelper)); - highlightHTML(div[0], originalText, { - matchCase: true + div.highlight(originalText, { + caseSensitive: true, + element: "span", + className: "highlighted" }); $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 e85eefcd8e4..636c069f941 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 highlightSearch from "discourse/lib/highlight-search"; +import highlightText from "discourse/lib/highlight-text"; 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) { - highlightSearch($html, this.term); + highlightText($html, this.term); } } diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 39626bfd195..3b36c27ea1e 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -29,4 +29,5 @@ //= require jquery.autoellipsis-1.0.10 //= require virtual-dom //= require virtual-dom-amd +//= require highlight.js //= require intersection-observer diff --git a/db/migrate/20200323155812_add_post_thumbnails.rb b/db/migrate/20200323155812_add_post_thumbnails.rb new file mode 100644 index 00000000000..01ea53c1b07 --- /dev/null +++ b/db/migrate/20200323155812_add_post_thumbnails.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddPostThumbnails < ActiveRecord::Migration[6.0] + def change + add_table :post_thumbnails do |t| + t.references :posts, foreign_key: { to_table: :posts }, null: false + end + + end +end diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index c9567ad8b59..1b930c7905f 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -94,7 +94,7 @@ QUnit.test("Search with context", async assert => { const highlighted = []; - find("#post_7 span.highlighted").map((_, span) => { + find("#post_7 span.highlight-strong").map((_, span) => { highlighted.push(span.innerText); }); diff --git a/test/javascripts/lib/highlight-search-test.js.es6 b/test/javascripts/lib/highlight-search-test.js.es6 deleted file mode 100644 index 2a2524f7653..00000000000 --- a/test/javascripts/lib/highlight-search-test.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -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(), "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 தமிழ் and русский text to highlight

- ` - ); - - highlightSearch(fixture(), "தமிழ் русский"); - - 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.es6 b/test/javascripts/lib/highlight-text-test.js.es6 new file mode 100644 index 00000000000..d222b8a7539 --- /dev/null +++ b/test/javascripts/lib/highlight-text-test.js.es6 @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000000..c13dd7ff9f8 --- /dev/null +++ b/vendor/assets/javascripts/highlight.js @@ -0,0 +1,108 @@ +// 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); + }); +};