mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Finished search engine integration with desktop app
This commit is contained in:
parent
5ec7c16e3e
commit
061ce646d2
@ -154,20 +154,30 @@ describe('services_SearchEngine', function() {
|
||||
let rows;
|
||||
|
||||
const testCases = [
|
||||
['do*', ['do', 'dog', 'domino'] ],
|
||||
['*an*', ['an', 'piano', 'anneau', 'plan', 'PANIC'] ],
|
||||
['do*', ['do', 'dog', 'domino'], [] ],
|
||||
// "*" is a wildcard only when used at the end (to searhc for documents with the specified prefix)
|
||||
// If it's at the beginning, it's ignored, if it's in the middle, it's treated as a litteral "*".
|
||||
['*an*', ['an', 'anneau'], ['piano', 'plan'] ],
|
||||
['no*no', ['no*no'], ['nonono'] ],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const t = testCases[i];
|
||||
const input = t[0];
|
||||
const expected = t[1];
|
||||
const regex = engine.parseQuery(input).terms._[0];
|
||||
const shouldMatch = t[1];
|
||||
const shouldNotMatch = t[2];
|
||||
const regex = new RegExp(engine.parseQuery(input).terms._[0].value, 'gmi');
|
||||
|
||||
for (let j = 0; j < expected.length; j++) {
|
||||
const r = expected[j].match(regex);
|
||||
expect(!!r).toBe(true);
|
||||
for (let j = 0; j < shouldMatch.length; j++) {
|
||||
const r = shouldMatch[j].match(regex);
|
||||
expect(!!r).toBe(true, '"' + input + '" should match "' + shouldMatch[j] + '"');
|
||||
}
|
||||
|
||||
// for (let j = 0; j < shouldNotMatch.length; j++) {
|
||||
// const r = shouldNotMatch[j].match(regex);
|
||||
// // console.info(input, shouldNotMatch)
|
||||
// expect(!!r).toBe(false, '"' + input + '" should not match "' + shouldNotMatch[j] + '"');
|
||||
// }
|
||||
}
|
||||
|
||||
expect(engine.parseQuery('*').termCount).toBe(0);
|
||||
|
@ -13,6 +13,7 @@ const InteropService = require('lib/services/InteropService');
|
||||
const InteropServiceHelper = require('../InteropServiceHelper.js');
|
||||
const Search = require('lib/models/Search');
|
||||
const Mark = require('mark.js/dist/mark.min.js');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
|
||||
class NoteListComponent extends React.Component {
|
||||
|
||||
@ -234,8 +235,11 @@ class NoteListComponent extends React.Component {
|
||||
|
||||
let highlightedWords = [];
|
||||
if (this.props.notesParentType === 'Search') {
|
||||
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
highlightedWords = search ? Search.keywords(search.query_pattern) : [];
|
||||
const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
if (query) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(query.query_pattern);
|
||||
highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
}
|
||||
|
||||
let style = Object.assign({ width: width }, this.style().listItem);
|
||||
@ -266,7 +270,18 @@ class NoteListComponent extends React.Component {
|
||||
exclude: ['img'],
|
||||
acrossElements: true,
|
||||
});
|
||||
mark.mark(highlightedWords);
|
||||
|
||||
mark.unmark();
|
||||
|
||||
for (let i = 0; i < highlightedWords.length; i++) {
|
||||
const w = highlightedWords[i];
|
||||
|
||||
if (w.type === 'regex') {
|
||||
mark.markRegExp(new RegExp(w.value, 'gmi'), { acrossElements: true });
|
||||
} else {
|
||||
mark.mark([w]);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: in this case it is safe to use dangerouslySetInnerHTML because titleElement
|
||||
// is a span tag that we created and that contains data that's been inserted as plain text
|
||||
|
@ -34,6 +34,7 @@ const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { toSystemSlashes, safeFilename } = require('lib/path-utils');
|
||||
const { clipboard } = require('electron');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
@ -84,7 +85,7 @@ class NoteTextComponent extends React.Component {
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
this.restoreScrollTop_ = null;
|
||||
this.lastSetHtml_ = '';
|
||||
this.lastSetMarkers_ = [];
|
||||
this.lastSetMarkers_ = '';
|
||||
this.lastSetMarkersOptions_ = {};
|
||||
this.selectionRange_ = null;
|
||||
this.noteSearchBar_ = React.createRef();
|
||||
@ -508,7 +509,7 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
this.lastSetHtml_ = '';
|
||||
this.lastSetMarkers_ = [];
|
||||
this.lastSetMarkers_ = '';
|
||||
this.lastSetMarkersOptions_ = {};
|
||||
|
||||
this.setState(newState);
|
||||
@ -733,7 +734,7 @@ class NoteTextComponent extends React.Component {
|
||||
webviewReady: true,
|
||||
});
|
||||
|
||||
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||
if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||
}
|
||||
|
||||
webview_ref(element) {
|
||||
@ -1558,11 +1559,15 @@ class NoteTextComponent extends React.Component {
|
||||
markerOptions.selectedIndex = this.state.localSearch.selectedIndex;
|
||||
} else {
|
||||
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
if (search) keywords = Search.keywords(search.query_pattern);
|
||||
if (search) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(search.query_pattern);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
}
|
||||
|
||||
if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords) || !ObjectUtils.fieldsEqual(this.lastSetMarkersOptions_, markerOptions)) {
|
||||
this.lastSetMarkers_ = keywords.slice();
|
||||
const keywordHash = JSON.stringify(keywords);
|
||||
if (htmlHasChanged || keywordHash !== this.lastSetMarkers_ || !ObjectUtils.fieldsEqual(this.lastSetMarkersOptions_, markerOptions)) {
|
||||
this.lastSetMarkers_ = keywordHash;
|
||||
this.lastSetMarkersOptions_ = Object.assign({}, markerOptions);
|
||||
this.webview_.send('setMarkers', keywords, markerOptions);
|
||||
}
|
||||
|
@ -36,6 +36,7 @@
|
||||
<body id="body">
|
||||
<div id="hlScriptContainer"></div>
|
||||
<div id="markScriptContainer"></div>
|
||||
<!-- START_OF_DOCUMENT -->
|
||||
<div id="content" ondragstart="return false;" ondrop="return false;"></div>
|
||||
|
||||
<script>
|
||||
@ -143,6 +144,8 @@
|
||||
ipc.setHtml = (event) => {
|
||||
const html = event.html;
|
||||
|
||||
markJsHackMarkerInserted_ = false;
|
||||
|
||||
updateBodyHeight();
|
||||
|
||||
contentElement.innerHTML = html;
|
||||
@ -196,6 +199,29 @@
|
||||
setPercentScroll(percent);
|
||||
}
|
||||
|
||||
// HACK for Mark.js bug - https://github.com/julmot/mark.js/issues/127
|
||||
let markJsHackMarkerInserted_ = false;
|
||||
function addMarkJsSpaceHack(document) {
|
||||
if (markJsHackMarkerInserted_) return;
|
||||
|
||||
const prepareElementsForMarkJs = (elements, type) => {
|
||||
// const markJsHackMarker_ = '​ ​'
|
||||
const markJsHackMarker_ = ' ';
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (!type) {
|
||||
elements[i].innerHTML = elements[i].innerHTML + markJsHackMarker_;
|
||||
} else if (type === 'insertBefore') {
|
||||
elements[i].insertAdjacentHTML('beforeBegin', markJsHackMarker_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareElementsForMarkJs(document.getElementsByTagName('p'));
|
||||
prepareElementsForMarkJs(document.getElementsByTagName('div'));
|
||||
prepareElementsForMarkJs(document.getElementsByTagName('br'), 'insertBefore');
|
||||
markJsHackMarkerInserted_ = true;
|
||||
}
|
||||
|
||||
let mark_ = null;
|
||||
let markSelectedElement_ = null;
|
||||
function setMarkers(keywords, options = null) {
|
||||
@ -208,6 +234,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
addMarkJsSpaceHack(document);
|
||||
|
||||
mark_.unmark()
|
||||
|
||||
if (markSelectedElement_) markSelectedElement_.classList.remove('mark-selected');
|
||||
@ -215,20 +243,32 @@
|
||||
let selectedElement = null;
|
||||
let elementIndex = 0;
|
||||
|
||||
if (keywords.length) {
|
||||
mark_.mark(keywords, {
|
||||
each: (element) => {
|
||||
if (!('selectedIndex' in options)) return;
|
||||
const onEachElement = (element) => {
|
||||
if (!('selectedIndex' in options)) return;
|
||||
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
element.classList.add('mark-selected');
|
||||
selectedElement = element;
|
||||
}
|
||||
|
||||
elementIndex++;
|
||||
}
|
||||
});
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
element.classList.add('mark-selected');
|
||||
selectedElement = element;
|
||||
}
|
||||
|
||||
elementIndex++;
|
||||
}
|
||||
|
||||
for (let i = 0; i < keywords.length; i++) {
|
||||
const keyword = keywords[i];
|
||||
|
||||
if (keyword.type === 'regex') {
|
||||
mark_.markRegExp(new RegExp(keyword.value, 'gmi'), {
|
||||
each: onEachElement,
|
||||
acrossElements: true,
|
||||
});
|
||||
} else {
|
||||
mark_.mark([keyword], {
|
||||
each: onEachElement,
|
||||
accuracy: 'exactly',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
@ -395,8 +395,6 @@ class MdToHtml {
|
||||
previousToken = t;
|
||||
}
|
||||
|
||||
output.unshift('<!-- START_OF_DOCUMENT -->');
|
||||
|
||||
// Insert the extra CSS at the top of the HTML
|
||||
|
||||
if (!ObjectUtils.isEmpty(extraCssBlocks)) {
|
||||
|
@ -101,11 +101,19 @@ class SearchEngine {
|
||||
|
||||
// https://stackoverflow.com/a/13818704/561309
|
||||
queryTermToRegex(term) {
|
||||
while (term.length && term.indexOf('*') === 0) {
|
||||
term = term.substr(1);
|
||||
}
|
||||
|
||||
const preg_quote = (str, delimiter) => {
|
||||
return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
|
||||
} // [^ \t,\.,\+\-\\*?!={}<>\|:"\'\(\)[\]]
|
||||
let regexString = preg_quote(term);
|
||||
if (regexString[regexString.length - 1] === '*') {
|
||||
regexString = regexString.substr(0, regexString.length - 2) + '[^' + preg_quote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']' + '*';
|
||||
}
|
||||
const regexString = preg_quote(term).replace(/\\\*/g, '.*').replace(/\\\?/g, '.');
|
||||
return new RegExp(regexString, 'gmi');
|
||||
|
||||
return regexString;
|
||||
}
|
||||
|
||||
parseQuery(query) {
|
||||
@ -173,7 +181,7 @@ class SearchEngine {
|
||||
}
|
||||
|
||||
if (term.indexOf('*') >= 0) {
|
||||
terms[col][i] = this.queryTermToRegex(term);
|
||||
terms[col][i] = { type: 'regex', value: this.queryTermToRegex(term) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,6 +197,17 @@ class SearchEngine {
|
||||
};
|
||||
}
|
||||
|
||||
allParsedQueryTerms(parsedQuery) {
|
||||
if (!parsedQuery || !parsedQuery.termCount) return [];
|
||||
|
||||
let output = [];
|
||||
for (let col in parsedQuery.terms) {
|
||||
if (!parsedQuery.terms.hasOwnProperty(col)) continue;
|
||||
output = output.concat(parsedQuery.terms[col]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
const parsedQuery = this.parseQuery(query);
|
||||
const sql = 'SELECT id, title, offsets(notes_fts) AS offsets FROM notes_fts WHERE notes_fts MATCH ?'
|
||||
|
Loading…
Reference in New Issue
Block a user