1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Mobile: Fixes #382: Implemented new search engine for mobile and highlight searched words in notes

This commit is contained in:
Laurent Cozic 2018-12-16 18:32:42 +01:00
parent 3231bfaff0
commit b1898141c3
9 changed files with 112 additions and 56 deletions

View File

@ -34,7 +34,7 @@ const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
const EncryptionService = require('lib/services/EncryptionService');
const ResourceFetcher = require('lib/services/ResourceFetcher');
const SearchEngine = require('lib/services/SearchEngine');
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseService = require('lib/services/BaseService');
@ -220,27 +220,7 @@ class BaseApplication {
notes = await Tag.notes(parentId, options);
} else if (parentType === BaseModel.TYPE_SEARCH) {
const search = BaseModel.byId(state.searches, parentId);
const results = await SearchEngine.instance().search(search.query_pattern);
const noteIds = results.map(n => n.id);
const previewOptions = {
order: [],
fields: Note.previewFields(),
conditions: ['id IN ("' + noteIds.join('","') + '")'],
}
notes = await Note.previews(null, previewOptions);
// By default, the notes will be returned in reverse order
// or maybe random order so sort them here in the correct order
// (search engine returns the results in order of relevance).
const sortedNotes = [];
for (let i = 0; i < notes.length; i++) {
const idx = noteIds.indexOf(notes[i].id);
sortedNotes[idx] = notes[i];
}
notes = sortedNotes;
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
}
}

View File

@ -7,6 +7,7 @@ const { shim } = require('lib/shim.js');
const { _ } = require('lib/locale');
const md5 = require('md5');
const MdToHtml_Katex = require('lib/MdToHtml_Katex');
const { pregQuote } = require('lib/string-utils.js');
class MdToHtml {
@ -413,10 +414,30 @@ class MdToHtml {
return output.join('');
}
applyHighlightedKeywords_(body, keywords) {
for (let i = 0; i < keywords.length; i++) {
const k = keywords[i];
let regexString = '';
if (k.type === 'regex') {
regexString = k.value;
} else {
regexString = pregQuote(k);
}
const re = new RegExp('(^|\n|\b)(' + regexString + ')(\n|\b|$)', 'gi');
body = body.replace(re, '$1<span class="highlighted-keyword">$2</span>$3');
}
return body;
}
render(body, style, options = null) {
if (!options) options = {};
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
if (!options.paddingBottom) options.paddingBottom = '0';
if (!options.highlightedKeywords) options.highlightedKeywords = [];
const cacheKey = this.makeContentKey(this.loadedResources_, body, style, options);
if (this.cachedContentKey_ === cacheKey) return this.cachedContent_;
@ -427,30 +448,27 @@ class MdToHtml {
html: true,
});
body = this.applyHighlightedKeywords_(body, options.highlightedKeywords);
// Add `file:` protocol in linkify to allow text in the format of "file://..." to translate into
// file-URL links in html view
md.linkify.add('file:', {
validate: function (text, pos, self) {
var tail = text.slice(pos);
if (!self.re.file) {
self.re.file = new RegExp(
'^[\\/]{2,3}[\\S]+' // matches all local file URI on Win/Unix/MacOS systems including reserved characters in some OS (i.e. no OS specific sanity check)
);
// matches all local file URI on Win/Unix/MacOS systems including reserved characters in some OS (i.e. no OS specific sanity check)
self.re.file = new RegExp('^[\\/]{2,3}[\\S]+');
}
if (self.re.file.test(tail)) {
return tail.match(self.re.file)[0].length;
}
return 0;
}
});
// enable file link URLs in MarkdownIt. Keeps other URL restrictions of MarkdownIt untouched.
// Format [link name](file://...)
md.validateLink = function (url) {
var BAD_PROTO_RE = /^(vbscript|javascript|data):/;
var GOOD_DATA_RE = /^data:image\/(gif|png|jpeg|webp);/;
@ -458,7 +476,6 @@ class MdToHtml {
var str = url.trim().toLowerCase();
return BAD_PROTO_RE.test(str) ? (GOOD_DATA_RE.test(str) ? true : false) : true;
}
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
@ -609,6 +626,11 @@ class MdToHtml {
padding-left: .2em;
}
.highlighted-keyword {
background-color: #F3B717;
color: black;
}
/*
This is to fix https://github.com/laurent22/joplin/issues/764
Without this, the tag attached to an equation float at an absoluate position of the page,

View File

@ -87,6 +87,7 @@ class NoteBodyViewer extends Component {
}, 100);
},
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
highlightedKeywords: this.props.highlightedKeywords,
};
let html = this.mdToHtml_.render(note ? note.body : '', this.props.webViewStyle, mdOptions);

View File

@ -37,6 +37,7 @@ const AlarmService = require('lib/services/AlarmService.js');
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
const ShareExtension = require('react-native-share-extension').default;
const CameraView = require('lib/components/CameraView');
const SearchEngine = require('lib/services/SearchEngine');
import FileViewer from 'react-native-file-viewer';
@ -587,12 +588,19 @@ class NoteScreenComponent extends BaseScreenComponent {
this.saveOneProperty('body', newBody);
};
let keywords = [];
if (this.props.searchQuery) {
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
}
bodyComponent = <NoteBodyViewer
onJoplinLinkClick={this.onJoplinLinkClick_}
ref="noteBodyViewer"
style={this.styles().noteBodyViewer}
webViewStyle={theme}
note={note}
highlightedKeywords={keywords}
onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}
/>
} else {
@ -737,6 +745,7 @@ const NoteScreen = connect(
folderId: state.selectedFolderId,
itemType: state.selectedItemType,
folders: state.folders,
searchQuery: state.searchQuery,
theme: state.settings.theme,
sharedData: state.sharedData,
showAdvancedOptions: state.settings.showAdvancedOptions,

View File

@ -9,6 +9,7 @@ const { NoteItem } = require('lib/components/note-item.js');
const { BaseScreenComponent } = require('lib/components/base-screen.js');
const { themeStyle } = require('lib/components/global-style.js');
const { dialogs } = require('lib/dialogs.js');
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
const DialogBox = require('react-native-dialogbox').default;
class SearchScreenComponent extends BaseScreenComponent {
@ -105,17 +106,22 @@ class SearchScreenComponent extends BaseScreenComponent {
let notes = []
if (query) {
let p = query.split(' ');
let temp = [];
for (let i = 0; i < p.length; i++) {
let t = p[i].trim();
if (!t) continue;
temp.push(t);
}
notes = await SearchEngineUtils.notesForQuery(query);
notes = await Note.previews(null, {
anywherePattern: '*' + temp.join('*') + '*',
});
// Keeping the code below in case of compatibility issue with old versions
// of Android and SQLite FTS.
// let p = query.split(' ');
// let temp = [];
// for (let i = 0; i < p.length; i++) {
// let t = p[i].trim();
// if (!t) continue;
// temp.push(t);
// }
// notes = await Note.previews(null, {
// anywherePattern: '*' + temp.join('*') + '*',
// });
}
if (!this.isMounted_) return;

View File

@ -4,6 +4,7 @@ const ItemChange = require('lib/models/ItemChange.js');
const Setting = require('lib/models/Setting.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { pregQuote } = require('lib/string-utils.js');
class SearchEngine {
@ -105,12 +106,9 @@ class SearchEngine {
term = term.substr(1);
}
const preg_quote = (str, delimiter) => {
return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
} // [^ \t,\.,\+\-\\*?!={}<>\|:"\'\(\)[\]]
let regexString = preg_quote(term);
let regexString = pregQuote(term);
if (regexString[regexString.length - 1] === '*') {
regexString = regexString.substr(0, regexString.length - 2) + '[^' + preg_quote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']' + '*';
regexString = regexString.substr(0, regexString.length - 2) + '[^' + pregQuote(' \t\n\r,.,+-*?!={}<>|:"\'()[]') + ']' + '*';
}
return regexString;

View File

@ -0,0 +1,32 @@
const SearchEngine = require('lib/services/SearchEngine');
const Note = require('lib/models/Note');
class SearchEngineUtils {
static async notesForQuery(query) {
const results = await SearchEngine.instance().search(query);
const noteIds = results.map(n => n.id);
const previewOptions = {
order: [],
fields: Note.previewFields(),
conditions: ['id IN ("' + noteIds.join('","') + '")'],
}
const notes = await Note.previews(null, previewOptions);
// By default, the notes will be returned in reverse order
// or maybe random order so sort them here in the correct order
// (search engine returns the results in order of relevance).
const sortedNotes = [];
for (let i = 0; i < notes.length; i++) {
const idx = noteIds.indexOf(notes[i].id);
sortedNotes[idx] = notes[i];
}
return sortedNotes;
}
}
module.exports = SearchEngineUtils;

View File

@ -224,4 +224,8 @@ function escapeHtml(s) {
.replace(/'/g, "&#039;");
}
module.exports = { removeDiacritics, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, escapeHtml };
function pregQuote(str, delimiter) {
return (str + '').replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&');
}
module.exports = { removeDiacritics, escapeFilename, wrap, splitCommandString, padLeft, toTitleCase, escapeHtml, pregQuote };

View File

@ -52,6 +52,7 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
const DropdownAlert = require('react-native-dropdownalert').default;
const ShareExtension = require('react-native-share-extension').default;
const ResourceFetcher = require('lib/services/ResourceFetcher');
const SearchEngine = require('lib/services/SearchEngine');
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
@ -495,6 +496,9 @@ async function initialize(dispatch) {
ResourceFetcher.instance().setLogger(reg.logger());
ResourceFetcher.instance().start();
SearchEngine.instance().setDb(reg.db());
SearchEngine.instance().setLogger(reg.logger());
reg.scheduleSync().then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.